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
12 changes: 12 additions & 0 deletions frontend/public/.well-known/apple-app-site-association
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
{
"/": "/desktop-auth/*",
"comment": "Matches any URL whose path starts with /desktop-auth/"
},
{
"/": "/payment-success*",
"comment": "Matches URLs for successful payments"
},
{
"/": "/payment-canceled*",
"comment": "Matches URLs for canceled payments"
},
{
"/": "/pricing*",
"comment": "Matches URLs for pricing page with parameters"
}
]
}
Expand Down
9 changes: 9 additions & 0 deletions frontend/src-tauri/capabilities/default.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,15 @@
},
{
"url": "https://trymaple.ai/*"
},
{
"url": "https://*.stripe.com/*"
},
{
"url": "https://*.stripe.network/*"
},
{
"url": "https://*.zaprite.com/*"
}
]
},
Expand Down
9 changes: 9 additions & 0 deletions frontend/src-tauri/capabilities/mobile-ios.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,15 @@
},
{
"url": "https://trymaple.ai/*"
},
{
"url": "https://*.stripe.com/*"
},
{
"url": "https://*.stripe.network/*"
},
{
"url": "https://*.zaprite.com/*"
}
]
},
Expand Down
90 changes: 90 additions & 0 deletions frontend/src/billing/billingApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,51 @@ export async function createCheckoutSession(

const { checkout_url } = await response.json();
console.log("Redirecting to checkout:", checkout_url);

try {
// Check if we're in a Tauri environment
const isTauri = await import("@tauri-apps/api/core")
.then((m) => m.isTauri())
.catch(() => false);

if (isTauri) {
// Log the URL we're about to open
console.log("[Billing] Opening URL in external browser:", checkout_url);

// For iOS, we need to force Safari to open with no fallback
const { type } = await import("@tauri-apps/plugin-os");
const platform = await type();
const { invoke } = await import("@tauri-apps/api/core");

if (platform === "ios") {
console.log("[Billing] iOS detected, using opener plugin to launch Safari");

// Use the opener plugin directly - with NO fallback for iOS
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 iOS payments."
);
});

// Add a small delay to ensure the browser has time to open
await new Promise((resolve) => setTimeout(resolve, 300));
return;
} else {
// For other platforms, maintain original behavior - use window.location directly
window.location.href = checkout_url;
return;
}
}
} catch (error) {
console.error("[Billing] Error opening URL with Tauri:", error);
}

// Fall back to regular navigation if not on Tauri or if Tauri opener fails
window.location.href = checkout_url;
}

Expand Down Expand Up @@ -179,6 +224,51 @@ export async function createZapriteCheckoutSession(

const { checkout_url } = await response.json();
console.log("Redirecting to Zaprite checkout:", checkout_url);

try {
// Check if we're in a Tauri environment
const isTauri = await import("@tauri-apps/api/core")
.then((m) => m.isTauri())
.catch(() => false);

if (isTauri) {
// Log the URL we're about to open
console.log("[Billing] Opening URL in external browser:", checkout_url);

// For iOS, we need to force Safari to open with no fallback
const { type } = await import("@tauri-apps/plugin-os");
const platform = await type();
const { invoke } = await import("@tauri-apps/api/core");

if (platform === "ios") {
console.log("[Billing] iOS detected, using opener plugin to launch Safari");

// Use the opener plugin directly - with NO fallback for iOS
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 iOS payments."
);
});

// Add a small delay to ensure the browser has time to open
await new Promise((resolve) => setTimeout(resolve, 300));
return;
} else {
// For other platforms, maintain original behavior - use window.location directly
window.location.href = checkout_url;
return;
}
}
} catch (error) {
console.error("[Billing] Error opening URL with Tauri:", error);
}

// Fall back to regular navigation if not on Tauri or if Tauri opener fails
window.location.href = checkout_url;
}

