From f7f529c00c86136985d4a1135acd060abb1d114c Mon Sep 17 00:00:00 2001 From: Anthony Ronning <101225832+AnthonyRonning@users.noreply.github.com> Date: Tue, 27 Oct 2026 22:23:13 +0000 Subject: [PATCH] fix: return mobile billing portals to the canonical app host --- .../frontend/src/billing/billingApi.ts | 4 +-- .../frontend/src/billing/returnOrigin.test.ts | 25 ++++++++++++++++++- .../frontend/src/billing/returnOrigin.ts | 11 +++++++- 3 files changed, 36 insertions(+), 4 deletions(-) diff --git a/apps/maple-research/frontend/src/billing/billingApi.ts b/apps/maple-research/frontend/src/billing/billingApi.ts index 4bb9166be..adb9f7484 100644 --- a/apps/maple-research/frontend/src/billing/billingApi.ts +++ b/apps/maple-research/frontend/src/billing/billingApi.ts @@ -1,5 +1,5 @@ import { isAndroid, isMobile, isTauri } from "@/utils/platform"; -import { billingReturnOrigin } from "./returnOrigin"; +import { billingPortalReturnOrigin } from "./returnOrigin"; import { isAllowedZapriteCheckoutUrl, parseBillingApiError, @@ -177,7 +177,7 @@ export async function fetchProducts(version?: string): Promise export async function fetchPortalUrl(thirdPartyToken: string): Promise { assertPurchasingAvailable(); const requestBody = { - return_url: billingReturnOrigin() + return_url: billingPortalReturnOrigin() }; const response = await fetch( diff --git a/apps/maple-research/frontend/src/billing/returnOrigin.test.ts b/apps/maple-research/frontend/src/billing/returnOrigin.test.ts index 2326cc94b..a5f1e4f61 100644 --- a/apps/maple-research/frontend/src/billing/returnOrigin.test.ts +++ b/apps/maple-research/frontend/src/billing/returnOrigin.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"; import * as platform from "@/utils/platform"; import { fetchPortalUrl } from "./billingApi"; -import { billingReturnOrigin } from "./returnOrigin"; +import { billingPortalReturnOrigin, billingReturnOrigin } from "./returnOrigin"; describe("billing browser returns", () => { const previousBillingApi = process.env.VITE_MAPLE_BILLING_API_URL; @@ -67,6 +67,7 @@ describe("billing browser returns", () => { Object.defineProperty(window.location, "origin", { value: origin, configurable: true }); process.env.VITE_MAPLE_BILLING_API_URL = "https://billing-dev.opensecret.cloud"; expect(billingReturnOrigin()).toBe(origin); + expect(billingPortalReturnOrigin()).toBe(origin); }); } @@ -75,4 +76,26 @@ describe("billing browser returns", () => { process.env.VITE_MAPLE_BILLING_API_URL = "https://billing-dev.opensecret.cloud"; expect(billingReturnOrigin()).toBe("https://trymaple.ai"); }); + + for (const [api, origin] of [ + ["https://billing-dev.opensecret.cloud", "https://app-dev.maple.ai"], + ["https://billing-dev.opensecret.cloud/", "https://app-dev.maple.ai"], + ["https://billing.opensecret.cloud", "https://app.maple.ai"] + ]) { + test(`mobile portal returns directly to ${origin} without changing payment links`, async () => { + spyOn(platform, "isMobile").mockReturnValue(true); + process.env.VITE_MAPLE_BILLING_API_URL = api; + const request = spyOn(globalThis, "fetch").mockResolvedValue( + Response.json({ portal_url: "https://billing.stripe.com/p/session/test" }) + ); + cleanup.push(() => request.mockRestore()); + + await fetchPortalUrl("fixture-token"); + expect(request).toHaveBeenCalledTimes(1); + expect(JSON.parse(String(request.mock.calls[0][1]?.body))).toEqual({ + return_url: origin + }); + expect(billingReturnOrigin()).toBe("https://trymaple.ai"); + }); + } }); diff --git a/apps/maple-research/frontend/src/billing/returnOrigin.ts b/apps/maple-research/frontend/src/billing/returnOrigin.ts index 89856fe58..e9946d3e9 100644 --- a/apps/maple-research/frontend/src/billing/returnOrigin.ts +++ b/apps/maple-research/frontend/src/billing/returnOrigin.ts @@ -8,7 +8,16 @@ export function billingReturnOrigin(): string { // Keep the existing Universal/App Link host for mobile clients. if (isMobile()) return "https://trymaple.ai"; - // PR desktop builds use hosted Dev billing without the mobile Dev app variant. + return nativeAppOrigin(); +} + +/** Portal exits stay on the website and do not need a mobile payment deep link. */ +export function billingPortalReturnOrigin(): string { + return isTauri() ? nativeAppOrigin() : window.location.origin; +} + +function nativeAppOrigin(): string { + // PR builds can use hosted Dev billing without the mobile Dev app variant. return import.meta.env.VITE_MAPLE_BILLING_API_URL?.replace(/\/+$/, "") === "https://billing-dev.opensecret.cloud" ? "https://app-dev.maple.ai"