@b3dotfun/sdk 0.1.68-alpha.6 → 0.1.68-alpha.8

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 (26) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.d.ts +2 -0
  2. package/dist/cjs/anyspend/react/components/AnySpend.js +6 -12
  3. package/dist/cjs/anyspend/react/components/checkout/CryptoPayPanel.js +1 -1
  4. package/dist/cjs/anyspend/react/components/checkout/FiatCheckoutPanel.js +32 -3
  5. package/dist/cjs/anyspend/react/components/checkout/KycGate.js +32 -10
  6. package/dist/cjs/anyspend/react/hooks/useAnyspendCreateOnrampOrder.js +7 -3
  7. package/dist/cjs/anyspend/react/hooks/useKycStatus.d.ts +5 -0
  8. package/dist/cjs/anyspend/react/hooks/useKycStatus.js +11 -0
  9. package/dist/esm/anyspend/react/components/AnySpend.d.ts +2 -0
  10. package/dist/esm/anyspend/react/components/AnySpend.js +6 -12
  11. package/dist/esm/anyspend/react/components/checkout/CryptoPayPanel.js +1 -1
  12. package/dist/esm/anyspend/react/components/checkout/FiatCheckoutPanel.js +34 -5
  13. package/dist/esm/anyspend/react/components/checkout/KycGate.js +35 -13
  14. package/dist/esm/anyspend/react/hooks/useAnyspendCreateOnrampOrder.js +8 -4
  15. package/dist/esm/anyspend/react/hooks/useKycStatus.d.ts +5 -0
  16. package/dist/esm/anyspend/react/hooks/useKycStatus.js +10 -0
  17. package/dist/styles/index.css +1 -1
  18. package/dist/types/anyspend/react/components/AnySpend.d.ts +2 -0
  19. package/dist/types/anyspend/react/hooks/useKycStatus.d.ts +5 -0
  20. package/package.json +1 -1
  21. package/src/anyspend/react/components/AnySpend.tsx +9 -12
  22. package/src/anyspend/react/components/checkout/CryptoPayPanel.tsx +4 -13
  23. package/src/anyspend/react/components/checkout/FiatCheckoutPanel.tsx +62 -4
  24. package/src/anyspend/react/components/checkout/KycGate.tsx +61 -25
  25. package/src/anyspend/react/hooks/useAnyspendCreateOnrampOrder.ts +8 -4
  26. package/src/anyspend/react/hooks/useKycStatus.ts +10 -0
@@ -66,4 +66,6 @@ export declare function AnySpend(props: {
66
66
  content?: AnySpendContent;
67
67
  /** Structured color/theme configuration */
68
68
  theme?: AnySpendTheme;
69
+ /** When true, shows a KYC gate before Stripe Web2 (credit card) payments. Default false. */
70
+ kycEnabled?: boolean;
69
71
  }): import("react/jsx-runtime").JSX.Element;
@@ -38,7 +38,6 @@ const GasIndicator_1 = require("./common/GasIndicator");
38
38
  const OrderDetails_1 = require("./common/OrderDetails");
39
39
  const OrderHistory_1 = require("./common/OrderHistory");
40
40
  const KycGate_1 = require("./checkout/KycGate");
41
- const useKycStatus_1 = require("../hooks/useKycStatus");
42
41
  const LoginStep_1 = require("../../../global-account/react/components/SignInWithB3/steps/LoginStep");
43
42
  const PanelOnramp_1 = require("./common/PanelOnramp");
44
43
  const PanelOnrampPayment_1 = require("./common/PanelOnrampPayment");
@@ -68,7 +67,7 @@ function AnySpend(props) {
68
67
  const fingerprintConfig = (0, AnySpendFingerprintWrapper_1.getFingerprintConfig)();
69
68
  return ((0, jsx_runtime_1.jsx)(AnySpendFingerprintWrapper_1.AnySpendFingerprintWrapper, { fingerprint: fingerprintConfig, children: (0, jsx_runtime_1.jsx)(AnySpendCustomizationContext_1.AnySpendCustomizationProvider, { slots: props.slots, content: props.content, theme: props.theme, children: (0, jsx_runtime_1.jsx)(AnySpendInner, { ...props }) }) }));
70
69
  }
71
- function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, mode = "modal", defaultActiveTab = "crypto", loadOrder, hideTransactionHistoryButton, recipientAddress: recipientAddressFromProps, onTokenSelect, onSuccess, customUsdInputValues, hideHeader, hideBottomNavigation = false, disableUrlParamManagement = false, returnToHomeUrl, customRecipientLabel, returnHomeLabel, classes, allowDirectTransfer = false, destinationTokenAmount, callbackMetadata, senderAddress, }) {
70
+ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, mode = "modal", defaultActiveTab = "crypto", loadOrder, hideTransactionHistoryButton, recipientAddress: recipientAddressFromProps, onTokenSelect, onSuccess, customUsdInputValues, hideHeader, hideBottomNavigation = false, disableUrlParamManagement = false, returnToHomeUrl, customRecipientLabel, returnHomeLabel, classes, allowDirectTransfer = false, destinationTokenAmount, callbackMetadata, senderAddress, kycEnabled = false, }) {
72
71
  const { slots, content } = (0, AnySpendCustomizationContext_1.useAnySpendCustomization)();
73
72
  const searchParams = (0, react_2.useSearchParamsSSR)();
74
73
  const router = (0, react_2.useRouter)();
@@ -80,11 +79,8 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
80
79
  // signature when the user actually clicks Buy, not on panel mount.
81
80
  // useRef so handleFiatOrder can read the updated value synchronously
82
81
  // in the same frame that onStatusResolved sets it (setState is async).
83
- const kycApprovedRef = (0, react_4.useRef)(false);
84
- // Pre-warm wallet auth headers inside user-gesture context (button click)
85
- // so the signing prompt fires before we navigate away — browsers block
86
- // wallet popups triggered from async/non-gesture contexts (React Query queryFn).
87
- const { getHeaders: getKycHeaders } = (0, useKycStatus_1.useWalletAuthHeaders)();
82
+ // When kycEnabled is false (default), pre-approve so the KYC gate is skipped.
83
+ const kycApprovedRef = (0, react_4.useRef)(!kycEnabled);
88
84
  // Determine if we're in "buy mode" based on whether destination token props are provided
89
85
  const isBuyMode = !!(destinationTokenAddress && destinationTokenChainId);
90
86
  // Add refs to track URL state
@@ -851,11 +847,9 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
851
847
  return;
852
848
  }
853
849
  if (!kycApprovedRef.current) {
854
- // Pre-sign the KYC auth message NOW (user-gesture context) so the
855
- // result is cached before useKycStatus fires its queryFn inside the
856
- // FIAT_KYC panel. Wallets/browsers block signing prompts from async
857
- // (non-gesture) contexts, which is exactly what React Query uses.
858
- await getKycHeaders().catch(() => { });
850
+ // Navigate to KYC gate. KycGate shows an explicit "Continue to Verify"
851
+ // CTA and pre-signs from that user-gesture context before enabling
852
+ // the useKycStatus query so no surprise signature popup here.
859
853
  navigateToPanel(PanelView.FIAT_KYC, "forward");
860
854
  return;
861
855
  }
