@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.
@@ -3,8 +3,10 @@ import {
3
3
  ThreeDSecureSessionsParams,
4
4
  ThreeDSecureSession,
5
5
  ThreeDSecureSessionResponse,
6
+ ThreeDSecureOptions,
6
7
  } from "./types";
7
8
  import { EV_API_DOMAIN } from "./config";
9
+ import { ThreeDSecureEvent } from "./event";
8
10
 
9
11
  export function stopPolling(
10
12
  intervalRef: React.MutableRefObject<NodeJS.Timeout | null>,
@@ -20,58 +22,108 @@ export function stopPolling(
20
22
 
21
23
  export async function startSession(
22
24
  session: ThreeDSecureSession,
23
- callbacks: ThreeDSecureCallbacks | undefined,
25
+ options: ThreeDSecureOptions | undefined,
24
26
  intervalRef: React.MutableRefObject<NodeJS.Timeout | null>,
25
27
  setIsVisible: (show: boolean) => void
26
28
  ) {
27
29
  try {
28
30
  const sessionState = await session.get();
29
31
 
32
+ function fail() {
33
+ stopPolling(intervalRef, setIsVisible);
34
+ options?.onFailure?.(new Error("3DS session failed"));
35
+ }
36
+
30
37
  switch (sessionState.status) {
31
- case "success":
38
+ case "success": {
32
39
  stopPolling(intervalRef, setIsVisible);
33
- callbacks?.onSuccess?.();
40
+ options?.onSuccess?.();
34
41
  break;
35
- case "failure":
36
- stopPolling(intervalRef, setIsVisible);
37
- callbacks?.onFailure?.(new Error("3DS session failed"));
42
+ }
43
+
44
+ case "failure": {
45
+ fail();
38
46
  break;
39
- case "action-required":
47
+ }
48
+
49
+ case "action-required": {
50
+ const failOnChallenge =
51
+ typeof options?.failOnChallenge === "function"
52
+ ? await options.failOnChallenge()
53
+ : options?.failOnChallenge ?? false;
54
+ if (failOnChallenge) {
55
+ fail();
56
+ break;
57
+ }
58
+
59
+ const event = new ThreeDSecureEvent("requestChallenge", session);
60
+ options?.onRequestChallenge?.(event);
61
+ if (event.defaultPrevented) {
62
+ fail();
63
+ break;
64
+ }
65
+
40
66
  setIsVisible(true);
41
- pollSession(session, callbacks, intervalRef, setIsVisible);
42
- break;
43
- default:
44
- break;
67
+ pollSession(session, options, intervalRef, setIsVisible);
68
+ }
45
69
  }
46
70
  } catch (error) {
47
71
  console.error("Error checking session state", error);
48
- callbacks?.onError?.(new Error("Failed to check 3DS session state"));
72
+ options?.onError?.(new Error("Failed to check 3DS session state"));
49
73
  }
50
74
  }
51
75
 
52
76
  export function pollSession(
53
77
  session: ThreeDSecureSession,
54
- callbacks: ThreeDSecureCallbacks | undefined,
78
+ options: ThreeDSecureOptions | undefined,
55
79
  intervalRef: React.MutableRefObject<NodeJS.Timeout | null>,
56
80
  setIsVisible: (show: boolean) => void,
57
81
  interval = 3000
58
82
  ) {
83
+ function fail() {
84
+ stopPolling(intervalRef, setIsVisible);
85
+ options?.onFailure?.(new Error("3DS session failed"));
86
+ }
87
+
59
88
  intervalRef.current = setInterval(async () => {
60
89
  try {
61
90
  const pollResponse: ThreeDSecureSessionResponse = await session.get();
62
- if (pollResponse.status === "success") {
63
- stopPolling(intervalRef, setIsVisible);
64
- callbacks?.onSuccess?.();
65
- } else if (pollResponse.status === "failure") {
66
- stopPolling(intervalRef, setIsVisible);
67
- callbacks?.onFailure?.(new Error("3DS session failed"));
68
- } else {
69
- setIsVisible(true);
91
+ switch (pollResponse.status) {
92
+ case "success": {
93
+ stopPolling(intervalRef, setIsVisible);
94
+ options?.onSuccess?.();
95
+ break;
96
+ }
97
+
98
+ case "failure": {
99
+ fail();
100
+ break;
101
+ }
102
+
103
+ case "action-required": {
104
+ const failOnChallenge =
105
+ typeof options?.failOnChallenge === "function"
106
+ ? await options.failOnChallenge()
107
+ : options?.failOnChallenge ?? false;
108
+ if (failOnChallenge) {
109
+ fail();
110
+ break;
111
+ }
112
+
113
+ const event = new ThreeDSecureEvent("requestChallenge", session);
114
+ options?.onRequestChallenge?.(event);
115
+ if (event.defaultPrevented) {
116
+ fail();
117
+ break;
118
+ }
119
+
120
+ setIsVisible(true);
121
+ }
70
122
  }
71
123
  } catch (error) {
72
124
  stopPolling(intervalRef, setIsVisible);
73
125
  console.error("Error polling session", error);
74
- callbacks?.onError?.(new Error("Error polling 3DS session"));
126
+ options?.onError?.(new Error("Error polling 3DS session"));
75
127
  }
76
128
  }, interval);
77
129
  }
@@ -79,7 +131,7 @@ export function pollSession(
79
131
  export function threeDSecureSession({
80
132
  sessionId,
81
133
  appId,
82
- callbacks,
134
+ options,
83
135
  intervalRef,
84
136
  setIsVisible,
85
137
  }: ThreeDSecureSessionsParams): ThreeDSecureSession {
@@ -116,7 +168,7 @@ export function threeDSecureSession({
116
168
  }
117
169
  );
118
170
 
119
- callbacks?.onFailure?.(new Error("3DS session cancelled by user"));
171
+ options?.onFailure?.(new Error("3DS session cancelled by user"));
120
172
  stopPolling(intervalRef, setIsVisible);
121
173
  } catch (error) {
122
174
  console.error("Error cancelling 3DS session", error);
@@ -1,4 +1,4 @@
1
- import { PropsWithChildren } from "react";
1
+ import { ThreeDSecureEvent } from "./event";
2
2
 
3
3
  export interface ThreeDSecureCallbacks {
4
4
  /**
@@ -6,6 +6,12 @@ export interface ThreeDSecureCallbacks {
6
6
  */
7
7
  onError?(error: Error): void;
8
8
 
9
+ /**
10
+ * The 'requestChallenge' event will be fired if the 3DS authentication process requires a challenge.
11
+ * If you'd like to fail the authentication, you should call `preventDefault` on the passed event.
12
+ */
13
+ onRequestChallenge?(event: ThreeDSecureEvent): void;
14
+
9
15
  /**
10
16
  * The 'failure' event will be fired if the 3DS authentication process fails. You should use this event to handle the failure and inform the user and prompt them to try again.
11
17
  * If the user cancels the 3DS authentication process this event will be fired.
@@ -20,6 +26,13 @@ export interface ThreeDSecureCallbacks {
20
26
  onSuccess?(): void;
21
27
  }
22
28
 
29
+ export interface ThreeDSecureOptions extends ThreeDSecureCallbacks {
30
+ /**
31
+ * If set to `true` (or a function that returns `true`), the authentication will fail if a challenge is required.
32
+ */
33
+ failOnChallenge?: boolean | (() => Promise<boolean>);
34
+ }
35
+
23
36
  export interface ThreeDSecureInitialState {
24
37
  session: ThreeDSecureSession | null;
25
38
  isVisible: boolean;
@@ -44,7 +57,7 @@ export interface ThreeDSecureSessionResponse {
44
57
 
45
58
  export interface ThreeDSecureSessionsParams {
46
59
  appId: string;
47
- callbacks?: ThreeDSecureCallbacks;
60
+ options?: ThreeDSecureOptions;
48
61
  intervalRef: React.MutableRefObject<NodeJS.Timeout | null>;
49
62
  sessionId: string;
50
63
  setIsVisible: (show: boolean) => void;
@@ -61,7 +74,7 @@ export interface ThreeDSecureState extends ThreeDSecureInitialState {
61
74
  * The `start()` function is used to kick off the 3DS authentication process.
62
75
  *
63
76
  * @param sessionId The 3DS session ID. A 3DS session can be created using the [Evervault API](https://docs.evervault.com/api-reference#createThreeDSSession).
64
- * @param callbacks The callbacks to be called when the 3DS authentication process is finished.
77
+ * @param options The options to be used for the 3DS authentication process.
65
78
  */
66
- start(sessionId: string, callbacks?: ThreeDSecureCallbacks): void;
79
+ start(sessionId: string, options?: ThreeDSecureOptions): void;
67
80
  }
@@ -2,6 +2,7 @@ import { PropsWithChildren } from "react";
2
2
  import { EvervaultProvider } from "../EvervaultProvider";
3
3
  import { act, renderHook } from "@testing-library/react-native";
4
4
  import { useThreeDSecure } from "./useThreeDSecure";
5
+ import { ThreeDSecureEvent } from "./event";
5
6
 
6
7
  function wrapper({ children }: PropsWithChildren) {
7
8
  return (
@@ -17,6 +18,10 @@ const callbacks = {
17
18
  onFailure: vi.fn(),
18
19
  };
19
20
 
21
+ beforeEach(() => {
22
+ vi.clearAllMocks();
23
+ });
24
+
20
25
  it("returns the correct state", () => {
21
26
  const { result } = renderHook(() => useThreeDSecure(), {
22
27
  wrapper,
@@ -31,7 +36,7 @@ it("returns the correct state", () => {
31
36
  });
32
37
 
33
38
  it("starts a session when action is required", async () => {
34
- const { result, rerender } = renderHook(() => useThreeDSecure(), {
39
+ const { result } = renderHook(() => useThreeDSecure(), {
35
40
  wrapper,
36
41
  });
37
42
 
@@ -49,6 +54,112 @@ it("starts a session when action is required", async () => {
49
54
  expect(result.current.isVisible).toBe(true);
50
55
  });
51
56
 
57
+ it("fails the session when failOnChallenge is true and a challenge is required", async () => {
58
+ const { result } = renderHook(() => useThreeDSecure(), {
59
+ wrapper,
60
+ });
61
+
62
+ vi.spyOn(global, "fetch").mockResolvedValue({
63
+ json: () => Promise.resolve({ status: "action-required" }),
64
+ } as any);
65
+
66
+ const onRequestChallenge = vi.fn();
67
+
68
+ await act(() =>
69
+ result.current.start("session_123", {
70
+ ...callbacks,
71
+ onRequestChallenge,
72
+ failOnChallenge: true,
73
+ })
74
+ );
75
+
76
+ expect(callbacks.onFailure).toHaveBeenCalled();
77
+ expect(onRequestChallenge).not.toHaveBeenCalled();
78
+ });
79
+
80
+ it("fails the session when failOnChallenge is a function that returns true and a challenge is required", async () => {
81
+ const { result } = renderHook(() => useThreeDSecure(), {
82
+ wrapper,
83
+ });
84
+
85
+ vi.spyOn(global, "fetch").mockResolvedValue({
86
+ json: () => Promise.resolve({ status: "action-required" }),
87
+ } as any);
88
+
89
+ const onRequestChallenge = vi.fn();
90
+
91
+ await act(() =>
92
+ result.current.start("session_123", {
93
+ ...callbacks,
94
+ onRequestChallenge,
95
+ failOnChallenge: () => Promise.resolve(true),
96
+ })
97
+ );
98
+
99
+ expect(callbacks.onFailure).toHaveBeenCalled();
100
+ expect(onRequestChallenge).not.toHaveBeenCalled();
101
+ });
102
+
103
+ it("fails the session when failOnChallenge resolves true at hook level and a challenge is required", async () => {
104
+ const { result } = renderHook(
105
+ () =>
106
+ useThreeDSecure({
107
+ failOnChallenge: () => Promise.resolve(true),
108
+ }),
109
+ {
110
+ wrapper,
111
+ }
112
+ );
113
+
114
+ vi.spyOn(global, "fetch").mockResolvedValue({
115
+ json: () => Promise.resolve({ status: "action-required" }),
116
+ } as any);
117
+
118
+ const onRequestChallenge = vi.fn();
119
+ await act(() =>
120
+ result.current.start("session_123", {
121
+ ...callbacks,
122
+ onRequestChallenge,
123
+ })
124
+ );
125
+
126
+ expect(onRequestChallenge).not.toHaveBeenCalled();
127
+ expect(callbacks.onFailure).toHaveBeenCalled();
128
+ });
129
+
130
+ it("fails the session when onRequestChallenge is called and defaultPrevented is true", async () => {
131
+ const { result } = renderHook(() => useThreeDSecure(), {
132
+ wrapper,
133
+ });
134
+
135
+ vi.spyOn(global, "fetch").mockResolvedValue({
136
+ json: () => Promise.resolve({ status: "action-required" }),
137
+ } as any);
138
+
139
+ let onRequestChallenge = vi.fn((event: ThreeDSecureEvent) =>
140
+ event.preventDefault()
141
+ );
142
+ await act(() =>
143
+ result.current.start("session_123", {
144
+ ...callbacks,
145
+ onRequestChallenge,
146
+ })
147
+ );
148
+ expect(onRequestChallenge).toHaveBeenCalled();
149
+ expect(callbacks.onFailure).toHaveBeenCalled();
150
+
151
+ let onFailure = vi.fn();
152
+ onRequestChallenge = vi.fn();
153
+ await act(() =>
154
+ result.current.start("session_123", {
155
+ onFailure,
156
+ onRequestChallenge,
157
+ })
158
+ );
159
+ expect(onRequestChallenge).toHaveBeenCalled();
160
+ expect(onFailure).not.toHaveBeenCalled();
161
+ });
162
+
52
163
  it("calls the success callback when the session is successful", async () => {
53
164
  const { result } = renderHook(() => useThreeDSecure(), {
54
165
  wrapper,
@@ -1,30 +1,47 @@
1
1
  import { useCallback, useMemo, useState } from "react";
2
2
  import { useRef } from "react";
3
3
  import { startSession, threeDSecureSession } from "./session";
4
- import { ThreeDSecureSession, ThreeDSecureState } from "./types";
4
+ import {
5
+ ThreeDSecureOptions,
6
+ ThreeDSecureSession,
7
+ ThreeDSecureState,
8
+ } from "./types";
5
9
  import { useEvervault } from "../useEvervault";
6
10
 
7
- export function useThreeDSecure(): ThreeDSecureState {
11
+ export interface UseThreeDSecureOptions {
12
+ failOnChallenge?: boolean | (() => Promise<boolean>);
13
+ }
14
+
15
+ export function useThreeDSecure(
16
+ options?: UseThreeDSecureOptions
17
+ ): ThreeDSecureState {
8
18
  const { appId } = useEvervault();
9
19
  const intervalRef = useRef<NodeJS.Timeout | null>(null);
10
20
  const [session, setSession] = useState<ThreeDSecureSession | null>(null);
11
21
  const [isVisible, setIsVisible] = useState(false);
12
22
 
23
+ const failOnChallenge = options?.failOnChallenge ?? false;
24
+
13
25
  const start = useCallback<ThreeDSecureState["start"]>(
14
- (sessionId, callbacks) => {
26
+ (sessionId, options) => {
27
+ const startOptions: ThreeDSecureOptions = {
28
+ ...options,
29
+ failOnChallenge: options?.failOnChallenge ?? failOnChallenge,
30
+ };
31
+
15
32
  const session = threeDSecureSession({
16
33
  sessionId,
17
34
  appId,
18
- callbacks,
35
+ options: startOptions,
19
36
  intervalRef,
20
37
  setIsVisible,
21
38
  });
22
39
 
23
40
  setSession(session);
24
41
 
25
- startSession(session, callbacks, intervalRef, setIsVisible);
42
+ startSession(session, startOptions, intervalRef, setIsVisible);
26
43
  },
27
- [appId]
44
+ [appId, failOnChallenge]
28
45
  );
29
46
 
30
47
  const cancel = useCallback<ThreeDSecureState["cancel"]>(async () => {