diff --git a/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts b/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts index 12f43ee3e..0b1f97b6d 100644 --- a/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts +++ b/apps/api/src/api/services/phases/handlers/moonbeam-to-pendulum-handler.ts @@ -39,7 +39,7 @@ export class MoonbeamToPendulumPhaseHandler extends BasePhaseHandler { const { substrateEphemeralAddress, moonbeamXcmTransactionHash, squidRouterReceiverId, squidRouterReceiverHash } = state.state as StateMetadata; - if (!substrateEphemeralAddress || !squidRouterReceiverId || !squidRouterReceiverId || !squidRouterReceiverHash) { + if (!substrateEphemeralAddress || !squidRouterReceiverId || !squidRouterReceiverHash) { throw new Error("MoonbeamToPendulumPhaseHandler: State metadata corrupted. This is a bug."); } diff --git a/apps/frontend/package.json b/apps/frontend/package.json index fb638b03d..c30bb8458 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -51,6 +51,8 @@ "i18next": "^24.2.3", "lottie-react": "^2.4.1", "motion": "^12.0.3", + "numora": "^3.0.2", + "numora-react": "3.0.3", "qrcode.react": "^4.2.0", "react": "=19.2.0", "react-dom": "=19.2.0", diff --git a/apps/frontend/src/components/NumericInput/helpers.ts b/apps/frontend/src/components/NumericInput/helpers.ts deleted file mode 100644 index 46a5d7e00..000000000 --- a/apps/frontend/src/components/NumericInput/helpers.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { ChangeEvent, ClipboardEvent } from "react"; - -const removeNonNumericCharacters = (value: string): string => value.replace(/[^0-9.]/g, ""); - -const removeExtraDots = (value: string): string => value.replace(/(\..*?)\./g, "$1"); - -function sanitizeNumericInput(value: string): string { - return removeExtraDots(removeNonNumericCharacters(value)); -} - -export function trimToMaxDecimals(value: string, maxDecimals: number): string { - const [integer, decimal] = value.split("."); - return decimal ? `${integer}.${decimal.slice(0, maxDecimals)}` : value; -} - -const replaceCommasWithDots = (value: string): string => value.replace(/,/g, "."); - -/** - * Handles the input change event to ensure the value does not exceed the maximum number of decimal places, - * replaces commas with dots, and removes invalid non-numeric characters. - * - * @param e - The keyboard event triggered by the input. - * @param maxDecimals - The maximum number of decimal places allowed. - */ -export function handleOnChangeNumericInput(e: ChangeEvent, maxDecimals: number): void { - const target = e.target as HTMLInputElement; - - target.value = replaceCommasWithDots(target.value); - - target.value = sanitizeNumericInput(target.value); - - target.value = trimToMaxDecimals(target.value, maxDecimals); - - target.value = handleLeadingZeros(target.value); - - target.value = replaceInvalidOrEmptyString(target.value); -} - -function replaceInvalidOrEmptyString(value: string): string { - if (value === "" || value === ".") { - return "0"; - } - return value; -} - -function handleLeadingZeros(value: string): string { - if (Number(value) >= 1) { - return value.replace(/^0+/, ""); - } - - // Add leading zeros for numbers < 1 that don't start with '0' - if (Number(value) < 1 && value[0] !== "0") { - return "0" + value; - } - - // No more than one leading zero - return value.replace(/^0+/, "0"); -} - -/** - * Handles the paste event to ensure the value does not exceed the maximum number of decimal places, - * replaces commas with dots, and removes invalid non-numeric characters. - * - * @param e - The clipboard event triggered by the input. - * @param maxDecimals - The maximum number of decimal places allowed. - * @returns The sanitized value after the paste event. - */ - -export function handleOnPasteNumericInput(e: ClipboardEvent, maxDecimals: number): string { - const inputElement = e.target as HTMLInputElement; - const { value, selectionStart, selectionEnd } = inputElement; - - const clipboardData = sanitizeNumericInput(e.clipboardData?.getData("text/plain") || ""); - - const combinedValue = value.slice(0, selectionStart || 0) + clipboardData + value.slice(selectionEnd || 0); - - const [integerPart, ...decimalParts] = combinedValue.split("."); - const sanitizedValue = integerPart + (decimalParts.length > 0 ? "." + decimalParts.join("") : ""); - - e.preventDefault(); - inputElement.value = trimToMaxDecimals(sanitizedValue, maxDecimals); - inputElement.value = handleLeadingZeros(inputElement.value); - - const newCursorPosition = (selectionStart || 0) + clipboardData.length - (combinedValue.length - sanitizedValue.length); - inputElement.setSelectionRange(newCursorPosition, newCursorPosition); - - return trimToMaxDecimals(sanitizedValue, maxDecimals); -} diff --git a/apps/frontend/src/components/NumericInput/index.tsx b/apps/frontend/src/components/NumericInput/index.tsx index c2d58b5c7..83debf870 100644 --- a/apps/frontend/src/components/NumericInput/index.tsx +++ b/apps/frontend/src/components/NumericInput/index.tsx @@ -1,14 +1,13 @@ -import { ChangeEvent, useEffect, useRef } from "react"; +import { NumoraInput } from "numora-react"; +import { ChangeEvent } from "react"; import { UseFormRegisterReturn, useFormContext, useWatch } from "react-hook-form"; import { cn } from "../../helpers/cn"; -import { handleOnChangeNumericInput, handleOnPasteNumericInput, trimToMaxDecimals } from "./helpers"; interface NumericInputProps { register: UseFormRegisterReturn; readOnly?: boolean; additionalStyle?: string; maxDecimals?: number; - defaultValue?: string; autoFocus?: boolean; disabled?: boolean; loading?: boolean; @@ -26,36 +25,17 @@ export const NumericInput = ({ disabled = false }: NumericInputProps) => { const { setValue } = useFormContext(); - const fieldName = register.name; + const { name: fieldName, ref, onBlur } = register; const inputValue = useWatch({ name: fieldName }); - const prevMaxDecimals = useRef(maxDecimals); - function handleOnChange(e: ChangeEvent): void { - handleOnChangeNumericInput(e, maxDecimals); - const value = e.target.value; - setValue(fieldName, value, { shouldDirty: true, shouldValidate: true }); + function handleChange(e: ChangeEvent): void { + setValue(fieldName, e.target.value, { shouldDirty: true, shouldValidate: true }); if (onChange) onChange(e); - register.onChange(e); } - // Watch for maxDecimals changes and trim value if needed - useEffect(() => { - if (prevMaxDecimals.current > maxDecimals) { - const trimmed = trimToMaxDecimals(inputValue, maxDecimals); - if (trimmed !== inputValue) { - setValue(fieldName, trimmed, { shouldDirty: true, shouldValidate: true }); - // Create a synthetic event for register.onChange - const syntheticEvent = { target: { value: trimmed } } as ChangeEvent; - register.onChange(syntheticEvent); - } - } - prevMaxDecimals.current = maxDecimals; - }, [maxDecimals, inputValue, setValue, fieldName, register]); - return (
- handleOnPasteNumericInput(event, maxDecimals)} - pattern="^[0-9]*[.,]?[0-9]*$" + maxDecimals={maxDecimals} + name={fieldName} + onBlur={onBlur} + onChange={handleChange} placeholder="0.0" readOnly={readOnly} + ref={ref} spellCheck={false} - step="any" - type="text" value={inputValue ?? ""} /> - {loading && ( - - )} + {loading && }
); }; diff --git a/bun.lock b/bun.lock index 547a1b13e..ace8a13a1 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "big.js": "^7.0.1", "husky": "^9.1.7", "lint-staged": "^16.1.0", + "numora-react": "^3.0.3", }, "devDependencies": { "@biomejs/biome": "2.0.0", @@ -152,6 +153,8 @@ "i18next": "^24.2.3", "lottie-react": "^2.4.1", "motion": "^12.0.3", + "numora": "^3.0.2", + "numora-react": "3.0.3", "qrcode.react": "^4.2.0", "react": "=19.2.0", "react-dom": "=19.2.0", @@ -3181,6 +3184,10 @@ "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], + "numora": ["numora@3.0.2", "", {}, "sha512-L1EQPudBSbX1G1v4vWtbdTaZrhJhexwffslc8TtcKXaN0ZNzo/+oPPyRNtVx3HWlDmRccTxlAiE+A4K5BGsuCQ=="], + + "numora-react": ["numora-react@3.0.3", "", {}, "sha512-42wqglFsDZNsYUwy09yBS5Kd1U0ZmBiKFpJDmdzAPnXZd4MmZfRA7NyTMO7uTrpQYCTkc5URJ/l56K1f5ISJWg=="], + "obj-multiplex": ["obj-multiplex@1.0.0", "", { "dependencies": { "end-of-stream": "^1.4.0", "once": "^1.4.0", "readable-stream": "^2.3.3" } }, "sha512-0GNJAOsHoBHeNTvl5Vt6IWnpUEcc3uSRxzBri7EDyIcMgYvnY2JL2qdeV5zTMjWQX5OHcD5amcW2HFfDh0gjIA=="], "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], diff --git a/package.json b/package.json index 8619a5ed5..01e142a5b 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,8 @@ "dependencies": { "big.js": "^7.0.1", "husky": "^9.1.7", - "lint-staged": "^16.1.0" + "lint-staged": "^16.1.0", + "numora-react": "^3.0.3" }, "devDependencies": { "@biomejs/biome": "2.0.0",