@evervault/react-native 2.3.0 → 2.5.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) => {
@@ -7602,6 +7583,23 @@ const defaultStyles = StyleSheet.create({
7602
7583
  },
7603
7584
  });
7604
7585
 
7586
+ class ThreeDSecureEvent {
7587
+ type;
7588
+ session;
7589
+ _defaultPrevented;
7590
+ constructor(type, session, _defaultPrevented = false) {
7591
+ this.type = type;
7592
+ this.session = session;
7593
+ this._defaultPrevented = _defaultPrevented;
7594
+ }
7595
+ preventDefault() {
7596
+ this._defaultPrevented = true;
7597
+ }
7598
+ get defaultPrevented() {
7599
+ return this._defaultPrevented;
7600
+ }
7601
+ }
7602
+
7605
7603
  function stopPolling(intervalRef, setIsVisible) {
7606
7604
  setIsVisible(false);
7607
7605
  if (intervalRef.current) {
@@ -7609,55 +7607,91 @@ function stopPolling(intervalRef, setIsVisible) {
7609
7607
  intervalRef.current = null;
7610
7608
  }
7611
7609
  }
7612
- async function startSession(session, callbacks, intervalRef, setIsVisible) {
7610
+ async function startSession(session, options, intervalRef, setIsVisible) {
7613
7611
  try {
7614
7612
  const sessionState = await session.get();
7613
+ function fail() {
7614
+ stopPolling(intervalRef, setIsVisible);
7615
+ options?.onFailure?.(new Error("3DS session failed"));
7616
+ }
7615
7617
  switch (sessionState.status) {
7616
- case "success":
7618
+ case "success": {
7617
7619
  stopPolling(intervalRef, setIsVisible);
7618
- callbacks?.onSuccess?.();
7620
+ options?.onSuccess?.();
7619
7621
  break;
7620
- case "failure":
7621
- stopPolling(intervalRef, setIsVisible);
7622
- callbacks?.onFailure?.(new Error("3DS session failed"));
7622
+ }
7623
+ case "failure": {
7624
+ fail();
7623
7625
  break;
7624
- case "action-required":
7626
+ }
7627
+ case "action-required": {
7628
+ const failOnChallenge = typeof options?.failOnChallenge === "function"
7629
+ ? await options.failOnChallenge()
7630
+ : options?.failOnChallenge ?? false;
7631
+ if (failOnChallenge) {
7632
+ fail();
7633
+ break;
7634
+ }
7635
+ const event = new ThreeDSecureEvent("requestChallenge", session);
7636
+ options?.onRequestChallenge?.(event);
7637
+ if (event.defaultPrevented) {
7638
+ fail();
7639
+ break;
7640
+ }
7625
7641
  setIsVisible(true);
7626
- pollSession(session, callbacks, intervalRef, setIsVisible);
7627
- break;
7628
- default:
7629
- break;
7642
+ pollSession(session, options, intervalRef, setIsVisible);
7643
+ }
7630
7644
  }
7631
7645
  }
7632
7646
  catch (error) {
7633
7647
  console.error("Error checking session state", error);
7634
- callbacks?.onError?.(new Error("Failed to check 3DS session state"));
7648
+ options?.onError?.(new Error("Failed to check 3DS session state"));
7635
7649
  }
7636
7650
  }
7637
- function pollSession(session, callbacks, intervalRef, setIsVisible, interval = 3000) {
7651
+ function pollSession(session, options, intervalRef, setIsVisible, interval = 3000) {
7652
+ function fail() {
7653
+ stopPolling(intervalRef, setIsVisible);
7654
+ options?.onFailure?.(new Error("3DS session failed"));
7655
+ }
7638
7656
  intervalRef.current = setInterval(async () => {
7639
7657
  try {
7640
7658
  const pollResponse = await session.get();
7641
- if (pollResponse.status === "success") {
7642
- stopPolling(intervalRef, setIsVisible);
7643
- callbacks?.onSuccess?.();
7644
- }
7645
- else if (pollResponse.status === "failure") {
7646
- stopPolling(intervalRef, setIsVisible);
7647
- callbacks?.onFailure?.(new Error("3DS session failed"));
7648
- }
7649
- else {
7650
- setIsVisible(true);
7659
+ switch (pollResponse.status) {
7660
+ case "success": {
7661
+ stopPolling(intervalRef, setIsVisible);
7662
+ options?.onSuccess?.();
7663
+ break;
7664
+ }
7665
+ case "failure": {
7666
+ fail();
7667
+ break;
7668
+ }
7669
+ case "action-required": {
7670
+ const failOnChallenge = typeof options?.failOnChallenge === "function"
7671
+ ? await options.failOnChallenge()
7672
+ : options?.failOnChallenge ?? false;
7673
+ if (failOnChallenge) {
7674
+ fail();
7675
+ break;
7676
+ }
7677
+ const event = new ThreeDSecureEvent("requestChallenge", session);
7678
+ options?.onRequestChallenge?.(event);
7679
+ if (event.defaultPrevented) {
7680
+ fail();
7681
+ break;
7682
+ }
7683
+ setIsVisible(true);
7684
+ }
7651
7685
  }
7652
7686
  }
7653
7687
  catch (error) {
7654
7688
  stopPolling(intervalRef, setIsVisible);
7655
7689
  console.error("Error polling session", error);
7656
- callbacks?.onError?.(new Error("Error polling 3DS session"));
7690
+ options?.onError?.(new Error("Error polling 3DS session"));
7657
7691
  }
7658
7692
  }, interval);
7659
7693
  }
7660
- function threeDSecureSession({ sessionId, appId, callbacks, intervalRef, setIsVisible, }) {
7694
+ function threeDSecureSession({ sessionId, appId, options, intervalRef, setIsVisible, }) {
7661
7695
  async function get() {
7662
7696
  try {
7663
7697
  const response = await fetch(`https://${EV_API_DOMAIN}/frontend/3ds/browser-sessions/${sessionId}`, {
@@ -7683,7 +7717,7 @@ function threeDSecureSession({ sessionId, appId, callbacks, intervalRef, setIsVi
7683
7717
  },
7684
7718
  body: JSON.stringify({ outcome: "cancelled" }),
7685
7719
  });
7686
- callbacks?.onFailure?.(new Error("3DS session cancelled by user"));
7720
+ options?.onFailure?.(new Error("3DS session cancelled by user"));
7687
7721
  stopPolling(intervalRef, setIsVisible);
7688
7722
  }
7689
7723
  catch (error) {
@@ -7698,22 +7732,27 @@ function threeDSecureSession({ sessionId, appId, callbacks, intervalRef, setIsVi
7698
7732
  };
7699
7733
  }
7700
7734
 
7701
- function useThreeDSecure() {
7735
+ function useThreeDSecure(options) {
7702
7736
  const { appId } = useEvervault();
7703
7737
  const intervalRef = useRef(null);
7704
7738
  const [session, setSession] = useState(null);
7705
7739
  const [isVisible, setIsVisible] = useState(false);
7706
- const start = useCallback((sessionId, callbacks) => {
7740
+ const failOnChallenge = options?.failOnChallenge ?? false;
7741
+ const start = useCallback((sessionId, options) => {
7742
+ const startOptions = {
7743
+ ...options,
7744
+ failOnChallenge: options?.failOnChallenge ?? failOnChallenge,
7745
+ };
7707
7746
  const session = threeDSecureSession({
7708
7747
  sessionId,
7709
7748
  appId,
7710
- callbacks,
7749
+ options: startOptions,
7711
7750
  intervalRef,
7712
7751
  setIsVisible,
7713
7752
  });
7714
7753
  setSession(session);
7715
- startSession(session, callbacks, intervalRef, setIsVisible);
7716
- }, [appId]);
7754
+ startSession(session, startOptions, intervalRef, setIsVisible);
7755
+ }, [appId, failOnChallenge]);
7717
7756
  const cancel = useCallback(async () => {
7718
7757
  if (session) {
7719
7758
  await session.cancel();
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.5.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
  });