Browse Source

chore(staging): auto-merge origin/master into staging

staging
Ali Alavi 2 weeks ago
parent
commit
4db33dd3f9
  1. 3
      package.json
  2. 14
      src/app/layout.tsx
  3. 255
      src/data/country-calling-codes.ts
  4. 37
      src/lib/auth-bridge.test.ts
  5. 16
      src/lib/geo-region.ts
  6. 119
      src/lib/marriage-cookie.test.ts

3
package.json

@ -16,8 +16,7 @@
"google-libphonenumber": "^3.2.44",
"next": "^16.2.10",
"react": "19.2.4",
"react-dom": "19.2.4",
"react-icons": "^5.6.0"
"react-dom": "19.2.4"
},
"devDependencies": {
"@babel/core": "^8.0.1",

14
src/app/layout.tsx

@ -102,6 +102,20 @@ export default async function RootLayout({
/>
<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"

255
src/data/country-calling-codes.ts

@ -0,0 +1,255 @@
/**
* ITU-T E.164 country calling codes mapped by ISO 3166-1 alpha-2 country code.
* Ultra-lightweight (~3KB) replacement for importing the entire google-libphonenumber
* library into the critical bootstrap and I18n bundles.
*/
export const COUNTRY_CALLING_CODES: Readonly<Record<string, string>> = {
AF: "93",
AL: "355",
DZ: "213",
AS: "1",
AD: "376",
AO: "244",
AI: "1",
AG: "1",
AR: "54",
AM: "374",
AW: "297",
AU: "61",
AT: "43",
AZ: "994",
BS: "1",
BH: "973",
BD: "880",
BB: "1",
BY: "375",
BE: "32",
BZ: "501",
BJ: "229",
BM: "1",
BT: "975",
BO: "591",
BA: "387",
BW: "267",
BR: "55",
IO: "246",
VG: "1",
BN: "673",
BG: "359",
BF: "226",
BI: "257",
KH: "855",
CM: "237",
CA: "1",
CV: "238",
KY: "1",
CF: "236",
TD: "235",
CL: "56",
CN: "86",
CX: "61",
CC: "61",
CO: "57",
KM: "269",
CK: "682",
CR: "506",
HR: "385",
CU: "53",
CW: "599",
CY: "357",
CZ: "420",
CD: "243",
DK: "45",
DJ: "253",
DM: "1",
DO: "1",
EC: "593",
EG: "20",
SV: "503",
GQ: "240",
ER: "291",
EE: "372",
SZ: "268",
ET: "251",
FK: "500",
FO: "298",
FJ: "679",
FI: "358",
FR: "33",
GF: "594",
PF: "689",
GA: "241",
GM: "220",
GE: "995",
DE: "49",
GH: "233",
GI: "350",
GR: "30",
GL: "299",
GD: "1",
GP: "590",
GU: "1",
GT: "502",
GG: "44",
GN: "224",
GW: "245",
GY: "592",
HT: "509",
HN: "504",
HK: "852",
HU: "36",
IS: "354",
IN: "91",
ID: "62",
IR: "98",
IQ: "964",
IE: "353",
IM: "44",
IL: "972",
IT: "39",
CI: "225",
JM: "1",
JP: "81",
JE: "44",
JO: "962",
KZ: "7",
KE: "254",
KI: "686",
XK: "383",
KW: "965",
KG: "996",
LA: "856",
LV: "371",
LB: "961",
LS: "266",
LR: "231",
LY: "218",
LI: "423",
LT: "370",
LU: "352",
MO: "853",
MG: "261",
MW: "265",
MY: "60",
MV: "960",
ML: "223",
MT: "356",
MH: "692",
MQ: "596",
MR: "222",
MU: "230",
YT: "262",
MX: "52",
FM: "691",
MD: "373",
MC: "377",
MN: "976",
ME: "382",
MS: "1",
MA: "212",
MZ: "258",
MM: "95",
NA: "264",
NR: "674",
NP: "977",
NL: "31",
NC: "687",
NZ: "64",
NI: "505",
NE: "227",
NG: "234",
NU: "683",
NF: "672",
KP: "850",
MK: "389",
MP: "1",
NO: "47",
OM: "968",
PK: "92",
PW: "680",
PS: "970",
PA: "507",
PG: "675",
PY: "595",
PE: "51",
PH: "63",
PL: "48",
PT: "351",
PR: "1",
QA: "974",
CG: "242",
RE: "262",
RO: "40",
RU: "7",
RW: "250",
BL: "590",
SH: "290",
KN: "1",
LC: "1",
MF: "590",
PM: "508",
VC: "1",
WS: "685",
SM: "378",
ST: "239",
SA: "966",
SN: "221",
RS: "381",
SC: "248",
SL: "232",
SG: "65",
SX: "1",
SK: "421",
SI: "386",
SB: "677",
SO: "252",
ZA: "27",
KR: "82",
SS: "211",
ES: "34",
LK: "94",
SD: "249",
SR: "597",
SE: "46",
CH: "41",
SY: "963",
TW: "886",
TJ: "992",
TZ: "255",
TH: "66",
TL: "670",
TG: "228",
TK: "690",
TO: "676",
TT: "1",
TN: "216",
TR: "90",
TM: "993",
TC: "1",
TV: "688",
UG: "256",
UA: "380",
AE: "971",
GB: "44",
US: "1",
UY: "598",
UZ: "998",
VU: "678",
VA: "379",
VE: "58",
VN: "84",
VI: "1",
WF: "681",
EH: "212",
YE: "967",
ZM: "260",
ZW: "263",
};
export function getCallingCodeForCountry(
isoCode?: string | null,
): string | undefined {
if (!isoCode) return undefined;
const upper = isoCode.trim().toUpperCase();
return COUNTRY_CALLING_CODES[upper];
}

37
src/lib/auth-bridge.test.ts

@ -88,4 +88,41 @@ describe("authBridge", () => {
expect(authBridge.getMarriageData()).toEqual(marriagePayload);
});
it("extracts and parses URI-encoded HABIB_MARRIAGE_DATA cookie with Persian text", async () => {
const marriagePayload = {
id: 999,
status: "match_found",
city: "اصفهان",
name: "سارا",
};
mocks.getClientCookie.mockImplementation((name: string) => {
if (name === "HABIB_MARRIAGE_DATA") {
return encodeURIComponent(JSON.stringify(marriagePayload));
}
return null;
});
const { authBridge } = await import("./auth-bridge");
expect(authBridge.getMarriageData()).toEqual(marriagePayload);
});
it("falls back to parsing unencoded HABIB_MARRIAGE_DATA cookie", async () => {
const marriagePayload = {
id: 888,
status: "intro",
city: "تبریز",
};
mocks.getClientCookie.mockImplementation((name: string) => {
if (name === "HABIB_MARRIAGE_DATA") {
return JSON.stringify(marriagePayload);
}
return null;
});
const { authBridge } = await import("./auth-bridge");
expect(authBridge.getMarriageData()).toEqual(marriagePayload);
});
});

16
src/lib/geo-region.ts

@ -1,7 +1,7 @@
"use client";
import { PhoneNumberUtil } from "google-libphonenumber";
import { resolveCountryName } from "@/data/countries";
import { getCallingCodeForCountry } from "@/data/country-calling-codes";
export type UserGeoRegion = {
ip?: string;
@ -13,8 +13,6 @@ export type UserGeoRegion = {
longitude?: number;
};
const phoneUtil = PhoneNumberUtil.getInstance();
const STORAGE_KEY = "user_geo_region";
const PHONE_STORAGE_KEY = "geoIPPhoneCode";
@ -84,14 +82,10 @@ export function setStoredUserGeoRegion(region: UserGeoRegion) {
function resolvePhoneCodeFromCountryCode(isoCode?: string): string | undefined {
if (!isoCode) return undefined;
try {
const callingCode = phoneUtil.getCountryCodeForRegion(
isoCode.toUpperCase(),
);
if (callingCode) {
return `+${callingCode}`;
}
} catch {}
const callingCode = getCallingCodeForCountry(isoCode);
if (callingCode) {
return `+${callingCode}`;
}
return undefined;
}

119
src/lib/marriage-cookie.test.ts

@ -0,0 +1,119 @@
import { describe, expect, it } from "vitest";
import { getInitialMarriageProfile } from "@/hooks/marriage/use-profile-main";
import type { MarriageProfileResponse } from "@/hooks/marriage/types";
/**
* Robust parser replicating the layout.tsx and auth-bridge.ts cookie resolution strategy.
*/
export function parseMarriageCookie(
rawCookie: string | null | undefined,
): { parsedJson: any; profile?: MarriageProfileResponse } | null {
if (!rawCookie || !rawCookie.trim()) return null;
const trimmed = rawCookie.trim();
// Strategy 1: Try decoding URI component first (Standard RFC-6265 percent-encoded cookie from Flutter)
try {
const decoded = decodeURIComponent(trimmed);
const parsed = JSON.parse(decoded);
return {
parsedJson: parsed,
profile: getInitialMarriageProfile(parsed),
};
} catch {
// Strategy 2: Fallback for legacy raw unencoded JSON cookies
try {
const parsed = JSON.parse(trimmed);
return {
parsedJson: parsed,
profile: getInitialMarriageProfile(parsed),
};
} catch {
return null;
}
}
}
describe("HABIB_MARRIAGE_DATA Cookie Encoding & Decoding Suite", () => {
it("successfully parses standard URL-encoded JSON with Persian characters (Uri.encodeComponent)", () => {
const originalData = {
id: 101,
status: "match_found",
gender: "male",
city: "تهران",
bio: "مهندس کامپیوتر، علاقه‌مند به کتاب‌خوانی و طبیعت‌گردی",
match_summary: {
id: 202,
gender: "female",
public_info: [
{ label: "شهر", value: "اصفهان" },
{ label: "تحصیلات", value: "کارشناسی ارشد" },
],
},
};
// Simulate Flutter: Uri.encodeComponent(json.encode(marriageData))
const encodedCookie = encodeURIComponent(JSON.stringify(originalData));
// Verify it's purely US-ASCII
expect(/^[\x00-\x7F]*$/.test(encodedCookie)).toBe(true);
const result = parseMarriageCookie(encodedCookie);
expect(result).not.toBeNull();
expect(result?.parsedJson.city).toBe("تهران");
expect(result?.parsedJson.bio).toBe("مهندس کامپیوتر، علاقه‌مند به کتاب‌خوانی و طبیعت‌گردی");
expect(result?.parsedJson.match_summary.public_info[0].value).toBe("اصفهان");
expect(result?.profile?.status).toBe("match_found");
});
it("successfully parses legacy raw unencoded JSON with Persian characters", () => {
const legacyRawCookie = JSON.stringify({
id: 102,
status: "active",
gender: "female",
city: "مشهد",
interests: ["زیارت", "خانواده"],
});
const result = parseMarriageCookie(legacyRawCookie);
expect(result).not.toBeNull();
expect(result?.parsedJson.city).toBe("مشهد");
expect(result?.parsedJson.interests).toEqual(["زیارت", "خانواده"]);
});
it("handles complex strings with percentage signs, quotes, and emojis without crashing", () => {
const complexData = {
id: 103,
status: "match_found",
note: "تطابق با دقت ۱۰۰% و رضایت ۹۵% 💍❤️",
query: "param1=val&param2=50%off",
};
const encodedCookie = encodeURIComponent(JSON.stringify(complexData));
const result = parseMarriageCookie(encodedCookie);
expect(result).not.toBeNull();
expect(result?.parsedJson.note).toBe("تطابق با دقت ۱۰۰% و رضایت ۹۵% 💍❤️");
expect(result?.parsedJson.query).toBe("param1=val&param2=50%off");
});
it("handles unencoded JSON containing raw percent sign gracefully via fallback", () => {
// If an unencoded cookie contains raw %, decodeURIComponent will throw URIError.
// The parser MUST catch it and fallback to JSON.parse directly.
const rawWithPercent = '{"status":"active","progress":"100% completed","city":"شیراز"}';
const result = parseMarriageCookie(rawWithPercent);
expect(result).not.toBeNull();
expect(result?.parsedJson.progress).toBe("100% completed");
expect(result?.parsedJson.city).toBe("شیراز");
});
it("returns null safely for corrupted, empty, or invalid cookies", () => {
expect(parseMarriageCookie(null)).toBeNull();
expect(parseMarriageCookie("")).toBeNull();
expect(parseMarriageCookie(" ")).toBeNull();
expect(parseMarriageCookie("undefined")).toBeNull();
expect(parseMarriageCookie("invalid-not-json")).toBeNull();
expect(parseMarriageCookie("%E0%A4%A")).toBeNull(); // Malformed URI sequence
});
});
Loading…
Cancel
Save