import { z } from 'zod'; import { AsYouType, Metadata, getCountryCallingCode, isValidPhoneNumber, parsePhoneNumberFromString, type CountryCode } from 'libphonenumber-js'; /** * Accepts any international number in E.164 (`+`) or a * bare national number, which is assumed Ethiopian (`0911223344` -> * `+251911223344`) for backward compatibility with existing records. */ export const phoneNumber = z .string() .trim() .transform((v) => parsePhoneNumberFromString(v, 'ET')?.number ?? v) .superRefine((v, ctx) => { if (!v) ctx.addIssue({ code: 'custom', message: 'Phone number is required' }); else if (!isValidPhoneNumber(v)) ctx.addIssue({ code: 'custom', message: 'Enter a valid phone number' }); }); /** Same rules, but blank is allowed. */ export const optionalPhoneNumber = z.union([z.literal(''), phoneNumber]).optional(); /** * Digits the field should hold after an edit. A keystroke that only removed * a formatting character (backspacing the ')' out of "(415)") leaves the * digits unchanged, which would otherwise make the caret stick — drop a real * digit in that case. */ export function nextNationalDigits(text: string, prevDisplay: string): string { const digits = text.replace(/\D/g, ''); const deleting = text.length < prevDisplay.length; if (deleting && digits === prevDisplay.replace(/\D/g, '')) return digits.slice(0, -1); return digits; } /** National digits of a stored value, for display in the number box. */ export function toNationalDigits(value: string, country: CountryCode): string { if (!value) return ''; // `country` also resolves legacy national records ("0911223344"). const parsed = parsePhoneNumberFromString(value, country); if (parsed) return parsed.nationalNumber; if (value.startsWith('+')) { const prefix = `+${getCountryCallingCode(country)}`; if (value.startsWith(prefix)) return value.slice(prefix.length).replace(/\D/g, ''); } return value.replace(/\D/g, ''); } /** * E.164 for the digits typed so far. Incomplete numbers don't parse, so they * fall back to a plain dial-code concatenation rather than collapsing to '' * — the value has to survive mid-typing for the field to be usable. */ export function toE164(digits: string, country: CountryCode): string { if (!digits) return ''; return ( parsePhoneNumberFromString(digits, country)?.number ?? `+${getCountryCallingCode(country)}${digits}` ); } /** * Digits grouped for display ("91 122 3344"). Formatted as an international * number with the dial code cut off: AsYouType's national mode leaves the * number ungrouped unless the trunk prefix was typed. */ export function formatNational(digits: string, country: CountryCode): string { if (!digits) return ''; const prefix = `+${getCountryCallingCode(country)}`; const formatted = new AsYouType().input(prefix + digits); return formatted.startsWith(prefix) ? formatted.slice(prefix.length).trimStart() : digits; } const metadata = new Metadata(); /** Longest national number the country's numbering plan allows. */ export function maxNationalLength(country: CountryCode): number { metadata.selectNumberingPlan(country); return Math.max(...(metadata.numberingPlan?.possibleLengths() ?? [15])); } /** * True when the typed digits exceed the country's longest number. Judged on * the national part once it parses, so a trunk prefix (0911223344) or typed * country code (251911223344) isn't counted against the limit. */ export function exceedsMaxLength(digits: string, country: CountryCode): boolean { const national = parsePhoneNumberFromString(digits, country)?.nationalNumber ?? digits; return national.length > maxNationalLength(country); }