Skip to content

Commit daaf236

Browse files
committed
Merge remote-tracking branch 'refs/remotes/origin/1cc_bugs' into 1cc_bugs
2 parents 7ed9e71 + dd06e0f commit daaf236

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+1054
-376
lines changed

e2e/tests/desktop/extra-features.spec.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,17 +38,49 @@ test('Verify passwordless login request', async ({page}) => {
3838
'**/mobify/slas/private/shopper/auth/v1/organizations/*/oauth2/passwordless/login'
3939
)
4040

41+
// Verify the passwordless login request
4142
expect(interceptedRequest).toBeTruthy()
4243
expect(interceptedRequest.method()).toBe('POST')
4344

44-
const postData = interceptedRequest.postData()
45+
let postData = interceptedRequest.postData()
4546
expect(postData).toBeTruthy()
4647

47-
const params = new URLSearchParams(postData)
48+
let params = new URLSearchParams(postData)
4849

4950
expect(params.get('user_id')).toBe(config.PWA_E2E_USER_EMAIL)
5051
expect(params.get('mode')).toBe('email')
5152
expect(params.get('channel_id')).toBe(config.EXTRA_FEATURES_E2E_RETAIL_APP_HOME_SITE)
53+
54+
await page.route(
55+
'**/mobify/slas/private/shopper/auth/v1/organizations/*/oauth2/passwordless/token',
56+
(route) => {
57+
interceptedRequest = route.request()
58+
route.continue()
59+
}
60+
)
61+
62+
// Wait for OTP input fields to appear and fill the 8-digit code
63+
const otpCode = '12345678' // Replace with actual OTP code
64+
const otpInputs = page.locator('input[inputmode="numeric"][maxlength="1"]')
65+
await otpInputs.first().waitFor()
66+
67+
// Fill each input field with one digit
68+
for (let i = 0; i < 8; i++) {
69+
await otpInputs.nth(i).fill(otpCode[i])
70+
}
71+
72+
await page.waitForResponse(
73+
'**/mobify/slas/private/shopper/auth/v1/organizations/*/oauth2/passwordless/token'
74+
)
75+
76+
// Verify the passwordless login token request
77+
expect(interceptedRequest).toBeTruthy()
78+
expect(interceptedRequest.method()).toBe('POST')
79+
postData = interceptedRequest.postData()
80+
expect(postData).toBeTruthy()
81+
params = new URLSearchParams(postData)
82+
expect(params.get('pwdless_login_token')).toBe(otpCode)
83+
expect(params.get('hint')).toBe('pwdless_login')
5284
})
5385

