diff --git a/src/Rokt-Kit.ts b/src/Rokt-Kit.ts index 124a522..d11a00d 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; @@ -178,6 +179,12 @@ interface LogEntry { code?: string; } +interface RoktExtensionConfig { + roktExtensionsQueryParams: string[]; + legacyRoktExtensions: string[]; + loadThankYouElement: boolean; +} + declare global { interface Window { Rokt?: RoktGlobal; @@ -206,6 +213,9 @@ const ROKT_IDENTITY_EVENT_TYPE = { MODIFY_USER: 'modify_user', 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]; @@ -245,10 +255,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 +264,39 @@ 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 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; } @@ -272,15 +313,33 @@ function parseSettingsString(settingsString?: string): T[] { return []; } -function extractRoktExtensions(settingsString?: string): string[] { +function extractRoktExtensionConfig(settingsString?: string): RoktExtensionConfig { const settings = settingsString ? parseSettingsString(settingsString) : []; - const roktExtensions: string[] = []; + const roktExtensionsQueryParams: string[] = []; + const legacyRoktExtensions: string[] = []; + let loadThankYouElement = false; for (let i = 0; i < settings.length; i++) { - roktExtensions.push(settings[i].value); + const extensionName = settings[i].value; + if (extensionName === 'thank-you-journey') { + loadThankYouElement = true; + legacyRoktExtensions.push(ROKT_THANK_YOU_JOURNEY_EXTENSION) + } else { + roktExtensionsQueryParams.push(extensionName); + } } - return roktExtensions; + return { + roktExtensionsQueryParams, + legacyRoktExtensions, + loadThankYouElement, + }; +} + +function registerLegacyExtensions(legacyExtensions: string[]) { + for (const extension of legacyExtensions) { + window.mParticle.Rokt.use(extension); + } } function generateMappedEventLookup(placementEventMapping: PlacementEventMappingEntry[]): Record { @@ -954,7 +1013,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 +1030,11 @@ class RoktKit implements KitInterface { } const domain = mp().Rokt?.domain; + const { + roktExtensionsQueryParams, + legacyRoktExtensions, + loadThankYouElement, + } = extractRoktExtensionConfig(kitSettings.roktExtensions); const launcherOptions: Record = { ...((mp().Rokt?.launcherOptions as Record) || {}), }; @@ -1012,7 +1075,8 @@ class RoktKit implements KitInterface { if (testMode) { this.testHelpers = { generateLauncherScript: generateLauncherScript, - extractRoktExtensions: extractRoktExtensions, + generateThankYouElementScript: generateThankYouElementScript, + extractRoktExtensionConfig: extractRoktExtensionConfig, hashEventMessage: hashEventMessage, parseSettingsString: parseSettingsString, generateMappedEventLookup: generateMappedEventLookup, @@ -1034,21 +1098,25 @@ 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, roktExtensions); - 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()) { this.attachLauncher(accountId, launcherOptions); + registerLegacyExtensions(legacyRoktExtensions); } else { console.error('Rokt object is not available after script load.'); } @@ -1200,6 +1268,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"] }