@applicaster/zapp-react-native-utils 13.0.0-alpha.3454305619 → 13.0.0-alpha.3546196593

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.
@@ -4,7 +4,7 @@ import { postAnalyticEvent } from "../manager";
4
4
  import { ANALYTICS_CORE_EVENTS } from "../events";
5
5
 
6
6
  type SendHeaderClickEventProps = {
7
- extraProps: Record<string, any>;
7
+ extraProps: ExtraProps;
8
8
  component?: ZappUIComponent;
9
9
  zappPipesData?: ZappPipesData;
10
10
  item?: ZappEntry;
@@ -1,11 +1,10 @@
1
- /// <reference types="../../" />
2
1
  import { log_error, log_debug } from "../logger";
3
2
  import { replaceAnalyticsPropsNils } from "./helper";
4
3
  import { postAnalyticEvent } from "../manager";
5
4
 
6
5
  import { ANALYTICS_CORE_EVENTS } from "../events";
7
6
 
8
- type AnalyticsDefaultHelperProperties = {
7
+ declare type AnalyticsDefaultHelperProperties = {
9
8
  analyticsScreenData: AnalyticsScreenProperties;
10
9
  extraProps: any;
11
10
  props;
@@ -1,3 +1,4 @@
1
+ /// <reference types="@applicaster/zapp-react-native-utils" />
1
2
  import * as R from "ramda";
2
3
  import * as React from "react";
3
4
  import { isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
@@ -30,7 +31,7 @@ import { ANALYTICS_CORE_EVENTS } from "./events";
30
31
  import { noop } from "../functionUtils";
31
32
 
32
33
  type ComponentWithChildrenProps = {
33
- children: React.ReactElement;
34
+ children: React.ReactChildren;
34
35
  };
35
36
 
36
37
  export function sendSelectCellEvent(item, component, headerTitle, itemIndex) {
@@ -119,11 +120,11 @@ export function getAnalyticsFunctions({
119
120
  export const AnalyticsContext =
120
121
  React.createContext<GetAnalyticsFunctions>(noop);
121
122
 
122
- export function AnalyticsProvider({ children }: ComponentWithChildrenProps) {
123
+ export function AnalyticsProvider(props: ComponentWithChildrenProps) {
123
124
  return (
124
125
  // @ts-ignore - this is a valid context provider
125
126
  <AnalyticsContext.Provider value={getAnalyticsFunctions}>
126
- {children}
127
+ {props?.children}
127
128
  </AnalyticsContext.Provider>
128
129
  );
129
130
  }
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable @typescript-eslint/no-use-before-define */
2
2
  import * as R from "ramda";
3
3
  import { NativeModules } from "react-native";
4
- import { ANALYTICS_CORE_EVENTS } from "./events";
4
+ import { ANALYTICS_CORE_EVENTS } from "@applicaster/zapp-react-native-utils/analyticsUtils/events";
5
5
 
6
6
  import { analyticsUtilsLogger } from "./logger";
7
7
 
@@ -1,7 +1,8 @@
1
1
  import * as ReactNative from "react-native";
2
- import { isTV, platformSelect } from "../reactUtils";
3
2
  import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
4
- import { useIsTablet } from "@applicaster/zapp-react-native-utils/reactHooks/device/useIsTablet";
3
+
4
+ import { isTV, platformSelect } from "../reactUtils";
5
+ import { useIsTablet } from "../reactHooks";
5
6
 
6
7
  import {
7
8
  QUICK_BRICK_EVENTS,
@@ -120,8 +121,15 @@ export const removeOrientationChangeListener = (listener: any): void => {
120
121
  */
121
122
  export const isOrientationCompatible = ({
122
123
  orientation,
123
- layoutData: { width, height },
124
+ layoutData: { width, height, isTablet, isTabletPortrait },
124
125
  }) => {
126
+ // We count portrait as compatible, because if default in zapp for mobile layout
127
+ // We count landscape as compatible, because if isTabletPortrait is false,
128
+ // it defaults for device
129
+ if (isTablet && !isTabletPortrait) {
130
+ return true;
131
+ }
132
+
125
133
  const isLandscape = width > height;
126
134
 
127
135
  if (
@@ -145,15 +153,6 @@ export const isOrientationCompatible = ({
145
153
  return true;
146
154
  };
147
155
 
148
- export const useIsOrientationCompatible = (orientation) => {
149
- const { height, width } = ReactNative.useWindowDimensions();
150
-
151
- return isOrientationCompatible({
152
- orientation,
153
- layoutData: { width, height },
154
- });
155
- };
156
-
157
156
  /**
158
157
  * Determine final screen orientation based on device type,
159
158
  * settings and orientation value provided by screen data settings
@@ -97,7 +97,7 @@ export class Player {
97
97
  this.onVideoLoad = this.onVideoLoad.bind(this);
98
98
  this.isFullScreenSupported = this.isFullScreenSupported.bind(this);
99
99
  this.onPlayerSeekComplete = this.onPlayerSeekComplete.bind(this);
100
-
100
+ this.onAdBreakEnd = this.onAdBreakEnd.bind(this);
101
101
  this.entryObservable = null;
102
102
  }
103
103
 
@@ -201,17 +201,25 @@ export class Player {
201
201
  adPosition: currentTime,
202
202
  };
203
203
  } else {
204
- if (!isEmptyOrNil(seekableDuration) && seekableDuration >= 0) {
205
- this.playerState.seekableDuration = seekableDuration;
206
- }
204
+ this.updateContentDuration({ seekableDuration, duration, currentTime });
205
+ }
206
+ };
207
207
 
208
- if (!isEmptyOrNil(duration) && duration >= 0) {
209
- this.playerState.contentDuration = duration;
210
- }
208
+ protected updateContentDuration = ({
209
+ seekableDuration = null,
210
+ duration = null,
211
+ currentTime = null,
212
+ }) => {
213
+ if (!isEmptyOrNil(seekableDuration) && seekableDuration >= 0) {
214
+ this.playerState.seekableDuration = seekableDuration;
215
+ }
211
216
 
212
- if (!isEmptyOrNil(currentTime) && currentTime >= 0) {
213
- this.playerState.contentPosition = currentTime;
214
- }
217
+ if (!isEmptyOrNil(duration) && duration >= 0) {
218
+ this.playerState.contentDuration = duration;
219
+ }
220
+
221
+ if (!isEmptyOrNil(currentTime) && currentTime >= 0) {
222
+ this.playerState.contentPosition = currentTime;
215
223
  }
216
224
  };
217
225
 
@@ -559,7 +567,7 @@ export class Player {
559
567
  });
560
568
  };
561
569
 
562
- onAdBreakEnd = (event) => {
570
+ onAdBreakEnd(event) {
563
571
  this.playerState.adState = null;
564
572
  this.playerState.isInAdBreak = false;
565
573
 
@@ -567,7 +575,7 @@ export class Player {
567
575
  callbackName: SharedPlayerCallBacksKeys.OnAdBreakEnd,
568
576
  event,
569
577
  });
570
- };
578
+ }
571
579
 
572
580
  onAdEnd = (event) => {
573
581
  this.playerState.adState = null;
@@ -736,6 +744,10 @@ export class Player {
736
744
  return null;
737
745
  };
738
746
 
747
+ public hasResumePosition() {
748
+ return false;
749
+ }
750
+
739
751
  public getConfig = () => this.config;
740
752
 
741
753
  // TODO: replace with enum of supported orientations
@@ -54,6 +54,10 @@ export class PlayerNative extends Player {
54
54
  });
55
55
  }
56
56
 
57
+ public hasResumePosition() {
58
+ return true;
59
+ }
60
+
57
61
  getContentPosition = () => {
58
62
  return this.playerState.contentPosition;
59
63
  };
@@ -221,11 +225,13 @@ export class PlayerNative extends Player {
221
225
  };
222
226
 
223
227
  onVideoLoad = (event) => {
224
- if (this.startPosition) {
225
- this.seekTo(this.startPosition);
226
- this.startPosition = null;
228
+ this.updateContentDuration(event);
227
229
 
230
+ if (this.startPosition && !this.playerState.isInAdBreak) {
228
231
  super.onVideoLoad({ ...event, ignoreContinueWatching: true });
232
+
233
+ this.seekTo(this.startPosition);
234
+ this.startPosition = null;
229
235
  } else {
230
236
  super.onVideoLoad(event);
231
237
  }
@@ -249,6 +255,15 @@ export class PlayerNative extends Player {
249
255
  }
250
256
  };
251
257
 
258
+ onAdBreakEnd = (event) => {
259
+ super.onAdBreakEnd(event);
260
+
261
+ if (this.startPosition) {
262
+ this.seekTo(this.startPosition);
263
+ this.startPosition = null;
264
+ }
265
+ };
266
+
252
267
  // This function is called when native player used directly without wrapped component,
253
268
  // Since event from native comes with wrapped in NativeEvent property
254
269
  getNativeViewListener = (): QuickBrickPlayer.SharedPlayerCallBacks => {
@@ -227,7 +227,9 @@ class FocusManager {
227
227
 
228
228
  const nextNextFocus = FocusManager.getNextFocusable(direction, nextFocus);
229
229
 
230
- FocusManager.instance.setFocus(nextNextFocus, options);
230
+ if (nextNextFocus) {
231
+ FocusManager.instance.setFocus(nextNextFocus, options);
232
+ }
231
233
  } else {
232
234
  FocusManager.instance.setFocusLocal(nextFocus);
233
235
 
@@ -100,7 +100,8 @@ type Props = {
100
100
  } & Record<string, any>;
101
101
 
102
102
  export const useIsFocusable = (props: Props): void => {
103
- const { component, zappPipesData } = props;
103
+ const { forceFocusable = false, component, zappPipesData } = props;
104
+
104
105
  const { setIsFocusable } = useFocusable();
105
106
 
106
107
  const componentCellsFocusable = toBooleanWithDefaultTrue(
@@ -117,25 +118,41 @@ export const useIsFocusable = (props: Props): void => {
117
118
  isNilOrEmpty(zappPipesData?.data) ||
118
119
  isNilOrEmpty(zappPipesData?.data?.entry);
119
120
 
121
+ const hasNoUIComponents = isNilOrEmpty(
122
+ component?.ui_components?.filter(
123
+ ({ component_type }) => component_type !== "empty_group_component"
124
+ )
125
+ );
126
+
120
127
  const hasDataError = !!zappPipesData?.error;
121
128
 
122
129
  const hasEmptyGroup = shouldShowEmptyGroup(component);
123
130
 
124
131
  const isGallery = component?.component_type === "gallery-qb";
125
132
 
133
+ const isGroup = component?.component_type === "group-qb";
134
+
126
135
  const isHiddenDueToEmptyData =
127
136
  !isLoadingData && hasNoData && hideIfDataEmpty && !hasEmptyGroup;
128
137
 
138
+ const isEmptyGroup = isGroup && hasNoUIComponents;
139
+
129
140
  const defaultFocusableLogic =
130
- componentCellsFocusable && (!hasNoData || !hideIfDataEmpty);
141
+ componentCellsFocusable &&
142
+ (!hasNoData || !hideIfDataEmpty) &&
143
+ !isEmptyGroup;
131
144
 
132
145
  React.useEffect(() => {
133
146
  if (isAndroidTVPlatform()) {
134
147
  let isFocusableNew = false;
135
148
 
136
- if (isGallery) {
137
- isFocusableNew = true;
138
- } else if (isFocusable(component)) {
149
+ if (forceFocusable) {
150
+ setIsFocusable(true);
151
+
152
+ return;
153
+ }
154
+
155
+ if (isFocusable(component)) {
139
156
  isFocusableNew = defaultFocusableLogic;
140
157
  }
141
158
 
@@ -50,6 +50,14 @@ function getPlayerConfiguration({ platform }) {
50
50
  label_tooltip:
51
51
  "Set the query params which control the size of the image e.g width, height. This will help to optimize the loading time, when magic background is enabled.",
52
52
  },
53
+ {
54
+ key: "audio_player_background_image_default_color",
55
+ label: "Audio Player Background Image Default Color",
56
+ initial_value: "",
57
+ type: "text_input",
58
+ label_tooltip:
59
+ "Set the default color for the audio player background image if not image or set of gradient colors are not provided.",
60
+ },
53
61
  ],
54
62
  };
55
63
 
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "13.0.0-alpha.3454305619",
3
+ "version": "13.0.0-alpha.3546196593",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
7
7
  "scripts": {
8
- "test": "jest -w=2",
9
8
  "build": "echo skipping hybrid ts build in main packages for now"
10
9
  },
11
10
  "publishConfig": {
@@ -28,7 +27,7 @@
28
27
  },
29
28
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
29
  "dependencies": {
31
- "@applicaster/applicaster-types": "13.0.0-alpha.3454305619",
30
+ "@applicaster/applicaster-types": "13.0.0-alpha.3546196593",
32
31
  "buffer": "^5.2.1",
33
32
  "camelize": "^1.0.0",
34
33
  "dayjs": "^1.11.10",
@@ -55,6 +55,14 @@ function getPlayerConfiguration({ platform }) {
55
55
  label_tooltip:
56
56
  "Set the query params which control the size of the image e.g width, height. This will help to optimize the loading time, when magic background is enabled.",
57
57
  },
58
+ {
59
+ key: "audio_player_background_image_default_color",
60
+ label: "Audio Player Background Image Default Color",
61
+ initial_value: "",
62
+ type: "text_input",
63
+ label_tooltip:
64
+ "Set the default color for the audio player background image if not image or set of gradient colors are not provided.",
65
+ },
58
66
  ],
59
67
  };
60
68
 
@@ -14,10 +14,10 @@ export function useAppState(returnIsActive) {
14
14
  };
15
15
 
16
16
  React.useEffect(() => {
17
- const unsubscribe = AppState.addEventListener("change", setAppState);
17
+ AppState.addEventListener("change", setAppState);
18
18
 
19
19
  return () => {
20
- unsubscribe.remove();
20
+ AppState.removeEventListener("change", setAppState);
21
21
  };
22
22
  }, []);
23
23
 
@@ -1,7 +1,8 @@
1
1
  import { renderHook } from "@testing-library/react-hooks";
2
- import { allFeedsIsReady, useBatchLoading } from "../useBatchLoading";
3
- import { WrappedWithProviders } from "@applicaster/zapp-react-native-utils/testUtils";
4
- import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
2
+ import * as reduxMockStore from "redux-mock-store";
3
+ import thunk from "redux-thunk";
4
+ import React from "react";
5
+ import * as ReactRedux from "react-redux";
5
6
 
6
7
  jest.mock("../../navigation");
7
8
 
@@ -12,18 +13,17 @@ jest.mock(
12
13
  })
13
14
  );
14
15
 
15
- const wrapper = WrappedWithProviders;
16
+ const useBatchLoading = require("../useBatchLoading").useBatchLoading;
17
+ const allFeedsIsReady = require("../useBatchLoading").allFeedsIsReady;
18
+
19
+ const mockStore = reduxMockStore.default([thunk]);
20
+
21
+ const wrapper: React.FC<any> = ({ children, store }) => (
22
+ <ReactRedux.Provider store={store}>{children}</ReactRedux.Provider>
23
+ );
16
24
 
17
25
  describe("useBatchLoading", () => {
18
- const data = [
19
- { data: { source: "url1" }, component_type: "any" },
20
- { data: { source: "url2" }, component_type: "any" },
21
- { data: { source: "url3" }, component_type: "any" },
22
- { data: { source: "url4" }, component_type: "any" },
23
- { data: { source: "url5" }, component_type: "any" },
24
- { data: { source: "url6" }, component_type: "any" },
25
- // ... more items
26
- ];
26
+ const useDispatchSpy = jest.spyOn(ReactRedux, "useDispatch");
27
27
 
28
28
  beforeAll(() => {
29
29
  jest.useFakeTimers();
@@ -34,7 +34,7 @@ describe("useBatchLoading", () => {
34
34
  });
35
35
 
36
36
  it("loadPipesData start loading not started requests", () => {
37
- const store = {
37
+ const store = mockStore({
38
38
  zappPipes: {
39
39
  url1: {
40
40
  loading: true,
@@ -53,17 +53,29 @@ describe("useBatchLoading", () => {
53
53
  },
54
54
  },
55
55
  test: "true",
56
- };
56
+ });
57
+
58
+ useDispatchSpy.mockReturnValue(store.dispatch);
57
59
 
58
60
  const initialBatchSize = 3;
59
61
  const riverId = "123";
60
62
 
63
+ const data = [
64
+ { data: { source: "url1" } },
65
+ { data: { source: "url2" } },
66
+ { data: { source: "url3" } },
67
+ { data: { source: "url4" } },
68
+ { data: { source: "url5" } },
69
+ { data: { source: "url6" } },
70
+ // ... more items
71
+ ];
72
+
61
73
  renderHook(() => useBatchLoading(data, { initialBatchSize, riverId }), {
62
74
  wrapper,
63
75
  initialProps: { store },
64
76
  });
65
77
 
66
- const actions = (appStore.getStore() as any).getActions();
78
+ const actions = store.getActions();
67
79
 
68
80
  expect(actions).toHaveLength(2);
69
81
 
@@ -79,7 +91,7 @@ describe("useBatchLoading", () => {
79
91
  });
80
92
 
81
93
  it("loadPipesData start loading new feed when 1 feed is done loading and 1 is in loading state", () => {
82
- const store = {
94
+ const store = mockStore({
83
95
  zappPipes: {
84
96
  url1: {
85
97
  loading: false,
@@ -98,17 +110,31 @@ describe("useBatchLoading", () => {
98
110
  },
99
111
  },
100
112
  test: "true",
101
- };
113
+ });
114
+
115
+ useDispatchSpy.mockReturnValue(store.dispatch);
102
116
 
103
117
  const initialBatchSize = 3;
104
118
  const riverId = "123";
105
119
 
120
+ const data = [
121
+ { data: { source: "url1" } },
122
+ { data: { source: "url2" } },
123
+ { data: { source: "url3" } },
124
+ { data: { source: "url4" } },
125
+ { data: { source: "url5" } },
126
+ { data: { source: "url6" } },
127
+ // ... more items
128
+ ];
129
+
130
+ expect(useDispatchSpy).toBeCalledTimes(0);
131
+
106
132
  renderHook(() => useBatchLoading(data, { initialBatchSize, riverId }), {
107
133
  wrapper,
108
134
  initialProps: { store },
109
135
  });
110
136
 
111
- const actions = (appStore.getStore() as any).getActions();
137
+ const actions = store.getActions();
112
138
 
113
139
  expect(actions).toHaveLength(1);
114
140
 
@@ -119,26 +145,38 @@ describe("useBatchLoading", () => {
119
145
  });
120
146
 
121
147
  it("loadPipesData has been called when no data cached", () => {
122
- const store = {
148
+ const store = mockStore({
123
149
  zappPipes: {},
124
150
  test: "true",
125
- };
151
+ });
152
+
153
+ useDispatchSpy.mockReturnValue(store.dispatch);
126
154
 
127
155
  const initialBatchSize = 3;
128
156
  const riverId = "123";
129
157
 
158
+ const data = [
159
+ { data: { source: "url1" } },
160
+ { data: { source: "url2" } },
161
+ { data: { source: "url3" } },
162
+ { data: { source: "url4" } },
163
+ { data: { source: "url5" } },
164
+ { data: { source: "url6" } },
165
+ // ... more items
166
+ ];
167
+
130
168
  renderHook(() => useBatchLoading(data, { initialBatchSize, riverId }), {
131
169
  wrapper,
132
170
  initialProps: { store },
133
171
  });
134
172
 
135
- const actions = (appStore.getStore() as any).getActions();
173
+ const actions = store.getActions();
136
174
 
137
175
  expect(actions).toHaveLength(3);
138
176
  });
139
177
 
140
178
  it("initial batch ready when all initial items loaded", () => {
141
- const store = {
179
+ const store = mockStore({
142
180
  zappPipes: {
143
181
  url1: {
144
182
  loading: false,
@@ -156,11 +194,19 @@ describe("useBatchLoading", () => {
156
194
  data: {},
157
195
  },
158
196
  },
159
- };
197
+ });
198
+
199
+ useDispatchSpy.mockReturnValue(store.dispatch);
160
200
 
161
201
  const initialBatchSize = 3;
162
202
  const riverId = "123";
163
203
 
204
+ const data: Partial<ZappUIComponent>[] = [
205
+ { data: { source: "url1" } },
206
+ { data: { source: "url2" } },
207
+ { data: { source: "url3" } },
208
+ ];
209
+
164
210
  const { result } = renderHook(
165
211
  () => useBatchLoading(data, { initialBatchSize, riverId }),
166
212
  { wrapper, initialProps: { store } }
@@ -170,10 +216,12 @@ describe("useBatchLoading", () => {
170
216
  });
171
217
 
172
218
  it("gallery-qb: loadPipesData should be called only once for first component in the gallery", () => {
173
- const store = {
219
+ const store = mockStore({
174
220
  zappPipes: {},
175
221
  test: "true",
176
- };
222
+ });
223
+
224
+ useDispatchSpy.mockReturnValue(store.dispatch);
177
225
 
178
226
  const initialBatchSize = 3;
179
227
  const riverId = "123";
@@ -183,11 +231,11 @@ describe("useBatchLoading", () => {
183
231
  component_type: "gallery-qb",
184
232
  ui_components: [{ data: { source: "url1" } }],
185
233
  },
186
- { data: { source: "url2" }, component_type: "any" },
187
- { data: { source: "url3" }, component_type: "any" },
188
- { data: { source: "url4" }, component_type: "any" },
189
- { data: { source: "url5" }, component_type: "any" },
190
- { data: { source: "url6" }, component_type: "any" },
234
+ { data: { source: "url2" } },
235
+ { data: { source: "url3" } },
236
+ { data: { source: "url4" } },
237
+ { data: { source: "url5" } },
238
+ { data: { source: "url6" } },
191
239
  // ... more items
192
240
  ];
193
241
 
@@ -196,13 +244,13 @@ describe("useBatchLoading", () => {
196
244
  initialProps: { store },
197
245
  });
198
246
 
199
- const actions = (appStore.getStore() as any).getActions();
247
+ const actions = store.getActions();
200
248
 
201
249
  expect(actions).toHaveLength(1);
202
250
  });
203
251
 
204
252
  it("gallery-qb: initial batch ready when all initial items loaded", () => {
205
- const store = {
253
+ const store = mockStore({
206
254
  zappPipes: {
207
255
  url1: {
208
256
  loading: false,
@@ -210,19 +258,20 @@ describe("useBatchLoading", () => {
210
258
  data: {},
211
259
  },
212
260
  },
213
- };
261
+ });
262
+
263
+ useDispatchSpy.mockReturnValue(store.dispatch);
214
264
 
215
265
  const initialBatchSize = 3;
216
266
  const riverId = "123";
217
267
 
218
- const data = [
268
+ const data: Partial<ZappUIComponent>[] = [
219
269
  {
220
270
  component_type: "gallery-qb",
221
- data: {},
222
- ui_components: [{ data: { source: "url1" } }] as any,
271
+ ui_components: [{ data: { source: "url1" } }],
223
272
  },
224
- { data: { source: "url2" }, component_type: "any" },
225
- { data: { source: "url3" }, component_type: "any" },
273
+ { data: { source: "url2" } },
274
+ { data: { source: "url3" } },
226
275
  ];
227
276
 
228
277
  const { result } = renderHook(
@@ -108,9 +108,11 @@ export function isNavBarVisible(
108
108
  return false;
109
109
  }
110
110
 
111
- const showSecondaryLevel = screenData?.navigations?.some(
112
- (navigation) => navigation.styles?.show_secondary_level_menu
113
- );
111
+ const showSecondaryLevel =
112
+ !isTV() ||
113
+ screenData?.navigations?.some(
114
+ (navigation) => navigation.styles?.show_secondary_level_menu
115
+ );
114
116
 
115
117
  if (canGoBack && !showSecondaryLevel) {
116
118
  return false;
@@ -125,10 +127,10 @@ export function isNavBarVisible(
125
127
 
126
128
  export const useBackHandler = (cb: () => boolean) => {
127
129
  useEffect(() => {
128
- const unsubscribe = BackHandler.addEventListener("hardwareBackPress", cb);
130
+ BackHandler.addEventListener("hardwareBackPress", cb);
129
131
 
130
132
  return () => {
131
- unsubscribe.remove();
133
+ BackHandler.removeEventListener("hardwareBackPress", cb);
132
134
  };
133
135
  }, [cb]);
134
136
  };