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.
322 lines
13 KiB
322 lines
13 KiB
import type { Metadata, Viewport } from "next";
|
|
import { Amiri } from "next/font/google";
|
|
import localFont from "next/font/local";
|
|
import { headers } from "next/headers";
|
|
import Script from "next/script";
|
|
import Providers from "./providers";
|
|
import "./globals.css";
|
|
import DevClickToComponent from "@/components/Componentes/dev-click-to-component";
|
|
import TokenSwitcher from "@/components/Componentes/token-switcher";
|
|
import {
|
|
defaultLocale,
|
|
isLocale,
|
|
localeDirections,
|
|
} from "@/translations/config";
|
|
|
|
const faminela = localFont({
|
|
src: "../../public/fonts/Faminela/Faminela.otf",
|
|
variable: "--font-faminela-local",
|
|
display: "swap",
|
|
preload: false,
|
|
fallback: ["Arial", "sans-serif"],
|
|
});
|
|
|
|
// Amiri has no UI consumer above the fold (arabic fonts are mapped to Segoe
|
|
// UI in globals.css), so preloading it only competes with critical resources
|
|
// on a cold start. `display: swap` lets it load lazily if a consumer appears.
|
|
const amiri = Amiri({
|
|
weight: ["400", "700"],
|
|
subsets: ["arabic"],
|
|
variable: "--font-amiri",
|
|
display: "swap",
|
|
preload: false,
|
|
fallback: ["Arial", "sans-serif"],
|
|
});
|
|
|
|
const isDevelopment = process.env.NODE_ENV !== "production";
|
|
|
|
export const metadata: Metadata = {
|
|
title: "Habib Marriage",
|
|
description: "Islamic Marriage Platform",
|
|
};
|
|
|
|
export const viewport: Viewport = {
|
|
width: "device-width",
|
|
initialScale: 1,
|
|
maximumScale: 1,
|
|
userScalable: false,
|
|
viewportFit: "cover",
|
|
themeColor: "#ffffff",
|
|
};
|
|
|
|
export default async function RootLayout({
|
|
children,
|
|
}: Readonly<{
|
|
children: React.ReactNode;
|
|
}>) {
|
|
const requestHeaders = await headers();
|
|
const headerLocale = requestHeaders.get("x-habib-locale");
|
|
const locale =
|
|
headerLocale && isLocale(headerLocale) ? headerLocale : defaultLocale;
|
|
|
|
return (
|
|
<html lang={locale} dir={localeDirections[locale] ?? "ltr"}>
|
|
<head>
|
|
<link rel="preconnect" href="https://habibapp.com" />
|
|
<link rel="dns-prefetch" href="https://habibapp.com" />
|
|
<Script
|
|
id="flutter-and-cookies-setup"
|
|
strategy="beforeInteractive"
|
|
// biome-ignore lint/security/noDangerouslySetInnerHtml: Static document-start bridge code must run before hydration.
|
|
dangerouslySetInnerHTML={{
|
|
__html: `
|
|
if (typeof window !== 'undefined') {
|
|
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';
|
|
var HABIB_ENTRY_PATH_COOKIE = 'HABIB_MARRIAGE_ENTRY_PATH';
|
|
var HABIB_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
|
|
|
|
function writeCookie(name, value) {
|
|
if (value === undefined || value === null || value === '') return;
|
|
|
|
var secure = window.location.protocol === 'https:';
|
|
var cookie = name + '=' + encodeURIComponent(String(value)) + '; Path=/; Max-Age=' + HABIB_COOKIE_MAX_AGE + '; SameSite=Lax';
|
|
|
|
if (secure) {
|
|
cookie += '; Secure';
|
|
}
|
|
|
|
document.cookie = cookie;
|
|
}
|
|
|
|
function clearCookie(name) {
|
|
var secure = window.location.protocol === 'https:';
|
|
var cookie = name + '=; Path=/; Max-Age=0; SameSite=Lax';
|
|
|
|
if (secure) {
|
|
cookie += '; Secure';
|
|
}
|
|
|
|
document.cookie = cookie;
|
|
}
|
|
|
|
function readCookie(name) {
|
|
var cookies = document.cookie ? document.cookie.split('; ') : [];
|
|
|
|
for (var i = 0; i < cookies.length; i += 1) {
|
|
var parts = cookies[i].split('=');
|
|
if (parts[0] === name) {
|
|
return decodeURIComponent(parts.slice(1).join('='));
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
if (!Object.getOwnPropertyDescriptor(window, 'HABIB_TOKEN')) {
|
|
Object.defineProperty(window, 'HABIB_TOKEN', {
|
|
configurable: true,
|
|
set: function(value) {
|
|
var previousToken = readCookie(HABIB_TOKEN_COOKIE) || readCookie('habib_token');
|
|
var nextToken = value === undefined || value === null ? '' : String(value).trim();
|
|
var hasToken = nextToken !== '' && nextToken !== 'NO_TOKEN';
|
|
|
|
this._habib_token = hasToken ? nextToken : undefined;
|
|
if (hasToken) {
|
|
if (!previousToken || previousToken !== nextToken) {
|
|
clearCookie(HABIB_ENTRY_PATH_COOKIE);
|
|
}
|
|
writeCookie(HABIB_TOKEN_COOKIE, nextToken);
|
|
try {
|
|
sessionStorage.setItem(HABIB_TOKEN_COOKIE, nextToken);
|
|
} catch (e) {}
|
|
} else {
|
|
clearCookie(HABIB_TOKEN_COOKIE);
|
|
clearCookie('habib_token');
|
|
clearCookie(HABIB_ENTRY_PATH_COOKIE);
|
|
try {
|
|
sessionStorage.removeItem(HABIB_TOKEN_COOKIE);
|
|
} catch (e) {}
|
|
}
|
|
window.dispatchEvent(new Event('habib:auth-token-changed'));
|
|
},
|
|
get: function() {
|
|
var ssVal;
|
|
try {
|
|
ssVal = sessionStorage.getItem(HABIB_TOKEN_COOKIE);
|
|
} catch (e) {}
|
|
return this._habib_token || readCookie(HABIB_TOKEN_COOKIE) || ssVal || undefined;
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!Object.getOwnPropertyDescriptor(window, 'HABIB_COINS')) {
|
|
Object.defineProperty(window, 'HABIB_COINS', {
|
|
configurable: true,
|
|
set: function(value) {
|
|
this._habib_coins = value;
|
|
if (value !== undefined && value !== null && value !== '') {
|
|
writeCookie(HABIB_COINS_COOKIE, value);
|
|
try {
|
|
sessionStorage.setItem(HABIB_COINS_COOKIE, String(value));
|
|
} catch (e) {}
|
|
}
|
|
},
|
|
get: function() {
|
|
var cookieValue = readCookie(HABIB_COINS_COOKIE);
|
|
var ssVal;
|
|
try {
|
|
ssVal = sessionStorage.getItem(HABIB_COINS_COOKIE);
|
|
} catch (e) {}
|
|
return this._habib_coins || parseInt(cookieValue || ssVal || '0');
|
|
}
|
|
});
|
|
}
|
|
|
|
// Performance monitoring
|
|
window.addEventListener('load', function() {
|
|
try {
|
|
var perfData = performance.getEntriesByType('navigation')[0];
|
|
if (perfData) {
|
|
console.log('⏱️ Load time:', Math.round(perfData.loadEventEnd - perfData.fetchStart), 'ms');
|
|
}
|
|
} catch (e) {}
|
|
});
|
|
}
|
|
`,
|
|
}}
|
|
/>
|
|
<Script
|
|
id="habib-web-bootstrap"
|
|
strategy="beforeInteractive"
|
|
// biome-ignore lint/security/noDangerouslySetInnerHtml: Static bootstrap code must receive Flutter config before the app is visible.
|
|
dangerouslySetInnerHTML={{
|
|
__html: `
|
|
(function() {
|
|
if (typeof window === 'undefined') return;
|
|
var root = document.documentElement;
|
|
var configApplied = false;
|
|
|
|
function apply(config) {
|
|
if (!config) return;
|
|
configApplied = true;
|
|
window.__HABIB_BOOTSTRAP__ = config;
|
|
var safe = config.safeArea || {};
|
|
root.style.setProperty('--safe-top', (Number(safe.top) || 0) + 'px');
|
|
root.style.setProperty('--safe-bottom', (Number(safe.bottom) || 0) + 'px');
|
|
root.style.setProperty('--safe-left', (Number(safe.left) || 0) + 'px');
|
|
root.style.setProperty('--safe-right', (Number(safe.right) || 0) + 'px');
|
|
var locale = config.locale || {};
|
|
var code = locale.languageCode || locale.language_code;
|
|
if (code) {
|
|
var secure = window.location.protocol === 'https:' ? '; Secure' : '';
|
|
document.cookie = 'HABIB_LANGUAGE=' + encodeURIComponent(String(code)) + '; Path=/; Max-Age=31536000; SameSite=Lax' + secure;
|
|
document.cookie = 'habib_language=' + encodeURIComponent(String(code)) + '; Path=/; Max-Age=31536000; SameSite=Lax' + secure;
|
|
root.lang = String(code);
|
|
root.dir = (locale.isRTL || locale.isRtl || locale.is_rtl) ? 'rtl' : 'ltr';
|
|
var segments = window.location.pathname.split('/');
|
|
var currentCode = segments[1];
|
|
if (currentCode && currentCode !== String(code)) {
|
|
root.dataset.webBootstrap = 'pending';
|
|
segments[1] = String(code);
|
|
window.location.replace(segments.join('/') + window.location.search + window.location.hash);
|
|
return;
|
|
}
|
|
}
|
|
root.dataset.webBootstrap = 'ready';
|
|
window.dispatchEvent(new CustomEvent('habib:bootstrap-ready', { detail: config }));
|
|
}
|
|
|
|
// Document-start injection: if Flutter placed the config on
|
|
// window before this script ran, apply it immediately so the
|
|
// pre-config frame never renders at all.
|
|
if (window.__HABIB_BOOTSTRAP__) {
|
|
apply(window.__HABIB_BOOTSTRAP__);
|
|
}
|
|
|
|
window.addFlutterResponseListener(function(event) {
|
|
var action = String(event && event.action || '').toLowerCase();
|
|
if (event && event.success !== false && action === 'initial_config') {
|
|
apply(event.data || event.payload);
|
|
}
|
|
});
|
|
|
|
// This script lives in <head>, so everything above runs before
|
|
// the body renders. Hide the shell before the first paint
|
|
// inside the Habib WebView so the untuned frame (wrong
|
|
// safe-area / locale) never becomes visible. Plain browsers
|
|
// (no HabibApp bridge) are never hidden.
|
|
var isWebView = !!(window.HabibApp && window.HabibApp.postMessage);
|
|
if (isWebView && !configApplied) {
|
|
root.dataset.webBootstrap = 'pending';
|
|
}
|
|
|
|
function announce() {
|
|
if (window.__habibWebReadySent || !window.HabibApp || !window.HabibApp.postMessage) return false;
|
|
window.__habibWebReadySent = true;
|
|
if (!configApplied) {
|
|
root.dataset.webBootstrap = 'pending';
|
|
}
|
|
window.HabibApp.postMessage(JSON.stringify({ action: 'web_ready' }));
|
|
return true;
|
|
}
|
|
|
|
function tryAnnounce() {
|
|
if (!announce()) return false;
|
|
// Watchdog: if the bridge never delivers initial_config
|
|
// (wedged WebView / very old client), release the shell so
|
|
// the app is never left invisible.
|
|
setTimeout(function() {
|
|
if (root.dataset.webBootstrap === 'pending') {
|
|
root.dataset.webBootstrap = 'ready';
|
|
}
|
|
}, 3000);
|
|
return true;
|
|
}
|
|
|
|
if (!tryAnnounce()) {
|
|
var attempts = 0;
|
|
var timer = setInterval(function() {
|
|
attempts += 1;
|
|
if (tryAnnounce() || attempts >= 40) clearInterval(timer);
|
|
}, 50);
|
|
}
|
|
})();
|
|
`,
|
|
}}
|
|
/>
|
|
</head>
|
|
<body className={`${faminela.variable} ${amiri.variable}`}>
|
|
<Providers>
|
|
{isDevelopment ? <TokenSwitcher /> : null}
|
|
<div className="app-shell">{children}</div>
|
|
</Providers>
|
|
{isDevelopment ? <DevClickToComponent /> : null}
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|