sina_sajjadi 2 months ago
parent
commit
877fb9e436
  1. 30
      next.config.ts
  2. 36
      src/app/api/proxy/route.ts
  3. 61
      src/app/intro/page.tsx
  4. 116
      src/app/layout.tsx
  5. 16
      src/app/loading.tsx
  6. 2
      src/app/providers.tsx
  7. 3
      src/app/questions-list/page.tsx
  8. 3
      src/components/questions/required-steps-card.tsx
  9. 25
      src/components/ui/report-actions-sheet.tsx
  10. 218
      src/lib/auth-bridge.ts
  11. 65
      src/lib/cookies.ts
  12. 23
      src/lib/http.ts
  13. 27
      src/lib/performance.ts
  14. 22
      src/types/window.d.ts

30
next.config.ts

@ -2,7 +2,13 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = { const nextConfig: NextConfig = {
output: 'standalone', output: 'standalone',
// Compression
compress: true,
// Image optimization
images: { images: {
formats: ['image/avif', 'image/webp'],
remotePatterns: [ remotePatterns: [
{ {
protocol: "https", protocol: "https",
@ -10,6 +16,30 @@ const nextConfig: NextConfig = {
}, },
], ],
}, },
// Headers for caching and preload
async headers() {
return [
{
source: '/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=3600, stale-while-revalidate=86400',
},
],
},
{
source: '/fonts/:path*',
headers: [
{
key: 'Cache-Control',
value: 'public, max-age=31536000, immutable',
},
],
},
];
},
}; };
export default nextConfig; export default nextConfig;

36
src/app/api/proxy/route.ts

@ -9,6 +9,8 @@ const REQUEST_HEADERS_TO_FORWARD = [
"accept", "accept",
"accept-language", "accept-language",
"authorization", "authorization",
"cookie",
"token",
"content-type", "content-type",
"x-csrf-token", "x-csrf-token",
"x-csrftoken", "x-csrftoken",
@ -94,20 +96,48 @@ function getTargetUrl(request: NextRequest) {
return targetUrl; return targetUrl;
} }
function getCookieValue(cookieHeader: string, name: string) {
const escapedName = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
const pattern = new RegExp(`(?:^|;\\s*)${escapedName}=([^;]*)`);
const match = cookieHeader.match(pattern);
return match ? decodeURIComponent(match[1]) : null;
}
function getRequestHeaders(request: NextRequest, targetUrl: URL) { function getRequestHeaders(request: NextRequest, targetUrl: URL) {
const headers = new Headers(); const headers = new Headers();
const authKey = process.env.NEXT_PUBLIC_AUTH_KEY; const authKey = process.env.NEXT_PUBLIC_AUTH_KEY;
const cookieHeader = request.headers.get("cookie") ?? "";
for (const header of REQUEST_HEADERS_TO_FORWARD) { for (const header of REQUEST_HEADERS_TO_FORWARD) {
const value = request.headers.get(header); const value = request.headers.get(header);
if (value) { if (value) {
headers.set(header, value);
if (header === "token") {
// Convert token header to Authorization
headers.set("authorization", `Token ${value}`);
} else {
headers.set(header, value);
}
}
}
// Prefer a cookie-based token when the browser session is anonymous.
if (!headers.has("authorization")) {
const cookieToken =
getCookieValue(cookieHeader, "HABIB_TOKEN") ??
getCookieValue(cookieHeader, "habib_token") ??
getCookieValue(cookieHeader, "token") ??
getCookieValue(cookieHeader, "auth_token");
if (cookieToken) {
headers.set("authorization", `Token ${cookieToken}`);
} }
} }
if (authKey) {
headers.set("authorization", `token ${authKey}`);
// Override with authKey if set and no authorization header exists.
if (authKey && !headers.has("authorization")) {
headers.set("authorization", `Token ${authKey}`);
} }
headers.set("accept-encoding", "identity"); headers.set("accept-encoding", "identity");

61
src/app/intro/page.tsx

@ -1,30 +1,31 @@
"use client"; "use client";
import Image from "next/image"; import Image from "next/image";
import { useState } from "react";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import Button from "@/components/ui/button"; import Button from "@/components/ui/button";
import NavigationButton from "@/components/ui/navigation-button"; import NavigationButton from "@/components/ui/navigation-button";
import ReportActionsSheet from "@/components/ui/report-actions-sheet"; import ReportActionsSheet from "@/components/ui/report-actions-sheet";
import { authBridge } from "@/lib/auth-bridge";
import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main"; import { useMarriageProfileQuery } from "@/hooks/marriage/use-profile-main";
import type { MarriageProfileResponse } from "@/hooks/marriage/types";
import { localizePath } from "@/i18n/config"; import { localizePath } from "@/i18n/config";
import { useI18n } from "@/i18n/provider"; import { useI18n } from "@/i18n/provider";
export default function Intro() {
const { dictionary: t, locale } = useI18n();
const { data: profile } = useMarriageProfileQuery();
const [isReportSheetOpen, setIsReportSheetOpen] = useState(false);
function getSubmitPath(profile: MarriageProfileResponse | undefined) {
const isInCase = profile?.status === "in_case"; const isInCase = profile?.status === "in_case";
const isFemaleAcceptedFlow = const isFemaleAcceptedFlow =
isInCase && isInCase &&
(profile?.active_case?.status === "payment_pending" || (profile?.active_case?.status === "payment_pending" ||
profile?.active_case?.status === "female_accepted" || profile?.active_case?.status === "female_accepted" ||
profile?.active_case?.status === "payment_done"); profile?.active_case?.status === "payment_done");
const submitPath = isFemaleAcceptedFlow
return isFemaleAcceptedFlow
? "/request-accepted" ? "/request-accepted"
: isInCase && profile?.active_case?.status === "male_accepted" : isInCase && profile?.active_case?.status === "male_accepted"
? "/request-sent" ? "/request-sent"
: profile?.status === "pending_onboarding" : profile?.status === "pending_onboarding"
? "/rules"
? "/terms"
: profile?.status === "pending_info" : profile?.status === "pending_info"
? "/questions-list" ? "/questions-list"
: profile?.status === "waiting" : profile?.status === "waiting"
@ -32,7 +33,47 @@ export default function Intro() {
: profile?.status === "in_case" || profile?.status === "matched" : profile?.status === "in_case" || profile?.status === "matched"
? "/new-match" ? "/new-match"
: "/terms"; : "/terms";
const submitHref = localizePath(submitPath, locale);
}
export default function Intro() {
const router = useRouter();
const { dictionary: t, locale } = useI18n();
const { data: profile, refetch } = useMarriageProfileQuery({
retry: false,
});
const [isReportSheetOpen, setIsReportSheetOpen] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
useEffect(() => {
if (!profile) {
return;
}
router.replace(localizePath(getSubmitPath(profile), locale));
}, [locale, profile, router]);
const handleSubmit = async () => {
if (isSubmitting) {
return;
}
setIsSubmitting(true);
try {
if (!authBridge.isAuthenticated()) {
const token = await authBridge.ensureToken();
if (!token) {
return;
}
}
const profileResponse = profile ?? (await refetch()).data;
const nextPath = localizePath(getSubmitPath(profileResponse), locale);
router.push(nextPath);
} finally {
setIsSubmitting(false);
}
};
return ( return (
<div className="pt-7"> <div className="pt-7">
@ -123,7 +164,9 @@ export default function Intro() {
/> />
</div> </div>
<div className="mt-7 pb-5"> <div className="mt-7 pb-5">
<Button href={submitHref}>{t.common.submit}</Button>
<Button onClick={handleSubmit} disabled={isSubmitting}>
{t.common.submit}
</Button>
</div> </div>
</main> </main>
</div> </div>

116
src/app/layout.tsx

@ -1,15 +1,16 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import { Amiri } from "next/font/google"; import { Amiri } from "next/font/google";
import localFont from "next/font/local"; import localFont from "next/font/local";
import Script from "next/script";
import DevClickToComponent from "@/components/dev/dev-click-to-component";
import Providers from "./providers"; import Providers from "./providers";
import "./globals.css"; import "./globals.css";
import DevClickToComponent from "@/components/dev/dev-click-to-component";
const faminela = localFont({ const faminela = localFont({
src: "../../public/fonts/Faminela/Faminela.otf", src: "../../public/fonts/Faminela/Faminela.otf",
variable: "--font-faminela-local", variable: "--font-faminela-local",
display: "swap", display: "swap",
preload: true,
fallback: ["Arial", "sans-serif"],
}); });
const amiri = Amiri({ const amiri = Amiri({
@ -17,11 +18,19 @@ const amiri = Amiri({
subsets: ["arabic"], subsets: ["arabic"],
variable: "--font-amiri", variable: "--font-amiri",
display: "swap", display: "swap",
preload: true,
fallback: ["Arial", "sans-serif"],
}); });
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
title: "Habib Marriage",
description: "Islamic Marriage Platform",
viewport: {
width: "device-width",
initialScale: 1,
maximumScale: 1,
},
themeColor: "#ffffff",
}; };
export default function RootLayout({ export default function RootLayout({
@ -32,13 +41,104 @@ export default function RootLayout({
return ( return (
<html lang="en"> <html lang="en">
<head> <head>
<Script
id="flutter-response-init"
strategy="beforeInteractive"
<link rel="preconnect" href="https://habibapp.com" />
<link rel="dns-prefetch" href="https://habibapp.com" />
<script
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
__html: ` __html: `
if (typeof window !== 'undefined') { 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';
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 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) {
this._habib_token = value;
if (value) {
writeCookie(HABIB_TOKEN_COOKIE, value);
sessionStorage.setItem(HABIB_TOKEN_COOKIE, value);
}
},
get: function() {
return this._habib_token || readCookie(HABIB_TOKEN_COOKIE) || sessionStorage.getItem(HABIB_TOKEN_COOKIE);
}
});
}
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);
sessionStorage.setItem(HABIB_COINS_COOKIE, String(value));
}
},
get: function() {
var cookieValue = readCookie(HABIB_COINS_COOKIE);
return this._habib_coins || parseInt(cookieValue || sessionStorage.getItem(HABIB_COINS_COOKIE) || '0');
}
});
}
// Performance monitoring
window.addEventListener('load', function() {
var perfData = performance.getEntriesByType('navigation')[0];
console.log('⏱️ Load time:', Math.round(perfData.loadEventEnd - perfData.fetchStart), 'ms');
});
} }
`, `,
}} }}

16
src/app/loading.tsx

@ -0,0 +1,16 @@
export default function Loading() {
return (
<div className="min-h-screen bg-white p-4 animate-pulse">
<div className="h-8 bg-gray-200 rounded-lg w-3/4 mb-6"></div>
<div className="space-y-3">
<div className="h-4 bg-gray-200 rounded w-full"></div>
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
<div className="h-4 bg-gray-200 rounded w-4/6"></div>
</div>
<div className="mt-8 space-y-4">
<div className="h-24 bg-gray-200 rounded-lg"></div>
<div className="h-24 bg-gray-200 rounded-lg"></div>
</div>
</div>
);
}

2
src/app/providers.tsx

@ -15,6 +15,8 @@ export default function Providers({ children }: ProvidersProps) {
queries: { queries: {
refetchOnWindowFocus: false, refetchOnWindowFocus: false,
retry: 1, retry: 1,
staleTime: 5 * 60 * 1000, // 5 minutes
gcTime: 10 * 60 * 1000, // 10 minutes (formerly cacheTime)
}, },
}, },
}), }),

