From 795f604b6b92174c645c0cd9d9680d82966c06d6 Mon Sep 17 00:00:00 2001
From: Sid Vishnoi <8426945+sidvishnoi@users.noreply.github.com>
Date: Fri, 4 Oct 2024 18:21:46 +0530
Subject: [PATCH] feat(ConnectWalletForm): ask consent for automatic key
 addition (#637)

---
 src/_locales/en/messages.json              | 29 +++++--
 src/background/services/keyAutoAdd.ts      |  9 ++
 src/background/services/openPayments.ts    | 21 ++++-
 src/popup/components/ConnectWalletForm.tsx | 99 ++++++++++++++++------
 src/popup/pages/Settings.tsx               |  2 +
 src/shared/messages.ts                     |  3 +-
 tests/e2e/connectAutoKeyTestWallet.spec.ts | 10 ++-
 7 files changed, 134 insertions(+), 39 deletions(-)

diff --git a/src/_locales/en/messages.json b/src/_locales/en/messages.json
index 979f21a6..cda65151 100755
--- a/src/_locales/en/messages.json
+++ b/src/_locales/en/messages.json
@@ -128,13 +128,6 @@
   "connectWallet_action_connect": {
     "message": "Connect"
   },
-  "connectWallet_text_footerNotice": {
-    "message": "We'll automatically connect with your wallet provider."
-  },
-  "connectWallet_text_footerNoticeLearnMore": {
-    "message": "Learn more",
-    "description": "Learn more about how this works"
-  },
   "connectWallet_error_urlRequired": {
     "message": "Wallet address is required."
   },
@@ -172,9 +165,31 @@
   "connectWallet_error_grantRejected": {
     "message": "Connect wallet cancelled. You rejected the request."
   },
+  "connectWalletKeyService_text_consentP1": {
+    "message": "We'll automatically connect with your wallet provider."
+  },
+  "connectWalletKeyService_text_consentLearnMore": {
+    "message": "Learn more",
+    "description": "Learn more about how this works"
+  },
+  "connectWalletKeyService_text_consentP2": {
+    "message": "By agreeing, you provide us consent to automatically access your wallet to securely add a key."
+  },
+  "connectWalletKeyService_text_consentP3": {
+    "message": "Please note, this process does not involve accessing or handling your funds."
+  },
+  "connectWalletKeyService_label_consentAccept": {
+    "message": "Agree"
+  },
+  "connectWalletKeyService_label_consentDecline": {
+    "message": "Decline"
+  },
   "connectWalletKeyService_error_notImplemented": {
     "message": "Automatic key addition is not implemented for given wallet provider yet."
   },
+  "connectWalletKeyService_error_noConsent": {
+    "message": "You declined consent for automatic key addition."
+  },
   "connectWalletKeyService_error_failed": {
     "message": "Automatic key addition failed at step “$STEP_ID$” with message “$MESSAGE$”.",
     "placeholders": {
diff --git a/src/background/services/keyAutoAdd.ts b/src/background/services/keyAutoAdd.ts
index a3d2075a..302f764b 100644
--- a/src/background/services/keyAutoAdd.ts
+++ b/src/background/services/keyAutoAdd.ts
@@ -172,6 +172,15 @@ export class KeyAutoAddService {
       }));
     }
   }
