@turtleclub/opportunities 0.1.0-beta.20 → 0.1.0-beta.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/package.json +7 -4
- package/src/cover-offer/README.md +46 -0
- package/src/cover-offer/components/CoverOfferCard.tsx +312 -286
- package/src/cover-offer/components/CoverRequestForm.tsx +342 -267
- package/src/cover-offer/components/CoveredEventsInfo.tsx +120 -0
- package/src/cover-offer/components/ExistingCoverInfo.tsx +74 -0
- package/src/cover-offer/components/NexusCoverSection.tsx +17 -55
- package/src/cover-offer/components/PurchaseButtonSection.tsx +106 -0
- package/src/cover-offer/constants.ts +25 -1
- package/src/cover-offer/hooks/useCheckNexusMembership.ts +26 -48
- package/src/cover-offer/hooks/useCoverQuote.ts +80 -76
- package/src/cover-offer/hooks/useCoverTokenSelection.ts +84 -0
- package/src/cover-offer/hooks/useDebouncedValue.ts +12 -0
- package/src/cover-offer/hooks/useExistingCovers.ts +29 -54
- package/src/cover-offer/hooks/useNexusProduct.ts +6 -1
- package/src/cover-offer/hooks/useNexusPurchase.ts +42 -40
- package/src/cover-offer/hooks/useTokenApproval.ts +118 -0
- package/src/cover-offer/hooks/useUserCoverNfts.ts +10 -67
- package/src/cover-offer/index.ts +1 -1
- package/src/cover-offer/types/index.ts +6 -30
- package/src/cover-offer/utils/index.ts +9 -1
- package/src/cover-offer/components/MembershipRequestCard.tsx +0 -77
|
@@ -1,102 +1,169 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import { useState } from "react";
|
|
4
|
-
import { Shield, ChevronDown, ChevronUp,
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
import { Shield, ChevronDown, ChevronUp, Info, type LucideProps } from "lucide-react";
|
|
5
5
|
import type { ComponentType } from "react";
|
|
6
6
|
import {
|
|
7
7
|
Button,
|
|
8
8
|
Slider,
|
|
9
9
|
Skeleton,
|
|
10
10
|
Checkbox,
|
|
11
|
+
Collapsible,
|
|
12
|
+
CollapsibleTrigger,
|
|
13
|
+
CollapsibleContent,
|
|
14
|
+
SwapInput,
|
|
11
15
|
Tooltip,
|
|
12
16
|
TooltipTrigger,
|
|
13
17
|
TooltipContent,
|
|
18
|
+
cn,
|
|
14
19
|
} from "@turtleclub/ui";
|
|
15
|
-
import {
|
|
16
|
-
import { useBalance } from "wagmi";
|
|
20
|
+
import { useSwitchChain } from "wagmi";
|
|
17
21
|
import { useCoverQuote } from "../hooks/useCoverQuote";
|
|
18
22
|
import { useNexusPurchase } from "../hooks/useNexusPurchase";
|
|
19
23
|
import { useExistingCovers } from "../hooks/useExistingCovers";
|
|
24
|
+
import { useCheckNexusMembership } from "../hooks/useCheckNexusMembership";
|
|
25
|
+
import { formatAPY } from "../utils";
|
|
26
|
+
import { useDebouncedValue } from "../hooks/useDebouncedValue";
|
|
20
27
|
import {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
parseCoverTiming,
|
|
26
|
-
} from "../utils";
|
|
27
|
-
import { NEXUS_KYC_REQUIREMENTS_URL, NEXUS_PROTOCOL_COVER_TERMS_URL } from "../constants";
|
|
28
|
+
NEXUS_KYC_REQUIREMENTS_URL,
|
|
29
|
+
NEXUS_PROTOCOL_COVER_TERMS_URL,
|
|
30
|
+
NEXUS_COVER_ROUTER,
|
|
31
|
+
} from "../constants";
|
|
28
32
|
import type { CoverOfferCardProps } from "../types";
|
|
33
|
+
import { PurchaseButtonSection } from "./PurchaseButtonSection";
|
|
34
|
+
import { ExistingCoverInfo } from "./ExistingCoverInfo";
|
|
35
|
+
import { CoveredEventsInfo } from "./CoveredEventsInfo";
|
|
36
|
+
|
|
37
|
+
import { useMultichainAccount } from "@turtleclub/multichain";
|
|
38
|
+
import { formatToken } from "@turtleclub/utils";
|
|
39
|
+
import { useTokenApproval } from "../hooks/useTokenApproval";
|
|
40
|
+
import { useCoverTokenSelection } from "../hooks/useCoverTokenSelection";
|
|
29
41
|
|
|
30
42
|
const ShieldIcon = Shield as ComponentType<LucideProps>;
|
|
31
43
|
const ChevronDownIcon = ChevronDown as ComponentType<LucideProps>;
|
|
32
44
|
const ChevronUpIcon = ChevronUp as ComponentType<LucideProps>;
|
|
33
|
-
const
|
|
45
|
+
const InfoIcon = Info as ComponentType<LucideProps>;
|
|
46
|
+
|
|
47
|
+
const DEBOUNCE_MS = 500;
|
|
34
48
|
|
|
35
49
|
export function CoverOfferCard({
|
|
36
50
|
productId,
|
|
37
51
|
opportunity,
|
|
38
|
-
amountToCover,
|
|
39
52
|
buyerAddress,
|
|
40
53
|
protocolName,
|
|
41
54
|
coverProductName,
|
|
42
55
|
onSuccess,
|
|
43
56
|
onError,
|
|
44
57
|
startExpanded,
|
|
58
|
+
coverAvailableAssets,
|
|
45
59
|
}: CoverOfferCardProps) {
|
|
46
60
|
const [isExpanded, setIsExpanded] = useState(startExpanded ?? false);
|
|
47
61
|
const [hasAgreedToTerms, setHasAgreedToTerms] = useState(false);
|
|
62
|
+
const [coverPeriod, setCoverPeriod] = useState(30);
|
|
63
|
+
const [amountToCover, setAmountToCover] = useState("");
|
|
64
|
+
|
|
65
|
+
const {
|
|
66
|
+
filteredTokens,
|
|
67
|
+
selectedTokenAddress,
|
|
68
|
+
setSelectedTokenAddress,
|
|
69
|
+
selectedTokenSymbol,
|
|
70
|
+
selectedTokenDecimals,
|
|
71
|
+
selectedTokenBalance,
|
|
72
|
+
handleMaxClick,
|
|
73
|
+
} = useCoverTokenSelection({
|
|
74
|
+
buyerAddress,
|
|
75
|
+
coverAvailableAssets,
|
|
76
|
+
amount: amountToCover,
|
|
77
|
+
setAmount: setAmountToCover,
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const debouncedAmount = useDebouncedValue(amountToCover, DEBOUNCE_MS);
|
|
81
|
+
const debouncedPeriod = useDebouncedValue(coverPeriod, DEBOUNCE_MS);
|
|
48
82
|
|
|
49
|
-
const {
|
|
83
|
+
const { isLoading, error, quoteResult, premium, originalAPY, adjustedAPY, coverCostPerc } =
|
|
50
84
|
useCoverQuote({
|
|
51
85
|
productId,
|
|
52
86
|
buyerAddress,
|
|
53
|
-
amountToCover,
|
|
87
|
+
amountToCover: debouncedAmount,
|
|
88
|
+
coverPeriod: debouncedPeriod,
|
|
89
|
+
tokenSymbol: selectedTokenSymbol,
|
|
90
|
+
decimals: selectedTokenDecimals,
|
|
91
|
+
opportunity,
|
|
54
92
|
});
|
|
55
93
|
|
|
94
|
+
// Check if user has insufficient balance for the premium (not the cover amount)
|
|
95
|
+
const hasInsufficientBalance = useMemo(() => {
|
|
96
|
+
if (!quoteResult?.displayInfo?.premiumInAsset || !selectedTokenBalance?.amount) {
|
|
97
|
+
return false;
|
|
98
|
+
}
|
|
99
|
+
const premiumInAsset = BigInt(quoteResult.displayInfo.premiumInAsset);
|
|
100
|
+
const userBalance = BigInt(selectedTokenBalance.amount);
|
|
101
|
+
return userBalance < premiumInAsset;
|
|
102
|
+
}, [quoteResult?.displayInfo?.premiumInAsset, selectedTokenBalance?.amount]);
|
|
103
|
+
|
|
104
|
+
const { chainId } = useMultichainAccount();
|
|
105
|
+
const { switchChainAsync } = useSwitchChain();
|
|
106
|
+
const { isMember, isLoading: isMembershipLoading } = useCheckNexusMembership(
|
|
107
|
+
buyerAddress,
|
|
108
|
+
chainId
|
|
109
|
+
);
|
|
56
110
|
const { purchase, isPurchasing } = useNexusPurchase({
|
|
57
111
|
onSuccess,
|
|
58
112
|
onError,
|
|
59
113
|
});
|
|
60
114
|
const { hasCoverForProduct, isLoading: isCoversLoading } = useExistingCovers(buyerAddress);
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
115
|
+
|
|
116
|
+
const approvalAmount = quoteResult
|
|
117
|
+
? BigInt(quoteResult.buyCoverInput.buyCoverParams.maxPremiumInAsset)
|
|
118
|
+
: null;
|
|
119
|
+
|
|
120
|
+
const { needsApproval, isCheckingApproval, approve, isApproving, refetchApprovalStatus } =
|
|
121
|
+
useTokenApproval({
|
|
122
|
+
userAddress: buyerAddress as `0x${string}`,
|
|
123
|
+
tokenAddress: selectedTokenAddress as `0x${string}`,
|
|
124
|
+
spenderAddress: NEXUS_COVER_ROUTER,
|
|
125
|
+
chainId: 1,
|
|
126
|
+
amount: approvalAmount,
|
|
127
|
+
});
|
|
70
128
|
|
|
71
129
|
const existingCover = hasCoverForProduct(productId);
|
|
72
130
|
const hasExistingCover = !!existingCover;
|
|
131
|
+
const isWrongNetwork = chainId !== 1;
|
|
73
132
|
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
133
|
+
const handleApprove = async () => {
|
|
134
|
+
try {
|
|
135
|
+
await approve();
|
|
136
|
+
await refetchApprovalStatus();
|
|
137
|
+
} catch (error) {
|
|
138
|
+
onError?.("Error approving spending allowance for Nexus cover");
|
|
139
|
+
}
|
|
140
|
+
};
|
|
78
141
|
|
|
79
142
|
const handlePurchase = async () => {
|
|
80
143
|
if (isPurchasing || !quoteResult) return;
|
|
81
144
|
await purchase(quoteResult);
|
|
82
145
|
};
|
|
83
146
|
|
|
84
|
-
const disabledReason = (() => {
|
|
147
|
+
const disabledReason = useMemo(() => {
|
|
85
148
|
if (!hasAgreedToTerms) return "Checkbox not marked";
|
|
86
149
|
if (!quoteResult) return "Quote not ready yet";
|
|
87
150
|
if (isLoading) return "Fetching quote...";
|
|
88
151
|
if (error) return error;
|
|
89
|
-
if (
|
|
152
|
+
if (hasInsufficientBalance) return "Insufficient balance for premium";
|
|
90
153
|
if (isPurchasing) return "Purchasing cover...";
|
|
91
154
|
return null;
|
|
92
|
-
})
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
155
|
+
}, [hasAgreedToTerms, quoteResult, isLoading, error, hasInsufficientBalance, isPurchasing]);
|
|
156
|
+
|
|
157
|
+
const isPurchaseDisabled = useMemo(
|
|
158
|
+
() =>
|
|
159
|
+
!quoteResult ||
|
|
160
|
+
isLoading ||
|
|
161
|
+
!!error ||
|
|
162
|
+
!hasAgreedToTerms ||
|
|
163
|
+
isPurchasing ||
|
|
164
|
+
hasInsufficientBalance,
|
|
165
|
+
[quoteResult, isLoading, error, hasAgreedToTerms, isPurchasing, hasInsufficientBalance]
|
|
166
|
+
);
|
|
100
167
|
|
|
101
168
|
const purchaseButton = (
|
|
102
169
|
<Button
|
|
@@ -110,11 +177,12 @@ export function CoverOfferCard({
|
|
|
110
177
|
);
|
|
111
178
|
|
|
112
179
|
return (
|
|
113
|
-
<
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
180
|
+
<Collapsible
|
|
181
|
+
open={isExpanded}
|
|
182
|
+
onOpenChange={setIsExpanded}
|
|
183
|
+
className="rounded-xl border overflow-hidden turtle-widget-root"
|
|
184
|
+
>
|
|
185
|
+
<CollapsibleTrigger className="w-full p-4 flex items-center cursor-pointer justify-between hover:bg-primary/5 transition-colors">
|
|
118
186
|
<div className="flex items-center gap-3">
|
|
119
187
|
<div className="p-2 rounded-lg bg-primary/10 border border-primary/20">
|
|
120
188
|
<ShieldIcon className="w-5 h-5 text-primary" />
|
|
@@ -122,7 +190,7 @@ export function CoverOfferCard({
|
|
|
122
190
|
<div className="text-left">
|
|
123
191
|
<div className="flex items-center gap-2">
|
|
124
192
|
<span className="text-sm font-semibold text-foreground">Protect Your Deposit</span>
|
|
125
|
-
<span className="px-2 py-0.5 text-
|
|
193
|
+
<span className="px-2 py-0.5 text-xs font-medium text-primary bg-primary/10 rounded-full border border-primary/20">
|
|
126
194
|
NEW
|
|
127
195
|
</span>
|
|
128
196
|
</div>
|
|
@@ -136,221 +204,176 @@ export function CoverOfferCard({
|
|
|
136
204
|
<ChevronDownIcon className="w-5 h-5 text-muted-foreground" />
|
|
137
205
|
)}
|
|
138
206
|
</div>
|
|
139
|
-
</
|
|
207
|
+
</CollapsibleTrigger>
|
|
208
|
+
|
|
209
|
+
<CollapsibleContent className="px-4 pb-4 space-y-4">
|
|
210
|
+
{/* Protocol & Cover Info */}
|
|
211
|
+
<div className="flex items-center justify-between text-sm">
|
|
212
|
+
<span className="text-muted-foreground">
|
|
213
|
+
Protocol: <span className="font-medium text-foreground">{protocolName}</span>
|
|
214
|
+
</span>
|
|
215
|
+
<span className="text-sm text-muted-foreground px-2 py-1 rounded bg-muted/50 border border-border">
|
|
216
|
+
{coverProductName}
|
|
217
|
+
</span>
|
|
218
|
+
</div>
|
|
140
219
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
<div className="flex items-center justify-between text-sm">
|
|
146
|
-
<span className="text-muted-foreground">
|
|
147
|
-
Protocol: <span className="font-medium text-foreground">{protocolName}</span>
|
|
148
|
-
</span>
|
|
149
|
-
<span className="text-[10px] text-muted-foreground px-2 py-1 rounded bg-muted/50 border border-border">
|
|
150
|
-
{coverProductName}
|
|
151
|
-
</span>
|
|
220
|
+
{/* Loading state for existing covers */}
|
|
221
|
+
{isCoversLoading && (
|
|
222
|
+
<div className="p-3 rounded-lg bg-background/50 border border-border">
|
|
223
|
+
<Skeleton className="h-4 w-48 mx-auto bg-neutral-alpha-10" />
|
|
152
224
|
</div>
|
|
225
|
+
)}
|
|
153
226
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
<div className="p-3 rounded-lg bg-background/50 border border-border">
|
|
157
|
-
<Skeleton className="h-4 w-48 mx-auto bg-neutral-alpha-10" />
|
|
158
|
-
</div>
|
|
159
|
-
)}
|
|
227
|
+
{/* Existing Cover Info */}
|
|
228
|
+
{hasExistingCover && existingCover && <ExistingCoverInfo cover={existingCover} />}
|
|
160
229
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
<div className="flex items-center gap-2 mb-2">
|
|
166
|
-
<CheckCircleIcon className="w-4 h-4 text-green-500" />
|
|
167
|
-
<span className="text-sm font-medium text-green-500">
|
|
168
|
-
You already have cover for this protocol
|
|
169
|
-
</span>
|
|
170
|
-
</div>
|
|
171
|
-
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
172
|
-
Your deposit is protected against smart contract bugs, oracle failures, and
|
|
173
|
-
governance attacks on this protocol. See{" "}
|
|
174
|
-
<a
|
|
175
|
-
href={NEXUS_PROTOCOL_COVER_TERMS_URL}
|
|
176
|
-
target="_blank"
|
|
177
|
-
rel="noopener noreferrer"
|
|
178
|
-
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
179
|
-
>
|
|
180
|
-
terms
|
|
181
|
-
</a>{" "}
|
|
182
|
-
for details.
|
|
183
|
-
</p>
|
|
184
|
-
</div>
|
|
230
|
+
{/* Purchase Form - only show if no existing cover */}
|
|
231
|
+
{!hasExistingCover && !isCoversLoading && (
|
|
232
|
+
<>
|
|
233
|
+
<CoveredEventsInfo />
|
|
185
234
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
).periodDays
|
|
206
|
-
}{" "}
|
|
207
|
-
days
|
|
208
|
-
</p>
|
|
209
|
-
</div>
|
|
210
|
-
</div>
|
|
235
|
+
{/* Coverage Amount Input */}
|
|
236
|
+
<div className="space-y-2 ">
|
|
237
|
+
<span className="text-xs text-muted-foreground">Coverage Amount</span>
|
|
238
|
+
<SwapInput
|
|
239
|
+
value={amountToCover}
|
|
240
|
+
onChange={setAmountToCover}
|
|
241
|
+
tokens={filteredTokens}
|
|
242
|
+
placeholder="Enter amount to cover"
|
|
243
|
+
selectedToken={selectedTokenAddress}
|
|
244
|
+
onTokenChange={setSelectedTokenAddress}
|
|
245
|
+
onMaxClick={handleMaxClick}
|
|
246
|
+
className={cn(
|
|
247
|
+
"p-5",
|
|
248
|
+
hasInsufficientBalance ? "border-destructive/50 border" : "border-border border"
|
|
249
|
+
)}
|
|
250
|
+
showTokenSelectorBalance={false}
|
|
251
|
+
showInputBalance={false}
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
211
254
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
255
|
+
{/* APY Comparison */}
|
|
256
|
+
<div className="grid grid-cols-3 gap-3">
|
|
257
|
+
<div className="p-3 rounded-lg bg-background border border-border text-center">
|
|
258
|
+
<div className="flex items-center justify-center gap-1 mb-1">
|
|
259
|
+
<p className="text-xs uppercase tracking-wide text-muted-foreground">Base APR</p>
|
|
260
|
+
<Tooltip>
|
|
261
|
+
<TooltipTrigger asChild>
|
|
262
|
+
<InfoIcon className="w-3 h-3 text-muted-foreground cursor-help" />
|
|
263
|
+
</TooltipTrigger>
|
|
264
|
+
<TooltipContent side="top" className="max-w-[200px]">
|
|
265
|
+
<p className="text-xs">
|
|
266
|
+
The current yield offered by this opportunity before any cover costs.
|
|
267
|
+
</p>
|
|
268
|
+
</TooltipContent>
|
|
269
|
+
</Tooltip>
|
|
218
270
|
</div>
|
|
271
|
+
<p className="text-lg font-bold text-foreground">{formatAPY(originalAPY)}</p>
|
|
219
272
|
</div>
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
<p className="text-
|
|
236
|
-
|
|
237
|
-
</p>
|
|
238
|
-
<ul className="text-[11px] text-muted-foreground space-y-1.5">
|
|
239
|
-
<li className="flex items-start gap-2">
|
|
240
|
-
<span className="text-primary mt-0.5">•</span>
|
|
241
|
-
<span>Smart contract bugs or exploits in the protocol</span>
|
|
242
|
-
</li>
|
|
243
|
-
<li className="flex items-start gap-2">
|
|
244
|
-
<span className="text-primary mt-0.5">•</span>
|
|
245
|
-
<span>Oracle manipulation or failure affecting the protocol</span>
|
|
246
|
-
</li>
|
|
247
|
-
<li className="flex items-start gap-2">
|
|
248
|
-
<span className="text-primary mt-0.5">•</span>
|
|
249
|
-
<span>Governance attacks on the protocol</span>
|
|
250
|
-
</li>
|
|
251
|
-
</ul>
|
|
252
|
-
<p className="text-[10px] text-muted-foreground/70 leading-relaxed">
|
|
253
|
-
Cover applies only to events directly related to this protocol. See full{" "}
|
|
254
|
-
<a
|
|
255
|
-
href={NEXUS_PROTOCOL_COVER_TERMS_URL}
|
|
256
|
-
target="_blank"
|
|
257
|
-
rel="noopener noreferrer"
|
|
258
|
-
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
259
|
-
>
|
|
260
|
-
terms and conditions
|
|
261
|
-
</a>{" "}
|
|
262
|
-
for exclusions.
|
|
263
|
-
</p>
|
|
264
|
-
</div>
|
|
273
|
+
<div className="p-3 rounded-lg bg-background/80 border border-border text-center">
|
|
274
|
+
<p className="text-xs uppercase tracking-wide text-muted-foreground mb-1"></p>
|
|
275
|
+
{isLoading ? (
|
|
276
|
+
<>
|
|
277
|
+
<Skeleton className="h-6 w-16 mx-auto bg-neutral-alpha-10" />
|
|
278
|
+
<Skeleton className="h-4 w-12 mx-auto mt-1 bg-neutral-alpha-10" />
|
|
279
|
+
</>
|
|
280
|
+
) : coverCostPerc !== null ? (
|
|
281
|
+
<>
|
|
282
|
+
<p className="text-lg font-bold text-orange-400">
|
|
283
|
+
-{coverCostPerc.toFixed(2)}%
|
|
284
|
+
</p>
|
|
285
|
+
<p className="text-xs text-muted-foreground">per year</p>
|
|
286
|
+
</>
|
|
287
|
+
) : (
|
|
288
|
+
<p className="text-lg font-bold text-muted-foreground">—</p>
|
|
289
|
+
)}
|
|
265
290
|
</div>
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
{isLoading ? (
|
|
278
|
-
<>
|
|
279
|
-
<Skeleton className="h-6 w-16 mx-auto bg-neutral-alpha-10" />
|
|
280
|
-
<Skeleton className="h-4 w-12 mx-auto mt-1 bg-neutral-alpha-10" />
|
|
281
|
-
</>
|
|
282
|
-
) : coverCostPerc !== null ? (
|
|
283
|
-
<>
|
|
284
|
-
<p className="text-lg font-bold text-orange-400">
|
|
285
|
-
-{coverCostPerc.toFixed(2)}%
|
|
291
|
+
<div className="p-3 rounded-lg bg-primary/5 border border-primary/20 text-center">
|
|
292
|
+
<div className="flex items-center justify-center gap-1 mb-1">
|
|
293
|
+
<p className="text-xs uppercase tracking-wide text-primary/70">Net APY</p>
|
|
294
|
+
<Tooltip>
|
|
295
|
+
<TooltipTrigger asChild>
|
|
296
|
+
<InfoIcon className="w-3 h-3 text-primary/70 cursor-help" />
|
|
297
|
+
</TooltipTrigger>
|
|
298
|
+
<TooltipContent side="top" className="max-w-[200px]">
|
|
299
|
+
<p className="text-xs">
|
|
300
|
+
Your effective yield after subtracting the annualized cover cost from the
|
|
301
|
+
Base APR.
|
|
286
302
|
</p>
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
) : (
|
|
290
|
-
<p className="text-lg font-bold text-muted-foreground">—</p>
|
|
291
|
-
)}
|
|
292
|
-
</div>
|
|
293
|
-
<div className="p-3 rounded-lg bg-primary/5 border border-primary/20 text-center">
|
|
294
|
-
<p className="text-[10px] uppercase tracking-wide text-primary/70 mb-1">
|
|
295
|
-
Net APY
|
|
296
|
-
</p>
|
|
297
|
-
{isLoading ? (
|
|
298
|
-
<>
|
|
299
|
-
<Skeleton className="h-6 w-14 mx-auto bg-neutral-alpha-10 " />
|
|
300
|
-
<Skeleton className="h-3 w-14 mx-auto mt-1 bg-neutral-alpha-10" />
|
|
301
|
-
</>
|
|
302
|
-
) : adjustedAPY !== null ? (
|
|
303
|
-
<>
|
|
304
|
-
<p className="text-lg font-bold text-primary">{formatAPY(adjustedAPY)}</p>
|
|
305
|
-
<p className="text-[10px] text-primary/60">protected</p>
|
|
306
|
-
</>
|
|
307
|
-
) : (
|
|
308
|
-
<p className="text-lg font-bold text-muted-foreground">—</p>
|
|
309
|
-
)}
|
|
303
|
+
</TooltipContent>
|
|
304
|
+
</Tooltip>
|
|
310
305
|
</div>
|
|
306
|
+
{isLoading ? (
|
|
307
|
+
<>
|
|
308
|
+
<Skeleton className="h-6 w-14 mx-auto bg-neutral-alpha-10 " />
|
|
309
|
+
<Skeleton className="h-3 w-14 mx-auto mt-1 bg-neutral-alpha-10" />
|
|
310
|
+
</>
|
|
311
|
+
) : adjustedAPY !== null ? (
|
|
312
|
+
<>
|
|
313
|
+
<p className="text-lg font-bold text-primary">{formatAPY(adjustedAPY)}</p>
|
|
314
|
+
<p className="text-xs text-primary/60">protected</p>
|
|
315
|
+
</>
|
|
316
|
+
) : (
|
|
317
|
+
<p className="text-lg font-bold text-muted-foreground">—</p>
|
|
318
|
+
)}
|
|
311
319
|
</div>
|
|
320
|
+
</div>
|
|
312
321
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
{isLoading ? (
|
|
320
|
-
<Skeleton className="h-4 w-20 bg-neutral-alpha-10" />
|
|
321
|
-
) : premiumEth ? (
|
|
322
|
-
<span className="text-sm text-muted-foreground">
|
|
323
|
-
({formatEthAmount(premiumEth)} ETH)
|
|
324
|
-
</span>
|
|
325
|
-
) : null}
|
|
326
|
-
</div>
|
|
327
|
-
</div>
|
|
328
|
-
<div className="w-full">
|
|
329
|
-
<Slider
|
|
330
|
-
value={[coverPeriod]}
|
|
331
|
-
onValueChange={(value) => setCoverPeriod(value[0])}
|
|
332
|
-
min={28}
|
|
333
|
-
max={365}
|
|
334
|
-
step={1}
|
|
335
|
-
/>
|
|
336
|
-
</div>
|
|
337
|
-
<div className="flex justify-between text-[10px] text-muted-foreground">
|
|
338
|
-
<span>28 days</span>
|
|
339
|
-
<span>365 days</span>
|
|
322
|
+
{/* Coverage Period Slider */}
|
|
323
|
+
<div className="space-y-3">
|
|
324
|
+
<div className="flex items-center justify-between">
|
|
325
|
+
<span className="text-xs text-muted-foreground">Coverage Period</span>
|
|
326
|
+
<div className="flex items-center gap-1">
|
|
327
|
+
<span className="text-sm font-medium text-foreground">{coverPeriod} days</span>
|
|
340
328
|
</div>
|
|
341
329
|
</div>
|
|
330
|
+
<div className="w-full">
|
|
331
|
+
<Slider
|
|
332
|
+
value={[coverPeriod]}
|
|
333
|
+
onValueChange={(value) => setCoverPeriod(value[0])}
|
|
334
|
+
min={28}
|
|
335
|
+
max={365}
|
|
336
|
+
step={1}
|
|
337
|
+
/>
|
|
338
|
+
</div>
|
|
339
|
+
<div className="flex justify-between text-xs text-muted-foreground">
|
|
340
|
+
<span>28 days</span>
|
|
341
|
+
<span>365 days</span>
|
|
342
|
+
</div>
|
|
343
|
+
</div>
|
|
342
344
|
|
|
343
|
-
|
|
345
|
+
{/* Premium Details */}
|
|
346
|
+
<div className="space-y-2">
|
|
347
|
+
<div className="flex items-center justify-between">
|
|
348
|
+
<span className="text-xs text-muted-foreground">Your Balance</span>
|
|
349
|
+
<span className="text-xs text-muted-foreground text-primary">
|
|
350
|
+
{selectedTokenBalance
|
|
351
|
+
? formatToken(
|
|
352
|
+
selectedTokenBalance.amount,
|
|
353
|
+
selectedTokenBalance.token,
|
|
354
|
+
true,
|
|
355
|
+
false,
|
|
356
|
+
5
|
|
357
|
+
)
|
|
358
|
+
: "0"}{" "}
|
|
359
|
+
{selectedTokenBalance?.token.symbol}
|
|
360
|
+
</span>
|
|
361
|
+
</div>
|
|
344
362
|
<div className="flex items-center justify-between p-3 rounded-lg bg-background border border-border">
|
|
345
363
|
<div className="flex items-center gap-2">
|
|
346
364
|
<ShieldIcon className="w-4 h-4 text-primary" />
|
|
347
|
-
<span className="text-sm text-foreground">Premium</span>
|
|
365
|
+
<span className="text-sm text-foreground">Premium Cost</span>
|
|
348
366
|
</div>
|
|
349
367
|
{isLoading ? (
|
|
350
368
|
<Skeleton className="h-5 w-20 bg-neutral-alpha-10" />
|
|
351
|
-
) :
|
|
352
|
-
<span
|
|
353
|
-
{
|
|
369
|
+
) : premium ? (
|
|
370
|
+
<span
|
|
371
|
+
className={cn(
|
|
372
|
+
"text-sm font-semibold",
|
|
373
|
+
hasInsufficientBalance ? "text-destructive" : "text-foreground"
|
|
374
|
+
)}
|
|
375
|
+
>
|
|
376
|
+
{premium} {selectedTokenSymbol}
|
|
354
377
|
</span>
|
|
355
378
|
) : error ? (
|
|
356
379
|
<span className="text-sm text-destructive">No cover available</span>
|
|
@@ -358,62 +381,65 @@ export function CoverOfferCard({
|
|
|
358
381
|
<span className="text-sm text-muted-foreground">—</span>
|
|
359
382
|
)}
|
|
360
383
|
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
{/* Terms Agreement */}
|
|
387
|
+
<div className="space-y-3 p-3 rounded-lg bg-muted/20 border border-border ">
|
|
388
|
+
<p className="text-xs text-muted-foreground leading-relaxed">
|
|
389
|
+
By buying Nexus Mutual Bundled Protocol Cover, you agree to the{" "}
|
|
390
|
+
<a
|
|
391
|
+
href={NEXUS_PROTOCOL_COVER_TERMS_URL}
|
|
392
|
+
target="_blank"
|
|
393
|
+
rel="noopener noreferrer"
|
|
394
|
+
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
395
|
+
>
|
|
396
|
+
terms and conditions
|
|
397
|
+
</a>
|
|
398
|
+
</p>
|
|
361
399
|
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
400
|
+
<label className="flex items-start gap-2 group">
|
|
401
|
+
<div className="relative flex-shrink-0 mt-0.5 cursor-pointer">
|
|
402
|
+
<Checkbox
|
|
403
|
+
checked={hasAgreedToTerms}
|
|
404
|
+
onCheckedChange={(hasAgreedToTerms) =>
|
|
405
|
+
setHasAgreedToTerms(hasAgreedToTerms === true)
|
|
406
|
+
}
|
|
407
|
+
className="size-4 cursor-pointer"
|
|
408
|
+
/>
|
|
409
|
+
</div>
|
|
410
|
+
<span className="text-xs text-muted-foreground leading-relaxed group-hover:text-foreground/70 transition-colors">
|
|
411
|
+
I confirm that I do not reside in the{" "}
|
|
366
412
|
<a
|
|
367
|
-
href={
|
|
413
|
+
href={NEXUS_KYC_REQUIREMENTS_URL}
|
|
368
414
|
target="_blank"
|
|
369
415
|
rel="noopener noreferrer"
|
|
370
416
|
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
417
|
+
onClick={(e) => e.stopPropagation()}
|
|
371
418
|
>
|
|
372
|
-
|
|
419
|
+
countries listed here
|
|
373
420
|
</a>
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
<Checkbox
|
|
379
|
-
checked={hasAgreedToTerms}
|
|
380
|
-
onCheckedChange={(hasAgreedToTerms) =>
|
|
381
|
-
setHasAgreedToTerms(hasAgreedToTerms === true)
|
|
382
|
-
}
|
|
383
|
-
/>
|
|
384
|
-
</div>
|
|
385
|
-
<span className="text-[10px] text-muted-foreground leading-relaxed group-hover:text-foreground/70 transition-colors">
|
|
386
|
-
I confirm that I do not reside in the{" "}
|
|
387
|
-
<a
|
|
388
|
-
href={NEXUS_KYC_REQUIREMENTS_URL}
|
|
389
|
-
target="_blank"
|
|
390
|
-
rel="noopener noreferrer"
|
|
391
|
-
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
392
|
-
onClick={(e) => e.stopPropagation()}
|
|
393
|
-
>
|
|
394
|
-
countries listed here
|
|
395
|
-
</a>
|
|
396
|
-
, and acknowledge that in the event of a loss, I will be required to join as a
|
|
397
|
-
member of Nexus Mutual to file my claim.
|
|
398
|
-
</span>
|
|
399
|
-
</label>
|
|
400
|
-
</div>
|
|
421
|
+
, and agree to the terms and conditions above.
|
|
422
|
+
</span>
|
|
423
|
+
</label>
|
|
424
|
+
</div>
|
|
401
425
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
426
|
+
{/* Purchase Button Section */}
|
|
427
|
+
<PurchaseButtonSection
|
|
428
|
+
isWrongNetwork={isWrongNetwork}
|
|
429
|
+
isMembershipLoading={isMembershipLoading}
|
|
430
|
+
isMember={isMember}
|
|
431
|
+
disabledReason={disabledReason}
|
|
432
|
+
purchaseButton={purchaseButton}
|
|
433
|
+
onSwitchNetwork={() => switchChainAsync({ chainId: 1 })}
|
|
434
|
+
needsApproval={needsApproval}
|
|
435
|
+
isCheckingApproval={isCheckingApproval}
|
|
436
|
+
onApprove={handleApprove}
|
|
437
|
+
isApproving={isApproving}
|
|
438
|
+
tokenSymbol={selectedTokenSymbol}
|
|
439
|
+
/>
|
|
440
|
+
</>
|
|
441
|
+
)}
|
|
442
|
+
</CollapsibleContent>
|
|
443
|
+
</Collapsible>
|
|
418
444
|
);
|
|
419
445
|
}
|