Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions packages/commerce-sdk-react/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
- Update `authorizePasswordless` to pass locale and simplify mode selection to respect user's explicit mode choice while still defaulting to callback mode for backward compatibility [#3492](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3492)
- Update `getPasswordResetToken` to default locale to the one in CommerceApiProvider and pass callback_uri and idp_name only when they are defined [#3547](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3547)
- Update `resetPassword` to default hint to `cross_device` and pass code_verifier only when it is defined [#3547](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3547)
- Update `getPasswordResetToken` to return raw response and throw an error with the error message if the status code is not 200 [#3574](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3574)

## v4.3.0 (Dec 17, 2025)

Expand Down
21 changes: 20 additions & 1 deletion packages/commerce-sdk-react/src/auth/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1066,14 +1066,33 @@ describe('Auth', () => {
const getPasswordResetTokenSpy = jest.spyOn((auth as any).client, 'getPasswordResetToken')
getPasswordResetTokenSpy.mockResolvedValueOnce(mockResponse)

await auth.getPasswordResetToken({user_id: 'user@example.com'} as any)
await auth.getPasswordResetToken({
user_id: 'user@example.com',
mode: 'email',
channel_id: 'channel_id'
})

expect(getPasswordResetTokenSpy).toHaveBeenCalled()
const callArgs = getPasswordResetTokenSpy.mock.calls[0][0] as any
expect(callArgs.headers.Authorization).toBeTruthy()
expect(callArgs.headers.Authorization).toContain('Basic ')
})

test('getPasswordResetToken throws error on non-200 response', async () => {
const auth = new Auth(configSLASPrivate)

const mockErrorResponse = {
status: 400,
json: jest.fn().mockResolvedValue({message: 'Invalid request'})
}
const getPasswordResetTokenSpy = jest.spyOn((auth as any).client, 'getPasswordResetToken')
getPasswordResetTokenSpy.mockReturnValueOnce(mockErrorResponse)

await expect(
auth.getPasswordResetToken({user_id: 'userid', mode: 'email', channel_id: 'channel_id'})
).rejects.toThrow('400 Invalid request')
})

test.each([
[
'with all parameters specified',
Expand Down
6 changes: 5 additions & 1 deletion packages/commerce-sdk-react/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1352,7 +1352,11 @@ class Auth {
)}`
}

const res = await slasClient.getPasswordResetToken(options)
const res = await slasClient.getPasswordResetToken(options, true)
if (res && res.status !== 200) {
const errorData = await res.json()
throw new Error(`${res.status} ${String(errorData.message)}`)
}
Comment on lines +1355 to +1359
Copy link
Collaborator Author

@hajinsuha1 hajinsuha1 Jan 14, 2026

Choose a reason for hiding this comment

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

calling getPasswordResetToken(options, true) returns the rawResponse which was need to pass the error message to retail-react-app for determining which error message to display

return res
}

Expand Down
2 changes: 1 addition & 1 deletion packages/template-retail-react-app/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
## v8.4.0-dev (Dec 17, 2025)
- [Feature] Add `fuzzyPathMatching` to reduce computational overhead of route generation at time of application load [#3530](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3530)
- [Bugfix] Fix Passwordless Login landingPath, Reset Password landingPath, and Social Login redirectUri value in config not being used [#3560](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3560)
- Update passwordless login and password reset to use email mode by default. The mode can now be configured across the login page, auth modal, and checkout page [#3492](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3492) [#3547](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3547)
- Update passwordless login and password reset to use email mode by default. The mode can now be configured across the login page, auth modal, and checkout page [#3525](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3525)
- Update "Continue Securely" button text to "Continue" for passwordless login [#3556](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3556)

## v8.3.0 (Dec 17, 2025)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
import React from 'react'
import PropTypes from 'prop-types'
import {FormattedMessage} from 'react-intl'
import {Button, Stack, Text} from '@salesforce/retail-react-app/app/components/shared/ui'
import {
Button,
Stack,
Text,
Alert,
AlertIcon
} from '@salesforce/retail-react-app/app/components/shared/ui'
import {BrandLogo} from '@salesforce/retail-react-app/app/components/icons'

const PasswordlessEmailConfirmation = ({form, submitForm, email = ''}) => {
Expand Down Expand Up @@ -40,6 +46,14 @@ const PasswordlessEmailConfirmation = ({form, submitForm, email = ''}) => {
id="auth_modal.check_email.title.check_your_email"
/>
</Text>
{form.formState.errors?.global && (
<Alert status="error">
<AlertIcon color="red.500" boxSize={4} />
<Text fontSize="sm" ml={3}>
{form.formState.errors.global.message}
</Text>
</Alert>
)}
Comment on lines +49 to +56
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

This has been added to the email confirmation page now that we need to display a "Too many requests" error

Image

<Stack spacing={10}>
<Text align="center" fontSize="md" id="email-confirmation-desc">
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,21 @@ test('renders PasswordlessEmailConfirmation component with passed email', () =>
renderWithProviders(<WrapperComponent email={email} />)
expect(screen.getByText(email)).toBeInTheDocument()
})

test('displays error message when form has global error', () => {
const WrapperWithError = () => {
const form = useForm()
const formWithError = {
...form,
formState: {
...form.formState,
errors: {
global: {message: 'test error'}
}
}
}
return <PasswordlessEmailConfirmation form={formWithError} />
}
renderWithProviders(<WrapperWithError />)
expect(screen.getByText('test error')).toBeInTheDocument()
})
6 changes: 0 additions & 6 deletions packages/template-retail-react-app/app/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -251,12 +251,6 @@ export const LOGIN_TYPES = {
SOCIAL: 'social'
}

export const PASSWORDLESS_ERROR_MESSAGES = [
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

this has been moved to auth-utils.js above and renamed to PASSWORDLESS_FEATURE_UNAVAILABLE_ERRORS as it is only used there

/callback_uri doesn't match/i,
/passwordless permissions error/i,
/client secret is not provided/i
]

export const INVALID_TOKEN_ERROR = /invalid token/i

/**
Expand Down
17 changes: 6 additions & 11 deletions packages/template-retail-react-app/app/hooks/use-auth-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@ import RegisterForm from '@salesforce/retail-react-app/app/components/register'
import PasswordlessEmailConfirmation from '@salesforce/retail-react-app/app/components/email-confirmation/index'
import {noop} from '@salesforce/retail-react-app/app/utils/utils'
import {
API_ERROR_MESSAGE,
FEATURE_UNAVAILABLE_ERROR_MESSAGE,
PASSWORDLESS_ERROR_MESSAGES
} from '@salesforce/retail-react-app/app/constants'
getPasswordlessErrorMessage,
getPasswordResetErrorMessage
} from '@salesforce/retail-react-app/app/utils/auth-utils'
import {API_ERROR_MESSAGE} from '@salesforce/retail-react-app/app/constants'
import useNavigation from '@salesforce/retail-react-app/app/hooks/use-navigation'
import {usePrevious} from '@salesforce/retail-react-app/app/hooks/use-previous'
import {usePasswordReset} from '@salesforce/retail-react-app/app/hooks/use-password-reset'
Expand Down Expand Up @@ -108,9 +108,7 @@ export const AuthModal = ({
})
setCurrentView(EMAIL_VIEW)
} catch (error) {
const message = PASSWORDLESS_ERROR_MESSAGES.some((msg) => msg.test(error.message))
? formatMessage(FEATURE_UNAVAILABLE_ERROR_MESSAGE)
: formatMessage(API_ERROR_MESSAGE)
const message = formatMessage(getPasswordlessErrorMessage(error.message))
form.setError('global', {type: 'manual', message})
}
}
Expand Down Expand Up @@ -185,10 +183,7 @@ export const AuthModal = ({
try {
await getPasswordResetToken(data.email)
} catch (e) {
const message =
e.response?.status === 400
? formatMessage(FEATURE_UNAVAILABLE_ERROR_MESSAGE)
: formatMessage(API_ERROR_MESSAGE)
const message = formatMessage(getPasswordResetErrorMessage(e.message))
form.setError('global', {type: 'manual', message})
}
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,19 @@ jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => ({
getConfig: jest.fn()
}))

const mockGetPasswordResetToken = jest.fn()
jest.mock('@salesforce/retail-react-app/app/hooks/use-password-reset', () => {
const originalModule = jest.requireActual(
'@salesforce/retail-react-app/app/hooks/use-password-reset'
)
return {
usePasswordReset: jest.fn(() => ({
...originalModule,
getPasswordResetToken: mockGetPasswordResetToken
}))
}
})

let authModal = undefined
const MockedComponent = (props) => {
const {initialView, isPasswordlessEnabled = false} = props
Expand Down Expand Up @@ -357,6 +370,45 @@ describe('Passwordless enabled', () => {
callbackURI: 'https://callback.com/passwordless?redirectUrl=/'
})
})

test.each([
['no callback_uri is registered for client', 'This feature is not currently available.'],
[
'Too many login requests were made. Please try again later.',
'Too many requests. For your security, please wait 10 minutes before trying again.'
],
['unexpected error message', 'Something went wrong. Try again!']
])(
'displays correct error message when passwordless login fails with "%s"',
async (apiErrorMessage, expectedMessage) => {
const {user} = renderWithProviders(<MockedComponent isPasswordlessEnabled={true} />)
const validEmail = 'test@salesforce.com'

// Mock the error
mockAuthHelperFunctions[
AuthHelpers.AuthorizePasswordless
].mutateAsync.mockImplementation(() => {
throw new Error(apiErrorMessage)
})

// open the modal
const trigger = screen.getByText(/open modal/i)
await user.click(trigger)

await waitFor(() => {
expect(screen.getByText(/Continue/i)).toBeInTheDocument()
})

// enter email and submit
await user.type(screen.getByLabelText('Email'), validEmail)
await user.click(screen.getByText(/Continue/i))

// Verify error message is displayed
await waitFor(() => {
expect(screen.getByText(expectedMessage)).toBeInTheDocument()
})
}
)
})

// TODO: Fix flaky/broken test
Expand Down Expand Up @@ -592,4 +644,43 @@ describe('Reset password', function () {
// check that the modal is closed
expect(authModal.isOpen).toBe(false)
})

test.each([
['no callback_uri is registered for client', 'This feature is not currently available.'],
[
'Too many password reset requests were made. Please try again later.',
'Too many requests. For your security, please wait 10 minutes before trying again.'
],
['unexpected error message', 'Something went wrong. Try again!']
])(
'displays correct error message when password reset fails with "%s"',
async (apiErrorMessage, expectedMessage) => {
// Mock getPasswordResetToken to throw error
mockGetPasswordResetToken.mockRejectedValue(new Error(apiErrorMessage))

const {user} = renderWithProviders(<MockedComponent initialView="password" />, {
wrapperProps: {
bypassAuth: false
}
})

// open the modal
const trigger = screen.getByText(/open modal/i)
await user.click(trigger)

// Wait for password reset form
let resetPwForm = await screen.findByTestId('sf-auth-modal-form')
expect(resetPwForm).toBeInTheDocument()
const withinForm = within(resetPwForm)

// Enter email and submit
await user.type(withinForm.getByLabelText('Email'), 'foo@test.com')
await user.click(withinForm.getByText(/reset password/i))

// Verify error message is displayed
await waitFor(() => {
expect(withinForm.getByText(expectedMessage)).toBeInTheDocument()
})
}
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -45,11 +45,7 @@ import {useAppOrigin} from '@salesforce/retail-react-app/app/hooks/use-app-origi
import {AuthHelpers, useAuthHelper, useShopperBasketsMutation} from '@salesforce/commerce-sdk-react'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
import {buildAbsoluteUrl} from '@salesforce/retail-react-app/app/utils/url'
import {
API_ERROR_MESSAGE,
FEATURE_UNAVAILABLE_ERROR_MESSAGE,
PASSWORDLESS_ERROR_MESSAGES
} from '@salesforce/retail-react-app/app/constants'
import {getPasswordlessErrorMessage} from '@salesforce/retail-react-app/app/utils/auth-utils'

const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, idps = []}) => {
const {formatMessage} = useIntl()
Expand Down Expand Up @@ -94,9 +90,7 @@ const ContactInfo = ({isSocialEnabled = false, isPasswordlessEnabled = false, id
setAuthModalView(EMAIL_VIEW)
authModal.onOpen()
} catch (error) {
const message = PASSWORDLESS_ERROR_MESSAGES.some((msg) => msg.test(error.message))
? formatMessage(FEATURE_UNAVAILABLE_ERROR_MESSAGE)
: formatMessage(API_ERROR_MESSAGE)
const message = formatMessage(getPasswordlessErrorMessage(error.message))
setError(message)
}
}
Expand Down
9 changes: 3 additions & 6 deletions packages/template-retail-react-app/app/pages/login/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,11 @@ import PasswordlessEmailConfirmation from '@salesforce/retail-react-app/app/comp
import {
API_ERROR_MESSAGE,
INVALID_TOKEN_ERROR,
INVALID_TOKEN_ERROR_MESSAGE,
FEATURE_UNAVAILABLE_ERROR_MESSAGE,
PASSWORDLESS_ERROR_MESSAGES
INVALID_TOKEN_ERROR_MESSAGE
} from '@salesforce/retail-react-app/app/constants'
import {usePrevious} from '@salesforce/retail-react-app/app/hooks/use-previous'
import {isServer, noop} from '@salesforce/retail-react-app/app/utils/utils'
import {getPasswordlessErrorMessage} from '@salesforce/retail-react-app/app/utils/auth-utils'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'

const LOGIN_ERROR_MESSAGE = defineMessage({
Expand Down Expand Up @@ -113,9 +112,7 @@ const Login = ({initialView = LOGIN_VIEW}) => {
setPasswordlessLoginEmail(email)
setCurrentView(EMAIL_VIEW)
} catch (error) {
const message = PASSWORDLESS_ERROR_MESSAGES.some((msg) => msg.test(error.message))
? formatMessage(FEATURE_UNAVAILABLE_ERROR_MESSAGE)
: formatMessage(API_ERROR_MESSAGE)
const message = formatMessage(getPasswordlessErrorMessage(error.message))
form.setError('global', {type: 'manual', message})
}
}
Expand Down
39 changes: 36 additions & 3 deletions packages/template-retail-react-app/app/pages/login/index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -334,9 +334,6 @@ describe('Passwordless login tests', () => {
beforeEach(() => {
// Clear the mock before each test
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync.mockClear()
})

test('allows passwordless login', async () => {
getConfig.mockReturnValue({
app: {
...mockConfig.app,
Expand All @@ -348,6 +345,9 @@ describe('Passwordless login tests', () => {
}
}
})
})

test('allows passwordless login', async () => {
const {user} = renderWithProviders(<MockedComponent />, {
wrapperProps: {
siteAlias: 'uk',
Expand Down Expand Up @@ -391,4 +391,37 @@ describe('Passwordless login tests', () => {
mode: 'email'
})
})

test.each([
[
"callback_uri doesn't match the registered callbacks",
'This feature is not currently available.'
],
[
'PasswordLess Permissions Error for clientId:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
'This feature is not currently available.'
],
['client secret is not provided', 'This feature is not currently available.'],
['unexpected error message', 'Something went wrong. Try again!']
])(
'displays correct error message when passwordless login fails with "%s"',
async (apiErrorMessage, expectedMessage) => {
mockAuthHelperFunctions[
AuthHelpers.AuthorizePasswordless
].mutateAsync.mockImplementation(() => {
throw new Error(apiErrorMessage)
})
const {user} = renderWithProviders(<MockedComponent />, {
wrapperProps: {
siteAlias: 'uk',
locale: {id: 'en-GB'},
appConfig: mockConfig.app,
bypassAuth: false
}
})
await user.type(screen.getByLabelText('Email'), 'customer@test.com')
await user.click(screen.getByRole('button', {name: /Continue/i}))
expect(screen.getByText(expectedMessage)).toBeInTheDocument()
}
)
})
Loading
Loading