Files
playX/src/features/authGuide.tsx

247 lines
8.0 KiB
TypeScript

import {useQuery, useQueryClient} from '@tanstack/react-query'
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'
import type {HostContextMessage} from '@/types'
const HOST_READY_MESSAGE = 'IFRAME_READY'
const HOST_HEIGHT_MESSAGE = 'IFRAME_HEIGHT'
const HOST_READY_INTERVAL = 500
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)
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 activeTokenRef = useRef('')
useEffect(() => {
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, token} = message.payload
if (typeof token === 'string' && token.trim()) {
const normalizedToken = token.trim()
if (activeTokenRef.current && activeTokenRef.current !== normalizedToken) {
clearUserInfo()
queryClient.removeQueries({queryKey: ['auth-bootstrap']})
}
hasHostContextRef.current = true
activeTokenRef.current = normalizedToken
setHostToken(normalizedToken)
}
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(hostToken),
enabled: Boolean(hostToken),
queryFn: async () => {
const validateResponse = await validateToken(hostToken)
const authInfo = validateResponse.data
const assetsResponse = await userAssets({
session_id: authInfo.session_id,
})
return {
userInfo: {token: hostToken},
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])
useEffect(() => {
if (window.parent === window) {
return
}
let frameId = 0
let timeoutId: number | null = null
let lastReportedHeight = 0
let lastReportedAt = 0
const measureHeight = () =>
Math.max(
document.body.scrollHeight,
document.documentElement.scrollHeight,
document.body.offsetHeight,
document.documentElement.offsetHeight,
)
const postHeight = (height: number) => {
lastReportedHeight = height
lastReportedAt = Date.now()
window.parent.postMessage(
{
type: HOST_HEIGHT_MESSAGE,
payload: {
height,
},
},
'*',
)
}
const flushPostHeight = () => {
frameId = 0
const nextHeight = measureHeight()
if (nextHeight === lastReportedHeight) {
return
}
const elapsed = Date.now() - lastReportedAt
if (elapsed >= HOST_HEIGHT_REPORT_INTERVAL) {
postHeight(nextHeight)
return
}
if (timeoutId != null) {
return
}
timeoutId = window.setTimeout(() => {
timeoutId = null
postHeight(measureHeight())
}, HOST_HEIGHT_REPORT_INTERVAL - elapsed)
}
const schedulePostHeight = () => {
if (frameId) {
window.cancelAnimationFrame(frameId)
}
frameId = window.requestAnimationFrame(() => {
flushPostHeight()
})
}
schedulePostHeight()
const resizeObserver = new ResizeObserver(() => {
schedulePostHeight()
})
resizeObserver.observe(document.body)
resizeObserver.observe(document.documentElement)
window.addEventListener('load', schedulePostHeight)
window.addEventListener('resize', schedulePostHeight)
return () => {
resizeObserver.disconnect()
window.removeEventListener('load', schedulePostHeight)
window.removeEventListener('resize', schedulePostHeight)
if (frameId) {
window.cancelAnimationFrame(frameId)
}
if (timeoutId != null) {
window.clearTimeout(timeoutId)
}
}
}, [])
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}</>
}