refactor(auth): 重构认证流程和会话管理

This commit is contained in:
JiaJun
2026-04-23 16:10:27 +08:00
parent d1ee6413a5
commit f6f50ee6c7
12 changed files with 117 additions and 86 deletions

View File

@@ -9,12 +9,14 @@ import {ArrowLeft, BadgeCheck, MapPinHouse, PencilLine, Plus, Trash2} from 'luci
import {useAddressBook} from '@/features/addressBook'
import {GoodsRedeemModal} from '@/features/goods'
import {notifySuccess} from '@/features/notifications'
import {ensureSessionForAction} from '@/lib/authGuard'
import {MotionButton, MotionDiv, tapMotionProps, tapMotionPropsIcon, tapMotionPropsSoft} from '@/lib/motion'
import type {AddressListItem} from '@/types/address.type.ts'
function AccountPage() {
const {t} = useTranslation()
const addressBook = useAddressBook({autoLoad: true})
const sessionId = addressBook.sessionId
const [addressModalOpen, setAddressModalOpen] = useState(false)
const [editingAddress, setEditingAddress] = useState<AddressListItem | null>(null)
const [deleteTarget, setDeleteTarget] = useState<AddressListItem | null>(null)
@@ -22,12 +24,20 @@ function AccountPage() {
const isAddressFormValid = addressBook.isAddressFormValid
const handleOpenAddAddress = () => {
if (!ensureSessionForAction()) {
return
}
setEditingAddress(null)
addressBook.resetAddressForm()
setAddressModalOpen(true)
}
const handleOpenEditAddress = (address: AddressListItem) => {
if (!ensureSessionForAction()) {
return
}
setEditingAddress(address)
addressBook.fillAddressForm(address)
setAddressModalOpen(true)
@@ -102,7 +112,11 @@ function AccountPage() {
<div className="min-h-0 flex-1 overflow-y-auto pb-[24px]">
<div className="mx-auto w-full max-w-[1000px]">
{addressBook.loading ? (
{!sessionId ? (
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
{t('validation.verificationRequired')}
</div>
) : addressBook.loading ? (
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
{t('account.loadingAddressList')}
</div>
@@ -156,7 +170,13 @@ function AccountPage() {
<MotionButton
type="button"
className="inline-flex items-center gap-[6px] rounded-full bg-[#4B1818]/70 px-[10px] py-[6px] text-[12px] text-[#FFB1B1] transition-colors hover:bg-[#612121]/80 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
onClick={() => setDeleteTarget(address)}
onClick={() => {
if (!ensureSessionForAction()) {
return
}
setDeleteTarget(address)
}}
{...tapMotionPropsSoft}
>
<Trash2 className="h-[12px] w-[12px]" aria-hidden="true" />

View File

@@ -1,7 +1,6 @@
import {useState} from 'react'
import {useMutation} from '@tanstack/react-query'
import {Check, Languages} from 'lucide-react'
import {useTranslation} from 'react-i18next'
import PageLayout from '@/components/layout'
@@ -31,15 +30,9 @@ import {
import {validateClaimSubmission} from '@/features/home/claimValidation'
import {claim} from '@/api/business.ts'
import {notifyError, notifySuccess} from '@/features/notifications'
import {MotionButton, MotionLink, tapMotionProps} from '@/lib/motion'
import {ensureSessionForAction} from '@/lib/authGuard'
import {MotionLink, tapMotionProps} from '@/lib/motion'
import {useUserStore} from "@/store/user.ts";
import {type AppLanguage, normalizeLanguage} from '@/lib/i18n'
const LANGUAGE_OPTIONS: Array<{value: AppLanguage; labelKey: 'switchToChinese' | 'switchToEnglish' | 'switchToMalay'}> = [
{value: 'zh', labelKey: 'switchToChinese'},
{value: 'en', labelKey: 'switchToEnglish'},
{value: 'ms', labelKey: 'switchToMalay'},
]
function QuickNavCard({icon: Icon, label, to}: QuickNavCardProps) {
return (
@@ -71,11 +64,8 @@ function getProgressPercent(current = 0, total = 0) {
function HomePage() {
const {t} = useTranslation()
const [claimModalOpen, setClaimModalOpen] = useState(false)
const [languageModalOpen, setLanguageModalOpen] = useState(false)
const navigate = useNavigate()
const authInfo = useUserStore(state => state.authInfo)
const language = useUserStore((state) => state.language)
const setLanguage = useUserStore((state) => state.setLanguage)
const {productCategories, loading} = useGoodsCatalog()
const {invalidateAssets} = useAssetsRefresh()
const redeem = useGoodsRedeem()
@@ -94,13 +84,16 @@ function HomePage() {
const {assetsInfo} = useAssetsQuery()
const claimProgress = getProgressPercent(assetsInfo?.today_claimed, assetsInfo?.today_limit)
const isClaimAvailable = (assetsInfo?.locked_points ?? 0) > 0
const currentLanguage = normalizeLanguage(language)
const previewCategories: ProductCategory[] = productCategories.map((category) => ({
...category,
items: category.items.slice(0, 4),
}))
const handleOpenClaimModal = () => {
if (!ensureSessionForAction()) {
return
}
if (!isClaimAvailable) {
notifyError(t('home.noClaimablePointsAvailable'))
return
@@ -115,6 +108,10 @@ function HomePage() {
}
const handleSyncBalance = async () => {
if (!ensureSessionForAction()) {
return
}
try {
await syncBalanceMutation.mutateAsync()
notifySuccess(t('home.balanceSyncedSuccessfully'))
@@ -124,6 +121,10 @@ function HomePage() {
}
const handleConfirmClaim = async () => {
if (!ensureSessionForAction()) {
return
}
const claimValidation = validateClaimSubmission(authInfo, assetsInfo)
if (!claimValidation.valid) {
notifyError(claimValidation.message)
@@ -144,33 +145,12 @@ function HomePage() {
navigate(`/goods?type=${type}`)
}
const handleSelectLanguage = (nextLanguage: AppLanguage) => {
setLanguage(nextLanguage)
setLanguageModalOpen(false)
}
return (
<PageLayout>
<div
className="grid grid-cols-3 gap-2 py-[14px] sm:ml-auto sm:flex sm:w-auto sm:grid-cols-none sm:justify-end">
className="grid grid-cols-2 gap-2 py-[14px] sm:ml-auto sm:flex sm:w-auto sm:grid-cols-none sm:justify-end">
<QuickNavCard to="/record" icon={History} label={t('nav.record')}/>
<QuickNavCard to="/account" icon={UserRound} label={t('nav.account')}/>
<MotionButton
type="button"
className="liquid-glass-bg flex items-center justify-between gap-[12px] rounded-[12px] px-[8px] py-[8px] text-[13px] text-white/88 transition-colors hover:bg-white/28 focus-visible:ring-2 focus-visible:ring-[#FE9F00] focus-visible:ring-offset-2 focus-visible:ring-offset-[#08070E]"
onClick={() => setLanguageModalOpen(true)}
{...tapMotionProps}
>
<div className="flex min-w-0 items-center gap-[10px]">
<div
className="flex h-[32px] w-[32px] items-center justify-center rounded-[10px] bg-linear-to-b from-[#FB8001] to-[#FCAA2C] text-white shadow-[0_8px_18px_rgba(250,109,2,0.24)]">
<Languages className="h-[16px] w-[16px] shrink-0" aria-hidden="true"/>
</div>
<div
className="truncate font-medium">{t(`nav.${LANGUAGE_OPTIONS.find((option) => option.value === currentLanguage)?.labelKey ?? 'switchToChinese'}`)}</div>
</div>
<ChevronRight className="h-[16px] w-[16px] shrink-0 text-white/70" aria-hidden="true"/>
</MotionButton>
</div>
<div className="mt-[4px]">
@@ -311,27 +291,6 @@ function HomePage() {
onChangeAddressForm={redeem.changeAddressForm}
/>
<Modal
open={languageModalOpen}
title={t('common.language')}
onClose={() => setLanguageModalOpen(false)}
className="max-w-[420px]"
bodyClassName="space-y-3"
>
{LANGUAGE_OPTIONS.map((option) => (
<Button
key={option.value}
type="button"
variant={option.value === currentLanguage ? 'orange' : 'gray'}
className="flex h-[46px] w-full items-center justify-between px-[14px] text-[14px]"
onClick={() => handleSelectLanguage(option.value)}
>
<span>{t(`nav.${option.labelKey}`)}</span>
{option.value === currentLanguage ? <Check className="h-[16px] w-[16px] shrink-0"/> : null}
</Button>
))}
</Modal>
<Modal
open={claimModalOpen}
title={t('home.confirmClaim')}

View File

@@ -371,6 +371,14 @@ function OrdersTabContent({
})
const orderRecords = ordersQuery.data ?? []
if (!sessionId) {
return (
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
{t('validation.verificationRequired')}
</div>
)
}
if (ordersQuery.isPending) {
return (
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
@@ -412,6 +420,14 @@ function PointsTabContent({sessionId}: {sessionId: string}) {
})
const pointsRecords = pointsLogsQuery.data ?? []
if (!sessionId) {
return (
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
{t('validation.verificationRequired')}
</div>
)
}
if (pointsLogsQuery.isPending) {
return (
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">