@evervault/react-native 2.2.1 → 2.4.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.
- package/build/Card/Cvc.d.ts +9 -2
- package/build/Card/Cvc.d.ts.map +1 -1
- package/build/Card/Number.d.ts +9 -2
- package/build/Card/Number.d.ts.map +1 -1
- package/build/Card/index.d.ts +2 -2
- package/build/Card/types.d.ts +1 -1
- package/build/Card/types.d.ts.map +1 -1
- package/build/Input.d.ts +1 -0
- package/build/Input.d.ts.map +1 -1
- package/build/index.cjs.js +98 -92
- package/build/index.cjs.js.map +1 -1
- package/build/index.esm.js +98 -92
- package/package.json +2 -2
- package/src/Card/Cvc.tsx +10 -3
- package/src/Card/Number.test.tsx +24 -3
- package/src/Card/Number.tsx +11 -4
- package/src/Card/types.ts +1 -0
- package/src/Input.test.tsx +84 -0
- package/src/Input.tsx +73 -47
package/build/index.esm.js
CHANGED
|
@@ -588,50 +588,6 @@ function useController(props) {
|
|
|
588
588
|
}), [field, formState, fieldState]);
|
|
589
589
|
}
|
|
590
590
|
|
|
591
|
-
/**
|
|
592
|
-
* Component based on `useController` hook to work with controlled component.
|
|
593
|
-
*
|
|
594
|
-
* @remarks
|
|
595
|
-
* [API](https://react-hook-form.com/docs/usecontroller/controller) • [Demo](https://codesandbox.io/s/react-hook-form-v6-controller-ts-jwyzw) • [Video](https://www.youtube.com/watch?v=N2UNk_UCVyA)
|
|
596
|
-
*
|
|
597
|
-
* @param props - the path name to the form field value, and validation rules.
|
|
598
|
-
*
|
|
599
|
-
* @returns provide field handler functions, field and form state.
|
|
600
|
-
*
|
|
601
|
-
* @example
|
|
602
|
-
* ```tsx
|
|
603
|
-
* function App() {
|
|
604
|
-
* const { control } = useForm<FormValues>({
|
|
605
|
-
* defaultValues: {
|
|
606
|
-
* test: ""
|
|
607
|
-
* }
|
|
608
|
-
* });
|
|
609
|
-
*
|
|
610
|
-
* return (
|
|
611
|
-
* <form>
|
|
612
|
-
* <Controller
|
|
613
|
-
* control={control}
|
|
614
|
-
* name="test"
|
|
615
|
-
* render={({ field: { onChange, onBlur, value, ref }, formState, fieldState }) => (
|
|
616
|
-
* <>
|
|
617
|
-
* <input
|
|
618
|
-
* onChange={onChange} // send value to hook form
|
|
619
|
-
* onBlur={onBlur} // notify when input is touched
|
|
620
|
-
* value={value} // return updated value
|
|
621
|
-
* ref={ref} // set ref for focus management
|
|
622
|
-
* />
|
|
623
|
-
* <p>{formState.isSubmitted ? "submitted" : ""}</p>
|
|
624
|
-
* <p>{fieldState.isTouched ? "touched" : ""}</p>
|
|
625
|
-
* </>
|
|
626
|
-
* )}
|
|
627
|
-
* />
|
|
628
|
-
* </form>
|
|
629
|
-
* );
|
|
630
|
-
* }
|
|
631
|
-
* ```
|
|
632
|
-
*/
|
|
633
|
-
const Controller = (props) => props.render(useController(props));
|
|
634
|
-
|
|
635
591
|
var appendErrors = (name, validateAllFieldCriteria, errors, type, message) => validateAllFieldCriteria
|
|
636
592
|
? {
|
|
637
593
|
...errors[name],
|
|
@@ -6774,7 +6730,7 @@ const h = [
|
|
|
6774
6730
|
isLocal: false,
|
|
6775
6731
|
numberValidationRules: {
|
|
6776
6732
|
luhnCheck: true,
|
|
6777
|
-
ranges: [[300, 305], 36, 38, 39],
|
|
6733
|
+
ranges: [[300, 305], 3095, 36, 38, 39],
|
|
6778
6734
|
lengths: [14, 16, 19]
|
|
6779
6735
|
},
|
|
6780
6736
|
securityCodeValidationRules: {
|
|
@@ -6786,7 +6742,7 @@ const h = [
|
|
|
6786
6742
|
isLocal: false,
|
|
6787
6743
|
numberValidationRules: {
|
|
6788
6744
|
luhnCheck: true,
|
|
6789
|
-
ranges: [6011, [644, 649],
|
|
6745
|
+
ranges: [6011, [644, 649], [65e4, 651999], [653150, 659999], 622],
|
|
6790
6746
|
lengths: [16, 19]
|
|
6791
6747
|
},
|
|
6792
6748
|
securityCodeValidationRules: {
|
|
@@ -6798,7 +6754,16 @@ const h = [
|
|
|
6798
6754
|
isLocal: false,
|
|
6799
6755
|
numberValidationRules: {
|
|
6800
6756
|
luhnCheck: true,
|
|
6801
|
-
ranges: [
|
|
6757
|
+
ranges: [
|
|
6758
|
+
2131,
|
|
6759
|
+
1800,
|
|
6760
|
+
[3088, 3094],
|
|
6761
|
+
[3096, 3102],
|
|
6762
|
+
[3112, 3120],
|
|
6763
|
+
[3158, 3159],
|
|
6764
|
+
[3337, 3349],
|
|
6765
|
+
[3528, 3589]
|
|
6766
|
+
],
|
|
6802
6767
|
lengths: [16, 17, 18, 19]
|
|
6803
6768
|
},
|
|
6804
6769
|
securityCodeValidationRules: {
|
|
@@ -6846,16 +6811,20 @@ const h = [
|
|
|
6846
6811
|
numberValidationRules: {
|
|
6847
6812
|
luhnCheck: true,
|
|
6848
6813
|
ranges: [
|
|
6814
|
+
5018,
|
|
6815
|
+
5020,
|
|
6816
|
+
5038,
|
|
6817
|
+
5893,
|
|
6818
|
+
6101,
|
|
6819
|
+
6304,
|
|
6820
|
+
6759,
|
|
6821
|
+
6761,
|
|
6822
|
+
6762,
|
|
6823
|
+
6763,
|
|
6849
6824
|
493698,
|
|
6850
6825
|
[5e5, 504174],
|
|
6851
6826
|
[504176, 506698],
|
|
6852
6827
|
[506779, 508999],
|
|
6853
|
-
[56, 59],
|
|
6854
|
-
60,
|
|
6855
|
-
61,
|
|
6856
|
-
63,
|
|
6857
|
-
64,
|
|
6858
|
-
67,
|
|
6859
6828
|
69
|
|
6860
6829
|
],
|
|
6861
6830
|
lengths: [12, 13, 14, 15, 16, 17, 18, 19]
|
|
@@ -6961,6 +6930,18 @@ const h = [
|
|
|
6961
6930
|
securityCodeValidationRules: {
|
|
6962
6931
|
lengths: [0]
|
|
6963
6932
|
}
|
|
6933
|
+
},
|
|
6934
|
+
{
|
|
6935
|
+
name: "rupay",
|
|
6936
|
+
isLocal: false,
|
|
6937
|
+
numberValidationRules: {
|
|
6938
|
+
luhnCheck: true,
|
|
6939
|
+
ranges: [60, 81, 82, 508, [652100, 653149], [817200, 819899]],
|
|
6940
|
+
lengths: [16]
|
|
6941
|
+
},
|
|
6942
|
+
securityCodeValidationRules: {
|
|
6943
|
+
lengths: [3]
|
|
6944
|
+
}
|
|
6964
6945
|
}
|
|
6965
6946
|
];
|
|
6966
6947
|
function g(n, e, t) {
|
|
@@ -7027,7 +7008,7 @@ function C(n, e) {
|
|
|
7027
7008
|
isValid: l
|
|
7028
7009
|
};
|
|
7029
7010
|
}
|
|
7030
|
-
function
|
|
7011
|
+
function R(n) {
|
|
7031
7012
|
var r;
|
|
7032
7013
|
const e = /^(0[1-9]|1[[0-2]).*$/, t = n.match(e), s = t ? parseInt(t[1].toString(), 10) : null, l = /^(0[1-9]|1[[0-2])(\d{2})$/, i = n.match(l), a = i ? parseInt(i[2].toString(), 10) : null;
|
|
7033
7014
|
if (s) {
|
|
@@ -7093,7 +7074,7 @@ function areValuesComplete(values) {
|
|
|
7093
7074
|
if ("number" in values && !f(values.number ?? "").isValid) {
|
|
7094
7075
|
return false;
|
|
7095
7076
|
}
|
|
7096
|
-
if ("expiry" in values && !
|
|
7077
|
+
if ("expiry" in values && !R(values.expiry ?? "").isValid) {
|
|
7097
7078
|
return false;
|
|
7098
7079
|
}
|
|
7099
7080
|
if ("cvc" in values &&
|
|
@@ -7114,7 +7095,7 @@ function isAcceptedBrand(acceptedBrands, cardNumberValidationResult) {
|
|
|
7114
7095
|
return isBrandAccepted || isLocalBrandAccepted;
|
|
7115
7096
|
}
|
|
7116
7097
|
function formatExpiry(expiry) {
|
|
7117
|
-
const parsedExpiry =
|
|
7098
|
+
const parsedExpiry = R(expiry);
|
|
7118
7099
|
if (!parsedExpiry.isValid) {
|
|
7119
7100
|
return null;
|
|
7120
7101
|
}
|
|
@@ -7137,7 +7118,7 @@ function getCardFormSchema(acceptedBrands) {
|
|
|
7137
7118
|
expiry: z
|
|
7138
7119
|
.string()
|
|
7139
7120
|
.min(1, "Required")
|
|
7140
|
-
.refine((value) =>
|
|
7121
|
+
.refine((value) => R(value).isValid, {
|
|
7141
7122
|
message: "Invalid expiry",
|
|
7142
7123
|
}),
|
|
7143
7124
|
cvc: z
|
|
@@ -7383,44 +7364,69 @@ function useForwardedInputRef(ref) {
|
|
|
7383
7364
|
return inputRef;
|
|
7384
7365
|
}
|
|
7385
7366
|
function mask(format) {
|
|
7386
|
-
|
|
7367
|
+
const maskArray = [];
|
|
7368
|
+
let isObfuscated = false;
|
|
7369
|
+
format.split("").forEach((char) => {
|
|
7370
|
+
if (char === "[") {
|
|
7371
|
+
isObfuscated = true;
|
|
7372
|
+
return;
|
|
7373
|
+
}
|
|
7374
|
+
else if (char === "]") {
|
|
7375
|
+
isObfuscated = false;
|
|
7376
|
+
return;
|
|
7377
|
+
}
|
|
7378
|
+
let value = char;
|
|
7387
7379
|
if (char === "9") {
|
|
7388
|
-
|
|
7380
|
+
value = isObfuscated ? [/\d/] : /\d/;
|
|
7389
7381
|
}
|
|
7390
|
-
|
|
7382
|
+
maskArray.push(value);
|
|
7391
7383
|
});
|
|
7384
|
+
return maskArray;
|
|
7392
7385
|
}
|
|
7393
|
-
const EvervaultInput = forwardRef(function EvervaultInput({ name, mask, ...props }, ref) {
|
|
7386
|
+
const EvervaultInput = forwardRef(function EvervaultInput({ name, mask, obfuscateValue, ...props }, ref) {
|
|
7394
7387
|
const { validationMode } = useContext(EvervaultInputContext);
|
|
7395
7388
|
const inputRef = useForwardedInputRef(ref);
|
|
7396
7389
|
const methods = useFormContext();
|
|
7397
|
-
|
|
7390
|
+
const { field, fieldState } = useController({
|
|
7391
|
+
control: methods.control,
|
|
7392
|
+
name,
|
|
7393
|
+
shouldUnregister: true,
|
|
7394
|
+
});
|
|
7395
|
+
const obfuscationCharacter = useMemo(() => {
|
|
7396
|
+
if (typeof obfuscateValue === "string") {
|
|
7397
|
+
return obfuscateValue;
|
|
7398
|
+
}
|
|
7399
|
+
else {
|
|
7400
|
+
return "•";
|
|
7401
|
+
}
|
|
7402
|
+
}, [obfuscateValue]);
|
|
7403
|
+
return (jsx(MaskInput
|
|
7404
|
+
// Overridable props
|
|
7405
|
+
, {
|
|
7398
7406
|
// Overridable props
|
|
7399
|
-
,
|
|
7400
|
-
|
|
7401
|
-
|
|
7402
|
-
|
|
7403
|
-
|
|
7404
|
-
|
|
7405
|
-
|
|
7406
|
-
|
|
7407
|
-
|
|
7408
|
-
|
|
7409
|
-
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
|
|
7413
|
-
|
|
7414
|
-
|
|
7415
|
-
|
|
7416
|
-
|
|
7417
|
-
|
|
7418
|
-
|
|
7419
|
-
|
|
7420
|
-
|
|
7421
|
-
|
|
7422
|
-
// Remove unwanted props
|
|
7423
|
-
defaultValue: undefined, onChange: undefined })) }));
|
|
7407
|
+
id: field.name, ...props,
|
|
7408
|
+
// Strict props
|
|
7409
|
+
ref: mergeRefs(inputRef, field.ref), editable: !field.disabled && (props.editable ?? true), onBlur: (evt) => {
|
|
7410
|
+
const shouldValidate = validationMode === "onBlur" ||
|
|
7411
|
+
validationMode === "onTouched" ||
|
|
7412
|
+
validationMode === "all";
|
|
7413
|
+
methods.setValue(field.name, field.value, {
|
|
7414
|
+
shouldDirty: true,
|
|
7415
|
+
shouldTouch: true,
|
|
7416
|
+
shouldValidate,
|
|
7417
|
+
});
|
|
7418
|
+
props.onBlur?.(evt);
|
|
7419
|
+
}, mask: mask, maskAutoComplete: !!mask, obfuscationCharacter: obfuscationCharacter, showObfuscatedValue: !!obfuscateValue, value: field.value, onChangeText: (masked, unmasked) => {
|
|
7420
|
+
const shouldValidate = (validationMode === "onTouched" && fieldState.isTouched) ||
|
|
7421
|
+
((validationMode === "onChange" || validationMode === "all") &&
|
|
7422
|
+
(!!fieldState.error || fieldState.isTouched));
|
|
7423
|
+
methods.setValue(field.name, unmasked, {
|
|
7424
|
+
shouldDirty: true,
|
|
7425
|
+
shouldValidate,
|
|
7426
|
+
});
|
|
7427
|
+
},
|
|
7428
|
+
// Remove unwanted props
|
|
7429
|
+
defaultValue: undefined, onChange: undefined }));
|
|
7424
7430
|
});
|
|
7425
7431
|
|
|
7426
7432
|
const DEFAULT_ACCEPTED_BRANDS = [];
|
|
@@ -7493,9 +7499,9 @@ const CardExpiry = forwardRef(function CardExpiry(props, ref) {
|
|
|
7493
7499
|
return (jsx(EvervaultInput, { placeholder: "MM / YY", ...props, ref: ref, name: "expiry", mask: CARD_EXPIRY_MASK, inputMode: "numeric", autoComplete: "cc-exp", keyboardType: "number-pad" }));
|
|
7494
7500
|
});
|
|
7495
7501
|
|
|
7496
|
-
const DEFAULT_CARD_CVC_MASK = mask("999");
|
|
7502
|
+
const DEFAULT_CARD_CVC_MASK = mask("[999]");
|
|
7497
7503
|
const CARD_CVC_MASKS = {
|
|
7498
|
-
"american-express": mask("9999"),
|
|
7504
|
+
"american-express": mask("[9999]"),
|
|
7499
7505
|
};
|
|
7500
7506
|
const CardCvc = forwardRef(function CardCvc(props, ref) {
|
|
7501
7507
|
const methods = useFormContext();
|
|
@@ -7513,10 +7519,10 @@ const CardCvc = forwardRef(function CardCvc(props, ref) {
|
|
|
7513
7519
|
return (jsx(EvervaultInput, { placeholder: "CVC", ...props, ref: ref, name: "cvc", mask: mask, inputMode: "numeric", autoComplete: "cc-csc", keyboardType: "number-pad" }));
|
|
7514
7520
|
});
|
|
7515
7521
|
|
|
7516
|
-
const DEFAULT_CARD_NUMBER_MASK = mask("9999
|
|
7522
|
+
const DEFAULT_CARD_NUMBER_MASK = mask("9999 99[99 9999 9999]");
|
|
7517
7523
|
const CARD_NUMBER_MASKS = {
|
|
7518
|
-
unionpay: mask("9999
|
|
7519
|
-
"american-express": mask("9999
|
|
7524
|
+
unionpay: mask("9999 99[99 9999 9999 999]"),
|
|
7525
|
+
"american-express": mask("9999 99[9999 99999]"),
|
|
7520
7526
|
};
|
|
7521
7527
|
const CardNumber = forwardRef(function CardNumber(props, ref) {
|
|
7522
7528
|
const mask = useCallback((text) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evervault/react-native",
|
|
3
3
|
"description": "Evervault SDK for React Native",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.4.0",
|
|
5
5
|
"source": "./src/index.ts",
|
|
6
6
|
"main": "./build/index.cjs.js",
|
|
7
7
|
"module": "./build/index.esm.js",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"react-hook-form": "^7.54.2",
|
|
74
74
|
"react-native-mask-input": "^1.2.3",
|
|
75
75
|
"zod": "^3.24.2",
|
|
76
|
-
"@evervault/card-validator": "1.
|
|
76
|
+
"@evervault/card-validator": "1.4.0"
|
|
77
77
|
},
|
|
78
78
|
"scripts": {
|
|
79
79
|
"prebuild": "pnpm codegen",
|
package/src/Card/Cvc.tsx
CHANGED
|
@@ -6,13 +6,20 @@ import { validateNumber } from "@evervault/card-validator";
|
|
|
6
6
|
import { useFormContext } from "react-hook-form";
|
|
7
7
|
import { CardBrandName } from "./types";
|
|
8
8
|
|
|
9
|
-
const DEFAULT_CARD_CVC_MASK = mask("999");
|
|
9
|
+
const DEFAULT_CARD_CVC_MASK = mask("[999]");
|
|
10
10
|
|
|
11
11
|
const CARD_CVC_MASKS: Partial<Record<CardBrandName, Mask>> = {
|
|
12
|
-
"american-express": mask("9999"),
|
|
12
|
+
"american-express": mask("[9999]"),
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export interface CardCvcProps extends BaseEvervaultInputProps {
|
|
16
|
+
/**
|
|
17
|
+
* Whether to obfuscate the entire CVC value.
|
|
18
|
+
*
|
|
19
|
+
* If a string is provided, it will be used to obfuscate the value.
|
|
20
|
+
*/
|
|
21
|
+
obfuscateValue?: boolean | string;
|
|
22
|
+
}
|
|
16
23
|
|
|
17
24
|
export type CardCvc = EvervaultInput;
|
|
18
25
|
|
package/src/Card/Number.test.tsx
CHANGED
|
@@ -13,7 +13,7 @@ function wrapper({ children }: PropsWithChildren) {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
it("uses 16 digits for mask by default", async () => {
|
|
16
|
-
const { getByTestId } = render(
|
|
16
|
+
const { rerender, getByTestId } = render(
|
|
17
17
|
<Card>
|
|
18
18
|
<CardNumber testID="number" />
|
|
19
19
|
</Card>,
|
|
@@ -24,10 +24,17 @@ it("uses 16 digits for mask by default", async () => {
|
|
|
24
24
|
const user = userEvent.setup();
|
|
25
25
|
await user.type(number, "4242424242424242");
|
|
26
26
|
expect(number).toHaveProp("value", "4242 4242 4242 4242");
|
|
27
|
+
|
|
28
|
+
rerender(
|
|
29
|
+
<Card>
|
|
30
|
+
<CardNumber testID="number" obfuscateValue />
|
|
31
|
+
</Card>
|
|
32
|
+
);
|
|
33
|
+
expect(number).toHaveProp("value", "4242 42•• •••• ••••");
|
|
27
34
|
});
|
|
28
35
|
|
|
29
36
|
it("uses 19 digits for mask for unionpay", async () => {
|
|
30
|
-
const { getByTestId } = render(
|
|
37
|
+
const { rerender, getByTestId } = render(
|
|
31
38
|
<Card>
|
|
32
39
|
<CardNumber testID="number" />
|
|
33
40
|
</Card>,
|
|
@@ -38,10 +45,17 @@ it("uses 19 digits for mask for unionpay", async () => {
|
|
|
38
45
|
const user = userEvent.setup();
|
|
39
46
|
await user.type(number, "6205500000000000004");
|
|
40
47
|
expect(number).toHaveProp("value", "6205 5000 0000 0000 004");
|
|
48
|
+
|
|
49
|
+
rerender(
|
|
50
|
+
<Card>
|
|
51
|
+
<CardNumber testID="number" obfuscateValue />
|
|
52
|
+
</Card>
|
|
53
|
+
);
|
|
54
|
+
expect(number).toHaveProp("value", "6205 50•• •••• •••• •••");
|
|
41
55
|
});
|
|
42
56
|
|
|
43
57
|
it("uses 15 digits for mask for american express", async () => {
|
|
44
|
-
const { getByTestId } = render(
|
|
58
|
+
const { rerender, getByTestId } = render(
|
|
45
59
|
<Card>
|
|
46
60
|
<CardNumber testID="number" />
|
|
47
61
|
</Card>,
|
|
@@ -52,4 +66,11 @@ it("uses 15 digits for mask for american express", async () => {
|
|
|
52
66
|
const user = userEvent.setup();
|
|
53
67
|
await user.type(number, "371449635398431");
|
|
54
68
|
expect(number).toHaveProp("value", "3714 496353 98431");
|
|
69
|
+
|
|
70
|
+
rerender(
|
|
71
|
+
<Card>
|
|
72
|
+
<CardNumber testID="number" obfuscateValue />
|
|
73
|
+
</Card>
|
|
74
|
+
);
|
|
75
|
+
expect(number).toHaveProp("value", "3714 49•••• •••••");
|
|
55
76
|
});
|
package/src/Card/Number.tsx
CHANGED
|
@@ -5,14 +5,21 @@ import { MaskArray } from "react-native-mask-input";
|
|
|
5
5
|
import { validateNumber } from "@evervault/card-validator";
|
|
6
6
|
import { CardBrandName } from "./types";
|
|
7
7
|
|
|
8
|
-
const DEFAULT_CARD_NUMBER_MASK = mask("9999
|
|
8
|
+
const DEFAULT_CARD_NUMBER_MASK = mask("9999 99[99 9999 9999]");
|
|
9
9
|
|
|
10
10
|
const CARD_NUMBER_MASKS: Partial<Record<CardBrandName, MaskArray>> = {
|
|
11
|
-
unionpay: mask("9999
|
|
12
|
-
"american-express": mask("9999
|
|
11
|
+
unionpay: mask("9999 99[99 9999 9999 999]"),
|
|
12
|
+
"american-express": mask("9999 99[9999 99999]"),
|
|
13
13
|
};
|
|
14
14
|
|
|
15
|
-
export
|
|
15
|
+
export interface CardNumberProps extends BaseEvervaultInputProps {
|
|
16
|
+
/**
|
|
17
|
+
* Whether to obfuscate the card number value (excluding the last 4 digits).
|
|
18
|
+
*
|
|
19
|
+
* If a string is provided, it will be used to obfuscate the value.
|
|
20
|
+
*/
|
|
21
|
+
obfuscateValue?: boolean | string;
|
|
22
|
+
}
|
|
16
23
|
|
|
17
24
|
export type CardNumber = EvervaultInput;
|
|
18
25
|
|
package/src/Card/types.ts
CHANGED
package/src/Input.test.tsx
CHANGED
|
@@ -27,6 +27,25 @@ describe("mask", () => {
|
|
|
27
27
|
/\d/,
|
|
28
28
|
]);
|
|
29
29
|
});
|
|
30
|
+
|
|
31
|
+
it("should account for obfuscation", () => {
|
|
32
|
+
expect(mask("[9999 9999] 9999")).toEqual([
|
|
33
|
+
[/\d/],
|
|
34
|
+
[/\d/],
|
|
35
|
+
[/\d/],
|
|
36
|
+
[/\d/],
|
|
37
|
+
" ",
|
|
38
|
+
[/\d/],
|
|
39
|
+
[/\d/],
|
|
40
|
+
[/\d/],
|
|
41
|
+
[/\d/],
|
|
42
|
+
" ",
|
|
43
|
+
/\d/,
|
|
44
|
+
/\d/,
|
|
45
|
+
/\d/,
|
|
46
|
+
/\d/,
|
|
47
|
+
]);
|
|
48
|
+
});
|
|
30
49
|
});
|
|
31
50
|
|
|
32
51
|
describe("EvervaultInput", () => {
|
|
@@ -333,4 +352,69 @@ describe("EvervaultInput", () => {
|
|
|
333
352
|
{ shouldDirty: true, shouldValidate: true }
|
|
334
353
|
);
|
|
335
354
|
});
|
|
355
|
+
|
|
356
|
+
it("should obfuscate the value when obfuscateValue=true", async () => {
|
|
357
|
+
const phoneMask = mask("[(999) 999]-9999");
|
|
358
|
+
const { rerender } = render(
|
|
359
|
+
<EvervaultInput testID="phone" mask={phoneMask} name="phone" />,
|
|
360
|
+
{
|
|
361
|
+
wrapper: Form,
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
|
|
365
|
+
const input = screen.getByTestId("phone");
|
|
366
|
+
const user = userEvent.setup();
|
|
367
|
+
|
|
368
|
+
await user.type(input, "1234567890");
|
|
369
|
+
expect(input).toHaveProp("value", "(123) 456-7890");
|
|
370
|
+
|
|
371
|
+
rerender(
|
|
372
|
+
<EvervaultInput
|
|
373
|
+
testID="phone"
|
|
374
|
+
mask={phoneMask}
|
|
375
|
+
name="phone"
|
|
376
|
+
obfuscateValue
|
|
377
|
+
/>
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
await user.type(input, "1234567890");
|
|
381
|
+
expect(input).toHaveProp("value", "(•••) •••-7890");
|
|
382
|
+
|
|
383
|
+
rerender(
|
|
384
|
+
<EvervaultInput
|
|
385
|
+
testID="phone"
|
|
386
|
+
mask={phoneMask}
|
|
387
|
+
name="phone"
|
|
388
|
+
obfuscateValue="#"
|
|
389
|
+
/>
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
await user.type(input, "1234567890");
|
|
393
|
+
expect(input).toHaveProp("value", "(###) ###-7890");
|
|
394
|
+
|
|
395
|
+
rerender(
|
|
396
|
+
<EvervaultInput
|
|
397
|
+
testID="phone"
|
|
398
|
+
mask={phoneMask}
|
|
399
|
+
name="phone"
|
|
400
|
+
obfuscateValue="🤔"
|
|
401
|
+
/>
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
await user.type(input, "1234567890");
|
|
405
|
+
expect(input).toHaveProp("value", "(🤔🤔🤔) 🤔🤔🤔-7890");
|
|
406
|
+
|
|
407
|
+
const unobfuscatedMask = mask("(999) 999-9999");
|
|
408
|
+
rerender(
|
|
409
|
+
<EvervaultInput
|
|
410
|
+
testID="phone"
|
|
411
|
+
mask={unobfuscatedMask}
|
|
412
|
+
name="phone"
|
|
413
|
+
obfuscateValue
|
|
414
|
+
/>
|
|
415
|
+
);
|
|
416
|
+
|
|
417
|
+
await user.type(input, "1234567890");
|
|
418
|
+
expect(input).toHaveProp("value", "(123) 456-7890");
|
|
419
|
+
});
|
|
336
420
|
});
|
package/src/Input.tsx
CHANGED
|
@@ -8,11 +8,13 @@ import {
|
|
|
8
8
|
useCallback,
|
|
9
9
|
useContext,
|
|
10
10
|
useImperativeHandle,
|
|
11
|
+
useMemo,
|
|
11
12
|
useRef,
|
|
13
|
+
useState,
|
|
12
14
|
} from "react";
|
|
13
15
|
import { TextInput, TextInputProps } from "react-native";
|
|
14
16
|
import { mergeRefs } from "./utils";
|
|
15
|
-
import { Controller, useFormContext } from "react-hook-form";
|
|
17
|
+
import { Controller, useController, useFormContext } from "react-hook-form";
|
|
16
18
|
import MaskInput, { Mask, MaskArray } from "react-native-mask-input";
|
|
17
19
|
|
|
18
20
|
export interface EvervaultInputContextValue {
|
|
@@ -82,73 +84,97 @@ export type BaseEvervaultInputProps = Omit<
|
|
|
82
84
|
>;
|
|
83
85
|
|
|
84
86
|
export function mask(format: string): MaskArray {
|
|
85
|
-
|
|
87
|
+
const maskArray: MaskArray = [];
|
|
88
|
+
|
|
89
|
+
let isObfuscated = false;
|
|
90
|
+
format.split("").forEach((char) => {
|
|
91
|
+
if (char === "[") {
|
|
92
|
+
isObfuscated = true;
|
|
93
|
+
return;
|
|
94
|
+
} else if (char === "]") {
|
|
95
|
+
isObfuscated = false;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let value: string | RegExp | [RegExp] = char;
|
|
86
100
|
if (char === "9") {
|
|
87
|
-
|
|
101
|
+
value = isObfuscated ? [/\d/] : /\d/;
|
|
88
102
|
}
|
|
89
|
-
|
|
103
|
+
maskArray.push(value);
|
|
90
104
|
});
|
|
105
|
+
|
|
106
|
+
return maskArray;
|
|
91
107
|
}
|
|
92
108
|
|
|
93
109
|
export interface EvervaultInputProps<Values extends Record<string, unknown>>
|
|
94
110
|
extends BaseEvervaultInputProps {
|
|
95
111
|
name: keyof Values;
|
|
96
112
|
mask?: Mask;
|
|
113
|
+
obfuscateValue?: boolean | string;
|
|
97
114
|
}
|
|
98
115
|
|
|
99
116
|
export const EvervaultInput = forwardRef<
|
|
100
117
|
EvervaultInput,
|
|
101
118
|
EvervaultInputProps<Record<string, unknown>>
|
|
102
|
-
>(function EvervaultInput({ name, mask, ...props }, ref) {
|
|
119
|
+
>(function EvervaultInput({ name, mask, obfuscateValue, ...props }, ref) {
|
|
103
120
|
const { validationMode } = useContext(EvervaultInputContext);
|
|
104
121
|
|
|
105
122
|
const inputRef = useForwardedInputRef(ref);
|
|
106
123
|
|
|
107
124
|
const methods = useFormContext();
|
|
108
125
|
|
|
126
|
+
const { field, fieldState } = useController({
|
|
127
|
+
control: methods.control,
|
|
128
|
+
name,
|
|
129
|
+
shouldUnregister: true,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
const obfuscationCharacter = useMemo(() => {
|
|
133
|
+
if (typeof obfuscateValue === "string") {
|
|
134
|
+
return obfuscateValue;
|
|
135
|
+
} else {
|
|
136
|
+
return "•";
|
|
137
|
+
}
|
|
138
|
+
}, [obfuscateValue]);
|
|
139
|
+
|
|
109
140
|
return (
|
|
110
|
-
<
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
// Remove unwanted props
|
|
148
|
-
defaultValue={undefined}
|
|
149
|
-
onChange={undefined}
|
|
150
|
-
/>
|
|
151
|
-
)}
|
|
141
|
+
<MaskInput
|
|
142
|
+
// Overridable props
|
|
143
|
+
id={field.name}
|
|
144
|
+
{...props}
|
|
145
|
+
// Strict props
|
|
146
|
+
ref={mergeRefs(inputRef, field.ref)}
|
|
147
|
+
editable={!field.disabled && (props.editable ?? true)}
|
|
148
|
+
onBlur={(evt) => {
|
|
149
|
+
const shouldValidate =
|
|
150
|
+
validationMode === "onBlur" ||
|
|
151
|
+
validationMode === "onTouched" ||
|
|
152
|
+
validationMode === "all";
|
|
153
|
+
methods.setValue(field.name, field.value, {
|
|
154
|
+
shouldDirty: true,
|
|
155
|
+
shouldTouch: true,
|
|
156
|
+
shouldValidate,
|
|
157
|
+
});
|
|
158
|
+
props.onBlur?.(evt);
|
|
159
|
+
}}
|
|
160
|
+
mask={mask}
|
|
161
|
+
maskAutoComplete={!!mask}
|
|
162
|
+
obfuscationCharacter={obfuscationCharacter}
|
|
163
|
+
showObfuscatedValue={!!obfuscateValue}
|
|
164
|
+
value={field.value}
|
|
165
|
+
onChangeText={(masked, unmasked) => {
|
|
166
|
+
const shouldValidate =
|
|
167
|
+
(validationMode === "onTouched" && fieldState.isTouched) ||
|
|
168
|
+
((validationMode === "onChange" || validationMode === "all") &&
|
|
169
|
+
(!!fieldState.error || fieldState.isTouched));
|
|
170
|
+
methods.setValue(field.name, unmasked, {
|
|
171
|
+
shouldDirty: true,
|
|
172
|
+
shouldValidate,
|
|
173
|
+
});
|
|
174
|
+
}}
|
|
175
|
+
// Remove unwanted props
|
|
176
|
+
defaultValue={undefined}
|
|
177
|
+
onChange={undefined}
|
|
152
178
|
/>
|
|
153
179
|
);
|
|
154
180
|
}) as <Values extends Record<string, unknown>>(
|