@@ -52,6 +52,8 @@ const PasskeyRegistrationModal = ({isOpen, onClose}) => {
5252 const config = getConfig ( )
5353 const webauthnConfig = config . app . login . passkey
5454 const authorizeWebauthnRegistration = useAuthHelper ( AuthHelpers . AuthorizeWebauthnRegistration )
55+ const startWebauthnUserRegistration = useAuthHelper ( AuthHelpers . StartWebauthnUserRegistration )
56+ const finishWebauthnUserRegistration = useAuthHelper ( AuthHelpers . FinishWebauthnUserRegistration )
5557
5658 const handleRegisterPasskey = async ( ) => {
5759 setIsLoading ( true )
@@ -82,9 +84,109 @@ const PasskeyRegistrationModal = ({isOpen, onClose}) => {
8284 }
8385 }
8486
87+ /**
88+ * Convert ArrayBuffer to base64url string
89+ */
90+ const arrayBufferToBase64Url = ( buffer ) => {
91+ const bytes = new Uint8Array ( buffer )
92+ let binary = ''
93+ for ( let i = 0 ; i < bytes . length ; i ++ ) {
94+ binary += String . fromCharCode ( bytes [ i ] )
95+ }
96+ const base64 = btoa ( binary )
97+ return base64 . replace ( / \+ / g, '-' ) . replace ( / \/ / g, '_' ) . replace ( / = / g, '' )
98+ }
99+
85100 const handleOtpVerification = async ( code ) => {
86- // TODO: Implement OTP verification
87- return { success : true }
101+ setIsLoading ( true )
102+ setError ( null )
103+
104+ try {
105+ // Step 1: Start WebAuthn registration
106+ const response = await startWebauthnUserRegistration . mutateAsync ( {
107+ user_id : customer . email ,
108+ pwd_action_token : code ,
109+ ...( passkeyNickname && { nick_name : passkeyNickname } )
110+ } )
111+
112+ // Step 2: Convert response to WebAuthn PublicKeyCredentialCreationOptions format
113+ const publicKey = window . PublicKeyCredential . parseCreationOptionsFromJSON ( response )
114+
115+ // Step 3: Call navigator.credentials.create()
116+ if ( ! navigator . credentials || ! navigator . credentials . create ) {
117+ throw new Error ( 'WebAuthn API not available in this browser' )
118+ }
119+
120+ // navigator.credentials.create() will show a browser/system prompt
121+ // This may appear to hang if the user doesn't interact with the prompt
122+ let credential
123+ try {
124+ credential = await navigator . credentials . create ( {
125+ publicKey
126+ } )
127+ } catch ( createError ) {
128+ // Handle user cancellation or other errors from the WebAuthn API
129+ if ( createError . name === 'NotAllowedError' || createError . name === 'AbortError' ) {
130+ throw new Error ( 'Passkey registration was cancelled or timed out' )
131+ }
132+ throw createError
133+ }
134+
135+ if ( ! credential ) {
136+ throw new Error ( 'Failed to create credential: user cancelled or operation failed' )
137+ }
138+
139+ // Step 4: Convert credential to JSON format before sending to SLAS
140+ // https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/toJSON
141+ let credentialJson
142+ try {
143+ credentialJson = credential . toJSON ( )
144+ } catch ( error ) {
145+ // Fallback to manual encoding if toJSON() fails
146+ // Some passkey providers (e.g., 1Password) may not support the toJSON() method and return an error
147+ const clientExtensionResults = credential . getClientExtensionResults ?. ( ) || { }
148+ credentialJson = {
149+ type : credential . type ,
150+ id : credential . id ,
151+ rawId : arrayBufferToBase64Url ( credential . rawId ) ,
152+ response : {
153+ attestationObject : arrayBufferToBase64Url (
154+ credential . response . attestationObject
155+ ) ,
156+ clientDataJSON : arrayBufferToBase64Url ( credential . response . clientDataJSON )
157+ } ,
158+ ...( Object . keys ( clientExtensionResults ) . length > 0 && { clientExtensionResults} )
159+ }
160+ }
161+
162+ // Step 5: Finish WebAuthn registration
163+ await finishWebauthnUserRegistration . mutateAsync ( {
164+ username : customer . email ,
165+ credential : credentialJson ,
166+ pwd_action_token : code
167+ } )
168+
169+ // Step 6: Close OTP modal and main modal on success
170+ setIsOtpAuthOpen ( false )
171+ onClose ( )
172+
173+ return { success : true }
174+ } catch ( err ) {
175+ const errorMessage =
176+ err . message ||
177+ formatMessage ( {
178+ id : 'passkey_registration.modal.error.registration_failed' ,
179+ defaultMessage : 'Failed to register passkey'
180+ } )
181+
182+ // Return error result for OTP component to display
183+ return {
184+ success : false ,
185+ error : errorMessage
186+ }
187+ } finally {
188+ setIsLoading ( false )
189+ }
88190 }
89191
90192 const resetState = ( ) => {
0 commit comments