@applicaster/zapp-react-native-ui-components 15.1.14 → 15.1.15-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/Components/GeneralContentScreen/GeneralContentScreenHookAdapter.tsx +15 -0
  2. package/Components/MasterCell/DefaultComponents/ActionButton.tsx +4 -1
  3. package/Components/ModalComponent/ActionsBottomSheet/ActionButton.tsx +56 -0
  4. package/Components/ModalComponent/ActionsBottomSheet/__tests__/openActionsBottomSheet.test.ts +38 -0
  5. package/Components/ModalComponent/ActionsBottomSheet/boundActionButton.tsx +24 -0
  6. package/Components/ModalComponent/ActionsBottomSheet/index.ts +9 -0
  7. package/Components/ModalComponent/ActionsBottomSheet/openActionsBottomSheet.ts +61 -0
  8. package/Components/ModalComponent/AudioPlayer/Components/Button.tsx +58 -97
  9. package/Components/ModalComponent/AudioPlayer/Components/Header.tsx +142 -142
  10. package/Components/ModalComponent/AudioPlayer/Components/Input.tsx +628 -0
  11. package/Components/ModalComponent/AudioPlayer/Components/Item.tsx +448 -175
  12. package/Components/ModalComponent/AudioPlayer/Components/NowPlayingHeaderSection.tsx +94 -0
  13. package/Components/ModalComponent/AudioPlayer/Components/__tests__/Item.test.tsx +113 -0
  14. package/Components/ModalComponent/AudioPlayer/Components/index.ts +6 -4
  15. package/Components/ModalComponent/AudioPlayer/utils/__tests__/mergeStyles.test.ts +248 -0
  16. package/Components/ModalComponent/AudioPlayer/utils/mergeStyles.ts +386 -0
  17. package/Components/ModalComponent/BottomSheetModalContent.tsx +425 -66
  18. package/Components/ModalComponent/Button/index.tsx +5 -3
  19. package/Components/ModalComponent/Header/index.tsx +20 -7
  20. package/Components/ModalComponent/Header/utils.ts +1 -1
  21. package/Components/ModalComponent/ModalBlocksStyleContext.tsx +10 -0
  22. package/Components/ModalComponent/SortableList.tsx +157 -0
  23. package/Components/ModalComponent/SortableList.web.tsx +102 -0
  24. package/Components/ModalComponent/TextInputContent.tsx +112 -0
  25. package/Components/ModalComponent/__tests__/BottomSheetModalContent.test.tsx +255 -0
  26. package/Components/ModalComponent/__tests__/TextInputContent.test.tsx +156 -0
  27. package/Components/ModalComponent/__tests__/showTextInput.test.ts +95 -0
  28. package/Components/ModalComponent/index.tsx +18 -9
  29. package/Components/ModalComponent/presets/__tests__/getCell.test.ts +90 -0
  30. package/Components/ModalComponent/presets/dynamicCollectionCell.tsx +78 -0
  31. package/Components/ModalComponent/presets/index.ts +27 -0
  32. package/Components/ModalComponent/presets/selectableCell.tsx +28 -0
  33. package/Components/ModalComponent/presets/standardCell.tsx +41 -0
  34. package/Components/ModalComponent/presets/types.ts +35 -0
  35. package/Components/ModalComponent/showTextInput.ts +43 -0
  36. package/Components/ModalComponent/utils.ts +82 -21
  37. package/Components/River/ComponentsMap/ComponentsMap.tsx +87 -60
  38. package/Components/River/__tests__/__snapshots__/componentsMap.test.js.snap +139 -75
  39. package/Components/River/__tests__/componentsMap.test.js +8 -3
  40. package/Components/Screen/navigationHandler.ts +8 -8
  41. package/Contexts/ModalNavigationContext/__tests__/index.test.tsx +15 -0
  42. package/Contexts/ModalNavigationContext/index.tsx +2 -2
  43. package/Hooks/useEntryActionsExpander.ts +63 -0
  44. package/package.json +6 -5
  45. package/Components/ModalComponent/AudioPlayer/Components/Action.tsx +0 -368
  46. /package/Decorators/ZappPipesDataConnector/__tests__/{Hero.js → Hero.tsx} +0 -0
