@@ -9,132 +9,148 @@ import React, { useEffect, useState } from 'react';
99import { useNavigate } from 'react-router-dom' ;
1010
1111import styles from '@/styles/registerPasskey.module.css' ;
12- import { isPasskeySupported } from './utils' ;
12+ import { isPasskeySupported , parseUserAgent } from './utils' ;
1313import { createFetchWithAuth } from './fetchWithAuth' ;
14+ import DeviceNameModal from './components/DeviceNameModal' ;
1415
1516const RegisterPasskey : React . FC = ( ) => {
1617 const { apiHost, mode } = useAuth ( ) ;
1718 const { validateToken } = useInternalAuth ( ) ;
1819 const navigate = useNavigate ( ) ;
20+
1921 const [ status , setStatus ] = useState < 'idle' | 'success' | 'error' | 'loading' > ( 'idle' ) ;
2022 const [ message , setMessage ] = useState ( '' ) ;
2123 const [ passkeyAvailable , setPasskeyAvailable ] = useState ( false ) ;
2224
25+ const [ showDeviceModal , setShowDeviceModal ] = useState ( false ) ;
26+ const [ pendingMetadata , setPendingMetadata ] = useState < {
27+ platform : string ;
28+ browser : string ;
29+ deviceInfo : string ;
30+ } | null > ( null ) ;
31+
2332 const fetchWithAuth = createFetchWithAuth ( {
2433 authMode : mode ,
2534 authHost : apiHost ,
2635 } ) ;
2736
28- const handlePasskeyRegister = async ( ) => {
37+ useEffect ( ( ) => {
38+ async function checkSupport ( ) {
39+ const supported = await isPasskeySupported ( ) ;
40+ setPasskeyAvailable ( supported ) ;
41+ }
42+
43+ checkSupport ( ) ;
44+ } , [ ] ) ;
45+
46+ const openDeviceModal = ( ) => {
47+ const { platform, browser, deviceInfo } = parseUserAgent ( ) ;
48+
49+ setPendingMetadata ( { platform, browser, deviceInfo } ) ;
50+ setShowDeviceModal ( true ) ;
51+ } ;
52+
53+ const continueRegistration = async ( friendlyName : string ) => {
54+ if ( ! pendingMetadata ) return ;
55+
56+ const { platform, browser, deviceInfo } = pendingMetadata ;
57+
2958 setStatus ( 'loading' ) ;
3059
3160 try {
3261 const challengeRes = await fetchWithAuth ( `/webAuthn/register/start` , {
3362 method : 'GET' ,
34- headers : {
35- 'Content-Type' : 'application/json' ,
36- } ,
63+ headers : { 'Content-Type' : 'application/json' } ,
3764 credentials : 'include' ,
3865 } ) ;
3966
4067 if ( ! challengeRes . ok ) {
41- setStatus ( 'error' ) ;
42- setMessage ( 'Something went wrong registering passkey.' ) ;
43- return ;
68+ throw new Error ( 'Failed to fetch challenge' ) ;
4469 }
4570
4671 const options = await challengeRes . json ( ) ;
4772
4873 let attResp : RegistrationResponseJSON ;
74+
4975 try {
5076 attResp = await startRegistration ( { optionsJSON : options } ) ;
51-
52- await verifyPassKey ( attResp ) ;
5377 } catch ( error ) {
5478 if ( error instanceof WebAuthnError ) {
55- console . error (
56- `Error occurred with webAuthn, ${ error . name } - ${ error . code } -${ error . stack } `
57- ) ;
58- setStatus ( 'error' ) ;
59- setMessage ( `Error: ${ error . name } ` ) ;
60- } else {
61- console . error ( 'A problem happened.' , error ) ;
62- setStatus ( 'error' ) ;
63- setMessage ( `Error: ${ error } ` ) ;
79+ throw new Error ( error . name ) ;
6480 }
65- return ;
81+ throw error ;
6682 }
6783
84+ await verifyPassKey ( attResp , {
85+ friendlyName,
86+ platform,
87+ browser,
88+ deviceInfo,
89+ } ) ;
90+
6891 setStatus ( 'success' ) ;
6992 setMessage ( 'Passkey registered successfully.' ) ;
7093 navigate ( '/' ) ;
71- } catch ( err ) {
72- console . error ( err ) ;
94+ } catch ( error ) {
95+ console . error ( error ) ;
7396 setStatus ( 'error' ) ;
74- setMessage ( 'Something went wrong registering passkey.' ) ;
97+ setMessage ( 'Error registering passkey.' ) ;
98+ } finally {
99+ setShowDeviceModal ( false ) ;
100+ setPendingMetadata ( null ) ;
75101 }
76102 } ;
77103
78- const verifyPassKey = async ( attResp : RegistrationResponseJSON ) => {
79- try {
80- const verificationResp = await fetchWithAuth ( `/webAuthn/register/finish` , {
81- method : 'POST' ,
82- headers : {
83- 'Content-Type' : 'application/json' ,
84- } ,
85- body : JSON . stringify ( {
86- attestationResponse : attResp ,
87- } ) ,
88- credentials : 'include' ,
89- } ) ;
90-
91- const verificationJSON = await verificationResp . json ( ) ;
92-
93- if ( ! verificationResp . ok ) {
94- setStatus ( 'error' ) ;
95- setMessage ( 'Something went wrong registering passkey.' ) ;
96- return ;
97- }
98-
99- if ( verificationJSON ?. verified ) {
100- await validateToken ( ) ;
101- }
102- } catch ( error ) {
103- console . error ( `An error occurred: ${ error } ` ) ;
104+ const verifyPassKey = async (
105+ attResp : RegistrationResponseJSON ,
106+ metadata : {
107+ friendlyName : string ;
108+ platform : string ;
109+ browser : string ;
110+ deviceInfo : string ;
104111 }
105- } ;
106-
107- useEffect ( ( ) => {
108- async function checkSupport ( ) {
109- const supported = await isPasskeySupported ( ) ;
110- setPasskeyAvailable ( supported ) ;
112+ ) => {
113+ const verificationResp = await fetchWithAuth ( `/webAuthn/register/finish` , {
114+ method : 'POST' ,
115+ headers : { 'Content-Type' : 'application/json' } ,
116+ body : JSON . stringify ( {
117+ attestationResponse : attResp ,
118+ metadata,
119+ } ) ,
120+ credentials : 'include' ,
121+ } ) ;
122+
123+ if ( ! verificationResp . ok ) {
124+ throw new Error ( 'Verification failed' ) ;
111125 }
112126
113- checkSupport ( ) ;
114- } , [ ] ) ;
127+ await validateToken ( ) ;
128+ } ;
115129
116130 return (
117- < div className = { styles . container } >
118- < div className = { styles . card } >
119- { ! passkeyAvailable ? (
120- < div className = { styles . loading } >
121- < div className = { styles . spinner } > </ div >
122- < span > Checking for Passkey Support... </ span >
123- </ div >
124- ) : (
125- passkeyAvailable && (
131+ < >
132+ < div className = { styles . container } >
133+ < div className = { styles . card } >
134+ { ! passkeyAvailable ? (
135+ < div className = { styles . loading } >
136+ < div className = { styles . spinner } > </ div >
137+ < span > Checking for Passkey Support... </ span >
138+ </ div >
139+ ) : (
126140 < div className = { styles . supported } >
127141 < h2 className = { styles . title } > Secure Your Account with a Passkey</ h2 >
128142 < p className = { styles . description } >
129143 Your device supports passkeys! Register one to skip passwords forever.
130144 </ p >
145+
131146 < button
132- onClick = { handlePasskeyRegister }
147+ onClick = { openDeviceModal }
133148 disabled = { status === 'loading' }
134149 className = { styles . button }
135150 >
136151 { status === 'loading' ? 'Registering...' : 'Register Passkey' }
137152 </ button >
153+
138154 { message && (
139155 < p
140156 className = { `${ styles . message } ${
@@ -145,10 +161,19 @@ const RegisterPasskey: React.FC = () => {
145161 </ p >
146162 ) }
147163 </ div >
148- )
149- ) }
164+ ) }
165+ </ div >
150166 </ div >
151- </ div >
167+
168+ < DeviceNameModal
169+ isOpen = { showDeviceModal }
170+ onCancel = { ( ) => {
171+ setShowDeviceModal ( false ) ;
172+ setPendingMetadata ( null ) ;
173+ } }
174+ onConfirm = { continueRegistration }
175+ />
176+ </ >
152177 ) ;
153178} ;
154179
0 commit comments