@evervault/react-native 2.6.0 → 2.6.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.
Files changed (41) hide show
  1. package/package.json +3 -2
  2. package/src/Card/Cvc.test.tsx +41 -0
  3. package/src/Card/Cvc.tsx +58 -0
  4. package/src/Card/Expiry.tsx +26 -0
  5. package/src/Card/Holder.tsx +27 -0
  6. package/src/Card/Number.test.tsx +76 -0
  7. package/src/Card/Number.tsx +54 -0
  8. package/src/Card/Root.test.tsx +341 -0
  9. package/src/Card/Root.tsx +150 -0
  10. package/src/Card/index.ts +28 -0
  11. package/src/Card/schema.ts +41 -0
  12. package/src/Card/types.ts +57 -0
  13. package/src/Card/utils.test.ts +271 -0
  14. package/src/Card/utils.ts +129 -0
  15. package/src/EvervaultProvider.test.tsx +24 -0
  16. package/src/EvervaultProvider.tsx +43 -0
  17. package/src/Input.test.tsx +420 -0
  18. package/src/Input.tsx +182 -0
  19. package/src/ThreeDSecure/Frame.test.tsx +87 -0
  20. package/src/ThreeDSecure/Frame.tsx +50 -0
  21. package/src/ThreeDSecure/Root.test.tsx +67 -0
  22. package/src/ThreeDSecure/Root.tsx +23 -0
  23. package/src/ThreeDSecure/config.ts +3 -0
  24. package/src/ThreeDSecure/context.ts +6 -0
  25. package/src/ThreeDSecure/event.ts +19 -0
  26. package/src/ThreeDSecure/index.ts +17 -0
  27. package/src/ThreeDSecure/session.test.ts +524 -0
  28. package/src/ThreeDSecure/session.ts +184 -0
  29. package/src/ThreeDSecure/types.ts +80 -0
  30. package/src/ThreeDSecure/useThreeDSecure.test.tsx +244 -0
  31. package/src/ThreeDSecure/useThreeDSecure.ts +64 -0
  32. package/src/__mocks__/NativeEvervault.ts +13 -0
  33. package/src/__mocks__/react-native-webview.tsx +6 -0
  34. package/src/context.ts +14 -0
  35. package/src/index.ts +21 -0
  36. package/src/sdk.test.ts +122 -0
  37. package/src/sdk.ts +71 -0
  38. package/src/specs/NativeEvervault.ts +67 -0
  39. package/src/useEvervault.test.tsx +31 -0
  40. package/src/useEvervault.ts +14 -0
  41. package/src/utils.ts +41 -0
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@evervault/react-native",
3
3
  "description": "Evervault SDK for React Native",
4
- "version": "2.6.0",
4
+ "version": "2.6.2",
5
5
  "source": "./src/index.ts",
6
6
  "main": "./build/cjs/index.js",
7
7
  "module": "./build/esm/index.js",
8
8
  "types": "./build/esm/index.d.ts",
9
9
  "files": [
10
+ "src",
10
11
  "build",
11
12
  "ios",
12
13
  "android",
@@ -61,7 +62,7 @@
61
62
  "react-hook-form": "^7.54.2",
62
63
  "react-native-mask-input": "^1.2.3",
63
64
  "zod": "^3.24.2",
64
- "@evervault/card-validator": "1.5.0"
65
+ "@evervault/card-validator": "1.5.1"
65
66
  },
