Repository navigation
feat: add app_pwakit_base B2C cartridge for transactional email delivery - #4012
Conversation
…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
|
Git2Gus App is installed but the |
✅ Snyk checks have passed. No issues have been found so far.
💻 Catch issues earlier using the plugins for VS Code, JetBrains IDEs, Visual Studio, and Eclipse. |
… of enablePWAKitPrivateClient
… flow when no client secret
…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
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.
…ustom API re-scan
…} 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)
…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
There was a problem hiding this comment.
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'
|
Addressed all three findings from the Oct 13 review in P1 — Release notes instruct the retired public-client model
P2 — Pre-merge SSR fixes not synchronized to generated templates
P2 — Generated config comments and create-app changelog contradict shipped behavior |
sf-jie-dai
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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.
The route is /reset-password-callback, not /callback/resetPassword.
sf-jie-dai
left a comment
There was a problem hiding this comment.
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
left a comment
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
[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 |
There was a problem hiding this comment.
[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); |
There was a problem hiding this comment.
[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') { |
There was a problem hiding this comment.
[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> ... |
There was a problem hiding this comment.
[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
left a comment
There was a problem hiding this comment.
+1 With follow up for smaller issues
…artridge-followup fix: address ddiazccrz review findings from #4012 (b2c-email-cartridge)
…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.
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.
…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.
Summary
Ports the
app_pwakit_baseB2C Commerce cartridge from sf-next into the PWA Kit template, and fixes the GLO verify authentication that was broken whenenablePWAKitPrivateClientis used.Deployed environment: https://scaffold-pwa-bens-playground.mobify-storefront.com/global/en-US
What's included
New:
app_pwakit_basecartridge (packages/template-retail-react-app/cartridges/app_pwakit_base/)scripts/hooks/sendOrderAccessCode.jssfcc.app.order.sendOrderAccessCode— called by SCAPI'srequestOrderAccessCodeto send the GLO access-code emailrest-apis/pwakit-notify/Notify.jsc_pwakit_notifyscope) — called by the PWA Kit SSR server after a SLAS passwordless/OTP/password-reset callback to send the corresponding emailscripts/helpers/sendNotification.jsdw/net/Mail(HTML body). Replace the body ofsend()to swap in any third-party provider (SendGrid, Mailchimp, etc.)scripts/helpers/getStorefrontHost.jspwakitStorefrontHostsite preference for this site. Returns null when unset — callers omit magic-link CTAs but still send the email. Stubbed for the futureSite.getCurrent().mrtExternalHostnameplatform API.scripts/helpers/buildOrderLookupUrl.jshttps://{host}/{siteId}/{locale}/order-lookup/verify/{orderNo}?token=…)templates/default/email/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.jsonstaticfiles/…/OrganizationPreferences.xmlpwakitNotifyEnabled(bool) under Administration → Global Preferences → PWA Kitstaticfiles/…/SitePreferences.xmlpwakitStorefrontHost(string) under Merchant Tools → Custom Preferences → pwakit (per-site)README.mdFix: 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 inlocalStorageand is never in any cookie. The client (verify.jsx) was already sendingAuthorization: Bearer <jwt>viauseAccessToken().getTokenWhenReady(). The fix readsreq.headers.authorizationdirectly, which works for both public and private client configurations.Fix: null-token guard in
verify.jsxAdded
if (!token) throw new Error('No access token available')aftergetTokenWhenReady()resolves. Without this, a null/empty token producesAuthorization: Bearer null— a truthy string that passes the server'sif (!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 mainssr.js. Updated to match.Enhancements over the sf-next version
getStorefrontHost.jscallerHostallowlist validation (org-level, comma-separated)pwakitStorefrontHostdirectly from the site preference for the current site — no allowlist, no caller-supplied hostname. Stubbed for futureSite.getCurrent().mrtExternalHostnameplatform API.pwakitNotifyEnabledpreferencesendOrderAccessCodehook chainingHookMgr.callNextHook()(not a valid B2C API — the platform chains hooks automatically)isNotifyEnabled()error handlingdw.systemexception and defaults to enabled — safe to deploy before importingOrganizationPreferences.xmlgloAccessCode,passwordlessMagicLink,otpVerification,passwordResetMagicLink)guestOrderLookup,passwordlessLogin,registrationVerification,passwordReset)passwordlessLoginemailmagicLinkPathand shown in the email so users can sign in without clicking the linksendOrderAccessCodewhen host unconfiguredhttps://null/…URLlookupLink: null— template degrades gracefully (CTA and copy-URL hidden via<isif>, disclaimer and access code still shown)buildOrderLookupUrl.jsshared helper — used by bothNotify.jsandsendOrderAccessCode.jsssr.jsextracts the locale from theredirectUrlquery param (e.g./en-US/account) viaextractLocaleFromUrland appends?locale=en-USto thepwakit-notifyAPI call. The cartridge readsrequest.locale(set by B2C from the query param) and passes it tosendNotification, which hands it to ISML so email templates render in the shopper's language rather than the site defaultHow to test
Deployed environment: https://scaffold-pwa-bens-playground.mobify-storefront.com/global/en-US
Prerequisites
RefArchGlobalOrganizationPreferences.xmlandSitePreferences.xmlimportedpwakitStorefrontHostset toscaffold-pwa-bens-playground.mobify-storefront.com(Merchant Tools → Custom Preferences → pwakit, per site)Flow 1 — Guest Order Lookup (GLO)
/global/en-US/order-lookupguestOrderLookup.ismlcontaining both an access code and a "View Order" buttonFlow 2 — Passwordless login
passwordlessLogin.ismlthat contains both a magic link button and an inline access codeFlow 3 — OTP verification
registrationVerification.ismlFlow 4 — Password reset
passwordReset.ismlDisabling the cartridge
falseDecisions worth calling out
1. Auth reads
req.headers.authorization, not cookiesThe GLO endpoints now trust the
Authorizationheader 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_*cookieAfter 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.enabledisfalseby defaultThe feature is off unless explicitly enabled in
config/default.js. This is intentional — GLO requires a B2C instance with therequestOrderAccessCodeSCAPI 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(), notsiteAliasThe magic link path is built as
/<siteId-lowercase>/<locale>/order-lookup/verify/<orderNo>?token=<code>. If the storefront registers asiteAliasinconfig/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 insendOrderAccessCode.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.
pwakitNotifyEnableddefaults totruewhen the preference is unsetOn first deploy, before
OrganizationPreferences.xmlis imported,isNotifyEnabled()returnstrue. 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.
pwakitStorefrontHostis a site preference, not an org preferenceThe 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
callerHostvalidation — 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, notAccept-LanguageThe SSR
sendMagicLinkEmailhandler receives theredirectUrlthat 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 viaextractLocaleFromUrl— 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 thepwakit-notifyAPI call. B2C setsrequest.localefrom this query param, which is then forwarded to ISML. This approach avoids relying on theAccept-Languageheader (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 toSite.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:
extractLocaleFromUrllocale 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/...)passwordless.mode !== 'callback'guard present on the passwordless and reset-password callback routes — the endpoint now returns 400 when callback mode is not configured401 Unauthorizedto the client instead of502 Service errorrequest.locale === 'default'sentinel: bothsendNotification.jsandsendOrderAccessCode.jsnow skip the'default'sentinel and fall through toSite.defaultLocalenotifyClientIddefault: restored thenotifyClientIdplaceholder inconfig/default.jsthat was accidentally blanked during a credential-revert commit