@applicaster/zapp-react-native-utils 15.0.0-alpha.3634430569 → 15.0.0-alpha.3735433464
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/actionUtils/index.ts +7 -0
- package/appUtils/focusManagerAux/utils/index.ts +10 -2
- package/cellUtils/__tests__/cellUtils.test.ts +39 -0
- package/cellUtils/index.ts +11 -1
- package/componentsUtils/index.ts +8 -0
- package/dateUtils/__tests__/dayjs.test.ts +0 -3
- package/dateUtils/index.ts +2 -0
- package/manifestUtils/keys.js +2 -2
- package/manifestUtils/mobileAction/button/__tests__/mobileActionButton.test.js +5 -5
- package/manifestUtils/mobileAction/button/index.js +1 -1
- package/manifestUtils/mobileAction/groups/defaults.js +2 -1
- package/package.json +2 -2
- package/pluginUtils/index.ts +4 -5
- package/reactHooks/casting/index.ts +1 -0
- package/reactHooks/casting/useIsCasting.tsx +57 -0
- package/reactHooks/resolvers/useComponentResolver.ts +2 -2
- package/reactHooks/screen/__tests__/useCurrentScreenIsHook.test.ts +103 -0
- package/reactHooks/screen/__tests__/useCurrentScreenIsStartupHook.test.ts +94 -0
- package/reactHooks/screen/index.ts +4 -0
- package/reactHooks/screen/useCurrentScreenIsHook.ts +9 -0
- package/reactHooks/screen/useCurrentScreenIsStartupHook.ts +8 -0
- package/refreshUtils/RefreshCoordinator/__tests__/refreshCoordinator.test.ts +45 -0
- package/refreshUtils/RefreshCoordinator/index.ts +34 -5
|
@@ -142,6 +142,10 @@ export const isTabsScreenOnContentFocused = (node) => {
|
|
|
142
142
|
};
|
|
143
143
|
|
|
144
144
|
export const isCurrentFocusOnMenu = (node) => {
|
|
145
|
+
if (isNil(node)) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
|
|
145
149
|
if (isRoot(node)) {
|
|
146
150
|
return false;
|
|
147
151
|
}
|
|
@@ -154,10 +158,14 @@ export const isCurrentFocusOnMenu = (node) => {
|
|
|
154
158
|
return false;
|
|
155
159
|
}
|
|
156
160
|
|
|
157
|
-
return isCurrentFocusOnMenu(node
|
|
161
|
+
return isCurrentFocusOnMenu(node?.parent);
|
|
158
162
|
};
|
|
159
163
|
|
|
160
164
|
export const isCurrentFocusOnContent = (node) => {
|
|
165
|
+
if (isNil(node)) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
|
|
161
169
|
if (isRoot(node)) {
|
|
162
170
|
return false;
|
|
163
171
|
}
|
|
@@ -170,7 +178,7 @@ export const isCurrentFocusOnContent = (node) => {
|
|
|
170
178
|
return true;
|
|
171
179
|
}
|
|
172
180
|
|
|
173
|
-
return isCurrentFocusOnContent(node
|
|
181
|
+
return isCurrentFocusOnContent(node?.parent);
|
|
174
182
|
};
|
|
175
183
|
|
|
176
184
|
export const isCurrentFocusOn = (id, node) => {
|
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
ifEmptyUseFallback,
|
|
4
4
|
textTransform,
|
|
5
5
|
getKeyForLabel,
|
|
6
|
+
getDataTransformForLabel,
|
|
6
7
|
getLabel,
|
|
7
8
|
getAspectRatio,
|
|
8
9
|
getCellWidth,
|
|
@@ -56,6 +57,44 @@ describe("getKeyForLabel", () => {
|
|
|
56
57
|
});
|
|
57
58
|
});
|
|
58
59
|
|
|
60
|
+
describe("getDataTransformForLabel", () => {
|
|
61
|
+
it("returns dataKey when dataKey is not other", () => {
|
|
62
|
+
expect(getDataTransformForLabel(VAL.dataKey, VAL.customKey)).toEqual(
|
|
63
|
+
VAL.dataKey
|
|
64
|
+
);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("returns customKey when dataKey is other", () => {
|
|
68
|
+
expect(getDataTransformForLabel(VAL.other, VAL.customKey)).toEqual(
|
|
69
|
+
VAL.customKey
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it("preserves full date formats containing dots", () => {
|
|
74
|
+
expect(getDataTransformForLabel(VAL.other, "DD.MM.YYYY")).toEqual(
|
|
75
|
+
"DD.MM.YYYY"
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("returns default date format when selected key is null or undefined", () => {
|
|
80
|
+
expect(getDataTransformForLabel(EMPTY.nulls, VAL.customKey)).toEqual(
|
|
81
|
+
"DD/MM/YYYY"
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(getDataTransformForLabel(EMPTY.undef, VAL.customKey)).toEqual(
|
|
85
|
+
"DD/MM/YYYY"
|
|
86
|
+
);
|
|
87
|
+
|
|
88
|
+
expect(getDataTransformForLabel(VAL.other, EMPTY.nulls)).toEqual(
|
|
89
|
+
"DD/MM/YYYY"
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
expect(getDataTransformForLabel(VAL.other, EMPTY.undef)).toEqual(
|
|
93
|
+
"DD/MM/YYYY"
|
|
94
|
+
);
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
|
|
59
98
|
describe("getLabel", () => {
|
|
60
99
|
it("returns a string from dataKey's path", () => {
|
|
61
100
|
expect(getLabel(VAL.dataKey, VAL.customKey)(entry)).toEqual(entry.id);
|
package/cellUtils/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as R from "ramda";
|
|
2
|
-
import dayjs from "
|
|
2
|
+
import { dayjs } from "../dateUtils";
|
|
3
3
|
import validateColor from "validate-color";
|
|
4
4
|
|
|
5
5
|
import { transformColorCode as fixColorHexCode } from "@applicaster/zapp-react-native-utils/transform";
|
|
@@ -31,6 +31,16 @@ export const getKeyForLabel = (dataKey, customKey) => {
|
|
|
31
31
|
return keyToUse.split(".");
|
|
32
32
|
};
|
|
33
33
|
|
|
34
|
+
/**
|
|
35
|
+
* Provides `format` for date formatting. Returns customKey if dataKey is "other", otherwise it returns dataKey.
|
|
36
|
+
* Should consist of valid dayjs tokens - https://day.js.org/docs/en/parse/string-format#list-of-all-available-parsing-tokens
|
|
37
|
+
*/
|
|
38
|
+
export const getDataTransformForLabel = (dataKey, customKey) => {
|
|
39
|
+
const keyToUse = dataKey === CUSTOM_KEY ? customKey : dataKey;
|
|
40
|
+
|
|
41
|
+
return keyToUse ?? "DD/MM/YYYY";
|
|
42
|
+
};
|
|
43
|
+
|
|
34
44
|
/**
|
|
35
45
|
* This method will return true if the argument passed to it is either empty or nil
|
|
36
46
|
* The method prevents zero from being evaluated as falsey in an || condition
|
package/componentsUtils/index.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { last } from "@applicaster/zapp-react-native-utils/utils";
|
|
2
|
+
|
|
1
3
|
const GROUP = "group-qb";
|
|
2
4
|
const GROUP_INFO = "group-info-qb";
|
|
3
5
|
const GROUP_INFO_OLD = "group-info";
|
|
@@ -29,6 +31,12 @@ export const isFirstComponentGallery = (
|
|
|
29
31
|
return isGallery(components?.[0]);
|
|
30
32
|
};
|
|
31
33
|
|
|
34
|
+
export const isLastComponentGallery = (
|
|
35
|
+
components: ZappUIComponent[]
|
|
36
|
+
): boolean => {
|
|
37
|
+
return isGallery(last(components));
|
|
38
|
+
};
|
|
39
|
+
|
|
32
40
|
export const isGroup = (item): boolean => item?.component_type === GROUP;
|
|
33
41
|
|
|
34
42
|
export const isEmptyGroup = (item): boolean =>
|
package/dateUtils/index.ts
CHANGED
|
@@ -4,12 +4,14 @@ import timezone from "dayjs/plugin/timezone";
|
|
|
4
4
|
import isoWeek from "dayjs/plugin/isoWeek";
|
|
5
5
|
import duration from "dayjs/plugin/duration";
|
|
6
6
|
import relativeTime from "dayjs/plugin/relativeTime";
|
|
7
|
+
import customParseFormat from "dayjs/plugin/customParseFormat";
|
|
7
8
|
|
|
8
9
|
// Extend dayjs with plugins
|
|
9
10
|
dayjs.extend(utc);
|
|
10
11
|
dayjs.extend(timezone);
|
|
11
12
|
dayjs.extend(isoWeek);
|
|
12
13
|
dayjs.extend(duration);
|
|
14
|
+
dayjs.extend(customParseFormat);
|
|
13
15
|
|
|
14
16
|
dayjs.extend(relativeTime, {
|
|
15
17
|
thresholds: [
|
package/manifestUtils/keys.js
CHANGED
|
@@ -541,8 +541,8 @@ const MOBILE_ACTION_BUTTON_FIELDS = [
|
|
|
541
541
|
},
|
|
542
542
|
{
|
|
543
543
|
type: ZAPPIFEST_FIELDS.select,
|
|
544
|
-
suffix: "action asset
|
|
545
|
-
options: ["
|
|
544
|
+
suffix: "action asset flavour",
|
|
545
|
+
options: ["flavour_1", "flavour_2"],
|
|
546
546
|
},
|
|
547
547
|
{
|
|
548
548
|
type: ZAPPIFEST_FIELDS.select,
|
|
@@ -18,7 +18,7 @@ describe("mobileActionButton", () => {
|
|
|
18
18
|
paddingBottom: 14,
|
|
19
19
|
paddingLeft: 16,
|
|
20
20
|
assetEnabled: true,
|
|
21
|
-
|
|
21
|
+
actionAssetFlavour: "flavour_1",
|
|
22
22
|
assetAlignment: "left",
|
|
23
23
|
assetHeight: 24,
|
|
24
24
|
assetWidth: 24,
|
|
@@ -99,13 +99,13 @@ describe("mobileActionButton", () => {
|
|
|
99
99
|
},
|
|
100
100
|
]);
|
|
101
101
|
|
|
102
|
-
const
|
|
103
|
-
(field) => field.key === "
|
|
102
|
+
const actionAssetFlavourField = result.fields.find(
|
|
103
|
+
(field) => field.key === "mobile_button_2_action_asset_flavour"
|
|
104
104
|
);
|
|
105
105
|
|
|
106
|
-
expect(
|
|
106
|
+
expect(actionAssetFlavourField.rules).toBe("all_conditions");
|
|
107
107
|
|
|
108
|
-
expect(
|
|
108
|
+
expect(actionAssetFlavourField.conditional_fields).toEqual([
|
|
109
109
|
{
|
|
110
110
|
key: "styles/mobile_button_2_button_enabled",
|
|
111
111
|
condition_value: true,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const DEFAULT_MOBILE_ACTION_BUTTONS_CONTAINER_DEFAULTS = {
|
|
2
2
|
buttonsEnabled: false,
|
|
3
|
+
|
|
3
4
|
align: "left",
|
|
4
5
|
overImagePosition: "bottom_left",
|
|
5
6
|
marginTop: 0,
|
|
@@ -27,7 +28,7 @@ const DEFAULT_MOBILE_ACTION_BUTTON_SHARED_DEFAULTS = {
|
|
|
27
28
|
paddingBottom: 14,
|
|
28
29
|
paddingLeft: 16,
|
|
29
30
|
assetEnabled: true,
|
|
30
|
-
|
|
31
|
+
actionAssetFlavour: "flavour_1",
|
|
31
32
|
assetAlignment: "left",
|
|
32
33
|
assetHeight: 24,
|
|
33
34
|
assetWidth: 24,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "15.0.0-alpha.
|
|
3
|
+
"version": "15.0.0-alpha.3735433464",
|
|
4
4
|
"description": "Applicaster Zapp React Native utilities package",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
},
|
|
28
28
|
"homepage": "https://github.com/applicaster/quickbrick#readme",
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@applicaster/applicaster-types": "15.0.0-alpha.
|
|
30
|
+
"@applicaster/applicaster-types": "15.0.0-alpha.3735433464",
|
|
31
31
|
"buffer": "^5.2.1",
|
|
32
32
|
"camelize": "^1.0.0",
|
|
33
33
|
"dayjs": "^1.11.10",
|
package/pluginUtils/index.ts
CHANGED
|
@@ -30,10 +30,7 @@ export function getComponentModule({
|
|
|
30
30
|
components: ComponentsMap;
|
|
31
31
|
plugins: Plugin[] | QuickBrickPlugin[];
|
|
32
32
|
}) {
|
|
33
|
-
const component =
|
|
34
|
-
R.prop(R.__, components),
|
|
35
|
-
toPascalCase
|
|
36
|
-
)(componentType);
|
|
33
|
+
const component = components[toPascalCase(componentType)];
|
|
37
34
|
|
|
38
35
|
if (component) {
|
|
39
36
|
return component;
|
|
@@ -60,7 +57,9 @@ export function findComponentByType({
|
|
|
60
57
|
? R.compose(...R.reverse(decorators))
|
|
61
58
|
: decorators;
|
|
62
59
|
|
|
63
|
-
const
|
|
60
|
+
const module = getComponentModule({ componentType, components, plugins });
|
|
61
|
+
|
|
62
|
+
const component = module?.Component || module;
|
|
64
63
|
|
|
65
64
|
return R.unless(R.isNil, applyDecorators)(component);
|
|
66
65
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { useIsCasting } from "./useIsCasting";
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
|
|
3
|
+
import { useAppState } from "@applicaster/zapp-react-native-utils/reactHooks/app";
|
|
4
|
+
import { isApplePlatform } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
5
|
+
|
|
6
|
+
const CHROMECAST_IDENTIFIER = "quick-brick-chromecast-action";
|
|
7
|
+
|
|
8
|
+
const fakeEntry: any = {
|
|
9
|
+
title: "Chromecast use is casting fake entry",
|
|
10
|
+
id: "quick-brick-use-is-casting-fake-entry",
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Returns the cast action state connection status, does not update when app is in background on Apple platforms
|
|
15
|
+
* On other platforms, it continues to check the cast action state even when the app is in background
|
|
16
|
+
*/
|
|
17
|
+
export const useIsCasting = () => {
|
|
18
|
+
const actionContext = useActions(CHROMECAST_IDENTIFIER);
|
|
19
|
+
const appState = useAppState();
|
|
20
|
+
const isAppInForeground = appState === "active";
|
|
21
|
+
const shouldOnlyUpdateInForeground = isApplePlatform();
|
|
22
|
+
|
|
23
|
+
const [castActionState, setCastActionState] = React.useState(
|
|
24
|
+
!actionContext ? null : actionContext.initialEntryState(fakeEntry)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
React.useEffect(() => {
|
|
28
|
+
if (!shouldOnlyUpdateInForeground || isAppInForeground) {
|
|
29
|
+
if (actionContext && castActionState === null) {
|
|
30
|
+
setCastActionState(actionContext.initialEntryState(fakeEntry));
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}, [
|
|
34
|
+
actionContext,
|
|
35
|
+
setCastActionState,
|
|
36
|
+
isAppInForeground,
|
|
37
|
+
shouldOnlyUpdateInForeground,
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
React.useEffect(() => {
|
|
41
|
+
if (typeof actionContext?.addListener === "function") {
|
|
42
|
+
return actionContext?.addListener(String(fakeEntry.id), (state) => {
|
|
43
|
+
// Only check foreground state on Apple platforms
|
|
44
|
+
if (!shouldOnlyUpdateInForeground || isAppInForeground) {
|
|
45
|
+
setCastActionState(state);
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}, [
|
|
50
|
+
setCastActionState,
|
|
51
|
+
actionContext,
|
|
52
|
+
isAppInForeground,
|
|
53
|
+
shouldOnlyUpdateInForeground,
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
return !!castActionState?.connected;
|
|
57
|
+
};
|
|
@@ -5,9 +5,9 @@ import { coreLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
|
5
5
|
|
|
6
6
|
import { findComponentByType } from "@applicaster/zapp-react-native-utils/pluginUtils";
|
|
7
7
|
import {
|
|
8
|
-
usePlugins,
|
|
9
|
-
useAppSelector,
|
|
10
8
|
selectComponents,
|
|
9
|
+
useAppSelector,
|
|
10
|
+
usePlugins,
|
|
11
11
|
} from "@applicaster/zapp-react-native-redux";
|
|
12
12
|
|
|
13
13
|
type Decorator = (component: React.Component<any>) => React.Component<any>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react-native";
|
|
2
|
+
import { useCurrentScreenIsHook } from "../useCurrentScreenIsHook";
|
|
3
|
+
|
|
4
|
+
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
5
|
+
import { last } from "@applicaster/zapp-react-native-utils/utils";
|
|
6
|
+
import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
|
|
7
|
+
|
|
8
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks");
|
|
9
|
+
jest.mock("@applicaster/zapp-react-native-utils/utils");
|
|
10
|
+
jest.mock("@applicaster/zapp-react-native-utils/booleanUtils");
|
|
11
|
+
|
|
12
|
+
const mockUseNavigation = useNavigation as jest.Mock;
|
|
13
|
+
const mockLast = last as jest.Mock;
|
|
14
|
+
const mockToBoolean = toBooleanWithDefaultFalse as jest.Mock;
|
|
15
|
+
|
|
16
|
+
describe("useCurrentScreenIsHook", () => {
|
|
17
|
+
beforeEach(() => {
|
|
18
|
+
jest.clearAllMocks();
|
|
19
|
+
|
|
20
|
+
// sensible defaults
|
|
21
|
+
mockUseNavigation.mockReturnValue({ mainStack: [] });
|
|
22
|
+
mockLast.mockReturnValue(undefined);
|
|
23
|
+
mockToBoolean.mockReturnValue(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("returns true when last route includes 'hook'", () => {
|
|
27
|
+
const stack = [{ route: "some-hook-screen" }];
|
|
28
|
+
|
|
29
|
+
mockUseNavigation.mockReturnValue({ mainStack: stack });
|
|
30
|
+
mockLast.mockReturnValue(stack[0]);
|
|
31
|
+
mockToBoolean.mockReturnValue(true);
|
|
32
|
+
|
|
33
|
+
const { result } = renderHook(() => useCurrentScreenIsHook());
|
|
34
|
+
|
|
35
|
+
expect(mockLast).toHaveBeenCalledWith(stack);
|
|
36
|
+
expect(mockToBoolean).toHaveBeenCalledWith(true);
|
|
37
|
+
expect(result.current).toBe(true);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("returns false when last route does not include 'hook'", () => {
|
|
41
|
+
const stack = [{ route: "home-screen" }];
|
|
42
|
+
|
|
43
|
+
mockUseNavigation.mockReturnValue({ mainStack: stack });
|
|
44
|
+
mockLast.mockReturnValue(stack[0]);
|
|
45
|
+
mockToBoolean.mockReturnValue(false);
|
|
46
|
+
|
|
47
|
+
const { result } = renderHook(() => useCurrentScreenIsHook());
|
|
48
|
+
|
|
49
|
+
expect(mockToBoolean).toHaveBeenCalledWith(false);
|
|
50
|
+
expect(result.current).toBe(false);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("returns false when route is undefined", () => {
|
|
54
|
+
const stack = [{}];
|
|
55
|
+
|
|
56
|
+
mockUseNavigation.mockReturnValue({ mainStack: stack });
|
|
57
|
+
mockLast.mockReturnValue(stack[0]);
|
|
58
|
+
mockToBoolean.mockReturnValue(false);
|
|
59
|
+
|
|
60
|
+
const { result } = renderHook(() => useCurrentScreenIsHook());
|
|
61
|
+
|
|
62
|
+
expect(mockToBoolean).toHaveBeenCalledWith(undefined);
|
|
63
|
+
expect(result.current).toBe(false);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it("returns false when last(mainStack) is undefined (empty stack)", () => {
|
|
67
|
+
mockUseNavigation.mockReturnValue({ mainStack: [] });
|
|
68
|
+
mockLast.mockReturnValue(undefined);
|
|
69
|
+
mockToBoolean.mockReturnValue(false);
|
|
70
|
+
|
|
71
|
+
const { result } = renderHook(() => useCurrentScreenIsHook());
|
|
72
|
+
|
|
73
|
+
expect(mockLast).toHaveBeenCalledWith([]);
|
|
74
|
+
expect(mockToBoolean).toHaveBeenCalledWith(undefined);
|
|
75
|
+
expect(result.current).toBe(false);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it("uses default empty array when mainStack is undefined", () => {
|
|
79
|
+
mockUseNavigation.mockReturnValue({});
|
|
80
|
+
mockLast.mockReturnValue(undefined);
|
|
81
|
+
mockToBoolean.mockReturnValue(false);
|
|
82
|
+
|
|
83
|
+
const { result } = renderHook(() => useCurrentScreenIsHook());
|
|
84
|
+
|
|
85
|
+
expect(mockLast).toHaveBeenCalledWith([]);
|
|
86
|
+
expect(result.current).toBe(false);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it("passes correct boolean result from includes('hook')", () => {
|
|
90
|
+
const stack = [{ route: "hook" }];
|
|
91
|
+
|
|
92
|
+
mockUseNavigation.mockReturnValue({ mainStack: stack });
|
|
93
|
+
mockLast.mockReturnValue(stack[0]);
|
|
94
|
+
|
|
95
|
+
// simulate actual includes result flowing through
|
|
96
|
+
mockToBoolean.mockImplementation((val) => Boolean(val));
|
|
97
|
+
|
|
98
|
+
const { result } = renderHook(() => useCurrentScreenIsHook());
|
|
99
|
+
|
|
100
|
+
expect(mockToBoolean).toHaveBeenCalledWith(true);
|
|
101
|
+
expect(result.current).toBe(true);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { renderHook } from "@testing-library/react-native";
|
|
2
|
+
import { useCurrentScreenIsStartupHook } from "../useCurrentScreenIsStartupHook";
|
|
3
|
+
|
|
4
|
+
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
5
|
+
import { isFilledArray } from "@applicaster/zapp-react-native-utils/arrayUtils";
|
|
6
|
+
|
|
7
|
+
jest.mock("@applicaster/zapp-react-native-utils/reactHooks");
|
|
8
|
+
jest.mock("@applicaster/zapp-react-native-utils/arrayUtils");
|
|
9
|
+
|
|
10
|
+
const mockUseNavigation = useNavigation as jest.Mock;
|
|
11
|
+
const mockIsFilledArray = isFilledArray as jest.Mock;
|
|
12
|
+
|
|
13
|
+
describe("useCurrentScreenIsStartupHook", () => {
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
jest.clearAllMocks();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
it("returns true when startUpHooks is a filled array", () => {
|
|
19
|
+
mockUseNavigation.mockReturnValue({
|
|
20
|
+
startUpHooks: ["hook1"],
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
mockIsFilledArray.mockReturnValue(true);
|
|
24
|
+
|
|
25
|
+
const { result } = renderHook(() => useCurrentScreenIsStartupHook());
|
|
26
|
+
|
|
27
|
+
expect(result.current).toBe(true);
|
|
28
|
+
expect(mockIsFilledArray).toHaveBeenCalledWith(["hook1"]);
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('returns true when startUpHooks is "in_process"', () => {
|
|
32
|
+
mockUseNavigation.mockReturnValue({
|
|
33
|
+
startUpHooks: "in_process",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
mockIsFilledArray.mockReturnValue(false);
|
|
37
|
+
|
|
38
|
+
const { result } = renderHook(() => useCurrentScreenIsStartupHook());
|
|
39
|
+
|
|
40
|
+
expect(result.current).toBe(true);
|
|
41
|
+
expect(mockIsFilledArray).toHaveBeenCalledWith("in_process");
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it("returns false when startUpHooks is empty array", () => {
|
|
45
|
+
mockUseNavigation.mockReturnValue({
|
|
46
|
+
startUpHooks: [],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
mockIsFilledArray.mockReturnValue(false);
|
|
50
|
+
|
|
51
|
+
const { result } = renderHook(() => useCurrentScreenIsStartupHook());
|
|
52
|
+
|
|
53
|
+
expect(result.current).toBe(false);
|
|
54
|
+
expect(mockIsFilledArray).toHaveBeenCalledWith([]);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("returns false when startUpHooks is undefined", () => {
|
|
58
|
+
mockUseNavigation.mockReturnValue({
|
|
59
|
+
startUpHooks: undefined,
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
mockIsFilledArray.mockReturnValue(false);
|
|
63
|
+
|
|
64
|
+
const { result } = renderHook(() => useCurrentScreenIsStartupHook());
|
|
65
|
+
|
|
66
|
+
expect(result.current).toBe(false);
|
|
67
|
+
expect(mockIsFilledArray).toHaveBeenCalledWith(undefined);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("returns false when startUpHooks is null", () => {
|
|
71
|
+
mockUseNavigation.mockReturnValue({
|
|
72
|
+
startUpHooks: null,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
mockIsFilledArray.mockReturnValue(false);
|
|
76
|
+
|
|
77
|
+
const { result } = renderHook(() => useCurrentScreenIsStartupHook());
|
|
78
|
+
|
|
79
|
+
expect(result.current).toBe(false);
|
|
80
|
+
expect(mockIsFilledArray).toHaveBeenCalledWith(null);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it("prioritizes filled array over string comparison", () => {
|
|
84
|
+
mockUseNavigation.mockReturnValue({
|
|
85
|
+
startUpHooks: ["hook1"],
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
mockIsFilledArray.mockReturnValue(true);
|
|
89
|
+
|
|
90
|
+
const { result } = renderHook(() => useCurrentScreenIsStartupHook());
|
|
91
|
+
|
|
92
|
+
expect(result.current).toBe(true);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -12,3 +12,7 @@ export {
|
|
|
12
12
|
} from "./useScreenContext";
|
|
13
13
|
|
|
14
14
|
export { useScreenBackgroundColor } from "./useScreenBackgroundColor";
|
|
15
|
+
|
|
16
|
+
export { useCurrentScreenIsHook } from "./useCurrentScreenIsHook";
|
|
17
|
+
|
|
18
|
+
export { useCurrentScreenIsStartupHook } from "./useCurrentScreenIsStartupHook";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
2
|
+
import { last } from "@applicaster/zapp-react-native-utils/utils";
|
|
3
|
+
import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
|
|
4
|
+
|
|
5
|
+
export const useCurrentScreenIsHook = (): boolean => {
|
|
6
|
+
const { mainStack = [] } = useNavigation();
|
|
7
|
+
|
|
8
|
+
return toBooleanWithDefaultFalse(last(mainStack)?.route?.includes("hook"));
|
|
9
|
+
};
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
|
|
2
|
+
import { isFilledArray } from "@applicaster/zapp-react-native-utils/arrayUtils";
|
|
3
|
+
|
|
4
|
+
export const useCurrentScreenIsStartupHook = (): boolean => {
|
|
5
|
+
const { startUpHooks } = useNavigation();
|
|
6
|
+
|
|
7
|
+
return isFilledArray(startUpHooks) || startUpHooks === "in_process";
|
|
8
|
+
};
|
|
@@ -130,6 +130,51 @@ describe("RefreshCoordinator", () => {
|
|
|
130
130
|
sub2.unsubscribe();
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
+
// ----------------------------------------
|
|
134
|
+
// triggerRefresh
|
|
135
|
+
// ----------------------------------------
|
|
136
|
+
it("triggerRefresh emits refresh events for each component", () => {
|
|
137
|
+
const component1 = { id: "c1" } as ZappUIComponent;
|
|
138
|
+
const component2 = { id: "c2" } as ZappUIComponent;
|
|
139
|
+
const listener = jest.fn();
|
|
140
|
+
|
|
141
|
+
// Subscribe to all events
|
|
142
|
+
(refreshCoordinator as any).refresh$.subscribe(listener);
|
|
143
|
+
|
|
144
|
+
refreshCoordinator.triggerRefresh([component1, component2], screenId);
|
|
145
|
+
|
|
146
|
+
expect(listener).toHaveBeenCalledTimes(2);
|
|
147
|
+
|
|
148
|
+
expect(listener).toHaveBeenCalledWith({
|
|
149
|
+
component: component1,
|
|
150
|
+
screenId,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(listener).toHaveBeenCalledWith({
|
|
154
|
+
component: component2,
|
|
155
|
+
screenId,
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it("triggerRefresh with empty components does nothing", () => {
|
|
160
|
+
const listener = jest.fn();
|
|
161
|
+
|
|
162
|
+
(refreshCoordinator as any).refresh$.subscribe(listener);
|
|
163
|
+
|
|
164
|
+
refreshCoordinator.triggerRefresh([], screenId);
|
|
165
|
+
|
|
166
|
+
expect(listener).not.toHaveBeenCalled();
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it("triggerRefresh events are picked up by subscribeTo", () => {
|
|
170
|
+
const listener = jest.fn();
|
|
171
|
+
|
|
172
|
+
refreshCoordinator.subscribeTo(component, screenId, listener);
|
|
173
|
+
refreshCoordinator.triggerRefresh([component], screenId);
|
|
174
|
+
|
|
175
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
176
|
+
});
|
|
177
|
+
|
|
133
178
|
// ----------------------------------------
|
|
134
179
|
// clear
|
|
135
180
|
// ----------------------------------------
|
|
@@ -16,12 +16,26 @@ type ComponentToRefresh = {
|
|
|
16
16
|
/**
|
|
17
17
|
* Public contract for refresh coordination.
|
|
18
18
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* The coordinator serves as an event bus for component refresh events.
|
|
20
|
+
* It supports two types of producers and any number of consumers:
|
|
21
|
+
*
|
|
22
|
+
* Producers (push events into refresh$):
|
|
23
|
+
* register() → starts a periodic timer that emits refresh events
|
|
24
|
+
* triggerRefresh() → imperatively emits refresh events (e.g. pull-to-refresh)
|
|
25
|
+
*
|
|
26
|
+
* Consumers (react to refresh events):
|
|
27
|
+
* subscribeTo() → listen to refresh events for a specific component/screen
|
|
28
|
+
* subscribeToAny() → listen to all refresh events with throttling
|
|
29
|
+
*
|
|
30
|
+
* Note: subscribeTo() works independently of register(). A component can
|
|
31
|
+
* subscribe to refresh events without having auto-refresh enabled.
|
|
32
|
+
* This is how pull-to-refresh works — triggerRefresh() pushes events that
|
|
33
|
+
* are picked up by existing subscribeTo() listeners in UrlFeedResolver,
|
|
34
|
+
* even for components that don't have periodic auto-refresh timers.
|
|
22
35
|
*/
|
|
23
36
|
interface IRefreshCoordinator {
|
|
24
37
|
register(component: ZappUIComponent, screenId: string): () => void;
|
|
38
|
+
triggerRefresh(components: ZappUIComponent[], screenId: string): void;
|
|
25
39
|
subscribeTo(
|
|
26
40
|
component: ZappUIComponent,
|
|
27
41
|
screenId: string,
|
|
@@ -37,8 +51,10 @@ interface IRefreshCoordinator {
|
|
|
37
51
|
*
|
|
38
52
|
* Design goals:
|
|
39
53
|
* - One shared coordinator across the app
|
|
40
|
-
* -
|
|
41
|
-
* - Multiple
|
|
54
|
+
* - Central refresh$ event bus for all refresh events
|
|
55
|
+
* - Multiple producers: periodic timers (register) and imperative triggers (triggerRefresh)
|
|
56
|
+
* - Multiple consumers: per-component (subscribeTo) and global (subscribeToAny)
|
|
57
|
+
* - Producers and consumers are independent — subscribeTo works without register
|
|
42
58
|
* - Safe cleanup of timers and subscriptions
|
|
43
59
|
*/
|
|
44
60
|
class RefreshCoordinator implements IRefreshCoordinator {
|
|
@@ -114,6 +130,19 @@ class RefreshCoordinator implements IRefreshCoordinator {
|
|
|
114
130
|
return (): void => this.unregister(componentId, screenId);
|
|
115
131
|
}
|
|
116
132
|
|
|
133
|
+
/**
|
|
134
|
+
* Imperatively triggers refresh for the given components on a screen.
|
|
135
|
+
*
|
|
136
|
+
* Pushes each component into the central refresh$ stream so that
|
|
137
|
+
* existing subscribers (e.g. UrlFeedResolver's reloadData) react
|
|
138
|
+
* to the event. Used by pull-to-refresh and programmatic refresh.
|
|
139
|
+
*/
|
|
140
|
+
public triggerRefresh(components: ZappUIComponent[], screenId: string): void {
|
|
141
|
+
components.forEach((component) => {
|
|
142
|
+
this.refresh$.next({ component, screenId });
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
117
146
|
/**
|
|
118
147
|
* Stops and removes timer for a specific component.
|
|
119
148
|
* Cleans up the RxJS subscription for that component's interval stream.
|