finprim 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,272 @@
1
+ 'use strict';
2
+
3
+ var react = require('react');
4
+
5
+ // src/react/index.ts
6
+
7
+ // src/iban.ts
8
+ var IBAN_LENGTHS = {
9
+ AL: 28,
10
+ AD: 24,
11
+ AT: 20,
12
+ AZ: 28,
13
+ BH: 22,
14
+ BE: 16,
15
+ BA: 20,
16
+ BR: 29,
17
+ BG: 22,
18
+ CR: 22,
19
+ HR: 21,
20
+ CY: 28,
21
+ CZ: 24,
22
+ DK: 18,
23
+ DO: 28,
24
+ EE: 20,
25
+ FI: 18,
26
+ FR: 27,
27
+ GE: 22,
28
+ DE: 22,
29
+ GI: 23,
30
+ GR: 27,
31
+ GT: 28,
32
+ HU: 28,
33
+ IS: 26,
34
+ IE: 22,
35
+ IL: 23,
36
+ IT: 27,
37
+ JO: 30,
38
+ KZ: 20,
39
+ KW: 30,
40
+ LV: 21,
41
+ LB: 28,
42
+ LI: 21,
43
+ LT: 20,
44
+ LU: 20,
45
+ MK: 19,
46
+ MT: 31,
47
+ MR: 27,
48
+ MU: 30,
49
+ MC: 27,
50
+ MD: 24,
51
+ ME: 22,
52
+ NL: 18,
53
+ NO: 15,
54
+ PK: 24,
55
+ PS: 29,
56
+ PL: 28,
57
+ PT: 25,
58
+ QA: 29,
59
+ RO: 24,
60
+ SM: 27,
61
+ SA: 24,
62
+ RS: 22,
63
+ SK: 24,
64
+ SI: 19,
65
+ ES: 24,
66
+ SE: 24,
67
+ CH: 21,
68
+ TN: 24,
69
+ TR: 26,
70
+ AE: 23,
71
+ GB: 22,
72
+ VG: 24
73
+ };
74
+ function mod97(value) {
75
+ let remainder = 0;
76
+ for (const char of value) {
77
+ remainder = (remainder * 10 + parseInt(char, 10)) % 97;
78
+ }
79
+ return remainder;
80
+ }
81
+ function ibanToDigits(iban) {
82
+ const rearranged = iban.slice(4) + iban.slice(0, 4);
83
+ return rearranged.split("").map((char) => {
84
+ const code = char.charCodeAt(0);
85
+ return code >= 65 && code <= 90 ? (code - 55).toString() : char;
86
+ }).join("");
87
+ }
88
+ function formatIBANString(iban) {
89
+ return iban.replace(/(.{4})/g, "$1 ").trim();
90
+ }
91
+ function validateIBAN(input) {
92
+ if (!input || typeof input !== "string") {
93
+ return { valid: false, error: "Input must be a non-empty string" };
94
+ }
95
+ const cleaned = input.replace(/\s/g, "").toUpperCase();
96
+ if (cleaned.length < 4) {
97
+ return { valid: false, error: "IBAN is too short" };
98
+ }
99
+ const countryCode = cleaned.slice(0, 2);
100
+ if (!/^[A-Z]{2}$/.test(countryCode)) {
101
+ return { valid: false, error: "IBAN must start with a 2-letter country code" };
102
+ }
103
+ const expectedLength = IBAN_LENGTHS[countryCode];
104
+ if (!expectedLength) {
105
+ return { valid: false, error: `Unsupported country code: ${countryCode}` };
106
+ }
107
+ if (cleaned.length !== expectedLength) {
108
+ return {
109
+ valid: false,
110
+ error: `Invalid length for ${countryCode} IBAN. Expected ${expectedLength} characters, got ${cleaned.length}`
111
+ };
112
+ }
113
+ if (!/^[A-Z0-9]+$/.test(cleaned)) {
114
+ return { valid: false, error: "IBAN contains invalid characters" };
115
+ }
116
+ const digits = ibanToDigits(cleaned);
117
+ if (mod97(digits) !== 1) {
118
+ return { valid: false, error: "IBAN checksum is invalid" };
119
+ }
120
+ return {
121
+ valid: true,
122
+ value: cleaned,
123
+ formatted: formatIBANString(cleaned)
124
+ };
125
+ }
126
+
127
+ // src/sortcode.ts
128
+ function validateUKSortCode(input) {
129
+ if (!input || typeof input !== "string") {
130
+ return { valid: false, error: "Input must be a non-empty string" };
131
+ }
132
+ const cleaned = input.replace(/[-\s]/g, "");
133
+ if (!/^\d{6}$/.test(cleaned)) {
134
+ return {
135
+ valid: false,
136
+ error: "Sort code must be exactly 6 digits. Accepted formats: 60-16-13, 601613, 60 16 13"
137
+ };
138
+ }
139
+ const formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 6)}`;
140
+ return {
141
+ valid: true,
142
+ value: cleaned,
143
+ formatted
144
+ };
145
+ }
146
+ function validateUKAccountNumber(input) {
147
+ if (!input || typeof input !== "string") {
148
+ return { valid: false, error: "Input must be a non-empty string" };
149
+ }
150
+ const cleaned = input.replace(/\s/g, "");
151
+ if (!/^\d{8}$/.test(cleaned)) {
152
+ return {
153
+ valid: false,
154
+ error: "UK account number must be exactly 8 digits"
155
+ };
156
+ }
157
+ const formatted = `${cleaned.slice(0, 4)} ${cleaned.slice(4, 8)}`;
158
+ return {
159
+ valid: true,
160
+ value: cleaned,
161
+ formatted
162
+ };
163
+ }
164
+
165
+ // src/currency.ts
166
+ var CURRENCY_LOCALES = {
167
+ GBP: "en-GB",
168
+ EUR: "de-DE",
169
+ USD: "en-US",
170
+ JPY: "ja-JP",
171
+ CHF: "de-CH",
172
+ CAD: "en-CA",
173
+ AUD: "en-AU",
174
+ NZD: "en-NZ"
175
+ };
176
+ function formatCurrency(amount, currency, locale) {
177
+ const resolvedLocale = locale ?? CURRENCY_LOCALES[currency] ?? "en-GB";
178
+ return new Intl.NumberFormat(resolvedLocale, {
179
+ style: "currency",
180
+ currency,
181
+ minimumFractionDigits: currency === "JPY" ? 0 : 2,
182
+ maximumFractionDigits: currency === "JPY" ? 0 : 2
183
+ }).format(amount);
184
+ }
185
+
186
+ // src/bic.ts
187
+ var BIC_REGEX = /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/;
188
+ function validateBIC(input) {
189
+ if (!input || typeof input !== "string") {
190
+ return { valid: false, error: "Input must be a non-empty string" };
191
+ }
192
+ const cleaned = input.replace(/\s/g, "").toUpperCase();
193
+ if (cleaned.length !== 8 && cleaned.length !== 11) {
194
+ return {
195
+ valid: false,
196
+ error: `BIC must be 8 or 11 characters. Got ${cleaned.length}`
197
+ };
198
+ }
199
+ if (!BIC_REGEX.test(cleaned)) {
200
+ return {
201
+ valid: false,
202
+ error: "Invalid BIC format. Expected: 4 letters + 2 letters + 2 alphanumeric + optional 3 alphanumeric"
203
+ };
204
+ }
205
+ const bankCode = cleaned.slice(0, 4);
206
+ const countryCode = cleaned.slice(4, 6);
207
+ const location = cleaned.slice(6, 8);
208
+ const branch = cleaned.length === 11 ? cleaned.slice(8, 11) : "XXX";
209
+ return {
210
+ valid: true,
211
+ value: cleaned,
212
+ formatted: `${bankCode} ${countryCode} ${location} ${branch}`
213
+ };
214
+ }
215
+
216
+ // src/react/index.ts
217
+ function useValidatedInput(validator, minLength = 1) {
218
+ const [value, setValue] = react.useState("");
219
+ const [result, setResult] = react.useState(null);
220
+ const onChange = react.useCallback(
221
+ (e) => {
222
+ const input = e.target.value;
223
+ setValue(input);
224
+ if (input.length >= minLength) {
225
+ setResult(validator(input));
226
+ } else {
227
+ setResult(null);
228
+ }
229
+ },
230
+ [validator, minLength]
231
+ );
232
+ return {
233
+ value,
234
+ formatted: result?.valid ? result.formatted : value,
235
+ valid: result === null ? null : result.valid,
236
+ error: result && !result.valid ? result.error : null,
237
+ onChange,
238
+ result
239
+ };
240
+ }
241
+ function useIBANInput() {
242
+ return useValidatedInput(validateIBAN, 5);
243
+ }
244
+ function useSortCodeInput() {
245
+ return useValidatedInput(validateUKSortCode, 6);
246
+ }
247
+ function useAccountNumberInput() {
248
+ return useValidatedInput(validateUKAccountNumber, 8);
249
+ }
250
+ function useBICInput() {
251
+ return useValidatedInput(validateBIC, 8);
252
+ }
253
+ function useCurrencyInput(currency, locale) {
254
+ const [rawValue, setRawValue] = react.useState(null);
255
+ const onChange = react.useCallback(
256
+ (e) => {
257
+ const num = parseFloat(e.target.value.replace(/[^0-9.]/g, ""));
258
+ setRawValue(isNaN(num) ? null : num);
259
+ },
260
+ []
261
+ );
262
+ const formatted = rawValue !== null ? formatCurrency(rawValue, currency, locale) : "";
263
+ return { rawValue, formatted, onChange };
264
+ }
265
+
266
+ exports.useAccountNumberInput = useAccountNumberInput;
267
+ exports.useBICInput = useBICInput;
268
+ exports.useCurrencyInput = useCurrencyInput;
269
+ exports.useIBANInput = useIBANInput;
270
+ exports.useSortCodeInput = useSortCodeInput;
271
+ //# sourceMappingURL=index.js.map
272
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/iban.ts","../../src/sortcode.ts","../../src/currency.ts","../../src/bic.ts","../../src/react/index.ts"],"names":["useState","useCallback"],"mappings":";;;;;;;AAGA,IAAM,YAAA,GAAuC;AAAA,EAC3C,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAC5D,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAC5D,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAC5D,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAC5D,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAC5D,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,E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","file":"index.js","sourcesContent":["import type { IBAN, ValidationResult } from './types'\n\n// Expected IBAN lengths per country (ISO 13616 registry)\nconst IBAN_LENGTHS: Record<string, number> = {\n AL: 28, AD: 24, AT: 20, AZ: 28, BH: 22, BE: 16, BA: 20, BR: 29,\n BG: 22, CR: 22, HR: 21, CY: 28, CZ: 24, DK: 18, DO: 28, EE: 20,\n FI: 18, FR: 27, GE: 22, DE: 22, GI: 23, GR: 27, GT: 28, HU: 28,\n IS: 26, IE: 22, IL: 23, IT: 27, JO: 30, KZ: 20, KW: 30, LV: 21,\n LB: 28, LI: 21, LT: 20, LU: 20, MK: 19, MT: 31, MR: 27, MU: 30,\n MC: 27, MD: 24, ME: 22, NL: 18, NO: 15, PK: 24, PS: 29, PL: 28,\n PT: 25, QA: 29, RO: 24, SM: 27, SA: 24, RS: 22, SK: 24, SI: 19,\n ES: 24, SE: 24, CH: 21, TN: 24, TR: 26, AE: 23, GB: 22, VG: 24,\n}\n\n// Process digit by digit to avoid JS integer overflow on large IBAN numbers\nfunction mod97(value: string): number {\n let remainder = 0\n for (const char of value) {\n remainder = (remainder * 10 + parseInt(char, 10)) % 97\n }\n return remainder\n}\n\n// Rearrange IBAN and convert letters to numbers per ISO 13616\nfunction ibanToDigits(iban: string): string {\n const rearranged = iban.slice(4) + iban.slice(0, 4)\n return rearranged\n .split('')\n .map((char) => {\n const code = char.charCodeAt(0)\n // A=10, B=11, ... Z=35\n return code >= 65 && code <= 90 ? (code - 55).toString() : char\n })\n .join('')\n}\n\nfunction formatIBANString(iban: string): string {\n return iban.replace(/(.{4})/g, '$1 ').trim()\n}\n\n/**\n * Validates an IBAN string.\n * Accepts IBANs with or without spaces.\n * Validates: country code, expected length, characters, and mod97 checksum.\n *\n * @example\n * validateIBAN('GB29NWBK60161331926819')\n * // { valid: true, value: 'GB29NWBK60161331926819', formatted: 'GB29 NWBK 6016 1331 9268 19' }\n *\n * validateIBAN('GB00NWBK60161331926819')\n * // { valid: false, error: 'IBAN checksum is invalid' }\n */\nexport function validateIBAN(input: string): ValidationResult<IBAN> {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n const cleaned = input.replace(/\\s/g, '').toUpperCase()\n\n if (cleaned.length < 4) {\n return { valid: false, error: 'IBAN is too short' }\n }\n\n const countryCode = cleaned.slice(0, 2)\n\n if (!/^[A-Z]{2}$/.test(countryCode)) {\n return { valid: false, error: 'IBAN must start with a 2-letter country code' }\n }\n\n const expectedLength = IBAN_LENGTHS[countryCode]\n\n if (!expectedLength) {\n return { valid: false, error: `Unsupported country code: ${countryCode}` }\n }\n\n if (cleaned.length !== expectedLength) {\n return {\n valid: false,\n error: `Invalid length for ${countryCode} IBAN. Expected ${expectedLength} characters, got ${cleaned.length}`,\n }\n }\n\n if (!/^[A-Z0-9]+$/.test(cleaned)) {\n return { valid: false, error: 'IBAN contains invalid characters' }\n }\n\n const digits = ibanToDigits(cleaned)\n\n if (mod97(digits) !== 1) {\n return { valid: false, error: 'IBAN checksum is invalid' }\n }\n\n return {\n valid: true,\n value: cleaned as IBAN,\n formatted: formatIBANString(cleaned),\n }\n}\n","import type { SortCode, AccountNumber, ValidationResult } from './types'\n\n/**\n * Validates a UK sort code.\n * Accepts formats: 60-16-13, 601613, 60 16 13\n *\n * @example\n * validateUKSortCode('60-16-13')\n * // { valid: true, value: '601613', formatted: '60-16-13' }\n *\n * validateUKSortCode('999')\n * // { valid: false, error: 'Sort code must be 6 digits...' }\n */\nexport function validateUKSortCode(input: string): ValidationResult<SortCode> {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n const cleaned = input.replace(/[-\\s]/g, '')\n\n if (!/^\\d{6}$/.test(cleaned)) {\n return {\n valid: false,\n error: 'Sort code must be exactly 6 digits. Accepted formats: 60-16-13, 601613, 60 16 13',\n }\n }\n\n const formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 6)}`\n\n return {\n valid: true,\n value: cleaned as SortCode,\n formatted,\n }\n}\n\n/**\n * Validates a UK bank account number.\n * Must be exactly 8 digits.\n *\n * @example\n * validateUKAccountNumber('31926819')\n * // { valid: true, value: '31926819', formatted: '3192 6819' }\n *\n * validateUKAccountNumber('1234')\n * // { valid: false, error: 'UK account number must be exactly 8 digits' }\n */\nexport function validateUKAccountNumber(input: string): ValidationResult<AccountNumber> {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n const cleaned = input.replace(/\\s/g, '')\n\n if (!/^\\d{8}$/.test(cleaned)) {\n return {\n valid: false,\n error: 'UK account number must be exactly 8 digits',\n }\n }\n\n const formatted = `${cleaned.slice(0, 4)} ${cleaned.slice(4, 8)}`\n\n return {\n valid: true,\n value: cleaned as AccountNumber,\n formatted,\n }\n}\n","import type { CurrencyCode, SupportedCurrency, MoneyResult, ValidationResult } from './types'\n\nexport const SUPPORTED_CURRENCIES: SupportedCurrency[] = [\n 'GBP', 'EUR', 'USD', 'JPY', 'CHF', 'CAD', 'AUD', 'NZD',\n]\n\nconst CURRENCY_LOCALES: Record<SupportedCurrency, string> = {\n GBP: 'en-GB',\n EUR: 'de-DE',\n USD: 'en-US',\n JPY: 'ja-JP',\n CHF: 'de-CH',\n CAD: 'en-CA',\n AUD: 'en-AU',\n NZD: 'en-NZ',\n}\n\nconst SYMBOL_MAP: Record<string, SupportedCurrency> = {\n '£': 'GBP',\n '€': 'EUR',\n '$': 'USD',\n '¥': 'JPY',\n 'CHF': 'CHF',\n}\n\n/**\n * Validates a currency code against supported ISO 4217 codes.\n *\n * @example\n * validateCurrencyCode('GBP')\n * // { valid: true, value: 'GBP', formatted: 'GBP' }\n *\n * validateCurrencyCode('XYZ')\n * // { valid: false, error: 'Unsupported currency code: XYZ' }\n */\nexport function validateCurrencyCode(input: string): ValidationResult<CurrencyCode> {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n const upper = input.toUpperCase() as SupportedCurrency\n\n if (!SUPPORTED_CURRENCIES.includes(upper)) {\n return {\n valid: false,\n error: `Unsupported currency code: ${input}. Supported: ${SUPPORTED_CURRENCIES.join(', ')}`,\n }\n }\n\n return {\n valid: true,\n value: upper as CurrencyCode,\n formatted: upper,\n }\n}\n\n/**\n * Formats a number as a locale-aware currency string.\n * Uses the built-in Intl.NumberFormat API — zero dependencies.\n *\n * @example\n * formatCurrency(1000.5, 'GBP') // '£1,000.50'\n * formatCurrency(1000.5, 'EUR', 'de-DE') // '1.000,50 €'\n * formatCurrency(1000.5, 'USD', 'en-US') // '$1,000.50'\n * formatCurrency(1000, 'JPY') // '¥1,000'\n */\nexport function formatCurrency(\n amount: number,\n currency: SupportedCurrency,\n locale?: string\n): string {\n const resolvedLocale = locale ?? CURRENCY_LOCALES[currency] ?? 'en-GB'\n\n return new Intl.NumberFormat(resolvedLocale, {\n style: 'currency',\n currency,\n minimumFractionDigits: currency === 'JPY' ? 0 : 2,\n maximumFractionDigits: currency === 'JPY' ? 0 : 2,\n }).format(amount)\n}\n\n/**\n * Parses a formatted currency string back into a structured money object.\n * Detects the currency from the symbol prefix.\n *\n * @example\n * parseMoney('£1,000.50')\n * // { valid: true, amount: 1000.5, currency: 'GBP', formatted: '£1,000.50' }\n *\n * parseMoney('not money')\n * // { valid: false, error: 'Could not detect currency from input' }\n */\nexport function parseMoney(input: string): MoneyResult {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n let currency: SupportedCurrency | undefined\n let cleaned = input.trim()\n\n for (const [symbol, code] of Object.entries(SYMBOL_MAP)) {\n if (cleaned.startsWith(symbol) || cleaned.endsWith(symbol)) {\n currency = code\n cleaned = cleaned.replace(symbol, '').trim()\n break\n }\n }\n\n if (!currency) {\n return { valid: false, error: 'Could not detect currency from input. Expected a symbol like £, €, $, ¥' }\n }\n\n // Remove thousands separators, normalise decimal separator\n const normalised = cleaned.replace(/,/g, '')\n const amount = parseFloat(normalised)\n\n if (isNaN(amount)) {\n return { valid: false, error: `Could not parse amount from: \"${cleaned}\"` }\n }\n\n return {\n valid: true,\n amount,\n currency,\n formatted: formatCurrency(amount, currency),\n }\n}\n","import type { BIC, ValidationResult } from './types'\n\n// BIC format: 4 letters (bank) + 2 letters (country) + 2 alphanumeric (location) + optional 3 alphanumeric (branch)\nconst BIC_REGEX = /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/\n\n/**\n * Validates a BIC (Bank Identifier Code) / SWIFT code.\n * Accepts both 8-character and 11-character BIC codes.\n *\n * Format: AAAABBCCXXX\n * - AAAA = Bank code (4 letters)\n * - BB = Country code (2 letters, ISO 3166-1)\n * - CC = Location code (2 alphanumeric)\n * - XXX = Branch code (3 alphanumeric, optional — 'XXX' means head office)\n *\n * @example\n * validateBIC('NWBKGB2L')\n * // { valid: true, value: 'NWBKGB2L', formatted: 'NWBKGB2L' }\n *\n * validateBIC('DEUTDEDB')\n * // { valid: true, value: 'DEUTDEDB', formatted: 'DEUTDEDB' }\n */\nexport function validateBIC(input: string): ValidationResult<BIC> {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n const cleaned = input.replace(/\\s/g, '').toUpperCase()\n\n if (cleaned.length !== 8 && cleaned.length !== 11) {\n return {\n valid: false,\n error: `BIC must be 8 or 11 characters. Got ${cleaned.length}`,\n }\n }\n\n if (!BIC_REGEX.test(cleaned)) {\n return {\n valid: false,\n error: 'Invalid BIC format. Expected: 4 letters + 2 letters + 2 alphanumeric + optional 3 alphanumeric',\n }\n }\n\n const bankCode = cleaned.slice(0, 4)\n const countryCode = cleaned.slice(4, 6)\n const location = cleaned.slice(6, 8)\n const branch = cleaned.length === 11 ? cleaned.slice(8, 11) : 'XXX'\n\n return {\n valid: true,\n value: cleaned as BIC,\n formatted: `${bankCode} ${countryCode} ${location} ${branch}`,\n }\n}\n","import { useState, useCallback } from 'react'\nimport { validateIBAN } from '../iban'\nimport { validateUKSortCode, validateUKAccountNumber } from '../sortcode'\nimport { formatCurrency } from '../currency'\nimport { validateBIC } from '../bic'\nimport type {\n SupportedCurrency,\n ValidationResult,\n IBAN,\n SortCode,\n AccountNumber,\n BIC,\n} from '../types'\n\ntype HookResult<T> = {\n value: string\n formatted: string\n valid: boolean | null\n error: string | null\n onChange: (e: React.ChangeEvent<HTMLInputElement>) => void\n result: ValidationResult<T> | null\n}\n\nfunction useValidatedInput<T>(\n validator: (val: string) => ValidationResult<T>,\n minLength = 1\n): HookResult<T> {\n const [value, setValue] = useState('')\n const [result, setResult] = useState<ValidationResult<T> | null>(null)\n\n const onChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const input = e.target.value\n setValue(input)\n if (input.length >= minLength) {\n setResult(validator(input))\n } else {\n setResult(null)\n }\n },\n [validator, minLength]\n )\n\n return {\n value,\n formatted: result?.valid ? result.formatted : value,\n valid: result === null ? null : result.valid,\n error: result && !result.valid ? result.error : null,\n onChange,\n result,\n }\n}\n\n/**\n * React hook for IBAN input fields.\n * Validates on change and returns formatted value and error state.\n *\n * @example\n * const { value, formatted, valid, error, onChange } = useIBANInput()\n *\n * return (\n * <input\n * value={formatted}\n * onChange={onChange}\n * aria-invalid={valid === false}\n * />\n * )\n */\nexport function useIBANInput(): HookResult<IBAN> {\n return useValidatedInput(validateIBAN, 5)\n}\n\n/**\n * React hook for UK sort code input fields.\n *\n * @example\n * const { formatted, valid, error, onChange } = useSortCodeInput()\n */\nexport function useSortCodeInput(): HookResult<SortCode> {\n return useValidatedInput(validateUKSortCode, 6)\n}\n\n/**\n * React hook for UK account number input fields.\n *\n * @example\n * const { formatted, valid, error, onChange } = useAccountNumberInput()\n */\nexport function useAccountNumberInput(): HookResult<AccountNumber> {\n return useValidatedInput(validateUKAccountNumber, 8)\n}\n\n/**\n * React hook for BIC / SWIFT code input fields.\n *\n * @example\n * const { formatted, valid, error, onChange } = useBICInput()\n */\nexport function useBICInput(): HookResult<BIC> {\n return useValidatedInput(validateBIC, 8)\n}\n\n/**\n * React hook for currency amount inputs with locale-aware formatting.\n * Returns both the raw numeric value and the formatted display string.\n *\n * @example\n * const { rawValue, formatted, onChange } = useCurrencyInput('GBP')\n *\n * return <input value={formatted} onChange={onChange} />\n */\nexport function useCurrencyInput(currency: SupportedCurrency, locale?: string) {\n const [rawValue, setRawValue] = useState<number | null>(null)\n\n const onChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const num = parseFloat(e.target.value.replace(/[^0-9.]/g, ''))\n setRawValue(isNaN(num) ? null : num)\n },\n []\n )\n\n const formatted = rawValue !== null ? formatCurrency(rawValue, currency, locale) : ''\n\n return { rawValue, formatted, onChange }\n}\n"]}
@@ -0,0 +1,266 @@
1
+ import { useState, useCallback } from 'react';
2
+
3
+ // src/react/index.ts
4
+
5
+ // src/iban.ts
6
+ var IBAN_LENGTHS = {
7
+ AL: 28,
8
+ AD: 24,
9
+ AT: 20,
10
+ AZ: 28,
11
+ BH: 22,
12
+ BE: 16,
13
+ BA: 20,
14
+ BR: 29,
15
+ BG: 22,
16
+ CR: 22,
17
+ HR: 21,
18
+ CY: 28,
19
+ CZ: 24,
20
+ DK: 18,
21
+ DO: 28,
22
+ EE: 20,
23
+ FI: 18,
24
+ FR: 27,
25
+ GE: 22,
26
+ DE: 22,
27
+ GI: 23,
28
+ GR: 27,
29
+ GT: 28,
30
+ HU: 28,
31
+ IS: 26,
32
+ IE: 22,
33
+ IL: 23,
34
+ IT: 27,
35
+ JO: 30,
36
+ KZ: 20,
37
+ KW: 30,
38
+ LV: 21,
39
+ LB: 28,
40
+ LI: 21,
41
+ LT: 20,
42
+ LU: 20,
43
+ MK: 19,
44
+ MT: 31,
45
+ MR: 27,
46
+ MU: 30,
47
+ MC: 27,
48
+ MD: 24,
49
+ ME: 22,
50
+ NL: 18,
51
+ NO: 15,
52
+ PK: 24,
53
+ PS: 29,
54
+ PL: 28,
55
+ PT: 25,
56
+ QA: 29,
57
+ RO: 24,
58
+ SM: 27,
59
+ SA: 24,
60
+ RS: 22,
61
+ SK: 24,
62
+ SI: 19,
63
+ ES: 24,
64
+ SE: 24,
65
+ CH: 21,
66
+ TN: 24,
67
+ TR: 26,
68
+ AE: 23,
69
+ GB: 22,
70
+ VG: 24
71
+ };
72
+ function mod97(value) {
73
+ let remainder = 0;
74
+ for (const char of value) {
75
+ remainder = (remainder * 10 + parseInt(char, 10)) % 97;
76
+ }
77
+ return remainder;
78
+ }
79
+ function ibanToDigits(iban) {
80
+ const rearranged = iban.slice(4) + iban.slice(0, 4);
81
+ return rearranged.split("").map((char) => {
82
+ const code = char.charCodeAt(0);
83
+ return code >= 65 && code <= 90 ? (code - 55).toString() : char;
84
+ }).join("");
85
+ }
86
+ function formatIBANString(iban) {
87
+ return iban.replace(/(.{4})/g, "$1 ").trim();
88
+ }
89
+ function validateIBAN(input) {
90
+ if (!input || typeof input !== "string") {
91
+ return { valid: false, error: "Input must be a non-empty string" };
92
+ }
93
+ const cleaned = input.replace(/\s/g, "").toUpperCase();
94
+ if (cleaned.length < 4) {
95
+ return { valid: false, error: "IBAN is too short" };
96
+ }
97
+ const countryCode = cleaned.slice(0, 2);
98
+ if (!/^[A-Z]{2}$/.test(countryCode)) {
99
+ return { valid: false, error: "IBAN must start with a 2-letter country code" };
100
+ }
101
+ const expectedLength = IBAN_LENGTHS[countryCode];
102
+ if (!expectedLength) {
103
+ return { valid: false, error: `Unsupported country code: ${countryCode}` };
104
+ }
105
+ if (cleaned.length !== expectedLength) {
106
+ return {
107
+ valid: false,
108
+ error: `Invalid length for ${countryCode} IBAN. Expected ${expectedLength} characters, got ${cleaned.length}`
109
+ };
110
+ }
111
+ if (!/^[A-Z0-9]+$/.test(cleaned)) {
112
+ return { valid: false, error: "IBAN contains invalid characters" };
113
+ }
114
+ const digits = ibanToDigits(cleaned);
115
+ if (mod97(digits) !== 1) {
116
+ return { valid: false, error: "IBAN checksum is invalid" };
117
+ }
118
+ return {
119
+ valid: true,
120
+ value: cleaned,
121
+ formatted: formatIBANString(cleaned)
122
+ };
123
+ }
124
+
125
+ // src/sortcode.ts
126
+ function validateUKSortCode(input) {
127
+ if (!input || typeof input !== "string") {
128
+ return { valid: false, error: "Input must be a non-empty string" };
129
+ }
130
+ const cleaned = input.replace(/[-\s]/g, "");
131
+ if (!/^\d{6}$/.test(cleaned)) {
132
+ return {
133
+ valid: false,
134
+ error: "Sort code must be exactly 6 digits. Accepted formats: 60-16-13, 601613, 60 16 13"
135
+ };
136
+ }
137
+ const formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 6)}`;
138
+ return {
139
+ valid: true,
140
+ value: cleaned,
141
+ formatted
142
+ };
143
+ }
144
+ function validateUKAccountNumber(input) {
145
+ if (!input || typeof input !== "string") {
146
+ return { valid: false, error: "Input must be a non-empty string" };
147
+ }
148
+ const cleaned = input.replace(/\s/g, "");
149
+ if (!/^\d{8}$/.test(cleaned)) {
150
+ return {
151
+ valid: false,
152
+ error: "UK account number must be exactly 8 digits"
153
+ };
154
+ }
155
+ const formatted = `${cleaned.slice(0, 4)} ${cleaned.slice(4, 8)}`;
156
+ return {
157
+ valid: true,
158
+ value: cleaned,
159
+ formatted
160
+ };
161
+ }
162
+
163
+ // src/currency.ts
164
+ var CURRENCY_LOCALES = {
165
+ GBP: "en-GB",
166
+ EUR: "de-DE",
167
+ USD: "en-US",
168
+ JPY: "ja-JP",
169
+ CHF: "de-CH",
170
+ CAD: "en-CA",
171
+ AUD: "en-AU",
172
+ NZD: "en-NZ"
173
+ };
174
+ function formatCurrency(amount, currency, locale) {
175
+ const resolvedLocale = locale ?? CURRENCY_LOCALES[currency] ?? "en-GB";
176
+ return new Intl.NumberFormat(resolvedLocale, {
177
+ style: "currency",
178
+ currency,
179
+ minimumFractionDigits: currency === "JPY" ? 0 : 2,
180
+ maximumFractionDigits: currency === "JPY" ? 0 : 2
181
+ }).format(amount);
182
+ }
183
+
184
+ // src/bic.ts
185
+ var BIC_REGEX = /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/;
186
+ function validateBIC(input) {
187
+ if (!input || typeof input !== "string") {
188
+ return { valid: false, error: "Input must be a non-empty string" };
189
+ }
190
+ const cleaned = input.replace(/\s/g, "").toUpperCase();
191
+ if (cleaned.length !== 8 && cleaned.length !== 11) {
192
+ return {
193
+ valid: false,
194
+ error: `BIC must be 8 or 11 characters. Got ${cleaned.length}`
195
+ };
196
+ }
197
+ if (!BIC_REGEX.test(cleaned)) {
198
+ return {
199
+ valid: false,
200
+ error: "Invalid BIC format. Expected: 4 letters + 2 letters + 2 alphanumeric + optional 3 alphanumeric"
201
+ };
202
+ }
203
+ const bankCode = cleaned.slice(0, 4);
204
+ const countryCode = cleaned.slice(4, 6);
205
+ const location = cleaned.slice(6, 8);
206
+ const branch = cleaned.length === 11 ? cleaned.slice(8, 11) : "XXX";
207
+ return {
208
+ valid: true,
209
+ value: cleaned,
210
+ formatted: `${bankCode} ${countryCode} ${location} ${branch}`
211
+ };
212
+ }
213
+
214
+ // src/react/index.ts
215
+ function useValidatedInput(validator, minLength = 1) {
216
+ const [value, setValue] = useState("");
217
+ const [result, setResult] = useState(null);
218
+ const onChange = useCallback(
219
+ (e) => {
220
+ const input = e.target.value;
221
+ setValue(input);
222
+ if (input.length >= minLength) {
223
+ setResult(validator(input));
224
+ } else {
225
+ setResult(null);
226
+ }
227
+ },
228
+ [validator, minLength]
229
+ );
230
+ return {
231
+ value,
232
+ formatted: result?.valid ? result.formatted : value,
233
+ valid: result === null ? null : result.valid,
234
+ error: result && !result.valid ? result.error : null,
235
+ onChange,
236
+ result
237
+ };
238
+ }
239
+ function useIBANInput() {
240
+ return useValidatedInput(validateIBAN, 5);
241
+ }
242
+ function useSortCodeInput() {
243
+ return useValidatedInput(validateUKSortCode, 6);
244
+ }
245
+ function useAccountNumberInput() {
246
+ return useValidatedInput(validateUKAccountNumber, 8);
247
+ }
248
+ function useBICInput() {
249
+ return useValidatedInput(validateBIC, 8);
250
+ }
251
+ function useCurrencyInput(currency, locale) {
252
+ const [rawValue, setRawValue] = useState(null);
253
+ const onChange = useCallback(
254
+ (e) => {
255
+ const num = parseFloat(e.target.value.replace(/[^0-9.]/g, ""));
256
+ setRawValue(isNaN(num) ? null : num);
257
+ },
258
+ []
259
+ );
260
+ const formatted = rawValue !== null ? formatCurrency(rawValue, currency, locale) : "";
261
+ return { rawValue, formatted, onChange };
262
+ }
263
+
264
+ export { useAccountNumberInput, useBICInput, useCurrencyInput, useIBANInput, useSortCodeInput };
265
+ //# sourceMappingURL=index.mjs.map
266
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../src/iban.ts","../../src/sortcode.ts","../../src/currency.ts","../../src/bic.ts","../../src/react/index.ts"],"names":[],"mappings":";;;;;AAGA,IAAM,YAAA,GAAuC;AAAA,EAC3C,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAC5D,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAC5D,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAC5D,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAC5D,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAC5D,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAAI,EAAA;AAAA,EAAI,EAAA,EAA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","file":"index.mjs","sourcesContent":["import type { IBAN, ValidationResult } from './types'\n\n// Expected IBAN lengths per country (ISO 13616 registry)\nconst IBAN_LENGTHS: Record<string, number> = {\n AL: 28, AD: 24, AT: 20, AZ: 28, BH: 22, BE: 16, BA: 20, BR: 29,\n BG: 22, CR: 22, HR: 21, CY: 28, CZ: 24, DK: 18, DO: 28, EE: 20,\n FI: 18, FR: 27, GE: 22, DE: 22, GI: 23, GR: 27, GT: 28, HU: 28,\n IS: 26, IE: 22, IL: 23, IT: 27, JO: 30, KZ: 20, KW: 30, LV: 21,\n LB: 28, LI: 21, LT: 20, LU: 20, MK: 19, MT: 31, MR: 27, MU: 30,\n MC: 27, MD: 24, ME: 22, NL: 18, NO: 15, PK: 24, PS: 29, PL: 28,\n PT: 25, QA: 29, RO: 24, SM: 27, SA: 24, RS: 22, SK: 24, SI: 19,\n ES: 24, SE: 24, CH: 21, TN: 24, TR: 26, AE: 23, GB: 22, VG: 24,\n}\n\n// Process digit by digit to avoid JS integer overflow on large IBAN numbers\nfunction mod97(value: string): number {\n let remainder = 0\n for (const char of value) {\n remainder = (remainder * 10 + parseInt(char, 10)) % 97\n }\n return remainder\n}\n\n// Rearrange IBAN and convert letters to numbers per ISO 13616\nfunction ibanToDigits(iban: string): string {\n const rearranged = iban.slice(4) + iban.slice(0, 4)\n return rearranged\n .split('')\n .map((char) => {\n const code = char.charCodeAt(0)\n // A=10, B=11, ... Z=35\n return code >= 65 && code <= 90 ? (code - 55).toString() : char\n })\n .join('')\n}\n\nfunction formatIBANString(iban: string): string {\n return iban.replace(/(.{4})/g, '$1 ').trim()\n}\n\n/**\n * Validates an IBAN string.\n * Accepts IBANs with or without spaces.\n * Validates: country code, expected length, characters, and mod97 checksum.\n *\n * @example\n * validateIBAN('GB29NWBK60161331926819')\n * // { valid: true, value: 'GB29NWBK60161331926819', formatted: 'GB29 NWBK 6016 1331 9268 19' }\n *\n * validateIBAN('GB00NWBK60161331926819')\n * // { valid: false, error: 'IBAN checksum is invalid' }\n */\nexport function validateIBAN(input: string): ValidationResult<IBAN> {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n const cleaned = input.replace(/\\s/g, '').toUpperCase()\n\n if (cleaned.length < 4) {\n return { valid: false, error: 'IBAN is too short' }\n }\n\n const countryCode = cleaned.slice(0, 2)\n\n if (!/^[A-Z]{2}$/.test(countryCode)) {\n return { valid: false, error: 'IBAN must start with a 2-letter country code' }\n }\n\n const expectedLength = IBAN_LENGTHS[countryCode]\n\n if (!expectedLength) {\n return { valid: false, error: `Unsupported country code: ${countryCode}` }\n }\n\n if (cleaned.length !== expectedLength) {\n return {\n valid: false,\n error: `Invalid length for ${countryCode} IBAN. Expected ${expectedLength} characters, got ${cleaned.length}`,\n }\n }\n\n if (!/^[A-Z0-9]+$/.test(cleaned)) {\n return { valid: false, error: 'IBAN contains invalid characters' }\n }\n\n const digits = ibanToDigits(cleaned)\n\n if (mod97(digits) !== 1) {\n return { valid: false, error: 'IBAN checksum is invalid' }\n }\n\n return {\n valid: true,\n value: cleaned as IBAN,\n formatted: formatIBANString(cleaned),\n }\n}\n","import type { SortCode, AccountNumber, ValidationResult } from './types'\n\n/**\n * Validates a UK sort code.\n * Accepts formats: 60-16-13, 601613, 60 16 13\n *\n * @example\n * validateUKSortCode('60-16-13')\n * // { valid: true, value: '601613', formatted: '60-16-13' }\n *\n * validateUKSortCode('999')\n * // { valid: false, error: 'Sort code must be 6 digits...' }\n */\nexport function validateUKSortCode(input: string): ValidationResult<SortCode> {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n const cleaned = input.replace(/[-\\s]/g, '')\n\n if (!/^\\d{6}$/.test(cleaned)) {\n return {\n valid: false,\n error: 'Sort code must be exactly 6 digits. Accepted formats: 60-16-13, 601613, 60 16 13',\n }\n }\n\n const formatted = `${cleaned.slice(0, 2)}-${cleaned.slice(2, 4)}-${cleaned.slice(4, 6)}`\n\n return {\n valid: true,\n value: cleaned as SortCode,\n formatted,\n }\n}\n\n/**\n * Validates a UK bank account number.\n * Must be exactly 8 digits.\n *\n * @example\n * validateUKAccountNumber('31926819')\n * // { valid: true, value: '31926819', formatted: '3192 6819' }\n *\n * validateUKAccountNumber('1234')\n * // { valid: false, error: 'UK account number must be exactly 8 digits' }\n */\nexport function validateUKAccountNumber(input: string): ValidationResult<AccountNumber> {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n const cleaned = input.replace(/\\s/g, '')\n\n if (!/^\\d{8}$/.test(cleaned)) {\n return {\n valid: false,\n error: 'UK account number must be exactly 8 digits',\n }\n }\n\n const formatted = `${cleaned.slice(0, 4)} ${cleaned.slice(4, 8)}`\n\n return {\n valid: true,\n value: cleaned as AccountNumber,\n formatted,\n }\n}\n","import type { CurrencyCode, SupportedCurrency, MoneyResult, ValidationResult } from './types'\n\nexport const SUPPORTED_CURRENCIES: SupportedCurrency[] = [\n 'GBP', 'EUR', 'USD', 'JPY', 'CHF', 'CAD', 'AUD', 'NZD',\n]\n\nconst CURRENCY_LOCALES: Record<SupportedCurrency, string> = {\n GBP: 'en-GB',\n EUR: 'de-DE',\n USD: 'en-US',\n JPY: 'ja-JP',\n CHF: 'de-CH',\n CAD: 'en-CA',\n AUD: 'en-AU',\n NZD: 'en-NZ',\n}\n\nconst SYMBOL_MAP: Record<string, SupportedCurrency> = {\n '£': 'GBP',\n '€': 'EUR',\n '$': 'USD',\n '¥': 'JPY',\n 'CHF': 'CHF',\n}\n\n/**\n * Validates a currency code against supported ISO 4217 codes.\n *\n * @example\n * validateCurrencyCode('GBP')\n * // { valid: true, value: 'GBP', formatted: 'GBP' }\n *\n * validateCurrencyCode('XYZ')\n * // { valid: false, error: 'Unsupported currency code: XYZ' }\n */\nexport function validateCurrencyCode(input: string): ValidationResult<CurrencyCode> {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n const upper = input.toUpperCase() as SupportedCurrency\n\n if (!SUPPORTED_CURRENCIES.includes(upper)) {\n return {\n valid: false,\n error: `Unsupported currency code: ${input}. Supported: ${SUPPORTED_CURRENCIES.join(', ')}`,\n }\n }\n\n return {\n valid: true,\n value: upper as CurrencyCode,\n formatted: upper,\n }\n}\n\n/**\n * Formats a number as a locale-aware currency string.\n * Uses the built-in Intl.NumberFormat API — zero dependencies.\n *\n * @example\n * formatCurrency(1000.5, 'GBP') // '£1,000.50'\n * formatCurrency(1000.5, 'EUR', 'de-DE') // '1.000,50 €'\n * formatCurrency(1000.5, 'USD', 'en-US') // '$1,000.50'\n * formatCurrency(1000, 'JPY') // '¥1,000'\n */\nexport function formatCurrency(\n amount: number,\n currency: SupportedCurrency,\n locale?: string\n): string {\n const resolvedLocale = locale ?? CURRENCY_LOCALES[currency] ?? 'en-GB'\n\n return new Intl.NumberFormat(resolvedLocale, {\n style: 'currency',\n currency,\n minimumFractionDigits: currency === 'JPY' ? 0 : 2,\n maximumFractionDigits: currency === 'JPY' ? 0 : 2,\n }).format(amount)\n}\n\n/**\n * Parses a formatted currency string back into a structured money object.\n * Detects the currency from the symbol prefix.\n *\n * @example\n * parseMoney('£1,000.50')\n * // { valid: true, amount: 1000.5, currency: 'GBP', formatted: '£1,000.50' }\n *\n * parseMoney('not money')\n * // { valid: false, error: 'Could not detect currency from input' }\n */\nexport function parseMoney(input: string): MoneyResult {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n let currency: SupportedCurrency | undefined\n let cleaned = input.trim()\n\n for (const [symbol, code] of Object.entries(SYMBOL_MAP)) {\n if (cleaned.startsWith(symbol) || cleaned.endsWith(symbol)) {\n currency = code\n cleaned = cleaned.replace(symbol, '').trim()\n break\n }\n }\n\n if (!currency) {\n return { valid: false, error: 'Could not detect currency from input. Expected a symbol like £, €, $, ¥' }\n }\n\n // Remove thousands separators, normalise decimal separator\n const normalised = cleaned.replace(/,/g, '')\n const amount = parseFloat(normalised)\n\n if (isNaN(amount)) {\n return { valid: false, error: `Could not parse amount from: \"${cleaned}\"` }\n }\n\n return {\n valid: true,\n amount,\n currency,\n formatted: formatCurrency(amount, currency),\n }\n}\n","import type { BIC, ValidationResult } from './types'\n\n// BIC format: 4 letters (bank) + 2 letters (country) + 2 alphanumeric (location) + optional 3 alphanumeric (branch)\nconst BIC_REGEX = /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/\n\n/**\n * Validates a BIC (Bank Identifier Code) / SWIFT code.\n * Accepts both 8-character and 11-character BIC codes.\n *\n * Format: AAAABBCCXXX\n * - AAAA = Bank code (4 letters)\n * - BB = Country code (2 letters, ISO 3166-1)\n * - CC = Location code (2 alphanumeric)\n * - XXX = Branch code (3 alphanumeric, optional — 'XXX' means head office)\n *\n * @example\n * validateBIC('NWBKGB2L')\n * // { valid: true, value: 'NWBKGB2L', formatted: 'NWBKGB2L' }\n *\n * validateBIC('DEUTDEDB')\n * // { valid: true, value: 'DEUTDEDB', formatted: 'DEUTDEDB' }\n */\nexport function validateBIC(input: string): ValidationResult<BIC> {\n if (!input || typeof input !== 'string') {\n return { valid: false, error: 'Input must be a non-empty string' }\n }\n\n const cleaned = input.replace(/\\s/g, '').toUpperCase()\n\n if (cleaned.length !== 8 && cleaned.length !== 11) {\n return {\n valid: false,\n error: `BIC must be 8 or 11 characters. Got ${cleaned.length}`,\n }\n }\n\n if (!BIC_REGEX.test(cleaned)) {\n return {\n valid: false,\n error: 'Invalid BIC format. Expected: 4 letters + 2 letters + 2 alphanumeric + optional 3 alphanumeric',\n }\n }\n\n const bankCode = cleaned.slice(0, 4)\n const countryCode = cleaned.slice(4, 6)\n const location = cleaned.slice(6, 8)\n const branch = cleaned.length === 11 ? cleaned.slice(8, 11) : 'XXX'\n\n return {\n valid: true,\n value: cleaned as BIC,\n formatted: `${bankCode} ${countryCode} ${location} ${branch}`,\n }\n}\n","import { useState, useCallback } from 'react'\nimport { validateIBAN } from '../iban'\nimport { validateUKSortCode, validateUKAccountNumber } from '../sortcode'\nimport { formatCurrency } from '../currency'\nimport { validateBIC } from '../bic'\nimport type {\n SupportedCurrency,\n ValidationResult,\n IBAN,\n SortCode,\n AccountNumber,\n BIC,\n} from '../types'\n\ntype HookResult<T> = {\n value: string\n formatted: string\n valid: boolean | null\n error: string | null\n onChange: (e: React.ChangeEvent<HTMLInputElement>) => void\n result: ValidationResult<T> | null\n}\n\nfunction useValidatedInput<T>(\n validator: (val: string) => ValidationResult<T>,\n minLength = 1\n): HookResult<T> {\n const [value, setValue] = useState('')\n const [result, setResult] = useState<ValidationResult<T> | null>(null)\n\n const onChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const input = e.target.value\n setValue(input)\n if (input.length >= minLength) {\n setResult(validator(input))\n } else {\n setResult(null)\n }\n },\n [validator, minLength]\n )\n\n return {\n value,\n formatted: result?.valid ? result.formatted : value,\n valid: result === null ? null : result.valid,\n error: result && !result.valid ? result.error : null,\n onChange,\n result,\n }\n}\n\n/**\n * React hook for IBAN input fields.\n * Validates on change and returns formatted value and error state.\n *\n * @example\n * const { value, formatted, valid, error, onChange } = useIBANInput()\n *\n * return (\n * <input\n * value={formatted}\n * onChange={onChange}\n * aria-invalid={valid === false}\n * />\n * )\n */\nexport function useIBANInput(): HookResult<IBAN> {\n return useValidatedInput(validateIBAN, 5)\n}\n\n/**\n * React hook for UK sort code input fields.\n *\n * @example\n * const { formatted, valid, error, onChange } = useSortCodeInput()\n */\nexport function useSortCodeInput(): HookResult<SortCode> {\n return useValidatedInput(validateUKSortCode, 6)\n}\n\n/**\n * React hook for UK account number input fields.\n *\n * @example\n * const { formatted, valid, error, onChange } = useAccountNumberInput()\n */\nexport function useAccountNumberInput(): HookResult<AccountNumber> {\n return useValidatedInput(validateUKAccountNumber, 8)\n}\n\n/**\n * React hook for BIC / SWIFT code input fields.\n *\n * @example\n * const { formatted, valid, error, onChange } = useBICInput()\n */\nexport function useBICInput(): HookResult<BIC> {\n return useValidatedInput(validateBIC, 8)\n}\n\n/**\n * React hook for currency amount inputs with locale-aware formatting.\n * Returns both the raw numeric value and the formatted display string.\n *\n * @example\n * const { rawValue, formatted, onChange } = useCurrencyInput('GBP')\n *\n * return <input value={formatted} onChange={onChange} />\n */\nexport function useCurrencyInput(currency: SupportedCurrency, locale?: string) {\n const [rawValue, setRawValue] = useState<number | null>(null)\n\n const onChange = useCallback(\n (e: React.ChangeEvent<HTMLInputElement>) => {\n const num = parseFloat(e.target.value.replace(/[^0-9.]/g, ''))\n setRawValue(isNaN(num) ? null : num)\n },\n []\n )\n\n const formatted = rawValue !== null ? formatCurrency(rawValue, currency, locale) : ''\n\n return { rawValue, formatted, onChange }\n}\n"]}
@@ -0,0 +1,32 @@
1
+ declare const __brand: unique symbol;
2
+ type Brand<T, B> = T & {
3
+ readonly [__brand]: B;
4
+ };
5
+ type IBAN = Brand<string, 'IBAN'>;
6
+ type SortCode = Brand<string, 'SortCode'>;
7
+ type AccountNumber = Brand<string, 'AccountNumber'>;
8
+ type CurrencyCode = Brand<string, 'CurrencyCode'>;
9
+ type BIC = Brand<string, 'BIC'>;
10
+ type CardNumber = Brand<string, 'CardNumber'>;
11
+ type SupportedCurrency = 'GBP' | 'EUR' | 'USD' | 'JPY' | 'CHF' | 'CAD' | 'AUD' | 'NZD';
12
+ type ValidationSuccess<T> = {
13
+ valid: true;
14
+ value: T;
15
+ formatted: string;
16
+ };
17
+ type ValidationFailure = {
18
+ valid: false;
19
+ error: string;
20
+ };
21
+ type ValidationResult<T> = ValidationSuccess<T> | ValidationFailure;
22
+ type MoneyResult = {
23
+ valid: true;
24
+ amount: number;
25
+ currency: SupportedCurrency;
26
+ formatted: string;
27
+ } | {
28
+ valid: false;
29
+ error: string;
30
+ };
31
+
32
+ export type { AccountNumber as A, BIC as B, CurrencyCode as C, IBAN as I, MoneyResult as M, SortCode as S, ValidationResult as V, SupportedCurrency as a, CardNumber as b, ValidationFailure as c, ValidationSuccess as d };