3
src/app/questions-list/page.tsx

@ -48,9 +48,8 @@ export default function QuestionsListPage() {
const profileContext = useMemo( const profileContext = useMemo(
() => ({ () => ({
gender: profile?.gender, gender: profile?.gender,
age: profile?.age,
}), }),
[profile?.gender, profile?.age],
[profile?.gender],
); );
const allRequiredSectionsCompleted = useMemo(() => { const allRequiredSectionsCompleted = useMemo(() => {
if (!sections?.length) { if (!sections?.length) {

3
src/components/questions/required-steps-card.tsx

@ -34,9 +34,8 @@ export default function RequiredStepsCard() {
const profileContext = useMemo( const profileContext = useMemo(
() => ({ () => ({
gender: profile?.gender, gender: profile?.gender,
age: profile?.age,
}), }),
[profile?.gender, profile?.age],
[profile?.gender],
); );
const fallbackRequiredSteps: RequiredStep[] = getQuestionListItems(locale) const fallbackRequiredSteps: RequiredStep[] = getQuestionListItems(locale)

25
src/components/ui/report-actions-sheet.tsx

@ -9,19 +9,6 @@ type ReportActionsSheetProps = {
onClose?: () => void; onClose?: () => void;
}; };
declare global {
interface Window {
HabibApp?: {
postMessage: (message: string) => void;
};
onFlutterResponse?: (event: {
action: string;
success: boolean;
data?: { latitude: number; longitude: number };
}) => void;
}
}
export function ReportActionsSheet({ onClose }: ReportActionsSheetProps) { export function ReportActionsSheet({ onClose }: ReportActionsSheetProps) {
const [isVisible, setIsVisible] = useState(true); const [isVisible, setIsVisible] = useState(true);
const [isEntering, setIsEntering] = useState(true); const [isEntering, setIsEntering] = useState(true);
@ -80,23 +67,17 @@ export function ReportActionsSheet({ onClose }: ReportActionsSheetProps) {
}, [isClosing, onClose]); }, [isClosing, onClose]);
useEffect(() => { useEffect(() => {
const handleFlutterResponse = (event: {
action: string;
success: boolean;
data?: { latitude: number; longitude: number };
}) => {
const handleFlutterResponse: NonNullable<Window["onFlutterResponse"]> = (event) => {
if (event.action === "get_location" && event.success && event.data) { if (event.action === "get_location" && event.success && event.data) {
const message = `Location: ${event.data.latitude}, ${event.data.longitude}`; const message = `Location: ${event.data.latitude}, ${event.data.longitude}`;
alert(message); alert(message);
} }
}; };
window.onFlutterResponse = handleFlutterResponse;
const unsubscribe = window.addFlutterResponseListener?.(handleFlutterResponse);
return () => { return () => {
if (window.onFlutterResponse === handleFlutterResponse) {
window.onFlutterResponse = undefined;
}
unsubscribe?.();
}; };
}, []); }, []);

218
src/lib/auth-bridge.ts

@ -0,0 +1,218 @@
import { getClientCookie } from "./cookies";
const TOKEN_COOKIE_NAME = "HABIB_TOKEN";
const COINS_COOKIE_NAME = "HABIB_COINS";
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) {
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;
}
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();
}
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;
}
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) {
return this.token;
}
return (
getClientCookie(TOKEN_COOKIE_NAME) ?? getClientCookie("habib_token")
);
}
public getCoins(): number {
return this.coins;
}
public isAuthenticated(): boolean {
return !!this.getToken();
}
}
export const authBridge = new AuthBridge();

