@applicaster/zapp-react-native-utils 16.0.0-alpha.8716141508 → 16.0.0-alpha.9024462102
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.
- package/actionsExecutor/ActionExecutor.ts +50 -3
- package/actionsExecutor/ActionExecutorContext.tsx +99 -17
- package/actionsExecutor/__tests__/ActionExecutor.test.ts +71 -0
- package/actionsExecutor/__tests__/feedDecorator.test.ts +61 -0
- package/actionsExecutor/actions/__tests__/dismissBottomSheet.test.ts +25 -0
- package/actionsExecutor/actions/__tests__/goBack.test.ts +62 -0
- package/actionsExecutor/actions/__tests__/goHome.test.ts +19 -0
- package/actionsExecutor/actions/__tests__/loadItemsFromSource.test.ts +88 -0
- package/actionsExecutor/actions/__tests__/navigateToScreen.test.ts +133 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.customContent.test.ts +76 -0
- package/actionsExecutor/actions/__tests__/openBottomSheet.inlineItems.test.ts +300 -0
- package/actionsExecutor/actions/__tests__/presentScreen.test.ts +127 -0
- package/actionsExecutor/actions/__tests__/showAlert.test.ts +49 -0
- package/actionsExecutor/actions/__tests__/showToast.test.ts +88 -0
- package/actionsExecutor/actions/dismissBottomSheet.ts +23 -0
- package/actionsExecutor/actions/goBack.ts +60 -0
- package/actionsExecutor/actions/goHome.ts +31 -0
- package/actionsExecutor/actions/index.ts +12 -0
- package/actionsExecutor/actions/navigateToScreen.ts +18 -4
- package/actionsExecutor/actions/openBottomSheet.ts +338 -0
- package/actionsExecutor/actions/presentScreen.ts +78 -0
- package/actionsExecutor/actions/refreshComponent.ts +6 -0
- package/actionsExecutor/actions/sendCloudEvent.ts +6 -1
- package/actionsExecutor/actions/showAlert.ts +51 -0
- package/actionsExecutor/actions/showToast.ts +64 -8
- package/actionsExecutor/consts.ts +27 -0
- package/actionsExecutor/feedDecorator.ts +6 -6
- package/analyticsUtils/PlayerAnalyticsManager.ts +1 -1
- package/appUtils/HooksManager/index.ts +9 -0
- package/appUtils/playerManager/OverlayObserver/OverlaysObserver.ts +131 -54
- package/appUtils/playerManager/OverlayObserver/__tests__/getUpcomingQueue.test.ts +67 -0
- package/appUtils/playerManager/OverlayObserver/__tests__/liveContent.test.ts +205 -0
- package/appUtils/playerManager/OverlayObserver/__tests__/utils.test.ts +56 -0
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.android.tv.ts +4 -0
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ios.tv.ts +4 -0
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.ts +24 -0
- package/appUtils/playerManager/OverlayObserver/getUpcomingQueue.web.ts +4 -0
- package/appUtils/playerManager/OverlayObserver/utils.ts +51 -20
- package/appUtils/playerManager/__tests__/playerContent.test.ts +403 -0
- package/appUtils/playerManager/__tests__/playerFactory.test.ts +1 -1
- package/appUtils/playerManager/__tests__/playerNative.test.ts +38 -0
- package/appUtils/playerManager/__tests__/usePlayerContent.test.tsx +64 -0
- package/appUtils/playerManager/{conts.ts → const.ts} +14 -0
- package/appUtils/playerManager/index.ts +1 -1
- package/appUtils/playerManager/player.ts +115 -3
- package/appUtils/playerManager/playerFactory.ts +1 -1
- package/appUtils/playerManager/playerNative.ts +6 -4
- package/appUtils/playerManager/usePlayerContent.tsx +43 -0
- package/appUtils/playerManager/usePlayerControllerSetup.tsx +1 -1
- package/appUtils/playerManager/userLanguagePreference.ts +1 -1
- package/arrayUtils/__tests__/reorderByIds.test.ts +63 -0
- package/arrayUtils/index.ts +62 -0
- package/cellUtils/index.ts +30 -3
- package/configurationUtils/__tests__/manifestKeyParser.test.ts +21 -0
- package/configurationUtils/__tests__/modalBlocksParser.test.ts +269 -0
- package/configurationUtils/manifestKeyParser.ts +50 -10
- package/configurationUtils/modalBlocksParser.ts +214 -0
- package/entryActions/__tests__/buildEntryActions.test.ts +235 -0
- package/entryActions/aliasPresentation.ts +108 -0
- package/entryActions/buildEntryActions.ts +167 -0
- package/entryActions/index.ts +15 -0
- package/entryActions/types.ts +39 -0
- package/manifestUtils/__tests__/player.audioControls.test.js +158 -0
- package/manifestUtils/_internals/index.js +8 -3
- package/manifestUtils/defaultManifestConfigurations/generalContent.js +6 -0
- package/manifestUtils/defaultManifestConfigurations/player.js +330 -1
- package/manifestUtils/index.js +3 -0
- package/manifestUtils/modalBlocks.js +304 -0
- package/modalState/ContentViewModel.ts +113 -0
- package/modalState/EditableCollection.ts +17 -0
- package/modalState/EditableCollectionRegistry.ts +51 -0
- package/modalState/ModalOrchestrator.ts +212 -0
- package/modalState/RemoteEditableCollection.ts +235 -0
- package/modalState/__tests__/ContentViewModel.editable.test.ts +69 -0
- package/modalState/__tests__/ContentViewModel.test.ts +165 -0
- package/modalState/__tests__/EditableCollectionRegistry.test.ts +112 -0
- package/modalState/__tests__/ModalOrchestrator.phase3.test.ts +63 -0
- package/modalState/__tests__/RemoteEditableCollection.test.ts +415 -0
- package/modalState/__tests__/index.test.ts +30 -1
- package/modalState/__tests__/useBottomSheetContent.test.ts +349 -0
- package/modalState/__tests__/useModalStoreState.test.ts +142 -0
- package/modalState/index.ts +35 -83
- package/modalState/store.ts +109 -0
- package/modalState/types.ts +162 -0
- package/modalState/useBottomSheetContent.ts +109 -0
- package/package.json +2 -2
- package/playerUtils/__tests__/contentDataKeys.test.ts +297 -0
- package/playerUtils/__tests__/resolvePlayerActions.test.ts +138 -0
- package/playerUtils/__tests__/resolvePlayerTitleSubtitle.test.ts +180 -0
- package/playerUtils/contentDataKeys.ts +134 -0
- package/playerUtils/index.ts +37 -16
- package/playerUtils/isAudioItem.ts +26 -0
- package/playerUtils/resolvePlayerActions.ts +71 -0
- package/playerUtils/resolvePlayerTitleSubtitle.ts +76 -0
- package/pluginUtils/__tests__/pluginUtils.test.js +32 -0
- package/pluginUtils/index.ts +29 -0
- package/reactHooks/actions/index.ts +67 -2
- package/reactHooks/cell-click/__tests__/index.test.js +64 -0
- package/reactHooks/cell-click/index.ts +21 -14
- package/reactHooks/connection/__tests__/index.test.js +154 -2
- package/reactHooks/connection/index.tsx +71 -7
- package/reactHooks/index.ts +2 -0
- package/reactHooks/navigation/__tests__/index.test.tsx +176 -1
- package/reactHooks/navigation/__tests__/usePresentScreen.test.ts +154 -0
- package/reactHooks/navigation/index.ts +2 -0
- package/reactHooks/navigation/usePresentScreen.ts +157 -0
- package/reactHooks/navigation/useRoute.ts +22 -5
- package/reactHooks/usePluginConfiguration.ts +4 -1
- package/reactHooks/utils/__tests__/index.test.js +22 -2
- package/reactHooks/utils/index.ts +11 -1
- package/uiActionsRegistrator/__tests__/resolveActionIdentifiers.test.ts +93 -0
- package/uiActionsRegistrator/__tests__/subscribe.test.ts +107 -0
- package/uiActionsRegistrator/__tests__/usableActionItems.test.ts +52 -0
- package/uiActionsRegistrator/index.ts +25 -203
- package/uiActionsRegistrator/registry.ts +335 -0
- package/uiActionsRegistrator/resolveActionIdentifiers.ts +100 -0
- package/uiActionsRegistrator/usableActionItems.ts +66 -0
- package/zappFrameworkUtils/localStorageHelper.ts +10 -2
- package/appUtils/playerManager/usePlayerTitleSummaryOverlay.tsx +0 -56
- package/playerUtils/__tests__/getPlayerActionButtons.test.ts +0 -54
- package/playerUtils/getPlayerActionButtons.ts +0 -17
- /package/reactHooks/actions/__tests__/{index.test.js → index.test.tsx} +0 -0
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
const mockAppStoreGet = jest.fn();
|
|
2
|
+
|
|
3
|
+
jest.mock("@applicaster/zapp-react-native-redux/AppStore", () => ({
|
|
4
|
+
appStore: { get: (key) => mockAppStoreGet(key) },
|
|
5
|
+
}));
|
|
6
|
+
|
|
1
7
|
import * as R from "ramda";
|
|
2
8
|
import * as pluginUtils from "../index";
|
|
3
9
|
|
|
@@ -148,6 +154,32 @@ describe("findPluginByIdentifier", () => {
|
|
|
148
154
|
});
|
|
149
155
|
});
|
|
150
156
|
|
|
157
|
+
describe("getPluginByIdentifier", () => {
|
|
158
|
+
const { getPluginByIdentifier } = pluginUtils;
|
|
159
|
+
|
|
160
|
+
const plugins = {
|
|
161
|
+
foo: pluginMock("foo", "foo", "some_plugin_type"),
|
|
162
|
+
bar: pluginMock("foo", "bar", "some_plugin_type"),
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
beforeEach(() => {
|
|
166
|
+
mockAppStoreGet.mockReset();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("reads plugins from appStore and returns the matching plugin", () => {
|
|
170
|
+
mockAppStoreGet.mockReturnValue(plugins);
|
|
171
|
+
|
|
172
|
+
expect(getPluginByIdentifier("foo")).toEqual(plugins.foo);
|
|
173
|
+
expect(mockAppStoreGet).toHaveBeenCalledWith("plugins");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("returns undefined when the plugin is not found", () => {
|
|
177
|
+
mockAppStoreGet.mockReturnValue(plugins);
|
|
178
|
+
|
|
179
|
+
expect(getPluginByIdentifier("do_not_exist")).toBeUndefined();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
|
|
151
183
|
describe("findPluginByType", () => {
|
|
152
184
|
const { findPluginByType } = pluginUtils;
|
|
153
185
|
|
package/pluginUtils/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as R from "ramda";
|
|
2
|
+
import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
|
|
2
3
|
import { toCamelCase, toPascalCase } from "../stringUtils";
|
|
3
4
|
|
|
4
5
|
import { getNavigationType } from "../navigationUtils";
|
|
@@ -97,6 +98,34 @@ export function findPluginByIdentifier(
|
|
|
97
98
|
return plugin;
|
|
98
99
|
}
|
|
99
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Like findPluginByIdentifier, but reads the current plugins list from appStore.
|
|
103
|
+
* Convenience for non-render / hook code that doesn't already have `plugins` in hand.
|
|
104
|
+
* @param identifier of the plugin to look for
|
|
105
|
+
* @param doNotWarn if set to true, will not log a warning if plugin is not found
|
|
106
|
+
* @return whole plugin if found, undefined otherwise
|
|
107
|
+
*/
|
|
108
|
+
export function getPluginByIdentifier(
|
|
109
|
+
identifier: string,
|
|
110
|
+
doNotWarn: boolean = false
|
|
111
|
+
): (QuickBrickPlugin | ZappPlugin | Plugin) | undefined {
|
|
112
|
+
try {
|
|
113
|
+
const plugins = appStore.get("plugins") ?? {};
|
|
114
|
+
|
|
115
|
+
return findPluginByIdentifier(identifier, plugins, doNotWarn);
|
|
116
|
+
} catch (error) {
|
|
117
|
+
if (!doNotWarn) {
|
|
118
|
+
pluginUtilsLogger.warning({
|
|
119
|
+
message:
|
|
120
|
+
"Could not read plugins from appStore while resolving plugin by identifier",
|
|
121
|
+
data: { identifier, error },
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return undefined;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
100
129
|
const pluginTypeForItemTypeMap = {
|
|
101
130
|
playable: "player",
|
|
102
131
|
};
|
|
@@ -6,6 +6,7 @@ import { ActionsContext } from "@applicaster/zapp-react-native-ui-components/Con
|
|
|
6
6
|
import {
|
|
7
7
|
observeEntryState,
|
|
8
8
|
RegisteredActionValue,
|
|
9
|
+
uiActionsRegistry,
|
|
9
10
|
} from "../../uiActionsRegistrator";
|
|
10
11
|
import { reactHooksLogger } from "../logger";
|
|
11
12
|
|
|
@@ -34,7 +35,13 @@ export function useActions<T = unknown>(plugId: string): any {
|
|
|
34
35
|
if (!logged) {
|
|
35
36
|
logger.warning({
|
|
36
37
|
message: `useActions: Couldn't find an action for ${plugId} plugin`,
|
|
37
|
-
|
|
38
|
+
// Identifiers only. The action map holds every plugin's module and
|
|
39
|
+
// live React context, which is far too large for a log line and can
|
|
40
|
+
// carry whatever state those plugins hold.
|
|
41
|
+
data: {
|
|
42
|
+
plugId,
|
|
43
|
+
availableActions: Object.keys(context?.actions ?? {}),
|
|
44
|
+
},
|
|
38
45
|
});
|
|
39
46
|
|
|
40
47
|
setLogged(true);
|
|
@@ -80,7 +87,31 @@ export function useEntryActionState(
|
|
|
80
87
|
useEffect(() => {
|
|
81
88
|
if (!action) return undefined;
|
|
82
89
|
|
|
83
|
-
|
|
90
|
+
// An action's stream is third-party code. Subscribing with only a `next`
|
|
91
|
+
// handler lets RxJS rethrow an error notification asynchronously, where no
|
|
92
|
+
// error boundary catches it - one plugin faulting would take the app down
|
|
93
|
+
// rather than this one button. The button keeps its last known state.
|
|
94
|
+
let subscription;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
subscription = observeEntryState(action, entry).subscribe({
|
|
98
|
+
next: setState,
|
|
99
|
+
error: (error) =>
|
|
100
|
+
logger.warning({
|
|
101
|
+
message:
|
|
102
|
+
"useEntryActionState: the action's state stream failed - the button keeps its last state",
|
|
103
|
+
data: { entryId, error },
|
|
104
|
+
}),
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
logger.warning({
|
|
108
|
+
message:
|
|
109
|
+
"useEntryActionState: could not observe the action - the button keeps its initial state",
|
|
110
|
+
data: { entryId, error },
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
return undefined;
|
|
114
|
+
}
|
|
84
115
|
|
|
85
116
|
return () => subscription.unsubscribe();
|
|
86
117
|
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
@@ -88,6 +119,18 @@ export function useEntryActionState(
|
|
|
88
119
|
|
|
89
120
|
const invokeAction = useCallback(
|
|
90
121
|
(options: InvokeArgsOptions = {}) => {
|
|
122
|
+
if (typeof action?.invokeAction !== "function") {
|
|
123
|
+
// A button was rendered and pressed, and the press goes nowhere. Only
|
|
124
|
+
// reachable on a user interaction, so it costs nothing on render. The
|
|
125
|
+
// action value carries no identifier, so the label is what locates the
|
|
126
|
+
// button that misbehaved.
|
|
127
|
+
logger.warning({
|
|
128
|
+
message:
|
|
129
|
+
"useEntryActionState: the pressed action has no invokeAction - the press did nothing",
|
|
130
|
+
data: { entryId, label: state?.label },
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
91
134
|
action?.invokeAction?.(entry, { updateState: setState, ...options });
|
|
92
135
|
},
|
|
93
136
|
// eslint-disable-next-line @wogns3623/better-exhaustive-deps/exhaustive-deps
|
|
@@ -96,3 +139,25 @@ export function useEntryActionState(
|
|
|
96
139
|
|
|
97
140
|
return { state, invokeAction };
|
|
98
141
|
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* A value that changes whenever a provider is added to or removed from
|
|
145
|
+
* `uiActionsRegistry`.
|
|
146
|
+
*
|
|
147
|
+
* Providers can register after the UI that wants them has rendered - the audio
|
|
148
|
+
* player registers playback speed only once its controller has state, and
|
|
149
|
+
* plugin-backed actions are bridged in from an effect. Put this in the
|
|
150
|
+
* dependency list of anything that resolves actions, so the list is re-read
|
|
151
|
+
* instead of being frozen at first paint.
|
|
152
|
+
*/
|
|
153
|
+
export function useRegistryRevision(): number {
|
|
154
|
+
const [revision, setRevision] = useState(0);
|
|
155
|
+
|
|
156
|
+
useEffect(
|
|
157
|
+
() =>
|
|
158
|
+
uiActionsRegistry.subscribe(() => setRevision((current) => current + 1)),
|
|
159
|
+
[]
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
return revision;
|
|
163
|
+
}
|
|
@@ -5,6 +5,7 @@ import { ActionExecutorContext } from "@applicaster/zapp-react-native-utils/acti
|
|
|
5
5
|
|
|
6
6
|
const mockNavigator = {
|
|
7
7
|
getPathname: () => "pathnameMock",
|
|
8
|
+
currentRoute: "pathnameMock",
|
|
8
9
|
push: jest.fn(),
|
|
9
10
|
data: {},
|
|
10
11
|
};
|
|
@@ -29,6 +30,7 @@ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen", () => ({
|
|
|
29
30
|
),
|
|
30
31
|
useTargetScreenData: jest.fn(() => ({})),
|
|
31
32
|
useCurrentScreenData: jest.fn(() => ({})),
|
|
33
|
+
useScreenContext: jest.fn(() => ({})),
|
|
32
34
|
}));
|
|
33
35
|
|
|
34
36
|
jest.mock("@applicaster/zapp-react-native-ui-components/Contexts", () => ({
|
|
@@ -66,6 +68,12 @@ const wrapper = ({ children }) => (
|
|
|
66
68
|
</CellTapContext.Provider>
|
|
67
69
|
);
|
|
68
70
|
|
|
71
|
+
const actionExecutorWrapper = ({ children }) => (
|
|
72
|
+
<ActionExecutorContext.Provider value={actionExecutor}>
|
|
73
|
+
{children}
|
|
74
|
+
</ActionExecutorContext.Provider>
|
|
75
|
+
);
|
|
76
|
+
|
|
69
77
|
describe("useCellClick", () => {
|
|
70
78
|
it("returns a function", () => {
|
|
71
79
|
const { result } = renderHook(() => useCellClick(mockProps));
|
|
@@ -95,4 +103,60 @@ describe("useCellClick", () => {
|
|
|
95
103
|
|
|
96
104
|
cleanup();
|
|
97
105
|
});
|
|
106
|
+
|
|
107
|
+
it("does not push when the cell finishes a hook", async () => {
|
|
108
|
+
mockNavigator.push.mockClear();
|
|
109
|
+
|
|
110
|
+
const item = {
|
|
111
|
+
id: "profile-1",
|
|
112
|
+
type: { value: "feed" },
|
|
113
|
+
extensions: {
|
|
114
|
+
tap_actions: {
|
|
115
|
+
actions: [
|
|
116
|
+
{ type: "sessionStorageSet" },
|
|
117
|
+
{ type: "finishHook", options: { success: true } },
|
|
118
|
+
],
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const { result } = renderHook(() => useCellClick({ item }), {
|
|
124
|
+
wrapper: actionExecutorWrapper,
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
await result.current();
|
|
128
|
+
|
|
129
|
+
await waitFor(() => {
|
|
130
|
+
expect(actionExecutor.handleEntryActions).toHaveBeenCalled();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(mockNavigator.push).not.toHaveBeenCalled();
|
|
134
|
+
cleanup();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("pushes after tap actions that do not finish a hook", async () => {
|
|
138
|
+
mockNavigator.push.mockClear();
|
|
139
|
+
|
|
140
|
+
const item = {
|
|
141
|
+
id: "episode-1",
|
|
142
|
+
type: { value: "feed" },
|
|
143
|
+
extensions: {
|
|
144
|
+
tap_actions: {
|
|
145
|
+
actions: [{ type: "sessionStorageSet" }],
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
const { result } = renderHook(() => useCellClick({ item }), {
|
|
151
|
+
wrapper: actionExecutorWrapper,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
await result.current();
|
|
155
|
+
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
expect(mockNavigator.push).toHaveBeenCalled();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
cleanup();
|
|
161
|
+
});
|
|
98
162
|
});
|
|
@@ -83,6 +83,12 @@ export const useCellClick = ({
|
|
|
83
83
|
|
|
84
84
|
logOnPress(selectedItem, pathname, component, onCellTap);
|
|
85
85
|
|
|
86
|
+
const tapActions = selectedItem?.extensions?.tap_actions?.actions;
|
|
87
|
+
|
|
88
|
+
const finishesHook =
|
|
89
|
+
Array.isArray(tapActions) &&
|
|
90
|
+
tapActions.some((action) => action?.type === "finishHook");
|
|
91
|
+
|
|
86
92
|
if (selectedItem) {
|
|
87
93
|
await actionExecutor?.handleEntryActions(selectedItem, {
|
|
88
94
|
component,
|
|
@@ -101,20 +107,21 @@ export const useCellClick = ({
|
|
|
101
107
|
url: selectedItem.link.href,
|
|
102
108
|
});
|
|
103
109
|
}
|
|
104
|
-
} else {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
} else if (isFunction(onCellTap)) {
|
|
111
|
+
onCellTap?.(selectedItem, _index);
|
|
112
|
+
} else if (!finishesHook) {
|
|
113
|
+
// finishHook already completes the hook and navigates to the original
|
|
114
|
+
// target. Pushing the cell as well stacks a second copy of that screen
|
|
115
|
+
// (e.g. home/home after a profile selector hook).
|
|
116
|
+
if (currentRoute === pathname || pathname.includes("video-modal")) {
|
|
117
|
+
const targetScreen = component?.data?.target;
|
|
118
|
+
|
|
119
|
+
push(
|
|
120
|
+
R.when(
|
|
121
|
+
() => targetScreen,
|
|
122
|
+
R.mergeLeft({ screen_type: targetScreen })
|
|
123
|
+
)(selectedItem)
|
|
124
|
+
);
|
|
118
125
|
}
|
|
119
126
|
}
|
|
120
127
|
},
|
|
@@ -1,7 +1,24 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import {
|
|
2
|
+
import { AppState } from "react-native";
|
|
3
|
+
import { renderHook, cleanup, act } from "@testing-library/react-native";
|
|
3
4
|
import { NetworkStatusContext } from "@applicaster/zapp-react-native-ui-components/Contexts/NetworkStatusContext";
|
|
4
5
|
|
|
6
|
+
let appStateListener;
|
|
7
|
+
|
|
8
|
+
AppState.currentState = "active";
|
|
9
|
+
|
|
10
|
+
jest.spyOn(AppState, "addEventListener").mockImplementation((_, listener) => {
|
|
11
|
+
appStateListener = listener;
|
|
12
|
+
|
|
13
|
+
return { remove: jest.fn() };
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
const changeAppState = (state) =>
|
|
17
|
+
act(() => {
|
|
18
|
+
AppState.currentState = state;
|
|
19
|
+
appStateListener(state);
|
|
20
|
+
});
|
|
21
|
+
|
|
5
22
|
const MockNetInfo = {
|
|
6
23
|
type: "none",
|
|
7
24
|
details: {},
|
|
@@ -9,7 +26,7 @@ const MockNetInfo = {
|
|
|
9
26
|
isConnected: false,
|
|
10
27
|
};
|
|
11
28
|
|
|
12
|
-
const { useConnectionInfo } = require("../");
|
|
29
|
+
const { useConnectionInfo, APP_ACTIVE_GRACE_PERIOD_MS } = require("../");
|
|
13
30
|
|
|
14
31
|
describe("useConnectionInfo", () => {
|
|
15
32
|
const wrapper = {
|
|
@@ -80,4 +97,139 @@ describe("useConnectionInfo", () => {
|
|
|
80
97
|
|
|
81
98
|
expect(current).toEqual(true);
|
|
82
99
|
});
|
|
100
|
+
|
|
101
|
+
describe("app state grace period", () => {
|
|
102
|
+
const offlineInfo = {
|
|
103
|
+
type: "none",
|
|
104
|
+
details: {},
|
|
105
|
+
isInternetReachable: false,
|
|
106
|
+
isConnected: false,
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const onlineInfo = {
|
|
110
|
+
type: "wifi",
|
|
111
|
+
details: {},
|
|
112
|
+
isInternetReachable: true,
|
|
113
|
+
isConnected: true,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const renderConnectionHook = (justOnlineStatus, initialInfo) => {
|
|
117
|
+
let info = initialInfo;
|
|
118
|
+
|
|
119
|
+
const hook = renderHook(() => useConnectionInfo(justOnlineStatus), {
|
|
120
|
+
// eslint-disable-next-line react/prop-types,react/display-name
|
|
121
|
+
wrapper: ({ children }) => (
|
|
122
|
+
<NetworkStatusContext.Provider value={info}>
|
|
123
|
+
{children}
|
|
124
|
+
</NetworkStatusContext.Provider>
|
|
125
|
+
),
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const setInfo = (next) => {
|
|
129
|
+
info = next;
|
|
130
|
+
hook.rerender();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
return { ...hook, setInfo };
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
jest.useFakeTimers();
|
|
138
|
+
AppState.currentState = "active";
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
afterEach(() => {
|
|
142
|
+
jest.useRealTimers();
|
|
143
|
+
cleanup();
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("returns true while the app is in background even if offline", () => {
|
|
147
|
+
const { result, setInfo } = renderConnectionHook(true, onlineInfo);
|
|
148
|
+
|
|
149
|
+
changeAppState("background");
|
|
150
|
+
setInfo(offlineInfo);
|
|
151
|
+
|
|
152
|
+
expect(result.current).toBe(true);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
it("ignores offline state for the grace period after coming back to foreground", () => {
|
|
156
|
+
const { result, setInfo } = renderConnectionHook(true, onlineInfo);
|
|
157
|
+
|
|
158
|
+
changeAppState("background");
|
|
159
|
+
setInfo(offlineInfo);
|
|
160
|
+
changeAppState("active");
|
|
161
|
+
|
|
162
|
+
expect(result.current).toBe(true);
|
|
163
|
+
|
|
164
|
+
act(() => {
|
|
165
|
+
jest.advanceTimersByTime(APP_ACTIVE_GRACE_PERIOD_MS - 1);
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
expect(result.current).toBe(true);
|
|
169
|
+
|
|
170
|
+
act(() => {
|
|
171
|
+
jest.advanceTimersByTime(1);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(result.current).toBe(false);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("reports online when network recovers during the grace period", () => {
|
|
178
|
+
const { result, setInfo } = renderConnectionHook(true, onlineInfo);
|
|
179
|
+
|
|
180
|
+
changeAppState("background");
|
|
181
|
+
setInfo(offlineInfo);
|
|
182
|
+
changeAppState("active");
|
|
183
|
+
setInfo(onlineInfo);
|
|
184
|
+
|
|
185
|
+
act(() => {
|
|
186
|
+
jest.advanceTimersByTime(APP_ACTIVE_GRACE_PERIOD_MS);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(result.current).toBe(true);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it("returns last connection info seen while active during the grace period", () => {
|
|
193
|
+
const { result, setInfo } = renderConnectionHook(false, onlineInfo);
|
|
194
|
+
|
|
195
|
+
changeAppState("background");
|
|
196
|
+
setInfo(offlineInfo);
|
|
197
|
+
changeAppState("active");
|
|
198
|
+
|
|
199
|
+
expect(result.current).toBe(onlineInfo);
|
|
200
|
+
|
|
201
|
+
act(() => {
|
|
202
|
+
jest.advanceTimersByTime(APP_ACTIVE_GRACE_PERIOD_MS);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
expect(result.current).toBe(offlineInfo);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it("restarts the grace period when the app goes to background again", () => {
|
|
209
|
+
const { result, setInfo } = renderConnectionHook(true, onlineInfo);
|
|
210
|
+
|
|
211
|
+
changeAppState("background");
|
|
212
|
+
setInfo(offlineInfo);
|
|
213
|
+
changeAppState("active");
|
|
214
|
+
|
|
215
|
+
act(() => {
|
|
216
|
+
jest.advanceTimersByTime(APP_ACTIVE_GRACE_PERIOD_MS - 1000);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
changeAppState("background");
|
|
220
|
+
changeAppState("active");
|
|
221
|
+
|
|
222
|
+
act(() => {
|
|
223
|
+
jest.advanceTimersByTime(APP_ACTIVE_GRACE_PERIOD_MS - 1);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(result.current).toBe(true);
|
|
227
|
+
|
|
228
|
+
act(() => {
|
|
229
|
+
jest.advanceTimersByTime(1);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(result.current).toBe(false);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
83
235
|
});
|
|
@@ -1,27 +1,91 @@
|
|
|
1
1
|
/* eslint-disable no-unused-vars, no-redeclare */
|
|
2
|
-
import React from "react";
|
|
2
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
3
3
|
import { NetInfoState } from "@react-native-community/netinfo";
|
|
4
4
|
import { NetworkStatusContext } from "@applicaster/zapp-react-native-ui-components/Contexts/NetworkStatusContext";
|
|
5
5
|
import { isOnline } from "@applicaster/quick-brick-core/App/NetworkStatusProvider/utils";
|
|
6
6
|
|
|
7
|
+
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
8
|
+
import { useAppState } from "../app/useAppState";
|
|
9
|
+
|
|
10
|
+
const { log_debug } = createLogger({
|
|
11
|
+
subsystem: "connection",
|
|
12
|
+
category: "useConnectionInfo",
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
type ConnectionInfo = NetInfoState & { deviceStatus: NetStatus };
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Time after the app returns to the foreground during which network changes are ignored.
|
|
19
|
+
* NetInfo may report a stale "offline" state right after resume (e.g. in power saving mode),
|
|
20
|
+
* see https://github.com/react-native-netinfo/react-native-netinfo/issues/538
|
|
21
|
+
*/
|
|
22
|
+
export const APP_ACTIVE_GRACE_PERIOD_MS = 3000;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Returns true while the app is not active, and for APP_ACTIVE_GRACE_PERIOD_MS after it becomes active again
|
|
26
|
+
*/
|
|
27
|
+
const useIsNetworkStatusUnreliable = (isAppActive: boolean): boolean => {
|
|
28
|
+
const [isSettling, setIsSettling] = useState(!isAppActive);
|
|
29
|
+
|
|
30
|
+
useEffect(() => {
|
|
31
|
+
if (!isAppActive) {
|
|
32
|
+
setIsSettling(true);
|
|
33
|
+
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const timer = setTimeout(
|
|
38
|
+
() => setIsSettling(false),
|
|
39
|
+
APP_ACTIVE_GRACE_PERIOD_MS
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return () => clearTimeout(timer);
|
|
43
|
+
}, [isAppActive]);
|
|
44
|
+
|
|
45
|
+
return !isAppActive || isSettling;
|
|
46
|
+
};
|
|
47
|
+
|
|
7
48
|
/**
|
|
8
49
|
* This hook returns connection type from RN NetInfo component or boolean for iConnected if justOnlineStatus flag is set
|
|
50
|
+
* While the app is in background and for APP_ACTIVE_GRACE_PERIOD_MS after it comes back to foreground,
|
|
51
|
+
* network changes are ignored: the boolean form returns true and the object form returns the last state seen while active
|
|
9
52
|
* @param {boolean} justOnlineStatus - flag if to return boolean for isConnected
|
|
10
53
|
* Returns connection object from NetInfo or boolean if isConnected
|
|
11
54
|
*/
|
|
12
55
|
|
|
13
56
|
export function useConnectionInfo(justOnlineStatus: true): boolean;
|
|
14
57
|
|
|
15
|
-
export function useConnectionInfo(
|
|
16
|
-
justOnlineStatus: false
|
|
17
|
-
): NetInfoState & { deviceStatus: NetStatus };
|
|
58
|
+
export function useConnectionInfo(justOnlineStatus: false): ConnectionInfo;
|
|
18
59
|
|
|
19
60
|
export function useConnectionInfo(
|
|
20
61
|
justOnlineStatus: boolean
|
|
21
|
-
): boolean |
|
|
62
|
+
): boolean | ConnectionInfo {
|
|
22
63
|
const connectionInfo = React.useContext(
|
|
23
64
|
NetworkStatusContext
|
|
24
|
-
) as
|
|
65
|
+
) as ConnectionInfo;
|
|
66
|
+
|
|
67
|
+
const appState = useAppState();
|
|
68
|
+
const isAppActive = appState === "active";
|
|
69
|
+
const ignoreNetworkChanges = useIsNetworkStatusUnreliable(isAppActive);
|
|
70
|
+
|
|
71
|
+
const lastReliableConnectionInfo = useRef(connectionInfo);
|
|
72
|
+
|
|
73
|
+
if (!ignoreNetworkChanges) {
|
|
74
|
+
lastReliableConnectionInfo.current = connectionInfo;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
useEffect(() => {
|
|
78
|
+
log_debug("useConnectionInfo: change", {
|
|
79
|
+
connectionInfo,
|
|
80
|
+
isOnline: isOnline(connectionInfo),
|
|
81
|
+
appState,
|
|
82
|
+
ignoreNetworkChanges,
|
|
83
|
+
});
|
|
84
|
+
}, [connectionInfo, appState, ignoreNetworkChanges]);
|
|
85
|
+
|
|
86
|
+
if (justOnlineStatus) {
|
|
87
|
+
return ignoreNetworkChanges || isOnline(connectionInfo) === true;
|
|
88
|
+
}
|
|
25
89
|
|
|
26
|
-
return
|
|
90
|
+
return lastReliableConnectionInfo.current;
|
|
27
91
|
}
|