diff --git a/backend/README.md b/backend/README.md index 940937f..9d3d3b8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,4 +1,5 @@ -Backend startup quickstarat: +Backend startup quickstart: + -----------------------------> BEST ## 1. brew services start postgresql@15 diff --git a/frontend/src/contexts/UserPropertiesContext.jsx b/frontend/src/contexts/UserPropertiesContext.jsx new file mode 100644 index 0000000..e2bd3e5 --- /dev/null +++ b/frontend/src/contexts/UserPropertiesContext.jsx @@ -0,0 +1,48 @@ +import { createContext, useContext, useReducer, useEffect } from "react"; +import { + userPropertiesReducer, + initialState, + ACTION_TYPES, +} from "./userPropertiesReducer"; + +const STORAGE_KEY = "kp_onboard_user"; + +function hydrateState() { + try { + const raw = sessionStorage.getItem(STORAGE_KEY); + if (raw) return { ...initialState, ...JSON.parse(raw) }; + } catch { + /* corrupt data — fall through */ + } + return initialState; +} + +const UserPropertiesContext = createContext(initialState); + +export function useUserProperties() { + const ctx = useContext(UserPropertiesContext); + if (!ctx) { + throw new Error( + "useUserProperties must be used within UserPropertiesProvider", + ); + } + return ctx; +} + +export function UserPropertiesProvider({ children }) { + const [state, dispatch] = useReducer(userPropertiesReducer, null, hydrateState); + + useEffect(() => { + if (state.email || state.password) { + sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + } else { + sessionStorage.removeItem(STORAGE_KEY); + } + }, [state]); + + return ( + + {children} + + ); +} diff --git a/frontend/src/contexts/userPropertiesReducer.js b/frontend/src/contexts/userPropertiesReducer.js new file mode 100644 index 0000000..c693ccb --- /dev/null +++ b/frontend/src/contexts/userPropertiesReducer.js @@ -0,0 +1,20 @@ +export const initialState = { + email: "", + password: "", +}; + +export const ACTION_TYPES = { + SET_USER_PROPERTIES: "SET_USER_PROPERTIES", + CLEAR_USER_PROPERTIES: "CLEAR_USER_PROPERTIES", +}; + +export function userPropertiesReducer(state, action) { + switch (action.type) { + case ACTION_TYPES.SET_USER_PROPERTIES: + return { ...state, ...action.payload }; + case ACTION_TYPES.CLEAR_USER_PROPERTIES: + return { ...initialState }; + default: + return state; + } +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx index 00913df..6c791fd 100644 --- a/frontend/src/main.jsx +++ b/frontend/src/main.jsx @@ -2,15 +2,18 @@ import React from "react"; import ReactDOM from "react-dom/client"; import { BrowserRouter } from "react-router-dom"; import { AuthProvider } from "./contexts/AuthContext"; +import { UserPropertiesProvider } from "./contexts/UserPropertiesContext"; import App from "./App"; import "./index.css"; ReactDOM.createRoot(document.getElementById("root")).render( - - - + + + + + , ); diff --git a/frontend/src/pages/subscribe/Subscribe.jsx b/frontend/src/pages/subscribe/Subscribe.jsx index 4031120..4a342a2 100644 --- a/frontend/src/pages/subscribe/Subscribe.jsx +++ b/frontend/src/pages/subscribe/Subscribe.jsx @@ -1,6 +1,7 @@ import { useState, useEffect } from "react"; import { useNavigate, useSearchParams } from "react-router-dom"; import { useAuth } from "../../contexts/AuthContext"; +import { useUserProperties } from "../../contexts/UserPropertiesContext"; import { createOnboardingCheckout, verifyCheckoutSession, @@ -15,6 +16,7 @@ const STEPS = ["Create Account", "Choose Plan"]; export default function Subscribe() { const { currentUser, signup } = useAuth(); + const { state: userProps, dispatch, ACTION_TYPES } = useUserProperties(); const navigate = useNavigate(); const [searchParams, setSearchParams] = useSearchParams(); @@ -51,9 +53,7 @@ export default function Subscribe() { // Phase 1: no account yet — create it. Auth state change remounts the // component (App.jsx swaps route trees) and Phase 2 runs on the next mount. if (!currentUser) { - const savedEmail = sessionStorage.getItem("kp_onboard_email"); - const savedPw = sessionStorage.getItem("kp_onboard_pw"); - if (!savedEmail || !savedPw) { + if (!userProps.email || !userProps.password) { setSearchParams({}, { replace: true }); setError("Session expired. Please start the signup process again."); setStep(1); @@ -61,7 +61,7 @@ export default function Subscribe() { return; } setLoading(true); - signup(savedEmail, savedPw).catch((err) => { + signup(userProps.email, userProps.password).catch((err) => { setSearchParams({}, { replace: true }); setError("Account creation failed: " + err.message); setStep(1); @@ -73,8 +73,7 @@ export default function Subscribe() { // Phase 2: authenticated — verify the checkout and go to the dashboard. setSearchParams({}, { replace: true }); setLoading(true); - sessionStorage.removeItem("kp_onboard_email"); - sessionStorage.removeItem("kp_onboard_pw"); + dispatch({ type: ACTION_TYPES.CLEAR_USER_PROPERTIES }); verifyCheckoutSession(sessionId) .then(() => { @@ -85,7 +84,7 @@ export default function Subscribe() { setStep(2); }) .finally(() => setLoading(false)); - }, [currentUser, searchParams, setSearchParams, signup, navigate]); + }, [currentUser, searchParams, setSearchParams, signup, navigate, userProps, dispatch, ACTION_TYPES]); // ── Step handlers ───────────────────────────────────────────────────────── @@ -124,8 +123,10 @@ export default function Subscribe() { return; } - sessionStorage.setItem("kp_onboard_email", data.email); - sessionStorage.setItem("kp_onboard_pw", data.password); + dispatch({ + type: ACTION_TYPES.SET_USER_PROPERTIES, + payload: { email: data.email, password: data.password }, + }); const { url } = await createOnboardingCheckout( data.email, data.selectedTier,