Skip to content

SMOODEV-2153: Phone formatting + validation on pre-chat form (libphonenumber-js)#17

Merged
brentrager merged 1 commit into
mainfrom
SMOODEV-2153-phone-format
Jun 29, 2026
Merged

SMOODEV-2153: Phone formatting + validation on pre-chat form (libphonenumber-js)#17
brentrager merged 1 commit into
mainfrom
SMOODEV-2153-phone-format

Conversation

@brentrager

Copy link
Copy Markdown
Contributor

Problem

The pre-chat form's Phone field had no client-side formatting or validation. Visitors typed free-form numbers and only the backend's authoritative E.164 normalization (smooai #2138, the backend half of SMOODEV-2153) caught problems. The field gave no feedback and the value sent was whatever the user typed.

Solution (frontend half of SMOODEV-2153)

Wire libphonenumber-js (US default region) into the pre-chat phone input — kept in the widget's hand-rolled, dependency-light idiom (no React).

  • As-you-type formatting via AsYouType('US') (e.g. 2133734253(213) 373-4253).
  • Inline validity hint driven by isValidPhoneNumber(value, 'US') — a subtle, themed valid/invalid state on the field + a small hint span. Empty stays neutral (the field is optional unless requirePhone).
  • On submit: block + show the hint when requirePhone and the number is invalid; allow submit when optional. Send canonical E.164 (parsePhoneNumber(value, 'US').number) when it parses, else fall back to the raw value — the backend re-parses and normalizes/nulls either way.
  • Autofill preserved: type="tel", autocomplete="tel", and the implicit <label> are unchanged; the value is also reformatted/validated on change so a browser-autofilled number is handled.

Caret / UX compromise (as-you-type)

AsYouType reformats the entire string, which on a mid-string edit moves the caret to the end (the well-known caveat). To avoid fighting the user, the value is only rewritten when the caret is at the end (the common append-a-digit case) and never on a deletion — so backspacing the formatting characters works naturally. The trade-off: editing in the middle of an already-formatted number does not live-reformat (it re-validates on change/submit). This is the standard pragmatic compromise for vanilla inputs without a masking library.

Bundle-size impact

libphonenumber-js/min (smaller metadata set, sufficient for AsYouType + isValidPhoneNumber + parsePhoneNumber).

Output Before After Δ
chat-widget.global.js (standalone IIFE) 140 kB / 39 kB gz 438 kB / 92 kB gz +298 kB / +53 kB gz
index.js (ESM, lib externalized) 120 kB / 35 kB gz 125 kB / 37 kB gz +5 kB / +2 kB gz

The IIFE jump is the inlined /min country metadata. The recommended loader embed lazy-loads chat-widget.global.js past the host's LCP, so this stays off the critical render path. The ESM build keeps the lib external so bundler hosts dedupe it.

⚠️ The IIFE required adding libphonenumber-js to deps.alwaysBundle in tsdown.config.ts — without it the bundle left a bare libphonenumber_js_min external reference (MISSING_GLOBAL_NAME) that would ReferenceError in a plain <script> embed. Verified the rebuilt IIFE closes })({}) with no external arg and evaluates clean.

Verification

  • pnpm typecheck
  • pnpm test ✅ — 101 passed (9 new phone tests: as-you-type US formatting, isValid good vs garbage, neutral-when-empty, no-reformat-while-deleting, autofill change reformat, E.164 on submit, requirePhone blocks invalid, optional forwards raw)
  • pnpm build ✅ — IIFE/global builds clean, no MISSING_GLOBAL_NAME, smoke-evaluated without ReferenceError

Version

Bumped to 0.8.0 (minor — new validation behavior) + changeset added.

Rollout

Do not merge yet — the main session coordinates the FE+BE rollout + the smoo.ai embed bump. Backend half is up as smooai #2138.

🤖 Generated with Claude Code

https://claude.ai/code/session_01AxL4LdeChNivUCZM6No2NJ

…enumber-js, US)

The pre-chat form's Phone field had no client-side formatting or validation —
visitors typed free-form numbers and only the backend's E.164 normalization
(smooai #2138) caught problems. This adds the frontend half so the field is
self-explaining and the value we send is already canonical when it parses.

- As-you-type formatting via AsYouType('US'). To avoid the standard AsYouType
  caret caveat (full-string reformat jumps the caret to the end), we only
  rewrite the value when the caret is at the end (the append-a-digit case) and
  never on a deletion — backspacing formatting characters works naturally.
- Inline, themed validity hint driven by isValidPhoneNumber(value,'US'). Empty
  stays neutral (the field is optional unless requirePhone).
- On submit: block + hint when requirePhone and the number is invalid; allow
  submit when optional. Send canonical E.164 (parsePhoneNumber.number) when it
  parses, else the raw value (the backend re-parses + normalizes/nulls).
- Autofill preserved: type="tel", autocomplete="tel", and the implicit <label>
  are unchanged, and we reformat/validate on `change` too so a browser-autofilled
  value is handled.
- Standalone IIFE bundle inlines libphonenumber-js/min (added to
  deps.alwaysBundle) — without it the IIFE left a bare `libphonenumber_js_min`
  external reference that would ReferenceError in a plain <script> embed.

Bundle-size impact (standalone IIFE chat-widget.global.js): 140 kB → 438 kB raw
(39 kB → 92 kB gzip) from the /min metadata. The recommended loader embed
lazy-loads this past the host's LCP. ESM index.js (lib externalized) is unchanged.

Version bumped to 0.8.0 (minor — new validation behavior) + changeset added.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AxL4LdeChNivUCZM6No2NJ
@changeset-bot

changeset-bot Bot commented Jun 29, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ff5c7ad

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@smooai/chat-widget Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@brentrager brentrager merged commit 6b8a36e into main Jun 29, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant