Skip to content

Commit

Permalink
add errorWithKey helpers
Browse files Browse the repository at this point in the history
  • Loading branch information
sidvishnoi committed Sep 23, 2024
1 parent f666a00 commit 8c6f887
Show file tree
Hide file tree
Showing 7 changed files with 120 additions and 51 deletions.
3 changes: 3 additions & 0 deletions src/_locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@
"connectWallet_error_invalidClient": {
"message": "Failed to connect. Please make sure you have added the key to the correct wallet address."
},
"connectWalletKeyService_error_notImplemented": {
"message": "Automatic key addition is not not implemented for give wallet provider yet"
},
"allInvalidLinks_state_text": {
"message": "At the moment, you can not pay this website."
}
Expand Down
5 changes: 5 additions & 0 deletions src/background/services/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import {
failure,
getNextOccurrence,
getWalletInformation,
isErrorWithKey,
success,
} from '@/shared/helpers';
import { OpenPaymentsClientError } from '@interledger/open-payments/dist/client/error';
Expand Down Expand Up @@ -250,6 +251,10 @@ export class Background {
return;
}
} catch (e) {
if (isErrorWithKey(e)) {
this.logger.error(message.action, e);
return failure({ key: e.key, substitutions: e.substitutions });
}
if (e instanceof OpenPaymentsClientError) {
this.logger.error(message.action, e.message, e.description);
return failure(
Expand Down
5 changes: 2 additions & 3 deletions src/background/services/openPayments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import type { Tabs } from 'webextension-polyfill';
import { getExchangeRates, getRateOfPay, toAmount } from '../utils';
import { exportJWK, generateEd25519KeyPair } from '@/shared/crypto';
import { bytesToHex } from '@noble/hashes/utils';
import { getWalletInformation } from '@/shared/helpers';
import { ErrorWithKey, getWalletInformation } from '@/shared/helpers';
import { AddFundsPayload, ConnectWalletPayload } from '@/shared/messages';
import {
DEFAULT_RATE_OF_PAY,
Expand Down Expand Up @@ -505,8 +505,7 @@ export class OpenPaymentsService {
}

private async addPublicKeyToWallet(_walletAddress: WalletAddress) {
const msg = `Automatic key addition is not not implemented for give wallet provider yet`;
throw new Error(`ADD_PUBLIC_KEY_TO_WALLET:${msg}`);
throw new ErrorWithKey('connectWalletKeyService_error_notImplemented');
}

private async redirectToWelcomeScreen(
Expand Down
75 changes: 38 additions & 37 deletions src/popup/components/ConnectWalletForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,13 @@ import {
toWalletAddressUrl,
} from '@/popup/lib/utils';
import { useTranslation } from '@/popup/lib/context';
import { cn, type TranslationKeys } from '@/shared/helpers';
import {
cn,
errorWithKey,
isErrorWithKey,
type IErrorWithKey,
type TranslationKeys,
} from '@/shared/helpers';
import type { WalletAddress } from '@interledger/open-payments';
import type { ConnectWalletPayload, Response } from '@/shared/messages';

Expand Down Expand Up @@ -97,17 +103,13 @@ export const ConnectWalletForm = ({
return;
}

const errCodeWalletAddressUrl = validateWalletAddressUrl(walletAddressUrl);
const errCodeAmount = validateAmount(amount);
if (errCodeAmount || errCodeWalletAddressUrl) {
const errWalletAddressUrl = validateWalletAddressUrl(walletAddressUrl);
const errAmount = validateAmount(amount, currencySymbol.symbol);
if (errAmount || errWalletAddressUrl) {
setErrors((_) => ({
..._,
walletAddressUrl: errCodeWalletAddressUrl && t(errCodeWalletAddressUrl),
amount: errCodeAmount
? errCodeAmount === 'connectWallet_error_amountMinimum'
? t(errCodeAmount, [`${currencySymbol.symbol}${amount}`])
: t(errCodeAmount)
: '',
walletAddressUrl: errWalletAddressUrl ? t(errWalletAddressUrl) : '',
amount: errAmount && t(errAmount),
}));
return;
}
Expand All @@ -129,9 +131,13 @@ export const ConnectWalletForm = ({
if (res.success) {
onConnect();
} else {
if (res.message.startsWith('ADD_PUBLIC_KEY_TO_WALLET:')) {
const message = res.message.replace('ADD_PUBLIC_KEY_TO_WALLET:', '');
setErrors((_) => ({ ..._, keyPair: message }));
if (isErrorWithKey(res.error)) {
const error = res.error;
if (error.key === 'connectWalletKeyService_error_notImplemented') {
setErrors((_) => ({ ..._, keyPair: t(error) }));
} else {
throw new Error(error.key);
}
} else {
throw new Error(res.message);
}
Expand Down Expand Up @@ -202,12 +208,8 @@ export const ConnectWalletForm = ({
setWalletAddressInfo(null);
setWalletAddressUrl(value);

const errorCode = validateWalletAddressUrl(value);
let error: string = errorCode;
if (errorCode) {
error = t(errorCode);
}
setErrors((_) => ({ ..._, walletAddressUrl: error }));
const error = validateWalletAddressUrl(value);
setErrors((_) => ({ ..._, walletAddressUrl: error ? t(error) : '' }));
if (!error) {
await getWalletInformation(value);
}
Expand Down Expand Up @@ -242,16 +244,8 @@ export const ConnectWalletForm = ({
return;
}

const errorCode = validateAmount(value);
let error: string = errorCode;
if (errorCode) {
if (errorCode === 'connectWallet_error_amountMinimum') {
error = t(errorCode, [`${currencySymbol}${Number(value)}`]);
} else {
error = t(errorCode);
}
}
setErrors((_) => ({ ..._, amount: error }));
const error = validateAmount(value, currencySymbol.symbol);
setErrors((_) => ({ ..._, amount: error ? t(error) : '' }));

const amountValue = formatNumber(+value, currencySymbol.scale);
if (!error) {
Expand Down Expand Up @@ -389,34 +383,41 @@ type ErrorCodeAmount = Extract<
`connectWallet_error_amount${string}`
>;

function validateWalletAddressUrl(value: string): '' | ErrorCodeUrl {
function validateWalletAddressUrl(
value: string,
): '' | IErrorWithKey<ErrorCodeUrl> {
if (!value) {
return 'connectWallet_error_urlRequired';
return errorWithKey('connectWallet_error_urlRequired');
}
let url: URL;
try {
url = new URL(toWalletAddressUrl(value));
} catch {
return 'connectWallet_error_urlInvalidUrl';
return errorWithKey('connectWallet_error_urlInvalidUrl');
}

if (url.protocol !== 'https:') {
return 'connectWallet_error_urlInvalidNotHttps';
return errorWithKey('connectWallet_error_urlInvalidNotHttps');
}

return '';
}

function validateAmount(value: string): '' | ErrorCodeAmount {
function validateAmount(
value: string,
currencySymbol: string,
): '' | IErrorWithKey<ErrorCodeAmount> {
if (!value) {
return 'connectWallet_error_amountRequired';
return errorWithKey('connectWallet_error_amountRequired');
}
const val = Number(value);
if (Number.isNaN(val)) {
return 'connectWallet_error_amountInvalidNumber';
return errorWithKey('connectWallet_error_amountInvalidNumber', [
`${currencySymbol}${value}`,
]);
}
if (val <= 0) {
return 'connectWallet_error_amountMinimum';
return errorWithKey('connectWallet_error_amountMinimum');
}
return '';
}
Expand Down
11 changes: 9 additions & 2 deletions src/popup/lib/context.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import React, { type PropsWithChildren } from 'react';
import type { Browser } from 'webextension-polyfill';
import { tFactory, type Translation } from '@/shared/helpers';
import {
tFactory,
type IErrorWithKey,
type Translation,
} from '@/shared/helpers';
import type { DeepNonNullable, PopupStore } from '@/shared/types';
import {
BACKGROUND_TO_POPUP_CONNECTION_NAME as CONNECTION_NAME,
Expand Down Expand Up @@ -153,7 +157,10 @@ export const BrowserContextProvider = ({
// #endregion

// #region Translation
const TranslationContext = React.createContext<Translation>((v: string) => v);
const TranslationContext = React.createContext<Translation>(
(v: string | IErrorWithKey) =>
typeof v === 'string' ? v : v.key,
);

export const useTranslation = () => React.useContext(TranslationContext);

Expand Down
70 changes: 61 additions & 9 deletions src/shared/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { parse, toSeconds } from 'iso8601-duration';
import type { Browser } from 'webextension-polyfill';
import type { Storage, RepeatingInterval, AmountValue } from './types';

export type TranslationKeys =
keyof typeof import('../_locales/en/messages.json');

export const cn = (...inputs: CxOptions) => {
return twMerge(cx(inputs));
};
Expand Down Expand Up @@ -60,16 +63,56 @@ export const getWalletInformation = async (
return json;
};

/**
* Error object with key and substitutions based on `_locales/[lang]/messages.json`
*/
export interface IErrorWithKey<T extends TranslationKeys = TranslationKeys> {
key: Extract<TranslationKeys, T>;
// Could be empty, but required for checking if an object follows this interface
substitutions: string[];
}

export class ErrorWithKey<T extends TranslationKeys = TranslationKeys>
extends Error
implements IErrorWithKey<T>
{
constructor(
public readonly key: IErrorWithKey<T>['key'],
public readonly substitutions: IErrorWithKey<T>['substitutions'] = [],
) {
super(key);
}
}

/**
* Same as {@linkcode ErrorWithKey} but creates plain object instead of Error
* instance.
* Easier than creating object ourselves, but more performant than Error.
*/
export const errorWithKey = <T extends TranslationKeys = TranslationKeys>(
key: IErrorWithKey<T>['key'],
substitutions: IErrorWithKey<T>['substitutions'] = [],
) => ({ key, substitutions });

export const isErrorWithKey = (err: any): err is IErrorWithKey => {
return (
err instanceof ErrorWithKey ||
(typeof err.key === 'string' && Array.isArray(err.substitutions))
);
};

export const success = <TPayload = undefined>(
payload: TPayload,
): SuccessResponse<TPayload> => ({
success: true,
payload,
});

export const failure = (message: string) => ({
success: false,
message,
export const failure = (message: string | IErrorWithKey) => ({
success: false as const,
...(typeof message === 'string'
? { message }
: { error: message, message: message.key }),
});

export const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
Expand Down Expand Up @@ -206,19 +249,28 @@ export function bigIntMax<T extends bigint | AmountValue>(a: T, b: T): T {
return BigInt(a) > BigInt(b) ? a : b;
}

export type TranslationKeys =
keyof typeof import('../_locales/en/messages.json');

export type Translation = ReturnType<typeof tFactory>;
export function tFactory(browser: Pick<Browser, 'i18n'>) {
/**
* Helper over calling cumbersome `this.browser.i18n.getMessage(key)` with
* added benefit that it type-checks if key exists in message.json
*/
return <T extends TranslationKeys>(
function t<T extends TranslationKeys>(
key: T,
substitutions?: string | string[],
) => browser.i18n.getMessage(key, substitutions);
substitutions?: string[],
): string;
function t<T extends TranslationKeys>(err: IErrorWithKey<T>): string;
function t<T extends TranslationKeys>(
key: T | IErrorWithKey<T>,
substitutions?: string[],
): string {
if (typeof key === 'string') {
return browser.i18n.getMessage(key, substitutions);
}
const err = key;
return browser.i18n.getMessage(err.key, err.substitutions);
}
return t;
}

type Primitive = string | number | boolean | null | undefined;
Expand Down
2 changes: 2 additions & 0 deletions src/shared/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type {
} from '@interledger/open-payments';
import type { Browser } from 'webextension-polyfill';
import type { AmountValue, Storage } from '@/shared/types';
import type { IErrorWithKey } from '@/shared/helpers';
import type { PopupState } from '@/popup/lib/context';

// #region MessageManager
Expand All @@ -15,6 +16,7 @@ export interface SuccessResponse<TPayload = void> {
export interface ErrorResponse {
success: false;
message: string;
error?: IErrorWithKey;
}

export type Response<TPayload = void> =
Expand Down

0 comments on commit 8c6f887

Please sign in to comment.