Skip to content

feat: add app_pwakit_base B2C cartridge for transactional email delivery - #4012

Merged
bendvc merged 67 commits into
developfrom
feature/b2c-email-cartridge
Oct 19, 2026
Merged

bendvc merged 67 commits into
developfrom
feature/b2c-email-cartridge

Conversation

@bendvc

@bendvc bendvc commented Sep 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Ports the app_pwakit_base B2C Commerce cartridge from sf-next into the PWA Kit template, and fixes the GLO verify authentication that was broken when enablePWAKitPrivateClient is used.

Deployed environment: https://scaffold-pwa-bens-playground.mobify-storefront.com/global/en-US

What's included

New: app_pwakit_base cartridge (packages/template-retail-react-app/cartridges/app_pwakit_base/)

Component Purpose
scripts/hooks/sendOrderAccessCode.js Implements sfcc.app.order.sendOrderAccessCode — called by SCAPI's requestOrderAccessCode to send the GLO access-code email
rest-apis/pwakit-notify/Notify.js SCAPI Custom REST API (c_pwakit_notify scope) — called by the PWA Kit SSR server after a SLAS passwordless/OTP/password-reset callback to send the corresponding email
scripts/helpers/sendNotification.js Renders ISML templates and delivers via dw/net/Mail (HTML body). Replace the body of send() to swap in any third-party provider (SendGrid, Mailchimp, etc.)
scripts/helpers/getStorefrontHost.js Reads the pwakitStorefrontHost site preference for this site. Returns null when unset — callers omit magic-link CTAs but still send the email. Stubbed for the future Site.getCurrent().mrtExternalHostname platform API.
scripts/helpers/buildOrderLookupUrl.js Shared helper that constructs the GLO verification URL (https://{host}/{siteId}/{locale}/order-lookup/verify/{orderNo}?token=…)
templates/default/email/ Four ISML templates: guestOrderLookup, passwordlessLogin (with inline access code), registrationVerification, passwordReset. CTA button and copy-URL fallback are conditionally rendered — templates degrade gracefully when no host can be resolved.
hooks.json + package.json Hook registration (required — SFCC discovers hooks only at code-version activation)
staticfiles/…/OrganizationPreferences.xml Registers pwakitNotifyEnabled (bool) under Administration → Global Preferences → PWA Kit
staticfiles/…/SitePreferences.xml Registers pwakitStorefrontHost (string) under Merchant Tools → Custom Preferences → pwakit (per-site)
README.md Full install, config, and customization guide

Fix: GLO verify 401 with enablePWAKitPrivateClient (packages/template-retail-react-app/app/ssr.js)

All 5 GLO SSR endpoints were reading the SLAS token from the cc-at_* cookie. This is wrong — cc-at_* is only written in SFRA hybrid mode. With a standard SLAS setup the access token lives in localStorage and is never in any cookie. The client (verify.jsx) was already sending Authorization: Bearer <jwt> via useAccessToken().getTokenWhenReady(). The fix reads req.headers.authorization directly, which works for both public and private client configurations.

Fix: null-token guard in verify.jsx

Added if (!token) throw new Error('No access token available') after getTokenWhenReady() resolves. Without this, a null/empty token produces Authorization: Bearer null — a truthy string that passes the server's if (!authorization) check and reaches SCAPI with an invalid credential. The guard routes this case into the existing catch block, showing the generic error UI. Test coverage added.

Fix: pwa-kit-create-app scaffold templates (packages/pwa-kit-create-app/assets/…/ssr.js.hbs)

Both scaffold templates had the same broken cc-at_* cookie auth as the main ssr.js. Updated to match.


Enhancements over the sf-next version

Area sf-next This PR
getStorefrontHost.js Hard-coded single site preference + caller-supplied callerHost allowlist validation (org-level, comma-separated) Simplified: reads pwakitStorefrontHost directly from the site preference for the current site — no allowlist, no caller-supplied hostname. Stubbed for future Site.getCurrent().mrtExternalHostname platform API.
pwakitNotifyEnabled preference Not present Allows disabling the cartridge's built-in delivery without removing it from the cartridge path — useful when plugging in Marketing Cloud or another provider
sendOrderAccessCode hook chaining Calls HookMgr.callNextHook() (not a valid B2C API — the platform chains hooks automatically) Removed; B2C handles chaining implicitly
isNotifyEnabled() error handling Crashes if preference schema not imported Catches the dw.system exception and defaults to enabled — safe to deploy before importing OrganizationPreferences.xml
Template naming Content-based (gloAccessCode, passwordlessMagicLink, otpVerification, passwordResetMagicLink) Flow-based (guestOrderLookup, passwordlessLogin, registrationVerification, passwordReset)
passwordlessLogin email Magic link only Includes inline access code display — the token is extracted from magicLinkPath and shown in the email so users can sign in without clicking the link
sendOrderAccessCode when host unconfigured Constructs a broken https://null/… URL Warns and sends the email with lookupLink: null — template degrades gracefully (CTA and copy-URL hidden via <isif>, disclaimer and access code still shown)
URL construction Inline in each call site Extracted into buildOrderLookupUrl.js shared helper — used by both Notify.js and sendOrderAccessCode.js
Locale forwarding Not present ssr.js extracts the locale from the redirectUrl query param (e.g. /en-US/account) via extractLocaleFromUrl and appends ?locale=en-US to the pwakit-notify API call. The cartridge reads request.locale (set by B2C from the query param) and passes it to sendNotification, which hands it to ISML so email templates render in the shopper's language rather than the site default

How to test

Deployed environment: https://scaffold-pwa-bens-playground.mobify-storefront.com/global/en-US

Prerequisites

  • Cartridge uploaded to WebDAV and on the cartridge path for RefArchGlobal
  • Both OrganizationPreferences.xml and SitePreferences.xml imported
  • Code version deactivated and reactivated (required for hook + Custom API discovery)
  • pwakitStorefrontHost set to scaffold-pwa-bens-playground.mobify-storefront.com (Merchant Tools → Custom Preferences → pwakit, per site)

Flow 1 — Guest Order Lookup (GLO)

Requires a B2C order placed as a guest on RefArchGlobal.

  1. Navigate to /global/en-US/order-lookup
  2. Enter a valid guest order number and the email used to place it, click Find My Order
  3. Check your inbox — you should receive an access code email rendered from guestOrderLookup.isml containing both an access code and a "View Order" button
  4. Enter the access code on the verify screen — you should land on the order detail page
  5. Magic link path: click the link in the email directly — it should pre-fill the code and auto-submit
  6. Error path: enter a wrong code — confirm "invalid or expired" error appears
  7. Rate-limit path: submit an incorrect code 5+ times in quick succession — confirm 429 "Too many attempts" error

Flow 2 — Passwordless login

  1. On the login page, choose Send Magic Link / passwordless option
  2. Enter your email, submit
  3. Check inbox — you should receive a passwordless email from passwordlessLogin.isml that contains both a magic link button and an inline access code
  4. Click the link — you should be logged in on the storefront
  5. Alternatively, enter the inline code on the sign-in screen to verify that path works too

Flow 3 — OTP verification

  1. Trigger an OTP verification flow (e.g. email change, depending on storefront config)
  2. Check inbox for OTP email from registrationVerification.isml
  3. Enter the code to confirm

Flow 4 — Password reset

  1. Click Forgot password on the login page
  2. Enter your email
  3. Check inbox — you should receive a reset-password magic-link email from passwordReset.isml
  4. Click the link — you should land on the reset-password landing page

Disabling the cartridge

  1. In Business Manager, set Administration → Global Preferences → Custom Preferences → PWA Kit → Notifications Enabled to false
  2. Repeat any flow above — emails should NOT be delivered, but the flows should complete without error (the hook no-ops and returns OK)

Decisions worth calling out

1. Auth reads req.headers.authorization, not cookies

The GLO endpoints now trust the Authorization header sent by the client. This means the SSR server does not independently verify the token — it forwards whatever the client sends. This is consistent with how all other SCAPI-proxied endpoints in PWA Kit work. A malicious client could theoretically send someone else's token, but that token is tied to their own SLAS session, not to the order — the real gate is the {orderViewCode, email} pair validated by SCAPI.

2. Email stored in cc-goa_* cookie

After verify, the server stores {email, verifiedCode} in an HttpOnly / Secure / SameSite=Strict session cookie (Max-Age=900). This is needed because subsequent endpoints (/order, /cancel, /return) must re-authenticate with SCAPI using the same {email, orderViewCode} pair, and the client pages don't re-supply the email. The alternative — having the client re-send email on every request — was considered and rejected because it would require the server to trust whatever email the client provides rather than what was actually verified.

3. guestOrderLookup.enabled is false by default

The feature is off unless explicitly enabled in config/default.js. This is intentional — GLO requires a B2C instance with the requestOrderAccessCode SCAPI endpoint enabled and the cartridge deployed. Turning it on for a project that doesn't have those prerequisites would silently fail.

4. Magic link uses siteId.toLowerCase(), not siteAlias

The magic link path is built as /<siteId-lowercase>/<locale>/order-lookup/verify/<orderNo>?token=<code>. If the storefront registers a siteAlias in config/default.js (e.g. RefArchGlobal → global), the generated path (/refarchglobal/…) won't match the registered route (/global/…). This is documented as a known limitation in the README and in sendOrderAccessCode.js. Adding an alias-mapping preference was explicitly rejected to avoid creating a second place to maintain the alias mapping — the recommended fix is a CDN redirect or a custom hook override.

5. pwakitNotifyEnabled defaults to true when the preference is unset

On first deploy, before OrganizationPreferences.xml is imported, isNotifyEnabled() returns true. This means emails fire on the first activation. The alternative (default-disabled until explicitly enabled) was considered worse UX for the common case where the merchant just wants email to work out of the box.

6. pwakitStorefrontHost is a site preference, not an org preference

The host used for magic-link URL construction is now configured per-site in Merchant Tools → Custom Preferences → pwakit, rather than as an org-level comma-separated allowlist. This is simpler to configure for multi-site setups and removes the need for callerHost validation — the cartridge reads the host it already knows from its own configuration rather than trusting a value supplied by the caller. When the B2C Script API gains native access to the MRT/eCDN hostname (e.g. Site.getCurrent().mrtExternalHostname), the site preference becomes a fallback only.

7. Locale forwarding uses extractLocaleFromUrl, not Accept-Language

The SSR sendMagicLinkEmail handler receives the redirectUrl that the storefront passes when initiating a passwordless or reset-password flow (e.g. https://mystore.com/en-US/account). The locale is extracted from the first or second path segment via extractLocaleFromUrl — the regex matches both /en-US/... (locale-first) and /us/en-US/... (site-alias-first, which is the retail-react-app default URL shape) — and appended as ?locale= to the pwakit-notify API call. B2C sets request.locale from this query param, which is then forwarded to ISML. This approach avoids relying on the Accept-Language header (which may not match the storefront locale) and works for both absolute URLs and relative paths.

The cartridge additionally treats request.locale === 'default' (B2C's sentinel for "no locale set") as unset and falls through to Site.defaultLocale, so emails always render in a real locale rather than the literal string 'default'.


Pre-merge review fixes

Addressed in the final round of review before merge:

  • extractLocaleFromUrl locale extraction: regex updated to match locale as the first or second URL path segment, so retail URLs (/us/en-US/...) are handled correctly alongside plain locale-first paths (/en-US/...)
  • Registration-verification callback gate: added the same passwordless.mode !== 'callback' guard present on the passwordless and reset-password callback routes — the endpoint now returns 400 when callback mode is not configured
  • GLO verify 401/403 pass-through: SCAPI 401/403 responses from the verify endpoint now return 401 Unauthorized to the client instead of 502 Service error
  • request.locale === 'default' sentinel: both sendNotification.js and sendOrderAccessCode.js now skip the 'default' sentinel and fall through to Site.defaultLocale
  • notifyClientId default: restored the notifyClientId placeholder in config/default.js that was accidentally blanked during a credential-revert commit

…O verify auth

Adds a new app_pwakit_base cartridge that handles transactional email delivery
(passwordless magic link, OTP verification, GLO access code) via B2C ISML
templates and dw/net/Mail, replacing the sfnext dependency for email hooks.

Key changes:
- New cartridge with sfcc.app.order.sendOrderAccessCode, sendNotification helper,
  ISML email templates, hooks.json, and OrganizationPreferences metadata
- Fix GLO /api/order-lookup/verify and related SSR endpoints to read the SLAS
  access token from the incoming Authorization header (sent by the client via
  useAccessToken) instead of cookies — cc-nx-g_ is the SLAS refresh token, not
  the access token, and the real JWT is never stored in any cookie
- Enable guestOrderLookup and passwordless feature flags in config/default.js
- Add GLO and email translation keys (en-US, en-GB)
- Reset-password-landing.jsx email field PII fix
- Remove dead buildPlainText/plainBody/Resource from sendNotification.js
  (was partially removed in sfnext because it broke email delivery)
- Revert test-only flags: enablePWAKitPrivateClient=false,
  guestOrderLookup.enabled=false
- Add null-token guard in verify.jsx; add test coverage for the case
  where getTokenWhenReady() resolves null (prevents 'Bearer null' reaching SCAPI)
- Sync both pwa-kit-create-app .hbs templates to use req.headers.authorization
  instead of the broken cc-at_ cookie approach (matches ssr.js fix)
- Add app_pwakit_base/README.md with install, config, and customization docs
- Update CHANGELOG: replace inaccurate Unreleased entry, fix stale GLO entry
- Clean up storefrontHostProvider.js: named functions, explicit module.exports
- Add known-limitation comment for siteAlias in sendOrderAccessCode.js
@bendvc
bendvc requested a review from a team as a code owner September 11, 2026 22:28
@git2gus

git2gus Bot commented Sep 11, 2026

Copy link
Copy Markdown

Git2Gus App is installed but the .git2gus/config.json doesn't have right values. You should add the required configuration.

@cc-prodsec

cc-prodsec commented Sep 11, 2026 •

Copy link
Copy Markdown
Collaborator

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

Status Scan Engine 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.

…cartridge

# Conflicts:
#	packages/template-retail-react-app/CHANGELOG.md
Test-only change that was not meant to be pushed. CI (Lighthouse) has
no PWA_KIT_SLAS_CLIENT_SECRET set, so useSLASPrivateClient: true causes
the dev server to hard-crash at startup before any tests can run.
Six violations in both .hbs templates:
- Split jose import onto multiple lines
- Wrap getConfig proxyPath expression in template literals
- Wrap encodeURIComponent(organizationId) in URL template literal
- Remove &email= from magicLinkPath (matches ssr.js; email is passed
  separately as the recipient arg, not embedded in the URL)
- Break sendViaB2cCartridge call arguments onto separate lines
GLO handler tests: add Authorization header to 5 tests that were written
for cookie-only auth. All handlers now check req.headers.authorization
first, so requests without it return 401 "Missing authorization" before
reaching the cookie-based session checks.

ssr.test.js: remove emailLink/Marketing Cloud tests. emailLink was
replaced by sendViaB2cCartridge; Marketing Cloud integration is no
longer part of this implementation.
…lo-access-code REST path

- Rename ISML templates and resource keys: gloAccessCode→guestOrderLookup,
  passwordlessMagicLink→passwordlessLogin, otpVerification→registrationVerification,
  passwordResetMagicLink→passwordReset
- Fix glo-access-code REST branch in Notify.js to construct lookupLink (was missing,
  causing broken button/URL in email)
- Guard decodeURIComponent in passwordless token extraction against malformed URIs
- Remove unused email and plaintext fallback resource keys
- Add inline access code block to passwordlessLogin.isml
- Remove HookMgr.callNextHook() from sendOrderAccessCode hook (B2C chains automatically)
…er, and graceful degradation from sf-next

- Add buildOrderLookupUrl.js shared helper for GLO verification URL construction
- Notify.js: read body.callerHost (renamed from host), validate per magic-link branch,
  return 503 on host failure, use buildOrderLookupUrl for glo-access-code
- sendOrderAccessCode.js: graceful degradation when pwakitStorefrontHosts unconfigured
  (warn + send with lookupLink: null instead of constructing https://null/...)
- storefrontHostProvider.js: case-insensitive allowlist comparison
- guestOrderLookup.isml, passwordlessLogin.isml, passwordReset.isml: wrap CTA button
  and copy-URL fallback in <isif condition="${!empty(pdict.lookupLink/magicLink)}">
  so emails degrade gracefully when no host can be resolved
- schema.yaml: rename host → callerHost with description
- ssr.js: rename host → callerHost in notify payload
…fy schema

SCAPI's strict validator fails on oneOf + headless discriminator (no mapping,
inline sub-schemas) because it can't resolve discriminator values to component
names. anyOf without discriminator passes validation correctly on all instances.
…ting rules, doc corrections, test coverage

- sendViaB2cCartridge: retry once with fresh token on 401 (cached SLAS guest token expired mid-flight)
- ssr.js: export getNotifyToken, sendViaB2cCartridge, _resetNotifyTokenCacheForTest for testability
- ssr.js: add registration-verification-callback route (mode=callback OTP for one-click checkout registration)
- ssr.js: fix SCAPI URL missing /sites/{siteId} segment
- ssr.js: add order-lookup and order-lookup/verify routes to eCDN routingRules
- ssr.test.js: add 10 tests covering getNotifyToken (cache, public/private client) and sendViaB2cCartridge (all 4 types, 401 retry, 5xx)
- one-click-user-registration.jsx: wire registration verification to mode=callback with registrationVerification callbackURI
- config/default.js: add registrationVerification.callbackURI
- storefrontHostProvider.js: fail-closed security — reject unrecognized caller hosts instead of falling back to B2C hostname
- buildOrderLookupUrl.js: add deliberate-decision comment on token in URL param
- Notify.js: add otp type comment clarifying token is a short SLAS TOTP
- README: fix instance:import → meta:import, add SLAS allowed redirect URI table, correct hook registration instructions
- CHANGELOG: fix #XXXX → #4012, correct deployment note (code:activate vs manual BM cycle)
- email.properties: remove 4 unused *.pageTitle keys
- package.json: add build:cartridge and deploy:cartridge scripts
- .gitignore: ignore cartridges/app_pwakit_base.zip
@bendvc
bendvc requested a review from a team as a code owner September 17, 2026 17:43
Replace the old zip+sfcc-ci workflow with b2c code deploy. Add the
Eclipse .project marker that b2c requires for cartridge discovery, and
pass --config dw.json so credentials resolve from the package root
without needing to cd into the cartridges directory.

Update README to match: b2c CLI install, flat dw.json requirement,
clean single First-time setup section, and b2c-based redeploy steps.

dw.json and cartridges/dw.json are added to .gitignore; the zip
artifact is removed since b2c handles archiving internally.
…} broke SCAPI routing

The code review mistakenly added /sites/{siteId} to the pwakit-notify URL path, treating
it as a site-scoped SCAPI endpoint. The API is org-level — siteId is a query param only
(as defined in schema.yaml). SCAPI 404s the site-level path form.

Restores the working URL: /custom/pwakit-notify/v1/organizations/{orgId}/notify?siteId={siteId}
Move siteId from query parameter to URL path in the pwakit-notify
Custom REST API so SCAPI establishes site context automatically before
invoking the cartridge. This makes Site.getCurrent() reliable inside
Notify.js and aligns the endpoint with SCAPI's site-scoped routing
convention used by built-in APIs.

- schema.yaml: path changed from /notify to /sites/{siteId}/notify;
  siteId moved from query to path parameter
- ssr.js: URL updated to include /sites/{siteId}/ in path, removing
  the siteId query param
- ssr.test.js: URL assertion updated to match new site-scoped path
- README: deployment docs updated; --reload makes separate activation
  step unnecessary
- config/default.js, _app-config: sandbox config (RefArch, private
  SLAS client)
bendvc added 3 commits October 9, 2026 14:32
…ault

- extractLocaleFromUrl: extend regex to match locale as first OR second
  path segment so retail URLs (/{site}/{locale}/...) are handled correctly
- registration-verification callback: add mode !== 'callback' guard,
  consistent with passwordless and reset-password callback routes
- verify endpoint: map 401/403 from SCAPI to 401 instead of 502
- sendNotification/sendOrderAccessCode: treat request.locale === 'default'
  as unset and fall through to Site defaultLocale
- validateSlasCallbackToken: add TODO comment about missing aud validation
- config/default.js: restore notifyClientId blanked by b4e911c
…cartridge

# Conflicts:
#	packages/pwa-kit-create-app/CHANGELOG.md
#	packages/template-retail-react-app/CHANGELOG.md
#	packages/template-retail-react-app/app/ssr.js

@sf-jie-dai sf-jie-dai left a comment •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Findings at 54478e4f161ecadf9af7952bfa075b2f2865c922
P1 - release documentation still tells merchants to restore the public notification credential. packages/template-retail-react-app/CHANGELOG.md:3 tells readers to add c_pwakit_notify to a generic SLAS client's default scopes because the guest-login helpers cannot request a scope. That no longer matches the implementation: getNotifyToken() requires notifyClientId plus PWA_KIT_NOTIFY_SLAS_CLIENT_SECRET, then uses loginGuestUserPrivate against the dedicated notify client. If a merchant follows the changelog using the shopper client, browser-minted tokens regain c_pwakit_notify and can call the public Custom API directly. The entry also names the removed pwakitStorefrontHosts preference. Replace it with the dedicated-private-client setup from the cartridge README, explicitly prohibit granting this scope to the shopper client, and name pwakitStorefrontHost.\n\n-
P2 - the pre-merge SSR fixes were not synchronized to either generated SSR template. e7084ee48 fixes the canonical app/ssr.js, but both assets/bootstrap/js/overrides/app/ssr.js.hbs:343-350 and assets/templates/@salesforce/retail-react-app/app/ssr.js.hbs:343-350 still only recognize a locale as the first URL segment. Thus a standard /{site}/{locale}/... redirect URL, such as /global/en-GB/..., drops the locale when sendMagicLinkEmail calls pwakit-notify. The same templates also omit the new registration-verification callback-mode guard (canonical app/ssr.js:735-741) and still map SCAPI 401/403 on GLO verification to 502 rather than 401 (canonical app/ssr.js:927-943). Mirror all three changes in both templates and add generated-project coverage for a site-and-locale redirect URL.\n\n-
P2 - generated-flow comments and the create-app release notes remain contradictory. Both generated config templates emit mode: 'callback' but their adjacent comments still say the default is email (for example assets/bootstrap/js/config/default.js.hbs:87-88,112-113). packages/pwa-kit-create-app/CHANGELOG.md:3 likewise still documents the removed fallback to public loginGuestUser when the private secret is missing. The generated SSR code instead fails fast without notifyClientId and PWA_KIT_NOTIFY_SLAS_CLIENT_SECRET. Correct this release/config guidance so it does not direct a merchant to remove the private-client prerequisite or configure the wrong default flow.\n\n## Verification\n\n- Reviewed the exact current head and the post-review commits, including the merge from develop.\n- All published CI checks are green, including lint, generated-project checks on Linux and Windows, e2e private-client, bundle size, and the Node-version matrix.\n- git diff --check origin/develop...54478e4f161ecadf9af7952bfa075b2f2865c922 passes.\n- GitHub still reports mergeStateStatus: DIRTY for the PR despite the branch incorporating the fetched develop head; recheck its mergeability after the above fixes.

…nd fix release notes

- CHANGELOG (template-retail-react-app): replace old public-client notify setup instructions
  with dedicated private notifyClientId requirement; rename pwakitStorefrontHosts → pwakitStorefrontHost;
  explicitly warn against adding c_pwakit_notify to the shopper client
- CHANGELOG (pwa-kit-create-app): remove stale public-client fallback description; document
  that getNotifyToken() requires notifyClientId + PWA_KIT_NOTIFY_SLAS_CLIENT_SECRET
- Both ssr.js.hbs templates: fix extractLocaleFromUrl regex to match locale as first OR second
  URL segment (handles /{site}/{locale}/... retail URLs)
- Both ssr.js.hbs templates: add registration-verification callback mode gate (mirrors
  canonical app/ssr.js:735-741) so the route rejects requests when passwordless mode ≠ callback
- Both ssr.js.hbs templates: map SCAPI 401/403 on GLO /verify to HTTP 401 instead of 502
- Both default.js.hbs templates: fix comments — mode defaults to 'callback', not 'email'
@bendvc

bendvc commented Oct 13, 2026

Copy link
Copy Markdown
Contributor Author

Addressed all three findings from the Oct 13 review in a6eadaacf:

P1 — Release notes instruct the retired public-client model
Fixed in both changelogs:

  • template-retail-react-app/CHANGELOG.md: replaced the old "add `c_pwakit_notify` to your SLAS client's default Scopes" guidance with the dedicated private-client requirement (`notifyClientId` + `PWA_KIT_NOTIFY_SLAS_CLIENT_SECRET`); renamed `pwakitStorefrontHosts` → `pwakitStorefrontHost`; added an explicit warning not to add this scope to the shopper/public client.
  • pwa-kit-create-app/CHANGELOG.md: removed the stale public-client fallback description; now correctly states that `getNotifyToken()` requires `notifyClientId` and `PWA_KIT_NOTIFY_SLAS_CLIENT_SECRET` with no fallback.

P2 — Pre-merge SSR fixes not synchronized to generated templates
Applied all three missing changes to both assets/bootstrap/js/overrides/app/ssr.js.hbs and assets/templates/@salesforce/retail-react-app/app/ssr.js.hbs:

  • extractLocaleFromUrl regex now matches locale as first or second URL segment (^(?:\/[^/]*)?\/([a-z]{2}-[A-Z]{2})(\/|$)), handling /{site}/{locale}/... retail URLs
  • Registration-verification callback route now includes the passwordless.mode !== 'callback' guard (mirrors canonical app/ssr.js:735-741)
  • GLO /verify now maps SCAPI 401/403 → HTTP 401 instead of 502

P2 — Generated config comments and create-app changelog contradict shipped behavior
Fixed in both default.js.hbs templates: comments now say Defaults to: 'callback' instead of 'email', matching the emitted value.

sf-jie-dai
sf-jie-dai previously approved these changes Oct 13, 2026

@sf-jie-dai sf-jie-dai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved at the requester's direction after reviewing current head a6eadaa. The prior generated-template and release-guidance findings are resolved. The PR remains conflicting with develop and has not completed its full CI matrix at this head; rebase/merge develop and required checks remain prerequisites to merge.

…cartridge

# Conflicts:
#	packages/template-retail-react-app/CHANGELOG.md
sf-jie-dai
sf-jie-dai previously approved these changes Oct 14, 2026

@sf-jie-dai sf-jie-dai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Approved after reviewing merge commit 2a01f43. The develop merge resolves the prior changelog conflict without regressing the reviewed dedicated-private-client, generated-template, or release-guidance changes. Full CI is green.

syadupathi-sf
syadupathi-sf previously approved these changes Oct 15, 2026
bendvc and others added 2 commits October 16, 2026 08:53
@bendvc
bendvc dismissed stale reviews from syadupathi-sf and sf-jie-dai via b1dcee6 October 16, 2026 18:14

@sf-jie-dai sf-jie-dai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Reviewed current head ba4ae17. The develop merge preserves the dedicated private notify-client, generated-template, and callback-route fixes. The new Vitest 4.1.11 override is correctly applied to the create-app package, generated extensible-app manifest, and retail template to cover npm 10 installs. All published CI checks, including generated projects on Linux and Windows and the Node/npm matrix, are green. Approved.

@sf-jie-dai sf-jie-dai left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Re-approving reviewed head ba4ae17 at requester direction. Published checks remain green.

// We do not check verified/unverified state — that is managed by SLAS and
// is not reliably exposed through the B2C scripting API.
var otpCustomer = CustomerMgr.getCustomerByLogin(recipient);
if (!otpCustomer || !otpCustomer.registered) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P1] This guard prevents new customers from receiving their registration OTP. SLAS creates the B2C profile only after the shopper exchanges the TOTP, not when authorizePasswordlessCustomer invokes this callback (platform contract). With the new register_customer: true caller, a fresh email therefore returns success here without sending the code needed to create the profile. Please remove the existing-customer requirement from the registration OTP branch, correct the corresponding README assumption, and test delivery when getCustomerByLogin() returns null. The dedicated notify-client scope and authenticated SSR callback should remain in place.


useEffect(() => {
if (!autoCode) return
if (!email) return // wait for the user to enter their email before auto-submitting

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] A magic-link arrival submits partial email addresses while the shopper types. This effect reruns on every email edit and submits after only 100 ms; the form uses noValidate, and onSubmit checks code completeness but not email validity. Opening ?token=123456 and pausing after typing a therefore sends that partial address to the verify endpoint, disables the input while pending, and consumes one of the five allowed attempts. Please limit auto-submission to email supplied through router state; for direct arrivals, prefill the code and require validated email plus explicit submission. Add a test asserting that typing an email does not trigger a verification request.

// Password reset requires a pre-existing registered account. Succeed
// silently for unrecognised addresses — this prevents email enumeration
// while avoiding delivery to non-existent accounts.
var prCustomer = CustomerMgr.getCustomerByLogin(recipient);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] The recipient email is not necessarily the customer's login. SLAS reset callbacks distinguish email_id from login_id and explicitly support profiles where they differ (contract). SSR forwards email_id as recipient, so a valid account with login shopper-login and email shopper@example.com is silently skipped by this login lookup. The passwordless branch has the same problem. Please resolve the profile by its email, or carry login_id through the authenticated callback and verify the resolved profile's email matches the recipient; cover differing login/email values in a regression test.

'/registration-verification-callback'
app.post(registrationVerificationCallback, async (req, res) => {
const appConfig = getConfig()?.app
if (appConfig?.login?.passwordless?.mode !== 'callback') {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] Registration delivery is gated by the unrelated passwordless-login mode. UserRegistration now requests mode: 'callback' for both initial send and resend using login.registrationVerification.callbackURI, independently of login.passwordless.mode. A storefront retaining native email mode for ordinary login therefore receives a 400 here for every checkout registration callback, even with valid notify credentials. Please use registration-specific configuration consistently with its caller rather than this login-mode guard, update both SSR templates, and test registration with passwordless.mode: 'email'.

Set the client secret as an environment variable on the MRT environment:

```bash
pwa-kit-dev push --set-env PWA_KIT_NOTIFY_SLAS_CLIENT_SECRET=<your-notify-client-secret> ...

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[P2] This command cannot configure the required notification secret. pwa-kit-dev push has no --set-env option; its CLI only uploads/deploys a bundle, so following this setup example cannot set PWA_KIT_NOTIFY_SLAS_CLIENT_SECRET and leaves callbacks failing on missing configuration. Please remove this command and use the MRT environment-variable UI already described below, or replace it with a verified supported environment-management command.

@ddiazccrz ddiazccrz left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

+1 With follow up for smaller issues

@bendvc
bendvc merged commit 8183cb5 into develop Oct 19, 2026
41 checks passed
@bendvc
bendvc deleted the feature/b2c-email-cartridge branch October 19, 2026 18:49
bendvc added a commit that referenced this pull request Oct 21, 2026
…artridge-followup

fix: address ddiazccrz review findings from #4012 (b2c-email-cartridge)
sf-samarth-urs pushed a commit to sf-samarth-urs/pwa-kit that referenced this pull request Oct 21, 2026
…callback

SLAS creates the B2C customer profile after the shopper exchanges the
TOTP, not before the callback fires. The guard unconditionally blocked
new-account registration by silently succeeding before sending the OTP.
Rate-limiting is enforced by SLAS upstream on /authorizePasswordlessCustomer.

Fixes ddiazccrz finding on Notify.js:199 in SalesforceCommerceCloud#4012.
sf-samarth-urs pushed a commit to sf-samarth-urs/pwa-kit that referenced this pull request Oct 21, 2026
The auto-submit effect fired on every email keystroke, consuming a
rate-limit attempt after 100ms with a partial address. Gate auto-submit
on emailFromContext (router-state email, always complete) instead of
email, so direct magic-link arrivals submit explicitly.

Fixes ddiazccrz finding on verify.jsx:71 in SalesforceCommerceCloud#4012.
sf-samarth-urs pushed a commit to sf-samarth-urs/pwa-kit that referenced this pull request Oct 21, 2026
…asswordless.mode

The route was guarded by login.passwordless.mode === 'callback', but
UserRegistration uses login.registrationVerification.callbackURI
independently. Storefronts with passwordless.mode 'email' received 400s
on every checkout OTP registration. Guard now checks for
registrationVerification.callbackURI, consistent with the other callback
handlers checking their own config sections.

Fixes ddiazccrz finding on ssr.js:737 in SalesforceCommerceCloud#4012.
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.

6 participants