@applicaster/zapp-react-dom-app 16.0.0-rc.99 → 17.0.0-rc.1
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/App/InteractionManager/__tests__/InteractionManager.test.tsx +319 -0
- package/App/InteractionManager/index.tsx +51 -29
- package/Polyfills/AppLoaderBridge/__tests__/AppLoaderBridge.test.ts +221 -0
- package/Polyfills/AppLoaderBridge/index.ts +91 -42
- package/Polyfills/AppLoaderBridge/utils/utils.ts +57 -36
- package/Polyfills/QuickBrickCommunicationModule/index.js +1 -1
- package/package.json +6 -6
|
@@ -0,0 +1,319 @@
|
|
|
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 {
|
|
4
|
+
focusManager,
|
|
5
|
+
KEYS,
|
|
6
|
+
playerManager,
|
|
7
|
+
} from "@applicaster/zapp-react-native-utils/appUtils";
|
|
8
|
+
import { KeyInputHandler } from "@applicaster/zapp-react-native-utils/appUtils/keyInputHandler/KeyInputHandler";
|
|
9
|
+
import { TVSeekController } from "@applicaster/zapp-react-native-utils/reactHooks/player/TVSeekControlller/TVSeekController";
|
|
10
|
+
import { SKIP_TIME_BASE } from "@applicaster/zapp-react-native-utils/reactHooks/player/const";
|
|
11
|
+
import { showConfirmationDialog } from "@applicaster/zapp-react-native-utils/alertUtils";
|
|
12
|
+
|
|
13
|
+
import { InteractionManagerClass } from "..";
|
|
14
|
+
|
|
15
|
+
// The web app runs with the web key map
|
|
16
|
+
jest.mock(
|
|
17
|
+
"@applicaster/zapp-react-native-utils/appUtils/keyCodes/keys/keys",
|
|
18
|
+
() =>
|
|
19
|
+
jest.requireActual(
|
|
20
|
+
"@applicaster/zapp-react-native-utils/appUtils/keyCodes/keys/keys.web"
|
|
21
|
+
)
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
jest.mock("@applicaster/zapp-react-native-utils/alertUtils", () => ({
|
|
25
|
+
showConfirmationDialog: jest.fn(),
|
|
26
|
+
}));
|
|
27
|
+
|
|
28
|
+
jest.mock(
|
|
29
|
+
"@applicaster/zapp-react-native-ui-components/Decorators/Navigator",
|
|
30
|
+
() => ({ withNavigator: (C) => C })
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
jest.mock("@applicaster/zapp-react-native-redux/utils/connectToStore", () => ({
|
|
34
|
+
connectToStore: () => (C) => C,
|
|
35
|
+
}));
|
|
36
|
+
|
|
37
|
+
jest.mock("@applicaster/zapp-react-native-utils/logger", () => {
|
|
38
|
+
const actual = jest.requireActual(
|
|
39
|
+
"@applicaster/zapp-react-native-utils/logger"
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
...actual,
|
|
44
|
+
getXray: () => ({ ...actual.getXray(), withXray: (C) => C }),
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const globalAny: any = global;
|
|
49
|
+
|
|
50
|
+
const createBackEvent = () => ({
|
|
51
|
+
code: "Escape",
|
|
52
|
+
preventDefault: jest.fn(),
|
|
53
|
+
stopPropagation: jest.fn(),
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
const createManager = (props = {}) => {
|
|
57
|
+
const navigator = {
|
|
58
|
+
canGoBack: jest.fn(() => false),
|
|
59
|
+
goBack: jest.fn(),
|
|
60
|
+
replace: jest.fn(),
|
|
61
|
+
currentRoute: "/river/home",
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const defaultProps = {
|
|
65
|
+
displayState: DISPLAY_STATES.DEFAULT,
|
|
66
|
+
setDisplayState: jest.fn(),
|
|
67
|
+
navigator,
|
|
68
|
+
rivers: {},
|
|
69
|
+
backToTopAction: jest.fn(() => "GO_BACK"),
|
|
70
|
+
emitFocusOnSelectedTopMenuItem: jest.fn(),
|
|
71
|
+
emitFocusOnSelectedTab: jest.fn(),
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
const mergedProps = { ...defaultProps, ...props };
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
manager: new InteractionManagerClass(mergedProps as any),
|
|
78
|
+
props: mergedProps,
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
describe("InteractionManager (web)", () => {
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
jest.clearAllMocks();
|
|
85
|
+
(focusManager as any).setInitialFocus = jest.fn();
|
|
86
|
+
confirmationDialogStore.getState().hideDialog();
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
afterEach(() => {
|
|
90
|
+
delete globalAny.webOS;
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("onBackPress with exit dialog visible", () => {
|
|
94
|
+
it.each([DISPLAY_STATES.DEFAULT, DISPLAY_STATES.PLAYER])(
|
|
95
|
+
"dismisses the dialog instead of navigating (displayState: %s)",
|
|
96
|
+
(displayState) => {
|
|
97
|
+
const { manager, props } = createManager({ displayState });
|
|
98
|
+
confirmationDialogStore.getState().showDialog();
|
|
99
|
+
|
|
100
|
+
const event = createBackEvent();
|
|
101
|
+
manager.onBackPress(event);
|
|
102
|
+
|
|
103
|
+
expect(confirmationDialogStore.getState().isDialogVisible).toBe(false);
|
|
104
|
+
expect(props.backToTopAction).not.toHaveBeenCalled();
|
|
105
|
+
expect(props.navigator.goBack).not.toHaveBeenCalled();
|
|
106
|
+
expect(event.preventDefault).toHaveBeenCalled();
|
|
107
|
+
|
|
108
|
+
expect(focusManager.setInitialFocus).toHaveBeenCalledWith(undefined, {
|
|
109
|
+
source: "cancel",
|
|
110
|
+
preserveScroll: true,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
);
|
|
114
|
+
|
|
115
|
+
it("resolves the back action normally when the dialog is hidden", () => {
|
|
116
|
+
const { manager, props } = createManager();
|
|
117
|
+
|
|
118
|
+
manager.onBackPress(createBackEvent());
|
|
119
|
+
|
|
120
|
+
expect(props.backToTopAction).toHaveBeenCalled();
|
|
121
|
+
expect(props.navigator.canGoBack).toHaveBeenCalled();
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
describe("handleWebOsBack", () => {
|
|
126
|
+
it("uses the webOS platformBack API", () => {
|
|
127
|
+
const platformBack = jest.fn();
|
|
128
|
+
globalAny.webOS = { platformBack };
|
|
129
|
+
|
|
130
|
+
const { manager } = createManager();
|
|
131
|
+
manager.handleWebOsBack();
|
|
132
|
+
|
|
133
|
+
expect(platformBack).toHaveBeenCalledTimes(1);
|
|
134
|
+
expect(showConfirmationDialog).not.toHaveBeenCalled();
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("falls back to the custom exit dialog when platformBack is unavailable", () => {
|
|
138
|
+
globalAny.webOS = {};
|
|
139
|
+
|
|
140
|
+
const { manager } = createManager();
|
|
141
|
+
manager.handleWebOsBack();
|
|
142
|
+
|
|
143
|
+
expect(showConfirmationDialog).toHaveBeenCalledWith(
|
|
144
|
+
expect.objectContaining({ title: "" })
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
describe("Back at Home on VIDAA", () => {
|
|
150
|
+
it("shows the exit dialog and closes the window on confirm", () => {
|
|
151
|
+
const navigator = window.navigator;
|
|
152
|
+
const close = window.close;
|
|
153
|
+
window.close = jest.fn();
|
|
154
|
+
|
|
155
|
+
(window as any).navigator = {
|
|
156
|
+
userAgent: "Mozilla/5.0 Model/Hisense-MT9602 VIDAA/5.0",
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const { manager } = createManager({
|
|
160
|
+
backToTopAction: jest.fn(() => "PLATFORM_TO_BACKGROUND"),
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
manager.onBackPress(createBackEvent());
|
|
164
|
+
|
|
165
|
+
expect(showConfirmationDialog).toHaveBeenCalledTimes(1);
|
|
166
|
+
|
|
167
|
+
(
|
|
168
|
+
showConfirmationDialog as jest.Mock
|
|
169
|
+
).mock.calls[0][0].confirmCompletion();
|
|
170
|
+
|
|
171
|
+
expect(window.close).toHaveBeenCalledTimes(1);
|
|
172
|
+
|
|
173
|
+
(window as any).navigator = navigator;
|
|
174
|
+
window.close = close;
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
describe("Forward / Rewind keys", () => {
|
|
179
|
+
const POSITION = 100;
|
|
180
|
+
|
|
181
|
+
const createPlayer = (supportsNativeControls: boolean) => ({
|
|
182
|
+
supportsNativeControls: () => supportsNativeControls,
|
|
183
|
+
isPaused: () => false,
|
|
184
|
+
isLive: () => false,
|
|
185
|
+
getPosition: () => POSITION,
|
|
186
|
+
getSeekableDuration: () => 1000,
|
|
187
|
+
pause: jest.fn(),
|
|
188
|
+
play: jest.fn(),
|
|
189
|
+
seekTo: jest.fn(),
|
|
190
|
+
forward: jest.fn(),
|
|
191
|
+
rewind: jest.fn(),
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
let removeListener = () => {};
|
|
195
|
+
|
|
196
|
+
// Same wiring as usePlayerControllerSetup
|
|
197
|
+
const setup = ({ supportsNativeControls, ...props }: any) => {
|
|
198
|
+
const player = createPlayer(supportsNativeControls);
|
|
199
|
+
|
|
200
|
+
jest
|
|
201
|
+
.spyOn(playerManager, "getInstanceController")
|
|
202
|
+
.mockReturnValue(player as any);
|
|
203
|
+
|
|
204
|
+
removeListener = KeyInputHandler.getInstance().addListener(
|
|
205
|
+
new TVSeekController(player as any)
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
return { player, ...createManager(props) };
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
const press = (manager, key) => {
|
|
212
|
+
manager.onKeyDown(KEYS[key]);
|
|
213
|
+
manager.onKeyUp(KEYS[key]);
|
|
214
|
+
jest.runAllTimers();
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
beforeEach(() => jest.useFakeTimers());
|
|
218
|
+
|
|
219
|
+
afterEach(() => {
|
|
220
|
+
removeListener();
|
|
221
|
+
jest.useRealTimers();
|
|
222
|
+
jest.restoreAllMocks();
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
describe("with a player rendering its own controls", () => {
|
|
226
|
+
const expectUntouched = (player, props) => {
|
|
227
|
+
expect(player.seekTo).not.toHaveBeenCalled();
|
|
228
|
+
expect(player.forward).not.toHaveBeenCalled();
|
|
229
|
+
expect(player.rewind).not.toHaveBeenCalled();
|
|
230
|
+
expect(player.pause).not.toHaveBeenCalled();
|
|
231
|
+
expect(props.setDisplayState).not.toHaveBeenCalled();
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
it.each(["Forward", "Rewind"])("%s is left to the player", (key) => {
|
|
235
|
+
const { player, manager, props } = setup({
|
|
236
|
+
supportsNativeControls: true,
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
const event = { ...KEYS[key], stopPropagation: jest.fn() };
|
|
240
|
+
|
|
241
|
+
manager.onKeyDown(event);
|
|
242
|
+
manager.onKeyUp(event);
|
|
243
|
+
jest.runAllTimers();
|
|
244
|
+
|
|
245
|
+
expectUntouched(player, props);
|
|
246
|
+
// the key has to reach the player's own handler
|
|
247
|
+
expect(event.stopPropagation).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("does not seek the player while the play-next overlay is visible", () => {
|
|
251
|
+
const { player, manager } = setup({ supportsNativeControls: true });
|
|
252
|
+
|
|
253
|
+
(manager.props.navigator as any).getPlayNextOverlay = () => ({});
|
|
254
|
+
|
|
255
|
+
press(manager, "Forward");
|
|
256
|
+
|
|
257
|
+
expect(player.seekTo).not.toHaveBeenCalled();
|
|
258
|
+
expect(player.forward).not.toHaveBeenCalled();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it("keeps the keys while a dialog is visible", () => {
|
|
262
|
+
const { player, manager } = setup({ supportsNativeControls: true });
|
|
263
|
+
|
|
264
|
+
confirmationDialogStore.getState().showDialog();
|
|
265
|
+
|
|
266
|
+
press(manager, "Forward");
|
|
267
|
+
|
|
268
|
+
expect(player.seekTo).toHaveBeenCalledWith(POSITION + SKIP_TIME_BASE);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("sends the key-up to the owner of the key-down when a dialog closes in between", () => {
|
|
272
|
+
const { player, manager } = setup({ supportsNativeControls: true });
|
|
273
|
+
|
|
274
|
+
confirmationDialogStore.getState().showDialog();
|
|
275
|
+
manager.onKeyDown(KEYS.Forward);
|
|
276
|
+
confirmationDialogStore.getState().hideDialog();
|
|
277
|
+
manager.onKeyUp(KEYS.Forward);
|
|
278
|
+
jest.runAllTimers();
|
|
279
|
+
|
|
280
|
+
// a single tap seek, not a long press left running
|
|
281
|
+
expect(player.seekTo).toHaveBeenCalledTimes(1);
|
|
282
|
+
expect(player.seekTo).toHaveBeenCalledWith(POSITION + SKIP_TIME_BASE);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it("leaves the key-up to the player when a dialog opens after a deferred key-down", () => {
|
|
286
|
+
const { player, manager, props } = setup({
|
|
287
|
+
supportsNativeControls: true,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
manager.onKeyDown(KEYS.Forward);
|
|
291
|
+
confirmationDialogStore.getState().showDialog();
|
|
292
|
+
manager.onKeyUp(KEYS.Forward);
|
|
293
|
+
jest.runAllTimers();
|
|
294
|
+
|
|
295
|
+
expectUntouched(player, props);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe("with QuickBrick's transport controls", () => {
|
|
300
|
+
it.each([
|
|
301
|
+
["Forward", POSITION + SKIP_TIME_BASE],
|
|
302
|
+
["Rewind", POSITION - SKIP_TIME_BASE],
|
|
303
|
+
])("%s keeps the delayed seek", (key, expectedPosition) => {
|
|
304
|
+
const { player, manager, props } = setup({
|
|
305
|
+
supportsNativeControls: false,
|
|
306
|
+
displayState: DISPLAY_STATES.PLAYER,
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
press(manager, key);
|
|
310
|
+
|
|
311
|
+
expect(props.setDisplayState).toHaveBeenCalledWith(DISPLAY_STATES.HUD);
|
|
312
|
+
expect(player.seekTo).toHaveBeenCalledWith(expectedPosition);
|
|
313
|
+
expect(player.play).toHaveBeenCalled();
|
|
314
|
+
expect(player.forward).not.toHaveBeenCalled();
|
|
315
|
+
expect(player.rewind).not.toHaveBeenCalled();
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
});
|
|
@@ -52,13 +52,14 @@ import { KeyboardLongPressManager } from "@applicaster/zapp-react-dom-ui-compone
|
|
|
52
52
|
import { KeyInputHandler } from "@applicaster/zapp-react-native-utils/appUtils/keyInputHandler/KeyInputHandler";
|
|
53
53
|
import { getHomeRiver } from "@applicaster/zapp-react-native-utils/reactHooks/state";
|
|
54
54
|
import { withBackToTopActionHOC } from "./hoc";
|
|
55
|
+
import { isVidaaPlatform } from "../Loader/utils/platform";
|
|
55
56
|
|
|
56
57
|
const { withXray } = getXray();
|
|
57
58
|
const globalAny: any = global;
|
|
58
59
|
|
|
59
60
|
const { log_debug, log_warning, log_info } = createLogger({
|
|
60
61
|
category: "InteractionManager",
|
|
61
|
-
subsystem: "
|
|
62
|
+
subsystem: "app/remote_control",
|
|
62
63
|
});
|
|
63
64
|
|
|
64
65
|
const shouldUseOnScreenKeyboard = () =>
|
|
@@ -89,7 +90,7 @@ type KeyCode = { code: string; keyCode: number | string };
|
|
|
89
90
|
const VIDEO_PLAYER_CONTROLS_NAVIGATION_MESSAGE =
|
|
90
91
|
"Video player controls - use left/right to navigate";
|
|
91
92
|
|
|
92
|
-
class InteractionManagerClass extends React.Component<Props, State> {
|
|
93
|
+
export class InteractionManagerClass extends React.Component<Props, State> {
|
|
93
94
|
onKeyDownListener: (keyCode: KeyCode) => void;
|
|
94
95
|
onMouseDownListener: (event: MouseEvent) => void;
|
|
95
96
|
onMouseMoveListener: (event: MouseEvent) => void;
|
|
@@ -101,6 +102,8 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
101
102
|
private sequenceTimeout: NodeJS.Timeout | null;
|
|
102
103
|
private userSequence: number[] = [];
|
|
103
104
|
private isDismissing = false;
|
|
105
|
+
// Whether each key's key-down was left to the player's own controls
|
|
106
|
+
private deferredKeyDowns = new Map<number, boolean>();
|
|
104
107
|
private confirmDialog: ConfirmationDialogState =
|
|
105
108
|
confirmationDialogStore.getState();
|
|
106
109
|
|
|
@@ -250,10 +253,14 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
250
253
|
if (isVizioPlatform()) {
|
|
251
254
|
window?.VIZIO?.exitApplication();
|
|
252
255
|
}
|
|
256
|
+
|
|
257
|
+
if (isVidaaPlatform()) {
|
|
258
|
+
window.close();
|
|
259
|
+
}
|
|
253
260
|
}
|
|
254
261
|
|
|
255
262
|
handleWebOsBack() {
|
|
256
|
-
const systemBack = globalAny?.webOS?.
|
|
263
|
+
const systemBack = globalAny?.webOS?.platformBack;
|
|
257
264
|
|
|
258
265
|
if (systemBack) {
|
|
259
266
|
systemBack();
|
|
@@ -277,7 +284,7 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
277
284
|
platformToBackground() {
|
|
278
285
|
try {
|
|
279
286
|
// TODO: temporary hack until this code is refactored
|
|
280
|
-
if (isSamsungPlatform() || isVizioPlatform()) {
|
|
287
|
+
if (isSamsungPlatform() || isVizioPlatform() || isVidaaPlatform()) {
|
|
281
288
|
const { isDialogVisible } = confirmationDialogStore.getState();
|
|
282
289
|
|
|
283
290
|
if (isDialogVisible) {
|
|
@@ -368,6 +375,18 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
368
375
|
focusManager.pressOut();
|
|
369
376
|
}
|
|
370
377
|
|
|
378
|
+
// The key-up goes to whoever got the key-down, even if a dialog or the
|
|
379
|
+
// play-next overlay opened or closed in between.
|
|
380
|
+
const deferred =
|
|
381
|
+
this.deferredKeyDowns.get(event.keyCode) ??
|
|
382
|
+
this.isDeferredToNativeControls(event);
|
|
383
|
+
|
|
384
|
+
this.deferredKeyDowns.delete(event.keyCode);
|
|
385
|
+
|
|
386
|
+
if (deferred) {
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
371
390
|
if (
|
|
372
391
|
keyCode(event).matches(KEYS.Forward) ||
|
|
373
392
|
(__DEV__ && keyCode(event).matches(KEYS.C))
|
|
@@ -420,18 +439,20 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
420
439
|
return;
|
|
421
440
|
}
|
|
422
441
|
|
|
442
|
+
// Focus is inside the dialog, so backToTopAction() can't resolve a
|
|
443
|
+
// meaningful action — Back must dismiss the dialog in every display state.
|
|
444
|
+
if (isDialogVisible) {
|
|
445
|
+
this.onConfirmDialogClose();
|
|
446
|
+
event.preventDefault();
|
|
447
|
+
event.stopPropagation();
|
|
448
|
+
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
|
|
423
452
|
const action = backToTopAction();
|
|
424
453
|
|
|
425
454
|
const effectiveDisplayState = this.getEffectiveDisplayState();
|
|
426
455
|
|
|
427
|
-
// TEMP(native-controls): remove after device verification
|
|
428
|
-
if (effectiveDisplayState !== displayState) {
|
|
429
|
-
log_debug("[NativeControls] Back handled as PLAYER", {
|
|
430
|
-
displayState,
|
|
431
|
-
action,
|
|
432
|
-
});
|
|
433
|
-
}
|
|
434
|
-
|
|
435
456
|
switch (effectiveDisplayState) {
|
|
436
457
|
case DISPLAY_STATES.DEFAULT:
|
|
437
458
|
if (action === "PLATFORM_TO_BACKGROUND") {
|
|
@@ -481,9 +502,7 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
481
502
|
|
|
482
503
|
break;
|
|
483
504
|
case DISPLAY_STATES.PLAYER:
|
|
484
|
-
if (
|
|
485
|
-
this.onConfirmDialogClose();
|
|
486
|
-
} else if (navigator.canGoBack()) {
|
|
505
|
+
if (navigator.canGoBack()) {
|
|
487
506
|
navigator.goBack();
|
|
488
507
|
} else if (this.isHomeScreen()) {
|
|
489
508
|
this.platformToBackground();
|
|
@@ -559,12 +578,22 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
559
578
|
return (
|
|
560
579
|
this.isNativePlayerControlsEnabled() &&
|
|
561
580
|
!this.isPlayNextOverlayVisible() &&
|
|
562
|
-
(keyCode(event).matchesAny(
|
|
563
|
-
|
|
564
|
-
|
|
581
|
+
(keyCode(event).matchesAny(
|
|
582
|
+
...ARROW_KEYS,
|
|
583
|
+
KEYS.Enter,
|
|
584
|
+
// The player seeks on its own with these keys
|
|
585
|
+
KEYS.Forward,
|
|
586
|
+
KEYS.Rewind
|
|
587
|
+
) ||
|
|
588
|
+
(__DEV__ && keyCode(event).matchesAny(KEYS.X, KEYS.C, KEYS.Z)))
|
|
565
589
|
);
|
|
566
590
|
};
|
|
567
591
|
|
|
592
|
+
// An open dialog owns the remote, whatever the player renders.
|
|
593
|
+
isDeferredToNativeControls = (event: KeyboardEvent) =>
|
|
594
|
+
!confirmationDialogStore.getState().isDialogVisible &&
|
|
595
|
+
this.shouldDeferPlaybackNavigationToNativeControls(event);
|
|
596
|
+
|
|
568
597
|
onKeyDown(event) {
|
|
569
598
|
const { setDisplayState, navigator, xray } = this.props;
|
|
570
599
|
const displayState = this.getEffectiveDisplayState();
|
|
@@ -603,18 +632,11 @@ class InteractionManagerClass extends React.Component<Props, State> {
|
|
|
603
632
|
// Only handle repeat events for arrow keys
|
|
604
633
|
if (event.repeat && !keyCode(event).matchesAny(...ARROW_KEYS)) return;
|
|
605
634
|
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
this.shouldDeferPlaybackNavigationToNativeControls(event)
|
|
609
|
-
) {
|
|
610
|
-
// The player's own spatial navigation handles these keys.
|
|
611
|
-
// TEMP(native-controls): remove after device verification
|
|
612
|
-
log_debug("[NativeControls] key deferred to player", {
|
|
613
|
-
key: event.key,
|
|
614
|
-
displayState,
|
|
615
|
-
activeElement: document.activeElement?.className,
|
|
616
|
-
});
|
|
635
|
+
const deferred = this.isDeferredToNativeControls(event);
|
|
636
|
+
this.deferredKeyDowns.set(event.keyCode, deferred);
|
|
617
637
|
|
|
638
|
+
if (deferred) {
|
|
639
|
+
// The player's own key handling takes these keys.
|
|
618
640
|
return;
|
|
619
641
|
}
|
|
620
642
|
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
|
|
3
|
+
jest.mock("axios", () => ({ get: jest.fn() }));
|
|
4
|
+
|
|
5
|
+
const mockAppData = {
|
|
6
|
+
accountsAccountId: "account",
|
|
7
|
+
bundleIdentifier: "com.app",
|
|
8
|
+
store: "samsung",
|
|
9
|
+
versionName: "1.0",
|
|
10
|
+
appFamilyId: "family",
|
|
11
|
+
// appData.json is camelized
|
|
12
|
+
riversConfigurationId: "default-layout",
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
jest.mock("@applicaster/zapp-react-native-redux/AppStore", () => ({
|
|
16
|
+
appStore: { get: () => mockAppData },
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const get = axios.get as jest.Mock;
|
|
20
|
+
|
|
21
|
+
const appUrl =
|
|
22
|
+
"https://assets-secure.applicaster.com/zapp/accounts/account/apps/com.app/samsung_app_store/1.0";
|
|
23
|
+
|
|
24
|
+
const familyUrl =
|
|
25
|
+
"https://assets-secure.applicaster.com/zapp/accounts/account/app_families/family";
|
|
26
|
+
|
|
27
|
+
const storage = new Map<string, string>();
|
|
28
|
+
|
|
29
|
+
(global as any).window = {
|
|
30
|
+
localStorage: {
|
|
31
|
+
getItem: (key) => (storage.has(key) ? storage.get(key) : null),
|
|
32
|
+
setItem: (key, value) => storage.set(key, String(value)),
|
|
33
|
+
},
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
const cacheKey = (key: string) => `loader_cache_::_${key}`;
|
|
37
|
+
|
|
38
|
+
const ok = (data: unknown, etag?: string) =>
|
|
39
|
+
Promise.resolve({
|
|
40
|
+
status: 200,
|
|
41
|
+
data: JSON.stringify(data),
|
|
42
|
+
headers: { etag },
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const requestedUrls = () => get.mock.calls.map(([url]) => url);
|
|
46
|
+
|
|
47
|
+
let AppLoaderBridge;
|
|
48
|
+
|
|
49
|
+
beforeEach(() => {
|
|
50
|
+
storage.clear();
|
|
51
|
+
get.mockReset();
|
|
52
|
+
process.env.ENABLE_REMOTE_DATA_IN_DEV = "true";
|
|
53
|
+
|
|
54
|
+
jest.isolateModules(() => {
|
|
55
|
+
require("../../AppData").setAppData(mockAppData);
|
|
56
|
+
AppLoaderBridge = require("..").AppLoaderBridge;
|
|
57
|
+
});
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
afterAll(() => {
|
|
61
|
+
delete process.env.ENABLE_REMOTE_DATA_IN_DEV;
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe("getFile", () => {
|
|
65
|
+
it("resolves the parsed file and caches its text and etag", async () => {
|
|
66
|
+
get.mockReturnValue(ok({ id: "config" }, "e1"));
|
|
67
|
+
|
|
68
|
+
await expect(
|
|
69
|
+
AppLoaderBridge.getFile("remoteConfigurations")
|
|
70
|
+
).resolves.toEqual({ id: "config" });
|
|
71
|
+
|
|
72
|
+
expect(get).toHaveBeenCalledWith(
|
|
73
|
+
`${appUrl}/remote_configurations/remote_configurations.json`,
|
|
74
|
+
expect.objectContaining({ headers: { "If-None-Match": null } })
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(storage.get(cacheKey("remoteConfigurations"))).toBe(
|
|
78
|
+
'{"id":"config"}'
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
expect(storage.get(cacheKey("remoteConfigurationsEtag"))).toBe("e1");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("resolves the cached file when the server answers 304", async () => {
|
|
85
|
+
storage.set(cacheKey("pipesEndpoints"), '{"id":"cached"}');
|
|
86
|
+
storage.set(cacheKey("pipesEndpointsEtag"), "e1");
|
|
87
|
+
get.mockResolvedValue({ status: 304, data: "", headers: {} });
|
|
88
|
+
|
|
89
|
+
await expect(AppLoaderBridge.getFile("pipesEndpoints")).resolves.toEqual({
|
|
90
|
+
id: "cached",
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
expect(get).toHaveBeenCalledWith(
|
|
94
|
+
`${familyUrl}/data_source_providers/endpoints.json`,
|
|
95
|
+
expect.objectContaining({ headers: { "If-None-Match": "e1" } })
|
|
96
|
+
);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("rejects with the configuration error when the body is not JSON", async () => {
|
|
100
|
+
get.mockResolvedValue({ status: 200, data: "<html>", headers: {} });
|
|
101
|
+
|
|
102
|
+
await expect(AppLoaderBridge.getFile("layout")).rejects.toThrow(
|
|
103
|
+
"cannot retrieve configuration data for layout"
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("prefetchFiles", () => {
|
|
109
|
+
it("sends the general requests before returning, and getFile uses them", async () => {
|
|
110
|
+
get.mockImplementation((url) => ok({ url }));
|
|
111
|
+
|
|
112
|
+
AppLoaderBridge.prefetchFiles();
|
|
113
|
+
|
|
114
|
+
expect(requestedUrls()).toEqual([
|
|
115
|
+
`${appUrl}/remote_configurations/remote_configurations.json`,
|
|
116
|
+
`${appUrl}/plugin_configurations/plugin_configurations.json`,
|
|
117
|
+
`${familyUrl}/data_source_providers/endpoints.json`,
|
|
118
|
+
]);
|
|
119
|
+
|
|
120
|
+
await expect(
|
|
121
|
+
AppLoaderBridge.getFile("pluginConfigurations")
|
|
122
|
+
).resolves.toEqual({
|
|
123
|
+
url: `${appUrl}/plugin_configurations/plugin_configurations.json`,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
expect(get).toHaveBeenCalledTimes(3);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("also requests the layout files for a layout id stored by a previous launch", () => {
|
|
130
|
+
storage.set(cacheKey("currentLayoutId"), "stored-layout");
|
|
131
|
+
get.mockImplementation((url) => ok({ url }));
|
|
132
|
+
|
|
133
|
+
AppLoaderBridge.prefetchFiles();
|
|
134
|
+
|
|
135
|
+
expect(requestedUrls()).toEqual(
|
|
136
|
+
expect.arrayContaining([
|
|
137
|
+
`${appUrl}/layouts/stored-layout.json`,
|
|
138
|
+
`${familyUrl}/layouts/stored-layout/cell_styles.json`,
|
|
139
|
+
`${familyUrl}/layouts/stored-layout/presets_mapping.json`,
|
|
140
|
+
])
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
expect(get).toHaveBeenCalledTimes(6);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it("falls back to the built-in layout id and stores it as the default", async () => {
|
|
147
|
+
get.mockImplementation((url) => ok({ url }));
|
|
148
|
+
|
|
149
|
+
await expect(AppLoaderBridge.getFile("layout")).resolves.toEqual({
|
|
150
|
+
url: `${appUrl}/layouts/default-layout.json`,
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
expect(storage.get(cacheKey("defaultLayoutId"))).toBe("default-layout");
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it("requests a file again when its URL changed after the prefetch", async () => {
|
|
157
|
+
storage.set(cacheKey("currentLayoutId"), "old-layout");
|
|
158
|
+
get.mockImplementation((url) => ok({ url }));
|
|
159
|
+
|
|
160
|
+
AppLoaderBridge.prefetchFiles();
|
|
161
|
+
await AppLoaderBridge.switchLayout("new-layout");
|
|
162
|
+
|
|
163
|
+
await expect(AppLoaderBridge.getFile("layout")).resolves.toEqual({
|
|
164
|
+
url: `${appUrl}/layouts/new-layout.json`,
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
expect(get).toHaveBeenCalledTimes(7);
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it("uses each prefetched request once", async () => {
|
|
171
|
+
get.mockImplementation((url) => ok({ url }));
|
|
172
|
+
|
|
173
|
+
AppLoaderBridge.prefetchFiles();
|
|
174
|
+
await AppLoaderBridge.getFile("remoteConfigurations");
|
|
175
|
+
await AppLoaderBridge.getFile("remoteConfigurations");
|
|
176
|
+
|
|
177
|
+
expect(get).toHaveBeenCalledTimes(4);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("requests a file again when its prefetch failed", async () => {
|
|
181
|
+
get.mockImplementation(() => Promise.reject(new Error("network")));
|
|
182
|
+
|
|
183
|
+
AppLoaderBridge.prefetchFiles();
|
|
184
|
+
|
|
185
|
+
get.mockImplementation((url) => ok({ url }));
|
|
186
|
+
|
|
187
|
+
await expect(
|
|
188
|
+
AppLoaderBridge.getFile("remoteConfigurations")
|
|
189
|
+
).resolves.toEqual({
|
|
190
|
+
url: `${appUrl}/remote_configurations/remote_configurations.json`,
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
expect(requestedUrls()[3]).toBe(
|
|
194
|
+
`${appUrl}/remote_configurations/remote_configurations.json`
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
expect(get).toHaveBeenCalledTimes(4);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("rejects getFile with the configuration error when the prefetch and its retry failed", async () => {
|
|
201
|
+
get.mockImplementation(() => Promise.reject(new Error("network")));
|
|
202
|
+
|
|
203
|
+
AppLoaderBridge.prefetchFiles();
|
|
204
|
+
|
|
205
|
+
await expect(
|
|
206
|
+
AppLoaderBridge.getFile("remoteConfigurations")
|
|
207
|
+
).rejects.toThrow(
|
|
208
|
+
"cannot retrieve configuration data for remoteConfigurations"
|
|
209
|
+
);
|
|
210
|
+
|
|
211
|
+
expect(get).toHaveBeenCalledTimes(4);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("does nothing in development unless remote data is enabled", () => {
|
|
215
|
+
delete process.env.ENABLE_REMOTE_DATA_IN_DEV;
|
|
216
|
+
|
|
217
|
+
AppLoaderBridge.prefetchFiles();
|
|
218
|
+
|
|
219
|
+
expect(get).not.toHaveBeenCalled();
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -1,67 +1,115 @@
|
|
|
1
|
-
import axios, {
|
|
1
|
+
import axios, { AxiosResponse } from "axios";
|
|
2
|
+
import { getAppData } from "../AppData";
|
|
2
3
|
import { getStorageModule } from "../Storage";
|
|
3
4
|
import { STORAGE } from "./utils/enums";
|
|
4
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
buildUrl,
|
|
7
|
+
createUrl,
|
|
8
|
+
getLayoutIds,
|
|
9
|
+
readLoaderCache,
|
|
10
|
+
writeLoaderCache,
|
|
11
|
+
} from "./utils/utils";
|
|
5
12
|
|
|
6
13
|
type StorageResponse = string | null;
|
|
7
14
|
|
|
15
|
+
type Prefetched = { url: string; response: Promise<AxiosResponse<string>> };
|
|
16
|
+
|
|
8
17
|
const localStorage: any = getStorageModule("localStorage");
|
|
9
18
|
|
|
19
|
+
const GENERAL_FILES = [
|
|
20
|
+
"remoteConfigurations",
|
|
21
|
+
"pluginConfigurations",
|
|
22
|
+
"pipesEndpoints",
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
const LAYOUT_FILES = ["layout", "cellStyles", "presetsMapping"];
|
|
26
|
+
|
|
27
|
+
function requestFile(type: string, url: string) {
|
|
28
|
+
return axios.get<string>(url, {
|
|
29
|
+
headers: {
|
|
30
|
+
// Sent even without a stored etag (as "null"). A conditional header
|
|
31
|
+
// keeps the browser's HTTP cache out of these requests, so every launch
|
|
32
|
+
// gets the current file.
|
|
33
|
+
"If-None-Match": readLoaderCache(`${type}Etag`),
|
|
34
|
+
},
|
|
35
|
+
validateStatus: (status: number): boolean =>
|
|
36
|
+
status === 200 || status === 304,
|
|
37
|
+
// Keep the body as text: it is stored as-is and parsed once in getFile.
|
|
38
|
+
transformResponse: [(body) => body],
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
10
42
|
export const AppLoaderBridge = (function () {
|
|
11
|
-
|
|
43
|
+
// In-flight requests started by prefetchFiles, each used by the first
|
|
44
|
+
// getFile call for the same file and URL, which requests the file again
|
|
45
|
+
// if the prefetch failed.
|
|
46
|
+
const prefetched = new Map<string, Prefetched>();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Resolves the parsed file. Native bridges resolve a JSON string instead;
|
|
50
|
+
* getFile callers accept both, which saves serializing and re-parsing
|
|
51
|
+
* every file on web.
|
|
52
|
+
*/
|
|
53
|
+
async function getFile(type: string): Promise<unknown> {
|
|
12
54
|
const url: string = await createUrl(type);
|
|
55
|
+
const pending = prefetched.get(type);
|
|
56
|
+
prefetched.delete(type);
|
|
13
57
|
|
|
14
58
|
try {
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
);
|
|
19
|
-
|
|
20
|
-
const config: AxiosRequestConfig = {
|
|
21
|
-
method: "get",
|
|
22
|
-
url,
|
|
23
|
-
headers: {
|
|
24
|
-
"If-None-Match": currentEtag,
|
|
25
|
-
},
|
|
26
|
-
validateStatus: (status: number): boolean =>
|
|
27
|
-
status === 200 || status === 304,
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const response: AxiosResponse = await axios(config);
|
|
31
|
-
|
|
32
|
-
const {
|
|
33
|
-
data,
|
|
34
|
-
headers: { etag },
|
|
35
|
-
status,
|
|
36
|
-
} = response;
|
|
59
|
+
const { data, headers, status } = await (pending?.url === url
|
|
60
|
+
? pending.response.catch(() => requestFile(type, url))
|
|
61
|
+
: requestFile(type, url));
|
|
37
62
|
|
|
38
63
|
if (status === 304) {
|
|
39
|
-
|
|
40
|
-
type,
|
|
41
|
-
STORAGE.APP_LOADER_BRIDGE_NAME_SPACE
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
return JSON.stringify(res);
|
|
64
|
+
return readLoaderCache(type);
|
|
45
65
|
}
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
`${type}Etag`,
|
|
49
|
-
etag,
|
|
50
|
-
STORAGE.APP_LOADER_BRIDGE_NAME_SPACE
|
|
51
|
-
);
|
|
67
|
+
const file = JSON.parse(data);
|
|
52
68
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
JSON.stringify(data),
|
|
56
|
-
STORAGE.APP_LOADER_BRIDGE_NAME_SPACE
|
|
57
|
-
);
|
|
69
|
+
writeLoaderCache(`${type}Etag`, headers.etag);
|
|
70
|
+
writeLoaderCache(type, data);
|
|
58
71
|
|
|
59
|
-
return
|
|
72
|
+
return file;
|
|
60
73
|
} catch (error) {
|
|
61
74
|
throw new Error("cannot retrieve configuration data for " + type);
|
|
62
75
|
}
|
|
63
76
|
}
|
|
64
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Starts the configuration requests while the app bundle is still
|
|
80
|
+
* evaluating, so the files download during it instead of after it.
|
|
81
|
+
* Layout files are only requested for a layout id stored by a previous
|
|
82
|
+
* launch; without one, the remote configuration may select another layout.
|
|
83
|
+
* A file whose URL changes by the time getFile asks for it is requested again.
|
|
84
|
+
*/
|
|
85
|
+
function prefetchFiles(): void {
|
|
86
|
+
// Mirrors appRemoteDataLoader, which skips remote data in development.
|
|
87
|
+
if (__DEV__ && process.env.ENABLE_REMOTE_DATA_IN_DEV !== "true") {
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const appData = getAppData();
|
|
93
|
+
const { storedLayoutId } = getLayoutIds(appData);
|
|
94
|
+
|
|
95
|
+
const types = storedLayoutId
|
|
96
|
+
? [...GENERAL_FILES, ...LAYOUT_FILES]
|
|
97
|
+
: GENERAL_FILES;
|
|
98
|
+
|
|
99
|
+
types.forEach((type) => {
|
|
100
|
+
const url = buildUrl(type, appData, storedLayoutId);
|
|
101
|
+
const response = requestFile(type, url);
|
|
102
|
+
|
|
103
|
+
// getFile reports failures; this only stops an early or unused
|
|
104
|
+
// failure from surfacing as an unhandled rejection.
|
|
105
|
+
response.catch(() => undefined);
|
|
106
|
+
prefetched.set(type, { url, response });
|
|
107
|
+
});
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Nothing prefetched; getFile requests the files as before.
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
65
113
|
async function switchLayout(layoutID: string): Promise<boolean> {
|
|
66
114
|
return await localStorage.setItem(
|
|
67
115
|
STORAGE.CURRENT_LAYOUT_ID,
|
|
@@ -86,6 +134,7 @@ export const AppLoaderBridge = (function () {
|
|
|
86
134
|
|
|
87
135
|
return {
|
|
88
136
|
getFile,
|
|
137
|
+
prefetchFiles,
|
|
89
138
|
switchLayout,
|
|
90
139
|
getCurrentLayoutId,
|
|
91
140
|
getDefaultLayoutId,
|
|
@@ -1,54 +1,68 @@
|
|
|
1
1
|
import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
|
|
2
|
-
import {
|
|
2
|
+
import { parseJsonIfNeeded } from "@applicaster/zapp-react-native-utils/functionUtils";
|
|
3
|
+
import { applyNamespaceToKeyName } from "../../Storage";
|
|
3
4
|
import { STORAGE, STORE_PLATFORM } from "./enums";
|
|
4
5
|
|
|
5
|
-
const
|
|
6
|
+
const cacheKey = (key: string) =>
|
|
7
|
+
applyNamespaceToKeyName(key, STORAGE.APP_LOADER_BRIDGE_NAME_SPACE);
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Reads the loader cache with the same keys and parsing as
|
|
11
|
+
* getStorageModule("localStorage"), but synchronously. Web storage is
|
|
12
|
+
* synchronous, and prefetchFiles needs it that way: it runs while the app
|
|
13
|
+
* bundle is still evaluating, and an awaited read would only resume after it.
|
|
14
|
+
*/
|
|
15
|
+
export function readLoaderCache(key: string) {
|
|
16
|
+
return parseJsonIfNeeded(window.localStorage.getItem(cacheKey(key)));
|
|
17
|
+
}
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
store,
|
|
14
|
-
versionName,
|
|
15
|
-
rivers_configuration_id: defaultLayoutId,
|
|
16
|
-
appFamilyId,
|
|
17
|
-
} = appData;
|
|
19
|
+
export function writeLoaderCache(key: string, value: string) {
|
|
20
|
+
window.localStorage.setItem(cacheKey(key), value);
|
|
21
|
+
}
|
|
18
22
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
23
|
+
/**
|
|
24
|
+
* The layout id to request, split by where it comes from.
|
|
25
|
+
* `storedLayoutId` is only set when a previous launch stored one.
|
|
26
|
+
*/
|
|
27
|
+
export function getLayoutIds(appData) {
|
|
28
|
+
// appData.json is camelized, so the built-in id is riversConfigurationId.
|
|
29
|
+
const defaultLayoutId =
|
|
30
|
+
appData.riversConfigurationId || appData.rivers_configuration_id;
|
|
31
|
+
|
|
32
|
+
const storedDefault = readLoaderCache(STORAGE.DEFAULT_LAYOUT_ID);
|
|
33
|
+
|
|
34
|
+
if ((!storedDefault || storedDefault === "undefined") && defaultLayoutId) {
|
|
35
|
+
writeLoaderCache(STORAGE.DEFAULT_LAYOUT_ID, defaultLayoutId);
|
|
30
36
|
}
|
|
31
37
|
|
|
32
|
-
const currentLayoutId =
|
|
33
|
-
STORAGE.CURRENT_LAYOUT_ID,
|
|
34
|
-
STORAGE.APP_LOADER_BRIDGE_NAME_SPACE
|
|
35
|
-
);
|
|
38
|
+
const currentLayoutId = readLoaderCache(STORAGE.CURRENT_LAYOUT_ID);
|
|
36
39
|
|
|
37
|
-
const remoteConfig: RemoteConfigurations =
|
|
38
|
-
STORAGE.REMOTE_CONFIGURATIONS
|
|
39
|
-
STORAGE.APP_LOADER_BRIDGE_NAME_SPACE
|
|
40
|
+
const remoteConfig: RemoteConfigurations = readLoaderCache(
|
|
41
|
+
STORAGE.REMOTE_CONFIGURATIONS
|
|
40
42
|
);
|
|
41
43
|
|
|
42
44
|
const remoteConfigLayoutId = remoteConfig?.general_settings?.layout_id;
|
|
43
45
|
|
|
44
46
|
/**
|
|
45
|
-
* It is very important to note the order used
|
|
47
|
+
* It is very important to note the order used for the layout id (createUrl uses storedLayoutId || defaultLayoutId).
|
|
46
48
|
* First we are attempting ot use the id set by layout plugin, it is an override of the defined layout.
|
|
47
49
|
* Or use layout id from the remote configuration, remote config is loaded first and it has current layoutId from zapp.
|
|
48
50
|
* Or use layout id that was built with the app, this id can be found in appData.json and is loaded into redux store.
|
|
49
51
|
*/
|
|
50
|
-
|
|
51
|
-
currentLayoutId || remoteConfigLayoutId
|
|
52
|
+
return {
|
|
53
|
+
storedLayoutId: currentLayoutId || remoteConfigLayoutId,
|
|
54
|
+
defaultLayoutId,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function buildUrl(type: string, appData, layoutId: string): string {
|
|
59
|
+
const {
|
|
60
|
+
accountsAccountId,
|
|
61
|
+
bundleIdentifier,
|
|
62
|
+
store,
|
|
63
|
+
versionName,
|
|
64
|
+
appFamilyId,
|
|
65
|
+
} = appData;
|
|
52
66
|
|
|
53
67
|
const baseURL = `https://assets-secure.applicaster.com/zapp/accounts/${accountsAccountId}`;
|
|
54
68
|
const urlTemplate = `apps/${bundleIdentifier}/${STORE_PLATFORM[store]}/${versionName}`;
|
|
@@ -59,14 +73,21 @@ export async function createUrl(type: string): Promise<string> {
|
|
|
59
73
|
case "pluginConfigurations":
|
|
60
74
|
return `${baseURL}/${urlTemplate}/plugin_configurations/plugin_configurations.json`;
|
|
61
75
|
case "layout":
|
|
62
|
-
return `${baseURL}/${urlTemplate}/layouts/${
|
|
76
|
+
return `${baseURL}/${urlTemplate}/layouts/${layoutId}.json`;
|
|
63
77
|
case "pipesEndpoints":
|
|
64
78
|
return `${baseURL}/app_families/${appFamilyId}/data_source_providers/endpoints.json`;
|
|
65
79
|
case "cellStyles":
|
|
66
|
-
return `${baseURL}/app_families/${appFamilyId}/layouts/${
|
|
80
|
+
return `${baseURL}/app_families/${appFamilyId}/layouts/${layoutId}/cell_styles.json`;
|
|
67
81
|
case "presetsMapping":
|
|
68
|
-
return `${baseURL}/app_families/${appFamilyId}/layouts/${
|
|
82
|
+
return `${baseURL}/app_families/${appFamilyId}/layouts/${layoutId}/presets_mapping.json`;
|
|
69
83
|
default:
|
|
70
84
|
return null;
|
|
71
85
|
}
|
|
72
86
|
}
|
|
87
|
+
|
|
88
|
+
export async function createUrl(type: string): Promise<string> {
|
|
89
|
+
const appData = appStore.get("appData");
|
|
90
|
+
const { storedLayoutId, defaultLayoutId } = getLayoutIds(appData);
|
|
91
|
+
|
|
92
|
+
return buildUrl(type, appData, storedLayoutId || defaultLayoutId);
|
|
93
|
+
}
|
|
@@ -8,7 +8,7 @@ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
|
8
8
|
|
|
9
9
|
const { log_error, log_debug, log_warning } = createLogger({
|
|
10
10
|
category: "QuickBrickCommunicationModule",
|
|
11
|
-
subsystem: "
|
|
11
|
+
subsystem: "react_native/bridge",
|
|
12
12
|
});
|
|
13
13
|
|
|
14
14
|
const QuickBrickEventEmitter = {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@applicaster/zapp-react-dom-app",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "17.0.0-rc.1",
|
|
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": "
|
|
26
|
-
"@applicaster/zapp-react-native-bridge": "
|
|
27
|
-
"@applicaster/zapp-react-native-redux": "
|
|
28
|
-
"@applicaster/zapp-react-native-ui-components": "
|
|
29
|
-
"@applicaster/zapp-react-native-utils": "
|
|
25
|
+
"@applicaster/zapp-react-dom-ui-components": "17.0.0-rc.1",
|
|
26
|
+
"@applicaster/zapp-react-native-bridge": "17.0.0-rc.1",
|
|
27
|
+
"@applicaster/zapp-react-native-redux": "17.0.0-rc.1",
|
|
28
|
+
"@applicaster/zapp-react-native-ui-components": "17.0.0-rc.1",
|
|
29
|
+
"@applicaster/zapp-react-native-utils": "17.0.0-rc.1",
|
|
30
30
|
"abortcontroller-polyfill": "^1.7.5",
|
|
31
31
|
"typeface-montserrat": "^0.0.54",
|
|
32
32
|
"video.js": "7.14.3",
|