mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-01 16:08:30 +00:00
**UX fixes:**
- Fixed a jarring post-auth redirect: the webapp auth layout used to bounce the user home the moment login/signup completed, then yank them to Stripe once the checkout session finished creating. The auth layout now owns the checkout redirect: it keeps the user on the auth screen with a "Taking you to secure checkout" spinner until the Stripe redirect fires (falling back to home if checkout fails to start) - The website signup and login pages show the same inline spinner while the checkout session is being created - A logged-in user opening a `/signup?plan=...` link directly is now sent straight to checkout instead of seeing the signup form - Signup pages now autofocus the email field (the form's `autoFocus` prop was never passed by either app)
This commit is contained in:
1 parent
c28cb63a8a
commit
ade1ff65a3
7 files changed
+232
-144
No files matched your search
@@ -1,8 +1,17 @@
|
||||
import { ReactNode } from "react";
|
||||
import { Outlet, useNavigate, Navigate } from "react-router-dom";
|
||||
import { ReactNode, useEffect, useRef } from "react";
|
||||
import {
|
||||
Outlet,
|
||||
useNavigate,
|
||||
Navigate,
|
||||
useSearchParams,
|
||||
} from "react-router-dom";
|
||||
import { faArrowLeft, faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import { TruchetPattern } from "@storyteller/ui-vfx";
|
||||
import {
|
||||
checkoutIntentFromSearchParams,
|
||||
redirectToCheckout,
|
||||
} from "@storyteller/ui-pricing-table";
|
||||
import { AuthShowcase } from "./auth-showcase";
|
||||
import { useMediaQuery } from "../ui/use-media-query";
|
||||
import { useSession } from "../../lib/session";
|
||||
@@ -21,13 +30,34 @@ export const AuthLayout = () => {
|
||||
const showShowcase = useMediaQuery("(min-width: 1024px)");
|
||||
const navigate = useNavigate();
|
||||
const { loggedIn, authChecked } = useSession();
|
||||
const [searchParams] = useSearchParams();
|
||||
// Set when a pricing-page purchase click routed the user through auth. The
|
||||
// layout owns the Stripe redirect: as soon as the session flips to logged
|
||||
// in (post signup/login, or arriving already signed in), it swaps the form
|
||||
// for a spinner and sends the user to checkout, instead of bouncing home
|
||||
// while the checkout session is still being created.
|
||||
const checkoutIntent = checkoutIntentFromSearchParams(searchParams);
|
||||
const startedCheckoutRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (!loggedIn || !checkoutIntent || startedCheckoutRef.current) {
|
||||
return;
|
||||
}
|
||||
startedCheckoutRef.current = true;
|
||||
redirectToCheckout(checkoutIntent).then((redirected) => {
|
||||
if (!redirected) {
|
||||
navigate("/", { replace: true });
|
||||
}
|
||||
});
|
||||
}, [loggedIn, checkoutIntent, navigate]);
|
||||
|
||||
const handleBack = () => {
|
||||
navigate("/");
|
||||
};
|
||||
|
||||
// Already signed in? Never show the auth form — go straight home.
|
||||
if (loggedIn) {
|
||||
// Already signed in? Never show the auth form — go straight home, unless a
|
||||
// checkout redirect is pending (handled above).
|
||||
if (loggedIn && !checkoutIntent) {
|
||||
return <Navigate to="/" replace />;
|
||||
}
|
||||
|
||||
@@ -74,7 +104,17 @@ export const AuthLayout = () => {
|
||||
|
||||
<div className="flex flex-1 flex-col justify-center px-8 py-10 sm:px-10">
|
||||
<div className="mx-auto w-full max-w-sm">
|
||||
{authChecked ? (
|
||||
{loggedIn && checkoutIntent ? (
|
||||
<div className="flex flex-col items-center gap-4 py-12 text-white/60">
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin text-2xl text-white/40"
|
||||
/>
|
||||
<span className="text-sm">
|
||||
Taking you to secure checkout…
|
||||
</span>
|
||||
</div>
|
||||
) : authChecked ? (
|
||||
<Outlet />
|
||||
) : (
|
||||
<div className="flex justify-center py-12">
|
||||
|
||||
@@ -9,10 +9,7 @@ import { Input } from "@storyteller/ui-input";
|
||||
import { useState } from "react";
|
||||
import { useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { UsersApi } from "@storyteller/api";
|
||||
import {
|
||||
checkoutIntentFromSearchParams,
|
||||
redirectToCheckout,
|
||||
} from "@storyteller/ui-pricing-table";
|
||||
import { checkoutIntentFromSearchParams } from "@storyteller/ui-pricing-table";
|
||||
import {
|
||||
getLandingUrl,
|
||||
getReferralCode,
|
||||
@@ -100,10 +97,10 @@ export const SignupForm = ({
|
||||
hasActiveSubscription(),
|
||||
]);
|
||||
|
||||
// A pricing-page purchase click brought the user here; resume Stripe
|
||||
// checkout for the plan they picked.
|
||||
const checkoutIntent = checkoutIntentFromSearchParams(searchParams);
|
||||
if (checkoutIntent && (await redirectToCheckout(checkoutIntent))) {
|
||||
// A pricing-page purchase click brought the user here; stay put. The
|
||||
// session refresh above flipped loggedIn, so AuthLayout is already
|
||||
// redirecting to Stripe checkout for the plan they picked.
|
||||
if (checkoutIntentFromSearchParams(searchParams)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -185,7 +182,10 @@ export const SignupForm = ({
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin"
|
||||
/>
|
||||
) : (
|
||||
"Create account"
|
||||
)}
|
||||
|
||||
@@ -9,11 +9,12 @@ import { Input } from "@storyteller/ui-input";
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { UsersApi } from "@storyteller/api";
|
||||
import { checkoutIntentFromSearchParams } from "@storyteller/ui-pricing-table";
|
||||
import {
|
||||
checkoutIntentFromSearchParams,
|
||||
redirectToCheckout,
|
||||
} from "@storyteller/ui-pricing-table";
|
||||
import { AuthHeader, AuthFooter, GoogleLoginButton } from "../../components/auth";
|
||||
AuthHeader,
|
||||
AuthFooter,
|
||||
GoogleLoginButton,
|
||||
} from "../../components/auth";
|
||||
import Seo from "../../components/seo";
|
||||
import { Reveal, RevealGroup } from "../../components/motion/reveal";
|
||||
import { refreshSession } from "../../lib/session";
|
||||
@@ -24,8 +25,9 @@ const Login = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const fromParam = searchParams.get("from");
|
||||
const redirectTo = fromParam && fromParam.startsWith("/") ? fromParam : "/";
|
||||
// Set when a pricing-page purchase click routed the user through auth;
|
||||
// after login we resume Stripe checkout for that plan.
|
||||
// Set when a pricing-page purchase click routed the user through auth.
|
||||
// AuthLayout owns the Stripe redirect once loggedIn flips; this page just
|
||||
// has to not navigate away in the meantime.
|
||||
const checkoutIntent = checkoutIntentFromSearchParams(searchParams);
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
@@ -51,7 +53,7 @@ const Login = () => {
|
||||
// navigating — otherwise RequireAuth on the destination sees loggedIn=false
|
||||
// and bounces straight back to /login?from=…
|
||||
await refreshSession(true);
|
||||
if (checkoutIntent && (await redirectToCheckout(checkoutIntent))) {
|
||||
if (checkoutIntent) {
|
||||
return;
|
||||
}
|
||||
navigate(redirectTo);
|
||||
@@ -67,7 +69,7 @@ const Login = () => {
|
||||
refreshSession(true),
|
||||
hasActiveSubscription(),
|
||||
]);
|
||||
if (checkoutIntent && (await redirectToCheckout(checkoutIntent))) {
|
||||
if (checkoutIntent) {
|
||||
return;
|
||||
}
|
||||
navigate(subscribed ? redirectTo : "/pricing");
|
||||
@@ -159,7 +161,10 @@ const Login = () => {
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<FontAwesomeIcon icon={faSpinnerThird} className="animate-spin" />
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin"
|
||||
/>
|
||||
) : (
|
||||
"Log in"
|
||||
)}
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import {
|
||||
checkoutIntentFromSearchParams,
|
||||
redirectToCheckout,
|
||||
} from "@storyteller/ui-pricing-table";
|
||||
import { checkoutIntentFromSearchParams } from "@storyteller/ui-pricing-table";
|
||||
import { AuthHeader, AuthFooter, SignupForm } from "../../components/auth";
|
||||
import Seo from "../../components/seo";
|
||||
import { Reveal } from "../../components/motion/reveal";
|
||||
@@ -12,10 +9,11 @@ const Signup = () => {
|
||||
const [searchParams] = useSearchParams();
|
||||
const checkoutIntent = checkoutIntentFromSearchParams(searchParams);
|
||||
|
||||
const handleSuccess = async () => {
|
||||
// A pricing-page purchase click brought the user here; resume Stripe
|
||||
// checkout for the plan they picked instead of the welcome flow.
|
||||
if (checkoutIntent && (await redirectToCheckout(checkoutIntent))) {
|
||||
const handleSuccess = () => {
|
||||
// A pricing-page purchase click brought the user here; stay put. Once the
|
||||
// session refresh flips loggedIn, AuthLayout swaps in a spinner and
|
||||
// redirects to Stripe checkout for the plan they picked.
|
||||
if (checkoutIntent) {
|
||||
return;
|
||||
}
|
||||
navigate("/welcome");
|
||||
@@ -27,15 +25,15 @@ const Signup = () => {
|
||||
title="Sign Up - ArtCraft"
|
||||
description="Create your ArtCraft account."
|
||||
/>
|
||||
<AuthHeader title="Create an Account" subtitle="Join thousands of creators" />
|
||||
<AuthHeader
|
||||
title="Create an Account"
|
||||
subtitle="Join thousands of creators"
|
||||
/>
|
||||
|
||||
{/* SignupForm runs its own field cascade internally (see signup-form.tsx).
|
||||
The footer picks up the tail of that cascade — the form's six staggered
|
||||
steps land by ~0.36s, so the footer follows at ~0.44s. */}
|
||||
<SignupForm
|
||||
onSuccess={handleSuccess}
|
||||
signupSource="artcraft"
|
||||
/>
|
||||
<SignupForm onSuccess={handleSuccess} signupSource="artcraft" autoFocus />
|
||||
|
||||
<Reveal inView={false} delay={0.44}>
|
||||
<AuthFooter>
|
||||
|
||||
@@ -21,6 +21,7 @@ const Login = () => {
|
||||
// Set when a pricing-page purchase click routed the user through auth;
|
||||
// after login we resume Stripe checkout for that plan.
|
||||
const checkoutIntent = checkoutIntentFromSearchParams(searchParams);
|
||||
const [redirectingToCheckout, setRedirectingToCheckout] = useState(false);
|
||||
const [email, setEmail] = useState("");
|
||||
const [password, setPassword] = useState("");
|
||||
const [showPassword, setShowPassword] = useState(false);
|
||||
@@ -42,8 +43,12 @@ const Login = () => {
|
||||
|
||||
if (response.success) {
|
||||
window.dispatchEvent(new Event("auth-change"));
|
||||
if (checkoutIntent && (await redirectToCheckout(checkoutIntent))) {
|
||||
return;
|
||||
if (checkoutIntent) {
|
||||
setRedirectingToCheckout(true);
|
||||
if (await redirectToCheckout(checkoutIntent)) {
|
||||
return;
|
||||
}
|
||||
setRedirectingToCheckout(false);
|
||||
}
|
||||
navigate("/");
|
||||
} else {
|
||||
@@ -81,13 +86,23 @@ const Login = () => {
|
||||
|
||||
<main className="relative z-10 flex-1 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md bg-[#1C1C20] border border-white/10 rounded-2xl p-6 py-8 shadow-2xl">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-semibold mb-2">Welcome Back</h1>
|
||||
<p className="text-white/60 text-sm">Log in to your account</p>
|
||||
</div>
|
||||
{redirectingToCheckout ? (
|
||||
<div className="flex flex-col items-center gap-4 py-12 text-white/60">
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin text-2xl text-white/40"
|
||||
/>
|
||||
<span className="text-sm">Taking you to secure checkout…</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-semibold mb-2">Welcome Back</h1>
|
||||
<p className="text-white/60 text-sm">Log in to your account</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/*<GoogleLoginButton
|
||||
<div className="space-y-4">
|
||||
{/*<GoogleLoginButton
|
||||
mode="login"
|
||||
onSuccess={handleGoogleSuccess}
|
||||
onError={handleGoogleError}
|
||||
@@ -103,89 +118,93 @@ const Login = () => {
|
||||
</div>
|
||||
*/}
|
||||
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleLogin();
|
||||
}}
|
||||
>
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 text-red-500 px-4 py-3 rounded-xl text-sm text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-white/70 ml-1">
|
||||
Email or Username
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@example.com or username"
|
||||
inputClassName="w-full bg-black/20 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center ml-1">
|
||||
<label className="text-xs font-semibold text-white/70">
|
||||
Password
|
||||
</label>
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="text-xs text-primary hover:text-primary-400 transition-colors"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Min. 8 characters"
|
||||
inputClassName="w-full bg-black/20 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors"
|
||||
>
|
||||
<FontAwesomeIcon icon={showPassword ? faEyeSlash : faEye} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
className="w-full bg-primary hover:bg-primary-600 text-white border-none justify-center font-bold h-10"
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
<form
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
handleLogin();
|
||||
}}
|
||||
>
|
||||
{isLoading ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin"
|
||||
/>
|
||||
) : (
|
||||
"Log in"
|
||||
{error && (
|
||||
<div className="bg-red-500/10 border border-red-500/20 text-red-500 px-4 py-3 rounded-xl text-sm text-center">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center text-sm text-white/60">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
to={{ pathname: "/signup", search: searchParams.toString() }}
|
||||
className="text-primary hover:text-primary-400 font-semibold transition-colors"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<label className="text-xs font-semibold text-white/70 ml-1">
|
||||
Email or Username
|
||||
</label>
|
||||
<Input
|
||||
type="text"
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
placeholder="you@example.com or username"
|
||||
inputClassName="w-full bg-black/20 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors"
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-center ml-1">
|
||||
<label className="text-xs font-semibold text-white/70">
|
||||
Password
|
||||
</label>
|
||||
<Link
|
||||
to="/forgot-password"
|
||||
className="text-xs text-primary hover:text-primary-400 transition-colors"
|
||||
>
|
||||
Forgot password?
|
||||
</Link>
|
||||
</div>
|
||||
<div className="relative">
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
placeholder="Min. 8 characters"
|
||||
inputClassName="w-full bg-black/20 border border-white/10 focus:border-primary/50 rounded-xl px-4 py-3 text-white placeholder-white/20 outline-none transition-colors pr-12"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setShowPassword(!showPassword)}
|
||||
className="absolute right-4 top-1/2 -translate-y-1/2 text-white/30 hover:text-white/60 transition-colors"
|
||||
>
|
||||
<FontAwesomeIcon
|
||||
icon={showPassword ? faEyeSlash : faEye}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="pt-2">
|
||||
<Button
|
||||
className="w-full bg-primary hover:bg-primary-600 text-white border-none justify-center font-bold h-10"
|
||||
type="submit"
|
||||
disabled={isLoading}
|
||||
>
|
||||
{isLoading ? (
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin"
|
||||
/>
|
||||
) : (
|
||||
"Log in"
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div className="mt-8 text-center text-sm text-white/60">
|
||||
Don't have an account?{" "}
|
||||
<Link
|
||||
to={{ pathname: "/signup", search: searchParams.toString() }}
|
||||
className="text-primary hover:text-primary-400 font-semibold transition-colors"
|
||||
>
|
||||
Sign up
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
import { useState } from "react";
|
||||
import { Link, useNavigate, useSearchParams } from "react-router-dom";
|
||||
import { faSpinnerThird } from "@fortawesome/pro-solid-svg-icons";
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import {
|
||||
checkoutIntentFromSearchParams,
|
||||
redirectToCheckout,
|
||||
@@ -10,12 +13,17 @@ const Signup = () => {
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const checkoutIntent = checkoutIntentFromSearchParams(searchParams);
|
||||
const [redirectingToCheckout, setRedirectingToCheckout] = useState(false);
|
||||
|
||||
const handleSuccess = async () => {
|
||||
// A pricing-page purchase click brought the user here; resume Stripe
|
||||
// checkout for the plan they picked instead of the welcome flow.
|
||||
if (checkoutIntent && (await redirectToCheckout(checkoutIntent))) {
|
||||
return;
|
||||
if (checkoutIntent) {
|
||||
setRedirectingToCheckout(true);
|
||||
if (await redirectToCheckout(checkoutIntent)) {
|
||||
return;
|
||||
}
|
||||
setRedirectingToCheckout(false);
|
||||
}
|
||||
navigate("/welcome");
|
||||
};
|
||||
@@ -37,25 +45,42 @@ const Signup = () => {
|
||||
|
||||
<main className="relative z-10 flex-1 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md bg-[#1C1C20] border border-white/10 rounded-2xl p-6 py-8 shadow-2xl">
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-semibold mb-2">Create an Account</h1>
|
||||
<p className="text-white/60 text-sm">Join thousands of creators</p>
|
||||
</div>
|
||||
{redirectingToCheckout ? (
|
||||
<div className="flex flex-col items-center gap-4 py-12 text-white/60">
|
||||
<FontAwesomeIcon
|
||||
icon={faSpinnerThird}
|
||||
className="animate-spin text-2xl text-white/40"
|
||||
/>
|
||||
<span className="text-sm">Taking you to secure checkout…</span>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
<div className="text-center mb-8">
|
||||
<h1 className="text-2xl font-semibold mb-2">
|
||||
Create an Account
|
||||
</h1>
|
||||
<p className="text-white/60 text-sm">
|
||||
Join thousands of creators
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<SignupForm
|
||||
onSuccess={handleSuccess}
|
||||
signupSource="artcraft"
|
||||
/>
|
||||
<SignupForm
|
||||
onSuccess={handleSuccess}
|
||||
signupSource="artcraft"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
<div className="mt-8 text-center text-sm text-white/60">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
to={{ pathname: "/login", search: searchParams.toString() }}
|
||||
className="text-primary hover:text-primary-400 font-semibold transition-colors"
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mt-8 text-center text-sm text-white/60">
|
||||
Already have an account?{" "}
|
||||
<Link
|
||||
to={{ pathname: "/login", search: searchParams.toString() }}
|
||||
className="text-primary hover:text-primary-400 font-semibold transition-colors"
|
||||
>
|
||||
Log in
|
||||
</Link>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</main>
|
||||
|
||||
|
||||
@@ -30,7 +30,8 @@ export const checkoutIntentFromSearchParams = (
|
||||
if (!plan || !PAID_PLAN_SLUGS.has(plan)) {
|
||||
return null;
|
||||
}
|
||||
const cadence = params.get(CADENCE_PARAM) === "monthly" ? "monthly" : "yearly";
|
||||
const cadence =
|
||||
params.get(CADENCE_PARAM) === "monthly" ? "monthly" : "yearly";
|
||||
return { plan, cadence };
|
||||
};
|
||||
|
||||
|
||||
Reference in new issue
Block a user