@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
@@ -0,0 +1,408 @@
1
+ import { WidgetValidationService } from "../services/WidgetValidationService";
2
+ import type { WidgetStateManager } from "../services/WidgetStateManager";
3
+ import type { WidgetOptions, WidgetSamplerLog } from "../domain";
4
+
5
+ describe("WidgetValidationService", () => {
6
+ let service: WidgetValidationService;
7
+ let mockStateManager: jest.Mocked<WidgetStateManager>;
8
+
9
+ const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
10
+ const MOCK_NOW = 1678200000000;
11
+
12
+ beforeEach(() => {
13
+ mockStateManager = {
14
+ getLogs: jest.fn(),
15
+ saveLogs: jest.fn(),
16
+ incrementAttempt: jest.fn(),
17
+ resetAttempts: jest.fn(),
18
+ markTransactionAnswered: jest.fn(),
19
+ hasAnsweredTransaction: jest.fn(),
20
+ clearLogs: jest.fn(),
21
+ hasLogs: jest.fn(),
22
+ updateTimestamp: jest.fn(),
23
+ overrideTimestamp: jest.fn(),
24
+ } as unknown as jest.Mocked<WidgetStateManager>;
25
+ service = new WidgetValidationService(mockStateManager);
26
+ });
27
+
28
+ const defaultLog = (): WidgetSamplerLog => ({ attempts: 0, answeredTransactionIds: [] });
29
+
30
+ describe("shouldDisplayForTransaction", () => {
31
+ it("should allow display when transaction ID is not provided", async () => {
32
+ const result = await service.shouldDisplayForTransactionAlreadyAnswered();
33
+
34
+ expect(result).toEqual({ canDisplay: true });
35
+ expect(mockStateManager.hasAnsweredTransaction).not.toHaveBeenCalled();
36
+ });
37
+
38
+ it("should block when transaction has already been answered", async () => {
39
+ mockStateManager.hasAnsweredTransaction.mockResolvedValue(true);
40
+
41
+ const result = await service.shouldDisplayForTransactionAlreadyAnswered("txn-123");
42
+
43
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_TRANSACTION_ALREADY_ANSWERED" });
44
+ });
45
+
46
+ it("should allow when transaction has not been answered", async () => {
47
+ mockStateManager.hasAnsweredTransaction.mockResolvedValue(false);
48
+
49
+ const result = await service.shouldDisplayForTransactionAlreadyAnswered("txn-123");
50
+
51
+ expect(result).toEqual({ canDisplay: true });
52
+ });
53
+ });
54
+
55
+ describe("shouldDisplayWidget", () => {
56
+ it("should allow display when user has no previous logs", async () => {
57
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
58
+ const result = await service.shouldDisplayWidget({});
59
+ expect(result).toEqual({ canDisplay: true });
60
+ });
61
+
62
+ it("should allow display when no wait days are configured", async () => {
63
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
64
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDismiss: MOCK_NOW - ONE_DAY_IN_MS, lastDisplay: MOCK_NOW - ONE_DAY_IN_MS });
65
+ const result = await service.shouldDisplayWidget({});
66
+ expect(result).toEqual({ canDisplay: true });
67
+ });
68
+
69
+ it("should allow display when all wait days fields are 0", async () => {
70
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
71
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDisplayAttempt: MOCK_NOW - ONE_DAY_IN_MS, lastFirstAccess: MOCK_NOW - ONE_DAY_IN_MS, lastDisplay: MOCK_NOW - ONE_DAY_IN_MS, lastDismiss: MOCK_NOW - ONE_DAY_IN_MS, lastSubmit: MOCK_NOW - ONE_DAY_IN_MS, lastPartialSubmit: MOCK_NOW - ONE_DAY_IN_MS });
72
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDisplayAttempt: 0, waitDaysAfterWidgetFirstAccess: 0, waitDaysAfterWidgetDisplay: 0, waitDaysAfterWidgetDismiss: 0, waitDaysAfterWidgetSubmit: 0, waitDaysAfterWidgetPartialSubmit: 0 });
73
+ expect(result).toEqual({ canDisplay: true });
74
+ });
75
+
76
+ it("should use default logs when getLogs throws error", async () => {
77
+ const spy = jest.spyOn(console, "error").mockImplementation(() => {});
78
+ mockStateManager.getLogs.mockRejectedValue(new Error("Storage unavailable"));
79
+ const result = await service.shouldDisplayWidget({});
80
+ expect(result).toEqual({ canDisplay: true });
81
+ expect(spy).toHaveBeenCalledWith("Error reading widget log:", expect.any(Error));
82
+ spy.mockRestore();
83
+ });
84
+
85
+ it("should handle empty widget options", async () => {
86
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
87
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
88
+ const result = await service.shouldDisplayWidget({});
89
+ expect(result).toEqual({ canDisplay: true });
90
+ });
91
+ });
92
+
93
+ describe("waitDaysAfterWidgetDisplayAttempt", () => {
94
+ it("should block when within interval", async () => {
95
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
96
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDisplayAttempt: MOCK_NOW - ONE_DAY_IN_MS * 2 });
97
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDisplayAttempt: 7 });
98
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_WIDGET_DISPLAY_ATTEMPT_INTERVAL" });
99
+ });
100
+ it("should allow when interval expired", async () => {
101
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
102
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDisplayAttempt: MOCK_NOW - ONE_DAY_IN_MS * 8 });
103
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDisplayAttempt: 7 });
104
+ expect(result).toEqual({ canDisplay: true });
105
+ });
106
+ });
107
+
108
+ describe("waitDaysAfterWidgetFirstAccess", () => {
109
+ it("should block when within interval", async () => {
110
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
111
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastFirstAccess: MOCK_NOW - ONE_DAY_IN_MS * 10 });
112
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetFirstAccess: 30 });
113
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_WIDGET_FIRST_ACCESS_INTERVAL" });
114
+ });
115
+ it("should allow when interval expired", async () => {
116
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
117
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastFirstAccess: MOCK_NOW - ONE_DAY_IN_MS * 31 });
118
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetFirstAccess: 30 });
119
+ expect(result).toEqual({ canDisplay: true });
120
+ });
121
+ });
122
+
123
+ describe("waitDaysAfterWidgetDisplay", () => {
124
+ it("should block when within interval", async () => {
125
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
126
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDisplay: MOCK_NOW - ONE_DAY_IN_MS * 3 });
127
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDisplay: 7 });
128
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_WIDGET_DISPLAY_INTERVAL" });
129
+ });
130
+ it("should allow when interval expired", async () => {
131
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
132
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDisplay: MOCK_NOW - ONE_DAY_IN_MS * 8 });
133
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDisplay: 7 });
134
+ expect(result).toEqual({ canDisplay: true });
135
+ });
136
+ });
137
+
138
+ describe("waitDaysAfterWidgetDismiss", () => {
139
+ it("should block when within interval", async () => {
140
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
141
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDismiss: MOCK_NOW - ONE_DAY_IN_MS * 30 });
142
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDismiss: 90 });
143
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_WIDGET_DISMISS_INTERVAL" });
144
+ });
145
+ it("should allow when interval expired", async () => {
146
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
147
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDismiss: MOCK_NOW - ONE_DAY_IN_MS * 91 });
148
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDismiss: 90 });
149
+ expect(result).toEqual({ canDisplay: true });
150
+ });
151
+ });
152
+
153
+ describe("waitDaysAfterWidgetSubmit", () => {
154
+ it("should block when within interval", async () => {
155
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
156
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastSubmit: MOCK_NOW - ONE_DAY_IN_MS * 15 });
157
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetSubmit: 30 });
158
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_WIDGET_SUBMIT_INTERVAL" });
159
+ });
160
+ it("should allow when interval expired", async () => {
161
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
162
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastSubmit: MOCK_NOW - ONE_DAY_IN_MS * 31 });
163
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetSubmit: 30 });
164
+ expect(result).toEqual({ canDisplay: true });
165
+ });
166
+ });
167
+
168
+ describe("waitDaysAfterWidgetPartialSubmit", () => {
169
+ it("should block when within interval", async () => {
170
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
171
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastPartialSubmit: MOCK_NOW - ONE_DAY_IN_MS * 10 });
172
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetPartialSubmit: 30 });
173
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_WIDGET_PARTIAL_SUBMIT_INTERVAL" });
174
+ });
175
+ it("should allow when interval expired", async () => {
176
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
177
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastPartialSubmit: MOCK_NOW - ONE_DAY_IN_MS * 31 });
178
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetPartialSubmit: 30 });
179
+ expect(result).toEqual({ canDisplay: true });
180
+ });
181
+ });
182
+
183
+ describe("multiple rules and edge cases", () => {
184
+ it("should block by first matching rule", async () => {
185
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
186
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDisplayAttempt: MOCK_NOW - ONE_DAY_IN_MS * 2, lastFirstAccess: MOCK_NOW - ONE_DAY_IN_MS * 2 });
187
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDisplayAttempt: 7, waitDaysAfterWidgetFirstAccess: 7 });
188
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_WIDGET_DISPLAY_ATTEMPT_INTERVAL" });
189
+ });
190
+
191
+ it("should block when any rule blocks", async () => {
192
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
193
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDisplayAttempt: MOCK_NOW - ONE_DAY_IN_MS * 100, lastFirstAccess: MOCK_NOW - ONE_DAY_IN_MS * 100, lastDisplay: MOCK_NOW - ONE_DAY_IN_MS * 100, lastDismiss: MOCK_NOW - ONE_DAY_IN_MS * 100, lastSubmit: MOCK_NOW - ONE_DAY_IN_MS * 5, lastPartialSubmit: MOCK_NOW - ONE_DAY_IN_MS * 100 });
194
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDisplayAttempt: 1, waitDaysAfterWidgetFirstAccess: 1, waitDaysAfterWidgetDisplay: 1, waitDaysAfterWidgetDismiss: 1, waitDaysAfterWidgetSubmit: 30, waitDaysAfterWidgetPartialSubmit: 1 });
195
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_WIDGET_SUBMIT_INTERVAL" });
196
+ });
197
+
198
+ it("should handle old logs without new timestamp fields", async () => {
199
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
200
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
201
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDismiss: 90, waitDaysAfterWidgetSubmit: 60 });
202
+ expect(result).toEqual({ canDisplay: true });
203
+ });
204
+
205
+ it("should not block when timestamp is 0", async () => {
206
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
207
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDismiss: 0 });
208
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDismiss: 90 });
209
+ expect(result).toEqual({ canDisplay: true });
210
+ });
211
+
212
+ it("should allow at exact boundary", async () => {
213
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
214
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDismiss: MOCK_NOW - ONE_DAY_IN_MS * 30 });
215
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDismiss: 30 });
216
+ expect(result).toEqual({ canDisplay: true });
217
+ });
218
+
219
+ it("should block one millisecond before boundary", async () => {
220
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
221
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDismiss: MOCK_NOW - (ONE_DAY_IN_MS * 30 - 1) });
222
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDismiss: 30 });
223
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_WIDGET_DISMISS_INTERVAL" });
224
+ });
225
+
226
+ it("should handle negative timestamp values", async () => {
227
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
228
+ mockStateManager.getLogs.mockResolvedValue({ attempts: 0, lastDismiss: -1000 });
229
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDismiss: 90 });
230
+ expect(result).toEqual({ canDisplay: true });
231
+ });
232
+
233
+ it("should allow when all intervals expired", async () => {
234
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
235
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDisplayAttempt: MOCK_NOW - ONE_DAY_IN_MS * 100, lastFirstAccess: MOCK_NOW - ONE_DAY_IN_MS * 100, lastDisplay: MOCK_NOW - ONE_DAY_IN_MS * 100, lastDismiss: MOCK_NOW - ONE_DAY_IN_MS * 100, lastSubmit: MOCK_NOW - ONE_DAY_IN_MS * 100, lastPartialSubmit: MOCK_NOW - ONE_DAY_IN_MS * 100 });
236
+ const result = await service.shouldDisplayWidget({ waitDaysAfterWidgetDisplayAttempt: 7, waitDaysAfterWidgetFirstAccess: 30, waitDaysAfterWidgetDisplay: 7, waitDaysAfterWidgetDismiss: 90, waitDaysAfterWidgetSubmit: 30, waitDaysAfterWidgetPartialSubmit: 60 });
237
+ expect(result).toEqual({ canDisplay: true });
238
+ });
239
+
240
+ it("should allow with height-only options", async () => {
241
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
242
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDismiss: MOCK_NOW - ONE_DAY_IN_MS });
243
+ const result = await service.shouldDisplayWidget({ height: 600 });
244
+ expect(result).toEqual({ canDisplay: true });
245
+ });
246
+ });
247
+
248
+ describe("maxAttemptsAfterDismiss", () => {
249
+ it("should block when attempts >= maxAttemptsAfterDismiss", async () => {
250
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), attempts: 5 });
251
+ const result = await service.shouldDisplayWidget({ maxAttemptsAfterDismiss: 5 });
252
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_MAX_ATTEMPTS" });
253
+ });
254
+
255
+ it("should block when attempts exceed maxAttemptsAfterDismiss", async () => {
256
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), attempts: 10 });
257
+ const result = await service.shouldDisplayWidget({ maxAttemptsAfterDismiss: 5 });
258
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_MAX_ATTEMPTS" });
259
+ });
260
+
261
+ it("should allow when attempts < maxAttemptsAfterDismiss", async () => {
262
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), attempts: 3 });
263
+ const result = await service.shouldDisplayWidget({ maxAttemptsAfterDismiss: 5 });
264
+ expect(result).toEqual({ canDisplay: true });
265
+ });
266
+
267
+ it("should allow when maxAttemptsAfterDismiss is 0 (no limit)", async () => {
268
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), attempts: 9999 });
269
+ const result = await service.shouldDisplayWidget({ maxAttemptsAfterDismiss: 0 });
270
+ expect(result).toEqual({ canDisplay: true });
271
+ });
272
+
273
+ it("should allow when maxAttemptsAfterDismiss is not set", async () => {
274
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), attempts: 9999 });
275
+ const result = await service.shouldDisplayWidget({});
276
+ expect(result).toEqual({ canDisplay: true });
277
+ });
278
+
279
+ it("should check maxAttemptsAfterDismiss before interval rules", async () => {
280
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
281
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), attempts: 5, lastDismiss: MOCK_NOW - ONE_DAY_IN_MS * 2 });
282
+ const result = await service.shouldDisplayWidget({ maxAttemptsAfterDismiss: 5, waitDaysAfterWidgetDismiss: 90 });
283
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_MAX_ATTEMPTS" });
284
+ });
285
+ });
286
+
287
+ describe("sampling percentage", () => {
288
+ it("should block when random value exceeds sampling percentage", async () => {
289
+ jest.spyOn(Math, "random").mockReturnValue(0.8); // 80 >= 50 → blocked
290
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
291
+ const result = await service.shouldDisplayWidget({ samplingPercentage: 50 });
292
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_SAMPLING" });
293
+ });
294
+
295
+ it("should allow when random value is below sampling percentage", async () => {
296
+ jest.spyOn(Math, "random").mockReturnValue(0.3); // 30 < 50 → allowed
297
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
298
+ const result = await service.shouldDisplayWidget({ samplingPercentage: 50 });
299
+ expect(result).toEqual({ canDisplay: true });
300
+ });
301
+
302
+ it("should skip sampling when percentage is 100 (show all)", async () => {
303
+ jest.spyOn(Math, "random").mockReturnValue(0.99);
304
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
305
+ const result = await service.shouldDisplayWidget({ samplingPercentage: 100 });
306
+ expect(result).toEqual({ canDisplay: true });
307
+ });
308
+
309
+ it("should skip sampling when percentage is undefined", async () => {
310
+ jest.spyOn(Math, "random").mockReturnValue(0.99);
311
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
312
+ const result = await service.shouldDisplayWidget({});
313
+ expect(result).toEqual({ canDisplay: true });
314
+ });
315
+
316
+ it("should block all when sampling percentage is 0", async () => {
317
+ jest.spyOn(Math, "random").mockReturnValue(0.01);
318
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
319
+ const result = await service.shouldDisplayWidget({ samplingPercentage: 0 });
320
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_SAMPLING" });
321
+ });
322
+
323
+ it("should check sampling after enabled check", async () => {
324
+ jest.spyOn(Math, "random").mockReturnValue(0.8);
325
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
326
+ const result = await service.shouldDisplayWidget({ enabled: false, samplingPercentage: 50 });
327
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_DISABLED" });
328
+ });
329
+ });
330
+
331
+ describe("enabled/disabled", () => {
332
+ it("should block immediately when enabled is false without checking other rules", async () => {
333
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
334
+ const result = await service.shouldDisplayWidget({ enabled: false, waitDaysAfterWidgetDismiss: 0 });
335
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_DISABLED" });
336
+ expect(mockStateManager.getLogs).not.toHaveBeenCalled();
337
+ });
338
+
339
+ it("should allow display when enabled is true", async () => {
340
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
341
+ const result = await service.shouldDisplayWidget({ enabled: true });
342
+ expect(result).toEqual({ canDisplay: true });
343
+ });
344
+
345
+ it("should allow display when enabled is undefined (default)", async () => {
346
+ mockStateManager.getLogs.mockResolvedValue(defaultLog());
347
+ const result = await service.shouldDisplayWidget({});
348
+ expect(result).toEqual({ canDisplay: true });
349
+ });
350
+
351
+ it("should block by disabled even if all intervals are expired", async () => {
352
+ jest.spyOn(Date, "now").mockReturnValue(MOCK_NOW);
353
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), lastDismiss: MOCK_NOW - ONE_DAY_IN_MS * 100 });
354
+ const result = await service.shouldDisplayWidget({ enabled: false, waitDaysAfterWidgetDismiss: 1 });
355
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_DISABLED" });
356
+ });
357
+ });
358
+
359
+ describe("experience ID reset", () => {
360
+ it("should reset attempts when experience ID changes", async () => {
361
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), attempts: 5, lastExperienceId: "old_journey" });
362
+ mockStateManager.saveLogs.mockResolvedValue(undefined);
363
+ const result = await service.shouldDisplayWidget({ maxAttemptsAfterDismiss: 5 }, "new_journey");
364
+ expect(result).toEqual({ canDisplay: true });
365
+ expect(mockStateManager.saveLogs).toHaveBeenCalledWith(expect.objectContaining({
366
+ attempts: 0,
367
+ lastExperienceId: "new_journey",
368
+ }));
369
+ });
370
+
371
+ it("should not reset attempts when experience ID is the same", async () => {
372
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), attempts: 5, lastExperienceId: "same_journey" });
373
+ const result = await service.shouldDisplayWidget({ maxAttemptsAfterDismiss: 5 }, "same_journey");
374
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_MAX_ATTEMPTS" });
375
+ expect(mockStateManager.saveLogs).not.toHaveBeenCalled();
376
+ });
377
+
378
+ it("should save experience ID on first call when not set", async () => {
379
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), attempts: 0 });
380
+ mockStateManager.saveLogs.mockResolvedValue(undefined);
381
+ const result = await service.shouldDisplayWidget({}, "first_journey");
382
+ expect(result).toEqual({ canDisplay: true });
383
+ expect(mockStateManager.saveLogs).toHaveBeenCalledWith(expect.objectContaining({
384
+ lastExperienceId: "first_journey",
385
+ }));
386
+ });
387
+
388
+ it("should not reset attempts when no experience ID is provided", async () => {
389
+ mockStateManager.getLogs.mockResolvedValue({ ...defaultLog(), attempts: 5, lastExperienceId: "some_journey" });
390
+ const result = await service.shouldDisplayWidget({ maxAttemptsAfterDismiss: 5 });
391
+ expect(result).toEqual({ canDisplay: false, blockReason: "BLOCKED_BY_MAX_ATTEMPTS" });
392
+ expect(mockStateManager.saveLogs).not.toHaveBeenCalled();
393
+ });
394
+
395
+ it("should preserve timestamp fields when resetting attempts", async () => {
396
+ const log = { ...defaultLog(), attempts: 5, lastExperienceId: "old_journey", lastDismiss: 12345, lastSubmit: 67890 };
397
+ mockStateManager.getLogs.mockResolvedValue(log);
398
+ mockStateManager.saveLogs.mockResolvedValue(undefined);
399
+ await service.shouldDisplayWidget({ maxAttemptsAfterDismiss: 5 }, "new_journey");
400
+ expect(mockStateManager.saveLogs).toHaveBeenCalledWith(expect.objectContaining({
401
+ attempts: 0,
402
+ lastExperienceId: "new_journey",
403
+ lastDismiss: 12345,
404
+ lastSubmit: 67890,
405
+ }));
406
+ });
407
+ });
408
+ });
@@ -9,8 +9,8 @@
9
9
 
