diff --git a/frontend/src/billing/billingApi.ts b/frontend/src/billing/billingApi.ts index 26bb287cc..f3e539bc5 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; @@ -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"; } @@ -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 window.open on desktop (less disruptive than navigating away) + window.open(checkout_url, "_blank"); + } }); // 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 window.open on desktop (less disruptive than navigating away) + window.open(checkout_url, "_blank"); + } }); // 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..04e84e922 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, @@ -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; @@ -120,10 +124,22 @@ 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); + if (isMobile()) { + throw new Error("Failed to open payment page in external browser."); + } else { + window.open(response.checkout_url, "_blank"); + } + } + ); + } 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 +148,22 @@ 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); + if (isMobile()) { + throw new Error("Failed to open payment page in external browser."); + } else { + window.open(response.checkout_url, "_blank"); + } + } + ); + } 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..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, @@ -564,13 +582,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 +598,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;