diff --git a/src/configManager.ts b/src/configManager.ts new file mode 100644 index 00000000..2d14f7a1 --- /dev/null +++ b/src/configManager.ts @@ -0,0 +1,93 @@ +import { ProductDetails } from './product'; +import { SdkOptions } from './sdkOptions'; + +type storedConfig = Pick< + SdkOptions, + 'baseUrl' | 'useCookie' | 'refreshRetryPeriod' | 'cookiePath' | 'cookieDomain' +>; + +const getConfigFromSdkOptions = (options: SdkOptions): storedConfig => { + const config: storedConfig = { + refreshRetryPeriod: options.refreshRetryPeriod, + baseUrl: options.baseUrl, + useCookie: options.useCookie, + cookiePath: options.cookiePath, + cookieDomain: options.cookieDomain, + }; + return config; +}; + +export const storeConfig = (options: SdkOptions, productDetails: ProductDetails) => { + if (options.useCookie) { + setConfigCookie(options, productDetails); + } else { + setConfigToLocalStorage(options, productDetails); + } +}; + +export const loadConfig = ( + options: SdkOptions, + productDetails: ProductDetails +): storedConfig | null => { + if (options.useCookie) { + return loadConfigFromCookie(productDetails); + } else { + return loadConfigFromLocalStorage(productDetails); + } +}; + +const setConfigCookie = (options: SdkOptions, productDetails: ProductDetails) => { + const cookieDomain = options.cookieDomain; + const path = options.cookiePath ?? '/'; + const value = JSON.stringify(getConfigFromSdkOptions(options)); + let cookie = + productDetails.cookieName + '_config' + '=' + encodeURIComponent(value) + ' ;path=' + path; + if (typeof cookieDomain !== 'undefined') { + cookie += ';domain=' + cookieDomain; + } + document.cookie = cookie; +}; + +const removeConfigCookie = (productDetails: ProductDetails) => { + document.cookie = + productDetails.cookieName + '_config' + '=;expires=Tue, 1 Jan 1980 23:59:59 GMT'; +}; + +const getConfigCookie = (productDetails: ProductDetails) => { + const docCookie = document.cookie; + if (docCookie) { + const payload = docCookie + .split('; ') + .find((row) => row.startsWith(productDetails.cookieName + '_config' + '=')); + if (payload) { + return decodeURIComponent(payload.split('=')[1]); + } + } +}; + +const loadConfigFromCookie = (productDetails: ProductDetails): storedConfig | null => { + const cookieData = getConfigCookie(productDetails); + if (cookieData) { + const result = JSON.parse(cookieData) as storedConfig; + return result; + } + return null; +}; + +const setConfigToLocalStorage = (options: SdkOptions, productDetails: ProductDetails) => { + const value = JSON.stringify(getConfigFromSdkOptions(options)); + localStorage.setItem(productDetails.localStorageKey + '_config', value); +}; + +const removeConfigFromLocalStorage = (productDetails: ProductDetails) => { + localStorage.removeItem(productDetails.localStorageKey + '_config'); +}; + +const loadConfigFromLocalStorage = (productDetails: ProductDetails): storedConfig | null => { + const data = localStorage.getItem(productDetails.localStorageKey + '_config'); + if (data) { + const result = JSON.parse(data) as storedConfig; + return result; + } + return null; +}; diff --git a/src/integrationTests/euidSdk.test.ts b/src/integrationTests/euidSdk.test.ts index d9751b37..7f978624 100644 --- a/src/integrationTests/euidSdk.test.ts +++ b/src/integrationTests/euidSdk.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals'; import * as mocks from '../mocks'; -import { sdkWindow, EUID, __euidInternalHandleScriptLoad } from '../euidSdk'; +import { sdkWindow, EUID, __euidInternalHandleScriptLoad, SdkOptions } from '../euidSdk'; import { EventType, CallbackHandler } from '../callbackManager'; import { __euidSSProviderScriptLoad } from '../secureSignalEuid'; import { UidSecureSignalProvider } from '../secureSignal_shared'; @@ -18,9 +18,32 @@ getAdvertisingTokenMock = jest.fn<() => Promise>(); const debugOutput = false; mocks.setupFakeTime(); +const mockDomain = 'www.uidapi.com'; const makeIdentity = mocks.makeIdentityV2; +const getConfigCookie = () => { + const docCookie = document.cookie; + if (docCookie) { + const payload = docCookie + .split('; ') + .find((row) => row.startsWith(EUID.COOKIE_NAME + '_config' + '=')); + if (payload) { + return JSON.parse(decodeURIComponent(payload.split('=')[1])); + } + } + return null; +}; + +const getConfigStorage = () => { + const data = localStorage.getItem('UID2-sdk-identity_config'); + if (data) { + const result = JSON.parse(data); + return result; + } + return null; +}; + describe('when a callback is provided', () => { beforeEach(() => { jest.clearAllMocks(); @@ -125,3 +148,39 @@ describe('when a callback is provided', () => { }); }); }); + +describe('Store config EUID', () => { + const identity = makeIdentity(); + const options: SdkOptions = { + baseUrl: 'http://test-host', + cookieDomain: mockDomain, + refreshRetryPeriod: 1000, + useCookie: false, + }; + + beforeEach(() => { + localStorage.removeItem('UID2-sdk-identity_config'); + document.cookie = EUID.COOKIE_NAME + '_config' + '=;expires=Tue, 1 Jan 1980 23:59:59 GMT'; + }); + + describe('when useCookie is true', () => { + test('should store config in cookie', () => { + euid.init({ callback: callback, identity: identity, ...options, useCookie: true }); + const cookie = getConfigCookie(); + expect(cookie).toBeInstanceOf(Object); + expect(cookie).toHaveProperty('cookieDomain'); + const storageConfig = getConfigStorage(); + expect(storageConfig).toBeNull(); + }); + }); + describe('when useCookie is false', () => { + test('should store config in local storage', () => { + euid.init({ callback: callback, identity: identity, ...options }); + const storageConfig = getConfigStorage(); + expect(storageConfig).toBeInstanceOf(Object); + expect(storageConfig).toHaveProperty('cookieDomain'); + const cookie = getConfigCookie(); + expect(cookie).toBeNull(); + }); + }); +}); diff --git a/src/integrationTests/options.test.ts b/src/integrationTests/options.test.ts index a67c4bc6..7263e7e8 100644 --- a/src/integrationTests/options.test.ts +++ b/src/integrationTests/options.test.ts @@ -1,7 +1,7 @@ import { afterEach, beforeEach, describe, expect, jest, test } from '@jest/globals'; import * as mocks from '../mocks'; -import { sdkWindow, UID2 } from '../uid2Sdk'; +import { SdkOptions, sdkWindow, UID2 } from '../uid2Sdk'; let callback: any; let uid2: UID2; @@ -33,6 +33,28 @@ const getUid2LocalStorage = mocks.getUid2LocalStorage; const removeUid2Cookie = mocks.removeUid2Cookie; const removeUid2LocalStorage = mocks.removeUid2LocalStorage; +const getConfigCookie = () => { + const docCookie = document.cookie; + if (docCookie) { + const payload = docCookie + .split('; ') + .find((row) => row.startsWith(UID2.COOKIE_NAME + '_config' + '=')); + if (payload) { + return JSON.parse(decodeURIComponent(payload.split('=')[1])); + } + } + return null; +}; + +const getConfigStorage = () => { + const data = localStorage.getItem('UID2-sdk-identity_config'); + if (data) { + const result = JSON.parse(data); + return result; + } + return null; +}; + describe('cookieDomain option', () => { describe('when using default value', () => { beforeEach(() => { @@ -193,3 +215,39 @@ describe('useCookie option', () => { }); }); }); + +describe('Store config UID2', () => { + const identity = makeIdentity(); + const options: SdkOptions = { + baseUrl: 'http://test-host', + cookieDomain: mockDomain, + refreshRetryPeriod: 1000, + useCookie: false, + }; + + beforeEach(() => { + localStorage.removeItem('UID2-sdk-identity_config'); + document.cookie = UID2.COOKIE_NAME + '_config' + '=;expires=Tue, 1 Jan 1980 23:59:59 GMT'; + }); + + describe('when useCookie is true', () => { + test('should store config in cookie', () => { + uid2.init({ callback: callback, identity: identity, ...options, useCookie: true }); + const cookie = getConfigCookie(); + expect(cookie).toBeInstanceOf(Object); + expect(cookie).toHaveProperty('cookieDomain'); + const storageConfig = getConfigStorage(); + expect(storageConfig).toBeNull(); + }); + }); + describe('when useCookie is false', () => { + test('should store config in local storage', () => { + uid2.init({ callback: callback, identity: identity, ...options }); + const storageConfig = getConfigStorage(); + expect(storageConfig).toBeInstanceOf(Object); + expect(storageConfig).toHaveProperty('cookieDomain'); + const cookie = getConfigCookie(); + expect(cookie).toBeNull(); + }); + }); +}); diff --git a/src/sdkBase.ts b/src/sdkBase.ts index eeea6481..f7eaea3e 100644 --- a/src/sdkBase.ts +++ b/src/sdkBase.ts @@ -15,6 +15,7 @@ import { PromiseHandler } from './promiseHandler'; import { StorageManager } from './storageManager'; import { hashAndEncodeIdentifier } from './encoding/hash'; import { ProductDetails, ProductName } from './product'; +import { storeConfig } from './configManager'; function hasExpired(expiry: number, now = Date.now()) { return expiry <= now; @@ -179,6 +180,8 @@ export abstract class SdkBase { if (!isSDKOptionsOrThrow(opts)) throw new TypeError(`Options provided to ${this._product.name} init couldn't be validated.`); + storeConfig(opts, this._product); + this._opts = opts; this._storageManager = new StorageManager( { ...opts }, diff --git a/src/sdkOptions.ts b/src/sdkOptions.ts index 8253e9e4..bb348905 100644 --- a/src/sdkOptions.ts +++ b/src/sdkOptions.ts @@ -5,7 +5,7 @@ import { Identity } from './Identity'; import { InitCallbackOptions } from './initCallbacks'; export type SdkOptions = BaseSdkOptions & InitCallbackOptions & CookieOptions & ApiClientOptions; -type BaseSdkOptions = { +export type BaseSdkOptions = { refreshRetryPeriod?: number; identity?: Identity; useCookie?: boolean;