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/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 0413bd3..47cc435 100644 --- a/frontend/src/contexts/AuthContext.jsx +++ b/frontend/src/contexts/AuthContext.jsx @@ -1,140 +1,54 @@ -/** - * 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: newUser + } + }); + }; + + 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 = { + authUser: state.name, + authUserTier: state.tier, + addAuthUser, + logoutAuthUser + }; + 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 useAuth = () => { + 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 useAuth; 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 fc3c3ca..98e44e7 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(); @@ -89,9 +91,7 @@ export default function Subscribe() { // Phase 2: authenticated — verify the checkout and go to the dashboard. setSearchParams({}, { replace: true }); setLoading(true); - if (signupLockKey) sessionStorage.removeItem(signupLockKey); - sessionStorage.removeItem("kp_onboard_email"); - sessionStorage.removeItem("kp_onboard_pw"); + dispatch({ type: ACTION_TYPES.CLEAR_USER_PROPERTIES }); verifyCheckoutSession(sessionId) .then(() => { @@ -102,7 +102,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 ───────────────────────────────────────────────────────── @@ -141,8 +141,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, diff --git a/frontend/src/reducers/authReducer.jsx b/frontend/src/reducers/authReducer.jsx new file mode 100644 index 0000000..dd1c505 --- /dev/null +++ b/frontend/src/reducers/authReducer.jsx @@ -0,0 +1,32 @@ +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.`); + } + 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