@b3dotfun/sdk 0.1.70-alpha.11 → 0.1.70-alpha.13

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 (41) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.d.ts +7 -0
  2. package/dist/cjs/anyspend/react/components/AnySpend.js +30 -17
  3. package/dist/cjs/anyspend/react/components/AnySpendDeposit.d.ts +15 -1
  4. package/dist/cjs/anyspend/react/components/AnySpendDeposit.js +50 -13
  5. package/dist/cjs/anyspend/react/components/QRDeposit.d.ts +14 -3
  6. package/dist/cjs/anyspend/react/components/QRDeposit.js +24 -15
  7. package/dist/cjs/global-account/react/components/SignInWithB3/BetterAuthSignIn.js +3 -1
  8. package/dist/cjs/global-account/react/components/SignInWithB3/components/AuthButton.js +2 -1
  9. package/dist/cjs/global-account/react/components/SignInWithB3/steps/LoginStepBetterAuth.js +1 -0
  10. package/dist/cjs/global-account/react/components/SignInWithB3/utils/signInUtils.js +2 -0
  11. package/dist/cjs/global-account/react/hooks/useBetterAuth.d.ts +1 -1
  12. package/dist/cjs/global-account/react/stores/useModalStore.d.ts +14 -0
  13. package/dist/esm/anyspend/react/components/AnySpend.d.ts +7 -0
  14. package/dist/esm/anyspend/react/components/AnySpend.js +30 -17
  15. package/dist/esm/anyspend/react/components/AnySpendDeposit.d.ts +15 -1
  16. package/dist/esm/anyspend/react/components/AnySpendDeposit.js +51 -14
  17. package/dist/esm/anyspend/react/components/QRDeposit.d.ts +14 -3
  18. package/dist/esm/anyspend/react/components/QRDeposit.js +25 -16
  19. package/dist/esm/global-account/react/components/SignInWithB3/BetterAuthSignIn.js +3 -1
  20. package/dist/esm/global-account/react/components/SignInWithB3/components/AuthButton.js +2 -1
  21. package/dist/esm/global-account/react/components/SignInWithB3/steps/LoginStepBetterAuth.js +1 -0
  22. package/dist/esm/global-account/react/components/SignInWithB3/utils/signInUtils.js +2 -0
  23. package/dist/esm/global-account/react/hooks/useBetterAuth.d.ts +1 -1
  24. package/dist/esm/global-account/react/stores/useModalStore.d.ts +14 -0
  25. package/dist/styles/index.css +1 -1
  26. package/dist/types/anyspend/react/components/AnySpend.d.ts +7 -0
  27. package/dist/types/anyspend/react/components/AnySpendDeposit.d.ts +15 -1
  28. package/dist/types/anyspend/react/components/QRDeposit.d.ts +14 -3
  29. package/dist/types/global-account/react/hooks/useBetterAuth.d.ts +1 -1
  30. package/dist/types/global-account/react/stores/useModalStore.d.ts +14 -0
  31. package/package.json +1 -1
  32. package/src/anyspend/react/components/AnySpend.tsx +42 -16
  33. package/src/anyspend/react/components/AnySpendDeposit.tsx +87 -12
  34. package/src/anyspend/react/components/QRDeposit.tsx +46 -18
  35. package/src/anyspend/react/components/__tests__/QRDeposit.test.tsx +256 -0
  36. package/src/global-account/react/components/SignInWithB3/BetterAuthSignIn.tsx +12 -1
  37. package/src/global-account/react/components/SignInWithB3/components/AuthButton.tsx +9 -1
  38. package/src/global-account/react/components/SignInWithB3/steps/LoginStepBetterAuth.tsx +1 -0
  39. package/src/global-account/react/components/SignInWithB3/utils/signInUtils.ts +2 -0
  40. package/src/global-account/react/hooks/useBetterAuth.ts +1 -1
  41. package/src/global-account/react/stores/useModalStore.ts +14 -0
