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.
469 lines
22 KiB
469 lines
22 KiB
import type { Metadata, Viewport } from "next";
|
|
import { cookies, headers } from "next/headers";
|
|
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";
|
|
import { getInitialMarriageProfile } from "@/hooks/marriage/use-profile-main";
|
|
import type { MarriageProfileResponse } from "@/hooks/marriage/types";
|
|
|
|
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: "#F5F5F5",
|
|
interactiveWidget: "overlays-content",
|
|
};
|
|
|
|
export default async function RootLayout({
|
|
children,
|
|
}: Readonly<{
|
|
children: React.ReactNode;
|
|
}>) {
|
|
const [cookieStore, requestHeaders] = await Promise.all([
|
|
cookies(),
|
|
headers(),
|
|
]);
|
|
const headerLocale = requestHeaders.get("x-habib-locale");
|
|
const cookieLocale =
|
|
cookieStore.get("HABIB_LANGUAGE")?.value ??
|
|
cookieStore.get("habib_language")?.value;
|
|
const locale =
|
|
headerLocale && isLocale(headerLocale)
|
|
? headerLocale
|
|
: isLocale(cookieLocale)
|
|
? cookieLocale
|
|
: defaultLocale;
|
|
|
|
const rawMarriageCookie =
|
|
cookieStore.get("HABIB_MARRIAGE_DATA")?.value ??
|
|
cookieStore.get("habib_marriage_data")?.value;
|
|
|
|
let initialMarriageJson: string | null = null;
|
|
let initialProfile: MarriageProfileResponse | undefined;
|
|
if (rawMarriageCookie) {
|
|
try {
|
|
const decoded = decodeURIComponent(rawMarriageCookie);
|
|
const parsed = JSON.parse(decoded);
|
|
initialMarriageJson = decoded;
|
|
initialProfile = getInitialMarriageProfile(parsed);
|
|
console.log("🖥️ [SSR Layout] Parsed rawMarriageCookie successfully:", {
|
|
status: initialProfile?.status,
|
|
gender: initialProfile?.gender,
|
|
has_match_summary: Boolean(initialProfile?.match_summary),
|
|
match_summary_id: initialProfile?.match_summary?.id,
|
|
public_info_count: initialProfile?.match_summary?.public_info?.length,
|
|
});
|
|
} catch {
|
|
try {
|
|
const parsed = JSON.parse(rawMarriageCookie);
|
|
initialMarriageJson = rawMarriageCookie;
|
|
initialProfile = getInitialMarriageProfile(parsed);
|
|
console.log("🖥️ [SSR Layout] Parsed rawMarriageCookie (non-decoded) successfully:", {
|
|
status: initialProfile?.status,
|
|
gender: initialProfile?.gender,
|
|
has_match_summary: Boolean(initialProfile?.match_summary),
|
|
match_summary_id: initialProfile?.match_summary?.id,
|
|
public_info_count: initialProfile?.match_summary?.public_info?.length,
|
|
});
|
|
} catch (e) {
|
|
console.warn("🖥️ [SSR Layout] Failed to parse rawMarriageCookie:", e);
|
|
}
|
|
}
|
|
} else {
|
|
console.log("🖥️ [SSR Layout] No rawMarriageCookie found in request cookies");
|
|
}
|
|
|
|
return (
|
|
<html
|
|
lang={locale}
|
|
dir={localeDirections[locale] ?? "ltr"}
|
|
suppressHydrationWarning
|
|
>
|
|
<head>
|
|
<style
|
|
dangerouslySetInnerHTML={{
|
|
__html: `html, body { background-color: #F5F5F5 !important; }`,
|
|
}}
|
|
/>
|
|
<link rel="preconnect" href="https://habibapp.com" />
|
|
<link rel="dns-prefetch" href="https://habibapp.com" />
|
|
<link
|
|
rel="preload"
|
|
href="/fonts/Faminela/Faminela.woff2"
|
|
as="font"
|
|
type="font/woff2"
|
|
crossOrigin="anonymous"
|
|
/>
|
|
<link
|
|
rel="preload"
|
|
href="/assets/fonts/YekanXFaNum-R.woff2"
|
|
as="font"
|
|
type="font/woff2"
|
|
crossOrigin="anonymous"
|
|
/>
|
|
{initialMarriageJson && (
|
|
<script
|
|
id="habib-early-marriage-initial-data"
|
|
dangerouslySetInnerHTML={{
|
|
__html: `window.__HABIB_MARRIAGE_INITIAL_DATA__ = ${initialMarriageJson}; window.HABIB_MARRIAGE = window.__HABIB_MARRIAGE_INITIAL_DATA__;`,
|
|
}}
|
|
/>
|
|
)}
|
|
<script
|
|
id="habib-core-bootstrap"
|
|
dangerouslySetInnerHTML={{
|
|
__html: `
|
|
(function() {
|
|
if (typeof window === 'undefined') return;
|
|
|
|
// 1. Setup Flutter response dispatcher bridge
|
|
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);
|
|
}
|
|
});
|
|
};
|
|
|
|
// 2. Cookie Management & Reactive Global Stores
|
|
var HABIB_TOKEN_COOKIE = 'HABIB_TOKEN';
|
|
var HABIB_COINS_COOKIE = 'HABIB_COINS';
|
|
var HABIB_ENTRY_PATH_COOKIE = 'HABIB_MARRIAGE_ENTRY_PATH';
|
|
var HABIB_MARRIAGE_DATA_COOKIE = 'HABIB_MARRIAGE_DATA';
|
|
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 '';
|
|
}
|
|
|
|
var earlyMarriageCookie = readCookie(HABIB_MARRIAGE_DATA_COOKIE) || readCookie('habib_marriage_data');
|
|
if (earlyMarriageCookie) {
|
|
try {
|
|
var parsedEarlyMarriage = JSON.parse(earlyMarriageCookie);
|
|
if (parsedEarlyMarriage && typeof parsedEarlyMarriage === 'object') {
|
|
window.__HABIB_MARRIAGE_INITIAL_DATA__ = parsedEarlyMarriage;
|
|
window.HABIB_MARRIAGE = parsedEarlyMarriage;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
|
|
// Check URL query parameters for auth token on initial script execution
|
|
try {
|
|
if (window.location && window.location.search) {
|
|
var searchParams = new URLSearchParams(window.location.search);
|
|
var urlTok = searchParams.get('token') || searchParams.get('auth_token') || searchParams.get('habib_token') || searchParams.get('HABIB_TOKEN');
|
|
if (urlTok && urlTok.trim() !== '' && urlTok !== 'NO_TOKEN') {
|
|
window.HABIB_TOKEN = urlTok.trim();
|
|
}
|
|
}
|
|
} catch (e) {}
|
|
|
|
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');
|
|
}
|
|
});
|
|
}
|
|
|
|
// 3. Document-Level Safe Area & Bootstrap
|
|
var root = document.documentElement;
|
|
var configApplied = false;
|
|
|
|
function apply(config) {
|
|
if (!config) return;
|
|
configApplied = true;
|
|
window.__HABIB_BOOTSTRAP__ = config;
|
|
|
|
var configToken =
|
|
config.token ||
|
|
config.auth_token ||
|
|
config.authToken ||
|
|
(config.data && (config.data.token || config.data.auth_token || config.data.authToken)) ||
|
|
(config.payload && (config.payload.token || config.payload.auth_token || config.payload.authToken));
|
|
|
|
if (configToken && typeof configToken === 'string' && configToken.trim() !== '' && configToken !== 'NO_TOKEN') {
|
|
console.log('⚡ [Layout Bootstrap] Applying auth token from Flutter bootstrap config');
|
|
window.HABIB_TOKEN = configToken.trim();
|
|
}
|
|
|
|
var marriageData =
|
|
config.marriage ||
|
|
config.marriageData ||
|
|
config.marriage_data ||
|
|
config.profile ||
|
|
(config.data && (config.data.marriage || config.data.profile || config.data.marriage_data)) ||
|
|
(config.payload && (config.payload.marriage || config.payload.profile || config.payload.marriage_data));
|
|
|
|
if (marriageData) {
|
|
console.log('⚡ [Layout Bootstrap] Applying marriage initial data from Flutter:', marriageData);
|
|
window.__HABIB_MARRIAGE_INITIAL_DATA__ = marriageData;
|
|
window.HABIB_MARRIAGE = marriageData;
|
|
window.dispatchEvent(new CustomEvent('habib:marriage-initial-data', { detail: marriageData }));
|
|
}
|
|
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 knownLocales = ['fa', 'ar', 'en', 'ur', 'tr', 'ru', 'fr', 'es', 'de', 'id', 'bn', 'hi', 'pt', 'ha', 'sw', 'uz', 'az', 'tg', 'ks', 'zh', 'da', 'he', 'gu', 'ul'];
|
|
var segments = window.location.pathname.split('/');
|
|
var currentCode = segments[1];
|
|
if (currentCode && knownLocales.indexOf(currentCode) !== -1 && 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 }));
|
|
}
|
|
|
|
// Global Error Forwarding to Flutter Native Log Channel
|
|
function forwardErrorToFlutter(type, err) {
|
|
try {
|
|
var message = '[WEB_ERROR] ' + type + ': ' + (err && (err.stack || err.message || String(err)) || 'Unknown error');
|
|
console.error(message);
|
|
if (window.HabibApp && window.HabibApp.postMessage) {
|
|
window.HabibApp.postMessage(JSON.stringify({
|
|
action: 'client_log',
|
|
message: message
|
|
}));
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
window.addEventListener('error', function(event) {
|
|
forwardErrorToFlutter('Runtime Error', event.error || event.message);
|
|
});
|
|
|
|
window.addEventListener('unhandledrejection', function(event) {
|
|
forwardErrorToFlutter('Unhandled Rejection', event.reason);
|
|
});
|
|
|
|
if (window.__HABIB_BOOTSTRAP__) {
|
|
apply(window.__HABIB_BOOTSTRAP__);
|
|
}
|
|
|
|
window.addFlutterResponseListener(function(event) {
|
|
var action = String(event && (event.action || event.type) || '').toLowerCase();
|
|
if (event && event.success !== false && (action === 'initial_config' || action === 'initialconfig')) {
|
|
apply(event.data || event.payload || event);
|
|
}
|
|
});
|
|
|
|
var isWebView = !!(window.HabibApp && window.HabibApp.postMessage);
|
|
|
|
// 4. Queued web_ready Delivery Protocol
|
|
//
|
|
// Pages call window.__announceHabibWebReady() when their
|
|
// destination UI is ready. This only sets a "readyRequested"
|
|
// flag and attempts delivery. If HabibApp is not yet injected,
|
|
// the request stays pending and is retried when HabibApp
|
|
// appears. The 3-second watchdog requests readiness as a
|
|
// safety net but does NOT cancel pending delivery attempts.
|
|
//
|
|
// Contract:
|
|
// readyRequested = a destination page says "my UI is ready"
|
|
// __habibWebReadySent = postMessage was actually executed
|
|
//
|
|
var readyRequested = false;
|
|
|
|
function deliverReadyIfPossible() {
|
|
if (window.__habibWebReadySent) return true;
|
|
if (!readyRequested) return false;
|
|
if (!window.HabibApp || !window.HabibApp.postMessage) return false;
|
|
|
|
window.__habibWebReadySent = true;
|
|
root.dataset.webBootstrap = 'ready';
|
|
window.HabibApp.postMessage(JSON.stringify({ action: 'web_ready' }));
|
|
return true;
|
|
}
|
|
|
|
function requestWebReady() {
|
|
readyRequested = true;
|
|
deliverReadyIfPossible();
|
|
}
|
|
|
|
window.__announceHabibWebReady = requestWebReady;
|
|
|
|
// Safety fallback: auto-request readiness after 3s if no page
|
|
// called __announceHabibWebReady(). This ensures the cover is
|
|
// never stuck forever, even for pages that forgot the call.
|
|
var _habibAutoAnnounceTimer = setTimeout(function() {
|
|
requestWebReady();
|
|
}, 3000);
|
|
|
|
// Poll for HabibApp if it wasn't available at parse time.
|
|
// When HabibApp appears, attempt delivery of any pending
|
|
// ready request. This closes the race where a page requests
|
|
// readiness before the bridge is injected.
|
|
if (!window.HabibApp || !window.HabibApp.postMessage) {
|
|
var attempts = 0;
|
|
var bridgePollTimer = setInterval(function() {
|
|
attempts += 1;
|
|
if (window.HabibApp && window.HabibApp.postMessage) {
|
|
clearInterval(bridgePollTimer);
|
|
deliverReadyIfPossible();
|
|
} else if (attempts >= 100) {
|
|
clearInterval(bridgePollTimer);
|
|
}
|
|
}, 50);
|
|
}
|
|
|
|
// 5. Document Boot ID — reload detection instrumentation.
|
|
// If this ID changes across a back navigation, a hard reload
|
|
// or WebView recreation happened (not SPA navigation).
|
|
window.__habibDocumentBootId = Date.now().toString(36) + Math.random().toString(36).slice(2, 6);
|
|
|
|
// 6. Hardware Back contract stub.
|
|
// Flutter should call: window.__habibHandleHardwareBack()
|
|
// The real handler stack is set up by React (use-hardware-back-handler.ts).
|
|
// This stub ensures the function exists even before React hydrates.
|
|
if (!window.__habibHandleHardwareBack) {
|
|
window.__habibHandleHardwareBack = function() {
|
|
return Promise.resolve({ handled: false });
|
|
};
|
|
}
|
|
if (!window.__habibHandleHardwareBackSync) {
|
|
window.__habibHandleHardwareBackSync = function() {
|
|
return false;
|
|
};
|
|
}
|
|
|
|
// 7. Deep Lifecycle & Focus Debug Instrumentation
|
|
function logLifecycle(name, detail) {
|
|
try {
|
|
var bodyBg = window.getComputedStyle ? window.getComputedStyle(document.body).backgroundColor : 'unknown';
|
|
var htmlBg = window.getComputedStyle ? window.getComputedStyle(document.documentElement).backgroundColor : 'unknown';
|
|
var msg = '[WEB_DEEP_LIFECYCLE] [' + Date.now() + '] Event: ' + name + ' | visibility: ' + document.visibilityState + ' | hasFocus: ' + document.hasFocus() + ' | bodyBg: ' + bodyBg + ' | htmlBg: ' + htmlBg + (detail ? ' | ' + JSON.stringify(detail) : '');
|
|
console.log(msg);
|
|
if (window.HabibApp && window.HabibApp.postMessage) {
|
|
window.HabibApp.postMessage(JSON.stringify({ action: 'client_log', message: msg }));
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
['visibilitychange', 'focus', 'blur', 'pageshow', 'pagehide', 'freeze', 'resume'].forEach(function(evt) {
|
|
window.addEventListener(evt, function(e) {
|
|
logLifecycle(evt, { type: e.type, persisted: e.persisted });
|
|
}, true);
|
|
});
|
|
|
|
document.addEventListener('visibilitychange', function() {
|
|
logLifecycle('document.visibilitychange', { visibilityState: document.visibilityState });
|
|
});
|
|
})();
|
|
`,
|
|
}}
|
|
/>
|
|
</head>
|
|
<body suppressHydrationWarning>
|
|
<Providers initialProfile={initialProfile}>
|
|
{isDevelopment ? <TokenSwitcher /> : null}
|
|
<div className="app-shell">{children}</div>
|
|
</Providers>
|
|
{isDevelopment ? <DevClickToComponent /> : null}
|
|
</body>
|
|
</html>
|
|
);
|
|
}
|