Repository: auth0/auth0-spa-js
Pull request #1108 (2023-06-14 10:49:21)

Lazily retrieve transaction from transaction storage
### Changes

When an instance of `Auth0Client` is created, we create an instance of our internal `TransactionManager`, which instantly accesses the storage.

When using `sessionStorage` in browsers that have blocked cookies and site data, this will cause an error when calling the `Auth0Client` constructor, resulting in rendering issues in our React wrapper as mentioned [here](https://github.com/auth0/auth0-react/issues/540).

By lazily accessing the transaction, we avoid this error when the constructor is called, but instead it will throw when the transaction is actually accessed, which will be caught by our React SDK.

### References
https://github.com/auth0/auth0-react/issues/540

### Checklist

- [x] I have read the [Auth0 general contribution guidelines](https://github.com/auth0/open-source-template/blob/master/GENERAL-CONTRIBUTING.md)
- [x] I have read the [Auth0 Code of Conduct](https://github.com/auth0/open-source-template/blob/master/CODE-OF-CONDUCT.md)
- [x] All code quality tools/guidelines have been run/followed



Patch:
diff --git a/src/transaction-manager.ts b/src/transaction-manager.ts
index 17a2207..40efc45 100644
--- a/src/transaction-manager.ts
+++ b/src/transaction-manager.ts
@@ -14,29 +14,28 @@ interface Transaction {
 }
 
 export class TransactionManager {
-  private transaction: Transaction | undefined;
   private storageKey: string;
 
-  constructor(private storage: ClientStorage, private clientId: string, private cookieDomain?: string) {
+  constructor(
+    private storage: ClientStorage,
+    private clientId: string,
+    private cookieDomain?: string
+  ) {
     this.storageKey = `${TRANSACTION_STORAGE_KEY_PREFIX}.${this.clientId}`;
-    this.transaction = this.storage.get(this.storageKey);
   }
 
   public create(transaction: Transaction) {
-    this.transaction = transaction;
-
     this.storage.save(this.storageKey, transaction, {
       daysUntilExpire: 1,
-      cookieDomain: this.cookieDomain,
+      cookieDomain: this.cookieDomain
     });
   }
 
   public get(): Transaction | undefined {
-    return this.transaction;
+    return this.storage.get(this.storageKey);
   }
 
   public remove() {
-    delete this.transaction;
     this.storage.remove(this.storageKey, {
       cookieDomain: this.cookieDomain
     });


Test patch:
diff --git a/__tests__/Auth0Client/handleRedirectCallback.test.ts b/__tests__/Auth0Client/handleRedirectCallback.test.ts
index f78d6c5..4ea5a5c 100644
--- a/__tests__/Auth0Client/handleRedirectCallback.test.ts
+++ b/__tests__/Auth0Client/handleRedirectCallback.test.ts
@@ -135,7 +135,7 @@ describe('Auth0Client', () => {
       expect(error.error).toBe('some-error');
       expect(error.error_description).toBe('some-error-description');
       expect(error.state).toBe('error-state');
-      expect(error.appState).toBe(appState);
+      expect(error.appState).toEqual(appState);
     });
 
     it('should clear the transaction data when the /authorize call redirects with a code param', async () => {
@@ -220,7 +220,7 @@ describe('Auth0Client', () => {
       const result = await loginWithRedirect(auth0, { appState });
 
       expect(result).toBeDefined();
-      expect(result.appState).toBe(appState);
+      expect(result.appState).toEqual(appState);
     });
 
     it('uses the custom http timeout value if specified', async () => {
@@ -234,7 +234,7 @@ describe('Auth0Client', () => {
 
       expect((http.switchFetch as jest.Mock).mock.calls[0][6]).toEqual(40000);
       expect(result).toBeDefined();
-      expect(result.appState).toBe(appState);
+      expect(result.appState).toEqual(appState);
     });
 
     it('does not store the scope from token endpoint if none was returned', async () => {
@@ -456,7 +456,7 @@ describe('Auth0Client', () => {
       expect(error.error).toBe('some-error');
       expect(error.error_description).toBe('some-error-description');
       expect(error.state).toBe('error-state');
-      expect(error.appState).toBe(appState);
+      expect(error.appState).toEqual(appState);
     });
 
     it('should clear the transaction data when the /authorize call redirects with a code param', async () => {
diff --git a/__tests__/Auth0Client/loginWithRedirect.test.ts b/__tests__/Auth0Client/loginWithRedirect.test.ts
index 45363a9..46ee9ac 100644
--- a/__tests__/Auth0Client/loginWithRedirect.test.ts
+++ b/__tests__/Auth0Client/loginWithRedirect.test.ts
@@ -35,7 +35,6 @@ import {
 } from '../constants';
 import version from '../../src/version';
 
-jest.mock('es-cookie');
 jest.mock('../../src/jwt');
 jest.mock('../../src/worker/token.worker');
 
@@ -59,6 +58,9 @@ describe('Auth0Client', () => {
   const oldWindowLocation = window.location;
 
   beforeEach(() => {
+    jest.spyOn(mockCookies, 'get');
+    jest.spyOn(mockCookies, 'set');
+    jest.spyOn(mockCookies, 'remove');
     // https://www.benmvp.com/blog/mocking-window-location-methods-jest-jsdom/
     delete window.location;
     window.location = Object.defineProperties(
diff --git a/__tests__/transaction-manager.test.ts b/__tests__/transaction-manager.test.ts
index e0846f6..3ec15e7 100644
--- a/__tests__/transaction-manager.test.ts
+++ b/__tests__/transaction-manager.test.ts
@@ -27,9 +27,12 @@ describe('transaction manager', () => {
     jest.resetAllMocks();
   });
 
-  describe('constructor', () => {
+  describe('get', () => {
     it('loads transactions from storage (per key)', () => {
       tm = new TransactionManager(SessionStorage, TEST_CLIENT_ID);
+
+      tm.get();
+
       expect(sessionStorage.getItem).toHaveBeenCalledWith(transactionKey());
     });
   });
@@ -59,7 +62,7 @@ describe('transaction manager', () => {
     });
 
     it('`get` with a transaction should return the transaction', () => {
-      tm.create(transaction);
+      jest.mocked(sessionStorage.getItem).mockReturnValue(transactionJson);
       expect(tm.get()).toMatchObject(transaction);
     });
 
@@ -81,11 +84,11 @@ describe('transaction manager', () => {
       expect(sessionStorage.removeItem).toHaveBeenCalledWith(transactionKey());
     });
   });
-  
- describe('CookieStorage usage', () => {
-    it("`create` saves the transaction in the storage with the provided domain", () => {
+
+  describe('CookieStorage usage', () => {
+    it('`create` saves the transaction in the storage with the provided domain', () => {
       CookieStorage.save = jest.fn();
-      const cookieDomain = "vanity.auth.com";
+      const cookieDomain = 'vanity.auth.com';
       tm = new TransactionManager(CookieStorage, TEST_CLIENT_ID, cookieDomain);
       tm.create(transaction);
 
@@ -99,18 +102,15 @@ describe('transaction manager', () => {
       );
     });
 
-    it("`remove` deletes the transaction in the storage with the provided domain", () => {
+    it('`remove` deletes the transaction in the storage with the provided domain', () => {
       CookieStorage.remove = jest.fn();
-      const cookieDomain = "vanity.auth.com";
+      const cookieDomain = 'vanity.auth.com';
       tm = new TransactionManager(CookieStorage, TEST_CLIENT_ID, cookieDomain);
       tm.remove();
 
-      expect(CookieStorage.remove).toHaveBeenCalledWith(
-        transactionKey(),
-        {
-          cookieDomain: cookieDomain
-        }
-      );
-    });    
-  });  
+      expect(CookieStorage.remove).toHaveBeenCalledWith(transactionKey(), {
+        cookieDomain: cookieDomain
+      });
+    });
+  });
 });