@b3dotfun/sdk 0.1.63 → 0.1.64

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 (31) hide show
  1. package/dist/cjs/anyspend/react/components/AnySpend.d.ts +2 -0
  2. package/dist/cjs/anyspend/react/components/AnySpend.js +25 -8
  3. package/dist/cjs/anyspend/react/components/AnySpendCustomExactIn.js +4 -5
  4. package/dist/cjs/anyspend/react/components/AnySpendDeposit.js +1 -1
  5. package/dist/cjs/anyspend/react/components/common/CryptoPaySection.d.ts +3 -1
  6. package/dist/cjs/anyspend/react/components/common/CryptoPaySection.js +2 -2
  7. package/dist/cjs/anyspend/react/components/common/OrderDetails.js +19 -0
  8. package/dist/cjs/anyspend/react/components/common/OrderTokenAmount.d.ts +3 -1
  9. package/dist/cjs/anyspend/react/components/common/OrderTokenAmount.js +16 -2
  10. package/dist/cjs/anyspend/react/components/common/PaymentStripeWeb2.js +2 -2
  11. package/dist/esm/anyspend/react/components/AnySpend.d.ts +2 -0
  12. package/dist/esm/anyspend/react/components/AnySpend.js +26 -9
  13. package/dist/esm/anyspend/react/components/AnySpendCustomExactIn.js +4 -5
  14. package/dist/esm/anyspend/react/components/AnySpendDeposit.js +1 -1
  15. package/dist/esm/anyspend/react/components/common/CryptoPaySection.d.ts +3 -1
  16. package/dist/esm/anyspend/react/components/common/CryptoPaySection.js +2 -2
  17. package/dist/esm/anyspend/react/components/common/OrderDetails.js +19 -0
  18. package/dist/esm/anyspend/react/components/common/OrderTokenAmount.d.ts +3 -1
  19. package/dist/esm/anyspend/react/components/common/OrderTokenAmount.js +16 -2
  20. package/dist/esm/anyspend/react/components/common/PaymentStripeWeb2.js +2 -2
  21. package/dist/types/anyspend/react/components/AnySpend.d.ts +2 -0
  22. package/dist/types/anyspend/react/components/common/CryptoPaySection.d.ts +3 -1
  23. package/dist/types/anyspend/react/components/common/OrderTokenAmount.d.ts +3 -1
  24. package/package.json +1 -1
  25. package/src/anyspend/react/components/AnySpend.tsx +30 -6
  26. package/src/anyspend/react/components/AnySpendCustomExactIn.tsx +4 -4
  27. package/src/anyspend/react/components/AnySpendDeposit.tsx +1 -0
  28. package/src/anyspend/react/components/common/CryptoPaySection.tsx +4 -0
  29. package/src/anyspend/react/components/common/OrderDetails.tsx +19 -0
  30. package/src/anyspend/react/components/common/OrderTokenAmount.tsx +19 -1
  31. package/src/anyspend/react/components/common/PaymentStripeWeb2.tsx +2 -2
@@ -51,4 +51,6 @@ export declare function AnySpend(props: {
51
51
  classes?: AnySpendClasses;
52
52
  /** When true, allows direct transfer without swap if source and destination token/chain are the same */
53
53
  allowDirectTransfer?: boolean;
54
+ /** Fixed destination token amount (in wei/smallest unit). When provided, user cannot change the amount. */
55
+ destinationTokenAmount?: string;
54
56
  }): import("react/jsx-runtime").JSX.Element;
@@ -61,7 +61,7 @@ function AnySpend(props) {
61
61
  console.log("[mitch] AnySpend rendered with fingerprintConfig:", props, fingerprintConfig);
62
62
  return ((0, jsx_runtime_1.jsx)(AnySpendFingerprintWrapper_1.AnySpendFingerprintWrapper, { fingerprint: fingerprintConfig, children: (0, jsx_runtime_1.jsx)(AnySpendInner, { ...props }) }));
63
63
  }
