more
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
Backend startup quickstarat:
|
Backend startup quickstart:
|
||||||
|
|
||||||
-----------------------------> BEST
|
-----------------------------> BEST
|
||||||
## 1. brew services start postgresql@15
|
## 1. brew services start postgresql@15
|
||||||
|
|
||||||
|
|||||||
48
frontend/src/contexts/UserPropertiesContext.jsx
Normal file
48
frontend/src/contexts/UserPropertiesContext.jsx
Normal file
@@ -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 (
|
||||||
|
<UserPropertiesContext.Provider value={{ state, dispatch, ACTION_TYPES }}>
|
||||||
|
{children}
|
||||||
|
</UserPropertiesContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
20
frontend/src/contexts/userPropertiesReducer.js
Normal file
20
frontend/src/contexts/userPropertiesReducer.js
Normal file
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|
||||||
@@ -51,9 +53,7 @@ export default function Subscribe() {
|
|||||||
// Phase 1: no account yet — create it. Auth state change remounts the
|
// 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.
|
// component (App.jsx swaps route trees) and Phase 2 runs on the next mount.
|
||||||
if (!currentUser) {
|
if (!currentUser) {
|
||||||
const savedEmail = sessionStorage.getItem("kp_onboard_email");
|
if (!userProps.email || !userProps.password) {
|
||||||
const savedPw = sessionStorage.getItem("kp_onboard_pw");
|
|
||||||
if (!savedEmail || !savedPw) {
|
|
||||||
setSearchParams({}, { replace: true });
|
setSearchParams({}, { replace: true });
|
||||||
setError("Session expired. Please start the signup process again.");
|
setError("Session expired. Please start the signup process again.");
|
||||||
setStep(1);
|
setStep(1);
|
||||||
@@ -61,7 +61,7 @@ export default function Subscribe() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
signup(savedEmail, savedPw).catch((err) => {
|
signup(userProps.email, userProps.password).catch((err) => {
|
||||||
setSearchParams({}, { replace: true });
|
setSearchParams({}, { replace: true });
|
||||||
setError("Account creation failed: " + err.message);
|
setError("Account creation failed: " + err.message);
|
||||||
setStep(1);
|
setStep(1);
|
||||||
@@ -73,8 +73,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);
|
||||||
sessionStorage.removeItem("kp_onboard_email");
|
dispatch({ type: ACTION_TYPES.CLEAR_USER_PROPERTIES });
|
||||||
sessionStorage.removeItem("kp_onboard_pw");
|
|
||||||
|
|
||||||
verifyCheckoutSession(sessionId)
|
verifyCheckoutSession(sessionId)
|
||||||
.then(() => {
|
.then(() => {
|
||||||
@@ -85,7 +84,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 ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -124,8 +123,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,
|
||||||
|
|||||||
Reference in New Issue
Block a user