@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.
- package/package.json +3 -2
- package/src/Card/Cvc.test.tsx +41 -0
- package/src/Card/Cvc.tsx +58 -0
- package/src/Card/Expiry.tsx +26 -0
- package/src/Card/Holder.tsx +27 -0
- package/src/Card/Number.test.tsx +76 -0
- package/src/Card/Number.tsx +54 -0
- package/src/Card/Root.test.tsx +341 -0
- package/src/Card/Root.tsx +150 -0
- package/src/Card/index.ts +28 -0
- package/src/Card/schema.ts +41 -0
- package/src/Card/types.ts +57 -0
- package/src/Card/utils.test.ts +271 -0
- package/src/Card/utils.ts +129 -0
- package/src/EvervaultProvider.test.tsx +24 -0
- package/src/EvervaultProvider.tsx +43 -0
- package/src/Input.test.tsx +420 -0
- package/src/Input.tsx +182 -0
- package/src/ThreeDSecure/Frame.test.tsx +87 -0
- package/src/ThreeDSecure/Frame.tsx +50 -0
- package/src/ThreeDSecure/Root.test.tsx +67 -0
- package/src/ThreeDSecure/Root.tsx +23 -0
- package/src/ThreeDSecure/config.ts +3 -0
- package/src/ThreeDSecure/context.ts +6 -0
- package/src/ThreeDSecure/event.ts +19 -0
- package/src/ThreeDSecure/index.ts +17 -0
- package/src/ThreeDSecure/session.test.ts +524 -0
- package/src/ThreeDSecure/session.ts +184 -0
- package/src/ThreeDSecure/types.ts +80 -0
- package/src/ThreeDSecure/useThreeDSecure.test.tsx +244 -0
- package/src/ThreeDSecure/useThreeDSecure.ts +64 -0
- package/src/__mocks__/NativeEvervault.ts +13 -0
- package/src/__mocks__/react-native-webview.tsx +6 -0
- package/src/context.ts +14 -0
- package/src/index.ts +21 -0
- package/src/sdk.test.ts +122 -0
- package/src/sdk.ts +71 -0
- package/src/specs/NativeEvervault.ts +67 -0
- package/src/useEvervault.test.tsx +31 -0
- package/src/useEvervault.ts +14 -0
- package/src/utils.ts +41 -0
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { EV_API_DOMAIN } from "./config";
|
|
2
|
+
import { ThreeDSecureEvent } from "./event";
|
|
3
|
+
import {
|
|
4
|
+
pollSession,
|
|
5
|
+
startSession,
|
|
6
|
+
stopPolling,
|
|
7
|
+
threeDSecureSession,
|
|
8
|
+
} from "./session";
|
|
9
|
+
import { ThreeDSecureSession } from "./types";
|
|
10
|
+
|
|
11
|
+
describe("stopPolling", () => {
|
|
12
|
+
it("should stop polling", () => {
|
|
13
|
+
const spy = vi.spyOn(global, "clearInterval");
|
|
14
|
+
|
|
15
|
+
const interval = setTimeout(() => {}, 0);
|
|
16
|
+
const intervalRef = { current: interval };
|
|
17
|
+
const setIsVisible = vi.fn();
|
|
18
|
+
|
|
19
|
+
stopPolling(intervalRef, setIsVisible);
|
|
20
|
+
|
|
21
|
+
expect(setIsVisible).toHaveBeenCalledWith(false);
|
|
22
|
+
expect(spy).toHaveBeenCalledWith(interval);
|
|
23
|
+
expect(intervalRef.current).toBeNull();
|
|
24
|
+
});
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
describe("startSession", () => {
|
|
28
|
+
const options = {
|
|
29
|
+
onSuccess: vi.fn(),
|
|
30
|
+
onFailure: vi.fn(),
|
|
31
|
+
onError: vi.fn(),
|
|
32
|
+
};
|
|
33
|
+
const intervalRef = { current: null };
|
|
34
|
+
const setIsVisible = vi.fn();
|
|
35
|
+
|
|
36
|
+
beforeEach(() => {
|
|
37
|
+
vi.clearAllMocks();
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("should start a successful session", async () => {
|
|
41
|
+
const session: ThreeDSecureSession = {
|
|
42
|
+
cancel: vi.fn(),
|
|
43
|
+
get: vi.fn(() => Promise.resolve({ status: "success" }) as any),
|
|
44
|
+
sessionId: "123",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
await startSession(session, options, intervalRef, setIsVisible);
|
|
48
|
+
|
|
49
|
+
expect(session.get).toHaveBeenCalled();
|
|
50
|
+
expect(options.onSuccess).toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("should start a failed session", async () => {
|
|
54
|
+
const session: ThreeDSecureSession = {
|
|
55
|
+
cancel: vi.fn(),
|
|
56
|
+
get: vi.fn(() => Promise.resolve({ status: "failure" }) as any),
|
|
57
|
+
sessionId: "123",
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
await startSession(session, options, intervalRef, setIsVisible);
|
|
61
|
+
|
|
62
|
+
expect(session.get).toHaveBeenCalled();
|
|
63
|
+
expect(options.onFailure).toHaveBeenCalledWith(
|
|
64
|
+
new Error("3DS session failed")
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it("should start a session that requires action", async () => {
|
|
69
|
+
const session: ThreeDSecureSession = {
|
|
70
|
+
cancel: vi.fn(),
|
|
71
|
+
get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
|
|
72
|
+
sessionId: "123",
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
await startSession(session, options, intervalRef, setIsVisible);
|
|
76
|
+
|
|
77
|
+
expect(session.get).toHaveBeenCalled();
|
|
78
|
+
expect(setIsVisible).toHaveBeenCalledWith(true);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should fail the session when failOnChallenge is true and a challenge is required", async () => {
|
|
82
|
+
const session: ThreeDSecureSession = {
|
|
83
|
+
cancel: vi.fn(),
|
|
84
|
+
get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
|
|
85
|
+
sessionId: "123",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
await startSession(
|
|
89
|
+
session,
|
|
90
|
+
{ ...options, failOnChallenge: true },
|
|
91
|
+
intervalRef,
|
|
92
|
+
setIsVisible
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
expect(session.get).toHaveBeenCalled();
|
|
96
|
+
expect(options.onFailure).toHaveBeenCalledWith(
|
|
97
|
+
new Error("3DS session failed")
|
|
98
|
+
);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("should fail the session when failOnChallenge is a function that returns true and a challenge is required", async () => {
|
|
102
|
+
const session: ThreeDSecureSession = {
|
|
103
|
+
cancel: vi.fn(),
|
|
104
|
+
get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
|
|
105
|
+
sessionId: "123",
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
await startSession(
|
|
109
|
+
session,
|
|
110
|
+
{ ...options, failOnChallenge: () => Promise.resolve(true) },
|
|
111
|
+
intervalRef,
|
|
112
|
+
setIsVisible
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
expect(session.get).toHaveBeenCalled();
|
|
116
|
+
expect(options.onFailure).toHaveBeenCalledWith(
|
|
117
|
+
new Error("3DS session failed")
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("should call onRequestChallenge when a challenge is required", async () => {
|
|
122
|
+
const session: ThreeDSecureSession = {
|
|
123
|
+
cancel: vi.fn(),
|
|
124
|
+
get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
|
|
125
|
+
sessionId: "123",
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
const onRequestChallenge = vi.fn();
|
|
129
|
+
|
|
130
|
+
await startSession(
|
|
131
|
+
session,
|
|
132
|
+
{ ...options, onRequestChallenge },
|
|
133
|
+
intervalRef,
|
|
134
|
+
setIsVisible
|
|
135
|
+
);
|
|
136
|
+
|
|
137
|
+
expect(session.get).toHaveBeenCalled();
|
|
138
|
+
expect(onRequestChallenge).toHaveBeenCalled();
|
|
139
|
+
expect(options.onFailure).not.toHaveBeenCalled();
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("should fail the session when onRequestChallenge is called and default is prevented", async () => {
|
|
143
|
+
const session: ThreeDSecureSession = {
|
|
144
|
+
cancel: vi.fn(),
|
|
145
|
+
get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
|
|
146
|
+
sessionId: "123",
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
const onRequestChallenge = vi.fn((event: ThreeDSecureEvent) => {
|
|
150
|
+
event.preventDefault();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
await startSession(
|
|
154
|
+
session,
|
|
155
|
+
{ ...options, onRequestChallenge },
|
|
156
|
+
intervalRef,
|
|
157
|
+
setIsVisible
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
expect(session.get).toHaveBeenCalled();
|
|
161
|
+
expect(onRequestChallenge).toHaveBeenCalled();
|
|
162
|
+
expect(options.onFailure).toHaveBeenCalledWith(
|
|
163
|
+
new Error("3DS session failed")
|
|
164
|
+
);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should call onError if the session fails to start", async () => {
|
|
168
|
+
const session: ThreeDSecureSession = {
|
|
169
|
+
cancel: vi.fn(),
|
|
170
|
+
get: vi.fn(() => Promise.reject(new Error("Failed to start session"))),
|
|
171
|
+
sessionId: "123",
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const consoleErrorSpy = vi
|
|
175
|
+
.spyOn(console, "error")
|
|
176
|
+
.mockImplementation(() => {});
|
|
177
|
+
|
|
178
|
+
await startSession(session, options, intervalRef, setIsVisible);
|
|
179
|
+
|
|
180
|
+
expect(session.get).toHaveBeenCalled();
|
|
181
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
182
|
+
"Error checking session state",
|
|
183
|
+
new Error("Failed to start session")
|
|
184
|
+
);
|
|
185
|
+
expect(options.onError).toHaveBeenCalledWith(
|
|
186
|
+
new Error("Failed to check 3DS session state")
|
|
187
|
+
);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe("pollSession", () => {
|
|
192
|
+
const options = {
|
|
193
|
+
onSuccess: vi.fn(),
|
|
194
|
+
onFailure: vi.fn(),
|
|
195
|
+
onError: vi.fn(),
|
|
196
|
+
};
|
|
197
|
+
const intervalRef = { current: null };
|
|
198
|
+
const setIsVisible = vi.fn();
|
|
199
|
+
|
|
200
|
+
beforeEach(() => {
|
|
201
|
+
vi.clearAllMocks();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("should start an interval", () => {
|
|
205
|
+
const session: ThreeDSecureSession = {
|
|
206
|
+
cancel: vi.fn(),
|
|
207
|
+
get: vi.fn(() => Promise.resolve({ status: "success" }) as any),
|
|
208
|
+
sessionId: "123",
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const intervalSpy = vi.spyOn(global, "setInterval");
|
|
212
|
+
pollSession(session, options, intervalRef, setIsVisible);
|
|
213
|
+
|
|
214
|
+
expect(intervalSpy).toHaveBeenCalledWith(expect.any(Function), 3000);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("should poll successful session", async () => {
|
|
218
|
+
const session: ThreeDSecureSession = {
|
|
219
|
+
cancel: vi.fn(),
|
|
220
|
+
get: vi.fn(() => Promise.resolve({ status: "success" }) as any),
|
|
221
|
+
sessionId: "123",
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
const intervalSpy = vi.spyOn(global, "setInterval");
|
|
225
|
+
pollSession(session, options, intervalRef, setIsVisible);
|
|
226
|
+
await intervalSpy.mock.calls[0][0]();
|
|
227
|
+
|
|
228
|
+
expect(session.get).toHaveBeenCalled();
|
|
229
|
+
expect(options.onSuccess).toHaveBeenCalled();
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("should poll failed session", async () => {
|
|
233
|
+
const session: ThreeDSecureSession = {
|
|
234
|
+
cancel: vi.fn(),
|
|
235
|
+
get: vi.fn(() => Promise.resolve({ status: "failure" }) as any),
|
|
236
|
+
sessionId: "123",
|
|
237
|
+
};
|
|
238
|
+
|
|
239
|
+
const intervalSpy = vi.spyOn(global, "setInterval");
|
|
240
|
+
pollSession(session, options, intervalRef, setIsVisible);
|
|
241
|
+
await intervalSpy.mock.calls[0][0]();
|
|
242
|
+
|
|
243
|
+
expect(session.get).toHaveBeenCalled();
|
|
244
|
+
expect(options.onFailure).toHaveBeenCalledWith(
|
|
245
|
+
new Error("3DS session failed")
|
|
246
|
+
);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("should poll session that requires action", async () => {
|
|
250
|
+
const session: ThreeDSecureSession = {
|
|
251
|
+
cancel: vi.fn(),
|
|
252
|
+
get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
|
|
253
|
+
sessionId: "123",
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const intervalSpy = vi.spyOn(global, "setInterval");
|
|
257
|
+
pollSession(session, options, intervalRef, setIsVisible);
|
|
258
|
+
await intervalSpy.mock.calls[0][0]();
|
|
259
|
+
|
|
260
|
+
expect(session.get).toHaveBeenCalled();
|
|
261
|
+
expect(setIsVisible).toHaveBeenCalledWith(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should fail the session when failOnChallenge is true and a challenge is required", async () => {
|
|
265
|
+
const session: ThreeDSecureSession = {
|
|
266
|
+
cancel: vi.fn(),
|
|
267
|
+
get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
|
|
268
|
+
sessionId: "123",
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
const intervalSpy = vi.spyOn(global, "setInterval");
|
|
272
|
+
const onRequestChallenge = vi.fn();
|
|
273
|
+
pollSession(
|
|
274
|
+
session,
|
|
275
|
+
{ ...options, failOnChallenge: true, onRequestChallenge },
|
|
276
|
+
intervalRef,
|
|
277
|
+
setIsVisible
|
|
278
|
+
);
|
|
279
|
+
await intervalSpy.mock.calls[0][0]();
|
|
280
|
+
|
|
281
|
+
expect(session.get).toHaveBeenCalled();
|
|
282
|
+
expect(onRequestChallenge).not.toHaveBeenCalled();
|
|
283
|
+
expect(options.onFailure).toHaveBeenCalledWith(
|
|
284
|
+
new Error("3DS session failed")
|
|
285
|
+
);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("should fail the session when failOnChallenge is a function that returns true and a challenge is required", async () => {
|
|
289
|
+
const session: ThreeDSecureSession = {
|
|
290
|
+
cancel: vi.fn(),
|
|
291
|
+
get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
|
|
292
|
+
sessionId: "123",
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const intervalSpy = vi.spyOn(global, "setInterval");
|
|
296
|
+
const onRequestChallenge = vi.fn();
|
|
297
|
+
pollSession(
|
|
298
|
+
session,
|
|
299
|
+
{
|
|
300
|
+
...options,
|
|
301
|
+
onRequestChallenge,
|
|
302
|
+
failOnChallenge: () => Promise.resolve(true),
|
|
303
|
+
},
|
|
304
|
+
intervalRef,
|
|
305
|
+
setIsVisible
|
|
306
|
+
);
|
|
307
|
+
await intervalSpy.mock.calls[0][0]();
|
|
308
|
+
|
|
309
|
+
expect(session.get).toHaveBeenCalled();
|
|
310
|
+
expect(onRequestChallenge).not.toHaveBeenCalled();
|
|
311
|
+
expect(options.onFailure).toHaveBeenCalledWith(
|
|
312
|
+
new Error("3DS session failed")
|
|
313
|
+
);
|
|
314
|
+
});
|
|
315
|
+
|
|
316
|
+
it("should call onRequestChallenge when a challenge is required", async () => {
|
|
317
|
+
const session: ThreeDSecureSession = {
|
|
318
|
+
cancel: vi.fn(),
|
|
319
|
+
get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
|
|
320
|
+
sessionId: "123",
|
|
321
|
+
};
|
|
322
|
+
|
|
323
|
+
const intervalSpy = vi.spyOn(global, "setInterval");
|
|
324
|
+
const onRequestChallenge = vi.fn();
|
|
325
|
+
pollSession(
|
|
326
|
+
session,
|
|
327
|
+
{ ...options, onRequestChallenge },
|
|
328
|
+
intervalRef,
|
|
329
|
+
setIsVisible
|
|
330
|
+
);
|
|
331
|
+
await intervalSpy.mock.calls[0][0]();
|
|
332
|
+
|
|
333
|
+
expect(session.get).toHaveBeenCalled();
|
|
334
|
+
expect(onRequestChallenge).toHaveBeenCalled();
|
|
335
|
+
expect(options.onFailure).not.toHaveBeenCalled();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("should fail the session when onRequestChallenge is called and default is prevented", async () => {
|
|
339
|
+
const session: ThreeDSecureSession = {
|
|
340
|
+
cancel: vi.fn(),
|
|
341
|
+
get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
|
|
342
|
+
sessionId: "123",
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const intervalSpy = vi.spyOn(global, "setInterval");
|
|
346
|
+
const onRequestChallenge = vi.fn((event: ThreeDSecureEvent) => {
|
|
347
|
+
event.preventDefault();
|
|
348
|
+
});
|
|
349
|
+
pollSession(
|
|
350
|
+
session,
|
|
351
|
+
{ ...options, onRequestChallenge },
|
|
352
|
+
intervalRef,
|
|
353
|
+
setIsVisible
|
|
354
|
+
);
|
|
355
|
+
await intervalSpy.mock.calls[0][0]();
|
|
356
|
+
|
|
357
|
+
expect(session.get).toHaveBeenCalled();
|
|
358
|
+
expect(onRequestChallenge).toHaveBeenCalled();
|
|
359
|
+
expect(options.onFailure).toHaveBeenCalledWith(
|
|
360
|
+
new Error("3DS session failed")
|
|
361
|
+
);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("should call onError if the session fails to poll", async () => {
|
|
365
|
+
const session: ThreeDSecureSession = {
|
|
366
|
+
cancel: vi.fn(),
|
|
367
|
+
get: vi.fn(() => Promise.reject(new Error("Failed to poll session"))),
|
|
368
|
+
sessionId: "123",
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const consoleErrorSpy = vi
|
|
372
|
+
.spyOn(console, "error")
|
|
373
|
+
.mockImplementation(() => {});
|
|
374
|
+
|
|
375
|
+
const intervalSpy = vi.spyOn(global, "setInterval");
|
|
376
|
+
pollSession(session, options, intervalRef, setIsVisible);
|
|
377
|
+
await intervalSpy.mock.calls[0][0]();
|
|
378
|
+
|
|
379
|
+
expect(session.get).toHaveBeenCalled();
|
|
380
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
381
|
+
"Error polling session",
|
|
382
|
+
new Error("Failed to poll session")
|
|
383
|
+
);
|
|
384
|
+
expect(options.onError).toHaveBeenCalledWith(
|
|
385
|
+
new Error("Error polling 3DS session")
|
|
386
|
+
);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe("threeDSecureSession", () => {
|
|
391
|
+
const options = {
|
|
392
|
+
onSuccess: vi.fn(),
|
|
393
|
+
onFailure: vi.fn(),
|
|
394
|
+
onError: vi.fn(),
|
|
395
|
+
};
|
|
396
|
+
const intervalRef = { current: null };
|
|
397
|
+
const setIsVisible = vi.fn();
|
|
398
|
+
|
|
399
|
+
it("should create a session", () => {
|
|
400
|
+
const session = threeDSecureSession({
|
|
401
|
+
sessionId: "123",
|
|
402
|
+
appId: "app_123",
|
|
403
|
+
options,
|
|
404
|
+
intervalRef,
|
|
405
|
+
setIsVisible,
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
expect(session).toBeDefined();
|
|
409
|
+
expect(session.sessionId).toBe("123");
|
|
410
|
+
expect(session.get).toBeInstanceOf(Function);
|
|
411
|
+
expect(session.cancel).toBeInstanceOf(Function);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
it("creates a get function that fetches the session", async () => {
|
|
415
|
+
const session = threeDSecureSession({
|
|
416
|
+
sessionId: "123",
|
|
417
|
+
appId: "app_123",
|
|
418
|
+
options,
|
|
419
|
+
intervalRef,
|
|
420
|
+
setIsVisible,
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
const fetchSpy = vi.spyOn(global, "fetch");
|
|
424
|
+
fetchSpy.mockImplementation(
|
|
425
|
+
() =>
|
|
426
|
+
Promise.resolve({
|
|
427
|
+
json: () => Promise.resolve({ status: "success" }),
|
|
428
|
+
}) as any
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
const result = await session.get();
|
|
432
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
433
|
+
`https://${EV_API_DOMAIN}/frontend/3ds/browser-sessions/123`,
|
|
434
|
+
{
|
|
435
|
+
headers: {
|
|
436
|
+
"x-evervault-app-id": "app_123",
|
|
437
|
+
},
|
|
438
|
+
}
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
expect(result).toEqual({ status: "success" });
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it("creates a get function that throws an error if the session fails to fetch", async () => {
|
|
445
|
+
const session = threeDSecureSession({
|
|
446
|
+
sessionId: "123",
|
|
447
|
+
appId: "app_123",
|
|
448
|
+
options,
|
|
449
|
+
intervalRef,
|
|
450
|
+
setIsVisible,
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
const fetchSpy = vi.spyOn(global, "fetch");
|
|
454
|
+
fetchSpy.mockImplementation(() =>
|
|
455
|
+
Promise.reject(new Error("Failed to fetch session"))
|
|
456
|
+
);
|
|
457
|
+
|
|
458
|
+
const consoleErrorSpy = vi
|
|
459
|
+
.spyOn(console, "error")
|
|
460
|
+
.mockImplementation(() => {});
|
|
461
|
+
|
|
462
|
+
await expect(session.get()).rejects.toThrow("Failed to fetch session");
|
|
463
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
464
|
+
"Error fetching 3DS session status",
|
|
465
|
+
new Error("Failed to fetch session")
|
|
466
|
+
);
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
it("creates a cancel function that cancels the session", async () => {
|
|
470
|
+
const session = threeDSecureSession({
|
|
471
|
+
sessionId: "123",
|
|
472
|
+
appId: "app_123",
|
|
473
|
+
options,
|
|
474
|
+
intervalRef,
|
|
475
|
+
setIsVisible,
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
const fetchSpy = vi.spyOn(global, "fetch");
|
|
479
|
+
fetchSpy.mockImplementation(() => Promise.resolve({}) as any);
|
|
480
|
+
|
|
481
|
+
await session.cancel();
|
|
482
|
+
|
|
483
|
+
expect(fetchSpy).toHaveBeenCalledWith(
|
|
484
|
+
`https://${EV_API_DOMAIN}/frontend/3ds/browser-sessions/123`,
|
|
485
|
+
{
|
|
486
|
+
method: "PATCH",
|
|
487
|
+
headers: {
|
|
488
|
+
"Content-Type": "application/json",
|
|
489
|
+
"x-evervault-app-id": "app_123",
|
|
490
|
+
},
|
|
491
|
+
body: JSON.stringify({ outcome: "cancelled" }),
|
|
492
|
+
}
|
|
493
|
+
);
|
|
494
|
+
|
|
495
|
+
expect(options.onFailure).toHaveBeenCalledWith(
|
|
496
|
+
new Error("3DS session cancelled by user")
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it("creates a cancel function that throws an error if the session fails to cancel", async () => {
|
|
501
|
+
const session = threeDSecureSession({
|
|
502
|
+
sessionId: "123",
|
|
503
|
+
appId: "app_123",
|
|
504
|
+
options,
|
|
505
|
+
intervalRef,
|
|
506
|
+
setIsVisible,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
const fetchSpy = vi.spyOn(global, "fetch");
|
|
510
|
+
fetchSpy.mockImplementation(() =>
|
|
511
|
+
Promise.reject(new Error("Failed to cancel session"))
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
const consoleErrorSpy = vi
|
|
515
|
+
.spyOn(console, "error")
|
|
516
|
+
.mockImplementation(() => {});
|
|
517
|
+
|
|
518
|
+
await expect(session.cancel()).rejects.toThrow("Failed to cancel session");
|
|
519
|
+
expect(consoleErrorSpy).toHaveBeenCalledWith(
|
|
520
|
+
"Error cancelling 3DS session",
|
|
521
|
+
new Error("Failed to cancel session")
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ThreeDSecureCallbacks,
|
|
3
|
+
ThreeDSecureSessionsParams,
|
|
4
|
+
ThreeDSecureSession,
|
|
5
|
+
ThreeDSecureSessionResponse,
|
|
6
|
+
ThreeDSecureOptions,
|
|
7
|
+
} from "./types";
|
|
8
|
+
import { EV_API_DOMAIN } from "./config";
|
|
9
|
+
import { ThreeDSecureEvent } from "./event";
|
|
10
|
+
|
|
11
|
+
export function stopPolling(
|
|
12
|
+
intervalRef: React.MutableRefObject<NodeJS.Timeout | null>,
|
|
13
|
+
setIsVisible: (show: boolean) => void
|
|
14
|
+
) {
|
|
15
|
+
setIsVisible(false);
|
|
16
|
+
|
|
17
|
+
if (intervalRef.current) {
|
|
18
|
+
clearInterval(intervalRef.current);
|
|
19
|
+
intervalRef.current = null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function startSession(
|
|
24
|
+
session: ThreeDSecureSession,
|
|
25
|
+
options: ThreeDSecureOptions | undefined,
|
|
26
|
+
intervalRef: React.MutableRefObject<NodeJS.Timeout | null>,
|
|
27
|
+
setIsVisible: (show: boolean) => void
|
|
28
|
+
) {
|
|
29
|
+
try {
|
|
30
|
+
const sessionState = await session.get();
|
|
31
|
+
|
|
32
|
+
function fail() {
|
|
33
|
+
stopPolling(intervalRef, setIsVisible);
|
|
34
|
+
options?.onFailure?.(new Error("3DS session failed"));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
switch (sessionState.status) {
|
|
38
|
+
case "success": {
|
|
39
|
+
stopPolling(intervalRef, setIsVisible);
|
|
40
|
+
options?.onSuccess?.();
|
|
41
|
+
break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
case "failure": {
|
|
45
|
+
fail();
|
|
46
|
+
break;
|
|
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
|
+
|
|
66
|
+
setIsVisible(true);
|
|
67
|
+
pollSession(session, options, intervalRef, setIsVisible);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
} catch (error) {
|
|
71
|
+
console.error("Error checking session state", error);
|
|
72
|
+
options?.onError?.(new Error("Failed to check 3DS session state"));
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function pollSession(
|
|
77
|
+
session: ThreeDSecureSession,
|
|
78
|
+
options: ThreeDSecureOptions | undefined,
|
|
79
|
+
intervalRef: React.MutableRefObject<NodeJS.Timeout | null>,
|
|
80
|
+
setIsVisible: (show: boolean) => void,
|
|
81
|
+
interval = 3000
|
|
82
|
+
) {
|
|
83
|
+
function fail() {
|
|
84
|
+
stopPolling(intervalRef, setIsVisible);
|
|
85
|
+
options?.onFailure?.(new Error("3DS session failed"));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
intervalRef.current = setInterval(async () => {
|
|
89
|
+
try {
|
|
90
|
+
const pollResponse: ThreeDSecureSessionResponse = await session.get();
|
|
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
|
+
}
|
|
122
|
+
}
|
|
123
|
+
} catch (error) {
|
|
124
|
+
stopPolling(intervalRef, setIsVisible);
|
|
125
|
+
console.error("Error polling session", error);
|
|
126
|
+
options?.onError?.(new Error("Error polling 3DS session"));
|
|
127
|
+
}
|
|
128
|
+
}, interval);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export function threeDSecureSession({
|
|
132
|
+
sessionId,
|
|
133
|
+
appId,
|
|
134
|
+
options,
|
|
135
|
+
intervalRef,
|
|
136
|
+
setIsVisible,
|
|
137
|
+
}: ThreeDSecureSessionsParams): ThreeDSecureSession {
|
|
138
|
+
async function get(): Promise<ThreeDSecureSessionResponse> {
|
|
139
|
+
try {
|
|
140
|
+
const response = await fetch(
|
|
141
|
+
`https://${EV_API_DOMAIN}/frontend/3ds/browser-sessions/${sessionId}`,
|
|
142
|
+
{
|
|
143
|
+
headers: {
|
|
144
|
+
"x-evervault-app-id": appId,
|
|
145
|
+
},
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const result = (await response.json()) as ThreeDSecureSessionResponse;
|
|
150
|
+
return result;
|
|
151
|
+
} catch (error) {
|
|
152
|
+
console.error("Error fetching 3DS session status", error);
|
|
153
|
+
throw error;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function cancel(): Promise<void> {
|
|
158
|
+
try {
|
|
159
|
+
await fetch(
|
|
160
|
+
`https://${EV_API_DOMAIN}/frontend/3ds/browser-sessions/${sessionId}`,
|
|
161
|
+
{
|
|
162
|
+
method: "PATCH",
|
|
163
|
+
headers: {
|
|
164
|
+
"Content-Type": "application/json",
|
|
165
|
+
"x-evervault-app-id": appId,
|
|
166
|
+
},
|
|
167
|
+
body: JSON.stringify({ outcome: "cancelled" }),
|
|
168
|
+
}
|
|
169
|
+
);
|
|
170
|
+
|
|
171
|
+
options?.onFailure?.(new Error("3DS session cancelled by user"));
|
|
172
|
+
stopPolling(intervalRef, setIsVisible);
|
|
173
|
+
} catch (error) {
|
|
174
|
+
console.error("Error cancelling 3DS session", error);
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return {
|
|
180
|
+
sessionId,
|
|
181
|
+
get,
|
|
182
|
+
cancel,
|
|
183
|
+
};
|
|
184
|
+
}
|