refactor(auth): 重构认证流程和会话管理
This commit is contained in:
@@ -4,6 +4,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
|
|||||||
import {addressAdd, addressDelete, addressEdit, addressList} from '@/api/address.ts'
|
import {addressAdd, addressDelete, addressEdit, addressList} from '@/api/address.ts'
|
||||||
import {validateAddressFormSubmission} from '@/features/addressBook/addressValidation'
|
import {validateAddressFormSubmission} from '@/features/addressBook/addressValidation'
|
||||||
import {notifyError} from '@/features/notifications'
|
import {notifyError} from '@/features/notifications'
|
||||||
|
import {ensureSessionForAction} from '@/lib/authGuard'
|
||||||
import {queryKeys} from '@/lib/queryKeys.ts'
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||||
import {useUserStore} from '@/store/user.ts'
|
import {useUserStore} from '@/store/user.ts'
|
||||||
import type {AddressListItem} from '@/types/address.type.ts'
|
import type {AddressListItem} from '@/types/address.type.ts'
|
||||||
@@ -65,7 +66,6 @@ export function useAddressBook(options?: UseAddressBookOptions) {
|
|||||||
session_id: sessionId!,
|
session_id: sessionId!,
|
||||||
receiver_name: addressForm.name.trim(),
|
receiver_name: addressForm.name.trim(),
|
||||||
phone: addressForm.phone.trim(),
|
phone: addressForm.phone.trim(),
|
||||||
region: editingAddress ? editingAddress.region.map((part) => part.trim()).filter(Boolean).join(', ') : '',
|
|
||||||
detail_address: addressForm.detailedAddress.trim(),
|
detail_address: addressForm.detailedAddress.trim(),
|
||||||
default_setting: addressForm.isDefault ? '1' : '0',
|
default_setting: addressForm.isDefault ? '1' : '0',
|
||||||
} as const
|
} as const
|
||||||
@@ -138,7 +138,7 @@ export function useAddressBook(options?: UseAddressBookOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const saveAddress = async (editingAddress?: AddressListItem | null) => {
|
const saveAddress = async (editingAddress?: AddressListItem | null) => {
|
||||||
if (!sessionId) {
|
if (!ensureSessionForAction() || !sessionId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,7 +163,7 @@ export function useAddressBook(options?: UseAddressBookOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeAddress = async (addressId: string) => {
|
const removeAddress = async (addressId: string) => {
|
||||||
if (!sessionId) {
|
if (!ensureSessionForAction() || !sessionId) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,7 +182,7 @@ export function useAddressBook(options?: UseAddressBookOptions) {
|
|||||||
sessionId,
|
sessionId,
|
||||||
addresses,
|
addresses,
|
||||||
addressOptions,
|
addressOptions,
|
||||||
loading: addressListQuery.isPending || loadAddressesLoading,
|
loading: Boolean(sessionId) && (addressListQuery.isPending || loadAddressesLoading),
|
||||||
addressForm,
|
addressForm,
|
||||||
isAddressFormValid,
|
isAddressFormValid,
|
||||||
submitLoading: saveAddressMutation.isPending,
|
submitLoading: saveAddressMutation.isPending,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {type PropsWithChildren, useEffect, useRef, useState} from 'react'
|
|||||||
|
|
||||||
import {validateToken} from '@/api/auth.ts'
|
import {validateToken} from '@/api/auth.ts'
|
||||||
import {userAssets} from '@/api/user.ts'
|
import {userAssets} from '@/api/user.ts'
|
||||||
import {useTranslation} from 'react-i18next'
|
|
||||||
import {normalizeLanguage} from '@/lib/i18n'
|
import {normalizeLanguage} from '@/lib/i18n'
|
||||||
import {queryKeys} from '@/lib/queryKeys.ts'
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||||
import {useUserStore} from '@/store/user.ts'
|
import {useUserStore} from '@/store/user.ts'
|
||||||
@@ -16,7 +15,6 @@ const HOST_READY_RETRY_LIMIT = 20
|
|||||||
const HOST_HEIGHT_REPORT_INTERVAL = 250
|
const HOST_HEIGHT_REPORT_INTERVAL = 250
|
||||||
|
|
||||||
export function AuthGuide({children}: PropsWithChildren) {
|
export function AuthGuide({children}: PropsWithChildren) {
|
||||||
const {t} = useTranslation()
|
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
const [hostToken, setHostToken] = useState('')
|
const [hostToken, setHostToken] = useState('')
|
||||||
const setUserInfo = useUserStore((state) => state.setUserInfo)
|
const setUserInfo = useUserStore((state) => state.setUserInfo)
|
||||||
@@ -73,6 +71,12 @@ export function AuthGuide({children}: PropsWithChildren) {
|
|||||||
hasHostContextRef.current = true
|
hasHostContextRef.current = true
|
||||||
activeTokenRef.current = normalizedToken
|
activeTokenRef.current = normalizedToken
|
||||||
setHostToken(normalizedToken)
|
setHostToken(normalizedToken)
|
||||||
|
} else {
|
||||||
|
hasHostContextRef.current = true
|
||||||
|
activeTokenRef.current = ''
|
||||||
|
setHostToken('')
|
||||||
|
clearUserInfo()
|
||||||
|
queryClient.removeQueries({queryKey: ['auth-bootstrap']})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof language === 'string' && language.trim()) {
|
if (typeof language === 'string' && language.trim()) {
|
||||||
@@ -236,24 +240,5 @@ export function AuthGuide({children}: PropsWithChildren) {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
if (!hostToken || authBootstrapQuery.isPending) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-screen items-center justify-center bg-[#08070E] px-6 text-center text-[14px] text-white/68">
|
|
||||||
{!hostToken ? t('auth.waitingForHostContext') : t('auth.loadingAccountData')}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (authBootstrapQuery.isError) {
|
|
||||||
return (
|
|
||||||
<div className="flex min-h-screen items-center justify-center bg-[#08070E] px-6 text-center">
|
|
||||||
<div className="max-w-[420px] rounded-[14px] border border-white/10 bg-white/4 px-[18px] py-[16px]">
|
|
||||||
<div className="text-[16px] font-semibold text-white">{t('auth.authenticationFailed')}</div>
|
|
||||||
<div className="mt-[8px] text-[13px] leading-[1.6] text-white/58">{t('auth.refreshAndTryAgain')}</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>{children}</>
|
return <>{children}</>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {useState} from 'react'
|
|||||||
import {bonusRedeem, physicalRedeem, withdrawApply} from '@/api/business.ts'
|
import {bonusRedeem, physicalRedeem, withdrawApply} from '@/api/business.ts'
|
||||||
import {useAddressBook} from '@/features/addressBook'
|
import {useAddressBook} from '@/features/addressBook'
|
||||||
import {notifyError, notifySuccess} from '@/features/notifications'
|
import {notifyError, notifySuccess} from '@/features/notifications'
|
||||||
|
import {ensureSessionForAction} from '@/lib/authGuard'
|
||||||
import i18n from '@/lib/i18n'
|
import i18n from '@/lib/i18n'
|
||||||
import {queryKeys} from '@/lib/queryKeys.ts'
|
import {queryKeys} from '@/lib/queryKeys.ts'
|
||||||
import {validateAddAddressSubmission, validateRedeemSubmission} from '@/features/goods/redeemValidation'
|
import {validateAddAddressSubmission, validateRedeemSubmission} from '@/features/goods/redeemValidation'
|
||||||
@@ -45,6 +46,10 @@ export function useGoodsRedeem() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const openRedeemModal = async (product: ProductItem, categoryId: ProductCategory['id']) => {
|
const openRedeemModal = async (product: ProductItem, categoryId: ProductCategory['id']) => {
|
||||||
|
if (!ensureSessionForAction()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setSelectedProduct({
|
setSelectedProduct({
|
||||||
product,
|
product,
|
||||||
categoryId,
|
categoryId,
|
||||||
@@ -70,6 +75,10 @@ export function useGoodsRedeem() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const openAddAddress = () => {
|
const openAddAddress = () => {
|
||||||
|
if (!ensureSessionForAction()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setModalMode('add-address')
|
setModalMode('add-address')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +89,10 @@ export function useGoodsRedeem() {
|
|||||||
const isAddAddressFormValid = addressBook.isAddressFormValid
|
const isAddAddressFormValid = addressBook.isAddressFormValid
|
||||||
|
|
||||||
const confirmRedeem = async () => {
|
const confirmRedeem = async () => {
|
||||||
|
if (!ensureSessionForAction()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (modalMode === 'add-address') {
|
if (modalMode === 'add-address') {
|
||||||
const addAddressValidation = validateAddAddressSubmission({
|
const addAddressValidation = validateAddAddressSubmission({
|
||||||
isAddAddressFormValid,
|
isAddAddressFormValid,
|
||||||
|
|||||||
34
src/lib/authGuard.ts
Normal file
34
src/lib/authGuard.ts
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
import {notifyError} from '@/features/notifications'
|
||||||
|
import i18n from '@/lib/i18n'
|
||||||
|
import {useUserStore} from '@/store/user.ts'
|
||||||
|
|
||||||
|
export function hasHostToken() {
|
||||||
|
return Boolean(useUserStore.getState().userInfo?.token?.trim())
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasSessionAuth() {
|
||||||
|
return Boolean(useUserStore.getState().authInfo?.session_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureTokenForAction() {
|
||||||
|
if (hasHostToken()) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyError(i18n.t('validation.verificationRequired'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
export function ensureSessionForAction() {
|
||||||
|
if (!hasHostToken()) {
|
||||||
|
notifyError(i18n.t('validation.verificationRequired'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasSessionAuth()) {
|
||||||
|
notifyError(i18n.t('validation.verificationInProgress'))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
@@ -137,6 +137,8 @@ const en = {
|
|||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
sessionExpired: 'Session expired. Please log in again.',
|
sessionExpired: 'Session expired. Please log in again.',
|
||||||
|
verificationRequired: 'Please log in first.',
|
||||||
|
verificationInProgress: 'Verification in progress. Please try again shortly.',
|
||||||
noProductSelected: 'No product selected.',
|
noProductSelected: 'No product selected.',
|
||||||
pleaseSelectShippingAddress: 'Please select a shipping address.',
|
pleaseSelectShippingAddress: 'Please select a shipping address.',
|
||||||
pleaseCompleteAddressFields: 'Please complete all required address fields.',
|
pleaseCompleteAddressFields: 'Please complete all required address fields.',
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ const ms = {
|
|||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
sessionExpired: 'Sesi telah tamat. Sila log masuk semula.',
|
sessionExpired: 'Sesi telah tamat. Sila log masuk semula.',
|
||||||
|
verificationRequired: 'Sila log masuk dahulu.',
|
||||||
|
verificationInProgress: 'Pengesahan sedang diproses. Sila cuba sebentar lagi.',
|
||||||
noProductSelected: 'Tiada barangan dipilih.',
|
noProductSelected: 'Tiada barangan dipilih.',
|
||||||
pleaseSelectShippingAddress: 'Sila pilih alamat penghantaran.',
|
pleaseSelectShippingAddress: 'Sila pilih alamat penghantaran.',
|
||||||
pleaseCompleteAddressFields: 'Sila lengkapkan semua maklumat alamat.',
|
pleaseCompleteAddressFields: 'Sila lengkapkan semua maklumat alamat.',
|
||||||
|
|||||||
@@ -137,6 +137,8 @@ const zh = {
|
|||||||
},
|
},
|
||||||
validation: {
|
validation: {
|
||||||
sessionExpired: '登录态已过期,请重新登录。',
|
sessionExpired: '登录态已过期,请重新登录。',
|
||||||
|
verificationRequired: '请先登录。',
|
||||||
|
verificationInProgress: '验证中,请稍后重试。',
|
||||||
noProductSelected: '未选择商品。',
|
noProductSelected: '未选择商品。',
|
||||||
pleaseSelectShippingAddress: '请选择收货地址。',
|
pleaseSelectShippingAddress: '请选择收货地址。',
|
||||||
pleaseCompleteAddressFields: '请填写完整地址信息。',
|
pleaseCompleteAddressFields: '请填写完整地址信息。',
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ export const useUserStore = create<UserState>()(
|
|||||||
}),
|
}),
|
||||||
{
|
{
|
||||||
name: 'playx-user',
|
name: 'playx-user',
|
||||||
storage: createJSONStorage(() => localStorage),
|
storage: createJSONStorage(() => sessionStorage),
|
||||||
partialize: (state) => ({
|
partialize: (state) => ({
|
||||||
userInfo: state.userInfo,
|
userInfo: state.userInfo,
|
||||||
authInfo: state.authInfo,
|
authInfo: state.authInfo,
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ export interface AddressInfo {
|
|||||||
receiver_name: string,
|
receiver_name: string,
|
||||||
/**@description 电话 */
|
/**@description 电话 */
|
||||||
phone: string,
|
phone: string,
|
||||||
/**@description 地区(数组或逗号分隔字符串) */
|
|
||||||
region: string,
|
|
||||||
/**@description 详细地址 */
|
/**@description 详细地址 */
|
||||||
detail_address: string,
|
detail_address: string,
|
||||||
/**@description 1 设为默认地址 */
|
/**@description 1 设为默认地址 */
|
||||||
|
|||||||
@@ -9,12 +9,14 @@ import {ArrowLeft, BadgeCheck, MapPinHouse, PencilLine, Plus, Trash2} from 'luci
|
|||||||
import {useAddressBook} from '@/features/addressBook'
|
import {useAddressBook} from '@/features/addressBook'
|
||||||
import {GoodsRedeemModal} from '@/features/goods'
|
import {GoodsRedeemModal} from '@/features/goods'
|
||||||
import {notifySuccess} from '@/features/notifications'
|
import {notifySuccess} from '@/features/notifications'
|
||||||
|
import {ensureSessionForAction} from '@/lib/authGuard'
|
||||||
import {MotionButton, MotionDiv, tapMotionProps, tapMotionPropsIcon, tapMotionPropsSoft} from '@/lib/motion'
|
import {MotionButton, MotionDiv, tapMotionProps, tapMotionPropsIcon, tapMotionPropsSoft} from '@/lib/motion'
|
||||||
import type {AddressListItem} from '@/types/address.type.ts'
|
import type {AddressListItem} from '@/types/address.type.ts'
|
||||||
|
|
||||||
function AccountPage() {
|
function AccountPage() {
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation()
|
||||||
const addressBook = useAddressBook({autoLoad: true})
|
const addressBook = useAddressBook({autoLoad: true})
|
||||||
|
const sessionId = addressBook.sessionId
|
||||||
const [addressModalOpen, setAddressModalOpen] = useState(false)
|
const [addressModalOpen, setAddressModalOpen] = useState(false)
|
||||||
const [editingAddress, setEditingAddress] = useState<AddressListItem | null>(null)
|
const [editingAddress, setEditingAddress] = useState<AddressListItem | null>(null)
|
||||||
const [deleteTarget, setDeleteTarget] = useState<AddressListItem | null>(null)
|
const [deleteTarget, setDeleteTarget] = useState<AddressListItem | null>(null)
|
||||||
@@ -22,12 +24,20 @@ function AccountPage() {
|
|||||||
const isAddressFormValid = addressBook.isAddressFormValid
|
const isAddressFormValid = addressBook.isAddressFormValid
|
||||||
|
|
||||||
const handleOpenAddAddress = () => {
|
const handleOpenAddAddress = () => {
|
||||||
|
if (!ensureSessionForAction()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setEditingAddress(null)
|
setEditingAddress(null)
|
||||||
addressBook.resetAddressForm()
|
addressBook.resetAddressForm()
|
||||||
setAddressModalOpen(true)
|
setAddressModalOpen(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleOpenEditAddress = (address: AddressListItem) => {
|
const handleOpenEditAddress = (address: AddressListItem) => {
|
||||||
|
if (!ensureSessionForAction()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
setEditingAddress(address)
|
setEditingAddress(address)
|
||||||
addressBook.fillAddressForm(address)
|
addressBook.fillAddressForm(address)
|
||||||
setAddressModalOpen(true)
|
setAddressModalOpen(true)
|
||||||
@@ -102,7 +112,11 @@ function AccountPage() {
|
|||||||
|
|
||||||
<div className="min-h-0 flex-1 overflow-y-auto pb-[24px]">
|
<div className="min-h-0 flex-1 overflow-y-auto pb-[24px]">
|
||||||
<div className="mx-auto w-full max-w-[1000px]">
|
<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">
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||||
{t('account.loadingAddressList')}
|
{t('account.loadingAddressList')}
|
||||||
</div>
|
</div>
|
||||||
@@ -156,7 +170,13 @@ function AccountPage() {
|
|||||||
<MotionButton
|
<MotionButton
|
||||||
type="button"
|
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]"
|
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}
|
{...tapMotionPropsSoft}
|
||||||
>
|
>
|
||||||
<Trash2 className="h-[12px] w-[12px]" aria-hidden="true" />
|
<Trash2 className="h-[12px] w-[12px]" aria-hidden="true" />
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import {useState} from 'react'
|
import {useState} from 'react'
|
||||||
|
|
||||||
import {useMutation} from '@tanstack/react-query'
|
import {useMutation} from '@tanstack/react-query'
|
||||||
import {Check, Languages} from 'lucide-react'
|
|
||||||
import {useTranslation} from 'react-i18next'
|
import {useTranslation} from 'react-i18next'
|
||||||
|
|
||||||
import PageLayout from '@/components/layout'
|
import PageLayout from '@/components/layout'
|
||||||
@@ -31,15 +30,9 @@ import {
|
|||||||
import {validateClaimSubmission} from '@/features/home/claimValidation'
|
import {validateClaimSubmission} from '@/features/home/claimValidation'
|
||||||
import {claim} from '@/api/business.ts'
|
import {claim} from '@/api/business.ts'
|
||||||
import {notifyError, notifySuccess} from '@/features/notifications'
|
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 {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) {
|
function QuickNavCard({icon: Icon, label, to}: QuickNavCardProps) {
|
||||||
return (
|
return (
|
||||||
@@ -71,11 +64,8 @@ function getProgressPercent(current = 0, total = 0) {
|
|||||||
function HomePage() {
|
function HomePage() {
|
||||||
const {t} = useTranslation()
|
const {t} = useTranslation()
|
||||||
const [claimModalOpen, setClaimModalOpen] = useState(false)
|
const [claimModalOpen, setClaimModalOpen] = useState(false)
|
||||||
const [languageModalOpen, setLanguageModalOpen] = useState(false)
|
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate()
|
||||||
const authInfo = useUserStore(state => state.authInfo)
|
const authInfo = useUserStore(state => state.authInfo)
|
||||||
const language = useUserStore((state) => state.language)
|
|
||||||
const setLanguage = useUserStore((state) => state.setLanguage)
|
|
||||||
const {productCategories, loading} = useGoodsCatalog()
|
const {productCategories, loading} = useGoodsCatalog()
|
||||||
const {invalidateAssets} = useAssetsRefresh()
|
const {invalidateAssets} = useAssetsRefresh()
|
||||||
const redeem = useGoodsRedeem()
|
const redeem = useGoodsRedeem()
|
||||||
@@ -94,13 +84,16 @@ function HomePage() {
|
|||||||
const {assetsInfo} = useAssetsQuery()
|
const {assetsInfo} = useAssetsQuery()
|
||||||
const claimProgress = getProgressPercent(assetsInfo?.today_claimed, assetsInfo?.today_limit)
|
const claimProgress = getProgressPercent(assetsInfo?.today_claimed, assetsInfo?.today_limit)
|
||||||
const isClaimAvailable = (assetsInfo?.locked_points ?? 0) > 0
|
const isClaimAvailable = (assetsInfo?.locked_points ?? 0) > 0
|
||||||
const currentLanguage = normalizeLanguage(language)
|
|
||||||
const previewCategories: ProductCategory[] = productCategories.map((category) => ({
|
const previewCategories: ProductCategory[] = productCategories.map((category) => ({
|
||||||
...category,
|
...category,
|
||||||
items: category.items.slice(0, 4),
|
items: category.items.slice(0, 4),
|
||||||
}))
|
}))
|
||||||
|
|
||||||
const handleOpenClaimModal = () => {
|
const handleOpenClaimModal = () => {
|
||||||
|
if (!ensureSessionForAction()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if (!isClaimAvailable) {
|
if (!isClaimAvailable) {
|
||||||
notifyError(t('home.noClaimablePointsAvailable'))
|
notifyError(t('home.noClaimablePointsAvailable'))
|
||||||
return
|
return
|
||||||
@@ -115,6 +108,10 @@ function HomePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleSyncBalance = async () => {
|
const handleSyncBalance = async () => {
|
||||||
|
if (!ensureSessionForAction()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await syncBalanceMutation.mutateAsync()
|
await syncBalanceMutation.mutateAsync()
|
||||||
notifySuccess(t('home.balanceSyncedSuccessfully'))
|
notifySuccess(t('home.balanceSyncedSuccessfully'))
|
||||||
@@ -124,6 +121,10 @@ function HomePage() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleConfirmClaim = async () => {
|
const handleConfirmClaim = async () => {
|
||||||
|
if (!ensureSessionForAction()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
const claimValidation = validateClaimSubmission(authInfo, assetsInfo)
|
const claimValidation = validateClaimSubmission(authInfo, assetsInfo)
|
||||||
if (!claimValidation.valid) {
|
if (!claimValidation.valid) {
|
||||||
notifyError(claimValidation.message)
|
notifyError(claimValidation.message)
|
||||||
@@ -144,33 +145,12 @@ function HomePage() {
|
|||||||
navigate(`/goods?type=${type}`)
|
navigate(`/goods?type=${type}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleSelectLanguage = (nextLanguage: AppLanguage) => {
|
|
||||||
setLanguage(nextLanguage)
|
|
||||||
setLanguageModalOpen(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout>
|
<PageLayout>
|
||||||
<div
|
<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="/record" icon={History} label={t('nav.record')}/>
|
||||||
<QuickNavCard to="/account" icon={UserRound} label={t('nav.account')}/>
|
<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>
|
||||||
|
|
||||||
<div className="mt-[4px]">
|
<div className="mt-[4px]">
|
||||||
@@ -311,27 +291,6 @@ function HomePage() {
|
|||||||
onChangeAddressForm={redeem.changeAddressForm}
|
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
|
<Modal
|
||||||
open={claimModalOpen}
|
open={claimModalOpen}
|
||||||
title={t('home.confirmClaim')}
|
title={t('home.confirmClaim')}
|
||||||
|
|||||||
@@ -371,6 +371,14 @@ function OrdersTabContent({
|
|||||||
})
|
})
|
||||||
const orderRecords = ordersQuery.data ?? []
|
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) {
|
if (ordersQuery.isPending) {
|
||||||
return (
|
return (
|
||||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
<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 ?? []
|
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) {
|
if (pointsLogsQuery.isPending) {
|
||||||
return (
|
return (
|
||||||
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
<div className="liquid-glass-bg px-[16px] py-[18px] text-[14px] text-white/60">
|
||||||
|
|||||||
Reference in New Issue
Block a user