@@ -30,6 +30,7 @@ interface RoktKitSettings {
3030 loggingUrl ?: string ;
3131 errorUrl ?: string ;
3232 isLoggingEnabled ?: string | boolean ;
33+ workspaceIdSyncApiKey ?: string ;
3334}
3435
3536interface EventAttributeCondition {
@@ -86,6 +87,29 @@ interface FilteredUser extends IMParticleUser {
8687 getUserIdentities ?: ( ) => { userIdentities : Record < string , string > } ;
8788}
8889
90+ // TODO: Replace with `IIdentitySearchResult` from `@mparticle/web-sdk` once
91+ // a version that exports it is published (currently on a feature branch in
92+ // mParticle/mparticle-web-sdk PR #1255). The shape below is intentionally
93+ // structurally identical so the swap is a one-line import change.
94+ interface WorkspaceIdSyncResult {
95+ httpCode : number ;
96+ body ?: {
97+ context ?: string | null ;
98+ mpid ?: string ;
99+ matched_identities ?: Record < string , string > ;
100+ is_ephemeral ?: boolean ;
101+ is_logged_in ?: boolean ;
102+ } ;
103+ }
104+
105+ // TODO: Replace with `IdentitySearchCallback`-compatible reference from
106+ // `@mparticle/web-sdk` once published (mirrors `SDKIdentityApi.search`).
107+ type WorkspaceIdSyncSearcher = (
108+ apiKey : string ,
109+ knownIdentities : { email : string } ,
110+ callback : ( result : WorkspaceIdSyncResult ) => void ,
111+ ) => void ;
112+
89113interface KitFilters {
90114 userAttributeFilters ?: string [ ] ;
91115 filterUserAttributes ?: ( attributes : Record < string , unknown > , filters ?: string [ ] ) => Record < string , unknown > ;
@@ -134,6 +158,7 @@ interface MParticleExtended {
134158 loggedEvents ?: Array < Record < string , unknown > > ;
135159 _registerErrorReportingService ?( service : ErrorReportingService ) : void ;
136160 _registerLoggingService ?( service : LoggingService ) : void ;
161+ Identity ?: { search ?: WorkspaceIdSyncSearcher } ;
137162}
138163
139164interface TestHelpers {
@@ -217,6 +242,13 @@ const ROKT_IDENTITY_EVENT_TYPE = {
217242const ROKT_THANK_YOU_JOURNEY_EXTENSION = 'ThankYouPageJourney' ;
218243const ROKT_INTEGRATION_SCRIPT_ID = 'rokt-launcher' ;
219244const ROKT_THANK_YOU_ELEMENT_SCRIPT_ID = 'rokt-thank-you-element' ;
245+ const USER_IDENTIFIED_IN_WORKSPACE_KEY = 'userIdentifiedInWorkspace' ;
246+
247+ // Bound on how long selectPlacements will wait for an in-flight Workspace
248+ // IDSync search before proceeding without the userIdentifiedInWorkspace flag.
249+ // Long enough to cover the typical /v1/search round-trip (~50ms); short enough that a
250+ // stalled search never blocks placement rendering on a thank-you page.
251+ const WORKSPACE_SEARCH_SELECT_TIMEOUT_MS = 500 ;
220252
221253type RoktIdentityEventType = ( typeof ROKT_IDENTITY_EVENT_TYPE ) [ keyof typeof ROKT_IDENTITY_EVENT_TYPE ] ;
222254
@@ -670,6 +702,9 @@ class RoktKit implements KitInterface {
670702 public launcher : RoktLauncher | null = null ;
671703 public filters : KitFilters = { } ;
672704 public userAttributes : Record < string , unknown > = { } ;
705+ // Flag set by the Workspace IDSync flow on a 200 response. Stored on the
706+ // kit instance and merged into placement attributes inside selectPlacements.
707+ public userIdentifiedInWorkspace = false ;
673708 public testHelpers : TestHelpers | null = null ;
674709 public placementEventMappingLookup : Record < string , string > = { } ;
675710 public placementEventAttributeMappingLookup : Record < string , PlacementEventRule [ ] > = { } ;
@@ -686,6 +721,17 @@ class RoktKit implements KitInterface {
686721 private _onboardingExpProvider ?: string ;
687722 private _thankYouElementOnLoadCallback : ( ( ) => void ) | null = null ;
688723 private _isThankYouElementLoaded = false ;
724+ private _workspaceIdSyncApiKey ?: string ;
725+
726+ // Held during a search dispatch so the next selectPlacements call;
727+ // can wait for the HTTP response before reading userIdentifiedInWorkspace;
728+ // — otherwise the first placement call ships without the flag.
729+ private _workspaceSearchInFlightPromise : Promise < void > | null = null ;
730+ // The email value sent in the most recent successful search
731+ // dispatch. If a subsequent identification arrives with the same email,
732+ // we skip the network call (the flag is still correct from the prior
733+ // search). Cleared on logout so a re-login re-evaluates fresh.
734+ private _workspaceLastSearchedEmail ?: string ;
689735
690736 // ---- Private helpers ----
691737
@@ -1044,6 +1090,10 @@ class RoktKit implements KitInterface {
10441090 this . _mappedEmailSha256Key = kitSettings . hashedEmailUserIdentityType . toLowerCase ( ) ;
10451091 }
10461092
1093+ this . _workspaceIdSyncApiKey = isString ( kitSettings . workspaceIdSyncApiKey )
1094+ ? kitSettings . workspaceIdSyncApiKey
1095+ : undefined ;
1096+
10471097 const domain = mp ( ) . Rokt ?. domain ;
10481098 const { roktExtensionsQueryParams, legacyRoktExtensions, loadThankYouElement } = extractRoktExtensionConfig (
10491099 kitSettings . roktExtensions ,
@@ -1195,15 +1245,75 @@ class RoktKit implements KitInterface {
11951245 }
11961246
11971247 public onUserIdentified ( user : IMParticleUser ) : string {
1198- this . filters . filteredUser = user as FilteredUser ;
1248+ const filteredUser = user as FilteredUser ;
1249+ this . filters . filteredUser = filteredUser ;
1250+ this . _workspaceSearchInFlightPromise = this . search ( filteredUser ) ;
11991251 return this . handleIdentityComplete ( user , ROKT_IDENTITY_EVENT_TYPE . IDENTIFY , 'onUserIdentified' ) ;
12001252 }
12011253
1254+ private search ( filteredUser : FilteredUser ) : Promise < void > {
1255+ const apiKey = this . _workspaceIdSyncApiKey ;
1256+ if ( ! apiKey ) {
1257+ this . userIdentifiedInWorkspace = false ;
1258+ this . _workspaceLastSearchedEmail = undefined ;
1259+ return Promise . resolve ( ) ;
1260+ }
1261+ const search = mp ( ) . Identity ?. search ;
1262+ if ( typeof search !== 'function' ) {
1263+ this . userIdentifiedInWorkspace = false ;
1264+ this . _workspaceLastSearchedEmail = undefined ;
1265+ return Promise . resolve ( ) ;
1266+ }
1267+ const userIdentities = filteredUser . getUserIdentities ? filteredUser . getUserIdentities ( ) . userIdentities : null ;
1268+ const email = userIdentities ?. email ;
1269+ if ( ! email || ! isString ( email ) ) {
1270+ this . userIdentifiedInWorkspace = false ;
1271+ this . _workspaceLastSearchedEmail = undefined ;
1272+ return Promise . resolve ( ) ;
1273+ }
1274+
1275+ // Same email as the last successful dispatch → skip the network call.
1276+ // The current flag value still reflects the correct match status.
1277+ if ( email === this . _workspaceLastSearchedEmail ) {
1278+ return Promise . resolve ( ) ;
1279+ }
1280+
1281+ // New / different email → reset and re-search. Cache the email up front
1282+ // so a second concurrent invocation with the same email also dedupes.
1283+ this . userIdentifiedInWorkspace = false ;
1284+ this . _workspaceLastSearchedEmail = email ;
1285+
1286+ return new Promise < void > ( ( resolve ) => {
1287+ try {
1288+ search ( apiKey , { email } , ( result : WorkspaceIdSyncResult ) => {
1289+ if ( result ?. httpCode === 200 ) {
1290+ this . userIdentifiedInWorkspace = true ;
1291+ }
1292+ resolve ( ) ;
1293+ } ) ;
1294+ } catch ( err ) {
1295+ console . error ( 'Rokt Kit: Workspace IDSync search failed' , err ) ;
1296+ // Dispatch failed — clear the cache so the same email can retry on
1297+ // the next identification rather than being stuck behind a poisoned
1298+ // entry that short-circuits future searches.
1299+ this . _workspaceLastSearchedEmail = undefined ;
1300+ resolve ( ) ;
1301+ }
1302+ } ) ;
1303+ }
1304+
12021305 public onLoginComplete ( user : IMParticleUser , _filteredIdentityRequest : unknown ) : string {
12031306 return this . handleIdentityComplete ( user , ROKT_IDENTITY_EVENT_TYPE . LOGIN , 'onLoginComplete' ) ;
12041307 }
12051308
12061309 public onLogoutComplete ( user : IMParticleUser , _filteredIdentityRequest : unknown ) : string {
1310+ // Anonymous sessions must not carry the previous user's match forward.
1311+ // Clear the flag explicitly here. Also clear the email cache so a
1312+ // re-login (possibly the same email) dispatches a fresh search rather
1313+ // than reusing a stale answer.
1314+ this . userIdentifiedInWorkspace = false ;
1315+ this . _workspaceSearchInFlightPromise = null ;
1316+ this . _workspaceLastSearchedEmail = undefined ;
12071317 return this . handleIdentityComplete ( user , ROKT_IDENTITY_EVENT_TYPE . LOGOUT , 'onLogoutComplete' ) ;
12081318 }
12091319
@@ -1213,8 +1323,39 @@ class RoktKit implements KitInterface {
12131323
12141324 /**
12151325 * Selects placements for Rokt Web SDK with merged attributes, filters, and experimentation options.
1326+ *
1327+ * If a Workspace IDSync search is in flight from a recent onUserIdentified
1328+ * call, this method waits up to `WORKSPACE_SEARCH_SELECT_TIMEOUT_MS` for it
1329+ * to settle so the first placement call can include the
1330+ * `userIdentifiedInWorkspace` flag without racing the network response.
1331+ * The timeout protects against a stalled or slow search blocking placement
1332+ * rendering — if it fires, selectPlacements proceeds without the flag.
1333+ *
1334+ * Implementation note: this method stays non-async deliberately. First,
1335+ * the public return type is `RoktSelection | Promise<RoktSelection> |
1336+ * undefined` — a superset of the `RoktSelection | Promise<RoktSelection>`
1337+ * shape declared for `RoktLauncher.selectPlacements` above (line ~70).
1338+ * Marking this `async` would narrow it to `Promise<RoktSelection |
1339+ * undefined>` and silently change the contract for callers that read
1340+ * the result synchronously. Second, `RoktSelection` has an optional
1341+ * `then?` member, so TS treats it as ambiguously promise-like and
1342+ * rejects it as the awaited return of an async function (TS1058) —
1343+ * working around that would require a cast or wrapping every return in
1344+ * `Promise.resolve(...)`. The inner work runs in `_dispatchPlacements`;
1345+ * this wrapper just gates it on the in-flight search via `Promise.race`.
12161346 */
12171347 public selectPlacements ( options : Record < string , unknown > ) : RoktSelection | Promise < RoktSelection > | undefined {
1348+ if ( this . _workspaceSearchInFlightPromise ) {
1349+ const inFlight = this . _workspaceSearchInFlightPromise ;
1350+ return Promise . race ( [
1351+ inFlight ,
1352+ new Promise < void > ( ( resolve ) => setTimeout ( resolve , WORKSPACE_SEARCH_SELECT_TIMEOUT_MS ) ) ,
1353+ ] ) . then ( ( ) => this . _dispatchPlacements ( options ) ) as Promise < RoktSelection > ;
1354+ }
1355+ return this . _dispatchPlacements ( options ) ;
1356+ }
1357+
1358+ private _dispatchPlacements ( options : Record < string , unknown > ) : RoktSelection | Promise < RoktSelection > | undefined {
12181359 const attributes = ( ( options && ( options . attributes as Record < string , unknown > ) ) || { } ) as Record < string , unknown > ;
12191360 const placementAttributes : Record < string , unknown > = { ...this . userAttributes , ...attributes } ;
12201361
@@ -1247,6 +1388,7 @@ class RoktKit implements KitInterface {
12471388 ...filteredAttributes ,
12481389 ...optimizelyAttributes ,
12491390 ...localSessionAttributes ,
1391+ ...( this . userIdentifiedInWorkspace ? { [ USER_IDENTIFIED_IN_WORKSPACE_KEY ] : true } : { } ) ,
12501392 mpid,
12511393 } ;
12521394
0 commit comments