@@ -320,5 +320,5 @@ function CryptoPayPanel({ recipientAddress, destinationTokenAddress, destination
320
320
  ? "Creating order..."
321
321
  : isSendingDeposit
322
322
  ? "Confirm in wallet..."
323
- : "Confirming transaction..."] })) : (buttonText) })), isMobile && hasWalletConnector ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => setQrExpanded(prev => !prev), className: "flex w-full items-center gap-3 py-1", children: [(0, jsx_runtime_1.jsx)("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" }), (0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-1 text-xs font-medium text-gray-400 dark:text-gray-500", children: [qrExpanded ? ("or send directly") : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(lucide_react_1.QrCode, { className: "h-3 w-3" }), " or send with QR code"] })), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: (0, cn_1.cn)("h-3 w-3 transition-transform", qrExpanded && "rotate-180") })] }), (0, jsx_runtime_1.jsx)("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 py-1", children: [(0, jsx_runtime_1.jsx)("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-medium text-gray-400 dark:text-gray-500", children: "or send directly" }), (0, jsx_runtime_1.jsx)("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" })] })), (0, jsx_runtime_1.jsx)(react_3.AnimatePresence, { initial: false, children: qrExpanded && ((0, jsx_runtime_1.jsx)(react_3.motion.div, { initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0 }, transition: { duration: 0.25, ease: "easeInOut" }, className: "overflow-hidden", children: isCreatingQrOrder && !globalAddress ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-center py-8", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Loader2, { className: "h-5 w-5 animate-spin text-gray-400" }), (0, jsx_runtime_1.jsx)("span", { className: "ml-2 text-sm text-gray-500 dark:text-gray-400", children: "Creating deposit address..." })] })) : globalAddress ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "shrink-0 rounded-xl bg-white p-2.5 shadow-sm ring-1 ring-gray-100 dark:bg-white dark:ring-gray-200", children: (0, jsx_runtime_1.jsx)(qrcode_react_1.QRCodeSVG, { value: qrValue, size: 132, level: "M", marginSize: 0 }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-2.5", children: [(0, jsx_runtime_1.jsxs)("p", { className: "text-sm font-medium text-gray-700 dark:text-gray-300", children: ["Send", " ", (0, jsx_runtime_1.jsxs)("span", { className: "font-semibold text-gray-900 dark:text-gray-100", children: [srcAmountFormatted, " ", selectedSrcToken?.symbol] }), " ", "on", " ", (0, jsx_runtime_1.jsxs)("span", { className: "font-semibold text-gray-900 dark:text-gray-100", children: [chainLogoUrl && ((0, jsx_runtime_1.jsx)("img", { src: chainLogoUrl, alt: "", className: "mb-px mr-0.5 inline h-3.5 w-3.5 rounded-full align-text-bottom" })), chainName] }), " ", "to:"] }), (0, jsx_runtime_1.jsxs)("button", { onClick: handleCopyAddress, className: "group flex items-start gap-1.5 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-left transition-colors hover:border-gray-300 hover:bg-gray-100 dark:border-neutral-700 dark:bg-neutral-800/60 dark:hover:border-neutral-600 dark:hover:bg-neutral-800", children: [(0, jsx_runtime_1.jsx)("span", { className: "min-w-0 break-all font-mono text-xs leading-relaxed text-gray-800 dark:text-gray-200", children: globalAddress }), (0, jsx_runtime_1.jsx)("span", { className: "mt-0.5 shrink-0 text-gray-400 transition-colors group-hover:text-gray-600 dark:group-hover:text-gray-300", children: copied ? (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "h-3.5 w-3.5 text-green-500" }) : (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { className: "h-3.5 w-3.5" }) })] }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs leading-snug text-orange-500/80 dark:text-orange-400/80", children: ["Only send ", selectedSrcToken?.symbol, " on", " ", chainLogoUrl && ((0, jsx_runtime_1.jsx)("img", { src: chainLogoUrl, alt: "", className: "mr-0.5 inline h-3 w-3 rounded-full align-text-bottom" })), chainName, ". Sending other tokens or using a different network may result in loss of funds."] })] })] })) : null }, "qr-section")) })] }));
323
+ : "Confirming transaction..."] })) : (buttonText) })), isMobile && hasWalletConnector ? ((0, jsx_runtime_1.jsxs)("button", { type: "button", onClick: () => setQrExpanded(prev => !prev), className: "flex w-full items-center gap-3 py-1", children: [(0, jsx_runtime_1.jsx)("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" }), (0, jsx_runtime_1.jsxs)("span", { className: "flex items-center gap-1 text-xs font-medium text-gray-400 dark:text-gray-500", children: [qrExpanded ? ("or send directly") : ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)(lucide_react_1.QrCode, { className: "h-3 w-3" }), " or send with QR code"] })), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronDown, { className: (0, cn_1.cn)("h-3 w-3 transition-transform", qrExpanded && "rotate-180") })] }), (0, jsx_runtime_1.jsx)("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" })] })) : ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-3 py-1", children: [(0, jsx_runtime_1.jsx)("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" }), (0, jsx_runtime_1.jsx)("span", { className: "text-xs font-medium text-gray-400 dark:text-gray-500", children: "or send directly" }), (0, jsx_runtime_1.jsx)("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" })] })), (0, jsx_runtime_1.jsx)(react_3.AnimatePresence, { initial: false, children: qrExpanded && ((0, jsx_runtime_1.jsx)(react_3.motion.div, { initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0 }, transition: { duration: 0.25, ease: "easeInOut" }, className: "overflow-hidden", children: isCreatingQrOrder && !globalAddress ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-center py-8", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Loader2, { className: "h-5 w-5 animate-spin text-gray-400" }), (0, jsx_runtime_1.jsx)("span", { className: "ml-2 text-sm text-gray-500 dark:text-gray-400", children: "Creating deposit address..." })] })) : globalAddress ? ((0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-4", children: [(0, jsx_runtime_1.jsx)("div", { className: "shrink-0 rounded-xl bg-white p-2.5 shadow-sm ring-1 ring-gray-100 dark:bg-white dark:ring-gray-200", children: (0, jsx_runtime_1.jsx)(qrcode_react_1.QRCodeSVG, { value: qrValue, size: 132, level: "M", marginSize: 0 }) }), (0, jsx_runtime_1.jsxs)("div", { className: "flex min-w-0 flex-1 flex-col gap-2.5", children: [(0, jsx_runtime_1.jsxs)("p", { className: "text-sm font-medium text-gray-700 dark:text-gray-300", children: ["Send", " ", (0, jsx_runtime_1.jsxs)("span", { className: "font-semibold text-gray-900 dark:text-gray-100", children: [srcAmountFormatted, " ", selectedSrcToken?.symbol] }), " ", "on", " ", (0, jsx_runtime_1.jsxs)("span", { className: "inline-flex items-center gap-1 rounded-full bg-gray-100 px-1.5 py-0.5 align-middle font-semibold text-gray-900 dark:bg-white/10 dark:text-gray-100", children: [chainLogoUrl && (0, jsx_runtime_1.jsx)("img", { src: chainLogoUrl, alt: "", className: "h-3.5 w-3.5 rounded-full" }), chainName] }), " ", "to:"] }), (0, jsx_runtime_1.jsxs)("button", { onClick: handleCopyAddress, className: "group flex items-start gap-1.5 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-left transition-colors hover:border-gray-300 hover:bg-gray-100 dark:border-neutral-700 dark:bg-neutral-800/60 dark:hover:border-neutral-600 dark:hover:bg-neutral-800", children: [(0, jsx_runtime_1.jsx)("span", { className: "min-w-0 break-all font-mono text-xs leading-relaxed text-gray-800 dark:text-gray-200", children: globalAddress }), (0, jsx_runtime_1.jsx)("span", { className: "mt-0.5 shrink-0 text-gray-400 transition-colors group-hover:text-gray-600 dark:group-hover:text-gray-300", children: copied ? (0, jsx_runtime_1.jsx)(lucide_react_1.Check, { className: "h-3.5 w-3.5 text-green-500" }) : (0, jsx_runtime_1.jsx)(lucide_react_1.Copy, { className: "h-3.5 w-3.5" }) })] }), (0, jsx_runtime_1.jsxs)("p", { className: "text-xs leading-snug text-orange-500/80 dark:text-orange-400/80", children: ["Only send ", selectedSrcToken?.symbol, " on ", (0, jsx_runtime_1.jsx)("span", { className: "font-semibold", children: chainName }), ". Sending other tokens or using a different network may result in loss of funds."] })] })] })) : null }, "qr-section")) })] }));
324
324
  }
@@ -9,17 +9,26 @@ const cn_1 = require("../../../../shared/utils/cn");
9
9
  const number_1 = require("../../../../shared/utils/number");
10
10
  const payment_utils_1 = require("../../../../shared/utils/payment.utils");
11
11
  const react_2 = require("../../../../global-account/react");
12
+ const b3Chain_1 = require("../../../../shared/constants/chains/b3Chain");
12
13
  const react_stripe_js_1 = require("@stripe/react-stripe-js");
13
14
  const lucide_react_1 = require("lucide-react");
14
15
  const react_3 = require("motion/react");
15
16
  const react_4 = require("react");
17
+ const wagmi_1 = require("wagmi");
16
18
  const KycGate_1 = require("./KycGate");
17
19
  function FiatCheckoutPanel({ recipientAddress, destinationTokenAddress, destinationTokenChainId, totalAmount, themeColor, onSuccess, onOrderCreated, onError, callbackMetadata, classes, feeOnTop, kycEnabled = false, }) {
18
20
  // Stable refs for callback props to avoid re-triggering effects
19
21
  const onErrorRef = (0, react_4.useRef)(onError);
20
22
  onErrorRef.current = onError;
23
+ const { address } = (0, wagmi_1.useAccount)();
24
+ const setB3ModalOpen = (0, react_2.useModalStore)(state => state.setB3ModalOpen);
25
+ const setB3ModalContentType = (0, react_2.useModalStore)(state => state.setB3ModalContentType);
21
26
  const { data: tokenData } = (0, react_2.useTokenData)(destinationTokenChainId, destinationTokenAddress);
22
- const { theme, stripePublishableKey } = (0, react_2.useB3Config)();
27
+ const { theme, stripePublishableKey, partnerId } = (0, react_2.useB3Config)();
28
+ const handleConnectWallet = (0, react_4.useCallback)(() => {
29
+ setB3ModalContentType({ type: "signInWithB3", showBackButton: false, chain: b3Chain_1.thirdwebB3Chain, partnerId });
30
+ setB3ModalOpen(true);
31
+ }, [setB3ModalContentType, setB3ModalOpen, partnerId]);
23
32
  // Clean decimal string for API calls (no commas, no subscripts)
24
33
  const formattedAmount = (0, react_4.useMemo)(() => {
25
34
  const decimals = tokenData?.decimals || 18;
@@ -76,13 +85,28 @@ function FiatCheckoutPanel({ recipientAddress, destinationTokenAddress, destinat
76
85
  }
77
86
  },
78
87
  onError: (error) => {
79
- setOrderError(error.message || "Failed to create payment order.");
88
+ // Backend requires KYC even when kycEnabled=false — reset to show KycGate
89
+ if (error.message?.includes("KYC verification required")) {
90
+ setKycApproved(false);
91
+ orderCreatedRef.current = false;
92
+ }
93
+ else {
94
+ setOrderError(error.message || "Failed to create payment order.");
95
+ }
80
96
  onErrorRef.current?.(error);
81
97
  },
82
98
  });
99
+ // Reset order error when wallet connects so the user gets a clean state
100
+ (0, react_4.useEffect)(() => {
101
+ if (address && orderError) {
102
+ setOrderError(null);
103
+ orderCreatedRef.current = false;
104
+ }
105
+ }, [address]); // eslint-disable-line react-hooks/exhaustive-deps
83
106
  // Auto-create onramp order when Stripe Web2 is supported, KYC approved, and all data is ready
84
107
  (0, react_4.useEffect)(() => {
85
- if (!isLoadingGeo &&
108
+ if (address &&
109
+ !isLoadingGeo &&
86
110
  (!isStablecoin ? !isLoadingAnyspendQuote : true) &&
87
111
  usdAmount &&
88
112
  parseFloat(usdAmount) > 0 &&
@@ -123,6 +147,7 @@ function FiatCheckoutPanel({ recipientAddress, destinationTokenAddress, destinat
123
147
  });
124
148
  }
125
149
  }, [
150
+ address,
126
151
  isLoadingGeo,
127
152
  isStablecoin,
128
153
  isLoadingAnyspendQuote,
@@ -156,6 +181,10 @@ function FiatCheckoutPanel({ recipientAddress, destinationTokenAddress, destinat
156
181
  if (!kycApproved) {
157
182
  return (0, jsx_runtime_1.jsx)(KycGate_1.KycGate, { themeColor: themeColor, classes: classes, enabled: true, onStatusResolved: handleKycResolved });
158
183
  }
184
+ // No wallet connected — prompt to connect before attempting card payment
185
+ if (!address) {
186
+ return ((0, jsx_runtime_1.jsxs)(react_3.motion.div, { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: (0, cn_1.cn)("anyspend-fiat-connect flex flex-col items-center gap-4 py-2", classes?.fiatPanel), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Wallet, { className: "h-8 w-8 text-gray-400" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Connect wallet to pay with card" }), (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: "A wallet connection is required to complete card payment." })] }), (0, jsx_runtime_1.jsx)(react_2.ShinyButton, { accentColor: themeColor || "hsl(var(--as-brand))", className: "w-full", textClassName: "text-white", onClick: handleConnectWallet, children: (0, jsx_runtime_1.jsxs)("span", { className: "flex items-center justify-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Wallet, { className: "h-4 w-4" }), "Connect Wallet"] }) })] }));
187
+ }
159
188
  // Order creation error - show with retry
160
189
  if (orderError) {
161
190
  return ((0, jsx_runtime_1.jsxs)(react_3.motion.div, { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: (0, cn_1.cn)("anyspend-fiat-error flex flex-col items-center gap-3 py-4", classes?.fiatPanel), children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm text-red-500", children: orderError }), (0, jsx_runtime_1.jsx)("button", { onClick: () => {
@@ -42,19 +42,29 @@ const b3Chain_1 = require("../../../../shared/constants/chains/b3Chain");
42
42
  const lucide_react_1 = require("lucide-react");
43
43
  const react_2 = require("motion/react");
44
44
  const react_3 = require("react");
45
+ const wagmi_1 = require("wagmi");
45
46
  const useKycStatus_1 = require("../../hooks/useKycStatus");
46
47
  function KycGate({ themeColor, classes, enabled = false, onStatusResolved }) {
47
- const { isAuthenticated, isAuthenticating } = (0, react_1.useAuth)();
48
- const { kycStatus, isLoadingKycStatus, refetchKycStatus } = (0, useKycStatus_1.useKycStatus)(enabled);
48
+ const { address } = (0, wagmi_1.useAccount)();
49
+ const { partnerId } = (0, react_1.useB3Config)();
50
+ // Gate the status fetch behind explicit user consent so the wallet
51
+ // signature prompt doesn't fire automatically on tab open.
52
+ const [userInitiated, setUserInitiated] = (0, react_3.useState)(false);
53
+ const { getHeaders: preCacheKycHeaders } = (0, useKycStatus_1.useWalletAuthHeaders)();
54
+ const { kycStatus, isLoadingKycStatus, refetchKycStatus } = (0, useKycStatus_1.useKycStatus)(enabled && userInitiated);
49
55
  const { createInquiry, isCreatingInquiry } = (0, useKycStatus_1.useCreateKycInquiry)();
50
56
  const { verifyKyc, isVerifying } = (0, useKycStatus_1.useVerifyKyc)();
51
57
  const setB3ModalOpen = (0, react_1.useModalStore)(state => state.setB3ModalOpen);
52
58
  const setB3ModalContentType = (0, react_1.useModalStore)(state => state.setB3ModalContentType);
53
- const { partnerId } = (0, react_1.useB3Config)();
54
59
  const [personaOpen, setPersonaOpen] = (0, react_3.useState)(false);
55
60
  const [personaError, setPersonaError] = (0, react_3.useState)(null);
56
61
  const [personaCancelled, setPersonaCancelled] = (0, react_3.useState)(false);
57
62
  const prevStatusRef = (0, react_3.useRef)(null);
63
+ // Reset consent gate when wallet changes so the signature prompt isn't
64
+ // skipped for a different (or reconnected) wallet with no cached headers.
65
+ (0, react_3.useEffect)(() => {
66
+ setUserInitiated(false);
67
+ }, [address]);
58
68
  // Notify parent when status resolves
59
69
  (0, react_3.useEffect)(() => {
60
70
  if (!kycStatus)
@@ -133,7 +143,7 @@ function KycGate({ themeColor, classes, enabled = false, onStatusResolved }) {
133
143
  setPersonaError(error instanceof Error ? error.message : "Failed to start verification");
134
144
  }
135
145
  }, [createInquiry, kycStatus, openPersonaFlow]);
136
- const handleSignIn = (0, react_3.useCallback)(() => {
146
+ const handleConnectWallet = (0, react_3.useCallback)(() => {
137
147
  setB3ModalContentType({ type: "signInWithB3", showBackButton: false, chain: b3Chain_1.thirdwebB3Chain, partnerId });
138
148
  setB3ModalOpen(true);
139
149
  }, [setB3ModalContentType, setB3ModalOpen, partnerId]);
@@ -148,13 +158,25 @@ function KycGate({ themeColor, classes, enabled = false, onStatusResolved }) {
148
158
  environment: kycStatus.config?.environment,
149
159
  });
150
160
  }, [kycStatus, openPersonaFlow]);
151
- // Auth loading state
152
- if (isAuthenticating) {
153
- return ((0, jsx_runtime_1.jsxs)(react_2.motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2, ease: "easeOut" }, className: (0, cn_1.cn)("anyspend-kyc-loading flex flex-col items-center gap-3 py-6", classes?.fiatPanel), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Loader2, { className: "h-5 w-5 animate-spin text-gray-400" }), (0, jsx_runtime_1.jsx)(react_1.TextShimmer, { duration: 1.5, className: "text-sm", children: "Checking authentication..." })] }));
161
+ // No wallet connected — prompt to connect wallet (same pattern as crypto tab)
162
+ if (!address) {
163
+ return ((0, jsx_runtime_1.jsxs)(react_2.motion.div, { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: (0, cn_1.cn)("anyspend-kyc-auth flex flex-col items-center gap-4 py-2", classes?.fiatPanel), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ShieldCheck, { className: "h-8 w-8 text-gray-400" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Connect wallet to pay with card" }), (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: "A wallet connection is required to complete identity verification." })] }), (0, jsx_runtime_1.jsx)(react_1.ShinyButton, { accentColor: themeColor || "hsl(var(--as-brand))", className: "w-full", textClassName: "text-white", onClick: handleConnectWallet, children: (0, jsx_runtime_1.jsxs)("span", { className: "flex items-center justify-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.Wallet, { className: "h-4 w-4" }), "Connect Wallet"] }) })] }));
154
164
  }