@@ -1,16 +1,65 @@
1
- import React, { useCallback, useEffect, useMemo, useRef } from "react";
2
- import { View, ScrollView } from "react-native";
1
+ import React, {
2
+ memo,
3
+ useCallback,
4
+ useDeferredValue,
5
+ useEffect,
6
+ useMemo,
7
+ useRef,
8
+ } from "react";
9
+ import {
10
+ ActivityIndicator,
11
+ ScrollView,
12
+ StyleSheet,
13
+ Text,
14
+ View,
15
+ } from "react-native";
3
16
  import { Button } from "./Button";
4
17
  import { ItemIconProps } from "./Button/ItemIcon";
5
18
  import { ItemProps } from "./Button/Item";
6
19
  import { ItemLabelProps } from "./Button/ItemLabel";
7
20
  import { getItemIconProps, getItemLabelProps, getItemProps } from "./utils";
8
21
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
22
+ import {
23
+ ContentViewModel,
24
+ OPERATIONS,
25
+ hasOperation,
26
+ useBottomSheetContent,
27
+ } from "@applicaster/zapp-react-native-utils/modalState";
28
+ import { getCell } from "./presets";
9
29
  import type { PluginConfiguration } from "./";
10
-
11
30
  import { ModalHeader as DefaultModalHeader } from "./Header";
12
31
  import { useSafeAreaInsets } from "react-native-safe-area-context";
32
+ import { SortableList } from "./SortableList";
33
+ import {
34
+ AudioPlayerButton,
35
+ NowPlayingHeaderSection,
36
+ VARIANT_WITH_ICON,
37
+ } from "./AudioPlayer/Components";
13
38
  import { BottomSheetDragArea } from "./BottomSheetDragArea";
39
+ import { ModalBlocksStyleContext } from "./ModalBlocksStyleContext";
40
+ import { usePlugins } from "@applicaster/zapp-react-native-redux/hooks";
41
+ import {
42
+ ModalBlocksConfig,
43
+ parseModalBlocksConfiguration,
44
+ } from "@applicaster/zapp-react-native-utils/configurationUtils/modalBlocksParser";
45
+ import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/localizationUtils";
46
+
47
+ const styles = StyleSheet.create({
48
+ centered: {
49
+ justifyContent: "center",
50
+ alignItems: "center",
51
+ padding: 16,
52
+ },
53
+ errorText: {
54
+ color: "#ff453a",
55
+ fontSize: 14,
56
+ textAlign: "center",
57
+ },
58
+ scrollView: {
59
+ flexShrink: 1,
60
+ },
61
+ someStyles: { paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12 },
62
+ });
14
63
 
