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,