Skip to content

Feature: Passwordless Login and Password Reset supports use of email mode#3525

Merged
hajinsuha1 merged 26 commits intodevelopfrom
feature/email-otp
Jan 28, 2026
Merged

Feature: Passwordless Login and Password Reset supports use of email mode#3525
hajinsuha1 merged 26 commits intodevelopfrom
feature/email-otp

Conversation

@hajinsuha1
Copy link
Collaborator

@hajinsuha1 hajinsuha1 commented Dec 17, 2025

Description

This feature allows the mode for passwordless login and password reset to be configured in default.js.
It also changes the default mode used in template-retail-react-app from callback to email.

# Email mode
passwordless: {
   mode: 'email',
  ...
},
resetPassword: {
   mode: 'email',
  ...
},

# Callback mode
passwordless: {
   mode: 'callback',
   callbackURI: 'https://webhook.site/566b6fe3-84c7-4204-9270-3b0552d7d76e,
  ...
},
resetPassword: {
   mode: 'callback',
   callbackURI: 'https://webhook.site/566b6fe3-84c7-4204-9270-3b0552d7d76e',
  ...
},

This branch contains the following PRs:

After this PR is merged, to ensure the changes to the E2E tests pass the following PR that updates the extra-features-e2e-branch will need to be merged

Types of Changes

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Breaking change (could cause existing functionality to not work as expected)
  • Other changes (non-breaking changes that does not fit any of the above)

Breaking changes include:

  • Removing a public function or component or prop
  • Adding a required argument to a function
  • Changing the data type of a function parameter or return value
  • Adding a new peer dependency to package.json

Changes

template-retail-react-app

  • support setting passwordless login mode in config
  • Set default passwordless mode to 'email' for apps created via pwa-kit-create-app
  • Update password reset to use email mode by default. The mode can now be configured via default.js
  • update passwordless login extra-features e2e tests
  • update "Continue Securely" button to "Continue"
  • display errors in EmailConfirmation page
  • added new auth-utils.js that contains utility methods for mapping passwordless and reset password API error messages to user-friendly error messages
  • added error message mappings for the following API error messages:
    • "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.
    • "Monthly quota for passwordless login mode email has been exceeded" -> "This feature is not currently available"

commerce-sdk-react

  • Update getPasswordResetToken and authorizePasswordless to default locale to the one in CommerceApiProvider and pass callback_uri and idp_name only when they are defined
  • Update getPasswordResetToken to return a raw response and throw an error with the error message if the status code is not 200
    pwa-kit-create-app
  • Update default.js and /_app-config/index.jsx template to use email mode by default for passwordless login and password reset.

How to Test-Drive This PR

Email mode

Changes have been deployed to https://wasatch-mrt-feature-private.mrt-storefront-staging.com/

  1. Setup template-retail-react-app with a private client and start the app. By default, default.js is configured to use email mode for passwordless and resetPassword
    # Checkout this branch
    git checkout feature/email-otp
    
    # Cherry pick the test commit that contains the private client setup
    git fetch origin 465b813ed6274197134202d5b9b4fa9af905fb3c
    git cherry-pick 465b813ed6274197134202d5b9b4fa9af905fb3c
    
    # Build commerce-sdk-react locally by building the entire app
    npm ci
    
    # Set the private client secret via env vars and start the `template-retail-react-app`
    export PWA_KIT_SLAS_CLIENT_SECRET=SECRET
    cd packages/template-retail-react-app
    npm start
    

Passwordless Login

  1. Navigate to http://localhost:3000/us/en-US and login using the Auth Modal by clicking on the Profile icon, entering your email, and clicking the Continue button to trigger passwordless login (verify the locale that is sent is en-US)
  2. Verify an email was sent to you with the OTP
  3. Repeat steps 1-2 for the login page: http://localhost:3000/us/en-US/login page (verify the locale that is sent is en-US instead of en-GB)
  4. Repeat steps 1-2 for the checkout page after adding an item to cart (verify the locale that is sent is en-US)

Password Reset

  1. Open the developer console, start the reset password flow using the Auth Modal by clicking on the Profile icon, entering your email, clicking the Password button, clicking Forgot Password, then clicking theReset Password button (verify the locale that is sent is en-US)
  2. Verify an email was sent to you with the magic link
  3. Repeat steps 1-2 for the http://localhost:3000/us/en-US/login page (verify the locale that is sent is en-US instead of en-GB in the /password/reset request)
  4. Repeat steps 1-2 for the login from the checkout page after adding an item to cart (verify the locale that is sent is en-US)

Callback mode

