@applicaster/zapp-react-native-utils 16.0.0-rc.8 → 16.0.0-rc.80

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 (189) hide show
  1. package/actionsExecutor/ActionExecutor.ts +47 -11
  2. package/actionsExecutor/ActionExecutorContext.tsx +102 -315
  3. package/actionsExecutor/__tests__/feedDecorator.test.ts +61 -0
  4. package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +25 -0
  5. package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
  6. package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
  7. package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
  8. package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +133 -0
  9. package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +76 -0
  10. package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +212 -0
  11. package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
  12. package/actionsExecutor/actions/appRestart.ts +24 -0
  13. package/actionsExecutor/actions/confirmDialog.ts +53 -0
  14. package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
  15. package/actionsExecutor/actions/goBack.ts +60 -0
  16. package/actionsExecutor/actions/goHome.ts +31 -0
  17. package/actionsExecutor/actions/index.ts +38 -0
  18. package/actionsExecutor/actions/localStorageRemove.ts +27 -0
  19. package/actionsExecutor/actions/localStorageSet.ts +28 -0
  20. package/actionsExecutor/actions/localStorageToggleFlag.ts +28 -0
  21. package/actionsExecutor/actions/navigateToScreen.ts +137 -0
  22. package/actionsExecutor/actions/openBottomSheet.ts +325 -0
  23. package/actionsExecutor/actions/refreshComponent.ts +85 -0
  24. package/actionsExecutor/actions/screenSetVariable.ts +35 -0
  25. package/actionsExecutor/actions/screenToggleFlag.ts +38 -0
  26. package/actionsExecutor/actions/sendCloudEvent.ts +93 -0
  27. package/actionsExecutor/actions/sessionStorageRemove.ts +19 -0
  28. package/actionsExecutor/actions/sessionStorageSet.ts +20 -0
  29. package/actionsExecutor/actions/sessionStorageToggleFlag.ts +15 -0
  30. package/actionsExecutor/actions/showToast.ts +87 -0
  31. package/actionsExecutor/actions/switchLayout.ts +40 -0
  32. package/actionsExecutor/consts.ts +25 -0
  33. package/actionsExecutor/feedDecorator.ts +6 -6
  34. package/actionsExecutor/types.ts +59 -0
  35. package/analyticsUtils/AnalyticsEvents/sendMenuClickEvent.ts +2 -2
  36. package/analyticsUtils/PlayerAnalyticsManager.ts +12 -2
  37. package/analyticsUtils/__tests__/analyticsMapper.test.ts +35 -0
  38. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testACP_events.json +1 -1
  39. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testBlockUnlistedParams_rules.json +1 -1
  40. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_events.json +4 -0
  41. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEmptyRenameKeepsName_rules.json +6 -0
  42. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testEventRegex_events.json +3 -9
  43. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_events.json +18 -0
  44. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testIgnoreOrdering_rules.json +16 -0
  45. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_events.json +4 -0
  46. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexEventNoFallback_rules.json +6 -0
  47. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_events.json +4 -0
  48. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexMultiGroupRename_rules.json +6 -0
  49. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_events.json +4 -0
  50. package/analyticsUtils/__tests__/fixtures/analytics_mapper_testRegexNonNamedFullMatch_rules.json +6 -0
  51. package/analyticsUtils/__tests__/fixtures/index.js +20 -0
  52. package/analyticsUtils/analyticsMapper.ts +4 -1
  53. package/analyticsUtils/playerAnalyticsTracker.ts +26 -3
  54. package/appUtils/HooksManager/index.ts +12 -8
  55. package/appUtils/contextKeysManager/__tests__/getKey/failure.test.ts +1 -1
  56. package/appUtils/contextKeysManager/__tests__/removeKey/failure.test.ts +1 -1
  57. package/appUtils/contextKeysManager/__tests__/setKey/failure/invalidKey.test.ts +4 -4
  58. package/appUtils/contextKeysManager/index.ts +1 -1
  59. package/appUtils/contextKeysManager/utils/index.ts +38 -25
  60. package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +143 -53
  61. package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
  62. package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +49 -0
  63. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
  64. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
  65. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +11 -0
  66. package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
  67. package/appUtils/playerManager/OverlayObserver/utils.ts +54 -20
  68. package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
  69. package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
  70. package/appUtils/playerManager/__tests__/playerNative.test.ts +38 -0
  71. package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
  72. package/appUtils/playerManager/{conts.ts → const.ts} +20 -0
  73. package/appUtils/playerManager/index.ts +1 -1
  74. package/appUtils/playerManager/player.ts +115 -3
  75. package/appUtils/playerManager/playerFactory.ts +1 -1
  76. package/appUtils/playerManager/playerNative.ts +6 -4
  77. package/appUtils/playerManager/usePlayerContent.tsx +43 -0
  78. package/appUtils/playerManager/usePlayerControllerSetup.tsx +1 -1
  79. package/appUtils/playerManager/userLanguagePreference.ts +1 -1
  80. package/arrayUtils/__tests__/{anyThruthy.test.ts → anyTruthy.test.ts} +8 -0
  81. package/arrayUtils/__tests__/arrayUtils.test.ts +165 -108
  82. package/arrayUtils/__tests__/isEmptyArray.test.ts +11 -0
  83. package/arrayUtils/__tests__/isFilledArray.test.ts +12 -0
  84. package/arrayUtils/__tests__/isFirst.test.ts +17 -0
  85. package/arrayUtils/__tests__/isIndexInRange.test.ts +14 -0
  86. package/arrayUtils/__tests__/isLast.test.ts +31 -0
  87. package/arrayUtils/__tests__/makeListOf.test.ts +31 -0
  88. package/arrayUtils/__tests__/makeListOfIndexes.test.ts +20 -0
  89. package/arrayUtils/__tests__/reorderByIds.test.ts +50 -0
  90. package/arrayUtils/__tests__/sample.test.ts +61 -0
  91. package/arrayUtils/index.ts +136 -20
  92. package/bottomSheet/__tests__/scrollContext.test.tsx +35 -0
  93. package/bottomSheet/index.ts +17 -0
  94. package/cellUtils/index.ts +33 -8
  95. package/colorUtils/__tests__/isTransparentColor.test.ts +76 -0
  96. package/colorUtils/__tests__/isValidColor.test.ts +70 -0
  97. package/colorUtils/index.ts +50 -0
  98. package/configurationUtils/__tests__/modalBlocksParser.test.ts +107 -0
  99. package/configurationUtils/manifestKeyParser.ts +38 -9
  100. package/configurationUtils/modalBlocksParser.ts +178 -0
  101. package/entryActions/__tests__/buildEntryActions.test.ts +156 -0
  102. package/entryActions/aliasPresentation.ts +124 -0
  103. package/entryActions/buildEntryActions.ts +166 -0
  104. package/entryActions/index.ts +17 -0
  105. package/entryActions/types.ts +39 -0
  106. package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
  107. package/manifestUtils/_internals/index.js +14 -3
  108. package/manifestUtils/defaultManifestConfigurations/generalContent.js +35 -0
  109. package/manifestUtils/defaultManifestConfigurations/player.js +349 -20
  110. package/manifestUtils/fieldUtils/__tests__/fieldUtils.test.js +84 -0
  111. package/manifestUtils/fieldUtils/index.js +125 -0
  112. package/manifestUtils/index.js +3 -0
  113. package/manifestUtils/keys.js +9 -0
  114. package/manifestUtils/mobileAction/button/index.js +16 -0
  115. package/manifestUtils/mobileAction/container/index.js +3 -1
  116. package/manifestUtils/mobileAction/groups/defaults.js +3 -1
  117. package/manifestUtils/modalBlocks.js +304 -0
  118. package/manifestUtils/platformIsTV.js +1 -0
  119. package/modalState/ContentViewModel.ts +113 -0
  120. package/modalState/EditableCollection.ts +17 -0
  121. package/modalState/EditableCollectionRegistry.ts +51 -0
  122. package/modalState/ModalOrchestrator.ts +211 -0
  123. package/modalState/RemoteEditableCollection.ts +227 -0
  124. package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
  125. package/modalState/__tests__/ContentViewModel.test.ts +165 -0
  126. package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
  127. package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +47 -0
  128. package/modalState/__tests__/RemoteEditableCollection.test.ts +326 -0
  129. package/modalState/__tests__/index.test.ts +30 -1
  130. package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
  131. package/modalState/index.ts +35 -83
  132. package/modalState/store.ts +102 -0
  133. package/modalState/types.ts +159 -0
  134. package/modalState/useBottomSheetContent.ts +104 -0
  135. package/numberUtils/__tests__/isMinusZero.test.ts +20 -0
  136. package/numberUtils/__tests__/isZero.test.ts +27 -0
  137. package/numberUtils/__tests__/requireNumber.test.ts +50 -0
  138. package/numberUtils/__tests__/toNumber.test.ts +27 -0
  139. package/numberUtils/__tests__/toNumberWithDefault.test.ts +64 -0
  140. package/numberUtils/__tests__/toNumberWithDefaultZero.test.ts +48 -0
  141. package/numberUtils/index.ts +27 -8
  142. package/package.json +2 -2
  143. package/pipesUtils/__tests__/buildUrlWithQuery.test.ts +33 -0
  144. package/pipesUtils/__tests__/withPipesEndpoint.test.tsx +56 -0
  145. package/pipesUtils/index.ts +1 -0
  146. package/pipesUtils/withPipesEndpoint.tsx +54 -0
  147. package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
  148. package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
  149. package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
  150. package/playerUtils/contentDataKeys.ts +134 -0
  151. package/playerUtils/index.ts +58 -15
  152. package/playerUtils/isAudioItem.ts +26 -0
  153. package/playerUtils/resolvePlayerActions.ts +71 -0
  154. package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
  155. package/reactHooks/actions/index.ts +117 -2
  156. package/reactHooks/cell-click/index.ts +18 -10
  157. package/reactHooks/feed/__mocks__/useMarkPipesDataStale.ts +4 -0
  158. package/reactHooks/feed/__tests__/useInflatedUrl.test.tsx +25 -0
  159. package/reactHooks/feed/index.ts +5 -1
  160. package/reactHooks/feed/useBatchLoading.ts +9 -1
  161. package/reactHooks/feed/{usePipesCacheReset.ts → useMarkPipesDataStale.ts} +12 -4
  162. package/reactHooks/index.ts +2 -0
  163. package/reactHooks/layout/index.ts +1 -1
  164. package/reactHooks/navigation/__mocks__/index.ts +4 -0
  165. package/reactHooks/navigation/__tests__/useIsScreenActive.test.ts +42 -0
  166. package/reactHooks/navigation/index.ts +1 -1
  167. package/reactHooks/navigation/useIsScreenActive.ts +29 -8
  168. package/reactHooks/state/useComponentScreenState.ts +1 -1
  169. package/reactHooks/usePluginConfiguration.ts +4 -1
  170. package/reactHooks/utils/__tests__/index.test.js +22 -2
  171. package/reactHooks/utils/index.ts +13 -2
  172. package/reactHooks/videoModal/hooks/useVideoModalScreenData.tsx +22 -4
  173. package/riverComponetsMeasurementProvider/index.tsx +12 -8
  174. package/stringUtils/__tests__/stringUtils.test.js +42 -0
  175. package/stringUtils/index.ts +2 -2
  176. package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
  177. package/uiActionsRegistrator/__tests__/subscribe.test.ts +107 -0
  178. package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
  179. package/uiActionsRegistrator/index.ts +25 -0
  180. package/uiActionsRegistrator/registry.ts +335 -0
  181. package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
  182. package/uiActionsRegistrator/usableActionItems.ts +66 -0
  183. package/zappFrameworkUtils/__tests__/localStorageHelper.test.ts +146 -0
  184. package/zappFrameworkUtils/localStorageHelper.ts +19 -15
  185. package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
  186. package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
  187. package/playerUtils/getPlayerActionButtons.ts +0 -17
  188. package/reactHooks/feed/__mocks__/usePipesCacheReset.ts +0 -1
  189. /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