66
67
  "scripts": {
67
68
  "prebuild": "pnpm codegen",
@@ -0,0 +1,41 @@
1
+ import { render, userEvent } from "@testing-library/react-native";
2
+ import { PropsWithChildren } from "react";
3
+ import { Card } from "./Root";
4
+ import { EvervaultProvider } from "../EvervaultProvider";
5
+ import { CardCvc } from "./Cvc";
6
+
7
+ function wrapper({ children }: PropsWithChildren) {
8
+ return (
9
+ <EvervaultProvider teamId="team_123" appId="app_123">
10
+ {children}
11
+ </EvervaultProvider>
12
+ );
13
+ }
14
+
15
+ it("uses 3 digits for mask by default", async () => {
16
+ const { getByTestId } = render(
17
+ <Card>
18
+ <CardCvc testID="cvc" />
19
+ </Card>,
20
+ { wrapper }
21
+ );
22
+ const cvc = getByTestId("cvc");
23
+
24
+ const user = userEvent.setup();
25
+ await user.type(cvc, "12345");
26
+ expect(cvc).toHaveProp("value", "123");
27
+ });
28
+
29
+ it("uses 4 digits for mask for american express", async () => {
30
+ const { getByTestId } = render(
31
+ <Card defaultValues={{ number: "378282246310005" }}>
32
+ <CardCvc testID="cvc" />
33
+ </Card>,
34
+ { wrapper }
35
+ );
36
+ const cvc = getByTestId("cvc");
37
+
38
+ const user = userEvent.setup();
39
+ await user.type(cvc, "12345");
40
+ expect(cvc).toHaveProp("value", "1234");
41
+ });
@@ -0,0 +1,58 @@
1
+ import { forwardRef, useMemo } from "react";
2
+ import { BaseEvervaultInputProps, EvervaultInput, mask } from "../Input";
3
+ import { CardFormValues } from "./schema";
4
+ import { Mask } from "react-native-mask-input";
5
+ import { validateNumber } from "@evervault/card-validator";
6
+ import { useFormContext } from "react-hook-form";
7
+ import { CardBrandName } from "./types";
8
+
9
+ const DEFAULT_CARD_CVC_MASK = mask("[999]");
10
+
11
+ const CARD_CVC_MASKS: Partial<Record<CardBrandName, Mask>> = {
12
+ "american-express": mask("[9999]"),
13
+ };
14
+
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
+ }
23
+
24
+ export type CardCvc = EvervaultInput;
25
+
26
+ export const CardCvc = forwardRef<CardCvc, CardCvcProps>(function CardCvc(
27
+ props,
28
+ ref
29
+ ) {
30
+ const methods = useFormContext<CardFormValues>();
31
+
32
+ const number = methods.watch("number");
33
+ const mask = useMemo<Mask>(() => {
34
+ if (!number) {
35
+ return DEFAULT_CARD_CVC_MASK;
36
+ }
37
+
38
+ const brand = validateNumber(number).brand;
39
+ if (brand && CARD_CVC_MASKS[brand]) {
40
+ return CARD_CVC_MASKS[brand];
41
+ }
42
+
43
+ return DEFAULT_CARD_CVC_MASK;
44
+ }, [number]);
45
+
46
+ return (
47
+ <EvervaultInput<CardFormValues>
48
+ placeholder="CVC"
49
+ {...props}
50
+ ref={ref}
51
+ name="cvc"
52
+ mask={mask}
53
+ inputMode="numeric"
54
+ autoComplete="cc-csc"
55
+ keyboardType="number-pad"
56
+ />
57
+ );
58
+ });
@@ -0,0 +1,26 @@
1
+ import { forwardRef } from "react";
2
+ import { BaseEvervaultInputProps, EvervaultInput, mask } from "../Input";
3
+ import { CardFormValues } from "./schema";
4
+
5
+ const CARD_EXPIRY_MASK = mask("99 / 99");
6
+
7
+ export type CardExpiryProps = BaseEvervaultInputProps;
8
+
9
+ export type CardExpiry = EvervaultInput;
10
+
11
+ export const CardExpiry = forwardRef<CardExpiry, CardExpiryProps>(
12
+ function CardExpiry(props, ref) {
13
+ return (
14
+ <EvervaultInput<CardFormValues>
15
+ placeholder="MM / YY"
16
+ {...props}
17
+ ref={ref}
18
+ name="expiry"
19
+ mask={CARD_EXPIRY_MASK}
20
+ inputMode="numeric"
21
+ autoComplete="cc-exp"
22
+ keyboardType="number-pad"
23
+ />
24
+ );
25
+ }
26
+ );
@@ -0,0 +1,27 @@
1
+ import { Platform } from "react-native";
2
+ import { forwardRef } from "react";
3
+ import { BaseEvervaultInputProps, EvervaultInput } from "../Input";
4
+ import { CardFormValues } from "./schema";
5
+
6
+ export type CardHolderProps = BaseEvervaultInputProps;
7
+
8
+ export type CardHolder = EvervaultInput;
9
+
10
+ export const CardHolder = forwardRef<CardHolder, CardHolderProps>(
11
+ function CardHolder(props, ref) {
12
+ return (
13
+ <EvervaultInput<CardFormValues>
14
+ placeholder="Johnny Appleseed"
15
+ {...props}
16
+ ref={ref}
17
+ name="name"
18
+ inputMode="text"
19
+ autoComplete={Platform.select({
20
+ ios: "cc-name",
21
+ default: "name",
22
+ })}
23
+ keyboardType="default"
24
+ />
25
+ );
26
+ }
27
+ );
@@ -0,0 +1,76 @@
1
+ import { render, userEvent } from "@testing-library/react-native";
2
+ import { PropsWithChildren } from "react";
3
+ import { Card } from "./Root";
4
+ import { EvervaultProvider } from "../EvervaultProvider";
5
+ import { CardNumber } from "./Number";
6
+
7
+ function wrapper({ children }: PropsWithChildren) {
8
+ return (
9
+ <EvervaultProvider teamId="team_123" appId="app_123">
10
+ {children}
11
+ </EvervaultProvider>
12
+ );
13
+ }
14
+
15
+ it("uses 16 digits for mask by default", async () => {
16
+ const { rerender, getByTestId } = render(
17
+ <Card>
18
+ <CardNumber testID="number" />
19
+ </Card>,
20
+ { wrapper }
21
+ );
22
+ const number = getByTestId("number");
23
+
24
+ const user = userEvent.setup();
25
+ await user.type(number, "4242424242424242");
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•• •••• ••••");
34
+ });
35
+
36
+ it("uses 19 digits for mask for unionpay", async () => {
37
+ const { rerender, getByTestId } = render(
38
+ <Card>
39
+ <CardNumber testID="number" />
40
+ </Card>,
41
+ { wrapper }
42
+ );
43
+ const number = getByTestId("number");
44
+
45
+ const user = userEvent.setup();
46
+ await user.type(number, "6205500000000000004");
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•• •••• •••• •••");
55
+ });
56
+
57
+ it("uses 15 digits for mask for american express", async () => {
58
+ const { rerender, getByTestId } = render(
59
+ <Card>
60
+ <CardNumber testID="number" />
61
+ </Card>,
62
+ { wrapper }
63
+ );
64
+ const number = getByTestId("number");
65
+
66
+ const user = userEvent.setup();
67
+ await user.type(number, "371449635398431");
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•••• •••••");
76
+ });
@@ -0,0 +1,54 @@
1
+ import { forwardRef, useCallback } from "react";
2
+ import { BaseEvervaultInputProps, EvervaultInput, mask } from "../Input";
3
+ import { CardFormValues } from "./schema";
4
+ import { MaskArray } from "react-native-mask-input";
5
+ import { validateNumber } from "@evervault/card-validator";
6
+ import { CardBrandName } from "./types";
7
+
8
+ const DEFAULT_CARD_NUMBER_MASK = mask("9999 99[99 9999 9999]");
9
+
10
+ const CARD_NUMBER_MASKS: Partial<Record<CardBrandName, MaskArray>> = {
11
+ unionpay: mask("9999 99[99 9999 9999 999]"),
12
+ "american-express": mask("9999 99[9999 99999]"),
13
+ };
14
+
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
+ }
23
+
24
+ export type CardNumber = EvervaultInput;
25
+
26
+ export const CardNumber = forwardRef<CardNumber, CardNumberProps>(
27
+ function CardNumber(props, ref) {
28
+ const mask = useCallback((text?: string): MaskArray => {
29
+ if (!text) {
30
+ return DEFAULT_CARD_NUMBER_MASK;
31
+ }
32
+
33
+ const brand = validateNumber(text).brand;
34
+ if (brand && CARD_NUMBER_MASKS[brand]) {
35
+ return CARD_NUMBER_MASKS[brand];
36
+ }
37
+
38
+ return DEFAULT_CARD_NUMBER_MASK;
39
+ }, []);
40
+
41
+ return (
42
+ <EvervaultInput<CardFormValues>
43
+ placeholder="1234 1234 1234 1234"
44
+ {...props}
45
+ ref={ref}
46
+ name="number"
47
+ mask={mask}
48
+ inputMode="numeric"
49
+ autoComplete="cc-number"
50
+ keyboardType="number-pad"
51
+ />
52
+ );
53
+ }
54
+ );
@@ -0,0 +1,341 @@
1
+ import { PropsWithChildren } from "react";
2
+ import { EvervaultProvider } from "../EvervaultProvider";
3
+ import {
4
+ act,
5
+ fireEvent,
6
+ render,
7
+ userEvent,
8
+ waitFor,
9
+ } from "@testing-library/react-native";
10
+ import { Card } from "./Root";
11
+ import { ErrorBoundary } from "../utils";
12
+ import { CardHolder } from "./Holder";
13
+ import { CardNumber } from "./Number";
14
+ import { CardExpiry } from "./Expiry";
15
+ import { CardCvc } from "./Cvc";
16
+ import { encryptedValue } from "../__mocks__/NativeEvervault";
17
+
18
+ function wrapper({ children }: PropsWithChildren) {
19
+ return (
20
+ <EvervaultProvider teamId="team_123" appId="app_123">
21
+ {children}
22
+ </EvervaultProvider>
23
+ );
24
+ }
25
+
26
+ it("fails if not wrapped in an EvervaultProvider", () => {
27
+ const onError = vi.fn();
28
+ render(
29
+ <ErrorBoundary onError={onError}>
30
+ <Card />
31
+ </ErrorBoundary>
32
+ );
33
+
34
+ expect(onError).toHaveBeenCalledWith(
35
+ new Error("`useEvervault` must be used within an `EvervaultProvider`.")
36
+ );
37
+ });
38
+
39
+ it("calls onChange when mounted", async () => {
40
+ const onChange = vi.fn();
41
+ render(<Card onChange={onChange} />, { wrapper });
42
+
43
+ await waitFor(() => {
44
+ expect(onChange).toHaveBeenCalledWith({
45
+ card: {
46
+ name: null,
47
+ brand: null,
48
+ localBrands: [],
49
+ number: null,
50
+ lastFour: null,
51
+ bin: null,
52
+ expiry: null,
53
+ cvc: null,
54
+ },
55
+ isValid: true,
56
+ isComplete: true,
57
+ errors: {},
58
+ });
59
+ });
60
+ });
61
+
62
+ it("renders card components", async () => {
63
+ const onChange = vi.fn();
64
+ const { getByTestId } = render(
65
+ <Card onChange={onChange}>
66
+ <CardHolder testID="holder" />
67
+ </Card>,
68
+ { wrapper }
69
+ );
70
+
71
+ const holder = getByTestId("holder");
72
+ expect(holder).toBeOnTheScreen();
73
+ expect(holder).toHaveProp("placeholder", "Johnny Appleseed");
74
+
75
+ const user = userEvent.setup();
76
+ await user.type(holder, "John Doe");
77
+ expect(holder).toHaveProp("value", "John Doe");
78
+
79
+ expect(onChange).toHaveBeenLastCalledWith({
80
+ card: {
81
+ name: "John Doe",
82
+ brand: null,
83
+ localBrands: [],
84
+ number: null,
85
+ lastFour: null,
86
+ bin: null,
87
+ expiry: null,
88
+ cvc: null,
89
+ },
90
+ isValid: true,
91
+ isComplete: true,
92
+ errors: {},
93
+ });
94
+ });
95
+
96
+ it("calls onChange when the user types", async () => {
97
+ const onChange = vi.fn();
98
+ const { getByTestId } = render(
99
+ <Card onChange={onChange}>
100
+ <CardHolder testID="holder" />
101
+ <CardNumber testID="number" />
102
+ <CardExpiry testID="expiry" />
103
+ <CardCvc testID="cvc" />
104
+ </Card>,
105
+ { wrapper }
106
+ );
107
+
108
+ const holder = getByTestId("holder");
109
+ const number = getByTestId("number");
110
+ const expiry = getByTestId("expiry");
111
+ const cvc = getByTestId("cvc");
112
+
113
+ const user = userEvent.setup();
114
+ await user.type(holder, "John Doe");
115
+ await user.type(number, "4242 4242 4242 4242");
116
+ await user.type(expiry, "12 / 34");
117
+ await user.type(cvc, "123");
118
+
119
+ expect(onChange).toHaveBeenLastCalledWith({
120
+ card: {
121
+ name: "John Doe",
122
+ brand: "visa",
123
+ localBrands: [],
124
+ number: encryptedValue,
125
+ lastFour: "4242",
126
+ bin: "42424242",
127
+ expiry: {
128
+ month: "12",
129
+ year: "34",
130
+ },
131
+ cvc: encryptedValue,
132
+ },
133
+ isValid: true,
134
+ isComplete: true,
135
+ errors: {},
136
+ });
137
+ });
138
+
139
+ it("resets all fields when reset is called", async () => {
140
+ const ref = { current: null as any };
141
+ const onChange = vi.fn();
142
+ const { getByTestId } = render(
143
+ <Card ref={ref} onChange={onChange}>
144
+ <CardHolder testID="holder" />
145
+ </Card>,
146
+ { wrapper }
147
+ );
148
+
149
+ const holder = getByTestId("holder");
150
+ const user = userEvent.setup();
151
+
152
+ await user.type(holder, "John Doe");
153
+ expect(holder).toHaveProp("value", "John Doe");
154
+ expect(onChange).toHaveBeenLastCalledWith({
155
+ card: {
156
+ name: "John Doe",
157
+ brand: null,
158
+ localBrands: [],
159
+ number: null,
160
+ lastFour: null,
161
+ bin: null,
162
+ expiry: null,
163
+ cvc: null,
164
+ },
165
+ isValid: true,
166
+ isComplete: true,
167
+ errors: {},
168
+ });
169
+
170
+ await act(() => ref.current?.reset());
171
+
172
+ expect(holder).toHaveProp("value", "");
173
+ expect(onChange).toHaveBeenCalledWith({
174
+ card: {
175
+ name: null,
176
+ brand: null,
177
+ localBrands: [],
178
+ number: null,
179
+ lastFour: null,
180
+ bin: null,
181
+ expiry: null,
182
+ cvc: null,
183
+ },
184
+ isValid: false,
185
+ isComplete: false,
186
+ errors: {},
187
+ });
188
+ });
189
+
190
+ it("adds Required error when input is blurred without a value", async () => {
191
+ const onChange = vi.fn();
192
+ const { getByTestId } = render(
193
+ <Card onChange={onChange}>
194
+ <CardNumber testID="number" />
195
+ </Card>,
196
+ { wrapper }
197
+ );
198
+
199
+ const number = getByTestId("number");
200
+
201
+ const user = userEvent.setup();
202
+ await user.type(number, "");
203
+ fireEvent(number, "blur");
204
+
205
+ await waitFor(() => {
206
+ expect(onChange).toHaveBeenLastCalledWith({
207
+ card: {
208
+ name: null,
209
+ brand: null,
210
+ localBrands: [],
211
+ number: null,
212
+ lastFour: null,
213
+ bin: null,
214
+ expiry: null,
215
+ cvc: null,
216
+ },
217
+ isValid: false,
218
+ isComplete: false,
219
+ errors: {
220
+ number: "Required",
221
+ },
222
+ });
223
+ });
224
+ });
225
+
226
+ it("adds Invalid error when input is blurred with an invalid value", async () => {
227
+ const onChange = vi.fn();
228
+ const { getByTestId } = render(
229
+ <Card onChange={onChange}>
230
+ <CardNumber testID="number" />
231
+ </Card>,
232
+ { wrapper }
233
+ );
234
+
235
+ const number = getByTestId("number");
236
+
237
+ const user = userEvent.setup();
238
+ await user.type(number, "4242");
239
+ fireEvent(number, "blur");
240
+
241
+ await waitFor(() => {
242
+ expect(onChange).toHaveBeenLastCalledWith({
243
+ card: {
244
+ name: null,
245
+ brand: "visa",
246
+ localBrands: [],
247
+ number: null,
248
+ lastFour: null,
249
+ bin: null,
250
+ expiry: null,
251
+ cvc: null,
252
+ },
253
+ isValid: false,
254
+ isComplete: false,
255
+ errors: {
256
+ number: "Invalid card number",
257
+ },
258
+ });
259
+ });
260
+ });
261
+
262
+ it("adds 'Brand not accepted' error when brand is not accepted", async () => {
263
+ const onChange = vi.fn();
264
+ const { getByTestId } = render(
265
+ <Card onChange={onChange} acceptedBrands={["american-express"]}>
266
+ <CardNumber testID="number" />
267
+ </Card>,
268
+ { wrapper }
269
+ );
270
+
271
+ const number = getByTestId("number");
272
+
273
+ const user = userEvent.setup();
274
+ await user.type(number, "4242");
275
+ fireEvent(number, "blur");
276
+
277
+ await waitFor(() => {
278
+ expect(onChange).toHaveBeenLastCalledWith({
279
+ card: {
280
+ name: null,
281
+ brand: "visa",
282
+ localBrands: [],
283
+ number: null,
284
+ lastFour: null,
285
+ bin: null,
286
+ expiry: null,
287
+ cvc: null,
288
+ },
289
+ isValid: false,
290
+ isComplete: false,
291
+ errors: {
292
+ number: "Invalid card number",
293
+ },
294
+ });
295
+ });
296
+
297
+ await user.type(number, "4242 4242 4242");
298
+ fireEvent(number, "blur");
299
+
300
+ await waitFor(() => {
301
+ expect(onChange).toHaveBeenLastCalledWith({
302
+ card: {
303
+ name: null,
304
+ brand: "visa",
305
+ localBrands: [],
306
+ number: expect.any(String),
307
+ lastFour: "4242",
308
+ bin: "42424242",
309
+ expiry: null,
310
+ cvc: null,
311
+ },
312
+ isValid: false,
313
+ isComplete: true,
314
+ errors: {
315
+ number: "Brand not accepted",
316
+ },
317
+ });
318
+ });
319
+
320
+ await user.clear(number);
321
+ await user.type(number, "3782 822463 10005");
322
+ fireEvent(number, "blur");
323
+
324
+ await waitFor(() => {
325
+ expect(onChange).toHaveBeenLastCalledWith({
326
+ card: {
327
+ name: null,
328
+ brand: "american-express",
329
+ localBrands: [],
330
+ number: expect.any(String),
331
+ lastFour: "0005",
332
+ bin: "378282",
333
+ expiry: null,
334
+ cvc: null,
335
+ },
336
+ isValid: true,
337
+ isComplete: true,
338
+ errors: {},
339
+ });
340
+ });
341
+ });