@solucx/react-native-solucx-widget 0.2.4 → 2.0.7

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 (256) hide show
  1. package/README.md +526 -182
  2. package/lib/SoluCXWidget.d.ts +50 -7
  3. package/lib/SoluCXWidget.d.ts.map +1 -1
  4. package/lib/SoluCXWidget.js +105 -100
  5. package/lib/SoluCXWidget.js.map +1 -1
  6. package/lib/SoluCXWidgetHost.d.ts +3 -0
  7. package/lib/SoluCXWidgetHost.d.ts.map +1 -0
  8. package/lib/SoluCXWidgetHost.js +34 -0
  9. package/lib/SoluCXWidgetHost.js.map +1 -0
  10. package/lib/SoluCXWidgetView.d.ts +12 -0
  11. package/lib/SoluCXWidgetView.d.ts.map +1 -0
  12. package/lib/SoluCXWidgetView.js +61 -0
  13. package/lib/SoluCXWidgetView.js.map +1 -0
  14. package/lib/components/CloseButton.d.ts +1 -1
  15. package/lib/components/CloseButton.d.ts.map +1 -1
  16. package/lib/components/CloseButton.js +4 -1
  17. package/lib/components/CloseButton.js.map +1 -1
  18. package/lib/components/InlineWidget.d.ts.map +1 -1
  19. package/lib/components/InlineWidget.js +2 -7
  20. package/lib/components/InlineWidget.js.map +1 -1
  21. package/lib/components/ModalWidget.d.ts +1 -1
  22. package/lib/components/ModalWidget.d.ts.map +1 -1
  23. package/lib/components/ModalWidget.js +3 -16
  24. package/lib/components/ModalWidget.js.map +1 -1
  25. package/lib/components/OverlayWidget.d.ts.map +1 -1
  26. package/lib/components/OverlayWidget.js +5 -15
  27. package/lib/components/OverlayWidget.js.map +1 -1
  28. package/lib/components/index.d.ts +5 -0
  29. package/lib/components/index.d.ts.map +1 -0
  30. package/lib/components/index.js +12 -0
  31. package/lib/components/index.js.map +1 -0
  32. package/lib/constants/Constants.d.ts +11 -0
  33. package/lib/constants/Constants.d.ts.map +1 -1
  34. package/lib/constants/Constants.js +16 -1
  35. package/lib/constants/Constants.js.map +1 -1
  36. package/lib/{interfaces → domain}/WidgetCallbacks.d.ts +2 -2
  37. package/lib/domain/WidgetCallbacks.d.ts.map +1 -0
  38. package/lib/domain/WidgetCallbacks.js.map +1 -0
  39. package/{src/interfaces/WidgetData.ts → lib/domain/WidgetData.d.ts} +5 -2
  40. package/lib/domain/WidgetData.d.ts.map +1 -0
  41. package/lib/{interfaces → domain}/WidgetData.js.map +1 -1
  42. package/lib/domain/WidgetDisplayResult.d.ts +6 -0
  43. package/lib/domain/WidgetDisplayResult.d.ts.map +1 -0
  44. package/lib/domain/WidgetDisplayResult.js +3 -0
  45. package/lib/domain/WidgetDisplayResult.js.map +1 -0
  46. package/lib/domain/WidgetOptions.d.ts +27 -0
  47. package/lib/domain/WidgetOptions.d.ts.map +1 -0
  48. package/lib/domain/WidgetOptions.js +30 -0
  49. package/lib/domain/WidgetOptions.js.map +1 -0
  50. package/lib/domain/WidgetResponse.d.ts +5 -0
  51. package/lib/domain/WidgetResponse.d.ts.map +1 -0
  52. package/lib/{interfaces/WidgetOptions.js → domain/WidgetResponse.js} +1 -1
  53. package/lib/domain/WidgetResponse.js.map +1 -0
  54. package/lib/domain/WidgetSamplerLog.d.ts +12 -0
  55. package/lib/domain/WidgetSamplerLog.d.ts.map +1 -0
  56. package/lib/domain/WidgetSamplerLog.js.map +1 -0
  57. package/lib/{interfaces → domain}/index.d.ts +1 -2
  58. package/lib/domain/index.d.ts.map +1 -0
  59. package/lib/{interfaces → domain}/index.js.map +1 -1
  60. package/lib/hooks/index.d.ts +2 -2
  61. package/lib/hooks/index.d.ts.map +1 -1
  62. package/lib/hooks/index.js +5 -5
  63. package/lib/hooks/index.js.map +1 -1
  64. package/lib/hooks/useClientVersionCollector.d.ts +3 -0
  65. package/lib/hooks/useClientVersionCollector.d.ts.map +1 -0
  66. package/lib/{services/ClientVersionCollector.js → hooks/useClientVersionCollector.js} +7 -2
  67. package/lib/hooks/useClientVersionCollector.js.map +1 -0
  68. package/lib/hooks/useHeightAnimation.d.ts +0 -1
  69. package/lib/hooks/useHeightAnimation.d.ts.map +1 -1
  70. package/lib/hooks/useHeightAnimation.js +4 -2
  71. package/lib/hooks/useHeightAnimation.js.map +1 -1
  72. package/lib/hooks/useWidget.d.ts +13 -0
  73. package/lib/hooks/useWidget.d.ts.map +1 -0
  74. package/lib/hooks/useWidget.js +44 -0
  75. package/lib/hooks/useWidget.js.map +1 -0
  76. package/lib/hooks/useWidgetBootstrap.d.ts +21 -0
  77. package/lib/hooks/useWidgetBootstrap.d.ts.map +1 -0
  78. package/lib/hooks/useWidgetBootstrap.js +87 -0
  79. package/lib/hooks/useWidgetBootstrap.js.map +1 -0
  80. package/lib/hooks/useWidgetServices.d.ts +19 -0
  81. package/lib/hooks/useWidgetServices.d.ts.map +1 -0
  82. package/lib/hooks/useWidgetServices.js +34 -0
  83. package/lib/hooks/useWidgetServices.js.map +1 -0
  84. package/lib/hooks/useWidgetUI.d.ts +9 -0
  85. package/lib/hooks/useWidgetUI.d.ts.map +1 -0
  86. package/lib/hooks/useWidgetUI.js +33 -0
  87. package/lib/hooks/useWidgetUI.js.map +1 -0
  88. package/lib/index.d.ts +10 -11
  89. package/lib/index.d.ts.map +1 -1
  90. package/lib/index.js +13 -38
  91. package/lib/index.js.map +1 -1
  92. package/lib/services/UserIdentificationService.d.ts +3 -0
  93. package/lib/services/UserIdentificationService.d.ts.map +1 -0
  94. package/lib/services/UserIdentificationService.js +17 -0
  95. package/lib/services/UserIdentificationService.js.map +1 -0
  96. package/lib/services/WidgetBootstrapService.d.ts +12 -0
  97. package/lib/services/WidgetBootstrapService.d.ts.map +1 -0
  98. package/lib/services/{widgetBootstrapService.js → WidgetBootstrapService.js} +36 -15
  99. package/lib/services/WidgetBootstrapService.js.map +1 -0
  100. package/lib/services/WidgetEventService.d.ts +8 -0
  101. package/lib/services/WidgetEventService.d.ts.map +1 -0
  102. package/lib/services/WidgetEventService.js +14 -0
  103. package/lib/services/WidgetEventService.js.map +1 -0
  104. package/lib/services/WidgetStateManager.d.ts +20 -0
  105. package/lib/services/WidgetStateManager.d.ts.map +1 -0
  106. package/lib/services/WidgetStateManager.js +93 -0
  107. package/lib/services/WidgetStateManager.js.map +1 -0
  108. package/lib/services/WidgetValidationService.d.ts +17 -0
  109. package/lib/services/WidgetValidationService.d.ts.map +1 -0
  110. package/lib/services/WidgetValidationService.js +132 -0
  111. package/lib/services/WidgetValidationService.js.map +1 -0
  112. package/lib/services/events/EventHandlerFactory.d.ts +18 -0
  113. package/lib/services/events/EventHandlerFactory.d.ts.map +1 -0
  114. package/lib/services/events/EventHandlerFactory.js +67 -0
  115. package/lib/services/events/EventHandlerFactory.js.map +1 -0
  116. package/lib/services/events/EventHandlers.d.ts +10 -0
  117. package/lib/services/events/EventHandlers.d.ts.map +1 -0
  118. package/lib/services/events/EventHandlers.js +72 -0
  119. package/lib/services/events/EventHandlers.js.map +1 -0
  120. package/lib/services/events/index.d.ts +3 -0
  121. package/lib/services/events/index.d.ts.map +1 -0
  122. package/lib/services/events/index.js +21 -0
  123. package/lib/services/events/index.js.map +1 -0
  124. package/lib/services/height/HeightStrategies.d.ts +3 -0
  125. package/lib/services/height/HeightStrategies.d.ts.map +1 -0
  126. package/lib/services/height/HeightStrategies.js +14 -0
  127. package/lib/services/height/HeightStrategies.js.map +1 -0
  128. package/lib/services/storage/AsyncStorageService.d.ts +13 -0
  129. package/lib/services/storage/AsyncStorageService.d.ts.map +1 -0
  130. package/lib/services/storage/AsyncStorageService.js +73 -0
  131. package/lib/services/storage/AsyncStorageService.js.map +1 -0
  132. package/lib/services/storage/IStorageService.d.ts +30 -0
  133. package/lib/services/storage/IStorageService.d.ts.map +1 -0
  134. package/lib/services/storage/IStorageService.js +3 -0
  135. package/lib/services/storage/IStorageService.js.map +1 -0
  136. package/lib/services/storage/StorageIdBuilder.d.ts +11 -0
  137. package/lib/services/storage/StorageIdBuilder.d.ts.map +1 -0
  138. package/lib/services/storage/StorageIdBuilder.js +17 -0
  139. package/lib/services/storage/StorageIdBuilder.js.map +1 -0
  140. package/lib/services/storage/index.d.ts +3 -0
  141. package/lib/services/storage/index.d.ts.map +1 -0
  142. package/lib/services/storage/index.js +6 -0
  143. package/lib/services/storage/index.js.map +1 -0
  144. package/lib/styles/widgetStyles.d.ts +1 -1
  145. package/lib/styles/widgetStyles.d.ts.map +1 -1
  146. package/package.json +8 -2
  147. package/src/SoluCXWidget.ts +144 -0
  148. package/src/SoluCXWidgetHost.tsx +44 -0
  149. package/src/SoluCXWidgetView.tsx +97 -0
  150. package/src/__tests__/ClientVersionCollector.test.ts +5 -5
  151. package/src/__tests__/OverlayWidget.rendering.test.tsx +12 -14
  152. package/src/__tests__/SoluCXWidget.rendering.test.tsx +103 -60
  153. package/src/__tests__/SoluCXWidget.test.ts +448 -0
  154. package/src/__tests__/WidgetValidationService.test.ts +408 -0
  155. package/src/__tests__/e2e/widget-lifecycle.test.tsx +14 -23
  156. package/src/__tests__/index.test.tsx +39 -0
  157. package/src/__tests__/integration/webview-communication-simple.test.tsx +8 -6
  158. package/src/__tests__/integration/webview-communication.test.tsx +127 -130
  159. package/src/__tests__/normalizeWidgetOptions.test.ts +80 -0
  160. package/src/__tests__/useWidgetBootstrap.test.ts +634 -0
  161. package/src/__tests__/useWidgetState.test.ts +56 -13
  162. package/src/__tests__/widgetBootstrapService.test.ts +15 -17
  163. package/src/components/CloseButton.tsx +6 -2
  164. package/src/components/InlineWidget.tsx +4 -9
  165. package/src/components/ModalWidget.tsx +15 -45
  166. package/src/components/OverlayWidget.tsx +5 -15
  167. package/src/components/index.ts +4 -0
  168. package/src/constants/Constants.ts +15 -0
  169. package/src/{interfaces → domain}/WidgetCallbacks.ts +2 -2
  170. package/{lib/interfaces/WidgetData.d.ts → src/domain/WidgetData.ts} +3 -2
  171. package/src/domain/WidgetDisplayResult.ts +16 -0
  172. package/src/domain/WidgetOptions.ts +53 -0
  173. package/src/domain/WidgetResponse.ts +5 -0
  174. package/src/domain/WidgetSamplerLog.ts +11 -0
  175. package/src/{interfaces → domain}/index.ts +1 -2
  176. package/src/hooks/index.ts +2 -2
  177. package/src/{services/ClientVersionCollector.ts → hooks/useClientVersionCollector.ts} +6 -0
  178. package/src/hooks/useHeightAnimation.ts +6 -3
  179. package/src/hooks/useWidget.ts +46 -0
  180. package/src/hooks/useWidgetBootstrap.ts +117 -0
  181. package/src/hooks/useWidgetServices.ts +44 -0
  182. package/src/hooks/useWidgetUI.ts +38 -0
  183. package/src/index.ts +16 -11
  184. package/src/services/UserIdentificationService.ts +14 -0
  185. package/src/services/{widgetBootstrapService.ts → WidgetBootstrapService.ts} +43 -19
  186. package/src/services/WidgetEventService.ts +15 -0
  187. package/src/services/WidgetStateManager.ts +115 -0
  188. package/src/services/WidgetValidationService.ts +149 -0
  189. package/src/services/events/EventHandlerFactory.ts +70 -0
  190. package/src/services/events/EventHandlers.ts +67 -0
  191. package/src/services/events/index.ts +2 -0
  192. package/src/services/height/HeightStrategies.ts +15 -0
  193. package/src/services/storage/AsyncStorageService.ts +74 -0
  194. package/src/services/storage/IStorageService.ts +32 -0
  195. package/src/services/storage/StorageIdBuilder.ts +15 -0
  196. package/src/services/storage/index.ts +2 -0
  197. package/src/styles/widgetStyles.ts +1 -1
  198. package/README.intern.md +0 -490
  199. package/lib/constants/webViewConstants.d.ts +0 -12
  200. package/lib/constants/webViewConstants.d.ts.map +0 -1
  201. package/lib/constants/webViewConstants.js +0 -19
  202. package/lib/constants/webViewConstants.js.map +0 -1
  203. package/lib/hooks/useWidgetHeight.d.ts +0 -13
  204. package/lib/hooks/useWidgetHeight.d.ts.map +0 -1
  205. package/lib/hooks/useWidgetHeight.js +0 -21
  206. package/lib/hooks/useWidgetHeight.js.map +0 -1
  207. package/lib/hooks/useWidgetState.d.ts +0 -15
  208. package/lib/hooks/useWidgetState.d.ts.map +0 -1
  209. package/lib/hooks/useWidgetState.js +0 -79
  210. package/lib/hooks/useWidgetState.js.map +0 -1
  211. package/lib/interfaces/WidgetCallbacks.d.ts.map +0 -1
  212. package/lib/interfaces/WidgetCallbacks.js.map +0 -1
  213. package/lib/interfaces/WidgetData.d.ts.map +0 -1
  214. package/lib/interfaces/WidgetOptions.d.ts +0 -9
  215. package/lib/interfaces/WidgetOptions.d.ts.map +0 -1
  216. package/lib/interfaces/WidgetOptions.js.map +0 -1
  217. package/lib/interfaces/WidgetResponse.d.ts +0 -10
  218. package/lib/interfaces/WidgetResponse.d.ts.map +0 -1
  219. package/lib/interfaces/WidgetResponse.js +0 -12
  220. package/lib/interfaces/WidgetResponse.js.map +0 -1
  221. package/lib/interfaces/WidgetSamplerLog.d.ts +0 -7
  222. package/lib/interfaces/WidgetSamplerLog.d.ts.map +0 -1
  223. package/lib/interfaces/WidgetSamplerLog.js.map +0 -1
  224. package/lib/interfaces/index.d.ts.map +0 -1
  225. package/lib/services/ClientVersionCollector.d.ts +0 -2
  226. package/lib/services/ClientVersionCollector.d.ts.map +0 -1
  227. package/lib/services/ClientVersionCollector.js.map +0 -1
  228. package/lib/services/storage.d.ts +0 -8
  229. package/lib/services/storage.d.ts.map +0 -1
  230. package/lib/services/storage.js +0 -23
  231. package/lib/services/storage.js.map +0 -1
  232. package/lib/services/widgetBootstrapService.d.ts +0 -6
  233. package/lib/services/widgetBootstrapService.d.ts.map +0 -1
  234. package/lib/services/widgetBootstrapService.js.map +0 -1
  235. package/lib/services/widgetEventService.d.ts +0 -19
  236. package/lib/services/widgetEventService.d.ts.map +0 -1
  237. package/lib/services/widgetEventService.js +0 -79
  238. package/lib/services/widgetEventService.js.map +0 -1
  239. package/lib/services/widgetValidationService.d.ts +0 -18
  240. package/lib/services/widgetValidationService.d.ts.map +0 -1
  241. package/lib/services/widgetValidationService.js +0 -71
  242. package/lib/services/widgetValidationService.js.map +0 -1
  243. package/src/SoluCXWidget.tsx +0 -178
  244. package/src/constants/webViewConstants.ts +0 -15
  245. package/src/hooks/useWidgetHeight.ts +0 -38
  246. package/src/hooks/useWidgetState.ts +0 -101
  247. package/src/interfaces/WidgetOptions.ts +0 -8
  248. package/src/interfaces/WidgetResponse.ts +0 -15
  249. package/src/interfaces/WidgetSamplerLog.ts +0 -6
  250. package/src/services/storage.ts +0 -21
  251. package/src/services/widgetEventService.ts +0 -110
  252. package/src/services/widgetValidationService.ts +0 -102
  253. /package/lib/{interfaces → domain}/WidgetCallbacks.js +0 -0
  254. /package/lib/{interfaces → domain}/WidgetData.js +0 -0
  255. /package/lib/{interfaces → domain}/WidgetSamplerLog.js +0 -0
  256. /package/lib/{interfaces → domain}/index.js +0 -0