155
- // Not authenticated prompt to login
156
- if (!isAuthenticated) {
157
- return ((0, jsx_runtime_1.jsxs)(react_2.motion.div, { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: (0, cn_1.cn)("anyspend-kyc-auth flex flex-col items-center gap-4 py-2", classes?.fiatPanel), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ShieldCheck, { className: "h-8 w-8 text-gray-400" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Login required to pay with card" }), (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: "Sign in to your B3 account to complete identity verification." })] }), (0, jsx_runtime_1.jsx)(react_1.ShinyButton, { accentColor: themeColor || "hsl(var(--as-brand))", className: "w-full", textClassName: "text-white", onClick: handleSignIn, children: (0, jsx_runtime_1.jsxs)("span", { className: "flex items-center justify-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ShieldCheck, { className: "h-4 w-4" }), "Sign In"] }) })] }));
165
+ // Wallet connected but user hasn't kicked off the KYC check yet
166
+ if (!userInitiated) {
167
+ return ((0, jsx_runtime_1.jsxs)(react_2.motion.div, { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: (0, cn_1.cn)("anyspend-kyc-prompt flex flex-col items-center gap-4 py-2", classes?.fiatPanel), children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ShieldCheck, { className: "h-8 w-8 text-blue-500" }), (0, jsx_runtime_1.jsxs)("div", { className: "text-center", children: [(0, jsx_runtime_1.jsx)("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Identity verification required" }), (0, jsx_runtime_1.jsx)("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: "Card payments require a one-time identity check. This takes about 2 minutes." })] }), (0, jsx_runtime_1.jsx)(react_1.ShinyButton, { accentColor: themeColor || "hsl(var(--as-brand))", className: "w-full", textClassName: "text-white", onClick: async () => {
168
+ // Pre-sign in user-gesture context so React Query's queryFn
169
+ // can reuse the cached headers without a second popup.
170
+ // Only enable the query on success — if the user rejects the
171
+ // signature, leave userInitiated=false and stay on this screen.
172
+ try {
173
+ await preCacheKycHeaders();
174
+ setUserInitiated(true);
175
+ }
176
+ catch {
177
+ // User rejected signature — stay on consent screen
178
+ }
179
+ }, children: (0, jsx_runtime_1.jsxs)("span", { className: "flex items-center justify-center gap-2", children: [(0, jsx_runtime_1.jsx)(lucide_react_1.ShieldCheck, { className: "h-4 w-4" }), "Continue to Verify"] }) })] }));
158
180
  }
