From 040a3806cb4fdc6d16f6ec0d78ca84ef83ae9e26 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:30:45 +0000 Subject: [PATCH 1/4] fix: use Tauri opener plugin for all Tauri platforms, not just mobile On desktop Tauri, window.open() and window.location.href don't work properly to open external URLs in the system browser. The code was only using the Tauri opener plugin for mobile platforms (isMobile()), but falling back to window.open() for desktop Tauri. Changed the condition from isMobile() to isTauri() in all billing- related URL opening code so the opener plugin is used for both desktop and mobile Tauri platforms. Web browsers continue to use window.open() / window.location.href as before. Files fixed: - AccountMenu.tsx: handleManageSubscription - TeamInviteDialog.tsx: handleManageSubscription - pricing.tsx: handleButtonClick portal URL section - billingApi.ts: createCheckoutSession, createZapriteCheckoutSession - ApiCreditsSection.tsx: handlePurchase checkout URLs Co-Authored-By: unknown <> --- frontend/src/billing/billingApi.ts | 44 ++++++++++++------- frontend/src/components/AccountMenu.tsx | 17 ++++--- .../components/apikeys/ApiCreditsSection.tsx | 34 ++++++++++---- .../src/components/team/TeamInviteDialog.tsx | 8 ++-- frontend/src/routes/pricing.tsx | 17 ++++--- 5 files changed, 78 insertions(+), 42 deletions(-) diff --git a/frontend/src/billing/billingApi.ts b/frontend/src/billing/billingApi.ts index 26bb287cc..2a9bedaa6 100644 --- a/frontend/src/billing/billingApi.ts +++ b/frontend/src/billing/billingApi.ts @@ -1,4 +1,4 @@ -import { isMobile } from "@/utils/platform"; +import { isMobile, isTauri } from "@/utils/platform"; // API Credit Purchase Constants export const MIN_PURCHASE_CREDITS = 10000; @@ -203,24 +203,29 @@ export async function createCheckoutSession( const { checkout_url } = await response.json(); console.log("Redirecting to checkout:", checkout_url); - // For mobile platforms, force external browser for payment (App Store restrictions) - if (isMobile()) { + // For all Tauri platforms (mobile and desktop), use opener plugin to launch external browser + if (isTauri()) { console.log( - "[Billing] Mobile platform detected, using opener plugin to launch external browser" + "[Billing] Tauri platform detected, using opener plugin to launch external browser" ); const { invoke } = await import("@tauri-apps/api/core"); - // Use the opener plugin directly - required for mobile payments + // Use the opener plugin directly - required for Tauri platforms await invoke("plugin:opener|open_url", { url: checkout_url }) .then(() => { console.log("[Billing] Successfully opened URL in external browser"); }) .catch((error: Error) => { console.error("[Billing] Failed to open external browser:", error); - throw new Error( - "Failed to open payment page in external browser. This is required for mobile payments." - ); + if (isMobile()) { + throw new Error( + "Failed to open payment page in external browser. This is required for mobile payments." + ); + } else { + // Fallback to regular navigation on desktop + window.location.href = checkout_url; + } }); // Add a small delay to ensure the browser has time to open @@ -228,7 +233,7 @@ export async function createCheckoutSession( return; } - // Fall back to regular navigation if not on Tauri or if Tauri opener fails + // Fall back to regular navigation for web platforms window.location.href = checkout_url; } @@ -269,24 +274,29 @@ export async function createZapriteCheckoutSession( const { checkout_url } = await response.json(); console.log("Redirecting to Zaprite checkout:", checkout_url); - // For mobile platforms, force external browser for crypto payments - if (isMobile()) { + // For all Tauri platforms (mobile and desktop), use opener plugin to launch external browser + if (isTauri()) { console.log( - "[Billing] Mobile platform detected, using opener plugin to launch external browser" + "[Billing] Tauri platform detected, using opener plugin to launch external browser" ); const { invoke } = await import("@tauri-apps/api/core"); - // Use the opener plugin directly - required for mobile payments + // Use the opener plugin directly - required for Tauri platforms await invoke("plugin:opener|open_url", { url: checkout_url }) .then(() => { console.log("[Billing] Successfully opened URL in external browser"); }) .catch((error: Error) => { console.error("[Billing] Failed to open external browser:", error); - throw new Error( - "Failed to open payment page in external browser. This is required for mobile payments." - ); + if (isMobile()) { + throw new Error( + "Failed to open payment page in external browser. This is required for mobile payments." + ); + } else { + // Fallback to regular navigation on desktop + window.location.href = checkout_url; + } }); // Add a small delay to ensure the browser has time to open @@ -294,7 +304,7 @@ export async function createZapriteCheckoutSession( return; } - // Fall back to regular navigation if not on mobile + // Fall back to regular navigation for web platforms window.location.href = checkout_url; } diff --git a/frontend/src/components/AccountMenu.tsx b/frontend/src/components/AccountMenu.tsx index 25adb3a85..5f8c0e75a 100644 --- a/frontend/src/components/AccountMenu.tsx +++ b/frontend/src/components/AccountMenu.tsx @@ -179,22 +179,27 @@ export function AccountMenu() { const billingService = getBillingService(); const url = await billingService.getPortalUrl(); - // Check if we're on a mobile platform - if (isMobile()) { + // Check if we're on any Tauri platform (mobile or desktop) + if (isTauri()) { console.log( - "[Billing] Mobile platform detected, using opener plugin to launch external browser for portal" + "[Billing] Tauri platform detected, using opener plugin to launch external browser for portal" ); const { invoke } = await import("@tauri-apps/api/core"); - // Use the opener plugin directly - with NO fallback for mobile platforms + // Use the opener plugin directly for all Tauri platforms await invoke("plugin:opener|open_url", { url }) .then(() => { console.log("[Billing] Successfully opened portal URL in external browser"); }) .catch((err: Error) => { console.error("[Billing] Failed to open external browser:", err); - alert("Failed to open browser. Please try again."); + if (isMobile()) { + alert("Failed to open browser. Please try again."); + } else { + // Fallback to window.open on desktop + window.open(url, "_blank"); + } }); // Add a small delay to ensure the browser has time to open @@ -202,7 +207,7 @@ export function AccountMenu() { return; } - // Default browser opening for non-mobile platforms + // Default browser opening for web platforms window.open(url, "_blank"); } catch (error) { console.error("Error fetching portal URL:", error); diff --git a/frontend/src/components/apikeys/ApiCreditsSection.tsx b/frontend/src/components/apikeys/ApiCreditsSection.tsx index 9d1fa0d7a..54fd41f4b 100644 --- a/frontend/src/components/apikeys/ApiCreditsSection.tsx +++ b/frontend/src/components/apikeys/ApiCreditsSection.tsx @@ -7,7 +7,7 @@ import { Loader2, CreditCard, Bitcoin, Coins, CheckCircle, Edit } from "lucide-r import { useQuery } from "@tanstack/react-query"; import { getBillingService } from "@/billing/billingService"; import { useOpenSecret } from "@opensecret/react"; -import { isMobile } from "@/utils/platform"; +import { isMobile, isTauri } from "@/utils/platform"; import { MIN_PURCHASE_CREDITS, MIN_PURCHASE_AMOUNT, @@ -120,10 +120,18 @@ export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSect cancel_url: cancelUrl || successUrl }); - // Redirect to Stripe checkout - // Note: This feature is only exposed on desktop/web (not mobile), where window.location.href works correctly. - // For mobile platforms, we would need special handling with invoke("plugin:opener|open_url"), but that's not needed here. - window.location.href = response.checkout_url; + // For Tauri platforms (desktop and mobile), use opener plugin to open in external browser + if (isTauri()) { + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("plugin:opener|open_url", { url: response.checkout_url }).catch( + (err: Error) => { + console.error("[Credits] Failed to open checkout with Tauri opener:", err); + window.location.href = response.checkout_url; + } + ); + } else { + window.location.href = response.checkout_url; + } } else { // For Zaprite, guest users pass empty string (backend infers from JWT) const response = await billingService.purchaseApiCreditsZaprite({ @@ -132,10 +140,18 @@ export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSect success_url: successUrl }); - // Redirect to Zaprite checkout - // Note: This feature is only exposed on desktop/web (not mobile), where window.location.href works correctly. - // For mobile platforms, we would need special handling with invoke("plugin:opener|open_url"), but that's not needed here. - window.location.href = response.checkout_url; + // For Tauri platforms (desktop and mobile), use opener plugin to open in external browser + if (isTauri()) { + const { invoke } = await import("@tauri-apps/api/core"); + await invoke("plugin:opener|open_url", { url: response.checkout_url }).catch( + (err: Error) => { + console.error("[Credits] Failed to open checkout with Tauri opener:", err); + window.location.href = response.checkout_url; + } + ); + } else { + window.location.href = response.checkout_url; + } } } catch (error) { console.error("Failed to create checkout session:", error); diff --git a/frontend/src/components/team/TeamInviteDialog.tsx b/frontend/src/components/team/TeamInviteDialog.tsx index 40579dbb9..e655f7ad8 100644 --- a/frontend/src/components/team/TeamInviteDialog.tsx +++ b/frontend/src/components/team/TeamInviteDialog.tsx @@ -15,7 +15,7 @@ import { Alert, AlertDescription } from "@/components/ui/alert"; import { Loader2, AlertCircle, UserPlus, Info, CreditCard } from "lucide-react"; import { getBillingService } from "@/billing/billingService"; import { useLocalState } from "@/state/useLocalState"; -import { isMobile } from "@/utils/platform"; +import { isTauri } from "@/utils/platform"; import type { TeamStatus } from "@/types/team"; interface TeamInviteDialogProps { @@ -44,8 +44,8 @@ export function TeamInviteDialog({ open, onOpenChange, teamStatus }: TeamInviteD const billingService = getBillingService(); const url = await billingService.getPortalUrl(); - // Use external browser for mobile platforms (iOS and Android) - if (isMobile()) { + // Use external browser for all Tauri platforms (mobile and desktop) + if (isTauri()) { try { // Dynamic import to avoid issues in web environments const { invoke } = await import("@tauri-apps/api/core"); @@ -67,7 +67,7 @@ export function TeamInviteDialog({ open, onOpenChange, teamStatus }: TeamInviteD } } - // Web or desktop flow + // Web flow window.open(url, "_blank", "noopener,noreferrer"); } catch (error) { console.error("Failed to open billing portal:", error); diff --git a/frontend/src/routes/pricing.tsx b/frontend/src/routes/pricing.tsx index 5ef0d4bbd..abdb00912 100644 --- a/frontend/src/routes/pricing.tsx +++ b/frontend/src/routes/pricing.tsx @@ -564,13 +564,13 @@ function PricingPage() { // For all other cases (upgrades/downgrades between paid plans, or downgrades to free), // use portal URL if it exists if (portalUrl) { - // Open in external browser for mobile platforms (iOS and Android) - if (isMobilePlatform) { + // Open in external browser for all Tauri platforms (mobile and desktop) + if (isTauri()) { console.log( - "[Billing] Mobile platform detected, using opener plugin to launch external browser for portal" + "[Billing] Tauri platform detected, using opener plugin to launch external browser for portal" ); - // Use the Tauri opener plugin for mobile platforms + // Use the Tauri opener plugin for all Tauri platforms import("@tauri-apps/api/core") .then((coreModule) => { return coreModule.invoke("plugin:opener|open_url", { url: portalUrl }); @@ -580,10 +580,15 @@ function PricingPage() { }) .catch((err) => { console.error("[Billing] Failed to open external browser:", err); - alert("Failed to open browser. Please try again."); + if (isMobilePlatform) { + alert("Failed to open browser. Please try again."); + } else { + // Fallback to window.open on desktop + window.open(portalUrl, "_blank"); + } }); } else { - // Default browser opening for desktop and web platforms + // Default browser opening for web platforms window.open(portalUrl, "_blank"); } return; From 353d66b67400911f81f0e7343514b867c5ea0d37 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:38:27 +0000 Subject: [PATCH 2/4] fix: use trymaple.ai return URLs for Tauri desktop instead of tauri://localhost When opening billing URLs in the system browser on Tauri desktop, the return/success/cancel URLs must be valid web URLs, not tauri://localhost which won't resolve in an external browser. - billingApi.ts fetchPortalUrl: isMobile() -> isTauri() for return URL - pricing.tsx newHandleSubscribe: add isTauri() branch for checkout URLs - ApiCreditsSection.tsx: add isTauri() branch for credit purchase URLs Addresses Devin Review feedback about tauri://localhost redirect issue. Co-Authored-By: unknown <> --- frontend/src/billing/billingApi.ts | 6 +++--- .../components/apikeys/ApiCreditsSection.tsx | 6 +++++- frontend/src/routes/pricing.tsx | 20 ++++++++++++++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/frontend/src/billing/billingApi.ts b/frontend/src/billing/billingApi.ts index 2a9bedaa6..53fb7e830 100644 --- a/frontend/src/billing/billingApi.ts +++ b/frontend/src/billing/billingApi.ts @@ -129,9 +129,9 @@ export async function fetchProducts(version?: string): Promise export async function fetchPortalUrl(thirdPartyToken: string): Promise { let returnUrl = window.location.origin; - // For mobile platforms, use the actual website origin instead of tauri://localhost - if (isMobile()) { - console.log("[Billing] Mobile platform detected, using trymaple.ai as return URL"); + // For all Tauri platforms, use the actual website origin instead of tauri://localhost + if (isTauri()) { + console.log("[Billing] Tauri platform detected, using trymaple.ai as return URL"); returnUrl = "https://trymaple.ai"; } diff --git a/frontend/src/components/apikeys/ApiCreditsSection.tsx b/frontend/src/components/apikeys/ApiCreditsSection.tsx index 54fd41f4b..9bde51075 100644 --- a/frontend/src/components/apikeys/ApiCreditsSection.tsx +++ b/frontend/src/components/apikeys/ApiCreditsSection.tsx @@ -105,8 +105,12 @@ export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSect successUrl = `https://trymaple.ai/payment-success-credits?source=${method}`; cancelUrl = method === "stripe" ? `https://trymaple.ai/payment-canceled?source=stripe` : undefined; + } else if (isTauri()) { + // For Tauri desktop, use trymaple.ai since tauri://localhost won't work in external browser + successUrl = `https://trymaple.ai/?credits_success=true`; + cancelUrl = method === "stripe" ? `https://trymaple.ai/` : undefined; } else { - // For web or desktop, use regular URLs with query params + // For web, use regular URLs with query params const baseUrl = window.location.origin; successUrl = `${baseUrl}/?credits_success=true`; cancelUrl = method === "stripe" ? `${baseUrl}/` : undefined; diff --git a/frontend/src/routes/pricing.tsx b/frontend/src/routes/pricing.tsx index abdb00912..3c12de9f8 100644 --- a/frontend/src/routes/pricing.tsx +++ b/frontend/src/routes/pricing.tsx @@ -419,8 +419,26 @@ function PricingPage() { quantity ); } + } else if (isTauri()) { + // For Tauri desktop, use trymaple.ai since tauri://localhost won't work in external browser + if (useBitcoin) { + await billingService.createZapriteCheckoutSession( + email, + productId, + `https://trymaple.ai/pricing?success=true`, + quantity + ); + } else { + await billingService.createCheckoutSession( + email, + productId, + `https://trymaple.ai/pricing?success=true`, + `https://trymaple.ai/pricing?canceled=true`, + quantity + ); + } } else { - // For web or desktop, use regular URLs + // For web, use regular URLs if (useBitcoin) { await billingService.createZapriteCheckoutSession( email, From b8f738d3be71d78ff9b865f1aa43fa88eb9d079c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:43:51 +0000 Subject: [PATCH 3/4] fix: use window.open instead of window.location.href for desktop Tauri fallback Using window.location.href navigates the Tauri webview away from the app, destroying SPA state. window.open is less disruptive and consistent with all other fallback handlers in this PR. Addresses Devin Review feedback on createCheckoutSession and createZapriteCheckoutSession fallback handlers. Co-Authored-By: unknown <> --- frontend/src/billing/billingApi.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/billing/billingApi.ts b/frontend/src/billing/billingApi.ts index 53fb7e830..f3e539bc5 100644 --- a/frontend/src/billing/billingApi.ts +++ b/frontend/src/billing/billingApi.ts @@ -223,8 +223,8 @@ export async function createCheckoutSession( "Failed to open payment page in external browser. This is required for mobile payments." ); } else { - // Fallback to regular navigation on desktop - window.location.href = checkout_url; + // Fallback to window.open on desktop (less disruptive than navigating away) + window.open(checkout_url, "_blank"); } }); @@ -294,8 +294,8 @@ export async function createZapriteCheckoutSession( "Failed to open payment page in external browser. This is required for mobile payments." ); } else { - // Fallback to regular navigation on desktop - window.location.href = checkout_url; + // Fallback to window.open on desktop (less disruptive than navigating away) + window.open(checkout_url, "_blank"); } }); From 0209badf2cc5ca0fb6d4adf4f06876c5966ad223 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Wed, 25 Feb 2026 17:48:17 +0000 Subject: [PATCH 4/4] fix: use window.open fallback in ApiCreditsSection Tauri opener catch handlers Consistent with billingApi.ts pattern: on desktop Tauri, fall back to window.open instead of window.location.href (which navigates the webview away). On mobile, throw an error instead. Addresses Devin Review feedback. Co-Authored-By: unknown <> --- .../src/components/apikeys/ApiCreditsSection.tsx | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/apikeys/ApiCreditsSection.tsx b/frontend/src/components/apikeys/ApiCreditsSection.tsx index 9bde51075..04e84e922 100644 --- a/frontend/src/components/apikeys/ApiCreditsSection.tsx +++ b/frontend/src/components/apikeys/ApiCreditsSection.tsx @@ -130,7 +130,11 @@ export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSect await invoke("plugin:opener|open_url", { url: response.checkout_url }).catch( (err: Error) => { console.error("[Credits] Failed to open checkout with Tauri opener:", err); - window.location.href = response.checkout_url; + if (isMobile()) { + throw new Error("Failed to open payment page in external browser."); + } else { + window.open(response.checkout_url, "_blank"); + } } ); } else { @@ -150,7 +154,11 @@ export function ApiCreditsSection({ showSuccessMessage = false }: ApiCreditsSect await invoke("plugin:opener|open_url", { url: response.checkout_url }).catch( (err: Error) => { console.error("[Credits] Failed to open checkout with Tauri opener:", err); - window.location.href = response.checkout_url; + if (isMobile()) { + throw new Error("Failed to open payment page in external browser."); + } else { + window.open(response.checkout_url, "_blank"); + } } ); } else {