fix: clean stale auth state on login/setup, unify language switcher

- LoginPage/SetupPage: clear localStorage auth tokens on mount
- AuthContext: clear onboarding state on register, invalidate config on logout
- Extract shared LanguageSwitcher component for consistent UI
- Merge duplicate config import in AuthContext
This commit is contained in:
shinchan-zhai
2026-03-31 15:23:00 +08:00
parent 1d6e99c74a
commit 608f02ed67
4 changed files with 65 additions and 30 deletions
+11
View File
@@ -5,6 +5,7 @@ import { useAuth } from '../../contexts/AuthContext'
import { useLanguage } from '../../contexts/LanguageContext'
import { t } from '../../i18n/translations'
import { DeepVoidBackground } from '../common/DeepVoidBackground'
import { LanguageSwitcher } from '../common/LanguageSwitcher'
import { OnboardingModeSelector } from './OnboardingModeSelector'
import type { UserMode } from '../../lib/onboarding'
@@ -19,6 +20,14 @@ export function LoginPage() {
const [expiredToastId, setExpiredToastId] = useState<string | number | null>(null)
const [mode, setMode] = useState<UserMode>('beginner')
// Clean up stale auth state once on mount
useEffect(() => {
localStorage.removeItem('auth_token')
localStorage.removeItem('auth_user')
localStorage.removeItem('user_id')
}, [])
// Show session-expired toast (re-runs on language change to update text)
useEffect(() => {
if (sessionStorage.getItem('from401') === 'true') {
const id = toast.warning(t('sessionExpired', language), { duration: Infinity })
@@ -44,6 +53,8 @@ export function LoginPage() {
return (
<DeepVoidBackground disableAnimation>
<LanguageSwitcher />
<div className="flex-1 flex items-center justify-center px-4 py-16">
<div className="w-full max-w-sm">
@@ -0,0 +1,34 @@
import React from 'react'
import { Globe } from 'lucide-react'
import { useLanguage } from '../../contexts/LanguageContext'
import type { Language } from '../../i18n/translations'
const languages: { code: Language; label: string }[] = [
{ code: 'zh', label: '中文' },
{ code: 'en', label: 'EN' },
{ code: 'id', label: 'ID' },
]
export function LanguageSwitcher() {
const { language, setLanguage } = useLanguage()
return (
<div className="absolute top-4 right-4 z-50 flex items-center gap-1 rounded-lg p-1 border border-white/10 bg-white/5 backdrop-blur-sm">
<Globe size={14} className="text-zinc-500 ml-1.5 mr-0.5" />
{languages.map(({ code, label }) => (
<button
key={code}
type="button"
onClick={() => setLanguage(code)}
className={`px-2.5 py-1 rounded text-xs font-semibold transition-all ${
language === code
? 'bg-nofx-gold/15 text-nofx-gold'
: 'text-zinc-500 hover:text-zinc-300 bg-transparent'
}`}
>
{label}
</button>
))}
</div>
)
}
+14 -29
View File
@@ -1,10 +1,11 @@
import React, { useState } from 'react'
import { Eye, EyeOff, Globe } from 'lucide-react'
import React, { useState, useEffect } from 'react'
import { Eye, EyeOff } from 'lucide-react'
import { useAuth } from '../../contexts/AuthContext'
import { invalidateSystemConfig } from '../../lib/config'
import { OnboardingModeSelector } from '../auth/OnboardingModeSelector'
import type { UserMode } from '../../lib/onboarding'
import { useLanguage } from '../../contexts/LanguageContext'
import { LanguageSwitcher } from '../common/LanguageSwitcher'
import type { Language } from '../../i18n/translations'
const labels = {
@@ -49,14 +50,8 @@ const labels = {
},
} as const
const langOptions: { value: Language; label: string }[] = [
{ value: 'en', label: 'English' },
{ value: 'zh', label: '中文' },
{ value: 'id', label: 'Bahasa' },
]
export function SetupPage() {
const { language, setLanguage } = useLanguage()
const { language } = useLanguage()
const { register } = useAuth()
const [email, setEmail] = useState('')
const [password, setPassword] = useState('')
@@ -65,6 +60,15 @@ export function SetupPage() {
const [loading, setLoading] = useState(false)
const [mode, setMode] = useState<UserMode>('beginner')
// Clean up any stale auth/onboarding state on setup page load
useEffect(() => {
localStorage.removeItem('auth_token')
localStorage.removeItem('auth_user')
localStorage.removeItem('user_id')
localStorage.removeItem('nofx_beginner_onboarding_completed')
localStorage.removeItem('nofx_beginner_wallet_address')
}, [])
const l = labels[language as keyof typeof labels] || labels.en
const handleSubmit = async (e: React.FormEvent) => {
@@ -124,26 +128,7 @@ export function SetupPage() {
{/* Blur overlay */}
<div className="absolute inset-0 backdrop-blur-md bg-black/60" />
{/* Language switcher */}
<div className="fixed top-4 right-4 z-50">
<div className="flex items-center gap-1.5 rounded-xl border border-white/10 bg-white/5 backdrop-blur-sm px-2 py-1.5">
<Globe className="h-3.5 w-3.5 text-zinc-500" />
{langOptions.map((opt) => (
<button
key={opt.value}
type="button"
onClick={() => setLanguage(opt.value)}
className={`rounded-lg px-2 py-1 text-xs font-medium transition ${
language === opt.value
? 'bg-nofx-gold/15 text-nofx-gold'
: 'text-zinc-500 hover:text-zinc-300'
}`}
>
{opt.label}
</button>
))}
</div>
</div>
<LanguageSwitcher />
{/* Modal card */}
<div className="relative z-10 flex min-h-screen items-center justify-center px-4 py-16">
+6 -1
View File
@@ -1,6 +1,6 @@
import React, { createContext, useContext, useState, useEffect } from 'react'
import { flushSync } from 'react-dom'
import { getSystemConfig } from '../lib/config'
import { getSystemConfig, invalidateSystemConfig } from '../lib/config'
import { reset401Flag, httpClient } from '../lib/httpClient'
import { getPostAuthPath, setUserMode, type UserMode } from '../lib/onboarding'
import { useLanguage } from './LanguageContext'
@@ -225,6 +225,10 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}>('/api/register', requestBody)
if (result.success && result.data) {
// Clear stale onboarding state so new users always see the welcome flow
localStorage.removeItem('nofx_beginner_onboarding_completed')
localStorage.removeItem('nofx_beginner_wallet_address')
const userInfo = { id: result.data.user_id, email: result.data.email }
handlePostAuthSuccess(result.data.token, userInfo, mode)
@@ -290,6 +294,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
setToken(null)
localStorage.removeItem('auth_token')
localStorage.removeItem('auth_user')
invalidateSystemConfig()
window.history.pushState({}, '', '/')
window.dispatchEvent(new PopStateEvent('popstate'))
}