Skip to content

Commit

Permalink
fix: restrict NumberInput to Decimal characters only (#213)
Browse files Browse the repository at this point in the history
* fix: restrict NumberInput to decimal characters only

- wrapped `Numeric.parse` with try/catch globally

* feat: add custom options to `Numeric`

* chore: rename overloadXplaNumeric to overrideXplaNumeric
  • Loading branch information
honeymaro authored Feb 25, 2024
1 parent 5f4683a commit efc6e69
Show file tree
Hide file tree
Showing 9 changed files with 225 additions and 131 deletions.
132 changes: 99 additions & 33 deletions src/components/Input/index.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { forwardRef, useEffect, useImperativeHandle, useRef } from "react";
import { forwardRef, useCallback, useRef } from "react";
import styled from "@emotion/styled";
import { css } from "@emotion/react";
import { formatDecimals } from "utils";
import { sanitizeNumberInput } from "utils";
import { MOBILE_SCREEN_CLASS } from "constants/layout";
import { Numeric } from "@xpla/xpla.js";

type InputVariant = "default" | "base" | "primary";
type InputSize = "default" | "large";
Expand Down Expand Up @@ -184,43 +185,108 @@ export interface NumberInputProps extends InputProps {
}

export const NumberInput = forwardRef<HTMLInputElement, NumberInputProps>(
({ decimals = 18, ...InputProps }, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => inputRef.current as HTMLInputElement);
useEffect(() => {
const elInput = inputRef.current;
const handleKeydown = (event: Event) => {
const target = event.target as HTMLInputElement;
({ decimals = 18, onKeyDown, onKeyUp, onChange, ...InputProps }, ref) => {
const isIMEActive = useRef(false);
const isKeyPressing = useRef(false);
const lastValue = useRef<string | undefined>(undefined);

target.value = target.value.replace(/[^0-9.]/g, "");

if (target.value?.split(".").length > 2) {
event.preventDefault();
const index = target.value.lastIndexOf(".");
target.value =
target.value.substring(0, index) +
target.value.substring(index + 1, target.value.length);
}
const handleInput = useCallback<React.FormEventHandler<HTMLInputElement>>(
(event) => {
const target = event.currentTarget;
if (
target.value.includes(".") &&
(target.value?.split(".").pop()?.length || 0) > decimals
isIMEActive.current ||
!isKeyPressing.current ||
target.value.split(".").length > 2
) {
if (lastValue.current) {
target.value = lastValue.current;
}
}
const sanitizedValue = sanitizeNumberInput(target.value, decimals);
if (target.value !== sanitizedValue) {
event.preventDefault();
target.value = formatDecimals(target.value, decimals);
target.value = sanitizedValue;
}
},
[decimals],
);

const handleKeyDown = useCallback<
React.KeyboardEventHandler<HTMLInputElement>
>(
(event) => {
const target = event.currentTarget;
isKeyPressing.current = true;
isIMEActive.current = event.key === "Process";
lastValue.current = target.value;
if (isIMEActive.current) {
event.preventDefault();
}
if (event.key.length === 1 && !event.ctrlKey) {
const regex = /[^0-9.]/g;
const isAllowedKey = !regex.test(event.key);
const selectedText = target.value?.substring(
target.selectionStart || 0,
target.selectionEnd || 0,
);
if (
!isAllowedKey ||
(event.key === "." &&
event.currentTarget.value?.includes(".") &&
!selectedText.includes("."))
) {
event.preventDefault();
}
}
};
elInput?.addEventListener("keydown", handleKeydown);
elInput?.addEventListener("keyup", handleKeydown);
elInput?.addEventListener("keypress", handleKeydown);
return () => {
if (elInput) {
elInput.removeEventListener("keydown", handleKeydown);
elInput.removeEventListener("keyup", handleKeydown);
elInput.removeEventListener("keypress", handleKeydown);
if (onKeyDown) {
onKeyDown(event);
}
};
}, [decimals]);
},
[onKeyDown],
);

const handleKeyUp = useCallback<
React.KeyboardEventHandler<HTMLInputElement>
>(
(event) => {
isKeyPressing.current = false;
if (onKeyUp) {
onKeyUp(event);
}
},
[onKeyUp],
);

return <Input ref={inputRef} {...InputProps} />;
const handleChange = useCallback<
React.ChangeEventHandler<HTMLInputElement>
>(
(event) => {
const target = event.currentTarget;
if (target.value) {
try {
Numeric.parse(target.value, { throwOnError: true });
if (onChange) {
onChange(event);
}
} catch (error) {
event.preventDefault();
}
}
},
[onChange],
);

return (
<Input
ref={ref}
onInput={handleInput}
onKeyUp={handleKeyUp}
onKeyDown={handleKeyDown}
onChange={handleChange}
pattern="^[0-9]*[.]?[0-9]*$"
inputMode="decimal"
{...InputProps}
/>
);
},
);
1 change: 1 addition & 0 deletions src/main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import "simplebar";
import "simplebar/dist/simplebar.css";
import ResizeObserver from "resize-observer-polyfill";
import "utils/overrideXplaNumeric";

window.ResizeObserver = ResizeObserver;

Expand Down
4 changes: 2 additions & 2 deletions src/pages/Earn/Lockdrop/Stake/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {
amountToValue,
cutDecimal,
ellipsisCenter,
filterNumberFormat,
sanitizeNumberInput,
formatDateTime,
formatNumber,
getTokenLink,
Expand Down Expand Up @@ -252,7 +252,7 @@ function StakePage() {
form.setValue(FormKey.lpValue, value);
}}
{...register(FormKey.lpValue, {
setValueAs: (value) => filterNumberFormat(value, LP_DECIMALS),
setValueAs: (value) => sanitizeNumberInput(value, LP_DECIMALS),
required: true,
})}
/>
Expand Down
26 changes: 18 additions & 8 deletions src/pages/Earn/Pools/Provide/InputGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import useBalance from "hooks/useBalance";
import { Numeric } from "@xpla/xpla.js";
import { UseControllerProps, useController } from "react-hook-form";
import useDashboardTokenDetail from "hooks/dashboard/useDashboardTokenDetail";
import { useMemo } from "react";
import AssetValueFormatter from "components/utils/AssetValueFormatter";

interface InputGroupProps extends NumberInputProps {
Expand Down Expand Up @@ -59,6 +60,22 @@ function InputGroup({
const balance = useBalance(asset?.token);
const dashboardToken = useDashboardTokenDetail(asset?.token || "");

const expectedUsdValue = useMemo(() => {
try {
if (dashboardToken?.price && field.value) {
return `= $${formatNumber(
formatDecimals(
Numeric.parse(dashboardToken?.price || 0).mul(field.value),
2,
),
)}`;
}
} catch (error) {
console.log(error);
}
return "-";
}, [dashboardToken, field.value]);

return (
<Box style={style}>
<Row justify="between" align="center" style={{ gap: 3, marginBottom: 5 }}>
Expand Down Expand Up @@ -134,14 +151,7 @@ function InputGroup({
text-align: right;
`}
>
{dashboardToken?.price && field.value
? `= $${formatNumber(
formatDecimals(
Numeric.parse(dashboardToken?.price || 0).mul(field.value),
2,
),
)}`
: "-"}
{expectedUsdValue}
</Typography>
</Col>
</Row>
Expand Down
74 changes: 41 additions & 33 deletions src/pages/Earn/Pools/Provide/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,8 @@ function ProvidePage() {
formData.asset1Value &&
asset2?.token &&
formData.asset2Value &&
Number(formData.asset1Value) &&
Number(formData.asset2Value) &&
!Numeric.parse(formData.asset1Value).isNaN() &&
!Numeric.parse(formData.asset2Value).isNaN()
? {
Expand Down Expand Up @@ -208,36 +210,41 @@ function ProvidePage() {
const asset2BalanceMinusFee = useBalanceMinusFee(asset2?.token, feeAmount);

const buttonMsg = useMemo(() => {
if (formData.asset1Value) {
if (
Numeric.parse(formData.asset1Value).gt(
Numeric.parse(
amountToValue(asset1BalanceMinusFee, asset1?.decimals) || 0,
),
)
) {
return `Insufficient ${asset1?.symbol} balance`;
}
try {
if (formData.asset1Value) {
if (
Numeric.parse(formData.asset1Value).gt(
Numeric.parse(
amountToValue(asset1BalanceMinusFee, asset1?.decimals) || 0,
),
)
) {
return `Insufficient ${asset1?.symbol} balance`;
}

if (
formData.asset2Value &&
Numeric.parse(formData.asset2Value).gt(
Numeric.parse(
amountToValue(asset2BalanceMinusFee, asset2?.decimals) || 0,
),
)
) {
return `Insufficient ${asset2?.symbol} balance`;
}
if (
formData.asset2Value &&
Numeric.parse(formData.asset2Value).gt(
Numeric.parse(
amountToValue(asset2BalanceMinusFee, asset2?.decimals) || 0,
),
)
) {
return `Insufficient ${asset2?.symbol} balance`;
}

if (formData.asset1Value && !formData.asset2Value) {
return `Enter ${asset2?.symbol} amount`;
if (formData.asset1Value && !formData.asset2Value) {
return `Enter ${asset2?.symbol} amount`;
}
return "Add";
}
return "Add";
}

if (formData.asset2Value && !formData.asset1Value) {
return `Enter ${asset1?.symbol} amount`;
if (formData.asset2Value && !formData.asset1Value) {
return `Enter ${asset1?.symbol} amount`;
}
} catch (error) {
console.log(error);
return "Enter an amount";
}

return "Enter an amount";
Expand Down Expand Up @@ -321,7 +328,7 @@ function ProvidePage() {
}, [asset2BalanceMinusFee, formData.asset2Value, form]);

useEffect(() => {
if (simulationResult && !simulationResult.isLoading && !isPoolEmpty) {
if (!simulationResult?.isLoading && !isPoolEmpty) {
form.setValue(
isReversed ? FormKey.asset1Value : FormKey.asset2Value,
amountToValue(
Expand Down Expand Up @@ -360,7 +367,7 @@ function ProvidePage() {
},
}}
asset={asset1}
onClick={() => {
onFocus={() => {
setIsReversed(false);
setBalanceApplied(false);
}}
Expand Down Expand Up @@ -397,12 +404,12 @@ function ProvidePage() {
},
}}
asset={asset2}
onClick={() => {
setIsReversed(false);
onFocus={() => {
setIsReversed(true);
setBalanceApplied(false);
}}
onBalanceClick={(value) => {
setIsReversed(false);
setIsReversed(true);
setBalanceApplied(true);
form.setValue(FormKey.asset2Value, value, {
shouldValidate: true,
Expand Down Expand Up @@ -442,7 +449,7 @@ function ProvidePage() {
label={
<Typography weight="bold">
{`1${asset1?.symbol} = ${
formData.asset1Value && formData.asset2Value
Number(formData.asset1Value) && Number(formData.asset2Value)
? cutDecimal(
Numeric.parse(formData.asset2Value || 0)
.div(formData.asset1Value || 1)
Expand Down Expand Up @@ -785,7 +792,8 @@ function ProvidePage() {
form.formState.isValidating ||
simulationResult.isLoading ||
isFeeLoading ||
isFeeFailed
isFeeFailed ||
buttonMsg !== "Add"
}
css={css`
margin-bottom: 10px;
Expand Down
Loading

0 comments on commit efc6e69

Please sign in to comment.