Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 87 additions & 17 deletions src/Rokt-Kit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: <T>(settingsString?: string) => T[];
generateMappedEventLookup: (placementEventMapping: PlacementEventMappingEntry[]) => Record<string, string>;
Expand Down Expand Up @@ -178,6 +179,12 @@ interface LogEntry {
code?: string;
}

interface RoktExtensionConfig {
roktExtensionsQueryParams: string[];
legacyRoktExtensions: string[];
loadThankYouElement: boolean;
}

declare global {
interface Window {
Rokt?: RoktGlobal;
Expand Down Expand Up @@ -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];

Expand Down Expand Up @@ -245,17 +255,48 @@ 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;
}
return baseUrl + '?extensions=' + extensions.join(',');
}

function generateThankYouElementScript(domain: string | undefined) {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this ever be undefined?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe it will be if partners don't define ROKT_DOMAIN.

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<string, unknown> {
return val != null && typeof val === 'object' && Array.isArray(val) === false;
}
Expand All @@ -272,15 +313,33 @@ function parseSettingsString<T>(settingsString?: string): T[] {
return [];
}

function extractRoktExtensions(settingsString?: string): string[] {
function extractRoktExtensionConfig(settingsString?: string): RoktExtensionConfig {
const settings = settingsString ? parseSettingsString<RoktExtensionEntry>(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);
}
}
Comment on lines +339 to +343
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this necessary? I thought we are doing this in the new script creation.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup. This is necessary to initialize the WSDK thank you page extension manager.


function generateMappedEventLookup(placementEventMapping: PlacementEventMappingEntry[]): Record<string, string> {
Expand Down Expand Up @@ -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;

Expand All @@ -972,6 +1030,11 @@ class RoktKit implements KitInterface {
}

const domain = mp().Rokt?.domain;
const {
roktExtensionsQueryParams,
legacyRoktExtensions,
loadThankYouElement,
} = extractRoktExtensionConfig(kitSettings.roktExtensions);
const launcherOptions: Record<string, unknown> = {
...((mp().Rokt?.launcherOptions as Record<string, unknown>) || {}),
};
Expand Down Expand Up @@ -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,
Expand All @@ -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.');
}
Expand Down Expand Up @@ -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<unknown> {
if (!this.isKitReady()) {
Expand Down
137 changes: 119 additions & 18 deletions test/src/tests.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
'[{&quot;jsmap&quot;:null,&quot;map&quot;:null,&quot;maptype&quot;:&quot;StaticList&quot;,&quot;value&quot;:&quot;cos-extension-detection&quot;},{&quot;jsmap&quot;:null,&quot;map&quot;:null,&quot;maptype&quot;:&quot;StaticList&quot;,&quot;value&quot;:&quot;experiment-monitoring&quot;}]';
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 },
);
});
});

Expand Down
7 changes: 4 additions & 3 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}