Phone input +1 prefix (#777) - Manual Test Plan โ
| Field | Value |
|---|---|
| Branch / PR | test/144-block-sever-e2e / #778 |
| Environment | Deployed dev (dev.ourlantern.app) post-merge, or local npm run dev (web :5173) |
| Build flags / config | none (behavior is default-on; no flag) |
| Build (commit) | see PR head |
| Tester | operator |
| Date | 2026-08-03 |
What changed โ
Every standard phone entry now shows a fixed, visible +1 prefix and accepts only the local number (formatted 619 555 0001). (The AppealFlow appeal-entry input is intentionally out of scope: a separate moderation flow through appealService, tracked separately.) The +1 is owned by the app; submit is gated on a full 10-digit number so no account can be created (and no lookup fired) under a malformed or partial number. Converted screens: PhonePinSignup, PhonePinLogin, AdminSignup, PhoneMigrationFlow, ForgotPassphraseModal (default neutral-900 style) and LockScreen, MerchantSignup (translucent white/5 style, via the component's className surface override). PhoneReclaimFlow has no phone input (it only displays a passed-in number). MerchantSignup previously shipped the raw typed string to the server; it now sends a normalized +1XXXXXXXXXX.
Summary โ
| # | Scenario | Result | Notes |
|---|---|---|---|
| 1 | Signup: type a local number, +1 visible, format-as-typed | [ ] pass [ ] fail [ ] blocked | |
| 2 | Signup: button disabled until 10 digits (no garbage submit) | [ ] pass [ ] fail [ ] blocked | |
| 3 | Signup: paste a full +1 (619) 555-0001 normalizes to local | [ ] pass [ ] fail [ ] blocked | |
| 4 | Signup: account created under a clean +1XXXXXXXXXX | [ ] pass [ ] fail [ ] blocked | |
| 5 | Signup: admin number goes through the same flow | [ ] pass [ ] fail [ ] blocked | |
| 6 | Login: sign in with an existing account works | [ ] pass [ ] fail [ ] blocked | |
| 7 | Login: button disabled until 10 digits | [ ] pass [ ] fail [ ] blocked | |
| 8 | Lock screen: +1 input on the white/5 surface unlocks | [ ] pass [ ] fail [ ] blocked | |
| 9 | Phone migration: linked E.164 number pre-fills as local + editable | [ ] pass [ ] fail [ ] blocked | |
| 10 | Forgot-passphrase recovery: +1 input + disabled-until-10 | [ ] pass [ ] fail [ ] blocked | |
| 11 | Merchant signup: +1 input, ships normalized +1 number | [ ] pass [ ] fail [ ] blocked |
Setup / preconditions โ
- Use a Prelude sandbox test number (
+16195550001..0005, OTP123456) for any signup, never a real number. Seedocs/engineering/testing/HEADLESS_E2E.md. - A fresh signup needs an invite (
?invite=...); mint one withnode tooling/e2e/onboard.mjsor from the admin portal. - Data to verify lives in
lantern-app-devFirestore. A newly created account's stored number is derivable via itsphoneHash; the reliable check is that the OTP send/verify used the exact+1XXXXXXXXXXstring (network tab ->/auth/phone/send-otpbody) and no account exists under any non-+1string. - Reset a test number between runs:
node --env-file=.env.local tooling/scripts/otp-local-test.mjs reset +16195550001(dev only).
Scenarios โ
1. Signup: visible +1 and format-as-typed โ
- Goal: the user types only the local number and sees it formatted.
- Steps:
- Open
#/signup?invite=<token>. - In the phone field, type
6195550001.
- Open
- Expected: a fixed
+1sits to the left of the field; the typed value renders as619 555 0001; no way to edit or delete the+1.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence: (screenshot)
Follow-up:2. Signup: button disabled until 10 digits โ
- Goal: a partial/malformed number cannot be submitted.
- Steps:
- On signup step 1, enter a valid birth date (18+).
- Type only
619555in the phone field. - Observe "Send Verification Code".
- Complete the number to
6195550001.
- Expected: the button is disabled at 6 digits and enables only at 10. Typing letters or symbols does nothing; pasting more than 10 digits caps at 10.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:3. Signup: paste normalization โ
- Goal: pasting a fully-formatted number lands on the right local digits.
- Steps:
- Paste
+1 (619) 555-0001into the phone field. - Then separately paste
16195550001.
- Paste
- Expected: both render as
619 555 0001(the pasted+1/ leading1is absorbed, not doubled).
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:4. Signup: clean account created + step-2 display โ
- Goal: a completed signup creates an account under a clean
+1number. - Steps:
- Enter
6195550001+ birth date, tap "Send Verification Code". - On step 2, read the "code sent to" line.
- Enter OTP
123456, finish signup (PIN + terms). - Check the network tab for the
/auth/phone/send-otp(andverify-otp) request body.
- Enter
- Expected: step 2 shows
+16195550001(not6195550001). The OTP requests carryphone: "+16195550001". The account is created and signs in. - Verify (data): no
usersdoc /auth_lookuprow exists under any non-+1variant of the number.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence: (network request body + screenshot)
Follow-up:5. Signup: admin number through the same flow โ
- Goal: an existing admin signing up for an app account still works (the bridge runs through this same step-1 field).
- Preconditions: a phone number that is registered as an admin (admin portal), used with a valid invite.
- Steps:
- Open signup with an invite, enter the admin's local number, tap "Send Verification Code".
- Expected: the "Admin account detected" panel appears (linking to the existing admin account), exactly as before. No difference from the free-text input other than the
+1affix.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:6. Login: existing account signs in โ
- Goal: login still works end to end.
- Steps:
- Open
#/login, type the local number of an existing test account. - Enter its PIN, tap "Sign In".
- Open
- Expected: the
+1affix shows; sign-in completes and lands in the app. The lookup request carries the+1XXXXXXXXXXnumber.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:7. Login: button disabled until 10 digits โ
- Goal: an incomplete number cannot trigger a lookup.
- Steps:
- On
#/login, type12in the phone field, enter any 6-digit PIN. - Observe "Sign In".
- On
- Expected: "Sign In" is disabled; no
/auth/phone/lookuprequest fires (network tab empty for it).
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:8. Lock screen: +1 input unlocks โ
- Goal: the lock-screen phone+PIN unlock works with the new input on its translucent surface.
- Preconditions: a signed-in account that has been locked (background the app / trigger the lock).
- Steps:
- On the lock screen, enter your local number in the phone field, then your PIN.
- Tap Unlock.
- Expected: the
+1affix shows on thewhite/5field (matching the lock-screen look); Unlock is disabled until 10 digits; unlocking succeeds and re-derives the key (no full sign-out).
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:9. Phone migration: linked number pre-fills as local โ
- Goal: an account migrating with an already-linked phone sees it correctly.
- Preconditions: an account whose
user.phoneNumberis already set (E.164). - Steps:
- Reach the phone-migration flow.
- Expected: the phone field shows the
+1affix with the linked number rendered as local digits (619 555 0001), editable, not a raw+16195550001. Submit works and normalizes to the correct+1number.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:10. Forgot-passphrase recovery: +1 input โ
- Goal: the recovery phone input matches the new behavior.
- Steps:
- Open "Forgot your PIN / passphrase" recovery.
- Enter a partial number, then a full 10-digit one.
- Expected:
+1affix visible; "Recover Account" is disabled until 10 digits; recovery proceeds against the normalized+1number.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up:11. Merchant signup: normalized +1 shipped โ
- Goal: the merchant application form uses the +1 input and sends a clean number.
- Steps:
- Open
#/merchant/signup, fill the form, enter a local number in Phone Number. - Submit and watch the network tab for the application POST.
- Open
- Expected:
+1affix on thewhite/5field; an incomplete number triggers the "fill in all required fields" error (no submit); the submitted application body carriesphoneNumber: "+1XXXXXXXXXX", not the raw typed string. - Verify (data/network): the merchant-application request body's
phoneNumberis E.164.
Result: [ ] pass [ ] fail [ ] blocked
Actual:
Evidence:
Follow-up: