@applicaster/zapp-react-native-utils 13.0.0-alpha.1199723621 → 13.0.0-alpha.1240837114
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/appUtils/HooksManager/index.ts +1 -1
- package/appUtils/focusManager/index.ts +1 -10
- package/appUtils/orientationHelper.ts +11 -13
- package/appUtils/playerManager/player.ts +24 -12
- package/appUtils/playerManager/playerNative.ts +18 -3
- package/focusManager/FocusManager.ts +3 -1
- package/focusManager/index.ts +3 -21
- package/functionUtils/index.ts +21 -8
- package/manifestUtils/defaultManifestConfigurations/player.js +8 -0
- package/numberUtils/index.ts +7 -5
- package/package.json +2 -6
- package/playerUtils/configurationGenerator.ts +8 -0
- package/reactHooks/autoscrolling/__tests__/useTrackedView.test.tsx +5 -7
- package/reactHooks/autoscrolling/useTrackedView.ts +12 -1
- package/reactHooks/navigation/__tests__/index.test.tsx +0 -11
- package/reactHooks/navigation/index.ts +5 -3
- package/services/js2native.ts +673 -0
- package/time/BackgroundTimer.ts +55 -0
- package/time/Timer.ts +6 -6
|
@@ -84,7 +84,7 @@ export function HooksManager({
|
|
|
84
84
|
plugins,
|
|
85
85
|
startUpHooksData = {},
|
|
86
86
|
}: HookManagerArgs): HookManager {
|
|
87
|
-
hooksManagerLogger.addContext({ targetScreen });
|
|
87
|
+
hooksManagerLogger.addContext({ targetScreenId: targetScreen.id });
|
|
88
88
|
|
|
89
89
|
function logHookEvent(func, message, data) {
|
|
90
90
|
func({
|
|
@@ -334,16 +334,7 @@ export const focusManager = (function () {
|
|
|
334
334
|
return { success: false };
|
|
335
335
|
}
|
|
336
336
|
|
|
337
|
-
function treeLoaded(
|
|
338
|
-
logger.log({
|
|
339
|
-
message: "treeLoaded",
|
|
340
|
-
data: {
|
|
341
|
-
val,
|
|
342
|
-
node,
|
|
343
|
-
currentFocusNode,
|
|
344
|
-
},
|
|
345
|
-
});
|
|
346
|
-
}
|
|
337
|
+
function treeLoaded(_val, _node) {}
|
|
347
338
|
|
|
348
339
|
function pressIn() {
|
|
349
340
|
const currentFocusable = getCurrentFocus();
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
/// <reference types="@types/react-native" />
|
|
2
1
|
import * as ReactNative from "react-native";
|
|
3
|
-
import { isTV, platformSelect } from "../reactUtils";
|
|
4
2
|
import { usePickFromState } from "@applicaster/zapp-react-native-redux/hooks";
|
|
5
|
-
|
|
3
|
+
|
|
4
|
+
import { isTV, platformSelect } from "../reactUtils";
|
|
5
|
+
import { useIsTablet } from "../reactHooks";
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
QUICK_BRICK_EVENTS,
|
|
@@ -121,8 +121,15 @@ export const removeOrientationChangeListener = (listener: any): void => {
|
|
|
121
121
|
*/
|
|
122
122
|
export const isOrientationCompatible = ({
|
|
123
123
|
orientation,
|
|
124
|
-
layoutData: { width, height },
|
|
124
|
+
layoutData: { width, height, isTablet, isTabletPortrait },
|
|
125
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
|
+
|
|
126
133
|
const isLandscape = width > height;
|
|
127
134
|
|
|
128
135
|
if (
|
|
@@ -146,15 +153,6 @@ export const isOrientationCompatible = ({
|
|
|
146
153
|
return true;
|
|
147
154
|
};
|
|
148
155
|
|
|
149
|
-
export const useIsOrientationCompatible = (orientation) => {
|
|
150
|
-
const { height, width } = ReactNative.useWindowDimensions();
|
|
151
|
-
|
|
152
|
-
return isOrientationCompatible({
|
|
153
|
-
orientation,
|
|
154
|
-
layoutData: { width, height },
|
|
155
|
-
});
|
|
156
|
-
};
|
|
157
|
-
|
|
158
156
|
/**
|
|
159
157
|
* Determine final screen orientation based on device type,
|
|
160
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
|
-
|
|
205
|
-
|
|
206
|
-
|
|
204
|
+
this.updateContentDuration({ seekableDuration, duration, currentTime });
|
|
205
|
+
}
|
|
206
|
+
};
|
|
207
207
|
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
213
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
230
|
+
if (nextNextFocus) {
|
|
231
|
+
FocusManager.instance.setFocus(nextNextFocus, options);
|
|
232
|
+
}
|
|
231
233
|
} else {
|
|
232
234
|
FocusManager.instance.setFocusLocal(nextFocus);
|
|
233
235
|
|
package/focusManager/index.ts
CHANGED
|
@@ -8,8 +8,6 @@ import { isNilOrEmpty } from "../reactUtils/helpers";
|
|
|
8
8
|
import { isAndroidTVPlatform } from "../reactUtils";
|
|
9
9
|
import { noop } from "../functionUtils";
|
|
10
10
|
|
|
11
|
-
const EMPTY_GROUP_COMPONENT = "empty_group_component";
|
|
12
|
-
|
|
13
11
|
export function useFocusManager() {
|
|
14
12
|
return focusManager;
|
|
15
13
|
}
|
|
@@ -76,14 +74,6 @@ export function useFocusEffect(onFocus, focusWatcher) {
|
|
|
76
74
|
}, focusWatcher);
|
|
77
75
|
}
|
|
78
76
|
|
|
79
|
-
export function shouldShowEmptyGroup(component) {
|
|
80
|
-
return (
|
|
81
|
-
component?.ui_components?.filter(
|
|
82
|
-
({ component_type }) => component_type === EMPTY_GROUP_COMPONENT
|
|
83
|
-
)?.length > 0
|
|
84
|
-
);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
77
|
export const isFocusable = (component: ZappEntry | ZappUIComponent | any) =>
|
|
88
78
|
R.tryCatch(
|
|
89
79
|
R.compose(
|
|
@@ -101,6 +91,7 @@ type Props = {
|
|
|
101
91
|
|
|
102
92
|
export const useIsFocusable = (props: Props): void => {
|
|
103
93
|
const { component, zappPipesData } = props;
|
|
94
|
+
|
|
104
95
|
const { setIsFocusable } = useFocusable();
|
|
105
96
|
|
|
106
97
|
const componentCellsFocusable = toBooleanWithDefaultTrue(
|
|
@@ -119,12 +110,7 @@ export const useIsFocusable = (props: Props): void => {
|
|
|
119
110
|
|
|
120
111
|
const hasDataError = !!zappPipesData?.error;
|
|
121
112
|
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
const isGallery = component?.component_type === "gallery-qb";
|
|
125
|
-
|
|
126
|
-
const isHiddenDueToEmptyData =
|
|
127
|
-
!isLoadingData && hasNoData && hideIfDataEmpty && !hasEmptyGroup;
|
|
113
|
+
const isHiddenDueToEmptyData = !isLoadingData && hasNoData && hideIfDataEmpty;
|
|
128
114
|
|
|
129
115
|
const defaultFocusableLogic =
|
|
130
116
|
componentCellsFocusable && (!hasNoData || !hideIfDataEmpty);
|
|
@@ -133,9 +119,7 @@ export const useIsFocusable = (props: Props): void => {
|
|
|
133
119
|
if (isAndroidTVPlatform()) {
|
|
134
120
|
let isFocusableNew = false;
|
|
135
121
|
|
|
136
|
-
if (
|
|
137
|
-
isFocusableNew = true;
|
|
138
|
-
} else if (isFocusable(component)) {
|
|
122
|
+
if (isFocusable(component)) {
|
|
139
123
|
isFocusableNew = defaultFocusableLogic;
|
|
140
124
|
}
|
|
141
125
|
|
|
@@ -144,9 +128,7 @@ export const useIsFocusable = (props: Props): void => {
|
|
|
144
128
|
}, [
|
|
145
129
|
defaultFocusableLogic,
|
|
146
130
|
isHiddenDueToEmptyData,
|
|
147
|
-
isGallery,
|
|
148
131
|
setIsFocusable,
|
|
149
132
|
hasDataError,
|
|
150
|
-
hasEmptyGroup,
|
|
151
133
|
]);
|
|
152
134
|
};
|
package/functionUtils/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as R from "ramda";
|
|
2
|
-
import { useRef } from "react";
|
|
2
|
+
import { useCallback, useEffect, useRef } from "react";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
*
|
|
@@ -9,31 +9,44 @@ import { useRef } from "react";
|
|
|
9
9
|
*/
|
|
10
10
|
export const useThrottle = (callback: () => void, delay: number) => {
|
|
11
11
|
const lastCallRef = useRef<number>(0);
|
|
12
|
-
const timeoutRef = useRef<
|
|
12
|
+
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
|
13
|
+
const callbackRef = useRef<() => void>(callback);
|
|
13
14
|
|
|
14
|
-
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
callbackRef.current = callback;
|
|
17
|
+
}, [callback]);
|
|
18
|
+
|
|
19
|
+
const throttledFunction = useCallback(() => {
|
|
15
20
|
const now = Date.now();
|
|
16
21
|
|
|
17
22
|
if (now - lastCallRef.current >= delay) {
|
|
18
|
-
|
|
23
|
+
callbackRef.current();
|
|
19
24
|
lastCallRef.current = now;
|
|
20
25
|
} else if (!timeoutRef.current) {
|
|
21
26
|
const remainingTime = delay - (now - lastCallRef.current);
|
|
22
27
|
|
|
23
28
|
timeoutRef.current = setTimeout(() => {
|
|
24
|
-
|
|
29
|
+
callbackRef.current();
|
|
25
30
|
lastCallRef.current = Date.now();
|
|
26
31
|
timeoutRef.current = null;
|
|
27
32
|
}, remainingTime);
|
|
28
33
|
}
|
|
29
|
-
};
|
|
34
|
+
}, [delay]);
|
|
30
35
|
|
|
31
|
-
const cancel = () => {
|
|
36
|
+
const cancel = useCallback(() => {
|
|
32
37
|
if (timeoutRef.current) {
|
|
33
38
|
clearTimeout(timeoutRef.current);
|
|
34
39
|
timeoutRef.current = null;
|
|
35
40
|
}
|
|
36
|
-
};
|
|
41
|
+
}, []);
|
|
42
|
+
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
return () => {
|
|
45
|
+
if (timeoutRef.current) {
|
|
46
|
+
clearTimeout(timeoutRef.current);
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
}, []);
|
|
37
50
|
|
|
38
51
|
return { throttledFunction, cancel };
|
|
39
52
|
};
|
|
@@ -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/numberUtils/index.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { logger } from "@applicaster/zapp-react-native-utils/logger";
|
|
|
3
3
|
|
|
4
4
|
const numberUtilsLogger = logger?.addSubsystem("NumberUtils");
|
|
5
5
|
|
|
6
|
-
export const toNumber = (value:
|
|
6
|
+
export const toNumber = (value: unknown): number | undefined => {
|
|
7
7
|
if (R.isNil(value) || R.isEmpty(value)) {
|
|
8
8
|
return undefined;
|
|
9
9
|
}
|
|
@@ -17,10 +17,12 @@ export const toNumber = (value: any): number | undefined => {
|
|
|
17
17
|
return undefined;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
export const toNumberWithDefault = (
|
|
21
|
-
|
|
20
|
+
export const toNumberWithDefault = (
|
|
21
|
+
defaultValue: number,
|
|
22
|
+
value: unknown
|
|
23
|
+
): number => toNumber(value) ?? defaultValue;
|
|
22
24
|
|
|
23
|
-
export const toNumberWithDefaultZero = (value:
|
|
25
|
+
export const toNumberWithDefaultZero = (value: unknown): number =>
|
|
24
26
|
toNumberWithDefault(0, value);
|
|
25
27
|
|
|
26
28
|
export const requireNumber = (obj, key: string) => {
|
|
@@ -39,7 +41,7 @@ export const requireNumber = (obj, key: string) => {
|
|
|
39
41
|
|
|
40
42
|
export const toFiniteNumberWithDefault = (
|
|
41
43
|
defaultValue: number,
|
|
42
|
-
value:
|
|
44
|
+
value: unknown
|
|
43
45
|
): number => {
|
|
44
46
|
const possibleNumber = toNumberWithDefault(defaultValue, value);
|
|
45
47
|
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-native-utils",
|
|
3
|
-
"version": "13.0.0-alpha.
|
|
3
|
+
"version": "13.0.0-alpha.1240837114",
|
|
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.
|
|
30
|
+
"@applicaster/applicaster-types": "13.0.0-alpha.1240837114",
|
|
32
31
|
"buffer": "^5.2.1",
|
|
33
32
|
"camelize": "^1.0.0",
|
|
34
33
|
"dayjs": "^1.11.10",
|
|
@@ -45,8 +44,5 @@
|
|
|
45
44
|
"uglify-js": "*",
|
|
46
45
|
"validate-color": "*",
|
|
47
46
|
"zustand": "*"
|
|
48
|
-
},
|
|
49
|
-
"devDependencies": {
|
|
50
|
-
"@types/react-native": "0.69.6"
|
|
51
47
|
}
|
|
52
48
|
}
|
|
@@ -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
|
|
|
@@ -35,25 +35,23 @@ const Wrapper = ({ children }: { children: React.ReactChild }) => (
|
|
|
35
35
|
const { useTrackedView } = require("../useTrackedView");
|
|
36
36
|
|
|
37
37
|
describe("useTrackCurrentAutoScrollingElement", () => {
|
|
38
|
-
it("should update position for selected component",
|
|
38
|
+
it("should update position for selected component - onViewportEnter", () => {
|
|
39
39
|
const { result } = renderHook(() => useTrackedView("123"), {
|
|
40
40
|
wrapper: Wrapper,
|
|
41
41
|
});
|
|
42
42
|
|
|
43
|
-
// Fast-forward until all timers have been executed
|
|
44
|
-
jest.advanceTimersByTime(3000);
|
|
45
|
-
|
|
46
43
|
expect(result.current.inViewPort).toBe(false);
|
|
47
44
|
|
|
48
45
|
const mockRect = {
|
|
49
46
|
rect: { left: 1, right: 1, top: 1, bottom: 1 },
|
|
50
47
|
};
|
|
51
48
|
|
|
52
|
-
act(() => {
|
|
53
|
-
result.current.onPositionUpdated(mockRect);
|
|
49
|
+
act(async () => {
|
|
50
|
+
await result.current.onPositionUpdated(mockRect);
|
|
54
51
|
});
|
|
55
52
|
|
|
56
|
-
|
|
53
|
+
// Fast-forward until all timers have been executed
|
|
54
|
+
jest.runAllTimers();
|
|
57
55
|
|
|
58
56
|
expect(result.current.inViewPort).toBe(true);
|
|
59
57
|
|
|
@@ -2,6 +2,10 @@ import { useCallback, useEffect, useMemo, useState } from "react";
|
|
|
2
2
|
import { useScreenTrackedViewPositionsContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenTrackedViewPositionsContext";
|
|
3
3
|
import { useDimensions } from "@applicaster/zapp-react-native-utils/reactHooks/layout";
|
|
4
4
|
import { findTrackedView } from "./utils/findTrackedView";
|
|
5
|
+
import { isNil } from "ramda";
|
|
6
|
+
import { withTimeout } from "@applicaster/zapp-react-native-utils/idleUtils";
|
|
7
|
+
|
|
8
|
+
const TIMEOUT_FOR_DELAY_ON_VIEWPORT_ENTER_EVENT = 500; // ms
|
|
5
9
|
|
|
6
10
|
const getNormalizedRect = (rect, componentId, screenWidth, screenHeight) => {
|
|
7
11
|
if (!rect) {
|
|
@@ -37,9 +41,16 @@ export const useTrackedView = (componentId: string) => {
|
|
|
37
41
|
);
|
|
38
42
|
|
|
39
43
|
const onPositionUpdated = useCallback(
|
|
40
|
-
({ rect }) => {
|
|
44
|
+
async ({ rect }) => {
|
|
41
45
|
const currentPosition = normalizedRect(componentId, rect);
|
|
42
46
|
|
|
47
|
+
// There is an issue on the native side where they are unable to guarantee
|
|
48
|
+
// that the order of onViewportLeave and onViewportEnter events is always correct.
|
|
49
|
+
// This workaround/hack delays the onViewportEnter event to ensure it is received last.
|
|
50
|
+
if (!isNil(currentPosition)) {
|
|
51
|
+
await withTimeout(TIMEOUT_FOR_DELAY_ON_VIEWPORT_ENTER_EVENT);
|
|
52
|
+
}
|
|
53
|
+
|
|
43
54
|
updateComponentsPositions(componentId, currentPosition as any);
|
|
44
55
|
|
|
45
56
|
setInViewPort(!!currentPosition);
|
|
@@ -366,16 +366,5 @@ describe("navigation", () => {
|
|
|
366
366
|
|
|
367
367
|
expect(navBarVisible).toBe(true);
|
|
368
368
|
});
|
|
369
|
-
|
|
370
|
-
it("returns true otherwise", () => {
|
|
371
|
-
const navBarVisible = isNavBarVisible(
|
|
372
|
-
"",
|
|
373
|
-
{ navigations: [] } as any,
|
|
374
|
-
true,
|
|
375
|
-
true
|
|
376
|
-
);
|
|
377
|
-
|
|
378
|
-
expect(navBarVisible).toBe(false);
|
|
379
|
-
});
|
|
380
369
|
});
|
|
381
370
|
});
|
|
@@ -108,9 +108,11 @@ export function isNavBarVisible(
|
|
|
108
108
|
return false;
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
const showSecondaryLevel =
|
|
112
|
-
(
|
|
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;
|
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { localStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/LocalStorage";
|
|
2
|
+
import { sessionStorage } from "@applicaster/zapp-react-native-bridge/ZappStorage/SessionStorage";
|
|
3
|
+
import { parseJsonIfNeeded } from "@applicaster/zapp-react-native-utils/functionUtils";
|
|
4
|
+
|
|
5
|
+
export const JS_BRIDGE_EVENTS = {
|
|
6
|
+
DISMISS_HOOK: "DISMISS_HOOK",
|
|
7
|
+
LOG: "LOG",
|
|
8
|
+
GET_SESSION_STORAGE: "GET_SESSION_STORAGE",
|
|
9
|
+
SET_SESSION_STORAGE: "SET_SESSION_STORAGE",
|
|
10
|
+
GET_LOCAL_STORAGE: "GET_LOCAL_STORAGE",
|
|
11
|
+
SET_LOCAL_STORAGE: "SET_LOCAL_STORAGE",
|
|
12
|
+
GET_SECURE_LOCAL_STORAGE: "GET_SECURE_LOCAL_STORAGE",
|
|
13
|
+
SET_SECURE_LOCAL_STORAGE: "SET_SECURE_LOCAL_STORAGE",
|
|
14
|
+
CLOSE_WEBVIEW: "CLOSE_WEBVIEW",
|
|
15
|
+
NAVIGATE_TO_SCREEN: "NAVIGATE_TO_SCREEN",
|
|
16
|
+
NAVIGATE_TO_ENTRY: "NAVIGATE_TO_ENTRY",
|
|
17
|
+
OPEN_URL_SCHEME: "OPEN_URL_SCHEME",
|
|
18
|
+
OPEN_NATIVE_SHARE: "OPEN_NATIVE_SHARE",
|
|
19
|
+
ADD_SESSION_STORAGE_LISTENER: "ADD_SESSION_STORAGE_LISTENER",
|
|
20
|
+
REMOVE_SESSION_STORAGE_LISTENER: "REMOVE_SESSION_STORAGE_LISTENER",
|
|
21
|
+
ADD_LOCAL_STORAGE_LISTENER: "ADD_LOCAL_STORAGE_LISTENER",
|
|
22
|
+
REMOVE_LOCAL_STORAGE_LISTENER: "REMOVE_LOCAL_STORAGE_LISTENER",
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const INVOKE_APPLICASTER_CALLBACK = "__invoke__applicaster__callback";
|
|
26
|
+
|
|
27
|
+
const APPLICASTER_CALLBACK_KEY = "__applicaster__js2native__callbacks";
|
|
28
|
+
const getSessionStorageItem = "getSessionStorageItem";
|
|
29
|
+
const setSessionStorageItem = "setSessionStorageItem";
|
|
30
|
+
const getLocalStorageItem = "getLocalStorageItem";
|
|
31
|
+
const setLocalStorageItem = "setLocalStorageItem";
|
|
32
|
+
const getSecureLocalStorageItem = "getSecureLocalStorageItem";
|
|
33
|
+
const setSecureLocalStorageItem = "setSecureLocalStorageItem";
|
|
34
|
+
const addSessionStorageListenerFn = "addSessionStorageListener";
|
|
35
|
+
const addLocalStorageListenerFn = "addLocalStorageListener";
|
|
36
|
+
|
|
37
|
+
const activeListeners: { [key: string]: () => void } = {};
|
|
38
|
+
|
|
39
|
+
const sendMessage = `
|
|
40
|
+
function sendMessage(type, payload, meta) {
|
|
41
|
+
if (window.ReactNativeWebView && typeof window.ReactNativeWebView.postMessage === 'function') {
|
|
42
|
+
window.ReactNativeWebView.postMessage(JSON.stringify({
|
|
43
|
+
type: type,
|
|
44
|
+
payload: payload,
|
|
45
|
+
meta: meta
|
|
46
|
+
}));
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
`;
|
|
50
|
+
|
|
51
|
+
const consolePolyfill = `
|
|
52
|
+
var consoleLog = console.log;
|
|
53
|
+
var consoleWarn = console.warn;
|
|
54
|
+
var consoleError = console.error;
|
|
55
|
+
|
|
56
|
+
function logInConsole(level, args) {
|
|
57
|
+
var serializableArgs = [];
|
|
58
|
+
try {
|
|
59
|
+
serializableArgs = args.map(function(arg) {
|
|
60
|
+
try {
|
|
61
|
+
return JSON.parse(JSON.stringify(arg));
|
|
62
|
+
} catch (e) {
|
|
63
|
+
if (typeof arg === 'function') return '[Function]';
|
|
64
|
+
if (typeof arg === 'symbol') return String(arg);
|
|
65
|
+
if (arg instanceof Error) return { name: arg.name, message: arg.message, stack: arg.stack };
|
|
66
|
+
return '[Unserializable]';
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
} catch(e) {
|
|
70
|
+
serializableArgs = ['Error processing console arguments'];
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
sendMessage("${JS_BRIDGE_EVENTS.LOG}", {
|
|
74
|
+
level: level,
|
|
75
|
+
messages: JSON.stringify(serializableArgs)
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function log() {
|
|
80
|
+
var args = [].slice.call(arguments);
|
|
81
|
+
consoleLog.apply(null, args);
|
|
82
|
+
try {
|
|
83
|
+
logInConsole("log", args);
|
|
84
|
+
} catch (e) {
|
|
85
|
+
consoleError("Error in custom console.log:", e);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
function warn() {
|
|
90
|
+
var args = [].slice.call(arguments);
|
|
91
|
+
consoleWarn.apply(null, args);
|
|
92
|
+
try {
|
|
93
|
+
logInConsole("warn", args);
|
|
94
|
+
} catch (e) {
|
|
95
|
+
consoleError("Error in custom console.warn:", e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function error() {
|
|
100
|
+
var args = [].slice.call(arguments);
|
|
101
|
+
consoleError.apply(null, args);
|
|
102
|
+
try {
|
|
103
|
+
logInConsole("error", args);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
consoleError("Error in custom console.error:", e);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
setTimeout(function() {
|
|
110
|
+
console.log = log;
|
|
111
|
+
console.warn = warn;
|
|
112
|
+
console.error = error;
|
|
113
|
+
}, 0);
|
|
114
|
+
`;
|
|
115
|
+
|
|
116
|
+
const callbackHandling = `
|
|
117
|
+
window.Applicaster = window.Applicaster || {};
|
|
118
|
+
window.Applicaster.${APPLICASTER_CALLBACK_KEY} = window.Applicaster.${APPLICASTER_CALLBACK_KEY} || {};
|
|
119
|
+
|
|
120
|
+
var ${INVOKE_APPLICASTER_CALLBACK} = function(key, args) {
|
|
121
|
+
var callback = window.Applicaster.${APPLICASTER_CALLBACK_KEY}[key];
|
|
122
|
+
|
|
123
|
+
var deleteCallback = true;
|
|
124
|
+
if (typeof callback === "function") {
|
|
125
|
+
var result = callback.apply(null, args);
|
|
126
|
+
if (result === false) {
|
|
127
|
+
deleteCallback = false;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if(deleteCallback) {
|
|
131
|
+
delete window.Applicaster.${APPLICASTER_CALLBACK_KEY}[key];
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
window.Applicaster.${INVOKE_APPLICASTER_CALLBACK} = ${INVOKE_APPLICASTER_CALLBACK};
|
|
136
|
+
`;
|
|
137
|
+
|
|
138
|
+
export function invokeWebCallback(key, args) {
|
|
139
|
+
const argsArray = Array.isArray(args) ? args : [args];
|
|
140
|
+
let stringifiedArgs = "[]";
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
stringifiedArgs = JSON.stringify(argsArray);
|
|
144
|
+
} catch (e) {
|
|
145
|
+
try {
|
|
146
|
+
stringifiedArgs = JSON.stringify([
|
|
147
|
+
{ error: "Failed to stringify arguments", details: e.message },
|
|
148
|
+
]);
|
|
149
|
+
} catch {
|
|
150
|
+
stringifiedArgs = "[]";
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return `
|
|
155
|
+
(function() {
|
|
156
|
+
var callbackFn = window.Applicaster && window.Applicaster.${APPLICASTER_CALLBACK_KEY};
|
|
157
|
+
if (callbackFn && typeof callbackFn["${key}"] === 'function') {
|
|
158
|
+
try {
|
|
159
|
+
setTimeout(function() {
|
|
160
|
+
callbackFn["${key}"].apply(null, ${stringifiedArgs});
|
|
161
|
+
}, 0);
|
|
162
|
+
} catch (e) {
|
|
163
|
+
console.error("Error invoking web callback ${key}:", e);
|
|
164
|
+
}
|
|
165
|
+
} else {
|
|
166
|
+
// console.warn("Web callback ${key} not found or not a function.");
|
|
167
|
+
}
|
|
168
|
+
return true;
|
|
169
|
+
})();
|
|
170
|
+
`;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const hookCallback = `
|
|
174
|
+
function hookCallback(options) {
|
|
175
|
+
var _opts = {};
|
|
176
|
+
|
|
177
|
+
if (typeof options !== "undefined") {
|
|
178
|
+
_opts = options;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
var success = typeof _opts.success === 'boolean' ? _opts.success : false;
|
|
182
|
+
var error = _opts.error || undefined;
|
|
183
|
+
var payload = _opts.payload || undefined;
|
|
184
|
+
|
|
185
|
+
sendMessage("${JS_BRIDGE_EVENTS.DISMISS_HOOK}", {
|
|
186
|
+
success: success,
|
|
187
|
+
error: error,
|
|
188
|
+
payload: payload
|
|
189
|
+
})
|
|
190
|
+
};
|
|
191
|
+
`;
|
|
192
|
+
|
|
193
|
+
const uuid = `
|
|
194
|
+
function uuid() {
|
|
195
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
196
|
+
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
197
|
+
return v.toString(16);
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
`;
|
|
201
|
+
|
|
202
|
+
const registerCallback = `
|
|
203
|
+
function registerCallback(cb, persistent) {
|
|
204
|
+
var index = uuid();
|
|
205
|
+
window.Applicaster = window.Applicaster || {};
|
|
206
|
+
window.Applicaster.${APPLICASTER_CALLBACK_KEY} = window.Applicaster.${APPLICASTER_CALLBACK_KEY} || {};
|
|
207
|
+
|
|
208
|
+
window.Applicaster.${APPLICASTER_CALLBACK_KEY}[index] = function() {
|
|
209
|
+
var result = cb.apply(null, arguments);
|
|
210
|
+
return persistent ? false : result;
|
|
211
|
+
};
|
|
212
|
+
return index;
|
|
213
|
+
}
|
|
214
|
+
`;
|
|
215
|
+
|
|
216
|
+
const getSetStorage = `
|
|
217
|
+
function getSetStorage(type, payload) {
|
|
218
|
+
return new Promise(function(resolve, reject) {
|
|
219
|
+
var index = registerCallback(function(result) {
|
|
220
|
+
if (result && result.success === false) {
|
|
221
|
+
reject(result.error || new Error("Storage operation failed"));
|
|
222
|
+
} else {
|
|
223
|
+
resolve(result);
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
sendMessage(type, payload, index);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
`;
|
|
230
|
+
|
|
231
|
+
const getInSessionStorage = `
|
|
232
|
+
function ${getSessionStorageItem}(key, namespace) {
|
|
233
|
+
return getSetStorage(
|
|
234
|
+
"${JS_BRIDGE_EVENTS.GET_SESSION_STORAGE}",
|
|
235
|
+
{ key: key, namespace: namespace }
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
`;
|
|
239
|
+
|
|
240
|
+
const setInSessionStorage = `
|
|
241
|
+
function ${setSessionStorageItem}(key, value, namespace) {
|
|
242
|
+
try {
|
|
243
|
+
JSON.stringify(value);
|
|
244
|
+
return getSetStorage(
|
|
245
|
+
"${JS_BRIDGE_EVENTS.SET_SESSION_STORAGE}",
|
|
246
|
+
{ key: key, value: value, namespace: namespace }
|
|
247
|
+
);
|
|
248
|
+
} catch (e) {
|
|
249
|
+
return Promise.reject(new Error("Value is not serializable: " + e.message));
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
`;
|
|
253
|
+
|
|
254
|
+
const getInLocalStorage = `
|
|
255
|
+
function ${getLocalStorageItem}(key, namespace) {
|
|
256
|
+
return getSetStorage(
|
|
257
|
+
"${JS_BRIDGE_EVENTS.GET_LOCAL_STORAGE}",
|
|
258
|
+
{ key: key, namespace: namespace }
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
`;
|
|
262
|
+
|
|
263
|
+
const setInLocalStorage = `
|
|
264
|
+
function ${setLocalStorageItem}(key, value, namespace) {
|
|
265
|
+
try {
|
|
266
|
+
JSON.stringify(value);
|
|
267
|
+
return getSetStorage(
|
|
268
|
+
"${JS_BRIDGE_EVENTS.SET_LOCAL_STORAGE}",
|
|
269
|
+
{ key: key, value: value, namespace: namespace }
|
|
270
|
+
);
|
|
271
|
+
} catch (e) {
|
|
272
|
+
return Promise.reject(new Error("Value is not serializable: " + e.message));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
`;
|
|
276
|
+
|
|
277
|
+
const getInSecureLocalStorage = `
|
|
278
|
+
function ${getSecureLocalStorageItem}(key, namespace) {
|
|
279
|
+
return getSetStorage(
|
|
280
|
+
"${JS_BRIDGE_EVENTS.GET_SECURE_LOCAL_STORAGE}",
|
|
281
|
+
{ key: key, namespace: namespace }
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
`;
|
|
285
|
+
|
|
286
|
+
const setInSecureLocalStorage = `
|
|
287
|
+
function ${setSecureLocalStorageItem}(key, value, namespace) {
|
|
288
|
+
try {
|
|
289
|
+
JSON.stringify(value);
|
|
290
|
+
return getSetStorage(
|
|
291
|
+
"${JS_BRIDGE_EVENTS.SET_SECURE_LOCAL_STORAGE}",
|
|
292
|
+
{ key: key, value: value, namespace: namespace }
|
|
293
|
+
);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
return Promise.reject(new Error("Value is not serializable: " + e.message));
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
`;
|
|
299
|
+
|
|
300
|
+
const addSessionStorageListener = `
|
|
301
|
+
function ${addSessionStorageListenerFn}(optionsOrCallback, callback) {
|
|
302
|
+
var opts = {};
|
|
303
|
+
var cb = callback;
|
|
304
|
+
|
|
305
|
+
if (typeof optionsOrCallback === 'function') {
|
|
306
|
+
cb = optionsOrCallback;
|
|
307
|
+
} else if (typeof optionsOrCallback === 'object' && optionsOrCallback !== null) {
|
|
308
|
+
opts = optionsOrCallback;
|
|
309
|
+
} else if (optionsOrCallback !== undefined && optionsOrCallback !== null) {
|
|
310
|
+
console.error("Invalid arguments for addSessionStorageListener. Expected ({ key, namespace }, callback) or (callback).");
|
|
311
|
+
return function() {};
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (typeof cb !== 'function') {
|
|
315
|
+
console.error("addSessionStorageListener requires a callback function.");
|
|
316
|
+
return function() {};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
var listenerId = registerCallback(cb, true);
|
|
320
|
+
|
|
321
|
+
sendMessage("${JS_BRIDGE_EVENTS.ADD_SESSION_STORAGE_LISTENER}", { key: opts.key, namespace: opts.namespace }, listenerId);
|
|
322
|
+
|
|
323
|
+
return function removeListener() {
|
|
324
|
+
sendMessage("${JS_BRIDGE_EVENTS.REMOVE_SESSION_STORAGE_LISTENER}", { listenerId: listenerId });
|
|
325
|
+
if (window.Applicaster && window.Applicaster.${APPLICASTER_CALLBACK_KEY}) {
|
|
326
|
+
delete window.Applicaster.${APPLICASTER_CALLBACK_KEY}[listenerId];
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
`;
|
|
331
|
+
|
|
332
|
+
const addLocalStorageListener = `
|
|
333
|
+
function ${addLocalStorageListenerFn}(optionsOrCallback, callback) {
|
|
334
|
+
var opts = {};
|
|
335
|
+
var cb = callback;
|
|
336
|
+
|
|
337
|
+
if (typeof optionsOrCallback === 'function') {
|
|
338
|
+
cb = optionsOrCallback;
|
|
339
|
+
} else if (typeof optionsOrCallback === 'object' && optionsOrCallback !== null) {
|
|
340
|
+
opts = optionsOrCallback;
|
|
341
|
+
} else if (optionsOrCallback !== undefined && optionsOrCallback !== null) {
|
|
342
|
+
console.error("Invalid arguments for addLocalStorageListener. Expected ({ key, namespace }, callback) or (callback).");
|
|
343
|
+
return function() {};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (typeof cb !== 'function') {
|
|
347
|
+
console.error("addLocalStorageListener requires a callback function.");
|
|
348
|
+
return function() {};
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
var listenerId = registerCallback(cb, true);
|
|
352
|
+
|
|
353
|
+
sendMessage("${JS_BRIDGE_EVENTS.ADD_LOCAL_STORAGE_LISTENER}", { key: opts.key, namespace: opts.namespace }, listenerId);
|
|
354
|
+
|
|
355
|
+
return function removeListener() {
|
|
356
|
+
sendMessage("${JS_BRIDGE_EVENTS.REMOVE_LOCAL_STORAGE_LISTENER}", { listenerId: listenerId });
|
|
357
|
+
if (window.Applicaster && window.Applicaster.${APPLICASTER_CALLBACK_KEY}) {
|
|
358
|
+
delete window.Applicaster.${APPLICASTER_CALLBACK_KEY}[listenerId];
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
`;
|
|
363
|
+
|
|
364
|
+
const closeWebview = `
|
|
365
|
+
function closeWebview() {
|
|
366
|
+
sendMessage("${JS_BRIDGE_EVENTS.CLOSE_WEBVIEW}")
|
|
367
|
+
}
|
|
368
|
+
`;
|
|
369
|
+
|
|
370
|
+
const navigateToScreen = `
|
|
371
|
+
function navigateToScreen(screenId) {
|
|
372
|
+
if(!screenId) { console.error("navigateToScreen requires a screenId"); return; }
|
|
373
|
+
sendMessage("${JS_BRIDGE_EVENTS.NAVIGATE_TO_SCREEN}", { screenId: screenId });
|
|
374
|
+
}
|
|
375
|
+
`;
|
|
376
|
+
|
|
377
|
+
const navigateToEntry = `
|
|
378
|
+
function navigateToEntry(entry) {
|
|
379
|
+
if(!entry) { console.error("navigateToEntry requires an entry object"); return; }
|
|
380
|
+
sendMessage("${JS_BRIDGE_EVENTS.NAVIGATE_TO_ENTRY}", { entry: entry });
|
|
381
|
+
}
|
|
382
|
+
`;
|
|
383
|
+
|
|
384
|
+
const openUrlScheme = `
|
|
385
|
+
function openUrlScheme(url) {
|
|
386
|
+
if(!url || typeof url !== 'string') { console.error("openUrlScheme requires a valid URL string"); return; }
|
|
387
|
+
sendMessage("${JS_BRIDGE_EVENTS.OPEN_URL_SCHEME}", { url: url });
|
|
388
|
+
}
|
|
389
|
+
`;
|
|
390
|
+
|
|
391
|
+
const runInvokeShare = `
|
|
392
|
+
function invokeShare(item, options) {
|
|
393
|
+
if(!item) { console.error("invokeShare requires an item object"); return; }
|
|
394
|
+
sendMessage("${JS_BRIDGE_EVENTS.OPEN_NATIVE_SHARE}", { item: item, options: options });
|
|
395
|
+
}
|
|
396
|
+
`;
|
|
397
|
+
|
|
398
|
+
const initializeBridge = (payload, hookPlugin, screens, safeAreaInsets) => `
|
|
399
|
+
(function() {
|
|
400
|
+
window.Applicaster = window.Applicaster || {};
|
|
401
|
+
|
|
402
|
+
${sendMessage}
|
|
403
|
+
${consolePolyfill}
|
|
404
|
+
${hookCallback}
|
|
405
|
+
${uuid}
|
|
406
|
+
${callbackHandling}
|
|
407
|
+
${registerCallback}
|
|
408
|
+
${getSetStorage}
|
|
409
|
+
${getInSessionStorage}
|
|
410
|
+
${setInSessionStorage}
|
|
411
|
+
${getInLocalStorage}
|
|
412
|
+
${setInLocalStorage}
|
|
413
|
+
${getInSecureLocalStorage}
|
|
414
|
+
${setInSecureLocalStorage}
|
|
415
|
+
${addSessionStorageListener}
|
|
416
|
+
${addLocalStorageListener}
|
|
417
|
+
${closeWebview}
|
|
418
|
+
${navigateToScreen}
|
|
419
|
+
${navigateToEntry}
|
|
420
|
+
${openUrlScheme}
|
|
421
|
+
${runInvokeShare}
|
|
422
|
+
|
|
423
|
+
var JS2Native = {
|
|
424
|
+
screenHook: {
|
|
425
|
+
data: {
|
|
426
|
+
payload: (function() { try { return JSON.parse(${payload}); } catch(e) { console.error('Failed to parse screenHook payload:', e); return null; } })(),
|
|
427
|
+
plugin: (function() { try { return JSON.parse(${hookPlugin}); } catch(e) { console.error('Failed to parse screenHook plugin:', e); return null; } })()
|
|
428
|
+
},
|
|
429
|
+
callback: hookCallback
|
|
430
|
+
},
|
|
431
|
+
sessionStorage: {
|
|
432
|
+
getItem: ${getSessionStorageItem},
|
|
433
|
+
setItem: ${setSessionStorageItem},
|
|
434
|
+
addListener: ${addSessionStorageListenerFn}
|
|
435
|
+
},
|
|
436
|
+
localStorage: {
|
|
437
|
+
getItem: ${getLocalStorageItem},
|
|
438
|
+
setItem: ${setLocalStorageItem},
|
|
439
|
+
getSecuredItem: ${getSecureLocalStorageItem},
|
|
440
|
+
setSecuredItem: ${setSecureLocalStorageItem},
|
|
441
|
+
addListener: ${addLocalStorageListenerFn}
|
|
442
|
+
},
|
|
443
|
+
nativeEnvironment: true,
|
|
444
|
+
navigation: {
|
|
445
|
+
closeWebview: closeWebview,
|
|
446
|
+
navigateToScreen: navigateToScreen,
|
|
447
|
+
navigateToEntry: navigateToEntry,
|
|
448
|
+
screens: (function() { try { return JSON.parse(${screens}); } catch(e) { console.error('Failed to parse navigation screens:', e); return []; } })(),
|
|
449
|
+
openUrlScheme: openUrlScheme
|
|
450
|
+
},
|
|
451
|
+
safeAreaInsets: (function() { try { return JSON.parse(${safeAreaInsets}); } catch(e) { console.error('Failed to parse safeAreaInsets:', e); return {}; } })(),
|
|
452
|
+
nativeShare: invokeShare
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
window.Applicaster.JS2Native = JS2Native;
|
|
456
|
+
window.JS2Native = JS2Native;
|
|
457
|
+
|
|
458
|
+
return JS2Native;
|
|
459
|
+
})();
|
|
460
|
+
`;
|
|
461
|
+
|
|
462
|
+
export const jsBridge = (hooksProps, screens, safeAreaInsets) => `
|
|
463
|
+
var Applicaster = window.Applicaster || {};
|
|
464
|
+
window.Applicaster = Applicaster;
|
|
465
|
+
|
|
466
|
+
${initializeBridge(
|
|
467
|
+
JSON.stringify(hooksProps?.payload),
|
|
468
|
+
JSON.stringify(hooksProps?.hookPlugin),
|
|
469
|
+
JSON.stringify(Object.values(screens || {})),
|
|
470
|
+
JSON.stringify(safeAreaInsets || {})
|
|
471
|
+
)}
|
|
472
|
+
|
|
473
|
+
true;
|
|
474
|
+
`;
|
|
475
|
+
|
|
476
|
+
// handler for removing listeners
|
|
477
|
+
async function removeStorageListenerHandler(payload: { listenerId?: string }) {
|
|
478
|
+
const { listenerId } = payload || {};
|
|
479
|
+
|
|
480
|
+
if (!listenerId) {
|
|
481
|
+
return;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const removeListener = activeListeners[listenerId];
|
|
485
|
+
|
|
486
|
+
if (removeListener) {
|
|
487
|
+
try {
|
|
488
|
+
removeListener();
|
|
489
|
+
delete activeListeners[listenerId];
|
|
490
|
+
} catch (error) {
|
|
491
|
+
//
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function log({ level, messages }) {
|
|
497
|
+
try {
|
|
498
|
+
const parsedMessages = parseJsonIfNeeded(messages);
|
|
499
|
+
const logFn = console[level] || console.log;
|
|
500
|
+
|
|
501
|
+
if (Array.isArray(parsedMessages)) {
|
|
502
|
+
logFn("JS LOG:", ...parsedMessages);
|
|
503
|
+
} else {
|
|
504
|
+
logFn("JS LOG:", parsedMessages);
|
|
505
|
+
}
|
|
506
|
+
} catch (e) {
|
|
507
|
+
// console.error("Error parsing or logging JS message:", e, messages);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
async function storageGetItem({ key, namespace }) {
|
|
512
|
+
try {
|
|
513
|
+
const result = await sessionStorage.getItem(key, namespace);
|
|
514
|
+
|
|
515
|
+
return { success: true, value: result };
|
|
516
|
+
} catch (error) {
|
|
517
|
+
return { success: false, error: error.message };
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
async function storageSetItem({ key, value, namespace }) {
|
|
522
|
+
try {
|
|
523
|
+
const result = await sessionStorage.setItem(key, value, namespace);
|
|
524
|
+
|
|
525
|
+
return { success: !!result };
|
|
526
|
+
} catch (error) {
|
|
527
|
+
return { success: false, error: error.message };
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
async function localStorageGetItem({ key, namespace }) {
|
|
532
|
+
try {
|
|
533
|
+
const result = await localStorage.getItem(key, namespace);
|
|
534
|
+
|
|
535
|
+
return { success: true, value: result };
|
|
536
|
+
} catch (error) {
|
|
537
|
+
return { success: false, error: error.message };
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
async function localStorageSetItem({ key, value, namespace }) {
|
|
542
|
+
try {
|
|
543
|
+
const result = await localStorage.setItem(key, value, namespace);
|
|
544
|
+
|
|
545
|
+
return { success: !!result };
|
|
546
|
+
} catch (error) {
|
|
547
|
+
return { success: false, error: error.message };
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function secureStorageGetItem({ key, namespace }) {
|
|
552
|
+
try {
|
|
553
|
+
const result = await localStorage.getKeychainItem(key, namespace);
|
|
554
|
+
|
|
555
|
+
return { success: true, value: result };
|
|
556
|
+
} catch (error) {
|
|
557
|
+
return { success: false, error: error.message };
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
async function secureStorageSetItem({ key, value, namespace }) {
|
|
562
|
+
try {
|
|
563
|
+
const result = await localStorage.setKeychainItem(key, value, namespace);
|
|
564
|
+
|
|
565
|
+
return { success: !!result };
|
|
566
|
+
} catch (error) {
|
|
567
|
+
return { success: false, error: error.message };
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
// handler for adding listeners
|
|
572
|
+
async function addStorageListenerHandler(
|
|
573
|
+
storageType: "local" | "session",
|
|
574
|
+
payload: { key?: string; namespace?: string },
|
|
575
|
+
callbackKey: string,
|
|
576
|
+
webViewRef: React.RefObject<any>
|
|
577
|
+
) {
|
|
578
|
+
if (!webViewRef?.current?.injectJavaScript) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
const storage = storageType === "local" ? localStorage : sessionStorage;
|
|
583
|
+
const { key, namespace } = payload || {};
|
|
584
|
+
|
|
585
|
+
const listenerCallback = (eventData: {
|
|
586
|
+
key: string;
|
|
587
|
+
namespace: string;
|
|
588
|
+
value: any;
|
|
589
|
+
}) => {
|
|
590
|
+
const run = invokeWebCallback(callbackKey, [eventData]);
|
|
591
|
+
|
|
592
|
+
if (webViewRef?.current?.injectJavaScript) {
|
|
593
|
+
webViewRef.current.injectJavaScript(run);
|
|
594
|
+
} else {
|
|
595
|
+
removeStorageListenerHandler({ listenerId: callbackKey });
|
|
596
|
+
}
|
|
597
|
+
};
|
|
598
|
+
|
|
599
|
+
try {
|
|
600
|
+
const removeListener = storage.addListener(
|
|
601
|
+
{ key, namespace },
|
|
602
|
+
listenerCallback
|
|
603
|
+
);
|
|
604
|
+
|
|
605
|
+
activeListeners[callbackKey] = removeListener;
|
|
606
|
+
} catch (error) {
|
|
607
|
+
const errorRun = invokeWebCallback(callbackKey, [
|
|
608
|
+
{ success: false, error: `Failed to add listener: ${error.message}` },
|
|
609
|
+
]);
|
|
610
|
+
|
|
611
|
+
if (webViewRef?.current?.injectJavaScript) {
|
|
612
|
+
webViewRef.current.injectJavaScript(errorRun);
|
|
613
|
+
|
|
614
|
+
webViewRef.current.injectJavaScript(
|
|
615
|
+
`delete window.Applicaster.${APPLICASTER_CALLBACK_KEY}['${callbackKey}']; true;`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
delete activeListeners[callbackKey];
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export function resolveBridgeAction(
|
|
624
|
+
type: string,
|
|
625
|
+
webViewRef: React.RefObject<any>
|
|
626
|
+
) {
|
|
627
|
+
switch (type) {
|
|
628
|
+
case JS_BRIDGE_EVENTS.LOG:
|
|
629
|
+
return (payload) => log(payload);
|
|
630
|
+
|
|
631
|
+
case JS_BRIDGE_EVENTS.GET_SESSION_STORAGE:
|
|
632
|
+
return storageGetItem;
|
|
633
|
+
case JS_BRIDGE_EVENTS.SET_SESSION_STORAGE:
|
|
634
|
+
return storageSetItem;
|
|
635
|
+
case JS_BRIDGE_EVENTS.GET_LOCAL_STORAGE:
|
|
636
|
+
return localStorageGetItem;
|
|
637
|
+
case JS_BRIDGE_EVENTS.SET_LOCAL_STORAGE:
|
|
638
|
+
return localStorageSetItem;
|
|
639
|
+
case JS_BRIDGE_EVENTS.GET_SECURE_LOCAL_STORAGE:
|
|
640
|
+
return secureStorageGetItem;
|
|
641
|
+
case JS_BRIDGE_EVENTS.SET_SECURE_LOCAL_STORAGE:
|
|
642
|
+
return secureStorageSetItem;
|
|
643
|
+
|
|
644
|
+
case JS_BRIDGE_EVENTS.ADD_LOCAL_STORAGE_LISTENER:
|
|
645
|
+
return (payload, callbackKey) =>
|
|
646
|
+
addStorageListenerHandler("local", payload, callbackKey, webViewRef);
|
|
647
|
+
|
|
648
|
+
case JS_BRIDGE_EVENTS.REMOVE_LOCAL_STORAGE_LISTENER:
|
|
649
|
+
return (payload) => removeStorageListenerHandler(payload);
|
|
650
|
+
|
|
651
|
+
case JS_BRIDGE_EVENTS.ADD_SESSION_STORAGE_LISTENER:
|
|
652
|
+
return (payload, callbackKey) =>
|
|
653
|
+
addStorageListenerHandler("session", payload, callbackKey, webViewRef);
|
|
654
|
+
|
|
655
|
+
case JS_BRIDGE_EVENTS.REMOVE_SESSION_STORAGE_LISTENER:
|
|
656
|
+
return (payload) => removeStorageListenerHandler(payload);
|
|
657
|
+
|
|
658
|
+
default:
|
|
659
|
+
return async (_payload, callbackKey) => {
|
|
660
|
+
if (callbackKey && webViewRef?.current?.injectJavaScript) {
|
|
661
|
+
const errorResult = {
|
|
662
|
+
success: false,
|
|
663
|
+
error: `Unknown action type: ${type}`,
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const errorRun = invokeWebCallback(callbackKey, [errorResult]);
|
|
667
|
+
webViewRef.current.injectJavaScript(errorRun);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
return Promise.reject(new Error(`Unknown action type: ${type}`));
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { DeviceEventEmitter, NativeModules } from "react-native";
|
|
2
|
+
import { platformSelect } from "../reactUtils";
|
|
3
|
+
|
|
4
|
+
const { BackgroundTimer: RNBackgroundTimer } = NativeModules;
|
|
5
|
+
|
|
6
|
+
class BackgroundTimer {
|
|
7
|
+
private uniqueId: number;
|
|
8
|
+
private readonly callbacks: Record<number, () => void>;
|
|
9
|
+
|
|
10
|
+
constructor() {
|
|
11
|
+
this.uniqueId = 0;
|
|
12
|
+
this.callbacks = {};
|
|
13
|
+
|
|
14
|
+
const EventEmitter = platformSelect({
|
|
15
|
+
android: DeviceEventEmitter,
|
|
16
|
+
ios: undefined,
|
|
17
|
+
default: undefined,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
EventEmitter?.addListener("BackgroundTimer.timer.fired", (id: number) => {
|
|
21
|
+
if (this.callbacks[id]) {
|
|
22
|
+
const callback = this.callbacks[id];
|
|
23
|
+
delete this.callbacks[id];
|
|
24
|
+
callback();
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setTimeout(callback, timeout): number | NodeJS.Timeout {
|
|
30
|
+
if (!RNBackgroundTimer) {
|
|
31
|
+
return global.setTimeout(callback, timeout);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
this.uniqueId += 1;
|
|
35
|
+
const timeoutId = this.uniqueId;
|
|
36
|
+
|
|
37
|
+
this.callbacks[timeoutId] = callback;
|
|
38
|
+
RNBackgroundTimer.setTimeout(timeoutId, timeout);
|
|
39
|
+
|
|
40
|
+
return timeoutId;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
clearTimeout(timeoutId: number | NodeJS.Timeout): void {
|
|
44
|
+
if (!RNBackgroundTimer) {
|
|
45
|
+
return global.clearTimeout(timeoutId as NodeJS.Timeout);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (this.callbacks[timeoutId as number]) {
|
|
49
|
+
delete this.callbacks[timeoutId as number];
|
|
50
|
+
RNBackgroundTimer.clearTimeout(timeoutId);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export default new BackgroundTimer();
|
package/time/Timer.ts
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
+
import BackgroundTimer from "./BackgroundTimer";
|
|
2
|
+
|
|
1
3
|
export class Timer {
|
|
2
4
|
private startTime: number;
|
|
3
|
-
private
|
|
4
|
-
private timeoutId: NodeJS.Timeout | null;
|
|
5
|
+
private timeoutId: number | NodeJS.Timeout | null;
|
|
5
6
|
|
|
6
7
|
constructor(
|
|
7
8
|
private callback: () => void,
|
|
8
|
-
private
|
|
9
|
+
private remaining: number = null
|
|
9
10
|
) {
|
|
10
|
-
this.remaining = this.duration;
|
|
11
11
|
this.timeoutId = null;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
start(): void {
|
|
15
15
|
this.startTime = Date.now();
|
|
16
|
-
this.timeoutId = setTimeout(this.callback, this.remaining);
|
|
16
|
+
this.timeoutId = BackgroundTimer.setTimeout(this.callback, this.remaining);
|
|
17
17
|
|
|
18
18
|
this.remaining = 0;
|
|
19
19
|
}
|
|
@@ -29,6 +29,6 @@ export class Timer {
|
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
clear(): void {
|
|
32
|
-
clearTimeout(this.timeoutId);
|
|
32
|
+
BackgroundTimer.clearTimeout(this.timeoutId);
|
|
33
33
|
}
|
|
34
34
|
}
|