From a006d7f4888dc07d3e0e26df3982702065e4cec5 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Mon, 12 Feb 2024 17:23:54 +1100 Subject: [PATCH 1/5] Add EUIDSdk class and initialization code. Add relevant EUID tests (mostly we'll rely on the UID2 tests). Make the callback manager handle SDKLoaded state separately for each SDK. --- .vscode/settings.json | 1 + src/euidSdk.ts | 49 +++++++++++++++++ src/integrationTests/euidSdk.test.ts | 78 ++++++++++++++++++++++++++++ src/uid2ApiClient.ts | 2 +- src/uid2CallbackManager.ts | 10 ++-- src/uid2Sdk.ts | 15 ++++-- 6 files changed, 146 insertions(+), 9 deletions(-) create mode 100644 src/euidSdk.ts create mode 100644 src/integrationTests/euidSdk.test.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index f74b7dee..7b36f739 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,6 +1,7 @@ { "cSpell.words": [ "cstg", + "EUID", "googletag", "initialised", "initialising", diff --git a/src/euidSdk.ts b/src/euidSdk.ts new file mode 100644 index 00000000..2d65bdd1 --- /dev/null +++ b/src/euidSdk.ts @@ -0,0 +1,49 @@ +import { EventType, Uid2CallbackHandler } from './uid2CallbackManager'; +import { CallbackContainer, ProductDetails, UID2SdkBase, UID2Setup } from './uid2Sdk'; + +export class EUID extends UID2SdkBase { + // Deprecated. Integrators should never access the cookie directly! + static get COOKIE_NAME() { + return '__euid'; + } + private static get Uid2Details(): ProductDetails { + return { + name: 'EUID', + defaultBaseUrl: 'https://prod.euid.eu', + localStorageKey: 'EUID-sdk-identity', + cookieName: '__euid', + }; + } + + constructor( + existingCallbacks: Uid2CallbackHandler[] | undefined = undefined, + callbackContainer: CallbackContainer = {} + ) { + super(existingCallbacks, EUID.Uid2Details); + const runCallbacks = () => { + this._callbackManager.runCallbacks(EventType.SdkLoaded, {}); + }; + if (window.__euid instanceof EUID) { + runCallbacks(); + } else { + // Need to defer running callbacks until this is assigned to the window global + callbackContainer.callback = runCallbacks; + } + } +} + +declare global { + interface Window { + __euid: EUID | UID2Setup | undefined; + } +} + +export function __euidInternalHandleScriptLoad() { + const callbacks = window?.__euid?.callbacks || []; + const callbackContainer: CallbackContainer = {}; + window.__euid = new EUID(callbacks, callbackContainer); + if (callbackContainer.callback) callbackContainer.callback(); +} +__euidInternalHandleScriptLoad(); + +export const sdkWindow = globalThis.window; diff --git a/src/integrationTests/euidSdk.test.ts b/src/integrationTests/euidSdk.test.ts new file mode 100644 index 00000000..a79e15c3 --- /dev/null +++ b/src/integrationTests/euidSdk.test.ts @@ -0,0 +1,78 @@ +import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals'; + +import * as mocks from '../mocks'; +import { sdkWindow, EUID, __euidInternalHandleScriptLoad } from '../euidSdk'; +import { EventType, Uid2CallbackHandler } from '../uid2CallbackManager'; + +let callback: any; +let asyncCallback: jest.Mock; +let euid: EUID; +let xhrMock: any; + +const debugOutput = false; + +mocks.setupFakeTime(); + +beforeEach(() => { + jest.clearAllMocks(); + mocks.resetFakeTime(); + jest.runOnlyPendingTimers(); + + callback = jest.fn(); + xhrMock = new mocks.XhrMock(sdkWindow); + mocks.setCookieMock(sdkWindow.document); + asyncCallback = jest.fn((event, payload) => { + if (debugOutput) { + console.log('Async Callback Event:', event); + console.log('Payload:', payload); + } + }); +}); + +afterEach(() => { + mocks.resetFakeTime(); +}); + +const makeIdentity = mocks.makeIdentityV2; + +describe('when a callback is provided', () => { + const refreshFrom = Date.now() + 100; + const identity = { ...makeIdentity(), refresh_from: refreshFrom }; + const refreshedIdentity = { + ...makeIdentity(), + advertising_token: 'refreshed_token', + }; + describe('before constructor is called', () => { + test('it should be called during the construction process', () => { + sdkWindow.__euid = { callbacks: [asyncCallback] }; + const calls = asyncCallback.mock.calls.length; + __euidInternalHandleScriptLoad(); + expect(asyncCallback).toBeCalledTimes(calls + 1); + expect(asyncCallback).toBeCalledWith(EventType.SdkLoaded, expect.anything()); + }); + test('it should not be called by the constructor itself', () => { + sdkWindow.__euid = { callbacks: [asyncCallback] }; + const calls = asyncCallback.mock.calls.length; + new EUID([asyncCallback]); + expect(asyncCallback).toBeCalledTimes(calls); + }); + }); + describe('before construction but the window global has already been assigned', () => { + // N.B. this is an artificial situation to check an edge case. + test('it should be called during construction', () => { + sdkWindow.__euid = new EUID(); + const calls = asyncCallback.mock.calls.length; + new EUID([asyncCallback]); + expect(asyncCallback).toBeCalledTimes(calls + 1); + }); + }); + describe('after construction', () => { + test('the SDKLoaded event is sent immediately', () => { + sdkWindow.__euid = new EUID(); + const calls = asyncCallback.mock.calls.length; + sdkWindow.__euid.callbacks!.push(asyncCallback); + expect(asyncCallback).toBeCalledTimes(calls + 1); + expect(asyncCallback).toBeCalledWith(EventType.SdkLoaded, expect.anything()); + }); + }); +}); diff --git a/src/uid2ApiClient.ts b/src/uid2ApiClient.ts index c2b70ea5..3d1d63b9 100644 --- a/src/uid2ApiClient.ts +++ b/src/uid2ApiClient.ts @@ -135,7 +135,7 @@ export class Uid2ApiClient { this._requestsInFlight.push(req); req.overrideMimeType('text/plain'); req.open('POST', url, true); - req.setRequestHeader('X-UID2-Client-Version', this._clientVersion); // TODO: EUID + req.setRequestHeader('X-UID2-Client-Version', this._clientVersion); // N.B. EUID and UID2 currently both use the same header let resolvePromise: (result: RefreshResult) => void; // eslint-disable-next-line @typescript-eslint/no-explicit-any let rejectPromise: (reason?: any) => void; diff --git a/src/uid2CallbackManager.ts b/src/uid2CallbackManager.ts index 898808fc..ec286934 100644 --- a/src/uid2CallbackManager.ts +++ b/src/uid2CallbackManager.ts @@ -20,22 +20,26 @@ export class Uid2CallbackManager { private _getIdentity: () => Uid2Identity | null | undefined; private _logger: Logger; private _sdk: UID2SdkBase; + private _productName: string; constructor( sdk: UID2SdkBase, + productName: string, getIdentity: () => Uid2Identity | null | undefined, logger: Logger ) { + this._productName = productName; this._logger = logger; this._getIdentity = getIdentity; this._sdk = sdk; this._sdk.callbacks.push = this.callbackPushInterceptor.bind(this); } - private static _sentSdkLoaded = false; //TODO: This needs to be fixed for EUID! + private static _sentSdkLoaded: Record = {}; //TODO: This needs to be fixed for EUID! private _sentInit = false; private callbackPushInterceptor(...args: Uid2CallbackHandler[]) { for (const c of args) { - if (Uid2CallbackManager._sentSdkLoaded) this.safeRunCallback(c, EventType.SdkLoaded, {}); + if (Uid2CallbackManager._sentSdkLoaded[this._productName]) + this.safeRunCallback(c, EventType.SdkLoaded, {}); if (this._sentInit) this.safeRunCallback(c, EventType.InitCompleted, { identity: this._getIdentity() ?? null, @@ -46,7 +50,7 @@ export class Uid2CallbackManager { public runCallbacks(event: EventType, payload: Uid2CallbackPayload) { if (event === EventType.InitCompleted) this._sentInit = true; - if (event === EventType.SdkLoaded) Uid2CallbackManager._sentSdkLoaded = true; + if (event === EventType.SdkLoaded) Uid2CallbackManager._sentSdkLoaded[this._productName] = true; if (!this._sentInit && event !== EventType.SdkLoaded) return; const enrichedPayload = { diff --git a/src/uid2Sdk.ts b/src/uid2Sdk.ts index 55915de6..b1deed78 100644 --- a/src/uid2Sdk.ts +++ b/src/uid2Sdk.ts @@ -20,10 +20,10 @@ function hasExpired(expiry: number, now = Date.now()) { return expiry <= now; } -type CallbackContainer = { callback?: () => void }; +export type CallbackContainer = { callback?: () => void }; -type ProductName = 'UID2' | 'EUID'; -type ProductDetails = { +export type ProductName = 'UID2' | 'EUID'; +export type ProductDetails = { name: ProductName; cookieName: string; localStorageKey: string; @@ -68,7 +68,12 @@ export abstract class UID2SdkBase { if (existingCallbacks) this.callbacks = existingCallbacks; this._tokenPromiseHandler = new UID2PromiseHandler(this); - this._callbackManager = new Uid2CallbackManager(this, () => this.getIdentity(), this._logger); + this._callbackManager = new Uid2CallbackManager( + this, + this._product.name, + () => this.getIdentity(), + this._logger + ); } public init(opts: Uid2Options) { @@ -450,7 +455,7 @@ export class UID2 extends UID2SdkBase { } } -type UID2Setup = { +export type UID2Setup = { callbacks: Uid2CallbackHandler[] | undefined; }; declare global { From b7c696a5f0f20e06dcd4a30f444d642caaaf5fa4 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Tue, 13 Feb 2024 11:15:48 +1100 Subject: [PATCH 2/5] Revert version change - no breaking changes so we don't need a major version bump. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 88b14bd1..f3b2a540 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@uid2/uid2-sdk", - "version": "4.0.0", + "version": "3.3.0", "description": "UID2 Client SDK", "main": "lib/uid2Sdk.js", "types": "lib/uid2Sdk.d.ts", From 755442cdc27d9cbb14c5995fe32af77336989fc1 Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 14 Feb 2024 16:15:09 +1100 Subject: [PATCH 3/5] Update launch configuration and build both SDKs when launching. --- .vscode/launch.json | 4 ++-- .vscode/tasks.json | 4 ++-- package-lock.json | 4 ++-- src/uid2ApiClient.ts | 11 +++++++---- webpack-dev-server.config.js | 7 +++++-- 5 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 897bd4ff..36fed78d 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -8,7 +8,7 @@ "name": "Launch CSTG (Chrome)", "type": "chrome", "request": "launch", - "url": "http://localhost:9090", + "url": "http://localhost:9190", "webRoot": "${workspaceRoot}/", "sourceMaps": true, "preLaunchTask": "Start CSTG", @@ -30,7 +30,7 @@ "name": "Launch Secure Signals (Chrome)", "type": "chrome", "request": "launch", - "url": "http://localhost:9090", + "url": "http://localhost:9190", "webRoot": "${workspaceRoot}/", "sourceMaps": true, "preLaunchTask": "Start Secure Signals", diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 6fe2c907..fdb4a8d1 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -61,7 +61,7 @@ "containerName": "uid2-cstg-example", "ports": [ { - "hostPort": 9090, + "hostPort": 9190, "containerPort": 80 } ], @@ -118,7 +118,7 @@ "containerName": "uid2-secure-signals-example", "ports": [ { - "hostPort": 9090, + "hostPort": 9190, "containerPort": 3000 } ], diff --git a/package-lock.json b/package-lock.json index 7b1492a3..35ebdeee 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@uid2/uid2-sdk", - "version": "3.2.0", + "version": "3.3.0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@uid2/uid2-sdk", - "version": "3.2.0", + "version": "3.3.0", "license": "Apache 2.0", "devDependencies": { "@jest/globals": "^29.2.2", diff --git a/src/uid2ApiClient.ts b/src/uid2ApiClient.ts index 3d1d63b9..49b2a233 100644 --- a/src/uid2ApiClient.ts +++ b/src/uid2ApiClient.ts @@ -1,4 +1,4 @@ -import { UID2SdkBase } from './uid2Sdk'; +import { ProductName, UID2SdkBase } from './uid2Sdk'; import { isValidIdentity, Uid2Identity } from './Uid2Identity'; import { UID2CstgBox } from './uid2CstgBox'; import { exportPublicKey } from './uid2CstgCrypto'; @@ -100,9 +100,9 @@ export type Uid2ApiClientOptions = { export class Uid2ApiClient { private _baseUrl: string; private _clientVersion: string; - private _productName: string; + private _productName: ProductName; private _requestsInFlight: XMLHttpRequest[] = []; - constructor(opts: Uid2ApiClientOptions, defaultBaseUrl: string, productName: string) { + constructor(opts: Uid2ApiClientOptions, defaultBaseUrl: string, productName: ProductName) { this._baseUrl = opts.baseUrl ?? defaultBaseUrl; this._productName = productName; this._clientVersion = productName.toLowerCase() + '-sdk-' + UID2SdkBase.VERSION; @@ -201,8 +201,11 @@ export class Uid2ApiClient { data: { emailHash: string } | { phoneHash: string }, opts: ClientSideIdentityOptions ): Promise { + const optoutPayload = this._productName == 'EUID' ? { optout_check: 1 } : {}; const request = - 'emailHash' in data ? { email_hash: data.emailHash } : { phone_hash: data.phoneHash }; + 'emailHash' in data + ? { email_hash: data.emailHash, ...optoutPayload } + : { phone_hash: data.phoneHash, ...optoutPayload }; const box = await UID2CstgBox.build(stripPublicKeyPrefix(opts.serverPublicKey)); diff --git a/webpack-dev-server.config.js b/webpack-dev-server.config.js index 03ebe2cd..007b641d 100644 --- a/webpack-dev-server.config.js +++ b/webpack-dev-server.config.js @@ -3,7 +3,10 @@ const path = require('path'); module.exports = { mode: 'development', devtool: 'inline-source-map', - entry: './src/uid2Sdk.ts', + entry: { + uid2: './src/uid2Sdk.ts', + euid: './src/euidSdk.ts', + }, module: { rules: [ { @@ -18,7 +21,7 @@ module.exports = { }, output: { path: path.resolve(__dirname, 'dist'), - filename: 'uid2-sdk.js', + filename: '[name]-sdk.js', }, devServer: { headers: { From 77653609f73c57b1c1674b96c6b9cdc0dddd799a Mon Sep 17 00:00:00 2001 From: Lionell Pack Date: Wed, 14 Feb 2024 17:10:52 +1100 Subject: [PATCH 4/5] Display UID2 and EUID side-by-side in the CSTG example. --- .vscode/tasks.json | 9 +- examples/cstg/html/index.html | 129 ++++++++++++++++------ examples/cstg/html/stylesheets/app.css | 26 +++-- examples/cstg/nginx/default.conf.template | 1 + 4 files changed, 122 insertions(+), 43 deletions(-) diff --git a/.vscode/tasks.json b/.vscode/tasks.json index fdb4a8d1..91a11783 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -75,8 +75,9 @@ "env": { "UID2_BASE_URL": "http://localhost:8080", "UID2_JS_SDK_URL": "http://localhost:9091/uid2-sdk.js", - "SERVER_PUBLIC_KEY": "UID2-X-I-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEtXJdTSZAYHvoRDWiehMHoWF1BNPuqLs5w2ZHiAZ1IJc7O4/z0ojPTB0V+KYX/wxQK0hxx6kxCvHj335eI/ZQsQ==", - "SUBSCRIPTION_ID": "4WvryDGbR5" + "EUID_JS_SDK_URL": "http://localhost:9091/euid-sdk.js", + "SERVER_PUBLIC_KEY": "UID2-X-L-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEWyCP9O/6ppffj8f5PUWsEhAoMNdTBnpnkiOPZBkVnLkxOyTjPsKzf5J3ApPHzutAGNGgKAzFc6TuCfo+BWsZtQ==", + "SUBSCRIPTION_ID": "LBk2xJsgrS" }, "remove": true } @@ -129,7 +130,9 @@ "permissions": "ro" } ], - "envFiles": ["${workspaceFolder}/examples/google-secure-signals-integration/with_sdk_v3/.env"], + "envFiles": [ + "${workspaceFolder}/examples/google-secure-signals-integration/with_sdk_v3/.env" + ], "remove": true } }, diff --git a/examples/cstg/html/index.html b/examples/cstg/html/index.html index 270b78b4..47501a22 100644 --- a/examples/cstg/html/index.html +++ b/examples/cstg/html/index.html @@ -7,22 +7,29 @@ + -

Server-Side Integration Example, UID2 JavaScript SDK

+

CSTG Integration Example, UID2 JavaScript SDK

- - - - - - - - - - - - - - - - - - - - - -
Ready for Targeted Advertising:
UID2 Advertising Token:
Is UID2 Login Required?
UID2 Identity Updated Counter:
UID2 Identity Callback State:
+
+ + + + + + + + + + + + + + + + + + + + + + + + +
UID2
Ready for Targeted Advertising:
UID2 Advertising Token:
Is UID2 Login Required?
UID2 Identity Updated Counter:
UID2 Identity Callback State:
+ + + + + + + + + + + + + + + + + + + + + + + + +
EUID
Ready for Targeted Advertising:
UID2 Advertising Token:
Is UID2 Login Required?
UID2 Identity Updated Counter:
UID2 Identity Callback State:
+