diff --git a/setupJest.js b/setupJest.js index f3ba198..347db21 100644 --- a/setupJest.js +++ b/setupJest.js @@ -10,17 +10,6 @@ expect.extend({ }); expect.extend({ - toBeInInitialisingState(uid2) { - expect(uid2.getAdvertisingToken()).toBeUndefined(); - expect(uid2.isLoginRequired()).toBeUndefined(); - - return { - pass: true, - message: () => - 'Expected getAdvertisingToken() returns undefined and isLoginRequired() returns undefined', - }; - }, - toBeInAvailableState(uid2, expectedAdvertisingToken) { if (expectedAdvertisingToken) { expect(uid2.getAdvertisingToken()).toBe(expectedAdvertisingToken); @@ -68,7 +57,7 @@ expect.extend({ return { pass: true, message: () => - 'Expected getAdvertisingToken() returns undefined and isLoginRequired() returns false', + 'Expected getAdvertisingToken() returns undefined and isLoginRequired() returns false', }; }, }); diff --git a/src/cookieManager.ts b/src/cookieManager.ts index b0abea5..72db3a7 100644 --- a/src/cookieManager.ts +++ b/src/cookieManager.ts @@ -29,6 +29,28 @@ function enrichIdentity(identity: LegacySDKCookie, now: number) { }; } +function getCookie(cookieName: string) { + const docCookie = document.cookie; + if (docCookie) { + const payload = docCookie.split('; ').find((row) => row.startsWith(cookieName + '=')); + if (payload) { + return decodeURIComponent(payload.split('=')[1]); + } + } +} + +export function loadIdentityFromCookieNoLegacy( + cookieName: string +): Identity | OptoutIdentity | null { + const payload = getCookie(cookieName); + if (payload) { + const result = JSON.parse(payload) as unknown; + if (isValidIdentity(result)) return result; + if (isOptoutIdentity(result)) return result; + } + return null; +} + export class CookieManager { private _opts: CookieOptions; private _cookieName: string; @@ -62,15 +84,6 @@ export class CookieManager { (previousOptions.cookieDomain ?? '') + ';expires=Tue, 1 Jan 1980 23:59:59 GMT'; } - private getCookie() { - const docCookie = document.cookie; - if (docCookie) { - const payload = docCookie.split('; ').find((row) => row.startsWith(this._cookieName + '=')); - if (payload) { - return decodeURIComponent(payload.split('=')[1]); - } - } - } private migrateLegacyCookie(identity: LegacySDKCookie, now: number): Identity { const newCookie = enrichIdentity(identity, now); @@ -79,7 +92,7 @@ export class CookieManager { } public loadIdentityFromCookie(): Identity | OptoutIdentity | null { - const payload = this.getCookie(); + const payload = getCookie(this._cookieName); if (payload) { const result = JSON.parse(payload) as unknown; if (isValidIdentity(result)) return result; diff --git a/src/integrationTests/basic.test.ts b/src/integrationTests/basic.test.ts index 18a0877..53ed423 100644 --- a/src/integrationTests/basic.test.ts +++ b/src/integrationTests/basic.test.ts @@ -90,16 +90,6 @@ testCookieAndLocalStorage(() => { }); }); - describe('initial state before init() is called', () => { - test('should be in initialising state', () => { - (expect(uid2) as any).toBeInInitialisingState(); - }); - - test('getAdvertisingToken should return undefined', () => { - expect(uid2.getAdvertisingToken()).toBeUndefined(); - }); - }); - describe('when initialising with invalid options', () => { test('should fail on no opts', () => { expect(() => (uid2 as any).init()).toThrow(TypeError); @@ -1103,3 +1093,37 @@ describe('SDK bootstraps itself if init has already been completed', () => { }).rejects.toThrow(); }); }); + +describe('Token retrieval and related public functions working without init', () => { + const makeIdentity = mocks.makeIdentityV2; + const email = 'test@test.com'; + const emailHash = 'lz3+Rj7IV4X1+Vr1ujkG7tstkxwk5pgkqJ6mXbpOgTs='; + + test('should be able to find identity set without init', async () => { + const identity = { ...makeIdentity(), refresh_from: Date.now() + 100 }; + const identityStorageFunctions = [ + { + setIdentity: () => mocks.setUid2LocalStorage(identity), + removeIdentity: () => mocks.removeUid2LocalStorage(), + }, + { + setIdentity: () => mocks.setUid2Cookie(identity), + removeIdentity: () => mocks.removeUid2Cookie(), + }, + ]; + + identityStorageFunctions.forEach((functions) => { + functions.setIdentity(); + expect(uid2.getAdvertisingToken()).toBe(identity.advertising_token); + expect(uid2.getIdentity()).toStrictEqual(identity); + expect(uid2.getAdvertisingTokenAsync()).resolves.toBe(identity.advertising_token); + expect(async () => { + await uid2.setIdentityFromEmail(email, mocks.makeCstgOption()); + }).rejects.toThrow(); + expect(async () => { + await uid2.setIdentityFromEmailHash(emailHash, mocks.makeCstgOption()); + }).rejects.toThrow(); + functions.removeIdentity(); + }); + }); +}); diff --git a/src/localStorageManager.ts b/src/localStorageManager.ts index ad4de3f..eaced60 100644 --- a/src/localStorageManager.ts +++ b/src/localStorageManager.ts @@ -1,5 +1,19 @@ import { isOptoutIdentity, isValidIdentity, OptoutIdentity, Identity } from './Identity'; +export function loadIdentityWithStorageKey(storageKey: string): Identity | OptoutIdentity | null { + const payload = getValue(storageKey); + if (payload) { + const result = JSON.parse(payload) as unknown; + if (isValidIdentity(result)) return result; + if (isOptoutIdentity(result)) return result; + } + return null; +} + +function getValue(storageKey: string) { + return localStorage.getItem(storageKey); +} + export class LocalStorageManager { private _storageKey: string; constructor(storageKey: string) { @@ -12,17 +26,8 @@ export class LocalStorageManager { public removeValue() { localStorage.removeItem(this._storageKey); } - private getValue() { - return localStorage.getItem(this._storageKey); - } public loadIdentityFromLocalStorage(): Identity | OptoutIdentity | null { - const payload = this.getValue(); - if (payload) { - const result = JSON.parse(payload) as unknown; - if (isValidIdentity(result)) return result; - if (isOptoutIdentity(result)) return result; - } - return null; + return loadIdentityWithStorageKey(this._storageKey); } } diff --git a/src/sdkBase.ts b/src/sdkBase.ts index c5c9752..e1bc674 100644 --- a/src/sdkBase.ts +++ b/src/sdkBase.ts @@ -16,6 +16,8 @@ import { StorageManager } from './storageManager'; import { hashAndEncodeIdentifier } from './encoding/hash'; import { ProductDetails, ProductName } from './product'; import { storeConfig, updateConfig } from './configManager'; +import { loadIdentityFromCookieNoLegacy } from './cookieManager'; +import { loadIdentityWithStorageKey } from './localStorageManager'; function hasExpired(expiry: number, now = Date.now()) { return expiry <= now; @@ -121,10 +123,12 @@ export abstract class SdkBase { } public getIdentity(): Identity | null { - return this._identity && !this.temporarilyUnavailable() && !isOptoutIdentity(this._identity) - ? this._identity + const identity = this._identity ?? this.getIdentityNoInit(); + return identity && !this.temporarilyUnavailable(identity) && !isOptoutIdentity(identity) + ? identity : null; } + // When the SDK has been initialized, this function should return the token // from the most recent refresh request, if there is a request, wait for the // new token. Otherwise, returns a promise which will be resolved after init. @@ -260,13 +264,10 @@ export abstract class SdkBase { return this._identity && !hasExpired(this._identity.refresh_expires); } - private temporarilyUnavailable() { - if (!this._identity && this._apiClient?.hasActiveRequests()) return true; - if ( - this._identity && - hasExpired(this._identity.identity_expires) && - !hasExpired(this._identity.refresh_expires) - ) + private temporarilyUnavailable(identity: Identity | OptoutIdentity | null | undefined) { + if (!identity && this._apiClient?.hasActiveRequests()) return true; + // returns true if identity is expired but refreshable + if (identity && hasExpired(identity.identity_expires) && !hasExpired(identity.refresh_expires)) return true; return false; } @@ -457,6 +458,13 @@ export abstract class SdkBase { ); } + private getIdentityNoInit() { + return ( + loadIdentityFromCookieNoLegacy(this._product.cookieName) ?? + loadIdentityWithStorageKey(this._product.localStorageKey) + ); + } + protected async callCstgAndSetIdentity( request: { emailHash: string } | { phoneHash: string }, opts: ClientSideIdentityOptions diff --git a/src/storageManager.ts b/src/storageManager.ts index 3f69eb5..2498062 100644 --- a/src/storageManager.ts +++ b/src/storageManager.ts @@ -25,9 +25,10 @@ export class StorageManager { } public loadIdentity(): Identity | OptoutIdentity | null { - return this._opts.useCookie - ? this._cookieManager.loadIdentityFromCookie() - : this._localStorageManager.loadIdentityFromLocalStorage(); + return ( + this._cookieManager.loadIdentityFromCookie() ?? + this._localStorageManager.loadIdentityFromLocalStorage() + ); } public setIdentity(identity: Identity) { @@ -63,10 +64,7 @@ export class StorageManager { } this._localStorageManager.setValue(value); - if ( - this._opts.useCookie === false && - this._localStorageManager.loadIdentityFromLocalStorage() - ) { + if (this._localStorageManager.loadIdentityFromLocalStorage()) { this._cookieManager.removeCookie(this._opts); } }