@applicaster/zapp-react-native-ui-components 15.0.0-alpha.4920595583 → 15.0.0-alpha.5104105031

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.
@@ -26,11 +26,15 @@ type Props = {
26
26
  componentAnchorPointY: number;
27
27
  headerOffset?: number;
28
28
  extraAnchorPointYOffset?: number;
29
+ componentPaddingTop?: number;
29
30
  }) => void;
30
31
  offsetUpdater: (arg1: string, arg2: number, arg3: number) => number;
31
32
  componentId: string;
32
33
  component: {
33
34
  id: string;
35
+ styles?: {
36
+ component_padding_top?: number;
37
+ };
34
38
  };
35
39
  selected?: boolean;
36
40
  CellRenderer: React.FunctionComponent<any> & {
@@ -178,6 +182,8 @@ export class CellComponent extends React.Component<Props, State> {
178
182
  componentAnchorPointY,
179
183
  headerOffset,
180
184
  extraAnchorPointYOffset,
185
+ componentPaddingTop:
186
+ this.props?.component?.styles?.component_padding_top,
181
187
  });
182
188
  }
183
189
  }
@@ -9,10 +9,9 @@ import {
9
9
  import { isEmptyOrNil } from "@applicaster/zapp-react-native-utils/cellUtils";
10
10
  import { Categories } from "./logger";
11
11
  import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
12
- import { useRoute } from "@applicaster/zapp-react-native-utils/reactHooks/navigation/useRoute";
12
+ import { useScreenContext } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
13
13
 
14
14
  import {
15
- ZappPipesEntryContext,
16
15
  ZappPipesScreenContext,
17
16
  ZappPipesSearchContext,
18
17
  } from "@applicaster/zapp-react-native-ui-components/Contexts";
@@ -136,7 +135,6 @@ export const useCurationAPI = (
136
135
  [components]
137
136
  );
138
137
 
139
- const { pathname } = useRoute();
140
138
  const [searchContext] = ZappPipesSearchContext.useZappPipesContext();
141
139
  const [screenContext] = ZappPipesScreenContext.useZappPipesContext();
142
140
 
@@ -146,10 +144,12 @@ export const useCurationAPI = (
146
144
  screenContextType === TABS_SCREEN_TYPE ||
147
145
  screenContextType === QB_TABS_SCREEN_TYPE;
148
146
 
149
- const [entryContext] = ZappPipesEntryContext.useZappPipesContext(
150
- pathname,
151
- isNestedScreen
152
- );
147
+ const screenContextData = useScreenContext();
148
+
149
+ const entryContext = ((isNestedScreen && screenContextData?.nested?.entry
150
+ ? screenContextData?.nested?.entry
151
+ : (screenContextData?.entry?.payload ?? screenContextData?.entry)) ||
152
+ {}) as ZappEntry;
153
153
 
154
154
  const urlsMap = useMemo<{ [key: string]: string }>(() => {
155
155
  const map = {};
@@ -3,8 +3,8 @@ import { isNil } from "ramda";
3
3
  import { ImageStyle, View } from "react-native";
4
4
  import {
5
5
  FIT_POSITION,
6
- IMAGE_SIZING_FIT,
7
6
  IMAGE_SIZING_FILL,
7
+ IMAGE_SIZING_FIT,
8
8
  } from "@applicaster/zapp-react-native-utils/manifestUtils/secondaryImage";
9
9
  import { QBImage as Image } from "@applicaster/zapp-react-native-ui-components/Components/Image";
10
10
 
@@ -25,49 +25,51 @@ interface Props {
25
25
  fitPosition: typeof FIT_POSITION;
26
26
  fixedWidth: number;
27
27
  fixedHeight: number;
28
- onAsyncRender: () => void;
28
+ onAsyncRender?: () => void;
29
29
  }
30
30
 
31
31
  /** Secondary Image Dynamic does not render until the image is loaded */
32
- const SecondaryImageDynamic = (props: Props) => {
33
- const { uri, style, displayMode, imageSizing, fitPosition, onAsyncRender } =
34
- props;
32
+ const SecondaryImageDynamic = withAsyncRenderHOC(
33
+ (props: Props & { onAsyncRender: () => void }) => {
34
+ const { uri, style, displayMode, imageSizing, fitPosition, onAsyncRender } =
35
+ props;
35
36
 
36
- const imageDimension = useGetImageDimensions(
37
- uri,
38
- style.width as number,
39
- isImageSizingFit(imageSizing) ? undefined : (style.height as number)
40
- );
37
+ const imageDimension = useGetImageDimensions(
38
+ uri,
39
+ style.width as number,
40
+ isImageSizingFit(imageSizing) ? undefined : (style.height as number)
41
+ );
41
42
 
42
- const containerHeight = imageDimension?.height;
43
+ const containerHeight = imageDimension?.height;
43
44
 
44
- const containerWidth = style?.width;
45
+ const containerWidth = style?.width;
45
46
 
46
- if (isNil(imageDimension?.aspectRatio)) {
47
- return null;
48
- }
47
+ if (isNil(imageDimension?.aspectRatio)) {
48
+ return null;
49
+ }
49
50
 
50
- return (
51
- <View style={style} onLayout={onAsyncRender}>
52
- <Image
53
- {...props}
54
- source={{ uri }}
55
- style={{
56
- ...getStyle({
57
- imageSizing,
58
- fitPosition,
59
- displayMode,
60
- imageDimension,
61
- containerHeight,
62
- containerWidth,
63
- }),
64
- borderRadius: style.borderRadius,
65
- aspectRatio: imageDimension.aspectRatio,
66
- }}
67
- />
68
- </View>
69
- );
70
- };
51
+ return (
52
+ <View style={style} onLayout={onAsyncRender}>
53
+ <Image
54
+ {...props}
55
+ source={{ uri }}
56
+ style={{
57
+ ...getStyle({
58
+ imageSizing,
59
+ fitPosition,
60
+ displayMode,
61
+ imageDimension,
62
+ containerHeight,
63
+ containerWidth,
64
+ }),
65
+ borderRadius: style.borderRadius,
66
+ aspectRatio: imageDimension.aspectRatio,
67
+ }}
68
+ />
69
+ </View>
70
+ );
71
+ }
72
+ );
71
73
 
72
74
  /** Secondary Image Fixed does not render the image until the image is loaded, but keep container rendered */
73
75
  const SecondaryImageFixed = (props: Props) => {
@@ -79,7 +81,6 @@ const SecondaryImageFixed = (props: Props) => {
79
81
  fitPosition,
80
82
  fixedHeight,
81
83
  fixedWidth,
82
- onAsyncRender,
83
84
  } = props;
84
85
 
85
86
  const imageDimension = useGetImageDimensions(
@@ -89,7 +90,7 @@ const SecondaryImageFixed = (props: Props) => {
89
90
  );
90
91
 
91
92
  return (
92
- <View style={style} onLayout={onAsyncRender}>
93
+ <View style={style}>
93
94
  {isNil(imageDimension?.aspectRatio) ? null : (
94
95
  <Image
95
96
  {...props}
@@ -128,4 +129,4 @@ const SecondaryImageComponent = (props: Props) => {
128
129
  );
129
130
  };
130
131
 
131
- export const SecondaryImage = withAsyncRenderHOC(SecondaryImageComponent);
132
+ export const SecondaryImage = SecondaryImageComponent;
@@ -10,6 +10,10 @@ describe("SecondaryImage - Image", () => {
10
10
  displayMode="dynamic"
11
11
  uri={undefined}
12
12
  style={{ width: 100 }}
13
+ imageSizing="fit"
14
+ fitPosition="center"
15
+ fixedWidth={0}
16
+ fixedHeight={0}
13
17
  />
14
18
  );
15
19
 
@@ -23,6 +27,10 @@ describe("SecondaryImage - Image", () => {
23
27
  displayMode="dynamic"
24
28
  uri="someurl"
25
29
  style={{ width: 100 }}
30
+ imageSizing="fit"
31
+ fitPosition="center"
32
+ fixedWidth={0}
33
+ fixedHeight={0}
26
34
  />
27
35
  );
28
36
 
@@ -36,6 +44,93 @@ describe("SecondaryImage - Image", () => {
36
44
  uri="someUrl"
37
45
  displayMode="dynamic"
38
46
  style={{ width: 100, height: 100, borderRadius: 10 }}
47
+ imageSizing="fill"
48
+ fitPosition="center"
49
+ fixedWidth={0}
50
+ fixedHeight={0}
51
+ />
52
+ );
53
+
54
+ expect(wrapper.toJSON()).not.toEqual(null);
55
+ expect(wrapper.toJSON()).toMatchSnapshot();
56
+ });
57
+
58
+ it("SecondaryImage should render in fixed mode without image until loaded", async () => {
59
+ const wrapper = await render(
60
+ <SecondaryImage
61
+ displayMode="fixed"
62
+ uri="someurl"
63
+ style={{ width: 100, height: 100, borderRadius: 5 }}
64
+ imageSizing="fit"
65
+ fitPosition="center"
66
+ fixedWidth={100}
67
+ fixedHeight={100}
68
+ />
69
+ );
70
+
71
+ expect(wrapper.toJSON()).toMatchSnapshot();
72
+ });
73
+
74
+ it("SecondaryImage should not render in dynamic mode without image", async () => {
75
+ const wrapper = await render(
76
+ <SecondaryImage
77
+ displayMode="dynamic"
78
+ uri={null}
79
+ style={{ width: 100, height: 100 }}
80
+ imageSizing="fit"
81
+ fitPosition="center"
82
+ fixedWidth={0}
83
+ fixedHeight={0}
84
+ />
85
+ );
86
+
87
+ expect(wrapper.toJSON()).toEqual(null);
88
+ expect(wrapper.toJSON()).toMatchSnapshot();
89
+ });
90
+
91
+ it("SecondaryImage should render in fixed mode with known dimensions", async () => {
92
+ const wrapper = await render(
93
+ <SecondaryImage
94
+ uri="someUrl"
95
+ displayMode="fixed"
96
+ style={{ width: 100, height: 100, borderRadius: 10 }}
97
+ imageSizing="fill"
98
+ fitPosition="center"
99
+ fixedWidth={100}
100
+ fixedHeight={100}
101
+ />
102
+ );
103
+
104
+ expect(wrapper.toJSON()).not.toEqual(null);
105
+ expect(wrapper.toJSON()).toMatchSnapshot();
106
+ });
107
+
108
+ it("SecondaryImage dynamic mode should call onAsyncRender when provided", async () => {
109
+ const wrapper = await render(
110
+ <SecondaryImage
111
+ uri="someUrl"
112
+ displayMode="dynamic"
113
+ style={{ width: 100, height: 100, borderRadius: 10 }}
114
+ imageSizing="fill"
115
+ fitPosition="center"
116
+ fixedWidth={0}
117
+ fixedHeight={0}
118
+ />
119
+ );
120
+
121
+ expect(wrapper.toJSON()).not.toEqual(null);
122
+ });
123
+
124
+ it("SecondaryImage fixed mode should not use async render props", async () => {
125
+ const wrapper = await render(
126
+ <SecondaryImage
127
+ uri="someUrl"
128
+ displayMode="fixed"
129
+ style={{ width: 100, height: 100, borderRadius: 10 }}
130
+ imageSizing="fill"
131
+ fitPosition="center"
132
+ fixedWidth={100}
133
+ fixedHeight={100}
39
134
  />
40
135
  );
41
136
 
@@ -1,9 +1,45 @@
1
1
  // Jest Snapshot v1, https://goo.gl/fbAQLP
2
2
 
3
+ exports[`SecondaryImage - Image SecondaryImage fixed mode should not use async render props 1`] = `
4
+ <View
5
+ style={
6
+ {
7
+ "borderRadius": 10,
8
+ "height": 100,
9
+ "width": 100,
10
+ }
11
+ }
12
+ >
13
+ <Image
14
+ displayMode="fixed"
15
+ fitPosition="center"
16
+ fixedHeight={100}
17
+ fixedWidth={100}
18
+ imageSizing="fill"
19
+ source={
20
+ {
21
+ "uri": "someUrl",
22
+ }
23
+ }
24
+ style={
25
+ {
26
+ "aspectRatio": 1,
27
+ "borderRadius": 10,
28
+ "height": 100,
29
+ "width": 100,
30
+ }
31
+ }
32
+ uri="someUrl"
33
+ />
34
+ </View>
35
+ `;
36
+
3
37
  exports[`SecondaryImage - Image SecondaryImage should not render if no aspect ratio (dynamic) 1`] = `null`;
4
38
 
5
39
  exports[`SecondaryImage - Image SecondaryImage should not render if no uri 1`] = `null`;
6
40
 
41
+ exports[`SecondaryImage - Image SecondaryImage should not render in dynamic mode without image 1`] = `null`;
42
+
7
43
  exports[`SecondaryImage - Image SecondaryImage should render if known dimensions 1`] = `
8
44
  <View
9
45
  onLayout={[Function]}
@@ -17,6 +53,10 @@ exports[`SecondaryImage - Image SecondaryImage should render if known dimensions
17
53
  >
18
54
  <Image
19
55
  displayMode="dynamic"
56
+ fitPosition="center"
57
+ fixedHeight={0}
58
+ fixedWidth={0}
59
+ imageSizing="fill"
20
60
  onAsyncRender={[Function]}
21
61
  source={
22
62
  {
@@ -35,3 +75,49 @@ exports[`SecondaryImage - Image SecondaryImage should render if known dimensions
35
75
  />
36
76
  </View>
37
77
  `;
78
+
79
+ exports[`SecondaryImage - Image SecondaryImage should render in fixed mode with known dimensions 1`] = `
80
+ <View
81
+ style={
82
+ {
83
+ "borderRadius": 10,
84
+ "height": 100,
85
+ "width": 100,
86
+ }
87
+ }
88
+ >
89
+ <Image
90
+ displayMode="fixed"
91
+ fitPosition="center"
92
+ fixedHeight={100}
93
+ fixedWidth={100}
94
+ imageSizing="fill"
95
+ source={
96
+ {
97
+ "uri": "someUrl",
98
+ }
99
+ }
100
+ style={
101
+ {
102
+ "aspectRatio": 1,
103
+ "borderRadius": 10,
104
+ "height": 100,
105
+ "width": 100,
106
+ }
107
+ }
108
+ uri="someUrl"
109
+ />
110
+ </View>
111
+ `;
112
+
113
+ exports[`SecondaryImage - Image SecondaryImage should render in fixed mode without image until loaded 1`] = `
114
+ <View
115
+ style={
116
+ {
117
+ "borderRadius": 5,
118
+ "height": 100,
119
+ "width": 100,
120
+ }
121
+ }
122
+ />
123
+ `;
@@ -0,0 +1,141 @@
1
+ import { SecondaryImage } from "../index";
2
+
3
+ describe("SecondaryImage - Configuration Builder", () => {
4
+ const mockValue = (config: any) => (key: string, transformer?: Function) => {
5
+ const value = config[key];
6
+
7
+ return transformer ? transformer(value) : value;
8
+ };
9
+
10
+ it("should pass false as second argument to image_src_from_media_item", () => {
11
+ const config = {
12
+ secondary_image_switch: true,
13
+ secondary_image_position: "over_image",
14
+ secondary_image_visibility: "always",
15
+ secondary_image_image_key: "custom_image",
16
+ secondary_image_display_mode: "dynamic",
17
+ secondary_image_dynamic_width: 100,
18
+ };
19
+
20
+ const result = SecondaryImage({
21
+ value: mockValue(config),
22
+ currentPosition: "over_image",
23
+ state: "default",
24
+ });
25
+
26
+ expect(result).not.toBeNull();
27
+ expect(result?.elements).toBeDefined();
28
+ expect(result?.elements[0].data).toBeDefined();
29
+
30
+ const imageData = result?.elements[0].data[0];
31
+ expect(imageData.func).toBe("image_src_from_media_item");
32
+ expect(imageData.args).toEqual(["custom_image", false]);
33
+ expect(imageData.propName).toBe("uri");
34
+ });
35
+
36
+ it("should return null when secondary image is disabled", () => {
37
+ const config = {
38
+ secondary_image_switch: false,
39
+ };
40
+
41
+ const result = SecondaryImage({
42
+ value: mockValue(config),
43
+ currentPosition: "over_image",
44
+ state: "default",
45
+ });
46
+
47
+ expect(result).toBeNull();
48
+ });
49
+
50
+ it("should return null when position does not match currentPosition", () => {
51
+ const config = {
52
+ secondary_image_switch: true,
53
+ secondary_image_position: "above_text_label_1",
54
+ secondary_image_visibility: "always",
55
+ };
56
+
57
+ const result = SecondaryImage({
58
+ value: mockValue(config),
59
+ currentPosition: "over_image",
60
+ state: "default",
61
+ });
62
+
63
+ expect(result).toBeNull();
64
+ });
65
+
66
+ it("should return null when visibility does not match state", () => {
67
+ const config = {
68
+ secondary_image_switch: true,
69
+ secondary_image_position: "over_image",
70
+ secondary_image_visibility: "focused",
71
+ };
72
+
73
+ const result = SecondaryImage({
74
+ value: mockValue(config),
75
+ currentPosition: "over_image",
76
+ state: "default",
77
+ });
78
+
79
+ expect(result).toBeNull();
80
+ });
81
+
82
+ it("should include display mode and image sizing in additionalProps", () => {
83
+ const config = {
84
+ secondary_image_switch: true,
85
+ secondary_image_position: "over_image",
86
+ secondary_image_visibility: "always",
87
+ secondary_image_image_key: "logo",
88
+ secondary_image_display_mode: "fixed",
89
+ secondary_image_image_sizing: "fill",
90
+ secondary_image_fit_position: "center",
91
+ secondary_image_fixed_width: 200,
92
+ secondary_image_fixed_height: 150,
93
+ };
94
+
95
+ const result = SecondaryImage({
96
+ value: mockValue(config),
97
+ currentPosition: "over_image",
98
+ state: "default",
99
+ });
100
+
101
+ expect(result?.elements[0].additionalProps).toMatchObject({
102
+ displayMode: "fixed",
103
+ imageSizing: "fill",
104
+ fitPosition: "center",
105
+ fixedWidth: 200,
106
+ fixedHeight: 150,
107
+ });
108
+ });
109
+
110
+ it("should apply correct styles including margins and border radius", () => {
111
+ const config = {
112
+ secondary_image_switch: true,
113
+ secondary_image_position: "over_image",
114
+ secondary_image_visibility: "always",
115
+ secondary_image_display_mode: "fixed",
116
+ secondary_image_fixed_width: 100,
117
+ secondary_image_fixed_height: 100,
118
+ secondary_image_corner_radius: 12,
119
+ secondary_image_margin_top: 10,
120
+ secondary_image_margin_left: 5,
121
+ secondary_image_margin_right: 5,
122
+ secondary_image_margin_bottom: 10,
123
+ };
124
+
125
+ const result = SecondaryImage({
126
+ value: mockValue(config),
127
+ currentPosition: "over_image",
128
+ state: "default",
129
+ });
130
+
131
+ expect(result?.elements[0].style).toMatchObject({
132
+ width: 100,
133
+ height: 100,
134
+ borderRadius: 12,
135
+ marginTop: 10,
136
+ marginLeft: 5,
137
+ marginRight: 5,
138
+ marginBottom: 10,
139
+ });
140
+ });
141
+ });
@@ -103,7 +103,7 @@ export const SecondaryImage = ({ value, currentPosition, state }: Props) => {
103
103
  data: [
104
104
  {
105
105
  func: "image_src_from_media_item",
106
- args: [imageKey],
106
+ args: [imageKey, false],
107
107
  propName: "uri",
108
108
  },
109
109
  ],
@@ -1,9 +1,15 @@
1
- import { getPluginIdentifier } from "..";
1
+ import { getPluginIdentifier, memoizedGetPluginIdentifier } from "..";
2
2
 
3
3
  describe("getPluginIdentifier", () => {
4
4
  const prefix = "tv_buttons";
5
5
 
6
+ beforeAll(() => {
7
+ memoizedGetPluginIdentifier.clear();
8
+ });
9
+
6
10
  it("returns the first valid plugin identifier", () => {
11
+ expect.assertions(2);
12
+
7
13
  const configuration = {
8
14
  tv_buttons_button_1_other: "value",
9
15
  tv_buttons_button_1_assign_action:
@@ -15,10 +21,19 @@ describe("getPluginIdentifier", () => {
15
21
  const index = 0;
16
22
  const result = getPluginIdentifier(configuration, prefix, index);
17
23
 
24
+ const memoizedResult = memoizedGetPluginIdentifier(
25
+ configuration,
26
+ prefix,
27
+ index
28
+ );
29
+
18
30
  expect(result).toEqual(configuration.tv_buttons_button_1_assign_action);
31
+ expect(memoizedResult).toEqual(result);
19
32
  });
20
33
 
21
34
  it("returns the second valid plugin identifier", () => {
35
+ expect.assertions(2);
36
+
22
37
  const configuration = {
23
38
  tv_buttons_button_1_other: "value_1",
24
39
  tv_buttons_button_1_assign_action:
@@ -31,19 +46,37 @@ describe("getPluginIdentifier", () => {
31
46
  const index = 1;
32
47
  const result = getPluginIdentifier(configuration, prefix, index);
33
48
 
49
+ const memoizedResult = memoizedGetPluginIdentifier(
50
+ configuration,
51
+ prefix,
52
+ index
53
+ );
54
+
34
55
  expect(result).toEqual(configuration.tv_buttons_button_2_assign_action);
56
+ expect(memoizedResult).toEqual(result);
35
57
  });
36
58
 
37
59
  it("returns undefined when there are no assign_action keys", () => {
60
+ expect.assertions(2);
61
+
38
62
  const configuration = {};
39
63
  const index = 0;
40
64
 
41
65
  const result = getPluginIdentifier(configuration, prefix, index);
42
66
 
67
+ const memoizedResult = memoizedGetPluginIdentifier(
68
+ configuration,
69
+ prefix,
70
+ index
71
+ );
72
+
43
73
  expect(result).toBeUndefined();
74
+ expect(memoizedResult).toBeUndefined();
44
75
  });
45
76
 
46
77
  it("skips undefined values and returns the correct plugin identifier", () => {
78
+ expect.assertions(2);
79
+
47
80
  const configuration = {
48
81
  tv_buttons_button_1_assign_action: "tv_buttons_button_1_assign_action",
49
82
  tv_buttons_button_2_assign_action: undefined,
@@ -53,10 +86,19 @@ describe("getPluginIdentifier", () => {
53
86
  const index = 1;
54
87
  const result = getPluginIdentifier(configuration, prefix, index);
55
88
 
89
+ const memoizedResult = memoizedGetPluginIdentifier(
90
+ configuration,
91
+ prefix,
92
+ index
93
+ );
94
+
56
95
  expect(result).toEqual(configuration.tv_buttons_button_3_assign_action);
96
+ expect(memoizedResult).toEqual(result);
57
97
  });
58
98
 
59
99
  it("handles missing intermediate keys and returns the correct plugin identifier", () => {
100
+ expect.assertions(2);
101
+
60
102
  const configuration = {
61
103
  tv_buttons_button_1_assign_action: "tv_buttons_button_1_assign_action",
62
104
  tv_buttons_button_3_assign_action: "tv_buttons_button_3_assign_action",
@@ -65,10 +107,19 @@ describe("getPluginIdentifier", () => {
65
107
  const index = 1;
66
108
  const result = getPluginIdentifier(configuration, prefix, index);
67
109
 
110
+ const memoizedResult = memoizedGetPluginIdentifier(
111
+ configuration,
112
+ prefix,
113
+ index
114
+ );
115
+
68
116
  expect(result).toEqual(configuration.tv_buttons_button_3_assign_action);
117
+ expect(memoizedResult).toEqual(result);
69
118
  });
70
119
 
71
120
  it("skips empty string values and returns the first non-empty assign_action", () => {
121
+ expect.assertions(2);
122
+
72
123
  const configuration = {
73
124
  tv_buttons_button_1_assign_action: "",
74
125
  tv_buttons_button_2_assign_action: "tv_buttons_button_2_assign_action",
@@ -77,10 +128,19 @@ describe("getPluginIdentifier", () => {
77
128
  const index = 0;
78
129
  const result = getPluginIdentifier(configuration, prefix, index);
79
130
 
131
+ const memoizedResult = memoizedGetPluginIdentifier(
132
+ configuration,
133
+ prefix,
134
+ index
135
+ );
136
+
80
137
  expect(result).toEqual(configuration.tv_buttons_button_2_assign_action);
138
+ expect(memoizedResult).toEqual(result);
81
139
  });
82
140
 
83
141
  it("returns undefined for negative index", () => {
142
+ expect.assertions(2);
143
+
84
144
  const configuration = {
85
145
  tv_buttons_button_1_assign_action: "a",
86
146
  tv_buttons_button_2_assign_action: "b",
@@ -89,10 +149,19 @@ describe("getPluginIdentifier", () => {
89
149
  const index = -1;
90
150
  const result = getPluginIdentifier(configuration, prefix, index);
91
151
 
152
+ const memoizedResult = memoizedGetPluginIdentifier(
153
+ configuration,
154
+ prefix,
155
+ index
156
+ );
157
+
92
158
  expect(result).toBeUndefined();
159
+ expect(memoizedResult).toBeUndefined();
93
160
  });
94
161
 
95
162
  it("returns undefined for out-of-bounds index", () => {
163
+ expect.assertions(2);
164
+
96
165
  const configuration = {
97
166
  tv_buttons_button_1_assign_action: "a",
98
167
  tv_buttons_button_2_assign_action: "b",
@@ -101,10 +170,19 @@ describe("getPluginIdentifier", () => {
101
170
  const index = 5;
102
171
  const result = getPluginIdentifier(configuration, prefix, index);
103
172
 
173
+ const memoizedResult = memoizedGetPluginIdentifier(
174
+ configuration,
175
+ prefix,
176
+ index
177
+ );
178
+
104
179
  expect(result).toBeUndefined();
180
+ expect(memoizedResult).toBeUndefined();
105
181
  });
106
182
 
107
183
  it("ignores keys with wrong prefix or format", () => {
184
+ expect.assertions(2);
185
+
108
186
  const configuration = {
109
187
  tv_buttons_button_1_assign_action: "a",
110
188
  tv_buttons_wrongprefix_2_assign_action: "b",
@@ -114,10 +192,19 @@ describe("getPluginIdentifier", () => {
114
192
  const index = 1;
115
193
  const result = getPluginIdentifier(configuration, prefix, index);
116
194
 
195
+ const memoizedResult = memoizedGetPluginIdentifier(
196
+ configuration,
197
+ prefix,
198
+ index
199
+ );
200
+
117
201
  expect(result).toBeUndefined();
202
+ expect(memoizedResult).toBeUndefined();
118
203
  });
119
204
 
120
205
  it("returns undefined if all assign_actions are null or empty", () => {
206
+ expect.assertions(2);
207
+
121
208
  const configuration = {
122
209
  tv_buttons_button_1_assign_action: null,
123
210
  tv_buttons_button_2_assign_action: undefined,
@@ -127,10 +214,19 @@ describe("getPluginIdentifier", () => {
127
214
  const index = 0;
128
215
  const result = getPluginIdentifier(configuration, prefix, index);
129
216
 
217
+ const memoizedResult = memoizedGetPluginIdentifier(
218
+ configuration,
219
+ prefix,
220
+ index
221
+ );
222
+
130
223
  expect(result).toBeUndefined();
224
+ expect(memoizedResult).toBeUndefined();
131
225
  });
132
226
 
133
227
  it("handles non-string values correctly", () => {
228
+ expect.assertions(6);
229
+
134
230
  const configuration = {
135
231
  tv_buttons_button_1_assign_action: 123,
136
232
  tv_buttons_button_2_assign_action: true,
@@ -143,10 +239,21 @@ describe("getPluginIdentifier", () => {
143
239
  expect(getPluginIdentifier(configuration, prefix, 2)).toEqual({
144
240
  nested: "value",
145
241
  });
242
+
243
+ expect(memoizedGetPluginIdentifier(configuration, prefix, 0)).toEqual(123);
244
+ expect(memoizedGetPluginIdentifier(configuration, prefix, 1)).toEqual(true);
245
+
246
+ expect(memoizedGetPluginIdentifier(configuration, prefix, 2)).toEqual({
247
+ nested: "value",
248
+ });
146
249
  });
147
250
  });
148
251
 
149
252
  describe("getPluginIdentifier - when configuration has duplicate values", () => {
253
+ beforeAll(() => {
254
+ memoizedGetPluginIdentifier.clear();
255
+ });
256
+
150
257
  const prefix = "tv_buttons";
151
258
 
152
259
  const configuration = {
@@ -155,16 +262,34 @@ describe("getPluginIdentifier - when configuration has duplicate values", () =>
155
262
  };
156
263
 
157
264
  it("returns the first plugin identifier when values are identical", () => {
265
+ expect.assertions(2);
266
+
158
267
  const index = 0;
159
268
  const result = getPluginIdentifier(configuration, prefix, index);
160
269
 
270
+ const memoizedResult = memoizedGetPluginIdentifier(
271
+ configuration,
272
+ prefix,
273
+ index
274
+ );
275
+
161
276
  expect(result).toEqual(configuration.tv_buttons_button_1_assign_action);
277
+ expect(memoizedResult).toEqual(result);
162
278
  });
163
279
 
164
280
  it("returns the second plugin identifier when values are identical", () => {
281
+ expect.assertions(2);
282
+
165
283
  const index = 1;
166
284
  const result = getPluginIdentifier(configuration, prefix, index);
167
285
 
286
+ const memoizedResult = memoizedGetPluginIdentifier(
287
+ configuration,
288
+ prefix,
289
+ index
290
+ );
291
+
168
292
  expect(result).toEqual(configuration.tv_buttons_button_2_assign_action);
293
+ expect(memoizedResult).toEqual(result);
169
294
  });
170
295
  });
@@ -40,9 +40,7 @@ export const getPluginIdentifier = (
40
40
  return undefined;
41
41
  };
42
42
 
43
- export const memoizedGetPluginIdentifier = memoizee(getPluginIdentifier, {
44
- primitive: true,
45
- });
43
+ export const memoizedGetPluginIdentifier = memoizee(getPluginIdentifier);
46
44
 
47
45
  type Label = {
48
46
  name: string;
@@ -0,0 +1,219 @@
1
+ import React from "react";
2
+ import { render } from "@testing-library/react-native";
3
+ import { Text, View } from "react-native";
4
+ import { withAsyncRenderHOC } from "../withAsyncRender";
5
+
6
+ describe("withAsyncRenderHOC", () => {
7
+ // Test component that requires onAsyncRender
8
+ const TestComponent = ({
9
+ onAsyncRender,
10
+ text,
11
+ }: {
12
+ onAsyncRender: () => void;
13
+ text?: string;
14
+ }) => {
15
+ React.useEffect(() => {
16
+ onAsyncRender();
17
+ }, [onAsyncRender]);
18
+
19
+ return (
20
+ <View>
21
+ <Text>{text || "Test"}</Text>
22
+ </View>
23
+ );
24
+ };
25
+
26
+ it("should wrap component and provide onAsyncRender callback", () => {
27
+ const WrappedComponent = withAsyncRenderHOC(TestComponent);
28
+
29
+ const wrapper = render(<WrappedComponent text="Hello" />);
30
+
31
+ expect(wrapper.toJSON()).not.toBeNull();
32
+ expect(wrapper.getByText("Hello")).toBeDefined();
33
+ });
34
+
35
+ it("should call emitAsyncElementRegistrate on mount", () => {
36
+ const mockRegistrate = jest.fn();
37
+ const WrappedComponent = withAsyncRenderHOC(TestComponent);
38
+
39
+ render(<WrappedComponent emitAsyncElementRegistrate={mockRegistrate} />);
40
+
41
+ expect(mockRegistrate).toHaveBeenCalledTimes(1);
42
+ });
43
+
44
+ it("should call emitAsyncElementLayout when onAsyncRender is called", () => {
45
+ const mockLayout = jest.fn();
46
+
47
+ const ComponentWithAsyncCall = ({
48
+ onAsyncRender,
49
+ }: {
50
+ onAsyncRender: () => void;
51
+ }) => {
52
+ return (
53
+ <View onLayout={onAsyncRender}>
54
+ <Text>Async Component</Text>
55
+ </View>
56
+ );
57
+ };
58
+
59
+ const WrappedComponent = withAsyncRenderHOC(ComponentWithAsyncCall);
60
+
61
+ const wrapper = render(
62
+ <WrappedComponent emitAsyncElementLayout={mockLayout} />
63
+ );
64
+
65
+ // Trigger onLayout
66
+ const view = wrapper.UNSAFE_getByType(View);
67
+ view.props.onLayout();
68
+
69
+ expect(mockLayout).toHaveBeenCalled();
70
+ });
71
+
72
+ it("should return unsubscribe function from emitAsyncElementRegistrate", () => {
73
+ const mockUnsubscribe = jest.fn();
74
+ const mockRegistrate = jest.fn(() => mockUnsubscribe);
75
+ const WrappedComponent = withAsyncRenderHOC(TestComponent);
76
+
77
+ const { unmount } = render(
78
+ <WrappedComponent emitAsyncElementRegistrate={mockRegistrate} />
79
+ );
80
+
81
+ expect(mockRegistrate).toHaveBeenCalledTimes(1);
82
+
83
+ // Unmount should trigger cleanup
84
+ unmount();
85
+
86
+ expect(mockUnsubscribe).toHaveBeenCalled();
87
+ });
88
+
89
+ it("should work without emitAsyncElementRegistrate prop", () => {
90
+ const WrappedComponent = withAsyncRenderHOC(TestComponent);
91
+
92
+ const wrapper = render(<WrappedComponent text="No Registration" />);
93
+
94
+ expect(wrapper.toJSON()).not.toBeNull();
95
+ expect(wrapper.getByText("No Registration")).toBeDefined();
96
+ });
97
+
98
+ it("should work without emitAsyncElementLayout prop", () => {
99
+ const ComponentWithLayout = ({
100
+ onAsyncRender,
101
+ }: {
102
+ onAsyncRender: () => void;
103
+ }) => {
104
+ return (
105
+ <View onLayout={onAsyncRender}>
106
+ <Text>Test Layout</Text>
107
+ </View>
108
+ );
109
+ };
110
+
111
+ const WrappedComponent = withAsyncRenderHOC(ComponentWithLayout);
112
+
113
+ const wrapper = render(<WrappedComponent />);
114
+
115
+ // Should not throw when calling onLayout without emitAsyncElementLayout
116
+ const view = wrapper.UNSAFE_getByType(View);
117
+ expect(() => view.props.onLayout()).not.toThrow();
118
+ });
119
+
120
+ it("should pass through all other props to wrapped component", () => {
121
+ const ComponentWithProps = ({
122
+ customProp,
123
+ anotherProp,
124
+ }: {
125
+ onAsyncRender: () => void;
126
+ customProp: string;
127
+ anotherProp: number;
128
+ }) => {
129
+ return (
130
+ <View>
131
+ <Text>{customProp}</Text>
132
+ <Text>{anotherProp}</Text>
133
+ </View>
134
+ );
135
+ };
136
+
137
+ const WrappedComponent = withAsyncRenderHOC(ComponentWithProps);
138
+
139
+ const wrapper = render(
140
+ <WrappedComponent customProp="custom value" anotherProp={42} />
141
+ );
142
+
143
+ expect(wrapper.getByText("custom value")).toBeDefined();
144
+ expect(wrapper.getByText("42")).toBeDefined();
145
+ });
146
+
147
+ it("should call emitAsyncElementLayout multiple times if onAsyncRender is called multiple times", () => {
148
+ const mockLayout = jest.fn();
149
+
150
+ const ComponentWithMultipleCalls = ({
151
+ onAsyncRender,
152
+ }: {
153
+ onAsyncRender: () => void;
154
+ }) => {
155
+ return (
156
+ <View>
157
+ <View onLayout={onAsyncRender} testID="view1">
158
+ <Text>First</Text>
159
+ </View>
160
+ <View onLayout={onAsyncRender} testID="view2">
161
+ <Text>Second</Text>
162
+ </View>
163
+ </View>
164
+ );
165
+ };
166
+
167
+ const WrappedComponent = withAsyncRenderHOC(ComponentWithMultipleCalls);
168
+
169
+ const wrapper = render(
170
+ <WrappedComponent emitAsyncElementLayout={mockLayout} />
171
+ );
172
+
173
+ // Trigger both onLayout callbacks
174
+ const view1 = wrapper.getByTestId("view1");
175
+ const view2 = wrapper.getByTestId("view2");
176
+
177
+ view1.props.onLayout();
178
+ view2.props.onLayout();
179
+
180
+ expect(mockLayout).toHaveBeenCalledTimes(2);
181
+ });
182
+
183
+ it("should handle components that use onAsyncRender in effects", () => {
184
+ const mockLayout = jest.fn();
185
+
186
+ const ComponentWithEffect = ({
187
+ onAsyncRender,
188
+ }: {
189
+ onAsyncRender: () => void;
190
+ }) => {
191
+ React.useEffect(() => {
192
+ // Simulate async operation completing
193
+ const timer = setTimeout(() => {
194
+ onAsyncRender();
195
+ }, 0);
196
+
197
+ return () => clearTimeout(timer);
198
+ }, [onAsyncRender]);
199
+
200
+ return (
201
+ <View>
202
+ <Text>Effect Component</Text>
203
+ </View>
204
+ );
205
+ };
206
+
207
+ const WrappedComponent = withAsyncRenderHOC(ComponentWithEffect);
208
+
209
+ render(<WrappedComponent emitAsyncElementLayout={mockLayout} />);
210
+
211
+ // The effect should have been triggered
212
+ return new Promise((resolve) => {
213
+ setTimeout(() => {
214
+ expect(mockLayout).toHaveBeenCalled();
215
+ resolve(true);
216
+ }, 10);
217
+ });
218
+ });
219
+ });
@@ -6,12 +6,12 @@ type withAsyncRenderHOCProps = {
6
6
  emitAsyncElementLayout?: () => void;
7
7
  };
8
8
 
9
- type withAsyncRenderHOCT = (
10
- Component: React.ComponentType<{ onAsyncRender: () => void }>
11
- ) => React.FC<withAsyncRenderHOCProps>;
12
-
13
- export const withAsyncRenderHOC: withAsyncRenderHOCT = (Component) => {
14
- const WithAsyncRender = (props: withAsyncRenderHOCProps) => {
9
+ export const withAsyncRenderHOC = <P extends { onAsyncRender: () => void }>(
10
+ Component: React.ComponentType<P>
11
+ ): React.FC<Omit<P, "onAsyncRender"> & withAsyncRenderHOCProps> => {
12
+ const WithAsyncRender = (
13
+ props: Omit<P, "onAsyncRender"> & withAsyncRenderHOCProps
14
+ ) => {
15
15
  const { emitAsyncElementRegistrate = noop, emitAsyncElementLayout = noop } =
16
16
  props;
17
17
 
@@ -27,7 +27,9 @@ export const withAsyncRenderHOC: withAsyncRenderHOCT = (Component) => {
27
27
  }
28
28
  }, [emitAsyncElementLayout]);
29
29
 
30
- return <Component {...props} onAsyncRender={onAsyncRender} />;
30
+ return (
31
+ <Component {...(props as unknown as P)} onAsyncRender={onAsyncRender} />
32
+ );
31
33
  };
32
34
 
33
35
  return WithAsyncRender;
@@ -1,4 +1,6 @@
1
1
  import * as React from "react";
2
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
3
+ import { useNetworkStatusLocalizations } from "./utils";
2
4
 
3
5
  type Props = {
4
6
  children?: React.ReactNode;
@@ -10,13 +12,6 @@ type Props = {
10
12
 
11
13
  const NOTIF_DURATION = 4500;
12
14
 
13
- export const ONLINE_MSG = "You are back online";
14
-
15
- export const OFFLINE_MSG = "No internet connection";
16
-
17
- const EXTRA_OFFLINE_MSG = "Please check your connection";
18
- const EXTRA_ONLINE_MSG = "Feel free to continue where you left off";
19
-
20
15
  const styles: Record<any, React.CSSProperties> = {
21
16
  body: {
22
17
  position: "absolute",
@@ -47,6 +42,9 @@ const styles: Record<any, React.CSSProperties> = {
47
42
  export const NotificationView = (props: Props) => {
48
43
  const { children, hidden, dismiss, previousOnline, online } = props;
49
44
 
45
+ const theme = useTheme<BaseThemePropertiesTV>();
46
+ const storedLocalizations = useNetworkStatusLocalizations();
47
+
50
48
  const [open, setOpen] = React.useState<boolean | null>(null);
51
49
 
52
50
  const timeout = React.useRef<NodeJS.Timeout>();
@@ -84,8 +82,18 @@ export const NotificationView = (props: Props) => {
84
82
  }, [hidden, online]);
85
83
 
86
84
  const showOnlineMsg = wentOnline || online;
87
- const MSG = showOnlineMsg ? ONLINE_MSG : OFFLINE_MSG;
88
- const EXTRA_MSG = showOnlineMsg ? EXTRA_ONLINE_MSG : EXTRA_OFFLINE_MSG;
85
+
86
+ const MSG = showOnlineMsg
87
+ ? (theme?.online_notification_title ??
88
+ storedLocalizations?.online_notification_title)
89
+ : (theme?.offline_notification_title ??
90
+ storedLocalizations?.offline_notification_title);
91
+
92
+ const EXTRA_MSG = showOnlineMsg
93
+ ? (theme?.online_notification_subtitle ??
94
+ storedLocalizations?.online_notification_subtitle)
95
+ : (theme?.offline_notification_subtitle ??
96
+ storedLocalizations?.offline_notification_subtitle);
89
97
 
90
98
  return (
91
99
  <div onClick={onClose}>
@@ -1,4 +1,6 @@
1
1
  import * as React from "react";
2
+ import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
3
+ import { useNetworkStatusLocalizations } from "./utils";
2
4
 
3
5
  type Props = {
4
6
  children?: React.ReactNode;
@@ -9,12 +11,6 @@ type Props = {
9
11
 
10
12
  const DURATION_TO_HIDE_AFTER_BACK_TO_ONLINE = 4500; // ms
11
13
 
12
- const ONLINE_TITLE = "You are back online";
13
- const ONLINE_SUBTITLE = "Feel free to continue where you left off";
14
-
15
- const OFFLINE_TITLE = "No internet connection";
16
- const OFFLINE_SUBTITLE = "Please check your connection";
17
-
18
14
  const styles: Record<any, React.CSSProperties> = {
19
15
  body: {
20
16
  position: "absolute",
@@ -47,6 +43,9 @@ let timer: NodeJS.Timeout;
47
43
  export const NotificationView = (props: Props) => {
48
44
  const { children, dismiss, previousOnline = true, online } = props;
49
45
 
46
+ const theme = useTheme<BaseThemePropertiesTV>();
47
+ const storedLocalizations = useNetworkStatusLocalizations();
48
+
50
49
  const [shown, setShown] = React.useState<boolean>(false);
51
50
 
52
51
  const onClose = () => {
@@ -79,8 +78,17 @@ export const NotificationView = (props: Props) => {
79
78
  }
80
79
  }, [online, previousOnline]);
81
80
 
82
- const title = online ? ONLINE_TITLE : OFFLINE_TITLE;
83
- const subtitle = online ? ONLINE_SUBTITLE : OFFLINE_SUBTITLE;
81
+ const title = online
82
+ ? (theme?.online_notification_title ??
83
+ storedLocalizations?.online_notification_title)
84
+ : (theme?.offline_notification_title ??
85
+ storedLocalizations?.offline_notification_title);
86
+
87
+ const subtitle = online
88
+ ? (theme?.online_notification_subtitle ??
89
+ storedLocalizations?.online_notification_subtitle)
90
+ : (theme?.offline_notification_subtitle ??
91
+ storedLocalizations?.offline_notification_subtitle);
84
92
 
85
93
  return (
86
94
  <div onClick={onClose}>
@@ -0,0 +1,34 @@
1
+ import * as React from "react";
2
+ import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
3
+
4
+ export const NETWORK_STATUS_LOCALIZATIONS_KEY = "network_status_localizations";
5
+
6
+ export const THEME_STORAGE_NAMESPACE = "quick-brick-theme";
7
+
8
+ export function useNetworkStatusLocalizations() {
9
+ const [storedLocalizations, setStoredLocalizations] = React.useState<Record<
10
+ string,
11
+ string
12
+ > | null>(null);
13
+
14
+ React.useEffect(() => {
15
+ async function loadStoredLocalizations() {
16
+ try {
17
+ const stored = await sessionStorage.getItem(
18
+ NETWORK_STATUS_LOCALIZATIONS_KEY,
19
+ THEME_STORAGE_NAMESPACE
20
+ );
21
+
22
+ if (stored) {
23
+ setStoredLocalizations(stored);
24
+ }
25
+ } catch (error) {
26
+ console.error("Error loading network status localizations", error);
27
+ }
28
+ }
29
+
30
+ loadStoredLocalizations();
31
+ }, []);
32
+
33
+ return storedLocalizations;
34
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "15.0.0-alpha.4920595583",
3
+ "version": "15.0.0-alpha.5104105031",
4
4
  "description": "Applicaster Zapp React Native ui components for the Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,10 +28,10 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "15.0.0-alpha.4920595583",
32
- "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.4920595583",
33
- "@applicaster/zapp-react-native-redux": "15.0.0-alpha.4920595583",
34
- "@applicaster/zapp-react-native-utils": "15.0.0-alpha.4920595583",
31
+ "@applicaster/applicaster-types": "15.0.0-alpha.5104105031",
32
+ "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.5104105031",
33
+ "@applicaster/zapp-react-native-redux": "15.0.0-alpha.5104105031",
34
+ "@applicaster/zapp-react-native-utils": "15.0.0-alpha.5104105031",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "url": "^0.11.0",