finprim 0.1.0 → 0.1.2
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.
- package/LICENSE +21 -0
- package/README.md +276 -0
- package/dist/card-D2-7wbam.d.mts +57 -0
- package/dist/card-D2-7wbam.d.ts +57 -0
- package/dist/index.d.mts +19 -113
- package/dist/index.d.ts +19 -113
- package/dist/index.js +228 -41
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +220 -42
- package/dist/index.mjs.map +1 -1
- package/dist/nest/index.d.mts +18 -0
- package/dist/nest/index.d.ts +18 -0
- package/dist/nest/index.js +448 -0
- package/dist/nest/index.js.map +1 -0
- package/dist/nest/index.mjs +438 -0
- package/dist/nest/index.mjs.map +1 -0
- package/dist/react/index.d.mts +5 -46
- package/dist/react/index.d.ts +5 -46
- package/dist/react/index.js +105 -28
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +105 -29
- package/dist/react/index.mjs.map +1 -1
- package/dist/zod/index.d.mts +3 -36
- package/dist/zod/index.d.ts +3 -36
- package/dist/zod/index.js +150 -42
- package/dist/zod/index.js.map +1 -1
- package/dist/zod/index.mjs +149 -43
- package/dist/zod/index.mjs.map +1 -1
- package/package.json +43 -14
- package/dist/types-KG-eFvWt.d.mts +0 -32
- package/dist/types-KG-eFvWt.d.ts +0 -32
package/dist/react/index.d.ts
CHANGED
|
@@ -1,63 +1,22 @@
|
|
|
1
|
-
import { V as ValidationResult, A as AccountNumber, B as BIC, a as SupportedCurrency,
|
|
1
|
+
import { V as ValidationResult, A as AccountNumber, B as BIC, d as CardNumber, e as CardValidationResult, a as SupportedCurrency, f as IBAN, S as SortCode } from '../card-D2-7wbam.js';
|
|
2
2
|
|
|
3
|
-
type HookResult<T
|
|
3
|
+
type HookResult<T, R = ValidationResult<T>> = {
|
|
4
4
|
value: string;
|
|
5
5
|
formatted: string;
|
|
6
6
|
valid: boolean | null;
|
|
7
7
|
error: string | null;
|
|
8
8
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
9
|
-
result:
|
|
9
|
+
result: R | null;
|
|
10
10
|
};
|
|
11
|
-
/**
|
|
12
|
-
* React hook for IBAN input fields.
|
|
13
|
-
* Validates on change and returns formatted value and error state.
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* const { value, formatted, valid, error, onChange } = useIBANInput()
|
|
17
|
-
*
|
|
18
|
-
* return (
|
|
19
|
-
* <input
|
|
20
|
-
* value={formatted}
|
|
21
|
-
* onChange={onChange}
|
|
22
|
-
* aria-invalid={valid === false}
|
|
23
|
-
* />
|
|
24
|
-
* )
|
|
25
|
-
*/
|
|
26
11
|
declare function useIBANInput(): HookResult<IBAN>;
|
|
27
|
-
/**
|
|
28
|
-
* React hook for UK sort code input fields.
|
|
29
|
-
*
|
|
30
|
-
* @example
|
|
31
|
-
* const { formatted, valid, error, onChange } = useSortCodeInput()
|
|
32
|
-
*/
|
|
33
12
|
declare function useSortCodeInput(): HookResult<SortCode>;
|
|
34
|
-
/**
|
|
35
|
-
* React hook for UK account number input fields.
|
|
36
|
-
*
|
|
37
|
-
* @example
|
|
38
|
-
* const { formatted, valid, error, onChange } = useAccountNumberInput()
|
|
39
|
-
*/
|
|
40
13
|
declare function useAccountNumberInput(): HookResult<AccountNumber>;
|
|
41
|
-
/**
|
|
42
|
-
* React hook for BIC / SWIFT code input fields.
|
|
43
|
-
*
|
|
44
|
-
* @example
|
|
45
|
-
* const { formatted, valid, error, onChange } = useBICInput()
|
|
46
|
-
*/
|
|
47
14
|
declare function useBICInput(): HookResult<BIC>;
|
|
48
|
-
|
|
49
|
-
* React hook for currency amount inputs with locale-aware formatting.
|
|
50
|
-
* Returns both the raw numeric value and the formatted display string.
|
|
51
|
-
*
|
|
52
|
-
* @example
|
|
53
|
-
* const { rawValue, formatted, onChange } = useCurrencyInput('GBP')
|
|
54
|
-
*
|
|
55
|
-
* return <input value={formatted} onChange={onChange} />
|
|
56
|
-
*/
|
|
15
|
+
declare function useCardNumberInput(): HookResult<CardNumber, CardValidationResult>;
|
|
57
16
|
declare function useCurrencyInput(currency: SupportedCurrency, locale?: string): {
|
|
58
17
|
rawValue: number | null;
|
|
59
18
|
formatted: string;
|
|
60
19
|
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
|
|
61
20
|
};
|
|
62
21
|
|
|
63
|
-
export { useAccountNumberInput, useBICInput, useCurrencyInput, useIBANInput, useSortCodeInput };
|
|
22
|
+
export { useAccountNumberInput, useBICInput, useCardNumberInput, useCurrencyInput, useIBANInput, useSortCodeInput };
|
package/dist/react/index.js
CHANGED
|
@@ -4,6 +4,24 @@ var react = require('react');
|
|
|
4
4
|
|
|
5
5
|
// src/react/index.ts
|
|
6
6
|
|
|
7
|
+
// src/_guard.ts
|
|
8
|
+
var MAX_SAFE_INPUT_LENGTH = 256;
|
|
9
|
+
function guardStringInput(input, label = "Input") {
|
|
10
|
+
if (input == null || typeof input !== "string") {
|
|
11
|
+
return { ok: false, error: `${label} must be a non-empty string` };
|
|
12
|
+
}
|
|
13
|
+
if (input.length === 0) {
|
|
14
|
+
return { ok: false, error: `${label} must be a non-empty string` };
|
|
15
|
+
}
|
|
16
|
+
if (input.length > MAX_SAFE_INPUT_LENGTH) {
|
|
17
|
+
return {
|
|
18
|
+
ok: false,
|
|
19
|
+
error: `${label} must not exceed ${MAX_SAFE_INPUT_LENGTH} characters`
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
return { ok: true, value: input };
|
|
23
|
+
}
|
|
24
|
+
|
|
7
25
|
// src/iban.ts
|
|
8
26
|
var IBAN_LENGTHS = {
|
|
9
27
|
AL: 28,
|
|
@@ -71,28 +89,29 @@ var IBAN_LENGTHS = {
|
|
|
71
89
|
GB: 22,
|
|
72
90
|
VG: 24
|
|
73
91
|
};
|
|
92
|
+
var LETTER_A = "A".codePointAt(0);
|
|
93
|
+
var LETTER_Z = "Z".codePointAt(0);
|
|
94
|
+
var LETTER_TO_DIGIT_OFFSET = 55;
|
|
74
95
|
function mod97(value) {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
return remainder;
|
|
96
|
+
return [...value].reduce(
|
|
97
|
+
(remainder, char) => (remainder * 10 + Number.parseInt(char, 10)) % 97,
|
|
98
|
+
0
|
|
99
|
+
);
|
|
80
100
|
}
|
|
81
101
|
function ibanToDigits(iban) {
|
|
82
102
|
const rearranged = iban.slice(4) + iban.slice(0, 4);
|
|
83
|
-
return rearranged.
|
|
84
|
-
const code = char.
|
|
85
|
-
return code >=
|
|
103
|
+
return [...rearranged].map((char) => {
|
|
104
|
+
const code = char.codePointAt(0) ?? 0;
|
|
105
|
+
return code >= LETTER_A && code <= LETTER_Z ? (code - LETTER_TO_DIGIT_OFFSET).toString() : char;
|
|
86
106
|
}).join("");
|
|
87
107
|
}
|
|
88
108
|
function formatIBANString(iban) {
|
|
89
109
|
return iban.replace(/(.{4})/g, "$1 ").trim();
|
|
90
110
|
}
|
|
91
111
|
function validateIBAN(input) {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const cleaned = input.replace(/\s/g, "").toUpperCase();
|
|
112
|
+
const guarded = guardStringInput(input);
|
|
113
|
+
if (!guarded.ok) return { valid: false, error: guarded.error };
|
|
114
|
+
const cleaned = guarded.value.replace(/\s/g, "").toUpperCase();
|
|
96
115
|
if (cleaned.length < 4) {
|
|
97
116
|
return { valid: false, error: "IBAN is too short" };
|
|
98
117
|
}
|
|
@@ -120,16 +139,16 @@ function validateIBAN(input) {
|
|
|
120
139
|
return {
|
|
121
140
|
valid: true,
|
|
122
141
|
value: cleaned,
|
|
123
|
-
formatted: formatIBANString(cleaned)
|
|
142
|
+
formatted: formatIBANString(cleaned),
|
|
143
|
+
countryCode
|
|
124
144
|
};
|
|
125
145
|
}
|
|
126
146
|
|
|
127
147
|
// src/sortcode.ts
|
|
128
148
|
function validateUKSortCode(input) {
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const cleaned = input.replace(/[-\s]/g, "");
|
|
149
|
+
const guarded = guardStringInput(input);
|
|
150
|
+
if (!guarded.ok) return { valid: false, error: guarded.error };
|
|
151
|
+
const cleaned = guarded.value.replace(/[-\s]/g, "");
|
|
133
152
|
if (!/^\d{6}$/.test(cleaned)) {
|
|
134
153
|
return {
|
|
135
154
|
valid: false,
|
|
@@ -144,10 +163,9 @@ function validateUKSortCode(input) {
|
|
|
144
163
|
};
|
|
145
164
|
}
|
|
146
165
|
function validateUKAccountNumber(input) {
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const cleaned = input.replace(/\s/g, "");
|
|
166
|
+
const guarded = guardStringInput(input);
|
|
167
|
+
if (!guarded.ok) return { valid: false, error: guarded.error };
|
|
168
|
+
const cleaned = guarded.value.replace(/\s/g, "");
|
|
151
169
|
if (!/^\d{8}$/.test(cleaned)) {
|
|
152
170
|
return {
|
|
153
171
|
valid: false,
|
|
@@ -174,6 +192,9 @@ var CURRENCY_LOCALES = {
|
|
|
174
192
|
NZD: "en-NZ"
|
|
175
193
|
};
|
|
176
194
|
function formatCurrency(amount, currency, locale) {
|
|
195
|
+
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
|
196
|
+
return "";
|
|
197
|
+
}
|
|
177
198
|
const resolvedLocale = locale ?? CURRENCY_LOCALES[currency] ?? "en-GB";
|
|
178
199
|
return new Intl.NumberFormat(resolvedLocale, {
|
|
179
200
|
style: "currency",
|
|
@@ -186,10 +207,9 @@ function formatCurrency(amount, currency, locale) {
|
|
|
186
207
|
// src/bic.ts
|
|
187
208
|
var BIC_REGEX = /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/;
|
|
188
209
|
function validateBIC(input) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const cleaned = input.replace(/\s/g, "").toUpperCase();
|
|
210
|
+
const guarded = guardStringInput(input);
|
|
211
|
+
if (!guarded.ok) return { valid: false, error: guarded.error };
|
|
212
|
+
const cleaned = guarded.value.replace(/\s/g, "").toUpperCase();
|
|
193
213
|
if (cleaned.length !== 8 && cleaned.length !== 11) {
|
|
194
214
|
return {
|
|
195
215
|
valid: false,
|
|
@@ -213,6 +233,57 @@ function validateBIC(input) {
|
|
|
213
233
|
};
|
|
214
234
|
}
|
|
215
235
|
|
|
236
|
+
// src/card.ts
|
|
237
|
+
function detectNetwork(digits) {
|
|
238
|
+
if (/^4/.test(digits)) return "Visa";
|
|
239
|
+
if (/^5[1-5]/.test(digits) || /^2[2-7]/.test(digits)) return "Mastercard";
|
|
240
|
+
if (/^3[47]/.test(digits)) return "Amex";
|
|
241
|
+
if (/^6(?:011|5)/.test(digits)) return "Discover";
|
|
242
|
+
return "Unknown";
|
|
243
|
+
}
|
|
244
|
+
function formatCardNumber(digits, network) {
|
|
245
|
+
if (network === "Amex") {
|
|
246
|
+
return `${digits.slice(0, 4)} ${digits.slice(4, 10)} ${digits.slice(10, 15)}`;
|
|
247
|
+
}
|
|
248
|
+
return digits.replace(/(.{4})/g, "$1 ").trim();
|
|
249
|
+
}
|
|
250
|
+
function validateCardNumber(input) {
|
|
251
|
+
const guarded = guardStringInput(input);
|
|
252
|
+
if (!guarded.ok) return { valid: false, error: guarded.error };
|
|
253
|
+
const digits = guarded.value.replace(/[\s-]/g, "");
|
|
254
|
+
if (!/^\d+$/.test(digits)) {
|
|
255
|
+
return { valid: false, error: "Card number must contain only digits" };
|
|
256
|
+
}
|
|
257
|
+
if (digits.length < 13 || digits.length > 19) {
|
|
258
|
+
return {
|
|
259
|
+
valid: false,
|
|
260
|
+
error: `Card number length invalid. Expected 13-19 digits, got ${digits.length}`
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
let sum = 0;
|
|
264
|
+
let shouldDouble = false;
|
|
265
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
266
|
+
let digit = Number.parseInt(digits[i], 10);
|
|
267
|
+
if (shouldDouble) {
|
|
268
|
+
digit *= 2;
|
|
269
|
+
if (digit > 9) digit -= 9;
|
|
270
|
+
}
|
|
271
|
+
sum += digit;
|
|
272
|
+
shouldDouble = !shouldDouble;
|
|
273
|
+
}
|
|
274
|
+
if (sum % 10 !== 0) {
|
|
275
|
+
return { valid: false, error: "Card number failed Luhn check \u2014 this is not a valid card number" };
|
|
276
|
+
}
|
|
277
|
+
const network = detectNetwork(digits);
|
|
278
|
+
return {
|
|
279
|
+
valid: true,
|
|
280
|
+
value: digits,
|
|
281
|
+
formatted: formatCardNumber(digits, network),
|
|
282
|
+
network,
|
|
283
|
+
last4: digits.slice(-4)
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
216
287
|
// src/react/index.ts
|
|
217
288
|
function useValidatedInput(validator, minLength = 1) {
|
|
218
289
|
const [value, setValue] = react.useState("");
|
|
@@ -229,11 +300,12 @@ function useValidatedInput(validator, minLength = 1) {
|
|
|
229
300
|
},
|
|
230
301
|
[validator, minLength]
|
|
231
302
|
);
|
|
303
|
+
const error = result?.valid === false ? result.error : null;
|
|
232
304
|
return {
|
|
233
305
|
value,
|
|
234
306
|
formatted: result?.valid ? result.formatted : value,
|
|
235
307
|
valid: result === null ? null : result.valid,
|
|
236
|
-
error
|
|
308
|
+
error,
|
|
237
309
|
onChange,
|
|
238
310
|
result
|
|
239
311
|
};
|
|
@@ -250,12 +322,16 @@ function useAccountNumberInput() {
|
|
|
250
322
|
function useBICInput() {
|
|
251
323
|
return useValidatedInput(validateBIC, 8);
|
|
252
324
|
}
|
|
325
|
+
function useCardNumberInput() {
|
|
326
|
+
return useValidatedInput(validateCardNumber, 8);
|
|
327
|
+
}
|
|
253
328
|
function useCurrencyInput(currency, locale) {
|
|
254
329
|
const [rawValue, setRawValue] = react.useState(null);
|
|
255
330
|
const onChange = react.useCallback(
|
|
256
331
|
(e) => {
|
|
257
|
-
const
|
|
258
|
-
|
|
332
|
+
const digits = e.target.value.replace(/[^0-9.]/g, "");
|
|
333
|
+
const num = Number.parseFloat(digits);
|
|
334
|
+
setRawValue(Number.isNaN(num) ? null : num);
|
|
259
335
|
},
|
|
260
336
|
[]
|
|
261
337
|
);
|
|
@@ -265,6 +341,7 @@ function useCurrencyInput(currency, locale) {
|
|
|
265
341
|
|
|
266
342
|
exports.useAccountNumberInput = useAccountNumberInput;
|
|
267
343
|
exports.useBICInput = useBICInput;
|
|
344
|
+
exports.useCardNumberInput = useCardNumberInput;
|
|
268
345
|
exports.useCurrencyInput = useCurrencyInput;
|
|
269
346
|
exports.useIBANInput = useIBANInput;
|
|
270
347
|
exports.useSortCodeInput = useSortCodeInput;
|
package/dist/react/index.js.map
CHANGED
|
@@ -1 +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"]}
|
|
1
|
+
{"version":3,"sources":["../../src/_guard.ts","../../src/iban.ts","../../src/sortcode.ts","../../src/currency.ts","../../src/bic.ts","../../src/card.ts","../../src/react/index.ts"],"names":["useState","useCallback"],"mappings":"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","file":"index.js","sourcesContent":["export const MAX_SAFE_INPUT_LENGTH = 256\n\nexport function guardStringInput(\n input: unknown,\n label = 'Input'\n): { ok: true; value: string } | { ok: false; error: string } {\n if (input == null || typeof input !== 'string') {\n return { ok: false, error: `${label} must be a non-empty string` }\n }\n if (input.length === 0) {\n return { ok: false, error: `${label} must be a non-empty string` }\n }\n if (input.length > MAX_SAFE_INPUT_LENGTH) {\n return {\n ok: false,\n error: `${label} must not exceed ${MAX_SAFE_INPUT_LENGTH} characters`,\n }\n }\n return { ok: true, value: input }\n}\n\nexport function guardNumber(\n value: unknown,\n options: { min?: number; max?: number; integer?: boolean; label?: string }\n): { ok: true; value: number } | { ok: false; error: string } {\n const label = options.label ?? 'Value'\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n return { ok: false, error: `${label} must be a finite number` }\n }\n if (Number.isNaN(value)) {\n return { ok: false, error: `${label} must not be NaN` }\n }\n if (options.integer && !Number.isInteger(value)) {\n return { ok: false, error: `${label} must be an integer` }\n }\n if (options.min !== undefined && value < options.min) {\n return { ok: false, error: `${label} must be at least ${options.min}` }\n }\n if (options.max !== undefined && value > options.max) {\n return { ok: false, error: `${label} must be at most ${options.max}` }\n }\n return { ok: true, value }\n}\n","import type { IBAN, IBANValidationResult } from './types'\nimport { guardStringInput } from './_guard'\n\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\nconst LETTER_A = 'A'.codePointAt(0)!\nconst LETTER_Z = 'Z'.codePointAt(0)!\nconst LETTER_TO_DIGIT_OFFSET = 55\n\nfunction mod97(value: string): number {\n return [...value].reduce(\n (remainder, char) => (remainder * 10 + Number.parseInt(char, 10)) % 97,\n 0\n )\n}\n\nfunction ibanToDigits(iban: string): string {\n const rearranged = iban.slice(4) + iban.slice(0, 4)\n return [...rearranged]\n .map((char) => {\n const code = char.codePointAt(0) ?? 0\n return code >= LETTER_A && code <= LETTER_Z\n ? (code - LETTER_TO_DIGIT_OFFSET).toString()\n : char\n })\n .join('')\n}\n\nfunction formatIBANString(iban: string): string {\n return iban.replace(/(.{4})/g, '$1 ').trim()\n}\n\nexport function formatIBAN(input: string): string {\n if (typeof input !== 'string') return ''\n const cleaned = input.replace(/\\s/g, '').toUpperCase().slice(0, 34)\n return formatIBANString(cleaned)\n}\n\nexport function validateIBAN(input: string): IBANValidationResult {\n const guarded = guardStringInput(input)\n if (!guarded.ok) return { valid: false, error: guarded.error }\n\n const cleaned = guarded.value.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 countryCode,\n }\n}\n","import type { SortCode, AccountNumber, ValidationResult } from './types'\nimport { guardStringInput } from './_guard'\n\nexport function formatSortCode(input: string): string {\n if (typeof input !== 'string') return ''\n const digits = input.replace(/[-\\s]/g, '').slice(0, 6)\n if (digits.length < 6) return digits\n return `${digits.slice(0, 2)}-${digits.slice(2, 4)}-${digits.slice(4, 6)}`\n}\n\nexport function formatUKAccountNumber(input: string): string {\n if (typeof input !== 'string') return ''\n const digits = input.replace(/\\s/g, '').slice(0, 8)\n if (digits.length < 8) return digits\n return `${digits.slice(0, 4)} ${digits.slice(4, 8)}`\n}\n\nexport function validateUKSortCode(input: string): ValidationResult<SortCode> {\n const guarded = guardStringInput(input)\n if (!guarded.ok) return { valid: false, error: guarded.error }\n\n const cleaned = guarded.value.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\nexport function validateUKAccountNumber(input: string): ValidationResult<AccountNumber> {\n const guarded = guardStringInput(input)\n if (!guarded.ok) return { valid: false, error: guarded.error }\n\n const cleaned = guarded.value.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'\nimport { guardStringInput } from './_guard'\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\nexport function validateCurrencyCode(input: string): ValidationResult<CurrencyCode> {\n const guarded = guardStringInput(input)\n if (!guarded.ok) return { valid: false, error: guarded.error }\n\n const upper = guarded.value.toUpperCase() as SupportedCurrency\n\n if (!SUPPORTED_CURRENCIES.includes(upper)) {\n return {\n valid: false,\n error: `Unsupported currency code: ${upper}. Supported: ${SUPPORTED_CURRENCIES.join(', ')}`,\n }\n }\n\n return {\n valid: true,\n value: upper as CurrencyCode,\n formatted: upper,\n }\n}\n\nexport function formatCurrency(\n amount: number,\n currency: SupportedCurrency,\n locale?: string\n): string {\n if (typeof amount !== 'number' || !Number.isFinite(amount)) {\n return ''\n }\n const resolvedLocale = locale ?? CURRENCY_LOCALES[currency] ?? 'en-GB'\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\nexport function parseMoney(input: string): MoneyResult {\n const guarded = guardStringInput(input)\n if (!guarded.ok) return { valid: false, error: guarded.error }\n\n let currency: SupportedCurrency | undefined\n let cleaned = guarded.value.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 const normalised = cleaned.replace(/,/g, '')\n const amount = Number.parseFloat(normalised)\n\n if (Number.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'\nimport { guardStringInput } from './_guard'\n\nconst BIC_REGEX = /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/\n\nexport function validateBIC(input: string): ValidationResult<BIC> {\n const guarded = guardStringInput(input)\n if (!guarded.ok) return { valid: false, error: guarded.error }\n\n const cleaned = guarded.value.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 type { CardNumber, ValidationResult } from './types'\nimport { guardStringInput } from './_guard'\n\nexport type CardNetwork = 'Visa' | 'Mastercard' | 'Amex' | 'Discover' | 'Unknown'\n\nexport type CardValidationResult =\n | { valid: true; value: CardNumber; formatted: string; network: CardNetwork; last4: string }\n | { valid: false; error: string }\n\nfunction detectNetwork(digits: string): CardNetwork {\n if (/^4/.test(digits)) return 'Visa'\n if (/^5[1-5]/.test(digits) || /^2[2-7]/.test(digits)) return 'Mastercard'\n if (/^3[47]/.test(digits)) return 'Amex'\n if (/^6(?:011|5)/.test(digits)) return 'Discover'\n return 'Unknown'\n}\n\nfunction formatCardNumber(digits: string, network: CardNetwork): string {\n if (network === 'Amex') {\n return `${digits.slice(0, 4)} ${digits.slice(4, 10)} ${digits.slice(10, 15)}`\n }\n return digits.replace(/(.{4})/g, '$1 ').trim()\n}\n\nexport function validateCardNumber(input: string): CardValidationResult {\n const guarded = guardStringInput(input)\n if (!guarded.ok) return { valid: false, error: guarded.error }\n\n const digits = guarded.value.replace(/[\\s-]/g, '')\n\n if (!/^\\d+$/.test(digits)) {\n return { valid: false, error: 'Card number must contain only digits' }\n }\n\n if (digits.length < 13 || digits.length > 19) {\n return {\n valid: false,\n error: `Card number length invalid. Expected 13-19 digits, got ${digits.length}`,\n }\n }\n\n let sum = 0\n let shouldDouble = false\n\n for (let i = digits.length - 1; i >= 0; i--) {\n let digit = Number.parseInt(digits[i]!, 10)\n if (shouldDouble) {\n digit *= 2\n if (digit > 9) digit -= 9\n }\n sum += digit\n shouldDouble = !shouldDouble\n }\n\n if (sum % 10 !== 0) {\n return { valid: false, error: 'Card number failed Luhn check — this is not a valid card number' }\n }\n\n const network = detectNetwork(digits)\n\n return {\n valid: true,\n value: digits as CardNumber,\n formatted: formatCardNumber(digits, network),\n network,\n last4: digits.slice(-4),\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 { validateCardNumber } from '../card'\nimport type { CardValidationResult } from '../card'\nimport type {\n SupportedCurrency,\n ValidationResult,\n IBAN,\n SortCode,\n AccountNumber,\n BIC,\n CardNumber,\n} from '../types'\n\ntype HookResult<T, R = ValidationResult<T>> = {\n value: string\n formatted: string\n valid: boolean | null\n error: string | null\n onChange: (e: React.ChangeEvent<HTMLInputElement>) => void\n result: R | null\n}\n\nfunction useValidatedInput<T, R extends ValidationResult<T> = ValidationResult<T>>(\n validator: (val: string) => R,\n minLength = 1\n): HookResult<T, R> {\n const [value, setValue] = useState('')\n const [result, setResult] = useState<R | 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 const error = result?.valid === false ? result.error : null\n\n return {\n value,\n formatted: result?.valid ? result.formatted : value,\n valid: result === null ? null : result.valid,\n error,\n onChange,\n result,\n }\n}\n\nexport function useIBANInput(): HookResult<IBAN> {\n return useValidatedInput(validateIBAN, 5)\n}\n\nexport function useSortCodeInput(): HookResult<SortCode> {\n return useValidatedInput(validateUKSortCode, 6)\n}\n\nexport function useAccountNumberInput(): HookResult<AccountNumber> {\n return useValidatedInput(validateUKAccountNumber, 8)\n}\n\nexport function useBICInput(): HookResult<BIC> {\n return useValidatedInput(validateBIC, 8)\n}\n\nexport function useCardNumberInput(): HookResult<CardNumber, CardValidationResult> {\n return useValidatedInput(validateCardNumber, 8)\n}\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 digits = e.target.value.replace(/[^0-9.]/g, '')\n const num = Number.parseFloat(digits)\n setRawValue(Number.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"]}
|
package/dist/react/index.mjs
CHANGED
|
@@ -2,6 +2,24 @@ import { useState, useCallback } from 'react';
|
|
|
2
2
|
|
|
3
3
|
// src/react/index.ts
|
|
4
4
|
|
|
5
|
+
// src/_guard.ts
|
|
6
|
+
var MAX_SAFE_INPUT_LENGTH = 256;
|
|
7
|
+
function guardStringInput(input, label = "Input") {
|
|
8
|
+
if (input == null || typeof input !== "string") {
|
|
9
|
+
return { ok: false, error: `${label} must be a non-empty string` };
|
|
10
|
+
}
|
|
11
|
+
if (input.length === 0) {
|
|
12
|
+
return { ok: false, error: `${label} must be a non-empty string` };
|
|
13
|
+
}
|
|
14
|
+
if (input.length > MAX_SAFE_INPUT_LENGTH) {
|
|
15
|
+
return {
|
|
16
|
+
ok: false,
|
|
17
|
+
error: `${label} must not exceed ${MAX_SAFE_INPUT_LENGTH} characters`
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
return { ok: true, value: input };
|
|
21
|
+
}
|
|
22
|
+
|
|
5
23
|
// src/iban.ts
|
|
6
24
|
var IBAN_LENGTHS = {
|
|
7
25
|
AL: 28,
|
|
@@ -69,28 +87,29 @@ var IBAN_LENGTHS = {
|
|
|
69
87
|
GB: 22,
|
|
70
88
|
VG: 24
|
|
71
89
|
};
|
|
90
|
+
var LETTER_A = "A".codePointAt(0);
|
|
91
|
+
var LETTER_Z = "Z".codePointAt(0);
|
|
92
|
+
var LETTER_TO_DIGIT_OFFSET = 55;
|
|
72
93
|
function mod97(value) {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return remainder;
|
|
94
|
+
return [...value].reduce(
|
|
95
|
+
(remainder, char) => (remainder * 10 + Number.parseInt(char, 10)) % 97,
|
|
96
|
+
0
|
|
97
|
+
);
|
|
78
98
|
}
|
|
79
99
|
function ibanToDigits(iban) {
|
|
80
100
|
const rearranged = iban.slice(4) + iban.slice(0, 4);
|
|
81
|
-
return rearranged.
|
|
82
|
-
const code = char.
|
|
83
|
-
return code >=
|
|
101
|
+
return [...rearranged].map((char) => {
|
|
102
|
+
const code = char.codePointAt(0) ?? 0;
|
|
103
|
+
return code >= LETTER_A && code <= LETTER_Z ? (code - LETTER_TO_DIGIT_OFFSET).toString() : char;
|
|
84
104
|
}).join("");
|
|
85
105
|
}
|
|
86
106
|
function formatIBANString(iban) {
|
|
87
107
|
return iban.replace(/(.{4})/g, "$1 ").trim();
|
|
88
108
|
}
|
|
89
109
|
function validateIBAN(input) {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const cleaned = input.replace(/\s/g, "").toUpperCase();
|
|
110
|
+
const guarded = guardStringInput(input);
|
|
111
|
+
if (!guarded.ok) return { valid: false, error: guarded.error };
|
|
112
|
+
const cleaned = guarded.value.replace(/\s/g, "").toUpperCase();
|
|
94
113
|
if (cleaned.length < 4) {
|
|
95
114
|
return { valid: false, error: "IBAN is too short" };
|
|
96
115
|
}
|
|
@@ -118,16 +137,16 @@ function validateIBAN(input) {
|
|
|
118
137
|
return {
|
|
119
138
|
valid: true,
|
|
120
139
|
value: cleaned,
|
|
121
|
-
formatted: formatIBANString(cleaned)
|
|
140
|
+
formatted: formatIBANString(cleaned),
|
|
141
|
+
countryCode
|
|
122
142
|
};
|
|
123
143
|
}
|
|
124
144
|
|
|
125
145
|
// src/sortcode.ts
|
|
126
146
|
function validateUKSortCode(input) {
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
const cleaned = input.replace(/[-\s]/g, "");
|
|
147
|
+
const guarded = guardStringInput(input);
|
|
148
|
+
if (!guarded.ok) return { valid: false, error: guarded.error };
|
|
149
|
+
const cleaned = guarded.value.replace(/[-\s]/g, "");
|
|
131
150
|
if (!/^\d{6}$/.test(cleaned)) {
|
|
132
151
|
return {
|
|
133
152
|
valid: false,
|
|
@@ -142,10 +161,9 @@ function validateUKSortCode(input) {
|
|
|
142
161
|
};
|
|
143
162
|
}
|
|
144
163
|
function validateUKAccountNumber(input) {
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
const cleaned = input.replace(/\s/g, "");
|
|
164
|
+
const guarded = guardStringInput(input);
|
|
165
|
+
if (!guarded.ok) return { valid: false, error: guarded.error };
|
|
166
|
+
const cleaned = guarded.value.replace(/\s/g, "");
|
|
149
167
|
if (!/^\d{8}$/.test(cleaned)) {
|
|
150
168
|
return {
|
|
151
169
|
valid: false,
|
|
@@ -172,6 +190,9 @@ var CURRENCY_LOCALES = {
|
|
|
172
190
|
NZD: "en-NZ"
|
|
173
191
|
};
|
|
174
192
|
function formatCurrency(amount, currency, locale) {
|
|
193
|
+
if (typeof amount !== "number" || !Number.isFinite(amount)) {
|
|
194
|
+
return "";
|
|
195
|
+
}
|
|
175
196
|
const resolvedLocale = locale ?? CURRENCY_LOCALES[currency] ?? "en-GB";
|
|
176
197
|
return new Intl.NumberFormat(resolvedLocale, {
|
|
177
198
|
style: "currency",
|
|
@@ -184,10 +205,9 @@ function formatCurrency(amount, currency, locale) {
|
|
|
184
205
|
// src/bic.ts
|
|
185
206
|
var BIC_REGEX = /^[A-Z]{4}[A-Z]{2}[A-Z0-9]{2}([A-Z0-9]{3})?$/;
|
|
186
207
|
function validateBIC(input) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
const cleaned = input.replace(/\s/g, "").toUpperCase();
|
|
208
|
+
const guarded = guardStringInput(input);
|
|
209
|
+
if (!guarded.ok) return { valid: false, error: guarded.error };
|
|
210
|
+
const cleaned = guarded.value.replace(/\s/g, "").toUpperCase();
|
|
191
211
|
if (cleaned.length !== 8 && cleaned.length !== 11) {
|
|
192
212
|
return {
|
|
193
213
|
valid: false,
|
|
@@ -211,6 +231,57 @@ function validateBIC(input) {
|
|
|
211
231
|
};
|
|
212
232
|
}
|
|
213
233
|
|
|
234
|
+
// src/card.ts
|
|
235
|
+
function detectNetwork(digits) {
|
|
236
|
+
if (/^4/.test(digits)) return "Visa";
|
|
237
|
+
if (/^5[1-5]/.test(digits) || /^2[2-7]/.test(digits)) return "Mastercard";
|
|
238
|
+
if (/^3[47]/.test(digits)) return "Amex";
|
|
239
|
+
if (/^6(?:011|5)/.test(digits)) return "Discover";
|
|
240
|
+
return "Unknown";
|
|
241
|
+
}
|
|
242
|
+
function formatCardNumber(digits, network) {
|
|
243
|
+
if (network === "Amex") {
|
|
244
|
+
return `${digits.slice(0, 4)} ${digits.slice(4, 10)} ${digits.slice(10, 15)}`;
|
|
245
|
+
}
|
|
246
|
+
return digits.replace(/(.{4})/g, "$1 ").trim();
|
|
247
|
+
}
|
|
248
|
+
function validateCardNumber(input) {
|
|
249
|
+
const guarded = guardStringInput(input);
|
|
250
|
+
if (!guarded.ok) return { valid: false, error: guarded.error };
|
|
251
|
+
const digits = guarded.value.replace(/[\s-]/g, "");
|
|
252
|
+
if (!/^\d+$/.test(digits)) {
|
|
253
|
+
return { valid: false, error: "Card number must contain only digits" };
|
|
254
|
+
}
|
|
255
|
+
if (digits.length < 13 || digits.length > 19) {
|
|
256
|
+
return {
|
|
257
|
+
valid: false,
|
|
258
|
+
error: `Card number length invalid. Expected 13-19 digits, got ${digits.length}`
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
let sum = 0;
|
|
262
|
+
let shouldDouble = false;
|
|
263
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
264
|
+
let digit = Number.parseInt(digits[i], 10);
|
|
265
|
+
if (shouldDouble) {
|
|
266
|
+
digit *= 2;
|
|
267
|
+
if (digit > 9) digit -= 9;
|
|
268
|
+
}
|
|
269
|
+
sum += digit;
|
|
270
|
+
shouldDouble = !shouldDouble;
|
|
271
|
+
}
|
|
272
|
+
if (sum % 10 !== 0) {
|
|
273
|
+
return { valid: false, error: "Card number failed Luhn check \u2014 this is not a valid card number" };
|
|
274
|
+
}
|
|
275
|
+
const network = detectNetwork(digits);
|
|
276
|
+
return {
|
|
277
|
+
valid: true,
|
|
278
|
+
value: digits,
|
|
279
|
+
formatted: formatCardNumber(digits, network),
|
|
280
|
+
network,
|
|
281
|
+
last4: digits.slice(-4)
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
214
285
|
// src/react/index.ts
|
|
215
286
|
function useValidatedInput(validator, minLength = 1) {
|
|
216
287
|
const [value, setValue] = useState("");
|
|
@@ -227,11 +298,12 @@ function useValidatedInput(validator, minLength = 1) {
|
|
|
227
298
|
},
|
|
228
299
|
[validator, minLength]
|
|
229
300
|
);
|
|
301
|
+
const error = result?.valid === false ? result.error : null;
|
|
230
302
|
return {
|
|
231
303
|
value,
|
|
232
304
|
formatted: result?.valid ? result.formatted : value,
|
|
233
305
|
valid: result === null ? null : result.valid,
|
|
234
|
-
error
|
|
306
|
+
error,
|
|
235
307
|
onChange,
|
|
236
308
|
result
|
|
237
309
|
};
|
|
@@ -248,12 +320,16 @@ function useAccountNumberInput() {
|
|
|
248
320
|
function useBICInput() {
|
|
249
321
|
return useValidatedInput(validateBIC, 8);
|
|
250
322
|
}
|
|
323
|
+
function useCardNumberInput() {
|
|
324
|
+
return useValidatedInput(validateCardNumber, 8);
|
|
325
|
+
}
|
|
251
326
|
function useCurrencyInput(currency, locale) {
|
|
252
327
|
const [rawValue, setRawValue] = useState(null);
|
|
253
328
|
const onChange = useCallback(
|
|
254
329
|
(e) => {
|
|
255
|
-
const
|
|
256
|
-
|
|
330
|
+
const digits = e.target.value.replace(/[^0-9.]/g, "");
|
|
331
|
+
const num = Number.parseFloat(digits);
|
|
332
|
+
setRawValue(Number.isNaN(num) ? null : num);
|
|
257
333
|
},
|
|
258
334
|
[]
|
|
259
335
|
);
|
|
@@ -261,6 +337,6 @@ function useCurrencyInput(currency, locale) {
|
|
|
261
337
|
return { rawValue, formatted, onChange };
|
|
262
338
|
}
|
|
263
339
|
|
|
264
|
-
export { useAccountNumberInput, useBICInput, useCurrencyInput, useIBANInput, useSortCodeInput };
|
|
340
|
+
export { useAccountNumberInput, useBICInput, useCardNumberInput, useCurrencyInput, useIBANInput, useSortCodeInput };
|
|
265
341
|
//# sourceMappingURL=index.mjs.map
|
|
266
342
|
//# sourceMappingURL=index.mjs.map
|