Skip to content
Merged
Show file tree
Hide file tree
Changes from 27 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
3791b65
Implement error handling for login with passkey in AuthModal and Logi…
hajinsuha1 Feb 17, 2026
8ec8140
Refactor passkey error handling in AuthModal and Login components; re…
hajinsuha1 Feb 17, 2026
98d8950
Add unit tests for passkey error message handling in auth-utils; incl…
hajinsuha1 Feb 17, 2026
315e3a1
fix unit tests by removing mocking AuthHelpers
hajinsuha1 Feb 17, 2026
1f45c06
Refactor passkey login tests to improve clarity and functionality; en…
hajinsuha1 Feb 18, 2026
54d6043
Enhance passkey login tests by refining error handling and improving …
hajinsuha1 Feb 18, 2026
b16b759
add error handling in passkey registration. add console error for deb…
hajinsuha1 Feb 18, 2026
767acfc
Refactor passkey registration handling in AuthModal and related compo…
hajinsuha1 Feb 18, 2026
1199fc6
Enhance passkey error handling in auth-utils by introducing new error…
hajinsuha1 Feb 18, 2026
977e9ea
use preview version for commerce-sdk-react
hajinsuha1 Feb 18, 2026
bbc4071
Revert "use preview version for commerce-sdk-react"
hajinsuha1 Feb 18, 2026
7c727b2
Enhance usePasskeyLogin hook to handle specific error responses grace…
hajinsuha1 Feb 19, 2026
a9bda52
Merge branch 'feature/webauthn-login' into W-21056536-error-handling-…
hajinsuha1 Feb 19, 2026
2e64f70
Refactor passkey registration and authentication error handling acros…
hajinsuha1 Feb 19, 2026
ca9c030
Merge branch 'W-21056536-error-handling-passkey-registration-and-logi…
hajinsuha1 Feb 19, 2026
cc79b74
Update comments in PasskeyRegistrationModal to correct step numbering…
hajinsuha1 Feb 19, 2026
303ed13
Merge branch 'feature/webauthn-login' into W-21056536-error-handling-…
hajinsuha1 Feb 19, 2026
b6f7f4f
lint
hajinsuha1 Feb 20, 2026
529b703
increase bundle size
hajinsuha1 Feb 20, 2026
1bf6075
fix commerce-sdk-react unit tests
hajinsuha1 Feb 20, 2026
f80c954
Merge branch 'feature/webauthn-login' into W-21056536-error-handling-…
hajinsuha1 Feb 20, 2026
e1a22ec
increase timeout to fix use-auth-modal unit tests
hajinsuha1 Feb 20, 2026
8271c9f
Merge branch 'feature/webauthn-login' into W-21056536-error-handling-…
hajinsuha1 Feb 20, 2026
faa5454
test commit (revert use-auth-modal to develop)
hajinsuha1 Feb 20, 2026
bba25a2
Revert "test commit (revert use-auth-modal to develop)"
hajinsuha1 Feb 21, 2026
eb67152
move renderWithProviders after getConfig is mocked in 'Allows passwor…
hajinsuha1 Feb 21, 2026
b2b9709
fix use-auth-modal race condition
hajinsuha1 Feb 21, 2026
e000ff0
Update error handling in PasskeyRegistrationModal to check for '401' …
hajinsuha1 Feb 23, 2026
22ad02c
Merge branch 'W-21056536-error-handling-passkey-registration-and-logi…
hajinsuha1 Feb 23, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,7 @@ describe('Shopper Login hooks', () => {
// These endpoints all return data in the response headers, rather than body, so they
// don't work well with the current implementation of mutation hooks.
'authenticateCustomer',
'authorizeWebauthnRegistration',
'finishWebauthnAuthentication',
'finishWebauthnUserRegistration',
'getTrustedAgentAuthorizationToken',
'startWebauthnAuthentication',
'startWebauthnUserRegistration'
'getTrustedAgentAuthorizationToken'
])
})
test('all mutations have cache update logic', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ 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
*/
Expand Down Expand Up @@ -73,13 +79,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)
}
Expand Down Expand Up @@ -107,18 +108,7 @@ const PasskeyRegistrationModal = ({isOpen, onClose}) => {

// 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
}
Comment on lines -110 to -121
Copy link
Collaborator Author

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

