166 lines
6.1 KiB
TypeScript
166 lines
6.1 KiB
TypeScript
import {useQuery, useQueryClient} from '@tanstack/react-query'
|
|
import {type PropsWithChildren, useEffect, useRef, useState} from 'react'
|
|
|
|
import {login, 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'
|
|
import type {HostContextMessage} from '@/types'
|
|
|
|
const HOST_READY_MESSAGE = 'PLAYX_READY'
|
|
const HOST_READY_INTERVAL = 500
|
|
const HOST_READY_RETRY_LIMIT = 20
|
|
const TEST_BOOTSTRAP_ENABLED = import.meta.env.VITE_BYPASS_IFRAME_CONTEXT === 'true'
|
|
const TEST_BOOTSTRAP_USERNAME = '+60777777777'
|
|
const TEST_BOOTSTRAP_LANGUAGE = 'zh'
|
|
|
|
export function AuthGuide({children}: PropsWithChildren) {
|
|
const {t} = useTranslation()
|
|
const queryClient = useQueryClient()
|
|
const [username, setUsername] = useState(TEST_BOOTSTRAP_ENABLED ? TEST_BOOTSTRAP_USERNAME : '')
|
|
const setUserInfo = useUserStore((state) => state.setUserInfo)
|
|
const setAuthInfo = useUserStore((state) => state.setAuthInfo)
|
|
const setAssetsInfo = useUserStore((state) => state.setAssetsInfo)
|
|
const setLanguage = useUserStore((state) => state.setLanguage)
|
|
const clearUserInfo = useUserStore((state) => state.clearUserInfo)
|
|
const hasHostContextRef = useRef(false)
|
|
const activeUsernameRef = useRef('')
|
|
|
|
useEffect(() => {
|
|
if (TEST_BOOTSTRAP_ENABLED) {
|
|
hasHostContextRef.current = true
|
|
activeUsernameRef.current = TEST_BOOTSTRAP_USERNAME
|
|
setLanguage(TEST_BOOTSTRAP_LANGUAGE)
|
|
return
|
|
}
|
|
|
|
const isEmbedded = window.parent !== window
|
|
|
|
const notifyParentReady = () => {
|
|
if (!isEmbedded || hasHostContextRef.current) {
|
|
return
|
|
}
|
|
|
|
window.parent.postMessage({type: HOST_READY_MESSAGE}, '*')
|
|
}
|
|
|
|
const handleMessage = (event: MessageEvent<HostContextMessage>) => {
|
|
if (isEmbedded && event.source !== window.parent) {
|
|
return
|
|
}
|
|
|
|
const message = event.data
|
|
|
|
if (!message || message.type !== 'IFRAME_CONTEXT' || !message.payload) {
|
|
return
|
|
}
|
|
|
|
const {language, username: nextUsername} = message.payload
|
|
|
|
if (typeof nextUsername === 'string' && nextUsername.trim()) {
|
|
const normalizedUsername = nextUsername.trim()
|
|
if (activeUsernameRef.current && activeUsernameRef.current !== normalizedUsername) {
|
|
clearUserInfo()
|
|
queryClient.removeQueries({queryKey: ['auth-bootstrap']})
|
|
}
|
|
hasHostContextRef.current = true
|
|
activeUsernameRef.current = normalizedUsername
|
|
setUsername(normalizedUsername)
|
|
}
|
|
|
|
if (typeof language === 'string' && language.trim()) {
|
|
setLanguage(normalizeLanguage(language))
|
|
}
|
|
}
|
|
|
|
window.addEventListener('message', handleMessage)
|
|
|
|
notifyParentReady()
|
|
|
|
const retryTimer = isEmbedded
|
|
? window.setInterval(() => {
|
|
notifyParentReady()
|
|
}, HOST_READY_INTERVAL)
|
|
: null
|
|
const stopRetryTimer = isEmbedded
|
|
? window.setTimeout(() => {
|
|
if (retryTimer != null) {
|
|
window.clearInterval(retryTimer)
|
|
}
|
|
}, HOST_READY_INTERVAL * HOST_READY_RETRY_LIMIT)
|
|
: null
|
|
|
|
return () => {
|
|
window.removeEventListener('message', handleMessage)
|
|
if (retryTimer != null) {
|
|
window.clearInterval(retryTimer)
|
|
}
|
|
if (stopRetryTimer != null) {
|
|
window.clearTimeout(stopRetryTimer)
|
|
}
|
|
}
|
|
}, [clearUserInfo, queryClient, setLanguage])
|
|
|
|
const authBootstrapQuery = useQuery({
|
|
queryKey: queryKeys.authBootstrap(username),
|
|
enabled: Boolean(username),
|
|
queryFn: async () => {
|
|
const loginResponse = await login({username})
|
|
const userInfo = loginResponse.data.userInfo
|
|
const validateResponse = await validateToken(userInfo.token)
|
|
const authInfo = validateResponse.data
|
|
const assetsResponse = await userAssets({
|
|
session_id: authInfo.session_id,
|
|
})
|
|
|
|
return {
|
|
userInfo,
|
|
authInfo,
|
|
assetsInfo: assetsResponse.data,
|
|
}
|
|
},
|
|
})
|
|
|
|
useEffect(() => {
|
|
if (!authBootstrapQuery.data) {
|
|
return
|
|
}
|
|
|
|
setUserInfo(authBootstrapQuery.data.userInfo)
|
|
setAuthInfo(authBootstrapQuery.data.authInfo)
|
|
setAssetsInfo(authBootstrapQuery.data.assetsInfo)
|
|
queryClient.setQueryData(queryKeys.assets(authBootstrapQuery.data.authInfo.session_id), authBootstrapQuery.data.assetsInfo)
|
|
}, [authBootstrapQuery.data, queryClient, setAssetsInfo, setAuthInfo, setUserInfo])
|
|
|
|
useEffect(() => {
|
|
if (!authBootstrapQuery.isError) {
|
|
return
|
|
}
|
|
|
|
clearUserInfo()
|
|
}, [authBootstrapQuery.isError, clearUserInfo])
|
|
|
|
if (!username || authBootstrapQuery.isPending) {
|
|
return (
|
|
<div className="flex min-h-screen items-center justify-center bg-[#08070E] px-6 text-center text-[14px] text-white/68">
|
|
{!username && !TEST_BOOTSTRAP_ENABLED ? 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}</>
|
|
}
|