Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
7f7d493
Add passwordless login mode configuration with email as default
hajinsuha1 Dec 3, 2025
5012c78
Update authorizePasswordless to require mode parameter and add locale…
hajinsuha1 Dec 3, 2025
f4368a2
Remove commented out passwordless callbackURI configuration
hajinsuha1 Dec 3, 2025
d697aa9
Remove passwordlessCallbackURI variable and simplify callbackURI cond…
hajinsuha1 Dec 3, 2025
53a54a3
Remove conditional callbackURI spread and add mode parameter to check…
hajinsuha1 Dec 3, 2025
0db1dad
Add buildCallbackURL utility function and conditionally spread callba…
hajinsuha1 Dec 4, 2025
391c537
Rename buildCallbackURL to buildAbsoluteUrl and make passwordlessLogi…
hajinsuha1 Dec 4, 2025
7362f46
update unit tests in template-retail-react-app and fix passwordless m…
hajinsuha1 Dec 4, 2025
587b3c2
make authorizePasswordless backward compatible and add unit tests for…
hajinsuha1 Dec 4, 2025
4dc22cc
Make passwordlessLoginCallbackURI non-optional with empty string default
hajinsuha1 Dec 4, 2025
ea88027
lint
hajinsuha1 Dec 4, 2025
0f3f211
Enable SLAS private client and passwordless login email mode, update …
hajinsuha1 Dec 5, 2025
66bcafa
Revert "Enable SLAS private client and passwordless login email mode,…
hajinsuha1 Dec 5, 2025
cb345aa
update changelog
hajinsuha1 Dec 5, 2025
465b813
Reapply "Enable SLAS private client and passwordless login email mode…
hajinsuha1 Dec 9, 2025
3737c6e
Use endsWith() to match passwordless login landing path and add test …
hajinsuha1 Dec 9, 2025
9dc54b1
lint
hajinsuha1 Dec 9, 2025
2a39c76
Revert "Reapply "Enable SLAS private client and passwordless login em…
hajinsuha1 Dec 9, 2025
a4e1cfd
Merge branch 'feature/email-otp' into W-20279798-support-setting-pass…
hajinsuha1 Dec 9, 2025
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
4 changes: 3 additions & 1 deletion packages/commerce-sdk-react/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
## v4.3.0-dev (Nov 05, 2025)
## v4.3.0-dev

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

## v4.2.0 (Nov 04, 2025)

Expand Down
110 changes: 79 additions & 31 deletions packages/commerce-sdk-react/src/auth/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,15 @@
import Auth, {AuthData} from './'
import {waitFor} from '@testing-library/react'
import jwt from 'jsonwebtoken'
import {
helpers,
ShopperCustomersTypes,
ShopperCustomers,
ShopperLogin
} from 'commerce-sdk-isomorphic'
import {helpers, ShopperCustomersTypes, ShopperCustomers} from 'commerce-sdk-isomorphic'
import * as utils from '../utils'
import {SLAS_SECRET_PLACEHOLDER} from '../constant'
import {ShopperLoginTypes} from 'commerce-sdk-isomorphic'
import {
DEFAULT_SLAS_REFRESH_TOKEN_REGISTERED_TTL,
DEFAULT_SLAS_REFRESH_TOKEN_GUEST_TTL
} from './index'
import {ApiClientConfigParams, RequireKeys} from '../hooks/types'
import {RequireKeys} from '../hooks/types'

const baseCustomer: RequireKeys<ShopperCustomersTypes.Customer, 'login'> = {
customerId: 'customerId',
Expand Down Expand Up @@ -100,7 +95,8 @@ const config = {
proxy: 'proxy',
redirectURI: 'redirectURI',
logger: console,
passwordlessLoginCallbackURI: 'passwordlessLoginCallbackURI'
passwordlessLoginCallbackURI: 'passwordlessLoginCallbackURI',
locale: 'en-US'
}

const configSLASPrivate = {
Expand All @@ -124,16 +120,6 @@ const JWTExpired = jwt.sign(
'secret'
)

const configPasswordlessSms = {
clientId: 'clientId',
organizationId: 'organizationId',
shortCode: 'shortCode',
siteId: 'siteId',
proxy: 'proxy',
redirectURI: 'redirectURI',
logger: console
}

const FAKE_SLAS_EXPIRY = DEFAULT_SLAS_REFRESH_TOKEN_REGISTERED_TTL - 1

const TOKEN_RESPONSE: ShopperLoginTypes.TokenResponse = {
Expand Down Expand Up @@ -921,32 +907,94 @@ describe('Auth', () => {
expect(result).toHaveProperty('codeVerifier')
})

test('authorizePasswordless calls isomorphic authorizePasswordless', async () => {
const auth = new Auth(config)
await auth.authorizePasswordless({
callbackURI: 'callbackURI',
userid: 'userid',
mode: 'callback'
})
test.each([
[
'with all parameters specified',
{callbackURI: 'callbackURI', userid: 'userid', mode: 'callback'},
{
callbackURI: 'callbackURI',
userid: 'userid',
mode: 'callback',
locale: configSLASPrivate.locale
}
],
[
'defaults mode to callback when not specified',
{userid: 'userid'},
{userid: 'userid', mode: 'callback'}
],
[
'defaults callbackURI to passwordlessLoginCallbackURI when not specified',
{userid: 'userid'},
{
userid: 'userid',
mode: 'callback',
callbackURI: configSLASPrivate.passwordlessLoginCallbackURI
}
],
['with mode email', {userid: 'userid', mode: 'email'}, {userid: 'userid', mode: 'email'}]
])('authorizePasswordless %s', async (_, input: any, expectedParams: any) => {
const auth = new Auth(configSLASPrivate)
// @ts-expect-error private method
auth.set('usid', 'test-usid-value')

await auth.authorizePasswordless(input)
expect(helpers.authorizePasswordless).toHaveBeenCalled()
const functionArg = (helpers.authorizePasswordless as jest.Mock).mock.calls[0][0]
expect(functionArg).toMatchObject({
credentials: {
clientSecret: SLAS_SECRET_PLACEHOLDER
},
parameters: {
callbackURI: 'callbackURI',
userid: 'userid',
mode: 'callback'
...expectedParams,
usid: 'test-usid-value'
}
})
})

test('authorizePasswordless sets mode to sms as configured', async () => {
const auth = new Auth(configPasswordlessSms)
test('authorizePasswordless without usid', async () => {
const auth = new Auth(configSLASPrivate)

await auth.authorizePasswordless({userid: 'userid'})
expect(helpers.authorizePasswordless).toHaveBeenCalled()
const functionArg = (helpers.authorizePasswordless as jest.Mock).mock.calls[0][0]
expect(functionArg).toMatchObject({
parameters: {userid: 'userid', mode: 'sms'}
parameters: {
userid: 'userid',
mode: 'callback',
callbackURI: configSLASPrivate.passwordlessLoginCallbackURI
}
})
// Verify usid is not in parameters when not set
expect(functionArg.parameters.usid).toBeUndefined()
})

test('authorizePasswordless without passwordlessLoginCallbackURI in config', async () => {
const configWithoutCallback = {
...configSLASPrivate,
passwordlessLoginCallbackURI: undefined
}
const auth = new Auth(configWithoutCallback)

await auth.authorizePasswordless({userid: 'userid'})
expect(helpers.authorizePasswordless).toHaveBeenCalled()
const functionArg = (helpers.authorizePasswordless as jest.Mock).mock.calls[0][0]
// callbackURI should not be in parameters when not configured
expect(functionArg.parameters.callbackURI).toBeUndefined()
})

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

const mockErrorResponse = {
status: 400,
json: jest.fn().mockResolvedValue({message: 'Invalid request'})
}
;(helpers.authorizePasswordless as jest.Mock).mockResolvedValueOnce(mockErrorResponse)

await expect(auth.authorizePasswordless({userid: 'userid'})).rejects.toThrow(
'400 Invalid request'
)
})

test('getPasswordLessAccessToken calls isomorphic getPasswordLessAccessToken', async () => {
Expand Down
20 changes: 13 additions & 7 deletions packages/commerce-sdk-react/src/auth/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import {
isOriginTrusted,
onClient,
getDefaultCookieAttributes,
isAbsoluteUrl,
stringToBase64,
extractCustomParameters
} from '../utils'
Expand Down Expand Up @@ -55,6 +54,7 @@ interface AuthConfig extends ApiClientConfigParams {
refreshTokenRegisteredCookieTTL?: number
refreshTokenGuestCookieTTL?: number
hybridAuthEnabled?: boolean
locale: string
}

interface JWTHeaders {
Expand Down Expand Up @@ -95,7 +95,7 @@ type AuthorizeIDPParams = {
type AuthorizePasswordlessParams = {
callbackURI?: string
userid: string
mode?: string
mode?: 'email' | 'callback'
}

type GetPasswordLessAccessTokenParams = {
Expand Down Expand Up @@ -265,6 +265,7 @@ class Auth {
| undefined

private hybridAuthEnabled: boolean
private locale: string

constructor(config: AuthConfig) {
// Special proxy endpoint for injecting SLAS private client secret.
Expand Down Expand Up @@ -358,10 +359,11 @@ class Auth {

this.isPrivate = !!this.clientSecret

const passwordlessLoginCallbackURI = config.passwordlessLoginCallbackURI
this.passwordlessLoginCallbackURI = passwordlessLoginCallbackURI || ''
this.passwordlessLoginCallbackURI = config.passwordlessLoginCallbackURI || ''

this.hybridAuthEnabled = config.hybridAuthEnabled || false

this.locale = config.locale
}

get(name: AuthDataKeys) {
Expand Down Expand Up @@ -1261,19 +1263,23 @@ class Auth {
*/
async authorizePasswordless(parameters: AuthorizePasswordlessParams) {
const usid = this.get('usid')
// Default to 'callback' mode for backward compatibility as older versions of the template-retail-react-app
// expect this mode. Newer versions should explicitly set mode.
const mode = parameters.mode || 'callback'
const callbackURI = parameters.callbackURI || this.passwordlessLoginCallbackURI
const finalMode = callbackURI ? 'callback' : parameters.mode || 'sms'
const locale = this.locale
Comment on lines +1268 to +1270
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

simplified mode selection to respect user's explicit mode choice while still defaulting to callback mode for backward compatibility


const res = await helpers.authorizePasswordless({
slasClient: this.client,
credentials: {
clientSecret: this.clientSecret
},
parameters: {
...(callbackURI && {callbackURI: callbackURI}),
...(callbackURI && {callbackURI}),
...(usid && {usid}),
...(locale && {locale}),
userid: parameters.userid,
mode: finalMode
mode
}
})
if (res && res.status !== 200) {
Expand Down
2 changes: 2 additions & 0 deletions packages/commerce-sdk-react/src/provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,7 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => {
organizationId,
shortCode,
siteId,
locale,
proxy,
redirectURI,
headers,
Expand All @@ -177,6 +178,7 @@ const CommerceApiProvider = (props: CommerceApiProviderProps): ReactElement => {
organizationId,
shortCode,
siteId,
locale,
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

the locale that is passed to the authorizePasswordless endpoint is the one associated to the current site in config/sites.js

For example,
http://localhost:3000/us/ sets locale to en-US
http://localhost:3000/global sets locale to en-GB
http://localhost:3000/us/en-CA sets locale to en-CA

proxy,
redirectURI,
headers,
Expand Down
2 changes: 2 additions & 0 deletions packages/template-retail-react-app/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
## v8.3.0-dev (Nov 05, 2025)
- [Bugfix] Fix Forgot Password link not working from Account Profile password update form [#3493](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3493)

- Update passwordless login to use email mode by default. The passwordless login mode can now be configured across the login page, auth modal, and checkout page [#3492](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3492)

## v8.2.0 (Nov 04, 2025)
- Add support for Rule Based Promotions for Choice of Bonus Products. We are currently supporting only one product level rule based promotion per product [#3418](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3418)
- Add support for Rule Based Promotions for Choice of Bonus Products. We are currently supporting only one product level rule based promotion per product [#3418](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3418)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,8 @@ import {
getEnvBasePath,
slasPrivateProxyPath
} from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths'
import {createUrlTemplate} from '@salesforce/retail-react-app/app/utils/url'
import {buildAbsoluteUrl, createUrlTemplate} from '@salesforce/retail-react-app/app/utils/url'
import createLogger from '@salesforce/pwa-kit-runtime/utils/logger-factory'
import {isAbsoluteURL} from '@salesforce/retail-react-app/app/page-designer/utils'

import {CommerceApiProvider} from '@salesforce/commerce-sdk-react'
import {withReactQuery} from '@salesforce/pwa-kit-react-sdk/ssr/universal/components/with-react-query'
Expand Down Expand Up @@ -89,9 +88,7 @@ const AppConfig = ({children, locals = {}}) => {
const redirectURI = `${appOrigin}${getEnvBasePath()}/callback`
const proxy = `${appOrigin}${getEnvBasePath()}${commerceApiConfig.proxyPath}`
const slasPrivateClientProxyEndpoint = `${appOrigin}${getEnvBasePath()}${slasPrivateProxyPath}`
const passwordlessLoginCallbackURI = isAbsoluteURL(passwordlessCallback)
? passwordlessCallback
: `${appOrigin}${getEnvBasePath()}${passwordlessCallback}`
const passwordlessLoginCallbackURI = buildAbsoluteUrl(appOrigin, passwordlessCallback)

return (
<CommerceApiProvider
Expand Down
14 changes: 7 additions & 7 deletions packages/template-retail-react-app/app/hooks/use-auth-modal.js
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,7 @@ import {usePrevious} from '@salesforce/retail-react-app/app/hooks/use-previous'
import {usePasswordReset} from '@salesforce/retail-react-app/app/hooks/use-password-reset'
import {isServer} from '@salesforce/retail-react-app/app/utils/utils'
import {getConfig} from '@salesforce/pwa-kit-runtime/utils/ssr-config'
import {getEnvBasePath} from '@salesforce/pwa-kit-runtime/utils/ssr-namespace-paths'
import {isAbsoluteURL} from '@salesforce/retail-react-app/app/page-designer/utils'
import {buildAbsoluteUrl} from '@salesforce/retail-react-app/app/utils/url'
import {useAppOrigin} from '@salesforce/retail-react-app/app/hooks/use-app-origin'

export const LOGIN_VIEW = 'login'
Expand Down Expand Up @@ -88,10 +87,10 @@ export const AuthModal = ({

const {getPasswordResetToken} = usePasswordReset()
const authorizePasswordlessLogin = useAuthHelper(AuthHelpers.AuthorizePasswordless)
const passwordlessConfigCallback = getConfig().app.login?.passwordless?.callbackURI
const callbackURL = isAbsoluteURL(passwordlessConfigCallback)
? passwordlessConfigCallback
: `${appOrigin}${getEnvBasePath()}${passwordlessConfigCallback}`
const passwordlessConfig = getConfig().app.login?.passwordless
const passwordlessConfigCallback = passwordlessConfig?.callbackURI
const passwordlessMode = passwordlessConfig?.mode
const callbackURL = buildAbsoluteUrl(appOrigin, passwordlessConfigCallback)

const {data: baskets} = useCustomerBaskets(
{parameters: {customerId}},
Expand All @@ -104,7 +103,8 @@ export const AuthModal = ({
const redirectPath = window.location.pathname + (window.location.search || '')
await authorizePasswordlessLogin.mutateAsync({
userid: email,
callbackURI: `${callbackURL}?redirectUrl=${redirectPath}`
mode: passwordlessMode,
...(callbackURL && {callbackURI: `${callbackURL}?redirectUrl=${redirectPath}`})
})
setCurrentView(EMAIL_VIEW)
} catch (error) {
Expand Down
Loading
Loading