65
src/lib/cookies.ts

@ -0,0 +1,65 @@
const DEFAULT_COOKIE_PATH = "/";
const DEFAULT_COOKIE_MAX_AGE = 60 * 60 * 24 * 30;
function isClient() {
return typeof document !== "undefined";
}
function escapeCookieName(name: string) {
return name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
export function getClientCookie(name: string) {
if (!isClient()) {
return null;
}
const match = document.cookie.match(
new RegExp(`(?:^|; )${escapeCookieName(name)}=([^;]*)`),
);
return match ? decodeURIComponent(match[1]) : null;
}
export function setClientCookie(
name: string,
value: string,
options?: {
maxAge?: number;
path?: string;
sameSite?: "Lax" | "Strict" | "None";
secure?: boolean;
},
) {
if (!isClient()) {
return;
}
const maxAge = options?.maxAge ?? DEFAULT_COOKIE_MAX_AGE;
const path = options?.path ?? DEFAULT_COOKIE_PATH;
const sameSite = options?.sameSite ?? "Lax";
const secure = options?.secure ?? window.location.protocol === "https:";
let cookie = `${name}=${encodeURIComponent(value)}; Path=${path}; Max-Age=${maxAge}; SameSite=${sameSite}`;
if (secure) {
cookie += "; Secure";
}
document.cookie = cookie;
}
export function deleteClientCookie(name: string, path = DEFAULT_COOKIE_PATH) {
if (!isClient()) {
return;
}
const secure = window.location.protocol === "https:";
let cookie = `${name}=; Path=${path}; Max-Age=0; SameSite=Lax`;
if (secure) {
cookie += "; Secure";
}
document.cookie = cookie;
}

23
src/lib/http.ts

@ -1,4 +1,6 @@
import axios, { type InternalAxiosRequestConfig } from "axios"; import axios, { type InternalAxiosRequestConfig } from "axios";
import { authBridge } from "./auth-bridge";
import { getClientCookie } from "./cookies";
const PROXY_PATH_PARAM = "__proxyPath"; const PROXY_PATH_PARAM = "__proxyPath";
const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]); const LOCALHOST_HOSTNAMES = new Set(["localhost", "127.0.0.1", "::1"]);
@ -7,12 +9,12 @@ function isAbsoluteUrl(url: string) {
return /^[a-z][a-z\d+\-.]*:\/\//i.test(url); return /^[a-z][a-z\d+\-.]*:\/\//i.test(url);
} }
function isLocalhost() {
function shouldUseProxy() {
if (typeof window === "undefined") { if (typeof window === "undefined") {
return false; return false;
} }
return LOCALHOST_HOSTNAMES.has(window.location.hostname);
return true;
} }
function withProxyPathParam( function withProxyPathParam(
@ -38,7 +40,7 @@ function withProxyPathParam(
} }
export function getApiRequestUrl(path: string) { export function getApiRequestUrl(path: string) {
if (!isAbsoluteUrl(path) && isLocalhost()) {
if (!isAbsoluteUrl(path) && shouldUseProxy()) {
const searchParams = new URLSearchParams({ const searchParams = new URLSearchParams({
[PROXY_PATH_PARAM]: path, [PROXY_PATH_PARAM]: path,
}); });
@ -50,7 +52,7 @@ export function getApiRequestUrl(path: string) {
} }
export const http = axios.create({ export const http = axios.create({
baseURL: isLocalhost() ? "/api/proxy" : process.env.NEXT_PUBLIC_API_BASE_URL,
baseURL: shouldUseProxy() ? "/api/proxy" : process.env.NEXT_PUBLIC_API_BASE_URL,
headers: { headers: {
Accept: "application/json", Accept: "application/json",
"Content-Type": "application/json", "Content-Type": "application/json",
@ -59,10 +61,21 @@ export const http = axios.create({
}); });
http.interceptors.request.use((config) => { http.interceptors.request.use((config) => {
if (isLocalhost() && config.url && !isAbsoluteUrl(config.url)) {
if (shouldUseProxy() && config.url && !isAbsoluteUrl(config.url)) {
config.params = withProxyPathParam(config.params, config.url); config.params = withProxyPathParam(config.params, config.url);
config.url = ""; config.url = "";
} }
const token = authBridge.getToken() ?? getClientCookie("HABIB_TOKEN");
if (token) {
config.headers.Authorization = `Token ${token}`;
}
return config; return config;
}); });
http.interceptors.response.use(
(response) => response,
(error) => Promise.reject(error)
);

27
src/lib/performance.ts

@ -0,0 +1,27 @@
export function measurePerformance() {
if (typeof window === 'undefined') return;
window.addEventListener('load', () => {
const perfData = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
const metrics = {
dns: perfData.domainLookupEnd - perfData.domainLookupStart,
tcp: perfData.connectEnd - perfData.connectStart,
ttfb: perfData.responseStart - perfData.requestStart,
download: perfData.responseEnd - perfData.responseStart,
domInteractive: perfData.domInteractive - perfData.fetchStart,
domComplete: perfData.domComplete - perfData.fetchStart,
loadComplete: perfData.loadEventEnd - perfData.fetchStart,
};
console.log('⏱️ Performance Metrics:', metrics);
// ارسال به analytics (اختیاری)
if ((window as any).HabibApp) {
(window as any).HabibApp.postMessage(JSON.stringify({
action: 'performance_metrics',
data: metrics,
}));
}
});
}

22
src/types/window.d.ts

@ -0,0 +1,22 @@
declare global {
interface FlutterResponseEvent {
action: string;
success: boolean;
data?: { latitude: number; longitude: number };
}
interface Window {
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;
};
}
}
export {};
Loading…
Cancel
Save