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 711b41d847..07c4e74e6f 100644 --- a/frontend/apps/artcraft-webapp/src/components/auth/signup-form.tsx +++ b/frontend/apps/artcraft-webapp/src/components/auth/signup-form.tsx @@ -15,7 +15,7 @@ import { getReferralUsername, getReferrer, } from "@storyteller/common"; -import { refreshSession, useSessionStore } from "../../lib/session"; +import { consumeNewUserWelcome, refreshSession } from "../../lib/session"; import { GoogleLoginButton } from "./GoogleLoginButton"; interface SignupFormProps { @@ -85,13 +85,13 @@ export const SignupForm = ({ } }; - const handleGoogleSuccess = async () => { - // Refresh so the session reflects the new cookie (and whether the account - // still needs a password), then route accordingly. New SSO users skip - // setting a password and go straight to pricing; everyone else goes home. + const handleGoogleSuccess = async (isNewUser: boolean) => { + // Refresh so the session reflects the new cookie, then route accordingly. + // Brand-new accounts see the welcome page once (mirrors the signup flow); + // returning users go home. await refreshSession(true); - if (useSessionStore.getState().passwordNotSet) { - navigate("/pricing"); + if (consumeNewUserWelcome(isNewUser)) { + navigate("/welcome"); } else { navigate("/"); } diff --git a/frontend/apps/artcraft-webapp/src/components/signup-cta-modal.tsx b/frontend/apps/artcraft-webapp/src/components/signup-cta-modal.tsx index 7fc28f045b..d9fb32df25 100644 --- a/frontend/apps/artcraft-webapp/src/components/signup-cta-modal.tsx +++ b/frontend/apps/artcraft-webapp/src/components/signup-cta-modal.tsx @@ -6,7 +6,7 @@ import { faCheck } from "@fortawesome/pro-solid-svg-icons"; import { Modal } from "@storyteller/ui-modal"; import { Button } from "@storyteller/ui-button"; import { GoogleLoginButton } from "./auth"; -import { refreshSession, useSession, useSessionStore } from "../lib/session"; +import { consumeNewUserWelcome, refreshSession, useSession } from "../lib/session"; interface SignupCtaState { isOpen: boolean; @@ -51,13 +51,13 @@ export function SignupCtaModal() { const [error, setError] = useState(null); // Mirror the login/signup pages: refresh the session so it reflects the new - // cookie, then send brand-new SSO users straight to pricing. Everyone else - // just closes the modal and stays on the page they were on. - const handleGoogleSuccess = async () => { + // cookie, then send brand-new accounts to the welcome page (once). Everyone + // else just closes the modal and stays on the page they were on. + const handleGoogleSuccess = async (isNewUser: boolean) => { await refreshSession(true); close(); - if (useSessionStore.getState().passwordNotSet) { - navigate("/pricing"); + if (consumeNewUserWelcome(isNewUser)) { + navigate("/welcome"); } }; diff --git a/frontend/apps/artcraft-webapp/src/lib/session.ts b/frontend/apps/artcraft-webapp/src/lib/session.ts index cf28a8cab4..f5b2c2fd10 100644 --- a/frontend/apps/artcraft-webapp/src/lib/session.ts +++ b/frontend/apps/artcraft-webapp/src/lib/session.ts @@ -81,6 +81,34 @@ export function updateSessionUser(partial: Partial): void { invalidateSession(); } +// Decide whether a just-authenticated user should see the one-time post-signup +// welcome page, and record that they have. Call once after `refreshSession`. +// +// `isNewUser` comes from the SSO response (`username_not_yet_customized`), which +// the backend forces to false for returning Google logins — so it never re-fires +// for normal repeat logins regardless of whether the user customized their +// username. As a belt-and-suspenders guard (e.g. the email→Google-link case where +// the flag can read true on an existing account), we also persist a per-user-token +// marker so the welcome page shows at most once per account on this device. +const WELCOME_SHOWN_KEY_PREFIX = "artcraft.welcomeShown."; + +export function consumeNewUserWelcome(isNewUser: boolean): boolean { + if (!isNewUser) return false; + if (typeof window === "undefined") return true; + + const userToken = useSessionStore.getState().user?.user_token; + if (!userToken) return true; + + const key = `${WELCOME_SHOWN_KEY_PREFIX}${userToken}`; + try { + if (window.localStorage.getItem(key)) return false; + window.localStorage.setItem(key, "1"); + } catch { + // localStorage blocked (e.g. privacy mode) — fall through and just show it. + } + return true; +} + // Attach the auth-change listener exactly once per page load. Login/logout/ // password-reset flows dispatch this event; every consumer shares the resulting // store update instead of each re-running its own effect. diff --git a/frontend/apps/artcraft-webapp/src/pages/login/login.tsx b/frontend/apps/artcraft-webapp/src/pages/login/login.tsx index ede3ad7797..405c19fb22 100644 --- a/frontend/apps/artcraft-webapp/src/pages/login/login.tsx +++ b/frontend/apps/artcraft-webapp/src/pages/login/login.tsx @@ -11,7 +11,7 @@ import { Link, useNavigate, useSearchParams } from "react-router-dom"; import { UsersApi } from "@storyteller/api"; import { AuthHeader, AuthFooter, GoogleLoginButton } from "../../components/auth"; import Seo from "../../components/seo"; -import { refreshSession, useSessionStore } from "../../lib/session"; +import { consumeNewUserWelcome, refreshSession } from "../../lib/session"; const Login = () => { const navigate = useNavigate(); @@ -48,13 +48,13 @@ const Login = () => { } }; - const handleGoogleSuccess = async () => { - // Refresh so the session store reflects the new cookie (and whether the - // account still needs a password) before deciding where to send them. + const handleGoogleSuccess = async (isNewUser: boolean) => { + // Refresh so the session store reflects the new cookie before navigating. await refreshSession(true); - if (useSessionStore.getState().passwordNotSet) { - // New SSO users skip setting a password and go straight to pricing. - navigate("/pricing"); + if (consumeNewUserWelcome(isNewUser)) { + // Brand-new accounts see the welcome page once (mirrors the signup flow); + // returning users go wherever they were headed. + navigate("/welcome"); } else { navigate(redirectTo); }