@b3dotfun/sdk 0.1.68-alpha.7 → 0.1.68-alpha.9

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 (50) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.js +3 -10
  2. package/dist/cjs/anyspend/react/components/checkout/CryptoPayPanel.js +1 -1
  3. package/dist/cjs/anyspend/react/components/checkout/FiatCheckoutPanel.js +32 -3
  4. package/dist/cjs/anyspend/react/components/checkout/KycGate.js +32 -10
  5. package/dist/cjs/anyspend/react/hooks/useAnyspendCreateOnrampOrder.js +7 -3
  6. package/dist/cjs/anyspend/react/hooks/useKycStatus.d.ts +5 -0
  7. package/dist/cjs/anyspend/react/hooks/useKycStatus.js +11 -0
  8. package/dist/cjs/global-account/react/components/SignInWithB3/SignIn.js +1 -1
  9. package/dist/cjs/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +13 -5
  10. package/dist/cjs/global-account/react/components/SignInWithB3/SignInWithB3Privy.js +1 -1
  11. package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStep.d.ts +1 -1
  12. package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStep.js +21 -24
  13. package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStepCustom.js +1 -1
  14. package/dist/cjs/global-account/react/hooks/useAuthentication.d.ts +3 -1
  15. package/dist/cjs/global-account/react/hooks/useAuthentication.js +51 -7
  16. package/dist/cjs/global-account/react/hooks/useGetAllTWSigners.js +2 -1
  17. package/dist/esm/anyspend/react/components/AnySpend.js +3 -10
  18. package/dist/esm/anyspend/react/components/checkout/CryptoPayPanel.js +1 -1
  19. package/dist/esm/anyspend/react/components/checkout/FiatCheckoutPanel.js +34 -5
  20. package/dist/esm/anyspend/react/components/checkout/KycGate.js +35 -13
  21. package/dist/esm/anyspend/react/hooks/useAnyspendCreateOnrampOrder.js +8 -4
  22. package/dist/esm/anyspend/react/hooks/useKycStatus.d.ts +5 -0
  23. package/dist/esm/anyspend/react/hooks/useKycStatus.js +10 -0
  24. package/dist/esm/global-account/react/components/SignInWithB3/SignIn.js +1 -1
  25. package/dist/esm/global-account/react/components/SignInWithB3/SignInWithB3Flow.js +13 -5
  26. package/dist/esm/global-account/react/components/SignInWithB3/SignInWithB3Privy.js +1 -1
  27. package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStep.d.ts +1 -1
  28. package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStep.js +21 -24
  29. package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStepCustom.js +1 -1
  30. package/dist/esm/global-account/react/hooks/useAuthentication.d.ts +3 -1
  31. package/dist/esm/global-account/react/hooks/useAuthentication.js +51 -7
  32. package/dist/esm/global-account/react/hooks/useGetAllTWSigners.js +2 -1
  33. package/dist/styles/index.css +1 -1
  34. package/dist/types/anyspend/react/hooks/useKycStatus.d.ts +5 -0
  35. package/dist/types/global-account/react/components/SignInWithB3/steps/LoginStep.d.ts +1 -1
  36. package/dist/types/global-account/react/hooks/useAuthentication.d.ts +3 -1
  37. package/package.json +1 -1
  38. package/src/anyspend/react/components/AnySpend.tsx +3 -11
  39. package/src/anyspend/react/components/checkout/CryptoPayPanel.tsx +4 -13
  40. package/src/anyspend/react/components/checkout/FiatCheckoutPanel.tsx +62 -4
  41. package/src/anyspend/react/components/checkout/KycGate.tsx +61 -25
  42. package/src/anyspend/react/hooks/useAnyspendCreateOnrampOrder.ts +8 -4
  43. package/src/anyspend/react/hooks/useKycStatus.ts +10 -0
  44. package/src/global-account/react/components/SignInWithB3/SignIn.tsx +1 -1
  45. package/src/global-account/react/components/SignInWithB3/SignInWithB3Flow.tsx +13 -5
  46. package/src/global-account/react/components/SignInWithB3/SignInWithB3Privy.tsx +1 -1
  47. package/src/global-account/react/components/SignInWithB3/steps/LoginStep.tsx +35 -25
  48. package/src/global-account/react/components/SignInWithB3/steps/LoginStepCustom.tsx +1 -1
  49. package/src/global-account/react/hooks/useAuthentication.ts +51 -8
  50. package/src/global-account/react/hooks/useGetAllTWSigners.tsx +2 -1
@@ -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),
@@ -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).
@@ -8,6 +8,16 @@ function buildWalletAuthMessage(walletAddress, timestamp) {
8
8
  }
