Skip to content

Commit

Permalink
fix(ConnectWalletForm): preserve errors on popup reopen (#638)
Browse files Browse the repository at this point in the history
  • Loading branch information
sidvishnoi authored Oct 2, 2024
1 parent b38ad6d commit e41f23e
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 26 deletions.
2 changes: 1 addition & 1 deletion src/background/services/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,7 +194,7 @@ export class Background {

case 'CONNECT_WALLET':
await this.openPaymentsService.connectWallet(message.payload);
if (message.payload.recurring) {
if (message.payload?.recurring) {
this.scheduleResetOutOfFundsState();
}
return success(undefined);
Expand Down
22 changes: 17 additions & 5 deletions src/background/services/keyAutoAdd.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
ErrorWithKey,
ensureEnd,
errorWithKeyToJSON,
isErrorWithKey,
withResolvers,
type ErrorWithKeyLike,
} from '@/shared/helpers';
import type { Browser, Runtime, Tabs } from 'webextension-polyfill';
import type { WalletAddress } from '@interledger/open-payments';
Expand Down Expand Up @@ -49,7 +51,7 @@ export class KeyAutoAddService {
'publicKey',
'keyId',
]);
this.setConnectState('connecting:key');
this.updateConnectState();
await this.process(info.url, {
publicKey,
keyId,
Expand All @@ -59,7 +61,9 @@ export class KeyAutoAddService {
});
await this.validate(walletAddress.id, keyId);
} catch (error) {
this.setConnectState('error:key');
if (!error.key || !error.key.startsWith('connectWallet_error_')) {
this.updateConnectState(error);
}
throw error;
}
}
Expand Down Expand Up @@ -152,9 +156,17 @@ export class KeyAutoAddService {
}
}

private setConnectState(status: 'connecting:key' | 'error:key' | null) {
const state = status ? { status } : null;
this.storage.setPopupTransientState('connect', () => state);
private updateConnectState(err?: ErrorWithKeyLike | { message: string }) {
if (err) {
this.storage.setPopupTransientState('connect', () => ({
status: 'error:key',
error: isErrorWithKey(err) ? errorWithKeyToJSON(err) : err.message,
}));
} else {
this.storage.setPopupTransientState('connect', () => ({
status: 'connecting:key',
}));
}
}
}

Expand Down
41 changes: 30 additions & 11 deletions src/background/services/openPayments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,13 @@ import { exportJWK, generateEd25519KeyPair } from '@/shared/crypto';
import { bytesToHex } from '@noble/hashes/utils';
import {
ErrorWithKey,
errorWithKeyToJSON,
getWalletInformation,
isErrorWithKey,
withResolvers,
type ErrorWithKeyLike,
} from '@/shared/helpers';
import { AddFundsPayload, ConnectWalletPayload } from '@/shared/messages';
import type { AddFundsPayload, ConnectWalletPayload } from '@/shared/messages';
import {
DEFAULT_RATE_OF_PAY,
MAX_RATE_OF_PAY,
Expand Down Expand Up @@ -332,12 +335,13 @@ export class OpenPaymentsService {
});
}

