diff --git a/packages/sdk/src/common/api.test.ts b/packages/sdk/src/common/api.test.ts
index 79011aff..25de2b30 100644
--- a/packages/sdk/src/common/api.test.ts
+++ b/packages/sdk/src/common/api.test.ts
@@ -1,24 +1,149 @@
import { describe, expect, test } from "vitest";
+import type { ExternalPaymentMethodRequest } from "./api.js";
import { zCreatePaymentMethodRequest } from "./api.js";
describe("zCreatePaymentMethodRequest", () => {
- test("accepts BinanceUSDC and BinanceUSDT exchange ids", () => {
- for (const exchangeId of ["BinanceUSDC", "BinanceUSDT"] as const) {
- expect(
- zCreatePaymentMethodRequest.parse({
- clientSecret: "secret",
- paymentMethod: {
- type: "exchange",
- exchangeId,
- amountUsd: 10,
+ test("accepts canonical USD exchange requests", () => {
+ expect(
+ zCreatePaymentMethodRequest.parse({
+ clientSecret: "secret",
+ paymentMethod: {
+ type: "exchange",
+ exchangeId: "Coinbase",
+ amountUsd: 10,
+ },
+ }).paymentMethod,
+ ).toEqual({
+ type: "exchange",
+ exchangeId: "Coinbase",
+ amountUsd: 10,
+ });
+ });
+
+ test("continues to accept legacy Cash App exchange requests", () => {
+ expect(
+ zCreatePaymentMethodRequest.parse({
+ clientSecret: "secret",
+ paymentMethod: {
+ type: "exchange",
+ exchangeId: "CashApp",
+ amountUsd: 10,
+ },
+ }).paymentMethod,
+ ).toEqual({
+ type: "exchange",
+ exchangeId: "CashApp",
+ amountUsd: 10,
+ });
+ });
+
+ test("accepts typed external methods and source currencies", () => {
+ expect(
+ zCreatePaymentMethodRequest.parse({
+ clientSecret: "secret",
+ paymentMethod: {
+ type: "external",
+ id: "Revolut",
+ countryCode: "DE",
+ sourceAmount: {
+ units: "10.50",
+ currency: "EUR",
+ },
+ },
+ }).paymentMethod,
+ ).toEqual({
+ type: "external",
+ id: "Revolut",
+ countryCode: "DE",
+ sourceAmount: {
+ units: "10.50",
+ currency: "EUR",
+ },
+ });
+ });
+
+ test("accepts Cash App without a country", () => {
+ const paymentMethod: ExternalPaymentMethodRequest = {
+ type: "external",
+ id: "CashApp",
+ sourceAmount: {
+ units: "10.50",
+ currency: "USD",
+ },
+ };
+ expect(
+ zCreatePaymentMethodRequest.parse({
+ clientSecret: "secret",
+ paymentMethod,
+ }).paymentMethod,
+ ).toEqual({
+ type: "external",
+ id: "CashApp",
+ sourceAmount: {
+ units: "10.50",
+ currency: "USD",
+ },
+ });
+ });
+
+ test("rejects malformed source amounts without encoding method policy", () => {
+ expect(() =>
+ zCreatePaymentMethodRequest.parse({
+ clientSecret: "secret",
+ paymentMethod: {
+ type: "external",
+ id: "Revolut",
+ countryCode: "DE",
+ sourceAmount: {
+ units: "0",
+ currency: "EUR",
+ },
+ },
+ }),
+ ).toThrow();
+ expect(() =>
+ zCreatePaymentMethodRequest.parse({
+ clientSecret: "secret",
+ paymentMethod: {
+ type: "external",
+ id: "Revolut",
+ countryCode: "DE",
+ sourceAmount: {
+ units: "10.00",
+ currency: "eur",
+ },
+ },
+ }),
+ ).toThrow();
+ });
+
+ test("keeps external methods out of the exchange enum", () => {
+ expect(() =>
+ zCreatePaymentMethodRequest.parse({
+ clientSecret: "secret",
+ paymentMethod: {
+ type: "exchange",
+ exchangeId: "Revolut",
+ amountUsd: 10,
+ },
+ }),
+ ).toThrow();
+ });
+
+ test("rejects untyped external method IDs", () => {
+ expect(() =>
+ zCreatePaymentMethodRequest.parse({
+ clientSecret: "secret",
+ paymentMethod: {
+ type: "external",
+ id: "FutureExternalMethod",
+ sourceAmount: {
+ units: "10.00",
+ currency: "EUR",
},
- }).paymentMethod,
- ).toEqual({
- type: "exchange",
- exchangeId,
- amountUsd: 10,
- });
- }
+ },
+ }),
+ ).toThrow();
});
});
diff --git a/packages/sdk/src/common/api.ts b/packages/sdk/src/common/api.ts
index f6652fc7..16ac2b37 100644
--- a/packages/sdk/src/common/api.ts
+++ b/packages/sdk/src/common/api.ts
@@ -1,11 +1,19 @@
import { z } from "zod";
import { zAccountRail } from "./account.js";
+import {
+ type ExternalPayment,
+ zExternalPaymentMethodId,
+} from "./externalPayment.js";
import type { TronAddress, UUID } from "./primitives.js";
import { zAddress, zSolanaAddress } from "./primitives.js";
import type { SessionPublicInfo } from "./session.js";
const zPlatform = z.enum(["ios", "android", "other", "desktop", "mobile"]);
+const zPositiveDecimalUnits = z
+ .string()
+ .max(100)
+ .regex(/^(?:[1-9]\d*(?:\.\d+)?|0\.\d*[1-9]\d*)$/);
export const zExchangeId = z.enum([
"Coinbase",
@@ -41,6 +49,20 @@ export const zCreatePaymentMethodRequest = z.object({
amountUsd: z.number().positive(),
platform: zPlatform.optional(),
}),
+ z.object({
+ type: z.literal("external"),
+ id: zExternalPaymentMethodId,
+ /** ISO-3166 alpha-2 country selected in the backend navigation tree. */
+ countryCode: z
+ .string()
+ .regex(/^[A-Z]{2}$/)
+ .optional(),
+ sourceAmount: z.object({
+ units: zPositiveDecimalUnits,
+ currency: z.string().regex(/^[A-Z]{3}$/),
+ }),
+ platform: zPlatform.optional(),
+ }),
z.object({
type: z.literal("stripe"),
amountUsd: z.number().positive(),
@@ -78,6 +100,12 @@ export type CreatePaymentMethodRequest = z.output<
typeof zCreatePaymentMethodRequest
>;
+/** Provider-neutral request for payment apps and hosted external handoffs. */
+export type ExternalPaymentMethodRequest = Extract<
+ CreatePaymentMethodRequest["paymentMethod"],
+ { type: "external" }
+>;
+
export type CheckSessionRequest = z.output;
export type TokenOptionsRequest = z.output;
@@ -112,7 +140,10 @@ export type CreatePaymentMethodResponse = {
/** Base64-encoded Solana transaction for the user to sign. */
serializedTx: string;
};
- /** Exchange-specific payment details, present when payment method is Exchange. */
+ /**
+ * @deprecated Use externalPayment. Retained for compatibility with servers
+ * and clients that predate the provider-neutral external payment contract.
+ */
exchange?: {
/** Deeplink URL for the exchange. */
url: string;
@@ -121,6 +152,8 @@ export type CreatePaymentMethodResponse = {
/** Invoice expiry time (unix seconds). Present for Lightning invoices. */
expiresAt?: number;
};
+ /** Provider-neutral details for any external payment handoff. */
+ externalPayment?: ExternalPayment;
/** Fiat payment details, present when payment method is fiat. */
fiat?: {
/** Hosted URL where the user completes KYC and the selected fiat flow. */
@@ -143,6 +176,13 @@ export type CreatePaymentMethodResponse = {
};
};
+export type {
+ ExternalPayment,
+ ExternalPaymentQuote,
+ ExternalPaymentMethodId,
+ Money,
+} from "./externalPayment.js";
+
export type CheckSessionResponse = {
/** Current session state. */
session: SessionPublicInfo;
diff --git a/packages/sdk/src/common/externalPayment.ts b/packages/sdk/src/common/externalPayment.ts
new file mode 100644
index 00000000..05de42fb
--- /dev/null
+++ b/packages/sdk/src/common/externalPayment.ts
@@ -0,0 +1,34 @@
+import { z } from "zod";
+
+export const zExternalPaymentMethodId = z.enum(["CashApp", "Revolut"]);
+
+export type ExternalPaymentMethodId = z.infer;
+
+export type Money = {
+ /** ISO 4217 currency or asset symbol. */
+ currency: string;
+ /** Decimal amount in currency units. */
+ units: string;
+};
+
+export type ExternalPayment = {
+ /** URL where the user completes the external payment. */
+ url: string;
+ /** Message to display while waiting for the payment. */
+ waitingMessage: string;
+ /** Link expiry time (unix seconds), when supplied by the backend. */
+ expiresAt?: number;
+ /** Optional estimate for quoted external payments. */
+ quote?: ExternalPaymentQuote;
+};
+
+export type ExternalPaymentQuote = {
+ /** Money paid by the user. */
+ sourceAmount: Money;
+ /** Estimated money delivered before the on-chain payment is observed. */
+ estimatedDestinationAmount: Money;
+ fees: {
+ kind: "service" | "network" | "partner";
+ amount: Money;
+ }[];
+};
diff --git a/packages/sdk/src/common/index.ts b/packages/sdk/src/common/index.ts
index d0243972..ee5993f9 100644
--- a/packages/sdk/src/common/index.ts
+++ b/packages/sdk/src/common/index.ts
@@ -2,6 +2,7 @@ export * from "./account.js";
export * from "./api.js";
export * from "./chain.js";
export * from "./errors.js";
+export * from "./externalPayment.js";
export * from "./primitives.js";
export * from "./session.js";
export * from "./theme.js";
diff --git a/packages/sdk/src/common/session.ts b/packages/sdk/src/common/session.ts
index de98f148..f5d4b26f 100644
--- a/packages/sdk/src/common/session.ts
+++ b/packages/sdk/src/common/session.ts
@@ -2,6 +2,7 @@ import type { Address, Hex } from "viem";
import { z } from "zod";
import type { AccountRail } from "./account.js";
+import type { ExternalPaymentMethodId, Money } from "./externalPayment.js";
import type { DaimoSessionTheme } from "./theme.js";
import type {
SolanaAddress,
@@ -96,7 +97,18 @@ export type PaymentMethod =
| PaymentMethodTron
| PaymentMethodSolana
| PaymentMethodStripe
- | PaymentMethodFiat;
+ | PaymentMethodFiat
+ | PaymentMethodExternal;
+
+export type PaymentMethodExternal = {
+ type: "external";
+ /** User-visible payment method identity. */
+ id: ExternalPaymentMethodId;
+ /** Money the user authorized in the external payment app. */
+ sourceAmount: Money;
+ /** When this payment method was created (unix seconds). */
+ createdAt: number;
+};
export type PaymentMethodStripe = {
type: "stripe";
diff --git a/packages/sdk/src/web/api/index.ts b/packages/sdk/src/web/api/index.ts
index b8237518..d315cacb 100644
--- a/packages/sdk/src/web/api/index.ts
+++ b/packages/sdk/src/web/api/index.ts
@@ -10,6 +10,9 @@ export type {
NavNodeDeeplink,
NavNodeDepositAddress,
NavNodeExchange,
+ NavExternalPaymentNode,
+ NavExternalHandoff,
+ NavNodeExternalPayment,
NavNodeFiat,
NavNodeKycRequirement,
NavNodeKycRequirementDisplayItem,
@@ -17,6 +20,7 @@ export type {
NavNodeKycRequirementItem,
NavNodeKycRequirementKind,
NavNodeTronDeposit,
+ NavSourceAmount,
SessionNavInfo,
SessionWithNav,
} from "./navTree.js";
diff --git a/packages/sdk/src/web/api/navTree.test.ts b/packages/sdk/src/web/api/navTree.test.ts
new file mode 100644
index 00000000..bfb427d4
--- /dev/null
+++ b/packages/sdk/src/web/api/navTree.test.ts
@@ -0,0 +1,99 @@
+import { describe, expect, test } from "vitest";
+
+import {
+ formatNavSourceAmountUnits,
+ getNavExternalHandoff,
+ getNavSourceAmount,
+ type NavNodeExchange,
+ type NavNodeExternalPayment,
+} from "./navTree.js";
+
+describe("external payment nav policy", () => {
+ test("uses backend-provided amount and handoff descriptors", () => {
+ const node: NavNodeExternalPayment = {
+ type: "ExternalPayment",
+ id: "ExternalPayment-Revolut",
+ title: "Revolut",
+ externalPaymentMethodId: "Revolut",
+ countryCode: "JP",
+ sourceAmount: {
+ currency: "JPY",
+ currencySymbol: "¥",
+ decimals: 0,
+ minimum: 100,
+ maximum: 10000,
+ },
+ externalHandoff: {
+ desktopBehavior: "popup",
+ popupName: "future-hosted-method",
+ },
+ };
+
+ expect(getNavSourceAmount(node)).toEqual(node.sourceAmount);
+ expect(getNavExternalHandoff(node)).toEqual(node.externalHandoff);
+ expect(formatNavSourceAmountUnits(125, node.sourceAmount)).toBe("125");
+ });
+
+ test("rejects invalid backend amount precision", () => {
+ expect(() =>
+ formatNavSourceAmountUnits(10, {
+ currency: "EUR",
+ currencySymbol: "€",
+ decimals: 21,
+ minimum: 1,
+ maximum: 100,
+ }),
+ ).toThrow("invalid external payment amount");
+ });
+
+ test("falls back to legacy USD nav fields", () => {
+ const node: NavNodeExchange = {
+ type: "Exchange",
+ id: "Exchange-Coinbase",
+ title: "Coinbase",
+ exchangeId: "Coinbase",
+ requiredUsd: 10,
+ minimumUsd: 1,
+ maximumUsd: 100,
+ };
+
+ expect(getNavSourceAmount(node)).toEqual({
+ currency: "USD",
+ currencySymbol: "$",
+ decimals: 2,
+ required: 10,
+ minimum: 1,
+ maximum: 100,
+ });
+ expect(getNavExternalHandoff(node)).toEqual({
+ desktopBehavior: "popup",
+ legacyQrPlaceholderDensity: "short",
+ });
+ });
+
+ test.each([
+ ["Coinbase", "popup", "short"],
+ ["MtPelerin", "popup", "short"],
+ ["Binance", "qr", "medium"],
+ ["BinanceUSDC", "qr", "medium"],
+ ["BinanceUSDT", "qr", "medium"],
+ ["Lemon", "qr", "short"],
+ ] as const)(
+ "preserves legacy %s handoff behavior",
+ (exchangeId, desktopBehavior, legacyQrPlaceholderDensity) => {
+ const node: NavNodeExchange = {
+ type: "Exchange",
+ id: `Exchange-${exchangeId}`,
+ title: exchangeId,
+ exchangeId,
+ minimumUsd: 1,
+ maximumUsd: 100,
+ };
+
+ expect(getNavExternalHandoff(node)).toEqual({
+ desktopBehavior,
+ legacyQrPlaceholderDensity,
+ });
+ },
+ );
+});
diff --git a/packages/sdk/src/web/api/navTree.ts b/packages/sdk/src/web/api/navTree.ts
index 626fbb0c..dbad4cbb 100644
--- a/packages/sdk/src/web/api/navTree.ts
+++ b/packages/sdk/src/web/api/navTree.ts
@@ -4,6 +4,7 @@ import type {
DepositPaymentInteraction,
} from "../../common/account.js";
import type { ExchangeId } from "../../common/api.js";
+import type { ExternalPaymentMethodId } from "../../common/externalPayment.js";
import type { SessionPublicInfo } from "../../common/session.js";
/** Session plus server-defined modal navigation data. */
@@ -89,23 +90,137 @@ export type NavNodeDeeplink = NavNodeCommon & {
pageIcon?: string;
};
+export type NavSourceAmount = {
+ /** ISO 4217 source currency selected by the backend. */
+ currency: string;
+ /** Presentation symbol selected by the backend, for example "$" or "€". */
+ currencySymbol: string;
+ /** Decimal places submitted to the backend. */
+ decimals: number;
+ required?: number;
+ minimum: number;
+ maximum: number;
+};
+
+export type NavExternalHandoff = {
+ desktopBehavior: "popup" | "qr";
+ popupName?: string;
+};
+
export type NavNodeExchange = NavNodeCommon & {
type: "Exchange";
exchangeId: ExchangeId;
icon?: string;
+ /** Backend-owned amount and currency policy. Optional for old servers. */
+ sourceAmount?: NavSourceAmount;
+ /** Backend-owned external handoff presentation. Optional for old servers. */
+ externalHandoff?: NavExternalHandoff;
+ /** @deprecated Use sourceAmount.required. */
requiredUsd?: number;
+ /** @deprecated Use sourceAmount.minimum. */
minimumUsd: number;
+ /** @deprecated Use sourceAmount.maximum. */
maximumUsd: number;
};
export type NavNodeCashApp = NavNodeCommon & {
type: "CashApp";
icon?: string;
+ /**
+ * Present when the server accepts the external-payment request for Cash App.
+ * Its absence preserves compatibility with pre-external-payment servers.
+ */
+ externalPaymentMethodId?: "CashApp";
+ /** Backend-owned amount and currency policy. Optional for old servers. */
+ sourceAmount?: NavSourceAmount;
+ /** Backend-owned external handoff presentation. Optional for old servers. */
+ externalHandoff?: NavExternalHandoff;
requiredUsd?: number;
minimumUsd: number;
maximumUsd: number;
};
+/**
+ * Generic external payment handoff. The SDK renders the interaction without
+ * knowing which implementation owns the method ID.
+ */
+export type NavNodeExternalPayment = NavNodeCommon & {
+ type: "ExternalPayment";
+ externalPaymentMethodId: ExternalPaymentMethodId;
+ /** ISO-3166 alpha-2 country to echo when initiating this payment. */
+ countryCode?: string;
+ icon?: string;
+ sourceAmount: NavSourceAmount;
+ externalHandoff: NavExternalHandoff;
+};
+
+export type NavExternalPaymentNode =
+ | NavNodeExchange
+ | NavNodeCashApp
+ | NavNodeExternalPayment;
+
+/** Compatibility fallback for nav trees produced by pre-sourceAmount servers. */
+export function getNavSourceAmount(
+ node: NavExternalPaymentNode,
+): NavSourceAmount {
+ if (node.type === "ExternalPayment") return node.sourceAmount;
+ if (node.sourceAmount != null) return node.sourceAmount;
+ return {
+ currency: "USD",
+ currencySymbol: "$",
+ decimals: 2,
+ required: node.requiredUsd,
+ minimum: node.minimumUsd,
+ maximum: node.maximumUsd,
+ };
+}
+
+/** Compatibility fallback for nav trees produced by pre-handoff servers. */
+export function getNavExternalHandoff(
+ node: NavExternalPaymentNode,
+): NavExternalHandoff & {
+ legacyQrPlaceholderDensity?: "short" | "medium" | "long";
+} {
+ if (node.type === "ExternalPayment") return node.externalHandoff;
+ if (node.externalHandoff != null) return node.externalHandoff;
+ return getLegacyNavExternalHandoff(node);
+}
+
+/** Preserve pre-handoff SDK behavior while old backend nav trees remain valid. */
+function getLegacyNavExternalHandoff(
+ node: NavNodeExchange | NavNodeCashApp,
+): NavExternalHandoff & {
+ legacyQrPlaceholderDensity: "short" | "medium";
+} {
+ const exchangeId = node.type === "CashApp" ? "CashApp" : node.exchangeId;
+ return {
+ desktopBehavior:
+ exchangeId === "Coinbase" || exchangeId === "MtPelerin" ? "popup" : "qr",
+ legacyQrPlaceholderDensity:
+ exchangeId === "Binance" ||
+ exchangeId === "BinanceUSDC" ||
+ exchangeId === "BinanceUSDT"
+ ? "medium"
+ : "short",
+ };
+}
+
+/** Format an amount exactly as required by the backend-owned source policy. */
+export function formatNavSourceAmountUnits(
+ amount: number,
+ sourceAmount: NavSourceAmount,
+): string {
+ if (
+ !Number.isFinite(amount) ||
+ !Number.isInteger(sourceAmount.decimals) ||
+ sourceAmount.decimals < 0 ||
+ sourceAmount.decimals > 20
+ ) {
+ throw new Error("invalid external payment amount");
+ }
+ return amount.toFixed(sourceAmount.decimals);
+}
+
export type NavNodeStripe = NavNodeCommon & {
type: "Stripe";
icon?: string;
@@ -144,6 +259,7 @@ export type NavNode =
| NavNodeDeeplink
| NavNodeExchange
| NavNodeCashApp
+ | NavNodeExternalPayment
| NavNodeStripe
| NavNodeTronDeposit
| NavNodeConnectedWallet
diff --git a/packages/sdk/src/web/components/DaimoModal.tsx b/packages/sdk/src/web/components/DaimoModal.tsx
index c0816322..99ec44f5 100644
--- a/packages/sdk/src/web/components/DaimoModal.tsx
+++ b/packages/sdk/src/web/components/DaimoModal.tsx
@@ -19,14 +19,14 @@ import type {
NavLocationOption,
RecreateSessionWithNavResponse,
} from "../api/index.js";
+import { getNavSourceAmount } from "../api/navTree.js";
import type {
+ NavExternalPaymentNode,
NavNode,
- NavNodeCashApp,
NavNodeChooseOption,
NavNodeConnectedWallet,
NavNodeDeeplink,
NavNodeDepositAddress,
- NavNodeExchange,
NavNodeStripe,
NavNodeTronDeposit,
SessionWithNav,
@@ -67,7 +67,7 @@ import { ChooseWalletPage } from "./ChooseWalletPage.js";
import { ConfirmationPage } from "./ConfirmationPage.js";
import { EmbeddedContainer, ModalContainer } from "./containers.js";
import { DeeplinkPage } from "./DeeplinkPage.js";
-import { ExchangePage } from "./ExchangePage.js";
+import { ExternalPaymentFlowPage } from "./ExternalPaymentFlowPage.js";
import { ExpiredPage } from "./ExpiredPage.js";
import { AccountPaymentInstructionsPage } from "./account/AccountBankDetailsPage.js";
import { AccountInstitutionPickerPage } from "./account/AccountBankPickerPage.js";
@@ -108,8 +108,6 @@ import { QRCode } from "./QRCode.js";
import { WaitingDepositAddressPage } from "./WaitingDepositAddressPage.js";
import { WalletAmountPage } from "./WalletAmountPage.js";
-type ExchangeLikeNode = NavNodeExchange | NavNodeCashApp;
-
export type DaimoModalLocalizationProps = {
/** Override country used to localize auto payment-method sessions. */
countryCode?: DaimoCountryCode;
@@ -821,8 +819,8 @@ function renderEntry(
return renderWaitingDeposit(entry, ctx);
case "waiting-tron":
return renderWaitingTron(entry, ctx);
- case "exchange-page":
- return renderExchangePage(entry, ctx);
+ case "external-payment":
+ return renderExternalPayment(entry, ctx);
case "stripe-onramp":
return renderStripeOnramp(entry, ctx);
case "wallet-choose-chain":
@@ -1113,8 +1111,8 @@ function renderSelectAmount(
return (
);
}
- if (entry.flowType === "exchange") {
- const exchangeNode = node as NavNodeExchange;
- return (
-
- );
- }
- if (entry.flowType === "cashapp") {
- const cashAppNode = node as NavNodeCashApp;
+ if (
+ entry.flowType === "exchange" ||
+ entry.flowType === "cashapp" ||
+ entry.flowType === "external"
+ ) {
+ if (
+ node.type !== "Exchange" &&
+ node.type !== "CashApp" &&
+ node.type !== "ExternalPayment"
+ ) {
+ return null;
+ }
+ const sourceAmount = getNavSourceAmount(node);
return (
);
return (
- void;
- onRetry?: () => void;
- baseUrl: string;
-};
-
-export function ExchangePage({
- node,
- platform,
- exchangeUrl,
- waitingMessage,
- isLoading,
- onBack,
- baseUrl,
-}: ExchangePageProps) {
- const exchangeId = node.type === "CashApp" ? "CashApp" : node.exchangeId;
- const desktopBehavior =
- exchangeId === "Coinbase" || exchangeId === "MtPelerin" ? "popup" : "qr";
- const usesDesktopQR = isDesktop(platform) && desktopBehavior === "qr";
-
- const isBinanceExchange =
- exchangeId === "Binance" ||
- exchangeId === "BinanceUSDC" ||
- exchangeId === "BinanceUSDT";
- const placeholderDensity = isBinanceExchange
- ? ("medium" as const)
- : ("short" as const);
-
- return (
-
- );
-}
diff --git a/packages/sdk/src/web/components/ExternalPaymentFlowPage.tsx b/packages/sdk/src/web/components/ExternalPaymentFlowPage.tsx
new file mode 100644
index 00000000..de2e480b
--- /dev/null
+++ b/packages/sdk/src/web/components/ExternalPaymentFlowPage.tsx
@@ -0,0 +1,77 @@
+import type { ExternalPaymentQuote } from "../../common/api.js";
+import { getNavExternalHandoff } from "../api/navTree.js";
+import type { NavExternalPaymentNode } from "../api/navTree.js";
+
+import { t } from "../hooks/locale.js";
+import { isDesktop, type DaimoPlatform } from "../platform.js";
+import { ExternalPaymentPage } from "./ExternalPaymentPage.js";
+
+type ExternalPaymentFlowPageProps = {
+ node: NavExternalPaymentNode;
+ platform: DaimoPlatform;
+ paymentUrl?: string;
+ waitingMessage?: string;
+ expiresAt?: number;
+ quote?: ExternalPaymentQuote;
+ isLoading?: boolean;
+ onBack: () => void;
+ onRetry?: () => void;
+ baseUrl: string;
+};
+
+export function ExternalPaymentFlowPage({
+ node,
+ platform,
+ paymentUrl,
+ waitingMessage,
+ quote,
+ isLoading,
+ onBack,
+ baseUrl,
+}: ExternalPaymentFlowPageProps) {
+ const handoff = getNavExternalHandoff(node);
+ const { desktopBehavior } = handoff;
+ const usesDesktopQR = isDesktop(platform) && desktopBehavior === "qr";
+
+ return (
+ ({
+ id: `fee-${fee.kind}-${index}`,
+ label:
+ fee.kind === "service"
+ ? t.serviceFee
+ : fee.kind === "network"
+ ? t.networkFee
+ : t.partnerFee,
+ value: `${fee.amount.units} ${fee.amount.currency}`,
+ })),
+ ]
+ }
+ />
+ );
+}
diff --git a/packages/sdk/src/web/components/ExternalPaymentPage.tsx b/packages/sdk/src/web/components/ExternalPaymentPage.tsx
index 4f2ae847..320717a2 100644
--- a/packages/sdk/src/web/components/ExternalPaymentPage.tsx
+++ b/packages/sdk/src/web/components/ExternalPaymentPage.tsx
@@ -15,6 +15,12 @@ import {
type QRDensity = "short" | "medium" | "long";
type DesktopBehavior = "popup" | "qr";
+type ExternalPaymentDetail = {
+ id: string;
+ label: string;
+ value: string;
+};
+
type ExternalPaymentPageProps = {
title: string;
platform: DaimoPlatform;
@@ -29,6 +35,7 @@ type ExternalPaymentPageProps = {
popupName?: string;
popupFeatures?: string;
placeholderDensity?: QRDensity;
+ details?: ExternalPaymentDetail[];
/** Called with the opened window (null when blocked by the browser). */
onOpened?: (popup: Window | null) => void;
};
@@ -50,6 +57,7 @@ export function ExternalPaymentPage({
popupName,
popupFeatures = DEFAULT_POPUP_FEATURES,
placeholderDensity,
+ details,
onOpened,
}: ExternalPaymentPageProps) {
const desktop = isDesktop(platform);
@@ -111,6 +119,23 @@ export function ExternalPaymentPage({
{message}
)}
+ {details != null && details.length > 0 && (
+
+ {details.map((detail) => (
+
+
-
+ {detail.label}
+
+ -
+ {detail.value}
+
+
+ ))}
+
+ )}
{!showQR && (
void;
- onContinue: (amountUsd: number) => void;
+ onContinue: (amount: number) => void;
isLoading?: boolean;
error?: string | null;
baseUrl: string;
@@ -30,8 +34,10 @@ type SelectAmountPageProps = {
export function SelectAmountPage({
node,
- minimumUsd,
- maximumUsd,
+ minimum,
+ maximum,
+ decimals,
+ currencySymbol,
tokenSuffix,
chainId,
onBack,
@@ -40,11 +46,7 @@ export function SelectAmountPage({
error,
baseUrl,
}: SelectAmountPageProps) {
- const {
- amount: amountUsd,
- isValid,
- handleChange,
- } = useAmountInput(minimumUsd, maximumUsd);
+ const { amount, isValid, handleChange } = useAmountInput(minimum, maximum);
// Create pseudo-token for display if tokenSuffix is USDC or USDT and chainId is provided
const selectedToken =
@@ -95,8 +97,10 @@ export function SelectAmountPage({
{/* Amount input */}
@@ -110,7 +114,7 @@ export function SelectAmountPage({
)}
isValid && !isLoading && onContinue(amountUsd)}
+ onClick={() => isValid && !isLoading && onContinue(amount)}
disabled={!isValid || isLoading}
className="daimo-max-w-none"
>
diff --git a/packages/sdk/src/web/components/shared.tsx b/packages/sdk/src/web/components/shared.tsx
index b6f4fadf..8a96ce6a 100644
--- a/packages/sdk/src/web/components/shared.tsx
+++ b/packages/sdk/src/web/components/shared.tsx
@@ -102,6 +102,8 @@ function useCopyToClipboard(resetDelayMs = 1500) {
type AmountInputProps = {
minimum: number;
maximum: number;
+ /** Maximum source-currency decimal places. Defaults to 2. */
+ decimals?: number;
/** Currency symbol prefix (e.g., "$", "CA$"). Defaults to "$". */
currencySymbol?: string;
/** Label shown below input (e.g., "Balance: $X.XX" or "Minimum $X.XX") */
@@ -121,6 +123,7 @@ type AmountInputProps = {
export function AmountInput({
minimum,
maximum,
+ decimals = 2,
currencySymbol = "$",
defaultLabel,
initialValue,
@@ -149,7 +152,7 @@ export function AmountInput({
const handleInputChange = (e: React.ChangeEvent) => {
const value = parseDisplayAmount(e.target.value);
- if (!isValidAmountInput(value, 2)) return;
+ if (!isValidAmountInput(value, decimals)) return;
setInputValue(value);
const newAmount = parseFloat(value) || 0;
@@ -164,18 +167,20 @@ export function AmountInput({
};
const displayValue = formatAmountInput(inputValue);
- const placeholder = formatAmountInput("0.00");
+ const placeholder = formatAmountInput(
+ decimals === 0 ? "0" : `0.${"0".repeat(decimals)}`,
+ );
const inputWidth =
displayValue.length === 0
? "3.55ch"
: `${Math.min(displayValue.length - (displayValue.match(/\./g) || []).length * 0.55, 12)}ch`;
const label = showMinWarning
- ? `${t.minimum} ${currencySymbol}${formatFixedAmount(minimum)}`
+ ? `${t.minimum} ${currencySymbol}${formatFixedAmount(minimum, decimals)}`
: showMaxWarning
- ? `${t.maximum} ${currencySymbol}${formatFixedAmount(maximum)}`
+ ? `${t.maximum} ${currencySymbol}${formatFixedAmount(maximum, decimals)}`
: (defaultLabel ??
- `${t.minimum} ${currencySymbol}${formatFixedAmount(minimum)}`);
+ `${t.minimum} ${currencySymbol}${formatFixedAmount(minimum, decimals)}`);
const labelClass =
showMinWarning || showMaxWarning
diff --git a/packages/sdk/src/web/hooks/locales/en.ts b/packages/sdk/src/web/hooks/locales/en.ts
index 4e8f781e..cd315e31 100644
--- a/packages/sdk/src/web/hooks/locales/en.ts
+++ b/packages/sdk/src/web/hooks/locales/en.ts
@@ -23,11 +23,15 @@ export const en = {
mobileWallets: "Mobile Wallets",
scanWithPhone: "Scan with your phone to open wallet",
- // ExchangePage
+ // ExternalPaymentFlowPage
continueTo: "Continue to",
toCompleteYourDeposit: "to complete your deposit",
open: "Open",
refreshInvoice: "Refresh",
+ estimatedOutput: "Estimated output",
+ serviceFee: "Service fee",
+ networkFee: "Network fee",
+ partnerFee: "Partner fee",
// SelectAmountPage
selectAmount: "Select Amount",
diff --git a/packages/sdk/src/web/hooks/locales/es.ts b/packages/sdk/src/web/hooks/locales/es.ts
index 2d937b99..b29cd6e5 100644
--- a/packages/sdk/src/web/hooks/locales/es.ts
+++ b/packages/sdk/src/web/hooks/locales/es.ts
@@ -23,6 +23,10 @@ export const es: typeof en = {
toCompleteYourDeposit: "para completar tu depósito",
open: "Abrir",
refreshInvoice: "Actualizar",
+ estimatedOutput: "Importe estimado",
+ serviceFee: "Comisión de servicio",
+ networkFee: "Comisión de red",
+ partnerFee: "Comisión del socio",
selectAmount: "Seleccionar monto",
loading: "Cargando",
continue: "Continuar",
diff --git a/packages/sdk/src/web/hooks/locales/ja.ts b/packages/sdk/src/web/hooks/locales/ja.ts
index d3f675e6..a3c3b50a 100644
--- a/packages/sdk/src/web/hooks/locales/ja.ts
+++ b/packages/sdk/src/web/hooks/locales/ja.ts
@@ -25,11 +25,15 @@ export const ja: typeof en = {
mobileWallets: "モバイルウォレット",
scanWithPhone: "スマートフォンでスキャンしてウォレットを開く",
- // ExchangePage
+ // ExternalPaymentFlowPage
continueTo: "",
toCompleteYourDeposit: "で入金を完了してください",
open: "開く",
refreshInvoice: "更新",
+ estimatedOutput: "受取額(見込み)",
+ serviceFee: "サービス手数料",
+ networkFee: "ネットワーク手数料",
+ partnerFee: "パートナー手数料",
// SelectAmountPage
selectAmount: "金額を選択",
diff --git a/packages/sdk/src/web/hooks/locales/ko.ts b/packages/sdk/src/web/hooks/locales/ko.ts
index d6b075a5..25501e4e 100644
--- a/packages/sdk/src/web/hooks/locales/ko.ts
+++ b/packages/sdk/src/web/hooks/locales/ko.ts
@@ -25,11 +25,15 @@ export const ko: typeof en = {
mobileWallets: "모바일 지갑",
scanWithPhone: "휴대폰으로 스캔하여 지갑 열기",
- // ExchangePage
+ // ExternalPaymentFlowPage
continueTo: "",
toCompleteYourDeposit: "에서 입금을 완료하세요",
open: "열기",
refreshInvoice: "새로고침",
+ estimatedOutput: "예상 수령액",
+ serviceFee: "서비스 수수료",
+ networkFee: "네트워크 수수료",
+ partnerFee: "파트너 수수료",
// SelectAmountPage
selectAmount: "금액 선택",
diff --git a/packages/sdk/src/web/hooks/locales/pt.ts b/packages/sdk/src/web/hooks/locales/pt.ts
index 8eb81e44..0e0cfb16 100644
--- a/packages/sdk/src/web/hooks/locales/pt.ts
+++ b/packages/sdk/src/web/hooks/locales/pt.ts
@@ -25,11 +25,15 @@ export const pt: typeof en = {
mobileWallets: "Carteiras móveis",
scanWithPhone: "Escaneie com seu telefone para abrir a carteira",
- // ExchangePage
+ // ExternalPaymentFlowPage
continueTo: "Continuar para",
toCompleteYourDeposit: "para concluir seu depósito",
open: "Abrir",
refreshInvoice: "Atualizar",
+ estimatedOutput: "Valor estimado",
+ serviceFee: "Taxa de serviço",
+ networkFee: "Taxa de rede",
+ partnerFee: "Taxa do parceiro",
// SelectAmountPage
selectAmount: "Selecionar valor",
diff --git a/packages/sdk/src/web/hooks/locales/zh.ts b/packages/sdk/src/web/hooks/locales/zh.ts
index 68341f5f..fcc5d3ac 100644
--- a/packages/sdk/src/web/hooks/locales/zh.ts
+++ b/packages/sdk/src/web/hooks/locales/zh.ts
@@ -25,11 +25,15 @@ export const zh: typeof en = {
mobileWallets: "移动钱包",
scanWithPhone: "用手机扫码打开钱包",
- // ExchangePage
+ // ExternalPaymentFlowPage
continueTo: "前往",
toCompleteYourDeposit: "以完成充值",
open: "打开",
refreshInvoice: "刷新",
+ estimatedOutput: "预计到账金额",
+ serviceFee: "服务费",
+ networkFee: "网络费",
+ partnerFee: "合作伙伴费用",
// SelectAmountPage
selectAmount: "选择金额",
diff --git a/packages/sdk/src/web/hooks/navEvent.ts b/packages/sdk/src/web/hooks/navEvent.ts
index 20178ff3..d2f224a9 100644
--- a/packages/sdk/src/web/hooks/navEvent.ts
+++ b/packages/sdk/src/web/hooks/navEvent.ts
@@ -17,11 +17,16 @@ export type NavEventAction =
| { action: "nav_select"; targetNodeId: string; targetNodeType: NavNodeType }
| { action: "nav_back" }
| { action: "nav_deeplink"; url: string }
- | { action: "flow_amount_continue"; amountUsd: number }
+ | ({
+ action: "flow_amount_continue";
+ } & (
+ | { amountUsd: number }
+ | { sourceAmountUnits: string; sourceCurrency: string }
+ ))
| { action: "flow_refresh" }
| {
- action: "flow_exchange_url";
- exchangeId: string;
+ action: "flow_external_payment";
+ paymentMethodId: string;
success: boolean;
url?: string;
error?: string;
diff --git a/packages/sdk/src/web/hooks/types.ts b/packages/sdk/src/web/hooks/types.ts
index 39b9af9b..e5877339 100644
--- a/packages/sdk/src/web/hooks/types.ts
+++ b/packages/sdk/src/web/hooks/types.ts
@@ -4,6 +4,7 @@ import type {
AccountRail,
DepositPaymentInteraction,
} from "../../common/account.js";
+import type { ExternalPaymentQuote } from "../../common/api.js";
import type { NavNode, SessionWithNav } from "../api/navTree.js";
import type { WalletPaymentOption } from "../api/walletTypes.js";
@@ -26,7 +27,13 @@ export type NavEntry =
| {
type: "select-amount";
nodeId: string;
- flowType: "deposit" | "tron" | "exchange" | "cashapp" | "stripe";
+ flowType:
+ | "deposit"
+ | "tron"
+ | "exchange"
+ | "cashapp"
+ | "external"
+ | "stripe";
autoNav?: boolean;
}
| {
@@ -49,12 +56,13 @@ export type NavEntry =
autoNav?: boolean;
}
| {
- type: "exchange-page";
+ type: "external-payment";
nodeId: string;
- amountUsd: number;
- exchangeUrl?: string;
+ sourceAmount: number;
+ paymentUrl?: string;
waitingMessage?: string;
expiresAt?: number;
+ quote?: ExternalPaymentQuote;
error?: string;
autoNav?: boolean;
}
diff --git a/packages/sdk/src/web/hooks/useSessionNav.ts b/packages/sdk/src/web/hooks/useSessionNav.ts
index 8a5cb5c1..95648d2c 100644
--- a/packages/sdk/src/web/hooks/useSessionNav.ts
+++ b/packages/sdk/src/web/hooks/useSessionNav.ts
@@ -11,17 +11,23 @@ import type {
AccountDepositStatus,
AccountRail,
} from "../../common/account.js";
-import type { ExchangeId } from "../../common/api.js";
+import type {
+ CreatePaymentMethodRequest,
+ ExchangeId,
+} from "../../common/api.js";
import type {
AccountAuthConfig,
DaimoCountryCode,
RecreateSessionWithNavResponse,
} from "../api/index.js";
+import {
+ formatNavSourceAmountUnits,
+ getNavSourceAmount,
+} from "../api/navTree.js";
import type {
+ NavExternalPaymentNode,
NavNode,
- NavNodeCashApp,
NavNodeChooseOption,
- NavNodeExchange,
NavNodeFiat,
NavNodeStripe,
NavNodeTronDeposit,
@@ -55,7 +61,6 @@ import type { InjectedWallet } from "./useInjectedWallets.js";
import { isUserRejection, type WalletFlowResult } from "./useWalletFlow.js";
type NodeContext = { nodeId: string | null; nodeType: NavNodeType | null };
-type ExchangeNode = NavNodeExchange | NavNodeCashApp;
type SessionNavResult = {
stack: NavEntry[];
@@ -66,7 +71,7 @@ type SessionNavResult = {
handleNavigate: (nodeId: string, options?: { autoNav?: boolean }) => void;
handleBack: () => void;
handleReset: () => void;
- handleAmountContinue: (amountUsd: number) => void;
+ handleAmountContinue: (amount: number) => void;
handleRetry: () => void;
handleRefresh: () => Promise;
handleAccountSessionRecreate: (depositAmount: string) => Promise;
@@ -87,8 +92,14 @@ type SessionNavResult = {
handleAccountLogout: () => void;
};
-function isExchangeNode(node: NavNode | null): node is ExchangeNode {
- return node?.type === "Exchange" || node?.type === "CashApp";
+function isExternalPaymentNode(
+ node: NavNode | null,
+): node is NavExternalPaymentNode {
+ return (
+ node?.type === "Exchange" ||
+ node?.type === "CashApp" ||
+ node?.type === "ExternalPayment"
+ );
}
function isStripeNode(node: NavNode | null): node is NavNodeStripe {
@@ -99,14 +110,41 @@ function isTrustTronNode(node: NavNode | null): node is NavNodeTronDeposit {
return node?.type === "TronDeposit" && node.id === "Trust-Tron";
}
-function getExchangeSelection(node: ExchangeNode): {
- exchangeId: ExchangeId;
- nodeType: "Exchange" | "CashApp";
-} {
+function getExternalPaymentSelection(node: NavExternalPaymentNode):
+ | {
+ kind: "exchange";
+ exchangeId: ExchangeId;
+ nodeType: "Exchange" | "CashApp";
+ }
+ | {
+ kind: "external";
+ externalPaymentMethodId: "CashApp" | "Revolut";
+ countryCode?: string;
+ nodeType: "CashApp" | "ExternalPayment";
+ } {
+ if (node.type === "ExternalPayment") {
+ return {
+ kind: "external",
+ externalPaymentMethodId: node.externalPaymentMethodId,
+ countryCode: node.countryCode,
+ nodeType: "ExternalPayment",
+ };
+ }
if (node.type === "CashApp") {
- return { exchangeId: "CashApp", nodeType: "CashApp" };
+ if (node.externalPaymentMethodId != null) {
+ return {
+ kind: "external",
+ externalPaymentMethodId: node.externalPaymentMethodId,
+ nodeType: "CashApp",
+ };
+ }
+ return { kind: "exchange", exchangeId: "CashApp", nodeType: "CashApp" };
}
- return { exchangeId: node.exchangeId, nodeType: "Exchange" };
+ return {
+ kind: "exchange",
+ exchangeId: node.exchangeId,
+ nodeType: "Exchange",
+ };
}
function replacePendingAccountEntry(
@@ -266,65 +304,93 @@ export function useSessionNav(
],
);
- const fetchExchangeUrl = useCallback(
- async (
- nodeId: string,
- exchangeId: ExchangeId,
- amountUsd: number,
- nodeType: "Exchange" | "CashApp",
- ) => {
+ const fetchExternalPayment = useCallback(
+ async (nodeId: string, node: NavExternalPaymentNode, amount: number) => {
+ const selection = getExternalPaymentSelection(node);
+ const sourceAmount = getNavSourceAmount(node);
+ const sourceAmountUnits = formatNavSourceAmountUnits(
+ amount,
+ sourceAmount,
+ );
+ const paymentMethod: CreatePaymentMethodRequest["paymentMethod"] =
+ selection.kind === "external"
+ ? {
+ type: "external",
+ id: selection.externalPaymentMethodId,
+ countryCode: selection.countryCode,
+ sourceAmount: {
+ units: sourceAmountUnits,
+ currency: sourceAmount.currency,
+ },
+ platform: effectivePlatform,
+ }
+ : {
+ type: "exchange",
+ exchangeId: selection.exchangeId,
+ amountUsd: amount,
+ platform: effectivePlatform,
+ };
try {
const result = await client.sessions.paymentMethods.create(
session.sessionId,
{
clientSecret: session.clientSecret,
- paymentMethod: {
- type: "exchange",
- exchangeId,
- amountUsd,
- platform: effectivePlatform,
- },
+ paymentMethod,
},
);
- if (!result.exchange) return;
+ const payment = result.externalPayment ?? result.exchange;
+ if (payment == null) {
+ throw new Error("external payment details not returned");
+ }
+ const paymentMethodId =
+ selection.kind === "external"
+ ? selection.externalPaymentMethodId
+ : selection.exchangeId;
logNavEvent(session.sessionId, session.clientSecret, {
nodeId,
- nodeType,
- action: "flow_exchange_url",
- exchangeId,
+ nodeType: selection.nodeType,
+ action: "flow_external_payment",
+ paymentMethodId,
success: true,
- url: result.exchange.url,
+ url: payment.url,
});
setStack((prev) => {
const top = prev[prev.length - 1];
- if (top?.type !== "exchange-page" || top.nodeId !== nodeId)
+ if (top?.type !== "external-payment" || top.nodeId !== nodeId)
return prev;
return [
...prev.slice(0, -1),
{
...top,
- exchangeUrl: result.exchange!.url,
- waitingMessage: result.exchange!.waitingMessage,
- expiresAt: result.exchange!.expiresAt,
+ paymentUrl: payment.url,
+ waitingMessage: payment.waitingMessage,
+ expiresAt: payment.expiresAt,
+ quote: result.externalPayment?.quote,
error: undefined,
},
];
});
} catch (error) {
- console.error("failed to get exchange url:", error);
+ console.error("failed to create external payment:", error);
const errorMsg =
- error instanceof Error ? error.message : "failed to get exchange url";
+ error instanceof Error
+ ? error.message
+ : "failed to create external payment";
+ const paymentMethodId =
+ selection.kind === "external"
+ ? selection.externalPaymentMethodId
+ : selection.exchangeId;
logNavEvent(session.sessionId, session.clientSecret, {
nodeId,
- nodeType,
- action: "flow_exchange_url",
- exchangeId,
+ nodeType: selection.nodeType,
+ action: "flow_external_payment",
+ paymentMethodId,
success: false,
error: errorMsg,
});
setStack((prev) => {
const top = prev[prev.length - 1];
- if (top?.type !== "exchange-page" || top.nodeId !== nodeId)
+ if (top?.type !== "external-payment" || top.nodeId !== nodeId)
return prev;
return [...prev.slice(0, -1), { ...top, error: errorMsg }];
});
@@ -728,15 +794,19 @@ export function useSessionNav(
return;
}
- if (isExchangeNode(targetNode)) {
- const { exchangeId, nodeType } = getExchangeSelection(targetNode);
- const requiredUsd = targetNode.requiredUsd ?? 0;
- if (requiredUsd > 0) {
+ if (isExternalPaymentNode(targetNode)) {
+ const required = getNavSourceAmount(targetNode).required ?? 0;
+ if (required > 0) {
setStack((prev) => [
...prev,
- { type: "exchange-page", nodeId, amountUsd: requiredUsd, autoNav },
+ {
+ type: "external-payment",
+ nodeId,
+ sourceAmount: required,
+ autoNav,
+ },
]);
- fetchExchangeUrl(nodeId, exchangeId, requiredUsd, nodeType);
+ fetchExternalPayment(nodeId, targetNode, required);
return;
}
setStack((prev) => [
@@ -744,7 +814,12 @@ export function useSessionNav(
{
type: "select-amount",
nodeId,
- flowType: targetNode.type === "CashApp" ? "cashapp" : "exchange",
+ flowType:
+ targetNode.type === "CashApp"
+ ? "cashapp"
+ : targetNode.type === "ExternalPayment"
+ ? "external"
+ : "exchange",
autoNav,
},
]);
@@ -782,7 +857,7 @@ export function useSessionNav(
session.sessionId,
getNodeCtx,
fetchTronAddress,
- fetchExchangeUrl,
+ fetchExternalPayment,
fetchStripeOnramp,
handleAccountNavigate,
enableFiatPopup,
@@ -829,9 +904,26 @@ export function useSessionNav(
// ─── Flow handlers ──────────────────────────────────────────────────────
const handleAmountContinue = useCallback(
- (amountUsd: number) => {
+ (amount: number) => {
if (!topEntry || topEntry.type !== "select-amount") return;
const { nodeId, flowType } = topEntry;
+ const selectedNode = findNode(nodeId, session.navTree);
+ const selectedSourceAmount = isExternalPaymentNode(selectedNode)
+ ? getNavSourceAmount(selectedNode)
+ : null;
+ const amountContext =
+ (flowType === "exchange" ||
+ flowType === "cashapp" ||
+ flowType === "external") &&
+ selectedSourceAmount != null
+ ? {
+ sourceAmountUnits: formatNavSourceAmountUnits(
+ amount,
+ selectedSourceAmount,
+ ),
+ sourceCurrency: selectedSourceAmount.currency,
+ }
+ : { amountUsd: amount };
logNavEvent(session.sessionId, session.clientSecret, {
nodeId,
@@ -842,41 +934,44 @@ export function useSessionNav(
? "TronDeposit"
: flowType === "cashapp"
? "CashApp"
- : flowType === "stripe"
- ? "Stripe"
- : "Exchange",
+ : flowType === "external"
+ ? "ExternalPayment"
+ : flowType === "stripe"
+ ? "Stripe"
+ : "Exchange",
action: "flow_amount_continue",
- amountUsd,
+ ...amountContext,
});
if (flowType === "deposit") {
setStack((prev) => [
...prev,
- { type: "waiting-deposit", nodeId, amountUsd },
+ { type: "waiting-deposit", nodeId, amountUsd: amount },
]);
} else if (flowType === "tron") {
setStack((prev) => [
...prev,
- { type: "waiting-tron", nodeId, amountUsd },
+ { type: "waiting-tron", nodeId, amountUsd: amount },
]);
- fetchTronAddress(nodeId, amountUsd);
- } else if (flowType === "exchange" || flowType === "cashapp") {
- const node = findNode(nodeId, session.navTree);
- if (!isExchangeNode(node)) return;
- const { exchangeId, nodeType } = getExchangeSelection(node);
+ fetchTronAddress(nodeId, amount);
+ } else if (
+ flowType === "exchange" ||
+ flowType === "cashapp" ||
+ flowType === "external"
+ ) {
+ if (!isExternalPaymentNode(selectedNode)) return;
setStack((prev) => [
...prev,
- { type: "exchange-page", nodeId, amountUsd },
+ { type: "external-payment", nodeId, sourceAmount: amount },
]);
- fetchExchangeUrl(nodeId, exchangeId, amountUsd, nodeType);
+ fetchExternalPayment(nodeId, selectedNode, amount);
} else if (flowType === "stripe") {
- const node = findNode(nodeId, session.navTree);
- if (!isStripeNode(node)) return;
+ if (!isStripeNode(selectedNode)) return;
setStack((prev) => [
...prev,
- { type: "stripe-onramp", nodeId, amountUsd },
+ { type: "stripe-onramp", nodeId, amountUsd: amount },
]);
- fetchStripeOnramp(nodeId, amountUsd);
+ fetchStripeOnramp(nodeId, amount);
}
},
[
@@ -884,7 +979,7 @@ export function useSessionNav(
session.sessionId,
session.navTree,
fetchTronAddress,
- fetchExchangeUrl,
+ fetchExternalPayment,
fetchStripeOnramp,
],
);
@@ -956,29 +1051,23 @@ export function useSessionNav(
return;
}
- if (topEntry.type === "exchange-page") {
+ if (topEntry.type === "external-payment") {
const node = findNode(topEntry.nodeId, session.navTree);
- if (!isExchangeNode(node)) return;
- const { exchangeId, nodeType } = getExchangeSelection(node);
+ if (!isExternalPaymentNode(node)) return;
setStack((prev) => {
const top = prev[prev.length - 1];
- if (top?.type !== "exchange-page") return prev;
+ if (top?.type !== "external-payment") return prev;
return [
...prev.slice(0, -1),
{
...top,
- exchangeUrl: undefined,
+ paymentUrl: undefined,
expiresAt: undefined,
error: undefined,
},
];
});
- fetchExchangeUrl(
- topEntry.nodeId,
- exchangeId,
- topEntry.amountUsd,
- nodeType,
- );
+ fetchExternalPayment(topEntry.nodeId, node, topEntry.sourceAmount);
return;
}
@@ -1003,7 +1092,7 @@ export function useSessionNav(
topEntry,
session.navTree,
fetchTronAddress,
- fetchExchangeUrl,
+ fetchExternalPayment,
fetchStripeOnramp,
doWalletSend,
]);