Skip to content

feat: add the account phone numbers block with input-otp and phone-input primitives - #42

Merged
yyyyaaa merged 4 commits into
mainfrom
feat/account-phone-numbers
Sep 23, 2026
Merged

yyyyaaa merged 4 commits into
mainfrom
feat/account-phone-numbers

Conversation

@yyyyaaa

@yyyyaaa yyyyaaa commented Sep 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports the phone numbers settings from the dashboard (constructive-io/dashboard#273) into Blocks so anyone can install it:

pnpm dlx shadcn@latest add @constructive/account-phone-numbers
  • @constructive-io/ui: two new primitives, each an npm subpath and a registry item.
    • input-otp: one box per digit; handles typing, Backspace, arrows, paste, and one-time-code autofill as one value; compact sm size.
    • phone-input: searchable country picker with flags and a national-format field that emits E.164. It defaults to the browser's language region and exports formatPhoneNumber, normalizePhoneNumber, phoneCallingCode, CountryFlag, and detectCountry. Picker labels can be localized.
    • Flags (~52 kB gzipped) load in their own chunk behind a same-size placeholder, so nothing shifts when they arrive.
    • react-phone-number-input is an optional peer. Because phone-input imports it directly, phone-input is subpath-only: the root barrel never requires the peer, and the parity check enforces this.
    • ComboboxPopup gains an align passthrough.
  • account-phone-numbers block (apps/blocks):
    • The host supplies a five-call adapter (list, add, sendCode, verify, remove). The block owns the add, text-a-code, verify, and remove flow and makes no requests itself.
    • Every outcome is scoped to the card, the add form, or the row that caused it.
    • Backend error codes, including GraphQL extensions.code, map to overridable messages. Throwing AccountPhoneNumbersUnavailableError shows a notice instead of the list. identityKey remounts the block per account.
    • AccountPhoneNumbersView and useAccountPhoneNumbers are exported for custom layouts; spreading the hook's result into the view keeps custom messages.
  • Docs: a new Account section with the block's page.
    • The page has a live preview covering every state, a Live mode backed by an in-memory adapter (code 123456), and wide and narrow widths.
    • It is linked from the sidebar, topbar install action, sitemap, llms.txt, and the Authentication feature pack page.
  • Create playground: the real block now appears on the Platform wall. Project icon moves one column over to keep the columns balanced.
  • Registry: adds the catalog entry, a smoke-install case, and agent skill routing. The packed-consumer check installs the optional peer explicitly.

Test plan

  • pnpm check
  • pnpm build:pages, including the static /blocks/account and /blocks/account/account-phone-numbers pages
  • pnpm build:storybook, with the new PhoneInput and InputOtp stories
  • pnpm pack:check: all 90 ESM and CJS entries resolve, including phone-input
  • pnpm --filter @constructive-io/registry build and the account-phone-numbers smoke install, which typechecks and compiles CSS in a clean consumer that has strictNullChecks off
  • Unit tests: 11 for the block's flow, error, and identity cases, and 14 for phone-input and input-otp
  • Manually walked through add → wrong code → verify → remove on the docs page, in both themes and at narrow width
  • Checked the Create Platform wall column heights after adding the card

InputOtp is a one-box-per-digit code field that handles typing, Backspace, arrows, paste, and one-time-code autofill as one value. PhoneInput pairs a searchable country picker with a national-format field that emits E.164, defaults to the browser's language region, and exports formatting and flag helpers. Flags load in their own chunk behind a same-size placeholder. react-phone-number-input is an optional peer, so phone-input is subpath-only and the root barrel never requires it; the parity check enforces that. ComboboxPopup gains an align passthrough.
A settings card to add phone numbers, text them a code, verify, and remove them. The host supplies a five-call adapter; the block owns the flow and scopes every outcome to the card, the add form, or the row that caused it. Backend error codes (including GraphQL extensions.code) map to overridable messages, identityKey remounts per account, and the view and hook are exported for custom layouts. Adds the registry item, catalog entry, install smoke case, and agent skill routing.
The docs page previews every state of the block, plus a live mode backed by an in-memory adapter, at wide and narrow widths. Account is linked from the sidebar, topbar install action, sitemap, llms.txt, and the Authentication feature pack page.
The real block renders its mixed-numbers state beside Recovery & sessions, so themes preview its flags, statuses, and add field. Project icon moves one column over to keep the wall's columns balanced.
@tenki-reviewer

tenki-reviewer Bot commented Sep 23, 2026 •

Copy link
Copy Markdown

Review complete. 🟡 1 medium

💬 Inline comments (1)


This PR introduces a full phone-number management block for the account showcase — add, verify (OTP), and remove flows — backed by a new PhoneInput (react-phone-number-input) and InputOtp component in @constructive-io/ui, with tests, stories, and docs/scenario wiring.

Files Change
packages/ui/src/components/phone-input.tsx, input-otp.tsx, combobox.tsx, exports/stories/tests New phone-input and OTP input components with country-code flag rendering, grouping, and paste/autofill handling
apps/blocks/src/blocks/account/account-phone-numbers/* New block contracts, hook, container, and view implementing add/verify/remove over the adapter
apps/blocks/src/app/llms.txt, sitemap.ts, blocks/account pages, site-sidebar/topbar, lib/account-blocks.ts Route, sitemap, and navigation wiring for the new account block docs pages
packages/ui/package.json, registry.json, scripts/check-package-parity.ts, apps/blocks/registry.json, catalog/blocks.json, apps/registry/scripts/smoke-install.ts, scripts/check-packed-packages.ts Registry entries, optional peer dependency for react-phone-number-input, and parity/smoke tooling updates

One medium-severity issue was confirmed: unguarded parsePhoneNumber calls throw on unparseable numbers, breaking the documented fallbacks. A few lower-confidence state-consistency concerns in the phone-numbers hook (stale refresh, busy-state clobbering) fell below the reporting threshold.

Reviewed commit: 0f60cdb

@socket-security

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addedreact-phone-number-input@​3.4.189910010089100

View full report

@yyyyaaa
yyyyaaa merged commit abdd95a into main Sep 23, 2026
6 checks passed

@tenki-reviewer tenki-reviewer Bot left a comment •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adds an account phone-numbers block with OTP verification plus PhoneInput/InputOtp components, docs pages, and registry/parity tooling updates.

Key findings

Comment on lines +129 to +133
/** The country calling code a number carries, e.g. +1, +44, +353; '' when it cannot be parsed. */
function phoneCallingCode(e164: string): string {
const code = BasePhoneInput.parsePhoneNumber(e164)?.countryCallingCode;
return code ? `+${code}` : '';
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 bug · medium

parsePhoneNumber throws on unparseable numbers

phoneCallingCode calls BasePhoneInput.parsePhoneNumber(e164) without a try/catch (packages/ui/src/components/phone-input.tsx:131), but parsePhoneNumber throws a ParseError for unparseable input instead of returning undefined — so the documented '' fallback is unreachable and the new test asserting phoneCallingCode('garbage') === '' (packages/ui/test/phone-input.test.tsx:47) throws instead of passing. CountryFlag (line 257) has the same unguarded call and renders arbitrary stored numbers from the adapter, so a malformed number stored server-side crashes the row — and the whole card — during render, for exactly the inputs the globe fallback exists for.

📋 Prompt for AI Agents

In packages/ui/src/components/phone-input.tsx, wrap the BasePhoneInput.parsePhoneNumber calls in phoneCallingCode (line 131) and CountryFlag (line 257) in try/catch blocks that return '' (phoneCallingCode) or undefined (CountryFlag) on a thrown ParseError, so the documented ''/globe fallbacks actually trigger on unparseable input. libphonenumber-js's parsePhoneNumber throws ParseError on garbage instead of returning undefined, and packages/ui/test/phone-input.test.tsx line 47 currently asserts behavior the code cannot satisfy.

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