diff --git a/dapps/pos-app/app/amount.tsx b/dapps/pos-app/app/amount.tsx index 66a327f2c..f6703a602 100644 --- a/dapps/pos-app/app/amount.tsx +++ b/dapps/pos-app/app/amount.tsx @@ -12,13 +12,9 @@ import { getCurrency, } from "@/utils/currency"; import { router } from "expo-router"; -import { Controller, useForm, useWatch } from "react-hook-form"; +import { useCallback, useState } from "react"; import { Platform, StyleSheet, View } from "react-native"; -interface FormData { - amount: string; -} - const formatAmount = (amount: string) => { if (!amount.includes(".")) { return `${amount}.00`; @@ -36,6 +32,21 @@ const formatAmount = (amount: string) => { return `${whole}.${paddedDecimal}`; }; +const getNextAmount = (prev: string, key: string) => { + if (key === "erase") return prev.slice(0, -1); + if (key === ".") { + if (prev.includes(".")) return prev; // Don't add multiple decimal separators + return prev === "" ? "0." : `${prev}.`; + } + // Limit to 2 decimal places + const decimalPart = prev.split(".")[1]; + if (decimalPart !== undefined && decimalPart.length >= 2) return prev; + const next = prev === "0" ? key : prev + key; + return exceedsU64Max(next) ? prev : next; +}; + +const isValidAmount = (amount: string) => !!amount && Number(amount) !== 0; + export default function AmountScreen() { const Theme = useTheme(); const testMode = useSettingsStore((state) => state.testMode); @@ -43,24 +54,20 @@ export default function AmountScreen() { const isTablet = useIsTablet(); const currencyCode = useSettingsStore((state) => state.currency); const currency = getCurrency(currencyCode); - const { - control, - handleSubmit, - formState: { isValid }, - } = useForm({ - defaultValues: { - amount: "", - }, - }); - const watchAmount = useWatch({ control, name: "amount" }); + const [amount, setAmount] = useState(""); + const isValid = isValidAmount(amount); - const onSubmit = ({ amount }: FormData) => { - const formattedAmount = formatAmount(amount); + // Stable identity so the memoized NumericKeyboard doesn't re-render per key. + const handleKeyPress = useCallback((key: string) => { + setAmount((prev) => getNextAmount(prev, key)); + }, []); + const onSubmit = () => { + if (!isValid) return; router.push({ pathname: "/scan", params: { - amount: formattedAmount, + amount: formatAmount(amount), }, }); }; @@ -76,68 +83,24 @@ export default function AmountScreen() { > - { - if ( - !value || - value === "0" || - value === "" || - Number(value) === 0 - ) { - return "Amount is required"; - } - return true; - }, - }} - render={({ field: { onChange, value: prev } }) => ( - { - let newDisplay; - if (key === "erase") { - newDisplay = prev?.slice(0, -1) || ""; - onChange?.(newDisplay); - } else if (key === ".") { - if (prev.includes(".")) return; // Don't add multiple decimal separators - if (prev === "") { - newDisplay = "0."; - } else { - newDisplay = prev + "."; - } - onChange?.(newDisplay); - } else { - // Limit to 2 decimal places - if (prev.includes(".")) { - const decimalPart = prev.split(".")[1] || ""; - if (decimalPart.length >= 2) return; - } - const newDisplay = prev === "0" ? key : prev + key; - if (exceedsU64Max(newDisplay)) return; - onChange?.(newDisplay); - } - }} - /> - )} - /> + diff --git a/dapps/pos-app/components/big-amount-input/components/AnimatedCharacter.tsx b/dapps/pos-app/components/big-amount-input/components/AnimatedCharacter.tsx index fd0d91c91..367dd1521 100644 --- a/dapps/pos-app/components/big-amount-input/components/AnimatedCharacter.tsx +++ b/dapps/pos-app/components/big-amount-input/components/AnimatedCharacter.tsx @@ -1,6 +1,6 @@ import type { CharacterItem } from "../utils/getCharactersArray"; import { memo, useEffect, useRef } from "react"; -import { StyleSheet } from "react-native"; +import { StyleSheet, Text } from "react-native"; import Animated, { Easing, useAnimatedStyle, @@ -113,9 +113,9 @@ function AnimatedCharacterComponent({ animatedStyle, ]} > - + {item.char} - + ); diff --git a/dapps/pos-app/components/big-amount-input/utils/formatAmount.ts b/dapps/pos-app/components/big-amount-input/utils/formatAmount.ts index fe77797e1..fb7d1b0a1 100644 --- a/dapps/pos-app/components/big-amount-input/utils/formatAmount.ts +++ b/dapps/pos-app/components/big-amount-input/utils/formatAmount.ts @@ -12,12 +12,23 @@ export type FormatAmountOptions = { const DEFAULT_LOCALE: SupportedLocale = "en-US"; +// Separators are fixed per locale, and constructing Intl.NumberFormat is slow on +// Android Hermes (ICU locale resolution over JNI), so resolve each locale once +// instead of on every keypress. +const decimalSeparators = new Map(); +const groupSeparators = new Map(); + export function getDecimalSeparator( locale: SupportedLocale = DEFAULT_LOCALE, ): string { - const numberWithDecimal = 1.1; - const formatted = new Intl.NumberFormat(locale).format(numberWithDecimal); - return formatted.charAt(1); + let separator = decimalSeparators.get(locale); + if (separator === undefined) { + const numberWithDecimal = 1.1; + const formatted = new Intl.NumberFormat(locale).format(numberWithDecimal); + separator = formatted.charAt(1); + decimalSeparators.set(locale, separator); + } + return separator; } export function getDefaultLocale(): SupportedLocale { @@ -32,11 +43,15 @@ export function parseRawValue(rawValue: string): number { } function getGroupSeparator(locale: SupportedLocale): string { - // Avoid formatToParts — not supported in Hermes - const formatted = new Intl.NumberFormat(locale).format(10000); - // Strip all digits — what remains is the group separator (e.g. "," or ".") - const sep = formatted.replace(/\d/g, ""); - return sep.charAt(0) || ","; + let separator = groupSeparators.get(locale); + if (separator === undefined) { + // Avoid formatToParts — not supported in Hermes + const formatted = new Intl.NumberFormat(locale).format(10000); + // Strip all digits — what remains is the group separator (e.g. "," or ".") + separator = formatted.replace(/\d/g, "").charAt(0) || ","; + groupSeparators.set(locale, separator); + } + return separator; } function addThousandsSeparators(integerStr: string, separator: string): string { diff --git a/dapps/pos-app/components/numeric-keyboard.tsx b/dapps/pos-app/components/numeric-keyboard.tsx index ed5fc7f54..bc9168d38 100644 --- a/dapps/pos-app/components/numeric-keyboard.tsx +++ b/dapps/pos-app/components/numeric-keyboard.tsx @@ -13,24 +13,21 @@ export interface NumericKeyboardProps { style?: StyleProp; } +const KEYS = [ + ["1", "2", "3"], + ["4", "5", "6"], + ["7", "8", "9"], + [".", "0", "erase"], +]; + function NumericKeyboardBase({ onKeyPress, style }: NumericKeyboardProps) { const Theme = useTheme(); const isTablet = useIsTablet(); const [assets] = useAssets([require("@/assets/images/backspace.png")]); - const keys = [ - ["1", "2", "3"], - ["4", "5", "6"], - ["7", "8", "9"], - [".", "0", "erase"], - ]; - - const handlePress = (key: string) => { - onKeyPress(key); - }; return ( - {keys.map((row, rowIndex) => ( + {KEYS.map((row, rowIndex) => ( ( handlePress(key)} + onPress={() => onKeyPress(key)} testID={key === "." ? "key-decimal" : `key-${key}`} accessibilityRole="button" accessibilityLabel={key === "erase" ? "Backspace" : key} diff --git a/dapps/pos-app/package-lock.json b/dapps/pos-app/package-lock.json index 37542b925..ce1e43087 100644 --- a/dapps/pos-app/package-lock.json +++ b/dapps/pos-app/package-lock.json @@ -36,7 +36,6 @@ "pressto": "0.6.1", "react": "19.2.3", "react-dom": "19.2.3", - "react-hook-form": "7.71.2", "react-native": "0.85.3", "react-native-device-info": "14.1.1", "react-native-gesture-handler": "2.31.2", @@ -14093,22 +14092,6 @@ "react": ">=17.0.0" } }, - "node_modules/react-hook-form": { - "version": "7.71.2", - "resolved": "https://registry.npmjs.org/react-hook-form/-/react-hook-form-7.71.2.tgz", - "integrity": "sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==", - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/react-hook-form" - }, - "peerDependencies": { - "react": "^16.8.0 || ^17 || ^18 || ^19" - } - }, "node_modules/react-is": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", diff --git a/dapps/pos-app/package.json b/dapps/pos-app/package.json index 963437516..4a6013fd8 100644 --- a/dapps/pos-app/package.json +++ b/dapps/pos-app/package.json @@ -25,6 +25,11 @@ "lint": "expo lint", "test": "jest" }, + "reanimated": { + "staticFeatureFlags": { + "ANDROID_SYNCHRONOUSLY_UPDATE_UI_PROPS": true + } + }, "dependencies": { "@rnrepo/expo-config-plugin": "0.3.6", "@sentry/react-native": "8.23.0", @@ -53,7 +58,6 @@ "pressto": "0.6.1", "react": "19.2.3", "react-dom": "19.2.3", - "react-hook-form": "7.71.2", "react-native": "0.85.3", "react-native-device-info": "14.1.1", "react-native-gesture-handler": "2.31.2",