64
- function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, mode = "modal", defaultActiveTab = "crypto", loadOrder, hideTransactionHistoryButton, recipientAddress: recipientAddressFromProps, onTokenSelect, onSuccess, customUsdInputValues, hideHeader, hideBottomNavigation = false, disableUrlParamManagement = false, returnToHomeUrl, customRecipientLabel, returnHomeLabel, classes, allowDirectTransfer = false, }) {
64
+ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, mode = "modal", defaultActiveTab = "crypto", loadOrder, hideTransactionHistoryButton, recipientAddress: recipientAddressFromProps, onTokenSelect, onSuccess, customUsdInputValues, hideHeader, hideBottomNavigation = false, disableUrlParamManagement = false, returnToHomeUrl, customRecipientLabel, returnHomeLabel, classes, allowDirectTransfer = false, destinationTokenAmount, }) {
65
65
  const searchParams = (0, react_2.useSearchParamsSSR)();
66
66
  const router = (0, react_2.useRouter)();
67
67
  const { partnerId } = (0, react_2.useB3Config)();
@@ -197,6 +197,17 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
197
197
  (0, react_4.useEffect)(() => {
198
198
  appliedDstMetadataRef.current = false;
199
199
  }, [selectedDstToken.address, selectedDstToken.chainId]);
200
+ // Prefill destination amount if provided (for fixed amount mode)
201
+ const appliedDestinationAmount = (0, react_4.useRef)(false);
202
+ (0, react_4.useEffect)(() => {
203
+ // Only apply when we have real metadata (not default decimals)
204
+ if (destinationTokenAmount && dstTokenMetadata?.decimals && !appliedDestinationAmount.current) {
205
+ appliedDestinationAmount.current = true;
206
+ const formattedAmount = (0, viem_1.formatUnits)(BigInt(destinationTokenAmount), dstTokenMetadata.decimals);
207
+ setDstAmount(formattedAmount);
208
+ setIsSrcInputDirty(false); // Switch to EXACT_OUTPUT mode
209
+ }
210
+ }, [destinationTokenAmount, dstTokenMetadata]);
200
211
  // Load swap configuration from URL on initial render
201
212
  (0, react_4.useEffect)(() => {
202
213
  // Skip if we've already processed the URL, if we have an order to load, or if URL param management is disabled
@@ -457,9 +468,12 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
457
468
  anyspendQuote.data.currencyOut?.amount &&
458
469
  anyspendQuote.data.currencyOut?.currency?.decimals) {
459
470
  if (isSrcInputDirty) {
460
- const amount = anyspendQuote.data.currencyOut.amount;
461
- const decimals = anyspendQuote.data.currencyOut.currency.decimals;
462
- setDstAmount((0, number_1.formatTokenAmount)(BigInt(amount), decimals, 6, false));
471
+ // Don't override dstAmount if we have a fixed destinationTokenAmount
472
+ if (!destinationTokenAmount) {
473
+ const amount = anyspendQuote.data.currencyOut.amount;
474
+ const decimals = anyspendQuote.data.currencyOut.currency.decimals;
475
+ setDstAmount((0, number_1.formatTokenAmount)(BigInt(amount), decimals, 6, false));
476
+ }
463
477
  }
464
478
  else {
465
479
  const amount = anyspendQuote.data.currencyIn.amount;
@@ -469,13 +483,16 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
469
483
  }
470
484
  else {
471
485
  if (isSrcInputDirty) {
472
- setDstAmount("");
486
+ // Don't reset dstAmount if we have a fixed destinationTokenAmount
487
+ if (!destinationTokenAmount) {
488
+ setDstAmount("");
489
+ }
473
490
  }
474
491
  else {
475
492
  setSrcAmount("");
476
493
  }
477
494
  }
478
- }, [anyspendQuote, isSrcInputDirty]);
495
+ }, [anyspendQuote, isSrcInputDirty, destinationTokenAmount]);
479
496
  (0, react_4.useEffect)(() => {
480
497
  if (oat?.data?.order.status === "executed" && !onSuccessCalled.current) {
481
498
  console.log("Calling onSuccess");
@@ -874,7 +891,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
874
891
  // Reset payment methods when switching tabs
875
892
  resetPaymentMethods();
876
893
  setSelectedFiatPaymentMethod(FiatPaymentMethod_1.FiatPaymentMethod.NONE);
877
- }, 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) => {
894
+ }, 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"), skipAutoMaxOnTokenChange: !!destinationTokenAmount })) : ((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) => {
878
895
  // Map panel index to navigation with direction
879
896
  const panelsWithForwardNav = [PanelView.FIAT_PAYMENT_METHOD, PanelView.RECIPIENT_SELECTION];
880
897
  if (panelsWithForwardNav.includes(panelIndex)) {
@@ -904,7 +921,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
904
921
  }, 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 => {
905
922
  setIsSrcInputDirty(false);
906
923
  setDstAmount(value);
907
- }, 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: [(0, jsx_runtime_1.jsx)(react_2.ShinyButton, { accentColor: "hsl(var(--as-brand))", disabled: btnInfo.disable, onClick: onMainButtonClick, className: (btnInfo.error && classes?.mainButtonError) ||
924
+ }, 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: [(0, jsx_runtime_1.jsx)(react_2.ShinyButton, { accentColor: "hsl(var(--as-brand))", disabled: btnInfo.disable, onClick: onMainButtonClick, className: (btnInfo.error && classes?.mainButtonError) ||
908
925
  (btnInfo.disable && classes?.mainButtonDisabled) ||
909
926
  classes?.mainButton ||
910
927
  (0, cn_1.cn)("as-main-button relative w-full", btnInfo.error ? "!bg-as-red" : btnInfo.disable ? "!bg-as-on-surface-2" : "!bg-as-brand"), textClassName: (0, cn_1.cn)(btnInfo.error ? "text-white" : btnInfo.disable ? "text-as-secondary" : "text-white"), children: (0, jsx_runtime_1.jsxs)("div", { className: "flex items-center justify-center gap-2", children: [btnInfo.loading && (0, jsx_runtime_1.jsx)(lucide_react_1.Loader2, { className: "h-4 w-4 animate-spin" }), btnInfo.text] }) }), !hideTransactionHistoryButton && (globalAddress || effectiveRecipientAddress) ? ((0, jsx_runtime_1.jsxs)(react_2.Button, { variant: "link", onClick: onClickHistory, className: classes?.historyButton ||
@@ -83,14 +83,13 @@ function AnySpendCustomExactInInner({ loadOrder, mode = "modal", recipientAddres
83
83
  // Prefill destination amount if provided (for EXACT_OUTPUT mode)
84
84
  const appliedDestinationAmount = (0, react_4.useRef)(false);
85
85
  (0, react_4.useEffect)(() => {
86
- if (destinationTokenAmount && !appliedDestinationAmount.current) {
86
+ if (destinationTokenAmount && destinationToken?.decimals && !appliedDestinationAmount.current) {
87
87
  appliedDestinationAmount.current = true;
88
- // Convert wei to human-readable format
89
88
  const formattedAmount = (0, number_1.formatUnits)(destinationTokenAmount, destinationToken.decimals);
90
89
  setDstAmountInput(formattedAmount);
91
90
  setIsSrcInputDirty(false); // Switch to EXACT_OUTPUT mode
92
91
  }
93
- }, [destinationTokenAmount, destinationToken.decimals, setDstAmountInput, setIsSrcInputDirty]);
92
+ }, [destinationTokenAmount, destinationToken, setDstAmountInput, setIsSrcInputDirty]);
94
93
  const selectedRecipientOrDefault = selectedRecipientAddress ?? recipientAddress;
95
94
  const expectedDstAmountRaw = anyspendQuote?.data?.currencyOut?.amount ?? "0";
96
95
  const buildCustomPayload = (_recipient) => {
@@ -229,8 +228,8 @@ function AnySpendCustomExactInInner({ loadOrder, mode = "modal", recipientAddres
229
228
  };
230
229
  const headerContent = header ? (header({ anyspendPrice: anyspendQuote, isLoadingAnyspendPrice: isLoadingAnyspendQuote })) : ((0, jsx_runtime_1.jsx)("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: (0, jsx_runtime_1.jsxs)("div", { children: [(0, jsx_runtime_1.jsx)("h1", { className: "text-as-primary text-xl font-bold", children: actionLabel }), (0, jsx_runtime_1.jsx)("p", { className: "text-as-secondary text-sm", children: "Pay from any token to execute a custom exact-in transaction." })] }) }));
231
230
  const mainView = ((0, jsx_runtime_1.jsxs)("div", { className: classes?.container ||
232
- "anyspend-custom-exact-in-container mx-auto flex w-[460px] max-w-full flex-col items-center gap-2", children: [headerContent, (0, jsx_runtime_1.jsx)("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: (0, jsx_runtime_1.jsxs)("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: [paymentType === "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: () => setActivePanel(useAnyspendFlow_1.PanelView.CRYPTO_PAYMENT_METHOD), anyspendQuote: anyspendQuote, onTokenSelect: onTokenSelect })) : ((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: srcAmount, setSrcAmountOnRamp: setSrcAmount, selectedPaymentMethod: selectedFiatPaymentMethod, setActivePanel: setActivePanel, _recipientAddress: selectedRecipientOrDefault, destinationToken: selectedDstToken, destinationChainId: selectedDstChainId, dstTokenSymbol: DESTINATION_TOKEN_DETAILS.SYMBOL, hideDstToken: true, destinationAmount: dstAmount, onDestinationTokenChange: () => { }, onDestinationChainChange: () => { }, fiatPaymentMethodIndex: useAnyspendFlow_1.PanelView.FIAT_PAYMENT_METHOD, recipientSelectionPanelIndex: useAnyspendFlow_1.PanelView.RECIPIENT_SELECTION, anyspendQuote: anyspendQuote, onShowPointsDetail: () => setActivePanel(useAnyspendFlow_1.PanelView.POINTS_DETAIL), onShowFeeDetail: () => setActivePanel(useAnyspendFlow_1.PanelView.FEE_DETAIL), customUsdInputValues: customUsdInputValues, customRecipientLabel: customRecipientLabel }) })), (0, jsx_runtime_1.jsx)("div", { className: (0, cn_1.cn)("relative -my-1 flex h-0 items-center justify-center", paymentType === "fiat" && "hidden"), children: (0, jsx_runtime_1.jsx)(react_2.Button, { variant: "ghost", className: classes?.swapDirectionButton ||
233
- "swap-direction-button border-as-stroke bg-as-surface-primary z-10 h-10 w-10 cursor-default rounded-xl border-2 sm:h-8 sm:w-8 sm:rounded-xl", 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" }) }) }) }), paymentType === "crypto" && ((0, jsx_runtime_1.jsx)(CryptoReceiveSection_1.CryptoReceiveSection, { isDepositMode: false, isBuyMode: false, effectiveRecipientAddress: selectedRecipientOrDefault, recipientName: recipientName || undefined, customRecipientLabel: customRecipientLabel, onSelectRecipient: () => setActivePanel(useAnyspendFlow_1.PanelView.RECIPIENT_SELECTION), dstAmount: isSrcInputDirty ? dstAmount : dstAmountInput, dstToken: selectedDstToken, dstTokenSymbol: DESTINATION_TOKEN_DETAILS.SYMBOL, dstTokenLogoURI: DESTINATION_TOKEN_DETAILS.LOGO_URI, selectedDstChainId: selectedDstChainId, setSelectedDstChainId: () => { }, setSelectedDstToken: () => { }, isSrcInputDirty: isSrcInputDirty, onChangeDstAmount: value => {
231
+ "anyspend-custom-exact-in-container mx-auto flex w-[460px] max-w-full flex-col items-center gap-2", children: [headerContent, (0, jsx_runtime_1.jsx)("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: (0, jsx_runtime_1.jsxs)("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: [paymentType === "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: () => setActivePanel(useAnyspendFlow_1.PanelView.CRYPTO_PAYMENT_METHOD), anyspendQuote: anyspendQuote, onTokenSelect: onTokenSelect, skipAutoMaxOnTokenChange: !!destinationTokenAmount })) : ((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: srcAmount, setSrcAmountOnRamp: setSrcAmount, selectedPaymentMethod: selectedFiatPaymentMethod, setActivePanel: setActivePanel, _recipientAddress: selectedRecipientOrDefault, destinationToken: selectedDstToken, destinationChainId: selectedDstChainId, dstTokenSymbol: DESTINATION_TOKEN_DETAILS.SYMBOL, hideDstToken: true, destinationAmount: dstAmount, onDestinationTokenChange: () => { }, onDestinationChainChange: () => { }, fiatPaymentMethodIndex: useAnyspendFlow_1.PanelView.FIAT_PAYMENT_METHOD, recipientSelectionPanelIndex: useAnyspendFlow_1.PanelView.RECIPIENT_SELECTION, anyspendQuote: anyspendQuote, onShowPointsDetail: () => setActivePanel(useAnyspendFlow_1.PanelView.POINTS_DETAIL), onShowFeeDetail: () => setActivePanel(useAnyspendFlow_1.PanelView.FEE_DETAIL), customUsdInputValues: customUsdInputValues, customRecipientLabel: customRecipientLabel }) })), (0, jsx_runtime_1.jsx)("div", { className: (0, cn_1.cn)("relative -my-1 flex h-0 items-center justify-center", paymentType === "fiat" && "hidden"), children: (0, jsx_runtime_1.jsx)(react_2.Button, { variant: "ghost", className: classes?.swapDirectionButton ||
232
+ "swap-direction-button border-as-stroke bg-as-surface-primary z-10 h-10 w-10 cursor-default rounded-xl border-2 sm:h-8 sm:w-8 sm:rounded-xl", 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" }) }) }) }), paymentType === "crypto" && ((0, jsx_runtime_1.jsx)(CryptoReceiveSection_1.CryptoReceiveSection, { isDepositMode: false, isBuyMode: false, effectiveRecipientAddress: selectedRecipientOrDefault, recipientName: recipientName || undefined, customRecipientLabel: customRecipientLabel, onSelectRecipient: () => setActivePanel(useAnyspendFlow_1.PanelView.RECIPIENT_SELECTION), dstAmount: isSrcInputDirty && !destinationTokenAmount ? dstAmount : dstAmountInput, dstToken: selectedDstToken, dstTokenSymbol: DESTINATION_TOKEN_DETAILS.SYMBOL, dstTokenLogoURI: DESTINATION_TOKEN_DETAILS.LOGO_URI, selectedDstChainId: selectedDstChainId, setSelectedDstChainId: () => { }, setSelectedDstToken: () => { }, isSrcInputDirty: isSrcInputDirty, onChangeDstAmount: value => {
234
233
  setIsSrcInputDirty(false);
235
234
  setDstAmountInput(value);
236
235
  }, disableAmountInput: !!destinationTokenAmount, anyspendQuote: isDirectTransfer ? undefined : anyspendQuote, onShowPointsDetail: isDirectTransfer ? undefined : () => setActivePanel(useAnyspendFlow_1.PanelView.POINTS_DETAIL), onShowFeeDetail: isDirectTransfer ? undefined : () => setActivePanel(useAnyspendFlow_1.PanelView.FEE_DETAIL) }))] }) }), (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.2, ease: "easeInOut" }, className: (0, cn_1.cn)("mt-4 flex w-full max-w-[460px] flex-col gap-2"), children: (0, jsx_runtime_1.jsx)(react_2.ShinyButton, { accentColor: "hsl(var(--as-brand))", disabled: btnInfo.disable, onClick: onMainButtonClick, className: (btnInfo.error && classes?.mainButtonError) ||
@@ -209,5 +209,5 @@ function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, paymentT
209
209
  // Deposit view
210
210
  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 ||
211
211
  "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 ||
212
- "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 }, selectedChainId)) : ((0, jsx_runtime_1.jsx)(AnySpend_1.AnySpend, { loadOrder: loadOrder, mode: mode, defaultActiveTab: paymentType, recipientAddress: recipientAddress, sourceChainId: selectedChainId, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, 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 }, selectedChainId)) }), (0, jsx_runtime_1.jsx)(WarningText_1.ChainWarningText, { chainId: destinationTokenChainId, classes: classes?.chainWarningText || { root: "px-4 pb-4" } })] }));
212
+ "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 }, selectedChainId)) : ((0, jsx_runtime_1.jsx)(AnySpend_1.AnySpend, { loadOrder: loadOrder, mode: mode, defaultActiveTab: paymentType, recipientAddress: recipientAddress, sourceChainId: selectedChainId, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, 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 }, selectedChainId)) }), (0, jsx_runtime_1.jsx)(WarningText_1.ChainWarningText, { chainId: destinationTokenChainId, classes: classes?.chainWarningText || { root: "px-4 pb-4" } })] }));
213
213
  }
@@ -18,6 +18,8 @@ interface CryptoPaySectionProps {
18
18
  }) => void;
19
19
  onShowFeeDetail?: () => void;
20
20
  classes?: CryptoPaySectionClasses;
21
+ /** When true, skip auto-setting max balance when token changes (used for fixed destination amount mode) */
22
+ skipAutoMaxOnTokenChange?: boolean;
21
23
  }
22
- export declare function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, selectedSrcToken, setSelectedSrcToken, srcAmount, setSrcAmount, isSrcInputDirty, setIsSrcInputDirty, selectedCryptoPaymentMethod, onSelectCryptoPaymentMethod, anyspendQuote, onTokenSelect, onShowFeeDetail, classes, }: CryptoPaySectionProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, selectedSrcToken, setSelectedSrcToken, srcAmount, setSrcAmount, isSrcInputDirty, setIsSrcInputDirty, selectedCryptoPaymentMethod, onSelectCryptoPaymentMethod, anyspendQuote, onTokenSelect, onShowFeeDetail, classes, skipAutoMaxOnTokenChange, }: CryptoPaySectionProps): import("react/jsx-runtime").JSX.Element;
23
25
  export {};
@@ -11,7 +11,7 @@ const useConnectedWalletDisplay_1 = require("../../hooks/useConnectedWalletDispl
11
11
  const CryptoPaymentMethodDisplay_1 = require("./CryptoPaymentMethodDisplay");
12
12
  const OrderTokenAmount_1 = require("./OrderTokenAmount");
13
13
  const TokenBalance_1 = require("./TokenBalance");
14
- function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, selectedSrcToken, setSelectedSrcToken, srcAmount, setSrcAmount, isSrcInputDirty, setIsSrcInputDirty, selectedCryptoPaymentMethod, onSelectCryptoPaymentMethod, anyspendQuote, onTokenSelect, onShowFeeDetail, classes, }) {
14
+ function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, selectedSrcToken, setSelectedSrcToken, srcAmount, setSrcAmount, isSrcInputDirty, setIsSrcInputDirty, selectedCryptoPaymentMethod, onSelectCryptoPaymentMethod, anyspendQuote, onTokenSelect, onShowFeeDetail, classes, skipAutoMaxOnTokenChange = false, }) {
15
15
  const { data: srcTokenMetadata } = (0, react_1.useTokenData)(selectedSrcToken?.chainId, selectedSrcToken?.address);
16
16
  // Use custom hook to determine wallet address based on payment method
17
17
  const { walletAddress } = (0, useConnectedWalletDisplay_1.useConnectedWalletDisplay)(selectedCryptoPaymentMethod);
@@ -46,7 +46,7 @@ function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, selectedS
46
46
  "text-as-tertiarry flex h-7 items-center gap-2 text-sm transition-colors focus:!outline-none", onClick: onSelectCryptoPaymentMethod, children: (0, jsx_runtime_1.jsx)(CryptoPaymentMethodDisplay_1.CryptoPaymentMethodDisplay, { paymentMethod: selectedCryptoPaymentMethod, connectedAddress: walletAddress, connectedName: connectedName }) })] }), (0, jsx_runtime_1.jsx)("div", { className: classes?.inputContainer, children: (0, jsx_runtime_1.jsx)(OrderTokenAmount_1.OrderTokenAmount, { address: walletAddress, walletAddress: walletAddress, context: "from", inputValue: srcAmount, onChangeInput: value => {
47
47
  setIsSrcInputDirty(true);
48
48
  setSrcAmount(value);
49
- }, chainId: selectedSrcChainId, setChainId: setSelectedSrcChainId, token: selectedSrcToken, setToken: setSelectedSrcToken, onTokenSelect: onTokenSelect }) }), (0, jsx_runtime_1.jsxs)("div", { className: classes?.balanceRow || "flex items-center justify-between", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-as-primary/50 flex h-5 items-center text-sm", children: (0, number_1.formatDisplayNumber)(anyspendQuote?.data?.currencyIn?.amountUsd, {
49
+ }, chainId: selectedSrcChainId, setChainId: setSelectedSrcChainId, token: selectedSrcToken, setToken: setSelectedSrcToken, onTokenSelect: onTokenSelect, skipAutoMaxOnTokenChange: skipAutoMaxOnTokenChange }) }), (0, jsx_runtime_1.jsxs)("div", { className: classes?.balanceRow || "flex items-center justify-between", children: [(0, jsx_runtime_1.jsx)("div", { className: "text-as-primary/50 flex h-5 items-center text-sm", children: (0, number_1.formatDisplayNumber)(anyspendQuote?.data?.currencyIn?.amountUsd, {
50
50
  style: "currency",
51
51
  fallback: "",
52
52
  }) }), (0, jsx_runtime_1.jsx)(TokenBalance_1.TokenBalance, { token: selectedSrcToken, walletAddress: walletAddress, onChangeInput: value => {
@@ -351,6 +351,25 @@ exports.OrderDetails = (0, react_5.memo)(function OrderDetails({ mode = "modal",
351
351
  handlePayment();
352
352
  }
353
353
  }, [isPayableState, isComponentReady, handlePayment]);
354
+ // Auto-redirect to redirectUrl when order is executed (for onramp orders)
355
+ (0, react_5.useEffect)(() => {
356
+ if (order.status === "executed" && order.onrampMetadata?.redirectUrl) {
357
+ const baseUrl = order.onrampMetadata.redirectUrl;
358
+ try {
359
+ const url = new URL(baseUrl);
360
+ // Prevent Open Redirect vulnerabilities by ensuring the protocol is http or https
361
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
362
+ console.error(`Attempted redirect to a URL with an invalid protocol: ${url.protocol}`);
363
+ return;
364
+ }
365
+ const redirectUrl = `${baseUrl.replace(/\/$/, "")}/${order.id}`;
366
+ window.location.href = redirectUrl;
367
+ }
368
+ catch (error) {
369
+ console.error("Invalid redirect URL provided:", baseUrl, error);
370
+ }
371
+ }
372
+ }, [order.status, order.onrampMetadata?.redirectUrl, order.id]);
354
373
  if (!srcToken || !dstToken) {
355
374
  return (0, jsx_runtime_1.jsx)("div", { children: "Loading..." });
356
375
  }
@@ -1,5 +1,5 @@
1
1
  import { components } from "../../../../anyspend/types/api";
2
- export declare function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, address, chainId, setChainId, token, setToken, hideTokenSelect, canEditAmount, className, innerClassName, amountClassName, tokenSelectClassName, onTokenSelect, walletAddress, }: {
2
+ export declare function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, address, chainId, setChainId, token, setToken, hideTokenSelect, canEditAmount, className, innerClassName, amountClassName, tokenSelectClassName, onTokenSelect, walletAddress, skipAutoMaxOnTokenChange, }: {
3
3
  disabled?: boolean;
4
4
  inputValue: string;
5
5
  onChangeInput: (value: string) => void;
@@ -19,4 +19,6 @@ export declare function OrderTokenAmount({ disabled, inputValue, onChangeInput,
19
19
  preventDefault: () => void;
20
20
  }) => void;
21
21
  walletAddress?: string | undefined;
22
+ /** When true, skip auto-setting max balance when token changes (used for fixed destination amount mode) */
23
+ skipAutoMaxOnTokenChange?: boolean;
22
24
  }): import("react/jsx-runtime").JSX.Element;
@@ -14,7 +14,7 @@ const react_2 = require("../../../../global-account/react");
14
14
  const utils_1 = require("../../../../shared/utils");
15
15
  const relay_kit_ui_1 = require("@relayprotocol/relay-kit-ui");
16
16
  const ChainTokenIcon_1 = require("./ChainTokenIcon");
17
- function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, address, chainId, setChainId, token, setToken, hideTokenSelect = false, canEditAmount = true, className, innerClassName, amountClassName, tokenSelectClassName, onTokenSelect, walletAddress, }) {
17
+ function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, address, chainId, setChainId, token, setToken, hideTokenSelect = false, canEditAmount = true, className, innerClassName, amountClassName, tokenSelectClassName, onTokenSelect, walletAddress, skipAutoMaxOnTokenChange = false, }) {
18
18
  // Track previous token to detect changes
19
19
  const prevTokenRef = (0, react_1.useRef)(token.address);
20
20
  // Only get token balance when context is "from" (for setting max amount)
@@ -26,6 +26,11 @@ function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, addres
26
26
  // Only handle "from" context
27
27
  if (context !== "from")
28
28
  return;
29
+ // Skip auto-max when in fixed destination amount mode
30
+ if (skipAutoMaxOnTokenChange) {
31
+ prevTokenRef.current = token.address;
32
+ return;
33
+ }
29
34
  // Check if token changed or if this is the initial load with balance
30
35
  const isTokenChanged = prevTokenRef.current !== token.address;
31
36
  if (isTokenChanged && rawBalance) {
@@ -46,7 +51,16 @@ function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, addres
46
51
  // Update refs
47
52
  prevTokenRef.current = token.address;
48
53
  }
49
- }, [token.address, token.chainId, token.decimals, chainId, context, onChangeInput, rawBalance]);
54
+ }, [
55
+ token.address,
56
+ token.chainId,
57
+ token.decimals,
58
+ chainId,
59
+ context,
60
+ onChangeInput,
61
+ rawBalance,
62
+ skipAutoMaxOnTokenChange,
63
+ ]);
50
64
  const handleTokenSelect = (newToken) => {
51
65
  const token = {
52
66
  address: newToken.address,
@@ -95,9 +95,9 @@ function StripePaymentForm({ order, clientSecret, onPaymentSuccess, }) {
95
95
  }
96
96
  // At this point TypeScript knows result.paymentIntent exists and error is undefined
97
97
  console.log("@@stripe-web2-payment:success:", JSON.stringify({ orderId: order.id, paymentIntentId: result.paymentIntent.id }, null, 2));
98
- // Payment succeeded without redirect - handle success in the modal
98
+ // Payment succeeded
99
99
  setMessage(null);
100
- // Add waitingForDeposit=true to query params
100
+ // Stay on page and show waiting state (redirect will happen in OrderDetails when order is executed)
101
101
  const currentUrl = new URL(window.location.href);
102
102
  currentUrl.searchParams.set("waitingForDeposit", "true");
103
103
  window.history.replaceState(null, "", currentUrl.toString());
@@ -51,4 +51,6 @@ export declare function AnySpend(props: {
51
51
  classes?: AnySpendClasses;
52
52
  /** When true, allows direct transfer without swap if source and destination token/chain are the same */
53
53
  allowDirectTransfer?: boolean;
54
+ /** Fixed destination token amount (in wei/smallest unit). When provided, user cannot change the amount. */
55
+ destinationTokenAmount?: string;
54
56
  }): import("react/jsx-runtime").JSX.Element;
@@ -12,7 +12,7 @@ import invariant from "invariant";
12
12
  import { ArrowDown, CheckCircle, HistoryIcon, Loader2 } from "lucide-react";
13
13
  import { motion } from "motion/react";
14
14
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
15
- import { parseUnits } from "viem";
15
+ import { formatUnits, parseUnits } from "viem";
16
16
  import { base, mainnet } from "viem/chains";
17
17
  import { useAutoSelectCryptoPaymentMethod } from "../hooks/useAutoSelectCryptoPaymentMethod.js";
18
18
  import { useConnectedWalletDisplay } from "../hooks/useConnectedWalletDisplay.js";
@@ -54,7 +54,7 @@ export function AnySpend(props) {
54
54
  console.log("[mitch] AnySpend rendered with fingerprintConfig:", props, fingerprintConfig);
55
55
  return (_jsx(AnySpendFingerprintWrapper, { fingerprint: fingerprintConfig, children: _jsx(AnySpendInner, { ...props }) }));
56
56
  }
57
- function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, mode = "modal", defaultActiveTab = "crypto", loadOrder, hideTransactionHistoryButton, recipientAddress: recipientAddressFromProps, onTokenSelect, onSuccess, customUsdInputValues, hideHeader, hideBottomNavigation = false, disableUrlParamManagement = false, returnToHomeUrl, customRecipientLabel, returnHomeLabel, classes, allowDirectTransfer = false, }) {
57
+ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationTokenChainId, mode = "modal", defaultActiveTab = "crypto", loadOrder, hideTransactionHistoryButton, recipientAddress: recipientAddressFromProps, onTokenSelect, onSuccess, customUsdInputValues, hideHeader, hideBottomNavigation = false, disableUrlParamManagement = false, returnToHomeUrl, customRecipientLabel, returnHomeLabel, classes, allowDirectTransfer = false, destinationTokenAmount, }) {
58
58
  const searchParams = useSearchParamsSSR();
59
59
  const router = useRouter();
60
60
  const { partnerId } = useB3Config();
@@ -190,6 +190,17 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
190
190
  useEffect(() => {
191
191
  appliedDstMetadataRef.current = false;
192
192
  }, [selectedDstToken.address, selectedDstToken.chainId]);
193
+ // Prefill destination amount if provided (for fixed amount mode)
194
+ const appliedDestinationAmount = useRef(false);
195
+ useEffect(() => {
196
+ // Only apply when we have real metadata (not default decimals)
197
+ if (destinationTokenAmount && dstTokenMetadata?.decimals && !appliedDestinationAmount.current) {
198
+ appliedDestinationAmount.current = true;
199
+ const formattedAmount = formatUnits(BigInt(destinationTokenAmount), dstTokenMetadata.decimals);
200
+ setDstAmount(formattedAmount);
201
+ setIsSrcInputDirty(false); // Switch to EXACT_OUTPUT mode
202
+ }
203
+ }, [destinationTokenAmount, dstTokenMetadata]);
193
204
  // Load swap configuration from URL on initial render
194
205
  useEffect(() => {
195
206
  // Skip if we've already processed the URL, if we have an order to load, or if URL param management is disabled
@@ -450,9 +461,12 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
450
461
  anyspendQuote.data.currencyOut?.amount &&
451
462
  anyspendQuote.data.currencyOut?.currency?.decimals) {
452
463
  if (isSrcInputDirty) {
453
- const amount = anyspendQuote.data.currencyOut.amount;
454
- const decimals = anyspendQuote.data.currencyOut.currency.decimals;
455
- setDstAmount(formatTokenAmount(BigInt(amount), decimals, 6, false));
464
+ // Don't override dstAmount if we have a fixed destinationTokenAmount
465
+ if (!destinationTokenAmount) {
466
+ const amount = anyspendQuote.data.currencyOut.amount;
467
+ const decimals = anyspendQuote.data.currencyOut.currency.decimals;
468
+ setDstAmount(formatTokenAmount(BigInt(amount), decimals, 6, false));
469
+ }
456
470
  }
457
471
  else {
458
472
  const amount = anyspendQuote.data.currencyIn.amount;
@@ -462,13 +476,16 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
462
476
  }
463
477
  else {
464
478
  if (isSrcInputDirty) {
465
- setDstAmount("");
479
+ // Don't reset dstAmount if we have a fixed destinationTokenAmount
480
+ if (!destinationTokenAmount) {
481
+ setDstAmount("");
482
+ }
466
483
  }
467
484
  else {
468
485
  setSrcAmount("");
469
486
  }
470
487
  }
471
- }, [anyspendQuote, isSrcInputDirty]);
488
+ }, [anyspendQuote, isSrcInputDirty, destinationTokenAmount]);
472
489
  useEffect(() => {
473
490
  if (oat?.data?.order.status === "executed" && !onSuccessCalled.current) {
474
491
  console.log("Calling onSuccess");
@@ -867,7 +884,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
867
884
  // Reset payment methods when switching tabs
868
885
  resetPaymentMethods();
869
886
  setSelectedFiatPaymentMethod(FiatPaymentMethod.NONE);
870
- }, 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) => {
887
+ }, 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"), skipAutoMaxOnTokenChange: !!destinationTokenAmount })) : (_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) => {
871
888
  // Map panel index to navigation with direction
872
889
  const panelsWithForwardNav = [PanelView.FIAT_PAYMENT_METHOD, PanelView.RECIPIENT_SELECTION];
873
890
  if (panelsWithForwardNav.includes(panelIndex)) {
@@ -897,7 +914,7 @@ function AnySpendInner({ sourceChainId, destinationTokenAddress, destinationToke
897
914
  }, 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 => {
898
915
  setIsSrcInputDirty(false);
899
916
  setDstAmount(value);
900
- }, 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: [_jsx(ShinyButton, { accentColor: "hsl(var(--as-brand))", disabled: btnInfo.disable, onClick: onMainButtonClick, className: (btnInfo.error && classes?.mainButtonError) ||
917
+ }, 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: [_jsx(ShinyButton, { accentColor: "hsl(var(--as-brand))", disabled: btnInfo.disable, onClick: onMainButtonClick, className: (btnInfo.error && classes?.mainButtonError) ||
901
918
  (btnInfo.disable && classes?.mainButtonDisabled) ||
902
919
  classes?.mainButton ||
903
920
  cn("as-main-button relative w-full", btnInfo.error ? "!bg-as-red" : btnInfo.disable ? "!bg-as-on-surface-2" : "!bg-as-brand"), textClassName: cn(btnInfo.error ? "text-white" : btnInfo.disable ? "text-as-secondary" : "text-white"), children: _jsxs("div", { className: "flex items-center justify-center gap-2", children: [btnInfo.loading && _jsx(Loader2, { className: "h-4 w-4 animate-spin" }), btnInfo.text] }) }), !hideTransactionHistoryButton && (globalAddress || effectiveRecipientAddress) ? (_jsxs(Button, { variant: "link", onClick: onClickHistory, className: classes?.historyButton ||
@@ -77,14 +77,13 @@ function AnySpendCustomExactInInner({ loadOrder, mode = "modal", recipientAddres
77
77
  // Prefill destination amount if provided (for EXACT_OUTPUT mode)
78
78
  const appliedDestinationAmount = useRef(false);
79
79
  useEffect(() => {
80
- if (destinationTokenAmount && !appliedDestinationAmount.current) {
80
+ if (destinationTokenAmount && destinationToken?.decimals && !appliedDestinationAmount.current) {
81
81
  appliedDestinationAmount.current = true;
82
- // Convert wei to human-readable format
83
82
  const formattedAmount = formatUnits(destinationTokenAmount, destinationToken.decimals);
84
83
  setDstAmountInput(formattedAmount);
85
84
  setIsSrcInputDirty(false); // Switch to EXACT_OUTPUT mode
86
85
  }
87
- }, [destinationTokenAmount, destinationToken.decimals, setDstAmountInput, setIsSrcInputDirty]);
86
+ }, [destinationTokenAmount, destinationToken, setDstAmountInput, setIsSrcInputDirty]);
88
87
  const selectedRecipientOrDefault = selectedRecipientAddress ?? recipientAddress;
89
88
  const expectedDstAmountRaw = anyspendQuote?.data?.currencyOut?.amount ?? "0";
90
89
  const buildCustomPayload = (_recipient) => {
@@ -223,8 +222,8 @@ function AnySpendCustomExactInInner({ loadOrder, mode = "modal", recipientAddres
223
222
  };
224
223
  const headerContent = header ? (header({ anyspendPrice: anyspendQuote, isLoadingAnyspendPrice: isLoadingAnyspendQuote })) : (_jsx("div", { className: "mb-4 flex flex-col items-center gap-3 text-center", children: _jsxs("div", { children: [_jsx("h1", { className: "text-as-primary text-xl font-bold", children: actionLabel }), _jsx("p", { className: "text-as-secondary text-sm", children: "Pay from any token to execute a custom exact-in transaction." })] }) }));
225
224
  const mainView = (_jsxs("div", { className: classes?.container ||
226
- "anyspend-custom-exact-in-container mx-auto flex w-[460px] max-w-full flex-col items-center gap-2", children: [headerContent, _jsx("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: _jsxs("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: [paymentType === "crypto" ? (_jsx(CryptoPaySection, { selectedSrcChainId: selectedSrcChainId, setSelectedSrcChainId: setSelectedSrcChainId, selectedSrcToken: selectedSrcToken, setSelectedSrcToken: setSelectedSrcToken, srcAmount: srcAmount, setSrcAmount: setSrcAmount, isSrcInputDirty: isSrcInputDirty, setIsSrcInputDirty: setIsSrcInputDirty, selectedCryptoPaymentMethod: effectiveCryptoPaymentMethod, onSelectCryptoPaymentMethod: () => setActivePanel(PanelView.CRYPTO_PAYMENT_METHOD), anyspendQuote: anyspendQuote, onTokenSelect: onTokenSelect })) : (_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: srcAmount, setSrcAmountOnRamp: setSrcAmount, selectedPaymentMethod: selectedFiatPaymentMethod, setActivePanel: setActivePanel, _recipientAddress: selectedRecipientOrDefault, destinationToken: selectedDstToken, destinationChainId: selectedDstChainId, dstTokenSymbol: DESTINATION_TOKEN_DETAILS.SYMBOL, hideDstToken: true, destinationAmount: dstAmount, onDestinationTokenChange: () => { }, onDestinationChainChange: () => { }, fiatPaymentMethodIndex: PanelView.FIAT_PAYMENT_METHOD, recipientSelectionPanelIndex: PanelView.RECIPIENT_SELECTION, anyspendQuote: anyspendQuote, onShowPointsDetail: () => setActivePanel(PanelView.POINTS_DETAIL), onShowFeeDetail: () => setActivePanel(PanelView.FEE_DETAIL), customUsdInputValues: customUsdInputValues, customRecipientLabel: customRecipientLabel }) })), _jsx("div", { className: cn("relative -my-1 flex h-0 items-center justify-center", paymentType === "fiat" && "hidden"), children: _jsx(Button, { variant: "ghost", className: classes?.swapDirectionButton ||
227
- "swap-direction-button border-as-stroke bg-as-surface-primary z-10 h-10 w-10 cursor-default rounded-xl border-2 sm:h-8 sm:w-8 sm:rounded-xl", children: _jsx("div", { className: "relative flex items-center justify-center transition-opacity", children: _jsx(ArrowDown, { className: "text-as-primary/50 h-5 w-5" }) }) }) }), paymentType === "crypto" && (_jsx(CryptoReceiveSection, { isDepositMode: false, isBuyMode: false, effectiveRecipientAddress: selectedRecipientOrDefault, recipientName: recipientName || undefined, customRecipientLabel: customRecipientLabel, onSelectRecipient: () => setActivePanel(PanelView.RECIPIENT_SELECTION), dstAmount: isSrcInputDirty ? dstAmount : dstAmountInput, dstToken: selectedDstToken, dstTokenSymbol: DESTINATION_TOKEN_DETAILS.SYMBOL, dstTokenLogoURI: DESTINATION_TOKEN_DETAILS.LOGO_URI, selectedDstChainId: selectedDstChainId, setSelectedDstChainId: () => { }, setSelectedDstToken: () => { }, isSrcInputDirty: isSrcInputDirty, onChangeDstAmount: value => {
225
+ "anyspend-custom-exact-in-container mx-auto flex w-[460px] max-w-full flex-col items-center gap-2", children: [headerContent, _jsx("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: _jsxs("div", { className: "relative flex w-full max-w-[calc(100vw-32px)] flex-col gap-2", children: [paymentType === "crypto" ? (_jsx(CryptoPaySection, { selectedSrcChainId: selectedSrcChainId, setSelectedSrcChainId: setSelectedSrcChainId, selectedSrcToken: selectedSrcToken, setSelectedSrcToken: setSelectedSrcToken, srcAmount: srcAmount, setSrcAmount: setSrcAmount, isSrcInputDirty: isSrcInputDirty, setIsSrcInputDirty: setIsSrcInputDirty, selectedCryptoPaymentMethod: effectiveCryptoPaymentMethod, onSelectCryptoPaymentMethod: () => setActivePanel(PanelView.CRYPTO_PAYMENT_METHOD), anyspendQuote: anyspendQuote, onTokenSelect: onTokenSelect, skipAutoMaxOnTokenChange: !!destinationTokenAmount })) : (_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: srcAmount, setSrcAmountOnRamp: setSrcAmount, selectedPaymentMethod: selectedFiatPaymentMethod, setActivePanel: setActivePanel, _recipientAddress: selectedRecipientOrDefault, destinationToken: selectedDstToken, destinationChainId: selectedDstChainId, dstTokenSymbol: DESTINATION_TOKEN_DETAILS.SYMBOL, hideDstToken: true, destinationAmount: dstAmount, onDestinationTokenChange: () => { }, onDestinationChainChange: () => { }, fiatPaymentMethodIndex: PanelView.FIAT_PAYMENT_METHOD, recipientSelectionPanelIndex: PanelView.RECIPIENT_SELECTION, anyspendQuote: anyspendQuote, onShowPointsDetail: () => setActivePanel(PanelView.POINTS_DETAIL), onShowFeeDetail: () => setActivePanel(PanelView.FEE_DETAIL), customUsdInputValues: customUsdInputValues, customRecipientLabel: customRecipientLabel }) })), _jsx("div", { className: cn("relative -my-1 flex h-0 items-center justify-center", paymentType === "fiat" && "hidden"), children: _jsx(Button, { variant: "ghost", className: classes?.swapDirectionButton ||
226
+ "swap-direction-button border-as-stroke bg-as-surface-primary z-10 h-10 w-10 cursor-default rounded-xl border-2 sm:h-8 sm:w-8 sm:rounded-xl", children: _jsx("div", { className: "relative flex items-center justify-center transition-opacity", children: _jsx(ArrowDown, { className: "text-as-primary/50 h-5 w-5" }) }) }) }), paymentType === "crypto" && (_jsx(CryptoReceiveSection, { isDepositMode: false, isBuyMode: false, effectiveRecipientAddress: selectedRecipientOrDefault, recipientName: recipientName || undefined, customRecipientLabel: customRecipientLabel, onSelectRecipient: () => setActivePanel(PanelView.RECIPIENT_SELECTION), dstAmount: isSrcInputDirty && !destinationTokenAmount ? dstAmount : dstAmountInput, dstToken: selectedDstToken, dstTokenSymbol: DESTINATION_TOKEN_DETAILS.SYMBOL, dstTokenLogoURI: DESTINATION_TOKEN_DETAILS.LOGO_URI, selectedDstChainId: selectedDstChainId, setSelectedDstChainId: () => { }, setSelectedDstToken: () => { }, isSrcInputDirty: isSrcInputDirty, onChangeDstAmount: value => {
228
227
  setIsSrcInputDirty(false);
229
228
  setDstAmountInput(value);
230
229
  }, disableAmountInput: !!destinationTokenAmount, anyspendQuote: isDirectTransfer ? undefined : anyspendQuote, onShowPointsDetail: isDirectTransfer ? undefined : () => setActivePanel(PanelView.POINTS_DETAIL), onShowFeeDetail: isDirectTransfer ? undefined : () => setActivePanel(PanelView.FEE_DETAIL) }))] }) }), _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.2, ease: "easeInOut" }, className: cn("mt-4 flex w-full max-w-[460px] flex-col gap-2"), children: _jsx(ShinyButton, { accentColor: "hsl(var(--as-brand))", disabled: btnInfo.disable, onClick: onMainButtonClick, className: (btnInfo.error && classes?.mainButtonError) ||
@@ -206,5 +206,5 @@ export function AnySpendDeposit({ loadOrder, mode = "modal", recipientAddress, p
206
206
  // Deposit view
207
207
  return (_jsxs("div", { className: depositClasses?.form || "anyspend-deposit anyspend-deposit-form relative", children: [shouldShowChainSelection && (_jsxs("button", { onClick: handleBack, className: depositClasses?.backButton ||
208
208
  "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 ||
209
- "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 }, selectedChainId)) : (_jsx(AnySpend, { loadOrder: loadOrder, mode: mode, defaultActiveTab: paymentType, recipientAddress: recipientAddress, sourceChainId: selectedChainId, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, 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 }, selectedChainId)) }), _jsx(ChainWarningText, { chainId: destinationTokenChainId, classes: classes?.chainWarningText || { root: "px-4 pb-4" } })] }));
209
+ "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 }, selectedChainId)) : (_jsx(AnySpend, { loadOrder: loadOrder, mode: mode, defaultActiveTab: paymentType, recipientAddress: recipientAddress, sourceChainId: selectedChainId, destinationTokenAddress: destinationTokenAddress, destinationTokenChainId: destinationTokenChainId, 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 }, selectedChainId)) }), _jsx(ChainWarningText, { chainId: destinationTokenChainId, classes: classes?.chainWarningText || { root: "px-4 pb-4" } })] }));
210
210
  }
@@ -18,6 +18,8 @@ interface CryptoPaySectionProps {
18
18
  }) => void;
19
19
  onShowFeeDetail?: () => void;
20
20
  classes?: CryptoPaySectionClasses;
21
+ /** When true, skip auto-setting max balance when token changes (used for fixed destination amount mode) */
22
+ skipAutoMaxOnTokenChange?: boolean;
21
23
  }
22
- export declare function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, selectedSrcToken, setSelectedSrcToken, srcAmount, setSrcAmount, isSrcInputDirty, setIsSrcInputDirty, selectedCryptoPaymentMethod, onSelectCryptoPaymentMethod, anyspendQuote, onTokenSelect, onShowFeeDetail, classes, }: CryptoPaySectionProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, selectedSrcToken, setSelectedSrcToken, srcAmount, setSrcAmount, isSrcInputDirty, setIsSrcInputDirty, selectedCryptoPaymentMethod, onSelectCryptoPaymentMethod, anyspendQuote, onTokenSelect, onShowFeeDetail, classes, skipAutoMaxOnTokenChange, }: CryptoPaySectionProps): import("react/jsx-runtime").JSX.Element;
23
25
  export {};
@@ -8,7 +8,7 @@ import { useConnectedWalletDisplay } from "../../hooks/useConnectedWalletDisplay
8
8
  import { CryptoPaymentMethodDisplay } from "./CryptoPaymentMethodDisplay.js";
9
9
  import { OrderTokenAmount } from "./OrderTokenAmount.js";
10
10
  import { TokenBalance } from "./TokenBalance.js";
11
- export function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, selectedSrcToken, setSelectedSrcToken, srcAmount, setSrcAmount, isSrcInputDirty, setIsSrcInputDirty, selectedCryptoPaymentMethod, onSelectCryptoPaymentMethod, anyspendQuote, onTokenSelect, onShowFeeDetail, classes, }) {
11
+ export function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, selectedSrcToken, setSelectedSrcToken, srcAmount, setSrcAmount, isSrcInputDirty, setIsSrcInputDirty, selectedCryptoPaymentMethod, onSelectCryptoPaymentMethod, anyspendQuote, onTokenSelect, onShowFeeDetail, classes, skipAutoMaxOnTokenChange = false, }) {
12
12
  const { data: srcTokenMetadata } = useTokenData(selectedSrcToken?.chainId, selectedSrcToken?.address);
13
13
  // Use custom hook to determine wallet address based on payment method
14
14
  const { walletAddress } = useConnectedWalletDisplay(selectedCryptoPaymentMethod);
@@ -43,7 +43,7 @@ export function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, se
43
43
  "text-as-tertiarry flex h-7 items-center gap-2 text-sm transition-colors focus:!outline-none", onClick: onSelectCryptoPaymentMethod, children: _jsx(CryptoPaymentMethodDisplay, { paymentMethod: selectedCryptoPaymentMethod, connectedAddress: walletAddress, connectedName: connectedName }) })] }), _jsx("div", { className: classes?.inputContainer, children: _jsx(OrderTokenAmount, { address: walletAddress, walletAddress: walletAddress, context: "from", inputValue: srcAmount, onChangeInput: value => {
44
44
  setIsSrcInputDirty(true);
45
45
  setSrcAmount(value);
46
- }, chainId: selectedSrcChainId, setChainId: setSelectedSrcChainId, token: selectedSrcToken, setToken: setSelectedSrcToken, onTokenSelect: onTokenSelect }) }), _jsxs("div", { className: classes?.balanceRow || "flex items-center justify-between", children: [_jsx("div", { className: "text-as-primary/50 flex h-5 items-center text-sm", children: formatDisplayNumber(anyspendQuote?.data?.currencyIn?.amountUsd, {
46
+ }, chainId: selectedSrcChainId, setChainId: setSelectedSrcChainId, token: selectedSrcToken, setToken: setSelectedSrcToken, onTokenSelect: onTokenSelect, skipAutoMaxOnTokenChange: skipAutoMaxOnTokenChange }) }), _jsxs("div", { className: classes?.balanceRow || "flex items-center justify-between", children: [_jsx("div", { className: "text-as-primary/50 flex h-5 items-center text-sm", children: formatDisplayNumber(anyspendQuote?.data?.currencyIn?.amountUsd, {
47
47
  style: "currency",
48
48
  fallback: "",
49
49
  }) }), _jsx(TokenBalance, { token: selectedSrcToken, walletAddress: walletAddress, onChangeInput: value => {
@@ -345,6 +345,25 @@ export const OrderDetails = memo(function OrderDetails({ mode = "modal", order,
345
345
  handlePayment();
346
346
  }
347
347
  }, [isPayableState, isComponentReady, handlePayment]);
348
+ // Auto-redirect to redirectUrl when order is executed (for onramp orders)
349
+ useEffect(() => {
350
+ if (order.status === "executed" && order.onrampMetadata?.redirectUrl) {
351
+ const baseUrl = order.onrampMetadata.redirectUrl;
352
+ try {
353
+ const url = new URL(baseUrl);
354
+ // Prevent Open Redirect vulnerabilities by ensuring the protocol is http or https
355
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
356
+ console.error(`Attempted redirect to a URL with an invalid protocol: ${url.protocol}`);
357
+ return;
358
+ }
359
+ const redirectUrl = `${baseUrl.replace(/\/$/, "")}/${order.id}`;
360
+ window.location.href = redirectUrl;
361
+ }
362
+ catch (error) {
363
+ console.error("Invalid redirect URL provided:", baseUrl, error);
364
+ }
365
+ }
366
+ }, [order.status, order.onrampMetadata?.redirectUrl, order.id]);
348
367
  if (!srcToken || !dstToken) {
349
368
  return _jsx("div", { children: "Loading..." });
350
369
  }
@@ -1,5 +1,5 @@
1
1
  import { components } from "../../../../anyspend/types/api";
2
- export declare function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, address, chainId, setChainId, token, setToken, hideTokenSelect, canEditAmount, className, innerClassName, amountClassName, tokenSelectClassName, onTokenSelect, walletAddress, }: {
2
+ export declare function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, address, chainId, setChainId, token, setToken, hideTokenSelect, canEditAmount, className, innerClassName, amountClassName, tokenSelectClassName, onTokenSelect, walletAddress, skipAutoMaxOnTokenChange, }: {
3
3
  disabled?: boolean;
4
4
  inputValue: string;
5
5
  onChangeInput: (value: string) => void;
@@ -19,4 +19,6 @@ export declare function OrderTokenAmount({ disabled, inputValue, onChangeInput,
19
19
  preventDefault: () => void;
20
20
  }) => void;
21
21
  walletAddress?: string | undefined;
22
+ /** When true, skip auto-setting max balance when token changes (used for fixed destination amount mode) */
23
+ skipAutoMaxOnTokenChange?: boolean;
22
24
  }): import("react/jsx-runtime").JSX.Element;
@@ -11,7 +11,7 @@ import { Button, useTokenBalance } from "../../../../global-account/react/index.
11
11
  import { cn } from "../../../../shared/utils/index.js";
12
12
  import { TokenSelector } from "@relayprotocol/relay-kit-ui";
13
13
  import { ChainTokenIcon } from "./ChainTokenIcon.js";
14
- export function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, address, chainId, setChainId, token, setToken, hideTokenSelect = false, canEditAmount = true, className, innerClassName, amountClassName, tokenSelectClassName, onTokenSelect, walletAddress, }) {
14
+ export function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, address, chainId, setChainId, token, setToken, hideTokenSelect = false, canEditAmount = true, className, innerClassName, amountClassName, tokenSelectClassName, onTokenSelect, walletAddress, skipAutoMaxOnTokenChange = false, }) {
15
15
  // Track previous token to detect changes
16
16
  const prevTokenRef = useRef(token.address);
17
17
  // Only get token balance when context is "from" (for setting max amount)
@@ -23,6 +23,11 @@ export function OrderTokenAmount({ disabled, inputValue, onChangeInput, context,
23
23
  // Only handle "from" context
24
24
  if (context !== "from")
25
25
  return;
26
+ // Skip auto-max when in fixed destination amount mode
27
+ if (skipAutoMaxOnTokenChange) {
28
+ prevTokenRef.current = token.address;
29
+ return;
30
+ }
26
31
  // Check if token changed or if this is the initial load with balance
27
32
  const isTokenChanged = prevTokenRef.current !== token.address;
28
33
  if (isTokenChanged && rawBalance) {
@@ -43,7 +48,16 @@ export function OrderTokenAmount({ disabled, inputValue, onChangeInput, context,
43
48
  // Update refs
44
49
  prevTokenRef.current = token.address;
45
50
  }
46
- }, [token.address, token.chainId, token.decimals, chainId, context, onChangeInput, rawBalance]);
51
+ }, [
52
+ token.address,
53
+ token.chainId,
54
+ token.decimals,
55
+ chainId,
56
+ context,
57
+ onChangeInput,
58
+ rawBalance,
59
+ skipAutoMaxOnTokenChange,
60
+ ]);
47
61
  const handleTokenSelect = (newToken) => {
48
62
  const token = {
49
63
  address: newToken.address,
@@ -89,9 +89,9 @@ function StripePaymentForm({ order, clientSecret, onPaymentSuccess, }) {
89
89
  }
90
90
  // At this point TypeScript knows result.paymentIntent exists and error is undefined
91
91
  console.log("@@stripe-web2-payment:success:", JSON.stringify({ orderId: order.id, paymentIntentId: result.paymentIntent.id }, null, 2));
92
- // Payment succeeded without redirect - handle success in the modal
92
+ // Payment succeeded
93
93
  setMessage(null);
94
- // Add waitingForDeposit=true to query params
94
+ // Stay on page and show waiting state (redirect will happen in OrderDetails when order is executed)
95
95
  const currentUrl = new URL(window.location.href);
96
96
  currentUrl.searchParams.set("waitingForDeposit", "true");
97
97
  window.history.replaceState(null, "", currentUrl.toString());
@@ -51,4 +51,6 @@ export declare function AnySpend(props: {
51
51
  classes?: AnySpendClasses;
52
52
  /** When true, allows direct transfer without swap if source and destination token/chain are the same */
53
53
  allowDirectTransfer?: boolean;
54
+ /** Fixed destination token amount (in wei/smallest unit). When provided, user cannot change the amount. */
55
+ destinationTokenAmount?: string;
54
56
  }): import("react/jsx-runtime").JSX.Element;
@@ -18,6 +18,8 @@ interface CryptoPaySectionProps {
18
18
  }) => void;
19
19
  onShowFeeDetail?: () => void;
20
20
  classes?: CryptoPaySectionClasses;
21
+ /** When true, skip auto-setting max balance when token changes (used for fixed destination amount mode) */
22
+ skipAutoMaxOnTokenChange?: boolean;
21
23
  }
22
- export declare function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, selectedSrcToken, setSelectedSrcToken, srcAmount, setSrcAmount, isSrcInputDirty, setIsSrcInputDirty, selectedCryptoPaymentMethod, onSelectCryptoPaymentMethod, anyspendQuote, onTokenSelect, onShowFeeDetail, classes, }: CryptoPaySectionProps): import("react/jsx-runtime").JSX.Element;
24
+ export declare function CryptoPaySection({ selectedSrcChainId, setSelectedSrcChainId, selectedSrcToken, setSelectedSrcToken, srcAmount, setSrcAmount, isSrcInputDirty, setIsSrcInputDirty, selectedCryptoPaymentMethod, onSelectCryptoPaymentMethod, anyspendQuote, onTokenSelect, onShowFeeDetail, classes, skipAutoMaxOnTokenChange, }: CryptoPaySectionProps): import("react/jsx-runtime").JSX.Element;
23
25
  export {};
@@ -1,5 +1,5 @@
1
1
  import { components } from "@b3dotfun/sdk/anyspend/types/api";
2
- export declare function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, address, chainId, setChainId, token, setToken, hideTokenSelect, canEditAmount, className, innerClassName, amountClassName, tokenSelectClassName, onTokenSelect, walletAddress, }: {
2
+ export declare function OrderTokenAmount({ disabled, inputValue, onChangeInput, context, address, chainId, setChainId, token, setToken, hideTokenSelect, canEditAmount, className, innerClassName, amountClassName, tokenSelectClassName, onTokenSelect, walletAddress, skipAutoMaxOnTokenChange, }: {
3
3
  disabled?: boolean;
4
4
  inputValue: string;
5
5
  onChangeInput: (value: string) => void;
@@ -19,4 +19,6 @@ export declare function OrderTokenAmount({ disabled, inputValue, onChangeInput,
19
19
  preventDefault: () => void;
20
20
  }) => void;
21
21
  walletAddress?: string | undefined;
22
+ /** When true, skip auto-setting max balance when token changes (used for fixed destination amount mode) */
23
+ skipAutoMaxOnTokenChange?: boolean;
22
24
  }): import("react/jsx-runtime").JSX.Element;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@b3dotfun/sdk",
3
- "version": "0.1.63",
3
+ "version": "0.1.64",
4
4
  "source": "src/index.ts",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "react-native": "./dist/cjs/index.native.js",
@@ -45,7 +45,7 @@ import invariant from "invariant";
45
45
  import { ArrowDown, CheckCircle, HistoryIcon, Loader2 } from "lucide-react";
46
46
  import { motion } from "motion/react";
47
47
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
48
- import { parseUnits } from "viem";
48
+ import { formatUnits, parseUnits } from "viem";
49
49
  import { base, mainnet } from "viem/chains";
50
50
  import { components } from "../../types/api";
51
51
  import { useAutoSelectCryptoPaymentMethod } from "../hooks/useAutoSelectCryptoPaymentMethod";
@@ -124,6 +124,8 @@ export function AnySpend(props: {
124
124
  classes?: AnySpendClasses;
125
125
  /** When true, allows direct transfer without swap if source and destination token/chain are the same */
126
126
  allowDirectTransfer?: boolean;
127
+ /** Fixed destination token amount (in wei/smallest unit). When provided, user cannot change the amount. */
128
+ destinationTokenAmount?: string;
127
129
  }) {
128
130
  const fingerprintConfig = getFingerprintConfig();
129
131
 
@@ -155,6 +157,7 @@ function AnySpendInner({
155
157
  returnHomeLabel,
156
158
  classes,
157
159
  allowDirectTransfer = false,
160
+ destinationTokenAmount,
158
161
  }: {
159
162
  sourceChainId?: number;
160
163
  destinationTokenAddress?: string;
@@ -175,6 +178,7 @@ function AnySpendInner({
175
178
  returnHomeLabel?: string;
176
179
  classes?: AnySpendClasses;
177
180
  allowDirectTransfer?: boolean;
181
+ destinationTokenAmount?: string;
178
182
  }) {
179
183
  const searchParams = useSearchParamsSSR();
180
184
  const router = useRouter();
@@ -368,6 +372,18 @@ function AnySpendInner({
368
372
  appliedDstMetadataRef.current = false;
369
373
  }, [selectedDstToken.address, selectedDstToken.chainId]);
370
374
 
375
+ // Prefill destination amount if provided (for fixed amount mode)
376
+ const appliedDestinationAmount = useRef(false);
377
+ useEffect(() => {
378
+ // Only apply when we have real metadata (not default decimals)
379
+ if (destinationTokenAmount && dstTokenMetadata?.decimals && !appliedDestinationAmount.current) {
380
+ appliedDestinationAmount.current = true;
381
+ const formattedAmount = formatUnits(BigInt(destinationTokenAmount), dstTokenMetadata.decimals);
382
+ setDstAmount(formattedAmount);
383
+ setIsSrcInputDirty(false); // Switch to EXACT_OUTPUT mode
384
+ }
385
+ }, [destinationTokenAmount, dstTokenMetadata]);
386
+
371
387
  // Load swap configuration from URL on initial render
372
388
  useEffect(() => {
373
389
  // Skip if we've already processed the URL, if we have an order to load, or if URL param management is disabled
@@ -665,9 +681,12 @@ function AnySpendInner({
665
681
  anyspendQuote.data.currencyOut?.currency?.decimals
666
682
  ) {
667
683
  if (isSrcInputDirty) {
668
- const amount = anyspendQuote.data.currencyOut.amount;
669
- const decimals = anyspendQuote.data.currencyOut.currency.decimals;
670
- setDstAmount(formatTokenAmount(BigInt(amount), decimals, 6, false));
684
+ // Don't override dstAmount if we have a fixed destinationTokenAmount
685
+ if (!destinationTokenAmount) {
686
+ const amount = anyspendQuote.data.currencyOut.amount;
687
+ const decimals = anyspendQuote.data.currencyOut.currency.decimals;
688
+ setDstAmount(formatTokenAmount(BigInt(amount), decimals, 6, false));
689
+ }
671
690
  } else {
672
691
  const amount = anyspendQuote.data.currencyIn.amount;
673
692
  const decimals = anyspendQuote.data.currencyIn.currency.decimals;
@@ -675,12 +694,15 @@ function AnySpendInner({
675
694
  }
676
695
  } else {
677
696
  if (isSrcInputDirty) {
678
- setDstAmount("");
697
+ // Don't reset dstAmount if we have a fixed destinationTokenAmount
698
+ if (!destinationTokenAmount) {
699
+ setDstAmount("");
700
+ }
679
701
  } else {
680
702
  setSrcAmount("");
681
703
  }
682
704
  }
683
- }, [anyspendQuote, isSrcInputDirty]);
705
+ }, [anyspendQuote, isSrcInputDirty, destinationTokenAmount]);
684
706
 
685
707
  useEffect(() => {
686
708
  if (oat?.data?.order.status === "executed" && !onSuccessCalled.current) {
@@ -1205,6 +1227,7 @@ function AnySpendInner({
1205
1227
  anyspendQuote={anyspendQuote}
1206
1228
  onTokenSelect={onTokenSelect}
1207
1229
  onShowFeeDetail={() => navigateToPanel(PanelView.FEE_DETAIL, "forward")}
1230
+ skipAutoMaxOnTokenChange={!!destinationTokenAmount}
1208
1231
  />
1209
1232
  ) : (
1210
1233
  <motion.div
@@ -1306,6 +1329,7 @@ function AnySpendInner({
1306
1329
  setIsSrcInputDirty(false);
1307
1330
  setDstAmount(value);
1308
1331
  }}
1332
+ disableAmountInput={!!destinationTokenAmount}
1309
1333
  anyspendQuote={isDirectTransfer ? undefined : anyspendQuote}
1310
1334
  onShowPointsDetail={
1311
1335
  isDirectTransfer ? undefined : () => navigateToPanel(PanelView.POINTS_DETAIL, "forward")
@@ -222,14 +222,13 @@ function AnySpendCustomExactInInner({
222
222
  // Prefill destination amount if provided (for EXACT_OUTPUT mode)
223
223
  const appliedDestinationAmount = useRef(false);
224
224
  useEffect(() => {
225
- if (destinationTokenAmount && !appliedDestinationAmount.current) {
225
+ if (destinationTokenAmount && destinationToken?.decimals && !appliedDestinationAmount.current) {
226
226
  appliedDestinationAmount.current = true;
227
- // Convert wei to human-readable format
228
227
  const formattedAmount = formatUnits(destinationTokenAmount, destinationToken.decimals);
229
228
  setDstAmountInput(formattedAmount);
230
229
  setIsSrcInputDirty(false); // Switch to EXACT_OUTPUT mode
231
230
  }
232
- }, [destinationTokenAmount, destinationToken.decimals, setDstAmountInput, setIsSrcInputDirty]);
231
+ }, [destinationTokenAmount, destinationToken, setDstAmountInput, setIsSrcInputDirty]);
233
232
 
234
233
  const selectedRecipientOrDefault = selectedRecipientAddress ?? recipientAddress;
235
234
 
@@ -419,6 +418,7 @@ function AnySpendCustomExactInInner({
419
418
  onSelectCryptoPaymentMethod={() => setActivePanel(PanelView.CRYPTO_PAYMENT_METHOD)}
420
419
  anyspendQuote={anyspendQuote}
421
420
  onTokenSelect={onTokenSelect}
421
+ skipAutoMaxOnTokenChange={!!destinationTokenAmount}
422
422
  />
423
423
  ) : (
424
424
  <motion.div
@@ -474,7 +474,7 @@ function AnySpendCustomExactInInner({
474
474
  recipientName={recipientName || undefined}
475
475
  customRecipientLabel={customRecipientLabel}
476
476
  onSelectRecipient={() => setActivePanel(PanelView.RECIPIENT_SELECTION)}
477
- dstAmount={isSrcInputDirty ? dstAmount : dstAmountInput}
477
+ dstAmount={isSrcInputDirty && !destinationTokenAmount ? dstAmount : dstAmountInput}
478
478
  dstToken={selectedDstToken}
479
479
  dstTokenSymbol={DESTINATION_TOKEN_DETAILS.SYMBOL}
480
480
  dstTokenLogoURI={DESTINATION_TOKEN_DETAILS.LOGO_URI}
@@ -719,6 +719,7 @@ export function AnySpendDeposit({
719
719
  returnHomeLabel={returnHomeLabel}
720
720
  classes={classes?.anySpend}
721
721
  allowDirectTransfer={allowDirectTransfer}
722
+ destinationTokenAmount={destinationTokenAmount}
722
723
  />
723
724
  )}
724
725
  </div>
@@ -32,6 +32,8 @@ interface CryptoPaySectionProps {
32
32
  onShowFeeDetail?: () => void;
33
33
  // Custom classes for styling
34
34
  classes?: CryptoPaySectionClasses;
35
+ /** When true, skip auto-setting max balance when token changes (used for fixed destination amount mode) */
36
+ skipAutoMaxOnTokenChange?: boolean;
35
37
  }
36
38
 
37
39
  export function CryptoPaySection({
@@ -49,6 +51,7 @@ export function CryptoPaySection({
49
51
  onTokenSelect,
50
52
  onShowFeeDetail,
51
53
  classes,
54
+ skipAutoMaxOnTokenChange = false,
52
55
  }: CryptoPaySectionProps) {
53
56
  const { data: srcTokenMetadata } = useTokenData(selectedSrcToken?.chainId, selectedSrcToken?.address);
54
57
 
@@ -134,6 +137,7 @@ export function CryptoPaySection({
134
137
  token={selectedSrcToken}
135
138
  setToken={setSelectedSrcToken}
136
139
  onTokenSelect={onTokenSelect}
140
+ skipAutoMaxOnTokenChange={skipAutoMaxOnTokenChange}
137
141
  />
138
142
  </div>
139
143
  <div className={classes?.balanceRow || "flex items-center justify-between"}>
@@ -493,6 +493,25 @@ export const OrderDetails = memo(function OrderDetails({
493
493
  }
494
494
  }, [isPayableState, isComponentReady, handlePayment]);
495
495
 
496
+ // Auto-redirect to redirectUrl when order is executed (for onramp orders)
497
+ useEffect(() => {
498
+ if (order.status === "executed" && order.onrampMetadata?.redirectUrl) {
499
+ const baseUrl = order.onrampMetadata.redirectUrl;
500
+ try {
501
+ const url = new URL(baseUrl);
502
+ // Prevent Open Redirect vulnerabilities by ensuring the protocol is http or https
503
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
504
+ console.error(`Attempted redirect to a URL with an invalid protocol: ${url.protocol}`);
505
+ return;
506
+ }
507
+ const redirectUrl = `${baseUrl.replace(/\/$/, "")}/${order.id}`;
508
+ window.location.href = redirectUrl;
509
+ } catch (error) {
510
+ console.error("Invalid redirect URL provided:", baseUrl, error);
511
+ }
512
+ }
513
+ }, [order.status, order.onrampMetadata?.redirectUrl, order.id]);
514
+
496
515
  if (!srcToken || !dstToken) {
497
516
  return <div>Loading...</div>;
498
517
  }
@@ -32,6 +32,7 @@ export function OrderTokenAmount({
32
32
  tokenSelectClassName,
33
33
  onTokenSelect,
34
34
  walletAddress,
35
+ skipAutoMaxOnTokenChange = false,
35
36
  }: {
36
37
  disabled?: boolean;
37
38
  inputValue: string;
@@ -50,6 +51,8 @@ export function OrderTokenAmount({
50
51
  tokenSelectClassName?: string;
51
52
  onTokenSelect?: (token: components["schemas"]["Token"], event: { preventDefault: () => void }) => void;
52
53
  walletAddress?: string | undefined;
54
+ /** When true, skip auto-setting max balance when token changes (used for fixed destination amount mode) */
55
+ skipAutoMaxOnTokenChange?: boolean;
53
56
  }) {
54
57
  // Track previous token to detect changes
55
58
  const prevTokenRef = useRef<string>(token.address);
@@ -64,6 +67,12 @@ export function OrderTokenAmount({
64
67
  // Only handle "from" context
65
68
  if (context !== "from") return;
66
69
 
70
+ // Skip auto-max when in fixed destination amount mode
71
+ if (skipAutoMaxOnTokenChange) {
72
+ prevTokenRef.current = token.address;
73
+ return;
74
+ }
75
+
67
76
  // Check if token changed or if this is the initial load with balance
68
77
  const isTokenChanged = prevTokenRef.current !== token.address;
69
78
 
@@ -88,7 +97,16 @@ export function OrderTokenAmount({
88
97
  // Update refs
89
98
  prevTokenRef.current = token.address;
90
99
  }
91
- }, [token.address, token.chainId, token.decimals, chainId, context, onChangeInput, rawBalance]);
100
+ }, [
101
+ token.address,
102
+ token.chainId,
103
+ token.decimals,
104
+ chainId,
105
+ context,
106
+ onChangeInput,
107
+ rawBalance,
108
+ skipAutoMaxOnTokenChange,
109
+ ]);
92
110
 
93
111
  const handleTokenSelect = (newToken: any) => {
94
112
  const token: components["schemas"]["Token"] = {
@@ -159,10 +159,10 @@ function StripePaymentForm({
159
159
  JSON.stringify({ orderId: order.id, paymentIntentId: result.paymentIntent.id }, null, 2),
160
160
  );
161
161
 
162
- // Payment succeeded without redirect - handle success in the modal
162
+ // Payment succeeded
163
163
  setMessage(null);
164
164
 
165
- // Add waitingForDeposit=true to query params
165
+ // Stay on page and show waiting state (redirect will happen in OrderDetails when order is executed)
166
166
  const currentUrl = new URL(window.location.href);
167
167
  currentUrl.searchParams.set("waitingForDeposit", "true");
168
168
  window.history.replaceState(null, "", currentUrl.toString());