@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.
Files changed (127) hide show
  1. package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.d.ts +50 -0
  2. package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckout.js +30 -0
  3. package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckoutTrigger.d.ts +47 -0
  4. package/dist/cjs/anyspend/react/components/checkout/AnySpendCheckoutTrigger.js +45 -0
  5. package/dist/cjs/anyspend/react/components/checkout/CartItemRow.d.ts +8 -0
  6. package/dist/cjs/anyspend/react/components/checkout/CartItemRow.js +9 -0
  7. package/dist/cjs/anyspend/react/components/checkout/CartSummary.d.ts +8 -0
  8. package/dist/cjs/anyspend/react/components/checkout/CartSummary.js +9 -0
  9. package/dist/cjs/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +12 -0
  10. package/dist/cjs/anyspend/react/components/checkout/CheckoutCartPanel.js +19 -0
  11. package/dist/cjs/anyspend/react/components/checkout/CheckoutLayout.d.ts +10 -0
  12. package/dist/cjs/anyspend/react/components/checkout/CheckoutLayout.js +25 -0
  13. package/dist/cjs/anyspend/react/components/checkout/CheckoutPaymentPanel.d.ts +20 -0
  14. package/dist/cjs/anyspend/react/components/checkout/CheckoutPaymentPanel.js +45 -0
  15. package/dist/cjs/anyspend/react/components/checkout/CheckoutSuccess.d.ts +10 -0
  16. package/dist/cjs/anyspend/react/components/checkout/CheckoutSuccess.js +11 -0
  17. package/dist/cjs/anyspend/react/components/checkout/CoinbaseCheckoutPanel.d.ts +16 -0
  18. package/dist/cjs/anyspend/react/components/checkout/CoinbaseCheckoutPanel.js +27 -0
  19. package/dist/cjs/anyspend/react/components/checkout/CryptoCheckoutPanel.d.ts +33 -0
  20. package/dist/cjs/anyspend/react/components/checkout/CryptoCheckoutPanel.js +317 -0
  21. package/dist/cjs/anyspend/react/components/checkout/FiatCheckoutPanel.d.ts +16 -0
  22. package/dist/cjs/anyspend/react/components/checkout/FiatCheckoutPanel.js +233 -0
  23. package/dist/cjs/anyspend/react/components/checkout/PoweredByBranding.d.ts +8 -0
  24. package/dist/cjs/anyspend/react/components/checkout/PoweredByBranding.js +9 -0
  25. package/dist/cjs/anyspend/react/components/checkout/QRCheckoutPanel.d.ts +17 -0
  26. package/dist/cjs/anyspend/react/components/checkout/QRCheckoutPanel.js +148 -0
  27. package/dist/cjs/anyspend/react/components/index.d.ts +5 -1
  28. package/dist/cjs/anyspend/react/components/index.js +6 -1
  29. package/dist/cjs/anyspend/react/components/types/classes.d.ts +32 -0
  30. package/dist/cjs/app.shared.js +8 -0
  31. package/dist/cjs/global-account/react/components/B3DynamicModal.js +5 -1
  32. package/dist/cjs/global-account/react/components/WalletImage/WalletImage.d.ts +1 -1
  33. package/dist/cjs/global-account/react/components/ui/command.d.ts +7 -7
  34. package/dist/cjs/global-account/react/hooks/useAuth.d.ts +1 -1
  35. package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +1 -1
  36. package/dist/cjs/global-account/react/hooks/useFirstEOA.d.ts +4 -4
  37. package/dist/cjs/global-account/react/hooks/useUser.d.ts +1 -1
  38. package/dist/cjs/global-account/react/hooks/useUserQuery.d.ts +1 -1
  39. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +53 -1
  40. package/dist/cjs/shared/constants/chains/b3Chain.d.ts +2 -2
  41. package/dist/cjs/shared/constants/chains/supported.d.ts +3 -3
  42. package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.d.ts +50 -0
  43. package/dist/esm/anyspend/react/components/checkout/AnySpendCheckout.js +27 -0
  44. package/dist/esm/anyspend/react/components/checkout/AnySpendCheckoutTrigger.d.ts +47 -0
  45. package/dist/esm/anyspend/react/components/checkout/AnySpendCheckoutTrigger.js +42 -0
  46. package/dist/esm/anyspend/react/components/checkout/CartItemRow.d.ts +8 -0
  47. package/dist/esm/anyspend/react/components/checkout/CartItemRow.js +6 -0
  48. package/dist/esm/anyspend/react/components/checkout/CartSummary.d.ts +8 -0
  49. package/dist/esm/anyspend/react/components/checkout/CartSummary.js +6 -0
  50. package/dist/esm/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +12 -0
  51. package/dist/esm/anyspend/react/components/checkout/CheckoutCartPanel.js +16 -0
  52. package/dist/esm/anyspend/react/components/checkout/CheckoutLayout.d.ts +10 -0
  53. package/dist/esm/anyspend/react/components/checkout/CheckoutLayout.js +22 -0
  54. package/dist/esm/anyspend/react/components/checkout/CheckoutPaymentPanel.d.ts +20 -0
  55. package/dist/esm/anyspend/react/components/checkout/CheckoutPaymentPanel.js +42 -0
  56. package/dist/esm/anyspend/react/components/checkout/CheckoutSuccess.d.ts +10 -0
  57. package/dist/esm/anyspend/react/components/checkout/CheckoutSuccess.js +8 -0
  58. package/dist/esm/anyspend/react/components/checkout/CoinbaseCheckoutPanel.d.ts +16 -0
  59. package/dist/esm/anyspend/react/components/checkout/CoinbaseCheckoutPanel.js +24 -0
  60. package/dist/esm/anyspend/react/components/checkout/CryptoCheckoutPanel.d.ts +33 -0
  61. package/dist/esm/anyspend/react/components/checkout/CryptoCheckoutPanel.js +313 -0
  62. package/dist/esm/anyspend/react/components/checkout/FiatCheckoutPanel.d.ts +16 -0
  63. package/dist/esm/anyspend/react/components/checkout/FiatCheckoutPanel.js +230 -0
  64. package/dist/esm/anyspend/react/components/checkout/PoweredByBranding.d.ts +8 -0
  65. package/dist/esm/anyspend/react/components/checkout/PoweredByBranding.js +6 -0
  66. package/dist/esm/anyspend/react/components/checkout/QRCheckoutPanel.d.ts +17 -0
  67. package/dist/esm/anyspend/react/components/checkout/QRCheckoutPanel.js +145 -0
  68. package/dist/esm/anyspend/react/components/index.d.ts +5 -1
  69. package/dist/esm/anyspend/react/components/index.js +3 -0
  70. package/dist/esm/anyspend/react/components/types/classes.d.ts +32 -0
  71. package/dist/esm/app.shared.js +8 -0
  72. package/dist/esm/global-account/react/components/B3DynamicModal.js +5 -1
  73. package/dist/esm/global-account/react/components/WalletImage/WalletImage.d.ts +1 -1
  74. package/dist/esm/global-account/react/components/ui/command.d.ts +7 -7
  75. package/dist/esm/global-account/react/hooks/useAuth.d.ts +1 -1
  76. package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +1 -1
  77. package/dist/esm/global-account/react/hooks/useFirstEOA.d.ts +4 -4
  78. package/dist/esm/global-account/react/hooks/useUser.d.ts +1 -1
  79. package/dist/esm/global-account/react/hooks/useUserQuery.d.ts +1 -1
  80. package/dist/esm/global-account/react/stores/useModalStore.d.ts +53 -1
  81. package/dist/esm/shared/constants/chains/b3Chain.d.ts +2 -2
  82. package/dist/esm/shared/constants/chains/supported.d.ts +3 -3
  83. package/dist/styles/index.css +1 -1
  84. package/dist/types/anyspend/react/components/checkout/AnySpendCheckout.d.ts +50 -0
  85. package/dist/types/anyspend/react/components/checkout/AnySpendCheckoutTrigger.d.ts +47 -0
  86. package/dist/types/anyspend/react/components/checkout/CartItemRow.d.ts +8 -0
  87. package/dist/types/anyspend/react/components/checkout/CartSummary.d.ts +8 -0
  88. package/dist/types/anyspend/react/components/checkout/CheckoutCartPanel.d.ts +12 -0
  89. package/dist/types/anyspend/react/components/checkout/CheckoutLayout.d.ts +10 -0
  90. package/dist/types/anyspend/react/components/checkout/CheckoutPaymentPanel.d.ts +20 -0
  91. package/dist/types/anyspend/react/components/checkout/CheckoutSuccess.d.ts +10 -0
  92. package/dist/types/anyspend/react/components/checkout/CoinbaseCheckoutPanel.d.ts +16 -0
  93. package/dist/types/anyspend/react/components/checkout/CryptoCheckoutPanel.d.ts +33 -0
  94. package/dist/types/anyspend/react/components/checkout/FiatCheckoutPanel.d.ts +16 -0
  95. package/dist/types/anyspend/react/components/checkout/PoweredByBranding.d.ts +8 -0
  96. package/dist/types/anyspend/react/components/checkout/QRCheckoutPanel.d.ts +17 -0
  97. package/dist/types/anyspend/react/components/index.d.ts +5 -1
  98. package/dist/types/anyspend/react/components/types/classes.d.ts +32 -0
  99. package/dist/types/global-account/react/components/WalletImage/WalletImage.d.ts +1 -1
  100. package/dist/types/global-account/react/components/ui/command.d.ts +7 -7
  101. package/dist/types/global-account/react/hooks/useAuth.d.ts +1 -1
  102. package/dist/types/global-account/react/hooks/useAuthentication.d.ts +1 -1
  103. package/dist/types/global-account/react/hooks/useFirstEOA.d.ts +4 -4
  104. package/dist/types/global-account/react/hooks/useUser.d.ts +1 -1
  105. package/dist/types/global-account/react/hooks/useUserQuery.d.ts +1 -1
  106. package/dist/types/global-account/react/stores/useModalStore.d.ts +53 -1
  107. package/dist/types/shared/constants/chains/b3Chain.d.ts +2 -2
  108. package/dist/types/shared/constants/chains/supported.d.ts +3 -3
  109. package/package.json +1 -1
  110. package/src/anyspend/react/components/checkout/AnySpendCheckout.tsx +127 -0
  111. package/src/anyspend/react/components/checkout/AnySpendCheckoutTrigger.tsx +166 -0
  112. package/src/anyspend/react/components/checkout/CartItemRow.tsx +43 -0
  113. package/src/anyspend/react/components/checkout/CartSummary.tsx +23 -0
  114. package/src/anyspend/react/components/checkout/CheckoutCartPanel.tsx +60 -0
  115. package/src/anyspend/react/components/checkout/CheckoutLayout.tsx +72 -0
  116. package/src/anyspend/react/components/checkout/CheckoutPaymentPanel.tsx +320 -0
  117. package/src/anyspend/react/components/checkout/CheckoutSuccess.tsx +91 -0
  118. package/src/anyspend/react/components/checkout/CoinbaseCheckoutPanel.tsx +90 -0
  119. package/src/anyspend/react/components/checkout/CryptoCheckoutPanel.tsx +643 -0
  120. package/src/anyspend/react/components/checkout/FiatCheckoutPanel.tsx +568 -0
  121. package/src/anyspend/react/components/checkout/PoweredByBranding.tsx +32 -0
  122. package/src/anyspend/react/components/checkout/QRCheckoutPanel.tsx +320 -0
  123. package/src/anyspend/react/components/index.ts +7 -0
  124. package/src/anyspend/react/components/types/classes.ts +48 -0
  125. package/src/app.shared.ts +11 -0
  126. package/src/global-account/react/components/B3DynamicModal.tsx +5 -0
  127. 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
+ }