-
Notifications
You must be signed in to change notification settings - Fork 212
@W-21056536 Error handling Passkey Registration and Login #3672
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
3791b65
8ec8140
98d8950
315e3a1
1f45c06
54d6043
b16b759
767acfc
1199fc6
977e9ea
bbc4071
7c727b2
a9bda52
2e64f70
ca9c030
cc79b74
303ed13
b6f7f4f
529b703
1bf6075
f80c954
e1a22ec
8271c9f
faa5454
bba25a2
eb67152
b2b9709
e000ff0
22ad02c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,6 +37,9 @@ import {arrayBufferToBase64Url} from '@salesforce/retail-react-app/app/utils/uti | |
| // SDK | ||
| import {AuthHelpers, useAuthHelper} from '@salesforce/commerce-sdk-react' | ||
|
|
||
| // Constants | ||
| import {API_ERROR_MESSAGE, INVALID_TOKEN_ERROR_MESSAGE} from '@salesforce/retail-react-app/app/constants' | ||
|
|
||
| /** | ||
| * Modal for registering a new passkey with a nickname | ||
| */ | ||
|
|
@@ -73,13 +76,8 @@ const PasskeyRegistrationModal = ({isOpen, onClose}) => { | |
| onClose() | ||
| setIsOtpAuthOpen(true) | ||
| } catch (err) { | ||
| setError( | ||
| err.message || | ||
| formatMessage({ | ||
| id: 'passkey_registration.modal.error.authorize_failed', | ||
| defaultMessage: 'Failed to authorize passkey registration' | ||
| }) | ||
| ) | ||
| // Set error message for the passkey registration modal | ||
| setError(formatMessage(API_ERROR_MESSAGE)) | ||
| } finally { | ||
| setIsLoading(false) | ||
| } | ||
|
|
@@ -105,26 +103,15 @@ const PasskeyRegistrationModal = ({isOpen, onClose}) => { | |
| throw new Error('WebAuthn API not available in this browser') | ||
| } | ||
|
|
||
| // navigator.credentials.create() will show a browser/system prompt | ||
| // Step 4: navigator.credentials.create() will show a browser/system prompt | ||
| // This may appear to hang if the user doesn't interact with the prompt | ||
| let credential | ||
| try { | ||
| credential = await navigator.credentials.create({ | ||
| publicKey | ||
| }) | ||
| } catch (createError) { | ||
| // Handle user cancellation or other errors from the WebAuthn API | ||
| if (createError.name === 'NotAllowedError' || createError.name === 'AbortError') { | ||
| throw new Error('Passkey registration was cancelled or timed out') | ||
| } | ||
| throw createError | ||
| } | ||
| const credential = await navigator.credentials.create({publicKey}) | ||
|
|
||
| if (!credential) { | ||
| throw new Error('Failed to create credential: user cancelled or operation failed') | ||
| } | ||
|
|
||
| // Step 4: Convert credential to JSON format before sending to SLAS | ||
| // Step 5: Convert credential to JSON format before sending to SLAS | ||
| // https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/toJSON | ||
| let credentialJson | ||
| try { | ||
|
|
@@ -147,30 +134,27 @@ const PasskeyRegistrationModal = ({isOpen, onClose}) => { | |
| } | ||
| } | ||
|
|
||
| // Step 5: Finish WebAuthn registration | ||
| // Step 6: Finish WebAuthn registration | ||
| await finishWebauthnUserRegistration.mutateAsync({ | ||
| username: customer.email, | ||
| credential: credentialJson, | ||
| pwd_action_token: code | ||
| }) | ||
|
|
||
| // Step 6: Close OTP modal and main modal on success | ||
| // Step 7: Close OTP modal and main modal on success | ||
| setIsOtpAuthOpen(false) | ||
| onClose() | ||
|
|
||
| return {success: true} | ||
| } catch (err) { | ||
| const errorMessage = | ||
| err.message || | ||
| formatMessage({ | ||
| id: 'passkey_registration.modal.error.registration_failed', | ||
| defaultMessage: 'Failed to register passkey' | ||
| }) | ||
|
|
||
| console.error('Error registering passkey:', err) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. although API errors are automatically logged, we want to ensure the errors from the browser and custom errors that are thrown are logged to help in debugging issues. |
||
| const message = /Unauthorized/i.test(err.message) | ||
| ? formatMessage(INVALID_TOKEN_ERROR_MESSAGE) | ||
| : formatMessage(API_ERROR_MESSAGE) | ||
| // Return error result for OTP component to display | ||
| return { | ||
| success: false, | ||
| error: errorMessage | ||
| error: message | ||
| } | ||
| } finally { | ||
| setIsLoading(false) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -36,7 +36,8 @@ import {API_ERROR_MESSAGE} from '@salesforce/retail-react-app/app/constants' | |
| import { | ||
| getAuthorizePasswordlessErrorMessage, | ||
| getPasswordResetErrorMessage, | ||
| getLoginPasswordlessErrorMessage | ||
| getLoginPasswordlessErrorMessage, | ||
| getPasskeyAuthenticateErrorMessage | ||
| } from '@salesforce/retail-react-app/app/utils/auth-utils' | ||
| import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation' | ||
| import {usePrevious} from '@salesforce/retail-react-app/app/hooks/use-previous' | ||
|
|
@@ -89,7 +90,6 @@ export const AuthModal = ({ | |
| const login = useAuthHelper(AuthHelpers.LoginRegisteredUserB2C) | ||
| const register = useAuthHelper(AuthHelpers.Register) | ||
| const {locale} = useMultiSite() | ||
| const config = getConfig() | ||
|
|
||
| const {getPasswordResetToken} = usePasswordReset() | ||
| const authorizePasswordlessLogin = useAuthHelper(AuthHelpers.AuthorizePasswordless) | ||
|
|
@@ -104,7 +104,7 @@ export const AuthModal = ({ | |
| ) | ||
| const mergeBasket = useShopperBasketsMutation('mergeBasket') | ||
|
|
||
| const {showToast} = usePasskeyRegistration() | ||
| const {showRegisterPasskeyToast} = usePasskeyRegistration() | ||
|
|
||
| const handlePasswordlessLogin = async (email) => { | ||
| try { | ||
|
|
@@ -239,8 +239,8 @@ export const AuthModal = ({ | |
| setCurrentView(initialView) | ||
| form.reset() | ||
| // Prompt user to login without username (discoverable credentials) | ||
| loginWithPasskey().catch((error) => { | ||
| // TODO W-21056536: Add error message handling | ||
| loginWithPasskey().catch(() => { | ||
| form.setError('global', {type: 'manual', message:formatMessage(API_ERROR_MESSAGE)}) | ||
| }) | ||
| } | ||
| }, [isOpen]) | ||
|
|
@@ -279,23 +279,8 @@ export const AuthModal = ({ | |
| onClose() | ||
| setIsOtpAuthOpen(false) | ||
|
|
||
| if (config?.app?.login?.passkey?.enabled) { | ||
| // Show passkey registration modal only if Webauthn feature flag is enabled and compatible with the browser | ||
| if ( | ||
| window.PublicKeyCredential && | ||
| window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable && | ||
| window.PublicKeyCredential.isConditionalMediationAvailable | ||
| ) { | ||
| Promise.all([ | ||
| window.PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable(), | ||
| window.PublicKeyCredential.isConditionalMediationAvailable() | ||
| ]).then((results) => { | ||
| if (results.every((r) => r === true)) { | ||
| showToast() | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
Comment on lines
-282
to
-298
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. moved this logic into |
||
| // Show passkey registration prompt if supported | ||
| showRegisterPasskeyToast() | ||
|
|
||
| // Show a toast only for those registed users returning to the site. | ||
| // Only show toast when customer data is available (user is logged in and data is loaded) | ||
|
|
@@ -429,7 +414,7 @@ AuthModal.propTypes = { | |
| */ | ||
| export const useAuthModal = (initialView = LOGIN_VIEW) => { | ||
| const {isOpen, onOpen, onClose} = useDisclosure() | ||
| const {passwordless = {}, social = {}, passkey = {}} = getConfig().app.login || {} | ||
| const {passwordless = {}, social = {}} = getConfig().app.login || {} | ||
|
|
||
| return { | ||
| initialView, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
during registration, any errors from the browser should tell the user that soemthing went wrong. Instead of throwing a custom error, we let the error be thrown automatically and handle it in the catch below