@applicaster/zapp-react-native-ui-components 15.0.0-rc.136 → 15.0.0-rc.138

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.
@@ -195,7 +195,6 @@ class TvOSCell extends React.Component<Props, State> {
195
195
  groupId,
196
196
  component,
197
197
  index,
198
- componentsMapOffset,
199
198
  } = this.props;
200
199
 
201
200
  this.setScreenLayout(componentAnchorPointY, screenLayout);
@@ -222,8 +221,7 @@ class TvOSCell extends React.Component<Props, State> {
222
221
  const totalOffset =
223
222
  headerOffset +
224
223
  toNumberWithDefaultZero(componentAnchorPointY) +
225
- extraAnchorPointYOffset -
226
- toNumberWithDefaultZero(componentsMapOffset) +
224
+ extraAnchorPointYOffset +
227
225
  componentMarginTop +
228
226
  componentPaddingTop;
229
227
 
@@ -39,8 +39,6 @@ type Props = {
39
39
  scrollViewExtraProps?: {};
40
40
  riverId?: string;
41
41
  getStaticComponentFeed: any;
42
- pullToRefreshPipesV1RefreshingStateUpdater: () => boolean;
43
- refreshingPipesV1?: boolean;
44
42
  stickyHeaderIndices?: number[];
45
43
  };
46
44
 
@@ -65,10 +63,6 @@ function ComponentsMapComponent(props: Props) {
65
63
  groupId,
66
64
  riverId,
67
65
  getStaticComponentFeed,
68
- // Method added to keep pipes v1 logic up to date with the pullToRefresh state.
69
- // TODO: Remove when pipes v1 is deprecated.
70
- pullToRefreshPipesV1RefreshingStateUpdater,
71
- refreshingPipesV1,
72
66
  stickyHeaderIndices,
73
67
  } = props;
74
68
 
@@ -163,16 +157,8 @@ function ComponentsMapComponent(props: Props) {
163
157
  usePipesCacheReset(riverId, riverComponents);
164
158
 
165
159
  const refreshControl = React.useMemo(
166
- () =>
167
- pullToRefreshEnabled ? (
168
- <RefreshControl
169
- pullToRefreshPipesV1RefreshingStateUpdater={
170
- pullToRefreshPipesV1RefreshingStateUpdater
171
- }
172
- refreshingPipesV1={refreshingPipesV1}
173
- />
174
- ) : null,
175
- []
160
+ () => (pullToRefreshEnabled ? <RefreshControl /> : null),
161
+ [pullToRefreshEnabled]
176
162
  );
177
163
 
178
164
  const navBarStore = useScreenContextV2()._navBarStore;
@@ -4,9 +4,7 @@ import {
4
4
  RefreshControl as RNRefreshControl,
5
5
  StyleSheet,
6
6
  } from "react-native";
7
- import * as R from "ramda";
8
- import { path } from "@applicaster/zapp-react-native-utils/utils";
9
- import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
7
+ import { get } from "@applicaster/zapp-react-native-utils/utils";
10
8
  import { useTheme } from "@applicaster/zapp-react-native-utils/theme";
11
9
  import { useLocalizedStrings } from "@applicaster/zapp-react-native-utils/localizationUtils";
12
10
  import { useAnalytics } from "@applicaster/zapp-react-native-utils/analyticsUtils";
@@ -15,8 +13,7 @@ import { useCurrentScreenData } from "@applicaster/zapp-react-native-utils/react
15
13
  import { useShallow } from "zustand/react/shallow";
16
14
  import { useScreenContextV2 } from "@applicaster/zapp-react-native-utils/reactHooks/screen/useScreenContext";
17
15
  import { useSafeAreaInsets } from "react-native-safe-area-context";
18
- import { useLoadPipesDataDispatch } from "@applicaster/zapp-react-native-utils/reactHooks";
19
- import { useGetUrlInflater } from "@applicaster/zapp-react-native-utils/reactHooks/feed/useInflatedUrl";
16
+ import { usePullToRefresh } from "./hooks";
20
17
 
21
18
  const BRIGHTNESS_THRESHOLD = 160;
22
19
  const ABOVE_DEFAULT_COLOR = "gray";
@@ -56,82 +53,6 @@ const getBrightness = (RGBcolor) => {
56
53
  );
57
54
  };
58
55
 
59
- export const usePullToRefresh = (
60
- riverComponents,
61
- pullToRefreshPipesV1RefreshingStateUpdater,
62
- refreshingPipesV1
63
- ) => {
64
- const isPipesV1 = !!pullToRefreshPipesV1RefreshingStateUpdater;
65
-
66
- const [refreshing, setRefreshing] = React.useState(false);
67
-
68
- const feedSources = React.useMemo(
69
- () =>
70
- (riverComponents || [])
71
- .map((riverComponent) => ({
72
- source: path(["data", "source"], riverComponent),
73
- mapping: path(["data", "mapping"], riverComponent),
74
- }))
75
- .filter(({ source }) => !isNilOrEmpty(source)),
76
- [riverComponents]
77
- );
78
-
79
- const feedsLength = feedSources.length;
80
-
81
- const [requestsCompletedCounter, setRequestsCompletedCounter] =
82
- React.useState(0);
83
-
84
- const loadPipesDataDispatcher = useLoadPipesDataDispatch();
85
- const urlInflater = useGetUrlInflater();
86
-
87
- React.useEffect(() => {
88
- // will not work for pipes v1 on 1st level screens
89
- if (refreshing && !isPipesV1) {
90
- feedSources.forEach(({ source, mapping }) => {
91
- const inflatedUrl = urlInflater(source, mapping);
92
-
93
- if (inflatedUrl) {
94
- loadPipesDataDispatcher(inflatedUrl, {
95
- silentRefresh: true,
96
- clearCache: true,
97
- callback: () => {
98
- setRequestsCompletedCounter(R.inc);
99
- },
100
- });
101
- } else {
102
- setRequestsCompletedCounter(R.inc);
103
- }
104
- });
105
- }
106
- }, [
107
- refreshing,
108
- isPipesV1,
109
- feedSources,
110
- loadPipesDataDispatcher,
111
- urlInflater,
112
- ]);
113
-
114
- React.useEffect(() => {
115
- if (requestsCompletedCounter === feedsLength) {
116
- setRefreshing(false);
117
- }
118
- }, [requestsCompletedCounter, feedsLength]);
119
-
120
- const onRefresh = React.useCallback(() => {
121
- if (isPipesV1) {
122
- pullToRefreshPipesV1RefreshingStateUpdater(true);
123
- } else {
124
- setRefreshing(true);
125
- setRequestsCompletedCounter(0);
126
- }
127
- }, [isPipesV1]);
128
-
129
- return {
130
- refreshing: isPipesV1 ? refreshingPipesV1 : refreshing,
131
- onRefresh,
132
- };
133
- };
134
-
135
56
  /** Returns the offset for the progress view of the RefreshControl component
136
57
  * based on navbar content position */
137
58
  export const useGetProgressViewOffset = () => {
@@ -160,16 +81,12 @@ export const useGetProgressViewOffset = () => {
160
81
  }
161
82
  };
162
83
 
163
- export function RefreshControl(props: {
164
- pullToRefreshPipesV1RefreshingStateUpdater?: (refreshing: boolean) => void;
165
- refreshingPipesV1?: boolean;
166
- }) {
84
+ export function RefreshControl() {
167
85
  const screenData = useCurrentScreenData();
168
86
 
169
87
  const { refreshing, onRefresh } = usePullToRefresh(
170
- screenData.ui_components,
171
- props.pullToRefreshPipesV1RefreshingStateUpdater,
172
- props.refreshingPipesV1
88
+ screenData.id,
89
+ screenData.ui_components
173
90
  );
174
91
 
175
92
  const { app_background_color: themeBackgroundColor } = useTheme();
@@ -187,29 +104,26 @@ export function RefreshControl(props: {
187
104
  displayTitleIOS,
188
105
  } = React.useMemo(
189
106
  () => ({
190
- indicatorColor: R.prop(
191
- "pull_to_refresh_indicator_color",
192
- screenData.styles
193
- ),
194
- titleUnderIndicatorColor: R.prop(
195
- "pull_to_refresh_title_color_under_indicator",
196
- screenData.styles
107
+ indicatorColor: get(screenData.styles, "pull_to_refresh_indicator_color"),
108
+ titleUnderIndicatorColor: get(
109
+ screenData.styles,
110
+ "pull_to_refresh_title_color_under_indicator"
197
111
  ),
198
- indicatorBackgroundColor: R.prop(
199
- "pull_to_refresh_indicator_bg_color",
200
- screenData.styles
112
+ indicatorBackgroundColor: get(
113
+ screenData.styles,
114
+ "pull_to_refresh_indicator_bg_color"
201
115
  ),
202
116
  indicatorSize:
203
- R.prop("pull_to_refresh_indicator_size", screenData.styles) === "large"
117
+ get(screenData.styles, "pull_to_refresh_indicator_size") === "large"
204
118
  ? "large"
205
119
  : "default",
206
- generalContentBackgroungColor: R.prop(
207
- "screen_background_color",
208
- screenData.styles
120
+ generalContentBackgroungColor: get(
121
+ screenData.styles,
122
+ "screen_background_color"
209
123
  ),
210
- displayTitleIOS: R.prop(
211
- "pull_to_refresh_display_title_ios",
212
- screenData.styles
124
+ displayTitleIOS: get(
125
+ screenData.styles,
126
+ "pull_to_refresh_display_title_ios"
213
127
  ),
214
128
  }),
215
129
  [screenData]
@@ -276,7 +190,6 @@ export function RefreshControl(props: {
276
190
 
277
191
  return (
278
192
  <RNRefreshControl
279
- {...props}
280
193
  refreshing={refreshing}
281
194
  onRefresh={onRefreshHandler}
282
195
  colors={[preparedIndicatorColor]}
@@ -2,7 +2,6 @@ import * as React from "react";
2
2
  import * as R from "ramda";
3
3
  import { GeneralContentScreen } from "@applicaster/zapp-react-native-ui-components/Components/GeneralContentScreen";
4
4
 
5
- import { FeedLoader } from "../FeedLoader";
6
5
  import { ScreenResolver } from "../ScreenResolver";
7
6
 
8
7
  type Props = ZappScreenProps & {
@@ -23,24 +22,13 @@ type Props = ZappScreenProps & {
23
22
  river: ZappRiver;
24
23
  };
25
24
 
26
- type State = {
27
- refreshing: boolean;
28
- };
29
-
30
- export class RiverComponent extends React.Component<Props, State> {
25
+ export class RiverComponent extends React.Component<Props> {
31
26
  private currentScreenTitle: string;
32
27
  private currentScreenSummary: string;
33
28
  constructor(props: Props) {
34
29
  super(props);
35
30
 
36
- this.state = {
37
- refreshing: false,
38
- };
39
-
40
31
  this.applyContexts();
41
-
42
- this.pullToRefreshPipesV1RefreshingStateUpdater =
43
- this.pullToRefreshPipesV1RefreshingStateUpdater.bind(this);
44
32
  }
45
33
 
46
34
  applyContexts() {
@@ -69,20 +57,9 @@ export class RiverComponent extends React.Component<Props, State> {
69
57
  }
70
58
  }
71
59
 
72
- usesPipesV1Layout() {
73
- return this.props?.appData?.layoutVersion === "v1";
74
- }
75
-
76
- // Method added to keep pipes v1 logic up to date with the pullToRefresh state.
77
- // TODO: Remove when pipes v1 is deprecated.
78
- pullToRefreshPipesV1RefreshingStateUpdater(refreshing) {
79
- this.setState({ refreshing });
80
- }
81
-
82
60
  render() {
83
61
  const {
84
62
  river,
85
- feedUrl,
86
63
  screenData,
87
64
  isInsideContainer,
88
65
  groupId,
@@ -91,22 +68,10 @@ export class RiverComponent extends React.Component<Props, State> {
91
68
 
92
69
  const { id, type } = river;
93
70
 
94
- const connectedFeedURL = R.path(["content", "src"], screenData);
95
- const _feedUrl = feedUrl || connectedFeedURL;
96
-
97
71
  if (type !== "general_content") {
98
- let riverWithConnectedDatasource;
99
-
100
- if (_feedUrl && this.usesPipesV1Layout()) {
101
- riverWithConnectedDatasource = {
102
- ...river,
103
- data: R.merge(river.data || {}, { source: _feedUrl }),
104
- };
105
- }
106
-
107
72
  return (
108
73
  <ScreenResolver
109
- screenData={R.mergeLeft(riverWithConnectedDatasource || river, {
74
+ screenData={R.mergeLeft(river, {
110
75
  groupId,
111
76
  ...screenData,
112
77
  })}
@@ -116,53 +81,15 @@ export class RiverComponent extends React.Component<Props, State> {
116
81
  );
117
82
  }
118
83
 
119
- if (!_feedUrl || !this.usesPipesV1Layout()) {
120
- this.currentScreenTitle = (screenData && screenData.title) || null;
121
-
122
- return (
123
- <GeneralContentScreen
124
- screenId={id}
125
- isScreenWrappedInContainer={isInsideContainer}
126
- groupId={groupId}
127
- scrollViewExtraProps={scrollViewExtraProps}
128
- />
129
- );
130
- }
84
+ this.currentScreenTitle = (screenData && screenData.title) || null;
131
85
 
132
86
  return (
133
- <FeedLoader
134
- feedUrl={_feedUrl}
135
- refreshing={this.state.refreshing}
136
- refreshCallback={() =>
137
- this.pullToRefreshPipesV1RefreshingStateUpdater(false)
138
- }
139
- >
140
- {(feed) => {
141
- if (!feed) {
142
- return null;
143
- }
144
-
145
- this.currentScreenSummary = (feed && feed.summary) || null;
146
-
147
- this.currentScreenTitle =
148
- (feed && feed.title) || (screenData && screenData.title) || null;
149
-
150
- return (
151
- <GeneralContentScreen
152
- screenId={id}
153
- groupId={groupId}
154
- feed={feed}
155
- isScreenWrappedInContainer={isInsideContainer}
156
- scrollViewExtraProps={scrollViewExtraProps}
157
- componentsMapExtraProps={{
158
- pullToRefreshPipesV1RefreshingStateUpdater:
159
- this.pullToRefreshPipesV1RefreshingStateUpdater,
160
- refreshingPipesV1: this.state.refreshing,
161
- }}
162
- />
163
- );
164
- }}
165
- </FeedLoader>
87
+ <GeneralContentScreen
88
+ screenId={id}
89
+ isScreenWrappedInContainer={isInsideContainer}
90
+ groupId={groupId}
91
+ scrollViewExtraProps={scrollViewExtraProps}
92
+ />
166
93
  );
167
94
  }
168
95
  }
@@ -0,0 +1,132 @@
1
+ import { act, renderHook } from "@testing-library/react-native";
2
+ import { refreshCoordinator } from "@applicaster/zapp-react-native-utils/refreshUtils/RefreshCoordinator";
3
+
4
+ import { usePullToRefresh } from "../usePullToRefresh";
5
+
6
+ jest.mock(
7
+ "@applicaster/zapp-react-native-utils/refreshUtils/RefreshCoordinator",
8
+ () => ({
9
+ refreshCoordinator: {
10
+ triggerRefresh: jest.fn(),
11
+ },
12
+ })
13
+ );
14
+
15
+ jest.useFakeTimers();
16
+
17
+ describe("usePullToRefresh", () => {
18
+ const screenId = "test-screen";
19
+ const components = [{ id: "comp1" }, { id: "comp2" }] as any;
20
+
21
+ beforeEach(() => {
22
+ jest.clearAllMocks();
23
+ });
24
+
25
+ it("should initialize with refreshing=false", () => {
26
+ const { result } = renderHook(() => usePullToRefresh(screenId, components));
27
+
28
+ expect(result.current.refreshing).toBe(false);
29
+ });
30
+
31
+ it("should trigger refresh and set refreshing=true", () => {
32
+ const { result } = renderHook(() => usePullToRefresh(screenId, components));
33
+
34
+ act(() => {
35
+ result.current.onRefresh();
36
+ });
37
+
38
+ expect(result.current.refreshing).toBe(true);
39
+
40
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledWith(
41
+ components,
42
+ screenId
43
+ );
44
+ });
45
+
46
+ it("should set refreshing=false after spinner duration", () => {
47
+ const { result } = renderHook(() => usePullToRefresh(screenId, components));
48
+
49
+ act(() => {
50
+ result.current.onRefresh();
51
+ });
52
+
53
+ expect(result.current.refreshing).toBe(true);
54
+
55
+ act(() => {
56
+ jest.advanceTimersByTime(1500);
57
+ });
58
+
59
+ expect(result.current.refreshing).toBe(false);
60
+ });
61
+
62
+ it("should not stop refreshing before spinner duration", () => {
63
+ const { result } = renderHook(() => usePullToRefresh(screenId, components));
64
+
65
+ act(() => {
66
+ result.current.onRefresh();
67
+ });
68
+
69
+ act(() => {
70
+ jest.advanceTimersByTime(1000);
71
+ });
72
+
73
+ expect(result.current.refreshing).toBe(true);
74
+ });
75
+
76
+ it("should clear timeout on unmount", () => {
77
+ const clearTimeoutSpy = jest.spyOn(global, "clearTimeout");
78
+
79
+ const { result, unmount } = renderHook(() =>
80
+ usePullToRefresh(screenId, components)
81
+ );
82
+
83
+ act(() => {
84
+ result.current.onRefresh();
85
+ });
86
+
87
+ unmount();
88
+
89
+ expect(clearTimeoutSpy).toHaveBeenCalled();
90
+ });
91
+
92
+ it("should handle multiple refresh calls correctly", () => {
93
+ const { result } = renderHook(() => usePullToRefresh(screenId, components));
94
+
95
+ act(() => {
96
+ result.current.onRefresh();
97
+ result.current.onRefresh();
98
+ });
99
+
100
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledTimes(2);
101
+ expect(result.current.refreshing).toBe(true);
102
+
103
+ act(() => {
104
+ jest.advanceTimersByTime(1500);
105
+ });
106
+
107
+ expect(result.current.refreshing).toBe(false);
108
+ });
109
+
110
+ it("should use latest props in callback", () => {
111
+ const { result, rerender } = renderHook(
112
+ ({ screenId, components }) => usePullToRefresh(screenId, components),
113
+ {
114
+ initialProps: { screenId, components },
115
+ }
116
+ );
117
+
118
+ const newComponents = [{ id: "new-comp" }] as any;
119
+ const newScreenId = "new-screen";
120
+
121
+ rerender({ screenId: newScreenId, components: newComponents });
122
+
123
+ act(() => {
124
+ result.current.onRefresh();
125
+ });
126
+
127
+ expect(refreshCoordinator.triggerRefresh).toHaveBeenCalledWith(
128
+ newComponents,
129
+ newScreenId
130
+ );
131
+ });
132
+ });
@@ -0,0 +1 @@
1
+ export { usePullToRefresh } from "./usePullToRefresh";
@@ -0,0 +1,51 @@
1
+ import { useState, useCallback, useRef, useEffect } from "react";
2
+ import { refreshCoordinator } from "@applicaster/zapp-react-native-utils/refreshUtils/RefreshCoordinator";
3
+
4
+ const SPINNER_DURATION_MS = 1500;
5
+
6
+ /**
7
+ * Pull-to-refresh hook.
8
+ *
9
+ * Triggers a refresh for all screen components via RefreshCoordinator.
10
+ * Each component's UrlFeedResolver already subscribes to refresh$ events
11
+ * and calls reloadData() when triggered — so this hook only needs to:
12
+ * 1. Push events into the refresh bus
13
+ * 2. Show a fixed-duration spinner as UX feedback
14
+ *
15
+ * Data updates arrive reactively via Redux (silentRefresh: true keeps
16
+ * old data visible while loading).
17
+ */
18
+ export const usePullToRefresh = (
19
+ screenId: string,
20
+ components: ZappUIComponent[] = []
21
+ ) => {
22
+ const [refreshing, setRefreshing] = useState(false);
23
+ const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
24
+
25
+ // Cleanup timer on unmount
26
+ useEffect(() => {
27
+ return () => {
28
+ if (timerRef.current) {
29
+ clearTimeout(timerRef.current);
30
+ }
31
+ };
32
+ }, []);
33
+
34
+ const onRefresh = useCallback(() => {
35
+ setRefreshing(true);
36
+ refreshCoordinator.triggerRefresh(components, screenId);
37
+
38
+ // Spinner is UX feedback for the gesture.
39
+ // Data updates arrive reactively via Redux (silentRefresh: true).
40
+ if (timerRef.current) {
41
+ clearTimeout(timerRef.current);
42
+ }
43
+
44
+ timerRef.current = setTimeout(
45
+ () => setRefreshing(false),
46
+ SPINNER_DURATION_MS
47
+ );
48
+ }, [components, screenId]);
49
+
50
+ return { refreshing, onRefresh };
51
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-ui-components",
3
- "version": "15.0.0-rc.136",
3
+ "version": "15.0.0-rc.138",
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-rc.136",
32
- "@applicaster/zapp-react-native-bridge": "15.0.0-rc.136",
33
- "@applicaster/zapp-react-native-redux": "15.0.0-rc.136",
34
- "@applicaster/zapp-react-native-utils": "15.0.0-rc.136",
31
+ "@applicaster/applicaster-types": "15.0.0-rc.138",
32
+ "@applicaster/zapp-react-native-bridge": "15.0.0-rc.138",
33
+ "@applicaster/zapp-react-native-redux": "15.0.0-rc.138",
34
+ "@applicaster/zapp-react-native-utils": "15.0.0-rc.138",
35
35
  "fast-json-stable-stringify": "^2.1.0",
36
36
  "promise": "^8.3.0",
37
37
  "url": "^0.11.0",