diff --git a/frontend/src/routes/__root.tsx b/frontend/src/routes/__root.tsx index f160202bc..c033c2108 100644 --- a/frontend/src/routes/__root.tsx +++ b/frontend/src/routes/__root.tsx @@ -6,8 +6,23 @@ interface RootRouterContext { os: OpenSecretContextType; } +export type RootSearchParams = { + login?: string; + next?: string; + selected_plan?: string; + success?: boolean; + canceled?: boolean; +}; + export const Route = createRootRouteWithContext()({ - component: Root + component: Root, + validateSearch: (search: Record): RootSearchParams => ({ + login: typeof search.login === "string" ? search.login : undefined, + next: typeof search.next === "string" ? search.next : undefined, + selected_plan: typeof search.selected_plan === "string" ? search.selected_plan : undefined, + success: typeof search.success === "boolean" ? search.success : undefined, + canceled: typeof search.canceled === "boolean" ? search.canceled : undefined + }) }); function Root() { diff --git a/frontend/src/routes/auth.$provider.callback.tsx b/frontend/src/routes/auth.$provider.callback.tsx index 65c8e68cb..848e258a2 100644 --- a/frontend/src/routes/auth.$provider.callback.tsx +++ b/frontend/src/routes/auth.$provider.callback.tsx @@ -50,8 +50,25 @@ function OAuthCallback() { } else { throw new Error("Unsupported provider"); } - // If successful, redirect to home page after a short delay - setTimeout(() => navigate({ to: "/" }), 2000); + + // Check for stored selected_plan + const selectedPlan = sessionStorage.getItem("selected_plan"); + // Clear it from storage + sessionStorage.removeItem("selected_plan"); + + // If successful, redirect after a short delay + setTimeout(() => { + if (selectedPlan) { + // If there was a selected plan, go to pricing + navigate({ + to: "/pricing", + search: { selected_plan: selectedPlan } + }); + } else { + // Otherwise go home (original behavior) + navigate({ to: "/" }); + } + }, 2000); } catch (error) { console.error(`${provider} callback error:`, error); if (error instanceof Error) { @@ -108,7 +125,10 @@ function OAuthCallback() { {formattedProvider} Authentication Successful - You have successfully authenticated with {formattedProvider}. Redirecting to home page... + You have successfully authenticated with {formattedProvider}. + {sessionStorage.getItem("selected_plan") + ? "Redirecting to complete your plan selection..." + : "Redirecting to home page..."} ); diff --git a/frontend/src/routes/login.tsx b/frontend/src/routes/login.tsx index 5ab66cad1..0582bc6a4 100644 --- a/frontend/src/routes/login.tsx +++ b/frontend/src/routes/login.tsx @@ -11,12 +11,14 @@ import { AuthMain } from "@/components/AuthMain"; type LoginSearchParams = { next?: string; + selected_plan?: string; }; export const Route = createFileRoute("/login")({ component: LoginPage, validateSearch: (search: Record): LoginSearchParams => ({ - next: typeof search.next === "string" ? search.next : undefined + next: typeof search.next === "string" ? search.next : undefined, + selected_plan: typeof search.selected_plan === "string" ? search.selected_plan : undefined }) }); @@ -25,7 +27,7 @@ type LoginMethod = "email" | "github" | "google" | null; function LoginPage() { const navigate = useNavigate(); const os = useOpenSecret(); - const { next } = Route.useSearch(); + const { next, selected_plan } = Route.useSearch(); const [loginMethod, setLoginMethod] = useState(null); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -33,9 +35,16 @@ function LoginPage() { // Redirect if already logged in useEffect(() => { if (os.auth.user) { - navigate({ to: next || "/" }); + if (selected_plan) { + navigate({ + to: "/pricing", + search: { selected_plan } + }); + } else { + navigate({ to: next || "/" }); + } } - }, [os.auth.user, navigate, next]); + }, [os.auth.user, navigate, next, selected_plan]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -48,7 +57,14 @@ function LoginPage() { try { await os.signIn(email, password); setTimeout(() => { - navigate({ to: next || "/" }); + if (selected_plan) { + navigate({ + to: "/pricing", + search: { selected_plan } + }); + } else { + navigate({ to: next || "/" }); + } window.scrollTo(0, 0); }, 100); } catch (error) { @@ -66,6 +82,9 @@ function LoginPage() { const handleGitHubLogin = async () => { try { const { auth_url } = await os.initiateGitHubAuth(""); + if (selected_plan) { + sessionStorage.setItem("selected_plan", selected_plan); + } window.location.href = auth_url; } catch (error) { console.error("Failed to initiate GitHub login:", error); @@ -76,6 +95,9 @@ function LoginPage() { const handleGoogleLogin = async () => { try { const { auth_url } = await os.initiateGoogleAuth(""); + if (selected_plan) { + sessionStorage.setItem("selected_plan", selected_plan); + } window.location.href = auth_url; } catch (error) { console.error("Failed to initiate Google login:", error); diff --git a/frontend/src/routes/pricing.tsx b/frontend/src/routes/pricing.tsx index df8b16896..93b81e900 100644 --- a/frontend/src/routes/pricing.tsx +++ b/frontend/src/routes/pricing.tsx @@ -12,6 +12,12 @@ import { useLocalState } from "@/state/useLocalState"; import { Button } from "@/components/ui/button"; import { Switch } from "@/components/ui/switch"; +type PricingSearchParams = { + selected_plan?: string; + success?: boolean; + canceled?: boolean; +}; + function PricingSkeletonCard() { return (
@@ -125,6 +131,7 @@ function PricingPage() { const os = useOpenSecret(); const { setBillingStatus } = useLocalState(); const isLoggedIn = !!os.auth.user; + const { selected_plan } = Route.useSearch(); // Fetch billing status if user is logged in const { data: freshBillingStatus, isLoading: isBillingStatusLoading } = useQuery({ @@ -258,6 +265,18 @@ function PricingPage() { const handleButtonClick = (product: any) => { if (!isLoggedIn) { + const targetPlanName = product.name.toLowerCase(); + if (!targetPlanName.includes("free")) { + // For paid plans, redirect to signup with the plan selection + navigate({ + to: "/signup", + search: { + next: "/pricing", + selected_plan: product.id + } + }); + return; + } navigate({ to: "/signup" }); return; } @@ -323,14 +342,14 @@ function PricingPage() { await billingService.createZapriteCheckoutSession( email, productId, - `${window.location.origin}/pricing?success=true` + `${window.location.origin}/` ); } else { await billingService.createCheckoutSession( email, productId, - `${window.location.origin}/pricing?success=true`, - `${window.location.origin}/pricing?canceled=true` + `${window.location.origin}/`, + `${window.location.origin}/` ); } } catch (err) { @@ -341,6 +360,32 @@ function PricingPage() { } }; + useEffect(() => { + let isSubscribed = true; + + // If user is logged in and there's a selected plan, trigger checkout + if (isLoggedIn && selected_plan && !isBillingStatusLoading) { + if (loadingProductId) return; // Prevent multiple triggers + const product = products?.find((p) => p.id === selected_plan); + if (product) { + if (isSubscribed) { + handleButtonClick(product); + } + } + } + + return () => { + isSubscribed = false; + }; + }, [ + isLoggedIn, + selected_plan, + isBillingStatusLoading, + products, + loadingProductId, + handleButtonClick + ]); + // Show loading state if we're fetching initial data if (productsLoading || (isLoggedIn && isBillingStatusLoading)) { return ( @@ -582,5 +627,10 @@ function PricingPage() { } export const Route = createFileRoute("/pricing")({ - component: PricingPage + component: PricingPage, + validateSearch: (search: Record): PricingSearchParams => ({ + selected_plan: typeof search.selected_plan === "string" ? search.selected_plan : undefined, + success: typeof search.success === "boolean" ? search.success : undefined, + canceled: typeof search.canceled === "boolean" ? search.canceled : undefined + }) }); diff --git a/frontend/src/routes/signup.tsx b/frontend/src/routes/signup.tsx index 9a639b6d4..8ca14a74a 100644 --- a/frontend/src/routes/signup.tsx +++ b/frontend/src/routes/signup.tsx @@ -11,12 +11,14 @@ import { AuthMain } from "@/components/AuthMain"; type SignupSearchParams = { next?: string; + selected_plan?: string; }; export const Route = createFileRoute("/signup")({ component: SignupPage, validateSearch: (search: Record): SignupSearchParams => ({ - next: typeof search.next === "string" ? search.next : undefined + next: typeof search.next === "string" ? search.next : undefined, + selected_plan: typeof search.selected_plan === "string" ? search.selected_plan : undefined }) }); @@ -25,7 +27,7 @@ type SignUpMethod = "email" | "github" | "google" | null; function SignupPage() { const navigate = useNavigate(); const os = useOpenSecret(); - const { next } = Route.useSearch(); + const { next, selected_plan } = Route.useSearch(); const [signUpMethod, setSignUpMethod] = useState(null); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); @@ -33,9 +35,16 @@ function SignupPage() { // Redirect if already logged in useEffect(() => { if (os.auth.user) { - navigate({ to: next || "/" }); + if (selected_plan) { + navigate({ + to: "/pricing", + search: { selected_plan } + }); + } else { + navigate({ to: next || "/" }); + } } - }, [os.auth.user, navigate, next]); + }, [os.auth.user, navigate, next, selected_plan]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -48,7 +57,14 @@ function SignupPage() { try { await os.signUp(email, password, "", "ANON"); setTimeout(() => { - navigate({ to: next || "/" }); + if (selected_plan) { + navigate({ + to: "/pricing", + search: { selected_plan } + }); + } else { + navigate({ to: next || "/" }); + } window.scrollTo(0, 0); }, 100); } catch (error) { @@ -66,6 +82,9 @@ function SignupPage() { const handleGitHubSignup = async () => { try { const { auth_url } = await os.initiateGitHubAuth(""); + if (selected_plan) { + sessionStorage.setItem("selected_plan", selected_plan); + } window.location.href = auth_url; } catch (error) { console.error("Failed to initiate GitHub signup:", error); @@ -76,6 +95,9 @@ function SignupPage() { const handleGoogleSignup = async () => { try { const { auth_url } = await os.initiateGoogleAuth(""); + if (selected_plan) { + sessionStorage.setItem("selected_plan", selected_plan); + } window.location.href = auth_url; } catch (error) { console.error("Failed to initiate Google signup:", error);