async connectWallet({
walletAddressUrl,
amount,
recurring,
skipAutoKeyShare,
}: ConnectWalletPayload) {
async connectWallet(params: ConnectWalletPayload | null) {
if (!params) {
this.setConnectState(null);
return;
}
const { walletAddressUrl, amount, recurring, skipAutoKeyShare } = params;

const walletAddress = await getWalletInformation(walletAddressUrl);
const exchangeRates = await getExchangeRates();

Expand Down Expand Up @@ -379,7 +383,8 @@ export class OpenPaymentsService {
);
} catch (error) {
if (
error.message === this.t('connectWallet_error_invalidClient') &&
isErrorWithKey(error) &&
error.key === 'connectWallet_error_invalidClient' &&
!skipAutoKeyShare
) {
// add key to wallet and try again
Expand All @@ -394,11 +399,11 @@ export class OpenPaymentsService {
tabId,
);
} catch (error) {
this.setConnectState('error');
this.updateConnectStateError(error);
throw error;
}
} else {
this.setConnectState('error');
this.updateConnectStateError(error);
throw error;
}
}
Expand Down Expand Up @@ -457,6 +462,9 @@ export class OpenPaymentsService {
amount: transformedAmount,
}).catch((err) => {
if (isInvalidClientError(err)) {
if (intent === InteractionIntent.CONNECT) {
throw new ErrorWithKey('connectWallet_error_invalidClient');
}
const msg = this.t('connectWallet_error_invalidClient');
throw new Error(msg, { cause: err });
}
Expand Down Expand Up @@ -578,10 +586,21 @@ export class OpenPaymentsService {
}
}

private setConnectState(status: 'connecting' | 'error' | null) {
private setConnectState(status: 'connecting' | null) {
const state = status ? { status } : null;
this.storage.setPopupTransientState('connect', () => state);
}
private updateConnectStateError(err: ErrorWithKeyLike | { message: string }) {
this.storage.setPopupTransientState('connect', (state) => {
if (state?.status === 'error:key') {
return state;
}
return {
status: 'error',
error: isErrorWithKey(err) ? errorWithKeyToJSON(err) : err.message,
};
});
}

private async redirectToWelcomeScreen(
tabId: NonNullable<Tabs.Tab['id']>,
Expand Down
48 changes: 43 additions & 5 deletions src/popup/components/ConnectWalletForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ interface ConnectWalletFormProps {
saveValue?: (key: keyof Inputs, val: Inputs[typeof key]) => void;
getWalletInfo: (walletAddressUrl: string) => Promise<WalletAddress>;
connectWallet: (data: ConnectWalletPayload) => Promise<Response>;
clearConnectState: () => Promise<unknown>;
onConnect?: () => void;
}

Expand All @@ -46,6 +47,7 @@ export const ConnectWalletForm = ({
state,
getWalletInfo,
connectWallet,
clearConnectState,
saveValue = () => {},
onConnect = () => {},
}: ConnectWalletFormProps) => {
Expand All @@ -60,16 +62,34 @@ export const ConnectWalletForm = ({
const [recurring, setRecurring] = React.useState<Inputs['recurring']>(
defaultValues.recurring || false,
);
const [autoKeyShareFailed, setAutoKeyShareFailed] = React.useState(false);
const [autoKeyShareFailed, setAutoKeyShareFailed] = React.useState(
isAutoKeyAddFailed(state),
);

const resetState = React.useCallback(async () => {
await clearConnectState();
setErrors((_) => ({ ..._, keyPair: '', connect: '' }));
setAutoKeyShareFailed(false);
}, [clearConnectState]);

const [walletAddressInfo, setWalletAddressInfo] =
React.useState<WalletAddress | null>(null);

const [errors, setErrors] = React.useState({
walletAddressUrl: '',
amount: '',
keyPair: '',
connect: '',
keyPair:
state?.status === 'error:key'
? isErrorWithKey(state.error)
? t(state.error)
: state.error
: '',
connect:
state?.status === 'error'
? isErrorWithKey(state.error)
? t(state.error)
: state.error
: '',
});
const [isValidating, setIsValidating] = React.useState({
walletAddressUrl: false,
Expand Down Expand Up @@ -239,6 +259,7 @@ export const ConnectWalletForm = ({
autoComplete="on"
spellCheck={false}
enterKeyHint="go"
readOnly={isSubmitting}
onPaste={async (ev) => {
const input = ev.currentTarget;
let value = ev.clipboardData.getData('text');
Expand All @@ -255,6 +276,7 @@ export const ConnectWalletForm = ({
}
}
const ok = await handleWalletAddressUrlChange(value, input);
resetState();
if (ok) document.getElementById('connectAmount')?.focus();
}}
onBlur={async (ev) => {
Expand All @@ -265,6 +287,7 @@ export const ConnectWalletForm = ({
}
}
await handleWalletAddressUrlChange(value, ev.currentTarget);
resetState();
}}
/>

Expand All @@ -286,7 +309,7 @@ export const ConnectWalletForm = ({
placeholder="5.00"
className="max-w-32"
defaultValue={amount}
readOnly={!walletAddressInfo?.assetCode}
readOnly={!walletAddressInfo?.assetCode || isSubmitting}
addOn={<span className="text-weak">{currencySymbol.symbol}</span>}
aria-invalid={!!errors.amount}
aria-describedby={errors.amount}
Expand Down Expand Up @@ -325,7 +348,7 @@ export const ConnectWalletForm = ({
details: errors.keyPair,
whyText: t('connectWallet_error_failedAutoKeyAddWhy'),
}}
hideError={autoKeyShareFailed}
hideError={!errors.keyPair}
text={t('connectWallet_label_publicKey')}
learnMoreText={t('connectWallet_text_publicKeyLearnMore')}
publicKey={publicKey}
Expand Down Expand Up @@ -408,6 +431,21 @@ const ManualKeyPairNeeded: React.FC<{
);
};

function isAutoKeyAddFailed(state: PopupTransientState['connect']) {
if (state?.status === 'error') {
return (
isErrorWithKey(state.error) &&
state.error.key !== 'connectWallet_error_tabClosed'
);
} else if (state?.status === 'error:key') {
return (
isErrorWithKey(state.error) &&
state.error.key.startsWith('connectWalletKeyService_error_')
);
}
return false;
}

const Footer: React.FC<{
text: string;
learnMoreText: string;
Expand Down
1 change: 1 addition & 0 deletions src/popup/pages/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export const Component = () => {
// But we reload it, as it's open all-time when running E2E tests
window.location.reload();
}}
clearConnectState={() => message.send('CONNECT_WALLET', null)}
/>
);
}
Expand Down
2 changes: 1 addition & 1 deletion src/shared/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export type PopupToBackgroundMessage = {
output: PopupState;
};
CONNECT_WALLET: {
input: ConnectWalletPayload;
input: null | ConnectWalletPayload;
output: void;
};
RECONNECT_WALLET: {
Expand Down
8 changes: 5 additions & 3 deletions src/shared/types.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import type { WalletAddress } from '@interledger/open-payments/dist/types';
import type { Tabs } from 'webextension-polyfill';
import type { ErrorWithKeyLike } from './helpers';

/** Bigint amount, before transformation with assetScale */
export type AmountValue = string;
Expand Down Expand Up @@ -109,9 +110,10 @@ export type PopupTabInfo = {
};

export type PopupTransientState = Partial<{
connect: Partial<{
status: 'connecting' | 'connecting:key' | 'error' | 'error:key' | null;
}> | null;
connect:
| null
| { status: 'connecting' | 'connecting:key' }
| { status: 'error' | 'error:key'; error: string | ErrorWithKeyLike };
}>;

export type PopupStore = Omit<
Expand Down

0 comments on commit e41f23e

Please sign in to comment.