@applicaster/quick-brick-core 17.0.0-alpha.1984136194 → 17.0.0-alpha.5804334713

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.
@@ -12,10 +12,6 @@ import * as ReactNative from "react-native";
12
12
  import { isWeb } from "@applicaster/zapp-react-native-utils/reactUtils";
13
13
  import { log_debug, log_error, log_info } from "../logger";
14
14
  import { parseUrl } from "../helpers";
15
- import {
16
- addSamsungAppControlListener,
17
- getSamsungAppControlUrl,
18
- } from "../SamsungAppControl";
19
15
  import { getAppUrlScheme } from "@applicaster/zapp-react-native-utils/appDataUtils";
20
16
  import {
21
17
  getZappPlatform,
@@ -91,17 +87,6 @@ function URLSchemeContextProvider(props: Props) {
91
87
  const url = await Linking.getInitialURL();
92
88
 
93
89
  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
-
105
90
  const presentDeepLink = getPresentDeepLink(url);
106
91
 
107
92
  if (presentDeepLink) {
@@ -195,22 +180,10 @@ function URLSchemeContextProvider(props: Props) {
195
180
  handleOpenURL
196
181
  );
197
182
 
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
-
209
183
  return () => {
210
184
  log_debug("URLSchemeContextProvider: Remove URL event listeners");
211
185
  listener.remove();
212
186
  linkingListener.remove();
213
- removeSamsungAppControlListener();
214
187
  };
215
188
  }
216
189
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/quick-brick-core",
3
- "version": "17.0.0-alpha.1984136194",
3
+ "version": "17.0.0-alpha.5804334713",
4
4
  "description": "Core package for Applicaster's Quick Brick App",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,13 +28,13 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "17.0.0-alpha.1984136194",
32
- "@applicaster/quick-brick-core-plugins": "17.0.0-alpha.1984136194",
33
- "@applicaster/zapp-pipes-v2-client": "17.0.0-alpha.1984136194",
34
- "@applicaster/zapp-react-native-bridge": "17.0.0-alpha.1984136194",
35
- "@applicaster/zapp-react-native-redux": "17.0.0-alpha.1984136194",
36
- "@applicaster/zapp-react-native-ui-components": "17.0.0-alpha.1984136194",
37
- "@applicaster/zapp-react-native-utils": "17.0.0-alpha.1984136194",
31
+ "@applicaster/applicaster-types": "17.0.0-alpha.5804334713",
32
+ "@applicaster/quick-brick-core-plugins": "17.0.0-alpha.5804334713",
33
+ "@applicaster/zapp-pipes-v2-client": "17.0.0-alpha.5804334713",
34
+ "@applicaster/zapp-react-native-bridge": "17.0.0-alpha.5804334713",
35
+ "@applicaster/zapp-react-native-redux": "17.0.0-alpha.5804334713",
36
+ "@applicaster/zapp-react-native-ui-components": "17.0.0-alpha.5804334713",
37
+ "@applicaster/zapp-react-native-utils": "17.0.0-alpha.5804334713",
38
38
  "atob": "^2.1.2",
39
39
  "axios": "^0.28.0",
40
40
  "btoa": "^1.2.1",
@@ -1,255 +0,0 @@
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
- });
@@ -1,223 +0,0 @@
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
- };