@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.
- 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/Input.d.ts +1 -0
- package/build/Input.d.ts.map +1 -1
- package/build/ThreeDSecure/event.d.ts +11 -0
- package/build/ThreeDSecure/event.d.ts.map +1 -0
- package/build/ThreeDSecure/session.d.ts +4 -4
- package/build/ThreeDSecure/session.d.ts.map +1 -1
- package/build/ThreeDSecure/types.d.ts +15 -3
- package/build/ThreeDSecure/types.d.ts.map +1 -1
- package/build/ThreeDSecure/useThreeDSecure.d.ts +4 -1
- package/build/ThreeDSecure/useThreeDSecure.d.ts.map +1 -1
- package/build/index.cjs.js +149 -110
- package/build/index.cjs.js.map +1 -1
- package/build/index.esm.js +149 -110
- package/package.json +1 -1
- package/src/Card/Cvc.tsx +10 -3
- package/src/Card/Number.test.tsx +24 -3
- package/src/Card/Number.tsx +11 -4
- package/src/Input.test.tsx +84 -0
- package/src/Input.tsx +73 -47
- package/src/ThreeDSecure/event.ts +19 -0
- package/src/ThreeDSecure/session.test.ts +219 -24
- package/src/ThreeDSecure/session.ts +76 -24
- package/src/ThreeDSecure/types.ts +17 -4
- package/src/ThreeDSecure/useThreeDSecure.test.tsx +112 -1
- package/src/ThreeDSecure/useThreeDSecure.ts +23 -6
|
@@ -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
|
-
|
|
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
|
-
|
|
40
|
+
options?.onSuccess?.();
|
|
34
41
|
break;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case "failure": {
|
|
45
|
+
fail();
|
|
38
46
|
break;
|
|
39
|
-
|
|
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,
|
|
42
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
77
|
+
* @param options The options to be used for the 3DS authentication process.
|
|
65
78
|
*/
|
|
66
|
-
start(sessionId: string,
|
|
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
|
|
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 {
|
|
4
|
+
import {
|
|
5
|
+
ThreeDSecureOptions,
|
|
6
|
+
ThreeDSecureSession,
|
|
7
|
+
ThreeDSecureState,
|
|
8
|
+
} from "./types";
|
|
5
9
|
import { useEvervault } from "../useEvervault";
|
|
6
10
|
|
|
7
|
-
export
|
|
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,
|
|
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
|
-
|
|
35
|
+
options: startOptions,
|
|
19
36
|
intervalRef,
|
|
20
37
|
setIsVisible,
|
|
21
38
|
});
|
|
22
39
|
|
|
23
40
|
setSession(session);
|
|
24
41
|
|
|
25
|
-
startSession(session,
|
|
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 () => {
|