diff --git a/src/features/addressBook/useAddressBook.ts b/src/features/addressBook/useAddressBook.ts
index 2b3b03d..c79a04b 100644
--- a/src/features/addressBook/useAddressBook.ts
+++ b/src/features/addressBook/useAddressBook.ts
@@ -4,6 +4,7 @@ import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
import {addressAdd, addressDelete, addressEdit, addressList} from '@/api/address.ts'
import {validateAddressFormSubmission} from '@/features/addressBook/addressValidation'
import {notifyError} from '@/features/notifications'
+import {ensureSessionForAction} from '@/lib/authGuard'
import {queryKeys} from '@/lib/queryKeys.ts'
import {useUserStore} from '@/store/user.ts'
import type {AddressListItem} from '@/types/address.type.ts'
@@ -65,7 +66,6 @@ export function useAddressBook(options?: UseAddressBookOptions) {
session_id: sessionId!,
receiver_name: addressForm.name.trim(),
phone: addressForm.phone.trim(),
- region: editingAddress ? editingAddress.region.map((part) => part.trim()).filter(Boolean).join(', ') : '',
detail_address: addressForm.detailedAddress.trim(),
default_setting: addressForm.isDefault ? '1' : '0',
} as const
@@ -138,7 +138,7 @@ export function useAddressBook(options?: UseAddressBookOptions) {
}
const saveAddress = async (editingAddress?: AddressListItem | null) => {
- if (!sessionId) {
+ if (!ensureSessionForAction() || !sessionId) {
return null
}
@@ -163,7 +163,7 @@ export function useAddressBook(options?: UseAddressBookOptions) {
}
const removeAddress = async (addressId: string) => {
- if (!sessionId) {
+ if (!ensureSessionForAction() || !sessionId) {
return null
}
@@ -182,7 +182,7 @@ export function useAddressBook(options?: UseAddressBookOptions) {
sessionId,
addresses,
addressOptions,
- loading: addressListQuery.isPending || loadAddressesLoading,
+ loading: Boolean(sessionId) && (addressListQuery.isPending || loadAddressesLoading),
addressForm,
isAddressFormValid,
submitLoading: saveAddressMutation.isPending,
diff --git a/src/features/authGuide.tsx b/src/features/authGuide.tsx
index 21c22aa..b67cbbb 100644
--- a/src/features/authGuide.tsx
+++ b/src/features/authGuide.tsx
@@ -3,7 +3,6 @@ import {type PropsWithChildren, useEffect, useRef, useState} from 'react'
import {validateToken} from '@/api/auth.ts'
import {userAssets} from '@/api/user.ts'
-import {useTranslation} from 'react-i18next'
import {normalizeLanguage} from '@/lib/i18n'
import {queryKeys} from '@/lib/queryKeys.ts'
import {useUserStore} from '@/store/user.ts'
@@ -16,7 +15,6 @@ const HOST_READY_RETRY_LIMIT = 20
const HOST_HEIGHT_REPORT_INTERVAL = 250
export function AuthGuide({children}: PropsWithChildren) {
- const {t} = useTranslation()
const queryClient = useQueryClient()
const [hostToken, setHostToken] = useState('')
const setUserInfo = useUserStore((state) => state.setUserInfo)
@@ -73,6 +71,12 @@ export function AuthGuide({children}: PropsWithChildren) {
hasHostContextRef.current = true
activeTokenRef.current = normalizedToken
setHostToken(normalizedToken)
+ } else {
+ hasHostContextRef.current = true
+ activeTokenRef.current = ''
+ setHostToken('')
+ clearUserInfo()
+ queryClient.removeQueries({queryKey: ['auth-bootstrap']})
}
if (typeof language === 'string' && language.trim()) {
@@ -236,24 +240,5 @@ export function AuthGuide({children}: PropsWithChildren) {
}
}, [])
- if (!hostToken || authBootstrapQuery.isPending) {
- return (
-
- {!hostToken ? t('auth.waitingForHostContext') : t('auth.loadingAccountData')}
-
- )
- }
-
- if (authBootstrapQuery.isError) {
- return (
-
-
-
{t('auth.authenticationFailed')}
-
{t('auth.refreshAndTryAgain')}
-
-
- )
- }
-
return <>{children}>
}
diff --git a/src/features/goods/useGoodsRedeem.ts b/src/features/goods/useGoodsRedeem.ts
index 46cff84..efb77b0 100644
--- a/src/features/goods/useGoodsRedeem.ts
+++ b/src/features/goods/useGoodsRedeem.ts
@@ -4,6 +4,7 @@ import {useState} from 'react'
import {bonusRedeem, physicalRedeem, withdrawApply} from '@/api/business.ts'
import {useAddressBook} from '@/features/addressBook'
import {notifyError, notifySuccess} from '@/features/notifications'
+import {ensureSessionForAction} from '@/lib/authGuard'
import i18n from '@/lib/i18n'
import {queryKeys} from '@/lib/queryKeys.ts'
import {validateAddAddressSubmission, validateRedeemSubmission} from '@/features/goods/redeemValidation'
@@ -45,6 +46,10 @@ export function useGoodsRedeem() {
})
const openRedeemModal = async (product: ProductItem, categoryId: ProductCategory['id']) => {
+ if (!ensureSessionForAction()) {
+ return
+ }
+
setSelectedProduct({
product,
categoryId,
@@ -70,6 +75,10 @@ export function useGoodsRedeem() {
}
const openAddAddress = () => {
+ if (!ensureSessionForAction()) {
+ return
+ }
+
setModalMode('add-address')
}
@@ -80,6 +89,10 @@ export function useGoodsRedeem() {
const isAddAddressFormValid = addressBook.isAddressFormValid
const confirmRedeem = async () => {
+ if (!ensureSessionForAction()) {
+ return
+ }
+
if (modalMode === 'add-address') {
const addAddressValidation = validateAddAddressSubmission({
isAddAddressFormValid,
diff --git a/src/lib/authGuard.ts b/src/lib/authGuard.ts
new file mode 100644
index 0000000..f4cc8dc
--- /dev/null
+++ b/src/lib/authGuard.ts
@@ -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
+}
diff --git a/src/locale/en.ts b/src/locale/en.ts
index 4295536..e7a76cf 100644
--- a/src/locale/en.ts
+++ b/src/locale/en.ts
@@ -137,6 +137,8 @@ const en = {
},
validation: {
sessionExpired: 'Session expired. Please log in again.',
+ verificationRequired: 'Please log in first.',
+ verificationInProgress: 'Verification in progress. Please try again shortly.',
noProductSelected: 'No product selected.',
pleaseSelectShippingAddress: 'Please select a shipping address.',
pleaseCompleteAddressFields: 'Please complete all required address fields.',
diff --git a/src/locale/ms.ts b/src/locale/ms.ts
index f3e3586..679b76e 100644
--- a/src/locale/ms.ts
+++ b/src/locale/ms.ts
@@ -137,6 +137,8 @@ const ms = {
},
validation: {
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.',
pleaseSelectShippingAddress: 'Sila pilih alamat penghantaran.',
pleaseCompleteAddressFields: 'Sila lengkapkan semua maklumat alamat.',
diff --git a/src/locale/zh.ts b/src/locale/zh.ts
index 1773e53..17dbc5d 100644
--- a/src/locale/zh.ts
+++ b/src/locale/zh.ts
@@ -137,6 +137,8 @@ const zh = {
},
validation: {
sessionExpired: '登录态已过期,请重新登录。',
+ verificationRequired: '请先登录。',
+ verificationInProgress: '验证中,请稍后重试。',
noProductSelected: '未选择商品。',
pleaseSelectShippingAddress: '请选择收货地址。',
pleaseCompleteAddressFields: '请填写完整地址信息。',
diff --git a/src/store/user.ts b/src/store/user.ts
index 053da06..75d65cf 100644
--- a/src/store/user.ts
+++ b/src/store/user.ts
@@ -31,7 +31,7 @@ export const useUserStore = create()(
}),
{
name: 'playx-user',
- storage: createJSONStorage(() => localStorage),
+ storage: createJSONStorage(() => sessionStorage),
partialize: (state) => ({
userInfo: state.userInfo,
authInfo: state.authInfo,
diff --git a/src/types/address.type.ts b/src/types/address.type.ts
index 745474c..0dbb963 100644
--- a/src/types/address.type.ts
+++ b/src/types/address.type.ts
@@ -3,8 +3,6 @@ export interface AddressInfo {
receiver_name: string,
/**@description 电话 */
phone: string,
- /**@description 地区(数组或逗号分隔字符串) */
- region: string,
/**@description 详细地址 */
detail_address: string,
/**@description 1 设为默认地址 */
diff --git a/src/views/account/index.tsx b/src/views/account/index.tsx
index 29e114b..88dc741 100644
--- a/src/views/account/index.tsx
+++ b/src/views/account/index.tsx
@@ -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(null)
const [deleteTarget, setDeleteTarget] = useState(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() {
- {addressBook.loading ? (
+ {!sessionId ? (
+
+ {t('validation.verificationRequired')}
+
+ ) : addressBook.loading ? (
{t('account.loadingAddressList')}
@@ -156,7 +170,13 @@ function AccountPage() {
setDeleteTarget(address)}
+ onClick={() => {
+ if (!ensureSessionForAction()) {
+ return
+ }
+
+ setDeleteTarget(address)
+ }}
{...tapMotionPropsSoft}
>
diff --git a/src/views/home/index.tsx b/src/views/home/index.tsx
index 91511c8..74c36ce 100644
--- a/src/views/home/index.tsx
+++ b/src/views/home/index.tsx
@@ -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 (
+ className="grid grid-cols-2 gap-2 py-[14px] sm:ml-auto sm:flex sm:w-auto sm:grid-cols-none sm:justify-end">
- setLanguageModalOpen(true)}
- {...tapMotionProps}
- >
-
-
-
-
-
{t(`nav.${LANGUAGE_OPTIONS.find((option) => option.value === currentLanguage)?.labelKey ?? 'switchToChinese'}`)}
-
-
-
@@ -311,27 +291,6 @@ function HomePage() {
onChangeAddressForm={redeem.changeAddressForm}
/>
- setLanguageModalOpen(false)}
- className="max-w-[420px]"
- bodyClassName="space-y-3"
- >
- {LANGUAGE_OPTIONS.map((option) => (
-
- ))}
-
-
+ {t('validation.verificationRequired')}
+
+ )
+ }
+
if (ordersQuery.isPending) {
return (
@@ -412,6 +420,14 @@ function PointsTabContent({sessionId}: {sessionId: string}) {
})
const pointsRecords = pointsLogsQuery.data ?? []
+ if (!sessionId) {
+ return (
+
+ {t('validation.verificationRequired')}
+
+ )
+ }
+
if (pointsLogsQuery.isPending) {
return (