Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .changeset/email-only-registration.md
Original file line number Diff line number Diff line change
@@ -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.
6 changes: 4 additions & 2 deletions src/client/createSeamlessAuthClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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 } : {}),
}),
}),
Expand Down
58 changes: 22 additions & 36 deletions src/views/Login.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -27,9 +26,7 @@ const Login: React.FC = () => {
const [identifier, setIdentifier] = useState<string>('');
const [email, setEmail] = useState<string>('');
const [mode, setMode] = useState<'login' | 'register'>('register');
const [phone, setPhone] = useState<string>('');
const [formErrors, setFormErrors] = useState<string>('');
const [phoneError, setPhoneError] = useState<string>('');
const [emailError, setEmailError] = useState<string>('');
const [identifierError, setIdentifierError] = useState<string>('');
const [showFallbackOptions, setShowFallbackOptions] = useState(false);
Expand Down Expand Up @@ -64,17 +61,15 @@ 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 () => {
setFormErrors('');

const { data, error } = await authClient.register({
email,
phone,
bootstrapToken,
});

Expand Down Expand Up @@ -226,36 +221,27 @@ const Login: React.FC = () => {
</div>
)}
{mode === 'register' && (
<>
<div className={styles.inputGroup}>
<label htmlFor="email" className={styles.label}>
Email Address
</label>
<input
id="email"
type="email"
value={email}
onChange={handleEmailChange}
autoComplete="off"
className={styles.input}
onBlur={() => {
if (email) {
const isValid = isValidEmail(email);
setEmailError(isValid ? '' : 'Please enter a valid email');
}
}}
required
/>
{emailError && <p className={styles.error}>{emailError}</p>}
</div>

<PhoneInputWithCountryCode
phone={phone}
setPhone={setPhone}
phoneError={phoneError}
setPhoneError={setPhoneError}
<div className={styles.inputGroup}>
<label htmlFor="email" className={styles.label}>
Email Address
</label>
<input
id="email"
type="email"
value={email}
onChange={handleEmailChange}
autoComplete="off"
className={styles.input}
onBlur={() => {
if (email) {
const isValid = isValidEmail(email);
setEmailError(isValid ? '' : 'Please enter a valid email');
}
}}
required
/>
</>
{emailError && <p className={styles.error}>{emailError}</p>}
</div>
)}
<button type="submit" className={styles.button} disabled={!canSubmit()}>
{mode === 'login' ? 'Login' : 'Register'}
Expand Down
22 changes: 8 additions & 14 deletions tests/login.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,6 @@ jest.mock('react-router-dom', () => ({
useHref: jest.fn((to: string) => to),
}));

jest.mock('@/components/phoneInput', () => (props: any) => (
<input
data-testid="phone-input"
value={props.phone}
onChange={e => props.setPhone(e.target.value)}
/>
));

jest.mock('@/components/AuthFallbackOptions', () => (props: any) => (
<div data-testid="fallback-options">
<button onClick={props.onMagicLink}>MagicLink</button>
Expand Down Expand Up @@ -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' },
Expand All @@ -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,
});
Expand All @@ -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');
});
});
Loading