Expand Down
70 changes: 58 additions & 12 deletions frontend/src/components/DeepLinkHandler.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,69 @@ export function DeepLinkHandler() {
console.log("[Deep Link] Received URL:", url);

try {
// Parse the URL to extract the tokens
// Parse the URL to extract parameters
const urlObj = new URL(url);
// The URL will look like: cloud.opensecret.maple://auth?access_token=...&refresh_token=...
const accessToken = urlObj.searchParams.get("access_token");
const refreshToken = urlObj.searchParams.get("refresh_token");
// The URL path structure will be: cloud.opensecret.maple://path?params
const pathParts = urlObj.pathname.split("/").filter(Boolean);
const firstPathPart = pathParts[0] || "";

if (accessToken && refreshToken) {
console.log("[Deep Link] Auth tokens received");
// Handle different types of deep links
if (firstPathPart === "auth" || firstPathPart === "") {
// Handle auth deep links
const accessToken = urlObj.searchParams.get("access_token");
const refreshToken = urlObj.searchParams.get("refresh_token");

// Store the tokens in localStorage with consistent naming
localStorage.setItem("access_token", accessToken);
localStorage.setItem("refresh_token", refreshToken);
if (accessToken && refreshToken) {
console.log("[Deep Link] Auth tokens received");

// Refresh the app state to reflect the logged-in status
window.location.href = "/"; // Reload the app
// Store the tokens in localStorage with consistent naming
localStorage.setItem("access_token", accessToken);
localStorage.setItem("refresh_token", refreshToken);

// Refresh the app state to reflect the logged-in status
window.location.href = "/"; // Reload the app
} else {
console.error("[Deep Link] Missing tokens in auth deep link");
}
} else if (
firstPathPart === "payment" ||
firstPathPart === "payment-success" ||
firstPathPart === "payment-canceled" ||
urlObj.searchParams.has("payment_success") ||
urlObj.searchParams.has("success")
) {
// Handle payment deep links from various sources
const isSuccess =
firstPathPart === "payment-success" ||
urlObj.searchParams.get("success") === "true" ||
urlObj.searchParams.get("payment_success") === "true";

const isCanceled =
firstPathPart === "payment-canceled" ||
urlObj.searchParams.get("canceled") === "true" ||
urlObj.searchParams.has("payment_canceled");

console.log("[Deep Link] Payment callback received:", {
isSuccess,
isCanceled,
path: firstPathPart,
source: urlObj.searchParams.get("source")
});

// Use window.location instead of navigate
if (isSuccess) {
// Navigate to the success page or show a success message
window.location.href = "/pricing?success=true";
} else if (isCanceled) {
// Navigate to the canceled page or show a canceled message
window.location.href = "/pricing?canceled=true";
} else {
// Handle unknown payment status
console.warn("[Deep Link] Unknown payment status in callback");
window.location.href = "/pricing";
}
} else {
console.error("[Deep Link] Missing tokens in deep link");
console.warn("[Deep Link] Unknown deep link type:", firstPathPart);
}
} catch (error) {
console.error("[Deep Link] Failed to process deep link:", error);
Expand Down
52 changes: 52 additions & 0 deletions frontend/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { Route as rootRoute } from './routes/__root'
import { Route as SignupImport } from './routes/signup'
import { Route as ProofImport } from './routes/proof'
import { Route as PricingImport } from './routes/pricing'
import { Route as PaymentSuccessImport } from './routes/payment-success'
import { Route as PaymentCanceledImport } from './routes/payment-canceled'
import { Route as PasswordResetImport } from './routes/password-reset'
import { Route as LoginImport } from './routes/login'
import { Route as DownloadsImport } from './routes/downloads'
Expand Down Expand Up @@ -45,6 +47,18 @@ const PricingRoute = PricingImport.update({
getParentRoute: () => rootRoute,
} as any)

const PaymentSuccessRoute = PaymentSuccessImport.update({
id: '/payment-success',
path: '/payment-success',
getParentRoute: () => rootRoute,
} as any)

const PaymentCanceledRoute = PaymentCanceledImport.update({
id: '/payment-canceled',
path: '/payment-canceled',
getParentRoute: () => rootRoute,
} as any)

const PasswordResetRoute = PasswordResetImport.update({
id: '/password-reset',
path: '/password-reset',
Expand Down Expand Up @@ -150,6 +164,20 @@ declare module '@tanstack/react-router' {
preLoaderRoute: typeof PasswordResetImport
parentRoute: typeof rootRoute
}
'/payment-canceled': {
id: '/payment-canceled'
path: '/payment-canceled'
fullPath: '/payment-canceled'
preLoaderRoute: typeof PaymentCanceledImport
parentRoute: typeof rootRoute
}
'/payment-success': {
id: '/payment-success'
path: '/payment-success'
fullPath: '/payment-success'
preLoaderRoute: typeof PaymentSuccessImport
parentRoute: typeof rootRoute
}
'/pricing': {
id: '/pricing'
path: '/pricing'
Expand Down Expand Up @@ -233,6 +261,8 @@ export interface FileRoutesByFullPath {
'/downloads': typeof DownloadsRoute
'/login': typeof LoginRoute
'/password-reset': typeof PasswordResetRouteWithChildren
'/payment-canceled': typeof PaymentCanceledRoute
'/payment-success': typeof PaymentSuccessRoute
'/pricing': typeof PricingRoute
'/proof': typeof ProofRoute
'/signup': typeof SignupRoute
Expand All @@ -249,6 +279,8 @@ export interface FileRoutesByTo {
'/downloads': typeof DownloadsRoute
'/login': typeof LoginRoute
'/password-reset': typeof PasswordResetRouteWithChildren
'/payment-canceled': typeof PaymentCanceledRoute
'/payment-success': typeof PaymentSuccessRoute
'/pricing': typeof PricingRoute
'/proof': typeof ProofRoute
'/signup': typeof SignupRoute
Expand All @@ -266,6 +298,8 @@ export interface FileRoutesById {
'/downloads': typeof DownloadsRoute
'/login': typeof LoginRoute
'/password-reset': typeof PasswordResetRouteWithChildren
'/payment-canceled': typeof PaymentCanceledRoute
'/payment-success': typeof PaymentSuccessRoute
'/pricing': typeof PricingRoute
'/proof': typeof ProofRoute
'/signup': typeof SignupRoute
Expand All @@ -284,6 +318,8 @@ export interface FileRouteTypes {
| '/downloads'
| '/login'
| '/password-reset'
| '/payment-canceled'
| '/payment-success'
| '/pricing'
| '/proof'
| '/signup'
Expand All @@ -299,6 +335,8 @@ export interface FileRouteTypes {
| '/downloads'
| '/login'
| '/password-reset'
| '/payment-canceled'
| '/payment-success'
| '/pricing'
| '/proof'
| '/signup'
Expand All @@ -314,6 +352,8 @@ export interface FileRouteTypes {
| '/downloads'
| '/login'
| '/password-reset'
| '/payment-canceled'
| '/payment-success'
| '/pricing'
| '/proof'
| '/signup'
Expand All @@ -331,6 +371,8 @@ export interface RootRouteChildren {
DownloadsRoute: typeof DownloadsRoute
LoginRoute: typeof LoginRoute
PasswordResetRoute: typeof PasswordResetRouteWithChildren
PaymentCanceledRoute: typeof PaymentCanceledRoute
PaymentSuccessRoute: typeof PaymentSuccessRoute
PricingRoute: typeof PricingRoute
ProofRoute: typeof ProofRoute
SignupRoute: typeof SignupRoute
Expand All @@ -345,6 +387,8 @@ const rootRouteChildren: RootRouteChildren = {
DownloadsRoute: DownloadsRoute,
LoginRoute: LoginRoute,
PasswordResetRoute: PasswordResetRouteWithChildren,
PaymentCanceledRoute: PaymentCanceledRoute,
PaymentSuccessRoute: PaymentSuccessRoute,
PricingRoute: PricingRoute,
ProofRoute: ProofRoute,
SignupRoute: SignupRoute,
Expand All @@ -368,6 +412,8 @@ export const routeTree = rootRoute
"/downloads",
"/login",
"/password-reset",
"/payment-canceled",
"/payment-success",
"/pricing",
"/proof",
"/signup",
Expand Down Expand Up @@ -399,6 +445,12 @@ export const routeTree = rootRoute
"/password-reset/confirm"
]
},
"/payment-canceled": {
"filePath": "payment-canceled.tsx"
},
"/payment-success": {
"filePath": "payment-success.tsx"
},
"/pricing": {
"filePath": "pricing.tsx"
},
Expand Down
1 change: 0 additions & 1 deletion frontend/src/routes/login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -201,7 +201,6 @@ function LoginPage() {
// Apple requires the nonce to be hashed with SHA-256
const hashedNonce = bytesToHex(sha256(new TextEncoder().encode(rawNonce)));


// Invoke the Apple Sign in plugin
// This will show the native Apple authentication UI
const result = await invoke<AppleCredential>(
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/routes/payment-canceled.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { createFileRoute, Navigate } from "@tanstack/react-router";

export const Route = createFileRoute("/payment-canceled")({
component: PaymentCanceledPage
});

function PaymentCanceledPage() {
// Just redirect to pricing page with canceled query parameter
return <Navigate to="/pricing" search={{ canceled: true }} />;
}
Loading