From c91bbdf2c2fa7f4a1f9c114e4324e68ef9a45e82 Mon Sep 17 00:00:00 2001 From: Zachary Williams Date: Tue, 21 Apr 2026 11:36:53 -0700 Subject: [PATCH 1/3] Embedd TYE in Joint SDK --- src/Rokt-Kit.ts | 69 +++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 64 insertions(+), 5 deletions(-) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 124a522..4f4cc37 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -178,6 +178,11 @@ interface LogEntry { code?: string; } +interface RoktExtensionConfig { + roktExtensionsQueryParams: string[]; + legacyRoktExtensions: string[]; +} + declare global { interface Window { Rokt?: RoktGlobal; @@ -206,6 +211,7 @@ const ROKT_IDENTITY_EVENT_TYPE = { MODIFY_USER: 'modify_user', IDENTIFY: 'identify', } as const; +const ROKT_THANK_YOU_JOURNEY_EXTENSION = 'ThankYouJourney'; type RoktIdentityEventType = (typeof ROKT_IDENTITY_EVENT_TYPE)[keyof typeof ROKT_IDENTITY_EVENT_TYPE]; @@ -245,10 +251,8 @@ function mp(): MParticleExtended { // ============================================================ function generateLauncherScript(domain: string | undefined, extensions: string[]): string { - const resolvedDomain = typeof domain !== 'undefined' ? domain : 'apps.rokt.com'; - const protocol = 'https://'; const launcherPath = '/wsdk/integrations/launcher.js'; - const baseUrl = [protocol, resolvedDomain, launcherPath].join(''); + const baseUrl = [generateBaseUrl(domain), launcherPath].join(''); if (!extensions || extensions.length === 0) { return baseUrl; @@ -256,6 +260,18 @@ function generateLauncherScript(domain: string | undefined, extensions: string[] return baseUrl + '?extensions=' + extensions.join(','); } +function generateThankYouElementScript(domain: string | undefined) { + const thankYouElementPath = '/rokt-elements/rokt-element-thank-you.js'; + return [generateBaseUrl(domain), thankYouElementPath].join(''); +} + +function generateBaseUrl(domain: string |undefined) { + const resolvedDomain = typeof domain !== 'undefined' ? domain : 'apps.rokt.com'; + const protocol = 'https://'; + + return [protocol, resolvedDomain].join(''); +} + function isObject(val: unknown): val is Record { return val != null && typeof val === 'object' && Array.isArray(val) === false; } @@ -272,6 +288,7 @@ function parseSettingsString(settingsString?: string): T[] { return []; } +// TODO Remove and fix test function extractRoktExtensions(settingsString?: string): string[] { const settings = settingsString ? parseSettingsString(settingsString) : []; const roktExtensions: string[] = []; @@ -283,6 +300,47 @@ function extractRoktExtensions(settingsString?: string): string[] { return roktExtensions; } +function extractRoktExtensionConfig(domain?: string, settingsString?: string): RoktExtensionConfig { + const settings = settingsString ? parseSettingsString(settingsString) : []; + const roktExtensionsQueryParams: string[] = []; + const legacyRoktExtensions: string[] = []; + + for (let i = 0; i < settings.length; i++) { + const extensionName = settings[i].value; + if (extensionName === 'thank-you-journey') { + loadRoktThankYouElement(domain); + legacyRoktExtensions.push(ROKT_THANK_YOU_JOURNEY_EXTENSION) + } else { + roktExtensionsQueryParams.push(settings[i].value); + } + } + + return { roktExtensionsQueryParams, legacyRoktExtensions }; +} + +function loadRoktThankYouElement(domain?: string) { + const scriptId = 'rokt-thank-you-element'; + if (document.getElementById(scriptId)) { + return; + } + + const target = document.head || document.body + const script = document.createElement('script'); + script.type = 'text/javascript'; + (script as HTMLScriptElement & { fetchPriority: string }).fetchPriority = 'high'; + script.src = generateThankYouElementScript(domain); + script.crossOrigin = 'anonymous'; + script.async = true; + script.id = scriptId; + target.appendChild(script) +} + +function registerLegacyExtensions(legacyExtensions: string[]) { + for (const extension of legacyExtensions) { + window.mParticle.Rokt.use(extension); + } +} + function generateMappedEventLookup(placementEventMapping: PlacementEventMappingEntry[]): Record { if (!placementEventMapping) { return {}; @@ -954,7 +1012,6 @@ class RoktKit implements KitInterface { ): string { const kitSettings = settings as unknown as RoktKitSettings; const accountId = kitSettings.accountId; - const roktExtensions = extractRoktExtensions(kitSettings.roktExtensions); this.userAttributes = filteredUserAttributes || {}; this._onboardingExpProvider = kitSettings.onboardingExpProvider; @@ -972,6 +1029,7 @@ class RoktKit implements KitInterface { } const domain = mp().Rokt?.domain; + const { roktExtensionsQueryParams, legacyRoktExtensions } = extractRoktExtensionConfig(domain, kitSettings.roktExtensions); const launcherOptions: Record = { ...((mp().Rokt?.launcherOptions as Record) || {}), }; @@ -1040,7 +1098,7 @@ class RoktKit implements KitInterface { const target = document.head || document.body; const script = document.createElement('script'); script.type = 'text/javascript'; - script.src = generateLauncherScript(domain, roktExtensions); + script.src = generateLauncherScript(domain, roktExtensionsQueryParams); script.async = true; script.crossOrigin = 'anonymous'; (script as HTMLScriptElement & { fetchPriority: string }).fetchPriority = 'high'; @@ -1049,6 +1107,7 @@ class RoktKit implements KitInterface { script.onload = () => { if (this.isLauncherReadyToAttach()) { this.attachLauncher(accountId, launcherOptions); + registerLegacyExtensions(legacyRoktExtensions); } else { console.error('Rokt object is not available after script load.'); } From 853dd4105a544f2b836908843ba157c1968ca6a2 Mon Sep 17 00:00:00 2001 From: Zachary Williams Date: Tue, 21 Apr 2026 14:57:18 -0700 Subject: [PATCH 2/3] address comments --- src/Rokt-Kit.ts | 86 +++++++++++++++++--------- test/src/tests.spec.ts | 137 +++++++++++++++++++++++++++++++++++------ tsconfig.json | 7 ++- 3 files changed, 180 insertions(+), 50 deletions(-) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 4f4cc37..7d1f0ae 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -137,7 +137,8 @@ interface MParticleExtended { interface TestHelpers { generateLauncherScript: (domain: string | undefined, extensions: string[]) => string; - extractRoktExtensions: (settingsString?: string) => string[]; + generateThankYouElementScript: (domain: string | undefined) => string; + extractRoktExtensionConfig: (settingsString?: string) => RoktExtensionConfig; hashEventMessage: (messageType: number, eventType: number, eventName: string) => string | number; parseSettingsString: (settingsString?: string) => T[]; generateMappedEventLookup: (placementEventMapping: PlacementEventMappingEntry[]) => Record; @@ -181,6 +182,7 @@ interface LogEntry { interface RoktExtensionConfig { roktExtensionsQueryParams: string[]; legacyRoktExtensions: string[]; + loadThankYouElement: boolean; } declare global { @@ -212,6 +214,8 @@ const ROKT_IDENTITY_EVENT_TYPE = { IDENTIFY: 'identify', } as const; const ROKT_THANK_YOU_JOURNEY_EXTENSION = 'ThankYouJourney'; +const ROKT_INTEGRATION_SCRIPT_ID = 'rokt-launcher'; +const ROKT_THANK_YOU_ELEMENT_SCRIPT_ID = 'rokt-thank-you-element'; type RoktIdentityEventType = (typeof ROKT_IDENTITY_EVENT_TYPE)[keyof typeof ROKT_IDENTITY_EVENT_TYPE]; @@ -265,13 +269,34 @@ function generateThankYouElementScript(domain: string | undefined) { return [generateBaseUrl(domain), thankYouElementPath].join(''); } -function generateBaseUrl(domain: string |undefined) { +function generateBaseUrl(domain: string | undefined) { const resolvedDomain = typeof domain !== 'undefined' ? domain : 'apps.rokt.com'; const protocol = 'https://'; return [protocol, resolvedDomain].join(''); } +function loadRoktScript(scriptId: string, source: string, appendToTarget: boolean = true) { + const preexistingScript = document.getElementById(scriptId); + if (preexistingScript) { + return preexistingScript; + } + + const target = document.head || document.body; + const script = document.createElement('script'); + script.type = 'text/javascript'; + (script as HTMLScriptElement & { fetchPriority: string }).fetchPriority = 'high'; + script.src = source; + script.crossOrigin = 'anonymous'; + script.async = true; + script.id = scriptId; + if (appendToTarget) { + target.appendChild(script); + } + + return script; +} + function isObject(val: unknown): val is Record { return val != null && typeof val === 'object' && Array.isArray(val) === false; } @@ -288,34 +313,27 @@ function parseSettingsString(settingsString?: string): T[] { return []; } -// TODO Remove and fix test -function extractRoktExtensions(settingsString?: string): string[] { - const settings = settingsString ? parseSettingsString(settingsString) : []; - const roktExtensions: string[] = []; - - for (let i = 0; i < settings.length; i++) { - roktExtensions.push(settings[i].value); - } - - return roktExtensions; -} - -function extractRoktExtensionConfig(domain?: string, settingsString?: string): RoktExtensionConfig { +function extractRoktExtensionConfig(settingsString?: string): RoktExtensionConfig { const settings = settingsString ? parseSettingsString(settingsString) : []; const roktExtensionsQueryParams: string[] = []; const legacyRoktExtensions: string[] = []; + let loadThankYouElement = false; for (let i = 0; i < settings.length; i++) { const extensionName = settings[i].value; if (extensionName === 'thank-you-journey') { - loadRoktThankYouElement(domain); + loadThankYouElement = true; legacyRoktExtensions.push(ROKT_THANK_YOU_JOURNEY_EXTENSION) } else { - roktExtensionsQueryParams.push(settings[i].value); + roktExtensionsQueryParams.push(extensionName); } } - return { roktExtensionsQueryParams, legacyRoktExtensions }; + return { + roktExtensionsQueryParams, + legacyRoktExtensions, + loadThankYouElement, + }; } function loadRoktThankYouElement(domain?: string) { @@ -324,7 +342,7 @@ function loadRoktThankYouElement(domain?: string) { return; } - const target = document.head || document.body + const target = document.head || document.body; const script = document.createElement('script'); script.type = 'text/javascript'; (script as HTMLScriptElement & { fetchPriority: string }).fetchPriority = 'high'; @@ -332,7 +350,7 @@ function loadRoktThankYouElement(domain?: string) { script.crossOrigin = 'anonymous'; script.async = true; script.id = scriptId; - target.appendChild(script) + target.appendChild(script); } function registerLegacyExtensions(legacyExtensions: string[]) { @@ -1029,7 +1047,11 @@ class RoktKit implements KitInterface { } const domain = mp().Rokt?.domain; - const { roktExtensionsQueryParams, legacyRoktExtensions } = extractRoktExtensionConfig(domain, kitSettings.roktExtensions); + const { + roktExtensionsQueryParams, + legacyRoktExtensions, + loadThankYouElement, + } = extractRoktExtensionConfig(kitSettings.roktExtensions); const launcherOptions: Record = { ...((mp().Rokt?.launcherOptions as Record) || {}), }; @@ -1070,7 +1092,8 @@ class RoktKit implements KitInterface { if (testMode) { this.testHelpers = { generateLauncherScript: generateLauncherScript, - extractRoktExtensions: extractRoktExtensions, + generateThankYouElementScript: generateThankYouElementScript, + extractRoktExtensionConfig: extractRoktExtensionConfig, hashEventMessage: hashEventMessage, parseSettingsString: parseSettingsString, generateMappedEventLookup: generateMappedEventLookup, @@ -1092,17 +1115,20 @@ class RoktKit implements KitInterface { return 'Successfully initialized: ' + name; } + if (loadThankYouElement) { + loadRoktScript( + ROKT_THANK_YOU_ELEMENT_SCRIPT_ID, generateThankYouElementScript(domain)); + } + if (this.isLauncherReadyToAttach()) { this.attachLauncher(accountId, launcherOptions); } else { const target = document.head || document.body; - const script = document.createElement('script'); - script.type = 'text/javascript'; - script.src = generateLauncherScript(domain, roktExtensionsQueryParams); - script.async = true; - script.crossOrigin = 'anonymous'; - (script as HTMLScriptElement & { fetchPriority: string }).fetchPriority = 'high'; - script.id = 'rokt-launcher'; + const script = loadRoktScript( + ROKT_INTEGRATION_SCRIPT_ID, + generateLauncherScript(domain, roktExtensionsQueryParams), + false, + ) script.onload = () => { if (this.isLauncherReadyToAttach()) { @@ -1259,6 +1285,8 @@ class RoktKit implements KitInterface { /** * Enables optional Integration Launcher extensions before selecting placements. + * + * @deprecated This functionality has been internalized and will be removed in a future release. */ public use(extensionName: string): Promise { if (!this.isKitReady()) { diff --git a/test/src/tests.spec.ts b/test/src/tests.spec.ts index 4bc97e5..8a44b7a 100644 --- a/test/src/tests.spec.ts +++ b/test/src/tests.spec.ts @@ -3272,38 +3272,139 @@ describe('Rokt Forwarder', () => { }); }); - describe('#roktExtensions', () => { - beforeEach(async () => { - (window as any).Rokt = new (MockRoktForwarder as any)(); - (window as any).mParticle.Rokt = (window as any).Rokt; + describe('#generateThankYouElementScript', () => { + const baseUrl = 'https://apps.rokt.com/rokt-elements/rokt-element-thank-you.js'; - await (window as any).mParticle.forwarder.init( - { - accountId: '123456', - }, + beforeEach(() => { + (window as any).mParticle.forwarder.init( + { accountId: '123456' }, reportService.cb, true, ); }); - describe('extractRoktExtensions', () => { - it('should correctly map known extension names to their query parameters', async () => { + it('should return base URL when no domain is passed', () => { + const url = (window as any).mParticle.forwarder.testHelpers.generateThankYouElementScript(undefined); + expect(url).toBe(baseUrl); + }); + + it('should return an updated base URL with CNAME when domain is passed', () => { + const url = (window as any).mParticle.forwarder.testHelpers.generateThankYouElementScript('cname.rokt.com'); + expect(url).toBe('https://cname.rokt.com/rokt-elements/rokt-element-thank-you.js'); + }); + }); + + describe('#roktExtensions', () => { + beforeEach(() => { + (window as any).mParticle.forwarder.init( + { accountId: '123456' }, + reportService.cb, + true, + ); + }); + + describe('extractRoktExtensionConfig', () => { + it('should correctly map known extension names to their query parameters', () => { const settingsString = '[{"jsmap":null,"map":null,"maptype":"StaticList","value":"cos-extension-detection"},{"jsmap":null,"map":null,"maptype":"StaticList","value":"experiment-monitoring"}]'; - const expectedExtensions = ['cos-extension-detection', 'experiment-monitoring']; - expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensions(settingsString)).toEqual( - expectedExtensions, - ); + const result = (window as any).mParticle.forwarder.testHelpers.extractRoktExtensionConfig(settingsString); + expect(result.roktExtensionsQueryParams).toEqual(['cos-extension-detection', 'experiment-monitoring']); + expect(result.legacyRoktExtensions).toEqual([]); + expect(result.loadThankYouElement).toBe(false); + }); + + it('should separate thank-you-journey into legacyRoktExtensions and set loadThankYouElement', () => { + const settingsString = + '[{"jsmap":null,"map":null,"maptype":"LegacyExtension","value":"thank-you-journey"},{"jsmap":null,"map":null,"maptype":"StaticList","value":"instant-purchase"}]'; + + const result = (window as any).mParticle.forwarder.testHelpers.extractRoktExtensionConfig(settingsString); + expect(result.roktExtensionsQueryParams).toEqual(['instant-purchase']); + expect(result.legacyRoktExtensions).toEqual(['ThankYouJourney']); + expect(result.loadThankYouElement).toBe(true); }); }); - it('should handle invalid setting strings', () => { - expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensions('NONE')).toEqual([]); + it('should fetch thank you element resource when thank you element extension is provided', async () => { + document.getElementById('rokt-thank-you-element')?.remove(); + document.getElementById('rokt-launcher')?.remove(); + + (window as any).Rokt = undefined; + (window as any).mParticle.Rokt = { + attachKit: async (kit: any) => { (window as any).mParticle.Rokt.kit = kit; }, + filters: { + userAttributesFilters: [], + filterUserAttributes: (attrs: any) => attrs, + filteredUser: { getMPID: () => '123' }, + }, + use: () => Promise.resolve(), + }; + + await (window as any).mParticle.forwarder.init( + { + accountId: '123456', + roktExtensions: '[{"jsmap":null,"map":null,"maptype":"LegacyExtension","value":"thank-you-journey"}]', + }, + reportService.cb, + false, + ); - expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensions(undefined)).toEqual([]); + const tyeScript = document.getElementById('rokt-thank-you-element') as HTMLScriptElement; + expect(tyeScript).not.toBeNull(); + expect(tyeScript.src).toContain('/rokt-elements/rokt-element-thank-you.js'); + }); + + it('should call mParticle.Rokt.use with ThankYouJourney when thank-you-journey extension is provided', async () => { + document.getElementById('rokt-thank-you-element')?.remove(); + document.getElementById('rokt-launcher')?.remove(); + + const useCalls: string[] = []; + + (window as any).Rokt = undefined; + (window as any).mParticle.Rokt = { + attachKit: async (kit: any) => { (window as any).mParticle.Rokt.kit = kit; }, + filters: { + userAttributesFilters: [], + filterUserAttributes: (attrs: any) => attrs, + filteredUser: { getMPID: () => '123' }, + }, + use: (name: string) => { + useCalls.push(name); + return Promise.resolve(); + }, + }; - expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensions(null)).toEqual([]); + await (window as any).mParticle.forwarder.init( + { + accountId: '123456', + roktExtensions: '[{"jsmap":null,"map":null,"maptype":"LegacyExtension","value":"thank-you-journey"}]', + }, + reportService.cb, + false, + ); + + (window as any).Rokt = new (MockRoktForwarder as any)(); + (window as any).Rokt.createLauncher = async () => + Promise.resolve({ selectPlacements: () => {}, hashAttributes: () => {}, use: () => Promise.resolve() }); + + const launcherScript = document.getElementById('rokt-launcher') as HTMLScriptElement; + launcherScript.onload!(new Event('load')); + + await waitForCondition(() => useCalls.length > 0); + + expect(useCalls).toContain('ThankYouJourney'); + }); + + it('should handle invalid setting strings', () => { + expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensionConfig('NONE')).toEqual( + { roktExtensionsQueryParams: [], legacyRoktExtensions: [], loadThankYouElement: false }, + ); + expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensionConfig(undefined)).toEqual( + { roktExtensionsQueryParams: [], legacyRoktExtensions: [], loadThankYouElement: false }, + ); + expect((window as any).mParticle.forwarder.testHelpers.extractRoktExtensionConfig(null)).toEqual( + { roktExtensionsQueryParams: [], legacyRoktExtensions: [], loadThankYouElement: false }, + ); }); }); diff --git a/tsconfig.json b/tsconfig.json index ee3c6d1..3c84a31 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,8 +12,9 @@ "forceConsistentCasingInFileNames": true, "lib": ["DOM", "ESNext"], "declaration": true, - "skipLibCheck": true + "skipLibCheck": true, + "types": ["vitest/globals"] }, - "include": ["src/**/*"], - "exclude": ["node_modules", "dist", "test"] + "include": ["src/**/*", "test/**/*"], + "exclude": ["node_modules", "dist"] } From 11a9f7824334d473b76414346e5c656e17138dc1 Mon Sep 17 00:00:00 2001 From: Zachary Williams Date: Tue, 21 Apr 2026 16:07:03 -0700 Subject: [PATCH 3/3] Remove artifact from last iteration --- src/Rokt-Kit.ts | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 7d1f0ae..d11a00d 100644 --- a/src/Rokt-Kit.ts +++ b/src/Rokt-Kit.ts @@ -336,23 +336,6 @@ function extractRoktExtensionConfig(settingsString?: string): RoktExtensionConfi }; } -function loadRoktThankYouElement(domain?: string) { - const scriptId = 'rokt-thank-you-element'; - if (document.getElementById(scriptId)) { - return; - } - - const target = document.head || document.body; - const script = document.createElement('script'); - script.type = 'text/javascript'; - (script as HTMLScriptElement & { fetchPriority: string }).fetchPriority = 'high'; - script.src = generateThankYouElementScript(domain); - script.crossOrigin = 'anonymous'; - script.async = true; - script.id = scriptId; - target.appendChild(script); -} - function registerLegacyExtensions(legacyExtensions: string[]) { for (const extension of legacyExtensions) { window.mParticle.Rokt.use(extension);