10
10
  import React from 'react';
11
11
  import { render, waitFor, fireEvent } from '@testing-library/react-native';
12
- import { SoluCXWidget } from '../../SoluCXWidget';
13
- import type { WidgetCallbacks } from '../../interfaces';
12
+ import { SoluCXWidgetView as SoluCXWidget } from '../../SoluCXWidgetView';
13
+ import type { WidgetCallbacks } from '../../domain';
14
14
 
15
15
  // Mock storage
16
16
  const mockStorage: Record<string, string> = {};
@@ -53,12 +53,14 @@ jest.mock('react-native-webview', () => {
53
53
  };
54
54
  });
55
55
 
56
- jest.mock('../../services/widgetBootstrapService', () => ({
57
- requestWidgetUrl: jest.fn().mockResolvedValue('https://widget.solucx.com/survey/456'),
56
+ jest.mock('../../services/WidgetBootstrapService', () => ({
57
+ requestWidgetUrl: jest.fn().mockResolvedValue({ available: true, url: 'https://widget.solucx.com/survey/456' }),
58
58
  }));
59
59
 
60
- jest.mock('../../services/widgetValidationService', () => ({
60
+ jest.mock('../../services/WidgetValidationService', () => ({
61
61
  WidgetValidationService: jest.fn().mockImplementation(() => ({
62
+ shouldDisplayForTransaction: jest.fn().mockResolvedValue({ canDisplay: true }),
63
+ shouldDisplayForTransactionAlreadyAnswered: jest.fn().mockResolvedValue({ canDisplay: true }),
62
64
  shouldDisplayWidget: jest.fn().mockResolvedValue({ canDisplay: true }),
63
65
  })),
64
66
  }));
@@ -103,7 +105,7 @@ describe('E2E: Widget Lifecycle', () => {
103
105
  // 2. Widget should render
104
106
  await waitFor(() => {
105
107
  expect(getByTestId('webview')).toBeTruthy();
106
- });
108
+ }, { timeout: 10000 });
107
109
 
108
110
  const webview = getByTestId('webview');
109
111
 
@@ -162,17 +164,15 @@ describe('E2E: Widget Lifecycle', () => {
162
164
 
163
165
  // Verify complete tracking flow after reset (question_answered + survey_completed + widget_closed)
164
166
  expect(analytics.track).toHaveBeenCalledTimes(3);
165
- });
167
+ }, 15000);
166
168
  });
167
169
 
168
170
  describe('SaaS NPS Survey Flow with Error Recovery', () => {
169
- it('should handle network error gracefully and still track partial completion', async () => {
171
+ it('should handle network error gracefully when error occurs', async () => {
170
172
  const errorHandler = jest.fn();
171
- const completionHandler = jest.fn();
172
173
 
173
174
  const callbacks: WidgetCallbacks = {
174
175
  onError: errorHandler,
175
- onCompleted: completionHandler,
176
176
  };
177
177
 
178
178
  const { getByTestId } = render(
@@ -182,7 +182,7 @@ describe('E2E: Widget Lifecycle', () => {
182
182
  data={{
183
183
  form_id: 'nps-survey',
184
184
  customer_id: 'user-123',
185
- plan: 'premium',
185
+ param_plan: 'premium',
186
186
  }}
187
187
  options={{ height: 300 }}
188
188
  callbacks={callbacks}
@@ -191,7 +191,7 @@ describe('E2E: Widget Lifecycle', () => {
191
191
 
192
192
  await waitFor(() => {
193
193
  expect(getByTestId('webview')).toBeTruthy();
194
- });
194
+ }, { timeout: 10000 });
195
195
 
196
196
  const webview = getByTestId('webview');
197
197
 
@@ -207,17 +207,8 @@ describe('E2E: Widget Lifecycle', () => {
207
207
 
208
208
  await waitFor(() => {
209
209
  expect(errorHandler).toHaveBeenCalledWith('Network timeout occurred');
210
- });
211
-
212
- // User tries again and succeeds
213
- fireEvent(webview, 'message', {
214
- nativeEvent: { data: 'FORM_COMPLETED' },
215
- });
216
-
217
- await waitFor(() => {
218
- expect(completionHandler).toHaveBeenCalledWith(expect.any(String));
219
- });
220
- });
210
+ }, { timeout: 10000 });
211
+ }, 15000);
221
212
  });
222
213
 
223
214
  describe('Banking Compliance Survey with Dynamic Resizing', () => {
@@ -0,0 +1,39 @@
1
+ import React from 'react';
2
+ import { SoluCXWidget, SoluCXWidgetView } from '../index';
3
+
4
+ jest.mock('react-native-webview', () => ({
5
+ WebView: 'WebView',
6
+ }));
7
+
8
+ jest.mock('@react-native-async-storage/async-storage', () => ({
9
+ __esModule: true,
10
+ default: {
11
+ setItem: jest.fn(() => Promise.resolve()),
12
+ getItem: jest.fn(() => Promise.resolve(null)),
13
+ removeItem: jest.fn(() => Promise.resolve()),
14
+ clear: jest.fn(() => Promise.resolve()),
15
+ },
16
+ }));
17
+
18
+ describe('package root exports', () => {
19
+ it('exposes SoluCXWidget as the component API', () => {
20
+ expect(SoluCXWidget).toBe(SoluCXWidgetView);
21
+ });
22
+
23
+ it('keeps the builder API on SoluCXWidget', () => {
24
+ expect(typeof SoluCXWidget.create).toBe('function');
25
+ expect(typeof SoluCXWidget.dismiss).toBe('function');
26
+ });
27
+
28
+ it('supports creating a JSX element with SoluCXWidget', () => {
29
+ const element = (
30
+ <SoluCXWidget
31
+ soluCXKey="test-key"
32
+ type="modal"
33
+ data={{ journey: 'checkout', user_id: 'user-1' }}
34
+ />
35
+ );
36
+
37
+ expect(element.type).toBe(SoluCXWidgetView);
38
+ });
39
+ });
@@ -5,7 +5,7 @@
5
5
 
6
6
  import React from 'react';
7
7
  import { render, waitFor, fireEvent } from '@testing-library/react-native';
8
- import { SoluCXWidget } from '../../SoluCXWidget';
8
+ import { SoluCXWidgetView as SoluCXWidget } from '../../SoluCXWidgetView';
9
9
 
10
10
  // Mock only external dependencies
11
11
  jest.mock('react-native-webview', () => {
@@ -27,12 +27,14 @@ jest.mock('react-native-webview', () => {
27
27
  };
28
28
  });
29
29
 
30
- jest.mock('../../services/widgetBootstrapService', () => ({
31
- requestWidgetUrl: jest.fn().mockResolvedValue('https://survey.url/test'),
30
+ jest.mock('../../services/WidgetBootstrapService', () => ({
31
+ requestWidgetUrl: jest.fn().mockResolvedValue({ available: true, url: 'https://survey.url/test' }),
32
32
  }));
33
33
 
34
- jest.mock('../../services/widgetValidationService', () => ({
34
+ jest.mock('../../services/WidgetValidationService', () => ({
35
35
  WidgetValidationService: jest.fn().mockImplementation(() => ({
36
+ shouldDisplayForTransaction: jest.fn().mockResolvedValue({ canDisplay: true }),
37
+ shouldDisplayForTransactionAlreadyAnswered: jest.fn().mockResolvedValue({ canDisplay: true }),
36
38
  shouldDisplayWidget: jest.fn().mockResolvedValue({ canDisplay: true }),
37
39
  })),
38
40
  }));
@@ -69,8 +71,8 @@ describe('Integration: WebView Communication - Simplified', () => {
69
71
 
70
72
  await waitFor(() => {
71
73
  expect(getByTestId('webview')).toBeTruthy();
72
- }, { timeout: 3000 });
73
- });
74
+ }, { timeout: 10000 });
75
+ }, 15000);
74
76
 
75
77
  it('should handle FORM_CLOSE message in form mode', async () => {
76
78
  const mockOnClosed = jest.fn();