From ade1ff65a3544b0ed0af98a3be786a4dfef1f7df Mon Sep 17 00:00:00 2001 From: BFlat Date: Thu, 2 Jul 2026 16:01:54 +0700 Subject: [PATCH] **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) --- .../src/components/auth/auth-layout.tsx | 50 ++++- .../src/components/auth/signup-form.tsx | 18 +- .../artcraft-webapp/src/pages/login/login.tsx | 23 ++- .../src/pages/signup/signup.tsx | 24 +-- .../src/pages/login/login.tsx | 195 ++++++++++-------- .../src/pages/signup/signup.tsx | 63 ++++-- .../pricing-table/src/lib/checkout-intent.ts | 3 +- 7 files changed, 232 insertions(+), 144 deletions(-) diff --git a/frontend/apps/artcraft-webapp/src/components/auth/auth-layout.tsx b/frontend/apps/artcraft-webapp/src/components/auth/auth-layout.tsx index a1037b86d2..518093b067 100644 --- a/frontend/apps/artcraft-webapp/src/components/auth/auth-layout.tsx +++ b/frontend/apps/artcraft-webapp/src/components/auth/auth-layout.tsx @@ -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 ; } @@ -74,7 +104,17 @@ export const AuthLayout = () => {
- {authChecked ? ( + {loggedIn && checkoutIntent ? ( +
+ + + Taking you to secure checkout… + +
+ ) : authChecked ? ( ) : (
diff --git a/frontend/apps/artcraft-webapp/src/components/auth/signup-form.tsx b/frontend/apps/artcraft-webapp/src/components/auth/signup-form.tsx index 9be41f5820..88f0e5da93 100644 --- a/frontend/apps/artcraft-webapp/src/components/auth/signup-form.tsx +++ b/frontend/apps/artcraft-webapp/src/components/auth/signup-form.tsx @@ -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 ? ( - + ) : ( "Create account" )} diff --git a/frontend/apps/artcraft-webapp/src/pages/login/login.tsx b/frontend/apps/artcraft-webapp/src/pages/login/login.tsx index fb70fb5c41..5d4a3492d2 100644 --- a/frontend/apps/artcraft-webapp/src/pages/login/login.tsx +++ b/frontend/apps/artcraft-webapp/src/pages/login/login.tsx @@ -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 ? ( - + ) : ( "Log in" )} diff --git a/frontend/apps/artcraft-webapp/src/pages/signup/signup.tsx b/frontend/apps/artcraft-webapp/src/pages/signup/signup.tsx index 9ee3689d61..b811619d71 100644 --- a/frontend/apps/artcraft-webapp/src/pages/signup/signup.tsx +++ b/frontend/apps/artcraft-webapp/src/pages/signup/signup.tsx @@ -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." /> - + {/* 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. */} - + diff --git a/frontend/apps/artcraft-website/src/pages/login/login.tsx b/frontend/apps/artcraft-website/src/pages/login/login.tsx index 767c4896f6..e30e686beb 100644 --- a/frontend/apps/artcraft-website/src/pages/login/login.tsx +++ b/frontend/apps/artcraft-website/src/pages/login/login.tsx @@ -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 = () => {
-
-

Welcome Back

-

Log in to your account

-
+ {redirectingToCheckout ? ( +
+ + Taking you to secure checkout… +
+ ) : ( + <> +
+

Welcome Back

+

Log in to your account

+
-
- {/* + {/* {
*/} -
{ - e.preventDefault(); - handleLogin(); - }} - > - {error && ( -
- {error} -
- )} - -
- - 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" - /> -
-
-
- - - Forgot password? - -
-
- 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" - /> - -
-
- -
- -
-
-
-
- Don't have an account?{" "} - - Sign up - -
+
+ + 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" + /> +
+
+
+ + + Forgot password? + +
+
+ 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" + /> + +
+
+ +
+ +
+ +
+ +
+ Don't have an account?{" "} + + Sign up + +
+ + )}
diff --git a/frontend/apps/artcraft-website/src/pages/signup/signup.tsx b/frontend/apps/artcraft-website/src/pages/signup/signup.tsx index 8058aa5c10..d76f40b229 100644 --- a/frontend/apps/artcraft-website/src/pages/signup/signup.tsx +++ b/frontend/apps/artcraft-website/src/pages/signup/signup.tsx @@ -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 = () => {
-
-

Create an Account

-

Join thousands of creators

-
+ {redirectingToCheckout ? ( +
+ + Taking you to secure checkout… +
+ ) : ( + <> +
+

+ Create an Account +

+

+ Join thousands of creators +

+
- + -
- Already have an account?{" "} - - Log in - -
+
+ Already have an account?{" "} + + Log in + +
+ + )}
diff --git a/frontend/libs/components/pricing-table/src/lib/checkout-intent.ts b/frontend/libs/components/pricing-table/src/lib/checkout-intent.ts index 6b705fb86f..63e111317f 100644 --- a/frontend/libs/components/pricing-table/src/lib/checkout-intent.ts +++ b/frontend/libs/components/pricing-table/src/lib/checkout-intent.ts @@ -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 }; };