Skip to content

Commit 3254cf4

Browse files
committed
Enhance passkey login handling in ContactInfo component
- Added `abortPasskeyLogin` to cleanly abort ongoing passkey login requests when the component unmounts or when the user logs in with a password. - Updated the logic to only prompt for passkey login when the user is a guest, improving user experience. - Enhanced tests to verify the correct behavior of the passkey login flow, including cleanup on unmount and handling user login transitions.
1 parent 9c8717a commit 3254cf4

File tree

2 files changed

+81
-7
lines changed

2 files changed

+81
-7
lines changed

packages/template-retail-react-app/app/pages/checkout/partials/contact-info.jsx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id
6262
const authorizePasswordlessLogin = useAuthHelper(AuthHelpers.AuthorizePasswordless)
6363
const updateCustomerForBasket = useShopperBasketsMutation('updateCustomerForBasket')
6464
const mergeBasket = useShopperBasketsMutation('mergeBasket')
65-
const {loginWithPasskey} = usePasskeyLogin()
65+
const {loginWithPasskey, abortPasskeyLogin} = usePasskeyLogin()
6666

6767
const {step, STEPS, goToStep, goToNextStep} = useCheckout()
6868

@@ -169,10 +169,16 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id
169169
}
170170
}
171171

172-
if (!customer.isRegistered) {
172+
// Only prompt for passkey when we know the user is a guest (not loading, not registered)
173+
if (customer && !customer.isRegistered) {
173174
handlePasskeyLogin()
174175
}
175-
}, [customer.isRegistered])
176+
177+
// Cleanup: abort passkey login when navigating away from checkout
178+
return () => {
179+
abortPasskeyLogin()
180+
}
181+
}, [customer?.isRegistered])
176182

177183
const onPasswordlessLoginClick = async (e) => {
178184
const isValid = await form.trigger('email')

packages/template-retail-react-app/app/pages/checkout/partials/contact-info.test.js

Lines changed: 72 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,14 @@ jest.mock('@salesforce/retail-react-app/app/hooks/use-current-basket', () => {
7272
})
7373

7474
const mockLoginWithPasskey = jest.fn().mockResolvedValue(undefined)
75+
const mockAbortPasskeyLogin = jest.fn()
7576

7677
jest.mock('@salesforce/retail-react-app/app/hooks/use-passkey-login', () => {
7778
return {
7879
__esModule: true,
7980
usePasskeyLogin: jest.fn(() => ({
80-
loginWithPasskey: mockLoginWithPasskey
81+
loginWithPasskey: mockLoginWithPasskey,
82+
abortPasskeyLogin: mockAbortPasskeyLogin
8183
}))
8284
}
8385
})
@@ -455,8 +457,8 @@ describe('passkey login', () => {
455457
})
456458
})
457459

458-
test('does not call loginWithPasskey when customer is registered', async () => {
459-
// Mock registered customer
460+
test('does not prompt for passkey when user is already logged in', async () => {
461+
// When customer is registered, we must not trigger passkey (no prompt)
460462
mockUseCurrentCustomer.mockReturnValue({
461463
data: {
462464
isRegistered: true,
@@ -466,9 +468,75 @@ describe('passkey login', () => {
466468

467469
renderWithProviders(<ContactInfo />)
468470

469-
// Wait a bit to ensure useEffect has run
470471
await waitFor(() => {
471472
expect(mockLoginWithPasskey).not.toHaveBeenCalled()
472473
})
473474
})
475+
476+
test('calls abortPasskeyLogin when component unmounts', async () => {
477+
const {unmount} = renderWithProviders(<ContactInfo />)
478+
479+
// Wait for passkey login to be triggered
480+
await waitFor(() => {
481+
expect(mockLoginWithPasskey).toHaveBeenCalled()
482+
})
483+
484+
// Verify abort hasn't been called yet
485+
expect(mockAbortPasskeyLogin).not.toHaveBeenCalled()
486+
487+
// Unmount the component (simulates navigating away)
488+
unmount()
489+
490+
// Verify abort was called during cleanup
491+
expect(mockAbortPasskeyLogin).toHaveBeenCalled()
492+
})
493+
494+
test('Passkey prompt is aborted when user logs in with password', async () => {
495+
// This test verifies that when the user logs in with password while the passkey
496+
// flow is pending, the useEffect cleanup runs (customer becomes registered) and
497+
// abortPasskeyLogin is called.
498+
//
499+
// The actual abort behavior is tested in use-passkey-login.test.js.
500+
501+
let customerRegistered = false
502+
mockUseCurrentCustomer.mockImplementation(() => ({
503+
data: {isRegistered: customerRegistered}
504+
}))
505+
506+
// When login succeeds, mark customer as registered so the next render triggers
507+
// the useEffect cleanup (abortPasskeyLogin)
508+
global.server.use(
509+
rest.post('*/oauth2/login', (req, res, ctx) => {
510+
customerRegistered = true
511+
return res(ctx.delay(0), ctx.status(200), ctx.json(mockedRegisteredCustomer))
512+
})
513+
)
514+
515+
const {user} = renderWithProviders(<ContactInfo />)
516+
517+
// Wait for passkey login to be triggered (passkey prompt is "pending")
518+
await waitFor(() => {
519+
expect(mockLoginWithPasskey).toHaveBeenCalled()
520+
})
521+
expect(mockAbortPasskeyLogin).not.toHaveBeenCalled()
522+
523+
// User switches to login and logs in with password
524+
const trigger = screen.getByText(/Already have an account\? Log in/i)
525+
await user.click(trigger)
526+
527+
await user.type(screen.getByLabelText('Email'), validEmail)
528+
await user.type(screen.getByLabelText('Password'), password)
529+
530+
const loginButton = screen.getByText('Log In')
531+
await user.click(loginButton)
532+
533+
// Login succeeds; component re-renders with isRegistered: true; cleanup runs
534+
await waitFor(
535+
() => {
536+
expect(mockAbortPasskeyLogin).toHaveBeenCalled()
537+
expect(mockGoToNextStep).toHaveBeenCalled()
538+
},
539+
{timeout: 3000}
540+
)
541+
})
474542
})

0 commit comments

Comments
 (0)