S280 Handoff — International phone input (UxPhoneInputComponent)

Engineering brief to replace US-only behavior with country-aware E.164 input without breaking auth, admin, or API contracts.

S280 Handoff: International UxPhoneInputComponent

Executive summary

  1. UxPhoneInputComponent (selector: ux-phone-input) is the only shared UI surface for collecting phone numbers for SMS/WhatsApp flows. It still assumes US (+1) and 10-digit national input in practice, which blocks non-US users across login, password reset, and admin phone verification.
  2. The platform contract upstream and downstream is unchanged: E.164 strings (+ + country calling code + national digits). API validation already matches ^\+[1-9]\d{6,14}$ in shared DTOs; the gap is UI + normalization, not the wire format.
  3. The smart move is to implement country metadata + variable length + paste/intl parsing in the ui-forms library, keep ControlValueAccessor output identical (E.164), and optionally align ValidationUtils.normalizePhoneToE164 so server- and client-side normalization do not diverge.

Why this matters (impact map)

Area Symptom today After fix
End users (non-US) Cannot complete SMS/WhatsApp login or SMS/WhatsApp password reset Same flows work when Twilio supports the destination
Admins gograb-admin user detail uses ux-phone-input inside a light card; control is visually “dark-theme” — cosmetic follow-up Functional parity; optional theme variant later
Backend Already stores/sends E.164; admin phone OTP is per user id No API redesign required if UI emits valid E.164

Confidence: Certain for E.164 contract (DTOs + services); likely for Twilio coverage per country (product/ops).


System contract (do not break)

flowchart LR
  subgraph ui["Browser"]
    UX[UxPhoneInputComponent]
  end
  subgraph api["backend-api"]
    DTO["PhoneAuthRequestDto / PhoneAuthVerifyDto\n@IsMatches E.164"]
    ADM["Admin phone verify\n(normalize + uniqueness)"]
  end
  UX -->|"string: +cc…"| DTO
  UX -->|"string: +cc…"| ADM

Related code (read before coding):


Current implementation snapshot

Behavior Detail
Prefix Template: static +{{ countryCode() }}; default countryCode input = '1'
Input cap National digits effectively US-length in onInput / display logic
writeValue Assumes value maps to selected country + national (today effectively +1 + 10 digits)
Styling Inline styles tuned for dark surfaces; consumers include light admin forms

Consumers (verify after changes):

App / lib Entry
libs/libs-ui/auth-ui login.component (template + TS)
apps/gograb-space forgot-password.page.ts (inline template)
apps/gograb-admin user-detail (profile phone + verification)

Run a quick ripgrep before shipping: ux-phone-input / UxPhoneInputComponent.


Requirements (ranked)

P0 — Must ship

  1. Country selection — User can choose calling country (ITU-T country calling code), not only +1.
  2. Variable national length — Do not hard-cap at 10 digits; length depends on selected country (and sometimes number type — see P1).
  3. Default country — If parent does not bind [countryCode], infer a sensible default from navigator.language / Intl locale (e.g. es-MX → Mexico). Document fallback ('1' or '44') when ambiguous.
  4. E.164 emission — On blur/commit (and when pasting full international number), emit one normalized E.164 string; ngModel / formControl unchanged semantically.
  5. Paste / typed international — Pasting +44 7911 123456 or 0049… should select correct country and fill national digits when unambiguous.
  6. Searchable country list — Filter by country name, ISO 3166-1 alpha-2, and calling code (keyboard and screen-reader friendly).
  7. Accessibility — Combobox or listbox pattern: label, aria-expanded, active option, no “flags only” without text.

P1 — Strongly recommended

P2 — Nice to have

Out of scope (explicit)


Design decisions (tradeoff matrix)

Option Pros Cons
A. Custom country table + libphonenumber-js Accurate validation, smaller than full Google lib, TS-native Bundle cost — measure per app; lazy-load country metadata if needed
B. Custom table only (min/max lengths) No new dependency Wrong for many carriers / variable lengths; high maintenance
C. Wrap a web component / CDN widget Fast POC Style integration, CSP, Angular CVA glue fragile

Recommendation: Option A — country list can stay a static curated JSON (top N countries + “rest” fallback), validation via libphonenumber-js.


Suggested file layout (library)

libs/libs-ui-core/ui-forms/src/lib/phone-input/
├── phone-input.component.ts       # CVA + template
├── phone-input.component.scss     # move inline styles; support variants
├── country-codes.ts               # ISO2, name, dialCode, priority, optional region
├── phone-country-picker.component.ts  # optional: searchable overlay (same bundle)
└── phone-parse.ts                 # paste + E.164 parse helpers (unit-test heavy)

Export new symbols only from libs/libs-ui-core/ui-forms/src/index.ts if consumers need the picker; otherwise keep private to the module.


Alignment with ValidationUtils

Today ValidationUtils.normalizePhoneToE164 is US-centric (10 digits → +{defaultCountryCode}). After international input ships:

Avoid two conflicting definitions of “normalized phone” in the monorepo.


Edge cases (minimum test matrix)

# Scenario Expected
1 User selects DE, enters valid mobile E.164 +49…
2 Paste full E.164 with spaces Country + national inferred; model E.164
3 Paste competing prefixes (00 vs +) Deterministic parse per chosen helper lib
4 Switch country mid-typing National digits truncated or re-validated — no silent wrong country
5 writeValue from API with stored E.164 Display matches stored number
6 Empty value null or '' per existing CVA behavior — unchanged contract
7 Invalid for region Show inline error; do not emit invalid E.164 on blur

Verification checklist (before merge)


Rollout / risk

Risk Mitigation
Bundle size Lazy-load libphonenumber-js metadata or import minimal entry point
Wrong default country Allow parent [countryCode] override; test en-GB, es-MX, de-DE
Multi-instance backend Unrelated to this UI task; admin OTP storage is server-side

Definition of done

  1. Non-US users can complete phone-based flows where Twilio supports the destination and the number is valid E.164.
  2. No change required in consumer templates unless we intentionally add optional inputs (e.g. variant) — default path should just work.
  3. Unit tests cover parse, paste, writeValue round-trip for at least 5 countries on three continents.
  4. This document’s P0 list is satisfied or explicitly deferred with a linked issue.

References


Appendix: locale → default country (algorithm sketch)

  1. Read navigator.language (e.g. en-GB).
  2. Map region subtag (GB) → calling code via static map (GB → 44). If only language (en) with no region, do not guess US — fall back to configured default (product decision: often 1 for current user base).
  3. If [countryCode] is set by parent, it wins over locale.

Document the fallback in component JSDoc so future agents do not “fix” it incorrectly.