@@ -14,6 +14,14 @@ export interface QRDepositProps {
14
14
  destinationToken: components["schemas"]["Token"];
15
15
  /** The destination chain ID */
16
16
  destinationChainId: number;
17
+ /**
18
+ * When true, the deposit is a PURE TRANSFER: the destination token/chain mirror
19
+ * the user's currently selected source token/chain, so the exact token the user
20
+ * sends lands in their wallet on the same chain — no swap/bridge order is created.
21
+ * The `destinationToken`/`destinationChainId` props are then used only as the
22
+ * initial source selection (they must be fully resolved before mount). Defaults to false.
23
+ */
24
+ pureTransferOnly?: boolean;
17
25
  /** Creator address (optional) */
18
26
  creatorAddress?: string;
19
27
  /** Contract config for custom execution after deposit */
@@ -24,8 +32,11 @@ export interface QRDepositProps {
24
32
  onClose?: () => void;
25
33
  /** Callback when order is created successfully */
26
34
  onOrderCreated?: (orderId: string) => void;
27
- /** Callback when deposit is completed */
28
- onSuccess?: (txHash?: string) => void;
35
+ /**
36
+ * Callback when deposit is completed. The argument carries the deposited amount string
37
+ * (e.g. "200.00") on the pure-transfer path, or the order tx hash otherwise.
38
+ */
39
+ onSuccess?: (amountOrTxHash?: string) => void;
29
40
  /** Custom classes for styling */
30
41
  classes?: QRDepositClasses;
31
42
  }
@@ -43,4 +54,4 @@ export interface QRDepositProps {
43
54
  * onSuccess={(txHash) => console.log("Deposit complete:", txHash)}
44
55
  * />
45
56
  */
46
- export declare function QRDeposit({ mode, recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }: QRDepositProps): import("react/jsx-runtime").JSX.Element;
57
+ export declare function QRDeposit({ mode, recipientAddress, sourceToken: sourceTokenProp, sourceChainId: sourceChainIdProp, destinationToken, destinationChainId, pureTransferOnly, creatorAddress, depositContractConfig, onBack, onClose, onOrderCreated, onSuccess, classes, }: QRDepositProps): import("react/jsx-runtime").JSX.Element;
@@ -1,4 +1,4 @@
1
- export type BetterAuthSocialProvider = "google" | "discord" | "apple" | "github" | "slack" | "microsoft";
1
+ export type BetterAuthSocialProvider = "google" | "discord" | "apple" | "github" | "slack" | "microsoft" | "twitter";
2
2
  /** Thrown when email verification is required before the user can sign in. */
3
3
  export declare class EmailVerificationRequiredError extends Error {
4
4
  constructor(message?: string);
@@ -617,8 +617,16 @@ export interface AnySpendDepositModalProps extends BaseModalProps {
617
617
  destinationTokenAddress: string;
618
618
  /** The destination chain ID */
619
619
  destinationTokenChainId: number;
620
+ /**
621
+ * When false, the destination token is user-selectable even though a default destination token is
622
+ * provided (the provided destination is used only as the initial/default value). Used by the
623
+ * wallet-funding deposit flow so the user can change the receive token away from the default
624
+ * (Base USDC). Defaults to true (locked destination). Does not affect the QR/pure-transfer path. */
625
+ lockDestinationToken?: boolean;
620
626
  /** Callback when deposit succeeds */
621
627
  onSuccess?: (amount: string) => void;
628
+ /** Callback when the modal's close control is pressed. */
629
+ onClose?: () => void;
622
630
  /** Callback for opening a custom modal (e.g., for special token handling) */
623
631
  onOpenCustomModal?: () => void;
624
632
  /** Custom footer content */
@@ -661,6 +669,12 @@ export interface AnySpendDepositModalProps extends BaseModalProps {
661
669
  classes?: AnySpendAllClasses;
662
670
  /** Whether to allow direct transfer without swap */
663
671
  allowDirectTransfer?: boolean;
672
+ /**
673
+ * When true, the QR-deposit path is a PURE TRANSFER: the destination token/chain
674
+ * mirror the user's selected source token, so the exact token they send lands in
675
+ * their wallet on the same chain (no swap/bridge).
676
+ */
677
+ pureTransferOnly?: boolean;
664
678
  /** Opaque metadata passed to the order for callbacks (e.g., workflow form data) */
665
679
  callbackMetadata?: Record<string, unknown>;
666
680
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.70-alpha.11",
3
+ "version": "0.1.70-alpha.13",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -110,6 +110,13 @@ export function AnySpend(props: {
110
110
  sourceChainId?: number;
111
111
  destinationTokenAddress?: string;
112
112
  destinationTokenChainId?: number;
113
+ /**
114
+ * When false, the destination token is user-selectable even if a default destination token is
115
+ * provided (the provided destination is used only as the initial/default value). Used by the
116
+ * wallet-funding deposit flow so the user can change the receive token away from the default
117
+ * (Base USDC). Defaults to true (locked buy-mode display, current behavior).
118
+ */
119
+ lockDestinationToken?: boolean;
113
120
  recipientAddress?: string;
114
121
  loadOrder?: string;
115
122
  hideTransactionHistoryButton?: boolean;
@@ -166,6 +173,7 @@ function AnySpendInner({
166
173
  sourceChainId,
167
174
  destinationTokenAddress,
168
175
  destinationTokenChainId,
176
+ lockDestinationToken = true,
169
177
  mode = "modal",
170
178
  defaultActiveTab = "crypto",
171
179
  loadOrder,
@@ -191,6 +199,7 @@ function AnySpendInner({
191
199
  sourceChainId?: number;
192
200
  destinationTokenAddress?: string;
193
201
  destinationTokenChainId?: number;
202
+ lockDestinationToken?: boolean;
194
203
  mode?: "page" | "modal";
195
204
  defaultActiveTab?: "crypto" | "fiat";
196
205
  loadOrder?: string;
@@ -230,8 +239,10 @@ function AnySpendInner({
230
239
  // in the same frame that onStatusResolved sets it (setState is async).
231
240
  // When kycEnabled is false (default), pre-approve so the KYC gate is skipped.
232
241
  const kycApprovedRef = useRef(!kycEnabled);
233
- // Determine if we're in "buy mode" based on whether destination token props are provided
234
- const isBuyMode = !!(destinationTokenAddress && destinationTokenChainId);
242
+ // Determine if we're in "buy mode" based on whether destination token props are provided.
243
+ // When lockDestinationToken is false, the provided destination is only a default and the user
244
+ // can change the receive token, so we stay out of buy mode (selectable swap mode).
245
+ const isBuyMode = !!(destinationTokenAddress && destinationTokenChainId) && lockDestinationToken;
235
246
 
236
247
  // Add refs to track URL state
237
248
  const initialUrlProcessed = useRef(false);
@@ -323,10 +334,16 @@ function AnySpendInner({
323
334
  sessionStorage.setItem("anyspend_fiat_method", selectedFiatPaymentMethod);
324
335
  }, [selectedFiatPaymentMethod]);
325
336
 
337
+ // Whether a default destination token was provided. When lockDestinationToken is false this is
338
+ // used only as the INITIAL/default value (the user can still change the receive token), so the
339
+ // wallet-funding deposit flow defaults to Base USDC but stays selectable.
340
+ const hasProvidedDestination = !!(destinationTokenAddress && destinationTokenChainId);
341
+
326
342
  // Get initial chain IDs from URL or defaults
327
343
  const initialSrcChainId = sourceChainId || parseInt(searchParams.get("fromChainId") || "0") || mainnet.id;
328
344
  const initialDstChainId =
329
- parseInt(searchParams.get("toChainId") || "0") || (isBuyMode ? destinationTokenChainId : base.id);
345
+ parseInt(searchParams.get("toChainId") || "0") ||
346
+ (isBuyMode ? destinationTokenChainId : hasProvidedDestination ? destinationTokenChainId : base.id);
330
347
 
331
348
  // State for source chain/token selection
332
349
  const [selectedSrcChainId, setSelectedSrcChainId] = useState<number>(initialSrcChainId);
@@ -368,19 +385,28 @@ function AnySpendInner({
368
385
  const isHyperliquidUSDCAddress = (address?: string) =>
369
386
  eqci(address, HYPERLIQUID_USDC_ADDRESS) || eqci(address, ZERO_ADDRESS);
370
387
 
371
- const defaultDstToken = isBuyMode
372
- ? // Special case: Hyperliquid uses zero address for USDC
373
- destinationTokenChainId === HYPERLIQUID_CHAIN_ID && isHyperliquidUSDCAddress(destinationTokenAddress)
374
- ? getHyperliquidUSDCToken()
375
- : {
376
- symbol: "",
377
- chainId: destinationTokenChainId,
378
- address: destinationTokenAddress,
379
- name: "",
380
- decimals: 18,
381
- metadata: {},
382
- }
383
- : getDefaultToken(selectedDstChainId);
388
+ // Build a token object from the provided destination props (handles the Hyperliquid USDC special case).
389
+ // The inline truthiness check lets TypeScript narrow both props to non-null, so it is `undefined`
390
+ // unless both props are provided — i.e. only meaningful when hasProvidedDestination/isBuyMode is true.
391
+ const providedDstToken: components["schemas"]["Token"] | undefined =
392
+ destinationTokenChainId && destinationTokenAddress
393
+ ? destinationTokenChainId === HYPERLIQUID_CHAIN_ID && isHyperliquidUSDCAddress(destinationTokenAddress)
394
+ ? getHyperliquidUSDCToken()
395
+ : {
396
+ symbol: "",
397
+ chainId: destinationTokenChainId,
398
+ address: destinationTokenAddress,
399
+ name: "",
400
+ decimals: 18,
401
+ metadata: {},
402
+ }
403
+ : undefined;
404
+
405
+ // In buy mode the provided destination is locked. Otherwise, if a default destination was provided
406
+ // (selectable deposit flow) seed it as the default; falling back to the chain's default token only
407
+ // when no destination prop is given (standard swap mode).
408
+ const defaultDstToken =
409
+ (isBuyMode || hasProvidedDestination) && providedDstToken ? providedDstToken : getDefaultToken(selectedDstChainId);
384
410
  const dstTokenFromUrl = useTokenFromUrl({
385
411
  defaultToken: defaultDstToken,
386
412
  prefix: "to",
@@ -1,3 +1,4 @@
1
+ import { USDC_BASE } from "@b3dotfun/sdk/anyspend";
1
2
  import { components } from "@b3dotfun/sdk/anyspend/types/api";
2
3
  import { GetQuoteResponse } from "@b3dotfun/sdk/anyspend/types/api_req_res";
3
4
  import { Skeleton, useAccountWallet, useSimBalance, useTokenData } from "@b3dotfun/sdk/global-account/react";
@@ -11,7 +12,7 @@ import {
11
12
  NetworkPolygonPos,
12
13
  } from "@web3icons/react";
13
14
  import { ChevronRight } from "lucide-react";
14
- import { useEffect, useMemo, useState } from "react";
15
+ import { useCallback, useEffect, useMemo, useState } from "react";
15
16
  import { AnySpend } from "./AnySpend";
16
17
  import { AnySpendCustomExactIn } from "./AnySpendCustomExactIn";
17
18
  import { ChainWarningText } from "./common/WarningText";
@@ -62,6 +63,13 @@ export interface AnySpendDepositProps {
62
63
  destinationTokenAddress: string;
63
64
  /** The destination chain ID */
64
65
  destinationTokenChainId: number;
66
+ /**
67
+ * When false, the destination token is user-selectable even though a default destination token is
68
+ * provided (the provided destination is used only as the initial/default value). Used by the
69
+ * wallet-funding deposit flow so the user can change the receive token away from the default
70
+ * (Base USDC). Defaults to true (locked destination, current behavior). Does not affect the
71
+ * QR/pure-transfer path. */
72
+ lockDestinationToken?: boolean;
65
73
  /** Callback when deposit succeeds */
66
74
  onSuccess?: (amount: string) => void;
67
75
  /** Callback for opening a custom modal (e.g., for special token handling) */
@@ -124,6 +132,13 @@ export interface AnySpendDepositProps {
124
132
  classes?: AnySpendAllClasses;
125
133
  /** When true, allows direct transfer without swap if source and destination token/chain are the same */
126
134
  allowDirectTransfer?: boolean;
135
+ /**
136
+ * When true, the QR-deposit path is a PURE TRANSFER: the destination token/chain
137
+ * mirror whatever source token the user selects, so the exact token they send lands
138
+ * in their wallet on the same chain (no swap/bridge). Forwarded to QRDeposit.
139
+ * Defaults to false.
140
+ */
141
+ pureTransferOnly?: boolean;
127
142
  /** Fixed destination token amount (in wei/smallest unit). When provided, user cannot change the amount. */
128
143
  destinationTokenAmount?: string;
129
144
  /** Opaque metadata passed to the order for callbacks (e.g., workflow form data) */
@@ -151,6 +166,21 @@ const DEFAULT_SUPPORTED_CHAINS: ChainConfig[] = [
151
166
  // Minimum pool size to filter out low liquidity tokens
152
167
  const DEFAULT_MIN_POOL_SIZE = 1_000_000;
153
168
 
169
+ /**
170
+ * Self-described fallback metadata for the default funding token(s), keyed by
171
+ * `${chainId}:${lowercased address}`. Used when `useTokenData` misses (returns
172
+ * null) so the destination token's decimals/symbol are correct WITHOUT depending
173
+ * on the network — critical in pure-transfer mode where there's no server-side
174
+ * correction and the wrong decimals would make `useWatchTransfer` show a wrong amount.
175
+ */
176
+ const KNOWN_FUNDING_TOKENS: Record<string, components["schemas"]["Token"]> = {
177
+ [`${USDC_BASE.chainId}:${USDC_BASE.address.toLowerCase()}`]: USDC_BASE,
178
+ };
179
+
180
+ function getKnownFundingToken(chainId: number, address: string): components["schemas"]["Token"] | undefined {
181
+ return KNOWN_FUNDING_TOKENS[`${chainId}:${address.toLowerCase()}`];
182
+ }
183
+
154
184
  type DepositStep = "select-chain" | "deposit" | "qr-deposit";
155
185
 
156
186
  function formatUsd(value: number): string {
@@ -239,6 +269,7 @@ export function AnySpendDeposit({
239
269
  sourceTokenChainId: initialSourceChainId,
240
270
  destinationTokenAddress,
241
271
  destinationTokenChainId,
272
+ lockDestinationToken = true,
242
273
  onSuccess,
243
274
  onOpenCustomModal,
244
275
  mainFooter,
@@ -261,6 +292,7 @@ export function AnySpendDeposit({
261
292
  isCustomDeposit = false,
262
293
  classes,
263
294
  allowDirectTransfer = false,
295
+ pureTransferOnly = false,
264
296
  destinationTokenAmount,
265
297
  callbackMetadata,
266
298
  senderAddress,
@@ -289,20 +321,26 @@ export function AnySpendDeposit({
289
321
  }, [showFiatOption, paymentType]);
290
322
 
291
323
  // Fetch destination token data
292
- const { data: destinationTokenData } = useTokenData(destinationTokenChainId, destinationTokenAddress);
324
+ const { data: destinationTokenData, isLoading: isDestinationTokenLoading } = useTokenData(
325
+ destinationTokenChainId,
326
+ destinationTokenAddress,
327
+ );
293
328
 
294
- // Construct full destination token object
295
- const destinationToken: components["schemas"]["Token"] = useMemo(
296
- () => ({
329
+ // Construct full destination token object. When `useTokenData` misses (it returns
330
+ // null on an API miss, not an error), fall back to a known-token entry so the
331
+ // decimals/symbol invariant holds regardless of the fetch — e.g. Base USDC is 6
332
+ // decimals, never the generic 18 fallback.
333
+ const destinationToken: components["schemas"]["Token"] = useMemo(() => {
334
+ const known = getKnownFundingToken(destinationTokenChainId, destinationTokenAddress);
335
+ return {
297
336
  address: destinationTokenAddress,
298
337
  chainId: destinationTokenChainId,
299
- symbol: destinationTokenData?.symbol ?? "",
300
- name: destinationTokenData?.name ?? "",
301
- decimals: destinationTokenData?.decimals ?? 18,
302
- metadata: { logoURI: destinationTokenData?.logoURI },
303
- }),
304
- [destinationTokenAddress, destinationTokenChainId, destinationTokenData],
305
- );
338
+ symbol: destinationTokenData?.symbol ?? known?.symbol ?? "",
339
+ name: destinationTokenData?.name ?? known?.name ?? "",
340
+ decimals: destinationTokenData?.decimals ?? known?.decimals ?? 18,
341
+ metadata: { logoURI: destinationTokenData?.logoURI ?? known?.metadata?.logoURI },
342
+ };
343
+ }, [destinationTokenAddress, destinationTokenChainId, destinationTokenData]);
306
344
 
307
345
  // Fetch balances for EOA wallet (use senderAddress as fallback for pre-filled balance display)
308
346
  const effectiveBalanceAddress = senderAddress || eoaAddress;
@@ -359,6 +397,13 @@ export function AnySpendDeposit({
359
397
  return Object.values(chainBalances).reduce((sum, chain) => sum + chain.totalUsdValue, 0);
360
398
  }, [chainBalances]);
361
399
 
400
+ const handleQRDepositSuccess = useCallback(
401
+ (txHash?: string) => {
402
+ onSuccess?.(txHash ?? "");
403
+ },
404
+ [onSuccess],
405
+ );
406
+
362
407
  if (!recipientAddress) return null;
363
408
 
364
409
  const tokenSymbol = destinationToken.symbol || "TOKEN";
@@ -644,15 +689,44 @@ export function AnySpendDeposit({
644
689
 
645
690
  // QR Deposit view
646
691
  if (step === "qr-deposit") {
692
+ // In pure-transfer mode QRDeposit captures `destinationToken` as its initial
693
+ // SOURCE token via useState, so it must be fully resolved (correct symbol +
694
+ // decimals) before QRDeposit mounts. Known funding tokens (e.g. Base USDC) are
695
+ // already fully resolved via the self-described fallback, so only show a spinner
696
+ // for unknown tokens whose metadata still has to load over the network.
697
+ const hasKnownDestination = !!getKnownFundingToken(destinationTokenChainId, destinationTokenAddress);
698
+ // NOTE: all current Fund Wallet callers derive `destinationTokenAddress` from
699
+ // `getDefaultDepositDestination`, which always returns Base USDC — exactly a
700
+ // `KNOWN_FUNDING_TOKENS` key — so `hasKnownDestination` is always true and this
701
+ // branch never fires for them. The guard is defensive for FUTURE callers that pass
702
+ // an unknown token together with `pureTransferOnly=true`.
703
+ if (pureTransferOnly && !hasKnownDestination && !destinationTokenData && isDestinationTokenLoading) {
704
+ return (
705
+ <div
706
+ className={cn(
707
+ "anyspend-deposit anyspend-deposit-qr-loading font-inter bg-as-surface-primary mx-auto w-full max-w-[460px] p-6",
708
+ mode === "page" && "border-as-border-secondary overflow-hidden rounded-2xl border shadow-xl",
709
+ )}
710
+ >
711
+ <div className="anyspend-deposit-qr-loading-content flex flex-col items-center justify-center gap-4 py-12">
712
+ <Skeleton className="h-8 w-8 rounded-full" />
713
+ <Skeleton className="h-4 w-40" />
714
+ </div>
715
+ </div>
716
+ );
717
+ }
718
+
647
719
  return (
648
720
  <QRDeposit
649
721
  mode={mode}
650
722
  recipientAddress={recipientAddress}
651
723
  destinationToken={destinationToken}
652
724
  destinationChainId={destinationTokenChainId}
725
+ pureTransferOnly={pureTransferOnly}
653
726
  depositContractConfig={depositContractConfig}
654
727
  onBack={handleBack}
655
728
  onClose={onClose ?? handleBack}
729
+ onSuccess={handleQRDepositSuccess}
656
730
  classes={classes?.qrDeposit}
657
731
  />
658
732
  );
@@ -746,6 +820,7 @@ export function AnySpendDeposit({
746
820
  sourceChainId={selectedChainId}
747
821
  destinationTokenAddress={destinationTokenAddress}
748
822
  destinationTokenChainId={destinationTokenChainId}
823
+ lockDestinationToken={lockDestinationToken}
749
824
  onSuccess={txHash => onSuccess?.(txHash ?? "")}
750
825
  onTokenSelect={onTokenSelect}
751
826
  customUsdInputValues={customUsdInputValues}
@@ -11,7 +11,7 @@ import { cn } from "@b3dotfun/sdk/shared/utils/cn";
11
11
  import { TokenSelector } from "@relayprotocol/relay-kit-ui";
12
12
  import { Check, ChevronsUpDown, Copy, Loader2 } from "lucide-react";
13
13
  import { QRCodeSVG } from "qrcode.react";
14
- import { useEffect, useRef, useState } from "react";
14
+ import { useCallback, useEffect, useRef, useState } from "react";
15
15
  import { useAnyspendOrderAndTransactions } from "../hooks/useAnyspendOrderAndTransactions";
16
16
  import { useCreateDepositFirstOrder } from "../hooks/useCreateDepositFirstOrder";
17
17
  import { useOnOrderSuccess } from "../hooks/useOnOrderSuccess";
@@ -36,6 +36,14 @@ export interface QRDepositProps {
36
36
  destinationToken: components["schemas"]["Token"];
37
37
  /** The destination chain ID */
38
38
  destinationChainId: number;
39
+ /**
40
+ * When true, the deposit is a PURE TRANSFER: the destination token/chain mirror
41
+ * the user's currently selected source token/chain, so the exact token the user
42
+ * sends lands in their wallet on the same chain — no swap/bridge order is created.
43
+ * The `destinationToken`/`destinationChainId` props are then used only as the
44
+ * initial source selection (they must be fully resolved before mount). Defaults to false.
45
+ */
46
+ pureTransferOnly?: boolean;
39
47
  /** Creator address (optional) */
40
48
  creatorAddress?: string;
41
49
  /** Contract config for custom execution after deposit */
@@ -46,8 +54,11 @@ export interface QRDepositProps {
46
54
  onClose?: () => void;
47
55
  /** Callback when order is created successfully */
48
56
  onOrderCreated?: (orderId: string) => void;
49
- /** Callback when deposit is completed */
50
- onSuccess?: (txHash?: string) => void;
57
+ /**
58
+ * Callback when deposit is completed. The argument carries the deposited amount string
59
+ * (e.g. "200.00") on the pure-transfer path, or the order tx hash otherwise.
60
+ */
61
+ onSuccess?: (amountOrTxHash?: string) => void;
51
62
  /** Custom classes for styling */
52
63
  classes?: QRDepositClasses;
53
64
  }
@@ -85,6 +96,7 @@ export function QRDeposit({
85
96
  sourceChainId: sourceChainIdProp,
86
97
  destinationToken,
87
98
  destinationChainId,
99
+ pureTransferOnly = false,
88
100
  creatorAddress,
89
101
  depositContractConfig,
90
102
  onBack,
@@ -99,31 +111,45 @@ export function QRDeposit({
99
111
  const orderCreatedRef = useRef(false);
100
112
  const [transferResult, setTransferResult] = useState<TransferResult | null>(null);
101
113
 
102
- // Source token/chain as state (can be changed by user)
103
- const [sourceChainId, setSourceChainId] = useState(sourceChainIdProp ?? 8453);
114
+ // Source token/chain as state (can be changed by user).
115
+ // In pure-transfer mode the initial source is the passed destination (caller sets
116
+ // it to the desired default funding token), so the deposit mirrors the user's selection.
117
+ const [sourceChainId, setSourceChainId] = useState(
118
+ sourceChainIdProp ?? (pureTransferOnly ? destinationChainId : 8453),
119
+ );
104
120
  const [sourceToken, setSourceToken] = useState<components["schemas"]["Token"]>(
105
- sourceTokenProp ?? DEFAULT_ETH_ON_BASE,
121
+ sourceTokenProp ?? (pureTransferOnly ? destinationToken : DEFAULT_ETH_ON_BASE),
106
122
  );
107
123
 
124
+ // In pure-transfer mode the effective destination mirrors the selected source,
125
+ // forcing isPureTransfer = true (no swap/bridge order is created).
126
+ const effectiveDestinationToken = pureTransferOnly ? sourceToken : destinationToken;
127
+ const effectiveDestinationChainId = pureTransferOnly ? sourceChainId : destinationChainId;
128
+
108
129
  // Check if this is a pure transfer (same chain and token)
109
130
  const isPureTransfer = isSameChainAndToken(
110
131
  sourceChainId,
111
132
  sourceToken.address,
112
- destinationChainId,
113
- destinationToken.address,
133
+ effectiveDestinationChainId,
134
+ effectiveDestinationToken.address,
135
+ );
136
+
137
+ const handleTransferDetected = useCallback(
138
+ (result: TransferResult) => {
139
+ setTransferResult(result);
140
+ onSuccess?.(result.formattedAmount);
141
+ },
142
+ [onSuccess],
114
143
  );
115
144
 
116
145
  // Watch for pure transfers (same chain and token)
117
- const { isWatching: isWatchingTransfer } = useWatchTransfer({
146
+ const { isWatching: isWatchingTransfer, reset: resetWatchTransfer } = useWatchTransfer({
118
147
  address: recipientAddress,
119
148
  chainId: sourceChainId,
120
149
  tokenAddress: sourceToken.address,
121
150
  tokenDecimals: sourceToken.decimals,
122
151
  enabled: isPureTransfer && !transferResult,
123
- onTransferDetected: result => {
124
- setTransferResult(result);
125
- onSuccess?.();
126
- },
152
+ onTransferDetected: handleTransferDetected,
127
153
  });
128
154
 
129
155
  // Handle token selection from TokenSelector
@@ -142,6 +168,8 @@ export function QRDeposit({
142
168
  setGlobalAddress(undefined);
143
169
  orderCreatedRef.current = false;
144
170
  setTransferResult(null);
171
+ // Reset the balance watcher baseline so the new token isn't compared against the old token's balance
172
+ resetWatchTransfer();
145
173
 
146
174
  // Update token and chain
147
175
  setSourceChainId(newToken.chainId);
@@ -176,18 +204,18 @@ export function QRDeposit({
176
204
  createOrder({
177
205
  recipientAddress,
178
206
  srcChain: sourceChainId,
179
- dstChain: destinationChainId,
207
+ dstChain: effectiveDestinationChainId,
180
208
  srcToken: sourceToken,
181
- dstToken: destinationToken,
209
+ dstToken: effectiveDestinationToken,
182
210
  creatorAddress,
183
211
  contractConfig: depositContractConfig,
184
212
  });
185
213
  }, [
186
214
  recipientAddress,
187
215
  sourceChainId,
188
- destinationChainId,
216
+ effectiveDestinationChainId,
189
217
  sourceToken,
190
- destinationToken,
218
+ effectiveDestinationToken,
191
219
  creatorAddress,
192
220
  depositContractConfig,
193
221
  createOrder,
@@ -425,7 +453,7 @@ export function QRDeposit({
425
453
  </div>
426
454
 
427
455
  {/* Warnings */}
428
- <ChainWarningText chainId={destinationChainId} />
456
+ <ChainWarningText chainId={effectiveDestinationChainId} />
429
457
  <WarningText>
430
458
  Only send {sourceToken.symbol} on {ALL_CHAINS[sourceChainId]?.name ?? "the specified chain"}. Other tokens
431
459
  will not be converted.