Appearance
Change my number — screen spec and design prompt
Status: SPECIFIED, NOT DESIGNED. This page is the prompt to send, not a record of an approved design. Written 2026-09-09 while building Account > Personal details, because that view cannot be completed as drawn.
1 · Why this exists
prototype/consumer/Account.dc.html's details view draws Mobile number as an ordinary editable text field, validated with phone.replace(/\D/g,'').length >= 10, committed by the same saveDetails() that saves the name and the email, and confirmed with the toast "Profile updated."
That cannot be implemented, and the reason is architectural rather than a missing endpoint.
- ADR-0032 (LOCKED) makes the phone a CREDENTIAL and never an address. Changing a credential is a re-verification, not a field write. An editable box that saves on the same button as a display name would be the wrong shape even if an endpoint existed.
- Supabase GoTrue enforces the same thing.
auth.updateUser({ phone })does not write the column: it setsnew_phone, leavesphoneuntouched, and enqueues a 6-digit code. The change completes only onverifyOtp({ phone, token, type: 'phone_change' }). There is no code path in which one Save commits a new number.
⚠ THE BACKEND IS ALREADY BUILT AND MEASURED, WHICH IS WHY THIS IS A DESIGN GAP AND NOT A BACKLOG ITEM. packages/data/src/auth/service.ts carries linkPhone(phoneE164) and verifyPhoneLink(phoneE164, code); the impl verifies with type: 'phone_change' (the wrong type reports an invalid code for a valid one, so this distinction is recorded there as measured on the live path), and delivery runs through QR Setu's own WhatsApp Business Account on Meta Cloud API via the send-auth-otp Send-SMS hook. No third-party messaging or authentication provider is in this path and none may be added.
So the only missing artefact is the screen. Until it lands, the field renders read-only with its reason on screen — never an editable box that cannot save.
2 · The decided contract to put IN the prompt
Per CLAUDE.md, an architecture-gated question is never sent to Claude Design as an open question. All of the following are decided and go into the prompt as constraints, not choices:
| Fact | Decided value |
|---|---|
| What the phone is | A credential (ADR-0032). Sign-in is WhatsApp OTP; Google is a permanent fallback; email is never a channel. |
| How a change completes | New number entered, 6-digit code delivered to the new number over WhatsApp, code verified. Two steps, never one. |
| What happens to the old number | It stops being a sign-in credential the moment the change verifies. It is not kept as a secondary address. |
| Who may do it | The signed-in account holder only. There is no admin-assisted path. |
| Failure the design must carry | The new number is already registered to another account. GoTrue refuses this, and the copy must not imply the person can take it. |
| What must NOT appear | Any mention of email as a channel · a marketplace framing ("Shops call this number about an order") · any privacy reassurance volunteered at the point of use (QRS-549). |
| Address unchanged | The /<slug> address is NOT derived from the phone (QRS-1078). A number change must not imply the address changes. |
3 · States the design must enumerate
The fourth rule applies: every state below is required, and a silent omission is the failure it exists to prevent.
- Entry — the current number shown, and the control that begins a change.
- Enter the new number — country context, validation, and what will happen next stated before the person commits.
- Waiting for the code — code sent to the NEW number, over WhatsApp, with a resend affordance and its cooldown.
- Code entered, verifying.
- Verified — the number is changed; confirm what is now true and what did not change (the address, the biodata, the shared links).
- Wrong or expired code — retryable, and the message must read as retryable.
- That number already belongs to an account — refused plainly, without confirming whose.
- The code never arrives — the honest dead end, with the one thing the person can do.
- Offline / send failed — the change has not happened; say so rather than leaving it ambiguous.
- Cancelled midway — the old number is still the credential and nothing changed.
4 · The prompt to paste
Paste everything inside the fence. It is fenced as text rather than quoted because check:design-prompt only reads a prompt inside a text block, and a prompt it cannot read is a prompt it cannot check: this page passed the gate while being skipped entirely until the fence was fixed.
text
**You are extending an app, not building one.**
This prototype already carries a consumer app. The account surface exists at
`prototype/consumer/Account.dc.html` (an eight-view screen: `hub`, `details`, `areas`, `notifs`,
`appearance`, `privacy`, `help`, `about`), and its `details` view already draws the profile form
this flow is reached from. Extend that surface. Do not create a new folder or a parallel
mini-app.
**The surface you are extending: `prototype/consumer/`.** Import the modules that are already
there rather than adding your own copies:
- `prototype/consumer/ds-base.js` — the design-system base this section already uses.
- `prototype/consumer/icons.js` — the icon set.
- `prototype/consumer/consumer-data.js` — the consumer view-model data, including `ACCOUNT_GROUPS`.
- `prototype/consumer/support.js` — the shared support helpers.
- `prototype/consumer/Account.dc.html` — the screen this flow belongs to.
Placement next to those files does not cause inheritance. Import them explicitly.
**What to design: changing the mobile number on a consumer account.**
Today `Account.dc.html`'s `details` view draws Mobile number as an editable text field saved by
the same button as the name. That is wrong for this product and cannot be built: the phone is the
**credential** people sign in with, so changing it is a verification flow, not a field write.
Replace the editable field with a row that shows the current number and opens this flow, and design
the flow itself.
**Fixed constraints — these are decided, please design to them rather than around them:**
- The code is delivered **to the new number, over WhatsApp**, from QR Setu's own WhatsApp Business
number. Never SMS. Never email. Email is not a channel anywhere in this product.
- It is **two steps**: enter the new number, then enter a 6-digit code. There is no one-tap change.
- The old number **stops working as a sign-in** as soon as the change verifies. It is not retained.
- The person's **address (`/<slug>`), their marriage biodata, and every link they have shared stay
exactly as they are.** A number change must not read as though any of that moves. Say so at the
point where a person would worry, in the confirmation, not as a disclaimer earlier.
- If the new number already belongs to another account, refuse it plainly and **do not reveal
anything about that account** — not that it exists, not who holds it.
- This product's marketplace is **off at launch**, so no copy may mention shops, orders, receipts
or deliveries. The account is for a family publishing a marriage biodata.
- Copy rules: **no em dashes or en dashes anywhere.** State what the product does, never what it
refrains from doing. Marathi, Hindi and English are all first-class, so keep sentences short
enough to survive Devanagari at the same width.
**Every state, as a scenario prop, because a state that is not drawn does not get built:**
entry (current number shown) · entering the new number · invalid number · waiting for the code
(with resend and its cooldown) · verifying · verified · wrong or expired code · that number already
belongs to an account · the code never arrived · offline or send failed · cancelled midway.
**Reuse, do not reinvent:** this prototype already designs a WhatsApp OTP experience for sign-in
in `prototype/onboarding/Onboarding.dc.html`. The number entry and code entry here should read as
the same product doing the same thing, not as a second OTP vocabulary. Show me where you reused it.
Register the flow in `prototype/consumer/consumer-data.js` where the account rows are declared, so
it is reachable from `Account.dc.html` rather than existing as an orphan artboard.5 · What is already true in the repo, so implementation is a wiring job
| Piece | State |
|---|---|
authService.linkPhone(phoneE164) | Built. Sends the phone_change code through the WhatsApp hook. |
authService.verifyPhoneLink(phoneE164, code) | Built. Verifies with type: 'phone_change' and returns the session for the SAME account. |
toE164 | Built in @qrsetu/domain. The leading + is required (QRS-936). |
| Delivery | Live. send-auth-otp + _shared/whatsapp.ts + the communication_messages ledger. |
| The read-only field and its reason | Shipped in DetailsView.tsx, with a test pinning editable === false. |
| The screen | This page. Not designed. |
⚠ One thing is measured for the SIGN-UP case and inferred for this one, and the difference is stated rather than blurred: linkPhone was verified on 2026-09-01 against an account with no phone (the Google fallback path), where GoTrue set new_phone and delivered a real message. The has-a-phone case uses the identical call and GoTrue documents the same behaviour, but it has not been exercised on a device. It is a device-suite case, not a claim.
6 · Related
- Screen coverage mandate — the copy-ready product-context block this prompt is built from.
- Consumer account parity contract — the
details_phone_read_onlyrow and its reason. - ADR-0032 — the phone as a credential, the slug as the address.