mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
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:
@@ -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>
|
||||
)
|
||||
}
|
||||
@@ -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">
|
||||
|
||||
@@ -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'))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user