From 3a52e7afb31442be496a2d3124977090c62d5ccd Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sun, 29 Mar 2026 00:02:38 -0400 Subject: [PATCH 1/5] more --- backend/README.md | 3 +- .../src/contexts/UserPropertiesContext.jsx | 48 +++++++++++++++++++ .../src/contexts/userPropertiesReducer.js | 20 ++++++++ frontend/src/main.jsx | 9 ++-- frontend/src/pages/subscribe/Subscribe.jsx | 19 ++++---- 5 files changed, 86 insertions(+), 13 deletions(-) create mode 100644 frontend/src/contexts/UserPropertiesContext.jsx create mode 100644 frontend/src/contexts/userPropertiesReducer.js 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, From c0aaaedaf13c47b3f0148d24f7ab93797f206690 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sun, 29 Mar 2026 01:31:20 -0400 Subject: [PATCH 2/5] more --- frontend/src/contexts/AuthContext.jsx | 176 +++++------------- .../src/contexts/UserPropertiesContext.jsx | 48 ----- .../src/contexts/userPropertiesReducer.js | 20 -- frontend/src/reducers/authReducer.jsx | 25 +++ 4 files changed, 69 insertions(+), 200 deletions(-) delete mode 100644 frontend/src/contexts/UserPropertiesContext.jsx delete mode 100644 frontend/src/contexts/userPropertiesReducer.js create mode 100644 frontend/src/reducers/authReducer.jsx diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index 0413bd3..81169ef 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -1,140 +1,52 @@ -/** - * AuthContext - Firebase Authentication State Management - * - * Provides authentication state, tier info, and methods throughout the app - */ +import { createContext, useReducer, useContext } from "react"; +import shopReducer, { initialState } from "./shopReducer"; -import { createContext, useContext, useEffect, useState, useCallback } from "react"; -import { - createUserWithEmailAndPassword, - signInWithEmailAndPassword, - signOut, - onAuthStateChanged, -} from "firebase/auth"; -import { auth } from "../firebase/config"; -import { getAccount } from "../api/account"; +const AuthContext = createContext(initialState); -const AuthContext = createContext(); +export const AuthProvider = ({ children }) => { + const [state, dispatch] = useReducer(shopReducer, initialState); -const DEFAULT_TIER_LIMITS = { - max_addresses: 1, - max_alert_types: 1, - allowed_channels: ["email"], + const addAuthUser = (args) => { + const updatedUser = state.user.concat(product); + updatePrice(updatedCart); + dispatch({ + type: "ADD_AUTH_USER", + payload: { + args: updatedCart + } + }); + }; + + const logoutAuthUser = (args) => { + const updatedUser = state.user.filter( + (currentUser) => currentUser.name !== args.name + ); + updateUser(updatedCart); + + dispatch({ + type: "REMOVE_AUTH_USER", + payload: { + args: updatedCart + } + }); + }; + + + const value = { + user: state.name, + user: state.tier + }; + return {children}; }; -/** - * Hook to access auth context - * @returns {Object} Auth context value - */ -export function useAuth() { - const context = useContext(AuthContext); - if (!context) { - throw new Error("useAuth must be used within AuthProvider"); - } - return context; -} +const useAuthContext = () => { + const context = useContext(ShopContext); -/** - * AuthProvider - Wraps app and provides auth state + tier info - */ -export function AuthProvider({ children }) { - const [currentUser, setCurrentUser] = useState(null); - const [loading, setLoading] = useState(true); - const [error, setError] = useState(null); + if (context === undefined) { + throw new Error("authUser must be used within AuthContext"); + } - const [userTier, setUserTier] = useState("free"); - const [tierLimits, setTierLimits] = useState(DEFAULT_TIER_LIMITS); - const [addressCount, setAddressCount] = useState(0); - const [subscriptionStatus, setSubscriptionStatus] = useState("none"); + return context; +}; - const refreshAccount = useCallback(async () => { - try { - const data = await getAccount(); - setUserTier(data.subscription_tier || "free"); - setTierLimits(data.tier_limits || DEFAULT_TIER_LIMITS); - setAddressCount(data.address_count || 0); - setSubscriptionStatus(data.subscription_status || "none"); - } catch { - // account fetch can fail during onboarding before subscription is active - } - }, []); - - async function signup(email, password) { - try { - setError(null); - const result = await createUserWithEmailAndPassword( - auth, - email, - password, - ); - return result.user; - } catch (err) { - setError(err.message); - throw err; - } - } - - async function login(email, password) { - try { - setError(null); - const result = await signInWithEmailAndPassword( - auth, - email, - password, - ); - return result.user; - } catch (err) { - setError(err.message); - throw err; - } - } - - async function logout() { - try { - setError(null); - setUserTier("free"); - setTierLimits(DEFAULT_TIER_LIMITS); - setAddressCount(0); - setSubscriptionStatus("none"); - await signOut(auth); - } catch (err) { - setError(err.message); - throw err; - } - } - - useEffect(() => { - const unsubscribe = onAuthStateChanged(auth, (user) => { - setCurrentUser(user); - setLoading(false); - if (user) { - refreshAccount(); - } - }); - return unsubscribe; - }, [refreshAccount]); - - const isSubscribed = - subscriptionStatus === "active" || subscriptionStatus === "trialing"; - - const value = { - currentUser, - signup, - login, - logout, - error, - loading, - userTier, - tierLimits, - addressCount, - subscriptionStatus, - isSubscribed, - refreshAccount, - }; - - return ( - - {!loading && children} - - ); -} +export default useAuthContext; diff --git a/frontend/src/contexts/UserPropertiesContext.jsx b/frontend/src/contexts/UserPropertiesContext.jsx deleted file mode 100644 index e2bd3e5..0000000 --- a/frontend/src/contexts/UserPropertiesContext.jsx +++ /dev/null @@ -1,48 +0,0 @@ -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 deleted file mode 100644 index c693ccb..0000000 --- a/frontend/src/contexts/userPropertiesReducer.js +++ /dev/null @@ -1,20 +0,0 @@ -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/reducers/authReducer.jsx b/frontend/src/reducers/authReducer.jsx new file mode 100644 index 0000000..d227086 --- /dev/null +++ b/frontend/src/reducers/authReducer.jsx @@ -0,0 +1,25 @@ +import { useReducer } from "react" + +const UserAuthContext = createContext(); + +export const initialState = { + authUser: null, + AuthTier: null +}; + +const authReducer = (state, action) => { + const { type, payload = "FREE_TIER" } = action + + switch (type) { + case "ADD_AUTH_USER": + console.log("ADD_AUTH_USER", payload); + return { + ...state, + auth: payload + }; + default: + throw new Error(`No case for type ${type} found.`); + } +} + +export default shopReducer \ No newline at end of file From 1418e7f97ca07eb7f6b7e36ac7c24a3099b0b2e2 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sun, 29 Mar 2026 01:48:55 -0400 Subject: [PATCH 3/5] more --- frontend/src/App.jsx | 5 +++- frontend/src/contexts/AuthContext.jsx | 10 +++++--- frontend/src/reducers/authReducer.jsx | 37 ++++++++++++++++++--------- 3 files changed, 35 insertions(+), 17 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 47fb0d3..52afc9c 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -1,5 +1,6 @@ import { Routes, Route, Navigate } from "react-router-dom"; import { useAuth } from "./contexts/AuthContext"; +import { useContext } from 'react' import Navbar from "./components/Navbar"; import Login from "./pages/login/Login"; import Signup from "./pages/Signup"; @@ -8,9 +9,11 @@ import Addresses from "./pages/addresses/Addresses"; import Alerts from "./pages/alerts/Alerts"; import AlertHistory from "./pages/alertHistory/AlertHistory"; import Account from "./pages/user_account/Account"; +import { AuthProvider } from "./ShopContext"; +import useAuth from "./ShopContext"; export default function App() { - const { currentUser, isSubscribed } = useAuth(); + const { context } = useContext(ShopContext) if (!currentUser) { return ( diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index 81169ef..ce70e46 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -33,13 +33,15 @@ export const AuthProvider = ({ children }) => { const value = { - user: state.name, - user: state.tier + authUser: state.name, + authUserTier: state.tier, + addAuthUser, + logoutAuthUser }; return {children}; }; -const useAuthContext = () => { +const useAuth = () => { const context = useContext(ShopContext); if (context === undefined) { @@ -49,4 +51,4 @@ const useAuthContext = () => { return context; }; -export default useAuthContext; +export default useAuth; diff --git a/frontend/src/reducers/authReducer.jsx b/frontend/src/reducers/authReducer.jsx index d227086..f29f16f 100644 --- a/frontend/src/reducers/authReducer.jsx +++ b/frontend/src/reducers/authReducer.jsx @@ -1,25 +1,38 @@ import { useReducer } from "react" -const UserAuthContext = createContext(); - export const initialState = { - authUser: null, - AuthTier: null + user: null, + tier: null, }; -const authReducer = (state, action) => { - const { type, payload = "FREE_TIER" } = action +const shopReducer = (state, action) => { + const { name, payload } = action; switch (type) { - case "ADD_AUTH_USER": - console.log("ADD_AUTH_USER", payload); + case "ADD_TO_CART": + console.log("ADD_TO_CART", payload); + return { ...state, - auth: payload + products: payload.products + }; + case "REMOVE_FROM_CART": + console.log("REMOVE_FROM_CART", payload); + + return { + ...state, + products: payload.products + }; + case "UPDATE_PRICE": + console.log("UPDATE_PRICE", payload); + + return { + ...state, + total: payload.total }; default: - throw new Error(`No case for type ${type} found.`); + throw new Error(`No case for type ${type} found in shopReducer.`); } -} +}; -export default shopReducer \ No newline at end of file +export default shopReducer; \ No newline at end of file From 9d2a0678be616ffba31149b6ed8e8244d0061c49 Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sun, 29 Mar 2026 07:02:20 -0400 Subject: [PATCH 4/5] more --- frontend/src/reducers/authReducer.jsx | 44 ++++++++++++--------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/frontend/src/reducers/authReducer.jsx b/frontend/src/reducers/authReducer.jsx index f29f16f..dd1c505 100644 --- a/frontend/src/reducers/authReducer.jsx +++ b/frontend/src/reducers/authReducer.jsx @@ -1,38 +1,32 @@ import { useReducer } from "react" +const UserAuthContext = createContext(); + export const initialState = { - user: null, - tier: null, + authUser: null, + AuthTier: null }; -const shopReducer = (state, action) => { - const { name, payload } = action; +const authReducer = (state, action) => { + const { type, payload = "FREE_TIER" } = action switch (type) { - case "ADD_TO_CART": - console.log("ADD_TO_CART", payload); - + case "ADD_AUTH_USER": + console.log("ADD_AUTH_USER", payload); return { ...state, - products: payload.products - }; - case "REMOVE_FROM_CART": - console.log("REMOVE_FROM_CART", payload); - - return { - ...state, - products: payload.products - }; - case "UPDATE_PRICE": - console.log("UPDATE_PRICE", payload); - - return { - ...state, - total: payload.total + auth: payload }; default: - throw new Error(`No case for type ${type} found in shopReducer.`); + throw new Error(`No case for type ${type} found.`); } -}; + switch (type) { + case "DELETE_AUTH_USER": + return { + ...state, + users: state.users.filter(user => user.id !== action.userIdToDelete) + }; + } +} -export default shopReducer; \ No newline at end of file +export default shopReducer \ No newline at end of file From 0920985a74dc76bff78fbbdccbc3815eeece686d Mon Sep 17 00:00:00 2001 From: KS Jannette Date: Sun, 29 Mar 2026 07:04:25 -0400 Subject: [PATCH 5/5] more --- frontend/src/contexts/AuthContext.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/contexts/AuthContext.jsx b/frontend/src/contexts/AuthContext.jsx index ce70e46..47cc435 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -12,7 +12,7 @@ export const AuthProvider = ({ children }) => { dispatch({ type: "ADD_AUTH_USER", payload: { - args: updatedCart + args: newUser } }); };