Merge branch 'master' into finishing-signup-stages

This commit is contained in:
S Jannette
2026-03-29 16:05:47 -04:00
committed by GitHub
6 changed files with 98 additions and 143 deletions

View File

@@ -1,4 +1,5 @@
Backend startup quickstarat: Backend startup quickstart:
-----------------------------> BEST -----------------------------> BEST
## 1. brew services start postgresql@15 ## 1. brew services start postgresql@15

View File

@@ -1,5 +1,6 @@
import { Routes, Route, Navigate } from "react-router-dom"; import { Routes, Route, Navigate } from "react-router-dom";
import { useAuth } from "./contexts/AuthContext"; import { useAuth } from "./contexts/AuthContext";
import { useContext } from 'react'
import Navbar from "./components/Navbar"; import Navbar from "./components/Navbar";
import Login from "./pages/login/Login"; import Login from "./pages/login/Login";
import Signup from "./pages/Signup"; import Signup from "./pages/Signup";
@@ -8,9 +9,11 @@ import Addresses from "./pages/addresses/Addresses";
import Alerts from "./pages/alerts/Alerts"; import Alerts from "./pages/alerts/Alerts";
import AlertHistory from "./pages/alertHistory/AlertHistory"; import AlertHistory from "./pages/alertHistory/AlertHistory";
import Account from "./pages/user_account/Account"; import Account from "./pages/user_account/Account";
import { AuthProvider } from "./ShopContext";
import useAuth from "./ShopContext";
export default function App() { export default function App() {
const { currentUser, isSubscribed } = useAuth(); const { context } = useContext(ShopContext)
if (!currentUser) { if (!currentUser) {
return ( return (

View File

@@ -1,140 +1,54 @@
/** import { createContext, useReducer, useContext } from "react";
* AuthContext - Firebase Authentication State Management import shopReducer, { initialState } from "./shopReducer";
*
* Provides authentication state, tier info, and methods throughout the app
*/
import { createContext, useContext, useEffect, useState, useCallback } from "react"; const AuthContext = createContext(initialState);
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
} from "firebase/auth";
import { auth } from "../firebase/config";
import { getAccount } from "../api/account";
const AuthContext = createContext(); export const AuthProvider = ({ children }) => {
const [state, dispatch] = useReducer(shopReducer, initialState);
const DEFAULT_TIER_LIMITS = { const addAuthUser = (args) => {
max_addresses: 1, const updatedUser = state.user.concat(product);
max_alert_types: 1, updatePrice(updatedCart);
allowed_channels: ["email"], 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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}; };
/** const useAuth = () => {
* Hook to access auth context const context = useContext(ShopContext);
* @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;
}
/** if (context === undefined) {
* AuthProvider - Wraps app and provides auth state + tier info throw new Error("authUser must be used within AuthContext");
*/ }
export function AuthProvider({ children }) {
const [currentUser, setCurrentUser] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
const [userTier, setUserTier] = useState("free"); return context;
const [tierLimits, setTierLimits] = useState(DEFAULT_TIER_LIMITS); };
const [addressCount, setAddressCount] = useState(0);
const [subscriptionStatus, setSubscriptionStatus] = useState("none");
const refreshAccount = useCallback(async () => { export default useAuth;
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 (
<AuthContext.Provider value={value}>
{!loading && children}
</AuthContext.Provider>
);
}

View File

@@ -2,15 +2,18 @@ import React from "react";
import ReactDOM from "react-dom/client"; import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { AuthProvider } from "./contexts/AuthContext"; import { AuthProvider } from "./contexts/AuthContext";
import { UserPropertiesProvider } from "./contexts/UserPropertiesContext";
import App from "./App"; import App from "./App";
import "./index.css"; import "./index.css";
ReactDOM.createRoot(document.getElementById("root")).render( ReactDOM.createRoot(document.getElementById("root")).render(
<React.StrictMode> <React.StrictMode>
<BrowserRouter> <BrowserRouter>
<AuthProvider> <UserPropertiesProvider>
<App /> <AuthProvider>
</AuthProvider> <App />
</AuthProvider>
</UserPropertiesProvider>
</BrowserRouter> </BrowserRouter>
</React.StrictMode>, </React.StrictMode>,
); );

View File

@@ -1,6 +1,7 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { useAuth } from "../../contexts/AuthContext"; import { useAuth } from "../../contexts/AuthContext";
import { useUserProperties } from "../../contexts/UserPropertiesContext";
import { import {
createOnboardingCheckout, createOnboardingCheckout,
verifyCheckoutSession, verifyCheckoutSession,
@@ -15,6 +16,7 @@ const STEPS = ["Create Account", "Choose Plan"];
export default function Subscribe() { export default function Subscribe() {
const { currentUser, signup } = useAuth(); const { currentUser, signup } = useAuth();
const { state: userProps, dispatch, ACTION_TYPES } = useUserProperties();
const navigate = useNavigate(); const navigate = useNavigate();
const [searchParams, setSearchParams] = useSearchParams(); const [searchParams, setSearchParams] = useSearchParams();
@@ -89,9 +91,7 @@ export default function Subscribe() {
// Phase 2: authenticated — verify the checkout and go to the dashboard. // Phase 2: authenticated — verify the checkout and go to the dashboard.
setSearchParams({}, { replace: true }); setSearchParams({}, { replace: true });
setLoading(true); setLoading(true);
if (signupLockKey) sessionStorage.removeItem(signupLockKey); dispatch({ type: ACTION_TYPES.CLEAR_USER_PROPERTIES });
sessionStorage.removeItem("kp_onboard_email");
sessionStorage.removeItem("kp_onboard_pw");
verifyCheckoutSession(sessionId) verifyCheckoutSession(sessionId)
.then(() => { .then(() => {
@@ -102,7 +102,7 @@ export default function Subscribe() {
setStep(2); setStep(2);
}) })
.finally(() => setLoading(false)); .finally(() => setLoading(false));
}, [currentUser, searchParams, setSearchParams, signup, navigate]); }, [currentUser, searchParams, setSearchParams, signup, navigate, userProps, dispatch, ACTION_TYPES]);
// ── Step handlers ───────────────────────────────────────────────────────── // ── Step handlers ─────────────────────────────────────────────────────────
@@ -141,8 +141,10 @@ export default function Subscribe() {
return; return;
} }
sessionStorage.setItem("kp_onboard_email", data.email); dispatch({
sessionStorage.setItem("kp_onboard_pw", data.password); type: ACTION_TYPES.SET_USER_PROPERTIES,
payload: { email: data.email, password: data.password },
});
const { url } = await createOnboardingCheckout( const { url } = await createOnboardingCheckout(
data.email, data.email,
data.selectedTier, data.selectedTier,

View File

@@ -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