diff --git a/package-lock.json b/package-lock.json index aa6b722..e627b1d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "daisyui": "^2.20.0", "date-fns": "^2.29.1", "froebel": "^0.18.0", + "ibantools": "^4.1.6", "myzod": "^1.8.7", "nanoid": "^4.0.0", "node-iso11649": "^2.1.2", @@ -4249,6 +4250,12 @@ "node": ">=10.17.0" } }, + "node_modules/ibantools": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/ibantools/-/ibantools-4.1.6.tgz", + "integrity": "sha512-BpIqMJj6tgFbx1YmH7tjstiqf8VgLkGL2d3K7XSOuGxbdy6gv6ovKjC/Yqgrh3OEeqeSzx8fVu5P96Q1bWg2bg==", + "dev": true + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -11180,6 +11187,12 @@ "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==" }, + "ibantools": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/ibantools/-/ibantools-4.1.6.tgz", + "integrity": "sha512-BpIqMJj6tgFbx1YmH7tjstiqf8VgLkGL2d3K7XSOuGxbdy6gv6ovKjC/Yqgrh3OEeqeSzx8fVu5P96Q1bWg2bg==", + "dev": true + }, "indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", diff --git a/package.json b/package.json index 0c901c1..a799ed2 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "daisyui": "^2.20.0", "date-fns": "^2.29.1", "froebel": "^0.18.0", + "ibantools": "^4.1.6", "myzod": "^1.8.7", "nanoid": "^4.0.0", "node-iso11649": "^2.1.2", diff --git a/src/components/Form.tsx b/src/components/Form.tsx index c90eed2..23fc663 100644 --- a/src/components/Form.tsx +++ b/src/components/Form.tsx @@ -28,6 +28,7 @@ export const TextInput: Component< suffix?: string | JSX.Element; size?: string; vertical?: boolean; + invalidate?: (v: any) => string | boolean; } & JSX.InputHTMLAttributes > = (p) => { p = mergeProps( @@ -48,6 +49,7 @@ export const TextInput: Component< "suffix", "vertical", "labelMinWidth", + "invalidate", ]); const sizes: Record = { xs: "input-xs", @@ -55,7 +57,10 @@ export const TextInput: Component< lg: "input-lg", }; - const [validate, vState] = validateInput({ value: () => rest.value }); + const [validate, vState] = validateInput({ + invalidate: props.invalidate, + value: () => rest.value, + }); return (
diff --git a/src/components/Settings/Overlay.tsx b/src/components/Settings/Overlay.tsx index 1a7704d..b5c1cae 100644 --- a/src/components/Settings/Overlay.tsx +++ b/src/components/Settings/Overlay.tsx @@ -17,6 +17,7 @@ import z from "myzod"; import Big from "big.js"; import { generate } from "node-iso11649"; import { customAlphabet } from "nanoid"; +import { isValidIBAN } from "ibantools"; import createAccordion from "../Accordion"; import { @@ -534,6 +535,17 @@ const SettingsOverlay: Component = () => { label="Iban" maxLength={50} value={localState.iban} + invalidate={(v) => { + // If the value is empty, the HTML5 "required" attribute will do its own validation, we can skip the IBAN check + if (v === "") { + return false; + } + + return ( + !isValidIBAN(v.replaceAll(" ", "")) && + "Diese IBAN ist womöglich ungültig." + ); + }} onInput={(evt) => setLocalState("iban", evt.currentTarget.value) } diff --git a/src/hooks/validation.tsx b/src/hooks/validation.tsx index 3446fd8..5a8c6c8 100644 --- a/src/hooks/validation.tsx +++ b/src/hooks/validation.tsx @@ -40,9 +40,11 @@ const EMPTY_OBJECT = {}; export const validateInput = function ({ value: getValue, + invalidate: invalidate, listen, }: { value?: () => any; + invalidate?: (v: any) => string | boolean; listen?: { input?: boolean; blur?: boolean; @@ -75,6 +77,19 @@ export const validateInput = function ({ // Update state on badInput (value stays unchanged on badInput), but only update the state once validity_.badInput != lastBadInput ) { + if (invalidate) { + const invalidateResult = invalidate(nextValue); + if (invalidateResult) { + el.setCustomValidity( + invalidateResult === true + ? "Input value is invalid." + : invalidateResult + ); + } else { + el.setCustomValidity(""); + } + } + name = name || el.name || el.id; setValidity(validity_); lastBadInput = validity_.badInput;