const credential = await navigator.credentials.create({publicKey})

if (!credential) {
throw new Error('Failed to create credential: user cancelled or operation failed')
Expand Down Expand Up @@ -160,17 +150,15 @@ const PasskeyRegistrationModal = ({isOpen, 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)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The 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 = /401/.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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -223,7 +223,7 @@ describe('PasskeyRegistrationModal', () => {
await user.click(registerButton)

await waitFor(() => {
expect(screen.getByText(errorMessage)).toBeInTheDocument()
expect(screen.getByText('Something went wrong. Try again!')).toBeInTheDocument()
})
})

Expand Down Expand Up @@ -457,7 +457,7 @@ describe('PasskeyRegistrationModal', () => {

expect(result).toEqual({
success: false,
error: errorMessage
error: 'Something went wrong. Try again!'
})

// Verify modals are not closed on error
Expand Down Expand Up @@ -498,7 +498,7 @@ describe('PasskeyRegistrationModal', () => {

expect(result).toEqual({
success: false,
error: 'WebAuthn API not available in this browser'
error: 'Something went wrong. Try again!'
})
})

Expand Down Expand Up @@ -537,7 +537,7 @@ describe('PasskeyRegistrationModal', () => {

expect(result).toEqual({
success: false,
error: 'Passkey registration was cancelled or timed out'
error: 'Something went wrong. Try again!'
})
})

Expand Down Expand Up @@ -573,7 +573,7 @@ describe('PasskeyRegistrationModal', () => {

expect(result).toEqual({
success: false,
error: 'Failed to create credential: user cancelled or operation failed'
error: 'Something went wrong. Try again!'
})
})

Expand Down Expand Up @@ -622,7 +622,7 @@ describe('PasskeyRegistrationModal', () => {

expect(result).toEqual({
success: false,
error: errorMessage
error: 'Something went wrong. Try again!'
})
})

Expand Down Expand Up @@ -661,7 +661,34 @@ describe('PasskeyRegistrationModal', () => {

expect(result).toEqual({
success: false,
error: 'Passkey registration was cancelled or timed out'
error: 'Something went wrong. Try again!'
})
})

test('returns INVALID_TOKEN_ERROR_MESSAGE when startWebauthnUserRegistration fails with 401', async () => {
const otpCode = '12345678'

mockStartWebauthnRegistration.mockRejectedValue(new Error('401'))
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

later if we touch this file again, we should update the tests to mock commerce-sdk-react using msw!


const {user} = renderWithProviders(
<PasskeyRegistrationModal isOpen={true} onClose={mockOnClose} />,
{
wrapperProps: {appConfig: mockConfig.app}
}
)

const registerButton = screen.getByText('Register Passkey')
await user.click(registerButton)

await waitFor(() => {
expect(otpVerificationHandler).toBeTruthy()
})

const result = await otpVerificationHandler(otpCode)

expect(result).toEqual({
success: false,
error: 'Invalid token, please try again.'
})
})
})
Expand Down
28 changes: 6 additions & 22 deletions packages/template-retail-react-app/app/hooks/use-auth-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,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)
Expand All @@ -104,7 +103,7 @@ export const AuthModal = ({
)
const mergeBasket = useShopperBasketsMutation('mergeBasket')

const {showToast} = usePasskeyRegistration()
const {showRegisterPasskeyToast} = usePasskeyRegistration()

const handlePasswordlessLogin = async (email) => {
try {
Expand Down Expand Up @@ -239,8 +238,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])
Expand Down Expand Up @@ -279,23 +278,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
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

moved this logic into showRegisterPasskeyToast as it's duplicated here and in the login page

// 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)
Expand Down Expand Up @@ -429,7 +413,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,
Expand Down
Loading