Skip to content

Commit

Permalink
address-amount form: handle asset precision (#409)
Browse files Browse the repository at this point in the history
* address-amount form handle precision

* fix Input value

* send-select-asset list: do not display if balance=0

* prettier & lint

* fix Input

* sanitaze input value
  • Loading branch information
louisinger authored Sep 23, 2022
1 parent 1d929a3 commit d72de35
Show file tree
Hide file tree
Showing 3 changed files with 99 additions and 110 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@ const mapStateToProps = (state: RootReducerState): SendSelectAssetProps => {
const balances = selectBalances(...selectAllAccountsIDsSpendableViaUI(state))(state);
const getAsset = assetGetterFromIAssets(state.assets);
return {
balanceAssets: Object.keys(balances).map(getAsset),
balanceAssets: Object.keys(balances)
.filter((asset) => balances[asset] > 0)
.map(getAsset),
balances,
};
};
Expand Down
145 changes: 66 additions & 79 deletions src/presentation/components/address-amount-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import type { FormikProps } from 'formik';
import { withFormik } from 'formik';
import type { RouteComponentProps } from 'react-router';
import type { ProxyStoreDispatch } from '../../application/redux/proxyStore';
import cx from 'classnames';
import Button from './button';
import {
setAddressesAndAmount,
Expand All @@ -18,10 +17,12 @@ import type { Account } from '../../domain/account';
import type { NetworkString } from 'ldk';
import { isValidAddressForNetwork } from '../../application/utils/address';
import { fromSatoshi, getMinAmountFromPrecision, toSatoshi } from '../utils';
import Input from './input';
import React from 'react';

interface AddressAmountFormValues {
address: string;
amount: number;
amount: string;
assetTicker: string;
assetPrecision: number;
balance: number;
Expand All @@ -37,95 +38,78 @@ interface AddressAmountFormProps {
account: Account;
}

/**
* Sanitize input amount
* @param eventDetailValue string
* @returns sanitizedValue string
*/
function sanitizeInputAmount(eventDetailValue: string, precision: number): string {
// Sanitize value
let sanitizedValue = eventDetailValue
// Replace comma by dot
.replace(',', '.')
// Remove non-numeric chars or period
.replace(/[^0-9.]/g, '');
// Prefix single dot
if (sanitizedValue === '.') sanitizedValue = '0.';
// Remove last dot. Remove all if consecutive
if ((sanitizedValue.match(/\./g) || []).length > 1) {
sanitizedValue = sanitizedValue.replace(/\.$/, '');
}
// No more than max decimal digits for respective unit
if (eventDetailValue.split(/[,.]/, 2)[1]?.length > precision) {
sanitizedValue = Number(eventDetailValue).toFixed(precision);
}

return sanitizedValue;
}

function isValidAmountString(precision: number, amount?: string) {
const splitted = amount?.replace(',', '.').split('.');
if (splitted && splitted.length < 2) return true;
return splitted !== undefined && splitted[1].length <= precision;
}

const AddressAmountForm = (props: FormikProps<AddressAmountFormValues>) => {
const {
errors,
handleChange,
handleBlur,
handleSubmit,
isSubmitting,
touched,
values,
setFieldValue,
setFieldTouched,
} = props;
const { errors, handleSubmit, isSubmitting, touched, values, setFieldValue, setFieldTouched } =
props;

const setMaxAmount = () => {
const maxAmount = values.balance;
setFieldValue('amount', maxAmount);
setFieldValue('amount', maxAmount, true);
setFieldTouched('amount', true, false);
};

return (
<form onSubmit={handleSubmit} className="mt-10">
<div className={cx({ 'mb-12': !errors.address || !touched.address })}>
<label className="block">
<p className="mb-2 text-base font-medium text-left">Address</p>
<input
className={cx(
'border-2 focus:ring-primary focus:border-primary placeholder-grayLight block w-full rounded-md',
{
'border-red': errors.address && touched.address,
'border-grayLight': !errors.address || !touched.address,
}
)}
id="address"
name="address"
onChange={handleChange}
onBlur={handleBlur}
placeholder=""
type="text"
value={values.address}
/>
</label>
{errors.address && touched.address && (
<p className="text-red h-10 mt-2 text-xs font-medium text-left">{errors.address}</p>
)}
</div>
<form onSubmit={handleSubmit} className="mt-8">
<p className="mb-2 text-base font-medium text-left">Address</p>
<Input name="address" placeholder="" type="text" {...props} />

<div className={cx({ 'mb-12': !errors.amount || !touched.amount })}>
<label className="block">
<p className="mb-2 text-base font-medium text-left">Amount</p>
<div
className={cx('focus-within:text-grayDark text-grayLight relative w-full', {
'text-grayDark': touched.amount,
})}
>
<input
className={cx(
'border-2 focus:ring-primary focus:border-primary placeholder-grayLight block w-full rounded-md',
{
'border-red': errors.amount && touched.amount,
'border-grayLight': !errors.amount || !touched.amount,
}
)}
id="amount"
name="amount"
onChange={handleChange}
onBlur={handleBlur}
placeholder="0"
type="number"
lang="en"
value={values.amount}
/>
<span className="absolute inset-y-0 right-0 flex items-center pr-2 text-base font-medium">
{values.assetTicker}
</span>
</div>
</label>
<p className="text-primary text-right">
<div className="flex content-center justify-between mb-2">
<p className="text-base font-medium text-left">Amount</p>
<div className="text-primary text-right">
<button
onClick={setMaxAmount}
className="background-transparent focus:outline-none px-3 py-1 mt-1 mb-1 mr-1 text-xs font-bold uppercase transition-all duration-150 ease-linear outline-none"
type="button"
>
SEND ALL
</button>
</p>
{errors.amount && touched.amount && (
<p className="text-red h-10 mt-1 text-xs font-medium text-left">{errors.amount}</p>
)}
</div>
</div>
<Input
{...props}
handleChange={(e: React.ChangeEvent<any>) => {
const amount = e.target.value as string;
setFieldValue('amount', sanitizeInputAmount(amount, values.assetPrecision), true);
}}
value={values.amount}
name="amount"
placeholder="0"
type="text"
inputSuffix={values.assetTicker}
validateOnChange={true}
/>

<div className="text-right">
<Button
Expand All @@ -150,8 +134,8 @@ const AddressAmountEnhancedForm = withFormik<AddressAmountFormProps, AddressAmou
// https://github.com/formium/formik/issues/321#issuecomment-478364302
amount:
props.transaction.sendAmount > 0
? fromSatoshi(props.transaction.sendAmount ?? 0, props.asset.precision)
: ('' as unknown as number),
? sanitizeInputAmount((props.transaction.sendAmount ?? 0).toString(), props.asset.precision)
: '',
assetTicker: props.asset.ticker ?? '??',
assetPrecision: props.asset.precision,
balance: fromSatoshi(props.balance ?? 0, props.asset.precision),
Expand All @@ -160,19 +144,22 @@ const AddressAmountEnhancedForm = withFormik<AddressAmountFormProps, AddressAmou
validationSchema: (props: AddressAmountFormProps): any =>
Yup.object().shape({
address: Yup.string()
.required('Please enter a valid address')
.required('Address is required')
.test(
'valid-address',
'Address is not valid',
(value) => value !== undefined && isValidAddressForNetwork(value, props.network)
),

amount: Yup.number()
.required('Please enter a valid amount')
.required('Amount is required')
.min(
getMinAmountFromPrecision(props.asset.precision),
'Amount should be at least 1 satoshi'
)
.test('invalid-amount', 'Invalid amount', (value) =>
isValidAmountString(props.asset.precision, value?.toString())
)
.test('insufficient-funds', 'Insufficient funds', (value) => {
return value !== undefined && value <= fromSatoshi(props.balance, props.asset.precision);
}),
Expand All @@ -195,7 +182,7 @@ const AddressAmountEnhancedForm = withFormik<AddressAmountFormProps, AddressAmou
await props
.dispatch(
setAddressesAndAmount(
toSatoshi(values.amount, values.assetPrecision),
toSatoshi(Number(values.amount), values.assetPrecision),
[changeAddress],
createAddress(values.address)
)
Expand Down
60 changes: 30 additions & 30 deletions src/presentation/components/input.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,56 +6,56 @@ interface InputProps<FormValues> extends FormikProps<FormValues> {
placeholder?: string;
title?: string;
type: 'text' | 'password';
inputSuffix?: string;
value?: string;
}

/**
* Generic Formik Input
*/
export default function Input<FormValues>({
errors,
status,
name,
placeholder = '',
title,
touched,
type = 'text',
values,
handleChange,
handleBlur,
inputSuffix,
value,
}: InputProps<FormValues>) {
return (
<div
className={cx({
'mb-12': !errors[name] || !touched[name],
})}
>
<div>
<label className="block text-left">
{title && <p className="mb-2 font-medium">{title}</p>}
<input
className={cx(
'border-2 focus:ring-primary focus:border-primary placeholder-grayLight block w-full sm:w-2/5 rounded-md',
{
'border-red': errors[name] && touched[name],
'border-grayLight': !errors[name],
}
<div className="relative">
<input
className={cx(
'border-2 focus:ring-primary focus:border-primary placeholder-grayLight block w-full sm:w-2/5 rounded-md',
{
'border-red': errors[name] && touched[name],
'border-grayLight': !errors[name],
}
)}
value={value}
id={name.toString()}
name={name.toString()}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
type={type}
/>
{inputSuffix && (
<span className="absolute inset-y-0 right-0 flex items-center pr-2 text-base font-medium">
{inputSuffix}
</span>
)}
id={name.toString()}
name={name.toString()}
onChange={handleChange}
onBlur={handleBlur}
placeholder={placeholder}
type={type}
value={String(values[name])}
/>
</div>
</label>
{status?.[name] ? (
<p className="text-red h-10 mt-2 text-xs text-left">{status[name]}</p>
) : (
errors[name] &&
touched[name] && (
<p className="text-red h-10 mt-2 text-xs text-left">{errors[name] as string}</p>
)
)}
<p className="text-red h-10 mt-2 text-xs text-left">
{errors?.[name] ? errors[name]?.toString() : ''}
</p>
</div>
);
}

0 comments on commit d72de35

Please sign in to comment.