159
181
  // Loading KYC status state
160
182
  if (isLoadingKycStatus) {
@@ -10,6 +10,7 @@ const react_query_1 = require("@tanstack/react-query");
10
10
  const react_2 = require("react");
11
11
  const viem_1 = require("viem");
12
12
  const chains_1 = require("viem/chains");
13
+ const wagmi_1 = require("wagmi");
13
14
  const useKycStatus_1 = require("./useKycStatus");
14
15
  const useValidatedClientReferenceId_1 = require("./useValidatedClientReferenceId");
15
16
  /**
@@ -19,7 +20,7 @@ const useValidatedClientReferenceId_1 = require("./useValidatedClientReferenceId
19
20
  function useAnyspendCreateOnrampOrder({ onSuccess, onError } = {}) {
20
21
  // Get B3 context values
21
22
  const { partnerId } = (0, react_1.useB3Config)();
22
- const { getHeaders: getWalletAuthHeaders } = (0, useKycStatus_1.useWalletAuthHeaders)();
23
+ const { address } = (0, wagmi_1.useAccount)();
23
24
  // Get validated client reference ID from B3 context
24
25
  const createValidatedClientReferenceId = (0, useValidatedClientReferenceId_1.useValidatedClientReferenceId)();
25
26
  // Get fingerprint data
@@ -43,9 +44,12 @@ function useAnyspendCreateOnrampOrder({ onSuccess, onError } = {}) {
43
44
  const srcAmountOnRampInWei = (0, viem_1.parseUnits)(srcFiatAmount, constants_1.USDC_BASE.decimals);
44
45
  // For card payments, include wallet auth headers so the backend can verify
45
46
  // KYC by the signing wallet address (may differ from the B3 JWT address).
47
+ // Only use already-cached headers — never trigger a fresh wallet signature
48
+ // here, as that would prompt the user without their explicit consent.
49
+ // KycGate pre-caches the headers in the "Continue to Verify" user-gesture.
46
50
  let kycWalletHeaders;
47
- if (onramp.vendor === "stripe-web2") {
48
- kycWalletHeaders = await getWalletAuthHeaders().catch(() => undefined);
51
+ if (onramp.vendor === "stripe-web2" && address) {
52
+ kycWalletHeaders = (0, useKycStatus_1.getCachedWalletHeaders)(address);
49
53
  }
50
54
  return await anyspend_1.anyspendService.createOrder({
51
55
  recipientAddress: (0, utils_1.normalizeAddress)(recipientAddress),
@@ -17,6 +17,11 @@ interface KycInquiryResponse {
17
17
  interface KycVerifyResponse {
18
18
  status: string;
19
19
  }
20
+ /**
21
+ * Returns cached wallet auth headers without triggering a wallet signature prompt.
22
+ * Returns undefined if no valid cache exists for the given address.
23
+ */
24
+ export declare function getCachedWalletHeaders(address: string): Record<string, string> | undefined;
20
25
  /**
21
26
  * Returns a function that builds the wallet-signature auth headers.
22
27
  * Caches signatures for 4 minutes (server allows 5-minute window).
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  "use client";
3
3
  Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.getCachedWalletHeaders = getCachedWalletHeaders;
4
5
  exports.useWalletAuthHeaders = useWalletAuthHeaders;
5
6
  exports.useKycStatus = useKycStatus;
6
7
  exports.useCreateKycInquiry = useCreateKycInquiry;
@@ -14,6 +15,16 @@ function buildWalletAuthMessage(walletAddress, timestamp) {
14
15
  }
15
16
  /** Module-level signature cache to avoid repeated wallet prompts within the 5-minute window. */
16
17
  const headerCache = new Map();
18
+ /**
19
+ * Returns cached wallet auth headers without triggering a wallet signature prompt.
20
+ * Returns undefined if no valid cache exists for the given address.
21
+ */
22
+ function getCachedWalletHeaders(address) {
23
+ const cached = headerCache.get(address.toLowerCase());
24
+ if (cached && Date.now() < cached.expiresAt)
25
+ return cached.headers;
26
+ return undefined;
27
+ }
17
28
  /**
18
29
  * Returns a function that builds the wallet-signature auth headers.
19
30
  * Caches signatures for 4 minutes (server allows 5-minute window).
@@ -66,4 +66,6 @@ export declare function AnySpend(props: {
66
66
  content?: AnySpendContent;
67
67
  /** Structured color/theme configuration */
68
68
  theme?: AnySpendTheme;
69
+ /** When true, shows a KYC gate before Stripe Web2 (credit card) payments. Default false. */
70
+ kycEnabled?: boolean;
69
71
  }): import("react/jsx-runtime").JSX.Element;
@@ -31,7 +31,6 @@ import { GasIndicator } from "./common/GasIndicator.js";
31
31
  import { OrderDetails, OrderDetailsLoadingView } from "./common/OrderDetails.js";
32
32
  import { OrderHistory } from "./common/OrderHistory.js";
33
33
  import { KycGate } from "./checkout/KycGate.js";
34
- import { useWalletAuthHeaders } from "../hooks/useKycStatus.js";
35
34
  import { LoginStep } from "../../../global-account/react/components/SignInWithB3/steps/LoginStep.js";
36
35
  import { PanelOnramp } from "./common/PanelOnramp.js";
37
36
  import { PanelOnrampPayment } from "./common/PanelOnrampPayment.js";
@@ -61,7 +60,7 @@ export function AnySpend(props) {
61
60
  const fingerprintConfig = getFingerprintConfig();
62
61
  return (_jsx(AnySpendFingerprintWrapper, { fingerprint: fingerprintConfig, children: _jsx(AnySpendCustomizationProvider, { slots: props.slots, content: props.content, theme: props.theme, children: _jsx(AnySpendInner, { ...props }) }) }));
63
62
  }
64
- function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, mode = "modal", defaultActiveTab = "crypto", loadOrder, hideTransactionHistoryButton, recipientAddress: recipientAddressFromProps, onTokenSelect, onSuccess, customUsdInputValues, hideHeader, hideBottomNavigation = false, disableUrlParamManagement = false, returnToHomeUrl, customRecipientLabel, returnHomeLabel, classes, allowDirectTransfer = false, destinationTokenAmount, callbackMetadata, senderAddress, }) {
63
+ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, mode = "modal", defaultActiveTab = "crypto", loadOrder, hideTransactionHistoryButton, recipientAddress: recipientAddressFromProps, onTokenSelect, onSuccess, customUsdInputValues, hideHeader, hideBottomNavigation = false, disableUrlParamManagement = false, returnToHomeUrl, customRecipientLabel, returnHomeLabel, classes, allowDirectTransfer = false, destinationTokenAmount, callbackMetadata, senderAddress, kycEnabled = false, }) {
65
64
  const { slots, content } = useAnySpendCustomization();
66
65
  const searchParams = useSearchParamsSSR();
67
66
  const router = useRouter();
@@ -73,11 +72,8 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
73
72
  // signature when the user actually clicks Buy, not on panel mount.
74
73
  // useRef so handleFiatOrder can read the updated value synchronously
75
74
  // in the same frame that onStatusResolved sets it (setState is async).
76
- const kycApprovedRef = useRef(false);
77
- // Pre-warm wallet auth headers inside user-gesture context (button click)
78
- // so the signing prompt fires before we navigate away — browsers block
79
- // wallet popups triggered from async/non-gesture contexts (React Query queryFn).
80
- const { getHeaders: getKycHeaders } = useWalletAuthHeaders();
75
+ // When kycEnabled is false (default), pre-approve so the KYC gate is skipped.
76
+ const kycApprovedRef = useRef(!kycEnabled);
81
77
  // Determine if we're in "buy mode" based on whether destination token props are provided
82
78
  const isBuyMode = !!(destinationTokenAddress && destinationTokenChainId);
83
79
  // Add refs to track URL state
@@ -844,11 +840,9 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
844
840
  return;
845
841
  }
846
842
  if (!kycApprovedRef.current) {
847
- // Pre-sign the KYC auth message NOW (user-gesture context) so the
848
- // result is cached before useKycStatus fires its queryFn inside the
849
- // FIAT_KYC panel. Wallets/browsers block signing prompts from async
850
- // (non-gesture) contexts, which is exactly what React Query uses.
851
- await getKycHeaders().catch(() => { });
843
+ // Navigate to KYC gate. KycGate shows an explicit "Continue to Verify"
844
+ // CTA and pre-signs from that user-gesture context before enabling
845
+ // the useKycStatus query so no surprise signature popup here.
852
846
  navigateToPanel(PanelView.FIAT_KYC, "forward");
853
847
  return;
854
848
  }