@@ -1,8 +1,8 @@
1
1
  import React from "react";
2
2
  import { render, waitFor } from "@testing-library/react-native";
3
- import { SoluCXWidget } from "../SoluCXWidget";
4
- import { WidgetData, WidgetOptions } from "../interfaces";
5
- import { requestWidgetUrl } from "../services/widgetBootstrapService";
3
+ import { SoluCXWidgetView as SoluCXWidget } from "../SoluCXWidgetView";
4
+ import { WidgetData, WidgetOptions } from "../domain";
5
+ import { requestWidgetOptions, requestWidgetUrl } from "../services/WidgetBootstrapService";
6
6
 
7
7
  jest.mock("react-native-webview", () => {
8
8
  const { forwardRef } = require("react");
@@ -13,38 +13,50 @@ jest.mock("react-native-webview", () => {
13
13
  };
14
14
  });
15
15
 
16
- const mockLoadSavedData = jest.fn();
17
16
  const mockClose = jest.fn();
18
- const mockSetIsWidgetVisible = jest.fn();
17
+ const mockHide = jest.fn();
19
18
  const mockOpen = jest.fn();
20
19
  const mockShouldDisplayWidget = jest.fn();
20
+ const mockShouldDisplayForTransaction = jest.fn();
21
+
22
+ const mockStateManager = {
23
+ getLogs: jest.fn().mockResolvedValue({ lastFirstAccess: 0, lastDisplay: 0 }),
24
+ updateTimestamp: jest.fn().mockResolvedValue(undefined),
25
+ incrementAttempt: jest.fn().mockResolvedValue(undefined),
26
+ hasAnsweredTransaction: jest.fn().mockResolvedValue(false),
27
+ markTransactionAnswered: jest.fn().mockResolvedValue(undefined),
28
+ resetAttempts: jest.fn().mockResolvedValue(undefined),
29
+ };
21
30
 
22
- jest.mock("../hooks/useWidgetState", () => ({
23
- useWidgetState: () => ({
31
+ jest.mock("../hooks/useWidget", () => ({
32
+ useWidget: () => ({
24
33
  widgetHeight: 400,
25
34
  isWidgetVisible: true,
26
- setIsWidgetVisible: mockSetIsWidgetVisible,
27
- loadSavedData: mockLoadSavedData,
35
+ hide: mockHide,
28
36
  resize: jest.fn(),
29
37
  open: mockOpen,
30
38
  close: mockClose,
31
39
  userId: "test-user-123",
40
+ stateManager: mockStateManager,
32
41
  }),
33
42
  }));
34
43
 
35
- jest.mock("../services/widgetEventService", () => ({
44
+ jest.mock("../services/WidgetEventService", () => ({
36
45
  WidgetEventService: jest.fn().mockImplementation(() => ({
37
46
  handleMessage: jest.fn(),
38
47
  })),
39
48
  }));
40
49
 
41
- jest.mock("../services/widgetValidationService", () => ({
50
+ jest.mock("../services/WidgetValidationService", () => ({
42
51
  WidgetValidationService: jest.fn().mockImplementation(() => ({
52
+ shouldDisplayForTransaction: mockShouldDisplayForTransaction,
53
+ shouldDisplayForTransactionAlreadyAnswered: mockShouldDisplayForTransaction,
43
54
  shouldDisplayWidget: mockShouldDisplayWidget,
44
55
  })),
45
56
  }));
46
57
 
47
- jest.mock("../services/widgetBootstrapService", () => ({
58
+ jest.mock("../services/WidgetBootstrapService", () => ({
59
+ requestWidgetOptions: jest.fn(),
48
60
  requestWidgetUrl: jest.fn(),
49
61
  }));
50
62
 
@@ -53,7 +65,7 @@ jest.mock("../constants/Constants", () => ({
53
65
  SDK_VERSION: "0.1.16",
54
66
  }));
55
67
 
56
- jest.mock("../services/ClientVersionCollector", () => ({
68
+ jest.mock("../hooks/useClientVersionCollector", () => ({
57
69
  getClientVersion: jest.fn(() => "1.0.0"),
58
70
  }));
59
71
 
@@ -73,6 +85,7 @@ jest.mock("../hooks/useDeviceInfoCollector", () => ({
73
85
  }));
74
86
 
75
87
  const mockRequestWidgetUrl = requestWidgetUrl as jest.MockedFunction<typeof requestWidgetUrl>;
88
+ const mockRequestWidgetOptions = requestWidgetOptions as jest.MockedFunction<typeof requestWidgetOptions>;
76
89
  const bootstrappedWidgetUrl = "https://widgets.solucx.com/widget/bootstrap-result";
77
90
 
78
91
  const baseProps = {
@@ -89,26 +102,30 @@ const baseProps = {
89
102
 
90
103
  beforeEach(() => {
91
104
  jest.clearAllMocks();
105
+ mockShouldDisplayForTransaction.mockReset();
106
+ mockShouldDisplayForTransaction.mockResolvedValue({ canDisplay: true });
92
107
  mockShouldDisplayWidget.mockReset();
93
108
  mockShouldDisplayWidget.mockResolvedValue({ canDisplay: true });
109
+ mockRequestWidgetOptions.mockReset();
110
+ mockRequestWidgetOptions.mockResolvedValue({ height: 400 });
94
111
  mockRequestWidgetUrl.mockReset();
95
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
112
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
96
113
  });
97
114
 
98
115
  describe("SoluCXWidget type routing", () => {
99
116
  it("should render ModalWidget when type is modal", async () => {
100
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
117
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
101
118
 
102
119
  const { UNSAFE_getByType } = render(<SoluCXWidget {...baseProps} type="modal" />);
103
120
 
104
121
  await waitFor(() => {
105
122
  const modal = UNSAFE_getByType(require("react-native").Modal);
106
123
  expect(modal).toBeTruthy();
107
- });
108
- }, 10000);
124
+ }, { timeout: 10000 });
125
+ }, 15000);
109
126
 
110
127
  it("should render close button inside ModalWidget", async () => {
111
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
128
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
112
129
 
113
130
  const { getByText } = render(<SoluCXWidget {...baseProps} type="modal" />);
114
131
 
@@ -118,7 +135,7 @@ describe("SoluCXWidget type routing", () => {
118
135
  });
119
136
 
120
137
  it("should render WebView inside ModalWidget with correct source", async () => {
121
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
138
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
122
139
 
123
140
  const { getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
124
141
 
@@ -129,7 +146,7 @@ describe("SoluCXWidget type routing", () => {
129
146
  });
130
147
 
131
148
  it("should render InlineWidget when type is inline", async () => {
132
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
149
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
133
150
 
134
151
  const { queryByTestId, getByText } = render(<SoluCXWidget {...baseProps} type="inline" />);
135
152
 
@@ -140,7 +157,7 @@ describe("SoluCXWidget type routing", () => {
140
157
  });
141
158
 
142
159
  it("should not render Modal when type is inline", async () => {
143
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
160
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
144
161
 
145
162
  const { UNSAFE_queryByType } = render(<SoluCXWidget {...baseProps} type="inline" />);
146
163
 
@@ -151,7 +168,7 @@ describe("SoluCXWidget type routing", () => {
151
168
  });
152
169
 
153
170
  it("should render OverlayWidget when type is bottom", async () => {
154
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
171
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
155
172
 
156
173
  const { queryByTestId, getByText } = render(<SoluCXWidget {...baseProps} type="bottom" />);
157
174
 
@@ -162,7 +179,7 @@ describe("SoluCXWidget type routing", () => {
162
179
  });
163
180
 
164
181
  it("should render OverlayWidget when type is top", async () => {
165
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
182
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
166
183
 
167
184
  const { queryByTestId, getByText } = render(<SoluCXWidget {...baseProps} type="top" />);
168
185
 
@@ -173,7 +190,7 @@ describe("SoluCXWidget type routing", () => {
173
190
  });
174
191
 
175
192
  it("should not render Modal when type is bottom", async () => {
176
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
193
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
177
194
 
178
195
  const { UNSAFE_queryByType } = render(<SoluCXWidget {...baseProps} type="bottom" />);
179
196
 
@@ -182,17 +199,11 @@ describe("SoluCXWidget type routing", () => {
182
199
  expect(modal).toBeNull();
183
200
  });
184
201
  });
185
-
186
- it("should call loadSavedData on mount", () => {
187
- render(<SoluCXWidget {...baseProps} type="modal" />);
188
-
189
- expect(mockLoadSavedData).toHaveBeenCalledTimes(1);
190
- });
191
202
  });
192
203
 
193
204
  describe("SoluCXWidget WebView configuration", () => {
194
205
  it("should set originWhitelist to allow all origins", async () => {
195
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
206
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
196
207
 
197
208
  const { getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
198
209
 
@@ -203,7 +214,7 @@ describe("SoluCXWidget WebView configuration", () => {
203
214
  });
204
215
 
205
216
  it("should set WebView width style to screen width", async () => {
206
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
217
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
207
218
 
208
219
  const { getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
209
220
 
@@ -215,7 +226,7 @@ describe("SoluCXWidget WebView configuration", () => {
215
226
  });
216
227
 
217
228
  it("should set WebView height style to widgetHeight", async () => {
218
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
229
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
219
230
 
220
231
  const { getByTestId } = render(<SoluCXWidget {...baseProps} type="modal" />);
221
232
 
@@ -227,7 +238,7 @@ describe("SoluCXWidget WebView configuration", () => {
227
238
  });
228
239
 
229
240
  it("should use bootstrapped URL for widgets", async () => {
230
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
241
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
231
242
 
232
243
  const { getByTestId } = render(<SoluCXWidget {...baseProps} type="inline" />);
233
244
 
@@ -240,7 +251,7 @@ describe("SoluCXWidget WebView configuration", () => {
240
251
 
241
252
  describe("SoluCXWidget bootstrap flow", () => {
242
253
  it("fetches widget URL for form widgets", async () => {
243
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
254
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
244
255
 
245
256
  const props = {
246
257
  ...baseProps,
@@ -270,7 +281,7 @@ describe("SoluCXWidget bootstrap flow", () => {
270
281
  });
271
282
 
272
283
  it("fetches widget URL before rendering non-form widgets", async () => {
273
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
284
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
274
285
 
275
286
  const props = {
276
287
  ...baseProps,
@@ -296,7 +307,7 @@ describe("SoluCXWidget bootstrap flow", () => {
296
307
  expect(webview.props.source.uri).toBe(bootstrappedWidgetUrl);
297
308
  });
298
309
 
299
- it("keeps widget hidden when identifier fetch fails", async () => {
310
+ it("keeps widget hidden when preflight fetch fails", async () => {
300
311
  mockRequestWidgetUrl.mockRejectedValue(new Error("network"));
301
312
 
302
313
  const props = {
@@ -313,15 +324,16 @@ describe("SoluCXWidget bootstrap flow", () => {
313
324
  expect(queryByTestId("webview")).toBeNull();
314
325
 
315
326
  await waitFor(() => {
327
+ // Validation runs BEFORE preflight to reduce API load at scale
328
+ expect(mockShouldDisplayWidget).toHaveBeenCalledTimes(1);
316
329
  expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
317
- expect(mockShouldDisplayWidget).not.toHaveBeenCalled();
318
330
  expect(queryByTestId("webview")).toBeNull();
319
331
  });
320
332
  });
321
333
 
322
- it("keeps widget hidden when validation fails after bootstrap", async () => {
323
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
324
- mockShouldDisplayWidget.mockResolvedValue({ canDisplay: false, blockReason: "BLOCKED_BY_MAX_RETRY_ATTEMPTS" });
334
+ it("keeps widget hidden when validation fails before preflight", async () => {
335
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
336
+ mockShouldDisplayWidget.mockResolvedValue({ canDisplay: false, blockReason: "BLOCKED_BY_MAX_ATTEMPTS" });
325
337
 
326
338
  const props = {
327
339
  ...baseProps,
@@ -337,12 +349,38 @@ describe("SoluCXWidget bootstrap flow", () => {
337
349
  expect(queryByTestId("webview")).toBeNull();
338
350
 
339
351
  await waitFor(() => {
340
- expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
352
+ // Validation blocks BEFORE preflight — preflight is never called
341
353
  expect(mockShouldDisplayWidget).toHaveBeenCalledTimes(1);
354
+ expect(mockRequestWidgetUrl).not.toHaveBeenCalled();
342
355
  expect(mockOpen).not.toHaveBeenCalled();
343
356
  expect(queryByTestId("webview")).toBeNull();
344
357
  });
345
358
  });
359
+
360
+ it("keeps widget hidden and skips all fetches when transaction was already answered", async () => {
361
+ mockShouldDisplayForTransaction.mockResolvedValue({
362
+ canDisplay: false,
363
+ blockReason: "BLOCKED_BY_TRANSACTION_ALREADY_ANSWERED",
364
+ });
365
+ const onBlocked = jest.fn();
366
+
367
+ const props = {
368
+ ...baseProps,
369
+ type: "bottom" as const,
370
+ callbacks: { onBlocked },
371
+ };
372
+
373
+ const { queryByTestId } = render(<SoluCXWidget {...props} />);
374
+
375
+ await waitFor(() => {
376
+ expect(mockShouldDisplayForTransaction).toHaveBeenCalledWith('txn-999');
377
+ expect(mockShouldDisplayWidget).not.toHaveBeenCalled();
378
+ expect(mockRequestWidgetOptions).not.toHaveBeenCalled();
379
+ expect(mockRequestWidgetUrl).not.toHaveBeenCalled();
380
+ expect(onBlocked).toHaveBeenCalledWith('BLOCKED_BY_TRANSACTION_ALREADY_ANSWERED');
381
+ expect(queryByTestId("webview")).toBeNull();
382
+ });
383
+ });
346
384
  });
347
385
 
348
386
  describe("SoluCXWidget callbacks", () => {
@@ -356,7 +394,7 @@ describe("SoluCXWidget callbacks", () => {
356
394
 
357
395
  render(<SoluCXWidget {...props} type="modal" />);
358
396
 
359
- expect(onError).toHaveBeenCalledTimes(1);
397
+ expect(onError).toHaveBeenCalled();
360
398
  expect(onError).toHaveBeenCalledWith("Widget data is required but was not provided");
361
399
  });
362
400
 
@@ -379,7 +417,7 @@ describe("SoluCXWidget callbacks", () => {
379
417
  await waitFor(() => {
380
418
  expect(onError).toHaveBeenCalledTimes(1);
381
419
  expect(onError).toHaveBeenCalledWith("Network timeout");
382
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
420
+ expect(mockHide).toHaveBeenCalledTimes(1);
383
421
  });
384
422
  });
385
423
 
@@ -449,13 +487,13 @@ describe("SoluCXWidget callbacks", () => {
449
487
  });
450
488
  });
451
489
 
452
- it("calls onBlock when validation fails", async () => {
453
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
490
+ it("calls onBlocked when validation fails", async () => {
491
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
454
492
  mockShouldDisplayWidget.mockResolvedValue({
455
493
  canDisplay: false,
456
- blockReason: "BLOCKED_BY_MAX_RETRY_ATTEMPTS",
494
+ blockReason: "BLOCKED_BY_MAX_ATTEMPTS",
457
495
  });
458
- const onBlock = jest.fn();
496
+ const onBlocked = jest.fn();
459
497
 
460
498
  const props = {
461
499
  ...baseProps,
@@ -464,20 +502,20 @@ describe("SoluCXWidget callbacks", () => {
464
502
  journey: "sac",
465
503
  },
466
504
  type: "bottom" as const,
467
- callbacks: { onBlock },
505
+ callbacks: { onBlocked },
468
506
  };
469
507
 
470
508
  render(<SoluCXWidget {...props} />);
471
509
 
472
510
  await waitFor(() => {
473
- expect(onBlock).toHaveBeenCalledTimes(1);
474
- expect(onBlock).toHaveBeenCalledWith("BLOCKED_BY_MAX_RETRY_ATTEMPTS");
475
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
511
+ expect(onBlocked).toHaveBeenCalledTimes(1);
512
+ expect(onBlocked).toHaveBeenCalledWith("BLOCKED_BY_MAX_ATTEMPTS");
513
+ expect(mockHide).toHaveBeenCalledTimes(1);
476
514
  });
477
515
  });
478
516
 
479
517
  it("calls onPreOpen, onOpened callbacks for successful bootstrap", async () => {
480
- mockRequestWidgetUrl.mockResolvedValue(bootstrappedWidgetUrl);
518
+ mockRequestWidgetUrl.mockResolvedValue({ available: true, url: bootstrappedWidgetUrl });
481
519
  mockShouldDisplayWidget.mockResolvedValue({ canDisplay: true });
482
520
  const onPreOpen = jest.fn();
483
521
  const onOpened = jest.fn();
@@ -520,7 +558,7 @@ describe("SoluCXWidget error handling from requestWidgetUrl", () => {
520
558
  expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
521
559
  expect(onError).toHaveBeenCalledTimes(1);
522
560
  expect(onError).toHaveBeenCalledWith("Failed to get error response: 404 Not Found");
523
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
561
+ expect(mockHide).toHaveBeenCalledTimes(1);
524
562
  expect(queryByTestId("webview")).toBeNull();
525
563
  });
526
564
  });
@@ -541,7 +579,7 @@ describe("SoluCXWidget error handling from requestWidgetUrl", () => {
541
579
  expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
542
580
  expect(onError).toHaveBeenCalledTimes(1);
543
581
  expect(onError).toHaveBeenCalledWith("Failed to get error response: 500 Internal Server Error");
544
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
582
+ expect(mockHide).toHaveBeenCalledTimes(1);
545
583
  expect(queryByTestId("webview")).toBeNull();
546
584
  });
547
585
  });
@@ -562,7 +600,7 @@ describe("SoluCXWidget error handling from requestWidgetUrl", () => {
562
600
  expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
563
601
  expect(onError).toHaveBeenCalledTimes(1);
564
602
  expect(onError).toHaveBeenCalledWith("Fetch is not available");
565
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
603
+ expect(mockHide).toHaveBeenCalledTimes(1);
566
604
  expect(queryByTestId("webview")).toBeNull();
567
605
  });
568
606
  });
@@ -583,12 +621,12 @@ describe("SoluCXWidget error handling from requestWidgetUrl", () => {
583
621
  expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
584
622
  expect(onError).toHaveBeenCalledTimes(1);
585
623
  expect(onError).toHaveBeenCalledWith("Failed to get the widget api response.");
586
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
624
+ expect(mockHide).toHaveBeenCalledTimes(1);
587
625
  expect(queryByTestId("webview")).toBeNull();
588
626
  });
589
627
  });
590
628
 
591
- it("does not call shouldDisplayWidget when requestWidgetUrl fails", async () => {
629
+ it("calls shouldDisplayWidget before requestWidgetUrl and handles preflight failure", async () => {
592
630
  mockRequestWidgetUrl.mockRejectedValue(new Error("Failed to fetch widget URL: 403 Forbidden"));
593
631
  const onError = jest.fn();
594
632
 
@@ -601,8 +639,9 @@ describe("SoluCXWidget error handling from requestWidgetUrl", () => {
601
639
  render(<SoluCXWidget {...props} />);
602
640
 
603
641
  await waitFor(() => {
642
+ // Validation runs BEFORE preflight to avoid unnecessary API calls
643
+ expect(mockShouldDisplayWidget).toHaveBeenCalled();
604
644
  expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
605
- expect(mockShouldDisplayWidget).not.toHaveBeenCalled();
606
645
  expect(mockOpen).not.toHaveBeenCalled();
607
646
  expect(onError).toHaveBeenCalledWith("Failed to fetch widget URL: 403 Forbidden");
608
647
  });
@@ -623,6 +662,8 @@ describe("SoluCXWidget error handling from requestWidgetUrl", () => {
623
662
  render(<SoluCXWidget {...props} />);
624
663
 
625
664
  await waitFor(() => {
665
+ // Validation runs first, then preflight fails
666
+ expect(mockShouldDisplayWidget).toHaveBeenCalled();
626
667
  expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
627
668
  expect(onPreOpen).not.toHaveBeenCalled();
628
669
  expect(onOpened).not.toHaveBeenCalled();
@@ -644,11 +685,13 @@ describe("SoluCXWidget error handling from requestWidgetUrl", () => {
644
685
  expect(queryByTestId("webview")).toBeNull();
645
686
 
646
687
  await waitFor(() => {
688
+ // Validation runs first, then preflight fails
689
+ expect(mockShouldDisplayWidget).toHaveBeenCalled();
647
690
  expect(mockRequestWidgetUrl).toHaveBeenCalledTimes(1);
648
691
  });
649
692
 
650
693
  // Should remain null after failure
651
694
  expect(queryByTestId("webview")).toBeNull();
652
- expect(mockSetIsWidgetVisible).toHaveBeenCalledWith(false);
695
+ expect(mockHide).toHaveBeenCalledTimes(1);
653
696
  });
654
697
  });