+
+  static supports(walletAddress: WalletAddress): boolean {
+    try {
+      walletAddressToProvider(walletAddress);
+      return true;
+    } catch {
+      return false;
+    }
+  }
 }
 
 export function walletAddressToProvider(walletAddress: WalletAddress): {
diff --git a/src/background/services/openPayments.ts b/src/background/services/openPayments.ts
index 1f7ecec0..d7a23725 100644
--- a/src/background/services/openPayments.ts
+++ b/src/background/services/openPayments.ts
@@ -340,7 +340,13 @@ export class OpenPaymentsService {
       this.setConnectState(null);
       return;
     }
-    const { walletAddressUrl, amount, recurring, skipAutoKeyShare } = params;
+    const {
+      walletAddressUrl,
+      amount,
+      recurring,
+      autoKeyAdd,
+      autoKeyAddConsent,
+    } = params;
 
     const walletAddress = await getWalletInformation(walletAddressUrl);
     const exchangeRates = await getExchangeRates();
@@ -385,8 +391,19 @@ export class OpenPaymentsService {
       if (
         isErrorWithKey(error) &&
         error.key === 'connectWallet_error_invalidClient' &&
-        !skipAutoKeyShare
+        autoKeyAdd
       ) {
+        if (!KeyAutoAddService.supports(walletAddress)) {
+          this.updateConnectStateError(error);
+          throw new ErrorWithKey(
+            'connectWalletKeyService_error_notImplemented',
+          );
+        }
+        if (!autoKeyAddConsent) {
+          this.updateConnectStateError(error);
+          throw new ErrorWithKey('connectWalletKeyService_error_noConsent');
+        }
+
         // add key to wallet and try again
         try {
           const tabId = await this.addPublicKeyToWallet(walletAddress);
diff --git a/src/popup/components/ConnectWalletForm.tsx b/src/popup/components/ConnectWalletForm.tsx
index 2ff01178..26694dba 100644
--- a/src/popup/components/ConnectWalletForm.tsx
+++ b/src/popup/components/ConnectWalletForm.tsx
@@ -27,6 +27,7 @@ interface Inputs {
   walletAddressUrl: string;
   amount: string;
   recurring: boolean;
+  autoKeyAddConsent: boolean;
 }
 
 type ErrorInfo = { message: string; info?: ErrorWithKeyLike };
@@ -69,6 +70,10 @@ export const ConnectWalletForm = ({
   const [autoKeyShareFailed, setAutoKeyShareFailed] = React.useState(
     isAutoKeyAddFailed(state),
   );
+  const [showConsent, setShowConsent] = React.useState(false);
+  const autoKeyAddConsent = React.useRef<boolean>(
+    defaultValues.autoKeyAddConsent || false,
+  );
 
   const resetState = React.useCallback(async () => {
     await clearConnectState();
@@ -162,8 +167,8 @@ export const ConnectWalletForm = ({
     [saveValue, currencySymbol, toErrorInfo],
   );
 
-  const handleSubmit = async (ev: React.FormEvent<HTMLFormElement>) => {
-    ev.preventDefault();
+  const handleSubmit = async (ev?: React.FormEvent<HTMLFormElement>) => {
+    ev?.preventDefault();
 
     const errWalletAddressUrl = validateWalletAddressUrl(walletAddressUrl);
     const errAmount = validateAmount(amount, currencySymbol.symbol);
@@ -188,7 +193,8 @@ export const ConnectWalletForm = ({
         walletAddressUrl: toWalletAddressUrl(walletAddressUrl),
         amount,
         recurring,
-        skipAutoKeyShare,
+        autoKeyAdd: !skipAutoKeyShare,
+        autoKeyAddConsent: autoKeyAddConsent.current,
       });
       if (res.success) {
         onConnect();
@@ -196,6 +202,10 @@ export const ConnectWalletForm = ({
         if (isErrorWithKey(res.error)) {
           const error = res.error;
           if (error.key.startsWith('connectWalletKeyService_error_')) {
+            if (error.key === 'connectWalletKeyService_error_noConsent') {
+              setShowConsent(true);
+              return;
+            }
             setErrors((prev) => ({ ...prev, keyPair: toErrorInfo(error) }));
           } else {
             setErrors((prev) => ({ ...prev, connect: toErrorInfo(error) }));
@@ -225,6 +235,24 @@ export const ConnectWalletForm = ({
     }
   }, [defaultValues.walletAddressUrl, handleWalletAddressUrlChange]);
 
+  if (showConsent) {
+    return (
+      <AutoKeyAddConsent
+        onAccept={() => {
+          autoKeyAddConsent.current = true;
+          // saveValue('autoKeyAddConsent', true);
+          setShowConsent(false);
+          handleSubmit();
+        }}
+        onDecline={() => {
+          const error = errorWithKey('connectWalletKeyService_error_noConsent');
+          setErrors((prev) => ({ ...prev, keyPair: toErrorInfo(error) }));
+          setShowConsent(false);
+        }}
+      />
+    );
+  }
+
   return (
     <form
       data-testid="connect-wallet-form"
@@ -380,13 +408,46 @@ export const ConnectWalletForm = ({
         >
           {t('connectWallet_action_connect')}
         </Button>
+      </div>
+    </form>
+  );
+};
 
-        {!errors.keyPair && !autoKeyShareFailed && (
-          <Footer
-            text={t('connectWallet_text_footerNotice')}
-            learnMoreText={t('connectWallet_text_footerNoticeLearnMore')}
-          />
-        )}
+const AutoKeyAddConsent: React.FC<{
+  onAccept: () => void;
+  onDecline: () => void;
+}> = ({ onAccept, onDecline }) => {
+  const t = useTranslation();
+  return (
+    <form
+      className="space-y-4 text-center"
+      data-testid="connect-wallet-auto-key-consent"
+    >
+      <p className="text-lg leading-snug text-weak">
+        {t('connectWalletKeyService_text_consentP1')}{' '}
+        <a
+          hidden
+          href="https://webmonetization.org"
+          className="text-primary hover:underline"
+          target="_blank"
+          rel="noreferrer"
+        >
+          {t('connectWalletKeyService_text_consentLearnMore')}
+        </a>
+      </p>
+
+      <div className="space-y-2 pt-12 text-medium">
+        <p>{t('connectWalletKeyService_text_consentP2')}</p>
+        <p>{t('connectWalletKeyService_text_consentP3')}</p>
+      </div>
+
+      <div className="mx-auto flex w-3/4 justify-around gap-4">
+        <Button onClick={onAccept}>
+          {t('connectWalletKeyService_label_consentAccept')}
+        </Button>
+        <Button onClick={onDecline} variant="destructive">
+          {t('connectWalletKeyService_label_consentDecline')}
+        </Button>
       </div>
     </form>
   );
@@ -462,30 +523,12 @@ function isAutoKeyAddFailed(state: PopupTransientState['connect']) {
 function canRetryAutoKeyAdd(err?: ErrorInfo['info']) {
   if (!err) return false;
   return (
+    err.key === 'connectWalletKeyService_error_noConsent' ||
     err.cause?.key === 'connectWalletKeyService_error_timeoutLogin' ||
     err.cause?.key === 'connectWalletKeyService_error_accountNotFound'
   );
 }
 
-const Footer: React.FC<{
-  text: string;
-  learnMoreText: string;
-}> = ({ text, learnMoreText }) => {
-  return (
-    <p className="text-center text-xs text-weak">
-      {text}{' '}
-      <a
-        href="https://webmonetization.org"
-        className="text-primary hover:underline"
-        target="_blank"
-        rel="noreferrer"
-      >
-        {learnMoreText}
-      </a>
-    </p>
-  );
-};
-
 function validateWalletAddressUrl(value: string): null | ErrorWithKeyLike {
   if (!value) {
     return errorWithKey('connectWallet_error_urlRequired');
diff --git a/src/popup/pages/Settings.tsx b/src/popup/pages/Settings.tsx
index 5e10314a..1c129d58 100644
--- a/src/popup/pages/Settings.tsx
+++ b/src/popup/pages/Settings.tsx
@@ -22,6 +22,8 @@ export const Component = () => {
           amount: localStorage?.getItem('connect.amount') || undefined,
           walletAddressUrl:
             localStorage?.getItem('connect.walletAddressUrl') || undefined,
+          autoKeyAddConsent:
+            localStorage?.getItem('connect.autoKeyAddConsent') === 'true',
         }}
         saveValue={(key, val) => {
           localStorage?.setItem(`connect.${key}`, val.toString());
diff --git a/src/shared/messages.ts b/src/shared/messages.ts
index fc0e8ccc..a3670e6d 100644
--- a/src/shared/messages.ts
+++ b/src/shared/messages.ts
@@ -93,7 +93,8 @@ export interface ConnectWalletPayload {
   walletAddressUrl: string;
   amount: string;
   recurring: boolean;
-  skipAutoKeyShare: boolean;
+  autoKeyAdd: boolean;
+  autoKeyAddConsent: boolean | null;
 }
 
 export interface AddFundsPayload {
diff --git a/tests/e2e/connectAutoKeyTestWallet.spec.ts b/tests/e2e/connectAutoKeyTestWallet.spec.ts
index 38e510d9..6b3d7538 100644
--- a/tests/e2e/connectAutoKeyTestWallet.spec.ts
+++ b/tests/e2e/connectAutoKeyTestWallet.spec.ts
@@ -42,9 +42,17 @@ test('Connect to test wallet with automatic key addition when not logged-in to w
     await page.close();
   });
 
-  page = await test.step('shows login page', async () => {
+  await test.step('asks for key-add consent', async () => {
     await connectButton.click();
+    await popup.waitForSelector(
+      `[data-testid="connect-wallet-auto-key-consent"]`,
+    );
 
+    expect(popup.getByTestId('connect-wallet-auto-key-consent')).toBeVisible();
+    await popup.getByRole('button', { name: 'Accept' }).click();
+  });
+
+  page = await test.step('shows login page', async () => {
     const openedPage = await context.waitForEvent('page', {
       predicate: (page) => page.url().startsWith(loginPageUrl),
       timeout: 3 * 1000,