6 changed files with 431 additions and 13 deletions
-
3package.json
-
14src/app/layout.tsx
-
255src/data/country-calling-codes.ts
-
37src/lib/auth-bridge.test.ts
-
10src/lib/geo-region.ts
-
119src/lib/marriage-cookie.test.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]; |
|||
} |
|||
@ -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¶m2=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¶m2=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
|
|||
}); |
|||
}); |
|||
Write
Preview
Loading…
Cancel
Save
Reference in new issue