Repository: interledger/web-monetization-extension
Pull request #631 (2024-09-26 11:47:06)

refactor(bg/openPayments): use `Promise.withResolvers`
Extracted from https://github.com/interledger/web-monetization-extension/pull/630

Reduced nesting in code. View diff with ignore-whitespace.

github-actions[bot]: <!-- __WM_EXTENSION_BUILD_PREVIEWS__ -->
<h2>Extension builds preview</h2>

<table role="table">
<thead>
  <tr>
    <th>Name</th>
    <th>Link</th>
  </tr>
</thead>
<tbody>
  <tr>
    <td align="center">Latest commit</td>
    <td align="center">1559718ad0e1c29c285067c02354768df1cce62e</td>
  </tr>
  <tr>
    <td align="center">Latest job logs</td>
    <td align="center"><a href="https://github.com/interledger/web-monetization-extension/actions/runs/11051568775">Run #11051568775</a></td>
  </tr>
  <tr><td align="center"><img src="https://img.shields.io/badge/success-3fb950?style=for-the-badge&label=Chrome (473.65KB)" alt="Badge" /></td><td align="center"><a href="https://github.com/interledger/web-monetization-extension/suites/28879282720/artifacts/1981998070">Download</a></td></tr><tr><td align="center"><img src="https://img.shields.io/badge/success-3fb950?style=for-the-badge&label=Firefox (473.68KB)" alt="Badge" /></td><td align="center"><a href="https://github.com/interledger/web-monetization-extension/suites/28879282720/artifacts/1981997859">Download</a></td></tr>
</tbody>
</table>

Patch:
diff --git a/src/background/services/openPayments.ts b/src/background/services/openPayments.ts
index cb62cc5..a248750 100644
--- a/src/background/services/openPayments.ts
+++ b/src/background/services/openPayments.ts
@@ -25,7 +25,11 @@ import type { Browser, Tabs } from 'webextension-polyfill';
 import { getExchangeRates, getRateOfPay, toAmount } from '../utils';
 import { exportJWK, generateEd25519KeyPair } from '@/shared/crypto';
 import { bytesToHex } from '@noble/hashes/utils';
-import { ErrorWithKey, getWalletInformation } from '@/shared/helpers';
+import {
+  ErrorWithKey,
+  getWalletInformation,
+  withResolvers,
+} from '@/shared/helpers';
 import { AddFundsPayload, ConnectWalletPayload } from '@/shared/messages';
 import {
   DEFAULT_RATE_OF_PAY,
@@ -609,48 +613,50 @@ export class OpenPaymentsService {
   }
 
   private async getInteractionInfo(url: string): Promise<InteractionParams> {
-    return await new Promise((resolve, reject) => {
-      this.browser.tabs.create({ url }).then((tab) => {
-        if (!tab.id) return;
-        const tabCloseListener: TabRemovedCallback = (tabId) => {
-          if (tabId !== tab.id) return;
+    const { resolve, reject, promise } = withResolvers<InteractionParams>();
+
+    const tab = await this.browser.tabs.create({ url });
+    if (!tab.id) {
+      reject(new Error('Could not create tab'));
+      return promise;
+    }
+
+    const tabCloseListener: TabRemovedCallback = (tabId) => {
+      if (tabId !== tab.id) return;
 
+      this.browser.tabs.onRemoved.removeListener(tabCloseListener);
+      reject(new ErrorWithKey('connectWallet_error_tabClosed'));
+    };
+
+    const getInteractionInfo: TabUpdateCallback = async (tabId, changeInfo) => {
+      if (tabId !== tab.id) return;
+      try {
+        const tabUrl = new URL(changeInfo.url || '');
+        const interactRef = tabUrl.searchParams.get('interact_ref');
+        const hash = tabUrl.searchParams.get('hash');
+        const result = tabUrl.searchParams.get('result');
+
+        if (
+          (interactRef && hash) ||
+          result === 'grant_rejected' ||
+          result === 'grant_invalid'
+        ) {
+          this.browser.tabs.onUpdated.removeListener(getInteractionInfo);
           this.browser.tabs.onRemoved.removeListener(tabCloseListener);
-          reject(new ErrorWithKey('connectWallet_error_tabClosed'));
-        };
-
-        const getInteractionInfo: TabUpdateCallback = async (
-          tabId,
-          changeInfo,
-        ) => {
-          if (tabId !== tab.id) return;
-          try {
-            const tabUrl = new URL(changeInfo.url || '');
-            const interactRef = tabUrl.searchParams.get('interact_ref');
-            const hash = tabUrl.searchParams.get('hash');
-            const result = tabUrl.searchParams.get('result');
-
-            if (
-              (interactRef && hash) ||
-              result === 'grant_rejected' ||
-              result === 'grant_invalid'
-            ) {
-              this.browser.tabs.onUpdated.removeListener(getInteractionInfo);
-              this.browser.tabs.onRemoved.removeListener(tabCloseListener);
-            }
-
-            if (interactRef && hash) {
-              resolve({ interactRef, hash, tabId });
-            }
-          } catch {
-            /* do nothing */
-          }
-        };
-
-        this.browser.tabs.onRemoved.addListener(tabCloseListener);
-        this.browser.tabs.onUpdated.addListener(getInteractionInfo);
-      });
-    });
+        }
+
+        if (interactRef && hash) {
+          resolve({ interactRef, hash, tabId });
+        }
+      } catch {
+        /* do nothing */
+      }
+    };
+
+    this.browser.tabs.onRemoved.addListener(tabCloseListener);
+    this.browser.tabs.onUpdated.addListener(getInteractionInfo);
+
+    return promise;
   }
 
   async disconnectWallet() {
diff --git a/src/shared/helpers.ts b/src/shared/helpers.ts
index af490c9..3c146a9 100644
--- a/src/shared/helpers.ts
+++ b/src/shared/helpers.ts
@@ -293,6 +293,20 @@ export const removeQueryParams = (urlString: string) => {
   return url.origin + url.pathname;
 };
 
+/**
+ * Polyfill for `Promise.withResolvers()`
+ */
+export function withResolvers<T>() {
+  let resolve: (value: T | PromiseLike<T>) => void;
+  let reject: (reason?: any) => void;
+  const promise = new Promise<T>((res, rej) => {
+    resolve = res;
+    reject = rej;
+  });
+  // @ts-expect-error we know TypeScript!
+  return { resolve, reject, promise };
+}
+
 export const isOkState = (state: Storage['state']) => {
   return Object.values(state).every((value) => value === false);
 };


Test patch:
diff --git a/src/shared/helpers.test.ts b/src/shared/helpers.test.ts
index 6c43a9e..7a9a867 100644
--- a/src/shared/helpers.test.ts
+++ b/src/shared/helpers.test.ts
@@ -4,6 +4,7 @@ import {
   objectEquals,
   removeQueryParams,
   getNextOccurrence,
+  withResolvers,
 } from './helpers';
 
 describe('objectEquals', () => {
@@ -34,6 +35,22 @@ describe('removeQueryParams', () => {
   });
 });
 
+describe('withResolvers', () => {
+  it('resolves', async () => {
+    const r = withResolvers<boolean>();
+    r.resolve(true);
+    r.reject(false);
+    await expect(r.promise).resolves.toBe(true);
+  });
+
+  it('rejects', async () => {
+    const r = withResolvers<boolean>();
+    r.reject(false);
+    r.resolve(true);
+    await expect(r.promise).rejects.toBe(false);
+  });
+});
+
 describe('isOkState', () => {
   it('should return true if no state is set', () => {
     expect(isOkState({})).toBe(true);