From 965830fcde50a4d46105eb00e13d8d9312293f08 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sun, 29 Mar 2026 08:17:11 -0400 Subject: [PATCH] tweak --- frontend/src/contexts/AuthContext.jsx | 63 ++++++++++++++-------- frontend/src/pages/subscribe/Subscribe.jsx | 12 +++-- 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index a0d6861..37436a9 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -1,4 +1,4 @@ -import { createContext, useReducer, useContext, useEffect } from "react"; +import { createContext, useReducer, useContext, useEffect, useCallback, useRef } from "react"; import authReducer, { initialState, ACTION_TYPES } from "../reducers/authReducer"; import { loginUser, registerAfterCheckout } from "../api/auth"; import { getAccount } from "../api/account"; @@ -7,58 +7,79 @@ const AuthContext = createContext(null); export function AuthProvider({ children }) { const [state, dispatch] = useReducer(authReducer, initialState); + const fetchedRef = useRef(false); - // On mount, if we have a token in localStorage, hydrate the user object - // by fetching the account from the backend. - useEffect(() => { - if (!state.token || state.user) return; - - getAccount() - .then((account) => { - dispatch({ - type: ACTION_TYPES.SET_USER, - payload: { - id: account.user_id, - email: account.email, - subscriptionStatus: account.subscription_status, - subscriptionTier: account.subscription_tier, - }, - }); - }) - .catch(() => { - dispatch({ type: ACTION_TYPES.LOGOUT }); + const fetchAccount = useCallback(async () => { + try { + const account = await getAccount(); + dispatch({ + type: ACTION_TYPES.SET_USER, + payload: { + id: account.user_id, + email: account.email, + subscriptionStatus: account.subscription_status, + subscriptionTier: account.subscription_tier, + tierLimits: account.tier_limits, + addressCount: account.address_count, + }, }); - }, [state.token, state.user]); + } catch { + dispatch({ type: ACTION_TYPES.LOGOUT }); + } + }, []); + + // On mount, if we have a token in localStorage, hydrate user from backend. + useEffect(() => { + if (!state.token || fetchedRef.current) return; + fetchedRef.current = true; + fetchAccount(); + }, [state.token, fetchAccount]); async function login(email, password) { const data = await loginUser(email, password); dispatch({ type: ACTION_TYPES.LOGIN_SUCCESS, payload: data }); + fetchedRef.current = false; return data; } async function register(email, password, sessionId) { const data = await registerAfterCheckout(email, password, sessionId); dispatch({ type: ACTION_TYPES.LOGIN_SUCCESS, payload: data }); + fetchedRef.current = false; return data; } function logout() { + fetchedRef.current = false; dispatch({ type: ACTION_TYPES.LOGOUT }); } + function refreshAccount() { + fetchAccount(); + } + const isAuthenticated = state.isAuthenticated && !!state.token; const isSubscribed = state.user?.subscriptionStatus === "active" || state.user?.subscriptionStatus === "trialing"; + const tierLimits = state.user?.tierLimits || { + max_addresses: 1, + max_alert_types: 1, + allowed_channels: ["email"], + }; + const value = { user: state.user, token: state.token, isAuthenticated, isSubscribed, + tierLimits, + userTier: state.user?.subscriptionTier || "free", login, register, logout, + refreshAccount, dispatch, ACTION_TYPES, }; diff --git a/frontend/src/pages/subscribe/Subscribe.jsx b/frontend/src/pages/subscribe/Subscribe.jsx index 590e526..3538deb 100644 --- a/frontend/src/pages/subscribe/Subscribe.jsx +++ b/frontend/src/pages/subscribe/Subscribe.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from "react"; +import { useState, useEffect, useRef } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useAuth } from "../../contexts/AuthContext"; import { createOnboardingCheckout, activateFreeTier } from "../../api/stripe"; @@ -26,12 +26,17 @@ export default function Subscribe() { confirmPassword: "", }); + const registerCalledRef = useRef(false); + function set(field, value) { setData((prev) => ({ ...prev, [field]: value })); } - // Handle return from Stripe checkout redirect + // Handle return from Stripe checkout redirect. + // The ref guard prevents React StrictMode from double-firing this. useEffect(() => { + if (registerCalledRef.current) return; + const payment = searchParams.get("payment"); const sessionId = searchParams.get("session_id"); @@ -44,7 +49,6 @@ export default function Subscribe() { if (payment !== "success" || !sessionId) return; - // Recover credentials stashed before the Stripe redirect const savedEmail = sessionStorage.getItem("kp_onboard_email"); const savedPassword = sessionStorage.getItem("kp_onboard_password"); @@ -56,6 +60,7 @@ export default function Subscribe() { return; } + registerCalledRef.current = true; setSearchParams({}, { replace: true }); setLoading(true); @@ -66,6 +71,7 @@ export default function Subscribe() { navigate("/addresses", { replace: true }); }) .catch((err) => { + registerCalledRef.current = false; setError("Registration failed: " + err.message); setStep(1); })