Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

Login page: support phone number + password sign-in #2416

Closed

Description

@os-zhuang

Summary

The Console login page currently supports email + password and phone + verification code (OTP), but not phone number + password. The backend already implements it — this is a frontend/SDK gap.

Background / research

Backend (framework @objectstack/plugin-auth) — already done:

  • better-auth's phoneNumber plugin exposes POST /sign-in/phone-number ({ phoneNumber, password } → session). It's registered whenever the phoneNumber plugin is enabled.
  • The platform does not set requireVerification, so an unverified phone can still sign in with a password (once a password is set).
  • Unlike OTP, phone+password needs no SMS service.
  • The capability is already advertised: getPublicConfig().features.phoneNumber = "phoneNumber plugin enabled" (distinct from features.phoneNumberOtp, which also requires a deliverable SMS service). The objectui type AuthPublicConfig.features.phoneNumber already exists too.

Frontend (objectui) — the gap:

  • LoginForm has mode: 'password' | 'phone-otp'. The password mode's field is Email only and submits via signIn(email, password) → /sign-in/email. There is no path to /sign-in/phone-number.
  • createAuthClient exposes signInWithPhoneOtp but no signInWithPhonePassword.

Critical correctness note — phone normalization:
The backend's normalizePhoneNumber just strips [\s\-().] and validates ^\+?[0-9]{6,15}$ — it does not force E.164 or inject a country code. Accounts are stored in that light-stripped form. The login form therefore must normalize identically (strip the same characters, send as-is); adding a country code or full E.164 conversion would break lookups for numbers stored without one. So no libphonenumber dependency is needed — and would in fact be wrong here.

How mainstream apps handle it

  • Unified identifier field ("Email or phone", auto-route by format) — Google, X, Facebook, LinkedIn, Amazon. Simplest UX.
  • Segmented / tabbed (Email | Phone, often with a country-code selector) — WeChat, Alipay, Taobao, Meituan, DiDi. Phone-first, common in China.

Chosen approach: unified identifier field in the existing password mode — minimal UI change, coexists with the phone-OTP mode, and matches the dominant global pattern.

Scope of work

  • createAuthClient: add signInWithPhonePassword(phoneNumber, password) → POST /sign-in/phone-number (mirror signInWithPhoneOtp token/session handling).
  • Wire it through AuthClient (types), AuthContext, AuthProvider, useAuth default.
  • Shared normalizePhoneIdentifier helper mirroring the backend (strip [\s\-().], validate ^\+?[0-9]{6,15}$).
  • LoginForm: in password mode, accept an email-or-phone identifier gated on features.phoneNumber; auto-route to /sign-in/email vs /sign-in/phone-number by format. Update the field label/type; keep SSO routing email-only.
  • Tests: identifier normalization/detection, client method, and LoginForm routing.

Out of scope

  • Country-code selector / E.164 migration (backend stores non-E.164; would be a separate, coordinated change).
  • Phone + password self-registration (signUpOnVerification is deliberately off; accounts are provisioned by admin/import).

Notes

Only works for accounts that have a phone number and a password set. Phone-only accounts created via import use the invite policy: first sign-in via OTP, then set a password — after which phone+password works.

Activity

  1. added a commit that references this issue on Jul 13, 2026
    94d00d4
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions