feat(auth): add stepped auth and password recovery flows
This commit is contained in:
196
src/context/AuthFlowContext.tsx
Normal file
196
src/context/AuthFlowContext.tsx
Normal file
@@ -0,0 +1,196 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user