15
64
  type ModalComponentProps = {
16
65
  items: any[];
@@ -22,13 +71,7 @@ type ModalComponentProps = {
22
71
  title?: string;
23
72
  maxHeight?: number;
24
73
  dismiss: () => void;
25
- headerComponent?: React.ComponentType;
26
-
27
- scrollViewContent?: {
28
- renderItem: (item: any, index: number) => React.ReactNode;
29
- items: any[];
30
- } | null;
31
-
74
+ headerComponent?: React.ComponentType<any>;
32
75
  buttonComponent?: React.ComponentType;
33
76
  iconProps?: ItemIconProps | ((theme: PluginConfiguration) => ItemIconProps);
34
77
  itemProps?:
@@ -37,16 +80,22 @@ type ModalComponentProps = {
37
80
  labelProps?:
38
81
  | ItemLabelProps
39
82
  | ((theme: PluginConfiguration, width: number) => ItemLabelProps);
40
- getSelectedItemIcon: (
83
+ getSelectedItemIcon?: (
41
84
  theme: PluginConfiguration
42
85
  ) => ItemIconProps["asset"] | null;
43
- getDefaultItemIcon: (
86
+ getDefaultItemIcon?: (
44
87
  theme: PluginConfiguration
45
88
  ) => ItemIconProps["asset"] | null;
46
89
  iconPlacement?: "left" | "right";
90
+ viewModel?: ContentViewModel;
91
+ /** Optional body that replaces the items list (header stays DefaultModalHeader). */
92
+ contentComponent?: React.ComponentType<any>;
93
+ contentComponentProps?: Record<string, any>;
94
+ styleOverrides?: ModalBlocksConfig | Record<string, any>;
95
+ measurementPhase?: boolean;
47
96
  };
48
97
 
49
- export function BottomSheetModalContent(props: ModalComponentProps) {
98
+ function _BottomSheetModalContent(props: ModalComponentProps) {
50
99
  const {
51
100
  items,
52
101
  currentRoute,
@@ -55,23 +104,110 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
55
104
  dismiss,
56
105
  summary,
57
106
  title,
58
- headerComponent = DefaultModalHeader,
59
- scrollViewContent = null,
60
-
107
+ headerComponent,
108
+ viewModel = null,
61
109
  buttonComponent: ButtonComponent = Button,
62
110
  getSelectedItemIcon = ({
63
111
  modal_bottom_sheet_item_selected_icon,
64
112
  }: BaseThemePropertiesMobile) => modal_bottom_sheet_item_selected_icon,
65
113
  getDefaultItemIcon = () => null,
66
114
  iconPlacement,
115
+ contentComponent: ContentComponent,
116
+ contentComponentProps = {},
117
+ styleOverrides: propStyleOverrides,
118
+ measurementPhase = false,
67
119
  } = props;
68
120
 
69
121
  const route = useRef(currentRoute);
70
122
  const theme = useTheme<BaseThemePropertiesMobile>();
123
+ const bottomInset = useSafeAreaInsets().bottom;
71
124
  const paddingTop = Number(theme.modal_bottom_sheet_padding_top);
72
125
  const paddingBottom = Number(theme.modal_bottom_sheet_padding_bottom);
73
126
 
74
- const Header = headerComponent;
127
+ const calculatedcontentContainerStyle = useMemo(() => {
128
+ return {
129
+ paddingBottom: paddingBottom + bottomInset,
130
+ };
131
+ }, [paddingBottom, bottomInset]);
132
+
133
+ const {
134
+ items: fetchedItems,
135
+ showCentralLoading,
136
+ error,
137
+ handleItemPress,
138
+ role,
139
+ behavior,
140
+ editableCollection,
141
+ themePluginId,
142
+ } = useBottomSheetContent({
143
+ onPress,
144
+ viewModel,
145
+ });
146
+
147
+ const plugins = usePlugins();
148
+
149
+ const plugin = useMemo(
150
+ () => plugins?.find((p) => p.identifier === themePluginId),
151
+ [plugins, themePluginId]
152
+ );
153
+
154
+ const styleOverrides = useMemo(() => {
155
+ if (propStyleOverrides) {
156
+ return propStyleOverrides;
157
+ }
158
+
159
+ const configuration =
160
+ plugin?.configuration || (plugin as any)?.configuration_json;
161
+
162
+ if (!configuration || !themePluginId) return null;
163
+
164
+ return parseModalBlocksConfiguration(configuration, themePluginId);
165
+ }, [propStyleOverrides, plugin, themePluginId]);
166
+
167
+ const pluginLocalizations = useLocalizedStrings({
168
+ localizations:
169
+ plugin?.configuration?.localizations ||
170
+ (plugin as any)?.configuration_json?.localizations,
171
+ });
172
+
173
+ const normalizedPluginId = themePluginId?.replace(/-/g, "_");
174
+
175
+ const localizedHeaderTitle = normalizedPluginId
176
+ ? pluginLocalizations?.[`${normalizedPluginId}_header_title`]
177
+ : undefined;
178
+
179
+ const createPlaylistTitle =
180
+ pluginLocalizations?.create_new_playlist_title || "Create New Playlist";
181
+
182
+ const nextUpTitle =
183
+ pluginLocalizations?.next_up_title ||
184
+ (normalizedPluginId
185
+ ? pluginLocalizations?.[`${normalizedPluginId}_next_up_title`]
186
+ : undefined) ||
187
+ "Next Up";
188
+
189
+ const nowPlayingTitle =
190
+ pluginLocalizations?.now_playing_title ||
191
+ (normalizedPluginId
192
+ ? pluginLocalizations?.[`${normalizedPluginId}_now_playing_title`]
193
+ : undefined) ||
194
+ "Now Playing";
195
+
196
+ const resolvedTitle = props.title || localizedHeaderTitle || title;
197
+
198
+ const itemsToRender = viewModel ? fetchedItems : items;
199
+ const operations = editableCollection?.operations || [];
200
+
201
+ const isSortable = useDeferredValue(
202
+ !measurementPhase && hasOperation(operations, OPERATIONS.REORDER)
203
+ );
204
+
205
+ const cell = getCell(
206
+ viewModel ? role : undefined,
207
+ viewModel ? behavior : undefined
208
+ );
209
+
210
+ const Header = headerComponent ?? DefaultModalHeader;
75
211
 
76
212
  useEffect(() => {
77
213
  if (currentRoute !== route.current) {
@@ -84,14 +220,24 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
84
220
  }, [theme, props.iconProps]);
85
221
 
86
222
  const itemBaseProps = useMemo<ItemProps>(() => {
87
- return getItemProps(theme, props.width, props.itemProps);
88
- }, [theme, props.width, props.itemProps]);
223
+ return getItemProps(
224
+ theme,
225
+ props.width,
226
+ props.itemProps,
227
+ styleOverrides?.item
228
+ );
229
+ }, [theme, props.width, props.itemProps, styleOverrides?.item]);
89
230
 
90
231
  const labelBaseProps = useMemo<ItemLabelProps>(() => {
91
- return getItemLabelProps(theme, props.width, props.labelProps);
92
- }, [theme, props.width, props.labelProps]);
232
+ return getItemLabelProps(
233
+ theme,
234
+ props.width,
235
+ props.labelProps,
236
+ styleOverrides?.item
237
+ );
238
+ }, [theme, props.width, props.labelProps, styleOverrides?.item]);
93
239
 
94
- const handlePress = useCallback(
240
+ const handleStaticPress = useCallback(
95
241
  (item: any) => {
96
242
  onPress(item);
97
243
  dismiss();
@@ -99,58 +245,271 @@ export function BottomSheetModalContent(props: ModalComponentProps) {
99
245
  [onPress, dismiss]
100
246
  );
101
247
 
102
- const bottomInset = useSafeAreaInsets().bottom;
248
+ const onItemPress = viewModel ? handleItemPress : handleStaticPress;
249
+
250
+ const handleDragEnd = useCallback(
251
+ (params: any) => {
252
+ const from = params?.fromIndex ?? params?.from;
253
+ const to = params?.toIndex ?? params?.to;
254
+
255
+ const reorderedItemIds = Array.isArray(params?.data)
256
+ ? params.data
257
+ .filter((item: any) => !item?.isHeader)
258
+ .map((item: any) => item?.id)
259
+ .filter(Boolean)
260
+ : undefined;
261
+
262
+ if (
263
+ (reorderedItemIds?.length ||
264
+ (typeof from === "number" &&
265
+ typeof to === "number" &&
266
+ from !== to)) &&
267
+ editableCollection?.move
268
+ ) {
269
+ editableCollection.move(reorderedItemIds || []);
270
+ }
271
+ },
272
+ [editableCollection]
273
+ );
274
+
275
+ const renderContext = useMemo(
276
+ () => ({
277
+ theme,
278
+ ButtonComponent,
279
+ current_selection,
280
+ iconBaseProps,
281
+ itemBaseProps,
282
+ labelBaseProps,
283
+ getSelectedItemIcon,
284
+ getDefaultItemIcon,
285
+ iconPlacement,
286
+ editableCollection,
287
+ behavior,
288
+ }),
289
+ [
290
+ theme,
291
+ ButtonComponent,
292
+ current_selection,
293
+ iconBaseProps,
294
+ itemBaseProps,
295
+ labelBaseProps,
296
+ getSelectedItemIcon,
297
+ getDefaultItemIcon,
298
+ iconPlacement,
299
+ editableCollection,
300
+ behavior,
301
+ ]
302
+ );
303
+
304
+ const sortableListRenderItem = useCallback(
305
+ ({ item, index, renderHandle }) =>
306
+ cell.renderItem({
307
+ item,
308
+ index,
309
+ onPress: onItemPress,
310
+ context: renderContext,
311
+ renderHandle,
312
+ }),
313
+ [cell, onItemPress, renderContext]
314
+ );
315
+
316
+ const hasNowPlaying = hasOperation(operations, OPERATIONS.NOW_PLAYING);
317
+ const hasClearAll = hasOperation(operations, OPERATIONS.CLEAR_ALL);
318
+ const hasAdd = hasOperation(operations, OPERATIONS.ADD);
319
+
320
+ const handleDeleteCollection = useCallback(() => {
321
+ editableCollection?.deleteCollection?.();
322
+ }, [editableCollection]);
323
+
324
+ const handleCreatePlaylist = useCallback(() => {
325
+ editableCollection?.add?.({
326
+ id: "create_new_playlist",
327
+ title: createPlaylistTitle,
328
+ });
329
+ }, [editableCollection, createPlaylistTitle]);
330
+
331
+ const headers = useMemo(() => {
332
+ const list: Array<{ index: number; component: () => React.ReactElement }> =
333
+ [];
334
+
335
+ if (hasNowPlaying) {
336
+ list.push({
337
+ index: list.length,
338
+ component: () => (
339
+ <NowPlayingHeaderSection
340
+ onClear={hasClearAll ? handleDeleteCollection : undefined}
341
+ nextUpTitle={nextUpTitle}
342
+ nowPlayingTitle={nowPlayingTitle}
343
+ />
344
+ ),
345
+ });
346
+ }
347
+
348
+ if (hasAdd) {
349
+ list.push({
350
+ index: list.length,
351
+ component: () => (
352
+ <View
353
+ style={{ paddingHorizontal: 20, paddingTop: 8, paddingBottom: 12 }}
354
+ >
355
+ <AudioPlayerButton
356
+ configuration={VARIANT_WITH_ICON}
357
+ data={{ title: createPlaylistTitle }}
358
+ onPress={handleCreatePlaylist}
359
+ />
360
+ </View>
361
+ ),
362
+ });
363
+ }
364
+
365
+ return list.length > 0 ? list : undefined;
366
+ }, [
367
+ hasNowPlaying,
368
+ hasClearAll,
369
+ handleDeleteCollection,
370
+ nextUpTitle,
371
+ nowPlayingTitle,
372
+ hasAdd,
373
+ handleCreatePlaylist,
374
+ createPlaylistTitle,
375
+ ]);
376
+
377
+ const _items = useMemo(() => {
378
+ return (itemsToRender || []).map((item, index) =>
379
+ cell.renderItem({
380
+ item,
381
+ index,
382
+ onPress: onItemPress,
383
+ context: renderContext,
384
+ })
385
+ );
386
+ }, [itemsToRender, cell, onItemPress, renderContext]);
387
+
388
+ const _styles = useMemo(() => {
389
+ return {
390
+ maxHeight: props.maxHeight,
391
+ paddingTop: paddingTop,
392
+ };
393
+ }, [props.maxHeight, paddingTop]);
394
+
395
+ const renderedBody = (() => {
396
+ if (ContentComponent) {
397
+ const shouldWrapInScrollView =
398
+ !contentComponentProps?.disableScrollViewWrap &&
399
+ !(ContentComponent as any)?.disableScrollViewWrap;
400
+
401
+ if (shouldWrapInScrollView) {
402
+ return (
403
+ <ScrollView
404
+ keyboardShouldPersistTaps="handled"
405
+ contentContainerStyle={calculatedcontentContainerStyle}
406
+ >
407
+ <ContentComponent
408
+ {...contentComponentProps}
409
+ dismiss={dismiss}
410
+ width={props.width}
411
+ maxHeight={props.maxHeight}
412
+ />
413
+ </ScrollView>
414
+ );
415
+ }
416
+
417
+ return (
418
+ <View style={[styles.scrollView, calculatedcontentContainerStyle]}>
419
+ <ContentComponent
420
+ {...contentComponentProps}
421
+ dismiss={dismiss}
422
+ width={props.width}
423
+ maxHeight={props.maxHeight}
424
+ />
425
+ </View>
426
+ );
427
+ }
428
+
429
+ if (showCentralLoading) {
430
+ const loaderColor =
431
+ (theme as any)?.modal_bottom_sheet_activity_indicator_color ||
432
+ (theme as any)?.cell_title_color ||
433
+ "#ffffff";
434
+
435
+ return (
436
+ <View
437
+ style={[
438
+ styles.centered,
439
+ { backgroundColor: theme.modal_bottom_sheet_background_color },
440
+ ]}
441
+ >
442
+ <ActivityIndicator color={loaderColor} />
443
+ </View>
444
+ );
445
+ }
446
+
447
+ if (error) {
448
+ return (
449
+ <View style={styles.centered}>
450
+ <Text style={styles.errorText}>
451
+ {typeof error === "string" ? error : error.message}
452
+ </Text>
453
+ </View>
454
+ );
455
+ }
456
+
457
+ if (isSortable) {
458
+ return (
459
+ <SortableList
460
+ sortableData={itemsToRender}
461
+ headers={headers}
462
+ scrollViewStyle={styles.scrollView}
463
+ contentContainerStyle={calculatedcontentContainerStyle}
464
+ onDragEnd={handleDragEnd}
465
+ renderItem={sortableListRenderItem}
466
+ />
467
+ );
468
+ }
469
+
470
+ return (
471
+ <ScrollView
472
+ keyboardShouldPersistTaps="handled"
473
+ overScrollMode="never"
474
+ bounces={false}
475
+ contentContainerStyle={calculatedcontentContainerStyle}
476
+ >
477
+ {hasNowPlaying ? (
478
+ <NowPlayingHeaderSection
479
+ onClear={hasClearAll ? handleDeleteCollection : undefined}
480
+ nextUpTitle={nextUpTitle}
481
+ nowPlayingTitle={nowPlayingTitle}
482
+ />
483
+ ) : null}
484
+ {hasAdd ? (
485
+ <View style={styles.someStyles}>
486
+ <AudioPlayerButton
487
+ configuration={VARIANT_WITH_ICON}
488
+ data={{ title: createPlaylistTitle }}
489
+ onPress={handleCreatePlaylist}
490
+ />
491
+ </View>
492
+ ) : null}
493
+ {_items}
494
+ </ScrollView>
495
+ );
496
+ })();
103
497
 
104
498
  return (
105
- <View
106
- style={{
107
- maxHeight: props.maxHeight,
108
- paddingTop: paddingTop,
109
- }}
110
- >
111
- {Header ? (
499
+ <ModalBlocksStyleContext.Provider value={styleOverrides as any}>
500
+ <View style={_styles}>
112
501
  <BottomSheetDragArea>
113
502
  <Header
114
503
  dismiss={dismiss}
115
504
  configuration={theme}
116
505
  summary={summary}
117
- title={title}
506
+ title={resolvedTitle}
118
507
  />
119
508
  </BottomSheetDragArea>
120
- ) : null}
121
- <ScrollView
122
- overScrollMode="never"
123
- bounces={false}
124
- showsVerticalScrollIndicator={false}
125
- contentContainerStyle={{
126
- paddingBottom: paddingBottom + bottomInset,
127
- }}
128
- >
129
- {scrollViewContent
130
- ? scrollViewContent.items.map((item, index) =>
131
- scrollViewContent.renderItem(item, index)
132
- )
133
- : null}
134
- {!scrollViewContent &&
135
- items.map((item, index) =>
136
- item ? (
137
- <ButtonComponent
138
- key={index}
139
- configuration={theme}
140
- selectedItem={current_selection}
141
- item={item}
142
- onPress={handlePress}
143
- label={theme[item?.label] ?? item?.label}
144
- iconBaseProps={iconBaseProps}
145
- itemBaseProps={itemBaseProps}
146
- labelBaseProps={labelBaseProps}
147
- selectedItemIcon={getSelectedItemIcon(theme)}
148
- defaultItemIcon={getDefaultItemIcon(theme)}
149
- iconPlacement={iconPlacement}
150
- />
151
- ) : null
152
- )}
153
- </ScrollView>
154
- </View>
509
+ {renderedBody}
510
+ </View>
511
+ </ModalBlocksStyleContext.Provider>
155
512
  );
156
513
  }
514
+
515
+ export const BottomSheetModalContent = memo(_BottomSheetModalContent);
@@ -43,11 +43,13 @@ export function Button({
43
43
  }: ButtonProps) {
44
44
  const [focused, setFocused] = useState(false);
45
45
 
46
- const selected = selectedItem && item.value === selectedItem?.value;
46
+ const selected = selectedItem && item?.value === selectedItem?.value;
47
47
 
48
48
  const itemIconPropsAssets = useMemo<ItemIconProps["asset"]>(
49
- () => configuration[item.asset] ?? item.asset ?? defaultItemIcon,
50
- [item.asset, defaultItemIcon]
49
+ () =>
50
+ (item ? (configuration[item.asset] ?? item.asset) : undefined) ??
51
+ defaultItemIcon,
52
+ [item?.asset, defaultItemIcon]
51
53
  );
52
54
 
53
55
  const selectedItemIconPropsAssets = useMemo<ItemIconProps["asset"]>(
@@ -4,6 +4,7 @@ import { Title, TitleProps } from "./Title";
4
4
  import { CloseButton, CloseButtonProps } from "./CloseButton";
5
5
  import { getCloseButtonProps, getTitleProps } from "./utils";
6
6
  import { PluginConfiguration } from "../index";
7
+ import { useModalBlocksStyle } from "../ModalBlocksStyleContext";
7
8
 
8
9
  type Props = {
9
10
  dismiss: () => void;
@@ -28,6 +29,14 @@ const styles = StyleSheet.create({
28
29
 
29
30
  export function ModalHeader(props: Props) {
30
31
  const { configuration, dismiss, onLayout, summary, title } = props;
32
+ const modalBlocksStyle = useModalBlocksStyle();
33
+ const headerBlock = modalBlocksStyle?.header;
34
+
35
+ const containerStyle = useMemo(() => {
36
+ const bg = headerBlock?.container?.default?.backgroundColor;
37
+
38
+ return [styles.container, bg ? { backgroundColor: bg } : null];
39
+ }, [headerBlock?.container?.default?.backgroundColor]);
31
40
 
32
41
  const closeButtonProps = useMemo<CloseButtonProps>(
33
42
  () => ({
@@ -37,13 +46,17 @@ export function ModalHeader(props: Props) {
37
46
  [configuration, dismiss]
38
47
  );
39
48
 
40
- const titleProps = useMemo<TitleProps>(
41
- () => ({
42
- ...getTitleProps(configuration, "title"),
49
+ const titleProps = useMemo<TitleProps>(() => {
50
+ const baseProps = getTitleProps(configuration, "title");
51
+ const titleOverride = headerBlock?.title?.default;
52
+
53
+ return {
54
+ ...baseProps,
43
55
  text: title,
44
- }),
45
- [configuration, title]
46
- );
56
+ ...(titleOverride?.fontColor ? { color: titleOverride.fontColor } : {}),
57
+ ...(titleOverride?.fontSize ? { fontSize: titleOverride.fontSize } : {}),
58
+ };
59
+ }, [configuration, title, headerBlock?.title?.default]);
47
60
 
48
61
  const summaryProps = useMemo<TitleProps>(
49
62
  () => ({
@@ -59,7 +72,7 @@ export function ModalHeader(props: Props) {
59
72
  configuration.modal_bottom_sheet_close_margin_right;
60
73
 
61
74
  return (
62
- <View onLayout={onLayout} style={styles.container}>
75
+ <View onLayout={onLayout} style={containerStyle}>
63
76
  <View style={styles.flex}>
64
77
  {title ? <Title {...titleProps} /> : null}
65
78
  {summary ? <Title {...summaryProps} /> : null}
@@ -18,7 +18,7 @@ export function getCloseButtonProps(config: PluginConfiguration) {
18
18
  return {
19
19
  asset: modal_bottom_sheet_close_asset,
20
20
  assetFocused: modal_bottom_sheet_close_focus_asset,
21
- enabled: modal_bottom_sheet_close_switch,
21
+ enabled: modal_bottom_sheet_close_switch !== false,
22
22
  width: modal_bottom_sheet_close_width,
23
23
  height: modal_bottom_sheet_close_height,
24
24
  marginTop: modal_bottom_sheet_close_margin_top,
@@ -0,0 +1,10 @@
1
+ import React, { useContext } from "react";
2
+ import type { ModalBlocksConfig } from "@applicaster/zapp-react-native-utils/configurationUtils/modalBlocksParser";
3
+
4
+ export const ModalBlocksStyleContext = React.createContext<
5
+ ModalBlocksConfig | Record<string, any> | undefined
6
+ >(undefined);
7
+
8
+ export const useModalBlocksStyle = () => {
9
+ return useContext(ModalBlocksStyleContext);
10
+ };