@evervault/react-native 2.3.0 → 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.
@@ -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],
@@ -7408,44 +7364,69 @@ function useForwardedInputRef(ref) {
7408
7364
  return inputRef;
7409
7365
  }
7410
7366
  function mask(format) {
7411
- return format.split("").map((char) => {
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;
7412
7379
  if (char === "9") {
7413
- return /\d/;
7380
+ value = isObfuscated ? [/\d/] : /\d/;
7414
7381
  }
7415
- return char;
7382
+ maskArray.push(value);
7416
7383
  });
7384
+ return maskArray;
7417
7385
  }
7418
- const EvervaultInput = forwardRef(function EvervaultInput({ name, mask, ...props }, ref) {
7386
+ const EvervaultInput = forwardRef(function EvervaultInput({ name, mask, obfuscateValue, ...props }, ref) {
7419
7387
  const { validationMode } = useContext(EvervaultInputContext);
7420
7388
  const inputRef = useForwardedInputRef(ref);
7421
7389
  const methods = useFormContext();
7422
- return (jsx(Controller, { control: methods.control, name: name, shouldUnregister: true, render: ({ field, fieldState }) => (jsx(MaskInput
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
+ , {
7423
7406
  // Overridable props
7424
- , {
7425
- // Overridable props
7426
- id: field.name, ...props,
7427
- // Strict props
7428
- ref: mergeRefs(inputRef, field.ref), editable: !field.disabled && (props.editable ?? true), onBlur: (evt) => {
7429
- const shouldValidate = validationMode === "onBlur" ||
7430
- validationMode === "onTouched" ||
7431
- validationMode === "all";
7432
- methods.setValue(field.name, field.value, {
7433
- shouldDirty: true,
7434
- shouldTouch: true,
7435
- shouldValidate,
7436
- });
7437
- props.onBlur?.(evt);
7438
- }, mask: mask, maskAutoComplete: !!mask, value: field.value, onChangeText: (masked, unmasked) => {
7439
- const shouldValidate = (validationMode === "onTouched" && fieldState.isTouched) ||
7440
- ((validationMode === "onChange" || validationMode === "all") &&
7441
- (!!fieldState.error || fieldState.isTouched));
7442
- methods.setValue(field.name, unmasked, {
7443
- shouldDirty: true,
7444
- shouldValidate,
7445
- });
7446
- },
7447
- // Remove unwanted props
7448
- 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 }));
7449
7430
  });
7450
7431
 
7451
7432
  const DEFAULT_ACCEPTED_BRANDS = [];
@@ -7518,9 +7499,9 @@ const CardExpiry = forwardRef(function CardExpiry(props, ref) {
7518
7499
  return (jsx(EvervaultInput, { placeholder: "MM / YY", ...props, ref: ref, name: "expiry", mask: CARD_EXPIRY_MASK, inputMode: "numeric", autoComplete: "cc-exp", keyboardType: "number-pad" }));
7519
7500
  });
7520
7501
 
7521
- const DEFAULT_CARD_CVC_MASK = mask("999");
7502
+ const DEFAULT_CARD_CVC_MASK = mask("[999]");
7522
7503
  const CARD_CVC_MASKS = {
7523
- "american-express": mask("9999"),
7504
+ "american-express": mask("[9999]"),
7524
7505
  };
7525
7506
  const CardCvc = forwardRef(function CardCvc(props, ref) {
7526
7507
  const methods = useFormContext();
@@ -7538,10 +7519,10 @@ const CardCvc = forwardRef(function CardCvc(props, ref) {
7538
7519
  return (jsx(EvervaultInput, { placeholder: "CVC", ...props, ref: ref, name: "cvc", mask: mask, inputMode: "numeric", autoComplete: "cc-csc", keyboardType: "number-pad" }));
7539
7520
  });
7540
7521
 
7541
- const DEFAULT_CARD_NUMBER_MASK = mask("9999 9999 9999 9999");
7522
+ const DEFAULT_CARD_NUMBER_MASK = mask("9999 99[99 9999 9999]");
7542
7523
  const CARD_NUMBER_MASKS = {
7543
- unionpay: mask("9999 9999 9999 9999 999"),
7544
- "american-express": mask("9999 999999 99999"),
7524
+ unionpay: mask("9999 99[99 9999 9999 999]"),
7525
+ "american-express": mask("9999 99[9999 99999]"),
7545
7526
  };
7546
7527
  const CardNumber = forwardRef(function CardNumber(props, ref) {
7547
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.3.0",
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",
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 type CardCvcProps = BaseEvervaultInputProps;
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
 
@@ -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
  });
@@ -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 9999 9999 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 9999 9999 9999 999"),
12
- "american-express": mask("9999 999999 99999"),
11
+ unionpay: mask("9999 99[99 9999 9999 999]"),
12
+ "american-express": mask("9999 99[9999 99999]"),
13
13
  };
14
14
 
15
- export type CardNumberProps = BaseEvervaultInputProps;
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
 
@@ -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
- return format.split("").map((char) => {
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
- return /\d/;
101
+ value = isObfuscated ? [/\d/] : /\d/;
88
102
  }
89
- return char;
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
- <Controller
111
- control={methods.control}
112
- name={name}
113
- shouldUnregister
114
- render={({ field, fieldState }) => (
115
- <MaskInput
116
- // Overridable props
117
- id={field.name}
118
- {...props}
119
- // Strict props
120
- ref={mergeRefs(inputRef, field.ref)}
121
- editable={!field.disabled && (props.editable ?? true)}
122
- onBlur={(evt) => {
123
- const shouldValidate =
124
- validationMode === "onBlur" ||
125
- validationMode === "onTouched" ||
126
- validationMode === "all";
127
- methods.setValue(field.name, field.value, {
128
- shouldDirty: true,
129
- shouldTouch: true,
130
- shouldValidate,
131
- });
132
- props.onBlur?.(evt);
133
- }}
134
- mask={mask}
135
- maskAutoComplete={!!mask}
136
- value={field.value}
137
- onChangeText={(masked, unmasked) => {
138
- const shouldValidate =
139
- (validationMode === "onTouched" && fieldState.isTouched) ||
140
- ((validationMode === "onChange" || validationMode === "all") &&
141
- (!!fieldState.error || fieldState.isTouched));
142
- methods.setValue(field.name, unmasked, {
143
- shouldDirty: true,
144
- shouldValidate,
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>>(