@turtleclub/opportunities 0.1.0-beta.6 → 0.1.0-beta.60
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 +224 -0
- package/README.md +179 -0
- package/package.json +13 -6
- package/src/components/index.ts +1 -1
- package/src/cover-offer/README.md +46 -0
- package/src/cover-offer/components/CoverOfferCard.tsx +445 -0
- package/src/cover-offer/components/CoverRequestForm.tsx +342 -0
- 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 +49 -0
- package/src/cover-offer/components/PurchaseButtonSection.tsx +106 -0
- package/src/cover-offer/constants.ts +32 -0
- package/src/cover-offer/hooks/useCheckNexusMembership.ts +32 -0
- package/src/cover-offer/hooks/useCoverQuote.ts +126 -0
- 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 +101 -0
- package/src/cover-offer/hooks/useNexusProduct.ts +79 -0
- package/src/cover-offer/hooks/useNexusPurchase.ts +67 -0
- package/src/cover-offer/hooks/useTokenApproval.ts +118 -0
- package/src/cover-offer/hooks/useUserCoverNfts.ts +26 -0
- package/src/cover-offer/index.ts +4 -0
- package/src/cover-offer/types/index.ts +41 -0
- package/src/cover-offer/utils/index.ts +90 -0
- package/src/deposit/NativeDepositSection.tsx +199 -0
- package/src/deposit/TemporalWrapper.tsx +155 -0
- package/src/{components → deposit/components}/balances-data-table.tsx +6 -0
- package/src/deposit/components/index.ts +3 -0
- package/src/deposit/components/swap-input-v3.tsx +194 -0
- package/src/deposit/components/token-selector-v3.tsx +122 -0
- package/src/deposit/index.ts +4 -0
- package/src/index.ts +9 -0
- package/src/opportunity-actions/OpportunityActions.tsx +182 -0
- package/src/opportunity-actions/index.ts +1 -0
- package/src/opportunity-table/components/opportunities-table.tsx +6 -5
- package/src/route-details/index.ts +6 -0
- package/src/route-details/route-details-v2.tsx +137 -0
- package/src/route-details/route-details.tsx +5 -4
- package/src/route-details/types.ts +7 -0
- package/src/transaction-status/hooks/useTransactionQueue.ts +1 -1
- package/src/withdraw/NativeWithdrawSection.tsx +45 -0
- package/src/withdraw/index.ts +1 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useState, useMemo } from "react";
|
|
4
|
+
import { Shield, ChevronDown, ChevronUp, Info, type LucideProps } from "lucide-react";
|
|
5
|
+
import type { ComponentType } from "react";
|
|
6
|
+
import {
|
|
7
|
+
Button,
|
|
8
|
+
Slider,
|
|
9
|
+
Skeleton,
|
|
10
|
+
Checkbox,
|
|
11
|
+
Collapsible,
|
|
12
|
+
CollapsibleTrigger,
|
|
13
|
+
CollapsibleContent,
|
|
14
|
+
SwapInput,
|
|
15
|
+
Tooltip,
|
|
16
|
+
TooltipTrigger,
|
|
17
|
+
TooltipContent,
|
|
18
|
+
cn,
|
|
19
|
+
} from "@turtleclub/ui";
|
|
20
|
+
import { useSwitchChain } from "wagmi";
|
|
21
|
+
import { useCoverQuote } from "../hooks/useCoverQuote";
|
|
22
|
+
import { useNexusPurchase } from "../hooks/useNexusPurchase";
|
|
23
|
+
import { useExistingCovers } from "../hooks/useExistingCovers";
|
|
24
|
+
import { useCheckNexusMembership } from "../hooks/useCheckNexusMembership";
|
|
25
|
+
import { formatAPY } from "../utils";
|
|
26
|
+
import { useDebouncedValue } from "../hooks/useDebouncedValue";
|
|
27
|
+
import {
|
|
28
|
+
NEXUS_KYC_REQUIREMENTS_URL,
|
|
29
|
+
NEXUS_PROTOCOL_COVER_TERMS_URL,
|
|
30
|
+
NEXUS_COVER_ROUTER,
|
|
31
|
+
} from "../constants";
|
|
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";
|
|
41
|
+
|
|
42
|
+
const ShieldIcon = Shield as ComponentType<LucideProps>;
|
|
43
|
+
const ChevronDownIcon = ChevronDown as ComponentType<LucideProps>;
|
|
44
|
+
const ChevronUpIcon = ChevronUp as ComponentType<LucideProps>;
|
|
45
|
+
const InfoIcon = Info as ComponentType<LucideProps>;
|
|
46
|
+
|
|
47
|
+
const DEBOUNCE_MS = 500;
|
|
48
|
+
|
|
49
|
+
export function CoverOfferCard({
|
|
50
|
+
productId,
|
|
51
|
+
opportunity,
|
|
52
|
+
buyerAddress,
|
|
53
|
+
protocolName,
|
|
54
|
+
coverProductName,
|
|
55
|
+
onSuccess,
|
|
56
|
+
onError,
|
|
57
|
+
startExpanded,
|
|
58
|
+
coverAvailableAssets,
|
|
59
|
+
}: CoverOfferCardProps) {
|
|
60
|
+
const [isExpanded, setIsExpanded] = useState(startExpanded ?? false);
|
|
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);
|
|
82
|
+
|
|
83
|
+
const { isLoading, error, quoteResult, premium, originalAPY, adjustedAPY, coverCostPerc } =
|
|
84
|
+
useCoverQuote({
|
|
85
|
+
productId,
|
|
86
|
+
buyerAddress,
|
|
87
|
+
amountToCover: debouncedAmount,
|
|
88
|
+
coverPeriod: debouncedPeriod,
|
|
89
|
+
tokenSymbol: selectedTokenSymbol,
|
|
90
|
+
decimals: selectedTokenDecimals,
|
|
91
|
+
opportunity,
|
|
92
|
+
});
|
|
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
|
+
);
|
|
110
|
+
const { purchase, isPurchasing } = useNexusPurchase({
|
|
111
|
+
onSuccess,
|
|
112
|
+
onError,
|
|
113
|
+
});
|
|
114
|
+
const { hasCoverForProduct, isLoading: isCoversLoading } = useExistingCovers(buyerAddress);
|
|
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
|
+
});
|
|
128
|
+
|
|
129
|
+
const existingCover = hasCoverForProduct(productId);
|
|
130
|
+
const hasExistingCover = !!existingCover;
|
|
131
|
+
const isWrongNetwork = chainId !== 1;
|
|
132
|
+
|
|
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
|
+
};
|
|
141
|
+
|
|
142
|
+
const handlePurchase = async () => {
|
|
143
|
+
if (isPurchasing || !quoteResult) return;
|
|
144
|
+
await purchase(quoteResult);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const disabledReason = useMemo(() => {
|
|
148
|
+
if (!hasAgreedToTerms) return "Checkbox not marked";
|
|
149
|
+
if (!quoteResult) return "Quote not ready yet";
|
|
150
|
+
if (isLoading) return "Fetching quote...";
|
|
151
|
+
if (error) return error;
|
|
152
|
+
if (hasInsufficientBalance) return "Insufficient balance for premium";
|
|
153
|
+
if (isPurchasing) return "Purchasing cover...";
|
|
154
|
+
return null;
|
|
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
|
+
);
|
|
167
|
+
|
|
168
|
+
const purchaseButton = (
|
|
169
|
+
<Button
|
|
170
|
+
onClick={handlePurchase}
|
|
171
|
+
disabled={isPurchaseDisabled}
|
|
172
|
+
className="w-full"
|
|
173
|
+
variant="green"
|
|
174
|
+
>
|
|
175
|
+
{isLoading ? "Getting Quote..." : isPurchasing ? "Purchasing Cover..." : "Purchase Cover"}
|
|
176
|
+
</Button>
|
|
177
|
+
);
|
|
178
|
+
|
|
179
|
+
return (
|
|
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">
|
|
186
|
+
<div className="flex items-center gap-3">
|
|
187
|
+
<div className="p-2 rounded-lg bg-primary/10 border border-primary/20">
|
|
188
|
+
<ShieldIcon className="w-5 h-5 text-primary" />
|
|
189
|
+
</div>
|
|
190
|
+
<div className="text-left">
|
|
191
|
+
<div className="flex items-center gap-2">
|
|
192
|
+
<span className="text-sm font-semibold text-foreground">Protect Your Deposit</span>
|
|
193
|
+
<span className="px-2 py-0.5 text-xs font-medium text-primary bg-primary/10 rounded-full border border-primary/20">
|
|
194
|
+
NEW
|
|
195
|
+
</span>
|
|
196
|
+
</div>
|
|
197
|
+
<p className="text-xs text-muted-foreground">Nexus Mutual Single Protocol Cover</p>
|
|
198
|
+
</div>
|
|
199
|
+
</div>
|
|
200
|
+
<div className="flex items-center gap-2">
|
|
201
|
+
{isExpanded ? (
|
|
202
|
+
<ChevronUpIcon className="w-5 h-5 text-muted-foreground" />
|
|
203
|
+
) : (
|
|
204
|
+
<ChevronDownIcon className="w-5 h-5 text-muted-foreground" />
|
|
205
|
+
)}
|
|
206
|
+
</div>
|
|
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>
|
|
219
|
+
|
|
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" />
|
|
224
|
+
</div>
|
|
225
|
+
)}
|
|
226
|
+
|
|
227
|
+
{/* Existing Cover Info */}
|
|
228
|
+
{hasExistingCover && existingCover && <ExistingCoverInfo cover={existingCover} />}
|
|
229
|
+
|
|
230
|
+
{/* Purchase Form - only show if no existing cover */}
|
|
231
|
+
{!hasExistingCover && !isCoversLoading && (
|
|
232
|
+
<>
|
|
233
|
+
<CoveredEventsInfo />
|
|
234
|
+
|
|
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>
|
|
254
|
+
|
|
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>
|
|
270
|
+
</div>
|
|
271
|
+
<p className="text-lg font-bold text-foreground">{formatAPY(originalAPY)}</p>
|
|
272
|
+
</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
|
+
)}
|
|
290
|
+
</div>
|
|
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.
|
|
302
|
+
</p>
|
|
303
|
+
</TooltipContent>
|
|
304
|
+
</Tooltip>
|
|
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
|
+
)}
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
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>
|
|
328
|
+
</div>
|
|
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>
|
|
344
|
+
|
|
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>
|
|
362
|
+
<div className="flex items-center justify-between p-3 rounded-lg bg-background border border-border">
|
|
363
|
+
<div className="flex items-center gap-2">
|
|
364
|
+
<ShieldIcon className="w-4 h-4 text-primary" />
|
|
365
|
+
<span className="text-sm text-foreground">Premium Cost</span>
|
|
366
|
+
</div>
|
|
367
|
+
{isLoading ? (
|
|
368
|
+
<Skeleton className="h-5 w-20 bg-neutral-alpha-10" />
|
|
369
|
+
) : premium ? (
|
|
370
|
+
<span
|
|
371
|
+
className={cn(
|
|
372
|
+
"text-sm font-semibold",
|
|
373
|
+
hasInsufficientBalance ? "text-destructive" : "text-foreground"
|
|
374
|
+
)}
|
|
375
|
+
>
|
|
376
|
+
{premium} {selectedTokenSymbol}
|
|
377
|
+
</span>
|
|
378
|
+
) : error ? (
|
|
379
|
+
<span className="text-sm text-destructive">No cover available</span>
|
|
380
|
+
) : (
|
|
381
|
+
<span className="text-sm text-muted-foreground">—</span>
|
|
382
|
+
)}
|
|
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>
|
|
399
|
+
|
|
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{" "}
|
|
412
|
+
<a
|
|
413
|
+
href={NEXUS_KYC_REQUIREMENTS_URL}
|
|
414
|
+
target="_blank"
|
|
415
|
+
rel="noopener noreferrer"
|
|
416
|
+
className="text-primary hover:text-primary/80 underline underline-offset-2"
|
|
417
|
+
onClick={(e) => e.stopPropagation()}
|
|
418
|
+
>
|
|
419
|
+
countries listed here
|
|
420
|
+
</a>
|
|
421
|
+
, and agree to the terms and conditions above.
|
|
422
|
+
</span>
|
|
423
|
+
</label>
|
|
424
|
+
</div>
|
|
425
|
+
|
|
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>
|
|
444
|
+
);
|
|
445
|
+
}
|