@@ -0,0 +1,349 @@
1
+ import { renderHook, act, waitFor } from "@testing-library/react-native";
2
+
3
+ import { modalOrchestrator } from "../ModalOrchestrator";
4
+ import { MenuItem } from "../types";
5
+ import { useBottomSheetContent } from "../useBottomSheetContent";
6
+ import type { ContentState } from "../ContentViewModel";
7
+
8
+ jest.mock("../ModalOrchestrator", () => ({
9
+ isPresentSubMenuAction: jest.requireActual("../types").isPresentSubMenuAction,
10
+ modalOrchestrator: {
11
+ canGoBack: false,
12
+ back: jest.fn(),
13
+ close: jest.fn(),
14
+ },
15
+ }));
16
+
17
+ type MockViewModel = {
18
+ getState: jest.Mock<ContentState, []>;
19
+ subscribe: jest.Mock<(listener: (state: ContentState) => void) => () => void>;
20
+ refetch: jest.Mock<Promise<void>, []>;
21
+ };
22
+
23
+ const createViewModel = (
24
+ initialState: ContentState
25
+ ): MockViewModel & {
26
+ emit: (nextState: ContentState) => void;
27
+ } => {
28
+ let listener: ((state: ContentState) => void) | undefined;
29
+ const unsubscribe = jest.fn();
30
+
31
+ const viewModel: MockViewModel & { emit: (nextState: ContentState) => void } =
32
+ {
33
+ getState: jest.fn(() => initialState),
34
+ subscribe: jest.fn((nextListener) => {
35
+ listener = nextListener;
36
+
37
+ return unsubscribe;
38
+ }),
39
+ refetch: jest.fn().mockResolvedValue(undefined),
40
+ emit: (nextState) => {
41
+ listener?.(nextState);
42
+ },
43
+ };
44
+
45
+ return viewModel;
46
+ };
47
+
48
+ describe("useBottomSheetContent", () => {
49
+ const onPress = jest.fn().mockResolvedValue(undefined);
50
+
51
+ beforeEach(() => {
52
+ jest.clearAllMocks();
53
+ modalOrchestrator.canGoBack = false;
54
+ });
55
+
56
+ it("returns initial state from the view model", () => {
57
+ const viewModel = createViewModel({
58
+ items: [{ title: "Item 1" }],
59
+ isLoading: false,
60
+ });
61
+
62
+ const { result } = renderHook(() =>
63
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
64
+ );
65
+
66
+ expect(result.current.items).toEqual([{ title: "Item 1" }]);
67
+ expect(result.current.isLoading).toBe(false);
68
+ expect(result.current.error).toBeUndefined();
69
+ expect(result.current.showCentralLoading).toBe(false);
70
+ expect(result.current.canGoBack).toBe(false);
71
+ expect(viewModel.subscribe).toHaveBeenCalledTimes(1);
72
+ });
73
+
74
+ it("computes showCentralLoading when loading with no items", () => {
75
+ const viewModel = createViewModel({
76
+ items: [],
77
+ isLoading: true,
78
+ });
79
+
80
+ const { result } = renderHook(() =>
81
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
82
+ );
83
+
84
+ expect(result.current.showCentralLoading).toBe(true);
85
+ });
86
+
87
+ it("does not show central loading when items exist while loading", () => {
88
+ const viewModel = createViewModel({
89
+ items: [{ title: "Item 1" }],
90
+ isLoading: true,
91
+ });
92
+
93
+ const { result } = renderHook(() =>
94
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
95
+ );
96
+
97
+ expect(result.current.showCentralLoading).toBe(false);
98
+ });
99
+
100
+ it("updates when the view model emits new state", () => {
101
+ const viewModel = createViewModel({
102
+ items: [],
103
+ isLoading: true,
104
+ });
105
+
106
+ const { result } = renderHook(() =>
107
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
108
+ );
109
+
110
+ act(() => {
111
+ viewModel.emit({
112
+ items: [{ title: "Fetched Item" }],
113
+ isLoading: false,
114
+ });
115
+ });
116
+
117
+ expect(result.current.items).toEqual([{ title: "Fetched Item" }]);
118
+ expect(result.current.isLoading).toBe(false);
119
+ expect(result.current.showCentralLoading).toBe(false);
120
+ });
121
+
122
+ it("exposes error state from the view model", () => {
123
+ const viewModel = createViewModel({
124
+ items: [],
125
+ isLoading: false,
126
+ error: "Network Error",
127
+ });
128
+
129
+ const { result } = renderHook(() =>
130
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
131
+ );
132
+
133
+ expect(result.current.error).toBe("Network Error");
134
+ });
135
+
136
+ it("unsubscribes from the view model on unmount", () => {
137
+ const viewModel = createViewModel({
138
+ items: [],
139
+ isLoading: false,
140
+ });
141
+
142
+ const { unmount } = renderHook(() =>
143
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
144
+ );
145
+
146
+ const unsubscribeFn = viewModel.subscribe.mock.results[0]
147
+ .value as jest.Mock;
148
+
149
+ unmount();
150
+
151
+ expect(unsubscribeFn).toHaveBeenCalledTimes(1);
152
+ });
153
+
154
+ describe("handleItemPress", () => {
155
+ it("delegates to onPress and refetches for leaf items", async () => {
156
+ const viewModel = createViewModel({
157
+ items: [{ title: "Leaf" }],
158
+ isLoading: false,
159
+ });
160
+
161
+ const item: MenuItem = { title: "Leaf" };
162
+
163
+ const { result } = renderHook(() =>
164
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
165
+ );
166
+
167
+ await act(async () => {
168
+ await result.current.handleItemPress(item);
169
+ });
170
+
171
+ expect(onPress).toHaveBeenCalledWith(item);
172
+ expect(viewModel.refetch).toHaveBeenCalledTimes(1);
173
+ });
174
+
175
+ it("does not refetch when the item opens a present submenu action", async () => {
176
+ const viewModel = createViewModel({
177
+ items: [{ title: "Parent" }],
178
+ isLoading: false,
179
+ });
180
+
181
+ const item: MenuItem = {
182
+ title: "Parent",
183
+ action: {
184
+ type: "presentSubMenu",
185
+ menu: {
186
+ content: {
187
+ title: "Submenu",
188
+ items: [{ title: "Child" }],
189
+ },
190
+ },
191
+ },
192
+ };
193
+
194
+ const { result } = renderHook(() =>
195
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
196
+ );
197
+
198
+ await act(async () => {
199
+ await result.current.handleItemPress(item);
200
+ });
201
+
202
+ expect(onPress).toHaveBeenCalledWith(item);
203
+ expect(viewModel.refetch).not.toHaveBeenCalled();
204
+ });
205
+
206
+ it("awaits onPress before refetching", async () => {
207
+ const viewModel = createViewModel({
208
+ items: [{ title: "Leaf" }],
209
+ isLoading: false,
210
+ });
211
+
212
+ const item: MenuItem = { title: "Leaf" };
213
+ const callOrder: string[] = [];
214
+
215
+ onPress.mockImplementation(async () => {
216
+ callOrder.push("onPress");
217
+ });
218
+
219
+ viewModel.refetch.mockImplementation(async () => {
220
+ callOrder.push("refetch");
221
+ });
222
+
223
+ const { result } = renderHook(() =>
224
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
225
+ );
226
+
227
+ await act(async () => {
228
+ await result.current.handleItemPress(item);
229
+ });
230
+
231
+ expect(callOrder).toEqual(["onPress", "refetch"]);
232
+ });
233
+ });
234
+
235
+ describe("goBackOrClose", () => {
236
+ it("calls back when the orchestrator can go back", () => {
237
+ modalOrchestrator.canGoBack = true;
238
+
239
+ const viewModel = createViewModel({
240
+ items: [],
241
+ isLoading: false,
242
+ });
243
+
244
+ const { result } = renderHook(() =>
245
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
246
+ );
247
+
248
+ act(() => {
249
+ result.current.goBackOrClose();
250
+ });
251
+
252
+ expect(modalOrchestrator.back).toHaveBeenCalledTimes(1);
253
+ expect(modalOrchestrator.close).not.toHaveBeenCalled();
254
+ });
255
+
256
+ it("calls close when the orchestrator cannot go back", () => {
257
+ modalOrchestrator.canGoBack = false;
258
+
259
+ const viewModel = createViewModel({
260
+ items: [],
261
+ isLoading: false,
262
+ });
263
+
264
+ const { result } = renderHook(() =>
265
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
266
+ );
267
+
268
+ act(() => {
269
+ result.current.goBackOrClose();
270
+ });
271
+
272
+ expect(modalOrchestrator.close).toHaveBeenCalledTimes(1);
273
+ expect(modalOrchestrator.back).not.toHaveBeenCalled();
274
+ });
275
+ });
276
+
277
+ it("exposes close from the modal orchestrator", () => {
278
+ const viewModel = createViewModel({
279
+ items: [],
280
+ isLoading: false,
281
+ });
282
+
283
+ const { result } = renderHook(() =>
284
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
285
+ );
286
+
287
+ act(() => {
288
+ result.current.close();
289
+ });
290
+
291
+ expect(modalOrchestrator.close).toHaveBeenCalledTimes(1);
292
+ });
293
+
294
+ it("reflects canGoBack from the modal orchestrator", () => {
295
+ modalOrchestrator.canGoBack = true;
296
+
297
+ const viewModel = createViewModel({
298
+ items: [],
299
+ isLoading: false,
300
+ });
301
+
302
+ const { result } = renderHook(() =>
303
+ useBottomSheetContent({ viewModel: viewModel as any, onPress })
304
+ );
305
+
306
+ expect(result.current.canGoBack).toBe(true);
307
+ });
308
+
309
+ it("resubscribes when the view model instance changes", async () => {
310
+ const firstViewModel = createViewModel({
311
+ items: [{ title: "First" }],
312
+ isLoading: false,
313
+ });
314
+
315
+ const secondViewModel = createViewModel({
316
+ items: [{ title: "Second" }],
317
+ isLoading: false,
318
+ });
319
+
320
+ const { result, rerender } = renderHook(
321
+ ({ viewModel }) =>
322
+ useBottomSheetContent({ viewModel: viewModel as any, onPress }),
323
+ {
324
+ initialProps: { viewModel: firstViewModel },
325
+ }
326
+ );
327
+
328
+ expect(result.current.items).toEqual([{ title: "First" }]);
329
+
330
+ rerender({ viewModel: secondViewModel });
331
+
332
+ expect(
333
+ firstViewModel.subscribe.mock.results[0].value
334
+ ).toHaveBeenCalledTimes(1);
335
+
336
+ expect(secondViewModel.subscribe).toHaveBeenCalledTimes(1);
337
+
338
+ act(() => {
339
+ secondViewModel.emit({
340
+ items: [{ title: "Updated Second" }],
341
+ isLoading: false,
342
+ });
343
+ });
344
+
345
+ await waitFor(() => {
346
+ expect(result.current.items).toEqual([{ title: "Updated Second" }]);
347
+ });
348
+ });
349
+ });
@@ -1,83 +1,35 @@
1
- import { useStore, createStore } from "zustand";
2
-
3
- interface ModalState {
4
- visible: boolean;
5
- screen: any;
6
- options: Record<string, unknown>;
7
- props: Record<string, unknown>;
8
- }
9
-
10
- interface ModalStore {
11
- modalState: ModalState;
12
- setModalState: (newState: Partial<ModalState>) => void;
13
- openModal: (
14
- item: any,
15
- options?: Record<string, unknown>,
16
- props?: Record<string, unknown>
17
- ) => void;
18
- dismissModal: () => void;
19
- }
20
-
21
- const initialModalState: ModalState = {
22
- visible: false,
23
- screen: null,
24
- options: {},
25
- props: {},
26
- };
27
-
28
- export const modalStore = createStore<ModalStore>((set) => ({
29
- modalState: initialModalState,
30
- setModalState: (newState) =>
31
- set((state) => ({
32
- modalState: { ...state.modalState, ...newState },
33
- })),
34
- openModal: ({ item, options = {}, props }: OpenModalArg) =>
35
- set({
36
- modalState: {
37
- visible: true,
38
- screen: item,
39
- options: {
40
- animated: true,
41
- animationType: "slide",
42
- onShow: () => {},
43
- presentationStyle: "overFullScreen",
44
- transparent: true,
45
- ...options,
46
- onDismiss: () => set({ modalState: initialModalState }),
47
- onRequestClose: () => set({ modalState: initialModalState }),
48
- },
49
- props,
50
- },
51
- }),
52
- dismissModal: () => set(() => ({ modalState: initialModalState })),
53
- }));
54
-
55
- export const useModalStore = <T>(selector: (state: ModalStore) => T) =>
56
- useStore(modalStore, selector);
57
-
58
- export const openModal = modalStore.getInitialState().openModal;
59
-
60
- export const dismissModal = modalStore.getInitialState().dismissModal;
61
-
62
- export const openBottomSheetModal = ({
63
- ModalBottomSheetContent,
64
- modalBottomSheetContentProps,
65
- props = {},
66
- options = {},
67
- }: OpenModalBottomSheetArgs) => {
68
- openModal({
69
- item: {
70
- ModalBottomSheetContent,
71
- modalBottomSheetContentProps,
72
- },
73
- props,
74
- options: {
75
- animationType: "none",
76
- animated: false,
77
- ...options,
78
- },
79
- });
80
- };
81
-
82
- export const useModalStoreState = (): ModalState =>
83
- useModalStore<ModalState>((state) => state.modalState);
1
+ export {
2
+ modalStore,
3
+ useModalStore,
4
+ openModal,
5
+ dismissModal,
6
+ openBottomSheetModal,
7
+ useModalStoreState,
8
+ } from "./store";
9
+
10
+ export type { ModalState, ModalStore } from "./store";
11
+
12
+ export { modalOrchestrator } from "./ModalOrchestrator";
13
+
14
+ export { ContentViewModel } from "./ContentViewModel";
15
+
16
+ export type {
17
+ Menu,
18
+ MenuItem,
19
+ Header,
20
+ Content,
21
+ Action,
22
+ PresentSubMenuAction,
23
+ SecondaryAction,
24
+ SelectMode,
25
+ SelectionBehavior,
26
+ } from "./types";
27
+
28
+ export {
29
+ isPresentSubMenuAction,
30
+ getMenuItemActions,
31
+ OPERATIONS,
32
+ hasOperation,
33
+ } from "./types";
34
+
35
+ export { useBottomSheetContent } from "./useBottomSheetContent";
@@ -0,0 +1,102 @@
1
+ import { createStore, useStore } from "zustand";
2
+
3
+ export interface ModalState {
4
+ visible: boolean;
5
+ screen: any;
6
+ options: Record<string, unknown>;
7
+ props: Record<string, unknown>;
8
+ }
9
+
10
+ export interface ModalStore {
11
+ modalState: ModalState;
12
+ setModalState: (newState: Partial<ModalState>) => void;
13
+ openModal: (
14
+ item: any,
15
+ options?: Record<string, unknown>,
16
+ props?: Record<string, unknown>
17
+ ) => void;
18
+ dismissModal: () => void;
19
+ }
20
+
21
+ const initialModalState: ModalState = {
22
+ visible: false,
23
+ screen: null,
24
+ options: {},
25
+ props: {},
26
+ };
27
+
28
+ export const modalStore = createStore<ModalStore>((set, get) => ({
29
+ modalState: initialModalState,
30
+ setModalState: (newState) =>
31
+ set((state) => ({
32
+ modalState: { ...state.modalState, ...newState },
33
+ })),
34
+ openModal: ({ item, options = {}, props }: OpenModalArg) => {
35
+ const resetStore = () => set({ modalState: initialModalState });
36
+ const { onDismiss, onRequestClose, ...restOptions } = options as any;
37
+
38
+ set({
39
+ modalState: {
40
+ visible: true,
41
+ screen: item,
42
+ options: {
43
+ animated: true,
44
+ animationType: "slide",
45
+ onShow: () => {},
46
+ presentationStyle: "overFullScreen",
47
+ transparent: true,
48
+ ...restOptions,
49
+ // Compose caller-provided callbacks with the store reset so both run
50
+ onDismiss: () => {
51
+ resetStore();
52
+ onDismiss?.();
53
+ },
54
+ onRequestClose: () => {
55
+ resetStore();
56
+ onRequestClose?.();
57
+ },
58
+ },
59
+ props,
60
+ },
61
+ });
62
+ },
63
+ dismissModal: () => {
64
+ const onDismiss = (get().modalState?.options as any)?.onDismiss;
65
+
66
+ if (typeof onDismiss === "function") {
67
+ onDismiss();
68
+ } else {
69
+ set({ modalState: initialModalState });
70
+ }
71
+ },
72
+ }));
73
+
74
+ export const useModalStore = <T>(selector: (state: ModalStore) => T) =>
75
+ useStore(modalStore, selector);
76
+
77
+ export const openModal = modalStore.getInitialState().openModal;
78
+
79
+ export const dismissModal = modalStore.getInitialState().dismissModal;
80
+
81
+ export const openBottomSheetModal = ({
82
+ ModalBottomSheetContent,
83
+ modalBottomSheetContentProps,
84
+ props = {},
85
+ options = {},
86
+ }: OpenModalBottomSheetArgs) => {
87
+ openModal({
88
+ item: {
89
+ ModalBottomSheetContent,
90
+ modalBottomSheetContentProps,
91
+ },
92
+ props,
93
+ options: {
94
+ animationType: "none",
95
+ animated: false,
96
+ ...options,
97
+ },
98
+ });
99
+ };
100
+
101
+ export const useModalStoreState = (): ModalState =>
102
+ useModalStore<ModalState>((state) => state.modalState);