@b3dotfun/sdk 0.1.70-alpha.21 → 0.1.70-alpha.22

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 (22) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.d.ts +19 -0
  2. package/dist/cjs/anyspend/react/components/AnySpend.js +45 -15
  3. package/dist/cjs/anyspend/react/components/AnySpendDeposit.d.ts +9 -1
  4. package/dist/cjs/anyspend/react/components/AnySpendDeposit.js +6 -2
  5. package/dist/cjs/anyspend/react/components/common/CryptoReceiveSection.js +6 -2
  6. package/dist/cjs/global-account/react/components/B3Provider/BetterAuthProvider.js +36 -12
  7. package/dist/esm/anyspend/react/components/AnySpend.d.ts +19 -0
  8. package/dist/esm/anyspend/react/components/AnySpend.js +45 -15
  9. package/dist/esm/anyspend/react/components/AnySpendDeposit.d.ts +9 -1
  10. package/dist/esm/anyspend/react/components/AnySpendDeposit.js +6 -2
  11. package/dist/esm/anyspend/react/components/common/CryptoReceiveSection.js +6 -2
  12. package/dist/esm/global-account/react/components/B3Provider/BetterAuthProvider.js +36 -12
  13. package/dist/types/anyspend/react/components/AnySpend.d.ts +19 -0
  14. package/dist/types/anyspend/react/components/AnySpendDeposit.d.ts +9 -1
  15. package/package.json +1 -1
  16. package/src/anyspend/react/components/AnySpend.tsx +66 -14
  17. package/src/anyspend/react/components/AnySpendDeposit.tsx +14 -0
  18. package/src/anyspend/react/components/__tests__/AnySpend.seedEffect.test.tsx +191 -0
  19. package/src/anyspend/react/components/__tests__/AnySpendDeposit.isDepositMode.test.tsx +117 -0
  20. package/src/anyspend/react/components/common/CryptoReceiveSection.tsx +23 -2
  21. package/src/anyspend/react/components/common/__tests__/CryptoReceiveSection.test.tsx +110 -0
  22. package/src/global-account/react/components/B3Provider/BetterAuthProvider.tsx +35 -11
@@ -64,6 +64,13 @@ export declare function AnySpend(props: {
64
64
  allowDirectTransfer?: boolean;
65
65
  /** Fixed destination token amount (in wei/smallest unit). When provided, user cannot change the amount. */
66
66
  destinationTokenAmount?: string;
67
+ /**
68
+ * Editable default destination token amount (in wei/smallest unit). Seeds the same
69
+ * destination-amount input as `destinationTokenAmount`, but leaves it user-editable. Ignored
70
+ * when `destinationTokenAmount` is also set — that prop locks the field and wins.
71
+ * Seeds the field once on first apply; a later change to this value does not re-fill it, so it never overwrites what the user typed.
72
+ */
73
+ defaultDestinationTokenAmount?: string;
67
74
  /** Opaque metadata passed to the order for callbacks (e.g., workflow form data) */
68
75
  callbackMetadata?: Record<string, unknown>;
69
76
  /** Optional sender (payer) address — pre-fills token balances when the user address is known ahead of time */
@@ -78,4 +85,16 @@ export declare function AnySpend(props: {
78
85
  kycEnabled?: boolean;
79
86
  /** Whether to show the "Pay with Fiat" tab. Defaults to true. When false, the fiat tab is hidden and activeTab is forced to "crypto". */
80
87
  showFiatOption?: boolean;
88
+ /**
89
+ * Internal: true when `AnySpendDeposit` has an amount to prefill (`defaultDestinationTokenAmount`
90
+ * set), as opposed to any other `AnySpend`/`AnySpendDeposit` usage. Only changes
91
+ * `CryptoReceiveSection`'s fixed-destination branch: it lets the receive amount become an
92
+ * editable input (still gated by `disableAmountInput`) instead of a read-only display. Scoped
93
+ * this narrowly on purpose — `AnySpendDeposit` backs 15+ funding modals that are source-exact-in
94
+ * (the user sets the "pay with" amount; the receive side is an intended computed display), so
95
+ * this must stay off for all of them and only turn on for the one page that actually prefills a
96
+ * receive amount. Not intended for external callers — `AnySpendDeposit` derives this itself.
97
+ * @internal
98
+ */
99
+ isDepositMode?: boolean;
81
100
  }): import("react/jsx-runtime").JSX.Element;
@@ -69,7 +69,7 @@ function AnySpend(props) {
69
69
  const fingerprintConfig = (0, AnySpendFingerprintWrapper_1.getFingerprintConfig)();
70
70
  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 }) }) }));
71
71
  }
72
- function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken = true, 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, showFiatOption = true, }) {
72
+ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken = true, mode = "modal", defaultActiveTab = "crypto", loadOrder, hideTransactionHistoryButton, recipientAddress: recipientAddressFromProps, onTokenSelect, onSuccess, customUsdInputValues, hideHeader, hideBottomNavigation = false, disableUrlParamManagement = false, returnToHomeUrl, customRecipientLabel, returnHomeLabel, classes, allowDirectTransfer = false, destinationTokenAmount, defaultDestinationTokenAmount, callbackMetadata, senderAddress, kycEnabled = false, showFiatOption = true, isDepositMode = false, }) {
73
73
  const { slots, content } = (0, AnySpendCustomizationContext_1.useAnySpendCustomization)();
74
74
  const searchParams = (0, react_2.useSearchParamsSSR)();
75
75
  const router = (0, react_2.useRouter)();
@@ -237,6 +237,21 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
237
237
  sessionStorage.setItem("anyspend_dst_token", JSON.stringify(selectedDstToken));
238
238
  }, [selectedDstToken, isBuyMode]);
239
239
  const [isSrcInputDirty, setIsSrcInputDirty] = (0, react_4.useState)(true);
240
+ // Synchronous mirror of `isSrcInputDirty`. `setIsSrcInputDirty` is async relative to OTHER
241
+ // effects in the same commit — the quote effect (declared later, same commit) would otherwise
242
+ // read a pre-flip closure value and clear a just-seeded receive amount before it ever paints.
243
+ // Every flip goes through `markSrcDirty`, which writes this ref in the same statement, so
244
+ // same-commit siblings always see the latest intent. Deliberately NOT kept in sync by its own
245
+ // effect: an effect reading `isSrcInputDirty` from its closure would itself go stale across
246
+ // React 18 StrictMode's dev-mode double-invoke of mount effects (same closure re-run twice)
247
+ // and re-stomp this ref back to the pre-flip value between the two passes.
248
+ const isSrcInputDirtyRef = (0, react_4.useRef)(isSrcInputDirty);
249
+ // Single writer for the direction flag: keeps state and its synchronous ref mirror in lockstep
250
+ // so no call site — parent or child (CryptoPaySection) — can flip one without the other.
251
+ const markSrcDirty = (0, react_4.useCallback)((next) => {
252
+ setIsSrcInputDirty(next);
253
+ isSrcInputDirtyRef.current = next;
254
+ }, []);
240
255
  // Add refs to track if we've applied metadata
241
256
  const appliedSrcMetadataRef = (0, react_4.useRef)(false);
242
257
  const appliedDstMetadataRef = (0, react_4.useRef)(false);
@@ -289,17 +304,24 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
289
304
  (0, react_4.useEffect)(() => {
290
305
  appliedDstMetadataRef.current = false;
291
306
  }, [selectedDstToken.address, selectedDstToken.chainId]);
292
- // Prefill destination amount if provided (for fixed amount mode)
307
+ // Prefill destination amount if provided fixed (locking) mode wins over the editable default,
308
+ // since `destinationTokenAmount` also disables the input (see `disableAmountInput` below).
293
309
  const appliedDestinationAmount = (0, react_4.useRef)(false);
310
+ const seedDestinationAmount = destinationTokenAmount ?? defaultDestinationTokenAmount;
294
311
  (0, react_4.useEffect)(() => {
295
312
  // Only apply when we have real metadata (not default decimals)
296
- if (destinationTokenAmount && dstTokenMetadata?.decimals && !appliedDestinationAmount.current) {
297
- appliedDestinationAmount.current = true;
298
- const formattedAmount = (0, viem_1.formatUnits)(BigInt(destinationTokenAmount), dstTokenMetadata.decimals);
299
- setDstAmount(formattedAmount);
300
- setIsSrcInputDirty(false); // Switch to EXACT_OUTPUT mode
313
+ if (seedDestinationAmount && dstTokenMetadata?.decimals && !appliedDestinationAmount.current) {
314
+ try {
315
+ const formattedAmount = (0, viem_1.formatUnits)(BigInt(seedDestinationAmount), dstTokenMetadata.decimals);
316
+ appliedDestinationAmount.current = true;
317
+ setDstAmount(formattedAmount);
318
+ markSrcDirty(false); // Switch to EXACT_OUTPUT mode
319
+ }
320
+ catch {
321
+ // invalid seed value — no-op rather than throw into the effect (fail-closed)
322
+ }
301
323
  }
302
- }, [destinationTokenAmount, dstTokenMetadata]);
324
+ }, [seedDestinationAmount, dstTokenMetadata, markSrcDirty]);
303
325
  // Load swap configuration from URL on initial render
