Skip to content

Commit 07802f3

Browse files
authored
Merge branch 'develop' into feature/email-otp
Signed-off-by: Jinsu Ha <91205717+hajinsuha1@users.noreply.github.com>
2 parents d9ed864 + 1c39b41 commit 07802f3

File tree

13 files changed

+457
-67
lines changed

13 files changed

+457
-67
lines changed

packages/commerce-sdk-react/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
## v4.4.0-dev (Dec 17, 2025)
2+
- [Bugfix]Ensure code_verifier can be optional in resetPassword call [#3567](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3567)
23

34
- 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)
45
- 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)

packages/commerce-sdk-react/src/auth/index.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1367,11 +1367,17 @@ class Auth {
13671367
Authorization: ''
13681368
},
13691369
body: {
1370+
// TODO: remove the eslint disabled after updating OAS
1371+
// user_id is a valid param for resetPassword
1372+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1373+
// @ts-ignore
1374+
...(parameters.user_id && {user_id: parameters.user_id}),
13701375
pwd_action_token: parameters.pwd_action_token,
13711376
channel_id: parameters.channel_id || slasClient.clientConfig.parameters.siteId,
13721377
client_id: parameters.client_id || slasClient.clientConfig.parameters.clientId,
13731378
new_password: parameters.new_password,
13741379
hint: parameters.hint || 'cross_device',
1380+
// hint='cross_device' and a defined user_id is required for code_verifier to be optional for this call
13751381
...(parameters.code_verifier && {code_verifier: parameters.code_verifier})
13761382
}
13771383
}
@@ -1382,9 +1388,6 @@ class Auth {
13821388
`${slasClient.clientConfig.parameters.clientId}:${this.clientSecret}`
13831389
)}`
13841390
}
1385-
// TODO: no code verifier needed with the fix blair has made, delete this when the fix has been merged to production
1386-
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
1387-
// @ts-ignore
13881391
const res = await this.client.resetPassword(options)
13891392
return res
13901393
}

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
- 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)
45
- Update "Continue Securely" button text to "Continue" for passwordless login [#3556](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3556)
56

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
@@ -20,6 +20,7 @@ export const usePasswordReset = () => {
2020
const appOrigin = useAppOrigin()
2121
const config = getConfig().app.login?.resetPassword
2222
const callbackURI = buildAbsoluteUrl(appOrigin, config?.callbackURI)
23+
const resetPasswordLandingPath = config.app.login?.resetPassword?.landingPath
2324

2425
const getPasswordResetTokenMutation = useAuthHelper(AuthHelpers.GetPasswordResetToken)
2526
const resetPasswordMutation = useAuthHelper(AuthHelpers.ResetPassword)
@@ -50,5 +51,5 @@ export const usePasswordReset = () => {
5051
)
5152
}
5253

53-
return {getPasswordResetToken, resetPassword}
54+
return {getPasswordResetToken, resetPassword, resetPasswordLandingPath}
5455
}

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ const mockToken = '123456'
1717
const mockNewPassword = 'new-password'
1818

1919
const MockComponent = () => {
20-
const {getPasswordResetToken, resetPassword} = usePasswordReset()
21-
20+
const {getPasswordResetToken, resetPassword, resetPasswordLandingPath} = usePasswordReset()
2221
return (
2322
<div>
2423
<button
@@ -35,6 +34,8 @@ const MockComponent = () => {
3534
})
3635
}
3736
/>
37+
38+
<div data-testid="reset-password-landing-path">{resetPasswordLandingPath}</div>
3839
</div>
3940
)
4041
}
@@ -152,4 +153,11 @@ describe('usePasswordReset', () => {
152153
position: 'bottom-right'
153154
})
154155
})
156+
157+
test('resetPasswordLandingPath is returned', () => {
158+
renderWithProviders(<MockComponent />)
159+
expect(screen.getByTestId('reset-password-landing-path')).toHaveTextContent(
160+
mockConfig.app.login.resetPassword.landingPath
161+
)
162+
})
155163
})

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'
@@ -62,6 +61,7 @@ const Login = ({initialView = LOGIN_VIEW}) => {
6261
const {passwordless = {}, social = {}} = getConfig().app.login || {}
6362
const isPasswordlessEnabled = !!passwordless?.enabled
6463
const passwordlessMode = passwordless?.mode
64+
const passwordlessLoginLandingPath = passwordless?.landingPath
6565
const isSocialEnabled = !!social?.enabled
6666
const idps = social?.idps
6767

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

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

Lines changed: 94 additions & 21 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,14 +50,13 @@ const MockedComponent = () => {
4550
)
4651
}
4752

48-
let mockRouteMatchPath = '/passwordless-login-landing'
53+
const mockUseRouteMatch = jest.fn(() => ({path: '/'}))
4954

5055
jest.mock('react-router', () => {
56+
const original = jest.requireActual('react-router')
5157
return {
52-
...jest.requireActual('react-router'),
53-
useRouteMatch: () => {
54-
return {path: mockRouteMatchPath}
55-
}
58+
...original,
59+
useRouteMatch: () => mockUseRouteMatch()
5660
}
5761
})
5862

@@ -74,7 +78,14 @@ jest.mock('@salesforce/commerce-sdk-react', () => {
7478

7579
// Set up and clean up
7680
beforeEach(() => {
77-
mockRouteMatchPath = '/passwordless-login-landing'
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+
7889
global.server.use(
7990
rest.post('*/customers', (req, res, ctx) => {
8091
return res(ctx.delay(0), ctx.status(200), ctx.json(mockedRegisteredCustomer))
@@ -95,12 +106,32 @@ beforeEach(() => {
95106
})
96107
)
97108
})
98-
afterEach(() => {
99-
jest.resetModules()
100-
jest.clearAllMocks()
101-
})
102109

103110
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+
104135
test('redirects to account page when redirect url is not passed', async () => {
105136
const token = '12345678'
106137
window.history.pushState(
@@ -156,33 +187,75 @@ describe('Passwordless landing tests', function () {
156187
})
157188
})
158189

159-
test('redirects to account page with correct locale when path includes locale', async () => {
160-
const token = '12345678'
161-
const localizedPath = '/us/en-CA/passwordless-login-landing'
162-
mockRouteMatchPath = localizedPath
163-
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})
164194
window.history.pushState(
165195
{},
166196
'Passwordless Login Landing',
167-
`${localizedPath}?token=${token}`
197+
createPathWithDefaults(`${loginPath}?token=${token}`)
168198
)
169-
170199
renderWithProviders(<MockedComponent />, {
171200
wrapperProps: {
172-
siteAlias: 'us',
173-
locale: {id: 'en-CA'},
201+
siteAlias: 'global',
202+
locale: {id: 'en-GB'},
174203
appConfig: mockConfig.app
175204
}
176205
})
177206

207+
await waitFor(() => {
208+
expect(
209+
mockAuthHelperFunctions[AuthHelpers.LoginPasswordlessUser].mutateAsync
210+
).toHaveBeenCalledWith({
211+
pwdlessLoginToken: token
212+
})
213+
})
214+
178215
expect(
179216
mockAuthHelperFunctions[AuthHelpers.LoginPasswordlessUser].mutateAsync
180217
).toHaveBeenCalledWith({
181218
pwdlessLoginToken: token
182219
})
220+
})
183221

184-
await waitFor(() => {
185-
expect(window.location.pathname).toBe('/us/en-CA/account')
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
186259
})
187260
})
188261
})

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)