5486
test('Verify password reset request', async ({page}) => {

e2e/tests/mobile/extra-features.spec.js

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,17 +40,49 @@ test('Verify passwordless login request on mobile', async ({page}) => {
4040
'**/mobify/slas/private/shopper/auth/v1/organizations/*/oauth2/passwordless/login'
4141
)
4242

43+
// Verify the passwordless login request
4344
expect(interceptedRequest).toBeTruthy()
4445
expect(interceptedRequest.method()).toBe('POST')
4546

46-
const postData = interceptedRequest.postData()
47+
let postData = interceptedRequest.postData()
4748
expect(postData).toBeTruthy()
4849

49-
const params = new URLSearchParams(postData)
50+
let params = new URLSearchParams(postData)
5051

5152
expect(params.get('user_id')).toBe(config.PWA_E2E_USER_EMAIL)
5253
expect(params.get('mode')).toBe('email')
5354
expect(params.get('channel_id')).toBe(config.EXTRA_FEATURES_E2E_RETAIL_APP_HOME_SITE)
55+
56+
await page.route(
57+
'**/mobify/slas/private/shopper/auth/v1/organizations/*/oauth2/passwordless/token',
58+
(route) => {
59+
interceptedRequest = route.request()
60+
route.continue()
61+
}
62+
)
63+
64+
// Wait for OTP input fields to appear and fill the 8-digit code
65+
const otpCode = '12345678' // Replace with actual OTP code
66+
const otpInputs = page.locator('input[inputmode="numeric"][maxlength="1"]')
67+
await otpInputs.first().waitFor()
68+
69+
// Fill each input field with one digit
70+
for (let i = 0; i < 8; i++) {
71+
await otpInputs.nth(i).fill(otpCode[i])
72+
}
73+
74+
await page.waitForResponse(
75+
'**/mobify/slas/private/shopper/auth/v1/organizations/*/oauth2/passwordless/token'
76+
)
77+
78+
// Verify the passwordless login token request
79+
expect(interceptedRequest).toBeTruthy()
80+
expect(interceptedRequest.method()).toBe('POST')
81+
postData = interceptedRequest.postData()
82+
expect(postData).toBeTruthy()
83+
params = new URLSearchParams(postData)
84+
expect(params.get('pwdless_login_token')).toBe(otpCode)
85+
expect(params.get('hint')).toBe('pwdless_login')
5486
})
5587

5688
test('Verify password reset request on mobile (extra features enabled)', async ({page}) => {

packages/pwa-kit-create-app/CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
## v3.16.0-dev (Dec 17, 2025)
22
- Add new One-Click Checkout configuration [#3609](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3609)
3-
4-
## v3.15.0-dev (Nov 05, 2025)
53
- Support email mode by default for passwordless login and password reset in a generated app. [#3525](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3525)
4+
- Util function for passwordless callback URI [#3630](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3630)
5+
- Add `tokenLength` to login configuration [#3554](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3554)
66

77
## v3.15.0 (Dec 17, 2025)
88
- Add new Google Cloud API configuration and Bonus Product configuration [#3523](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3523)

packages/pwa-kit-create-app/assets/bootstrap/js/config/default.js.hbs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
/* eslint-disable @typescript-eslint/no-var-requires */
88
const sites = require('./sites.js')
9-
const {parseSettings} = require('./utils.js')
9+
const {parseSettings, validateOtpTokenLength} = require('./utils.js')
1010

1111
module.exports = {
1212
app: {
@@ -57,6 +57,9 @@ module.exports = {
5757
interpretPlusSignAsSpace: false
5858
},
5959
login: {
60+
// The length of the token for OTP authentication. Used by passwordless login and reset password.
61+
// If the env var `OTP_TOKEN_LENGTH` is set, it will override the config value. Valid values are 6 or 8. Defaults to: 8
62+
tokenLength: validateOtpTokenLength(process.env.OTP_TOKEN_LENGTH),
6063
passwordless: {
6164
// Enables or disables passwordless login for the site. Defaults to: false
6265
{{#if answers.project.demo.enableDemoSettings}}

packages/pwa-kit-create-app/assets/bootstrap/js/config/utils.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,42 @@
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8+
/**
9+
* Valid OTP token lengths supported by the authentication system.
10+
* These values are enforced to ensure compatibility with the OTP verification flow.
11+
*/
12+
const VALID_OTP_TOKEN_LENGTHS = [6, 8]
13+
const DEFAULT_OTP_TOKEN_LENGTH = 8
14+
15+
/**
16+
* Validates and normalizes the OTP token length configuration.
17+
* Throws an error if the token length is invalid.
18+
*
19+
* @param {string|number|undefined} tokenLength - The token length from config or env var
20+
* @returns {number} Validated token length (6 or 8)
21+
* @throws {Error} If tokenLength is invalid (not 6 or 8)
22+
*/
23+
function validateOtpTokenLength(tokenLength) {
24+
// If undefined, return default
25+
if (tokenLength === undefined) {
26+
return DEFAULT_OTP_TOKEN_LENGTH
27+
}
28+
29+
// Parse to number (handles string numbers like "6" or "8")
30+
const parsedLength = Number(tokenLength)
31+
32+
// Check if it's one of the allowed values (includes() will return false for NaN or invalid numbers)
33+
if (!VALID_OTP_TOKEN_LENGTHS.includes(parsedLength)) {
34+
throw new Error(
35+
`Invalid OTP token length: ${tokenLength}. Valid values are ${VALID_OTP_TOKEN_LENGTHS.join(
36+
' or '
37+
)}. `
38+
)
39+
}
40+
41+
return parsedLength
42+
}
43+
844
/**
945
* Safely parses settings from either a JSON string or object
1046
* @param {string|object} settings - The settings
@@ -30,5 +66,8 @@ function parseSettings(settings) {
3066
}
3167

3268
module.exports = {
33-
parseSettings
69+
parseSettings,
70+
validateOtpTokenLength,
71+
DEFAULT_OTP_TOKEN_LENGTH,
72+
VALID_OTP_TOKEN_LENGTHS
3473
}

packages/pwa-kit-create-app/assets/bootstrap/js/overrides/app/components/_app-config/index.jsx.hbs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,9 @@ import {
2222
getEnvBasePath,
2323
slasPrivateProxyPath
2424
} from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths'
25-
import {absoluteUrl, createUrlTemplate} from '@salesforce/retail-react-app/app/utils/url'
25+
import {createUrlTemplate} from '@salesforce/retail-react-app/app/utils/url'
2626
import createLogger from '@salesforce/pwa-kit-runtime/utils/logger-factory'
27+
import {getPasswordlessCallbackUrl} from '@salesforce/retail-react-app/app/utils/auth-utils'
2728

2829
import {CommerceApiProvider} from '@salesforce/commerce-sdk-react'
2930
import {withReactQuery} from '@salesforce/pwa-kit-react-sdk/ssr/universal/components/with-react-query'
@@ -62,8 +63,9 @@ const AppConfig = ({children, locals = {}}) => {
6263
const commerceApiConfig = locals.appConfig.commerceAPI
6364

6465
const appOrigin = getAppOrigin()
65-
66-
const passwordlessCallback = locals.appConfig.login?.passwordless?.callbackURI
66+
const passwordlessCallbackURI = getPasswordlessCallbackUrl(
67+
locals.appConfig.login?.passwordless?.callbackURI
68+
)
6769

6870
const storeLocatorConfig = {
6971
radius: STORE_LOCATOR_RADIUS,
@@ -79,7 +81,6 @@ const AppConfig = ({children, locals = {}}) => {
7981
const redirectURI = `${appOrigin}${getEnvBasePath()}/callback`
8082
const proxy = `${appOrigin}${getEnvBasePath()}${commerceApiConfig.proxyPath}`
8183
const slasPrivateClientProxyEndpoint = `${appOrigin}${getEnvBasePath()}${slasPrivateProxyPath}`
82-
const passwordlessLoginCallbackURI = absoluteUrl(passwordlessCallback, appOrigin)
8384

8485
return (
8586
<CommerceApiProvider
@@ -94,7 +95,7 @@ const AppConfig = ({children, locals = {}}) => {
9495
headers={headers}
9596
defaultDnt={DEFAULT_DNT_STATE}
9697
logger={createLogger({packageName: 'commerce-sdk-react'})}
97-
passwordlessLoginCallbackURI={passwordlessLoginCallbackURI}
98+
passwordlessLoginCallbackURI={passwordlessCallbackURI}
9899
// Set 'enablePWAKitPrivateClient' to true to use SLAS private client login flows.
99100
// Make sure to also enable useSLASPrivateClient in ssr.js when enabling this setting.
100101
{{#if answers.project.commerce.isSlasPrivate}}

packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/app/components/_app-config/index.jsx.hbs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,9 @@ import {
2323
getEnvBasePath,
2424
slasPrivateProxyPath
2525
} from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths'
26-
import {absoluteUrl, createUrlTemplate} from '@salesforce/retail-react-app/app/utils/url'
26+
import {createUrlTemplate} from '@salesforce/retail-react-app/app/utils/url'
2727
import createLogger from '@salesforce/pwa-kit-runtime/utils/logger-factory'
28+
import {getPasswordlessCallbackUrl} from '@salesforce/retail-react-app/app/utils/auth-utils'
2829

2930
import {CommerceApiProvider} from '@salesforce/commerce-sdk-react'
3031
import {withReactQuery} from '@salesforce/pwa-kit-react-sdk/ssr/universal/components/with-react-query'
@@ -63,7 +64,9 @@ const AppConfig = ({children, locals = {}}) => {
6364

6465
const appOrigin = useAppOrigin()
6566

66-
const passwordlessCallback = locals.appConfig.login?.passwordless?.callbackURI
67+
const passwordlessCallbackURI = getPasswordlessCallbackUrl(
68+
locals.appConfig.login?.passwordless?.callbackURI
69+
)
6770

6871
const storeLocatorConfig = {
6972
radius: STORE_LOCATOR_RADIUS,
@@ -79,7 +82,6 @@ const AppConfig = ({children, locals = {}}) => {
7982
const redirectURI = `${appOrigin}${getEnvBasePath()}/callback`
8083
const proxy = `${appOrigin}${getEnvBasePath()}${commerceApiConfig.proxyPath}`
8184
const slasPrivateClientProxyEndpoint = `${appOrigin}${getEnvBasePath()}${slasPrivateProxyPath}`
82-
const passwordlessLoginCallbackURI = absoluteUrl(passwordlessCallback, appOrigin)
8385

8486
return (
8587
<CommerceApiProvider
@@ -94,7 +96,7 @@ const AppConfig = ({children, locals = {}}) => {
9496
headers={headers}
9597
defaultDnt={DEFAULT_DNT_STATE}
9698
logger={createLogger({packageName: 'commerce-sdk-react'})}
97-
passwordlessLoginCallbackURI={passwordlessLoginCallbackURI}
99+
passwordlessLoginCallbackURI={passwordlessCallbackURI}
98100
// Set 'enablePWAKitPrivateClient' to true to use SLAS private client login flows.
99101
// Make sure to also enable useSLASPrivateClient in ssr.js when enabling this setting.
100102
{{#if answers.project.commerce.isSlasPrivate}}

packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/config/default.js.hbs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
*/
77
/* eslint-disable @typescript-eslint/no-var-requires */
88
const sites = require('./sites.js')
9-
const {parseSettings} = require('./utils.js')
9+
const {parseSettings, validateOtpTokenLength} = require('./utils.js')
1010

1111
module.exports = {
1212
app: {
@@ -57,6 +57,9 @@ module.exports = {
5757
interpretPlusSignAsSpace: false
5858
},
5959
login: {
60+
// The length of the token for OTP authentication. Used by passwordless login and reset password.
61+
// If the env var `OTP_TOKEN_LENGTH` is set, it will override the config value. Valid values are 6 or 8. Defaults to: 8
62+
tokenLength: validateOtpTokenLength(process.env.OTP_TOKEN_LENGTH),
6063
passwordless: {
6164
// Enables or disables passwordless login for the site. Defaults to: false
6265
{{#if answers.project.demo.enableDemoSettings}}

packages/pwa-kit-create-app/assets/templates/@salesforce/retail-react-app/config/utils.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,42 @@
55
* For full license text, see the LICENSE file in the repo root or https://opensource.org/licenses/BSD-3-Clause
66
*/
77

8+
/**
9+
* Valid OTP token lengths supported by the authentication system.
10+
* These values are enforced to ensure compatibility with the OTP verification flow.
11+
*/
12+
const VALID_OTP_TOKEN_LENGTHS = [6, 8]
13+
const DEFAULT_OTP_TOKEN_LENGTH = 8
14+
15+
/**
16+
* Validates and normalizes the OTP token length configuration.
17+
* Throws an error if the token length is invalid.
18+
*
19+
* @param {string|number|undefined} tokenLength - The token length from config or env var
20+
* @returns {number} Validated token length (6 or 8)
21+
* @throws {Error} If tokenLength is invalid (not 6 or 8)
22+
*/
23+
function validateOtpTokenLength(tokenLength) {
24+
// If undefined, return default
25+
if (tokenLength === undefined) {
26+
return DEFAULT_OTP_TOKEN_LENGTH
27+
}
28+
29+
// Parse to number (handles string numbers like "6" or "8")
30+
const parsedLength = Number(tokenLength)
31+
32+
// Check if it's one of the allowed values (includes() will return false for NaN or invalid numbers)
33+
if (!VALID_OTP_TOKEN_LENGTHS.includes(parsedLength)) {
34+
throw new Error(
35+
`Invalid OTP token length: ${tokenLength}. Valid values are ${VALID_OTP_TOKEN_LENGTHS.join(
36+
' or '
37+
)}. `
38+
)
39+
}
40+
41+
return parsedLength
42+
}
43+
844
/**
945
* Safely parses settings from either a JSON string or object
1046
* @param {string|object} settings - The settings
@@ -30,5 +66,8 @@ function parseSettings(settings) {
3066
}
3167

3268
module.exports = {
33-
parseSettings
69+
parseSettings,
70+
validateOtpTokenLength,
71+
DEFAULT_OTP_TOKEN_LENGTH,
72+
VALID_OTP_TOKEN_LENGTHS
3473
}

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
## v8.5.0-dev (Jan 19, 2026)
1+
## v9.0.0-dev (Jan 19, 2026)
22
- [Feature] One Click Checkout [#3552](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3552)
3-
4-
## v8.4.0-dev (Dec 17, 2025)
53
- [Feature] Add `fuzzyPathMatching` to reduce computational overhead of route generation at time of application load [#3530](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3530)
64
- [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)
75
- [Feature] PWA Integration with OMS
@@ -11,6 +9,9 @@
119
- BOPIS multishipment with OMS [#3613] (https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3613)
1210
- [Feature] 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)
1311
- Update "Continue Securely" button text to "Continue" for passwordless login [#3556](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3556)
12+
- Util function for passwordless callback URI [#3630](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3630)
13+
- [BREAKING] Remove unused absoluteUrl util from retail react app [#3633](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3633)
14+
- Allow shopper to manually input OTP during passwordless login [#3554](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3554)
1415

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

0 commit comments

Comments
 (0)