197 lines
4.7 KiB
TypeScript
197 lines
4.7 KiB
TypeScript
import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react"
|
|
|
|
type FlowName = "login" | "signup" | "forgotPassword"
|
|
export type CooldownKey =
|
|
| "loginOtpSend"
|
|
| "signupOtpSend"
|
|
| "forgotPasswordOtpSend"
|
|
| "loginPassword"
|
|
| "loginOtpVerify"
|
|
|
|
interface FlowBranchState {
|
|
mobile: string
|
|
code: string
|
|
}
|
|
|
|
interface CooldownState {
|
|
loginOtpSend: number
|
|
signupOtpSend: number
|
|
forgotPasswordOtpSend: number
|
|
loginPassword: number
|
|
loginOtpVerify: number
|
|
}
|
|
|
|
interface AuthFlowState {
|
|
login: FlowBranchState
|
|
signup: FlowBranchState
|
|
forgotPassword: FlowBranchState
|
|
cooldowns: CooldownState
|
|
}
|
|
|
|
interface AuthFlowContextValue {
|
|
state: AuthFlowState
|
|
setMobile: (flow: FlowName, mobile: string) => void
|
|
setCode: (flow: FlowName, code: string) => void
|
|
setCooldown: (key: CooldownKey, seconds: number) => void
|
|
clearCooldown: (key: CooldownKey) => void
|
|
resetFlow: (flow: FlowName) => void
|
|
}
|
|
|
|
const STORAGE_KEY = "auth_flow_state:v1"
|
|
|
|
const defaultState: AuthFlowState = {
|
|
login: {
|
|
mobile: "",
|
|
code: "",
|
|
},
|
|
signup: {
|
|
mobile: "",
|
|
code: "",
|
|
},
|
|
forgotPassword: {
|
|
mobile: "",
|
|
code: "",
|
|
},
|
|
cooldowns: {
|
|
loginOtpSend: 0,
|
|
signupOtpSend: 0,
|
|
forgotPasswordOtpSend: 0,
|
|
loginPassword: 0,
|
|
loginOtpVerify: 0,
|
|
},
|
|
}
|
|
|
|
const AuthFlowContext = createContext<AuthFlowContextValue | null>(null)
|
|
|
|
const parseStoredState = (): AuthFlowState => {
|
|
if (typeof window === "undefined") {
|
|
return defaultState
|
|
}
|
|
|
|
const raw = window.sessionStorage.getItem(STORAGE_KEY)
|
|
if (!raw) {
|
|
return defaultState
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(raw) as Partial<AuthFlowState>
|
|
|
|
return {
|
|
login: {
|
|
mobile: parsed.login?.mobile ?? "",
|
|
code: parsed.login?.code ?? "",
|
|
},
|
|
signup: {
|
|
mobile: parsed.signup?.mobile ?? "",
|
|
code: parsed.signup?.code ?? "",
|
|
},
|
|
forgotPassword: {
|
|
mobile: parsed.forgotPassword?.mobile ?? "",
|
|
code: parsed.forgotPassword?.code ?? "",
|
|
},
|
|
cooldowns: {
|
|
loginOtpSend: parsed.cooldowns?.loginOtpSend ?? 0,
|
|
signupOtpSend: parsed.cooldowns?.signupOtpSend ?? 0,
|
|
forgotPasswordOtpSend: parsed.cooldowns?.forgotPasswordOtpSend ?? 0,
|
|
loginPassword: parsed.cooldowns?.loginPassword ?? 0,
|
|
loginOtpVerify: parsed.cooldowns?.loginOtpVerify ?? 0,
|
|
},
|
|
}
|
|
} catch {
|
|
return defaultState
|
|
}
|
|
}
|
|
|
|
export function AuthFlowProvider({ children }: { children: ReactNode }) {
|
|
const [state, setState] = useState<AuthFlowState>(parseStoredState)
|
|
|
|
useEffect(() => {
|
|
window.sessionStorage.setItem(STORAGE_KEY, JSON.stringify(state))
|
|
}, [state])
|
|
|
|
useEffect(() => {
|
|
if (!Object.values(state.cooldowns).some((value) => value > 0)) {
|
|
return
|
|
}
|
|
|
|
const timer = window.setInterval(() => {
|
|
setState((current) => ({
|
|
...current,
|
|
cooldowns: {
|
|
loginOtpSend: Math.max(0, current.cooldowns.loginOtpSend - 1),
|
|
signupOtpSend: Math.max(0, current.cooldowns.signupOtpSend - 1),
|
|
forgotPasswordOtpSend: Math.max(0, current.cooldowns.forgotPasswordOtpSend - 1),
|
|
loginPassword: Math.max(0, current.cooldowns.loginPassword - 1),
|
|
loginOtpVerify: Math.max(0, current.cooldowns.loginOtpVerify - 1),
|
|
},
|
|
}))
|
|
}, 1000)
|
|
|
|
return () => window.clearInterval(timer)
|
|
}, [state.cooldowns])
|
|
|
|
const value = useMemo<AuthFlowContextValue>(
|
|
() => ({
|
|
state,
|
|
setMobile: (flow, mobile) => {
|
|
setState((current) => ({
|
|
...current,
|
|
[flow]: {
|
|
...current[flow],
|
|
mobile,
|
|
},
|
|
}))
|
|
},
|
|
setCode: (flow, code) => {
|
|
setState((current) => ({
|
|
...current,
|
|
[flow]: {
|
|
...current[flow],
|
|
code,
|
|
},
|
|
}))
|
|
},
|
|
setCooldown: (key, seconds) => {
|
|
setState((current) => ({
|
|
...current,
|
|
cooldowns: {
|
|
...current.cooldowns,
|
|
[key]: Math.max(current.cooldowns[key], seconds),
|
|
},
|
|
}))
|
|
},
|
|
clearCooldown: (key) => {
|
|
setState((current) => ({
|
|
...current,
|
|
cooldowns: {
|
|
...current.cooldowns,
|
|
[key]: 0,
|
|
},
|
|
}))
|
|
},
|
|
resetFlow: (flow) => {
|
|
setState((current) => ({
|
|
...current,
|
|
[flow]: {
|
|
mobile: "",
|
|
code: "",
|
|
},
|
|
}))
|
|
},
|
|
}),
|
|
[state],
|
|
)
|
|
|
|
return <AuthFlowContext.Provider value={value}>{children}</AuthFlowContext.Provider>
|
|
}
|
|
|
|
export function useAuthFlow() {
|
|
const context = useContext(AuthFlowContext)
|
|
|
|
if (!context) {
|
|
throw new Error("useAuthFlow must be used within an AuthFlowProvider")
|
|
}
|
|
|
|
return context
|
|
}
|