@evervault/react-native 2.0.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.
Files changed (177) hide show
  1. package/README.md +63 -0
  2. package/android/app/build/generated/source/codegen/java/com/facebook/fbreact/specs/NativeEvervaultSpec.java +60 -0
  3. package/android/app/build/generated/source/codegen/java/com/facebook/fbreact/specs/NativeRNCWebViewModuleSpec.java +42 -0
  4. package/android/app/build/generated/source/codegen/java/com/facebook/react/viewmanagers/RNCWebViewManagerDelegate.java +294 -0
  5. package/android/app/build/generated/source/codegen/java/com/facebook/react/viewmanagers/RNCWebViewManagerInterface.java +104 -0
  6. package/android/app/build/generated/source/codegen/jni/CMakeLists.txt +36 -0
  7. package/android/app/build/generated/source/codegen/jni/NativeEvervaultSpec-generated.cpp +62 -0
  8. package/android/app/build/generated/source/codegen/jni/NativeEvervaultSpec.h +31 -0
  9. package/android/app/build/generated/source/codegen/jni/RNCWebViewSpec-generated.cpp +38 -0
  10. package/android/app/build/generated/source/codegen/jni/RNCWebViewSpec.h +31 -0
  11. package/android/app/build/generated/source/codegen/jni/react/renderer/components/NativeEvervaultSpec/NativeEvervaultSpecJSI-generated.cpp +68 -0
  12. package/android/app/build/generated/source/codegen/jni/react/renderer/components/NativeEvervaultSpec/NativeEvervaultSpecJSI.h +112 -0
  13. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/ComponentDescriptors.cpp +22 -0
  14. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/ComponentDescriptors.h +24 -0
  15. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/EventEmitters.cpp +241 -0
  16. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/EventEmitters.h +263 -0
  17. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/Props.cpp +99 -0
  18. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/Props.h +488 -0
  19. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/RNCWebViewSpecJSI-generated.cpp +35 -0
  20. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/RNCWebViewSpecJSI.h +76 -0
  21. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/ShadowNodes.cpp +17 -0
  22. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/ShadowNodes.h +32 -0
  23. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/States.cpp +16 -0
  24. package/android/app/build/generated/source/codegen/jni/react/renderer/components/RNCWebViewSpec/States.h +29 -0
  25. package/android/build.gradle +32 -0
  26. package/android/src/main/java/com/nativeevervault/EvervaultModule.kt +114 -0
  27. package/android/src/main/java/com/nativeevervault/EvervaultPackage.kt +29 -0
  28. package/build/Card/Cvc.d.ts +5 -0
  29. package/build/Card/Cvc.d.ts.map +1 -0
  30. package/build/Card/Cvc.test.d.ts +2 -0
  31. package/build/Card/Cvc.test.d.ts.map +1 -0
  32. package/build/Card/Expiry.d.ts +5 -0
  33. package/build/Card/Expiry.d.ts.map +1 -0
  34. package/build/Card/Holder.d.ts +5 -0
  35. package/build/Card/Holder.d.ts.map +1 -0
  36. package/build/Card/Number.d.ts +5 -0
  37. package/build/Card/Number.d.ts.map +1 -0
  38. package/build/Card/Number.test.d.ts +2 -0
  39. package/build/Card/Number.test.d.ts.map +1 -0
  40. package/build/Card/Root.d.ts +36 -0
  41. package/build/Card/Root.d.ts.map +1 -0
  42. package/build/Card/Root.test.d.ts +2 -0
  43. package/build/Card/Root.test.d.ts.map +1 -0
  44. package/build/Card/index.d.ts +23 -0
  45. package/build/Card/index.d.ts.map +1 -0
  46. package/build/Card/schema.d.ts +30 -0
  47. package/build/Card/schema.d.ts.map +1 -0
  48. package/build/Card/types.d.ts +31 -0
  49. package/build/Card/types.d.ts.map +1 -0
  50. package/build/Card/utils.d.ts +17 -0
  51. package/build/Card/utils.d.ts.map +1 -0
  52. package/build/Card/utils.test.d.ts +2 -0
  53. package/build/Card/utils.test.d.ts.map +1 -0
  54. package/build/EvervaultProvider.d.ts +7 -0
  55. package/build/EvervaultProvider.d.ts.map +1 -0
  56. package/build/EvervaultProvider.test.d.ts +2 -0
  57. package/build/EvervaultProvider.test.d.ts.map +1 -0
  58. package/build/Input.d.ts +14 -0
  59. package/build/Input.d.ts.map +1 -0
  60. package/build/Input.test.d.ts +2 -0
  61. package/build/Input.test.d.ts.map +1 -0
  62. package/build/ThreeDSecure/Frame.d.ts +6 -0
  63. package/build/ThreeDSecure/Frame.d.ts.map +1 -0
  64. package/build/ThreeDSecure/Frame.test.d.ts +2 -0
  65. package/build/ThreeDSecure/Frame.test.d.ts.map +1 -0
  66. package/build/ThreeDSecure/Root.d.ts +10 -0
  67. package/build/ThreeDSecure/Root.d.ts.map +1 -0
  68. package/build/ThreeDSecure/Root.test.d.ts +2 -0
  69. package/build/ThreeDSecure/Root.test.d.ts.map +1 -0
  70. package/build/ThreeDSecure/config.d.ts +3 -0
  71. package/build/ThreeDSecure/config.d.ts.map +1 -0
  72. package/build/ThreeDSecure/context.d.ts +3 -0
  73. package/build/ThreeDSecure/context.d.ts.map +1 -0
  74. package/build/ThreeDSecure/index.d.ts +10 -0
  75. package/build/ThreeDSecure/index.d.ts.map +1 -0
  76. package/build/ThreeDSecure/session.d.ts +6 -0
  77. package/build/ThreeDSecure/session.d.ts.map +1 -0
  78. package/build/ThreeDSecure/session.test.d.ts +2 -0
  79. package/build/ThreeDSecure/session.test.d.ts.map +1 -0
  80. package/build/ThreeDSecure/types.d.ts +57 -0
  81. package/build/ThreeDSecure/types.d.ts.map +1 -0
  82. package/build/ThreeDSecure/useThreeDSecure.d.ts +3 -0
  83. package/build/ThreeDSecure/useThreeDSecure.d.ts.map +1 -0
  84. package/build/ThreeDSecure/useThreeDSecure.test.d.ts +2 -0
  85. package/build/ThreeDSecure/useThreeDSecure.test.d.ts.map +1 -0
  86. package/build/__mocks__/NativeEvervault.d.ts +4 -0
  87. package/build/__mocks__/NativeEvervault.d.ts.map +1 -0
  88. package/build/__mocks__/react-native-webview.d.ts +3 -0
  89. package/build/__mocks__/react-native-webview.d.ts.map +1 -0
  90. package/build/context.d.ts +9 -0
  91. package/build/context.d.ts.map +1 -0
  92. package/build/generated/ios/FBReactNativeSpec/FBReactNativeSpec-generated.mm +2321 -0
  93. package/build/generated/ios/FBReactNativeSpec/FBReactNativeSpec.h +2761 -0
  94. package/build/generated/ios/FBReactNativeSpecJSI-generated.cpp +2923 -0
  95. package/build/generated/ios/FBReactNativeSpecJSI.h +7718 -0
  96. package/build/generated/ios/NativeEvervaultSpec/NativeEvervaultSpec-generated.mm +74 -0
  97. package/build/generated/ios/NativeEvervaultSpec/NativeEvervaultSpec.h +80 -0
  98. package/build/generated/ios/NativeEvervaultSpecJSI-generated.cpp +68 -0
  99. package/build/generated/ios/NativeEvervaultSpecJSI.h +112 -0
  100. package/build/generated/ios/RCTModulesConformingToProtocolsProvider.h +18 -0
  101. package/build/generated/ios/RCTModulesConformingToProtocolsProvider.mm +33 -0
  102. package/build/generated/ios/RNCWebViewSpec/RNCWebViewSpec-generated.mm +46 -0
  103. package/build/generated/ios/RNCWebViewSpec/RNCWebViewSpec.h +62 -0
  104. package/build/generated/ios/RNCWebViewSpecJSI-generated.cpp +35 -0
  105. package/build/generated/ios/RNCWebViewSpecJSI.h +76 -0
  106. package/build/generated/ios/react/renderer/components/RNCWebViewSpec/ComponentDescriptors.cpp +22 -0
  107. package/build/generated/ios/react/renderer/components/RNCWebViewSpec/ComponentDescriptors.h +24 -0
  108. package/build/generated/ios/react/renderer/components/RNCWebViewSpec/EventEmitters.cpp +241 -0
  109. package/build/generated/ios/react/renderer/components/RNCWebViewSpec/EventEmitters.h +263 -0
  110. package/build/generated/ios/react/renderer/components/RNCWebViewSpec/Props.cpp +99 -0
  111. package/build/generated/ios/react/renderer/components/RNCWebViewSpec/Props.h +488 -0
  112. package/build/generated/ios/react/renderer/components/RNCWebViewSpec/RCTComponentViewHelpers.h +218 -0
  113. package/build/generated/ios/react/renderer/components/RNCWebViewSpec/ShadowNodes.cpp +17 -0
  114. package/build/generated/ios/react/renderer/components/RNCWebViewSpec/ShadowNodes.h +32 -0
  115. package/build/generated/ios/react/renderer/components/RNCWebViewSpec/States.cpp +16 -0
  116. package/build/generated/ios/react/renderer/components/RNCWebViewSpec/States.h +29 -0
  117. package/build/index.cjs.js +7732 -0
  118. package/build/index.cjs.js.map +1 -0
  119. package/build/index.d.ts +9 -0
  120. package/build/index.d.ts.map +1 -0
  121. package/build/index.esm.js +7702 -0
  122. package/build/sdk.d.ts +9 -0
  123. package/build/sdk.d.ts.map +1 -0
  124. package/build/sdk.test.d.ts +2 -0
  125. package/build/sdk.test.d.ts.map +1 -0
  126. package/build/specs/NativeEvervault.d.ts +59 -0
  127. package/build/specs/NativeEvervault.d.ts.map +1 -0
  128. package/build/useEvervault.d.ts +2 -0
  129. package/build/useEvervault.d.ts.map +1 -0
  130. package/build/useEvervault.test.d.ts +2 -0
  131. package/build/useEvervault.test.d.ts.map +1 -0
  132. package/build/utils.d.ts +15 -0
  133. package/build/utils.d.ts.map +1 -0
  134. package/ios/NativeEvervault-Bridging-Header.h +2 -0
  135. package/ios/NativeEvervault.mm +38 -0
  136. package/ios/NativeEvervault.swift +62 -0
  137. package/native-evervault.podspec +20 -0
  138. package/package.json +85 -0
  139. package/src/Card/Cvc.test.tsx +41 -0
  140. package/src/Card/Cvc.tsx +51 -0
  141. package/src/Card/Expiry.tsx +26 -0
  142. package/src/Card/Holder.tsx +27 -0
  143. package/src/Card/Number.test.tsx +55 -0
  144. package/src/Card/Number.tsx +47 -0
  145. package/src/Card/Root.test.tsx +260 -0
  146. package/src/Card/Root.tsx +118 -0
  147. package/src/Card/index.ts +28 -0
  148. package/src/Card/schema.ts +51 -0
  149. package/src/Card/types.ts +50 -0
  150. package/src/Card/utils.test.ts +271 -0
  151. package/src/Card/utils.ts +127 -0
  152. package/src/EvervaultProvider.test.tsx +24 -0
  153. package/src/EvervaultProvider.tsx +43 -0
  154. package/src/Input.test.tsx +138 -0
  155. package/src/Input.tsx +136 -0
  156. package/src/ThreeDSecure/Frame.test.tsx +87 -0
  157. package/src/ThreeDSecure/Frame.tsx +50 -0
  158. package/src/ThreeDSecure/Root.test.tsx +67 -0
  159. package/src/ThreeDSecure/Root.tsx +23 -0
  160. package/src/ThreeDSecure/config.ts +3 -0
  161. package/src/ThreeDSecure/context.ts +6 -0
  162. package/src/ThreeDSecure/index.ts +17 -0
  163. package/src/ThreeDSecure/session.test.ts +329 -0
  164. package/src/ThreeDSecure/session.ts +132 -0
  165. package/src/ThreeDSecure/types.ts +67 -0
  166. package/src/ThreeDSecure/useThreeDSecure.test.tsx +133 -0
  167. package/src/ThreeDSecure/useThreeDSecure.ts +47 -0
  168. package/src/__mocks__/NativeEvervault.ts +13 -0
  169. package/src/__mocks__/react-native-webview.tsx +6 -0
  170. package/src/context.ts +14 -0
  171. package/src/index.ts +21 -0
  172. package/src/sdk.test.ts +122 -0
  173. package/src/sdk.ts +71 -0
  174. package/src/specs/NativeEvervault.ts +67 -0
  175. package/src/useEvervault.test.tsx +31 -0
  176. package/src/useEvervault.ts +14 -0
  177. package/src/utils.ts +41 -0
