Skip to content

Commit 1c39b41

Browse files
authored
@W-20450671 [Bug Fix] Passwordless Login landingPath, Reset Password landingPath, and Social Login redirectUri value in config not being used (#3560)
* fix password reset and passwordless landing path not changing based on default.js config
1 parent 7faf699 commit 1c39b41

File tree

11 files changed

+473
-51
lines changed

11 files changed

+473
-51
lines changed

packages/template-retail-react-app/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
## v8.4.0-dev (Dec 17, 2025)
22
- [Feature] Add `fuzzyPathMatching` to reduce computational overhead of route generation at time of application load [#3530](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3530)
3+
- [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)
34

45
## v8.3.0 (Dec 17, 2025)
56
- [Bugfix] Fix Forgot Password link not working from Account Profile password update form [#3493](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3493)

packages/template-retail-react-app/app/constants.js

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -251,12 +251,6 @@ export const LOGIN_TYPES = {
251251
SOCIAL: 'social'
252252
}
253253

254-
// Constants for Password Reset
255-
export const RESET_PASSWORD_LANDING_PATH = '/reset-password-landing'
256-
257-
// Constants for Passwordless Login
258-
export const PASSWORDLESS_LOGIN_LANDING_PATH = '/passwordless-login-landing'
259-
260254
export const PASSWORDLESS_ERROR_MESSAGES = [
261255
/callback_uri doesn't match/i,
262256
/passwordless permissions error/i,

packages/template-retail-react-app/app/hooks/use-password-reset.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export const usePasswordReset = () => {
2525
const callbackURI = isAbsoluteURL(resetPasswordCallback)
2626
? resetPasswordCallback
2727
: `${appOrigin}${getEnvBasePath()}${resetPasswordCallback}`
28+
const resetPasswordLandingPath = config.app.login?.resetPassword?.landingPath
2829

2930
const getPasswordResetTokenMutation = useAuthHelper(AuthHelpers.GetPasswordResetToken)
3031
const resetPasswordMutation = useAuthHelper(AuthHelpers.ResetPassword)
@@ -54,5 +55,5 @@ export const usePasswordReset = () => {
5455
)
5556
}
5657

57-
return {getPasswordResetToken, resetPassword}
58+
return {getPasswordResetToken, resetPassword, resetPasswordLandingPath}
5859
}

packages/template-retail-react-app/app/hooks/use-password-reset.test.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,14 @@ import {fireEvent, screen, waitFor} from '@testing-library/react'
99
import {useAuthHelper, AuthHelpers} from '@salesforce/commerce-sdk-react'
1010
import {renderWithProviders} from '@salesforce/retail-react-app/app/utils/test-utils'
1111
import {usePasswordReset} from '@salesforce/retail-react-app/app/hooks/use-password-reset'
12+
import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
1213

1314
const mockEmail = 'test@email.com'
1415
const mockToken = '123456'
1516
const mockNewPassword = 'new-password'
1617

1718
const MockComponent = () => {
18-
const {getPasswordResetToken, resetPassword} = usePasswordReset()
19-
19+
const {getPasswordResetToken, resetPassword, resetPasswordLandingPath} = usePasswordReset()
2020
return (
2121
<div>
2222
<button
@@ -33,6 +33,8 @@ const MockComponent = () => {
3333
})
3434
}
3535
/>
36+
37+
<div data-testid="reset-password-landing-path">{resetPasswordLandingPath}</div>
3638
</div>
3739
)
3840
}
@@ -91,4 +93,11 @@ describe('usePasswordReset', () => {
9193
)
9294
})
9395
})
96+
97+
test('resetPasswordLandingPath is returned', () => {
98+
renderWithProviders(<MockComponent />)
99+
expect(screen.getByTestId('reset-password-landing-path')).toHaveTextContent(
100+
mockConfig.app.login.resetPassword.landingPath
101+
)
102+
})
94103
})

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import {
3131
INVALID_TOKEN_ERROR,
3232
INVALID_TOKEN_ERROR_MESSAGE,
3333
FEATURE_UNAVAILABLE_ERROR_MESSAGE,
34-
PASSWORDLESS_LOGIN_LANDING_PATH,
3534
PASSWORDLESS_ERROR_MESSAGES
3635
} from '@salesforce/retail-react-app/app/constants'
3736
import {usePrevious} from '@salesforce/retail-react-app/app/hooks/use-previous'
@@ -61,6 +60,7 @@ const Login = ({initialView = LOGIN_VIEW}) => {
6160
const authorizePasswordlessLogin = useAuthHelper(AuthHelpers.AuthorizePasswordless)
6261
const {passwordless = {}, social = {}} = getConfig().app.login || {}
6362
const isPasswordlessEnabled = !!passwordless?.enabled
63+
const passwordlessLoginLandingPath = passwordless?.landingPath
6464
const isSocialEnabled = !!social?.enabled
6565
const idps = social?.idps
6666

@@ -147,7 +147,7 @@ const Login = ({initialView = LOGIN_VIEW}) => {
147147
// executing a passwordless login attempt using the token. The process waits for the
148148
// customer baskets to be loaded to guarantee proper basket merging.
149149
useEffect(() => {
150-
if (path === PASSWORDLESS_LOGIN_LANDING_PATH && isSuccessCustomerBaskets) {
150+
if (path.endsWith(passwordlessLoginLandingPath) && isSuccessCustomerBaskets) {
151151
const token = decodeURIComponent(queryParams.get('token'))
152152
if (queryParams.get('redirect_url')) {
153153
setRedirectPath(decodeURIComponent(queryParams.get('redirect_url')))

packages/template-retail-react-app/app/pages/login/passwordless-landing.test.js

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,11 @@ import Account from '@salesforce/retail-react-app/app/pages/account'
1717
import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
1818
import {mockedRegisteredCustomer} from '@salesforce/retail-react-app/app/mocks/mock-data'
1919
import {AuthHelpers} from '@salesforce/commerce-sdk-react'
20+
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
21+
22+
jest.mock('@salesforce/pwa-kit-runtime/utils/ssr-config', () => ({
23+
getConfig: jest.fn(() => mockConfig)
24+
}))
2025

2126
const mockMergedBasket = {
2227
basketId: 'a10ff320829cb0eef93ca5310a',
@@ -45,12 +50,13 @@ const MockedComponent = () => {
4550
)
4651
}
4752

53+
const mockUseRouteMatch = jest.fn(() => ({path: '/'}))
54+
4855
jest.mock('react-router', () => {
56+
const original = jest.requireActual('react-router')
4957
return {
50-
...jest.requireActual('react-router'),
51-
useRouteMatch: () => {
52-
return {path: '/passwordless-login-landing'}
53-
}
58+
...original,
59+
useRouteMatch: () => mockUseRouteMatch()
5460
}
5561
})
5662

@@ -72,6 +78,14 @@ jest.mock('@salesforce/commerce-sdk-react', () => {
7278

7379
// Set up and clean up
7480
beforeEach(() => {
81+
jest.clearAllMocks()
82+
getConfig.mockReturnValue(mockConfig)
83+
84+
// Reset useRouteMatch mock to return path based on window.location.pathname
85+
mockUseRouteMatch.mockImplementation(() => ({
86+
path: typeof window !== 'undefined' && window.location ? window.location.pathname : '/'
87+
}))
88+
7589
global.server.use(
7690
rest.post('*/customers', (req, res, ctx) => {
7791
return res(ctx.delay(0), ctx.status(200), ctx.json(mockedRegisteredCustomer))
@@ -92,11 +106,32 @@ beforeEach(() => {
92106
})
93107
)
94108
})
95-
afterEach(() => {
96-
jest.resetModules()
97-
})
98109

99110
describe('Passwordless landing tests', function () {
111+
test('does not run passwordless login when landing path does not match', async () => {
112+
const token = '11111111'
113+
const invalidLoginPath = '/invalid-passwordless-login-landing'
114+
115+
window.history.pushState(
116+
{},
117+
'Passwordless Login Landing',
118+
createPathWithDefaults(`${invalidLoginPath}?token=${token}`)
119+
)
120+
renderWithProviders(<MockedComponent />, {
121+
wrapperProps: {
122+
siteAlias: 'uk',
123+
locale: {id: 'en-GB'},
124+
appConfig: mockConfig.app
125+
}
126+
})
127+
128+
await waitFor(() => {
129+
expect(
130+
mockAuthHelperFunctions[AuthHelpers.LoginPasswordlessUser].mutateAsync
131+
).not.toHaveBeenCalled()
132+
})
133+
})
134+
100135
test('redirects to account page when redirect url is not passed', async () => {
101136
const token = '12345678'
102137
window.history.pushState(
@@ -151,4 +186,76 @@ describe('Passwordless landing tests', function () {
151186
expect(window.location.pathname).toBe('/uk/en-GB/womens-tops')
152187
})
153188
})
189+
190+
test('detects landing path when at the end of path', async () => {
191+
const token = '33333333'
192+
const loginPath = '/global/en-GB/passwordless-login-landing'
193+
// mockRouteMatch.mockReturnValue({path: loginPath})
194+
window.history.pushState(
195+
{},
196+
'Passwordless Login Landing',
197+
createPathWithDefaults(`${loginPath}?token=${token}`)
198+
)
199+
renderWithProviders(<MockedComponent />, {
200+
wrapperProps: {
201+
siteAlias: 'global',
202+
locale: {id: 'en-GB'},
203+
appConfig: mockConfig.app
204+
}
205+
})
206+
207+
await waitFor(() => {
208+
expect(
209+
mockAuthHelperFunctions[AuthHelpers.LoginPasswordlessUser].mutateAsync
210+
).toHaveBeenCalledWith({
211+
pwdlessLoginToken: token
212+
})
213+
})
214+
215+
expect(
216+
mockAuthHelperFunctions[AuthHelpers.LoginPasswordlessUser].mutateAsync
217+
).toHaveBeenCalledWith({
218+
pwdlessLoginToken: token
219+
})
220+
})
221+
222+
test('landing path changes based on config', async () => {
223+
const token = '44444444'
224+
const customLandingPath = '/custom-passwordless-login-landing'
225+
const mockConfigWithCustomLandingPath = {
226+
...mockConfig,
227+
app: {
228+
...mockConfig.app,
229+
login: {
230+
...mockConfig.app.login,
231+
passwordless: {
232+
...mockConfig.app.login.passwordless,
233+
enabled: true,
234+
landingPath: customLandingPath
235+
}
236+
}
237+
}
238+
}
239+
240+
getConfig.mockReturnValue(mockConfigWithCustomLandingPath)
241+
242+
window.history.pushState(
243+
{},
244+
'Passwordless Login Landing',
245+
createPathWithDefaults(`${customLandingPath}?token=${token}`)
246+
)
247+
renderWithProviders(<MockedComponent />, {
248+
wrapperProps: {
249+
siteAlias: 'uk',
250+
locale: {id: 'en-GB'},
251+
appConfig: mockConfigWithCustomLandingPath.app
252+
}
253+
})
254+
255+
expect(
256+
mockAuthHelperFunctions[AuthHelpers.LoginPasswordlessUser].mutateAsync
257+
).toHaveBeenCalledWith({
258+
pwdlessLoginToken: token
259+
})
260+
})
154261
})

packages/template-retail-react-app/app/pages/reset-password/index.jsx

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ import {useLocation} from 'react-router-dom'
2020
import {useRouteMatch} from 'react-router'
2121
import {usePasswordReset} from '@salesforce/retail-react-app/app/hooks/use-password-reset'
2222
import {
23-
RESET_PASSWORD_LANDING_PATH,
2423
API_ERROR_MESSAGE,
2524
FEATURE_UNAVAILABLE_ERROR_MESSAGE
2625
} from '@salesforce/retail-react-app/app/constants'
@@ -33,7 +32,7 @@ const ResetPassword = () => {
3332
const dataCloud = useDataCloud()
3433
const {pathname} = useLocation()
3534
const {path} = useRouteMatch()
36-
const {getPasswordResetToken} = usePasswordReset()
35+
const {getPasswordResetToken, resetPasswordLandingPath} = usePasswordReset()
3736

3837
const submitForm = async ({email}) => {
3938
try {
@@ -71,7 +70,7 @@ const ResetPassword = () => {
7170
marginBottom={8}
7271
borderRadius="base"
7372
>
74-
{path === RESET_PASSWORD_LANDING_PATH ? (
73+
{path.endsWith(resetPasswordLandingPath) ? (
7574
<ResetPasswordLanding />
7675
) : (
7776
<ResetPasswordForm

packages/template-retail-react-app/app/pages/reset-password/index.test.jsx

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,16 @@ import {
1414
import ResetPassword from '.'
1515
import mockConfig from '@salesforce/retail-react-app/config/mocks/default'
1616

17+
const mockUseRouteMatch = jest.fn(() => ({path: '/'}))
18+
19+
jest.mock('react-router', () => {
20+
const original = jest.requireActual('react-router')
21+
return {
22+
...original,
23+
useRouteMatch: () => mockUseRouteMatch()
24+
}
25+
})
26+
1727
const MockedComponent = () => {
1828
return (
1929
<div>
@@ -24,7 +34,10 @@ const MockedComponent = () => {
2434

2535
// Set up and clean up
2636
beforeEach(() => {
27-
jest.resetModules()
37+
// Reset useRouteMatch mock to return path based on window.location.pathname
38+
mockUseRouteMatch.mockImplementation(() => ({
39+
path: typeof window !== 'undefined' && window.location ? window.location.pathname : '/'
40+
}))
2841
window.history.pushState({}, 'Reset Password', createPathWithDefaults('/reset-password'))
2942
})
3043
afterEach(() => {
@@ -75,3 +88,20 @@ test('Allows customer to generate password token', async () => {
7588
expect(window.location.pathname).toBe('/uk/en-GB/login')
7689
})
7790
})
91+
92+
test.each([
93+
['base path', '/reset-password-landing'],
94+
['path with site and locale', '/uk/en-GB/reset-password-landing']
95+
])('renders reset password landing page when using %s', async (_, landingPath) => {
96+
window.history.pushState({}, 'Reset Password', createPathWithDefaults(landingPath))
97+
98+
// render our test component
99+
renderWithProviders(<MockedComponent />, {
100+
wrapperProps: {siteAlias: 'uk', appConfig: mockConfig.app}
101+
})
102+
103+
// check if the landing page is rendered
104+
await waitFor(() => {
105+
expect(screen.getByText(/confirm new password/i)).toBeInTheDocument()
106+
})
107+
})

0 commit comments

Comments
 (0)