From 66ec448b1e0bd73d093c6e9bad356145a5d91ab0 Mon Sep 17 00:00:00 2001 From: Brandon Corbett Date: Mon, 20 Jul 2026 19:45:21 -0400 Subject: [PATCH] fix(login): register with email only in the bundled view --- .changeset/email-only-registration.md | 7 ++++ src/client/createSeamlessAuthClient.ts | 6 ++- src/views/Login.tsx | 58 ++++++++++---------------- tests/login.test.tsx | 22 ++++------ 4 files changed, 41 insertions(+), 52 deletions(-) create mode 100644 .changeset/email-only-registration.md diff --git a/.changeset/email-only-registration.md b/.changeset/email-only-registration.md new file mode 100644 index 0000000..ed8b06d --- /dev/null +++ b/.changeset/email-only-registration.md @@ -0,0 +1,7 @@ +--- +'@seamless-auth/react': patch +--- + +The bundled registration screen now asks for an email only. It previously required a phone number as well, but registration only needs an email (the API treats phone as optional and it can be added and verified later), so the field was a mismatch that blocked email-only sign-up. + +`RegisterInput.phone` is now optional (`phone?: string | null`) and is only sent when a caller provides one, so headless consumers can still submit a phone at registration if they want to. diff --git a/src/client/createSeamlessAuthClient.ts b/src/client/createSeamlessAuthClient.ts index c713966..22f1caa 100644 --- a/src/client/createSeamlessAuthClient.ts +++ b/src/client/createSeamlessAuthClient.ts @@ -47,7 +47,9 @@ export interface LoginStartResult { export interface RegisterInput { email: string; - phone: string; + // Registration only needs an email. A phone can be added and verified later, + // so it is optional here and only sent when a caller supplies one. + phone?: string | null; bootstrapToken?: string | null; } @@ -471,7 +473,7 @@ export const createSeamlessAuthClient = ( method: 'POST', body: JSON.stringify({ email: input.email, - phone: input.phone, + ...(input.phone ? { phone: input.phone } : {}), ...(input.bootstrapToken ? { bootstrapToken: input.bootstrapToken } : {}), }), }), diff --git a/src/views/Login.tsx b/src/views/Login.tsx index a7909a4..8fea805 100644 --- a/src/views/Login.tsx +++ b/src/views/Login.tsx @@ -5,7 +5,6 @@ */ import { useAuth } from '@/AuthProvider'; -import PhoneInputWithCountryCode from '@/components/phoneInput'; import React, { useEffect, useState } from 'react'; import { useAuthClient } from '@/hooks/useAuthClient'; import { usePasskeySupport } from '@/hooks/usePasskeySupport'; @@ -27,9 +26,7 @@ const Login: React.FC = () => { const [identifier, setIdentifier] = useState(''); const [email, setEmail] = useState(''); const [mode, setMode] = useState<'login' | 'register'>('register'); - const [phone, setPhone] = useState(''); const [formErrors, setFormErrors] = useState(''); - const [phoneError, setPhoneError] = useState(''); const [emailError, setEmailError] = useState(''); const [identifierError, setIdentifierError] = useState(''); const [showFallbackOptions, setShowFallbackOptions] = useState(false); @@ -64,9 +61,8 @@ const Login: React.FC = () => { return isValidEmail(identifier) || isValidPhoneNumber(identifier); } - // Registration starts with just an email; a phone is optional but, if given, - // must be valid. - return isValidEmail(email) && (!phone || isValidPhoneNumber(phone)); + // Registration only needs a valid email. A phone can be added later. + return isValidEmail(email); }; const register = async () => { @@ -74,7 +70,6 @@ const Login: React.FC = () => { const { data, error } = await authClient.register({ email, - phone, bootstrapToken, }); @@ -226,36 +221,27 @@ const Login: React.FC = () => { )} {mode === 'register' && ( - <> -
- - { - if (email) { - const isValid = isValidEmail(email); - setEmailError(isValid ? '' : 'Please enter a valid email'); - } - }} - required - /> - {emailError &&

{emailError}

} -
- - + + { + if (email) { + const isValid = isValidEmail(email); + setEmailError(isValid ? '' : 'Please enter a valid email'); + } + }} + required /> - + {emailError &&

{emailError}

} + )} @@ -276,9 +268,8 @@ describe('Login', () => { }); }); - test('register mode submits registration', async () => { + test('register mode submits with only an email', async () => { (isValidEmail as jest.Mock).mockReturnValue(true); - (isValidPhoneNumber as jest.Mock).mockReturnValue(true); mockAuthClient.register.mockResolvedValueOnce({ data: { message: 'Success' }, @@ -289,14 +280,13 @@ describe('Login', () => { fireEvent.click(screen.getByText(/don't have an account/i)); + // Registration collects only an email; there is no phone field to fill. + expect(screen.queryByTestId('phone-input')).toBeNull(); + fireEvent.change(screen.getByLabelText(/email address/i), { target: { value: 'test@example.com' }, }); - fireEvent.change(screen.getByTestId('phone-input'), { - target: { value: '+15555555555' }, - }); - const registerButton = await screen.findByRole('button', { name: /register/i, }); @@ -309,6 +299,10 @@ describe('Login', () => { fireEvent.click(registerButton); }); + expect(mockAuthClient.register).toHaveBeenCalledWith( + expect.objectContaining({ email: 'test@example.com' }) + ); + expect(mockAuthClient.register.mock.calls[0][0]).not.toHaveProperty('phone'); expect(navigate).toHaveBeenCalledWith('/verify-email-otp'); }); });