@@ -317,5 +317,5 @@ export function CryptoPayPanel({ recipientAddress, destinationTokenAddress, dest
317
317
  ? "Creating order..."
318
318
  : isSendingDeposit
319
319
  ? "Confirm in wallet..."
320
- : "Confirming transaction..."] })) : (buttonText) })), isMobile && hasWalletConnector ? (_jsxs("button", { type: "button", onClick: () => setQrExpanded(prev => !prev), className: "flex w-full items-center gap-3 py-1", children: [_jsx("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" }), _jsxs("span", { className: "flex items-center gap-1 text-xs font-medium text-gray-400 dark:text-gray-500", children: [qrExpanded ? ("or send directly") : (_jsxs(_Fragment, { children: [_jsx(QrCode, { className: "h-3 w-3" }), " or send with QR code"] })), _jsx(ChevronDown, { className: cn("h-3 w-3 transition-transform", qrExpanded && "rotate-180") })] }), _jsx("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" })] })) : (_jsxs("div", { className: "flex items-center gap-3 py-1", children: [_jsx("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" }), _jsx("span", { className: "text-xs font-medium text-gray-400 dark:text-gray-500", children: "or send directly" }), _jsx("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" })] })), _jsx(AnimatePresence, { initial: false, children: qrExpanded && (_jsx(motion.div, { initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0 }, transition: { duration: 0.25, ease: "easeInOut" }, className: "overflow-hidden", children: isCreatingQrOrder && !globalAddress ? (_jsxs("div", { className: "flex items-center justify-center py-8", children: [_jsx(Loader2, { className: "h-5 w-5 animate-spin text-gray-400" }), _jsx("span", { className: "ml-2 text-sm text-gray-500 dark:text-gray-400", children: "Creating deposit address..." })] })) : globalAddress ? (_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "shrink-0 rounded-xl bg-white p-2.5 shadow-sm ring-1 ring-gray-100 dark:bg-white dark:ring-gray-200", children: _jsx(QRCodeSVG, { value: qrValue, size: 132, level: "M", marginSize: 0 }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-2.5", children: [_jsxs("p", { className: "text-sm font-medium text-gray-700 dark:text-gray-300", children: ["Send", " ", _jsxs("span", { className: "font-semibold text-gray-900 dark:text-gray-100", children: [srcAmountFormatted, " ", selectedSrcToken?.symbol] }), " ", "on", " ", _jsxs("span", { className: "font-semibold text-gray-900 dark:text-gray-100", children: [chainLogoUrl && (_jsx("img", { src: chainLogoUrl, alt: "", className: "mb-px mr-0.5 inline h-3.5 w-3.5 rounded-full align-text-bottom" })), chainName] }), " ", "to:"] }), _jsxs("button", { onClick: handleCopyAddress, className: "group flex items-start gap-1.5 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-left transition-colors hover:border-gray-300 hover:bg-gray-100 dark:border-neutral-700 dark:bg-neutral-800/60 dark:hover:border-neutral-600 dark:hover:bg-neutral-800", children: [_jsx("span", { className: "min-w-0 break-all font-mono text-xs leading-relaxed text-gray-800 dark:text-gray-200", children: globalAddress }), _jsx("span", { className: "mt-0.5 shrink-0 text-gray-400 transition-colors group-hover:text-gray-600 dark:group-hover:text-gray-300", children: copied ? _jsx(Check, { className: "h-3.5 w-3.5 text-green-500" }) : _jsx(Copy, { className: "h-3.5 w-3.5" }) })] }), _jsxs("p", { className: "text-xs leading-snug text-orange-500/80 dark:text-orange-400/80", children: ["Only send ", selectedSrcToken?.symbol, " on", " ", chainLogoUrl && (_jsx("img", { src: chainLogoUrl, alt: "", className: "mr-0.5 inline h-3 w-3 rounded-full align-text-bottom" })), chainName, ". Sending other tokens or using a different network may result in loss of funds."] })] })] })) : null }, "qr-section")) })] }));
320
+ : "Confirming transaction..."] })) : (buttonText) })), isMobile && hasWalletConnector ? (_jsxs("button", { type: "button", onClick: () => setQrExpanded(prev => !prev), className: "flex w-full items-center gap-3 py-1", children: [_jsx("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" }), _jsxs("span", { className: "flex items-center gap-1 text-xs font-medium text-gray-400 dark:text-gray-500", children: [qrExpanded ? ("or send directly") : (_jsxs(_Fragment, { children: [_jsx(QrCode, { className: "h-3 w-3" }), " or send with QR code"] })), _jsx(ChevronDown, { className: cn("h-3 w-3 transition-transform", qrExpanded && "rotate-180") })] }), _jsx("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" })] })) : (_jsxs("div", { className: "flex items-center gap-3 py-1", children: [_jsx("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" }), _jsx("span", { className: "text-xs font-medium text-gray-400 dark:text-gray-500", children: "or send directly" }), _jsx("div", { className: "h-px flex-1 bg-gray-200 dark:bg-neutral-700" })] })), _jsx(AnimatePresence, { initial: false, children: qrExpanded && (_jsx(motion.div, { initial: { height: 0, opacity: 0 }, animate: { height: "auto", opacity: 1 }, exit: { height: 0, opacity: 0 }, transition: { duration: 0.25, ease: "easeInOut" }, className: "overflow-hidden", children: isCreatingQrOrder && !globalAddress ? (_jsxs("div", { className: "flex items-center justify-center py-8", children: [_jsx(Loader2, { className: "h-5 w-5 animate-spin text-gray-400" }), _jsx("span", { className: "ml-2 text-sm text-gray-500 dark:text-gray-400", children: "Creating deposit address..." })] })) : globalAddress ? (_jsxs("div", { className: "flex items-center gap-4", children: [_jsx("div", { className: "shrink-0 rounded-xl bg-white p-2.5 shadow-sm ring-1 ring-gray-100 dark:bg-white dark:ring-gray-200", children: _jsx(QRCodeSVG, { value: qrValue, size: 132, level: "M", marginSize: 0 }) }), _jsxs("div", { className: "flex min-w-0 flex-1 flex-col gap-2.5", children: [_jsxs("p", { className: "text-sm font-medium text-gray-700 dark:text-gray-300", children: ["Send", " ", _jsxs("span", { className: "font-semibold text-gray-900 dark:text-gray-100", children: [srcAmountFormatted, " ", selectedSrcToken?.symbol] }), " ", "on", " ", _jsxs("span", { className: "inline-flex items-center gap-1 rounded-full bg-gray-100 px-1.5 py-0.5 align-middle font-semibold text-gray-900 dark:bg-white/10 dark:text-gray-100", children: [chainLogoUrl && _jsx("img", { src: chainLogoUrl, alt: "", className: "h-3.5 w-3.5 rounded-full" }), chainName] }), " ", "to:"] }), _jsxs("button", { onClick: handleCopyAddress, className: "group flex items-start gap-1.5 rounded-lg border border-gray-200 bg-gray-50 px-3 py-2 text-left transition-colors hover:border-gray-300 hover:bg-gray-100 dark:border-neutral-700 dark:bg-neutral-800/60 dark:hover:border-neutral-600 dark:hover:bg-neutral-800", children: [_jsx("span", { className: "min-w-0 break-all font-mono text-xs leading-relaxed text-gray-800 dark:text-gray-200", children: globalAddress }), _jsx("span", { className: "mt-0.5 shrink-0 text-gray-400 transition-colors group-hover:text-gray-600 dark:group-hover:text-gray-300", children: copied ? _jsx(Check, { className: "h-3.5 w-3.5 text-green-500" }) : _jsx(Copy, { className: "h-3.5 w-3.5" }) })] }), _jsxs("p", { className: "text-xs leading-snug text-orange-500/80 dark:text-orange-400/80", children: ["Only send ", selectedSrcToken?.symbol, " on ", _jsx("span", { className: "font-semibold", children: chainName }), ". Sending other tokens or using a different network may result in loss of funds."] })] })] })) : null }, "qr-section")) })] }));
321
321
  }
@@ -5,18 +5,27 @@ import { USDC_BASE } from "../../../../anyspend/constants/index.js";
5
5
  import { cn } from "../../../../shared/utils/cn.js";
6
6
  import { formatUnits } from "../../../../shared/utils/number.js";
7
7
  import { getStripePromise } from "../../../../shared/utils/payment.utils.js";
8
- import { ShinyButton, TextShimmer, useB3Config, useTokenData } from "../../../../global-account/react/index.js";
8
+ import { ShinyButton, TextShimmer, useB3Config, useModalStore, useTokenData } from "../../../../global-account/react/index.js";
9
+ import { thirdwebB3Chain } from "../../../../shared/constants/chains/b3Chain.js";
9
10
  import { AddressElement, Elements, PaymentElement, useElements, useStripe } from "@stripe/react-stripe-js";
10
- import { Loader2, Lock } from "lucide-react";
11
+ import { Loader2, Lock, Wallet } from "lucide-react";
11
12
  import { AnimatePresence, motion } from "motion/react";
12
13
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
14
+ import { useAccount } from "wagmi";
13
15
  import { KycGate } from "./KycGate.js";
14
16
  export function FiatCheckoutPanel({ recipientAddress, destinationTokenAddress, destinationTokenChainId, totalAmount, themeColor, onSuccess, onOrderCreated, onError, callbackMetadata, classes, feeOnTop, kycEnabled = false, }) {
15
17
  // Stable refs for callback props to avoid re-triggering effects
16
18
  const onErrorRef = useRef(onError);
17
19
  onErrorRef.current = onError;
20
+ const { address } = useAccount();
21
+ const setB3ModalOpen = useModalStore(state => state.setB3ModalOpen);
22
+ const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType);
18
23
  const { data: tokenData } = useTokenData(destinationTokenChainId, destinationTokenAddress);
19
- const { theme, stripePublishableKey } = useB3Config();
24
+ const { theme, stripePublishableKey, partnerId } = useB3Config();
25
+ const handleConnectWallet = useCallback(() => {
26
+ setB3ModalContentType({ type: "signInWithB3", showBackButton: false, chain: thirdwebB3Chain, partnerId });
27
+ setB3ModalOpen(true);
28
+ }, [setB3ModalContentType, setB3ModalOpen, partnerId]);
20
29
  // Clean decimal string for API calls (no commas, no subscripts)
21
30
  const formattedAmount = useMemo(() => {
22
31
  const decimals = tokenData?.decimals || 18;
@@ -73,13 +82,28 @@ export function FiatCheckoutPanel({ recipientAddress, destinationTokenAddress, d
73
82
  }
74
83
  },
75
84
  onError: (error) => {
76
- setOrderError(error.message || "Failed to create payment order.");
85
+ // Backend requires KYC even when kycEnabled=false — reset to show KycGate
86
+ if (error.message?.includes("KYC verification required")) {
87
+ setKycApproved(false);
88
+ orderCreatedRef.current = false;
89
+ }
90
+ else {
91
+ setOrderError(error.message || "Failed to create payment order.");
92
+ }
77
93
  onErrorRef.current?.(error);
78
94
  },
79
95
  });
96
+ // Reset order error when wallet connects so the user gets a clean state
97
+ useEffect(() => {
98
+ if (address && orderError) {
99
+ setOrderError(null);
100
+ orderCreatedRef.current = false;
101
+ }
102
+ }, [address]); // eslint-disable-line react-hooks/exhaustive-deps
80
103
  // Auto-create onramp order when Stripe Web2 is supported, KYC approved, and all data is ready
81
104
  useEffect(() => {
82
- if (!isLoadingGeo &&
105
+ if (address &&
106
+ !isLoadingGeo &&
83
107
  (!isStablecoin ? !isLoadingAnyspendQuote : true) &&
84
108
  usdAmount &&
85
109
  parseFloat(usdAmount) > 0 &&
@@ -120,6 +144,7 @@ export function FiatCheckoutPanel({ recipientAddress, destinationTokenAddress, d
120
144
  });
121
145
  }
122
146
  }, [
147
+ address,
123
148
  isLoadingGeo,
124
149
  isStablecoin,
125
150
  isLoadingAnyspendQuote,
@@ -153,6 +178,10 @@ export function FiatCheckoutPanel({ recipientAddress, destinationTokenAddress, d
153
178
  if (!kycApproved) {
154
179
  return _jsx(KycGate, { themeColor: themeColor, classes: classes, enabled: true, onStatusResolved: handleKycResolved });
155
180
  }
181
+ // No wallet connected — prompt to connect before attempting card payment
182
+ if (!address) {
183
+ return (_jsxs(motion.div, { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: cn("anyspend-fiat-connect flex flex-col items-center gap-4 py-2", classes?.fiatPanel), children: [_jsx(Wallet, { className: "h-8 w-8 text-gray-400" }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Connect wallet to pay with card" }), _jsx("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: "A wallet connection is required to complete card payment." })] }), _jsx(ShinyButton, { accentColor: themeColor || "hsl(var(--as-brand))", className: "w-full", textClassName: "text-white", onClick: handleConnectWallet, children: _jsxs("span", { className: "flex items-center justify-center gap-2", children: [_jsx(Wallet, { className: "h-4 w-4" }), "Connect Wallet"] }) })] }));
184
+ }
156
185
  // Order creation error - show with retry
157
186
  if (orderError) {
158
187
  return (_jsxs(motion.div, { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: cn("anyspend-fiat-error flex flex-col items-center gap-3 py-4", classes?.fiatPanel), children: [_jsx("p", { className: "text-sm text-red-500", children: orderError }), _jsx("button", { onClick: () => {
@@ -1,24 +1,34 @@
1
1
  "use client";
2
2
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
3
  import { cn } from "../../../../shared/utils/cn.js";
4
- import { ShinyButton, TextShimmer, useAuth, useB3Config, useModalStore } from "../../../../global-account/react/index.js";
4
+ import { ShinyButton, TextShimmer, useB3Config, useModalStore } from "../../../../global-account/react/index.js";
5
5
  import { thirdwebB3Chain } from "../../../../shared/constants/chains/b3Chain.js";
6
- import { Loader2, ShieldCheck, AlertTriangle, Clock } from "lucide-react";
6
+ import { Loader2, ShieldCheck, AlertTriangle, Clock, Wallet } from "lucide-react";
7
7
  import { AnimatePresence, motion } from "motion/react";
8
8
  import { useCallback, useEffect, useRef, useState } from "react";
9
- import { useCreateKycInquiry, useKycStatus, useVerifyKyc } from "../../hooks/useKycStatus.js";
9
+ import { useAccount } from "wagmi";
10
+ import { useCreateKycInquiry, useKycStatus, useVerifyKyc, useWalletAuthHeaders } from "../../hooks/useKycStatus.js";
10
11
  export function KycGate({ themeColor, classes, enabled = false, onStatusResolved }) {
11
- const { isAuthenticated, isAuthenticating } = useAuth();
12
- const { kycStatus, isLoadingKycStatus, refetchKycStatus } = useKycStatus(enabled);
12
+ const { address } = useAccount();
13
+ const { partnerId } = useB3Config();
14
+ // Gate the status fetch behind explicit user consent so the wallet
15
+ // signature prompt doesn't fire automatically on tab open.
16
+ const [userInitiated, setUserInitiated] = useState(false);
17
+ const { getHeaders: preCacheKycHeaders } = useWalletAuthHeaders();
18
+ const { kycStatus, isLoadingKycStatus, refetchKycStatus } = useKycStatus(enabled && userInitiated);
13
19
  const { createInquiry, isCreatingInquiry } = useCreateKycInquiry();
14
20
  const { verifyKyc, isVerifying } = useVerifyKyc();
15
21
  const setB3ModalOpen = useModalStore(state => state.setB3ModalOpen);
16
22
  const setB3ModalContentType = useModalStore(state => state.setB3ModalContentType);
17
- const { partnerId } = useB3Config();
18
23
  const [personaOpen, setPersonaOpen] = useState(false);
19
24
  const [personaError, setPersonaError] = useState(null);
20
25
  const [personaCancelled, setPersonaCancelled] = useState(false);
21
26
  const prevStatusRef = useRef(null);
27
+ // Reset consent gate when wallet changes so the signature prompt isn't
28
+ // skipped for a different (or reconnected) wallet with no cached headers.
29
+ useEffect(() => {
30
+ setUserInitiated(false);
31
+ }, [address]);
22
32
  // Notify parent when status resolves
23
33
  useEffect(() => {
24
34
  if (!kycStatus)
@@ -97,7 +107,7 @@ export function KycGate({ themeColor, classes, enabled = false, onStatusResolved
97
107
  setPersonaError(error instanceof Error ? error.message : "Failed to start verification");
98
108
  }
99
109
  }, [createInquiry, kycStatus, openPersonaFlow]);
100
- const handleSignIn = useCallback(() => {
110
+ const handleConnectWallet = useCallback(() => {
101
111
  setB3ModalContentType({ type: "signInWithB3", showBackButton: false, chain: thirdwebB3Chain, partnerId });
102
112
  setB3ModalOpen(true);
103
113
  }, [setB3ModalContentType, setB3ModalOpen, partnerId]);
@@ -112,13 +122,25 @@ export function KycGate({ themeColor, classes, enabled = false, onStatusResolved
112
122
  environment: kycStatus.config?.environment,
113
123
  });
114
124
  }, [kycStatus, openPersonaFlow]);
115
- // Auth loading state
116
- if (isAuthenticating) {
117
- return (_jsxs(motion.div, { initial: { opacity: 0 }, animate: { opacity: 1 }, transition: { duration: 0.2, ease: "easeOut" }, className: cn("anyspend-kyc-loading flex flex-col items-center gap-3 py-6", classes?.fiatPanel), children: [_jsx(Loader2, { className: "h-5 w-5 animate-spin text-gray-400" }), _jsx(TextShimmer, { duration: 1.5, className: "text-sm", children: "Checking authentication..." })] }));
125
+ // No wallet connected — prompt to connect wallet (same pattern as crypto tab)
126
+ if (!address) {
127
+ return (_jsxs(motion.div, { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: cn("anyspend-kyc-auth flex flex-col items-center gap-4 py-2", classes?.fiatPanel), children: [_jsx(ShieldCheck, { className: "h-8 w-8 text-gray-400" }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Connect wallet to pay with card" }), _jsx("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: "A wallet connection is required to complete identity verification." })] }), _jsx(ShinyButton, { accentColor: themeColor || "hsl(var(--as-brand))", className: "w-full", textClassName: "text-white", onClick: handleConnectWallet, children: _jsxs("span", { className: "flex items-center justify-center gap-2", children: [_jsx(Wallet, { className: "h-4 w-4" }), "Connect Wallet"] }) })] }));
118
128
  }
119
- // Not authenticated prompt to login
120
- if (!isAuthenticated) {
121
- return (_jsxs(motion.div, { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: cn("anyspend-kyc-auth flex flex-col items-center gap-4 py-2", classes?.fiatPanel), children: [_jsx(ShieldCheck, { className: "h-8 w-8 text-gray-400" }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Login required to pay with card" }), _jsx("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: "Sign in to your B3 account to complete identity verification." })] }), _jsx(ShinyButton, { accentColor: themeColor || "hsl(var(--as-brand))", className: "w-full", textClassName: "text-white", onClick: handleSignIn, children: _jsxs("span", { className: "flex items-center justify-center gap-2", children: [_jsx(ShieldCheck, { className: "h-4 w-4" }), "Sign In"] }) })] }));
129
+ // Wallet connected but user hasn't kicked off the KYC check yet
130
+ if (!userInitiated) {
131
+ return (_jsxs(motion.div, { initial: { opacity: 0, y: 8 }, animate: { opacity: 1, y: 0 }, transition: { duration: 0.25, ease: "easeOut" }, className: cn("anyspend-kyc-prompt flex flex-col items-center gap-4 py-2", classes?.fiatPanel), children: [_jsx(ShieldCheck, { className: "h-8 w-8 text-blue-500" }), _jsxs("div", { className: "text-center", children: [_jsx("p", { className: "text-sm font-medium text-gray-900 dark:text-gray-100", children: "Identity verification required" }), _jsx("p", { className: "mt-1 text-xs text-gray-500 dark:text-gray-400", children: "Card payments require a one-time identity check. This takes about 2 minutes." })] }), _jsx(ShinyButton, { accentColor: themeColor || "hsl(var(--as-brand))", className: "w-full", textClassName: "text-white", onClick: async () => {
132
+ // Pre-sign in user-gesture context so React Query's queryFn
133
+ // can reuse the cached headers without a second popup.
134
+ // Only enable the query on success — if the user rejects the
135
+ // signature, leave userInitiated=false and stay on this screen.
136
+ try {
137
+ await preCacheKycHeaders();
138
+ setUserInitiated(true);
139
+ }
140
+ catch {
141
+ // User rejected signature — stay on consent screen
142
+ }
143
+ }, children: _jsxs("span", { className: "flex items-center justify-center gap-2", children: [_jsx(ShieldCheck, { className: "h-4 w-4" }), "Continue to Verify"] }) })] }));
122
144
  }
123
145
  // Loading KYC status state
124
146
  if (isLoadingKycStatus) {
@@ -7,7 +7,8 @@ import { useMutation } from "@tanstack/react-query";
7
7
  import { useMemo } from "react";
8
8
  import { parseUnits } from "viem";
9
9
  import { base } from "viem/chains";
10
- import { useWalletAuthHeaders } from "./useKycStatus.js";
10
+ import { useAccount } from "wagmi";
11
+ import { getCachedWalletHeaders } from "./useKycStatus.js";
11
12
  import { useValidatedClientReferenceId } from "./useValidatedClientReferenceId.js";
12
13
  /**
13
14
  * Hook for creating onramp orders in the Anyspend protocol
@@ -16,7 +17,7 @@ import { useValidatedClientReferenceId } from "./useValidatedClientReferenceId.j
16
17
  export function useAnyspendCreateOnrampOrder({ onSuccess, onError } = {}) {
17
18
  // Get B3 context values
18
19
  const { partnerId } = useB3Config();
19
- const { getHeaders: getWalletAuthHeaders } = useWalletAuthHeaders();
20
+ const { address } = useAccount();
20
21
  // Get validated client reference ID from B3 context
21
22
  const createValidatedClientReferenceId = useValidatedClientReferenceId();
22
23
  // Get fingerprint data
@@ -40,9 +41,12 @@ export function useAnyspendCreateOnrampOrder({ onSuccess, onError } = {}) {
40
41
  const srcAmountOnRampInWei = parseUnits(srcFiatAmount, USDC_BASE.decimals);
41
42
  // For card payments, include wallet auth headers so the backend can verify
42
43
  // KYC by the signing wallet address (may differ from the B3 JWT address).
44
+ // Only use already-cached headers — never trigger a fresh wallet signature
45
+ // here, as that would prompt the user without their explicit consent.
46
+ // KycGate pre-caches the headers in the "Continue to Verify" user-gesture.
43
47
  let kycWalletHeaders;
44
- if (onramp.vendor === "stripe-web2") {
45
- kycWalletHeaders = await getWalletAuthHeaders().catch(() => undefined);
48
+ if (onramp.vendor === "stripe-web2" && address) {
49
+ kycWalletHeaders = getCachedWalletHeaders(address);
46
50
  }
47
51
  return await anyspendService.createOrder({
48
52
  recipientAddress: normalizeAddress(recipientAddress),