diff --git a/packages/commerce-sdk-react/CHANGELOG.md b/packages/commerce-sdk-react/CHANGELOG.md
index ad58c918fd..e6bb618451 100644
--- a/packages/commerce-sdk-react/CHANGELOG.md
+++ b/packages/commerce-sdk-react/CHANGELOG.md
@@ -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)
diff --git a/packages/commerce-sdk-react/src/auth/index.test.ts b/packages/commerce-sdk-react/src/auth/index.test.ts
index 038ba645c2..32aba6aad3 100644
--- a/packages/commerce-sdk-react/src/auth/index.test.ts
+++ b/packages/commerce-sdk-react/src/auth/index.test.ts
@@ -1066,7 +1066,11 @@ 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
@@ -1074,6 +1078,21 @@ describe('Auth', () => {
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',
diff --git a/packages/commerce-sdk-react/src/auth/index.ts b/packages/commerce-sdk-react/src/auth/index.ts
index 8f8e30599a..a604166c1a 100644
--- a/packages/commerce-sdk-react/src/auth/index.ts
+++ b/packages/commerce-sdk-react/src/auth/index.ts
@@ -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)}`)
+ }
return res
}
diff --git a/packages/template-retail-react-app/CHANGELOG.md b/packages/template-retail-react-app/CHANGELOG.md
index 9e88a80dc0..3cf4708bca 100644
--- a/packages/template-retail-react-app/CHANGELOG.md
+++ b/packages/template-retail-react-app/CHANGELOG.md
@@ -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)
diff --git a/packages/template-retail-react-app/app/components/email-confirmation/index.jsx b/packages/template-retail-react-app/app/components/email-confirmation/index.jsx
index 91f65abddd..dde4cba95f 100644
--- a/packages/template-retail-react-app/app/components/email-confirmation/index.jsx
+++ b/packages/template-retail-react-app/app/components/email-confirmation/index.jsx
@@ -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 = ''}) => {
@@ -40,6 +46,14 @@ const PasswordlessEmailConfirmation = ({form, submitForm, email = ''}) => {
id="auth_modal.check_email.title.check_your_email"
/>
+ {form.formState.errors?.global && (
+
+
+
+ {form.formState.errors.global.message}
+
+
+ )}
renderWithProviders()
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
+ }
+ renderWithProviders()
+ expect(screen.getByText('test error')).toBeInTheDocument()
+})
diff --git a/packages/template-retail-react-app/app/constants.js b/packages/template-retail-react-app/app/constants.js
index f5045382c6..4765905116 100644
--- a/packages/template-retail-react-app/app/constants.js
+++ b/packages/template-retail-react-app/app/constants.js
@@ -251,12 +251,6 @@ export const LOGIN_TYPES = {
SOCIAL: 'social'
}
-export const PASSWORDLESS_ERROR_MESSAGES = [
- /callback_uri doesn't match/i,
- /passwordless permissions error/i,
- /client secret is not provided/i
-]
-
export const INVALID_TOKEN_ERROR = /invalid token/i
/**
diff --git a/packages/template-retail-react-app/app/hooks/use-auth-modal.js b/packages/template-retail-react-app/app/hooks/use-auth-modal.js
index 6d828669ad..00672a1522 100644
--- a/packages/template-retail-react-app/app/hooks/use-auth-modal.js
+++ b/packages/template-retail-react-app/app/hooks/use-auth-modal.js
@@ -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'
@@ -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})
}
}
@@ -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})
}
},
diff --git a/packages/template-retail-react-app/app/hooks/use-auth-modal.test.js b/packages/template-retail-react-app/app/hooks/use-auth-modal.test.js
index 6325045d0b..a2cc2cd6aa 100644
--- a/packages/template-retail-react-app/app/hooks/use-auth-modal.test.js
+++ b/packages/template-retail-react-app/app/hooks/use-auth-modal.test.js
@@ -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
@@ -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()
+ 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
@@ -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(, {
+ 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()
+ })
+ }
+ )
})
diff --git a/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.jsx b/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.jsx
index c49ad3743b..16f4d21d5a 100644
--- a/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.jsx
+++ b/packages/template-retail-react-app/app/pages/checkout/partials/contact-info.jsx
@@ -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()
@@ -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)
}
}
diff --git a/packages/template-retail-react-app/app/pages/login/index.jsx b/packages/template-retail-react-app/app/pages/login/index.jsx
index f000ce5c6b..2330366bfe 100644
--- a/packages/template-retail-react-app/app/pages/login/index.jsx
+++ b/packages/template-retail-react-app/app/pages/login/index.jsx
@@ -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({
@@ -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})
}
}
diff --git a/packages/template-retail-react-app/app/pages/login/index.test.js b/packages/template-retail-react-app/app/pages/login/index.test.js
index 1578a485a0..f9c7ac1c37 100644
--- a/packages/template-retail-react-app/app/pages/login/index.test.js
+++ b/packages/template-retail-react-app/app/pages/login/index.test.js
@@ -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,
@@ -348,6 +345,9 @@ describe('Passwordless login tests', () => {
}
}
})
+ })
+
+ test('allows passwordless login', async () => {
const {user} = renderWithProviders(, {
wrapperProps: {
siteAlias: 'uk',
@@ -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(, {
+ 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()
+ }
+ )
})
diff --git a/packages/template-retail-react-app/app/pages/reset-password/index.jsx b/packages/template-retail-react-app/app/pages/reset-password/index.jsx
index e467b7c3af..d44410b804 100644
--- a/packages/template-retail-react-app/app/pages/reset-password/index.jsx
+++ b/packages/template-retail-react-app/app/pages/reset-password/index.jsx
@@ -19,10 +19,7 @@ import useDataCloud from '@salesforce/retail-react-app/app/hooks/use-datacloud'
import {useLocation} from 'react-router-dom'
import {useRouteMatch} from 'react-router'
import {usePasswordReset} from '@salesforce/retail-react-app/app/hooks/use-password-reset'
-import {
- API_ERROR_MESSAGE,
- FEATURE_UNAVAILABLE_ERROR_MESSAGE
-} from '@salesforce/retail-react-app/app/constants'
+import {getPasswordResetErrorMessage} from '@salesforce/retail-react-app/app/utils/auth-utils'
const ResetPassword = () => {
const {formatMessage} = useIntl()
@@ -38,10 +35,7 @@ const ResetPassword = () => {
try {
await getPasswordResetToken(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})
}
}
diff --git a/packages/template-retail-react-app/app/pages/reset-password/index.test.jsx b/packages/template-retail-react-app/app/pages/reset-password/index.test.jsx
index eaf2d316aa..9ff499fa84 100644
--- a/packages/template-retail-react-app/app/pages/reset-password/index.test.jsx
+++ b/packages/template-retail-react-app/app/pages/reset-password/index.test.jsx
@@ -89,6 +89,38 @@ test('Allows customer to generate password token', async () => {
})
})
+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) => {
+ global.server.use(
+ rest.post('*/oauth2/password/reset', (req, res, ctx) =>
+ res(ctx.delay(0), ctx.status(400), ctx.json({message: apiErrorMessage}))
+ )
+ )
+ // render our test component
+ const {user} = renderWithProviders(, {
+ wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app}
+ })
+
+ // enter credentials and submit
+ await user.type(await screen.findByLabelText('Email'), 'foo@test.com')
+ await user.click(
+ within(await screen.findByTestId('sf-auth-modal-form')).getByText(/reset password/i)
+ )
+
+ await waitFor(() => {
+ expect(screen.getByText(expectedMessage)).toBeInTheDocument()
+ })
+ }
+)
+
test.each([
['base path', '/reset-password-landing'],
['path with site and locale', '/uk/en-GB/reset-password-landing']
diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json
index b87abefb3e..52df882aac 100644
--- a/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json
+++ b/packages/template-retail-react-app/app/static/translations/compiled/en-GB.json
@@ -1881,6 +1881,12 @@
"value": "Something went wrong. Try again!"
}
],
+ "global.error.too_many_requests": [
+ {
+ "type": 0,
+ "value": "Too many requests. For your security, please wait 10 minutes before trying again."
+ }
+ ],
"global.info.added_to_wishlist": [
{
"type": 1,
diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json
index b87abefb3e..52df882aac 100644
--- a/packages/template-retail-react-app/app/static/translations/compiled/en-US.json
+++ b/packages/template-retail-react-app/app/static/translations/compiled/en-US.json
@@ -1881,6 +1881,12 @@
"value": "Something went wrong. Try again!"
}
],
+ "global.error.too_many_requests": [
+ {
+ "type": 0,
+ "value": "Too many requests. For your security, please wait 10 minutes before trying again."
+ }
+ ],
"global.info.added_to_wishlist": [
{
"type": 1,
diff --git a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json
index 41031efa05..54587b6448 100644
--- a/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json
+++ b/packages/template-retail-react-app/app/static/translations/compiled/en-XA.json
@@ -3857,6 +3857,20 @@
"value": "]"
}
],
+ "global.error.too_many_requests": [
+ {
+ "type": 0,
+ "value": "["
+ },
+ {
+ "type": 0,
+ "value": "Ŧǿǿǿǿ ḿȧȧƞẏ řḗḗɋŭŭḗḗşŧş. Ƒǿǿř ẏǿǿŭŭř şḗḗƈŭŭřīŧẏ, ƥŀḗḗȧȧşḗḗ ẇȧȧīŧ 10 ḿīƞŭŭŧḗḗş ƀḗḗƒǿǿřḗḗ ŧřẏīƞɠ ȧȧɠȧȧīƞ."
+ },
+ {
+ "type": 0,
+ "value": "]"
+ }
+ ],
"global.info.added_to_wishlist": [
{
"type": 0,
diff --git a/packages/template-retail-react-app/app/utils/auth-utils.js b/packages/template-retail-react-app/app/utils/auth-utils.js
new file mode 100644
index 0000000000..3deb9b8922
--- /dev/null
+++ b/packages/template-retail-react-app/app/utils/auth-utils.js
@@ -0,0 +1,69 @@
+/*
+ * Copyright (c) 2021, salesforce.com, inc.
+ * All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
+ */
+
+import {defineMessage} from 'react-intl'
+import {
+ API_ERROR_MESSAGE,
+ FEATURE_UNAVAILABLE_ERROR_MESSAGE
+} from '@salesforce/retail-react-app/app/constants'
+
+export const TOO_MANY_REQUESTS_ERROR_MESSAGE = defineMessage({
+ defaultMessage:
+ 'Too many requests. For your security, please wait 10 minutes before trying again.',
+ id: 'global.error.too_many_requests'
+})
+
+// Shared error patterns for token-based auth features (passwordless login, password reset)
+const TOKEN_BASED_AUTH_FEATURE_UNAVAILABLE_ERRORS = [
+ /no callback_uri is registered/i,
+ /callback_uri doesn't match/i,
+ /monthly quota/i
+]
+
+const PASSWORDLESS_FEATURE_UNAVAILABLE_ERRORS = [
+ ...TOKEN_BASED_AUTH_FEATURE_UNAVAILABLE_ERRORS,
+ /passwordless permissions error/i,
+ /client secret is not provided/i
+]
+
+const PASSWORD_RESET_FEATURE_UNAVAILABLE_ERRORS = TOKEN_BASED_AUTH_FEATURE_UNAVAILABLE_ERRORS
+
+const TOO_MANY_REQUESTS_ERROR = /too many .* requests/i
+
+/**
+ * Maps an error message to the appropriate user-friendly error message descriptor
+ * for passwordless login feature errors.
+ *
+ * @param {string} errorMessage - The error message from the API
+ * @returns {Object} - The message descriptor object (from defineMessage) that can be passed to formatMessage
+ */
+export const getPasswordlessErrorMessage = (errorMessage) => {
+ if (PASSWORDLESS_FEATURE_UNAVAILABLE_ERRORS.some((msg) => msg.test(errorMessage))) {
+ return FEATURE_UNAVAILABLE_ERROR_MESSAGE
+ }
+ if (TOO_MANY_REQUESTS_ERROR.test(errorMessage)) {
+ return TOO_MANY_REQUESTS_ERROR_MESSAGE
+ }
+ return API_ERROR_MESSAGE
+}
+
+/**
+ * Maps an error message to the appropriate user-friendly error message descriptor
+ * for password reset feature errors.
+ *
+ * @param {string} errorMessage - The error message from the API
+ * @returns {Object} - The message descriptor object (from defineMessage) that can be passed to formatMessage
+ */
+export const getPasswordResetErrorMessage = (errorMessage) => {
+ if (PASSWORD_RESET_FEATURE_UNAVAILABLE_ERRORS.some((msg) => msg.test(errorMessage))) {
+ return FEATURE_UNAVAILABLE_ERROR_MESSAGE
+ }
+ if (TOO_MANY_REQUESTS_ERROR.test(errorMessage)) {
+ return TOO_MANY_REQUESTS_ERROR_MESSAGE
+ }
+ return API_ERROR_MESSAGE
+}
diff --git a/packages/template-retail-react-app/app/utils/auth-utils.test.js b/packages/template-retail-react-app/app/utils/auth-utils.test.js
new file mode 100644
index 0000000000..2841957bb8
--- /dev/null
+++ b/packages/template-retail-react-app/app/utils/auth-utils.test.js
@@ -0,0 +1,64 @@
+/*
+ * Copyright (c) 2021, salesforce.com, inc.
+ * All rights reserved.
+ * SPDX-License-Identifier: BSD-3-Clause
+ * For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
+ */
+import {
+ API_ERROR_MESSAGE,
+ FEATURE_UNAVAILABLE_ERROR_MESSAGE
+} from '@salesforce/retail-react-app/app/constants'
+import {
+ getPasswordlessErrorMessage,
+ getPasswordResetErrorMessage,
+ TOO_MANY_REQUESTS_ERROR_MESSAGE
+} from '@salesforce/retail-react-app/app/utils/auth-utils'
+
+describe('getPasswordlessErrorMessage', () => {
+ test.each([
+ ['no callback_uri is registered for client', FEATURE_UNAVAILABLE_ERROR_MESSAGE],
+ ["callback_uri doesn't match the registered callbacks", FEATURE_UNAVAILABLE_ERROR_MESSAGE],
+ [
+ 'Monthly quota for passwordless login mode email has been exceeded',
+ FEATURE_UNAVAILABLE_ERROR_MESSAGE
+ ],
+ [
+ 'PasswordLess Permissions Error for clientId:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',
+ FEATURE_UNAVAILABLE_ERROR_MESSAGE
+ ],
+ ['client secret is not provided', FEATURE_UNAVAILABLE_ERROR_MESSAGE],
+ [
+ 'Too many login requests were made. Please try again later.',
+ TOO_MANY_REQUESTS_ERROR_MESSAGE
+ ],
+ ['unexpected error message', API_ERROR_MESSAGE],
+ [null, API_ERROR_MESSAGE]
+ ])(
+ 'maps passwordless error "%s" to the correct message descriptor',
+ (errorMessage, expectedMessage) => {
+ expect(getPasswordlessErrorMessage(errorMessage)).toBe(expectedMessage)
+ }
+ )
+})
+
+describe('getPasswordResetErrorMessage', () => {
+ test.each([
+ ['no callback_uri is registered for client', FEATURE_UNAVAILABLE_ERROR_MESSAGE],
+ ["callback_uri doesn't match the registered callbacks", FEATURE_UNAVAILABLE_ERROR_MESSAGE],
+ [
+ 'Monthly quota for passwordless login mode email has been exceeded',
+ FEATURE_UNAVAILABLE_ERROR_MESSAGE
+ ],
+ [
+ 'Too many password reset requests were made. Please try again later.',
+ TOO_MANY_REQUESTS_ERROR_MESSAGE
+ ],
+ ['unexpected error message', API_ERROR_MESSAGE],
+ [null, API_ERROR_MESSAGE]
+ ])(
+ 'maps password reset error "%s" to the correct message descriptor',
+ (errorMessage, expectedMessage) => {
+ expect(getPasswordResetErrorMessage(errorMessage)).toBe(expectedMessage)
+ }
+ )
+})
diff --git a/packages/template-retail-react-app/translations/en-GB.json b/packages/template-retail-react-app/translations/en-GB.json
index 4adcc0f0c5..edaa4427e2 100644
--- a/packages/template-retail-react-app/translations/en-GB.json
+++ b/packages/template-retail-react-app/translations/en-GB.json
@@ -744,6 +744,9 @@
"global.error.something_went_wrong": {
"defaultMessage": "Something went wrong. Try again!"
},
+ "global.error.too_many_requests": {
+ "defaultMessage": "Too many requests. For your security, please wait 10 minutes before trying again."
+ },
"global.info.added_to_wishlist": {
"defaultMessage": "{quantity} {quantity, plural, one {item} other {items}} added to wishlist"
},
diff --git a/packages/template-retail-react-app/translations/en-US.json b/packages/template-retail-react-app/translations/en-US.json
index 4adcc0f0c5..edaa4427e2 100644
--- a/packages/template-retail-react-app/translations/en-US.json
+++ b/packages/template-retail-react-app/translations/en-US.json
@@ -744,6 +744,9 @@
"global.error.something_went_wrong": {
"defaultMessage": "Something went wrong. Try again!"
},
+ "global.error.too_many_requests": {
+ "defaultMessage": "Too many requests. For your security, please wait 10 minutes before trying again."
+ },
"global.info.added_to_wishlist": {
"defaultMessage": "{quantity} {quantity, plural, one {item} other {items}} added to wishlist"
},