diff --git a/i18n/locales/en/trading.json b/i18n/locales/en/trading.json index 6127446e9..ae9d9d969 100644 --- a/i18n/locales/en/trading.json +++ b/i18n/locales/en/trading.json @@ -55,9 +55,10 @@ "validation": { "primary-amount-missing": "No amount specified.", "primary-asset-missing": "No asset selected.", - "secondary-asset-missing": "No asset selected.", "invalid-amount": "Invalid amount specified.", - "invalid-price": "Invalid price" + "invalid-price": "Invalid price", + "not-enough-balance": "Exceeds your spendable balance", + "secondary-asset-missing": "No asset selected." }, "warning": { "title": "Warning", diff --git a/src/Assets/components/CustomTrustline.tsx b/src/Assets/components/CustomTrustline.tsx index 7a24cee90..00203763e 100644 --- a/src/Assets/components/CustomTrustline.tsx +++ b/src/Assets/components/CustomTrustline.tsx @@ -56,7 +56,7 @@ function CustomTrustlineDialog(props: Props) { /> /^[0-9]*([\.,][0-9]+)?$/.test(amount) + +// replaces ',' with '.' in a string +export function replaceCommaWithDot(input: string) { + return input.replace(/,/g, ".") +} + +// should be used when creating a Big from user input because there are issues with +// parsing a Big from number-strings with a comma on iOS +export function FormBigNumber(value: BigSource) { + if (typeof value === "string") { + return BigNumber(replaceCommaWithDot(value)) + } else { + return BigNumber(value) + } +} diff --git a/src/Payment/components/PaymentForm.tsx b/src/Payment/components/PaymentForm.tsx index c978d5a61..0383dfa6e 100644 --- a/src/Payment/components/PaymentForm.tsx +++ b/src/Payment/components/PaymentForm.tsx @@ -22,6 +22,7 @@ import { PriceInput, QRReader } from "~Generic/components/FormFields" import { formatBalance } from "~Generic/lib/balances" import { HorizontalLayout } from "~Layout/components/Box" import Portal from "~Generic/components/Portal" +import { FormBigNumber, isValidAmount, replaceCommaWithDot } from "~Generic/lib/form" export interface PaymentFormValues { amount: string @@ -211,8 +212,15 @@ const PaymentForm = React.memo(function PaymentForm(props: PaymentFormProps) { error={Boolean(form.errors.amount)} inputRef={form.register({ required: t("payment.validation.no-price"), - pattern: { value: /^[0-9]+(\.[0-9]+)?$/, message: t("payment.validation.invalid-price") }, - validate: value => BigNumber(value).lte(spendableBalance) || t("payment.validation.not-enough-funds") + validate: value => { + if (!isValidAmount(value) || FormBigNumber(value).eq(0)) { + return t("payment.validation.invalid-price") + } else if (FormBigNumber(value).gt(spendableBalance)) { + return t("payment.validation.not-enough-funds") + } else { + return undefined + } + } })} label={form.errors.amount ? form.errors.amount.message : t("payment.inputs.price.label")} margin="normal" @@ -355,7 +363,7 @@ function PaymentFormContainer(props: Props) { const payment = await createPaymentOperation({ asset: asset || Asset.native(), - amount: formValues.amount, + amount: replaceCommaWithDot(formValues.amount), destination, horizon }) diff --git a/src/Trading/components/TradingForm.tsx b/src/Trading/components/TradingForm.tsx index c2912bcb9..3e712bd4a 100644 --- a/src/Trading/components/TradingForm.tsx +++ b/src/Trading/components/TradingForm.tsx @@ -1,4 +1,3 @@ -import BigNumber from "big.js" import React from "react" import { Controller, useForm } from "react-hook-form" import { useTranslation } from "react-i18next" @@ -32,9 +31,10 @@ import { getAccountMinimumBalance, getSpendableBalance } from "~Generic/lib/stellar" +import { FormBigNumber, isValidAmount } from "~Generic/lib/form" import { createTransaction } from "~Generic/lib/transaction" import { Box, HorizontalLayout, VerticalLayout } from "~Layout/components/Box" -import { bigNumberToInputValue, isValidAmount, TradingFormValues, useCalculation } from "../hooks/form" +import { bigNumberToInputValue, TradingFormValues, useCalculation } from "../hooks/form" import TradingPrice from "./TradingPrice" const useStyles = makeStyles({ @@ -141,8 +141,8 @@ function TradingForm(props: Props) { } const validateManualPrice = React.useCallback(() => { - const value = BigNumber(manualPrice).gt(0) ? manualPrice : defaultPrice - const valid = isValidAmount(value) && BigNumber(value).gt(0) + const value = FormBigNumber(manualPrice).gt(0) ? manualPrice : defaultPrice + const valid = isValidAmount(value) && FormBigNumber(value).gt(0) if (!valid) { if (!expanded) { setExpanded(true) @@ -272,17 +272,23 @@ function TradingForm(props: Props) { inputRef={form.register({ required: t("trading.validation.primary-amount-missing"), validate: value => { - const amountInvalid = - primaryAmount.lt(0) || - (value.length > 0 && primaryAmount.eq(0)) || + const amountInvalid = primaryAmount.lt(0) || (value.length > 0 && primaryAmount.eq(0)) + const exceedsBalance = (props.primaryAction === "sell" && primaryBalance && primaryAmount.gt(spendablePrimaryBalance)) || (props.primaryAction === "buy" && secondaryBalance && secondaryAmount.gt(spendableSecondaryBalance)) - return !amountInvalid || t("trading.validation.invalid-amount") + + if (amountInvalid) { + return t("trading.validation.invalid-amount") + } else if (exceedsBalance) { + return t("trading.validation.not-enough-balance") + } else { + return true + } } })} error={Boolean(form.errors.primaryAmountString)} inputProps={{ - pattern: "[0-9]*", + pattern: "^[0-9]*(.[0-9]+)?$", inputMode: "decimal", min: "0.0000001", max: maxPrimaryAmount.toFixed(7), @@ -320,7 +326,6 @@ function TradingForm(props: Props) { )} required style={{ flexGrow: 1, flexShrink: 1, width: "55%" }} - type="number" /> @@ -394,6 +399,12 @@ function TradingForm(props: Props) { } control={form.control} name="manualPrice" + rules={{ + validate: value => { + const valid = isValidAmount(value) + return valid || t("trading.validation.invalid-price") + } + }} valueName="manualPrice" /> diff --git a/src/Trading/components/TradingPrice.tsx b/src/Trading/components/TradingPrice.tsx index b3e6286a7..4cc5386d1 100644 --- a/src/Trading/components/TradingPrice.tsx +++ b/src/Trading/components/TradingPrice.tsx @@ -52,7 +52,7 @@ const TradingPrice = React.forwardRef(function TradingPrice(props: TradingPriceP return ( event.target.select() : undefined} style={props.style} - type="number" value={props.defaultPrice ? props.defaultPrice : props.manualPrice} /> ) diff --git a/src/Trading/hooks/form.ts b/src/Trading/hooks/form.ts index 70364e5ed..65c2e0582 100644 --- a/src/Trading/hooks/form.ts +++ b/src/Trading/hooks/form.ts @@ -2,14 +2,13 @@ import BigNumber from "big.js" import { Asset, Horizon } from "stellar-sdk" import { AccountData } from "~Generic/lib/account" import { formatBalance, BalanceFormattingOptions } from "~Generic/lib/balances" +import { FormBigNumber, isValidAmount } from "~Generic/lib/form" import { calculateSpread, FixedOrderbookRecord } from "~Generic/lib/orderbook" import { BASE_RESERVE, balancelineToAsset, getAccountMinimumBalance, getSpendableBalance } from "~Generic/lib/stellar" import { useConversionOffers } from "./conversion" export const bigNumberToInputValue = (bignum: BigNumber, overrides?: BalanceFormattingOptions) => - formatBalance(bignum, { minimumSignificants: 3, maximumSignificants: 9, ...overrides }) - -export const isValidAmount = (amount: string) => /^[0-9]+([\.,][0-9]+)?$/.test(amount) + formatBalance(bignum, { minimumSignificants: 3, maximumSignificants: 9, groupThousands: false, ...overrides }) function findMatchingBalance(balances: AccountData["balances"], asset: Asset) { return balances.find(balance => balancelineToAsset(balance).equals(asset)) @@ -61,14 +60,14 @@ export function useCalculation(parameters: CalculationParameters): CalculationRe const price = manualPrice && isValidAmount(manualPrice) ? priceMode === "secondary" - ? BigNumber(manualPrice) - : BigNumber(manualPrice).eq(0) // prevent division by zero + ? FormBigNumber(manualPrice) + : FormBigNumber(manualPrice).eq(0) // prevent division by zero ? BigNumber(0) - : BigNumber(1).div(manualPrice) + : BigNumber(1).div(FormBigNumber(manualPrice)) : BigNumber(0) const primaryAmount = - primaryAmountString && isValidAmount(primaryAmountString) ? BigNumber(primaryAmountString) : BigNumber(0) + primaryAmountString && isValidAmount(primaryAmountString) ? FormBigNumber(primaryAmountString) : BigNumber(0) const primaryBalance = primaryAsset ? findMatchingBalance(accountData.balances, primaryAsset) : undefined const secondaryBalance = secondaryAsset ? findMatchingBalance(accountData.balances, secondaryAsset) : undefined diff --git a/src/Transaction/stories/SubmissionProgress.tsx b/src/Transaction/stories/SubmissionProgress.tsx index 11982d8ff..28dd196e9 100644 --- a/src/Transaction/stories/SubmissionProgress.tsx +++ b/src/Transaction/stories/SubmissionProgress.tsx @@ -18,6 +18,6 @@ storiesOf("SubmissionProgress", module) undefined} + onRetry={() => Promise.resolve()} /> ))