Skip to content

Commit

Permalink
refactor(popup): extract InputAmount from ConnectWalletForm (#672)
Browse files Browse the repository at this point in the history
  • Loading branch information
sidvishnoi authored Oct 16, 2024
1 parent 77e86ed commit e5f238c
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 87 deletions.
103 changes: 22 additions & 81 deletions src/popup/components/ConnectWalletForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,8 @@ import { Switch } from '@/popup/components/ui/Switch';
import { Code } from '@/popup/components/ui/Code';
import { ErrorMessage } from '@/popup/components/ErrorMessage';
import { LoadingSpinner } from '@/popup/components/LoadingSpinner';
import {
charIsNumber,
formatNumber,
getCurrencySymbol,
toWalletAddressUrl,
} from '@/popup/lib/utils';
import { InputAmount, validateAmount } from '@/popup/components/InputAmount';
import { toWalletAddressUrl } from '@/popup/lib/utils';
import { useTranslation } from '@/popup/lib/context';
import {
cn,
Expand Down Expand Up @@ -107,11 +103,6 @@ export const ConnectWalletForm = ({
state?.status?.startsWith('connecting') || false,
);

const [currencySymbol, setCurrencySymbol] = React.useState<{
symbol: string;
scale: number;
}>({ symbol: '$', scale: 2 });

const getWalletInformation = React.useCallback(
async (walletAddressUrl: string): Promise<boolean> => {
setErrors((prev) => ({ ...prev, walletAddressUrl: null }));
Expand Down Expand Up @@ -153,25 +144,19 @@ export const ConnectWalletForm = ({
);

const handleAmountChange = React.useCallback(
(value: string, input: HTMLInputElement) => {
const error = validateAmount(value, currencySymbol.symbol);
setErrors((prev) => ({ ...prev, amount: toErrorInfo(error) }));

const amountValue = formatNumber(+value, currencySymbol.scale);
if (!error) {
setAmount(amountValue);
input.value = amountValue;
}
saveValue('amount', error ? value : amountValue);
(amountValue: string) => {
setErrors((prev) => ({ ...prev, amount: null }));
setAmount(amountValue);
saveValue('amount', amountValue);
},
[saveValue, currencySymbol, toErrorInfo],
[saveValue],
);

const handleSubmit = async (ev?: React.FormEvent<HTMLFormElement>) => {
ev?.preventDefault();

const errWalletAddressUrl = validateWalletAddressUrl(walletAddressUrl);
const errAmount = validateAmount(amount, currencySymbol.symbol);
const errAmount = validateAmount(amount, walletAddressInfo!);
if (errAmount || errWalletAddressUrl) {
setErrors((prev) => ({
...prev,
Expand Down Expand Up @@ -221,14 +206,6 @@ export const ConnectWalletForm = ({
}
};

React.useEffect(() => {
if (!walletAddressInfo) return;
setCurrencySymbol({
symbol: getCurrencySymbol(walletAddressInfo.assetCode),
scale: walletAddressInfo.assetScale,
});
}, [walletAddressInfo]);

React.useEffect(() => {
if (defaultValues.walletAddressUrl) {
handleWalletAddressUrlChange(defaultValues.walletAddressUrl);
Expand Down Expand Up @@ -332,27 +309,23 @@ export const ConnectWalletForm = ({
{t('connectWallet_labelGroup_amount')}
</legend>
<div className="flex items-center gap-6">
<Input
<InputAmount
id="connectAmount"
type="text"
inputMode="numeric"
aria-label={t('connectWallet_label_amount')}
placeholder="5.00"
className="max-w-32"
defaultValue={amount}
label={t('connectWallet_label_amount')}
labelHidden={true}
amount={amount}
walletAddress={
walletAddressInfo || { assetCode: 'USD', assetScale: 2 }
}
errorMessage={errors.amount?.message}
errorHidden={true}
readOnly={!walletAddressInfo?.assetCode || isSubmitting}
addOn={<span className="text-weak">{currencySymbol.symbol}</span>}
aria-invalid={!!errors.amount}
aria-describedby={errors.amount?.message}
required={true}
onKeyDown={allowOnlyNumericInput}
onBlur={(ev) => {
const value = ev.currentTarget.value;
if (value === amount && !ev.currentTarget.required) {
return;
}
handleAmountChange(value, ev.currentTarget);
onError={(err) => {
setErrors((prev) => ({ ...prev, amount: toErrorInfo(err) }));
}}
onChange={handleAmountChange}
className="max-w-32"
placeholder="5.00"
/>

<Switch
Expand Down Expand Up @@ -547,35 +520,3 @@ function validateWalletAddressUrl(value: string): null | ErrorWithKeyLike {

return null;
}

function validateAmount(
value: string,
currencySymbol: string,
): null | ErrorWithKeyLike {
if (!value) {
return errorWithKey('connectWallet_error_amountRequired');
}
const val = Number(value);
if (Number.isNaN(val)) {
return errorWithKey('connectWallet_error_amountInvalidNumber', [
`${currencySymbol}${value}`,
]);
}
if (val <= 0) {
return errorWithKey('connectWallet_error_amountMinimum');
}
return null;
}

function allowOnlyNumericInput(ev: React.KeyboardEvent<HTMLInputElement>) {
if (
(!charIsNumber(ev.key) &&
ev.key !== 'Backspace' &&
ev.key !== 'Delete' &&
ev.key !== 'Enter' &&
ev.key !== 'Tab') ||
(ev.key === '.' && ev.currentTarget.value.includes('.'))
) {
ev.preventDefault();
}
}
113 changes: 113 additions & 0 deletions src/popup/components/InputAmount.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React from 'react';
import { Input } from './ui/Input';
import type { WalletAddress } from '@interledger/open-payments';
import { charIsNumber, formatNumber, getCurrencySymbol } from '../lib/utils';
import {
errorWithKey,
ErrorWithKeyLike,
formatCurrency,
} from '@/shared/helpers';

interface Props {
id: string;
label: string;
walletAddress: Pick<WalletAddress, 'assetCode' | 'assetScale'>;
amount: string;
onChange: (amount: string, inputEl: HTMLInputElement) => void;
onError: (error: ErrorWithKeyLike) => void;
className?: string;
placeholder?: string;
errorMessage?: string;
readOnly?: boolean;
labelHidden?: boolean;
errorHidden?: boolean;
min?: number;
max?: number;
controls?: boolean;
}

export const InputAmount = ({
label,
id,
walletAddress,
amount,
className,
placeholder,
errorMessage,
onChange,
onError,
labelHidden,
errorHidden,
min = 0,
max,
readOnly,
}: Props) => {
const currencySymbol = getCurrencySymbol(walletAddress.assetCode);
return (
<Input
id={id}
type="text"
inputMode="numeric"
label={labelHidden ? null : label}
aria-label={labelHidden ? label : undefined}
placeholder={placeholder}
className={className}
defaultValue={amount}
readOnly={readOnly}
addOn={<span className="text-weak">{currencySymbol}</span>}
errorMessage={errorHidden ? '' : errorMessage}
aria-invalid={errorHidden ? !!errorMessage : false}
required={true}
onKeyDown={allowOnlyNumericInput}
onBlur={(ev) => {
const input = ev.currentTarget;
const value = input.value;
if (value === amount && !input.required) {
return;
}
const error = validateAmount(value, walletAddress, min, max);
if (error) {
onError(error);
} else {
const amountValue = formatNumber(+value, walletAddress.assetScale);
input.value = amountValue;
onChange(amountValue, input);
}
}}
/>
);
};

export function validateAmount(
value: string,
walletAddress: Pick<WalletAddress, 'assetCode' | 'assetScale'>,
min: number = 0,
_max?: number,
): null | ErrorWithKeyLike {
if (!value) {
return errorWithKey('connectWallet_error_amountRequired');
}
const val = Number(value);
if (Number.isNaN(val)) {
return errorWithKey('connectWallet_error_amountInvalidNumber');
}
if (val <= min) {
return errorWithKey('connectWallet_error_amountMinimum', [
formatCurrency(min, walletAddress.assetCode, walletAddress.assetScale),
]);
}
return null;
}

function allowOnlyNumericInput(ev: React.KeyboardEvent<HTMLInputElement>) {
if (
(!charIsNumber(ev.key) &&
ev.key !== 'Backspace' &&
ev.key !== 'Delete' &&
ev.key !== 'Enter' &&
ev.key !== 'Tab') ||
(ev.key === '.' && ev.currentTarget.value.includes('.'))
) {
ev.preventDefault();
}
}
17 changes: 11 additions & 6 deletions src/shared/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,17 @@ export const cn = (...inputs: CxOptions) => {
return twMerge(cx(inputs));
};

export const formatCurrency = (value: any): string => {
if (value < 1) {
return `${Math.round(value * 100)}c`;
} else {
return `$${parseFloat(value).toFixed(2)}`;
}
export const formatCurrency = (
value: string | number,
currency: string,
maximumFractionDigits = 2,
locale?: string,
): string => {
return new Intl.NumberFormat(locale, {
style: 'currency',
currency,
maximumFractionDigits,
}).format(Number(value));
};

const isWalletAddress = (o: any): o is WalletAddress => {
Expand Down

0 comments on commit e5f238c

Please sign in to comment.