1224 lines
32 KiB
TypeScript
1224 lines
32 KiB
TypeScript
|
|
import {
|
||
|
|
createContext,
|
||
|
|
FC,
|
||
|
|
PropsWithChildren,
|
||
|
|
useCallback,
|
||
|
|
useContext,
|
||
|
|
useEffect,
|
||
|
|
useMemo,
|
||
|
|
useRef,
|
||
|
|
useState,
|
||
|
|
} from "react"
|
||
|
|
import { AppState, AppStateStatus } from "react-native"
|
||
|
|
import { GoogleSignin, statusCodes } from "@react-native-google-signin/google-signin"
|
||
|
|
import i18n from "i18next"
|
||
|
|
import { useMMKVString } from "react-native-mmkv"
|
||
|
|
|
||
|
|
import Config from "@/config"
|
||
|
|
import { useToast } from "@/context/ToastContext"
|
||
|
|
import { authApi } from "@/services/api/authApi"
|
||
|
|
import type {
|
||
|
|
ForgotPasswordStep,
|
||
|
|
LoginStep,
|
||
|
|
RegisterStep,
|
||
|
|
User,
|
||
|
|
UpdateProfileRequest,
|
||
|
|
} from "@/services/api/authTypes"
|
||
|
|
import { translateAuthError } from "@/utils/authErrorTranslator"
|
||
|
|
|
||
|
|
// Initialize Google Sign-In
|
||
|
|
GoogleSignin.configure({
|
||
|
|
webClientId: Config.GOOGLE_WEB_CLIENT_ID,
|
||
|
|
iosClientId: Config.GOOGLE_IOS_CLIENT_ID,
|
||
|
|
})
|
||
|
|
|
||
|
|
// Email change step type
|
||
|
|
export type EmailChangeStep = "idle" | "verify-current" | "bind-new"
|
||
|
|
|
||
|
|
export type AuthContextType = {
|
||
|
|
// State
|
||
|
|
isAuthenticated: boolean
|
||
|
|
user: User | null
|
||
|
|
accessToken?: string
|
||
|
|
refreshToken?: string
|
||
|
|
accessTokenExpiry?: string
|
||
|
|
refreshTokenExpiry?: string
|
||
|
|
pendingEmail: string
|
||
|
|
loginStep: LoginStep
|
||
|
|
registerStep: RegisterStep
|
||
|
|
forgotPasswordStep: ForgotPasswordStep
|
||
|
|
emailChangeStep: EmailChangeStep
|
||
|
|
isLoading: boolean
|
||
|
|
error: string
|
||
|
|
|
||
|
|
// Legacy - for backward compatibility
|
||
|
|
authToken?: string
|
||
|
|
authEmail?: string
|
||
|
|
setAuthToken: (token?: string) => void
|
||
|
|
setAuthEmail: (email: string) => void
|
||
|
|
validationError: string
|
||
|
|
|
||
|
|
// Login methods
|
||
|
|
preLogin: (email: string, password: string) => Promise<boolean>
|
||
|
|
resendCode: () => Promise<boolean>
|
||
|
|
verifyLogin: (code: string) => Promise<boolean>
|
||
|
|
googleLogin: () => Promise<boolean>
|
||
|
|
resetLoginStep: () => void
|
||
|
|
|
||
|
|
// Register methods
|
||
|
|
preRegister: (
|
||
|
|
username: string,
|
||
|
|
password: string,
|
||
|
|
email: string,
|
||
|
|
nickname?: string,
|
||
|
|
referralCode?: string,
|
||
|
|
) => Promise<boolean>
|
||
|
|
resendRegisterCode: () => Promise<boolean>
|
||
|
|
verifyRegister: (code: string) => Promise<boolean>
|
||
|
|
resetRegisterStep: () => void
|
||
|
|
|
||
|
|
// Forgot password methods
|
||
|
|
forgotPassword: (email: string) => Promise<boolean>
|
||
|
|
resendForgotPasswordCode: () => Promise<boolean>
|
||
|
|
resetPassword: (code: string, newPassword: string) => Promise<boolean>
|
||
|
|
resetForgotPasswordStep: () => void
|
||
|
|
|
||
|
|
// Profile methods
|
||
|
|
fetchProfile: () => Promise<boolean>
|
||
|
|
updateProfile: (data: UpdateProfileRequest) => Promise<boolean>
|
||
|
|
|
||
|
|
// Password methods
|
||
|
|
changePassword: (
|
||
|
|
oldPassword: string,
|
||
|
|
newPassword: string,
|
||
|
|
logoutOtherDevices?: boolean,
|
||
|
|
) => Promise<boolean>
|
||
|
|
|
||
|
|
// Email change methods
|
||
|
|
sendEmailCode: (email: string) => Promise<boolean>
|
||
|
|
verifyCurrentEmail: (code: string) => Promise<boolean>
|
||
|
|
bindNewEmail: (newEmail: string, code: string) => Promise<boolean>
|
||
|
|
resetEmailChangeStep: () => void
|
||
|
|
|
||
|
|
// Common methods
|
||
|
|
clearError: () => void
|
||
|
|
logout: () => void
|
||
|
|
}
|
||
|
|
|
||
|
|
export const AuthContext = createContext<AuthContextType | null>(null)
|
||
|
|
|
||
|
|
export interface AuthProviderProps {}
|
||
|
|
|
||
|
|
// One day in milliseconds
|
||
|
|
const ONE_DAY_MS = 24 * 60 * 60 * 1000
|
||
|
|
|
||
|
|
export const AuthProvider: FC<PropsWithChildren<AuthProviderProps>> = ({ children }) => {
|
||
|
|
// Toast for error notifications
|
||
|
|
const toast = useToast()
|
||
|
|
|
||
|
|
// MMKV persistence
|
||
|
|
const [accessToken, setAccessToken] = useMMKVString("AuthProvider.accessToken")
|
||
|
|
const [refreshToken, setRefreshToken] = useMMKVString("AuthProvider.refreshToken")
|
||
|
|
const [accessTokenExpiry, setAccessTokenExpiry] = useMMKVString("AuthProvider.accessTokenExpiry")
|
||
|
|
const [refreshTokenExpiry, setRefreshTokenExpiry] = useMMKVString(
|
||
|
|
"AuthProvider.refreshTokenExpiry",
|
||
|
|
)
|
||
|
|
const [userJson, setUserJson] = useMMKVString("AuthProvider.user")
|
||
|
|
|
||
|
|
// Legacy compatibility
|
||
|
|
const [authEmail, setAuthEmail] = useMMKVString("AuthProvider.authEmail")
|
||
|
|
|
||
|
|
// Local state
|
||
|
|
const [pendingEmail, setPendingEmail] = useState("")
|
||
|
|
const [loginStep, setLoginStep] = useState<LoginStep>("credentials")
|
||
|
|
const [registerStep, setRegisterStep] = useState<RegisterStep>("credentials")
|
||
|
|
const [forgotPasswordStep, setForgotPasswordStep] = useState<ForgotPasswordStep>("email")
|
||
|
|
const [emailChangeStep, setEmailChangeStep] = useState<EmailChangeStep>("idle")
|
||
|
|
const [isLoading, setIsLoading] = useState(false)
|
||
|
|
const [error, setError] = useState("")
|
||
|
|
|
||
|
|
// Ref to track if token refresh is in progress
|
||
|
|
const isRefreshing = useRef(false)
|
||
|
|
|
||
|
|
// Parse user from JSON
|
||
|
|
const user = useMemo<User | null>(() => {
|
||
|
|
if (!userJson) return null
|
||
|
|
try {
|
||
|
|
return JSON.parse(userJson)
|
||
|
|
} catch {
|
||
|
|
return null
|
||
|
|
}
|
||
|
|
}, [userJson])
|
||
|
|
|
||
|
|
// Legacy validation error
|
||
|
|
const validationError = useMemo(() => {
|
||
|
|
if (!authEmail || authEmail.length === 0) return "can't be blank"
|
||
|
|
if (authEmail.length < 6) return "must be at least 6 characters"
|
||
|
|
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(authEmail)) return "must be a valid email address"
|
||
|
|
return ""
|
||
|
|
}, [authEmail])
|
||
|
|
|
||
|
|
// Check if token needs refresh (within 1 day of expiry)
|
||
|
|
const shouldRefreshToken = useCallback(() => {
|
||
|
|
if (!accessTokenExpiry || !refreshToken) return false
|
||
|
|
|
||
|
|
const expiryTime = new Date(accessTokenExpiry).getTime()
|
||
|
|
const now = Date.now()
|
||
|
|
const timeUntilExpiry = expiryTime - now
|
||
|
|
|
||
|
|
// Refresh if within 1 day of expiry
|
||
|
|
return timeUntilExpiry > 0 && timeUntilExpiry <= ONE_DAY_MS
|
||
|
|
}, [accessTokenExpiry, refreshToken])
|
||
|
|
|
||
|
|
// Check if refresh token is expired
|
||
|
|
const isRefreshTokenExpired = useCallback(() => {
|
||
|
|
if (!refreshTokenExpiry) return true
|
||
|
|
|
||
|
|
const expiryTime = new Date(refreshTokenExpiry).getTime()
|
||
|
|
return Date.now() >= expiryTime
|
||
|
|
}, [refreshTokenExpiry])
|
||
|
|
|
||
|
|
// Perform token refresh
|
||
|
|
const performTokenRefresh = useCallback(async (): Promise<boolean> => {
|
||
|
|
if (!refreshToken || isRefreshing.current) return false
|
||
|
|
|
||
|
|
// Check if refresh token is expired
|
||
|
|
if (isRefreshTokenExpired()) {
|
||
|
|
if (__DEV__) console.log("Refresh token expired, logging out...")
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
isRefreshing.current = true
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.refreshToken(refreshToken)
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success && result.data.data) {
|
||
|
|
const { accessToken: newAccessToken, accessTokenExpiry: newExpiry } = result.data.data
|
||
|
|
setAccessToken(newAccessToken)
|
||
|
|
setAccessTokenExpiry(newExpiry)
|
||
|
|
if (__DEV__) console.log("Token refreshed successfully")
|
||
|
|
isRefreshing.current = false
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (__DEV__) console.log("Token refresh failed:", result)
|
||
|
|
isRefreshing.current = false
|
||
|
|
return false
|
||
|
|
} catch (e) {
|
||
|
|
if (__DEV__) console.error("Token refresh error:", e)
|
||
|
|
isRefreshing.current = false
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [refreshToken, isRefreshTokenExpired, setAccessToken, setAccessTokenExpiry])
|
||
|
|
|
||
|
|
// Auto-refresh token on app state change (when app comes to foreground)
|
||
|
|
useEffect(() => {
|
||
|
|
const handleAppStateChange = async (nextAppState: AppStateStatus) => {
|
||
|
|
if (nextAppState === "active" && accessToken) {
|
||
|
|
// Check if refresh token is expired first
|
||
|
|
if (isRefreshTokenExpired()) {
|
||
|
|
if (__DEV__) console.log("Refresh token expired on app active, logging out...")
|
||
|
|
// Clear auth state
|
||
|
|
setAccessToken(undefined)
|
||
|
|
setRefreshToken(undefined)
|
||
|
|
setAccessTokenExpiry(undefined)
|
||
|
|
setRefreshTokenExpiry(undefined)
|
||
|
|
setUserJson(undefined)
|
||
|
|
return
|
||
|
|
}
|
||
|
|
|
||
|
|
// Check if we should refresh
|
||
|
|
if (shouldRefreshToken()) {
|
||
|
|
await performTokenRefresh()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const subscription = AppState.addEventListener("change", handleAppStateChange)
|
||
|
|
|
||
|
|
// Also check on mount
|
||
|
|
if (accessToken) {
|
||
|
|
if (isRefreshTokenExpired()) {
|
||
|
|
if (__DEV__) console.log("Refresh token expired on mount, logging out...")
|
||
|
|
setAccessToken(undefined)
|
||
|
|
setRefreshToken(undefined)
|
||
|
|
setAccessTokenExpiry(undefined)
|
||
|
|
setRefreshTokenExpiry(undefined)
|
||
|
|
setUserJson(undefined)
|
||
|
|
} else if (shouldRefreshToken()) {
|
||
|
|
performTokenRefresh()
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return () => subscription.remove()
|
||
|
|
}, [
|
||
|
|
accessToken,
|
||
|
|
shouldRefreshToken,
|
||
|
|
isRefreshTokenExpired,
|
||
|
|
performTokenRefresh,
|
||
|
|
setAccessToken,
|
||
|
|
setRefreshToken,
|
||
|
|
setAccessTokenExpiry,
|
||
|
|
setRefreshTokenExpiry,
|
||
|
|
setUserJson,
|
||
|
|
])
|
||
|
|
|
||
|
|
// Clear error
|
||
|
|
const clearError = useCallback(() => {
|
||
|
|
setError("")
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
// Set error and show toast
|
||
|
|
const showError = useCallback(
|
||
|
|
(errorMessage: string) => {
|
||
|
|
setError(errorMessage)
|
||
|
|
toast.error(errorMessage)
|
||
|
|
},
|
||
|
|
[toast],
|
||
|
|
)
|
||
|
|
|
||
|
|
// Reset login step
|
||
|
|
const resetLoginStep = useCallback(() => {
|
||
|
|
setLoginStep("credentials")
|
||
|
|
setPendingEmail("")
|
||
|
|
setError("")
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
// Helper to save tokens with expiry
|
||
|
|
const saveTokens = useCallback(
|
||
|
|
(tokens: {
|
||
|
|
accessToken: string
|
||
|
|
refreshToken: string
|
||
|
|
accessTokenExpiry: string
|
||
|
|
refreshTokenExpiry: string
|
||
|
|
}) => {
|
||
|
|
setAccessToken(tokens.accessToken)
|
||
|
|
setRefreshToken(tokens.refreshToken)
|
||
|
|
setAccessTokenExpiry(tokens.accessTokenExpiry)
|
||
|
|
setRefreshTokenExpiry(tokens.refreshTokenExpiry)
|
||
|
|
},
|
||
|
|
[setAccessToken, setRefreshToken, setAccessTokenExpiry, setRefreshTokenExpiry],
|
||
|
|
)
|
||
|
|
|
||
|
|
// Get current language code (e.g., "en", "zh")
|
||
|
|
const getCurrentLanguage = useCallback(() => {
|
||
|
|
return i18n.language?.split("-")[0] || "en"
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
// Pre-login: verify password and send verification code
|
||
|
|
const preLogin = useCallback(async (email: string, password: string): Promise<boolean> => {
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.preLogin(email, password, getCurrentLanguage())
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success) {
|
||
|
|
setPendingEmail(email)
|
||
|
|
setLoginStep("verification")
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
// Handle API errors (login page shows error inline, no toast needed)
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
setError(getErrorMessage(result.kind, result.errorCode, result.errorMessage))
|
||
|
|
} else {
|
||
|
|
setError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
setError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
// Resend verification code
|
||
|
|
const resendCode = useCallback(async (): Promise<boolean> => {
|
||
|
|
if (!pendingEmail) {
|
||
|
|
setError(getErrorMessage(undefined, "E013"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.resendCode(pendingEmail, getCurrentLanguage())
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success) {
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
setError(getErrorMessage(result.kind, result.errorCode, result.errorMessage))
|
||
|
|
} else {
|
||
|
|
setError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
setError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [pendingEmail, getCurrentLanguage])
|
||
|
|
|
||
|
|
// Verify login: verify code and get tokens
|
||
|
|
const verifyLogin = useCallback(
|
||
|
|
async (code: string): Promise<boolean> => {
|
||
|
|
if (!pendingEmail) {
|
||
|
|
setError(getErrorMessage(undefined, "E013"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.verifyLogin(pendingEmail, code)
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success && result.data.data?.tokens) {
|
||
|
|
const { tokens, user } = result.data.data
|
||
|
|
// Save tokens with expiry
|
||
|
|
saveTokens(tokens)
|
||
|
|
if (user) {
|
||
|
|
setUserJson(JSON.stringify(user))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reset login state
|
||
|
|
setPendingEmail("")
|
||
|
|
setLoginStep("credentials")
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
setError(getErrorMessage(result.kind))
|
||
|
|
} else {
|
||
|
|
setError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
setError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[pendingEmail, saveTokens, setUserJson],
|
||
|
|
)
|
||
|
|
|
||
|
|
// Google login - always show account picker so user can choose account
|
||
|
|
const googleLogin = useCallback(async (): Promise<boolean> => {
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
// Check Play Services (Android)
|
||
|
|
await GoogleSignin.hasPlayServices()
|
||
|
|
|
||
|
|
// Sign out first to ensure user can pick a different account
|
||
|
|
try {
|
||
|
|
await GoogleSignin.signOut()
|
||
|
|
} catch {
|
||
|
|
// Ignore sign out errors
|
||
|
|
}
|
||
|
|
|
||
|
|
// Show Google Sign-In UI
|
||
|
|
const signInResult = await GoogleSignin.signIn()
|
||
|
|
|
||
|
|
// User cancelled - just return false without error
|
||
|
|
if (signInResult.type === "cancelled") {
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
const idToken = signInResult.type === "success" ? signInResult.data.idToken : null
|
||
|
|
|
||
|
|
if (!idToken) {
|
||
|
|
setError(getErrorMessage(undefined, "E019"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
// Call backend API
|
||
|
|
const result = await authApi.googleLogin(idToken)
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success && result.data.data?.tokens) {
|
||
|
|
const { tokens, user } = result.data.data
|
||
|
|
// Save tokens with expiry
|
||
|
|
saveTokens(tokens)
|
||
|
|
if (user) {
|
||
|
|
setUserJson(JSON.stringify(user))
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
setError(getErrorMessage(result.kind, result.errorCode, result.errorMessage))
|
||
|
|
} else {
|
||
|
|
setError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch (e) {
|
||
|
|
const error = e as { code?: string }
|
||
|
|
// User cancelled the sign in
|
||
|
|
if (error.code === statusCodes.SIGN_IN_CANCELLED) {
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
if (__DEV__) {
|
||
|
|
console.error("Google login error:", e)
|
||
|
|
}
|
||
|
|
setError(getErrorMessage(undefined, "E018"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [saveTokens, setUserJson])
|
||
|
|
|
||
|
|
// Reset register step
|
||
|
|
const resetRegisterStep = useCallback(() => {
|
||
|
|
setRegisterStep("credentials")
|
||
|
|
setPendingEmail("")
|
||
|
|
setError("")
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
// Pre-register: validate data and send verification code
|
||
|
|
const preRegister = useCallback(
|
||
|
|
async (
|
||
|
|
username: string,
|
||
|
|
password: string,
|
||
|
|
email: string,
|
||
|
|
nickname?: string,
|
||
|
|
referralCode?: string,
|
||
|
|
): Promise<boolean> => {
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.preRegister(
|
||
|
|
username,
|
||
|
|
password,
|
||
|
|
email,
|
||
|
|
nickname,
|
||
|
|
referralCode,
|
||
|
|
getCurrentLanguage(),
|
||
|
|
)
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success) {
|
||
|
|
setPendingEmail(email)
|
||
|
|
setRegisterStep("verification")
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
setError(getErrorMessage(result.kind))
|
||
|
|
} else {
|
||
|
|
setError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
setError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[getCurrentLanguage],
|
||
|
|
)
|
||
|
|
|
||
|
|
// Resend register verification code
|
||
|
|
const resendRegisterCode = useCallback(async (): Promise<boolean> => {
|
||
|
|
if (!pendingEmail) {
|
||
|
|
setError(getErrorMessage(undefined, "E004"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.resendRegisterCode(pendingEmail, getCurrentLanguage())
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success) {
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
setError(getErrorMessage(result.kind, result.errorCode, result.errorMessage))
|
||
|
|
} else {
|
||
|
|
setError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
setError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [pendingEmail, getCurrentLanguage])
|
||
|
|
|
||
|
|
// Verify register: verify code and create user
|
||
|
|
const verifyRegister = useCallback(
|
||
|
|
async (code: string): Promise<boolean> => {
|
||
|
|
if (!pendingEmail) {
|
||
|
|
setError(getErrorMessage(undefined, "E004"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.verifyRegister(pendingEmail, code)
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success && result.data.data?.tokens) {
|
||
|
|
const { tokens, user } = result.data.data
|
||
|
|
// Save tokens with expiry
|
||
|
|
saveTokens(tokens)
|
||
|
|
if (user) {
|
||
|
|
setUserJson(JSON.stringify(user))
|
||
|
|
}
|
||
|
|
|
||
|
|
// Reset register state
|
||
|
|
setPendingEmail("")
|
||
|
|
setRegisterStep("credentials")
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
setError(getErrorMessage(result.kind))
|
||
|
|
} else {
|
||
|
|
setError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
setError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[pendingEmail, saveTokens, setUserJson],
|
||
|
|
)
|
||
|
|
|
||
|
|
// Reset forgot password step
|
||
|
|
const resetForgotPasswordStep = useCallback(() => {
|
||
|
|
setForgotPasswordStep("email")
|
||
|
|
setPendingEmail("")
|
||
|
|
setError("")
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
// Forgot password: send verification code
|
||
|
|
const forgotPassword = useCallback(
|
||
|
|
async (email: string): Promise<boolean> => {
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.forgotPassword(email, getCurrentLanguage())
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success) {
|
||
|
|
setPendingEmail(email)
|
||
|
|
setForgotPasswordStep("reset")
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
setError(getErrorMessage(result.kind, result.errorCode, result.errorMessage))
|
||
|
|
} else {
|
||
|
|
setError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
setError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[getCurrentLanguage],
|
||
|
|
)
|
||
|
|
|
||
|
|
// Resend forgot password verification code
|
||
|
|
const resendForgotPasswordCode = useCallback(async (): Promise<boolean> => {
|
||
|
|
if (!pendingEmail) {
|
||
|
|
setError(getErrorMessage(undefined, "E025"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.resendForgotPasswordCode(pendingEmail, getCurrentLanguage())
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success) {
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
setError(getErrorMessage(result.kind, result.errorCode, result.errorMessage))
|
||
|
|
} else {
|
||
|
|
setError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
setError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [pendingEmail, getCurrentLanguage])
|
||
|
|
|
||
|
|
// Reset password: verify code and set new password
|
||
|
|
const resetPassword = useCallback(
|
||
|
|
async (code: string, newPassword: string): Promise<boolean> => {
|
||
|
|
if (!pendingEmail) {
|
||
|
|
setError(getErrorMessage(undefined, "E025"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.resetPassword(pendingEmail, code, newPassword)
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success) {
|
||
|
|
// Reset state
|
||
|
|
setPendingEmail("")
|
||
|
|
setForgotPasswordStep("email")
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
setError(getErrorMessage(result.kind))
|
||
|
|
} else {
|
||
|
|
setError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
setError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[pendingEmail],
|
||
|
|
)
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// Profile Methods
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
// Fetch user profile
|
||
|
|
const fetchProfile = useCallback(async (): Promise<boolean> => {
|
||
|
|
if (!accessToken) {
|
||
|
|
showError(getErrorMessage(undefined, "E013"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.getProfile(accessToken)
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success && result.data.data) {
|
||
|
|
// Update user data
|
||
|
|
const profileData = result.data.data
|
||
|
|
const updatedUser: User = {
|
||
|
|
_id: user?._id || "",
|
||
|
|
userId: profileData.userID,
|
||
|
|
username: profileData.username,
|
||
|
|
email: profileData.email,
|
||
|
|
emailVerified: user?.emailVerified ?? true,
|
||
|
|
loginMethods: user?.loginMethods || [],
|
||
|
|
roles: profileData.roles,
|
||
|
|
profile: profileData.profile,
|
||
|
|
status: profileData.status,
|
||
|
|
referralCode: profileData.referralCode,
|
||
|
|
referredByCode: profileData.referredByCode,
|
||
|
|
lastLogin: profileData.lastLogin,
|
||
|
|
createdAt: profileData.createdAt,
|
||
|
|
updatedAt: user?.updatedAt || profileData.createdAt,
|
||
|
|
}
|
||
|
|
setUserJson(JSON.stringify(updatedUser))
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
showError(getErrorMessage(result.kind, result.errorCode, result.errorMessage))
|
||
|
|
} else {
|
||
|
|
showError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
showError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
}, [accessToken, user, setUserJson])
|
||
|
|
|
||
|
|
// Update user profile
|
||
|
|
const updateProfile = useCallback(
|
||
|
|
async (data: UpdateProfileRequest): Promise<boolean> => {
|
||
|
|
if (!accessToken) {
|
||
|
|
showError(getErrorMessage(undefined, "E013"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.updateProfile(accessToken, data)
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success && result.data.data?.user) {
|
||
|
|
// Update user data
|
||
|
|
setUserJson(JSON.stringify(result.data.data.user))
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
showError(getErrorMessage(result.kind))
|
||
|
|
} else {
|
||
|
|
showError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
showError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[accessToken, setUserJson],
|
||
|
|
)
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// Password Methods
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
// Change password
|
||
|
|
const changePassword = useCallback(
|
||
|
|
async (
|
||
|
|
oldPassword: string,
|
||
|
|
newPassword: string,
|
||
|
|
logoutOtherDevices?: boolean,
|
||
|
|
): Promise<boolean> => {
|
||
|
|
if (!accessToken) {
|
||
|
|
showError(getErrorMessage(undefined, "E013"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.changePassword(accessToken, {
|
||
|
|
oldPassword,
|
||
|
|
newPassword,
|
||
|
|
logoutOtherDevices,
|
||
|
|
})
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success) {
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
showError(getErrorMessage(result.kind))
|
||
|
|
} else {
|
||
|
|
showError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
showError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[accessToken],
|
||
|
|
)
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// Email Change Methods
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
// Reset email change step
|
||
|
|
const resetEmailChangeStep = useCallback(() => {
|
||
|
|
setEmailChangeStep("idle")
|
||
|
|
setPendingEmail("")
|
||
|
|
setError("")
|
||
|
|
}, [])
|
||
|
|
|
||
|
|
// Send email verification code
|
||
|
|
const sendEmailCode = useCallback(
|
||
|
|
async (email: string): Promise<boolean> => {
|
||
|
|
if (!accessToken) {
|
||
|
|
showError(getErrorMessage(undefined, "E013"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.sendEmailCode(accessToken, email, getCurrentLanguage())
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success) {
|
||
|
|
// Update step to verify-current if currently idle (first time sending to current email)
|
||
|
|
if (emailChangeStep === "idle") {
|
||
|
|
setEmailChangeStep("verify-current")
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
showError(getErrorMessage(result.kind))
|
||
|
|
} else {
|
||
|
|
showError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
showError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[accessToken, getCurrentLanguage, emailChangeStep],
|
||
|
|
)
|
||
|
|
|
||
|
|
// Verify current email (Step 1)
|
||
|
|
const verifyCurrentEmail = useCallback(
|
||
|
|
async (code: string): Promise<boolean> => {
|
||
|
|
if (!accessToken) {
|
||
|
|
showError(getErrorMessage(undefined, "E013"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.verifyCurrentEmail(accessToken, code)
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success) {
|
||
|
|
// Move to next step
|
||
|
|
setEmailChangeStep("bind-new")
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
showError(getErrorMessage(result.kind))
|
||
|
|
} else {
|
||
|
|
showError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
showError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[accessToken],
|
||
|
|
)
|
||
|
|
|
||
|
|
// Bind new email (Step 2)
|
||
|
|
const bindNewEmail = useCallback(
|
||
|
|
async (newEmail: string, code: string): Promise<boolean> => {
|
||
|
|
if (!accessToken) {
|
||
|
|
showError(getErrorMessage(undefined, "E013"))
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
|
||
|
|
setIsLoading(true)
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
try {
|
||
|
|
const result = await authApi.bindNewEmail(accessToken, newEmail, code)
|
||
|
|
|
||
|
|
if (result.kind === "ok" && result.data.success && result.data.data?.user) {
|
||
|
|
// Update user data
|
||
|
|
setUserJson(JSON.stringify(result.data.data.user))
|
||
|
|
// Reset email change state
|
||
|
|
setEmailChangeStep("idle")
|
||
|
|
setPendingEmail("")
|
||
|
|
setIsLoading(false)
|
||
|
|
return true
|
||
|
|
}
|
||
|
|
|
||
|
|
if (result.kind !== "ok") {
|
||
|
|
showError(getErrorMessage(result.kind))
|
||
|
|
} else {
|
||
|
|
showError(
|
||
|
|
getErrorMessage(
|
||
|
|
undefined,
|
||
|
|
extractErrorCode(result.data),
|
||
|
|
extractErrorMessage(result.data),
|
||
|
|
),
|
||
|
|
)
|
||
|
|
}
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
} catch {
|
||
|
|
showError(getErrorMessage("cannot-connect"))
|
||
|
|
setIsLoading(false)
|
||
|
|
return false
|
||
|
|
}
|
||
|
|
},
|
||
|
|
[accessToken, setUserJson],
|
||
|
|
)
|
||
|
|
|
||
|
|
// ============================================
|
||
|
|
// Logout
|
||
|
|
// ============================================
|
||
|
|
|
||
|
|
const logout = useCallback(async () => {
|
||
|
|
// Call logout API if we have a token
|
||
|
|
if (accessToken) {
|
||
|
|
try {
|
||
|
|
await authApi.logoutApi(accessToken)
|
||
|
|
} catch {
|
||
|
|
// Ignore logout API errors
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
setAccessToken(undefined)
|
||
|
|
setRefreshToken(undefined)
|
||
|
|
setAccessTokenExpiry(undefined)
|
||
|
|
setRefreshTokenExpiry(undefined)
|
||
|
|
setUserJson(undefined)
|
||
|
|
setAuthEmail("")
|
||
|
|
setPendingEmail("")
|
||
|
|
setLoginStep("credentials")
|
||
|
|
setRegisterStep("credentials")
|
||
|
|
setForgotPasswordStep("email")
|
||
|
|
setEmailChangeStep("idle")
|
||
|
|
setError("")
|
||
|
|
|
||
|
|
// Sign out from Google if signed in
|
||
|
|
try {
|
||
|
|
await GoogleSignin.signOut()
|
||
|
|
} catch {
|
||
|
|
// Ignore Google sign out errors
|
||
|
|
}
|
||
|
|
}, [
|
||
|
|
accessToken,
|
||
|
|
setAccessToken,
|
||
|
|
setRefreshToken,
|
||
|
|
setAccessTokenExpiry,
|
||
|
|
setRefreshTokenExpiry,
|
||
|
|
setUserJson,
|
||
|
|
setAuthEmail,
|
||
|
|
])
|
||
|
|
|
||
|
|
// Legacy setAuthToken for backward compatibility
|
||
|
|
const setAuthToken = useCallback(
|
||
|
|
(token?: string) => {
|
||
|
|
setAccessToken(token)
|
||
|
|
},
|
||
|
|
[setAccessToken],
|
||
|
|
)
|
||
|
|
|
||
|
|
const value: AuthContextType = {
|
||
|
|
// State
|
||
|
|
isAuthenticated: !!accessToken,
|
||
|
|
user,
|
||
|
|
accessToken,
|
||
|
|
refreshToken,
|
||
|
|
accessTokenExpiry,
|
||
|
|
refreshTokenExpiry,
|
||
|
|
pendingEmail,
|
||
|
|
loginStep,
|
||
|
|
registerStep,
|
||
|
|
forgotPasswordStep,
|
||
|
|
emailChangeStep,
|
||
|
|
isLoading,
|
||
|
|
error,
|
||
|
|
|
||
|
|
// Legacy
|
||
|
|
authToken: accessToken,
|
||
|
|
authEmail,
|
||
|
|
setAuthToken,
|
||
|
|
setAuthEmail,
|
||
|
|
validationError,
|
||
|
|
|
||
|
|
// Login methods
|
||
|
|
preLogin,
|
||
|
|
resendCode,
|
||
|
|
verifyLogin,
|
||
|
|
googleLogin,
|
||
|
|
resetLoginStep,
|
||
|
|
|
||
|
|
// Register methods
|
||
|
|
preRegister,
|
||
|
|
resendRegisterCode,
|
||
|
|
verifyRegister,
|
||
|
|
resetRegisterStep,
|
||
|
|
|
||
|
|
// Forgot password methods
|
||
|
|
forgotPassword,
|
||
|
|
resendForgotPasswordCode,
|
||
|
|
resetPassword,
|
||
|
|
resetForgotPasswordStep,
|
||
|
|
|
||
|
|
// Profile methods
|
||
|
|
fetchProfile,
|
||
|
|
updateProfile,
|
||
|
|
|
||
|
|
// Password methods
|
||
|
|
changePassword,
|
||
|
|
|
||
|
|
// Email change methods
|
||
|
|
sendEmailCode,
|
||
|
|
verifyCurrentEmail,
|
||
|
|
bindNewEmail,
|
||
|
|
resetEmailChangeStep,
|
||
|
|
|
||
|
|
// Common methods
|
||
|
|
clearError,
|
||
|
|
logout,
|
||
|
|
}
|
||
|
|
|
||
|
|
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>
|
||
|
|
}
|
||
|
|
|
||
|
|
export const useAuth = () => {
|
||
|
|
const context = useContext(AuthContext)
|
||
|
|
if (!context) throw new Error("useAuth must be used within an AuthProvider")
|
||
|
|
return context
|
||
|
|
}
|
||
|
|
|
||
|
|
// Helper function to get translated error message
|
||
|
|
function getErrorMessage(problemKind?: string, errorCode?: string, message?: string): string {
|
||
|
|
return translateAuthError(errorCode, message, problemKind)
|
||
|
|
}
|
||
|
|
|
||
|
|
// Helper to extract error code from API response (handles both code and error.code)
|
||
|
|
function extractErrorCode(data: { code?: string; error?: { code?: string } }): string | undefined {
|
||
|
|
return data.code || data.error?.code
|
||
|
|
}
|
||
|
|
|
||
|
|
// Helper to extract error message from API response (handles both message and error.message)
|
||
|
|
function extractErrorMessage(data: {
|
||
|
|
message?: string
|
||
|
|
error?: { message?: string }
|
||
|
|
}): string | undefined {
|
||
|
|
return data.message || data.error?.message
|
||
|
|
}
|