@applicaster/quick-brick-core 16.0.0-rc.99 → 17.0.0-alpha.1984136194
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/ActionSetters/index.ts +2 -2
- package/App/ActionsProvider/ActionsProvider.tsx +1 -1
- package/App/ActionsProvider/LegacyActionsRegistryAdapter.tsx +17 -1
- package/App/ActionsProvider/__tests__/LegacyActionsRegistryAdapter.test.tsx +64 -0
- package/App/AssetCache/logger.ts +1 -1
- package/App/DeepLinking/SamsungAppControl/__tests__/samsungAppControl.test.ts +255 -0
- package/App/DeepLinking/SamsungAppControl/index.ts +223 -0
- package/App/DeepLinking/URLSchemeHandler/SchemeHandlerHooks/usePresentSchemeHandler.ts +3 -0
- package/App/DeepLinking/URLSchemeListener/URLSchemeContextProvider.tsx +31 -1
- package/App/DeepLinking/logger.ts +1 -1
- package/App/ErrorBoundary/index.tsx +5 -2
- package/App/NavigationProvider/NavigationProvider.tsx +17 -11
- package/App/NavigationProvider/__tests__/navToolbox.test.ts +227 -0
- package/App/NavigationProvider/__tests__/navigationProvider.test.tsx +56 -0
- package/App/NavigationProvider/navToolbox.ts +224 -0
- package/App/NavigationProvider/utils.ts +23 -2
- package/App/NetworkStatusProvider/NetworkStatusProvider.tsx +40 -4
- package/App/NetworkStatusProvider/ResumeVerificationController.ts +282 -0
- package/App/NetworkStatusProvider/__tests__/NetworkStatusProvider.test.tsx +167 -1
- package/App/NetworkStatusProvider/__tests__/ResumeVerificationController.test.ts +486 -0
- package/App/NetworkStatusProvider/__tests__/resumeVerification.test.ts +126 -0
- package/App/NetworkStatusProvider/__tests__/useResumeVerifiedNetInfo.test.ts +204 -0
- package/App/NetworkStatusProvider/resumeVerification.ts +54 -0
- package/App/NetworkStatusProvider/useResumeVerifiedNetInfo.ts +33 -0
- package/App/ThemeManager/index.tsx +6 -1
- package/App/appLifeCycleManager/index.tsx +2 -2
- package/App/appRemoteDataLoader/index.tsx +2 -2
- package/App/appRemoteDataLoader/remoteDataLoader.ts +2 -2
- package/App/index.tsx +8 -2
- package/App/layoutPreview/LayoutPreviewReminder.tsx +136 -0
- package/App/layoutPreview/__tests__/persistence.test.ts +63 -0
- package/App/layoutPreview/persistence.ts +228 -0
- package/App/logger.js +2 -2
- package/App/remoteContextReloader/__tests__/getNativeRemoteContextData.test.ts +148 -0
- package/App/remoteContextReloader/__tests__/getPromiseForType.test.js +31 -0
- package/App/remoteContextReloader/getRemoteContextData/getNativeRemoteContextData.ts +107 -29
- package/App/remoteContextReloader/helpers.ts +2 -1
- package/App/uiActions/index.ts +13 -0
- package/App/uiActions/muteUnmute/__tests__/assetResolver.test.ts +226 -0
- package/App/uiActions/muteUnmute/__tests__/binding.test.ts +51 -0
- package/App/uiActions/muteUnmute/__tests__/defaultAssets.test.ts +47 -0
- package/App/uiActions/muteUnmute/__tests__/entryState.test.ts +57 -0
- package/App/uiActions/muteUnmute/__tests__/labelResolver.test.ts +35 -0
- package/App/uiActions/muteUnmute/assetResolver.ts +87 -0
- package/App/uiActions/muteUnmute/defaultAssets.ts +44 -0
- package/App/uiActions/muteUnmute/entryState.ts +28 -0
- package/App/uiActions/muteUnmute/index.ts +79 -0
- package/App/uiActions/muteUnmute/labelResolver.ts +15 -0
- package/App/uiActions/muteUnmute/types.ts +18 -0
- package/package.json +8 -8
|
@@ -4,8 +4,8 @@ import { allSettled } from "promise";
|
|
|
4
4
|
import { useActions } from "@applicaster/zapp-react-native-utils/reactHooks/actions";
|
|
5
5
|
|
|
6
6
|
import * as React from "react";
|
|
7
|
-
import {
|
|
8
|
-
const logger =
|
|
7
|
+
import { subsystemLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
8
|
+
const logger = subsystemLogger("app/lifecycle", "AppLifeCycleManager");
|
|
9
9
|
|
|
10
10
|
const debugActions = (actions) =>
|
|
11
11
|
logger.debug({
|
|
@@ -8,7 +8,7 @@ import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
|
8
8
|
import { LegacyActionsRegistryAdapter } from "./LegacyActionsRegistryAdapter";
|
|
9
9
|
|
|
10
10
|
const { log_debug } = createLogger({
|
|
11
|
-
subsystem: "
|
|
11
|
+
subsystem: "core/actions",
|
|
12
12
|
category: "ActionRegistration",
|
|
13
13
|
});
|
|
14
14
|
|
|
@@ -5,7 +5,7 @@ import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiAction
|
|
|
5
5
|
import { createLogger } from "@applicaster/zapp-react-native-utils/logger";
|
|
6
6
|
|
|
7
7
|
const { log_debug, log_warning } = createLogger({
|
|
8
|
-
subsystem: "
|
|
8
|
+
subsystem: "core/actions/legacy_registry",
|
|
9
9
|
category: "ActionRegistration",
|
|
10
10
|
});
|
|
11
11
|
|
|
@@ -66,6 +66,22 @@ function LegacyActionBridge({ plugin }: { plugin: LegacyActionPlugin }) {
|
|
|
66
66
|
};
|
|
67
67
|
}, [plugin.identifier]);
|
|
68
68
|
|
|
69
|
+
// Consumers resolve a snapshot of `value`; when the plugin publishes a new
|
|
70
|
+
// one (e.g. cast devices found), make them re-resolve so availability
|
|
71
|
+
// predicates are not read from a stale value. Registration already
|
|
72
|
+
// notifies, so the first render is skipped.
|
|
73
|
+
const isFirstValueRef = React.useRef(true);
|
|
74
|
+
|
|
75
|
+
React.useEffect(() => {
|
|
76
|
+
if (isFirstValueRef.current) {
|
|
77
|
+
isFirstValueRef.current = false;
|
|
78
|
+
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
uiActionsRegistry.notifyProviderChanged(plugin.identifier);
|
|
83
|
+
}, [value, plugin.identifier]);
|
|
84
|
+
|
|
69
85
|
return null;
|
|
70
86
|
}
|
|
71
87
|
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { render } from "@testing-library/react-native";
|
|
3
|
+
import { uiActionsRegistry } from "@applicaster/zapp-react-native-utils/uiActionsRegistrator";
|
|
4
|
+
import { LegacyActionsRegistryAdapter } from "../LegacyActionsRegistryAdapter";
|
|
5
|
+
|
|
6
|
+
const CastContext = React.createContext<any>(null);
|
|
7
|
+
|
|
8
|
+
const plugin = {
|
|
9
|
+
identifier: "cast-action-test",
|
|
10
|
+
module: { context: CastContext },
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const renderWithValue = (value) => (
|
|
14
|
+
<CastContext.Provider value={value}>
|
|
15
|
+
<LegacyActionsRegistryAdapter actionPlugins={{ cast: plugin } as any} />
|
|
16
|
+
</CastContext.Provider>
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
describe("LegacyActionsRegistryAdapter", () => {
|
|
20
|
+
it("makes consumers re-resolve when the plugin publishes a new value", () => {
|
|
21
|
+
const noDevices = { isActionAvailable: () => false };
|
|
22
|
+
const devicesFound = { isActionAvailable: () => true };
|
|
23
|
+
|
|
24
|
+
const { rerender, unmount } = render(renderWithValue(noDevices));
|
|
25
|
+
|
|
26
|
+
// what a consumer resolved at first paint
|
|
27
|
+
expect(
|
|
28
|
+
uiActionsRegistry
|
|
29
|
+
.getAction("cast-action-test")
|
|
30
|
+
?.isActionAvailable?.({} as any)
|
|
31
|
+
).toBe(false);
|
|
32
|
+
|
|
33
|
+
const listener = jest.fn();
|
|
34
|
+
const unsubscribe = uiActionsRegistry.subscribe(listener);
|
|
35
|
+
|
|
36
|
+
rerender(renderWithValue(devicesFound));
|
|
37
|
+
|
|
38
|
+
expect(listener).toHaveBeenCalledTimes(1);
|
|
39
|
+
|
|
40
|
+
expect(
|
|
41
|
+
uiActionsRegistry
|
|
42
|
+
.getAction("cast-action-test")
|
|
43
|
+
?.isActionAvailable?.({} as any)
|
|
44
|
+
).toBe(true);
|
|
45
|
+
|
|
46
|
+
unsubscribe();
|
|
47
|
+
unmount();
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("does not notify again when the value is unchanged", () => {
|
|
51
|
+
const stable = { isActionAvailable: () => true };
|
|
52
|
+
const { rerender, unmount } = render(renderWithValue(stable));
|
|
53
|
+
|
|
54
|
+
const listener = jest.fn();
|
|
55
|
+
const unsubscribe = uiActionsRegistry.subscribe(listener);
|
|
56
|
+
|
|
57
|
+
rerender(renderWithValue(stable));
|
|
58
|
+
|
|
59
|
+
expect(listener).not.toHaveBeenCalled();
|
|
60
|
+
|
|
61
|
+
unsubscribe();
|
|
62
|
+
unmount();
|
|
63
|
+
});
|
|
64
|
+
});
|
package/App/AssetCache/logger.ts
CHANGED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import {
|
|
2
|
+
actionDataToUrl,
|
|
3
|
+
addSamsungAppControlListener,
|
|
4
|
+
getSamsungAppControlUrl,
|
|
5
|
+
resetSamsungAppControlState,
|
|
6
|
+
} from "..";
|
|
7
|
+
|
|
8
|
+
jest.mock("@applicaster/zapp-react-native-utils/appDataUtils", () => ({
|
|
9
|
+
getAppUrlScheme: () => "self",
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
const QUERY =
|
|
13
|
+
"feed_locator=https%3A%2F%2Fzapp-pf-axnm.web.app%2Faxinom%2Fmovies%2F488059942906&id=488059942906";
|
|
14
|
+
|
|
15
|
+
const DEEP_LINK = `pureflix://open?${QUERY}`;
|
|
16
|
+
|
|
17
|
+
const mockTizen = (data?: { key: string; value: string[] }[]) => {
|
|
18
|
+
const getRequestedAppControl = jest.fn(() =>
|
|
19
|
+
data
|
|
20
|
+
? {
|
|
21
|
+
appControl: {
|
|
22
|
+
operation: "http://tizen.org/appcontrol/operation/default",
|
|
23
|
+
data,
|
|
24
|
+
},
|
|
25
|
+
}
|
|
26
|
+
: null
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
window.tizen = {
|
|
31
|
+
application: { getCurrentApplication: () => ({ getRequestedAppControl }) },
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
return getRequestedAppControl;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const payload = (values: unknown) => [
|
|
38
|
+
{ key: "PAYLOAD", value: [JSON.stringify({ values })] },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// The RN jest environment has no DOM events on window
|
|
42
|
+
const target = new EventTarget();
|
|
43
|
+
// @ts-ignore
|
|
44
|
+
window.addEventListener = target.addEventListener.bind(target);
|
|
45
|
+
// @ts-ignore
|
|
46
|
+
window.removeEventListener = target.removeEventListener.bind(target);
|
|
47
|
+
// @ts-ignore
|
|
48
|
+
window.dispatchEvent = target.dispatchEvent.bind(target);
|
|
49
|
+
|
|
50
|
+
const documentTarget = new EventTarget();
|
|
51
|
+
let documentHidden = false;
|
|
52
|
+
|
|
53
|
+
// @ts-ignore
|
|
54
|
+
global.document = {
|
|
55
|
+
get hidden() {
|
|
56
|
+
return documentHidden;
|
|
57
|
+
},
|
|
58
|
+
addEventListener: documentTarget.addEventListener.bind(documentTarget),
|
|
59
|
+
removeEventListener: documentTarget.removeEventListener.bind(documentTarget),
|
|
60
|
+
dispatchEvent: documentTarget.dispatchEvent.bind(documentTarget),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const setVisibility = (hidden: boolean) => {
|
|
64
|
+
documentHidden = hidden;
|
|
65
|
+
document.dispatchEvent(new Event("visibilitychange"));
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
delete window.tizen;
|
|
71
|
+
documentHidden = false;
|
|
72
|
+
resetSamsungAppControlState();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
describe("actionDataToUrl", () => {
|
|
76
|
+
it("keeps a full deep link", () => {
|
|
77
|
+
expect(actionDataToUrl(DEEP_LINK)).toBe(DEEP_LINK);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("builds an open deep link from a query string", () => {
|
|
81
|
+
expect(actionDataToUrl(QUERY)).toBe(`self://open?${QUERY}`);
|
|
82
|
+
expect(actionDataToUrl(`?${QUERY}`)).toBe(`self://open?${QUERY}`);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("reads url / deep_link / values from JSON", () => {
|
|
86
|
+
expect(actionDataToUrl(JSON.stringify({ url: DEEP_LINK }))).toBe(DEEP_LINK);
|
|
87
|
+
expect(actionDataToUrl({ deep_link: DEEP_LINK })).toBe(DEEP_LINK);
|
|
88
|
+
|
|
89
|
+
expect(actionDataToUrl(JSON.stringify({ values: DEEP_LINK }))).toBe(
|
|
90
|
+
DEEP_LINK
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
expect(
|
|
94
|
+
actionDataToUrl(
|
|
95
|
+
JSON.stringify({ values: JSON.stringify({ url: DEEP_LINK }) })
|
|
96
|
+
)
|
|
97
|
+
).toBe(DEEP_LINK);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns null for non deep link data", () => {
|
|
101
|
+
expect(actionDataToUrl("")).toBeNull();
|
|
102
|
+
expect(actionDataToUrl("hello")).toBeNull();
|
|
103
|
+
expect(actionDataToUrl(undefined)).toBeNull();
|
|
104
|
+
expect(actionDataToUrl("{not json")).toBeNull();
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
describe("getSamsungAppControlUrl", () => {
|
|
109
|
+
it("returns null outside Tizen", () => {
|
|
110
|
+
expect(getSamsungAppControlUrl()).toBeNull();
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns null when the app was launched without PAYLOAD", () => {
|
|
114
|
+
mockTizen([]);
|
|
115
|
+
expect(getSamsungAppControlUrl()).toBeNull();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("returns null when there is no requested app control", () => {
|
|
119
|
+
mockTizen();
|
|
120
|
+
expect(getSamsungAppControlUrl()).toBeNull();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("returns the deep link from PAYLOAD", () => {
|
|
124
|
+
mockTizen(payload(DEEP_LINK));
|
|
125
|
+
expect(getSamsungAppControlUrl()).toBe(DEEP_LINK);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("does not throw when getRequestedAppControl throws", () => {
|
|
129
|
+
// @ts-ignore
|
|
130
|
+
window.tizen = {
|
|
131
|
+
application: {
|
|
132
|
+
getCurrentApplication: () => ({
|
|
133
|
+
getRequestedAppControl: () => {
|
|
134
|
+
throw new Error("NotFoundError");
|
|
135
|
+
},
|
|
136
|
+
}),
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
expect(getSamsungAppControlUrl()).toBeNull();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("addSamsungAppControlListener", () => {
|
|
145
|
+
it("is a no-op outside Tizen", () => {
|
|
146
|
+
const onUrl = jest.fn();
|
|
147
|
+
const remove = addSamsungAppControlListener(onUrl);
|
|
148
|
+
window.dispatchEvent(new Event("appcontrol"));
|
|
149
|
+
expect(onUrl).not.toHaveBeenCalled();
|
|
150
|
+
remove();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("calls onUrl with the deep link on appcontrol event and unsubscribes", () => {
|
|
154
|
+
mockTizen(payload(DEEP_LINK));
|
|
155
|
+
const onUrl = jest.fn();
|
|
156
|
+
const remove = addSamsungAppControlListener(onUrl);
|
|
157
|
+
|
|
158
|
+
window.dispatchEvent(new Event("appcontrol"));
|
|
159
|
+
expect(onUrl).toHaveBeenCalledWith(DEEP_LINK);
|
|
160
|
+
|
|
161
|
+
remove();
|
|
162
|
+
window.dispatchEvent(new Event("appcontrol"));
|
|
163
|
+
expect(onUrl).toHaveBeenCalledTimes(1);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("ignores appcontrol events without a deep link", () => {
|
|
167
|
+
mockTizen([]);
|
|
168
|
+
const onUrl = jest.fn();
|
|
169
|
+
const remove = addSamsungAppControlListener(onUrl);
|
|
170
|
+
window.dispatchEvent(new Event("appcontrol"));
|
|
171
|
+
expect(onUrl).not.toHaveBeenCalled();
|
|
172
|
+
remove();
|
|
173
|
+
});
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
describe("deduplication", () => {
|
|
177
|
+
it("returns the same PAYLOAD only once until a launch without PAYLOAD", () => {
|
|
178
|
+
const tizenData = payload(DEEP_LINK);
|
|
179
|
+
const getRequested = mockTizen(tizenData);
|
|
180
|
+
|
|
181
|
+
expect(getSamsungAppControlUrl()).toBe(DEEP_LINK);
|
|
182
|
+
expect(getSamsungAppControlUrl()).toBeNull();
|
|
183
|
+
|
|
184
|
+
// App opened from the icon: app control without PAYLOAD
|
|
185
|
+
getRequested.mockReturnValueOnce({
|
|
186
|
+
appControl: { operation: "default", data: [] },
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
expect(getSamsungAppControlUrl()).toBeNull();
|
|
190
|
+
expect(getSamsungAppControlUrl()).toBe(DEEP_LINK);
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
it("handles a different PAYLOAD right away", () => {
|
|
194
|
+
const getRequested = mockTizen(payload(DEEP_LINK));
|
|
195
|
+
expect(getSamsungAppControlUrl()).toBe(DEEP_LINK);
|
|
196
|
+
|
|
197
|
+
const other = DEEP_LINK.replace(/488059942906/g, "111");
|
|
198
|
+
|
|
199
|
+
getRequested.mockReturnValue({
|
|
200
|
+
appControl: { operation: "default", data: payload(other) },
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
expect(getSamsungAppControlUrl()).toBe(other);
|
|
204
|
+
});
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
describe("visibilitychange (app brought back to foreground)", () => {
|
|
208
|
+
it("calls onUrl when the page becomes visible with a new PAYLOAD", () => {
|
|
209
|
+
mockTizen(payload(DEEP_LINK));
|
|
210
|
+
const onUrl = jest.fn();
|
|
211
|
+
const remove = addSamsungAppControlListener(onUrl);
|
|
212
|
+
|
|
213
|
+
setVisibility(true);
|
|
214
|
+
expect(onUrl).not.toHaveBeenCalled();
|
|
215
|
+
|
|
216
|
+
setVisibility(false);
|
|
217
|
+
expect(onUrl).toHaveBeenCalledWith(DEEP_LINK);
|
|
218
|
+
|
|
219
|
+
remove();
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("does not reopen the same deep link on a plain return to the app", () => {
|
|
223
|
+
mockTizen(payload(DEEP_LINK));
|
|
224
|
+
const onUrl = jest.fn();
|
|
225
|
+
const remove = addSamsungAppControlListener(onUrl);
|
|
226
|
+
|
|
227
|
+
setVisibility(false);
|
|
228
|
+
setVisibility(true);
|
|
229
|
+
setVisibility(false);
|
|
230
|
+
expect(onUrl).toHaveBeenCalledTimes(1);
|
|
231
|
+
|
|
232
|
+
remove();
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
it("handles appcontrol + visibilitychange for one launch only once", () => {
|
|
236
|
+
mockTizen(payload(DEEP_LINK));
|
|
237
|
+
const onUrl = jest.fn();
|
|
238
|
+
const remove = addSamsungAppControlListener(onUrl);
|
|
239
|
+
|
|
240
|
+
window.dispatchEvent(new Event("appcontrol"));
|
|
241
|
+
setVisibility(false);
|
|
242
|
+
expect(onUrl).toHaveBeenCalledTimes(1);
|
|
243
|
+
|
|
244
|
+
remove();
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it("does not react after unsubscribe", () => {
|
|
248
|
+
mockTizen(payload(DEEP_LINK));
|
|
249
|
+
const onUrl = jest.fn();
|
|
250
|
+
addSamsungAppControlListener(onUrl)();
|
|
251
|
+
|
|
252
|
+
setVisibility(false);
|
|
253
|
+
expect(onUrl).not.toHaveBeenCalled();
|
|
254
|
+
});
|
|
255
|
+
});
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Samsung (Tizen) deep links.
|
|
3
|
+
*
|
|
4
|
+
* Samsung delivers deep links (Smart Hub Preview, Universal Guide, search, voice,
|
|
5
|
+
* partner launchers) as an app control with a `PAYLOAD` data key:
|
|
6
|
+
* PAYLOAD = '{"values":"<action data>"}'
|
|
7
|
+
*
|
|
8
|
+
* - Cold start: the app control that launched the app is available through
|
|
9
|
+
* tizen.application.getCurrentApplication().getRequestedAppControl().
|
|
10
|
+
* - App already running (multitasking): the platform fires a window "appcontrol"
|
|
11
|
+
* event and getRequestedAppControl() returns the new app control.
|
|
12
|
+
*
|
|
13
|
+
* The action data may be a full deep link (`<scheme>://open?feed_locator=...&id=...`),
|
|
14
|
+
* a bare query string (`feed_locator=...&id=...`) or a JSON object with a `url`,
|
|
15
|
+
* `deep_link` or `deeplink` field. It is normalized to a URL handled by
|
|
16
|
+
* URLSchemeListener (routing is by host, e.g. "open" / "present").
|
|
17
|
+
*/
|
|
18
|
+
import { getAppUrlScheme } from "@applicaster/zapp-react-native-utils/appDataUtils";
|
|
19
|
+
|
|
20
|
+
import { log_debug, log_error, log_info } from "../logger";
|
|
21
|
+
|
|
22
|
+
export const PAYLOAD_KEY = "PAYLOAD";
|
|
23
|
+
|
|
24
|
+
export const APP_CONTROL_EVENT = "appcontrol";
|
|
25
|
+
|
|
26
|
+
type AppControlData = { key: string; value?: string[] };
|
|
27
|
+
|
|
28
|
+
const SCHEME_REGEX = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
29
|
+
|
|
30
|
+
const getTizenApplication = () => {
|
|
31
|
+
try {
|
|
32
|
+
// @ts-ignore tizen is a global injected by the Tizen web runtime
|
|
33
|
+
const tizenApi = typeof window !== "undefined" ? window.tizen : undefined;
|
|
34
|
+
|
|
35
|
+
return tizenApi?.application?.getCurrentApplication?.() ?? null;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
log_debug("SamsungAppControl: Tizen application API is not available", {
|
|
38
|
+
error,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const parseJSON = (value: string): unknown => {
|
|
46
|
+
try {
|
|
47
|
+
return JSON.parse(value);
|
|
48
|
+
} catch {
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Converts Samsung action data into a deep link URL, or null if it is not one.
|
|
55
|
+
*/
|
|
56
|
+
export const actionDataToUrl = (raw: unknown, depth = 0): string | null => {
|
|
57
|
+
if (depth > 6 || raw === null || raw === undefined) return null;
|
|
58
|
+
|
|
59
|
+
if (typeof raw === "object") {
|
|
60
|
+
const data = raw as Record<string, unknown>;
|
|
61
|
+
|
|
62
|
+
return actionDataToUrl(
|
|
63
|
+
data.url ?? data.deep_link ?? data.deeplink ?? data.values,
|
|
64
|
+
depth + 1
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (typeof raw !== "string") return null;
|
|
69
|
+
|
|
70
|
+
const value = raw.trim();
|
|
71
|
+
|
|
72
|
+
if (!value) return null;
|
|
73
|
+
|
|
74
|
+
if (value.startsWith("{")) {
|
|
75
|
+
const parsed = parseJSON(value);
|
|
76
|
+
|
|
77
|
+
if (parsed) return actionDataToUrl(parsed, depth + 1);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (SCHEME_REGEX.test(value)) {
|
|
81
|
+
// Already a deep link (<scheme>://<host>?...). http(s) links are not deep
|
|
82
|
+
// links: only their query string is used below.
|
|
83
|
+
if (!/^https?:\/\//i.test(value)) return value;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const queryIndex = value.indexOf("?");
|
|
87
|
+
const query = queryIndex >= 0 ? value.slice(queryIndex + 1) : value;
|
|
88
|
+
|
|
89
|
+
if (!query.includes("=")) return null;
|
|
90
|
+
|
|
91
|
+
return `${getAppUrlScheme()}://open?${query}`;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Key of the last app control whose PAYLOAD was handed to the app. Used to avoid
|
|
96
|
+
* opening the same deep link again when the app is merely brought back to the
|
|
97
|
+
* foreground. A launch without PAYLOAD (e.g. from the app icon) replaces the
|
|
98
|
+
* requested app control and resets this key, so the same link can be sent again.
|
|
99
|
+
*/
|
|
100
|
+
let lastHandledPayloadKey: string | null = null;
|
|
101
|
+
|
|
102
|
+
/** Test helper */
|
|
103
|
+
export const resetSamsungAppControlState = () => {
|
|
104
|
+
lastHandledPayloadKey = null;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Returns the deep link carried by the app control that launched / resumed the
|
|
109
|
+
* app, or null if there is none or it was already handled.
|
|
110
|
+
*/
|
|
111
|
+
export const getSamsungAppControlUrl = (source = "launch"): string | null => {
|
|
112
|
+
const application = getTizenApplication();
|
|
113
|
+
|
|
114
|
+
if (!application) return null;
|
|
115
|
+
|
|
116
|
+
try {
|
|
117
|
+
const requested = application.getRequestedAppControl?.();
|
|
118
|
+
const data: AppControlData[] = requested?.appControl?.data ?? [];
|
|
119
|
+
const payload = data.find((item) => item?.key === PAYLOAD_KEY);
|
|
120
|
+
|
|
121
|
+
if (!payload) {
|
|
122
|
+
lastHandledPayloadKey = null;
|
|
123
|
+
|
|
124
|
+
log_debug(
|
|
125
|
+
`SamsungAppControl[${source}]: no PAYLOAD in requested app control`,
|
|
126
|
+
{
|
|
127
|
+
operation: requested?.appControl?.operation,
|
|
128
|
+
keys: data.map((item) => item?.key),
|
|
129
|
+
}
|
|
130
|
+
);
|
|
131
|
+
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const payloadKey = JSON.stringify(payload.value ?? null);
|
|
136
|
+
|
|
137
|
+
if (payloadKey === lastHandledPayloadKey) {
|
|
138
|
+
log_debug(
|
|
139
|
+
`SamsungAppControl[${source}]: PAYLOAD already handled, skipping`,
|
|
140
|
+
{
|
|
141
|
+
payload: payload.value,
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return null;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
lastHandledPayloadKey = payloadKey;
|
|
149
|
+
|
|
150
|
+
const url = actionDataToUrl(payload.value?.[0]);
|
|
151
|
+
|
|
152
|
+
if (!url) {
|
|
153
|
+
log_error(`SamsungAppControl[${source}]: PAYLOAD is not a deep link`, {
|
|
154
|
+
payload: payload.value,
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
log_info(
|
|
161
|
+
`SamsungAppControl[${source}]: deep link from app control: ${url}`,
|
|
162
|
+
{
|
|
163
|
+
payload: payload.value,
|
|
164
|
+
}
|
|
165
|
+
);
|
|
166
|
+
|
|
167
|
+
return url;
|
|
168
|
+
} catch (error) {
|
|
169
|
+
log_error(
|
|
170
|
+
`SamsungAppControl[${source}]: failed to read requested app control`,
|
|
171
|
+
{
|
|
172
|
+
error,
|
|
173
|
+
}
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Listens for deep links sent while the app is already running.
|
|
182
|
+
*
|
|
183
|
+
* On Samsung TVs (verified on a 2023 Tizen 7 model) a running app relaunched with
|
|
184
|
+
* an app control is only brought to the foreground: the new app control is
|
|
185
|
+
* available through getRequestedAppControl(), but no "appcontrol" event is fired.
|
|
186
|
+
* So the app control is checked both on "appcontrol" (fired by some runtimes /
|
|
187
|
+
* configurations) and when the page becomes visible again.
|
|
188
|
+
*
|
|
189
|
+
* Returns an unsubscribe function.
|
|
190
|
+
*/
|
|
191
|
+
export const addSamsungAppControlListener = (
|
|
192
|
+
onUrl: (url: string) => void
|
|
193
|
+
): (() => void) => {
|
|
194
|
+
if (
|
|
195
|
+
typeof window === "undefined" ||
|
|
196
|
+
typeof document === "undefined" ||
|
|
197
|
+
typeof window.addEventListener !== "function" ||
|
|
198
|
+
typeof document.addEventListener !== "function" ||
|
|
199
|
+
!getTizenApplication()
|
|
200
|
+
) {
|
|
201
|
+
return () => {};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const check = (source: string) => {
|
|
205
|
+
const url = getSamsungAppControlUrl(source);
|
|
206
|
+
|
|
207
|
+
if (url) onUrl(url);
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const onAppControl = () => check("appcontrol");
|
|
211
|
+
|
|
212
|
+
const onVisibilityChange = () => {
|
|
213
|
+
if (!document.hidden) check("visibilitychange");
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
window.addEventListener(APP_CONTROL_EVENT, onAppControl);
|
|
217
|
+
document.addEventListener("visibilitychange", onVisibilityChange);
|
|
218
|
+
|
|
219
|
+
return () => {
|
|
220
|
+
window.removeEventListener(APP_CONTROL_EVENT, onAppControl);
|
|
221
|
+
document.removeEventListener("visibilitychange", onVisibilityChange);
|
|
222
|
+
};
|
|
223
|
+
};
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
|
|
29
29
|
import { log_error, log_info } from "../../logger";
|
|
30
30
|
import { useRivers } from "@applicaster/zapp-react-native-utils/reactHooks/state";
|
|
31
|
+
import { rememberLayoutPreview } from "../../../layoutPreview/persistence";
|
|
31
32
|
|
|
32
33
|
const getFeed = (query) => ({
|
|
33
34
|
content: { src: query.data_source },
|
|
@@ -155,6 +156,8 @@ export function usePresentSchemeHandler({
|
|
|
155
156
|
rivers_configuration_id
|
|
156
157
|
);
|
|
157
158
|
|
|
159
|
+
await rememberLayoutPreview(rivers_configuration_id);
|
|
160
|
+
|
|
158
161
|
onFinish((done) => {
|
|
159
162
|
actions?.setAppReady();
|
|
160
163
|
navigator.replace(homeRiver);
|
|
@@ -3,6 +3,7 @@ import React, {
|
|
|
3
3
|
createContext,
|
|
4
4
|
useEffect,
|
|
5
5
|
useCallback,
|
|
6
|
+
useMemo,
|
|
6
7
|
ComponentType,
|
|
7
8
|
ReactChild,
|
|
8
9
|
} from "react";
|
|
@@ -11,6 +12,10 @@ import * as ReactNative from "react-native";
|
|
|
11
12
|
import { isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
|
|
12
13
|
import { log_debug, log_error, log_info } from "../logger";
|
|
13
14
|
import { parseUrl } from "../helpers";
|
|
15
|
+
import {
|
|
16
|
+
addSamsungAppControlListener,
|
|
17
|
+
getSamsungAppControlUrl,
|
|
18
|
+
} from "../SamsungAppControl";
|
|
14
19
|
import { getAppUrlScheme } from "@applicaster/zapp-react-native-utils/appDataUtils";
|
|
15
20
|
import {
|
|
16
21
|
getZappPlatform,
|
|
@@ -86,6 +91,17 @@ function URLSchemeContextProvider(props: Props) {
|
|
|
86
91
|
const url = await Linking.getInitialURL();
|
|
87
92
|
|
|
88
93
|
if (isWeb()) {
|
|
94
|
+
// Samsung: deep link from the app control that launched the app
|
|
95
|
+
const samsungDeepLink = getSamsungAppControlUrl();
|
|
96
|
+
|
|
97
|
+
if (samsungDeepLink) {
|
|
98
|
+
log_info(
|
|
99
|
+
`URLSchemeContextProvider: Retrieved initial Samsung deep link: ${samsungDeepLink}`
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
return setState(samsungDeepLink);
|
|
103
|
+
}
|
|
104
|
+
|
|
89
105
|
const presentDeepLink = getPresentDeepLink(url);
|
|
90
106
|
|
|
91
107
|
if (presentDeepLink) {
|
|
@@ -179,10 +195,22 @@ function URLSchemeContextProvider(props: Props) {
|
|
|
179
195
|
handleOpenURL
|
|
180
196
|
);
|
|
181
197
|
|
|
198
|
+
// Samsung: deep links sent while the app is running ("appcontrol" event)
|
|
199
|
+
const removeSamsungAppControlListener = addSamsungAppControlListener(
|
|
200
|
+
(appControlUrl) => {
|
|
201
|
+
log_info(
|
|
202
|
+
`URLSchemeContextProvider: Samsung appcontrol deep link received — url=${appControlUrl}`
|
|
203
|
+
);
|
|
204
|
+
|
|
205
|
+
setState(appControlUrl);
|
|
206
|
+
}
|
|
207
|
+
);
|
|
208
|
+
|
|
182
209
|
return () => {
|
|
183
210
|
log_debug("URLSchemeContextProvider: Remove URL event listeners");
|
|
184
211
|
listener.remove();
|
|
185
212
|
linkingListener.remove();
|
|
213
|
+
removeSamsungAppControlListener();
|
|
186
214
|
};
|
|
187
215
|
}
|
|
188
216
|
|
|
@@ -196,9 +224,11 @@ function URLSchemeContextProvider(props: Props) {
|
|
|
196
224
|
};
|
|
197
225
|
}, []);
|
|
198
226
|
|
|
227
|
+
const contextValue = useMemo(() => ({ url: state, done }), [state, done]);
|
|
228
|
+
|
|
199
229
|
return (
|
|
200
230
|
<View style={fillStyles.container}>
|
|
201
|
-
<URLSchemeContext.Provider value={
|
|
231
|
+
<URLSchemeContext.Provider value={contextValue}>
|
|
202
232
|
{props.children}
|
|
203
233
|
</URLSchemeContext.Provider>
|
|
204
234
|
</View>
|