Changes have been deployed to https://wasatch-mrt-passwordless-poc.mrt-storefront-staging.com/

  1. Update passwordless in default.js to

                passwordless: {
                    enabled: true,
                    mode: 'callback',
                    callbackURI: 'https://webhook.site/566b6fe3-84c7-4204-9270-3b0552d7d76e',
                    landingPath: '/passwordless-login-landing'
                },
    
  2. Update resetPassword in default.js to

                resetPassword: {
                    mode: 'callback',
                    callbackURI: 'https://webhook.site/566b6fe3-84c7-4204-9270-3b0552d7d76e',
                    landingPath: '/reset-password-landing'
                }
    
  3. Restart the server

    npm start
    

Passwordless Login

  1. Navigate to http://localhost:3000/us/en-US and login using the Auth Modal by clicking on the Profile icon, entering your email, and clicking the Continue button to trigger passwordless login. (verify the locale that is sent is en-US)
  2. Verify SLAS sent a POST request to the webhook callback uri: https://webhook.site/#!/view/566b6fe3-84c7-4204-9270-3b0552d7d76e
  3. Repeat steps 1-2 for the login page: http://localhost:3000/us/en-US/login page (verify the locale that is sent is en-US instead of en-GB)
  4. Repeat steps 1-2 for the checkout page after adding an item to cart

Password Reset

  1. Navigate to http://localhost:3000/us/en-US, Open the developer console, start the reset password flow using the Auth Modal by clicking on the Profile icon, entering your email, clicking the Password button, clicking Forgot Password, then clicking theReset Password button
  2. Verify SLAS sent a POST request to the webhook callback uri: https://webhook.site/566b6fe3-84c7-4204-9270-3b0552d7d76e
  3. Repeat steps 1-2 for the login page: http://localhost:3000/us/en-US/login page (verify the locale that is sent is en-US instead of en-GB)
  4. Repeat steps 1-2 for the login from the checkout page after adding an item to cart

Backwards compatibility (mode field missing)

The previous version of template-retail-react-app does not contain a mode field for passwordless and resetPassword in default.js. Let's make sure our changes in commerce-sdk-react are backwards compatible by making sure that when mode is missing and callbackURI is set in default.js, callback mode is used.

  1. Update passwordless in default.js to
                passwordless: {
                    enabled: true,
                    callbackURI: 'https://webhook.site/566b6fe3-84c7-4204-9270-3b0552d7d76e',
                    landingPath: '/passwordless-login-landing'
                },
    
  2. Update resetPassword in default.js to
                resetPassword: {
                    callbackURI: 'https://webhook.site/566b6fe3-84c7-4204-9270-3b0552d7d76e',
                    landingPath: '/reset-password-landing'
                }
    
  3. Restart the server
    npm start
    

Passwordless Login

  1. Login using the Auth Modal by clicking on the Profile icon, entering your email, and clicking the Continue button to trigger passwordless login
  2. Verify SLAS sent a POST request to the webhook callback uri: https://webhook.site/#!/view/566b6fe3-84c7-4204-9270-3b0552d7d76e
  3. Repeat steps 1-2 for the login page with a different locale: http://localhost:3000/us/en-US/login page (verify the locale that is sent is en-US instead of en-GB)
  4. Repeat steps 1-2 for the checkout page after adding an item to cart

Password Reset

  1. Open the developer console, start the reset password flow using the Auth Modal by clicking on the Profile icon, entering your email, clicking the Password button, clicking Forgot Password, then clicking theReset Password button
  2. Verify SLAS sent a POST request to the webhook callback uri: https://webhook.site/566b6fe3-84c7-4204-9270-3b0552d7d76e
  3. Repeat steps 1-2 for the login page with a different locale: http://localhost:3000/us/en-US/login page (verify the locale that is sent is en-US instead of en-GB)
  4. Repeat steps 1-2 for the login from the checkout page after adding an item to cart

Error Handling

These error messages align with the messages reviewed by UX: https://salesforce.quip.com/97bPANYv5D2U

Too many requests error

  1. Navigate to https://wasatch-mrt-feature-private.mrt-storefront-staging.com/us/en-us
  2. Open the developer console
  3. Click on the profile icon on the top right, enter your email, click the Continue button. Then click Resend Link button 6 times in 10 minutes.
  4. Verify Too many requests. For your security, please wait 10 minutes before trying again. error is displayed
    Screenshot 2026-01-22 at 12 29 18 PM
  5. Close and reopen the auth modal, enter your email, click the Continue button.
  6. Verify Too many requests. For your security, please wait 10 minutes before trying again. error is displayed
    Screenshot 2026-01-22 at 12 29 40 PM
  7. Click Password button, click Forgot Password link. Click Reset Password and repeat this 3 more times to trigger the too many requests error
  8. Verify Too many requests. For your security, please wait 10 minutes before trying again. error is displayed
    Screenshot 2026-01-22 at 12 29 50 PM
  9. Repeat these steps in the login page: https://wasatch-mrt-feature-private.mrt-storefront-staging.com/us/en-us/login
    Screenshot 2026-01-22 at 12 29 30 PM
    Screenshot 2026-01-22 at 12 27 56 PM
  10. Repeat these steps in the checkout page
    Screenshot 2026-01-22 at 12 30 12 PM
    Screenshot 2026-01-22 at 12 30 24 PM

no callback_uri is registered for client error

  1. Navigate to https://wasatch-mrt-passwordless-poc.mrt-storefront-staging.com/us/en-us
  2. Click on the profile icon on the top right, enter your email, click the Continue button.
  3. Verify This feature is not currently available. error is displayed
    Screenshot 2026-01-14 at 5 16 14 PM
  4. Click Password button, click Forgot Password link. Click Reset Password.
  5. Verify This feature is not currently available. error is displayed
    Screenshot 2026-01-14 at 5 16 02 PM
  6. Repeat these steps in the login page: https://wasatch-mrt-passwordless-poc.mrt-storefront-staging.com/us/en-us/login

