Browse Source

feat action report login

master
mortezaei 2 months ago
parent
commit
675a34382e
  1. 24
      src/app/layout.tsx
  2. 6
      src/components/ui/report-actions-sheet.tsx
  3. 168
      src/lib/auth-bridge.ts
  4. 14
      src/lib/http.ts
  5. 4
      src/types/window.d.ts

24
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';

6
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?.();
};
}, []);

168
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<string, unknown>) {
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) {
return false;
}
if (token) {
this.token = token;
this.coins = coinsValue ?? Number(coinsCookie ?? 0);
return true;
}
return false;
private setupFlutterResponseListener() {
if (typeof window === "undefined" || this.flutterResponseUnsubscribe) {
return;
}
private init() {
if (typeof window === "undefined") {
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;
}
if (this.hydrateFromWindow()) {
this.isReady = true;
this.notifyReady();
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 handleLoginResponse(success: boolean) {
if (success && this.syncFromStorage()) {
this.loginRequested = false;
this.markReady(this.token);
return;
}
this.waitForInjection();
if (!success) {
this.loginRequested = false;
console.warn("⚠️ Flutter login failed");
this.resolvePending(null);
this.markReady(null);
}
}
private requestLogin() {
if (this.loginRequested || this.token) {
return;
}
private waitForInjection() {
let attempts = 0;
const maxAttempts = 50;
this.loginRequested = true;
const sent = postFlutterMessage({ action: "login" });
const checkInterval = window.setInterval(() => {
if (this.hydrateFromWindow()) {
this.isReady = true;
window.clearInterval(checkInterval);
this.notifyReady();
if (!sent) {
this.loginRequested = false;
return;
}
if (++attempts >= maxAttempts) {
window.clearInterval(checkInterval);
console.warn("⚠️ Token not received from Flutter");
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);
}
}, 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();
}
}, 100);
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 {
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<string | null>((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 {

14
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;
});

4
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;
};

Loading…
Cancel
Save