Skip to content

Commit

Permalink
feat: APP-396 two decimal digits for usd amount (#2544)
Browse files Browse the repository at this point in the history
  • Loading branch information
r41ph authored Dec 9, 2024
1 parent de77bea commit 27e158b
Show file tree
Hide file tree
Showing 10 changed files with 193 additions and 80 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,12 @@ export function getSpendingCap(
cardSellOrders: Array<CardSellOrder>,
) {
return paymentOption === PAYMENT_OPTIONS.CARD
? cardSellOrders.reduce(
(prev, cur) => prev + Number(cur.quantity) * cur.usdPrice,
0,
)
? cardSellOrders.reduce((prev, cur) => {
return formatCurrencyAmountTwoDecimals(
prev + Number(cur.quantity) * cur.usdPrice,
true,
);
}, 0)
: microToDenom(
filteredCryptoSellOrders?.reduce(
(prev, cur) => prev + Number(cur.quantity) * Number(cur.askAmount),
Expand All @@ -53,7 +55,9 @@ export const getCreditsAmount = ({
orderedSellOrders,
creditTypePrecision,
}: GetCreditsAmountParams) => {
const currentCurrencyAmount = card ? value : denomToMicro(value);
const currentCurrencyAmount = card
? formatCurrencyAmountTwoDecimals(value, true)
: denomToMicro(value);
let currentCreditsAmount = 0;
let currencyAmountLeft = currentCurrencyAmount;
const sellOrders = [];
Expand All @@ -65,9 +69,9 @@ export const getCreditsAmount = ({
const orderTotalAmount = quantity * price;

if (currencyAmountLeft >= orderTotalAmount) {
currencyAmountLeft = parseFloat(
(currencyAmountLeft - orderTotalAmount).toFixed(6),
);
currencyAmountLeft = card
? formatCurrencyAmountTwoDecimals(currencyAmountLeft - orderTotalAmount)
: parseFloat((currencyAmountLeft - orderTotalAmount).toFixed(6));
currentCreditsAmount += quantity;
sellOrders.push(formatSellOrder({ order, card, price }));
if (currencyAmountLeft === 0) break;
Expand Down Expand Up @@ -138,7 +142,7 @@ export const getCurrencyAmount = ({

return {
[CURRENCY_AMOUNT]: card
? parseFloat(currentCurrencyAmount.toFixed(6))
? formatCurrencyAmountTwoDecimals(currentCurrencyAmount, true)
: parseFloat(microToDenom(currentCurrencyAmount).toFixed(6)),
sellOrders,
};
Expand Down Expand Up @@ -171,3 +175,34 @@ export const formatSellOrder = ({
price: card ? parseFloat((price * 100).toFixed(2)) : undefined, // stripe amounts should be in the smallest currency unit (e.g., 100 cents to charge $1.00)
};
};

/**
* Formats a given value to a currency amount with max two decimals.
*
* @param value - The value to format. Can be a string or a number.
* @param roundUpDecimal - Optional. If true, rounds the value up to the nearest two decimals. Defaults to false.
* @returns The formatted currency amount as a number with two decimals.
*
*/
export const formatCurrencyAmountTwoDecimals = (
value: string | number,
roundUpDecimal = false,
) => {
const numericValue = typeof value === 'string' ? parseFloat(value) : value;
const stringValue = value.toString();
const [integerPart, decimalPart] = stringValue.split('.');

if (isNaN(numericValue)) {
return 0;
}

// Round to two decimals, either returning the value with
// the first two decimals only, if any, or rounding them up.
const formattedValue = roundUpDecimal
? +(Math.ceil(numericValue * 100) / 100).toFixed(2)
: decimalPart?.length > 2
? +`${integerPart}.${decimalPart.slice(0, 2)}`
: +`${integerPart}`;

return formattedValue;
};
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeEvent } from 'react';
import { ChangeEvent, FocusEvent, useCallback } from 'react';
import { useFormContext } from 'react-hook-form';
import { msg, Trans } from '@lingui/macro';
import { useLingui } from '@lingui/react';
Expand All @@ -24,17 +24,38 @@ export const CreditsInput = ({
const { onChange } = register(CREDITS_AMOUNT);

const onHandleChange = (event: ChangeEvent<HTMLInputElement>) => {
// Remove zeros in non decimal values and update the value
const value = event.target.value;
if (!value.includes('.')) setValue(CREDITS_AMOUNT, Number(value));
onChange(event);
handleCreditsAmountChange(event);
};

const handleInput = useCallback(
(event: ChangeEvent<HTMLInputElement>): void => {
let value = event.target.value;
if (/^0[0-9]/.test(value)) {
value = value.replace(/^0+/, '');
setValue(CREDITS_AMOUNT, parseFloat(Number(value).toFixed(2)), {
shouldValidate: true,
});
}
},
[setValue],
);

const handleOnBlur = useCallback(
(event: FocusEvent<HTMLInputElement>): void => {
// If the value is empty, set it to 0
const value = event.target.value;
if (value === '') {
setValue(CREDITS_AMOUNT, 0, { shouldValidate: true });
}
},
[setValue],
);

return (
<div className="flex-1 relative">
<TextField
className={`border border-solid border-grey-300 focus-within:border-grey-500 focus-within:border-2 border border-solid border-grey-300 flex items-center pr-10 sm:h-60`}
className={`focus-within:border-grey-500 focus-within:border-2 border border-solid border-grey-300 flex items-center pr-10 sm:h-60`}
type="number"
customInputProps={{
step: '0.000001',
Expand All @@ -44,6 +65,8 @@ export const CreditsInput = ({
}}
{...register(CREDITS_AMOUNT)}
onChange={onHandleChange}
onInput={handleInput}
onBlur={handleOnBlur}
sx={{
'& .MuiInputBase-root': {
border: 'none',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ChangeEvent, lazy, useCallback } from 'react';
import { ChangeEvent, FocusEvent, lazy, useCallback } from 'react';
import { useFormContext, useWatch } from 'react-hook-form';
import { msg } from '@lingui/macro';
import { useLingui } from '@lingui/react';
Expand All @@ -18,6 +18,7 @@ import { useMultiStep } from 'components/templates/MultiStepTemplate';
import { findDisplayDenom } from '../DenomLabel/DenomLabel.utils';
import { CURRENCY, CURRENCY_AMOUNT } from './CreditsAmount.constants';
import { CurrencyInputProps } from './CreditsAmount.types';
import { formatCurrencyAmountTwoDecimals } from './CreditsAmount.utils';

const CustomSelect = lazy(
() =>
Expand Down Expand Up @@ -45,7 +46,7 @@ export const CurrencyInput = ({
const { data, handleSave, activeStep } =
useMultiStep<BuyCreditsSchemaTypes>();
const paymentOption = useAtomValue(paymentOptionAtom);

const card = paymentOption === PAYMENT_OPTIONS.CARD;
const { onChange } = register(CURRENCY_AMOUNT);

const currency = useWatch({
Expand All @@ -55,13 +56,46 @@ export const CurrencyInput = ({

const handleOnChange = useCallback(
(event: ChangeEvent<HTMLInputElement>) => {
// Remove zeros in non decimal values and update the value
const value = event.target.value;
if (!value.includes('.')) setValue(CURRENCY_AMOUNT, Number(value));
onChange(event);
handleCurrencyAmountChange(event);
},
[handleCurrencyAmountChange, onChange, setValue],
[handleCurrencyAmountChange, onChange],
);

const handleInput = useCallback(
(event: ChangeEvent<HTMLInputElement>): void => {
let value = event.target.value;
const decimalPart = value.split('.')?.[1];
// Check if the value has a decimal part longer than 2 digits,
// or if the value starts with leading zero/s
if (
(decimalPart &&
decimalPart.length > 2 &&
paymentOption === PAYMENT_OPTIONS.CARD) ||
/^0[0-9]/.test(value)
) {
value = value.replace(/^0+/, '');
setValue(
CURRENCY_AMOUNT,
formatCurrencyAmountTwoDecimals(value, false),
{
shouldValidate: true,
},
);
}
},
[paymentOption, setValue],
);

const handleOnBlur = useCallback(
(event: FocusEvent<HTMLInputElement>): void => {
// If the value is empty, set it to 0
const value = event.target.value;
if (value === '') {
setValue(CURRENCY_AMOUNT, 0, { shouldValidate: true });
}
},
[setValue],
);

const onHandleCurrencyChange = useCallback(
Expand Down Expand Up @@ -89,29 +123,30 @@ export const CurrencyInput = ({

return (
<div className="grow sm:flex-1 w-full sm:w-auto relative">
{paymentOption === PAYMENT_OPTIONS.CARD && (
{card && (
<span className="text-xs sm:text-sm absolute top-[13px] left-[9px] sm:top-[18px] sm:left-10 z-50">
$
</span>
)}
<TextField
{...register(CURRENCY_AMOUNT)}
onChange={handleOnChange}
onInput={handleInput}
onBlur={handleOnBlur}
type="number"
className={`border border-solid border-grey-300 focus-within:border-grey-500 focus-within:border-2 ${
paymentOption === PAYMENT_OPTIONS.CARD ? 'pl-5' : ''
card ? 'pl-5' : ''
} w-full sm:w-auto flex justify-start relative sm:h-60 rounded-sm items-center`}
customInputProps={{
max: maxCurrencyAmount,
min: 0,
step: '0.000001',
min: card ? 0.5 : 0,
step: card ? '0.01' : '0.000001',
'aria-label': _(msg`Currency Input`),
}}
sx={{
'& .MuiInputBase-root': {
border: 'none',
paddingRight: theme =>
paymentOption === PAYMENT_OPTIONS.CARD ? theme.spacing(5) : 0,
paddingRight: theme => (card ? theme.spacing(5) : 0),
'& input': {
overflow: 'hidden',
textOverflow: 'ellipsis',
Expand All @@ -137,7 +172,7 @@ export const CurrencyInput = ({
},
}}
endAdornment={
paymentOption === PAYMENT_OPTIONS.CARD ? (
card ? (
<DenomIconWithCurrency
baseDenom={currency?.askBaseDenom}
displayDenom={displayDenom}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,6 @@ export function OrderSummaryContent({
const paymentOption = useAtomValue(paymentOptionAtom);
const { projectName, currency, pricePerCredit, credits, currencyAmount } =
order;

const displayDenom = useMemo(
() =>
findDisplayDenom({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { msg } from '@lingui/macro';

export const MAX_AMOUNT = msg`Amount cannot exceed`;
export const MIN_USD_AMOUNT = msg`Must be at least`;
export const MAX_CREDITS = msg`Credits cannot exceed`;
export const POSITIVE_NUMBER = msg`Must be positive`;
export const NOT_ENOUGH_BALANCE = msg`You don’t have enough`;
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { PAYMENT_OPTIONS } from 'pages/BuyCredits/BuyCredits.constants';
import {
MAX_AMOUNT,
MAX_CREDITS,
MIN_USD_AMOUNT,
NOT_ENOUGH_BALANCE,
POSITIVE_NUMBER,
} from './ChooseCreditsForm.constants';
Expand Down Expand Up @@ -46,6 +47,12 @@ export const createChooseCreditsFormSchema = ({
{
message: `${i18n._(NOT_ENOUGH_BALANCE)}`,
},
)
.refine(
value => paymentOption === PAYMENT_OPTIONS.CRYPTO || value >= 0.5,
{
message: `${i18n._(MIN_USD_AMOUNT)} 0.5`,
},
),
[CREDITS_AMOUNT]: z.coerce
.number()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,12 @@ export const ChooseCreditsForm = React.memo(
handlePaymentOptions,
]);

useEffect(() => {
if (spendingCap && creditsAvailable) {
form.trigger();
}
}, [form, form.trigger, creditsAvailable, spendingCap]);

// Advanced settings not enabled for MVP
// const [advanceSettingsOpen, setAdvanceSettingsOpen] = useState(false);
// const creditVintageOptions = useWatch({
Expand Down
Loading

0 comments on commit 27e158b

Please sign in to comment.