pwa-kit-create-app

  1. Generate the project using pwa-kit-create-app
    GENERATOR_PRESET=retail-react-app-demo node packages/pwa-kit-create-app/scripts/create-mobify-app-dev.js --outputDir generated-retail-react-app-demo
    
  2. Select The Retail app with demo Commerce Cloud instance
  3. Once completed open default.js in the generated app
    code generated-retail-react-app-demo/config/default.js
    
  4. Verify in default.js, app.login.passwordless.mode and app.login.resetPassword.mode is set to email and callbackURI is commented out
  5. Verify in /_app-config/index.jsx the absoluteUrl utility is used
  6. In default.js
  7. set app.login.passwordless.enabled to true
    passwordless: { // Enables or disables passwordless login for the site. Defaults to: false enabled: true,
  8. update commerceAPI to use an instance of SLAS that has email otp enabled
    commerceAPI: { proxyPath: `/mobify/proxy/api`, parameters: { clientId: 'CLIENT_ID' organizationId: 'f_ecom_zzrf_001', shortCode: 'staging-001', siteId: 'RefArchGlobal' } },
  9. update ssrParameters to use the ecom instance with email OTP enabled
        ssrParameters: {
            ssrFunctionNodeVersion: '22.x',
            proxyConfigs: [
                {
                    host: 'staging-001.api.commercecloud.salesforce.com',
                    path: 'api'
                },
    
  10. In overrides/app/ssr.js
    1. set useSLASPrivateClient to true
    2. uncomment the applySLASPrivateClientToEndpoints property
        applySLASPrivateClientToEndpoints:
            /\/oauth2\/(token|passwordless\/(login|token)|password\/(reset|action))/,
    
  11. In overrides/app/components/_app-config/index.jsx add enablePWAKitPrivateClient={true} to the fields of CommerceApiProvider
  12. Set the private client secret and Start the app
    cd generated-retail-react-app-demo 
    export PWA_KIT_SLAS_CLIENT_SECRET=SECRET
    npm start
    
  13. Verify passwordless login works
  14. Verify password reset works

E2E Tests

  1. Start the playwright tests
    # Set the playwright tests to run against a `template-retail-react-app` with the new email OTP feature
    export PWA_E2E_USER_EMAIL=e2e.pwa.kit@gmail.com PWA_E2E_USER_PASSWORD=PASSWORD EXTRA_FEATURES_E2E_RETAIL_APP_HOME=https://wasatch-mrt-passwordless-test.mrt-storefront-staging.com
    
    npx playwright test --project=extra-features-desktop --project=extra-features-mobile --ui
    
  2. Verify all tests (desktop and mobile) pass except Verify password reset callback request on mobile when extra login features are not enabled. This tests fails as it runs against https://scaffold-pwa-e2e-pwa-kit-private.mobify-storefront.com/ which does not have the email otp changes yet
    Screenshot 2026-01-02 at 3 17 49 PM

Checklists

General

  • Changes are covered by test cases
  • CHANGELOG.md updated with a short description of changes (not required for documentation updates)

Accessibility Compliance

You must check off all items in one of the follow two lists:

  • There are no changes to UI

or...

Localization

  • Changes include a UI text update in the Retail React App (which requires translation)

* Add passwordless login mode configuration with email as default
* Update authorizePasswordless to require mode parameter and add locale support
* Remove commented out passwordless callbackURI configuration
* Remove passwordlessCallbackURI variable and simplify callbackURI condition in authorizePasswordlessLogin
* Remove conditional callbackURI spread and add mode parameter to checkout passwordless login
* Add buildCallbackURL utility function and conditionally spread callbackURI in passwordless login
* Rename buildCallbackURL to buildAbsoluteUrl and make passwordlessLoginCallbackURI optional
* update unit tests in template-retail-react-app and fix passwordless mode in login page
* make authorizePasswordless backward compatible and add unit tests for commerce-sdk-react
* Make passwordlessLoginCallbackURI non-optional with empty string default
* update changelog
* Use endsWith() to match passwordless login landing path and add test for localized paths
@cc-prodsec
Copy link
Collaborator

cc-prodsec commented Dec 17, 2025

Snyk checks have passed. No issues have been found so far.

Status Scanner Critical High Medium Low Total (0)
Open Source Security 0 0 0 0 0 issues
Licenses 0 0 0 0 0 issues

💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse.

@hajinsuha1 hajinsuha1 added the do not merge No matter what, do not merge this pr label Dec 17, 2025
Signed-off-by: Jinsu Ha <91205717+hajinsuha1@users.noreply.github.com>
@hajinsuha1 hajinsuha1 changed the title [DRAFT][DO NOT MERGE] Feature: Passwordless Login using SLAS Native Email OTP Feature: Passwordless Login using SLAS Native Email OTP Dec 30, 2025
@hajinsuha1 hajinsuha1 changed the title Feature: Passwordless Login using SLAS Native Email OTP [DO NOT MERGE] Feature: Passwordless Login using SLAS Native Email OTP Dec 30, 2025
* Update getPasswordResetToken to default locale to the one in CommerceApiProvider and pass callback_uri and idp_name only when they are defined
* Update resetPassword to default hint to cross_device and pass code_verifier only when it is defined
pwa-kit-create-app
* Update default.js and /_app-config/index.jsx template to use email mode by default for passwordless login and password reset.
* Update password reset to use email mode by default. The mode can now be configured via default.js
…ly" to "Continue" (#3556)

* update passwordless and password reset e2e tests to veirfy mode email is used

* Update EXTRA_FEATURES_E2E_RETAIL_APP_HOME to support environment variable configuration

* Update mobile e2e tests to verify 'email' mode for passwordless login and password reset requests

* update "continue securely" to "continue"
@hajinsuha1 hajinsuha1 changed the title [DO NOT MERGE] Feature: Passwordless Login using SLAS Native Email OTP [DO NOT MERGE] Feature: Passwordless Login and Password Reset supports use of SLAS Native Email OTP Jan 8, 2026
@hajinsuha1 hajinsuha1 changed the title [DO NOT MERGE] Feature: Passwordless Login and Password Reset supports use of SLAS Native Email OTP [DO NOT MERGE] Feature: Passwordless Login and Password Reset supports use of email mode Jan 8, 2026
hajinsuha1 and others added 3 commits January 9, 2026 10:56
Signed-off-by: Jinsu Ha <91205717+hajinsuha1@users.noreply.github.com>
@hajinsuha1
Copy link
Collaborator Author

hajinsuha1 commented Jan 12, 2026

When the SLAS APIs /password/reset return a 400 due to exceeding the email limit of 3 per 10min the PWA Kit displays the error msg "Feature is unavailable..." Should we change this to display the general API error "Something went wrong..." or a new error message like "Too many requests. For your security, please wait 10 minutes before trying again"?
Screenshot 2026-01-12 at 2 03 37 PM

@hajinsuha1 hajinsuha1 marked this pull request as ready for review January 13, 2026 16:20
@hajinsuha1 hajinsuha1 requested a review from a team as a code owner January 13, 2026 16:20
@hajinsuha1 hajinsuha1 changed the title [DO NOT MERGE] Feature: Passwordless Login and Password Reset supports use of email mode Feature: Passwordless Login and Password Reset supports use of email mode Jan 13, 2026
@hajinsuha1 hajinsuha1 removed the do not merge No matter what, do not merge this pr label Jan 13, 2026
…asswordless and reset password (#3574)

* Enhance error handling for password reset functionality and add new error message for "too many requests" errors
* display errors in Email Confirmation page
* added new auth-utils.js that contains utility methods for mapping passwordless and reset password API error messages to user-friendly error messages
* added mapping of the following API error messages: "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., "Monthly quota for passwordless login mode email has been exceeded" -> "This feature is not currently available"
* changelog updates and translations
Signed-off-by: Jinsu Ha <91205717+hajinsuha1@users.noreply.github.com>
@hajinsuha1 hajinsuha1 requested a review from yunakim714 January 16, 2026 15:02
@hajinsuha1 hajinsuha1 mentioned this pull request Jan 22, 2026
12 tasks
const result = await auth.resetPassword(input)
expect(result).toBe(mockResponse)
expect(resetPasswordSpy).toHaveBeenCalled()
const callArgs = resetPasswordSpy.mock.calls[0][0] as any
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we reduce the any explicit assignment?

const usid = this.get('usid')
// Default to 'callback' mode for backward compatibility as older versions of the template-retail-react-app
// do not pass the mode parameter. Newer versions should explicitly pass the mode.
const mode = parameters.mode || 'callback'
Copy link
Contributor

Choose a reason for hiding this comment

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

The upcoming release is gonna be breaking change version. If you want to change the default, now is the time

Copy link
Contributor

Choose a reason for hiding this comment

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

Make sure you document this behavior somewhere to make it clear

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

will make sure it's documented in the dev docs


- 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 pass locale, 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)
Copy link
Contributor

Choose a reason for hiding this comment

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

Let's bundle these up into one feature PR (this one since this feature branch has all the code changes). customers do not need to know about non-feature branches.

await user.click(screen.getByText(/Continue/i))

expect(
mockAuthHelperFunctions[AuthHelpers.AuthorizePasswordless].mutateAsync
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we mock this at the api level with msw?

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.',
Copy link
Contributor

Choose a reason for hiding this comment

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

Nit: while doing this is okay since we want to test if the error is being rendered properly when occurring, but realistically, we may want to trigger 6 times on resent link click, and on the 6th time is when error is mocked and returned.

[AuthHelpers.AuthorizePasswordless]: {mutateAsync: jest.fn()}
}

jest.mock('@salesforce/commerce-sdk-react', () => {
Copy link
Contributor

Choose a reason for hiding this comment

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

for consistency, can we mock the data at api level to avoid doing this since we are currently using mws for various calls in this test file? Besides, this is a page component, it would be better to mock data at api-level


// Verify that authorizePasswordless is called with correct parameters
await waitFor(() => {
expect(
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we mock this call at api-level?

hajinsuha1 and others added 4 commits January 22, 2026 16:46
Co-authored-by: Alex Vuong <alex.vuong@salesforce.com>
Signed-off-by: Jinsu Ha <91205717+hajinsuha1@users.noreply.github.com>
- mock at api level for tests
- update changelog
@yunakim714
Copy link
Collaborator

yunakim714 commented Jan 26, 2026

Hey Jinsu - I'm testing email mode on https://wasatch-mrt-feature-private.mrt-storefront-staging.com/ , and when I click Continue, I get the email sent successfully but I see the wrong login link. Wanted to double check on this
Screenshot 2026-01-26 at 2 37 14 PM
(Same for password reset)

},
parameters: {
...(callbackURI && {callbackURI: callbackURI}),
...(callbackURI && {callbackURI}),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we omit this if mode === email ?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I chose to let the API handle the scenario when mode = email and callbackURI is set.
The API will ignore the callback URI when mode is set to email
Screenshot 2026-01-26 at 3 34 47 PM

channel_id: parameters.channel_id || slasClient.clientConfig.parameters.siteId,
client_id: parameters.client_id || slasClient.clientConfig.parameters.clientId,
callback_uri: parameters.callback_uri,
...(parameters.callback_uri && {callback_uri: parameters.callback_uri}),
Copy link
Collaborator

Choose a reason for hiding this comment

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

Same here? If mode === email, do not set this ? Or is this ignored if mode === email ?

hajinsuha1 and others added 2 commits January 26, 2026 15:06
Signed-off-by: Jinsu Ha <91205717+hajinsuha1@users.noreply.github.com>
Co-authored-by: Yuna Kim <84923642+yunakim714@users.noreply.github.com>
Signed-off-by: Jinsu Ha <91205717+hajinsuha1@users.noreply.github.com>
@jeremy-jung1
Copy link
Collaborator

Screenshot 2026-01-26 at 5 06 57 PM Can see that correlation ID is getting included and surfaces in log center in case of issues

@hajinsuha1 hajinsuha1 requested a review from yunakim714 January 28, 2026 15:07
@hajinsuha1 hajinsuha1 merged commit 5c30741 into develop Jan 28, 2026
40 checks passed
@hajinsuha1 hajinsuha1 deleted the feature/email-otp branch January 28, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants