Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 30 additions & 20 deletions frontend/src/billing/billingApi.ts
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -129,9 +129,9 @@ export async function fetchProducts(version?: string): Promise<BillingProduct[]>
export async function fetchPortalUrl(thirdPartyToken: string): Promise<string> {
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";
}

Expand Down Expand Up @@ -203,32 +203,37 @@ 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");
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
});

// Add a small delay to ensure the browser has time to open
await new Promise((resolve) => setTimeout(resolve, 300));
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;
}

Expand Down Expand Up @@ -269,32 +274,37 @@ 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");
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
});

// Add a small delay to ensure the browser has time to open
await new Promise((resolve) => setTimeout(resolve, 300));
return;
}

// Fall back to regular navigation if not on mobile
// Fall back to regular navigation for web platforms
window.location.href = checkout_url;
}

Expand Down
17 changes: 11 additions & 6 deletions frontend/src/components/AccountMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -179,30 +179,35 @@ 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
await new Promise((resolve) => setTimeout(resolve, 300));
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);
Expand Down
48 changes: 38 additions & 10 deletions frontend/src/components/apikeys/ApiCreditsSection.tsx
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
Expand All @@ -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");
}
}
);
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
} else {
window.location.href = response.checkout_url;
}
} else {
// For Zaprite, guest users pass empty string (backend infers from JWT)
const response = await billingService.purchaseApiCreditsZaprite({
Expand All @@ -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);
Expand Down
8 changes: 4 additions & 4 deletions frontend/src/components/team/TeamInviteDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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");
Expand All @@ -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);
Expand Down
37 changes: 30 additions & 7 deletions frontend/src/routes/pricing.tsx
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 });
Expand All @@ -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;
Expand Down
Loading