You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
271 lines
6.1 KiB
271 lines
6.1 KiB
import { getClientCookie } from "./cookies";
|
|
|
|
const TOKEN_COOKIE_NAME = "HABIB_TOKEN";
|
|
const COINS_COOKIE_NAME = "HABIB_COINS";
|
|
const REDIRECT_SESSION_KEY = "redirect";
|
|
|
|
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;
|
|
|
|
constructor() {
|
|
this.init();
|
|
}
|
|
|
|
private syncFromStorage() {
|
|
if (typeof window === "undefined") {
|
|
return false;
|
|
}
|
|
|
|
const win = window as Window & {
|
|
HABIB_TOKEN?: string;
|
|
HABIB_COINS?: number;
|
|
};
|
|
|
|
const token =
|
|
win.HABIB_TOKEN ??
|
|
getClientCookie(TOKEN_COOKIE_NAME) ??
|
|
getClientCookie("habib_token");
|
|
const coinsValue = win.HABIB_COINS;
|
|
const coinsCookie =
|
|
getClientCookie(COINS_COOKIE_NAME) ?? getClientCookie("habib_coins");
|
|
|
|
if (!token || token === "NO_TOKEN" || token.trim() === "") {
|
|
this.token = null;
|
|
return false;
|
|
}
|
|
|
|
this.token = token;
|
|
this.coins = coinsValue ?? Number(coinsCookie ?? 0);
|
|
return true;
|
|
}
|
|
|
|
private setupFlutterResponseListener() {
|
|
if (typeof window === "undefined" || this.flutterResponseUnsubscribe) {
|
|
return;
|
|
}
|
|
|
|
const win = window as Window & {
|
|
addFlutterResponseListener?: (
|
|
listener: (event: FlutterResponseEvent) => void,
|
|
) => () => void;
|
|
};
|
|
|
|
const attach = () => {
|
|
if (typeof win.addFlutterResponseListener !== "function") {
|
|
return false;
|
|
}
|
|
|
|
this.flutterResponseUnsubscribe = win.addFlutterResponseListener(
|
|
(event) => {
|
|
if (event.action === "login") {
|
|
this.handleLoginResponse(event.success);
|
|
}
|
|
},
|
|
);
|
|
|
|
return true;
|
|
};
|
|
|
|
if (attach()) {
|
|
return;
|
|
}
|
|
|
|
const fallbackInterval = window.setInterval(() => {
|
|
if (attach()) {
|
|
window.clearInterval(fallbackInterval);
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
private handleLoginResponse(success: boolean) {
|
|
if (success) {
|
|
this.loginRequested = false;
|
|
|
|
if (this.syncFromStorage()) {
|
|
this.markReady(this.token);
|
|
return;
|
|
}
|
|
|
|
console.warn(
|
|
"⚠️ Flutter login succeeded, but token is not available yet. Set cookie before emitting success.",
|
|
);
|
|
this.resolvePending(null);
|
|
this.markReady(null);
|
|
return;
|
|
}
|
|
|
|
this.loginRequested = false;
|
|
console.warn("⚠️ Flutter login failed");
|
|
this.resolvePending(null);
|
|
this.markReady(null);
|
|
}
|
|
|
|
private requestLogin() {
|
|
if (this.loginRequested || this.token) {
|
|
return true;
|
|
}
|
|
|
|
if (typeof window !== "undefined") {
|
|
try {
|
|
window.sessionStorage.setItem(REDIRECT_SESSION_KEY, "true");
|
|
} catch (e) {
|
|
console.warn("sessionStorage is not accessible:", e);
|
|
}
|
|
}
|
|
|
|
this.loginRequested = true;
|
|
const sent = postFlutterMessage({ action: "login" });
|
|
|
|
if (!sent) {
|
|
this.loginRequested = false;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private markReady(token: string | null) {
|
|
this.isReady = true;
|
|
this.resolvePending(token);
|
|
this.notifyReady();
|
|
if (typeof window !== "undefined" && (window as any).__queryClient) {
|
|
(window as any).__queryClient.invalidateQueries();
|
|
}
|
|
}
|
|
|
|
private resolvePending(token: string | null) {
|
|
const resolvers = this.pendingResolvers.splice(
|
|
0,
|
|
this.pendingResolvers.length,
|
|
);
|
|
for (const resolve of resolvers) {
|
|
resolve(token);
|
|
}
|
|
}
|
|
|
|
private notifyReady() {
|
|
const callbacks = this.readyCallbacks.splice(0, this.readyCallbacks.length);
|
|
for (const callback of callbacks) {
|
|
callback();
|
|
}
|
|
}
|
|
|
|
private init() {
|
|
if (typeof window === "undefined") {
|
|
return;
|
|
}
|
|
|
|
this.setupFlutterResponseListener();
|
|
|
|
if (this.syncFromStorage()) {
|
|
this.markReady(this.token);
|
|
return;
|
|
}
|
|
|
|
this.isReady = true;
|
|
this.notifyReady();
|
|
}
|
|
|
|
public onReady(callback: () => void) {
|
|
if (this.isReady) {
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
this.readyCallbacks.push(callback);
|
|
}
|
|
|
|
public async ensureToken() {
|
|
if (typeof window === "undefined") {
|
|
return null;
|
|
}
|
|
|
|
if (this.syncFromStorage()) {
|
|
return this.token === "NO_TOKEN" ? null : this.token;
|
|
}
|
|
|
|
this.setupFlutterResponseListener();
|
|
|
|
return await new Promise<string | null>((resolve) => {
|
|
this.pendingResolvers.push(resolve);
|
|
if (!this.requestLogin()) {
|
|
this.resolvePending(null);
|
|
}
|
|
});
|
|
}
|
|
|
|
public getToken(): string | null {
|
|
if (this.token === "NO_TOKEN") {
|
|
return null;
|
|
}
|
|
|
|
let sessionToken: string | null = null;
|
|
if (typeof window !== "undefined") {
|
|
try {
|
|
sessionToken = sessionStorage.getItem(TOKEN_COOKIE_NAME);
|
|
} catch (e) {
|
|
console.warn("sessionStorage is not accessible:", e);
|
|
}
|
|
}
|
|
|
|
const cookieToken =
|
|
getClientCookie(TOKEN_COOKIE_NAME) ??
|
|
getClientCookie("habib_token") ??
|
|
sessionToken;
|
|
|
|
const effectiveCookie =
|
|
cookieToken && cookieToken !== "NO_TOKEN" && cookieToken.trim() !== ""
|
|
? cookieToken
|
|
: null;
|
|
|
|
if (this.token && this.token !== "NO_TOKEN" && this.token.trim() !== "") {
|
|
return this.token;
|
|
}
|
|
|
|
if (effectiveCookie) {
|
|
return effectiveCookie;
|
|
}
|
|
|
|
const isDevelopment = process.env.NODE_ENV !== "production";
|
|
if (
|
|
isDevelopment &&
|
|
process.env.NEXT_PUBLIC_DEFAULT_TOKEN &&
|
|
process.env.NEXT_PUBLIC_DEFAULT_TOKEN !== "NO_TOKEN"
|
|
) {
|
|
return process.env.NEXT_PUBLIC_DEFAULT_TOKEN;
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
public getCoins(): number {
|
|
return this.coins;
|
|
}
|
|
|
|
public isAuthenticated(): boolean {
|
|
const token = this.getToken();
|
|
return !!token && token !== "NO_TOKEN" && token.trim() !== "";
|
|
}
|
|
}
|
|
|
|
export const authBridge = new AuthBridge();
|