@b3dotfun/sdk 0.1.66-alpha.0 → 0.1.66-alpha.2
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/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.d.ts +50 -0
- package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.js +30 -0
- package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckoutTrigger.d.ts +47 -0
- package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckoutTrigger.js +45 -0
- package/dist/cjs/anyspend/react/components/checkout/CartItemRow.d.ts +8 -0
- package/dist/cjs/anyspend/react/components/checkout/CartItemRow.js +9 -0
- package/dist/cjs/anyspend/react/components/checkout/CartSummary.d.ts +8 -0
- package/dist/cjs/anyspend/react/components/checkout/CartSummary.js +9 -0
- package/dist/cjs/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +12 -0
- package/dist/cjs/anyspend/react/components/checkout/CheckoutCartPanel.js +19 -0
- package/dist/cjs/anyspend/react/components/checkout/CheckoutLayout.d.ts +10 -0
- package/dist/cjs/anyspend/react/components/checkout/CheckoutLayout.js +25 -0
- package/dist/cjs/anyspend/react/components/checkout/CheckoutPaymentPanel.d.ts +20 -0
- package/dist/cjs/anyspend/react/components/checkout/CheckoutPaymentPanel.js +45 -0
- package/dist/cjs/anyspend/react/components/checkout/CheckoutSuccess.d.ts +10 -0
- package/dist/cjs/anyspend/react/components/checkout/CheckoutSuccess.js +11 -0
- package/dist/cjs/anyspend/react/components/checkout/CoinbaseCheckoutPanel.d.ts +16 -0
- package/dist/cjs/anyspend/react/components/checkout/CoinbaseCheckoutPanel.js +27 -0
- package/dist/cjs/anyspend/react/components/checkout/CryptoCheckoutPanel.d.ts +33 -0
- package/dist/cjs/anyspend/react/components/checkout/CryptoCheckoutPanel.js +317 -0
- package/dist/cjs/anyspend/react/components/checkout/FiatCheckoutPanel.d.ts +16 -0
- package/dist/cjs/anyspend/react/components/checkout/FiatCheckoutPanel.js +233 -0
- package/dist/cjs/anyspend/react/components/checkout/PoweredByBranding.d.ts +8 -0
- package/dist/cjs/anyspend/react/components/checkout/PoweredByBranding.js +9 -0
- package/dist/cjs/anyspend/react/components/checkout/QRCheckoutPanel.d.ts +17 -0
- package/dist/cjs/anyspend/react/components/checkout/QRCheckoutPanel.js +148 -0
- package/dist/cjs/anyspend/react/components/index.d.ts +5 -1
- package/dist/cjs/anyspend/react/components/index.js +6 -1
- package/dist/cjs/anyspend/react/components/types/classes.d.ts +32 -0
- package/dist/cjs/app.shared.js +8 -0
- package/dist/cjs/global-account/react/components/B3DynamicModal.js +5 -1
- package/dist/cjs/global-account/react/components/WalletImage/WalletImage.d.ts +1 -1
- package/dist/cjs/global-account/react/components/ui/command.d.ts +7 -7
- package/dist/cjs/global-account/react/hooks/useAuth.d.ts +1 -1
- package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +1 -1
- package/dist/cjs/global-account/react/hooks/useFirstEOA.d.ts +4 -4
- package/dist/cjs/global-account/react/hooks/useUser.d.ts +1 -1
- package/dist/cjs/global-account/react/hooks/useUserQuery.d.ts +1 -1
- package/dist/cjs/global-account/react/stores/useModalStore.d.ts +53 -1
- package/dist/cjs/shared/constants/chains/b3Chain.d.ts +2 -2
- package/dist/cjs/shared/constants/chains/supported.d.ts +3 -3
- package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.d.ts +50 -0
- package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.js +27 -0
- package/dist/esm/anyspend/react/components/checkout/AnySpendCheckoutTrigger.d.ts +47 -0
- package/dist/esm/anyspend/react/components/checkout/AnySpendCheckoutTrigger.js +42 -0
- package/dist/esm/anyspend/react/components/checkout/CartItemRow.d.ts +8 -0
- package/dist/esm/anyspend/react/components/checkout/CartItemRow.js +6 -0
- package/dist/esm/anyspend/react/components/checkout/CartSummary.d.ts +8 -0
- package/dist/esm/anyspend/react/components/checkout/CartSummary.js +6 -0
- package/dist/esm/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +12 -0
- package/dist/esm/anyspend/react/components/checkout/CheckoutCartPanel.js +16 -0
- package/dist/esm/anyspend/react/components/checkout/CheckoutLayout.d.ts +10 -0
- package/dist/esm/anyspend/react/components/checkout/CheckoutLayout.js +22 -0
- package/dist/esm/anyspend/react/components/checkout/CheckoutPaymentPanel.d.ts +20 -0
- package/dist/esm/anyspend/react/components/checkout/CheckoutPaymentPanel.js +42 -0
- package/dist/esm/anyspend/react/components/checkout/CheckoutSuccess.d.ts +10 -0
- package/dist/esm/anyspend/react/components/checkout/CheckoutSuccess.js +8 -0
- package/dist/esm/anyspend/react/components/checkout/CoinbaseCheckoutPanel.d.ts +16 -0
- package/dist/esm/anyspend/react/components/checkout/CoinbaseCheckoutPanel.js +24 -0
- package/dist/esm/anyspend/react/components/checkout/CryptoCheckoutPanel.d.ts +33 -0
- package/dist/esm/anyspend/react/components/checkout/CryptoCheckoutPanel.js +313 -0
- package/dist/esm/anyspend/react/components/checkout/FiatCheckoutPanel.d.ts +16 -0
- package/dist/esm/anyspend/react/components/checkout/FiatCheckoutPanel.js +230 -0
- package/dist/esm/anyspend/react/components/checkout/PoweredByBranding.d.ts +8 -0
- package/dist/esm/anyspend/react/components/checkout/PoweredByBranding.js +6 -0
- package/dist/esm/anyspend/react/components/checkout/QRCheckoutPanel.d.ts +17 -0
- package/dist/esm/anyspend/react/components/checkout/QRCheckoutPanel.js +145 -0
- package/dist/esm/anyspend/react/components/index.d.ts +5 -1
- package/dist/esm/anyspend/react/components/index.js +3 -0
- package/dist/esm/anyspend/react/components/types/classes.d.ts +32 -0
- package/dist/esm/app.shared.js +8 -0
- package/dist/esm/global-account/react/components/B3DynamicModal.js +5 -1
- package/dist/esm/global-account/react/components/WalletImage/WalletImage.d.ts +1 -1
- package/dist/esm/global-account/react/components/ui/command.d.ts +7 -7
- package/dist/esm/global-account/react/hooks/useAuth.d.ts +1 -1
- package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +1 -1
- package/dist/esm/global-account/react/hooks/useFirstEOA.d.ts +4 -4
- package/dist/esm/global-account/react/hooks/useUser.d.ts +1 -1
- package/dist/esm/global-account/react/hooks/useUserQuery.d.ts +1 -1
- package/dist/esm/global-account/react/stores/useModalStore.d.ts +53 -1
- package/dist/esm/shared/constants/chains/b3Chain.d.ts +2 -2
- package/dist/esm/shared/constants/chains/supported.d.ts +3 -3
- package/dist/styles/index.css +1 -1
- package/dist/types/anyspend/react/components/checkout/AnySpendCheckout.d.ts +50 -0
- package/dist/types/anyspend/react/components/checkout/AnySpendCheckoutTrigger.d.ts +47 -0
- package/dist/types/anyspend/react/components/checkout/CartItemRow.d.ts +8 -0
- package/dist/types/anyspend/react/components/checkout/CartSummary.d.ts +8 -0
- package/dist/types/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +12 -0
- package/dist/types/anyspend/react/components/checkout/CheckoutLayout.d.ts +10 -0
- package/dist/types/anyspend/react/components/checkout/CheckoutPaymentPanel.d.ts +20 -0
- package/dist/types/anyspend/react/components/checkout/CheckoutSuccess.d.ts +10 -0
- package/dist/types/anyspend/react/components/checkout/CoinbaseCheckoutPanel.d.ts +16 -0
- package/dist/types/anyspend/react/components/checkout/CryptoCheckoutPanel.d.ts +33 -0
- package/dist/types/anyspend/react/components/checkout/FiatCheckoutPanel.d.ts +16 -0
- package/dist/types/anyspend/react/components/checkout/PoweredByBranding.d.ts +8 -0
- package/dist/types/anyspend/react/components/checkout/QRCheckoutPanel.d.ts +17 -0
- package/dist/types/anyspend/react/components/index.d.ts +5 -1
- package/dist/types/anyspend/react/components/types/classes.d.ts +32 -0
- package/dist/types/global-account/react/components/WalletImage/WalletImage.d.ts +1 -1
- package/dist/types/global-account/react/components/ui/command.d.ts +7 -7
- package/dist/types/global-account/react/hooks/useAuth.d.ts +1 -1
- package/dist/types/global-account/react/hooks/useAuthentication.d.ts +1 -1
- package/dist/types/global-account/react/hooks/useFirstEOA.d.ts +4 -4
- package/dist/types/global-account/react/hooks/useUser.d.ts +1 -1
- package/dist/types/global-account/react/hooks/useUserQuery.d.ts +1 -1
- package/dist/types/global-account/react/stores/useModalStore.d.ts +53 -1
- package/dist/types/shared/constants/chains/b3Chain.d.ts +2 -2
- package/dist/types/shared/constants/chains/supported.d.ts +3 -3
- package/package.json +1 -1
- package/src/anyspend/react/components/checkout/AnySpendCheckout.tsx +127 -0
- package/src/anyspend/react/components/checkout/AnySpendCheckoutTrigger.tsx +166 -0
- package/src/anyspend/react/components/checkout/CartItemRow.tsx +43 -0
- package/src/anyspend/react/components/checkout/CartSummary.tsx +23 -0
- package/src/anyspend/react/components/checkout/CheckoutCartPanel.tsx +60 -0
- package/src/anyspend/react/components/checkout/CheckoutLayout.tsx +72 -0
- package/src/anyspend/react/components/checkout/CheckoutPaymentPanel.tsx +320 -0
- package/src/anyspend/react/components/checkout/CheckoutSuccess.tsx +91 -0
- package/src/anyspend/react/components/checkout/CoinbaseCheckoutPanel.tsx +90 -0
- package/src/anyspend/react/components/checkout/CryptoCheckoutPanel.tsx +643 -0
- package/src/anyspend/react/components/checkout/FiatCheckoutPanel.tsx +568 -0
- package/src/anyspend/react/components/checkout/PoweredByBranding.tsx +32 -0
- package/src/anyspend/react/components/checkout/QRCheckoutPanel.tsx +320 -0
- package/src/anyspend/react/components/index.ts +7 -0
- package/src/anyspend/react/components/types/classes.ts +48 -0
- package/src/app.shared.ts +11 -0
- package/src/global-account/react/components/B3DynamicModal.tsx +5 -0
- package/src/global-account/react/stores/useModalStore.ts +52 -1
|
@@ -0,0 +1,643 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { components } from "@b3dotfun/sdk/anyspend/types/api";
|
|
4
|
+
import { useAnyspendQuote } from "@b3dotfun/sdk/anyspend/react/hooks/useAnyspendQuote";
|
|
5
|
+
import { useAnyspendCreateOrder } from "@b3dotfun/sdk/anyspend/react/hooks/useAnyspendCreateOrder";
|
|
6
|
+
import { useAnyspendOrderAndTransactions } from "@b3dotfun/sdk/anyspend/react/hooks/useAnyspendOrderAndTransactions";
|
|
7
|
+
import { useAnyspendTokenList } from "@b3dotfun/sdk/anyspend/react/hooks/useAnyspendTokens";
|
|
8
|
+
import { useOnOrderSuccess } from "@b3dotfun/sdk/anyspend/react/hooks/useOnOrderSuccess";
|
|
9
|
+
import { ALL_CHAINS } from "@b3dotfun/sdk/anyspend";
|
|
10
|
+
import { EVM_MAINNET } from "@b3dotfun/sdk/anyspend/utils/chain";
|
|
11
|
+
import {
|
|
12
|
+
useAccountWallet,
|
|
13
|
+
useB3Config,
|
|
14
|
+
useModalStore,
|
|
15
|
+
useSimBalance,
|
|
16
|
+
useSimTokenBalance,
|
|
17
|
+
useTokenData,
|
|
18
|
+
useUnifiedChainSwitchAndExecute,
|
|
19
|
+
} from "@b3dotfun/sdk/global-account/react";
|
|
20
|
+
import { thirdwebB3Chain } from "@b3dotfun/sdk/shared/constants/chains/b3Chain";
|
|
21
|
+
import { formatTokenAmount } from "@b3dotfun/sdk/shared/utils/number";
|
|
22
|
+
import { isNativeToken } from "@b3dotfun/sdk/anyspend/utils/token";
|
|
23
|
+
import { cn } from "@b3dotfun/sdk/shared/utils/cn";
|
|
24
|
+
import { TextShimmer } from "@b3dotfun/sdk/global-account/react";
|
|
25
|
+
import { useIsMobile } from "@b3dotfun/sdk/global-account/react";
|
|
26
|
+
import {
|
|
27
|
+
Dialog,
|
|
28
|
+
DialogContent,
|
|
29
|
+
DialogDescription,
|
|
30
|
+
DialogTitle,
|
|
31
|
+
} from "@b3dotfun/sdk/global-account/react/components/ui/dialog";
|
|
32
|
+
import {
|
|
33
|
+
Drawer,
|
|
34
|
+
DrawerContent,
|
|
35
|
+
DrawerDescription,
|
|
36
|
+
DrawerTitle,
|
|
37
|
+
} from "@b3dotfun/sdk/global-account/react/components/ui/drawer";
|
|
38
|
+
import { ChevronDown, Loader2, Search } from "lucide-react";
|
|
39
|
+
import { encodeFunctionData, erc20Abi } from "viem";
|
|
40
|
+
import { AnimatePresence, motion } from "motion/react";
|
|
41
|
+
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
42
|
+
import { ChainTokenIcon } from "../common/ChainTokenIcon";
|
|
43
|
+
import type { AnySpendCheckoutClasses } from "./AnySpendCheckout";
|
|
44
|
+
|
|
45
|
+
interface CryptoCheckoutPanelProps {
|
|
46
|
+
recipientAddress: string;
|
|
47
|
+
destinationTokenAddress: string;
|
|
48
|
+
destinationTokenChainId: number;
|
|
49
|
+
totalAmount: string;
|
|
50
|
+
buttonText?: string;
|
|
51
|
+
themeColor?: string;
|
|
52
|
+
onSuccess?: (result: { txHash?: string; orderId?: string }) => void;
|
|
53
|
+
onError?: (error: Error) => void;
|
|
54
|
+
callbackMetadata?: Record<string, unknown>;
|
|
55
|
+
classes?: AnySpendCheckoutClasses;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function CryptoCheckoutPanel({
|
|
59
|
+
recipientAddress,
|
|
60
|
+
destinationTokenAddress,
|
|
61
|
+
destinationTokenChainId,
|
|
62
|
+
totalAmount,
|
|
63
|
+
buttonText = "Pay",
|
|
64
|
+
themeColor,
|
|
65
|
+
onSuccess,
|
|
66
|
+
onError,
|
|
67
|
+
callbackMetadata,
|
|
68
|
+
classes,
|
|
69
|
+
}: CryptoCheckoutPanelProps) {
|
|
70
|
+
const [selectedSrcChainId, setSelectedSrcChainId] = useState(destinationTokenChainId);
|
|
71
|
+
const [selectedSrcToken, setSelectedSrcToken] = useState<components["schemas"]["Token"] | null>(null);
|
|
72
|
+
const [showTokenSelector, setShowTokenSelector] = useState(false);
|
|
73
|
+
const [tokenSearchQuery, setTokenSearchQuery] = useState("");
|
|
74
|
+
|
|
75
|
+
// Get wallet & modal
|
|
76
|
+
const { address: walletAddress } = useAccountWallet();
|
|
77
|
+
const { partnerId } = useB3Config();
|
|
78
|
+
const setB3ModalOpen = useModalStore(state => state.setB3ModalOpen);
|
|
79
|
+
const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType);
|
|
80
|
+
|
|
81
|
+
// Get destination token data
|
|
82
|
+
const { data: dstTokenData } = useTokenData(destinationTokenChainId, destinationTokenAddress);
|
|
83
|
+
|
|
84
|
+
// Get token list for source chain
|
|
85
|
+
const { data: tokenList, isLoading: isLoadingTokens } = useAnyspendTokenList(selectedSrcChainId, tokenSearchQuery);
|
|
86
|
+
|
|
87
|
+
// Set default source token to destination token (same-chain, no swap needed)
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (!selectedSrcToken && tokenList && tokenList.length > 0) {
|
|
90
|
+
// Try to find the destination token in the list
|
|
91
|
+
const dstToken = tokenList.find(
|
|
92
|
+
(t: components["schemas"]["Token"]) =>
|
|
93
|
+
t.address.toLowerCase() === destinationTokenAddress.toLowerCase() && t.chainId === destinationTokenChainId,
|
|
94
|
+
);
|
|
95
|
+
if (dstToken) {
|
|
96
|
+
setSelectedSrcToken(dstToken);
|
|
97
|
+
} else {
|
|
98
|
+
// Default to first token
|
|
99
|
+
setSelectedSrcToken(tokenList[0]);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}, [tokenList, selectedSrcToken, destinationTokenAddress, destinationTokenChainId]);
|
|
103
|
+
|
|
104
|
+
// Compute source amount from destination amount using quote
|
|
105
|
+
const isSameToken =
|
|
106
|
+
selectedSrcToken &&
|
|
107
|
+
selectedSrcToken.address.toLowerCase() === destinationTokenAddress.toLowerCase() &&
|
|
108
|
+
selectedSrcToken.chainId === destinationTokenChainId;
|
|
109
|
+
|
|
110
|
+
const { anyspendQuote, isLoadingAnyspendQuote } = useAnyspendQuote({
|
|
111
|
+
type: "swap",
|
|
112
|
+
srcChain: selectedSrcChainId,
|
|
113
|
+
dstChain: destinationTokenChainId,
|
|
114
|
+
srcTokenAddress: selectedSrcToken?.address || "",
|
|
115
|
+
dstTokenAddress: destinationTokenAddress,
|
|
116
|
+
tradeType: "EXACT_OUTPUT",
|
|
117
|
+
amount: totalAmount,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Get balance
|
|
121
|
+
const tokenAddress = selectedSrcToken
|
|
122
|
+
? isNativeToken(selectedSrcToken.address)
|
|
123
|
+
? "native"
|
|
124
|
+
: selectedSrcToken.address
|
|
125
|
+
: undefined;
|
|
126
|
+
const { data: balanceData } = useSimTokenBalance(walletAddress, tokenAddress, selectedSrcChainId);
|
|
127
|
+
|
|
128
|
+
const balance = useMemo(() => {
|
|
129
|
+
const b = balanceData?.balances?.[0];
|
|
130
|
+
if (!b?.amount) return { raw: BigInt(0), formatted: "0", decimals: 18 };
|
|
131
|
+
return {
|
|
132
|
+
raw: BigInt(b.amount),
|
|
133
|
+
formatted: formatTokenAmount(BigInt(b.amount), b.decimals),
|
|
134
|
+
decimals: b.decimals,
|
|
135
|
+
};
|
|
136
|
+
}, [balanceData]);
|
|
137
|
+
|
|
138
|
+
// Determine the amount to pay in source token
|
|
139
|
+
const srcAmount = useMemo(() => {
|
|
140
|
+
if (isSameToken) return totalAmount;
|
|
141
|
+
return anyspendQuote?.data?.currencyIn?.amount || "0";
|
|
142
|
+
}, [isSameToken, totalAmount, anyspendQuote]);
|
|
143
|
+
|
|
144
|
+
const srcAmountFormatted = useMemo(() => {
|
|
145
|
+
if (!selectedSrcToken) return "0";
|
|
146
|
+
const decimals = selectedSrcToken.decimals || 18;
|
|
147
|
+
return formatTokenAmount(BigInt(srcAmount || "0"), decimals);
|
|
148
|
+
}, [srcAmount, selectedSrcToken]);
|
|
149
|
+
|
|
150
|
+
// Check if user has enough balance
|
|
151
|
+
const hasEnoughBalance = balance.raw >= BigInt(srcAmount || "0");
|
|
152
|
+
|
|
153
|
+
// Order tracking state
|
|
154
|
+
const [orderId, setOrderId] = useState<string | undefined>();
|
|
155
|
+
const [isSendingDeposit, setIsSendingDeposit] = useState(false);
|
|
156
|
+
const depositSentRef = useRef(false);
|
|
157
|
+
|
|
158
|
+
// Wallet transaction execution
|
|
159
|
+
const { switchChainAndExecute } = useUnifiedChainSwitchAndExecute();
|
|
160
|
+
|
|
161
|
+
// Create order
|
|
162
|
+
const { createOrder, isCreatingOrder } = useAnyspendCreateOrder({
|
|
163
|
+
onSuccess: (data: any) => {
|
|
164
|
+
const id = data?.data?.id;
|
|
165
|
+
if (id) {
|
|
166
|
+
setOrderId(id);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
onError: (error: Error) => {
|
|
170
|
+
setIsSendingDeposit(false);
|
|
171
|
+
onError?.(error);
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// Poll order status until executed
|
|
176
|
+
const { orderAndTransactions: oat } = useAnyspendOrderAndTransactions(orderId);
|
|
177
|
+
|
|
178
|
+
// Send deposit transaction once order is created and ready
|
|
179
|
+
useEffect(() => {
|
|
180
|
+
if (!oat?.data?.order || depositSentRef.current) return;
|
|
181
|
+
const order = oat.data.order;
|
|
182
|
+
if (order.status !== "scanning_deposit_transaction") return;
|
|
183
|
+
if (oat.data.depositTxs?.length) return; // Already deposited
|
|
184
|
+
|
|
185
|
+
depositSentRef.current = true;
|
|
186
|
+
|
|
187
|
+
const sendDeposit = async () => {
|
|
188
|
+
try {
|
|
189
|
+
setIsSendingDeposit(true);
|
|
190
|
+
const amount = BigInt(order.srcAmount);
|
|
191
|
+
|
|
192
|
+
if (isNativeToken(order.srcTokenAddress)) {
|
|
193
|
+
await switchChainAndExecute(order.srcChain, {
|
|
194
|
+
to: order.globalAddress as `0x${string}`,
|
|
195
|
+
value: amount,
|
|
196
|
+
});
|
|
197
|
+
} else {
|
|
198
|
+
const data = encodeFunctionData({
|
|
199
|
+
abi: erc20Abi,
|
|
200
|
+
functionName: "transfer",
|
|
201
|
+
args: [order.globalAddress as `0x${string}`, amount],
|
|
202
|
+
});
|
|
203
|
+
await switchChainAndExecute(order.srcChain, {
|
|
204
|
+
to: order.srcTokenAddress as `0x${string}`,
|
|
205
|
+
data,
|
|
206
|
+
value: BigInt(0),
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
} catch (error: any) {
|
|
210
|
+
depositSentRef.current = false;
|
|
211
|
+
onError?.(error instanceof Error ? error : new Error(error?.message || "Transaction rejected"));
|
|
212
|
+
} finally {
|
|
213
|
+
setIsSendingDeposit(false);
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
sendDeposit();
|
|
218
|
+
}, [oat, switchChainAndExecute, onError]);
|
|
219
|
+
|
|
220
|
+
// Only call onSuccess when order is actually executed with a real txHash
|
|
221
|
+
useOnOrderSuccess({
|
|
222
|
+
orderData: oat,
|
|
223
|
+
orderId,
|
|
224
|
+
onSuccess: (txHash?: string) => {
|
|
225
|
+
onSuccess?.({ orderId, txHash });
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const isWaitingForExecution = !!orderId && oat?.data?.order.status !== "executed";
|
|
230
|
+
|
|
231
|
+
const handlePay = useCallback(() => {
|
|
232
|
+
if (!selectedSrcToken || !walletAddress) return;
|
|
233
|
+
|
|
234
|
+
depositSentRef.current = false;
|
|
235
|
+
|
|
236
|
+
const dstToken: components["schemas"]["Token"] = {
|
|
237
|
+
address: destinationTokenAddress,
|
|
238
|
+
chainId: destinationTokenChainId,
|
|
239
|
+
decimals: dstTokenData?.decimals || 18,
|
|
240
|
+
symbol: dstTokenData?.symbol || "",
|
|
241
|
+
name: dstTokenData?.name || "",
|
|
242
|
+
metadata: {
|
|
243
|
+
logoURI: dstTokenData?.logoURI || "",
|
|
244
|
+
},
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
createOrder({
|
|
248
|
+
recipientAddress,
|
|
249
|
+
orderType: "swap",
|
|
250
|
+
srcChain: selectedSrcChainId,
|
|
251
|
+
dstChain: destinationTokenChainId,
|
|
252
|
+
srcToken: selectedSrcToken,
|
|
253
|
+
dstToken,
|
|
254
|
+
srcAmount,
|
|
255
|
+
expectedDstAmount: totalAmount,
|
|
256
|
+
callbackMetadata,
|
|
257
|
+
});
|
|
258
|
+
}, [
|
|
259
|
+
selectedSrcToken,
|
|
260
|
+
walletAddress,
|
|
261
|
+
recipientAddress,
|
|
262
|
+
selectedSrcChainId,
|
|
263
|
+
destinationTokenChainId,
|
|
264
|
+
destinationTokenAddress,
|
|
265
|
+
dstTokenData,
|
|
266
|
+
srcAmount,
|
|
267
|
+
totalAmount,
|
|
268
|
+
callbackMetadata,
|
|
269
|
+
createOrder,
|
|
270
|
+
]);
|
|
271
|
+
|
|
272
|
+
const handleSelectToken = (token: components["schemas"]["Token"]) => {
|
|
273
|
+
setSelectedSrcToken(token);
|
|
274
|
+
setSelectedSrcChainId(token.chainId);
|
|
275
|
+
setShowTokenSelector(false);
|
|
276
|
+
setTokenSearchQuery("");
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
const isLoading = isLoadingAnyspendQuote || isLoadingTokens;
|
|
280
|
+
const isPending = isCreatingOrder || isSendingDeposit || isWaitingForExecution;
|
|
281
|
+
const canPay = walletAddress && selectedSrcToken && hasEnoughBalance && !isLoading && !isPending;
|
|
282
|
+
|
|
283
|
+
return (
|
|
284
|
+
<div className={cn("anyspend-crypto-panel flex flex-col gap-4", classes?.cryptoPanel)}>
|
|
285
|
+
{/* Token Selector */}
|
|
286
|
+
<div className="anyspend-token-selector">
|
|
287
|
+
<label className="anyspend-token-label mb-1.5 block text-sm font-medium text-gray-700 dark:text-gray-300">
|
|
288
|
+
Pay with
|
|
289
|
+
</label>
|
|
290
|
+
<button
|
|
291
|
+
onClick={() => setShowTokenSelector(true)}
|
|
292
|
+
className={cn(
|
|
293
|
+
"anyspend-token-btn flex w-full items-center justify-between rounded-xl border border-gray-200 bg-white px-4 py-3 transition-colors hover:border-gray-300 dark:border-gray-700 dark:bg-gray-800 dark:hover:border-gray-600",
|
|
294
|
+
classes?.tokenSelector,
|
|
295
|
+
)}
|
|
296
|
+
>
|
|
297
|
+
{selectedSrcToken ? (
|
|
298
|
+
<div className="flex items-center gap-3">
|
|
299
|
+
<ChainTokenIcon
|
|
300
|
+
chainUrl={ALL_CHAINS[selectedSrcToken.chainId]?.logoUrl || ""}
|
|
301
|
+
tokenUrl={selectedSrcToken.metadata?.logoURI}
|
|
302
|
+
className="h-8 w-8"
|
|
303
|
+
/>
|
|
304
|
+
<div className="text-left">
|
|
305
|
+
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">{selectedSrcToken.symbol}</p>
|
|
306
|
+
<p className="text-xs text-gray-500 dark:text-gray-400">Balance: {balance.formatted}</p>
|
|
307
|
+
</div>
|
|
308
|
+
</div>
|
|
309
|
+
) : (
|
|
310
|
+
<span className="text-sm text-gray-400">Select token</span>
|
|
311
|
+
)}
|
|
312
|
+
<ChevronDown className="h-4 w-4 text-gray-400" />
|
|
313
|
+
</button>
|
|
314
|
+
</div>
|
|
315
|
+
|
|
316
|
+
{/* Token Selector Modal */}
|
|
317
|
+
<TokenSelectorModal
|
|
318
|
+
open={showTokenSelector}
|
|
319
|
+
onClose={() => {
|
|
320
|
+
setShowTokenSelector(false);
|
|
321
|
+
setTokenSearchQuery("");
|
|
322
|
+
}}
|
|
323
|
+
tokenList={tokenList}
|
|
324
|
+
isLoadingTokens={isLoadingTokens}
|
|
325
|
+
tokenSearchQuery={tokenSearchQuery}
|
|
326
|
+
onSearchChange={setTokenSearchQuery}
|
|
327
|
+
onSelectToken={handleSelectToken}
|
|
328
|
+
selectedToken={selectedSrcToken}
|
|
329
|
+
walletAddress={walletAddress}
|
|
330
|
+
chainId={selectedSrcChainId}
|
|
331
|
+
onChainChange={chainId => {
|
|
332
|
+
setSelectedSrcChainId(chainId);
|
|
333
|
+
setSelectedSrcToken(null);
|
|
334
|
+
setTokenSearchQuery("");
|
|
335
|
+
}}
|
|
336
|
+
/>
|
|
337
|
+
|
|
338
|
+
{/* Quote Display */}
|
|
339
|
+
<motion.div
|
|
340
|
+
initial={{ opacity: 0, y: 6 }}
|
|
341
|
+
animate={{ opacity: 1, y: 0 }}
|
|
342
|
+
transition={{ duration: 0.25, ease: "easeOut" }}
|
|
343
|
+
className={cn(
|
|
344
|
+
"anyspend-quote-display rounded-xl border border-gray-200 bg-gray-50 px-4 py-3 dark:border-gray-700 dark:bg-gray-800/50",
|
|
345
|
+
classes?.quoteDisplay,
|
|
346
|
+
)}
|
|
347
|
+
>
|
|
348
|
+
<div className="flex items-center justify-between">
|
|
349
|
+
<span className="text-sm text-gray-500 dark:text-gray-400">You pay</span>
|
|
350
|
+
<AnimatePresence mode="wait">
|
|
351
|
+
{isLoadingAnyspendQuote ? (
|
|
352
|
+
<motion.div
|
|
353
|
+
key="quote-loading"
|
|
354
|
+
initial={{ opacity: 0 }}
|
|
355
|
+
animate={{ opacity: 1 }}
|
|
356
|
+
exit={{ opacity: 0 }}
|
|
357
|
+
transition={{ duration: 0.15 }}
|
|
358
|
+
>
|
|
359
|
+
<TextShimmer duration={1} className="text-sm">
|
|
360
|
+
Fetching quote...
|
|
361
|
+
</TextShimmer>
|
|
362
|
+
</motion.div>
|
|
363
|
+
) : (
|
|
364
|
+
<motion.span
|
|
365
|
+
key="quote-amount"
|
|
366
|
+
initial={{ opacity: 0 }}
|
|
367
|
+
animate={{ opacity: 1 }}
|
|
368
|
+
exit={{ opacity: 0 }}
|
|
369
|
+
transition={{ duration: 0.15 }}
|
|
370
|
+
className="text-sm font-medium text-gray-900 dark:text-gray-100"
|
|
371
|
+
>
|
|
372
|
+
{srcAmountFormatted} {selectedSrcToken?.symbol || ""}
|
|
373
|
+
</motion.span>
|
|
374
|
+
)}
|
|
375
|
+
</AnimatePresence>
|
|
376
|
+
</div>
|
|
377
|
+
</motion.div>
|
|
378
|
+
|
|
379
|
+
{/* Insufficient Balance Warning */}
|
|
380
|
+
<AnimatePresence>
|
|
381
|
+
{walletAddress && selectedSrcToken && !hasEnoughBalance && !isLoading && (
|
|
382
|
+
<motion.p
|
|
383
|
+
key="balance-warning"
|
|
384
|
+
initial={{ opacity: 0, height: 0 }}
|
|
385
|
+
animate={{ opacity: 1, height: "auto" }}
|
|
386
|
+
exit={{ opacity: 0, height: 0 }}
|
|
387
|
+
transition={{ duration: 0.2, ease: "easeOut" }}
|
|
388
|
+
className="anyspend-balance-warning text-center text-sm text-red-500"
|
|
389
|
+
>
|
|
390
|
+
Insufficient {selectedSrcToken.symbol} balance
|
|
391
|
+
</motion.p>
|
|
392
|
+
)}
|
|
393
|
+
</AnimatePresence>
|
|
394
|
+
|
|
395
|
+
{/* Pay / Connect Wallet Button */}
|
|
396
|
+
{!walletAddress ? (
|
|
397
|
+
<button
|
|
398
|
+
onClick={() => {
|
|
399
|
+
setB3ModalContentType({ type: "signInWithB3", showBackButton: false, chain: thirdwebB3Chain, partnerId });
|
|
400
|
+
setB3ModalOpen(true);
|
|
401
|
+
}}
|
|
402
|
+
className={cn(
|
|
403
|
+
"anyspend-crypto-pay-btn w-full rounded-xl px-4 py-3.5 text-sm font-semibold text-white transition-all",
|
|
404
|
+
"bg-blue-600 hover:bg-blue-700 active:scale-[0.98]",
|
|
405
|
+
classes?.payButton,
|
|
406
|
+
)}
|
|
407
|
+
style={themeColor ? { backgroundColor: themeColor } : undefined}
|
|
408
|
+
>
|
|
409
|
+
Connect Wallet to Pay
|
|
410
|
+
</button>
|
|
411
|
+
) : (
|
|
412
|
+
<button
|
|
413
|
+
onClick={handlePay}
|
|
414
|
+
disabled={!canPay}
|
|
415
|
+
className={cn(
|
|
416
|
+
"anyspend-crypto-pay-btn w-full rounded-xl px-4 py-3.5 text-sm font-semibold text-white transition-all",
|
|
417
|
+
canPay ? "bg-blue-600 hover:bg-blue-700 active:scale-[0.98]" : "cursor-not-allowed bg-blue-600 opacity-50",
|
|
418
|
+
classes?.payButton,
|
|
419
|
+
)}
|
|
420
|
+
style={!canPay ? undefined : themeColor ? { backgroundColor: themeColor } : undefined}
|
|
421
|
+
>
|
|
422
|
+
{isPending ? (
|
|
423
|
+
<span className="flex items-center justify-center gap-2">
|
|
424
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
425
|
+
{isCreatingOrder
|
|
426
|
+
? "Creating order..."
|
|
427
|
+
: isSendingDeposit
|
|
428
|
+
? "Confirm in wallet..."
|
|
429
|
+
: "Confirming transaction..."}
|
|
430
|
+
</span>
|
|
431
|
+
) : (
|
|
432
|
+
buttonText
|
|
433
|
+
)}
|
|
434
|
+
</button>
|
|
435
|
+
)}
|
|
436
|
+
</div>
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// -------------------------------------------------------------------
|
|
441
|
+
// Token Selector Modal
|
|
442
|
+
// -------------------------------------------------------------------
|
|
443
|
+
|
|
444
|
+
export interface TokenSelectorModalProps {
|
|
445
|
+
open: boolean;
|
|
446
|
+
onClose: () => void;
|
|
447
|
+
tokenList: components["schemas"]["Token"][] | undefined;
|
|
448
|
+
isLoadingTokens: boolean;
|
|
449
|
+
tokenSearchQuery: string;
|
|
450
|
+
onSearchChange: (query: string) => void;
|
|
451
|
+
onSelectToken: (token: components["schemas"]["Token"]) => void;
|
|
452
|
+
selectedToken: components["schemas"]["Token"] | null;
|
|
453
|
+
walletAddress?: string;
|
|
454
|
+
chainId: number;
|
|
455
|
+
onChainChange: (chainId: number) => void;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const SOURCE_CHAINS = Object.values(EVM_MAINNET).map(c => ({ id: c.id, name: c.name, logoUrl: c.logoUrl }));
|
|
459
|
+
|
|
460
|
+
export function TokenSelectorModal({
|
|
461
|
+
open,
|
|
462
|
+
onClose,
|
|
463
|
+
tokenList,
|
|
464
|
+
isLoadingTokens,
|
|
465
|
+
tokenSearchQuery,
|
|
466
|
+
onSearchChange,
|
|
467
|
+
onSelectToken,
|
|
468
|
+
selectedToken,
|
|
469
|
+
walletAddress,
|
|
470
|
+
chainId,
|
|
471
|
+
onChainChange,
|
|
472
|
+
}: TokenSelectorModalProps) {
|
|
473
|
+
const isMobile = useIsMobile();
|
|
474
|
+
const searchInputRef = useRef<HTMLInputElement>(null);
|
|
475
|
+
|
|
476
|
+
// Fetch all balances for the wallet on this chain
|
|
477
|
+
const { data: balanceData } = useSimBalance(walletAddress, [chainId]);
|
|
478
|
+
|
|
479
|
+
// Build a lookup map: lowercase token address -> balance info
|
|
480
|
+
const balanceMap = useMemo(() => {
|
|
481
|
+
const map = new Map<string, { raw: bigint; formatted: string; decimals: number }>();
|
|
482
|
+
if (!balanceData?.balances) return map;
|
|
483
|
+
for (const b of balanceData.balances) {
|
|
484
|
+
if (b.amount && BigInt(b.amount) > BigInt(0)) {
|
|
485
|
+
map.set(b.address.toLowerCase(), {
|
|
486
|
+
raw: BigInt(b.amount),
|
|
487
|
+
formatted: formatTokenAmount(BigInt(b.amount), b.decimals),
|
|
488
|
+
decimals: b.decimals,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
return map;
|
|
493
|
+
}, [balanceData]);
|
|
494
|
+
|
|
495
|
+
// Sort tokens: tokens with balance first (sorted by balance desc), then the rest
|
|
496
|
+
const sortedTokenList = useMemo(() => {
|
|
497
|
+
if (!tokenList) return undefined;
|
|
498
|
+
const withBalance: components["schemas"]["Token"][] = [];
|
|
499
|
+
const withoutBalance: components["schemas"]["Token"][] = [];
|
|
500
|
+
for (const token of tokenList) {
|
|
501
|
+
const bal = balanceMap.get(token.address.toLowerCase());
|
|
502
|
+
if (bal) {
|
|
503
|
+
withBalance.push(token);
|
|
504
|
+
} else {
|
|
505
|
+
withoutBalance.push(token);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
withBalance.sort((a, b) => {
|
|
509
|
+
const balA = balanceMap.get(a.address.toLowerCase())?.raw || BigInt(0);
|
|
510
|
+
const balB = balanceMap.get(b.address.toLowerCase())?.raw || BigInt(0);
|
|
511
|
+
if (balB > balA) return 1;
|
|
512
|
+
if (balB < balA) return -1;
|
|
513
|
+
return 0;
|
|
514
|
+
});
|
|
515
|
+
return [...withBalance, ...withoutBalance];
|
|
516
|
+
}, [tokenList, balanceMap]);
|
|
517
|
+
|
|
518
|
+
// Keep showing the previous list while new chain tokens are loading
|
|
519
|
+
const prevListRef = useRef<components["schemas"]["Token"][] | undefined>(undefined);
|
|
520
|
+
if (sortedTokenList && sortedTokenList.length > 0) {
|
|
521
|
+
prevListRef.current = sortedTokenList;
|
|
522
|
+
}
|
|
523
|
+
const displayList =
|
|
524
|
+
sortedTokenList && sortedTokenList.length > 0
|
|
525
|
+
? sortedTokenList
|
|
526
|
+
: isLoadingTokens
|
|
527
|
+
? prevListRef.current
|
|
528
|
+
: sortedTokenList;
|
|
529
|
+
|
|
530
|
+
// Focus search input when modal opens
|
|
531
|
+
useEffect(() => {
|
|
532
|
+
if (open) {
|
|
533
|
+
const timer = setTimeout(() => searchInputRef.current?.focus(), 100);
|
|
534
|
+
return () => clearTimeout(timer);
|
|
535
|
+
}
|
|
536
|
+
}, [open]);
|
|
537
|
+
|
|
538
|
+
const ModalComponent = isMobile ? Drawer : Dialog;
|
|
539
|
+
const ModalContent = isMobile ? DrawerContent : DialogContent;
|
|
540
|
+
const ModalTitle = isMobile ? DrawerTitle : DialogTitle;
|
|
541
|
+
const ModalDescription = isMobile ? DrawerDescription : DialogDescription;
|
|
542
|
+
|
|
543
|
+
return (
|
|
544
|
+
<ModalComponent
|
|
545
|
+
open={open}
|
|
546
|
+
onOpenChange={(v: boolean) => {
|
|
547
|
+
if (!v) onClose();
|
|
548
|
+
}}
|
|
549
|
+
>
|
|
550
|
+
{/* Hide the Global Account branding footer from the SDK dialog */}
|
|
551
|
+
<style
|
|
552
|
+
dangerouslySetInnerHTML={{
|
|
553
|
+
__html: `.anyspend-token-modal .b3-modal-ga-branding { display: none; } .anyspend-token-modal .modal-inner-content { margin-bottom: 0; }`,
|
|
554
|
+
}}
|
|
555
|
+
/>
|
|
556
|
+
<ModalContent className="anyspend-token-modal flex max-h-[80dvh] flex-col overflow-hidden rounded-2xl bg-white p-0 shadow-xl sm:max-h-[70dvh] dark:bg-gray-900">
|
|
557
|
+
<ModalTitle className="sr-only">Select token</ModalTitle>
|
|
558
|
+
<ModalDescription className="sr-only">Choose a token to pay with</ModalDescription>
|
|
559
|
+
|
|
560
|
+
<div className="flex min-h-0 flex-1 flex-col">
|
|
561
|
+
{/* Header */}
|
|
562
|
+
<div className="flex items-center justify-between px-5 py-4">
|
|
563
|
+
<h3 className="text-base font-semibold text-gray-900 dark:text-gray-100">Select token</h3>
|
|
564
|
+
</div>
|
|
565
|
+
|
|
566
|
+
{/* Chain Selector */}
|
|
567
|
+
<div className="anyspend-chain-selector flex items-center gap-2 px-5 pb-3">
|
|
568
|
+
{SOURCE_CHAINS.map(chain => (
|
|
569
|
+
<button
|
|
570
|
+
key={chain.id}
|
|
571
|
+
onClick={() => onChainChange(chain.id)}
|
|
572
|
+
title={chain.name}
|
|
573
|
+
className="relative shrink-0 rounded-full transition-opacity"
|
|
574
|
+
style={{ opacity: chain.id === chainId ? 1 : 0.4 }}
|
|
575
|
+
>
|
|
576
|
+
<img src={chain.logoUrl} alt={chain.name} className="h-7 w-7 rounded-full" />
|
|
577
|
+
{chain.id === chainId && (
|
|
578
|
+
<div className="absolute inset-0 rounded-full" style={{ boxShadow: "0 0 0 2px #3b82f6" }} />
|
|
579
|
+
)}
|
|
580
|
+
</button>
|
|
581
|
+
))}
|
|
582
|
+
</div>
|
|
583
|
+
|
|
584
|
+
{/* Search */}
|
|
585
|
+
<div className="anyspend-token-search flex items-center gap-2 border-b border-gray-100 px-5 py-2.5 dark:border-gray-800">
|
|
586
|
+
<Search className="h-4 w-4 shrink-0 text-gray-400" />
|
|
587
|
+
<input
|
|
588
|
+
ref={searchInputRef}
|
|
589
|
+
type="text"
|
|
590
|
+
value={tokenSearchQuery}
|
|
591
|
+
onChange={e => onSearchChange(e.target.value)}
|
|
592
|
+
placeholder="Search tokens..."
|
|
593
|
+
className="anyspend-token-search-input w-full bg-transparent text-sm outline-none placeholder:text-gray-400 dark:text-gray-100"
|
|
594
|
+
/>
|
|
595
|
+
</div>
|
|
596
|
+
|
|
597
|
+
{/* Token List */}
|
|
598
|
+
<div className="anyspend-token-list relative flex-1 overflow-y-auto" style={{ minHeight: 300 }}>
|
|
599
|
+
{displayList?.map((token: components["schemas"]["Token"]) => {
|
|
600
|
+
const isSelected =
|
|
601
|
+
selectedToken &&
|
|
602
|
+
selectedToken.address.toLowerCase() === token.address.toLowerCase() &&
|
|
603
|
+
selectedToken.chainId === token.chainId;
|
|
604
|
+
const tokenBalance = balanceMap.get(token.address.toLowerCase());
|
|
605
|
+
|
|
606
|
+
return (
|
|
607
|
+
<button
|
|
608
|
+
key={`${token.chainId}-${token.address}`}
|
|
609
|
+
onClick={() => onSelectToken(token)}
|
|
610
|
+
className={cn(
|
|
611
|
+
"anyspend-token-option flex w-full items-center gap-3 px-5 py-3 text-left transition-colors hover:bg-gray-50 dark:hover:bg-gray-800",
|
|
612
|
+
isSelected && "bg-blue-50 dark:bg-blue-900/20",
|
|
613
|
+
)}
|
|
614
|
+
>
|
|
615
|
+
<ChainTokenIcon
|
|
616
|
+
chainUrl={ALL_CHAINS[token.chainId]?.logoUrl || ""}
|
|
617
|
+
tokenUrl={token.metadata?.logoURI}
|
|
618
|
+
className="h-8 w-8"
|
|
619
|
+
/>
|
|
620
|
+
<div className="min-w-0 flex-1">
|
|
621
|
+
<p className="text-sm font-medium text-gray-900 dark:text-gray-100">{token.symbol}</p>
|
|
622
|
+
<p className="truncate text-xs text-gray-500 dark:text-gray-400">{token.name}</p>
|
|
623
|
+
</div>
|
|
624
|
+
<div className="flex items-center gap-2">
|
|
625
|
+
{tokenBalance && (
|
|
626
|
+
<span className="text-xs font-medium text-gray-600 dark:text-gray-300">
|
|
627
|
+
{tokenBalance.formatted}
|
|
628
|
+
</span>
|
|
629
|
+
)}
|
|
630
|
+
{isSelected && <div className="h-2 w-2 rounded-full bg-blue-600" />}
|
|
631
|
+
</div>
|
|
632
|
+
</button>
|
|
633
|
+
);
|
|
634
|
+
})}
|
|
635
|
+
{!isLoadingTokens && displayList && displayList.length === 0 && (
|
|
636
|
+
<div className="px-5 py-8 text-center text-sm text-gray-400">No tokens found</div>
|
|
637
|
+
)}
|
|
638
|
+
</div>
|
|
639
|
+
</div>
|
|
640
|
+
</ModalContent>
|
|
641
|
+
</ModalComponent>
|
|
642
|
+
);
|
|
643
|
+
}
|