From 615cc777e3477da5c8c94c02ad20065079a81eca Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Wed, 21 Jan 2026 22:57:59 +0100 Subject: [PATCH 1/5] implement numora config and numora input --- apps/frontend/package.json | 1 + .../src/components/NumericInput/helpers.ts | 88 ------------------- .../src/components/NumericInput/index.tsx | 32 +++---- bun.lock | 3 + 4 files changed, 18 insertions(+), 106 deletions(-) delete mode 100644 apps/frontend/src/components/NumericInput/helpers.ts diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 5f2dbfce0..e3981f805 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -50,6 +50,7 @@ "i18next": "^24.2.3", "lottie-react": "^2.4.1", "motion": "^12.0.3", + "numora-react": "^3.0.2", "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..2edaee095 100644 --- a/apps/frontend/src/components/NumericInput/index.tsx +++ b/apps/frontend/src/components/NumericInput/index.tsx @@ -1,7 +1,7 @@ +import { NumoraInput } from "numora-react"; import { ChangeEvent, useEffect, useRef } 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; @@ -15,6 +15,11 @@ interface NumericInputProps { onChange?: (e: ChangeEvent) => void; } +function trimToMaxDecimals(value: string, maxDecimals: number): string { + const [integer, decimal] = value.split("."); + return decimal ? `${integer}.${decimal.slice(0, maxDecimals)}` : value; +} + export const NumericInput = ({ register, readOnly = false, @@ -30,21 +35,19 @@ export const NumericInput = ({ const inputValue = useWatch({ name: fieldName }); const prevMaxDecimals = useRef(maxDecimals); - function handleOnChange(e: ChangeEvent): void { - handleOnChangeNumericInput(e, maxDecimals); + function handleChange(e: ChangeEvent): void { const value = e.target.value; setValue(fieldName, value, { shouldDirty: true, shouldValidate: true }); - if (onChange) onChange(e); register.onChange(e); + if (onChange) onChange(e); } // Watch for maxDecimals changes and trim value if needed useEffect(() => { - if (prevMaxDecimals.current > maxDecimals) { + if (prevMaxDecimals.current > maxDecimals && inputValue) { 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); } @@ -54,8 +57,7 @@ export const NumericInput = ({ return (
- handleOnPasteNumericInput(event, maxDecimals)} - pattern="^[0-9]*[.,]?[0-9]*$" + maxDecimals={maxDecimals} + name={fieldName} + onChange={handleChange} placeholder="0.0" readOnly={readOnly} spellCheck={false} - step="any" - type="text" value={inputValue ?? ""} /> - {loading && ( - - )} + {loading && }
); }; diff --git a/bun.lock b/bun.lock index a17c19278..6c861303d 100644 --- a/bun.lock +++ b/bun.lock @@ -151,6 +151,7 @@ "i18next": "^24.2.3", "lottie-react": "^2.4.1", "motion": "^12.0.3", + "numora-react": "^3.0.2", "qrcode.react": "^4.2.0", "react": "=19.2.0", "react-dom": "=19.2.0", @@ -3168,6 +3169,8 @@ "nullthrows": ["nullthrows@1.1.1", "", {}, "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw=="], + "numora-react": ["numora-react@3.0.2", "", {}, "sha512-PsWjhFsvk1AxB4C2jA8Lj4pNJ5fvCEIjJtop48gxJjZyPjfOZBqLzdt4UwA6+rogOc/gYeMhCzS9uyjGKQBpjA=="], + "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=="], From e974c6afab5bc676b4cb820ed575f460e60c6e9e Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Mon, 9 Feb 2026 18:28:42 +0100 Subject: [PATCH 2/5] fix numora event handling --- apps/frontend/src/components/NumericInput/index.tsx | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/apps/frontend/src/components/NumericInput/index.tsx b/apps/frontend/src/components/NumericInput/index.tsx index 2edaee095..99c6a7f51 100644 --- a/apps/frontend/src/components/NumericInput/index.tsx +++ b/apps/frontend/src/components/NumericInput/index.tsx @@ -8,7 +8,6 @@ interface NumericInputProps { readOnly?: boolean; additionalStyle?: string; maxDecimals?: number; - defaultValue?: string; autoFocus?: boolean; disabled?: boolean; loading?: boolean; @@ -31,14 +30,12 @@ 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 handleChange(e: ChangeEvent): void { - const value = e.target.value; - setValue(fieldName, value, { shouldDirty: true, shouldValidate: true }); - register.onChange(e); + setValue(fieldName, e.target.value, { shouldDirty: true, shouldValidate: true }); if (onChange) onChange(e); } @@ -48,12 +45,10 @@ export const NumericInput = ({ const trimmed = trimToMaxDecimals(inputValue, maxDecimals); if (trimmed !== inputValue) { setValue(fieldName, trimmed, { shouldDirty: true, shouldValidate: true }); - const syntheticEvent = { target: { value: trimmed } } as ChangeEvent; - register.onChange(syntheticEvent); } } prevMaxDecimals.current = maxDecimals; - }, [maxDecimals, inputValue, setValue, fieldName, register]); + }, [maxDecimals, inputValue, setValue, fieldName]); return (
@@ -70,9 +65,11 @@ export const NumericInput = ({ disabled={disabled} maxDecimals={maxDecimals} name={fieldName} + onBlur={onBlur} onChange={handleChange} placeholder="0.0" readOnly={readOnly} + ref={ref} spellCheck={false} value={inputValue ?? ""} /> From ae8f0aef890b2b3d2d8f51e4af934c916a633dac Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Tue, 10 Feb 2026 08:19:06 +0100 Subject: [PATCH 3/5] remove double check --- .../services/phases/handlers/moonbeam-to-pendulum-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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."); } From f8a43a709b3a0e816216c6cf3beffd5a9ae4dc70 Mon Sep 17 00:00:00 2001 From: Kacper Szarkiewicz Date: Tue, 10 Feb 2026 08:19:18 +0100 Subject: [PATCH 4/5] update numora-react version --- apps/frontend/package.json | 2 +- .../src/components/NumericInput/index.tsx | 19 +------------------ bun.lock | 5 +++-- package.json | 3 ++- 4 files changed, 7 insertions(+), 22 deletions(-) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index 73b66b5fa..f9b06ffd2 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -51,7 +51,7 @@ "i18next": "^24.2.3", "lottie-react": "^2.4.1", "motion": "^12.0.3", - "numora-react": "^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/index.tsx b/apps/frontend/src/components/NumericInput/index.tsx index 99c6a7f51..83debf870 100644 --- a/apps/frontend/src/components/NumericInput/index.tsx +++ b/apps/frontend/src/components/NumericInput/index.tsx @@ -1,5 +1,5 @@ import { NumoraInput } from "numora-react"; -import { ChangeEvent, useEffect, useRef } from "react"; +import { ChangeEvent } from "react"; import { UseFormRegisterReturn, useFormContext, useWatch } from "react-hook-form"; import { cn } from "../../helpers/cn"; @@ -14,11 +14,6 @@ interface NumericInputProps { onChange?: (e: ChangeEvent) => void; } -function trimToMaxDecimals(value: string, maxDecimals: number): string { - const [integer, decimal] = value.split("."); - return decimal ? `${integer}.${decimal.slice(0, maxDecimals)}` : value; -} - export const NumericInput = ({ register, readOnly = false, @@ -32,24 +27,12 @@ export const NumericInput = ({ const { setValue } = useFormContext(); const { name: fieldName, ref, onBlur } = register; const inputValue = useWatch({ name: fieldName }); - const prevMaxDecimals = useRef(maxDecimals); function handleChange(e: ChangeEvent): void { setValue(fieldName, e.target.value, { shouldDirty: true, shouldValidate: true }); if (onChange) onChange(e); } - // Watch for maxDecimals changes and trim value if needed - useEffect(() => { - if (prevMaxDecimals.current > maxDecimals && inputValue) { - const trimmed = trimToMaxDecimals(inputValue, maxDecimals); - if (trimmed !== inputValue) { - setValue(fieldName, trimmed, { shouldDirty: true, shouldValidate: true }); - } - } - prevMaxDecimals.current = maxDecimals; - }, [maxDecimals, inputValue, setValue, fieldName]); - return (
Date: Tue, 10 Feb 2026 08:59:14 +0100 Subject: [PATCH 5/5] fix package version --- apps/frontend/package.json | 1 + bun.lock | 3 +++ 2 files changed, 4 insertions(+) diff --git a/apps/frontend/package.json b/apps/frontend/package.json index f9b06ffd2..c30bb8458 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -51,6 +51,7 @@ "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", diff --git a/bun.lock b/bun.lock index c2403a4b6..ace8a13a1 100644 --- a/bun.lock +++ b/bun.lock @@ -153,6 +153,7 @@ "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", @@ -3183,6 +3184,8 @@ "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=="],