@applicaster/zapp-react-dom-app 16.0.0-alpha.9225550512 → 16.0.0-alpha.9235607812
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.
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { DISPLAY_STATES } from "@applicaster/zapp-react-native-ui-components/Contexts/DisplayStateContext";
|
|
2
|
+
import { confirmationDialogStore } from "@applicaster/zapp-react-native-ui-components/Contexts/ConfirmDialogState";
|
|
3
|
+
import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils";
|
|
4
|
+
import { showConfirmationDialog } from "@applicaster/zapp-react-native-utils/alertUtils";
|
|
5
|
+
|
|
6
|
+
import { InteractionManagerClass } from "..";
|
|
7
|
+
|
|
8
|
+
jest.mock("@applicaster/zapp-react-native-utils/alertUtils", () => ({
|
|
9
|
+
showConfirmationDialog: jest.fn(),
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
jest.mock(
|
|
13
|
+
"@applicaster/zapp-react-native-ui-components/Decorators/Navigator",
|
|
14
|
+
() => ({ withNavigator: (C) => C })
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
jest.mock("@applicaster/zapp-react-native-redux/utils/connectToStore", () => ({
|
|
18
|
+
connectToStore: () => (C) => C,
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
jest.mock("@applicaster/zapp-react-native-utils/logger", () => {
|
|
22
|
+
const actual = jest.requireActual(
|
|
23
|
+
"@applicaster/zapp-react-native-utils/logger"
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
return {
|
|
27
|
+
...actual,
|
|
28
|
+
getXray: () => ({ ...actual.getXray(), withXray: (C) => C }),
|
|
29
|
+
};
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const globalAny: any = global;
|
|
33
|
+
|
|
34
|
+
const createBackEvent = () => ({
|
|
35
|
+
code: "Escape",
|
|
36
|
+
preventDefault: jest.fn(),
|
|
37
|
+
stopPropagation: jest.fn(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const createManager = (props = {}) => {
|
|
41
|
+
const navigator = {
|
|
42
|
+
canGoBack: jest.fn(() => false),
|
|
43
|
+
goBack: jest.fn(),
|
|
44
|
+
replace: jest.fn(),
|
|
45
|
+
currentRoute: "/river/home",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const defaultProps = {
|
|
49
|
+
displayState: DISPLAY_STATES.DEFAULT,
|
|
50
|
+
setDisplayState: jest.fn(),
|
|
51
|
+
navigator,
|
|
52
|
+
rivers: {},
|
|
53
|
+
backToTopAction: jest.fn(() => "GO_BACK"),
|
|
54
|
+
emitFocusOnSelectedTopMenuItem: jest.fn(),
|
|
55
|
+
emitFocusOnSelectedTab: jest.fn(),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const mergedProps = { ...defaultProps, ...props };
|
|
59
|
+
|
|
60
|
+
return {
|
|
61
|
+
manager: new InteractionManagerClass(mergedProps as any),
|
|
62
|
+
props: mergedProps,
|
|
63
|
+
};
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
describe("InteractionManager (web)", () => {
|
|
67
|
+
beforeEach(() => {
|
|
68
|
+
jest.clearAllMocks();
|
|
69
|
+
(focusManager as any).setInitialFocus = jest.fn();
|
|
70
|
+
confirmationDialogStore.getState().hideDialog();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
afterEach(() => {
|
|
74
|
+
delete globalAny.webOS;
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("onBackPress with exit dialog visible", () => {
|
|
78
|
+
it.each([DISPLAY_STATES.DEFAULT, DISPLAY_STATES.PLAYER])(
|
|
79
|
+
"dismisses the dialog instead of navigating (displayState: %s)",
|
|
80
|
+
(displayState) => {
|
|
81
|
+
const { manager, props } = createManager({ displayState });
|
|
82
|
+
confirmationDialogStore.getState().showDialog();
|
|
83
|
+
|
|
84
|
+
const event = createBackEvent();
|
|
85
|
+
manager.onBackPress(event);
|
|
86
|
+
|
|
87
|
+
expect(confirmationDialogStore.getState().isDialogVisible).toBe(false);
|
|
88
|
+
expect(props.backToTopAction).not.toHaveBeenCalled();
|
|
89
|
+
expect(props.navigator.goBack).not.toHaveBeenCalled();
|
|
90
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
91
|
+
|
|
92
|
+
expect(focusManager.setInitialFocus).toHaveBeenCalledWith(undefined, {
|
|
93
|
+
source: "cancel",
|
|
94
|
+
preserveScroll: true,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
it("resolves the back action normally when the dialog is hidden", () => {
|
|
100
|
+
const { manager, props } = createManager();
|
|
101
|
+
|
|
102
|
+
manager.onBackPress(createBackEvent());
|
|
103
|
+
|
|
104
|
+
expect(props.backToTopAction).toHaveBeenCalled();
|
|
105
|
+
expect(props.navigator.canGoBack).toHaveBeenCalled();
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe("handleWebOsBack", () => {
|
|
110
|
+
it("uses the webOS platformBack API", () => {
|
|
111
|
+
const platformBack = jest.fn();
|
|
112
|
+
globalAny.webOS = { platformBack };
|
|
113
|
+
|
|
114
|
+
const { manager } = createManager();
|
|
115
|
+
manager.handleWebOsBack();
|
|
116
|
+
|
|
117
|
+
expect(platformBack).toHaveBeenCalledTimes(1);
|
|
118
|
+
expect(showConfirmationDialog).not.toHaveBeenCalled();
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("falls back to the custom exit dialog when platformBack is unavailable", () => {
|
|
122
|
+
globalAny.webOS = {};
|
|
123
|
+
|
|
124
|
+
const { manager } = createManager();
|
|
125
|
+
manager.handleWebOsBack();
|
|
126
|
+
|
|
127
|
+
expect(showConfirmationDialog).toHaveBeenCalledWith(
|
|
128
|
+
expect.objectContaining({ title: "" })
|
|
129
|
+
);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -89,7 +89,7 @@ type KeyCode = { code: string; keyCode: number | string };
|
|
|
89
89
|
const VIDEO_PLAYER_CONTROLS_NAVIGATION_MESSAGE =
|
|
90
90
|
"Video player controls - use left/right to navigate";
|
|
91
91
|
|
|
92
|
-
class InteractionManagerClass extends React.Component<Props, State> {
|
|
92
|
+
export class InteractionManagerClass extends React.Component<Props, State> {
|
|
93
93
|
onKeyDownListener: (keyCode: KeyCode) => void;
|
|
94
94
|
onMouseDownListener: (event: MouseEvent) => void;
|
|
95
95
|
onMouseMoveListener: (event: MouseEvent) => void;
|
|
@@ -204,7 +204,7 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
204
204
|
}
|
|
205
205
|
|
|
206
206
|
componentWillUnmount() {
|
|
207
|
-
window.removeEventListener("keydown", this.onKeyDownListener);
|
|
207
|
+
window.removeEventListener("keydown", this.onKeyDownListener, true);
|
|
208
208
|
window.removeEventListener("keyup", this.onKeyUp);
|
|
209
209
|
document.removeEventListener("wheel", this.onScrollListener);
|
|
210
210
|
|
|
@@ -253,7 +253,7 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
253
253
|
}
|
|
254
254
|
|
|
255
255
|
handleWebOsBack() {
|
|
256
|
-
const systemBack = globalAny?.webOS?.
|
|
256
|
+
const systemBack = globalAny?.webOS?.platformBack;
|
|
257
257
|
|
|
258
258
|
if (systemBack) {
|
|
259
259
|
systemBack();
|
|
@@ -420,9 +420,29 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
420
420
|
return;
|
|
421
421
|
}
|
|
422
422
|
|
|
423
|
+
// Focus is inside the dialog, so backToTopAction() can't resolve a
|
|
424
|
+
// meaningful action — Back must dismiss the dialog in every display state.
|
|
425
|
+
if (isDialogVisible) {
|
|
426
|
+
this.onConfirmDialogClose();
|
|
427
|
+
event.preventDefault();
|
|
428
|
+
event.stopPropagation();
|
|
429
|
+
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
423
433
|
const action = backToTopAction();
|
|
424
434
|
|
|
425
|
-
|
|
435
|
+
const effectiveDisplayState = this.getEffectiveDisplayState();
|
|
436
|
+
|
|
437
|
+
// TEMP(native-controls): remove after device verification
|
|
438
|
+
if (effectiveDisplayState !== displayState) {
|
|
439
|
+
log_debug("[NativeControls] Back handled as PLAYER", {
|
|
440
|
+
displayState,
|
|
441
|
+
action,
|
|
442
|
+
});
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
switch (effectiveDisplayState) {
|
|
426
446
|
case DISPLAY_STATES.DEFAULT:
|
|
427
447
|
if (action === "PLATFORM_TO_BACKGROUND") {
|
|
428
448
|
log_info(action);
|
|
@@ -471,9 +491,7 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
471
491
|
|
|
472
492
|
break;
|
|
473
493
|
case DISPLAY_STATES.PLAYER:
|
|
474
|
-
if (
|
|
475
|
-
this.onConfirmDialogClose();
|
|
476
|
-
} else if (navigator.canGoBack()) {
|
|
494
|
+
if (navigator.canGoBack()) {
|
|
477
495
|
navigator.goBack();
|
|
478
496
|
} else if (this.isHomeScreen()) {
|
|
479
497
|
this.platformToBackground();
|
|
@@ -522,8 +540,42 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
522
540
|
this.onBackPress(event);
|
|
523
541
|
}
|
|
524
542
|
|
|
543
|
+
// True only while the active (primary) player renders its own controls.
|
|
544
|
+
isNativePlayerControlsEnabled = () =>
|
|
545
|
+
!!playerManager.getInstanceController?.()?.supportsNativeControls?.();
|
|
546
|
+
|
|
547
|
+
// PLAYER/HUD are set by QuickBrick's TV transport controls, which are not
|
|
548
|
+
// mounted when the player renders native controls — the state would stay
|
|
549
|
+
// DEFAULT and keys would reach the screen behind the player (Enter re-opens it).
|
|
550
|
+
// The play-next overlay is the exception: it is QuickBrick UI, so the real
|
|
551
|
+
// state applies and Enter presses the focused overlay button.
|
|
552
|
+
getEffectiveDisplayState = () =>
|
|
553
|
+
this.isNativePlayerControlsEnabled() && !this.isPlayNextOverlayVisible()
|
|
554
|
+
? DISPLAY_STATES.PLAYER
|
|
555
|
+
: this.props.displayState;
|
|
556
|
+
|
|
557
|
+
// True when the player closed something of its own instead.
|
|
558
|
+
playerConsumesBack = () =>
|
|
559
|
+
!!playerManager.getInstanceController?.()?.handlesBackKey?.();
|
|
560
|
+
|
|
561
|
+
// While the play-next overlay is up it owns the remote: its buttons are
|
|
562
|
+
// QuickBrick focusables, so the keys must not go to the player's own controls.
|
|
563
|
+
isPlayNextOverlayVisible = () =>
|
|
564
|
+
!!this.props.navigator?.getPlayNextOverlay?.();
|
|
565
|
+
|
|
566
|
+
shouldDeferPlaybackNavigationToNativeControls = (event: KeyboardEvent) => {
|
|
567
|
+
return (
|
|
568
|
+
this.isNativePlayerControlsEnabled() &&
|
|
569
|
+
!this.isPlayNextOverlayVisible() &&
|
|
570
|
+
(keyCode(event).matchesAny(...ARROW_KEYS) ||
|
|
571
|
+
keyCode(event).matches(KEYS.Enter) ||
|
|
572
|
+
(__DEV__ && keyCode(event).matches(KEYS.X)))
|
|
573
|
+
);
|
|
574
|
+
};
|
|
575
|
+
|
|
525
576
|
onKeyDown(event) {
|
|
526
|
-
const {
|
|
577
|
+
const { setDisplayState, navigator, xray } = this.props;
|
|
578
|
+
const displayState = this.getEffectiveDisplayState();
|
|
527
579
|
const { isDialogVisible } = confirmationDialogStore.getState();
|
|
528
580
|
const { isKeyboardVisible } = this.state;
|
|
529
581
|
|
|
@@ -541,6 +593,12 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
541
593
|
}
|
|
542
594
|
|
|
543
595
|
if (keyCode(event).matchesAny(KEYS.Back, KEYS.Escape, KEYS.EscapeDevice)) {
|
|
596
|
+
// A player with native controls can have something of its own open that
|
|
597
|
+
// Back should close first (e.g. an ad click-through overlay).
|
|
598
|
+
if (this.isNativePlayerControlsEnabled() && this.playerConsumesBack()) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
544
602
|
this.onBackPress(event);
|
|
545
603
|
|
|
546
604
|
return;
|
|
@@ -553,6 +611,21 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
553
611
|
// Only handle repeat events for arrow keys
|
|
554
612
|
if (event.repeat && !keyCode(event).matchesAny(...ARROW_KEYS)) return;
|
|
555
613
|
|
|
614
|
+
if (
|
|
615
|
+
!isDialogVisible &&
|
|
616
|
+
this.shouldDeferPlaybackNavigationToNativeControls(event)
|
|
617
|
+
) {
|
|
618
|
+
// The player's own spatial navigation handles these keys.
|
|
619
|
+
// TEMP(native-controls): remove after device verification
|
|
620
|
+
log_debug("[NativeControls] key deferred to player", {
|
|
621
|
+
key: event.key,
|
|
622
|
+
displayState,
|
|
623
|
+
activeElement: document.activeElement?.className,
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
|
|
556
629
|
/**
|
|
557
630
|
When the player is displayed, we listen to Native Play / Pause / Stop
|
|
558
631
|
buttons. Any other key press will display the transport controls
|
|
@@ -65,6 +65,9 @@ export const PLATFORMS = {
|
|
|
65
65
|
mobile: "Mobile",
|
|
66
66
|
vizio: "VIZIO",
|
|
67
67
|
vidaa: "VIDAA",
|
|
68
|
+
// VIDAA U3 and earlier (2019 and earlier) carry no "VIDAA" token at all —
|
|
69
|
+
// "Hisense" is the only signal on those models. See isVidaaPlatform().
|
|
70
|
+
hisense: "Hisense",
|
|
68
71
|
smartcast: "SmartCast",
|
|
69
72
|
conjure: "Conjure",
|
|
70
73
|
};
|
|
@@ -75,14 +78,42 @@ export const PLATFORMS = {
|
|
|
75
78
|
|
|
76
79
|
/**
|
|
77
80
|
* Simple way of identifying if we have access to the tizen APIs
|
|
81
|
+
*
|
|
82
|
+
* NOTE: evaluated once, when this module is first imported. The native API
|
|
83
|
+
* scripts are injected asynchronously by loader.js, so this can be `false`
|
|
84
|
+
* simply because the bundle executed first. Prefer `hasTizenAPIs()` for
|
|
85
|
+
* runtime checks; this constant is retained for the DEFAULT values below,
|
|
86
|
+
* which are themselves import-time fallbacks.
|
|
78
87
|
*/
|
|
79
88
|
export const hasTizen = typeof window?.tizen !== "undefined";
|
|
80
89
|
|
|
81
90
|
/**
|
|
82
91
|
* Simple way of identifying if we have access to the webOS
|
|
92
|
+
*
|
|
93
|
+
* NOTE: same import-time caveat as `hasTizen` — prefer `hasWebOSAPIs()`.
|
|
83
94
|
*/
|
|
84
95
|
export const hasWebOS = typeof window?.webOS !== "undefined";
|
|
85
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Checks, at call time, whether the Tizen APIs are available.
|
|
99
|
+
*
|
|
100
|
+
* loader.js injects `$WEBAPIS/webapis/webapis.js` via a dynamically-created
|
|
101
|
+
* <script>, which is async by default, so `window.tizen` may not exist yet when
|
|
102
|
+
* this module is imported. Checking on each call avoids latching a stale
|
|
103
|
+
* `false` for the lifetime of the app.
|
|
104
|
+
*
|
|
105
|
+
* @returns {boolean} True if the Tizen APIs are available.
|
|
106
|
+
*/
|
|
107
|
+
export const hasTizenAPIs = () => typeof window?.tizen !== "undefined";
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Checks, at call time, whether the webOS APIs are available.
|
|
111
|
+
* See `hasTizenAPIs()` for why this is a function rather than a constant.
|
|
112
|
+
*
|
|
113
|
+
* @returns {boolean} True if the webOS APIs are available.
|
|
114
|
+
*/
|
|
115
|
+
export const hasWebOSAPIs = () => typeof window?.webOS !== "undefined";
|
|
116
|
+
|
|
86
117
|
/**
|
|
87
118
|
* Simple way of identifying if we are on a web based platform
|
|
88
119
|
*/
|
|
@@ -3,13 +3,7 @@
|
|
|
3
3
|
/* eslint-disable no-console */
|
|
4
4
|
|
|
5
5
|
import { Platform } from "react-native";
|
|
6
|
-
import {
|
|
7
|
-
DEFAULT,
|
|
8
|
-
desiredKeysMap,
|
|
9
|
-
hasTizen,
|
|
10
|
-
hasWebOS,
|
|
11
|
-
PLATFORMS,
|
|
12
|
-
} from "./const";
|
|
6
|
+
import { desiredKeysMap, hasTizenAPIs, hasWebOSAPIs, PLATFORMS } from "./const";
|
|
13
7
|
|
|
14
8
|
/**
|
|
15
9
|
* @typedef {"tv" | "web" | "other"} DeviceType
|
|
@@ -27,7 +21,7 @@ export const getUserAgent = () =>
|
|
|
27
21
|
* Checks if the device is HD (1280 width).
|
|
28
22
|
* @returns {boolean} True if the device is HD, false otherwise.
|
|
29
23
|
*/
|
|
30
|
-
export const isHD = () => window
|
|
24
|
+
export const isHD = () => window?.innerWidth === 1280;
|
|
31
25
|
|
|
32
26
|
/**
|
|
33
27
|
* Checks if the platform is LG.
|
|
@@ -40,7 +34,7 @@ export const isLgPlatform = () => {
|
|
|
40
34
|
return false;
|
|
41
35
|
}
|
|
42
36
|
|
|
43
|
-
return
|
|
37
|
+
return hasWebOSAPIs() || userAgent.includes(PLATFORMS.webos);
|
|
44
38
|
};
|
|
45
39
|
|
|
46
40
|
/**
|
|
@@ -55,7 +49,7 @@ export const isSamsungPlatform = () => {
|
|
|
55
49
|
}
|
|
56
50
|
|
|
57
51
|
return (
|
|
58
|
-
|
|
52
|
+
hasTizenAPIs() ||
|
|
59
53
|
userAgent.includes(PLATFORMS.samsung) ||
|
|
60
54
|
userAgent.includes(PLATFORMS.tizen)
|
|
61
55
|
);
|
|
@@ -68,6 +62,10 @@ export const isSamsungPlatform = () => {
|
|
|
68
62
|
export const isVizioPlatform = () => {
|
|
69
63
|
const userAgent = getUserAgent();
|
|
70
64
|
|
|
65
|
+
if (!userAgent) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
71
69
|
return (
|
|
72
70
|
userAgent.includes(PLATFORMS.vizio) ||
|
|
73
71
|
userAgent.includes(PLATFORMS.smartcast) ||
|
|
@@ -76,13 +74,37 @@ export const isVizioPlatform = () => {
|
|
|
76
74
|
};
|
|
77
75
|
|
|
78
76
|
/**
|
|
79
|
-
* Checks if the platform is
|
|
80
|
-
*
|
|
77
|
+
* Checks if the platform is VIDAA.
|
|
78
|
+
*
|
|
79
|
+
* Two tokens are needed because the userAgent format changed with VIDAA U4:
|
|
80
|
+
* - 2020 and later (U4/U5/U6/U7): always carries a "VIDAA" token, e.g.
|
|
81
|
+
* `Model/Hisense-MT9602 VIDAA/5.0(Hisense;SmartTV;50A53FSV;...)`
|
|
82
|
+
* - 2019 and earlier (U3 and prior): carries NO "VIDAA" token at all — the only
|
|
83
|
+
* signal is "Hisense", e.g. `Model/Hisense-MSD6586 (Hisense;HE50N3050UWTS;...)`
|
|
84
|
+
*
|
|
85
|
+
* Matching "VIDAA" alone silently misses every U3-and-earlier device, which the
|
|
86
|
+
* VIDAA JS SDK still supports (getDeviceID/getBrand/config are all U3U4U5+).
|
|
87
|
+
*
|
|
88
|
+
* Caveat: "Hisense" also appears in the userAgent of Hisense-branded Android TV
|
|
89
|
+
* models, which are not VIDAA. That is tolerable here because this is only
|
|
90
|
+
* consulted on a VIDAA-targeted build (see getDeviceType), but it is why the
|
|
91
|
+
* vendor docs warn against using "Hisense" as a general-purpose signal. A
|
|
92
|
+
* stricter check would test for the native `Hisense_GetDeviceID` global, which
|
|
93
|
+
* VIDAA exposes from U3 onwards and Android TV does not.
|
|
94
|
+
*
|
|
95
|
+
* @see https://partner-doc.vidaa.com/vdocs/development/open-capacity.html
|
|
96
|
+
* @returns {boolean} True if the platform is VIDAA, false otherwise.
|
|
81
97
|
*/
|
|
82
98
|
export const isVidaaPlatform = () => {
|
|
83
99
|
const userAgent = getUserAgent();
|
|
84
100
|
|
|
85
|
-
|
|
101
|
+
if (!userAgent) {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return (
|
|
106
|
+
userAgent.includes(PLATFORMS.vidaa) || userAgent.includes(PLATFORMS.hisense)
|
|
107
|
+
);
|
|
86
108
|
};
|
|
87
109
|
|
|
88
110
|
/**
|
|
@@ -91,11 +113,201 @@ export const isVidaaPlatform = () => {
|
|
|
91
113
|
*/
|
|
92
114
|
export const hasVizioAPIs = () => {
|
|
93
115
|
return (
|
|
94
|
-
typeof window
|
|
116
|
+
typeof window?.VIZIO !== "undefined" &&
|
|
95
117
|
window?.applicaster?.vizioLibraryDidLoad
|
|
96
118
|
);
|
|
97
119
|
};
|
|
98
120
|
|
|
121
|
+
/**
|
|
122
|
+
* Checks if the VIDAA JS SDK has finished loading.
|
|
123
|
+
* @returns {boolean} True if the `vidaatv` object is available.
|
|
124
|
+
*/
|
|
125
|
+
export const hasVidaaAPIs = () => typeof window?.vidaatv !== "undefined";
|
|
126
|
+
|
|
127
|
+
/** How long to wait for a native device API callback before giving up, in ms. */
|
|
128
|
+
const NATIVE_API_TIMEOUT_MS = 3000;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Wraps a promise so that it *always* settles, and always resolves.
|
|
132
|
+
*
|
|
133
|
+
* The native device APIs are callback-based and wrapped in promises that can
|
|
134
|
+
* fail in two ways this guards against:
|
|
135
|
+
*
|
|
136
|
+
* 1. **Rejection.** Device info and connection info are requested together with
|
|
137
|
+
* `Promise.all`, which rejects as a whole on the first failure — so a routine
|
|
138
|
+
* "no network" error from the connection lookup would discard the model and
|
|
139
|
+
* OS version that were retrieved successfully.
|
|
140
|
+
* 2. **Never settling.** `webOS.deviceInfo(cb)`, `tizen.systeminfo(cb)` and
|
|
141
|
+
* `VIZIO.getFirmwareVersion(cb)` resolve only from their callbacks. If a
|
|
142
|
+
* callback never fires the promise hangs forever, and because `getDeviceData`
|
|
143
|
+
* is awaited during startup, app boot hangs with it.
|
|
144
|
+
*
|
|
145
|
+
* @param {Promise} promise The promise to guard.
|
|
146
|
+
* @param {object} options
|
|
147
|
+
* @param {string} options.label Name used in warnings.
|
|
148
|
+
* @param {number} [options.timeout=NATIVE_API_TIMEOUT_MS] Milliseconds to wait.
|
|
149
|
+
* @param {*} [options.fallback=null] Value to resolve with on failure/timeout.
|
|
150
|
+
* @returns {Promise<*>} Resolves with the value, or `fallback`.
|
|
151
|
+
*/
|
|
152
|
+
const settleWithin = (
|
|
153
|
+
promise,
|
|
154
|
+
{ label, timeout = NATIVE_API_TIMEOUT_MS, fallback = null } = {}
|
|
155
|
+
) => {
|
|
156
|
+
return new Promise((resolve) => {
|
|
157
|
+
let settled = false;
|
|
158
|
+
|
|
159
|
+
const finish = (value) => {
|
|
160
|
+
if (settled) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
settled = true;
|
|
165
|
+
clearTimeout(timeoutId);
|
|
166
|
+
resolve(value);
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const timeoutId = setTimeout(() => {
|
|
170
|
+
console.warn(`${label}: timed out after ${timeout}ms`);
|
|
171
|
+
finish(fallback);
|
|
172
|
+
}, timeout);
|
|
173
|
+
|
|
174
|
+
Promise.resolve(promise)
|
|
175
|
+
.then(finish)
|
|
176
|
+
.catch((error) => {
|
|
177
|
+
console.warn(`${label}: failed`, error);
|
|
178
|
+
finish(fallback);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** How long to wait for the VIDAA SDK before giving up, in ms. */
|
|
184
|
+
const VIDAA_SDK_TIMEOUT_MS = 3000;
|
|
185
|
+
|
|
186
|
+
/** How often to re-check for the VIDAA SDK while waiting, in ms. */
|
|
187
|
+
const VIDAA_SDK_POLL_MS = 50;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The value the VIDAA SDK returns for APIs the current platform version does
|
|
191
|
+
* not implement, per the vendor docs ("For features Not Supported by the
|
|
192
|
+
* platform, the interface returns `Not Supported`"). Treated as absent.
|
|
193
|
+
*/
|
|
194
|
+
const VIDAA_NOT_SUPPORTED = "Not Supported";
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Waits for the VIDAA JS SDK to become available on `window`.
|
|
198
|
+
*
|
|
199
|
+
* The SDK is injected by loader.js as a dynamically-created <script>, which is
|
|
200
|
+
* async by default and fetched from the VIDAA CDN. The app bundle is served
|
|
201
|
+
* locally and can therefore boot before that request completes, so reading
|
|
202
|
+
* `window.vidaatv` directly at startup is a race. This resolves as soon as the
|
|
203
|
+
* SDK appears, or with `null` once the timeout elapses.
|
|
204
|
+
*
|
|
205
|
+
* Resolves rather than rejects on timeout so a missing SDK degrades to partial
|
|
206
|
+
* device data instead of failing the whole `getDeviceData` call.
|
|
207
|
+
*
|
|
208
|
+
* @param {number} [timeout=VIDAA_SDK_TIMEOUT_MS] Milliseconds to wait.
|
|
209
|
+
* @returns {Promise<object|null>} The `vidaatv` object, or null if it never arrived.
|
|
210
|
+
*/
|
|
211
|
+
export const waitForVidaaAPIs = (timeout = VIDAA_SDK_TIMEOUT_MS) => {
|
|
212
|
+
return new Promise((resolve) => {
|
|
213
|
+
if (hasVidaaAPIs()) {
|
|
214
|
+
resolve(window.vidaatv);
|
|
215
|
+
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const startedAt = Date.now();
|
|
220
|
+
|
|
221
|
+
const intervalId = setInterval(() => {
|
|
222
|
+
if (hasVidaaAPIs()) {
|
|
223
|
+
clearInterval(intervalId);
|
|
224
|
+
resolve(window.vidaatv);
|
|
225
|
+
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (Date.now() - startedAt >= timeout) {
|
|
230
|
+
clearInterval(intervalId);
|
|
231
|
+
|
|
232
|
+
console.warn(
|
|
233
|
+
`waitForVidaaAPIs: VIDAA SDK did not load within ${timeout}ms`
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
resolve(null);
|
|
237
|
+
}
|
|
238
|
+
}, VIDAA_SDK_POLL_MS);
|
|
239
|
+
});
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Reads a single value from the VIDAA SDK, tolerating APIs that the current
|
|
244
|
+
* platform version does not implement.
|
|
245
|
+
*
|
|
246
|
+
* Every accessor is optional: older platforms omit the method entirely, and
|
|
247
|
+
* supported-but-unavailable values come back as the string "Not Supported".
|
|
248
|
+
*
|
|
249
|
+
* @param {object} sdk The `vidaatv` object.
|
|
250
|
+
* @param {string} method The accessor name, e.g. "getModelName".
|
|
251
|
+
* @returns {string|null} The value, or null when unavailable.
|
|
252
|
+
*/
|
|
253
|
+
const readVidaaValue = (sdk, method) => {
|
|
254
|
+
try {
|
|
255
|
+
if (typeof sdk?.[method] !== "function") {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const value = sdk[method]();
|
|
260
|
+
|
|
261
|
+
return value && value !== VIDAA_NOT_SUPPORTED ? value : null;
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.warn(`readVidaaValue: ${method} threw`, error);
|
|
264
|
+
|
|
265
|
+
return null;
|
|
266
|
+
}
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Gets device info from the VIDAA JS SDK.
|
|
271
|
+
*
|
|
272
|
+
* Field availability varies by VIDAA version, so each value is read
|
|
273
|
+
* defensively and omitted when unavailable:
|
|
274
|
+
* - getBrand / getModelName / getFirmWareVersion / getCountryCode are U3+
|
|
275
|
+
* - getOSVersion / getNetType are U4+, hence the firmware fallback for
|
|
276
|
+
* osVersion so pre-2020 models still report something
|
|
277
|
+
*
|
|
278
|
+
* @returns {Promise<object|null>} Device info, or null if the SDK never loaded.
|
|
279
|
+
*/
|
|
280
|
+
export const getVidaaInfo = async () => {
|
|
281
|
+
const sdk = await waitForVidaaAPIs();
|
|
282
|
+
|
|
283
|
+
if (!sdk) {
|
|
284
|
+
return null;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const modelName = readVidaaValue(sdk, "getModelName");
|
|
288
|
+
const brand = readVidaaValue(sdk, "getBrand");
|
|
289
|
+
const networkType = readVidaaValue(sdk, "getNetType");
|
|
290
|
+
const countryCode = readVidaaValue(sdk, "getCountryCode");
|
|
291
|
+
|
|
292
|
+
// getOSVersion is U4+; fall back to the firmware version, which is U3+.
|
|
293
|
+
const osVersion =
|
|
294
|
+
readVidaaValue(sdk, "getOSVersion") ||
|
|
295
|
+
readVidaaValue(sdk, "getFirmWareVersion");
|
|
296
|
+
|
|
297
|
+
// Human-readable name for the `deviceName` context key, e.g.
|
|
298
|
+
// "Hisense 50A53FSV". Falls back to whichever half is available.
|
|
299
|
+
const deviceName = [brand, modelName].filter(Boolean).join(" ");
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
...(modelName ? { modelName } : {}),
|
|
303
|
+
...(brand ? { deviceMake: brand } : {}),
|
|
304
|
+
...(deviceName ? { deviceName } : {}),
|
|
305
|
+
...(osVersion ? { osVersion } : {}),
|
|
306
|
+
...(networkType ? { networkType } : {}),
|
|
307
|
+
...(countryCode ? { countryCode } : {}),
|
|
308
|
+
};
|
|
309
|
+
};
|
|
310
|
+
|
|
99
311
|
/**
|
|
100
312
|
* Determines the device type based on the platform.
|
|
101
313
|
* @returns {DeviceType} The device type ("tv", "web", or "other").
|
|
@@ -165,7 +377,7 @@ export const getWebOSConnectionInfo = () => {
|
|
|
165
377
|
*/
|
|
166
378
|
export const getTizenConnectionInfo = () => {
|
|
167
379
|
return new Promise((resolve, reject) => {
|
|
168
|
-
if (!isSamsungPlatform) {
|
|
380
|
+
if (!isSamsungPlatform()) {
|
|
169
381
|
resolve(null);
|
|
170
382
|
|
|
171
383
|
return;
|
|
@@ -189,6 +401,36 @@ export const getTizenConnectionInfo = () => {
|
|
|
189
401
|
});
|
|
190
402
|
};
|
|
191
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Retrieves the firmware version from the Vizio companion library.
|
|
406
|
+
*
|
|
407
|
+
* `VIZIO.getFirmwareVersion` is callback-based. It was previously called
|
|
408
|
+
* without being awaited, assigning onto `deviceData` after `getDeviceData` had
|
|
409
|
+
* already returned — by which point the caller had spread the object into
|
|
410
|
+
* session storage, so the value was silently dropped whenever the callback did
|
|
411
|
+
* not fire synchronously. Wrapping it in a promise lets it be awaited like
|
|
412
|
+
* every other platform's device info.
|
|
413
|
+
*
|
|
414
|
+
* @returns {Promise<string|null>} The firmware version, or null if unavailable.
|
|
415
|
+
*/
|
|
416
|
+
export const getVizioFirmwareVersion = () => {
|
|
417
|
+
return new Promise((resolve, reject) => {
|
|
418
|
+
if (!hasVizioAPIs()) {
|
|
419
|
+
resolve(null);
|
|
420
|
+
|
|
421
|
+
return;
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
try {
|
|
425
|
+
window.VIZIO.getFirmwareVersion((firmwareVersion) => {
|
|
426
|
+
resolve(firmwareVersion);
|
|
427
|
+
});
|
|
428
|
+
} catch (error) {
|
|
429
|
+
reject(error);
|
|
430
|
+
}
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
|
|
192
434
|
/**
|
|
193
435
|
* Gets all of the webOS device info available to us via webOS.deviceInfo
|
|
194
436
|
*/
|
|
@@ -204,7 +446,12 @@ export const getWebOSInfo = () => {
|
|
|
204
446
|
if (info) {
|
|
205
447
|
resolve({
|
|
206
448
|
...info,
|
|
207
|
-
|
|
449
|
+
// `deviceName` is a persisted context key; the previous `name` was
|
|
450
|
+
// absent from desiredKeysMap, so it was computed and dropped. Format
|
|
451
|
+
// is "<make> <model>", matching Samsung/Vizio/VIDAA. The old string
|
|
452
|
+
// was `${info.modelName} - ${info.version}`, which omitted the make
|
|
453
|
+
// and embedded a version already reported as osVersion.
|
|
454
|
+
deviceName: `LG ${info.modelName}`,
|
|
208
455
|
osVersion: info.sdkVersion, // info.sdkVersion is the OS version, info.version is software version
|
|
209
456
|
});
|
|
210
457
|
} else {
|
|
@@ -231,16 +478,22 @@ export const getTizenInfo = () => {
|
|
|
231
478
|
"http://tizen.org/system/model_name"
|
|
232
479
|
);
|
|
233
480
|
|
|
234
|
-
|
|
481
|
+
// Previously `${DEFAULT.make} ${modelName}` — DEFAULT has no `make` key
|
|
482
|
+
// (it is `deviceMake`), so this interpolated the literal string
|
|
483
|
+
// "undefined". It went unnoticed because the value was named `name`,
|
|
484
|
+
// which is absent from desiredKeysMap and therefore never persisted.
|
|
485
|
+
const deviceName = `Samsung ${modelName}`;
|
|
235
486
|
|
|
236
487
|
const osVersion = tizen.systeminfo.getCapability(
|
|
237
488
|
"http://tizen.org/feature/platform.version"
|
|
238
489
|
);
|
|
239
490
|
|
|
240
|
-
|
|
491
|
+
// `deviceName` is derived from modelName, so checking it here would be
|
|
492
|
+
// redundant — it is truthy whenever modelName is.
|
|
493
|
+
if (modelName && osVersion) {
|
|
241
494
|
resolve({
|
|
242
495
|
modelName,
|
|
243
|
-
|
|
496
|
+
deviceName,
|
|
244
497
|
osVersion,
|
|
245
498
|
});
|
|
246
499
|
} else {
|
|
@@ -279,9 +532,13 @@ export const getDeviceData = async () => {
|
|
|
279
532
|
};
|
|
280
533
|
|
|
281
534
|
if (isLgPlatform()) {
|
|
535
|
+
// Guarded individually so a failure in one lookup does not discard the
|
|
536
|
+
// other — see settleWithin.
|
|
282
537
|
const [webOSInfo, webOSConnectionInfo] = await Promise.all([
|
|
283
|
-
getWebOSInfo(),
|
|
284
|
-
getWebOSConnectionInfo(),
|
|
538
|
+
settleWithin(getWebOSInfo(), { label: "getWebOSInfo" }),
|
|
539
|
+
settleWithin(getWebOSConnectionInfo(), {
|
|
540
|
+
label: "getWebOSConnectionInfo",
|
|
541
|
+
}),
|
|
285
542
|
]);
|
|
286
543
|
|
|
287
544
|
deviceData = {
|
|
@@ -294,8 +551,10 @@ export const getDeviceData = async () => {
|
|
|
294
551
|
};
|
|
295
552
|
} else if (isSamsungPlatform()) {
|
|
296
553
|
const [tizenInfo, tizenConnectionInfo] = await Promise.all([
|
|
297
|
-
getTizenInfo(),
|
|
298
|
-
getTizenConnectionInfo(),
|
|
554
|
+
settleWithin(getTizenInfo(), { label: "getTizenInfo" }),
|
|
555
|
+
settleWithin(getTizenConnectionInfo(), {
|
|
556
|
+
label: "getTizenConnectionInfo",
|
|
557
|
+
}),
|
|
299
558
|
]);
|
|
300
559
|
|
|
301
560
|
deviceData = {
|
|
@@ -311,17 +570,39 @@ export const getDeviceData = async () => {
|
|
|
311
570
|
...deviceData,
|
|
312
571
|
platform: "vizio",
|
|
313
572
|
deviceMake: "Vizio",
|
|
314
|
-
deviceType:
|
|
573
|
+
deviceType: getDeviceType(),
|
|
315
574
|
...deviceDimensions,
|
|
316
575
|
};
|
|
317
576
|
|
|
318
577
|
if (hasVizioAPIs()) {
|
|
319
|
-
|
|
320
|
-
|
|
578
|
+
const firmwareVersion = await settleWithin(getVizioFirmwareVersion(), {
|
|
579
|
+
label: "getVizioFirmwareVersion",
|
|
321
580
|
});
|
|
322
581
|
|
|
582
|
+
if (firmwareVersion) {
|
|
583
|
+
deviceData.osVersion = firmwareVersion;
|
|
584
|
+
}
|
|
585
|
+
|
|
323
586
|
deviceData.deviceModel = window.VIZIO.deviceModel;
|
|
587
|
+
|
|
588
|
+
if (deviceData.deviceModel) {
|
|
589
|
+
deviceData.deviceName = `Vizio ${deviceData.deviceModel}`;
|
|
590
|
+
}
|
|
324
591
|
}
|
|
592
|
+
} else if (isVidaaPlatform()) {
|
|
593
|
+
// Waits for the VIDAA SDK, which loader.js loads asynchronously from the
|
|
594
|
+
// VIDAA CDN. Resolves to null on timeout, leaving the static fields below.
|
|
595
|
+
const vidaaInfo = await getVidaaInfo();
|
|
596
|
+
|
|
597
|
+
deviceData = {
|
|
598
|
+
...vidaaInfo,
|
|
599
|
+
platform: "vidaa",
|
|
600
|
+
// Brand is read from the SDK rather than hardcoded: VIDAA ships on
|
|
601
|
+
// OEM sets beyond Hisense, so getBrand() is the accurate source.
|
|
602
|
+
deviceMake: vidaaInfo?.deviceMake || "Hisense",
|
|
603
|
+
deviceType: getDeviceType(),
|
|
604
|
+
...deviceDimensions,
|
|
605
|
+
};
|
|
325
606
|
}
|
|
326
607
|
|
|
327
608
|
return deviceData;
|
|
@@ -332,6 +613,10 @@ export const getDeviceData = async () => {
|
|
|
332
613
|
error
|
|
333
614
|
);
|
|
334
615
|
|
|
335
|
-
|
|
616
|
+
// Returns an empty object rather than null: the caller
|
|
617
|
+
// (loadSessionStorageData) does `delete deviceData.sdkVersion` on the
|
|
618
|
+
// result, which throws a TypeError on null and turns a handled error into
|
|
619
|
+
// an unhandled one during startup.
|
|
620
|
+
return {};
|
|
336
621
|
}
|
|
337
622
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-dom-app",
|
|
3
|
-
"version": "16.0.0-alpha.
|
|
3
|
+
"version": "16.0.0-alpha.9235607812",
|
|
4
4
|
"description": "Zapp App Component for Applicaster's Quick Brick React Native App",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,11 +22,11 @@
|
|
|
22
22
|
},
|
|
23
23
|
"homepage": "https://github.com/applicaster/zapp-react-dom-app#readme",
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@applicaster/zapp-react-dom-ui-components": "16.0.0-alpha.
|
|
26
|
-
"@applicaster/zapp-react-native-bridge": "16.0.0-alpha.
|
|
27
|
-
"@applicaster/zapp-react-native-redux": "16.0.0-alpha.
|
|
28
|
-
"@applicaster/zapp-react-native-ui-components": "16.0.0-alpha.
|
|
29
|
-
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.
|
|
25
|
+
"@applicaster/zapp-react-dom-ui-components": "16.0.0-alpha.9235607812",
|
|
26
|
+
"@applicaster/zapp-react-native-bridge": "16.0.0-alpha.9235607812",
|
|
27
|
+
"@applicaster/zapp-react-native-redux": "16.0.0-alpha.9235607812",
|
|
28
|
+
"@applicaster/zapp-react-native-ui-components": "16.0.0-alpha.9235607812",
|
|
29
|
+
"@applicaster/zapp-react-native-utils": "16.0.0-alpha.9235607812",
|
|
30
30
|
"abortcontroller-polyfill": "^1.7.5",
|
|
31
31
|
"typeface-montserrat": "^0.0.54",
|
|
32
32
|
"video.js": "7.14.3",
|