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) => { 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 (
{!hostToken ? t('auth.waitingForHostContext') : t('auth.loadingAccountData')}
) } if (authBootstrapQuery.isError) { return (
{t('auth.authenticationFailed')}
{t('auth.refreshAndTryAgain')}
) } return <>{children} }