diff --git a/dapp/src/components/TransactionModal/ActionFormModal.tsx b/dapp/src/components/TransactionModal/ActionFormModal.tsx
index 8c50be64e..53ab0e42b 100644
--- a/dapp/src/components/TransactionModal/ActionFormModal.tsx
+++ b/dapp/src/components/TransactionModal/ActionFormModal.tsx
@@ -107,7 +107,7 @@ function ActionFormModal({ type }: { type: ActionFlowType }) {
<>
{!isLoading && }
{heading}
-
+
diff --git a/dapp/src/components/TransactionModal/ActiveStakingStep/StakeFormModal/index.tsx b/dapp/src/components/TransactionModal/ActiveStakingStep/StakeFormModal/index.tsx
index 1c5037a76..4149e4fc7 100644
--- a/dapp/src/components/TransactionModal/ActiveStakingStep/StakeFormModal/index.tsx
+++ b/dapp/src/components/TransactionModal/ActiveStakingStep/StakeFormModal/index.tsx
@@ -3,7 +3,8 @@ import TokenAmountForm from "#/components/shared/TokenAmountForm"
import { TokenAmountFormValues } from "#/components/shared/TokenAmountForm/TokenAmountFormBase"
import { useMinDepositAmount, useWallet } from "#/hooks"
import { FormSubmitButton } from "#/components/shared/Form"
-import { BaseFormProps } from "#/types"
+import { ACTION_FLOW_TYPES, BaseFormProps } from "#/types"
+import { fixedPointNumberToString, getCurrencyByType } from "#/utils"
import StakeDetails from "./StakeDetails"
function StakeFormModal({
@@ -12,11 +13,16 @@ function StakeFormModal({
const minDepositAmount = useMinDepositAmount()
const { balance: tokenBalance } = useWallet()
+ const { decimals } = getCurrencyByType("bitcoin")
+ const inputPlaceholder = `Minimum ${fixedPointNumberToString(minDepositAmount, decimals)} BTC`
+ const tokenAmountLabel = "Wallet balance"
+
return (
{children}
diff --git a/dapp/src/components/shared/Form/FormTokenBalanceInput.tsx b/dapp/src/components/shared/Form/FormTokenBalanceInput.tsx
index e813a3033..fc922ea8c 100644
--- a/dapp/src/components/shared/Form/FormTokenBalanceInput.tsx
+++ b/dapp/src/components/shared/Form/FormTokenBalanceInput.tsx
@@ -7,6 +7,7 @@ export type FormTokenBalanceInputProps = {
name: string
defaultValue?: bigint
} & Omit
+
export function FormTokenBalanceInput({
name,
defaultValue,
@@ -17,9 +18,11 @@ export function FormTokenBalanceInput({
const setAmount = useCallback(
(value?: bigint) => {
if (!meta.touched) logPromiseFailure(helpers.setTouched(true))
+ if (meta.error) helpers.setError(undefined)
+
logPromiseFailure(helpers.setValue(value))
},
- [helpers, meta.touched],
+ [helpers, meta.touched, meta.error],
)
useEffect(() => {
@@ -34,7 +37,7 @@ export function FormTokenBalanceInput({
{...field}
amount={defaultValue ?? (meta.value as bigint)}
setAmount={setAmount}
- hasError={Boolean(meta.touched && meta.error)}
+ hasError={Boolean(meta.error)}
errorMsgText={meta.error}
/>
)
diff --git a/dapp/src/components/shared/NumberFormatInput/index.tsx b/dapp/src/components/shared/NumberFormatInput/index.tsx
index ec4f5b274..7d0daac37 100644
--- a/dapp/src/components/shared/NumberFormatInput/index.tsx
+++ b/dapp/src/components/shared/NumberFormatInput/index.tsx
@@ -1,8 +1,12 @@
import React from "react"
-import { NumericFormat, NumericFormatProps } from "react-number-format"
+import {
+ NumberFormatValues,
+ NumericFormat,
+ NumericFormatProps,
+} from "react-number-format"
import { InputProps, chakra, useMultiStyleConfig } from "@chakra-ui/react"
-const ChakraWrapper = chakra(NumericFormat)
+const ChakraNumericFormat = chakra(NumericFormat)
export type NumberFormatInputValues = {
formattedValue: string
@@ -12,8 +16,9 @@ export type NumberFormatInputValues = {
export type NumberFormatInputProps = {
onValueChange: (values: NumberFormatInputValues) => void
+ integerScale?: number
} & InputProps &
- Pick
+ Pick
/**
* Component is from the Threshold Network React Components repository.
@@ -29,17 +34,31 @@ const NumberFormatInput = React.forwardRef<
>((props, ref) => {
const { field: css } = useMultiStyleConfig("Input", props)
- const { decimalScale, isDisabled, isInvalid, ...restProps } = props
+ const { decimalScale, isDisabled, isInvalid, integerScale, ...restProps } =
+ props
+
+ const handleLengthValidation = (values: NumberFormatValues) => {
+ const { value, floatValue } = values
+ if (
+ floatValue === undefined ||
+ value === undefined ||
+ integerScale === undefined
+ )
+ return true
+
+ const [integerPart] = value.split(".")
+ return integerPart.length <= integerScale
+ }
return (
-
)
diff --git a/dapp/src/components/shared/TokenAmountForm/TokenAmountFormBase.tsx b/dapp/src/components/shared/TokenAmountForm/TokenAmountFormBase.tsx
index 6487627bc..f14f3f2d6 100644
--- a/dapp/src/components/shared/TokenAmountForm/TokenAmountFormBase.tsx
+++ b/dapp/src/components/shared/TokenAmountForm/TokenAmountFormBase.tsx
@@ -24,9 +24,9 @@ export type TokenAmountFormBaseProps = {
formId?: string
tokenBalance: bigint
tokenBalanceInputPlaceholder: string
+ tokenAmountLabel?: string
currency: CurrencyType
withMaxButton: boolean
- fiatCurrency?: CurrencyType
children?: React.ReactNode
defaultAmount?: bigint
}
@@ -35,8 +35,8 @@ export default function TokenAmountFormBase({
formId,
tokenBalance,
currency,
- fiatCurrency,
tokenBalanceInputPlaceholder,
+ tokenAmountLabel,
withMaxButton,
children,
defaultAmount,
@@ -48,10 +48,11 @@ export default function TokenAmountFormBase({
name={TOKEN_AMOUNT_FIELD_NAME}
tokenBalance={tokenBalance}
placeholder={tokenBalanceInputPlaceholder}
+ tokenAmountLabel={tokenAmountLabel}
currency={currency}
- fiatCurrency={fiatCurrency}
withMaxButton={withMaxButton}
defaultValue={defaultAmount}
+ autoFocus
/>
{children}
diff --git a/dapp/src/components/shared/TokenAmountForm/index.tsx b/dapp/src/components/shared/TokenAmountForm/index.tsx
index 1aa49ad9d..a1360ed8e 100644
--- a/dapp/src/components/shared/TokenAmountForm/index.tsx
+++ b/dapp/src/components/shared/TokenAmountForm/index.tsx
@@ -1,12 +1,13 @@
import { FormikErrors, withFormik } from "formik"
import { getErrorsObj, validateTokenAmount } from "#/utils"
-import { BaseFormProps } from "#/types"
+import { ActionFlowType, BaseFormProps } from "#/types"
import TokenAmountFormBase, {
TokenAmountFormBaseProps,
TokenAmountFormValues,
} from "./TokenAmountFormBase"
type TokenAmountFormProps = {
+ actionType: ActionFlowType
minTokenAmount: bigint
} & TokenAmountFormBaseProps &
BaseFormProps
@@ -16,10 +17,14 @@ const TokenAmountForm = withFormik(
mapPropsToValues: () => ({
amount: undefined,
}),
- validate: ({ amount }, { tokenBalance, currency, minTokenAmount }) => {
+ validate: (
+ { amount },
+ { tokenBalance, currency, minTokenAmount, actionType },
+ ) => {
const errors: FormikErrors = {}
errors.amount = validateTokenAmount(
+ actionType,
amount,
tokenBalance,
minTokenAmount,
@@ -31,6 +36,8 @@ const TokenAmountForm = withFormik(
handleSubmit: (values, { props }) => {
props.onSubmitForm(values)
},
+ validateOnBlur: false,
+ validateOnChange: false,
},
)(TokenAmountFormBase)
diff --git a/dapp/src/components/shared/TokenBalanceInput/index.tsx b/dapp/src/components/shared/TokenBalanceInput/index.tsx
index 9339c0781..456882fb2 100644
--- a/dapp/src/components/shared/TokenBalanceInput/index.tsx
+++ b/dapp/src/components/shared/TokenBalanceInput/index.tsx
@@ -1,4 +1,4 @@
-import React, { useRef } from "react"
+import React, { ChangeEventHandler, useRef } from "react"
import {
Box,
Button,
@@ -15,6 +15,7 @@ import {
import {
fixedPointNumberToString,
getCurrencyByType,
+ isFormError,
userAmountToBigInt,
} from "#/utils"
import { CurrencyType } from "#/types"
@@ -25,6 +26,7 @@ import NumberFormatInput, {
NumberFormatInputProps,
} from "../NumberFormatInput"
import { CurrencyBalance } from "../CurrencyBalance"
+import { Alert, AlertIcon, AlertDescription } from "../Alert"
const VARIANT = "balance"
@@ -42,7 +44,12 @@ function HelperErrorText({
if (hasError) {
return (
- {errorMsgText || "Please enter a valid value"}
+
+
+
+ {errorMsgText || "Please enter a valid value"}
+
+
)
}
@@ -102,6 +109,7 @@ export type TokenBalanceInputProps = {
fiatCurrency?: CurrencyType
setAmount: (value?: bigint) => void
withMaxButton?: boolean
+ tokenAmountLabel?: string
} & InputProps &
HelperErrorTextProps &
Pick
@@ -118,27 +126,37 @@ export default function TokenBalanceInput({
hasError = false,
fiatCurrency,
withMaxButton = false,
+ tokenAmountLabel = "Amount",
...inputProps
}: TokenBalanceInputProps) {
const valueRef = useRef(amount)
const styles = useMultiStyleConfig("TokenBalanceInput", { size })
- const { decimals } = getCurrencyByType(currency)
+ const { decimals, symbol } = getCurrencyByType(currency)
const handleValueChange = (value: string) => {
valueRef.current = value ? userAmountToBigInt(value, decimals) : undefined
}
+ const handleChange: ChangeEventHandler = () => {
+ setAmount(valueRef?.current)
+ }
+
+ const isBalanceExceeded =
+ typeof errorMsgText === "string" &&
+ isFormError("EXCEEDED_VALUE", errorMsgText)
+
return (
-
+
Amount
- Balance
+ {tokenAmountLabel}
handleValueChange(values.value)
}
- onChange={() => {
- setAmount(valueRef?.current)
- }}
+ onChange={handleChange}
decimalScale={decimals}
allowNegative={false}
+ integerScale={10}
/>
{withMaxButton && (
diff --git a/dapp/src/constants/errors.ts b/dapp/src/constants/errors.ts
index 2465ff22e..320b53195 100644
--- a/dapp/src/constants/errors.ts
+++ b/dapp/src/constants/errors.ts
@@ -1,4 +1,4 @@
-import { ConnectionErrorData } from "#/types"
+import { ACTION_FLOW_TYPES, ConnectionErrorData } from "#/types"
export const CONNECTION_ERRORS: Record = {
REJECTED: {
@@ -24,3 +24,19 @@ export const CONNECTION_ERRORS: Record = {
description: "We encountered an error. Please try again.",
},
}
+
+export const TOKEN_FORM_ERRORS = {
+ REQUIRED: "Please enter an amount.",
+ EXCEEDED_VALUE:
+ "The amount exceeds your current wallet balance. Add more funds to your wallet or lower the deposit amount.",
+ INSUFFICIENT_VALUE: (minValue: string) =>
+ `The amount is below the minimum required deposit of ${minValue} BTC.`,
+}
+
+export const ACTION_FORM_ERRORS = {
+ [ACTION_FLOW_TYPES.STAKE]: TOKEN_FORM_ERRORS,
+ [ACTION_FLOW_TYPES.UNSTAKE]: {
+ ...TOKEN_FORM_ERRORS,
+ EXCEEDED_VALUE: "Your Acre balance is insufficient.",
+ },
+}
diff --git a/dapp/src/theme/Button.ts b/dapp/src/theme/Button.ts
index a9e1e05f3..c6ac23059 100644
--- a/dapp/src/theme/Button.ts
+++ b/dapp/src/theme/Button.ts
@@ -28,6 +28,9 @@ export const buttonTheme: ComponentSingleStyleConfig = {
color: "white",
_hover: {
bg: "brand.500",
+ _disabled: {
+ bg: "grey.400",
+ },
},
_active: {
bg: "brand.400",
@@ -38,6 +41,10 @@ export const buttonTheme: ComponentSingleStyleConfig = {
opacity: 1,
},
},
+ _disabled: {
+ bg: "grey.500",
+ color: "gold.200",
+ },
},
outline: ({ colorScheme }: StyleFunctionProps) => {
const defaultStyles = {
diff --git a/dapp/src/theme/FormError.ts b/dapp/src/theme/FormError.ts
index d9a52b265..5cc44f3ad 100644
--- a/dapp/src/theme/FormError.ts
+++ b/dapp/src/theme/FormError.ts
@@ -4,7 +4,9 @@ import { formErrorAnatomy as parts } from "@chakra-ui/anatomy"
const baseStyleText = defineStyle({
fontWeight: "medium",
- color: "red.400",
+ color: "grey.700",
+ mt: 4,
+ fontSize: "md",
})
const multiStyleConfig = createMultiStyleConfigHelpers(parts.keys)
diff --git a/dapp/src/theme/Input.ts b/dapp/src/theme/Input.ts
index e6e9d8695..32ecb1269 100644
--- a/dapp/src/theme/Input.ts
+++ b/dapp/src/theme/Input.ts
@@ -8,6 +8,7 @@ const variantBalanceField = defineStyle({
fontWeight: "bold",
bg: "opacity.white.5",
paddingRight: 20,
+ minH: 14,
// TODO: Set the color correctly without using the chakra variable.
caretColor: "var(--chakra-colors-brand-400)",
@@ -17,7 +18,7 @@ const variantBalanceField = defineStyle({
},
_invalid: {
- color: "red.400",
+ borderColor: "red.400",
},
})
diff --git a/dapp/src/utils/forms.ts b/dapp/src/utils/forms.ts
index d1c8caf4f..6196f3b26 100644
--- a/dapp/src/utils/forms.ts
+++ b/dapp/src/utils/forms.ts
@@ -1,14 +1,8 @@
-import { CurrencyType } from "#/types"
+import { ACTION_FORM_ERRORS, TOKEN_FORM_ERRORS } from "#/constants"
+import { ActionFlowType, CurrencyType } from "#/types"
import { getCurrencyByType } from "./currency"
import { fixedPointNumberToString } from "./numbers"
-const ERRORS = {
- REQUIRED: "Required.",
- EXCEEDED_VALUE: "The amount exceeds your current balance.",
- INSUFFICIENT_VALUE: (minValue: string) =>
- `The minimum amount must be at least ${minValue} BTC.`,
-}
-
export function getErrorsObj(errors: { [key in keyof T]: string }) {
return (Object.keys(errors) as Array).every((name) => !errors[name])
? {}
@@ -16,23 +10,54 @@ export function getErrorsObj(errors: { [key in keyof T]: string }) {
}
export function validateTokenAmount(
+ actionType: ActionFlowType,
value: bigint | undefined,
maxValue: bigint,
minValue: bigint,
currency: CurrencyType,
): string | undefined {
- if (value === undefined) return ERRORS.REQUIRED
+ const ERRORS_BY_ACTION_TYPE = ACTION_FORM_ERRORS[actionType]
+
+ if (value === undefined) return ERRORS_BY_ACTION_TYPE.REQUIRED
const { decimals } = getCurrencyByType(currency)
const isMaximumValueExceeded = value > maxValue
const isMinimumValueFulfilled = value >= minValue
- if (isMaximumValueExceeded) return ERRORS.EXCEEDED_VALUE
+ if (isMaximumValueExceeded) return ERRORS_BY_ACTION_TYPE.EXCEEDED_VALUE
if (!isMinimumValueFulfilled)
- return ERRORS.INSUFFICIENT_VALUE(
+ return ERRORS_BY_ACTION_TYPE.INSUFFICIENT_VALUE(
fixedPointNumberToString(minValue, decimals),
)
return undefined
}
+
+type ParametrizedError = (value: number) => string
+export const isFormError = (
+ type: keyof typeof TOKEN_FORM_ERRORS,
+ message: string,
+) => {
+ let errorPredicates = [
+ ACTION_FORM_ERRORS.STAKE[type],
+ ACTION_FORM_ERRORS.UNSTAKE[type],
+ ]
+
+ const isParametrizedError = errorPredicates.every(
+ (predicate) => typeof predicate === "function",
+ )
+
+ if (isParametrizedError) {
+ const errorParameter = (message.match(/\d*\.\d+|\d+/g) ?? []).map(
+ parseFloat,
+ )[0]
+
+ // Already checked that all predicates are functions
+ errorPredicates = (errorPredicates as unknown as ParametrizedError[]).map(
+ (predicate) => predicate(errorParameter),
+ )
+ }
+
+ return errorPredicates.includes(message)
+}