diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 91c06e1..a392008 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -48,7 +48,29 @@ export default function RootLayout({ dangerouslySetInnerHTML={{ __html: ` if (typeof window !== 'undefined') { - window.onFlutterResponse = window.onFlutterResponse || function() {}; + var flutterResponseListeners = window.__flutterResponseListeners || []; + window.__flutterResponseListeners = flutterResponseListeners; + + window.addFlutterResponseListener = window.addFlutterResponseListener || function(listener) { + flutterResponseListeners.push(listener); + + return function() { + var index = flutterResponseListeners.indexOf(listener); + if (index >= 0) { + flutterResponseListeners.splice(index, 1); + } + }; + }; + + window.onFlutterResponse = function(event) { + flutterResponseListeners.slice().forEach(function(listener) { + try { + listener(event); + } catch (error) { + console.error('Flutter response listener failed', error); + } + }); + }; var HABIB_TOKEN_COOKIE = 'HABIB_TOKEN'; var HABIB_COINS_COOKIE = 'HABIB_COINS'; diff --git a/src/components/ui/report-actions-sheet.tsx b/src/components/ui/report-actions-sheet.tsx index 367a24a..bc5d8ac 100644 --- a/src/components/ui/report-actions-sheet.tsx +++ b/src/components/ui/report-actions-sheet.tsx @@ -74,12 +74,10 @@ export function ReportActionsSheet({ onClose }: ReportActionsSheetProps) { } }; - window.onFlutterResponse = handleFlutterResponse; + const unsubscribe = window.addFlutterResponseListener?.(handleFlutterResponse); return () => { - if (window.onFlutterResponse === handleFlutterResponse) { - window.onFlutterResponse = undefined; - } + unsubscribe?.(); }; }, []); diff --git a/src/lib/auth-bridge.ts b/src/lib/auth-bridge.ts index 724ea21..0e64fa9 100644 --- a/src/lib/auth-bridge.ts +++ b/src/lib/auth-bridge.ts @@ -2,18 +2,37 @@ import { getClientCookie } from "./cookies"; const TOKEN_COOKIE_NAME = "HABIB_TOKEN"; const COINS_COOKIE_NAME = "HABIB_COINS"; +const LOGIN_TIMEOUT_MS = 30_000; + +function postFlutterMessage(payload: Record) { + if (typeof window === "undefined") { + return false; + } + + const app = window.HabibApp; + if (!app?.postMessage) { + return false; + } + + app.postMessage(JSON.stringify(payload)); + return true; +} class AuthBridge { private token: string | null = null; private coins = 0; private isReady = false; + private loginRequested = false; private readyCallbacks: Array<() => void> = []; + private pendingResolvers: Array<(token: string | null) => void> = []; + private flutterResponseUnsubscribe?: () => void; + private loginTimeoutId?: number; constructor() { this.init(); } - private hydrateFromWindow() { + private syncFromStorage() { if (typeof window === "undefined") { return false; } @@ -28,67 +47,164 @@ class AuthBridge { getClientCookie(TOKEN_COOKIE_NAME) ?? getClientCookie("habib_token"); const coinsValue = win.HABIB_COINS; - const coinsCookie = getClientCookie(COINS_COOKIE_NAME) ?? getClientCookie("habib_coins"); + const coinsCookie = + getClientCookie(COINS_COOKIE_NAME) ?? getClientCookie("habib_coins"); - if (token) { - this.token = token; - this.coins = coinsValue ?? Number(coinsCookie ?? 0); - return true; + if (!token) { + return false; } - return false; + this.token = token; + this.coins = coinsValue ?? Number(coinsCookie ?? 0); + return true; } - private init() { - if (typeof window === "undefined") { + private setupFlutterResponseListener() { + if (typeof window === "undefined" || this.flutterResponseUnsubscribe) { return; } - if (this.hydrateFromWindow()) { - this.isReady = true; - this.notifyReady(); + const win = window as Window & { + addFlutterResponseListener?: ( + listener: (event: FlutterResponseEvent) => void, + ) => () => void; + }; + + if (typeof win.addFlutterResponseListener === "function") { + this.flutterResponseUnsubscribe = win.addFlutterResponseListener((event) => { + if (event.action === "login") { + this.handleLoginResponse(event.success); + } + }); return; } - this.waitForInjection(); + const fallbackInterval = window.setInterval(() => { + if (typeof win.addFlutterResponseListener === "function") { + window.clearInterval(fallbackInterval); + this.flutterResponseUnsubscribe = win.addFlutterResponseListener((event) => { + if (event.action === "login") { + this.handleLoginResponse(event.success); + } + }); + } + }, 50); } - private waitForInjection() { - let attempts = 0; - const maxAttempts = 50; + private handleLoginResponse(success: boolean) { + if (success && this.syncFromStorage()) { + this.loginRequested = false; + this.markReady(this.token); + return; + } - const checkInterval = window.setInterval(() => { - if (this.hydrateFromWindow()) { - this.isReady = true; - window.clearInterval(checkInterval); - this.notifyReady(); - return; - } + if (!success) { + this.loginRequested = false; + console.warn("⚠️ Flutter login failed"); + this.resolvePending(null); + this.markReady(null); + } + } + + private requestLogin() { + if (this.loginRequested || this.token) { + return; + } - if (++attempts >= maxAttempts) { - window.clearInterval(checkInterval); - console.warn("⚠️ Token not received from Flutter"); - this.isReady = true; - this.notifyReady(); + this.loginRequested = true; + const sent = postFlutterMessage({ action: "login" }); + + if (!sent) { + this.loginRequested = false; + return; + } + + if (this.loginTimeoutId) { + window.clearTimeout(this.loginTimeoutId); + } + + this.loginTimeoutId = window.setTimeout(() => { + if (!this.token) { + console.warn("⚠️ Flutter login timeout"); + this.loginRequested = false; + this.resolvePending(null); } - }, 100); + }, LOGIN_TIMEOUT_MS); + } + + private markReady(token: string | null) { + if (this.loginTimeoutId) { + window.clearTimeout(this.loginTimeoutId); + this.loginTimeoutId = undefined; + } + + this.isReady = true; + this.resolvePending(token); + this.notifyReady(); + } + + private resolvePending(token: string | null) { + const resolvers = this.pendingResolvers.splice(0, this.pendingResolvers.length); + for (const resolve of resolvers) { + resolve(token); + } } private notifyReady() { - this.readyCallbacks.forEach((cb) => cb()); - this.readyCallbacks = []; + const callbacks = this.readyCallbacks.splice(0, this.readyCallbacks.length); + for (const callback of callbacks) { + callback(); + } + } + + private init() { + if (typeof window === "undefined") { + return; + } + + if (this.syncFromStorage()) { + this.markReady(this.token); + return; + } + + this.setupFlutterResponseListener(); + this.requestLogin(); } public onReady(callback: () => void) { if (this.isReady) { callback(); - } else { - this.readyCallbacks.push(callback); + return; } + + this.readyCallbacks.push(callback); + } + + public async ensureToken() { + if (typeof window === "undefined") { + return null; + } + + if (this.syncFromStorage()) { + return this.token; + } + + this.setupFlutterResponseListener(); + this.requestLogin(); + + return await new Promise((resolve) => { + this.pendingResolvers.push(resolve); + }); } public getToken(): string | null { - return this.token ?? getClientCookie(TOKEN_COOKIE_NAME) ?? getClientCookie("habib_token"); + if (this.token) { + return this.token; + } + + return ( + getClientCookie(TOKEN_COOKIE_NAME) ?? getClientCookie("habib_token") + ); } public getCoins(): number { diff --git a/src/lib/http.ts b/src/lib/http.ts index 360e710..a1b0efa 100644 --- a/src/lib/http.ts +++ b/src/lib/http.ts @@ -60,18 +60,18 @@ export const http = axios.create({ withCredentials: true, }); -http.interceptors.request.use((config) => { - const token = authBridge.getToken() ?? getClientCookie("HABIB_TOKEN"); - - if (token) { - config.headers.Authorization = `Token ${token}`; - } - +http.interceptors.request.use(async (config) => { if (shouldUseProxy() && config.url && !isAbsoluteUrl(config.url)) { config.params = withProxyPathParam(config.params, config.url); config.url = ""; } + const token = await authBridge.ensureToken(); + + if (token) { + config.headers.Authorization = `Token ${token}`; + } + return config; }); diff --git a/src/types/window.d.ts b/src/types/window.d.ts index 8eb1dfd..27f73c6 100644 --- a/src/types/window.d.ts +++ b/src/types/window.d.ts @@ -9,6 +9,10 @@ declare global { HABIB_TOKEN?: string; HABIB_COINS?: number; onFlutterResponse?: (event: FlutterResponseEvent) => void; + __flutterResponseListeners?: Array<(event: FlutterResponseEvent) => void>; + addFlutterResponseListener?: ( + listener: (event: FlutterResponseEvent) => void, + ) => () => void; HabibApp?: { postMessage: (message: string) => void; };