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
20 changes: 20 additions & 0 deletions .changeset/quiet-pandas-enrol.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@seamless-auth/react': minor
---

Drop the naming step from the bundled passkey enrolment view.

Choosing "Register Passkey" (or "Use a security key instead") opened a modal asking for a
friendly name, and the browser's own passkey prompt only appeared once that form was
submitted. A user who came to the screen to press one button was handed a text field
first, at the point in the flow where they had the least idea what to type.

Registration now starts on the click. The credential still carries a `friendlyName`, and
the view fills it with the device the passkey was enrolled on (`mac • chrome`), which is
what the naming prompt suggested people write anyway. Renaming stays available through
`updateCredential`.

Nothing in the public API moves: `PasskeyMetadata.friendlyName` is unchanged and callers
building their own enrolment screen keep setting it themselves. Only the bundled
`/register-passkey` view changes, so an adopter relying on that screen to collect a name
needs their own screen for it.
59 changes: 0 additions & 59 deletions src/components/DeviceNameModal.tsx

This file was deleted.

57 changes: 0 additions & 57 deletions src/styles/deviceNameModal.module.css

This file was deleted.

192 changes: 81 additions & 111 deletions src/views/PassKeyRegistration.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ import { useNavigate } from 'react-router-dom';

import styles from '@/styles/registerPasskey.module.css';
import { parseUserAgent } from '@/utils';
import DeviceNameModal from '@/components/DeviceNameModal';