9
9
  /** Module-level signature cache to avoid repeated wallet prompts within the 5-minute window. */
10
10
  const headerCache = new Map();
11
+ /**
12
+ * Returns cached wallet auth headers without triggering a wallet signature prompt.
13
+ * Returns undefined if no valid cache exists for the given address.
14
+ */
15
+ export function getCachedWalletHeaders(address) {
16
+ const cached = headerCache.get(address.toLowerCase());
17
+ if (cached && Date.now() < cached.expiresAt)
18
+ return cached.headers;
19
+ return undefined;
20
+ }
11
21
  /**
12
22
  * Returns a function that builds the wallet-signature auth headers.
13
23
  * Caches signatures for 4 minutes (server allows 5-minute window).
@@ -13,7 +13,7 @@ export function SignIn(props) {
13
13
  const { address: globalAddress, ensName, connectedSmartWallet, connectedEOAWallet, isActiveSmartWallet, isActiveEOAWallet, smartWalletIcon, } = useAccountWallet();
14
14
  const { data: walletImage } = useWalletImage(connectedEOAWallet?.id);
15
15
  const isMobile = useIsMobile();
16
- const { logout } = useAuthentication(partnerId);
16
+ const { logout } = useAuthentication(partnerId, { skipAutoConnect: true });
17
17
  const onDisconnect = async () => {
18
18
  await logout();
19
19
  };
@@ -14,16 +14,23 @@ const MAX_REFETCH_ATTEMPTS = 20;
14
14
  */
15
15
  export function SignInWithB3Flow({ strategies, onLoginSuccess, onSessionKeySuccess, onError, chain, sessionKeyAddress, partnerId, closeAfterLogin = false, source = "signInWithB3Button", signersEnabled = false, }) {
16
16
  const { automaticallySetFirstEoa } = useB3Config();
17
- const { user, logout } = useAuthentication(partnerId);
18
- // FIXME Logout before login to ensure a clean state
17
+ // skipAutoConnect: this component intentionally logs out on mount to show a fresh login screen.
18
+ // AuthenticationProvider is the sole owner of useAutoConnect to avoid competing auth cycles.
19
+ const { user, logout } = useAuthentication(partnerId, { skipAutoConnect: true });
20
+ // Tracks whether the pre-login logout has finished.
21
+ // We must not render ConnectEmbed until logout (wallet disconnect) completes,
22
+ // otherwise the wallet state disrupts ConnectEmbed causing a blank modal.
23
+ const [readyToShowLogin, setReadyToShowLogin] = useState(source === "requestPermissions");
19
24
  const hasLoggedOutRef = useRef(false);
20
25
  useEffect(() => {
21
26
  if (hasLoggedOutRef.current)
22
27
  return;
23
28
  if (source !== "requestPermissions") {
24
29
  debug("Logging out before login");
25
- logout();
26
30
  hasLoggedOutRef.current = true;
31
+ logout().finally(() => {
32
+ setReadyToShowLogin(true);
33
+ });
27
34
  }
28
35
  }, [source, logout]);
29
36
  const [step, setStep] = useState(source === "requestPermissions" ? null : "login");
@@ -228,8 +235,9 @@ export function SignInWithB3Flow({ strategies, onLoginSuccess, onSessionKeySucce
228
235
  content = (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx("div", { className: "p-4 text-center text-red-500", children: refetchError }) }));
229
236
  }
230
237
  else if (step === "login") {
231
- // Show loading spinner
232
- if (isAuthenticating || (isFetchingSigners && step === "login") || source === "requestPermissions") {
238
+ // Show loading spinner while: authenticating, waiting for pre-login logout to finish,
239
+ // or fetching signers.
240
+ if (!readyToShowLogin || isAuthenticating || isFetchingSigners) {
233
241
  content = (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx("div", { className: "my-8 flex min-h-[350px] items-center justify-center", children: _jsx(Loading, { variant: "white", size: "lg" }) }) }));
234
242
  }
235
243
  else {
@@ -8,7 +8,7 @@ export function SignInWithB3Privy({ onSuccess, onError, chain }) {
8
8
  const { isLoading, connectTw, fullToken } = useHandleConnectWithPrivy(chain, onSuccess);
9
9
  const setIsAuthenticating = useAuthStore(state => state.setIsAuthenticating);
10
10
  const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
11
- const { logout } = useAuthentication(partnerId);
11
+ const { logout } = useAuthentication(partnerId, { skipAutoConnect: true });
12
12
  debug("@@SignInWithB3Privy", {
13
13
  isLoading,
14
14
  fullToken,
@@ -19,5 +19,5 @@ interface LoginStepContainerProps {
19
19
  partnerId?: string;
20
20
  }
21
21
  export declare function LoginStepContainer({ children, partnerId }: LoginStepContainerProps): import("react/jsx-runtime").JSX.Element;
22
- export declare function LoginStep({ onSuccess, chain }: LoginStepProps): import("react/jsx-runtime").JSX.Element;
22
+ export declare function LoginStep({ onSuccess, chain }: LoginStepProps): import("react/jsx-runtime").JSX.Element | null;
23
23
  export {};
@@ -3,6 +3,7 @@ import { useAuthentication, useB3Config, useQueryB3 } from "../../../../../globa
3
3
  import { ecosystemWalletId } from "../../../../../shared/constants/index.js";
4
4
  import { client } from "../../../../../shared/utils/thirdweb.js";
5
5
  import { ConnectEmbed, darkTheme, lightTheme } from "thirdweb/react";
6
+ import { useMemo } from "react";
6
7
  import { ecosystemWallet } from "thirdweb/wallets";
7
8
  export function LoginStepContainer({ children, partnerId }) {
8
9
  const { data: partner } = useQueryB3("global-accounts-partners", "find", {
@@ -14,30 +15,17 @@ export function LoginStepContainer({ children, partnerId }) {
14
15
  const partnerLogo = partner?.data?.[0]?.loginCustomization?.logoUrl;
15
16
  return (_jsxs("div", { className: "bg-b3-react-background flex flex-col items-center justify-center pt-6", children: [partnerLogo && (_jsx("img", { src: partnerLogo, alt: "Partner Logo", className: "partner-logo mb-6 h-12 w-auto object-contain" })), children] }));
16
17
  }
17
- export function LoginStep({ onSuccess, chain }) {
18
- const { partnerId, theme } = useB3Config();
19
- const wallet = ecosystemWallet(ecosystemWalletId, {
20
- partnerId: partnerId,
21
- });
22
- const { onConnect } = useAuthentication(partnerId);
23
- return (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx(ConnectEmbed, { showThirdwebBranding: false, client: client, chain: chain, wallets: [wallet], theme: theme === "light"
24
- ? lightTheme({
25
- colors: {
26
- modalBg: "hsl(var(--b3-react-background))",
27
- },
28
- })
29
- : darkTheme({
30
- colors: {
31
- modalBg: "hsl(var(--b3-react-background))",
32
- },
33
- }), style: {
34
- width: "100%",
35
- height: "100%",
36
- border: 0,
37
- }, header: {
38
- title: "Sign in with B3",
39
- titleIcon: "https://cdn.b3.fun/b3_logo.svg",
40
- }, className: "b3-login-step", onConnect: async (wallet, allConnectedWallets) => {
18
+ /** Inner component that only mounts when partnerId is a non-empty string.
19
+ * Keeps all hooks unconditional without calling useAuthentication(""). */
20
+ function LoginStepContent({ onSuccess, chain, partnerId, theme, }) {
21
+ const wallet = useMemo(() => ecosystemWallet(ecosystemWalletId, { partnerId }), [partnerId]);
22
+ // skipAutoConnect: AuthenticationProvider already owns the auto-connect instance.
23
+ // Creating another here would cause a second authentication cycle (another 401 attempt)
24
+ // that makes the modal flash between spinner and blank before finally showing the login form.
25
+ const { onConnect } = useAuthentication(partnerId, { skipAutoConnect: true });
26
+ return (_jsx(LoginStepContainer, { partnerId: partnerId, children: _jsx(ConnectEmbed, { showThirdwebBranding: false, autoConnect: false, client: client, chain: chain, wallets: [wallet], theme: theme === "light"
27
+ ? lightTheme({ colors: { modalBg: "hsl(var(--b3-react-background))" } })
28
+ : darkTheme({ colors: { modalBg: "hsl(var(--b3-react-background))" } }), style: { width: "100%", height: "100%", border: 0 }, header: { title: "Sign in with B3", titleIcon: "https://cdn.b3.fun/b3_logo.svg" }, className: "b3-login-step", onConnect: async (wallet, allConnectedWallets) => {
41
29
  await onConnect(wallet, allConnectedWallets);
42
30
  const account = wallet.getAccount();
43
31
  if (!account)
@@ -45,3 +33,12 @@ export function LoginStep({ onSuccess, chain }) {
45
33
  await onSuccess(account);
46
34
  } }) }));
47
35
  }
36
+ export function LoginStep({ onSuccess, chain }) {
37
+ const { partnerId, theme } = useB3Config();
38
+ // partnerId may be undefined during the brief B3Provider hydration window.
39
+ // Return null rather than rendering ConnectEmbed with an invalid ecosystem
40
+ // wallet config (which causes a blank screen).
41
+ if (!partnerId)
42
+ return null;
43
+ return _jsx(LoginStepContent, { onSuccess: onSuccess, chain: chain, partnerId: partnerId, theme: theme });
44
+ }
@@ -13,7 +13,7 @@ export function LoginStepCustom({ onSuccess, onError, chain, strategies, maxInit
13
13
  const { connect } = useConnect(partnerId, chain);
14
14
  const setIsAuthenticating = useAuthStore(state => state.setIsAuthenticating);
15
15
  const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
16
- const { logout } = useAuthentication(partnerId);
16
+ const { logout } = useAuthentication(partnerId, { skipAutoConnect: true });
17
17
  const { connect: connectTW } = useConnectTW();
18
18
  // Split strategies into auth and wallet types
19
19
  const authStrategies = strategies.filter(s => !isWalletType(s));
@@ -1,6 +1,8 @@
1
1
  import { Wallet } from "thirdweb/wallets";
2
2
  import { preAuthenticate } from "thirdweb/wallets/in-app";
3
- export declare function useAuthentication(partnerId: string): {
3
+ export declare function useAuthentication(partnerId: string, { skipAutoConnect }?: {
4
+ skipAutoConnect?: boolean;
5
+ }): {
4
6
  logout: (callback?: () => void) => Promise<void>;
5
7
  isAuthenticated: boolean;
6
8
  isReady: boolean;
@@ -15,11 +15,26 @@ import { createWagmiConfig } from "../utils/createWagmiConfig.js";
15
15
  import { useTWAuth } from "./useTWAuth.js";
16
16
  import { useUserQuery } from "./useUserQuery.js";
17
17
  const debug = debugB3React("useAuthentication");
18
- export function useAuthentication(partnerId) {
18
+ export function useAuthentication(partnerId, { skipAutoConnect = false } = {}) {
19
19
  const { onConnectCallback, onLogoutCallback } = useContext(LocalSDKContext);
20
20
  const { disconnect } = useDisconnect();
21
21
  const wallets = useConnectedWallets();
22
+ // Keep refs so logout() always disconnects current wallets, not stale closure values.
23
+ // autoConnectCore captures onConnect (and thus logout) from the first render before wallets
24
+ // are populated — without these refs, logout() would capture wallets=[] and disconnect nothing.
25
+ const walletsRef = useRef(wallets);
26
+ useEffect(() => {
27
+ walletsRef.current = wallets;
28
+ }, [wallets]);
22
29
  const activeWallet = useActiveWallet();
30
+ // Track the active wallet by ref so logout() can disconnect the exact reference
31
+ // stored in thirdweb's activeWalletStore. walletsRef.current (from useConnectedWallets)
32
+ // may hold a different object reference than what thirdweb considers "active",
33
+ // causing the identity check in onWalletDisconnect to fail silently.
34
+ const activeWalletRef = useRef(activeWallet);
35
+ useEffect(() => {
36
+ activeWalletRef.current = activeWallet;
37
+ }, [activeWallet]);
23
38
  const isAuthenticated = useAuthStore(state => state.isAuthenticated);
24
39
  const setIsAuthenticated = useAuthStore(state => state.setIsAuthenticated);
25
40
  const setIsConnected = useAuthStore(state => state.setIsConnected);
@@ -127,13 +142,26 @@ export function useAuthentication(partnerId) {
127
142
  }, [activeWallet, partnerId, authenticate, setIsAuthenticated, setIsAuthenticating, setUser, setHasStartedConnecting]);
128
143
  const logout = useCallback(async (callback) => {
129
144
  // Only disconnect ecosystem/smart wallets, preserve EOA wallets (e.g. MetaMask)
130
- // so they remain available after re-login
131
- wallets.forEach(wallet => {
145
+ // so they remain available after re-login.
146
+ // Use walletsRef.current (not the stale closure value) so we always get current wallets —
147
+ // autoConnectCore captures logout from the first render when wallets is still [].
148
+ walletsRef.current.forEach(wallet => {
132
149
  debug("@@logout:wallet", wallet.id);
133
150
  if (wallet.id.startsWith("ecosystem.") || wallet.id === "smart") {
134
151
  disconnect(wallet);
135
152
  }
136
153
  });
154
+ // Also disconnect the active wallet using the exact reference from thirdweb's
155
+ // activeWalletStore. The wallets in walletsRef (from useConnectedWallets) may be
156
+ // different object references than what thirdweb holds as "active". Thirdweb's
157
+ // onWalletDisconnect uses strict identity (===) to decide whether to clear
158
+ // activeAccountStore — if the reference doesn't match, activeAccount stays set
159
+ // and ConnectEmbed renders show=false (blank).
160
+ if (activeWalletRef.current &&
161
+ (activeWalletRef.current.id.startsWith("ecosystem.") || activeWalletRef.current.id === "smart")) {
162
+ debug("@@logout:disconnecting active wallet", activeWalletRef.current.id);
163
+ disconnect(activeWalletRef.current);
164
+ }
137
165
  // Clear user-specific storage but preserve wallet connection state
138
166
  // so EOA wallets (e.g. MetaMask) can auto-reconnect on next login
139
167
  if (typeof localStorage !== "undefined") {
@@ -144,12 +172,19 @@ export function useAuthentication(partnerId) {
144
172
  debug("@@logout:loggedOut");
145
173
  setIsAuthenticated(false);
146
174
  setIsConnected(false);
175
+ // Reset isAuthenticating so any in-flight page-load auto-connect that set it true
176
+ // does not keep the login modal spinner stuck after logout() is called.
177
+ setIsAuthenticating(false);
147
178
  setUser();
148
179
  callback?.();
149
180
  if (onLogoutCallback) {
150
181
  await onLogoutCallback();
151
182
  }
152
- }, [disconnect, wallets, setIsAuthenticated, setUser, setIsConnected, onLogoutCallback]);
183
+ },
184
+ // wallets intentionally omitted — we use walletsRef.current so this callback stays stable
185
+ // and always operates on current wallets even when captured in stale closures.
186
+ // eslint-disable-next-line react-hooks/exhaustive-deps
187
+ [disconnect, setIsAuthenticated, setIsAuthenticating, setUser, setIsConnected, onLogoutCallback]);
153
188
  const onConnect = useCallback(async (_walleAutoConnectedWith, allConnectedWallets) => {
154
189
  debug("@@useAuthentication:onConnect", { _walleAutoConnectedWith, allConnectedWallets });
155
190
  try {
@@ -197,23 +232,32 @@ export function useAuthentication(partnerId) {
197
232
  ]);
198
233
  const { isLoading: useAutoConnectLoading } = useAutoConnect({
199
234
  client,
200
- wallets: [wallet],
235
+ // When skipAutoConnect is true (e.g. LoginStepContent, SignInWithB3Flow), pass an empty
236
+ // wallets array so useAutoConnect completes immediately without firing onConnect.
237
+ // Only AuthenticationProvider (the primary instance) should own auto-connect.
238
+ wallets: skipAutoConnect ? [] : [wallet],
201
239
  onConnect,
202
240
  onTimeout: () => {
241
+ if (skipAutoConnect)
242
+ return;
203
243
  logout().catch(error => {
204
244
  debug("@@useAuthentication:logout on timeout failed", { error });
205
245
  });
206
246
  },
207
247
  });
208
248
  /**
209
- * useAutoConnectLoading starts as false
249
+ * useAutoConnectLoading starts as false.
250
+ * Only the primary (non-skip) instance manages isAuthenticating via this effect
251
+ * to avoid race conditions when multiple useAuthentication instances are mounted.
210
252
  */
211
253
  useEffect(() => {
254
+ if (skipAutoConnect)
255
+ return;
212
256
  if (!useAutoConnectLoading && useAutoConnectLoadingPrevious.current && !hasStartedConnecting) {
213
257
  setIsAuthenticating(false);
214
258
  }
215
259
  useAutoConnectLoadingPrevious.current = useAutoConnectLoading;
216
- }, [useAutoConnectLoading, hasStartedConnecting, setIsAuthenticating]);
260
+ }, [useAutoConnectLoading, hasStartedConnecting, setIsAuthenticating, skipAutoConnect]);
217
261
  const isReady = isAuthenticated && !isAuthenticating;
218
262
  return {
219
263
  logout,
@@ -62,7 +62,8 @@ export function useGetAllTWSigners({ chain, accountAddress, queryOptions }) {
62
62
  });
63
63
  return result;
64
64
  },
65
- enabled: Boolean(chain && accountAddress),
65
+ // Respect queryOptions.enabled if explicitly set (e.g. signersEnabled=false from SignInWithB3Flow)
66
+ enabled: queryOptions?.enabled !== false && Boolean(chain && accountAddress),
66
67
  refetchOnMount: true,
67
68
  refetchOnWindowFocus: true,
68
69
  refetchOnReconnect: true,