@salesforce/retail-react-app 10.2.0-nightly-20260812155833 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +1 -4
- package/app/components/shopper-agent/index.jsx +8 -368
- package/app/components/shopper-agent/index.test.js +2 -404
- package/app/components/shopper-agent/token-bridge.js +55 -33
- package/app/components/shopper-agent/token-bridge.test.js +42 -109
- package/app/ssr.js +1 -18
- package/app/utils/shopper-agent-utils.js +0 -37
- package/app/utils/shopper-agent-utils.test.js +0 -111
- package/config/default.js +0 -2
- package/package.json +6 -6
- package/app/components/shopper-agent/auth-link-proxy.js +0 -317
- package/app/components/shopper-agent/auth-link-proxy.test.js +0 -518
- package/app/components/shopper-agent/salesforce-domain-allowlist.js +0 -87
- package/app/components/shopper-agent/salesforce-domain-allowlist.test.js +0 -94
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
## v10.2.0
|
|
2
|
-
- [Feature] Forward two backend-gating signals on the Commerce Client shopper-agent widget's `routingAttributes`: `clientVersion` (sourced from `cc_cdnVersion`, so the runtime can gate rich components on the client's capability version; omitted when unset) and `isCartMgmtSupported` (string `'true'`/`'false'`, default `'false'`, read from `cc_routingAttributes`). Brings the PWA commerce-client path to parity with the SFRA cartridge.
|
|
3
|
-
- [Feature] Enable shopper identity (customer context) for the Commerce Client (Cimulate) shopper-agent provider. Unlike MIAW, Commerce Client widgets do not expose `getAuthLinkKey`, so the browser extracts the Commerce Client JWT from the deployment-scoped `cim_af_ct_<orgId>_<embeddedServiceName>` key (`sessionStorage` is authoritative; `localStorage` is a compatibility fallback) and POSTs it to a new same-origin proxy `POST /api/agent/authlink` (`registerAuthLinkRoute`, mounted in `app/ssr.js`). The proxy calls SCRT2's `/iamessage/api/v2/authorization/authlink` with an `Authorization: Bearer <jwt>` header to obtain an `auth_link_key`; the SCRT2 origin is read server-side from `scrt2Url` in `COMMERCE_AGENT_SETTINGS` (a different host from Core's My Domain) and validated against a Salesforce domain allowlist, with an Origin/Referer CSRF check, to prevent SSRF. The `auth_link_key` is then forwarded to the existing Token Bridge to bind the conversation to the shopper's SLAS session. Linking is idempotent — deduped by `conversationId` + SLAS identity — and re-runs both on a new conversation (`onCimulateWidgetReady`) and on a SLAS identity transition (guest ↔ registered / account switch), gated on `my_domain` resolution. [#3943](https://github.com/SalesforceCommerceCloud/pwa-kit/pull/3943)
|
|
4
|
-
- [Bugfix] Fix the Shopper Agent Token Bridge proxy (`/api/agent/identity/bridge`) failing to reach Core in two cases: (1) same-origin POSTs were rejected with 403 `FORBIDDEN_ORIGIN` because the CSRF Origin check compared `URL.hostname` (no port) against `req.headers.host` (with port), so a non-standard dev port like `localhost:3000` never matched — now compares `URL.host`; (2) a scheme-less My Domain value (e.g. `orgfarm-1234.my.salesforce.com`) was rejected as 400 `UNTRUSTED_MYDOMAIN` because `new URL()` threw on it — the value is now normalized to an absolute `https://` origin before validation and fetch. The `AGENT_MYDOMAIN` environment variable configures the My Domain.
|
|
1
|
+
## v10.2.0 (Aug 12, 2026)
|
|
5
2
|
- [Bugfix] Fix the Data Cloud recommender catalog event field name: `personalizationContextId` → `personalizationContentId`. The official `@salesforce/cc-datacloud-typescript` SDK type and Salesforce docs both define `personalizationContentId` (Content); the `Context` spelling did not match, so the recommender UUID (`__recoUUID`) was sent under a key Data Cloud does not recognize and is expected to be dropped on ingest.
|
|
6
3
|
- [Feature] Add `cc_showFab` for the Commerce Client shopper-agent widget: when `'true'`, renders a floating action button at `cc_widgetPosition` that opens the agent panel and hides while it is open. Defaults to `'false'`.
|
|
7
4
|
- [Bugfix] Honor `cc_dialogFullHeight: 'false'`, which was previously dropped from the widget options so the bundle's full-height default always won.
|
|
@@ -31,14 +31,12 @@ import {
|
|
|
31
31
|
getPersistedCommerceClientOpenState,
|
|
32
32
|
persistCommerceClientOpenState,
|
|
33
33
|
resetEmbeddedMessagingForCommerceSessionChange,
|
|
34
|
-
resolveCommerceClientRoutingAttributes,
|
|
35
34
|
resolveCommerceClientScriptUrl,
|
|
36
35
|
validateCommerceClientAgentSettings
|
|
37
36
|
} from '@salesforce/retail-react-app/app/utils/shopper-agent-utils'
|
|
38
37
|
import {resolveCommerceClientOverrideOptions} from '@salesforce/retail-react-app/app/utils/commerce-client-overrides'
|
|
39
38
|
import {callTokenBridge} from '@salesforce/retail-react-app/app/components/shopper-agent/token-bridge'
|
|
40
39
|
import CommerceClientFab from '@salesforce/retail-react-app/app/components/shopper-agent/commerce-client-fab'
|
|
41
|
-
import {callAuthLinkProxy} from '@salesforce/retail-react-app/app/components/shopper-agent/auth-link-proxy'
|
|
42
40
|
|
|
43
41
|
const onClient = typeof window !== 'undefined'
|
|
44
42
|
|
|
@@ -209,7 +207,6 @@ const ShopperAgentWindow = ({commerceAgentConfiguration, domainUrl}) => {
|
|
|
209
207
|
salesforceOrgId,
|
|
210
208
|
commerceOrgId,
|
|
211
209
|
siteId,
|
|
212
|
-
provider = 'miaw',
|
|
213
210
|
enableConversationContext = 'false',
|
|
214
211
|
conversationContext = [],
|
|
215
212
|
enableAgentFromFloatingButton = 'true'
|
|
@@ -236,12 +233,6 @@ const ShopperAgentWindow = ({commerceAgentConfiguration, domainUrl}) => {
|
|
|
236
233
|
/**
|
|
237
234
|
* Reset embedded messaging whenever customerType changes (login, logout, registration).
|
|
238
235
|
* This ensures the chat context is cleared when user authentication state changes.
|
|
239
|
-
*
|
|
240
|
-
* MIAW-only: resetEmbeddedMessagingForCommerceSessionChange() drives
|
|
241
|
-
* window.embeddedservice_bootstrap.userVerificationAPI.clearSession, which
|
|
242
|
-
* exists only for the Salesforce Embedded Messaging (MIAW) provider. The
|
|
243
|
-
* Commerce Client provider re-links its conversation in place instead of
|
|
244
|
-
* clearing the session, so we no-op here for any non-MIAW provider.
|
|
245
236
|
*/
|
|
246
237
|
useEffect(() => {
|
|
247
238
|
const prev = prevCommerceCustomerTypeRef.current
|
|
@@ -252,11 +243,11 @@ const ShopperAgentWindow = ({commerceAgentConfiguration, domainUrl}) => {
|
|
|
252
243
|
return
|
|
253
244
|
}
|
|
254
245
|
|
|
255
|
-
// Reset on any customerType change (login, logout, register)
|
|
256
|
-
if (prev !== customerType
|
|
246
|
+
// Reset on any customerType change (login, logout, register)
|
|
247
|
+
if (prev !== customerType) {
|
|
257
248
|
resetEmbeddedMessagingForCommerceSessionChange()
|
|
258
249
|
}
|
|
259
|
-
}, [customerType
|
|
250
|
+
}, [customerType])
|
|
260
251
|
|
|
261
252
|
const formatMessageRef = useRef(formatMessage)
|
|
262
253
|
formatMessageRef.current = formatMessage
|
|
@@ -596,11 +587,6 @@ const DEFAULT_COMMERCE_CLIENT_PANEL_WIDTH = '420px'
|
|
|
596
587
|
*
|
|
597
588
|
* @param {Object} props - Component props
|
|
598
589
|
* @param {Object} props.commerceAgentConfiguration - Commerce agent configuration object
|
|
599
|
-
* @param {Object} props.lastAuthLinkKeyRef - Last successful conversation/shopper link key
|
|
600
|
-
* @param {Object} props.lastCommerceClientJWTRef - Last successfully linked Commerce Client JWT
|
|
601
|
-
* @param {Object} props.authLinkGenerationRef - Monotonic auth-link attempt generation
|
|
602
|
-
* @param {Object} props.authLinkQueueRef - Serialized auth-link operation queue
|
|
603
|
-
* @param {Object} props.lastAttemptedCommerceClientJWTRef - JWT used by the latest auth-link attempt
|
|
604
590
|
* @param {string} props.commerceAgentConfiguration.scrt2Url - SCRT2 URL (passed to `messagingConfig.scrt2Url`)
|
|
605
591
|
* @param {string} props.commerceAgentConfiguration.salesforceOrgId - Salesforce org ID (passed to `messagingConfig.orgId`)
|
|
606
592
|
* @param {string} [props.commerceAgentConfiguration.cc_esDeveloperName] - Embedded Service developer name
|
|
@@ -623,19 +609,12 @@ const DEFAULT_COMMERCE_CLIENT_PANEL_WIDTH = '420px'
|
|
|
623
609
|
* @param {string} [props.commerceAgentConfiguration.cc_isOpen] - When 'true', the widget opens automatically on page load (forwarded as `componentConfig.isOpen`); defaults to 'false'
|
|
624
610
|
* @param {string} [props.commerceAgentConfiguration.cc_isDevelopment] - When 'true', logs widget events to the console (forwarded as `isDevelopment`)
|
|
625
611
|
* @param {Object} [props.commerceAgentConfiguration.cc_theme] - Partial theme overrides for the widget
|
|
626
|
-
* @param {Object} [props.commerceAgentConfiguration.cc_routingAttributes] - Optional Agentforce routing attributes forwarded to the widget as `routingAttributes
|
|
612
|
+
* @param {Object} [props.commerceAgentConfiguration.cc_routingAttributes] - Optional Agentforce routing attributes forwarded to the widget as `routingAttributes`
|
|
627
613
|
* @param {string} [props.commerceAgentConfiguration.cc_overridesUrl] - Optional HTTPS URL of a component override script, forwarded as `overridesUrl`
|
|
628
614
|
* @param {Object} [props.commerceAgentConfiguration.cc_overrides] - Optional inline map of widget override keys (e.g. `ProductTile`) to already-registered custom element tag names, forwarded as `overrides`. Mutually exclusive with `cc_overridesUrl`, which it takes precedence over
|
|
629
615
|
* @returns {JSX.Element} A container element the Commerce Client widget is rendered into
|
|
630
616
|
*/
|
|
631
|
-
const CommerceClientAgentWindow = ({
|
|
632
|
-
commerceAgentConfiguration,
|
|
633
|
-
lastAuthLinkKeyRef,
|
|
634
|
-
lastCommerceClientJWTRef,
|
|
635
|
-
authLinkGenerationRef,
|
|
636
|
-
authLinkQueueRef,
|
|
637
|
-
lastAttemptedCommerceClientJWTRef
|
|
638
|
-
}) => {
|
|
617
|
+
const CommerceClientAgentWindow = ({commerceAgentConfiguration}) => {
|
|
639
618
|
const {
|
|
640
619
|
scrt2Url,
|
|
641
620
|
salesforceOrgId,
|
|
@@ -657,8 +636,6 @@ const CommerceClientAgentWindow = ({
|
|
|
657
636
|
cc_enableDownloadTranscript = 'true',
|
|
658
637
|
cc_theme,
|
|
659
638
|
cc_searchConfig,
|
|
660
|
-
cc_cdnVersion,
|
|
661
|
-
commerceClientScriptSourceUrl,
|
|
662
639
|
cc_routingAttributes,
|
|
663
640
|
cc_overridesUrl,
|
|
664
641
|
cc_overrides
|
|
@@ -666,319 +643,6 @@ const CommerceClientAgentWindow = ({
|
|
|
666
643
|
|
|
667
644
|
// Loads the Commerce Client messaging UMD bundle, which exposes window.CimulateMessaging.
|
|
668
645
|
const scriptLoadStatus = useScript(resolveCommerceClientScriptUrl(commerceAgentConfiguration))
|
|
669
|
-
const {formatMessage} = useIntl()
|
|
670
|
-
const toast = useToast()
|
|
671
|
-
const toastRef = useRef(toast)
|
|
672
|
-
toastRef.current = toast
|
|
673
|
-
|
|
674
|
-
// Customer details and auth tokens for Commerce Client session init
|
|
675
|
-
const {getTokenWhenReady} = useAccessToken()
|
|
676
|
-
const getTokenWhenReadyRef = useRef(getTokenWhenReady)
|
|
677
|
-
getTokenWhenReadyRef.current = getTokenWhenReady
|
|
678
|
-
|
|
679
|
-
const {organizationId, siteId: configSiteId} = useConfig()
|
|
680
|
-
|
|
681
|
-
// Fetch my_domain from the Shopper Configurations API. Auth-linking calls
|
|
682
|
-
// Core (via the Token Bridge), which is only reachable once my_domain has
|
|
683
|
-
// resolved, so we gate performAuthLink on it exactly like the MIAW provider.
|
|
684
|
-
const {data: configurationsData} = useConfigurations({})
|
|
685
|
-
const myDomain = configurationsData?.configurations?.find(
|
|
686
|
-
(config) => config.configurationType === 'globalConfiguration' && config.id === 'my_domain'
|
|
687
|
-
)?.value
|
|
688
|
-
|
|
689
|
-
const configRef = useRef({organizationId, configSiteId, myDomain})
|
|
690
|
-
configRef.current = {organizationId, configSiteId, myDomain}
|
|
691
|
-
|
|
692
|
-
const formatMessageRef = useRef(formatMessage)
|
|
693
|
-
formatMessageRef.current = formatMessage
|
|
694
|
-
|
|
695
|
-
// --- Auth-link triggers ---
|
|
696
|
-
// The SLAS shopper identity. Auth-linking binds the Commerce Client
|
|
697
|
-
// conversation to THIS shopper, so a change on either side (new
|
|
698
|
-
// conversation, or guest<->registered / account switch) must re-link.
|
|
699
|
-
const {usid} = useUsid()
|
|
700
|
-
const {customerType} = useCustomerType()
|
|
701
|
-
|
|
702
|
-
// Latest identity values, read from a ref so the (stable, mount-once)
|
|
703
|
-
// window listeners always see current values without re-subscribing.
|
|
704
|
-
const identityRef = useRef({usid, customerType})
|
|
705
|
-
identityRef.current = {usid, customerType}
|
|
706
|
-
|
|
707
|
-
const commerceClientStorageScope = `${salesforceOrgId}_${
|
|
708
|
-
cc_esDeveloperName || embeddedServiceName
|
|
709
|
-
}`
|
|
710
|
-
const commerceClientTokenKey = `cim_af_ct_${commerceClientStorageScope}`
|
|
711
|
-
const commerceClientConversationKey = `cim_af_conv_${commerceClientStorageScope}`
|
|
712
|
-
const isCommerceClientReady =
|
|
713
|
-
!scriptLoadStatus?.error &&
|
|
714
|
-
(scriptLoadStatus?.loaded || (onClient && Boolean(window.CimulateMessaging)))
|
|
715
|
-
const effectiveScriptLoadStatus = useMemo(
|
|
716
|
-
() => ({loaded: isCommerceClientReady, error: Boolean(scriptLoadStatus?.error)}),
|
|
717
|
-
[isCommerceClientReady, scriptLoadStatus?.error]
|
|
718
|
-
)
|
|
719
|
-
|
|
720
|
-
// The helpers below feed the two re-link triggers (new conversation, SLAS
|
|
721
|
-
// identity change), which both route through the idempotent performAuthLink().
|
|
722
|
-
|
|
723
|
-
/**
|
|
724
|
-
* Resolve a stable identifier for the current SLAS shopper. Guests share
|
|
725
|
-
* the sentinel "guest" so a guest re-open does not look like a new identity,
|
|
726
|
-
* while a login/logout/account-switch produces a different value.
|
|
727
|
-
*/
|
|
728
|
-
const getSlasIdentity = () => {
|
|
729
|
-
const {usid: currentUsid, customerType: currentType} = identityRef.current
|
|
730
|
-
if (currentType === 'registered') {
|
|
731
|
-
return `registered:${currentUsid || ''}`
|
|
732
|
-
}
|
|
733
|
-
return 'guest'
|
|
734
|
-
}
|
|
735
|
-
|
|
736
|
-
/**
|
|
737
|
-
* Read the Commerce Client conversationId from this widget's scoped session
|
|
738
|
-
* key (value shape: {"conversationId":"...","storedAt":...}). Returns null
|
|
739
|
-
* if not present yet (e.g. conversation still being created).
|
|
740
|
-
*/
|
|
741
|
-
const readConversationId = () => {
|
|
742
|
-
try {
|
|
743
|
-
const value = window.sessionStorage.getItem(commerceClientConversationKey)
|
|
744
|
-
return value ? JSON.parse(value)?.conversationId || null : null
|
|
745
|
-
} catch (err) {
|
|
746
|
-
console.error('[Commerce Client] Failed to read conversationId', err)
|
|
747
|
-
return null
|
|
748
|
-
}
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Extract this widget's Commerce Client JWT from its scoped cim_af_ct_* key.
|
|
753
|
-
* The session-scoped value is authoritative; localStorage is a compatibility
|
|
754
|
-
* fallback for SDK versions that persist the same scoped key there.
|
|
755
|
-
*/
|
|
756
|
-
const extractCommerceClientJWT = (excludedJWT = null) => {
|
|
757
|
-
const stores = [window.sessionStorage, window.localStorage]
|
|
758
|
-
for (const store of stores) {
|
|
759
|
-
try {
|
|
760
|
-
const data = JSON.parse(store.getItem(commerceClientTokenKey) || 'null')
|
|
761
|
-
const accessToken = data?.accessToken
|
|
762
|
-
if (typeof accessToken === 'string' && accessToken.trim().length > 0) {
|
|
763
|
-
return accessToken !== excludedJWT ? accessToken : null
|
|
764
|
-
}
|
|
765
|
-
} catch (err) {
|
|
766
|
-
console.error('[Commerce Client] Failed to parse storage for JWT', err)
|
|
767
|
-
}
|
|
768
|
-
}
|
|
769
|
-
return null
|
|
770
|
-
}
|
|
771
|
-
|
|
772
|
-
/**
|
|
773
|
-
* Poll storage for the Commerce Client JWT with capped exponential backoff.
|
|
774
|
-
* The SDK stores the JWT asynchronously after a conversation is created, so
|
|
775
|
-
* on the `onCimulateWidgetReady` signal the token may not exist yet.
|
|
776
|
-
*
|
|
777
|
-
* The per-attempt delay is capped (`maxDelay`) and we do NOT sleep after the
|
|
778
|
-
* final check, so the worst-case wait is bounded (~4.5s with the defaults)
|
|
779
|
-
* rather than growing unbounded with the retry count. The token normally
|
|
780
|
-
* lands within the first second; the cap only affects the give-up path.
|
|
781
|
-
*/
|
|
782
|
-
const waitForCommerceClientJWT = async (
|
|
783
|
-
excludedJWT = null,
|
|
784
|
-
maxRetries = 8,
|
|
785
|
-
initialDelay = 100,
|
|
786
|
-
maxDelay = 1000
|
|
787
|
-
) => {
|
|
788
|
-
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
|
789
|
-
const jwt = extractCommerceClientJWT(excludedJWT)
|
|
790
|
-
if (jwt) return jwt
|
|
791
|
-
// No point sleeping after the last check — nothing reads the result.
|
|
792
|
-
if (attempt === maxRetries - 1) break
|
|
793
|
-
const delay = Math.min(maxDelay, initialDelay * Math.pow(2, attempt))
|
|
794
|
-
await new Promise((resolve) => setTimeout(resolve, delay))
|
|
795
|
-
}
|
|
796
|
-
return null
|
|
797
|
-
}
|
|
798
|
-
|
|
799
|
-
/**
|
|
800
|
-
* Idempotently link the current Commerce Client conversation to the current
|
|
801
|
-
* SLAS shopper. Deduped by `${conversationId}:${slasIdentity}` — a no-op if
|
|
802
|
-
* that exact pair was already linked successfully.
|
|
803
|
-
*
|
|
804
|
-
* @param {Object} opts
|
|
805
|
-
* @param {string} opts.reason - Diagnostic label for the triggering signal.
|
|
806
|
-
*/
|
|
807
|
-
const performAuthLink = ({reason, excludedJWT = null}) => {
|
|
808
|
-
const generation = ++authLinkGenerationRef.current
|
|
809
|
-
const scheduledJWT = extractCommerceClientJWT()
|
|
810
|
-
if (scheduledJWT) {
|
|
811
|
-
lastAttemptedCommerceClientJWTRef.current = scheduledJWT
|
|
812
|
-
}
|
|
813
|
-
const run = async () => {
|
|
814
|
-
const {organizationId: orgId, configSiteId: sid, myDomain: domain} = configRef.current
|
|
815
|
-
if (!orgId || !sid) {
|
|
816
|
-
console.error('[Commerce Client] performAuthLink: missing organizationId or siteId')
|
|
817
|
-
return
|
|
818
|
-
}
|
|
819
|
-
|
|
820
|
-
// The Token Bridge reaches Core, which needs my_domain resolved. The mount
|
|
821
|
-
// is already gated on !isConfigurationsLoading, so this is a defensive skip
|
|
822
|
-
// rather than a call to the bridge with an unresolved domain.
|
|
823
|
-
if (!domain) {
|
|
824
|
-
console.warn(
|
|
825
|
-
`[Commerce Client] performAuthLink(${reason}): my_domain not resolved yet`
|
|
826
|
-
)
|
|
827
|
-
return
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
const slasIdentity = getSlasIdentity()
|
|
831
|
-
|
|
832
|
-
try {
|
|
833
|
-
const commerceClientJWT = await waitForCommerceClientJWT(excludedJWT)
|
|
834
|
-
if (!commerceClientJWT) {
|
|
835
|
-
console.warn(
|
|
836
|
-
`[Commerce Client] performAuthLink(${reason}): no JWT after polling`
|
|
837
|
-
)
|
|
838
|
-
return
|
|
839
|
-
}
|
|
840
|
-
lastAttemptedCommerceClientJWTRef.current = commerceClientJWT
|
|
841
|
-
if (generation !== authLinkGenerationRef.current) return
|
|
842
|
-
|
|
843
|
-
// Dedup on (conversation, shopper). conversationId is read here — after
|
|
844
|
-
// the JWT poll — so a still-creating conversation has time to appear.
|
|
845
|
-
// A new-conversation trigger waits for a different JWT before reaching
|
|
846
|
-
// this guard, so it can bypass a still-stale conversationId safely.
|
|
847
|
-
const conversationId = readConversationId()
|
|
848
|
-
const linkKey = `${conversationId || 'unknown'}:${slasIdentity}`
|
|
849
|
-
if (!excludedJWT && lastAuthLinkKeyRef.current === linkKey) {
|
|
850
|
-
// Already linked this exact (conversation, shopper) pair.
|
|
851
|
-
console.warn(
|
|
852
|
-
`[Commerce Client] performAuthLink(${reason}): already linked, skipping`
|
|
853
|
-
)
|
|
854
|
-
return
|
|
855
|
-
}
|
|
856
|
-
|
|
857
|
-
const isHttpOnly =
|
|
858
|
-
typeof window !== 'undefined'
|
|
859
|
-
? window.__MRT_ENABLE_HTTPONLY_SESSION_COOKIES__ === 'true'
|
|
860
|
-
: process.env.MRT_ENABLE_HTTPONLY_SESSION_COOKIES === 'true'
|
|
861
|
-
|
|
862
|
-
const slasAccessToken = isHttpOnly
|
|
863
|
-
? undefined
|
|
864
|
-
: await getTokenWhenReadyRef.current()
|
|
865
|
-
if (generation !== authLinkGenerationRef.current) return
|
|
866
|
-
|
|
867
|
-
// Step 1: auth link key from SCRT. The SCRT authlink endpoint
|
|
868
|
-
// authenticates with the Commerce Client JWT (Bearer) alone — it
|
|
869
|
-
// needs neither the siteId nor the conversationId.
|
|
870
|
-
const authLinkResponse = await callAuthLinkProxy({commerceClientJWT})
|
|
871
|
-
if (generation !== authLinkGenerationRef.current) return
|
|
872
|
-
const authLinkKey = authLinkResponse?.auth_link_key || authLinkResponse?.authLinkKey
|
|
873
|
-
if (!authLinkKey || typeof authLinkKey !== 'string') {
|
|
874
|
-
console.error(
|
|
875
|
-
`[Commerce Client] performAuthLink(${reason}): no auth link key`,
|
|
876
|
-
authLinkResponse
|
|
877
|
-
)
|
|
878
|
-
return
|
|
879
|
-
}
|
|
880
|
-
|
|
881
|
-
// Step 2: token bridge (links conversation to the SLAS shopper).
|
|
882
|
-
// Operations are serialized so the newest identity is the final
|
|
883
|
-
// server-side mutation even when an older request was already sent.
|
|
884
|
-
const result = await callTokenBridge({authLinkKey, slasAccessToken, siteId: sid})
|
|
885
|
-
if (generation !== authLinkGenerationRef.current) return
|
|
886
|
-
if (result.status !== HTTP_OK) {
|
|
887
|
-
const errorCode = result.body?.error || `HTTP_${result.status}`
|
|
888
|
-
console.error(
|
|
889
|
-
`[Commerce Client] performAuthLink(${reason}): token bridge failed`,
|
|
890
|
-
{
|
|
891
|
-
status: result.status,
|
|
892
|
-
error: errorCode
|
|
893
|
-
}
|
|
894
|
-
)
|
|
895
|
-
toastRef.current({
|
|
896
|
-
title: formatMessageRef.current(SESSION_INIT_ERROR_MESSAGE),
|
|
897
|
-
status: 'error'
|
|
898
|
-
})
|
|
899
|
-
return
|
|
900
|
-
}
|
|
901
|
-
|
|
902
|
-
lastAuthLinkKeyRef.current = linkKey
|
|
903
|
-
lastCommerceClientJWTRef.current = commerceClientJWT
|
|
904
|
-
} catch (error) {
|
|
905
|
-
if (generation !== authLinkGenerationRef.current) return
|
|
906
|
-
console.error(`[Commerce Client] performAuthLink(${reason}) threw`, error)
|
|
907
|
-
toastRef.current({
|
|
908
|
-
title: formatMessageRef.current(SESSION_INIT_ERROR_MESSAGE),
|
|
909
|
-
status: 'error'
|
|
910
|
-
})
|
|
911
|
-
}
|
|
912
|
-
}
|
|
913
|
-
|
|
914
|
-
const queued = authLinkQueueRef.current.then(run, run)
|
|
915
|
-
authLinkQueueRef.current = queued
|
|
916
|
-
return queued
|
|
917
|
-
}
|
|
918
|
-
|
|
919
|
-
// Keep a stable ref so window listeners registered once always call the
|
|
920
|
-
// latest performAuthLink closure (which reads current config/identity).
|
|
921
|
-
const performAuthLinkRef = useRef(performAuthLink)
|
|
922
|
-
performAuthLinkRef.current = performAuthLink
|
|
923
|
-
|
|
924
|
-
/**
|
|
925
|
-
* Trigger 1 — new Commerce Client conversation.
|
|
926
|
-
* `onCimulateWidgetReady` is a cancelable handshake the widget dispatches
|
|
927
|
-
* ONLY when it creates a NEW conversation (not on sessionStorage resume).
|
|
928
|
-
* We use it purely as the "new conversation identity" signal and run the
|
|
929
|
-
* link asynchronously — we do NOT call preventDefault()/done() here, so the
|
|
930
|
-
* widget proceeds immediately; the JWT poll in performAuthLink waits for the
|
|
931
|
-
* new conversation's token to land.
|
|
932
|
-
*/
|
|
933
|
-
useEffect(() => {
|
|
934
|
-
if (!isCommerceClientReady) {
|
|
935
|
-
return
|
|
936
|
-
}
|
|
937
|
-
const handleWidgetReady = () => {
|
|
938
|
-
// Clear-chat can dispatch this event before storage rotates. Wait for
|
|
939
|
-
// the new token rather than authenticating the previous conversation.
|
|
940
|
-
const excludedJWT =
|
|
941
|
-
lastAttemptedCommerceClientJWTRef.current || lastCommerceClientJWTRef.current
|
|
942
|
-
performAuthLinkRef.current({
|
|
943
|
-
reason: 'widget-ready',
|
|
944
|
-
excludedJWT: excludedJWT || null
|
|
945
|
-
})
|
|
946
|
-
}
|
|
947
|
-
window.addEventListener('onCimulateWidgetReady', handleWidgetReady)
|
|
948
|
-
return () => {
|
|
949
|
-
window.removeEventListener('onCimulateWidgetReady', handleWidgetReady)
|
|
950
|
-
}
|
|
951
|
-
}, [isCommerceClientReady])
|
|
952
|
-
|
|
953
|
-
/**
|
|
954
|
-
* Trigger 2 — SLAS shopper identity transition.
|
|
955
|
-
* Login, logout, registration and account switch all rotate the SLAS token
|
|
956
|
-
* (reflected here as a change in customerType/usid). The conversation is
|
|
957
|
-
* unchanged but is now linked to the wrong (or anonymous) shopper, so we
|
|
958
|
-
* re-link. Skips the initial mount; the composite dedup key in
|
|
959
|
-
* performAuthLink prevents a redundant link if nothing effectively changed.
|
|
960
|
-
*/
|
|
961
|
-
const prevSlasIdentityRef = useRef(undefined)
|
|
962
|
-
useEffect(() => {
|
|
963
|
-
if (!isCommerceClientReady) {
|
|
964
|
-
return
|
|
965
|
-
}
|
|
966
|
-
const identity = getSlasIdentity()
|
|
967
|
-
const prev = prevSlasIdentityRef.current
|
|
968
|
-
prevSlasIdentityRef.current = identity
|
|
969
|
-
|
|
970
|
-
// A resumed conversation does not emit widget-ready, so link it here.
|
|
971
|
-
// A genuine cold start still waits for the widget-ready trigger.
|
|
972
|
-
if (prev === undefined) {
|
|
973
|
-
if (readConversationId()) {
|
|
974
|
-
performAuthLinkRef.current({reason: 'resumed-conversation'})
|
|
975
|
-
}
|
|
976
|
-
return
|
|
977
|
-
}
|
|
978
|
-
if (prev !== identity) {
|
|
979
|
-
performAuthLinkRef.current({reason: 'slas-identity-change'})
|
|
980
|
-
}
|
|
981
|
-
}, [customerType, usid, isCommerceClientReady])
|
|
982
646
|
|
|
983
647
|
const isDialog = cc_displayType === 'dialog'
|
|
984
648
|
const isFullHeight = isDialog && cc_dialogFullHeight === 'true'
|
|
@@ -1016,11 +680,7 @@ const CommerceClientAgentWindow = ({
|
|
|
1016
680
|
capabilitiesVersion: cc_capabilitiesVersion,
|
|
1017
681
|
enableEscalationToAgent: cc_enableEscalationToAgent !== 'false',
|
|
1018
682
|
enableDownloadTranscript: cc_enableDownloadTranscript !== 'false',
|
|
1019
|
-
routingAttributes:
|
|
1020
|
-
cc_routingAttributes,
|
|
1021
|
-
cc_cdnVersion,
|
|
1022
|
-
commerceClientScriptSourceUrl
|
|
1023
|
-
}),
|
|
683
|
+
routingAttributes: cc_routingAttributes,
|
|
1024
684
|
logoUrl: cc_logoUrl,
|
|
1025
685
|
headerText: cc_headerText,
|
|
1026
686
|
disclaimerMarkdown: cc_disclaimerMarkdown,
|
|
@@ -1051,8 +711,6 @@ const CommerceClientAgentWindow = ({
|
|
|
1051
711
|
cc_enableEscalationToAgent,
|
|
1052
712
|
cc_enableDownloadTranscript,
|
|
1053
713
|
cc_routingAttributes,
|
|
1054
|
-
cc_cdnVersion,
|
|
1055
|
-
commerceClientScriptSourceUrl,
|
|
1056
714
|
cc_logoUrl,
|
|
1057
715
|
cc_headerText,
|
|
1058
716
|
cc_disclaimerMarkdown,
|
|
@@ -1071,7 +729,7 @@ const CommerceClientAgentWindow = ({
|
|
|
1071
729
|
)
|
|
1072
730
|
|
|
1073
731
|
// Inject the widget into the container once the bundle is loaded
|
|
1074
|
-
useCommerceClientMessaging(
|
|
732
|
+
useCommerceClientMessaging(scriptLoadStatus, widgetOptions)
|
|
1075
733
|
|
|
1076
734
|
return (
|
|
1077
735
|
<>
|
|
@@ -1094,12 +752,7 @@ CommerceClientAgentWindow.propTypes = {
|
|
|
1094
752
|
* @type {Object}
|
|
1095
753
|
* @required
|
|
1096
754
|
*/
|
|
1097
|
-
commerceAgentConfiguration: PropTypes.object.isRequired
|
|
1098
|
-
lastAuthLinkKeyRef: PropTypes.shape({current: PropTypes.string}).isRequired,
|
|
1099
|
-
lastCommerceClientJWTRef: PropTypes.shape({current: PropTypes.string}).isRequired,
|
|
1100
|
-
authLinkGenerationRef: PropTypes.shape({current: PropTypes.number}).isRequired,
|
|
1101
|
-
authLinkQueueRef: PropTypes.shape({current: PropTypes.object}).isRequired,
|
|
1102
|
-
lastAttemptedCommerceClientJWTRef: PropTypes.shape({current: PropTypes.string}).isRequired
|
|
755
|
+
commerceAgentConfiguration: PropTypes.object.isRequired
|
|
1103
756
|
}
|
|
1104
757
|
|
|
1105
758
|
/**
|
|
@@ -1135,14 +788,6 @@ CommerceClientAgentWindow.propTypes = {
|
|
|
1135
788
|
* @see {@link isEnabled} - Enabled state checker
|
|
1136
789
|
*/
|
|
1137
790
|
const ShopperAgent = ({commerceAgentConfiguration, basketDoneLoading}) => {
|
|
1138
|
-
// Preserve successful-link state while the inner widget unmounts during a
|
|
1139
|
-
// basket refresh so a same-identity remount stays deduped and login re-links.
|
|
1140
|
-
const lastAuthLinkKeyRef = useRef(null)
|
|
1141
|
-
const lastCommerceClientJWTRef = useRef(null)
|
|
1142
|
-
const authLinkGenerationRef = useRef(0)
|
|
1143
|
-
const authLinkQueueRef = useRef(Promise.resolve())
|
|
1144
|
-
const lastAttemptedCommerceClientJWTRef = useRef(null)
|
|
1145
|
-
|
|
1146
791
|
// Extract enabled state and provider from configuration.
|
|
1147
792
|
// `provider` defaults to 'miaw' to preserve backwards compatibility with the
|
|
1148
793
|
// existing Salesforce Embedded Messaging (MIAW) integration.
|
|
@@ -1172,11 +817,6 @@ const ShopperAgent = ({commerceAgentConfiguration, basketDoneLoading}) => {
|
|
|
1172
817
|
<div data-testid="shopper-agent">
|
|
1173
818
|
<CommerceClientAgentWindow
|
|
1174
819
|
commerceAgentConfiguration={commerceAgentConfiguration}
|
|
1175
|
-
lastAuthLinkKeyRef={lastAuthLinkKeyRef}
|
|
1176
|
-
lastCommerceClientJWTRef={lastCommerceClientJWTRef}
|
|
1177
|
-
authLinkGenerationRef={authLinkGenerationRef}
|
|
1178
|
-
authLinkQueueRef={authLinkQueueRef}
|
|
1179
|
-
lastAttemptedCommerceClientJWTRef={lastAttemptedCommerceClientJWTRef}
|
|
1180
820
|
/>
|
|
1181
821
|
</div>
|
|
1182
822
|
) : null
|