| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269 |
- "use client";
- import {
- createContext,
- useCallback,
- useContext,
- useEffect,
- useMemo,
- useState,
- type ReactNode,
- } from "react";
- import {
- clearUser,
- ensureThresholdReachedAt,
- loadUser,
- saveUser,
- sumOrders,
- type MockOrder,
- type UserSession,
- } from "@/lib/auth-types";
- import { SPEND_THRESHOLD_USD } from "@/lib/quiz-rules";
- import { ApiError, setApiAuthToken } from "@/lib/api";
- import { loginWithPassword } from "@/lib/auth-api";
- import { isRegisterPasswordValid } from "@/lib/password-rules";
- import { registerWithEmail } from "@/lib/register-api";
- import { fetchCustomUserInfo } from "@/lib/user-info-api";
- type AuthContextValue = {
- user: UserSession | null;
- isReady: boolean;
- login: (
- email: string,
- password: string,
- ) => Promise<{ ok: boolean; error?: string; message?: string }>;
- register: (input: {
- country: string;
- email: string;
- password: string;
- name: string;
- code: string;
- }) => Promise<{ ok: boolean; error?: string; message?: string }>;
- logout: () => void;
- updateProfile: (
- patch: Partial<Pick<UserSession, "name" | "phone" | "identity" | "scholarship">>,
- ) => void;
- addMockOrder: (order: Omit<MockOrder, "id" | "createdAt">) => void;
- /** 本地演示:将门槛达成时间设为 181 天前,用于验证问答解锁 */
- demoUnlockQuizCountdown: () => void;
- };
- const AuthContext = createContext<AuthContextValue | null>(null);
- function newUserSession(
- email: string,
- name: string,
- seedOrders: MockOrder[] = [],
- ): UserSession {
- const thresholdReachedAt = ensureThresholdReachedAt(seedOrders, null);
- return {
- email,
- name,
- phone: "",
- identity: "",
- thresholdReachedAt,
- orders: seedOrders,
- scholarship: {},
- };
- }
- export function AuthProvider({ children }: { children: ReactNode }) {
- const [user, setUser] = useState<UserSession | null>(null);
- const [isReady, setIsReady] = useState(false);
- useEffect(() => {
- /* eslint-disable react-hooks/set-state-in-effect -- hydrate once from localStorage after mount */
- setUser(loadUser());
- setIsReady(true);
- /* eslint-enable react-hooks/set-state-in-effect */
- }, []);
- const persist = useCallback((next: UserSession | null) => {
- setUser(next);
- if (next) saveUser(next);
- else clearUser();
- }, []);
- const login = useCallback(
- async (email: string, password: string) => {
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
- return { ok: false, error: "invalid_email" };
- }
- try {
- const { token, displayName } = await loginWithPassword(
- email.trim(),
- password,
- );
- if (token) setApiAuthToken(token);
- const base = newUserSession(email.trim(), displayName);
- try {
- const info = await fetchCustomUserInfo();
- persist({
- ...base,
- email: info.email || base.email,
- name: info.name || base.name,
- phone: info.phone || base.phone,
- identity: info.identity || base.identity,
- });
- } catch {
- persist(base);
- }
- return { ok: true };
- } catch (e) {
- const message = e instanceof ApiError ? e.message : "登录失败";
- return { ok: false, error: "api", message };
- }
- },
- [persist],
- );
- const register = useCallback(
- async (input: {
- country: string;
- email: string;
- password: string;
- name: string;
- code: string;
- }) => {
- if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(input.email)) {
- return { ok: false, error: "invalid_email" };
- }
- if (!isRegisterPasswordValid(input.password)) {
- return { ok: false, error: "weak_password" };
- }
- if (!input.code.trim()) {
- return { ok: false, error: "invalid_code" };
- }
- if (!input.country.trim()) {
- return { ok: false, error: "country_required" };
- }
- try {
- const { token } = await registerWithEmail({
- country: input.country.trim(),
- name: input.name.trim() || "学员",
- email: input.email.trim(),
- emailCode: input.code.trim(),
- password: input.password,
- });
- if (token) setApiAuthToken(token);
- const base = newUserSession(input.email.trim(), input.name.trim() || "学员");
- try {
- const info = await fetchCustomUserInfo();
- persist({
- ...base,
- email: info.email || base.email,
- name: info.name || base.name,
- phone: info.phone || base.phone,
- identity: info.identity || base.identity,
- });
- } catch {
- persist(base);
- }
- return { ok: true };
- } catch (e) {
- const message = e instanceof ApiError ? e.message : "注册失败";
- return { ok: false, error: "api", message };
- }
- },
- [persist],
- );
- const logout = useCallback(() => {
- setApiAuthToken(null);
- persist(null);
- }, [persist]);
- const updateProfile = useCallback(
- (
- patch: Partial<
- Pick<UserSession, "name" | "phone" | "identity" | "scholarship">
- >,
- ) => {
- if (!user) return;
- persist({
- ...user,
- ...patch,
- name: patch.name ?? user.name,
- phone: patch.phone ?? user.phone,
- identity: patch.identity ?? user.identity,
- scholarship: { ...user.scholarship, ...patch.scholarship },
- });
- },
- [user, persist],
- );
- const addMockOrder = useCallback(
- (order: Omit<MockOrder, "id" | "createdAt">) => {
- if (!user) return;
- const nextOrder: MockOrder = {
- ...order,
- id: `ord_${Date.now()}`,
- createdAt: new Date().toISOString(),
- };
- const orders = [...user.orders, nextOrder];
- const thresholdReachedAt = ensureThresholdReachedAt(
- orders,
- user.thresholdReachedAt,
- );
- persist({
- ...user,
- orders,
- thresholdReachedAt,
- });
- },
- [user, persist],
- );
- const demoUnlockQuizCountdown = useCallback(() => {
- if (!user) return;
- let orders = user.orders;
- if (sumOrders(orders) < SPEND_THRESHOLD_USD) {
- orders = [
- ...orders,
- {
- id: `ord_demo_${Date.now()}`,
- amount: SPEND_THRESHOLD_USD,
- createdAt: new Date().toISOString(),
- title: "演示订单(验证问答解锁)",
- },
- ];
- }
- const past = new Date();
- past.setDate(past.getDate() - 181);
- persist({
- ...user,
- orders,
- thresholdReachedAt: past.toISOString(),
- });
- }, [user, persist]);
- const value = useMemo<AuthContextValue>(
- () => ({
- user,
- isReady,
- login,
- register,
- logout,
- updateProfile,
- addMockOrder,
- demoUnlockQuizCountdown,
- }),
- [
- user,
- isReady,
- login,
- register,
- logout,
- updateProfile,
- addMockOrder,
- demoUnlockQuizCountdown,
- ],
- );
- return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
- }
- export function useAuth() {
- const ctx = useContext(AuthContext);
- if (!ctx) throw new Error("useAuth must be used within AuthProvider");
- return ctx;
- }
|