|
1 | 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ |
2 | 2 |
|
3 | | -export function waitForNativeToken(timeoutMs = 10000): Promise<string> { |
| 3 | +declare global { |
| 4 | + interface Window { |
| 5 | + Android?: { getAccessToken?: () => string | null | undefined }; |
| 6 | + } |
| 7 | +} |
| 8 | + |
| 9 | +type WaitTokenOptions = { |
| 10 | + timeoutMs?: number; // default 10s |
| 11 | + pollMs?: number; // default 30ms |
| 12 | +}; |
| 13 | + |
| 14 | +/** |
| 15 | + * 네이티브 이벤트(native-token-refreshed) + 브리지 + sessionStorage를 |
| 16 | + * 동시에 기다렸다가, 가장 먼저 들어오는 토큰을 반환한다. |
| 17 | + */ |
| 18 | +export function waitForNativeToken(opts: WaitTokenOptions = {}): Promise<string> { |
| 19 | + const { timeoutMs = 10000, pollMs = 30 } = opts; |
| 20 | + |
4 | 21 | return new Promise((resolve, reject) => { |
5 | | - const onToken = (e: Event) => { |
6 | | - const token = (e as CustomEvent).detail?.token ?? sessionStorage.getItem('accessToken'); |
7 | | - cleanup(); |
8 | | - if (token) resolve(token); |
9 | | - else reject(new Error('Token event received but empty')); |
| 22 | + if (typeof window === 'undefined') { |
| 23 | + reject(new Error('window is undefined (SSR)')); |
| 24 | + return; |
| 25 | + } |
| 26 | + |
| 27 | + let done = false; |
| 28 | + let timer: ReturnType<typeof setTimeout> | null = null; |
| 29 | + let poller: ReturnType<typeof setInterval> | null = null; |
| 30 | + |
| 31 | + const cleanup = () => { |
| 32 | + if (timer) clearTimeout(timer); |
| 33 | + if (poller) clearInterval(poller); |
| 34 | + window.removeEventListener('native-token-refreshed', onEvent as any); |
10 | 35 | }; |
11 | | - const onTimeout = () => { |
| 36 | + |
| 37 | + const resolveOnce = (token: string) => { |
| 38 | + if (done) return; |
| 39 | + done = true; |
12 | 40 | cleanup(); |
13 | | - reject(new Error('Token refresh timeout')); |
| 41 | + resolve(token); |
14 | 42 | }; |
15 | | - const cleanup = () => { |
16 | | - window.removeEventListener('native-token-refreshed', onToken as any); |
17 | | - clearTimeout(timer); |
| 43 | + |
| 44 | + const rejectOnce = (err: Error) => { |
| 45 | + if (done) return; |
| 46 | + done = true; |
| 47 | + cleanup(); |
| 48 | + reject(err); |
18 | 49 | }; |
19 | 50 |
|
20 | | - window.addEventListener('native-token-refreshed', onToken as any, { once: true }); |
| 51 | + // 1) 이벤트 리스너 (네이티브가 새 토큰을 던져줌) |
| 52 | + const onEvent = (e: Event) => { |
| 53 | + const token = (e as CustomEvent).detail?.token ?? null; |
| 54 | + if (token) resolveOnce(String(token)); |
| 55 | + }; |
| 56 | + window.addEventListener('native-token-refreshed', onEvent as any, { once: true }); |
21 | 57 |
|
22 | | - const existing = sessionStorage.getItem('accessToken'); |
23 | | - if (existing) { |
24 | | - cleanup(); |
25 | | - resolve(existing); |
26 | | - return; |
| 58 | + // 2) 즉시 가용 토큰 체크 (브리지 → 세션스토리지 순) |
| 59 | + try { |
| 60 | + const viaBridge = window.Android?.getAccessToken?.(); |
| 61 | + if (viaBridge) return resolveOnce(String(viaBridge)); |
| 62 | + } catch { |
| 63 | + /* empty */ |
27 | 64 | } |
| 65 | + try { |
| 66 | + const viaSession = window.sessionStorage.getItem('accessToken'); |
| 67 | + if (viaSession) return resolveOnce(viaSession); |
| 68 | + } catch { |
| 69 | + /* empty */ |
| 70 | + } |
| 71 | + |
| 72 | + // 3) 폴링 (브리지/세션스토리지) |
| 73 | + poller = setInterval(() => { |
| 74 | + try { |
| 75 | + const br = window.Android?.getAccessToken?.(); |
| 76 | + if (br) return resolveOnce(String(br)); |
| 77 | + } catch { |
| 78 | + /* empty */ |
| 79 | + } |
| 80 | + try { |
| 81 | + const ss = window.sessionStorage.getItem('accessToken'); |
| 82 | + if (ss) return resolveOnce(ss); |
| 83 | + } catch { |
| 84 | + /* empty */ |
| 85 | + } |
| 86 | + }, pollMs); |
28 | 87 |
|
29 | | - const timer = setTimeout(onTimeout, timeoutMs); |
| 88 | + // 4) 타임아웃 |
| 89 | + timer = setTimeout(() => { |
| 90 | + rejectOnce(new Error('Token refresh timeout')); |
| 91 | + }, timeoutMs); |
30 | 92 | }); |
31 | 93 | } |
0 commit comments