304
326
  (0, react_4.useEffect)(() => {
305
327
  // Skip if we've already processed the URL, if we have an order to load, or if URL param management is disabled
@@ -322,7 +344,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
322
344
  }
323
345
  else {
324
346
  setSrcAmount(fromAmountParam);
325
- setIsSrcInputDirty(true);
347
+ markSrcDirty(true);
326
348
  }
327
349
  }
328
350
  }
@@ -336,7 +358,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
336
358
  }
337
359
  // Mark that we've processed the initial URL
338
360
  initialUrlProcessed.current = true;
339
- }, [searchParams, loadOrder, disableUrlParamManagement]);
361
+ }, [searchParams, loadOrder, disableUrlParamManagement, markSrcDirty]);
340
362
  // Update URL when swap configuration changes - but not on initial load
341
363
  const updateSwapParamsInURL = (0, react_4.useCallback)(() => {
342
364
  // Skip if:
@@ -558,12 +580,20 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
558
580
  }, [globalAddress, effectiveRecipientAddress, customRecipients.length, setSelectedRecipientAddress]);
559
581
  // Update dependent amount when relay price changes
560
582
  (0, react_4.useEffect)(() => {
583
+ // Read the ref (not the closed-over `isSrcInputDirty` param) for the branch decision: the
584
+ // SEED effect (declared above, same commit when it fires) writes to the ref synchronously
585
+ // when it flips this to exact-out mode, so this effect sees that flip immediately even
586
+ // though `setIsSrcInputDirty`'s state update won't be visible in THIS render's closure until
587
+ // the next commit. Using the stale closure value here was the actual bug — it caused this
588
+ // effect to still treat a freshly-seeded exact-out amount as exact-in-with-no-data and clear
589
+ // it before it ever painted. The dependency array below stays on the real state, so this
590
+ // effect still re-runs correctly on every genuine transition.
561
591
  if (anyspendQuote?.data &&
562
592
  anyspendQuote.data.currencyIn?.amount &&
563
593
  anyspendQuote.data.currencyIn?.currency?.decimals &&
564
594
  anyspendQuote.data.currencyOut?.amount &&
565
595
  anyspendQuote.data.currencyOut?.currency?.decimals) {
566
- if (isSrcInputDirty) {
596
+ if (isSrcInputDirtyRef.current) {
567
597
  // The user is driving the SOURCE (exact-in): reflect the quote's OUTPUT even
568
598
  // when a fixed destinationTokenAmount pre-filled the receive. That lock only
569
599
  // governs the INITIAL exact-out state (untouched source → compute the pay for
@@ -581,7 +611,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
581
611
  }
582
612
  }
583
613
  else {
584
- if (isSrcInputDirty) {
614
+ if (isSrcInputDirtyRef.current) {
585
615
  setDstAmount("");
586
616
  }
587
617
  else {
@@ -1067,7 +1097,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
1067
1097
  // Reset payment methods when switching tabs
1068
1098
  resetPaymentMethods();
1069
1099
  setSelectedFiatPaymentMethod(FiatPaymentMethod_1.FiatPaymentMethod.NONE);
1070
- }, setSelectedCryptoPaymentMethod: setSelectedCryptoPaymentMethod, setSelectedFiatPaymentMethod: setSelectedFiatPaymentMethod })), (0, jsx_runtime_1.jsxs)("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: [activeTab === "crypto" ? ((0, jsx_runtime_1.jsx)(CryptoPaySection_1.CryptoPaySection, { selectedSrcChainId: selectedSrcChainId, setSelectedSrcChainId: setSelectedSrcChainId, selectedSrcToken: selectedSrcToken, setSelectedSrcToken: setSelectedSrcToken, srcAmount: srcAmount, setSrcAmount: setSrcAmount, isSrcInputDirty: isSrcInputDirty, setIsSrcInputDirty: setIsSrcInputDirty, selectedCryptoPaymentMethod: effectiveCryptoPaymentMethod, onSelectCryptoPaymentMethod: () => navigateToPanel(PanelView.CRYPTO_PAYMENT_METHOD, "forward"), anyspendQuote: anyspendQuote, onTokenSelect: onTokenSelect, onShowFeeDetail: () => navigateToPanel(PanelView.FEE_DETAIL, "forward") })) : ((0, jsx_runtime_1.jsx)(react_3.motion.div, { initial: { opacity: 0, y: 20, filter: "blur(10px)" }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, transition: { duration: 0.3, delay: 0, ease: "easeInOut" }, children: (0, jsx_runtime_1.jsx)(PanelOnramp_1.PanelOnramp, { srcAmountOnRamp: srcAmountOnRamp, setSrcAmountOnRamp: setSrcAmountOnRamp, selectedPaymentMethod: selectedFiatPaymentMethod, setActivePanel: (panelIndex) => {
1100
+ }, setSelectedCryptoPaymentMethod: setSelectedCryptoPaymentMethod, setSelectedFiatPaymentMethod: setSelectedFiatPaymentMethod })), (0, jsx_runtime_1.jsxs)("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: [activeTab === "crypto" ? ((0, jsx_runtime_1.jsx)(CryptoPaySection_1.CryptoPaySection, { selectedSrcChainId: selectedSrcChainId, setSelectedSrcChainId: setSelectedSrcChainId, selectedSrcToken: selectedSrcToken, setSelectedSrcToken: setSelectedSrcToken, srcAmount: srcAmount, setSrcAmount: setSrcAmount, isSrcInputDirty: isSrcInputDirty, setIsSrcInputDirty: markSrcDirty, selectedCryptoPaymentMethod: effectiveCryptoPaymentMethod, onSelectCryptoPaymentMethod: () => navigateToPanel(PanelView.CRYPTO_PAYMENT_METHOD, "forward"), anyspendQuote: anyspendQuote, onTokenSelect: onTokenSelect, onShowFeeDetail: () => navigateToPanel(PanelView.FEE_DETAIL, "forward") })) : ((0, jsx_runtime_1.jsx)(react_3.motion.div, { initial: { opacity: 0, y: 20, filter: "blur(10px)" }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, transition: { duration: 0.3, delay: 0, ease: "easeInOut" }, children: (0, jsx_runtime_1.jsx)(PanelOnramp_1.PanelOnramp, { srcAmountOnRamp: srcAmountOnRamp, setSrcAmountOnRamp: setSrcAmountOnRamp, selectedPaymentMethod: selectedFiatPaymentMethod, setActivePanel: (panelIndex) => {
1071
1101
  // Map panel index to navigation with direction
1072
1102
  const panelsWithForwardNav = [PanelView.FIAT_PAYMENT_METHOD, PanelView.RECIPIENT_SELECTION];
1073
1103
  if (panelsWithForwardNav.includes(panelIndex)) {
@@ -1094,8 +1124,8 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
1094
1124
  // Swap amounts
1095
1125
  setSrcAmount(dstAmount);
1096
1126
  setDstAmount(srcAmount);
1097
- }, children: (0, jsx_runtime_1.jsx)("div", { className: "relative flex items-center justify-center transition-opacity", children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { className: "text-as-primary/50 h-5 w-5" }) }) }) })), activeTab === "crypto" && ((0, jsx_runtime_1.jsx)(CryptoReceiveSection_1.CryptoReceiveSection, { isDepositMode: false, isBuyMode: isBuyMode, effectiveRecipientAddress: effectiveRecipientAddress, recipientName: recipientName || undefined, customRecipientLabel: customRecipientLabel, onSelectRecipient: () => navigateToPanel(PanelView.RECIPIENT_SELECTION, "forward"), dstAmount: dstAmount, dstToken: selectedDstToken, selectedDstChainId: selectedDstChainId, setSelectedDstChainId: setSelectedDstChainId, setSelectedDstToken: setSelectedDstToken, isSrcInputDirty: isSrcInputDirty, onChangeDstAmount: value => {
1098
- setIsSrcInputDirty(false);
1127
+ }, children: (0, jsx_runtime_1.jsx)("div", { className: "relative flex items-center justify-center transition-opacity", children: (0, jsx_runtime_1.jsx)(lucide_react_1.ArrowDown, { className: "text-as-primary/50 h-5 w-5" }) }) }) })), activeTab === "crypto" && ((0, jsx_runtime_1.jsx)(CryptoReceiveSection_1.CryptoReceiveSection, { isDepositMode: isDepositMode, isBuyMode: isBuyMode, effectiveRecipientAddress: effectiveRecipientAddress, recipientName: recipientName || undefined, customRecipientLabel: customRecipientLabel, onSelectRecipient: () => navigateToPanel(PanelView.RECIPIENT_SELECTION, "forward"), dstAmount: dstAmount, dstToken: selectedDstToken, selectedDstChainId: selectedDstChainId, setSelectedDstChainId: setSelectedDstChainId, setSelectedDstToken: setSelectedDstToken, isSrcInputDirty: isSrcInputDirty, onChangeDstAmount: value => {
1128
+ markSrcDirty(false);
1099
1129
  setDstAmount(value);
1100
1130
  }, disableAmountInput: !!destinationTokenAmount, anyspendQuote: isDirectTransfer ? undefined : anyspendQuote, onShowPointsDetail: isDirectTransfer ? undefined : () => navigateToPanel(PanelView.POINTS_DETAIL, "forward"), onShowFeeDetail: isDirectTransfer ? undefined : () => navigateToPanel(PanelView.FEE_DETAIL, "forward") }))] }), gasPriceData && !isLoadingGas && activeTab === "crypto" && !isDirectTransfer && ((0, jsx_runtime_1.jsx)(GasIndicator_1.GasIndicator, { gasPrice: gasPriceData, className: classes?.gasIndicator || "mt-2 w-full" })), (0, jsx_runtime_1.jsxs)(react_3.motion.div, { initial: { opacity: 0, y: 20, filter: "blur(10px)" }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, transition: { duration: 0.3, delay: 0.2, ease: "easeInOut" }, className: (0, cn_1.cn)("mt-4 flex w-full max-w-[460px] flex-col gap-2"), children: [slots.actionButton ? (slots.actionButton({
1101
1131
  onClick: onMainButtonClick,
@@ -116,6 +116,14 @@ export interface AnySpendDepositProps {
116
116
  pureTransferOnly?: boolean;
117
117
  /** Fixed destination token amount (in wei/smallest unit). When provided, user cannot change the amount. */
118
118
  destinationTokenAmount?: string;
119
+ /**
120
+ * Editable default destination token amount (in wei/smallest unit). Seeds the same
121
+ * destination-amount input as `destinationTokenAmount`, but leaves it user-editable. Ignored
122
+ * when `destinationTokenAmount` is also set — that prop locks the field and wins. Applies to
123
+ * the standard deposit flow only; custom-function deposits ignore it.
124
+ * Seeds the field once on first apply; a later change to this value does not re-fill it, so it never overwrites what the user typed.
125
+ */
126
+ defaultDestinationTokenAmount?: string;
119
127
  /** Opaque metadata passed to the order for callbacks (e.g., workflow form data) */
120
128
  callbackMetadata?: Record<string, unknown>;
121
129
  /** Optional sender (payer) address — pre-fills token balances when the user address is known ahead of time */
@@ -162,4 +170,4 @@ export interface AnySpendDepositProps {
162
170
  * onSuccess={(amount) => console.log(`Deposited ${amount}`)}
163
171
  * />
164
172
  */
165
- export declare function AnySpendDeposit({ loadOrder, mode, recipientAddress, paymentType: initialPaymentType, sourceTokenAddress, sourceTokenChainId: initialSourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken, onSuccess, onOpenCustomModal, mainFooter, onTokenSelect, customUsdInputValues, preferEoa, minDestinationAmount, header, orderType, depositContractConfig, showChainSelection, showFiatOption, supportedChains, topChainsCount, onClose, returnToHomeUrl, customRecipientLabel, returnHomeLabel, isCustomDeposit, classes, allowDirectTransfer, pureTransferOnly, destinationTokenAmount, callbackMetadata, senderAddress, slots, content, theme, }: AnySpendDepositProps): import("react/jsx-runtime").JSX.Element | null;
173
+ export declare function AnySpendDeposit({ loadOrder, mode, recipientAddress, paymentType: initialPaymentType, sourceTokenAddress, sourceTokenChainId: initialSourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken, onSuccess, onOpenCustomModal, mainFooter, onTokenSelect, customUsdInputValues, preferEoa, minDestinationAmount, header, orderType, depositContractConfig, showChainSelection, showFiatOption, supportedChains, topChainsCount, onClose, returnToHomeUrl, customRecipientLabel, returnHomeLabel, isCustomDeposit, classes, allowDirectTransfer, pureTransferOnly, destinationTokenAmount, defaultDestinationTokenAmount, callbackMetadata, senderAddress, slots, content, theme, }: AnySpendDepositProps): import("react/jsx-runtime").JSX.Element | null;
@@ -110,7 +110,7 @@ function ChainIcon({ chainId, className }) {
110
110
  * onSuccess={(amount) => console.log(`Deposited ${amount}`)}
111
111
  * />
112
112
  */
113
- function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, paymentType: initialPaymentType, sourceTokenAddress, sourceTokenChainId: initialSourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken = true, onSuccess, onOpenCustomModal, mainFooter, onTokenSelect, customUsdInputValues, preferEoa, minDestinationAmount, header, orderType, depositContractConfig, showChainSelection, showFiatOption = true, supportedChains = DEFAULT_SUPPORTED_CHAINS, topChainsCount = 3, onClose, returnToHomeUrl, customRecipientLabel, returnHomeLabel, isCustomDeposit = false, classes, allowDirectTransfer = false, pureTransferOnly = false, destinationTokenAmount, callbackMetadata, senderAddress, slots, content, theme, }) {
113
+ function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, paymentType: initialPaymentType, sourceTokenAddress, sourceTokenChainId: initialSourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken = true, onSuccess, onOpenCustomModal, mainFooter, onTokenSelect, customUsdInputValues, preferEoa, minDestinationAmount, header, orderType, depositContractConfig, showChainSelection, showFiatOption = true, supportedChains = DEFAULT_SUPPORTED_CHAINS, topChainsCount = 3, onClose, returnToHomeUrl, customRecipientLabel, returnHomeLabel, isCustomDeposit = false, classes, allowDirectTransfer = false, pureTransferOnly = false, destinationTokenAmount, defaultDestinationTokenAmount, callbackMetadata, senderAddress, slots, content, theme, }) {
114
114
  // Extract deposit-specific classes for convenience
115
115
  const depositClasses = classes?.deposit;
116
116
  const { connectedEOAWallet } = (0, react_1.useAccountWallet)();
@@ -269,5 +269,9 @@ function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, paymentT
269
269
  // Deposit view
270
270
  return ((0, jsx_runtime_1.jsxs)("div", { className: depositClasses?.form || "anyspend-deposit anyspend-deposit-form relative", children: [shouldShowChainSelection && ((0, jsx_runtime_1.jsxs)("button", { onClick: handleBack, className: depositClasses?.backButton ||
271
271
  "anyspend-deposit-back-button text-as-secondary hover:text-as-primary absolute left-4 top-4 z-10 flex items-center gap-1", children: [(0, jsx_runtime_1.jsx)("svg", { className: depositClasses?.backIcon || "anyspend-deposit-back-icon h-5 w-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }), (0, jsx_runtime_1.jsx)("span", { className: depositClasses?.backText || "anyspend-deposit-back-text text-sm", children: "Back" })] })), onClose && ((0, jsx_runtime_1.jsx)("button", { onClick: onClose, className: depositClasses?.closeButton ||
272
- "anyspend-deposit-close-button text-as-secondary hover:text-as-primary absolute right-4 top-4 z-10", children: (0, jsx_runtime_1.jsx)("svg", { className: "h-6 w-6", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })), (0, jsx_runtime_1.jsx)("div", { className: depositClasses?.formContent || (0, cn_1.cn)("anyspend-deposit-form-content", shouldShowChainSelection && "pt-8"), children: isCustomDeposit ? ((0, jsx_runtime_1.jsx)(AnySpendCustomExactIn_1.AnySpendCustomExactIn, { loadOrder: loadOrder, mode: mode, recipientAddress: recipientAddress, paymentType: paymentType, sourceTokenAddress: sourceTokenAddress, sourceTokenChainId: selectedChainId, destinationToken: destinationToken, destinationChainId: destinationTokenChainId, orderType: effectiveOrderType, minDestinationAmount: minDestinationAmount, header: header ?? defaultHeader, onSuccess: onSuccess, onOpenCustomModal: onOpenCustomModal, mainFooter: mainFooter, onTokenSelect: onTokenSelect, customUsdInputValues: customUsdInputValues, preferEoa: preferEoa, customExactInConfig: depositContractConfig, returnToHomeUrl: returnToHomeUrl, customRecipientLabel: customRecipientLabel, returnHomeLabel: returnHomeLabel, classes: classes?.customExactIn, allowDirectTransfer: allowDirectTransfer, destinationTokenAmount: destinationTokenAmount, callbackMetadata: callbackMetadata, senderAddress: senderAddress, showFiatOption: showFiatOption, slots: slots, content: content, theme: theme }, selectedChainId)) : ((0, jsx_runtime_1.jsx)(AnySpend_1.AnySpend, { loadOrder: loadOrder, mode: mode, defaultActiveTab: paymentType, recipientAddress: recipientAddress, sourceChainId: selectedChainId, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, lockDestinationToken: lockDestinationToken, onSuccess: txHash => onSuccess?.(txHash ?? ""), onTokenSelect: onTokenSelect, customUsdInputValues: customUsdInputValues, hideHeader: true, hideBottomNavigation: true, disableUrlParamManagement: true, returnToHomeUrl: returnToHomeUrl, customRecipientLabel: customRecipientLabel, returnHomeLabel: returnHomeLabel, classes: classes?.anySpend, allowDirectTransfer: allowDirectTransfer, destinationTokenAmount: destinationTokenAmount, callbackMetadata: callbackMetadata, senderAddress: senderAddress, showFiatOption: showFiatOption, slots: slots, content: content, theme: theme }, selectedChainId)) }), (0, jsx_runtime_1.jsx)(WarningText_1.ChainWarningText, { chainId: destinationTokenChainId, classes: classes?.chainWarningText || { root: "px-4 pb-4" } })] }));
272
+ "anyspend-deposit-close-button text-as-secondary hover:text-as-primary absolute right-4 top-4 z-10", children: (0, jsx_runtime_1.jsx)("svg", { className: "h-6 w-6", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: (0, jsx_runtime_1.jsx)("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })), (0, jsx_runtime_1.jsx)("div", { className: depositClasses?.formContent || (0, cn_1.cn)("anyspend-deposit-form-content", shouldShowChainSelection && "pt-8"), children: isCustomDeposit ? ((0, jsx_runtime_1.jsx)(AnySpendCustomExactIn_1.AnySpendCustomExactIn, { loadOrder: loadOrder, mode: mode, recipientAddress: recipientAddress, paymentType: paymentType, sourceTokenAddress: sourceTokenAddress, sourceTokenChainId: selectedChainId, destinationToken: destinationToken, destinationChainId: destinationTokenChainId, orderType: effectiveOrderType, minDestinationAmount: minDestinationAmount, header: header ?? defaultHeader, onSuccess: onSuccess, onOpenCustomModal: onOpenCustomModal, mainFooter: mainFooter, onTokenSelect: onTokenSelect, customUsdInputValues: customUsdInputValues, preferEoa: preferEoa, customExactInConfig: depositContractConfig, returnToHomeUrl: returnToHomeUrl, customRecipientLabel: customRecipientLabel, returnHomeLabel: returnHomeLabel, classes: classes?.customExactIn, allowDirectTransfer: allowDirectTransfer, destinationTokenAmount: destinationTokenAmount, callbackMetadata: callbackMetadata, senderAddress: senderAddress, showFiatOption: showFiatOption, slots: slots, content: content, theme: theme }, selectedChainId)) : ((0, jsx_runtime_1.jsx)(AnySpend_1.AnySpend, { loadOrder: loadOrder, mode: mode, defaultActiveTab: paymentType, recipientAddress: recipientAddress, sourceChainId: selectedChainId, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, lockDestinationToken: lockDestinationToken,
273
+ // Scoped to ONLY the prefill case (not every AnySpendDeposit caller — 15+ other
274
+ // funding modals rely on the source-exact-in "computed display" behavior this would
275
+ // otherwise change): editable destination amount only when there's a value to seed.
276
+ isDepositMode: !!defaultDestinationTokenAmount, onSuccess: txHash => onSuccess?.(txHash ?? ""), onTokenSelect: onTokenSelect, customUsdInputValues: customUsdInputValues, hideHeader: true, hideBottomNavigation: true, disableUrlParamManagement: true, returnToHomeUrl: returnToHomeUrl, customRecipientLabel: customRecipientLabel, returnHomeLabel: returnHomeLabel, classes: classes?.anySpend, allowDirectTransfer: allowDirectTransfer, destinationTokenAmount: destinationTokenAmount, defaultDestinationTokenAmount: defaultDestinationTokenAmount, callbackMetadata: callbackMetadata, senderAddress: senderAddress, showFiatOption: showFiatOption, slots: slots, content: content, theme: theme }, selectedChainId)) }), (0, jsx_runtime_1.jsx)(WarningText_1.ChainWarningText, { chainId: destinationTokenChainId, classes: classes?.chainWarningText || { root: "px-4 pb-4" } })] }));
273
277
  }
@@ -14,8 +14,12 @@ function CryptoReceiveSection({ isDepositMode = false, isBuyMode = false, effect
14
14
  return ((0, jsx_runtime_1.jsxs)(react_1.motion.div, { initial: { opacity: 0, y: 20, filter: "blur(10px)" }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, transition: { duration: 0.3, delay: 0.1, ease: "easeInOut" }, className: classes?.container ||
15
15
  "receive-section bg-as-surface-secondary border-as-border-secondary relative flex w-full flex-col gap-2 rounded-2xl border p-4 sm:p-6", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex w-full items-center justify-between", children: [(0, jsx_runtime_1.jsxs)("div", { className: classes?.label || "text-as-primary/50 flex h-7 items-center gap-1.5 text-sm", children: [isDepositMode ? "Deposit" : "Receive", isSrcInputDirty && anyspendQuote?.data?.fee && onShowFeeDetail && ((0, jsx_runtime_1.jsx)("button", { onClick: onShowFeeDetail, className: "text-as-primary/40 hover:text-as-primary/60 transition-colors", children: (0, jsx_runtime_1.jsx)(lucide_react_1.Info, { className: "h-4 w-4" }) }))] }), effectiveRecipientAddress ? ((0, jsx_runtime_1.jsx)("button", { className: classes?.recipientButton || "text-as-tertiary flex h-7 items-center gap-2 rounded-lg", onClick: onSelectRecipient, children: (0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsx)("span", { className: classes?.recipientValue || "text-as-tertiary flex items-center gap-1 text-sm", children: customRecipientLabel ||
16
16
  (recipientName ? (0, utils_1.formatUsername)(recipientName) : (0, formatAddress_1.shortenAddress)(effectiveRecipientAddress || "")) }), (0, jsx_runtime_1.jsx)(lucide_react_1.ChevronRight, { className: "h-4 w-4" })] }) })) : ((0, jsx_runtime_1.jsx)("button", { className: classes?.recipientButton || "text-as-primary/70 flex items-center gap-1 rounded-lg", onClick: onSelectRecipient, children: (0, jsx_runtime_1.jsx)("div", { className: "text-sm font-medium", children: "Select recipient" }) }))] }), isBuyMode || isDepositMode ? (
17
- // Fixed destination token display for buy mode and deposit mode
18
- (0, jsx_runtime_1.jsxs)("div", { className: classes?.inputContainer || "flex items-center justify-between", children: [(0, jsx_runtime_1.jsx)("div", { className: classes?.input || "text-as-primary text-2xl font-bold", children: dstAmount || "0" }), (0, jsx_runtime_1.jsxs)("div", { className: classes?.tokenSelector ||
17
+ // Fixed destination TOKEN for buy mode and deposit mode. The AMOUNT differs: buy mode
18
+ // always derives the receive amount from the source pay-in (read-only here), but deposit
19
+ // mode is the flow where the user states how much to receive directly — so when it's not
20
+ // locked (no destinationTokenAmount from the caller), render an editable amount input
21
+ // while keeping the token side fixed/non-interactive (hideTokenSelect).
22
+ (0, jsx_runtime_1.jsxs)("div", { className: classes?.inputContainer || "flex items-center justify-between", children: [isDepositMode && !disableAmountInput ? ((0, jsx_runtime_1.jsx)(OrderTokenAmount_1.OrderTokenAmount, { address: effectiveRecipientAddress, context: "to", inputValue: dstAmount, onChangeInput: onChangeDstAmount || (() => { }), chainId: selectedDstChainId || dstToken.chainId, setChainId: () => { }, token: dstToken, setToken: () => { }, hideTokenSelect: true, className: "w-auto flex-1 gap-0 rounded-none border-0 p-0", innerClassName: "gap-0", amountClassName: classes?.input || "text-as-primary text-2xl font-bold" })) : ((0, jsx_runtime_1.jsx)("div", { className: classes?.input || "text-as-primary text-2xl font-bold", children: dstAmount || "0" })), (0, jsx_runtime_1.jsxs)("div", { className: classes?.tokenSelector ||
19
23
  "bg-as-brand/10 border-as-brand/30 flex items-center gap-3 rounded-xl border px-4 py-3", children: [(dstTokenLogoURI || dstToken.metadata?.logoURI) && ((0, jsx_runtime_1.jsxs)("div", { className: "relative", children: [(0, jsx_runtime_1.jsx)("img", { src: dstTokenLogoURI || dstToken.metadata?.logoURI, alt: dstTokenSymbol || dstToken.symbol, className: classes?.tokenIcon || "h-8 w-8 rounded-full" }), anyspend_1.ALL_CHAINS[dstToken.chainId]?.logoUrl && ((0, jsx_runtime_1.jsx)("img", { src: anyspend_1.ALL_CHAINS[dstToken.chainId].logoUrl, alt: "Chain", className: classes?.chainBadge || "absolute -bottom-1 -right-1 h-4 w-4 rounded-full border border-white" }))] })), (0, jsx_runtime_1.jsx)("span", { className: classes?.tokenSymbol || "text-as-brand text-lg font-bold", children: dstTokenSymbol || dstToken.symbol })] })] })) : (
20
24
  // Token selection for regular swap mode
21
25
  (0, jsx_runtime_1.jsx)("div", { className: classes?.inputContainer, children: (0, jsx_runtime_1.jsx)(OrderTokenAmount_1.OrderTokenAmount, { address: effectiveRecipientAddress, context: "to", inputValue: dstAmount, onChangeInput: onChangeDstAmount || (() => { }), chainId: selectedDstChainId || dstToken.chainId, setChainId: setSelectedDstChainId || (() => { }), token: dstToken, setToken: setSelectedDstToken || (() => { }), disabled: disableAmountInput }) })), (0, jsx_runtime_1.jsxs)("div", { className: classes?.feeRow || "text-as-primary/50 flex h-5 items-center justify-start gap-2 text-sm", children: [(0, jsx_runtime_1.jsxs)("div", { className: "flex items-center gap-2", children: [(0, number_1.formatDisplayNumber)(anyspendQuote?.data?.currencyOut?.amountUsd, {
@@ -64,20 +64,27 @@ const BetterAuthProvider = ({ partnerId }) => {
64
64
  hasAttemptedRestore.current = true;
65
65
  const restoreSession = async () => {
66
66
  debug("Attempting session restore");
67
+ // A _ba_token in the URL OUTRANKS a cached Feathers JWT: the user just
68
+ // arrived through a live sign-in link (magic link / OAuth callback), and
69
+ // restoring a previous identity first would strand them as the wrong
70
+ // user with the token silently dropped.
71
+ const incomingBaToken = new URLSearchParams(window.location.search).get("_ba_token");
67
72
  // 1. Try existing Feathers JWT first (fastest — no network call to Better Auth)
68
- try {
69
- const response = await app_1.default.reAuthenticate();
70
- if (response?.user) {
71
- debug("Feathers JWT restored", { userId: response.user.userId });
72
- setUser(response.user);
73
- setIsAuthenticated(true);
74
- setIsConnected(true);
75
- setIsAuthenticating(false);
76
- return;
73
+ if (!incomingBaToken) {
74
+ try {
75
+ const response = await app_1.default.reAuthenticate();
76
+ if (response?.user) {
77
+ debug("Feathers JWT restored", { userId: response.user.userId });
78
+ setUser(response.user);
79
+ setIsAuthenticated(true);
80
+ setIsConnected(true);
81
+ setIsAuthenticating(false);
82
+ return;
83
+ }
84
+ }
85
+ catch {
86
+ debug("No existing Feathers JWT");
77
87
  }
78
- }
79
- catch {
80
- debug("No existing Feathers JWT");
81
88
  }
82
89
  // 2. Check for _ba_token in URL (OAuth callback with third-party cookie bypass).
83
90
  // When the API and frontend are on different domains, browsers with third-party
@@ -117,6 +124,23 @@ const BetterAuthProvider = ({ partnerId }) => {
117
124
  }
118
125
  catch (err) {
119
126
  debug("_ba_token exchange failed", err);
127
+ // A dead injected token (expired link param, revoked session) must
128
+ // not strand a user who HAS a valid cached session — fall back to
129
+ // the reAuthenticate lane the token's presence skipped.
130
+ try {
131
+ const cached = await app_1.default.reAuthenticate();
132
+ if (cached?.user) {
133
+ debug("Fell back to cached Feathers JWT after _ba_token failure", { userId: cached.user.userId });
134
+ setUser(cached.user);
135
+ setIsAuthenticated(true);
136
+ setIsConnected(true);
137
+ setIsAuthenticating(false);
138
+ return;
139
+ }
140
+ }
141
+ catch {
142
+ debug("No cached session to fall back to");
143
+ }
120
144
  }
121
145
  }
122
146
  // 3. Check for a Better Auth session via cookie (works when API and frontend
@@ -64,6 +64,13 @@ export declare function AnySpend(props: {
64
64
  allowDirectTransfer?: boolean;
65
65
  /** Fixed destination token amount (in wei/smallest unit). When provided, user cannot change the amount. */
66
66
  destinationTokenAmount?: string;
67
+ /**
68
+ * Editable default destination token amount (in wei/smallest unit). Seeds the same
69
+ * destination-amount input as `destinationTokenAmount`, but leaves it user-editable. Ignored
70
+ * when `destinationTokenAmount` is also set — that prop locks the field and wins.
71
+ * Seeds the field once on first apply; a later change to this value does not re-fill it, so it never overwrites what the user typed.
72
+ */
73
+ defaultDestinationTokenAmount?: string;
67
74
  /** Opaque metadata passed to the order for callbacks (e.g., workflow form data) */
68
75
  callbackMetadata?: Record<string, unknown>;
69
76
  /** Optional sender (payer) address — pre-fills token balances when the user address is known ahead of time */
@@ -78,4 +85,16 @@ export declare function AnySpend(props: {
78
85
  kycEnabled?: boolean;
79
86
  /** Whether to show the "Pay with Fiat" tab. Defaults to true. When false, the fiat tab is hidden and activeTab is forced to "crypto". */
80
87
  showFiatOption?: boolean;
88
+ /**
89
+ * Internal: true when `AnySpendDeposit` has an amount to prefill (`defaultDestinationTokenAmount`
90
+ * set), as opposed to any other `AnySpend`/`AnySpendDeposit` usage. Only changes
91
+ * `CryptoReceiveSection`'s fixed-destination branch: it lets the receive amount become an
92
+ * editable input (still gated by `disableAmountInput`) instead of a read-only display. Scoped
93
+ * this narrowly on purpose — `AnySpendDeposit` backs 15+ funding modals that are source-exact-in
94
+ * (the user sets the "pay with" amount; the receive side is an intended computed display), so
95
+ * this must stay off for all of them and only turn on for the one page that actually prefills a
96
+ * receive amount. Not intended for external callers — `AnySpendDeposit` derives this itself.
97
+ * @internal
98
+ */
99
+ isDepositMode?: boolean;
81
100
  }): import("react/jsx-runtime").JSX.Element;
@@ -62,7 +62,7 @@ export function AnySpend(props) {
62
62
  const fingerprintConfig = getFingerprintConfig();
63
63
  return (_jsx(AnySpendFingerprintWrapper, { fingerprint: fingerprintConfig, children: _jsx(AnySpendCustomizationProvider, { slots: props.slots, content: props.content, theme: props.theme, children: _jsx(AnySpendInner, { ...props }) }) }));
64
64
  }
65
- function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken = true, 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, showFiatOption = true, }) {
65
+ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken = true, mode = "modal", defaultActiveTab = "crypto", loadOrder, hideTransactionHistoryButton, recipientAddress: recipientAddressFromProps, onTokenSelect, onSuccess, customUsdInputValues, hideHeader, hideBottomNavigation = false, disableUrlParamManagement = false, returnToHomeUrl, customRecipientLabel, returnHomeLabel, classes, allowDirectTransfer = false, destinationTokenAmount, defaultDestinationTokenAmount, callbackMetadata, senderAddress, kycEnabled = false, showFiatOption = true, isDepositMode = false, }) {
66
66
  const { slots, content } = useAnySpendCustomization();
67
67
  const searchParams = useSearchParamsSSR();
68
68
  const router = useRouter();
@@ -230,6 +230,21 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
230
230
  sessionStorage.setItem("anyspend_dst_token", JSON.stringify(selectedDstToken));
231
231
  }, [selectedDstToken, isBuyMode]);
232
232
  const [isSrcInputDirty, setIsSrcInputDirty] = useState(true);
233
+ // Synchronous mirror of `isSrcInputDirty`. `setIsSrcInputDirty` is async relative to OTHER
234
+ // effects in the same commit — the quote effect (declared later, same commit) would otherwise
235
+ // read a pre-flip closure value and clear a just-seeded receive amount before it ever paints.
236
+ // Every flip goes through `markSrcDirty`, which writes this ref in the same statement, so
237
+ // same-commit siblings always see the latest intent. Deliberately NOT kept in sync by its own
238
+ // effect: an effect reading `isSrcInputDirty` from its closure would itself go stale across
239
+ // React 18 StrictMode's dev-mode double-invoke of mount effects (same closure re-run twice)
240
+ // and re-stomp this ref back to the pre-flip value between the two passes.
241
+ const isSrcInputDirtyRef = useRef(isSrcInputDirty);
242
+ // Single writer for the direction flag: keeps state and its synchronous ref mirror in lockstep
243
+ // so no call site — parent or child (CryptoPaySection) — can flip one without the other.
244
+ const markSrcDirty = useCallback((next) => {
245
+ setIsSrcInputDirty(next);
246
+ isSrcInputDirtyRef.current = next;
247
+ }, []);
233
248
  // Add refs to track if we've applied metadata
234
249
  const appliedSrcMetadataRef = useRef(false);
235
250
  const appliedDstMetadataRef = useRef(false);
@@ -282,17 +297,24 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
282
297
  useEffect(() => {
283
298
  appliedDstMetadataRef.current = false;
284
299
  }, [selectedDstToken.address, selectedDstToken.chainId]);
285
- // Prefill destination amount if provided (for fixed amount mode)
300
+ // Prefill destination amount if provided fixed (locking) mode wins over the editable default,
301
+ // since `destinationTokenAmount` also disables the input (see `disableAmountInput` below).
286
302
  const appliedDestinationAmount = useRef(false);
303
+ const seedDestinationAmount = destinationTokenAmount ?? defaultDestinationTokenAmount;
287
304
  useEffect(() => {
288
305
  // Only apply when we have real metadata (not default decimals)
289
- if (destinationTokenAmount && dstTokenMetadata?.decimals && !appliedDestinationAmount.current) {
290
- appliedDestinationAmount.current = true;
291
- const formattedAmount = formatUnits(BigInt(destinationTokenAmount), dstTokenMetadata.decimals);
292
- setDstAmount(formattedAmount);
293
- setIsSrcInputDirty(false); // Switch to EXACT_OUTPUT mode
306
+ if (seedDestinationAmount && dstTokenMetadata?.decimals && !appliedDestinationAmount.current) {
307
+ try {
308
+ const formattedAmount = formatUnits(BigInt(seedDestinationAmount), dstTokenMetadata.decimals);
309
+ appliedDestinationAmount.current = true;
310
+ setDstAmount(formattedAmount);
311
+ markSrcDirty(false); // Switch to EXACT_OUTPUT mode
312
+ }
313
+ catch {
314
+ // invalid seed value — no-op rather than throw into the effect (fail-closed)
315
+ }
294
316
  }
295
- }, [destinationTokenAmount, dstTokenMetadata]);
317
+ }, [seedDestinationAmount, dstTokenMetadata, markSrcDirty]);
296
318
  // Load swap configuration from URL on initial render
297
319
  useEffect(() => {
298
320
  // Skip if we've already processed the URL, if we have an order to load, or if URL param management is disabled
@@ -315,7 +337,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
315
337
  }
316
338
  else {
317
339
  setSrcAmount(fromAmountParam);
318
- setIsSrcInputDirty(true);
340
+ markSrcDirty(true);
319
341
  }
320
342
  }
321
343
  }
@@ -329,7 +351,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
329
351
  }
330
352
  // Mark that we've processed the initial URL
331
353
  initialUrlProcessed.current = true;
332
- }, [searchParams, loadOrder, disableUrlParamManagement]);
354
+ }, [searchParams, loadOrder, disableUrlParamManagement, markSrcDirty]);
333
355
  // Update URL when swap configuration changes - but not on initial load
334
356
  const updateSwapParamsInURL = useCallback(() => {
335
357
  // Skip if:
@@ -551,12 +573,20 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
551
573
  }, [globalAddress, effectiveRecipientAddress, customRecipients.length, setSelectedRecipientAddress]);
552
574
  // Update dependent amount when relay price changes
553
575
  useEffect(() => {
576
+ // Read the ref (not the closed-over `isSrcInputDirty` param) for the branch decision: the
577
+ // SEED effect (declared above, same commit when it fires) writes to the ref synchronously
578
+ // when it flips this to exact-out mode, so this effect sees that flip immediately even
579
+ // though `setIsSrcInputDirty`'s state update won't be visible in THIS render's closure until
580
+ // the next commit. Using the stale closure value here was the actual bug — it caused this
581
+ // effect to still treat a freshly-seeded exact-out amount as exact-in-with-no-data and clear
582
+ // it before it ever painted. The dependency array below stays on the real state, so this
583
+ // effect still re-runs correctly on every genuine transition.
554
584
  if (anyspendQuote?.data &&
555
585
  anyspendQuote.data.currencyIn?.amount &&
556
586
  anyspendQuote.data.currencyIn?.currency?.decimals &&
557
587
  anyspendQuote.data.currencyOut?.amount &&
558
588
  anyspendQuote.data.currencyOut?.currency?.decimals) {
559
- if (isSrcInputDirty) {
589
+ if (isSrcInputDirtyRef.current) {
560
590
  // The user is driving the SOURCE (exact-in): reflect the quote's OUTPUT even
561
591
  // when a fixed destinationTokenAmount pre-filled the receive. That lock only
562
592
  // governs the INITIAL exact-out state (untouched source → compute the pay for
@@ -574,7 +604,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
574
604
  }
575
605
  }
576
606
  else {
577
- if (isSrcInputDirty) {
607
+ if (isSrcInputDirtyRef.current) {
578
608
  setDstAmount("");
579
609
  }
580
610
  else {
@@ -1060,7 +1090,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
1060
1090
  // Reset payment methods when switching tabs
1061
1091
  resetPaymentMethods();
1062
1092
  setSelectedFiatPaymentMethod(FiatPaymentMethod.NONE);
1063
- }, setSelectedCryptoPaymentMethod: setSelectedCryptoPaymentMethod, setSelectedFiatPaymentMethod: setSelectedFiatPaymentMethod })), _jsxs("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: [activeTab === "crypto" ? (_jsx(CryptoPaySection, { selectedSrcChainId: selectedSrcChainId, setSelectedSrcChainId: setSelectedSrcChainId, selectedSrcToken: selectedSrcToken, setSelectedSrcToken: setSelectedSrcToken, srcAmount: srcAmount, setSrcAmount: setSrcAmount, isSrcInputDirty: isSrcInputDirty, setIsSrcInputDirty: setIsSrcInputDirty, selectedCryptoPaymentMethod: effectiveCryptoPaymentMethod, onSelectCryptoPaymentMethod: () => navigateToPanel(PanelView.CRYPTO_PAYMENT_METHOD, "forward"), anyspendQuote: anyspendQuote, onTokenSelect: onTokenSelect, onShowFeeDetail: () => navigateToPanel(PanelView.FEE_DETAIL, "forward") })) : (_jsx(motion.div, { initial: { opacity: 0, y: 20, filter: "blur(10px)" }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, transition: { duration: 0.3, delay: 0, ease: "easeInOut" }, children: _jsx(PanelOnramp, { srcAmountOnRamp: srcAmountOnRamp, setSrcAmountOnRamp: setSrcAmountOnRamp, selectedPaymentMethod: selectedFiatPaymentMethod, setActivePanel: (panelIndex) => {
1093
+ }, setSelectedCryptoPaymentMethod: setSelectedCryptoPaymentMethod, setSelectedFiatPaymentMethod: setSelectedFiatPaymentMethod })), _jsxs("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: [activeTab === "crypto" ? (_jsx(CryptoPaySection, { selectedSrcChainId: selectedSrcChainId, setSelectedSrcChainId: setSelectedSrcChainId, selectedSrcToken: selectedSrcToken, setSelectedSrcToken: setSelectedSrcToken, srcAmount: srcAmount, setSrcAmount: setSrcAmount, isSrcInputDirty: isSrcInputDirty, setIsSrcInputDirty: markSrcDirty, selectedCryptoPaymentMethod: effectiveCryptoPaymentMethod, onSelectCryptoPaymentMethod: () => navigateToPanel(PanelView.CRYPTO_PAYMENT_METHOD, "forward"), anyspendQuote: anyspendQuote, onTokenSelect: onTokenSelect, onShowFeeDetail: () => navigateToPanel(PanelView.FEE_DETAIL, "forward") })) : (_jsx(motion.div, { initial: { opacity: 0, y: 20, filter: "blur(10px)" }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, transition: { duration: 0.3, delay: 0, ease: "easeInOut" }, children: _jsx(PanelOnramp, { srcAmountOnRamp: srcAmountOnRamp, setSrcAmountOnRamp: setSrcAmountOnRamp, selectedPaymentMethod: selectedFiatPaymentMethod, setActivePanel: (panelIndex) => {
1064
1094
  // Map panel index to navigation with direction
1065
1095
  const panelsWithForwardNav = [PanelView.FIAT_PAYMENT_METHOD, PanelView.RECIPIENT_SELECTION];
1066
1096
  if (panelsWithForwardNav.includes(panelIndex)) {
@@ -1087,8 +1117,8 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
1087
1117
  // Swap amounts
1088
1118
  setSrcAmount(dstAmount);
1089
1119
  setDstAmount(srcAmount);
1090
- }, children: _jsx("div", { className: "relative flex items-center justify-center transition-opacity", children: _jsx(ArrowDown, { className: "text-as-primary/50 h-5 w-5" }) }) }) })), activeTab === "crypto" && (_jsx(CryptoReceiveSection, { isDepositMode: false, isBuyMode: isBuyMode, effectiveRecipientAddress: effectiveRecipientAddress, recipientName: recipientName || undefined, customRecipientLabel: customRecipientLabel, onSelectRecipient: () => navigateToPanel(PanelView.RECIPIENT_SELECTION, "forward"), dstAmount: dstAmount, dstToken: selectedDstToken, selectedDstChainId: selectedDstChainId, setSelectedDstChainId: setSelectedDstChainId, setSelectedDstToken: setSelectedDstToken, isSrcInputDirty: isSrcInputDirty, onChangeDstAmount: value => {
1091
- setIsSrcInputDirty(false);
1120
+ }, children: _jsx("div", { className: "relative flex items-center justify-center transition-opacity", children: _jsx(ArrowDown, { className: "text-as-primary/50 h-5 w-5" }) }) }) })), activeTab === "crypto" && (_jsx(CryptoReceiveSection, { isDepositMode: isDepositMode, isBuyMode: isBuyMode, effectiveRecipientAddress: effectiveRecipientAddress, recipientName: recipientName || undefined, customRecipientLabel: customRecipientLabel, onSelectRecipient: () => navigateToPanel(PanelView.RECIPIENT_SELECTION, "forward"), dstAmount: dstAmount, dstToken: selectedDstToken, selectedDstChainId: selectedDstChainId, setSelectedDstChainId: setSelectedDstChainId, setSelectedDstToken: setSelectedDstToken, isSrcInputDirty: isSrcInputDirty, onChangeDstAmount: value => {
1121
+ markSrcDirty(false);
1092
1122
  setDstAmount(value);
1093
1123
  }, disableAmountInput: !!destinationTokenAmount, anyspendQuote: isDirectTransfer ? undefined : anyspendQuote, onShowPointsDetail: isDirectTransfer ? undefined : () => navigateToPanel(PanelView.POINTS_DETAIL, "forward"), onShowFeeDetail: isDirectTransfer ? undefined : () => navigateToPanel(PanelView.FEE_DETAIL, "forward") }))] }), gasPriceData && !isLoadingGas && activeTab === "crypto" && !isDirectTransfer && (_jsx(GasIndicator, { gasPrice: gasPriceData, className: classes?.gasIndicator || "mt-2 w-full" })), _jsxs(motion.div, { initial: { opacity: 0, y: 20, filter: "blur(10px)" }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, transition: { duration: 0.3, delay: 0.2, ease: "easeInOut" }, className: cn("mt-4 flex w-full max-w-[460px] flex-col gap-2"), children: [slots.actionButton ? (slots.actionButton({
1094
1124
  onClick: onMainButtonClick,
@@ -116,6 +116,14 @@ export interface AnySpendDepositProps {
116
116
  pureTransferOnly?: boolean;
117
117
  /** Fixed destination token amount (in wei/smallest unit). When provided, user cannot change the amount. */
118
118
  destinationTokenAmount?: string;
119
+ /**
120
+ * Editable default destination token amount (in wei/smallest unit). Seeds the same
121
+ * destination-amount input as `destinationTokenAmount`, but leaves it user-editable. Ignored
122
+ * when `destinationTokenAmount` is also set — that prop locks the field and wins. Applies to
123
+ * the standard deposit flow only; custom-function deposits ignore it.
124
+ * Seeds the field once on first apply; a later change to this value does not re-fill it, so it never overwrites what the user typed.
125
+ */
126
+ defaultDestinationTokenAmount?: string;
119
127
  /** Opaque metadata passed to the order for callbacks (e.g., workflow form data) */
120
128
  callbackMetadata?: Record<string, unknown>;
121
129
  /** Optional sender (payer) address — pre-fills token balances when the user address is known ahead of time */
@@ -162,4 +170,4 @@ export interface AnySpendDepositProps {
162
170
  * onSuccess={(amount) => console.log(`Deposited ${amount}`)}
163
171
  * />
164
172
  */
165
- export declare function AnySpendDeposit({ loadOrder, mode, recipientAddress, paymentType: initialPaymentType, sourceTokenAddress, sourceTokenChainId: initialSourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken, onSuccess, onOpenCustomModal, mainFooter, onTokenSelect, customUsdInputValues, preferEoa, minDestinationAmount, header, orderType, depositContractConfig, showChainSelection, showFiatOption, supportedChains, topChainsCount, onClose, returnToHomeUrl, customRecipientLabel, returnHomeLabel, isCustomDeposit, classes, allowDirectTransfer, pureTransferOnly, destinationTokenAmount, callbackMetadata, senderAddress, slots, content, theme, }: AnySpendDepositProps): import("react/jsx-runtime").JSX.Element | null;
173
+ export declare function AnySpendDeposit({ loadOrder, mode, recipientAddress, paymentType: initialPaymentType, sourceTokenAddress, sourceTokenChainId: initialSourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken, onSuccess, onOpenCustomModal, mainFooter, onTokenSelect, customUsdInputValues, preferEoa, minDestinationAmount, header, orderType, depositContractConfig, showChainSelection, showFiatOption, supportedChains, topChainsCount, onClose, returnToHomeUrl, customRecipientLabel, returnHomeLabel, isCustomDeposit, classes, allowDirectTransfer, pureTransferOnly, destinationTokenAmount, defaultDestinationTokenAmount, callbackMetadata, senderAddress, slots, content, theme, }: AnySpendDepositProps): import("react/jsx-runtime").JSX.Element | null;
@@ -107,7 +107,7 @@ function ChainIcon({ chainId, className }) {
107
107
  * onSuccess={(amount) => console.log(`Deposited ${amount}`)}
108
108
  * />
109
109
  */
110
- export function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, paymentType: initialPaymentType, sourceTokenAddress, sourceTokenChainId: initialSourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken = true, onSuccess, onOpenCustomModal, mainFooter, onTokenSelect, customUsdInputValues, preferEoa, minDestinationAmount, header, orderType, depositContractConfig, showChainSelection, showFiatOption = true, supportedChains = DEFAULT_SUPPORTED_CHAINS, topChainsCount = 3, onClose, returnToHomeUrl, customRecipientLabel, returnHomeLabel, isCustomDeposit = false, classes, allowDirectTransfer = false, pureTransferOnly = false, destinationTokenAmount, callbackMetadata, senderAddress, slots, content, theme, }) {
110
+ export function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, paymentType: initialPaymentType, sourceTokenAddress, sourceTokenChainId: initialSourceChainId, destinationTokenAddress, destinationTokenChainId, lockDestinationToken = true, onSuccess, onOpenCustomModal, mainFooter, onTokenSelect, customUsdInputValues, preferEoa, minDestinationAmount, header, orderType, depositContractConfig, showChainSelection, showFiatOption = true, supportedChains = DEFAULT_SUPPORTED_CHAINS, topChainsCount = 3, onClose, returnToHomeUrl, customRecipientLabel, returnHomeLabel, isCustomDeposit = false, classes, allowDirectTransfer = false, pureTransferOnly = false, destinationTokenAmount, defaultDestinationTokenAmount, callbackMetadata, senderAddress, slots, content, theme, }) {
111
111
  // Extract deposit-specific classes for convenience
112
112
  const depositClasses = classes?.deposit;
113
113
  const { connectedEOAWallet } = useAccountWallet();
@@ -266,5 +266,9 @@ export function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, p
266
266
  // Deposit view
267
267
  return (_jsxs("div", { className: depositClasses?.form || "anyspend-deposit anyspend-deposit-form relative", children: [shouldShowChainSelection && (_jsxs("button", { onClick: handleBack, className: depositClasses?.backButton ||
268
268
  "anyspend-deposit-back-button text-as-secondary hover:text-as-primary absolute left-4 top-4 z-10 flex items-center gap-1", children: [_jsx("svg", { className: depositClasses?.backIcon || "anyspend-deposit-back-icon h-5 w-5", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M15 19l-7-7 7-7" }) }), _jsx("span", { className: depositClasses?.backText || "anyspend-deposit-back-text text-sm", children: "Back" })] })), onClose && (_jsx("button", { onClick: onClose, className: depositClasses?.closeButton ||
269
- "anyspend-deposit-close-button text-as-secondary hover:text-as-primary absolute right-4 top-4 z-10", children: _jsx("svg", { className: "h-6 w-6", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })), _jsx("div", { className: depositClasses?.formContent || cn("anyspend-deposit-form-content", shouldShowChainSelection && "pt-8"), children: isCustomDeposit ? (_jsx(AnySpendCustomExactIn, { loadOrder: loadOrder, mode: mode, recipientAddress: recipientAddress, paymentType: paymentType, sourceTokenAddress: sourceTokenAddress, sourceTokenChainId: selectedChainId, destinationToken: destinationToken, destinationChainId: destinationTokenChainId, orderType: effectiveOrderType, minDestinationAmount: minDestinationAmount, header: header ?? defaultHeader, onSuccess: onSuccess, onOpenCustomModal: onOpenCustomModal, mainFooter: mainFooter, onTokenSelect: onTokenSelect, customUsdInputValues: customUsdInputValues, preferEoa: preferEoa, customExactInConfig: depositContractConfig, returnToHomeUrl: returnToHomeUrl, customRecipientLabel: customRecipientLabel, returnHomeLabel: returnHomeLabel, classes: classes?.customExactIn, allowDirectTransfer: allowDirectTransfer, destinationTokenAmount: destinationTokenAmount, callbackMetadata: callbackMetadata, senderAddress: senderAddress, showFiatOption: showFiatOption, slots: slots, content: content, theme: theme }, selectedChainId)) : (_jsx(AnySpend, { loadOrder: loadOrder, mode: mode, defaultActiveTab: paymentType, recipientAddress: recipientAddress, sourceChainId: selectedChainId, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, lockDestinationToken: lockDestinationToken, onSuccess: txHash => onSuccess?.(txHash ?? ""), onTokenSelect: onTokenSelect, customUsdInputValues: customUsdInputValues, hideHeader: true, hideBottomNavigation: true, disableUrlParamManagement: true, returnToHomeUrl: returnToHomeUrl, customRecipientLabel: customRecipientLabel, returnHomeLabel: returnHomeLabel, classes: classes?.anySpend, allowDirectTransfer: allowDirectTransfer, destinationTokenAmount: destinationTokenAmount, callbackMetadata: callbackMetadata, senderAddress: senderAddress, showFiatOption: showFiatOption, slots: slots, content: content, theme: theme }, selectedChainId)) }), _jsx(ChainWarningText, { chainId: destinationTokenChainId, classes: classes?.chainWarningText || { root: "px-4 pb-4" } })] }));
269
+ "anyspend-deposit-close-button text-as-secondary hover:text-as-primary absolute right-4 top-4 z-10", children: _jsx("svg", { className: "h-6 w-6", fill: "none", viewBox: "0 0 24 24", stroke: "currentColor", children: _jsx("path", { strokeLinecap: "round", strokeLinejoin: "round", strokeWidth: 2, d: "M6 18L18 6M6 6l12 12" }) }) })), _jsx("div", { className: depositClasses?.formContent || cn("anyspend-deposit-form-content", shouldShowChainSelection && "pt-8"), children: isCustomDeposit ? (_jsx(AnySpendCustomExactIn, { loadOrder: loadOrder, mode: mode, recipientAddress: recipientAddress, paymentType: paymentType, sourceTokenAddress: sourceTokenAddress, sourceTokenChainId: selectedChainId, destinationToken: destinationToken, destinationChainId: destinationTokenChainId, orderType: effectiveOrderType, minDestinationAmount: minDestinationAmount, header: header ?? defaultHeader, onSuccess: onSuccess, onOpenCustomModal: onOpenCustomModal, mainFooter: mainFooter, onTokenSelect: onTokenSelect, customUsdInputValues: customUsdInputValues, preferEoa: preferEoa, customExactInConfig: depositContractConfig, returnToHomeUrl: returnToHomeUrl, customRecipientLabel: customRecipientLabel, returnHomeLabel: returnHomeLabel, classes: classes?.customExactIn, allowDirectTransfer: allowDirectTransfer, destinationTokenAmount: destinationTokenAmount, callbackMetadata: callbackMetadata, senderAddress: senderAddress, showFiatOption: showFiatOption, slots: slots, content: content, theme: theme }, selectedChainId)) : (_jsx(AnySpend, { loadOrder: loadOrder, mode: mode, defaultActiveTab: paymentType, recipientAddress: recipientAddress, sourceChainId: selectedChainId, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, lockDestinationToken: lockDestinationToken,
270
+ // Scoped to ONLY the prefill case (not every AnySpendDeposit caller — 15+ other
271
+ // funding modals rely on the source-exact-in "computed display" behavior this would
272
+ // otherwise change): editable destination amount only when there's a value to seed.
273
+ isDepositMode: !!defaultDestinationTokenAmount, onSuccess: txHash => onSuccess?.(txHash ?? ""), onTokenSelect: onTokenSelect, customUsdInputValues: customUsdInputValues, hideHeader: true, hideBottomNavigation: true, disableUrlParamManagement: true, returnToHomeUrl: returnToHomeUrl, customRecipientLabel: customRecipientLabel, returnHomeLabel: returnHomeLabel, classes: classes?.anySpend, allowDirectTransfer: allowDirectTransfer, destinationTokenAmount: destinationTokenAmount, defaultDestinationTokenAmount: defaultDestinationTokenAmount, callbackMetadata: callbackMetadata, senderAddress: senderAddress, showFiatOption: showFiatOption, slots: slots, content: content, theme: theme }, selectedChainId)) }), _jsx(ChainWarningText, { chainId: destinationTokenChainId, classes: classes?.chainWarningText || { root: "px-4 pb-4" } })] }));
270
274
  }
@@ -11,8 +11,12 @@ export function CryptoReceiveSection({ isDepositMode = false, isBuyMode = false,
11
11
  return (_jsxs(motion.div, { initial: { opacity: 0, y: 20, filter: "blur(10px)" }, animate: { opacity: 1, y: 0, filter: "blur(0px)" }, transition: { duration: 0.3, delay: 0.1, ease: "easeInOut" }, className: classes?.container ||
12
12
  "receive-section bg-as-surface-secondary border-as-border-secondary relative flex w-full flex-col gap-2 rounded-2xl border p-4 sm:p-6", children: [_jsxs("div", { className: "flex w-full items-center justify-between", children: [_jsxs("div", { className: classes?.label || "text-as-primary/50 flex h-7 items-center gap-1.5 text-sm", children: [isDepositMode ? "Deposit" : "Receive", isSrcInputDirty && anyspendQuote?.data?.fee && onShowFeeDetail && (_jsx("button", { onClick: onShowFeeDetail, className: "text-as-primary/40 hover:text-as-primary/60 transition-colors", children: _jsx(Info, { className: "h-4 w-4" }) }))] }), effectiveRecipientAddress ? (_jsx("button", { className: classes?.recipientButton || "text-as-tertiary flex h-7 items-center gap-2 rounded-lg", onClick: onSelectRecipient, children: _jsxs(_Fragment, { children: [_jsx("span", { className: classes?.recipientValue || "text-as-tertiary flex items-center gap-1 text-sm", children: customRecipientLabel ||
13
13
  (recipientName ? formatUsername(recipientName) : shortenAddress(effectiveRecipientAddress || "")) }), _jsx(ChevronRight, { className: "h-4 w-4" })] }) })) : (_jsx("button", { className: classes?.recipientButton || "text-as-primary/70 flex items-center gap-1 rounded-lg", onClick: onSelectRecipient, children: _jsx("div", { className: "text-sm font-medium", children: "Select recipient" }) }))] }), isBuyMode || isDepositMode ? (
14
- // Fixed destination token display for buy mode and deposit mode
15
- _jsxs("div", { className: classes?.inputContainer || "flex items-center justify-between", children: [_jsx("div", { className: classes?.input || "text-as-primary text-2xl font-bold", children: dstAmount || "0" }), _jsxs("div", { className: classes?.tokenSelector ||
14
+ // Fixed destination TOKEN for buy mode and deposit mode. The AMOUNT differs: buy mode
15
+ // always derives the receive amount from the source pay-in (read-only here), but deposit
16
+ // mode is the flow where the user states how much to receive directly — so when it's not
17
+ // locked (no destinationTokenAmount from the caller), render an editable amount input
18
+ // while keeping the token side fixed/non-interactive (hideTokenSelect).
19
+ _jsxs("div", { className: classes?.inputContainer || "flex items-center justify-between", children: [isDepositMode && !disableAmountInput ? (_jsx(OrderTokenAmount, { address: effectiveRecipientAddress, context: "to", inputValue: dstAmount, onChangeInput: onChangeDstAmount || (() => { }), chainId: selectedDstChainId || dstToken.chainId, setChainId: () => { }, token: dstToken, setToken: () => { }, hideTokenSelect: true, className: "w-auto flex-1 gap-0 rounded-none border-0 p-0", innerClassName: "gap-0", amountClassName: classes?.input || "text-as-primary text-2xl font-bold" })) : (_jsx("div", { className: classes?.input || "text-as-primary text-2xl font-bold", children: dstAmount || "0" })), _jsxs("div", { className: classes?.tokenSelector ||
16
20
  "bg-as-brand/10 border-as-brand/30 flex items-center gap-3 rounded-xl border px-4 py-3", children: [(dstTokenLogoURI || dstToken.metadata?.logoURI) && (_jsxs("div", { className: "relative", children: [_jsx("img", { src: dstTokenLogoURI || dstToken.metadata?.logoURI, alt: dstTokenSymbol || dstToken.symbol, className: classes?.tokenIcon || "h-8 w-8 rounded-full" }), ALL_CHAINS[dstToken.chainId]?.logoUrl && (_jsx("img", { src: ALL_CHAINS[dstToken.chainId].logoUrl, alt: "Chain", className: classes?.chainBadge || "absolute -bottom-1 -right-1 h-4 w-4 rounded-full border border-white" }))] })), _jsx("span", { className: classes?.tokenSymbol || "text-as-brand text-lg font-bold", children: dstTokenSymbol || dstToken.symbol })] })] })) : (
17
21
  // Token selection for regular swap mode
18
22
  _jsx("div", { className: classes?.inputContainer, children: _jsx(OrderTokenAmount, { address: effectiveRecipientAddress, context: "to", inputValue: dstAmount, onChangeInput: onChangeDstAmount || (() => { }), chainId: selectedDstChainId || dstToken.chainId, setChainId: setSelectedDstChainId || (() => { }), token: dstToken, setToken: setSelectedDstToken || (() => { }), disabled: disableAmountInput }) })), _jsxs("div", { className: classes?.feeRow || "text-as-primary/50 flex h-5 items-center justify-start gap-2 text-sm", children: [_jsxs("div", { className: "flex items-center gap-2", children: [formatDisplayNumber(anyspendQuote?.data?.currencyOut?.amountUsd, {