const POLICY_REFUSAL_MESSAGES: Record<PasskeyPolicyErrorCode, string> = {
attachment_not_allowed:
Expand Down Expand Up @@ -43,14 +42,6 @@ const PasskeyRegistration: React.FC = () => {
const [status, setStatus] = useState<'idle' | 'success' | 'error' | 'loading'>('idle');
const [message, setMessage] = useState('');

const [showDeviceModal, setShowDeviceModal] = useState(false);
const [pendingMetadata, setPendingMetadata] = useState<{
platform: string;
browser: string;
deviceInfo: string;
} | null>(null);
const [pendingAttachment, setPendingAttachment] = useState<PasskeyAttachment>();

// The session already exists by the time this screen renders: the OTP step
// that led here established it. A passkey is an addition to that session
// rather than what completes registration, which is what makes leaving
Expand All @@ -65,29 +56,23 @@ const PasskeyRegistration: React.FC = () => {
navigate('/');
};

const openDeviceModal = (attachment?: PasskeyAttachment) => {
const registerPasskey = async (attachment?: PasskeyAttachment) => {
const { platform, browser, deviceInfo } = parseUserAgent();

setPendingMetadata({ platform, browser, deviceInfo });
setPendingAttachment(attachment);
setShowDeviceModal(true);
};

const continueRegistration = async (friendlyName: string) => {
if (!pendingMetadata) return;

const metadata: PasskeyMetadata = {
friendlyName,
...pendingMetadata,
// The credential still carries a label, but asking for one here put a
// form between the user and the browser prompt they came for. The device
// it was enrolled on identifies it well enough to rename later.
friendlyName: deviceInfo,
platform,
browser,
deviceInfo,
};

setStatus('loading');

try {
const { error } = await authClient.registerPasskey({
metadata,
attachment: pendingAttachment,
});
const { error } = await authClient.registerPasskey({ metadata, attachment });

if (error) {
throw error;
Expand All @@ -103,108 +88,93 @@ const PasskeyRegistration: React.FC = () => {
// A policy refusal names something the user can act on, for example
// reaching for a security key instead. Anything else stays generic.
setMessage(policyRefusalMessage(error) ?? 'Error registering passkey.');
} finally {
setShowDeviceModal(false);
setPendingMetadata(null);
setPendingAttachment(undefined);
}
};

return (
<>
<div className={styles.container}>
<div className={styles.card}>
{passkeySupportLoading || loginMethodsLoading ? (
<div className={styles.loading}>
<div className={styles.spinner}></div>
<span>Checking for Passkey Support...</span>
</div>
) : !passkeySupported ? (
// This used to be the end of the road: a message and no control of
// any kind, on a screen the user could not leave. Whether there is a
// way forward depends on the instance, so say which case this is.
<div className={styles.supported}>
<h2 className={styles.title}>Passkeys are not available here</h2>
<p className={styles.description}>
{canSkip
? 'This device does not support passkeys. You can continue without one and add a passkey later from a device that does.'
: 'This device does not support passkeys, and this application requires one to sign in. Try again from a device or browser that supports them.'}
</p>

{canSkip && (
<button
type="button"
onClick={finishWithoutPasskey}
className={styles.button}
>
Continue
</button>
)}
</div>
) : (
<div className={styles.supported}>
<h2 className={styles.title}>Secure Your Account with a Passkey</h2>
<p className={styles.description}>
Your device supports passkeys! Register one to skip passwords forever.
</p>

<div className={styles.container}>
<div className={styles.card}>
{passkeySupportLoading || loginMethodsLoading ? (
<div className={styles.loading}>
<div className={styles.spinner}></div>
<span>Checking for Passkey Support...</span>
</div>
) : !passkeySupported ? (
// This used to be the end of the road: a message and no control of
// any kind, on a screen the user could not leave. Whether there is a
// way forward depends on the instance, so say which case this is.
<div className={styles.supported}>
<h2 className={styles.title}>Passkeys are not available here</h2>
<p className={styles.description}>
{canSkip
? 'This device does not support passkeys. You can continue without one and add a passkey later from a device that does.'
: 'This device does not support passkeys, and this application requires one to sign in. Try again from a device or browser that supports them.'}
</p>

{canSkip && (
<button
onClick={() => openDeviceModal()}
disabled={status === 'loading'}
type="button"
onClick={finishWithoutPasskey}
className={styles.button}
>
{status === 'loading' ? 'Registering...' : 'Register Passkey'}
Continue
</button>
)}
</div>
) : (
<div className={styles.supported}>
<h2 className={styles.title}>Secure Your Account with a Passkey</h2>
<p className={styles.description}>
Your device supports passkeys! Register one to skip passwords forever.
</p>

<button
onClick={() => registerPasskey()}
disabled={status === 'loading'}
className={styles.button}
>
{status === 'loading' ? 'Registering...' : 'Register Passkey'}
</button>

{/*
The default above leaves the choice to the deployment policy,
which offers both kinds. This is the deliberate path for someone
who has been handed an issued key and should not have to find it
in the browser's picker.
*/}
<button
type="button"
onClick={() => registerPasskey('cross-platform')}
disabled={status === 'loading'}
className={styles.secondary}
>
Use a security key instead
</button>

{message && (
<p
className={`${styles.message} ${
status === 'success' ? styles.success : styles.error
}`}
>
{message}
</p>
)}

{/*
The default above leaves the choice to the deployment policy,
which offers both kinds. This is the deliberate path for someone
who has been handed an issued key and should not have to find it
in the browser's picker.
*/}
{canSkip && (
<button
type="button"
onClick={() => openDeviceModal('cross-platform')}
onClick={finishWithoutPasskey}
disabled={status === 'loading'}
className={styles.secondary}
className={styles.skip}
>
Use a security key instead
Skip for now
</button>

{message && (
<p
className={`${styles.message} ${
status === 'success' ? styles.success : styles.error
}`}
>
{message}
</p>
)}

{canSkip && (
<button
type="button"
onClick={finishWithoutPasskey}
disabled={status === 'loading'}
className={styles.skip}
>
Skip for now
</button>
)}
</div>
)}
</div>
)}
</div>
)}
</div>

<DeviceNameModal
isOpen={showDeviceModal}
onCancel={() => {
setShowDeviceModal(false);
setPendingMetadata(null);
}}
onConfirm={continueRegistration}
/>
</>
</div>
);
};

Expand Down
Loading
Loading