Skip to content

Commit 7c727b2

Browse files
committed
Enhance usePasskeyLogin hook to handle specific error responses gracefully, including early returns for 412 status. Improve passkey authentication error handling by adding console error logging in AuthModal and Login components.
1 parent bbc4071 commit 7c727b2

File tree

11 files changed

+137
-93
lines changed

11 files changed

+137
-93
lines changed

packages/template-retail-react-app/app/hooks/use-auth-modal.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,7 @@ export const AuthModal = ({
242242
loginWithPasskey().catch((error) => {
243243
const message = formatMessage(getPasskeyAuthenticateErrorMessage(error))
244244
form.setError('global', {type: 'manual', message})
245+
console.error('Error authenticating passkey:', error)
245246
})
246247
}
247248
}, [isOpen])

packages/template-retail-react-app/app/hooks/use-passkey-login.js

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,17 @@ export const usePasskeyLogin = () => {
3939
return
4040
}
4141

42-
const startWebauthnAuthenticationResponse = await startWebauthnAuthentication.mutateAsync(
43-
{}
44-
)
42+
let startWebauthnAuthenticationResponse
43+
try {
44+
startWebauthnAuthenticationResponse = await startWebauthnAuthentication.mutateAsync({})
45+
} catch (error) {
46+
// 412 is returned when user attempts to authenticate within 1 minute of a previous attempt
47+
// We return early in this case to avoid showing an error to the user
48+
if (error.response?.status === 412) {
49+
return
50+
}
51+
throw error
52+
}
4553

4654
// Transform response for WebAuthn API to send to navigator.credentials.get()
4755
// https://developer.mozilla.org/en-US/docs/Web/API/PublicKeyCredential/parseRequestOptionsFromJSON_static

packages/template-retail-react-app/app/hooks/use-passkey-login.test.js

Lines changed: 86 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,13 @@
77
import React from 'react'
88
import {rest} from 'msw'
99
import {fireEvent, screen, waitFor} from '@testing-library/react'
10-
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
10+
import {
11+
renderWithProviders,
12+
registerUserToken
13+
} from '@salesforce/retail-react-app/app/utils/test-utils'
1114
import {usePasskeyLogin} from '@salesforce/retail-react-app/app/hooks/use-passkey-login'
1215
import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
1316
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
14-
import {registerUserToken} from '@salesforce/retail-react-app/app/utils/test-utils'
1517

1618
const mockCredential = {
1719
id: 'test-credential-id',
@@ -78,9 +80,16 @@ const mockParseRequestOptionsFromJSON = jest.fn()
7880

7981
const MockComponent = () => {
8082
const {loginWithPasskey} = usePasskeyLogin()
83+
const [result, setResult] = React.useState(null)
84+
const handleClick = () => {
85+
loginWithPasskey()
86+
.then(() => setResult('resolved'))
87+
.catch(() => setResult('rejected'))
88+
}
8189
return (
8290
<div>
83-
<button data-testid="login-with-passkey" onClick={() => loginWithPasskey()} />
91+
<button data-testid="login-with-passkey" onClick={handleClick} />
92+
{result && <span data-testid="login-result">{result}</span>}
8493
</div>
8594
)
8695
}
@@ -261,26 +270,90 @@ describe('usePasskeyLogin', () => {
261270
})
262271

263272
test('returns early without error when NotAllowedError is thrown from navigator.credentials.get', async () => {
264-
// Create a NotAllowedError (typically thrown when user cancels passkey login)
273+
// Create a NotAllowedError (thrown when user cancels passkey login)
265274
const notAllowedError = new Error('User cancelled')
266275
notAllowedError.name = 'NotAllowedError'
267-
268-
// Mock navigator.credentials.get to throw NotAllowedError
269276
mockGetCredentials.mockRejectedValue(notAllowedError)
270277

271278
renderWithProviders(<MockComponent />)
272279

273280
const trigger = screen.getByTestId('login-with-passkey')
281+
fireEvent.click(trigger)
282+
283+
await waitFor(() => expect(mockGetCredentials).toHaveBeenCalled())
284+
await waitFor(() =>
285+
expect(screen.getByTestId('login-result')).toHaveTextContent('resolved')
286+
)
287+
})
288+
289+
test('returns early without error when 412 is returned from startWebauthnAuthentication', async () => {
290+
global.server.use(
291+
rest.post('*/oauth2/webauthn/authenticate/start', (req, res, ctx) => {
292+
return res(
293+
ctx.delay(0),
294+
ctx.status(412),
295+
ctx.json({message: 'Authenticate not started for: user@example.com"'})
296+
)
297+
})
298+
)
274299

275-
// Click the button - should not throw an error even though NotAllowedError is thrown
300+
renderWithProviders(<MockComponent />)
301+
302+
const trigger = screen.getByTestId('login-with-passkey')
276303
fireEvent.click(trigger)
277304

278-
// Wait for navigator.credentials.get to be called
279-
await waitFor(() => {
280-
expect(mockGetCredentials).toHaveBeenCalled()
281-
})
305+
expect(mockGetCredentials).not.toHaveBeenCalled()
306+
await waitFor(() =>
307+
expect(screen.getByTestId('login-result')).toHaveTextContent('resolved')
308+
)
309+
})
310+
311+
test('throws error when other error is returned from startWebauthnAuthentication', async () => {
312+
global.server.use(
313+
rest.post('*/oauth2/webauthn/authenticate/start', (req, res, ctx) => {
314+
return res(ctx.delay(0), ctx.status(500), ctx.json({message: '500 Error'}))
315+
})
316+
)
282317

283-
// Verify that no error message is displayed
284-
expect(screen.queryByText('Something went wrong. Try again!')).not.toBeInTheDocument()
318+
renderWithProviders(<MockComponent />)
319+
320+
const trigger = screen.getByTestId('login-with-passkey')
321+
fireEvent.click(trigger)
322+
323+
await waitFor(() =>
324+
expect(screen.getByTestId('login-result')).toHaveTextContent('rejected')
325+
)
326+
})
327+
328+
test('throws error when other error is returned from finishWebauthnAuthentication', async () => {
329+
global.server.use(
330+
rest.post('*/oauth2/webauthn/authenticate/finish', (req, res, ctx) => {
331+
return res(ctx.delay(0), ctx.status(500), ctx.json({message: '500 Error'}))
332+
})
333+
)
334+
335+
renderWithProviders(<MockComponent />)
336+
337+
const trigger = screen.getByTestId('login-with-passkey')
338+
fireEvent.click(trigger)
339+
340+
await waitFor(() =>
341+
expect(screen.getByTestId('login-result')).toHaveTextContent('rejected')
342+
)
343+
})
344+
345+
test('throws error when other error is returned from navigator.credentials.get', async () => {
346+
const networkError = new Error('NetworkError')
347+
networkError.name = 'NetworkError'
348+
mockGetCredentials.mockRejectedValue(networkError)
349+
350+
renderWithProviders(<MockComponent />)
351+
352+
const trigger = screen.getByTestId('login-with-passkey')
353+
fireEvent.click(trigger)
354+
355+
await waitFor(() =>
356+
expect(screen.getByTestId('login-result')).toHaveTextContent('rejected')
357+
)
285358
})
286359
})