@@ -0,0 +1,329 @@
1
+ import { EV_API_DOMAIN } from "./config";
2
+ import {
3
+ pollSession,
4
+ startSession,
5
+ stopPolling,
6
+ threeDSecureSession,
7
+ } from "./session";
8
+ import { ThreeDSecureSession } from "./types";
9
+
10
+ describe("stopPolling", () => {
11
+ it("should stop polling", () => {
12
+ const spy = vi.spyOn(global, "clearInterval");
13
+
14
+ const interval = setTimeout(() => {}, 0);
15
+ const intervalRef = { current: interval };
16
+ const setIsVisible = vi.fn();
17
+
18
+ stopPolling(intervalRef, setIsVisible);
19
+
20
+ expect(setIsVisible).toHaveBeenCalledWith(false);
21
+ expect(spy).toHaveBeenCalledWith(interval);
22
+ expect(intervalRef.current).toBeNull();
23
+ });
24
+ });
25
+
26
+ describe("startSession", () => {
27
+ const callbacks = {
28
+ onSuccess: vi.fn(),
29
+ onFailure: vi.fn(),
30
+ onError: vi.fn(),
31
+ };
32
+ const intervalRef = { current: null };
33
+ const setIsVisible = vi.fn();
34
+
35
+ it("should start a successful session", async () => {
36
+ const session: ThreeDSecureSession = {
37
+ cancel: vi.fn(),
38
+ get: vi.fn(() => Promise.resolve({ status: "success" }) as any),
39
+ sessionId: "123",
40
+ };
41
+
42
+ await startSession(session, callbacks, intervalRef, setIsVisible);
43
+
44
+ expect(session.get).toHaveBeenCalled();
45
+ expect(callbacks.onSuccess).toHaveBeenCalled();
46
+ });
47
+
48
+ it("should start a failed session", async () => {
49
+ const session: ThreeDSecureSession = {
50
+ cancel: vi.fn(),
51
+ get: vi.fn(() => Promise.resolve({ status: "failure" }) as any),
52
+ sessionId: "123",
53
+ };
54
+
55
+ await startSession(session, callbacks, intervalRef, setIsVisible);
56
+
57
+ expect(session.get).toHaveBeenCalled();
58
+ expect(callbacks.onFailure).toHaveBeenCalledWith(
59
+ new Error("3DS session failed")
60
+ );
61
+ });
62
+
63
+ it("should start a session that requires action", async () => {
64
+ const session: ThreeDSecureSession = {
65
+ cancel: vi.fn(),
66
+ get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
67
+ sessionId: "123",
68
+ };
69
+
70
+ await startSession(session, callbacks, intervalRef, setIsVisible);
71
+
72
+ expect(session.get).toHaveBeenCalled();
73
+ expect(setIsVisible).toHaveBeenCalledWith(true);
74
+ });
75
+
76
+ it("should call onError if the session fails to start", async () => {
77
+ const session: ThreeDSecureSession = {
78
+ cancel: vi.fn(),
79
+ get: vi.fn(() => Promise.reject(new Error("Failed to start session"))),
80
+ sessionId: "123",
81
+ };
82
+
83
+ const consoleErrorSpy = vi
84
+ .spyOn(console, "error")
85
+ .mockImplementation(() => {});
86
+
87
+ await startSession(session, callbacks, intervalRef, setIsVisible);
88
+
89
+ expect(session.get).toHaveBeenCalled();
90
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
91
+ "Error checking session state",
92
+ new Error("Failed to start session")
93
+ );
94
+ expect(callbacks.onError).toHaveBeenCalledWith(
95
+ new Error("Failed to check 3DS session state")
96
+ );
97
+ });
98
+ });
99
+
100
+ describe("pollSession", () => {
101
+ const callbacks = {
102
+ onSuccess: vi.fn(),
103
+ onFailure: vi.fn(),
104
+ onError: vi.fn(),
105
+ };
106
+ const intervalRef = { current: null };
107
+ const setIsVisible = vi.fn();
108
+
109
+ it("should start an interval", () => {
110
+ const session: ThreeDSecureSession = {
111
+ cancel: vi.fn(),
112
+ get: vi.fn(() => Promise.resolve({ status: "success" }) as any),
113
+ sessionId: "123",
114
+ };
115
+
116
+ const intervalSpy = vi.spyOn(global, "setInterval");
117
+ pollSession(session, callbacks, intervalRef, setIsVisible);
118
+
119
+ expect(intervalSpy).toHaveBeenCalledWith(expect.any(Function), 3000);
120
+ });
121
+
122
+ it("should poll successful session", async () => {
123
+ const session: ThreeDSecureSession = {
124
+ cancel: vi.fn(),
125
+ get: vi.fn(() => Promise.resolve({ status: "success" }) as any),
126
+ sessionId: "123",
127
+ };
128
+
129
+ const intervalSpy = vi.spyOn(global, "setInterval");
130
+ pollSession(session, callbacks, intervalRef, setIsVisible);
131
+ await intervalSpy.mock.calls[0][0]();
132
+
133
+ expect(session.get).toHaveBeenCalled();
134
+ expect(callbacks.onSuccess).toHaveBeenCalled();
135
+ });
136
+
137
+ it("should poll failed session", async () => {
138
+ const session: ThreeDSecureSession = {
139
+ cancel: vi.fn(),
140
+ get: vi.fn(() => Promise.resolve({ status: "failure" }) as any),
141
+ sessionId: "123",
142
+ };
143
+
144
+ const intervalSpy = vi.spyOn(global, "setInterval");
145
+ pollSession(session, callbacks, intervalRef, setIsVisible);
146
+ await intervalSpy.mock.calls[0][0]();
147
+
148
+ expect(session.get).toHaveBeenCalled();
149
+ expect(callbacks.onFailure).toHaveBeenCalledWith(
150
+ new Error("3DS session failed")
151
+ );
152
+ });
153
+
154
+ it("should poll session that requires action", async () => {
155
+ const session: ThreeDSecureSession = {
156
+ cancel: vi.fn(),
157
+ get: vi.fn(() => Promise.resolve({ status: "action-required" }) as any),
158
+ sessionId: "123",
159
+ };
160
+
161
+ const intervalSpy = vi.spyOn(global, "setInterval");
162
+ pollSession(session, callbacks, intervalRef, setIsVisible);
163
+ await intervalSpy.mock.calls[0][0]();
164
+
165
+ expect(session.get).toHaveBeenCalled();
166
+ expect(setIsVisible).toHaveBeenCalledWith(true);
167
+ });
168
+
169
+ it("should call onError if the session fails to poll", async () => {
170
+ const session: ThreeDSecureSession = {
171
+ cancel: vi.fn(),
172
+ get: vi.fn(() => Promise.reject(new Error("Failed to poll session"))),
173
+ sessionId: "123",
174
+ };
175
+
176
+ const consoleErrorSpy = vi
177
+ .spyOn(console, "error")
178
+ .mockImplementation(() => {});
179
+
180
+ const intervalSpy = vi.spyOn(global, "setInterval");
181
+ pollSession(session, callbacks, intervalRef, setIsVisible);
182
+ await intervalSpy.mock.calls[0][0]();
183
+
184
+ expect(session.get).toHaveBeenCalled();
185
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
186
+ "Error polling session",
187
+ new Error("Failed to poll session")
188
+ );
189
+ expect(callbacks.onError).toHaveBeenCalledWith(
190
+ new Error("Error polling 3DS session")
191
+ );
192
+ });
193
+ });
194
+
195
+ describe("threeDSecureSession", () => {
196
+ const callbacks = {
197
+ onSuccess: vi.fn(),
198
+ onFailure: vi.fn(),
199
+ onError: vi.fn(),
200
+ };
201
+ const intervalRef = { current: null };
202
+ const setIsVisible = vi.fn();
203
+
204
+ it("should create a session", () => {
205
+ const session = threeDSecureSession({
206
+ sessionId: "123",
207
+ appId: "app_123",
208
+ callbacks,
209
+ intervalRef,
210
+ setIsVisible,
211
+ });
212
+
213
+ expect(session).toBeDefined();
214
+ expect(session.sessionId).toBe("123");
215
+ expect(session.get).toBeInstanceOf(Function);
216
+ expect(session.cancel).toBeInstanceOf(Function);
217
+ });
218
+
219
+ it("creates a get function that fetches the session", async () => {
220
+ const session = threeDSecureSession({
221
+ sessionId: "123",
222
+ appId: "app_123",
223
+ callbacks,
224
+ intervalRef,
225
+ setIsVisible,
226
+ });
227
+
228
+ const fetchSpy = vi.spyOn(global, "fetch");
229
+ fetchSpy.mockImplementation(
230
+ () =>
231
+ Promise.resolve({
232
+ json: () => Promise.resolve({ status: "success" }),
233
+ }) as any
234
+ );
235
+
236
+ const result = await session.get();
237
+ expect(fetchSpy).toHaveBeenCalledWith(
238
+ `https://${EV_API_DOMAIN}/frontend/3ds/browser-sessions/123`,
239
+ {
240
+ headers: {
241
+ "x-evervault-app-id": "app_123",
242
+ },
243
+ }
244
+ );
245
+
246
+ expect(result).toEqual({ status: "success" });
247
+ });
248
+
249
+ it("creates a get function that throws an error if the session fails to fetch", async () => {
250
+ const session = threeDSecureSession({
251
+ sessionId: "123",
252
+ appId: "app_123",
253
+ callbacks,
254
+ intervalRef,
255
+ setIsVisible,
256
+ });
257
+
258
+ const fetchSpy = vi.spyOn(global, "fetch");
259
+ fetchSpy.mockImplementation(() =>
260
+ Promise.reject(new Error("Failed to fetch session"))
261
+ );
262
+
263
+ const consoleErrorSpy = vi
264
+ .spyOn(console, "error")
265
+ .mockImplementation(() => {});
266
+
267
+ await expect(session.get()).rejects.toThrow("Failed to fetch session");
268
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
269
+ "Error fetching 3DS session status",
270
+ new Error("Failed to fetch session")
271
+ );
272
+ });
273
+
274
+ it("creates a cancel function that cancels the session", async () => {
275
+ const session = threeDSecureSession({
276
+ sessionId: "123",
277
+ appId: "app_123",
278
+ callbacks,
279
+ intervalRef,
280
+ setIsVisible,
281
+ });
282
+
283
+ const fetchSpy = vi.spyOn(global, "fetch");
284
+ fetchSpy.mockImplementation(() => Promise.resolve({}) as any);
285
+
286
+ await session.cancel();
287
+
288
+ expect(fetchSpy).toHaveBeenCalledWith(
289
+ `https://${EV_API_DOMAIN}/frontend/3ds/browser-sessions/123`,
290
+ {
291
+ method: "PATCH",
292
+ headers: {
293
+ "Content-Type": "application/json",
294
+ "x-evervault-app-id": "app_123",
295
+ },
296
+ body: JSON.stringify({ outcome: "cancelled" }),
297
+ }
298
+ );
299
+
300
+ expect(callbacks.onFailure).toHaveBeenCalledWith(
301
+ new Error("3DS session cancelled by user")
302
+ );
303
+ });
304
+
305
+ it("creates a cancel function that throws an error if the session fails to cancel", async () => {
306
+ const session = threeDSecureSession({
307
+ sessionId: "123",
308
+ appId: "app_123",
309
+ callbacks,
310
+ intervalRef,
311
+ setIsVisible,
312
+ });
313
+
314
+ const fetchSpy = vi.spyOn(global, "fetch");
315
+ fetchSpy.mockImplementation(() =>
316
+ Promise.reject(new Error("Failed to cancel session"))
317
+ );
318
+
319
+ const consoleErrorSpy = vi
320
+ .spyOn(console, "error")
321
+ .mockImplementation(() => {});
322
+
323
+ await expect(session.cancel()).rejects.toThrow("Failed to cancel session");
324
+ expect(consoleErrorSpy).toHaveBeenCalledWith(
325
+ "Error cancelling 3DS session",
326
+ new Error("Failed to cancel session")
327
+ );
328
+ });
329
+ });
@@ -0,0 +1,132 @@
1
+ import {
2
+ ThreeDSecureCallbacks,
3
+ ThreeDSecureSessionsParams,
4
+ ThreeDSecureSession,
5
+ ThreeDSecureSessionResponse,
6
+ } from "./types";
7
+ import { EV_API_DOMAIN } from "./config";
8
+
9
+ export function stopPolling(
10
+ intervalRef: React.MutableRefObject<NodeJS.Timeout | null>,
11
+ setIsVisible: (show: boolean) => void
12
+ ) {
13
+ setIsVisible(false);
14
+
15
+ if (intervalRef.current) {
16
+ clearInterval(intervalRef.current);
17
+ intervalRef.current = null;
18
+ }
19
+ }
20
+
21
+ export async function startSession(
22
+ session: ThreeDSecureSession,
23
+ callbacks: ThreeDSecureCallbacks | undefined,
24
+ intervalRef: React.MutableRefObject<NodeJS.Timeout | null>,
25
+ setIsVisible: (show: boolean) => void
26
+ ) {
27
+ try {
28
+ const sessionState = await session.get();
29
+
30
+ switch (sessionState.status) {
31
+ case "success":
32
+ stopPolling(intervalRef, setIsVisible);
33
+ callbacks?.onSuccess?.();
34
+ break;
35
+ case "failure":
36
+ stopPolling(intervalRef, setIsVisible);
37
+ callbacks?.onFailure?.(new Error("3DS session failed"));
38
+ break;
39
+ case "action-required":
40
+ setIsVisible(true);
41
+ pollSession(session, callbacks, intervalRef, setIsVisible);
42
+ break;
43
+ default:
44
+ break;
45
+ }
46
+ } catch (error) {
47
+ console.error("Error checking session state", error);
48
+ callbacks?.onError?.(new Error("Failed to check 3DS session state"));
49
+ }
50
+ }
51
+
52
+ export function pollSession(
53
+ session: ThreeDSecureSession,
54
+ callbacks: ThreeDSecureCallbacks | undefined,
55
+ intervalRef: React.MutableRefObject<NodeJS.Timeout | null>,
56
+ setIsVisible: (show: boolean) => void,
57
+ interval = 3000
58
+ ) {
59
+ intervalRef.current = setInterval(async () => {
60
+ try {
61
+ 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);
70
+ }
71
+ } catch (error) {
72
+ stopPolling(intervalRef, setIsVisible);
73
+ console.error("Error polling session", error);
74
+ callbacks?.onError?.(new Error("Error polling 3DS session"));
75
+ }
76
+ }, interval);
77
+ }
78
+
79
+ export function threeDSecureSession({
80
+ sessionId,
81
+ appId,
82
+ callbacks,
83
+ intervalRef,
84
+ setIsVisible,
85
+ }: ThreeDSecureSessionsParams): ThreeDSecureSession {
86
+ async function get(): Promise<ThreeDSecureSessionResponse> {
87
+ try {
88
+ const response = await fetch(
89
+ `https://${EV_API_DOMAIN}/frontend/3ds/browser-sessions/${sessionId}`,
90
+ {
91
+ headers: {
92
+ "x-evervault-app-id": appId,
93
+ },
94
+ }
95
+ );
96
+
97
+ const result = (await response.json()) as ThreeDSecureSessionResponse;
98
+ return result;
99
+ } catch (error) {
100
+ console.error("Error fetching 3DS session status", error);
101
+ throw error;
102
+ }
103
+ }
104
+
105
+ async function cancel(): Promise<void> {
106
+ try {
107
+ await fetch(
108
+ `https://${EV_API_DOMAIN}/frontend/3ds/browser-sessions/${sessionId}`,
109
+ {
110
+ method: "PATCH",
111
+ headers: {
112
+ "Content-Type": "application/json",
113
+ "x-evervault-app-id": appId,
114
+ },
115
+ body: JSON.stringify({ outcome: "cancelled" }),
116
+ }
117
+ );
118
+
119
+ callbacks?.onFailure?.(new Error("3DS session cancelled by user"));
120
+ stopPolling(intervalRef, setIsVisible);
121
+ } catch (error) {
122
+ console.error("Error cancelling 3DS session", error);
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ return {
128
+ sessionId,
129
+ get,
130
+ cancel,
131
+ };
132
+ }
@@ -0,0 +1,67 @@
1
+ import { PropsWithChildren } from "react";
2
+
3
+ export interface ThreeDSecureCallbacks {
4
+ /**
5
+ * The error event will be fired if the component fails to load.
6
+ */
7
+ onError?(error: Error): void;
8
+
9
+ /**
10
+ * 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
+ * If the user cancels the 3DS authentication process this event will be fired.
12
+ */
13
+ onFailure?(error: Error): void;
14
+
15
+ /**
16
+ * The 'success' event will be fired once the 3DS authentication process has been completed successfully.
17
+ * You should use this event to trigger your backend to finalize the payment.
18
+ * Your backend can use the [Retrieve 3DS Session](https://docs.evervault.com/api-reference#retrieveThreeDSSession) endpoint to retrieve the cryptogram for the session and complete the payment.
19
+ */
20
+ onSuccess?(): void;
21
+ }
22
+
23
+ export interface ThreeDSecureInitialState {
24
+ session: ThreeDSecureSession | null;
25
+ isVisible: boolean;
26
+ }
27
+
28
+ export interface ThreeDSecureSession {
29
+ cancel(): Promise<void>;
30
+ get(): Promise<ThreeDSecureSessionResponse>;
31
+ sessionId: string;
32
+ }
33
+
34
+ export type SessionStatus = "action-required" | "failure" | "success";
35
+
36
+ export interface ThreeDSecureSessionResponse {
37
+ nextAction: {
38
+ creq?: string;
39
+ type: string;
40
+ url?: string;
41
+ };
42
+ status: SessionStatus;
43
+ }
44
+
45
+ export interface ThreeDSecureSessionsParams {
46
+ appId: string;
47
+ callbacks?: ThreeDSecureCallbacks;
48
+ intervalRef: React.MutableRefObject<NodeJS.Timeout | null>;
49
+ sessionId: string;
50
+ setIsVisible: (show: boolean) => void;
51
+ }
52
+
53
+ export interface ThreeDSecureState extends ThreeDSecureInitialState {
54
+ /**
55
+ * The `cancel()` function is used to cancel the ongoing 3DS authentication process.
56
+ * This can be particularly useful for canceling a session when a custom cancel button is triggered.
57
+ */
58
+ cancel(): Promise<void>;
59
+
60
+ /**
61
+ * The `start()` function is used to kick off the 3DS authentication process.
62
+ *
63
+ * @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.
65
+ */
66
+ start(sessionId: string, callbacks?: ThreeDSecureCallbacks): void;
67
+ }
@@ -0,0 +1,133 @@
1
+ import { PropsWithChildren } from "react";
2
+ import { EvervaultProvider } from "../EvervaultProvider";
3
+ import { act, renderHook } from "@testing-library/react-native";
4
+ import { useThreeDSecure } from "./useThreeDSecure";
5
+
6
+ function wrapper({ children }: PropsWithChildren) {
7
+ return (
8
+ <EvervaultProvider teamId="team_123" appId="app_123">
9
+ {children}
10
+ </EvervaultProvider>
11
+ );
12
+ }
13
+
14
+ const callbacks = {
15
+ onSuccess: vi.fn(),
16
+ onError: vi.fn(),
17
+ onFailure: vi.fn(),
18
+ };
19
+
20
+ it("returns the correct state", () => {
21
+ const { result } = renderHook(() => useThreeDSecure(), {
22
+ wrapper,
23
+ });
24
+
25
+ expect(result.current).toEqual({
26
+ start: expect.any(Function),
27
+ cancel: expect.any(Function),
28
+ session: null,
29
+ isVisible: false,
30
+ });
31
+ });
32
+
33
+ it("starts a session when action is required", async () => {
34
+ const { result, rerender } = renderHook(() => useThreeDSecure(), {
35
+ wrapper,
36
+ });
37
+
38
+ vi.spyOn(global, "fetch").mockResolvedValue({
39
+ json: () => Promise.resolve({ status: "action-required" }),
40
+ } as any);
41
+
42
+ await act(() => result.current.start("session_123", callbacks));
43
+
44
+ expect(result.current.session).toEqual({
45
+ sessionId: "session_123",
46
+ cancel: expect.any(Function),
47
+ get: expect.any(Function),
48
+ });
49
+ expect(result.current.isVisible).toBe(true);
50
+ });
51
+
52
+ it("calls the success callback when the session is successful", async () => {
53
+ const { result } = renderHook(() => useThreeDSecure(), {
54
+ wrapper,
55
+ });
56
+
57
+ vi.spyOn(global, "fetch").mockResolvedValue({
58
+ json: () => Promise.resolve({ status: "success" }),
59
+ } as any);
60
+
61
+ await act(() => result.current.start("session_123", callbacks));
62
+
63
+ expect(callbacks.onSuccess).toHaveBeenCalled();
64
+ });
65
+
66
+ it("calls the failure callback when the session fails on start", async () => {
67
+ const { result } = renderHook(() => useThreeDSecure(), {
68
+ wrapper,
69
+ });
70
+
71
+ vi.spyOn(global, "fetch").mockResolvedValue({
72
+ json: () => Promise.resolve({ status: "failure" }),
73
+ } as any);
74
+
75
+ await act(() => result.current.start("session_123", callbacks));
76
+
77
+ expect(callbacks.onFailure).toHaveBeenCalled();
78
+ });
79
+
80
+ it("calls the error callback when the fetch fails on start", async () => {
81
+ const { result } = renderHook(() => useThreeDSecure(), {
82
+ wrapper,
83
+ });
84
+
85
+ vi.spyOn(global, "fetch").mockRejectedValue(new Error("Session failed"));
86
+
87
+ await act(() => result.current.start("session_123", callbacks));
88
+
89
+ expect(callbacks.onError).toHaveBeenCalled();
90
+ });
91
+
92
+ it("silently fails if the session is not found", async () => {
93
+ const { result } = renderHook(() => useThreeDSecure(), {
94
+ wrapper,
95
+ });
96
+
97
+ const consoleWarnSpy = vi.spyOn(console, "warn");
98
+
99
+ await act(() => result.current.cancel());
100
+
101
+ expect(result.current.session).toBeNull();
102
+ expect(result.current.isVisible).toBe(false);
103
+ expect(consoleWarnSpy).toHaveBeenCalledWith("No 3DS session to cancel");
104
+ });
105
+
106
+ it("cancels the session when the user cancels", async () => {
107
+ const { result } = renderHook(() => useThreeDSecure(), {
108
+ wrapper,
109
+ });
110
+
111
+ vi.spyOn(global, "fetch").mockResolvedValue({
112
+ json: () => Promise.resolve({ status: "action-required" }),
113
+ } as any);
114
+
115
+ await act(() => result.current.start("session_123", callbacks));
116
+
117
+ expect(result.current.session).toEqual({
118
+ sessionId: "session_123",
119
+ cancel: expect.any(Function),
120
+ get: expect.any(Function),
121
+ });
122
+ expect(result.current.isVisible).toBe(true);
123
+
124
+ await act(() => result.current.session?.cancel());
125
+
126
+ expect(result.current.session).toEqual({
127
+ sessionId: "session_123",
128
+ cancel: expect.any(Function),
129
+ get: expect.any(Function),
130
+ });
131
+ expect(result.current.isVisible).toBe(false);
132
+ expect(callbacks.onFailure).toHaveBeenCalled();
133
+ });
@@ -0,0 +1,47 @@
1
+ import { useCallback, useMemo, useState } from "react";
2
+ import { useRef } from "react";
3
+ import { startSession, threeDSecureSession } from "./session";
4
+ import { ThreeDSecureSession, ThreeDSecureState } from "./types";
5
+ import { useEvervault } from "../useEvervault";
6
+
7
+ export function useThreeDSecure(): ThreeDSecureState {
8
+ const { appId } = useEvervault();
9
+ const intervalRef = useRef<NodeJS.Timeout | null>(null);
10
+ const [session, setSession] = useState<ThreeDSecureSession | null>(null);
11
+ const [isVisible, setIsVisible] = useState(false);
12
+
13
+ const start = useCallback<ThreeDSecureState["start"]>(
14
+ (sessionId, callbacks) => {
15
+ const session = threeDSecureSession({
16
+ sessionId,
17
+ appId,
18
+ callbacks,
19
+ intervalRef,
20
+ setIsVisible,
21
+ });
22
+
23
+ setSession(session);
24
+
25
+ startSession(session, callbacks, intervalRef, setIsVisible);
26
+ },
27
+ [appId]
28
+ );
29
+
30
+ const cancel = useCallback<ThreeDSecureState["cancel"]>(async () => {
31
+ if (session) {
32
+ await session.cancel();
33
+ } else {
34
+ console.warn("No 3DS session to cancel");
35
+ }
36
+ }, [session]);
37
+
38
+ return useMemo(
39
+ () => ({
40
+ start,
41
+ cancel,
42
+ session,
43
+ isVisible,
44
+ }),
45
+ [start, cancel, session, isVisible]
46
+ );
47
+ }