packages/template-retail-react-app/app/pages/login/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,9 +193,9 @@ const Login = ({initialView = LOGIN_VIEW}) => {
193193

194194
useEffect(() => {
195195
loginWithPasskey().catch((error) => {
196-
console.error('Error authenticating passkey:', error)
197196
const message = formatMessage(getPasskeyAuthenticateErrorMessage(error))
198197
form.setError('global', {type: 'manual', message})
198+
console.error('Error authenticating passkey:', error)
199199
})
200200
}, [])
201201

packages/template-retail-react-app/app/static/translations/compiled/en-GB.json

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2091,6 +2091,12 @@
20912091
"value": "Invalid token, please try again."
20922092
}
20932093
],
2094+
"global.error.passkey_feature_unavailable": [
2095+
{
2096+
"type": 0,
2097+
"value": "The passkey feature is currently unavailable"
2098+
}
2099+
],
20942100
"global.error.something_went_wrong": [
20952101
{
20962102
"type": 0,
@@ -3255,18 +3261,6 @@
32553261
"value": "Register Passkey"
32563262
}
32573263
],
3258-
"passkey_registration.modal.error.authorize_failed": [
3259-
{
3260-
"type": 0,
3261-
"value": "Failed to authorize passkey registration"
3262-
}
3263-
],
3264-
"passkey_registration.modal.error.registration_failed": [
3265-
{
3266-
"type": 0,
3267-
"value": "Failed to register passkey"
3268-
}
3269-
],
32703264
"passkey_registration.modal.label.nickname": [
32713265
{
32723266
"type": 0,

packages/template-retail-react-app/app/static/translations/compiled/en-US.json

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2091,6 +2091,12 @@
20912091
"value": "Invalid token, please try again."
20922092
}
20932093
],
2094+
"global.error.passkey_feature_unavailable": [
2095+
{
2096+
"type": 0,
2097+
"value": "The passkey feature is currently unavailable"
2098+
}
2099+
],
20942100
"global.error.something_went_wrong": [
20952101
{
20962102
"type": 0,
@@ -3255,18 +3261,6 @@
32553261
"value": "Register Passkey"
32563262
}
32573263
],
3258-
"passkey_registration.modal.error.authorize_failed": [
3259-
{
3260-
"type": 0,
3261-
"value": "Failed to authorize passkey registration"
3262-
}
3263-
],
3264-
"passkey_registration.modal.error.registration_failed": [
3265-
{
3266-
"type": 0,
3267-
"value": "Failed to register passkey"
3268-
}
3269-
],
32703264
"passkey_registration.modal.label.nickname": [
32713265
{
32723266
"type": 0,

packages/template-retail-react-app/app/static/translations/compiled/en-XA.json

Lines changed: 14 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4347,6 +4347,20 @@
43474347
"value": "]"
43484348
}
43494349
],
4350+
"global.error.passkey_feature_unavailable": [
4351+
{
4352+
"type": 0,
4353+
"value": "["
4354+
},
4355+
{
4356+
"type": 0,
4357+
"value": "Ŧħḗḗ ƥȧȧşşķḗḗẏ ƒḗḗȧȧŧŭŭřḗḗ īş ƈŭŭřřḗḗƞŧŀẏ ŭŭƞȧȧṽȧȧīŀȧȧƀŀḗḗ"
4358+
},
4359+
{
4360+
"type": 0,
4361+
"value": "]"
4362+
}
4363+
],
43504364
"global.error.something_went_wrong": [
43514365
{
43524366
"type": 0,
@@ -6847,34 +6861,6 @@
68476861
"value": "]"
68486862
}
68496863
],
6850-
"passkey_registration.modal.error.authorize_failed": [
6851-
{
6852-
"type": 0,
6853-
"value": "["
6854-
},
6855-
{
6856-
"type": 0,
6857-
"value": "Ƒȧȧīŀḗḗḓ ŧǿǿ ȧȧŭŭŧħǿǿřīẑḗḗ ƥȧȧşşķḗḗẏ řḗḗɠīşŧřȧȧŧīǿǿƞ"
6858-
},
6859-
{
6860-
"type": 0,
6861-
"value": "]"
6862-
}
6863-
],
6864-
"passkey_registration.modal.error.registration_failed": [
6865-
{
6866-
"type": 0,
6867-
"value": "["
6868-
},
6869-
{
6870-
"type": 0,
6871-
"value": "Ƒȧȧīŀḗḗḓ ŧǿǿ řḗḗɠīşŧḗḗř ƥȧȧşşķḗḗẏ"
6872-
},
6873-
{
6874-
"type": 0,
6875-
"value": "]"
6876-
}
6877-
],
68786864
"passkey_registration.modal.label.nickname": [
68796865
{
68806866
"type": 0,

packages/template-retail-react-app/app/utils/auth-utils.js

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,6 @@ export const PASSKEY_FEATURE_UNAVAILABLE_ERROR_MESSAGE = defineMessage({
3333
id: 'global.error.passkey_feature_unavailable',
3434
defaultMessage: 'The passkey feature is currently unavailable'
3535
})
36-
export const PASSKEY_AUTHENTICATION_API_ERROR_MESSAGE = defineMessage({
37-
id: 'global.error.passkey_api_error',
38-
defaultMessage:
39-
'Something went wrong while authenticating your passkey. Try again or use a different login method.'
40-
})
4136

4237
// Shared error patterns for token-based auth features (passwordless login, password reset)
4338
const TOKEN_BASED_AUTH_FEATURE_UNAVAILABLE_ERRORS = [
@@ -137,7 +132,7 @@ export const getPasskeyAuthenticateErrorMessage = (error) => {
137132
if (error.response?.status === 400 || error.response?.status === 401) {
138133
return PASSKEY_FEATURE_UNAVAILABLE_ERROR_MESSAGE
139134
}
140-
return PASSKEY_AUTHENTICATION_API_ERROR_MESSAGE
135+
return API_ERROR_MESSAGE
141136
}
142137

143138
/**

packages/template-retail-react-app/app/utils/auth-utils.test.js

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,7 @@ import {
1818
getPasskeyRegistrationErrorMessage,
1919
TOO_MANY_LOGIN_ATTEMPTS_ERROR_MESSAGE,
2020
TOO_MANY_PASSWORD_RESET_ATTEMPTS_ERROR_MESSAGE,
21-
PASSKEY_FEATURE_UNAVAILABLE_ERROR_MESSAGE,
22-
PASSKEY_AUTHENTICATION_API_ERROR_MESSAGE
21+
PASSKEY_FEATURE_UNAVAILABLE_ERROR_MESSAGE
2322
} from '@salesforce/retail-react-app/app/utils/auth-utils'
2423

2524
afterEach(() => {
@@ -124,10 +123,10 @@ describe('getPasskeyAuthenticateErrorMessage', () => {
124123
test.each([
125124
[{response: {status: 400}}, PASSKEY_FEATURE_UNAVAILABLE_ERROR_MESSAGE],
126125
[{response: {status: 401}}, PASSKEY_FEATURE_UNAVAILABLE_ERROR_MESSAGE],
127-
[{response: {status: 403}}, PASSKEY_AUTHENTICATION_API_ERROR_MESSAGE],
128-
[{response: {status: 412}}, PASSKEY_AUTHENTICATION_API_ERROR_MESSAGE],
129-
[{response: {status: 500}}, PASSKEY_AUTHENTICATION_API_ERROR_MESSAGE],
130-
[new Error('Network Error'), PASSKEY_AUTHENTICATION_API_ERROR_MESSAGE]
126+
[{response: {status: 403}}, API_ERROR_MESSAGE],
127+
[{response: {status: 412}}, API_ERROR_MESSAGE],
128+
[{response: {status: 500}}, API_ERROR_MESSAGE],
129+
[new Error('Network Error'), API_ERROR_MESSAGE]
131130
])('maps passkey error to the correct message descriptor', (error, expectedMessage) => {
132131
expect(getPasskeyAuthenticateErrorMessage(error)).toBe(expectedMessage)
133132
})

packages/template-retail-react-app/translations/en-GB.json

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -849,6 +849,9 @@
849849
"global.error.invalid_token": {
850850
"defaultMessage": "Invalid token, please try again."
851851
},
852+
"global.error.passkey_feature_unavailable": {
853+
"defaultMessage": "The passkey feature is currently unavailable"
854+
},
852855
"global.error.something_went_wrong": {
853856
"defaultMessage": "Something went wrong. Try again!"
854857
},
@@ -1354,12 +1357,6 @@
13541357
"passkey_registration.modal.button.register": {
13551358
"defaultMessage": "Register Passkey"
13561359
},
1357-
"passkey_registration.modal.error.authorize_failed": {
1358-
"defaultMessage": "Failed to authorize passkey registration"
1359-
},
1360-
"passkey_registration.modal.error.registration_failed": {
1361-
"defaultMessage": "Failed to register passkey"
1362-
},
13631360
"passkey_registration.modal.label.nickname": {
13641361
"defaultMessage": "Passkey Nickname (optional)"
13651362
},

0 commit comments

Comments
 (0)