@applicaster/zapp-react-native-utils 12.0.0-alpha.4837064762 → 12.0.0-alpha.4840257570

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.
@@ -7,6 +7,10 @@ import { sendHeaderClickEvent } from "./sendHeaderClickEvent";
7
7
  import { sendHardwareBackButtonClickEvent } from "./sendHardwareBackButtonClickEvent";
8
8
  import { sendDownloadsEvent } from "./sendDownloadsEvent";
9
9
  import { sendScreenEvent } from "./sendScreenEvent";
10
+ import {
11
+ sendSearchCanceledEvent,
12
+ sendSearchExecutedEvent,
13
+ } from "./searchEvents";
10
14
 
11
15
  export {
12
16
  sendScreenEvent,
@@ -18,4 +22,6 @@ export {
18
22
  sendHeaderClickEvent,
19
23
  sendHardwareBackButtonClickEvent,
20
24
  sendDownloadsEvent,
25
+ sendSearchCanceledEvent,
26
+ sendSearchExecutedEvent,
21
27
  };
@@ -0,0 +1,21 @@
1
+ import { log_debug } from "../logger";
2
+ import { postAnalyticEvent } from "../manager";
3
+ import { ANALYTICS_CORE_EVENTS } from "../events";
4
+
5
+ export const sendSearchCanceledEvent = (analyticsProps) => {
6
+ const eventName = ANALYTICS_CORE_EVENTS.SEARCH_CANCELED;
7
+ postAnalyticEvent(eventName, analyticsProps);
8
+
9
+ log_debug(`sendSearchCanceledEvent: send event: ${eventName}`, {
10
+ analyticsProps,
11
+ });
12
+ };
13
+
14
+ export const sendSearchExecutedEvent = (analyticsProps) => {
15
+ const eventName = ANALYTICS_CORE_EVENTS.SEARCH_EXECUTED;
16
+ postAnalyticEvent(eventName, analyticsProps);
17
+
18
+ log_debug(`sendSearchExecutedEvent: send event: ${eventName}`, {
19
+ analyticsProps,
20
+ });
21
+ };
@@ -23,10 +23,16 @@ export const TAPPING_EVENTS = {
23
23
  TAP_NAVBAR_BACK_BUTTON: "tap_navbar_back_button",
24
24
  };
25
25
 
26
+ export const SEARCH_EVENTS = {
27
+ SEARCH_EXECUTED: "search_executed",
28
+ SEARCH_CANCELED: "search_canceled",
29
+ };
30
+
26
31
  export const ANALYTICS_CORE_EVENTS = {
27
32
  ...LEGACY_EVENTS,
28
33
  ...SCREEN_VIEW_EVENTS,
29
34
  ...TAPPING_EVENTS,
35
+ ...SEARCH_EVENTS,
30
36
  };
31
37
 
32
38
  export const WEBVIEW_EVENTS = {
@@ -18,6 +18,8 @@ import {
18
18
  sendHardwareBackButtonClickEvent,
19
19
  sendDownloadsEvent,
20
20
  sendScreenEvent,
21
+ sendSearchCanceledEvent,
22
+ sendSearchExecutedEvent,
21
23
  } from "./AnalyticsEvents";
22
24
 
23
25
  import { postAnalyticEvent } from "./manager";
@@ -66,6 +68,8 @@ type AnalyticsFunctions = {
66
68
  item: ZappEntry;
67
69
  state: OfflineItemState;
68
70
  }) => void;
71
+ sendSearchCanceledEvent: (props: AnalyticsDefaultProperties) => void;
72
+ sendSearchExecutedEvent: (props: AnalyticsDefaultProperties) => void;
69
73
  };
70
74
 
71
75
  type GetAnalyticsFunctions = (
@@ -102,6 +106,8 @@ export function getAnalyticsFunctions({
102
106
  sendHardwareBackButtonClickEvent,
103
107
  sendScreenEvent,
104
108
  sendDownloadsEvent,
109
+ sendSearchCanceledEvent,
110
+ sendSearchExecutedEvent,
105
111
  };
106
112
  }
107
113
 
@@ -1,6 +1,4 @@
1
1
  import * as R from "ramda";
2
- import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
3
-
4
2
  import { Tree } from "./treeDataStructure/Tree";
5
3
  import { findFocusableNode, findPriorityItem } from "./treeDataStructure/Utils";
6
4
  import { subscriber } from "../../functionUtils";
@@ -19,22 +17,6 @@ const isFocusEnabled = (focusableItem): boolean => {
19
17
  return true;
20
18
  };
21
19
 
22
- const waitUntil = (conditionFn) => {
23
- let interval;
24
- return new Promise((resolve, reject) => {
25
- interval = setInterval(() => {
26
- if (conditionFn()) {
27
- clearInterval(interval);
28
- resolve(undefined);
29
- }
30
- }, 1000);
31
-
32
- setTimeout(() => {
33
- reject();
34
- }, 2500);
35
- });
36
- };
37
-
38
20
  /**
39
21
  * The focusManager is meant to keep references of focusables elements in the UI, and provide
40
22
  * ways to assign the focus to one of the focusables.
@@ -64,7 +46,7 @@ export const focusManager = (function () {
64
46
  let lastFocusedId = null;
65
47
  let initialFocusOnContent = null;
66
48
  let focusDisabled = false;
67
- let focusAutoEnabled = false;
49
+ let focusAutoEnabled = true;
68
50
  const eventHandler = subscriber();
69
51
 
70
52
  function disableAutomaticFocus() {
@@ -111,7 +93,7 @@ export const focusManager = (function () {
111
93
  !currentFocusable.isGroup &&
112
94
  currentFocusable.isMounted()
113
95
  ) {
114
- currentFocusable.blur(direction);
96
+ currentFocusable.blur(currentFocusable, direction);
115
97
  currentFocusable.hasLostFocus(getCurrentFocus(), direction);
116
98
  }
117
99
  }
@@ -244,41 +226,31 @@ export const focusManager = (function () {
244
226
 
245
227
  logger.log({ message: "setInitialFocus", data: { preferredFocus } });
246
228
 
247
- if (isNilOrEmpty(preferredFocus)) {
248
- return;
249
- }
250
-
251
- const focusCandidate = preferredFocus[0];
252
- const focusableItem = focusCandidate.component;
253
-
254
- const currentFocusedId = R.path(["props", "id"], getCurrentFocus());
255
- const nextFocusedId = R.path(["props", "id"], focusableItem);
229
+ if (preferredFocus && preferredFocus.length > 0) {
230
+ const focusCandidate = preferredFocus[0];
231
+ const focusableItem = focusCandidate.component;
232
+
233
+ if (
234
+ focusableItem &&
235
+ !focusableItem.isGroup &&
236
+ isFocusEnabled(focusableItem)
237
+ ) {
238
+ if (initialFocusOnContent) {
239
+ blur();
240
+ initialFocusOnContent = false;
241
+ }
256
242
 
257
- if (currentFocusedId === nextFocusedId) {
258
- // we are going to move focus to the same node(already focused) -> nothing to do
259
- return;
260
- }
243
+ logger.log({
244
+ message: "setInitialFocus – currentFocusNode",
245
+ data: {
246
+ focusableItem,
247
+ },
248
+ });
261
249
 
262
- if (
263
- focusableItem &&
264
- !focusableItem.isGroup &&
265
- isFocusEnabled(focusableItem)
266
- ) {
267
- if (initialFocusOnContent) {
268
- blur();
269
- initialFocusOnContent = false;
250
+ currentFocusNode = focusCandidate;
251
+ focusableItem.willReceiveFocus(focusableItem, null);
252
+ focusableItem && setFocus(focusCandidate.id, null);
270
253
  }
271
-
272
- logger.log({
273
- message: "setInitialFocus – currentFocusNode",
274
- data: {
275
- focusableItem,
276
- },
277
- });
278
-
279
- currentFocusNode = focusCandidate;
280
- focusableItem.willReceiveFocus(focusableItem, null);
281
- focusableItem && setFocus(focusCandidate.id, null);
282
254
  }
283
255
  }
284
256
 
@@ -351,8 +323,6 @@ export const focusManager = (function () {
351
323
  * @returns {React.Component} the registered component
352
324
  */
353
325
  function registerGroup(id, component) {
354
- // console.log("debug_2", "FM registerGroup", { id, component });
355
-
356
326
  focusableTree.addNode({ id, component });
357
327
 
358
328
  return component;
@@ -368,8 +338,6 @@ export const focusManager = (function () {
368
338
  * @returns {React.Component} the registered component
369
339
  */
370
340
  function registerItem(id, component) {
371
- // console.log("debug_2", "FM registerItem", { id, component });
372
-
373
341
  focusableTree.addNode({ id, component });
374
342
 
375
343
  if (!currentFocusNode && id === lastFocusedId && !id.includes("menu")) {
@@ -386,8 +354,6 @@ export const focusManager = (function () {
386
354
  * @param {String} id of the group to unregister
387
355
  */
388
356
  function unregisterGroup(id) {
389
- // console.log("debug_2", "FM unregisterGroup", { id });
390
-
391
357
  focusableTree.findAndRemoveNode(id);
392
358
  }
393
359
 
@@ -399,8 +365,6 @@ export const focusManager = (function () {
399
365
  * @param {String} id of the Focusable to unregister
400
366
  */
401
367
  function unregisterItem(id) {
402
- // console.log("debug_2", "FM unregisterItem", { id });
403
-
404
368
  focusableTree.findAndRemoveNode(id);
405
369
 
406
370
  if (currentFocusNode && currentFocusNode.id === id) {
@@ -439,8 +403,6 @@ export const focusManager = (function () {
439
403
  }
440
404
 
441
405
  function setupInitialFocus(id: string, focusOnContent?: boolean) {
442
- console.log("debug_2", "FM - setupInitialFocus", { id, focusOnContent });
443
-
444
406
  if (focusOnContent) {
445
407
  initialFocusOnContent = focusOnContent;
446
408
  } else {
@@ -448,62 +410,6 @@ export const focusManager = (function () {
448
410
  }
449
411
  }
450
412
 
451
- function setFocusOnMenu() {
452
- const activeScreen = R.last(focusableTree.root.children);
453
- const navbar = R.find(
454
- (child) => R.startsWith("navbar", child.id),
455
- activeScreen.children
456
- );
457
-
458
- const selectedMenuItemId = R.find(
459
- (child) => child.component.props.selected,
460
- navbar?.children || []
461
- )?.id;
462
-
463
- if (selectedMenuItemId) {
464
- // set focus on selected menu item
465
- setFocus(selectedMenuItemId);
466
- return;
467
- }
468
-
469
- const firstMenuItemId = R.path(["children", 0, "id"], navbar);
470
-
471
- if (firstMenuItemId) {
472
- // set focus on first menu item
473
- setFocus(firstMenuItemId);
474
- }
475
- }
476
-
477
- function setFocusOnContent() {
478
- // we need to put focus on content here
479
- // For doing it we have to wait registration of first focusable node into focusableTree
480
- // this is the temporary hack with waiting until content node starts having children
481
- const getContentNode = () => {
482
- const activeScreen = R.last(focusableTree.root.children);
483
- const content = R.find(
484
- (child) => R.startsWith("content", child.id),
485
- activeScreen.children
486
- );
487
-
488
- return content;
489
- };
490
-
491
- const contentHasAnyChildren = () => {
492
- return getContentNode()?.children.length > 0;
493
- };
494
-
495
- waitUntil(contentHasAnyChildren)
496
- .then(() => {
497
- const content = getContentNode();
498
-
499
- setInitialFocus(content);
500
- })
501
- .catch(() => {
502
- // we failed to wait any content, put focus on menu as a last chance
503
- setFocusOnMenu();
504
- });
505
- }
506
-
507
413
  /**
508
414
  * moves the focus in a provided direction
509
415
  * @public
@@ -516,13 +422,6 @@ export const focusManager = (function () {
516
422
  currentFocusNode.component.willLoseFocus(currentFocusNode, direction);
517
423
  const nextFocusable = findFocusableNode(direction, currentFocusNode);
518
424
 
519
- console.log("debug_2", "FM moveFocus", {
520
- direction: direction?.value,
521
- currentFocusNode,
522
- focusableTree,
523
- nextFocusable,
524
- });
525
-
526
425
  if (nextFocusable?.component?.isMounted?.()) {
527
426
  const nextRect = nextFocusable.component.getRect();
528
427
 
@@ -619,7 +518,5 @@ export const focusManager = (function () {
619
518
  enableAutomaticFocus,
620
519
  getGroupRootById,
621
520
  isGroupItemFocused,
622
- setFocusOnMenu,
623
- setFocusOnContent,
624
521
  };
625
522
  })();
@@ -1,4 +1,4 @@
1
- import { allPass, pathOr, startsWith, isNil } from "ramda";
1
+ import * as R from "ramda";
2
2
  import { isNilOrEmpty } from "@applicaster/zapp-react-native-utils/reactUtils/helpers";
3
3
 
4
4
  import { rejectItemByPath } from "../../../../objectUtils";
@@ -143,49 +143,7 @@ const hasChildren = (target) =>
143
143
 
144
144
  const hasComponent = (target) => !!target?.component;
145
145
 
146
- const hasComponentAndChildren = allPass([hasComponent, hasChildren]);
147
-
148
- const positiveOrZero = (value) => value >= 0;
149
-
150
- const excludeFromFocusSearching = pathOr(false, [
151
- "component",
152
- "props",
153
- "excludeFromFocusSearching",
154
- ]);
155
-
156
- // const isAllScreens = all(excludeFromFocusSearching);
157
-
158
- const isNavBar = (target) => startsWith("navbar", target?.id);
159
-
160
- const ignoreMenuIfMultipleTargets = (nodes) => {
161
- if (nodes.length > 1 && nodes.map((n) => n?.target?.id).includes("navbar")) {
162
- return nodes.filter((n) => !isNavBar(n?.target));
163
- }
164
-
165
- return nodes;
166
- };
167
-
168
- const toDistance = ({ target, direction, comparisonRect }) => {
169
- const componentRect = target.component?.getRect();
170
-
171
- const orientationDistance = measureDistance(
172
- direction,
173
- comparisonRect,
174
- componentRect
175
- );
176
-
177
- if (orientationDistance === -1) {
178
- // ignores offscreen targets when moving up/down
179
- return null;
180
- }
181
-
182
- const euclidDistance = measureEuclidianDistanceToCenter(
183
- comparisonRect,
184
- componentRect
185
- );
186
-
187
- return orientationDistance > 0 ? euclidDistance : orientationDistance;
188
- };
146
+ const hasComponentAndChildren = R.allPass([hasComponent, hasChildren]);
189
147
 
190
148
  /**
191
149
  * Get target in shortest distance in specified direction
@@ -215,32 +173,52 @@ export function getTargetInDirection(
215
173
 
216
174
  const distances = targets
217
175
  .filter(hasComponentAndChildren)
218
- .filter((target) => !excludeFromFocusSearching(target))
219
- .map((target) => ({
220
- distance: toDistance({ target, direction, comparisonRect }),
221
- target,
222
- }))
223
- .filter(({ distance }) => positiveOrZero(distance));
176
+ .map((target) => {
177
+ const componentRect = target.component?.getRect();
178
+
179
+ const distance = measureDistance(
180
+ direction,
181
+ comparisonRect,
182
+ componentRect
183
+ );
184
+
185
+ if (distance === -1) {
186
+ // ignores offscreen targets when moving up/down
187
+ return null;
188
+ }
224
189
 
225
- // if (isAllScreens(targets)) {
226
- // return last(targets);
227
- // }
190
+ const orientationDistance = measureDistance(
191
+ direction,
192
+ comparisonRect,
193
+ componentRect
194
+ );
195
+
196
+ const euclidDistance = measureEuclidianDistanceToCenter(
197
+ comparisonRect,
198
+ componentRect
199
+ );
200
+
201
+ return {
202
+ distance:
203
+ orientationDistance > 0 ? euclidDistance : orientationDistance,
204
+ target,
205
+ };
206
+ })
207
+ .filter((obj) => obj && obj.distance >= 0);
228
208
 
229
209
  const match = distances
230
210
  .sort((a, b) => a.distance - b.distance) // sorts the distances
231
211
  .filter(ignoreMenuIfMultipleTargets)[0]; // filters out multiple targets if necessary // gets the first element
232
212
 
233
- // console.log("debug_2", "FM getTargetInDirection", {
234
- // direction: direction?.value,
235
- // currentNode,
236
- // targetNodes,
237
- // targets,
238
- // comparisonRect,
239
- // distances,
240
- // match,
241
- // });
242
-
243
- return match?.target;
213
+ return distances && distances.length > 0 ? match.target : null;
214
+ }
215
+
216
+ function ignoreMenuIfMultipleTargets(nodes) {
217
+ if (nodes.length > 1 && nodes.map((n) => n?.target?.id).includes("navbar")) {
218
+ return nodes.filter((n) => n?.target?.id !== "navbar");
219
+ }
220
+
221
+ return nodes;
244
222
  }
245
223
 
246
224
  /**
@@ -331,36 +309,3 @@ export function hasPriorityInDirection(direction, component) {
331
309
 
332
310
  return (prioritiseFocusOn & mappedDirection) > 0;
333
311
  }
334
-
335
- export const getNodeBeforeRoot = (node) => {
336
- if (isNil(node)) {
337
- return undefined;
338
- }
339
-
340
- if (node.parent.id === "root") {
341
- return node;
342
- }
343
-
344
- return getNodeBeforeRoot(node.parent);
345
- };
346
-
347
- export const isAllowedToMoveTo = (currentNode, nextNode) => {
348
- const screenForCurrentNode = getNodeBeforeRoot(currentNode);
349
- const screenForNextNode = getNodeBeforeRoot(nextNode);
350
-
351
- return screenForCurrentNode.id === screenForNextNode.id;
352
-
353
- // console.log("debug_2", "isAllowedToMoveTo", {
354
- // nodeBeforeRoot,
355
- // });
356
-
357
- // if (nodeBeforeRoot?.id === "navbar") {
358
- // return true;
359
- // }
360
-
361
- // if (nodeBeforeRoot?.component?.props?.isActive) {
362
- // return true;
363
- // }
364
-
365
- // return false;
366
- };
@@ -1,6 +1,7 @@
1
1
  /* global tizen */
2
2
  const isTizen = typeof tizen !== "undefined";
3
3
 
4
+ import { isVizioPlatform } from "@applicaster/zapp-react-dom-app/App/Loader/utils/platform";
4
5
  import * as platformKeys from "./keysByPlatform";
5
6
  import { Platform } from "react-native";
6
7
 
@@ -21,6 +22,13 @@ export const ARROW_KEYS = [
21
22
  KEYS.ArrowRight,
22
23
  ];
23
24
 
25
+ /**
26
+ * Platform.OS currently identifies Vizio as LG_TV, therefore we are using isVizioPlatform to identify platform and add keys.
27
+ */
28
+ if (isVizioPlatform()) {
29
+ Object.assign(KEYS, platformKeys.vizio);
30
+ }
31
+
24
32
  /**
25
33
  * In TizenOS we need to register all of the keys that we are going to use.
26
34
  * You need to register using the name Tizen has for the key.
@@ -102,4 +102,14 @@ const tvos = {
102
102
  Menu: { code: "Menu", eventType: "menu" },
103
103
  };
104
104
 
105
- export { lg_tv, samsung_tv, web, tvos, android_tv };
105
+ const vizio = {
106
+ ...samsung_tv,
107
+ Exit: { code: "Exit", keyCode: 27 },
108
+ PageUp: { code: "PageUp", keyCode: 33 },
109
+ PageDown: { code: "PageDown", keyCode: 34 },
110
+ MediaTrackNext: { code: "MediaTrackNext", keyCode: 418 },
111
+ MediaTrackPrevious: { code: "MediaTrackPrevious", keyCode: 419 },
112
+ PrevCh: { code: "PrevCh", keyCode: 500 },
113
+ };
114
+
115
+ export { lg_tv, samsung_tv, web, tvos, android_tv, vizio };
@@ -24,7 +24,6 @@ import {
24
24
  EMPTY,
25
25
  FALLBACK,
26
26
  TRANSFORM,
27
- defaultColor,
28
27
  } from "./fixtures";
29
28
 
30
29
  describe("getKeyForLabel", () => {
@@ -395,9 +394,7 @@ describe("getColorFromData", () => {
395
394
  it("should return value directly if valid color", () => {
396
395
  const result = getColorFromData({
397
396
  data: entry,
398
- key: "background_color",
399
- getValue: getValue(componentStyle),
400
- defaultColor,
397
+ valueFromLayout: "#123456",
401
398
  });
402
399
 
403
400
  expect(result).toBe("#123456");
@@ -406,9 +403,7 @@ describe("getColorFromData", () => {
406
403
  it("should return valid rgba color name", () => {
407
404
  const result = getColorFromData({
408
405
  data: entry,
409
- key: "component_background_color",
410
- getValue: getValue(componentStyle),
411
- defaultColor,
406
+ valueFromLayout: "rgba(255, 255, 255, 0)",
412
407
  });
413
408
 
414
409
  expect(result).toBe("rgba(255, 255, 255, 0)");
@@ -417,33 +412,27 @@ describe("getColorFromData", () => {
417
412
  it("should return path value if path value is set and is valid color", () => {
418
413
  const result = getColorFromData({
419
414
  data: entry,
420
- key: VAL.colorKeyPath,
421
- getValue: getValue(componentStyle),
422
- defaultColor,
415
+ valueFromLayout: "background_color",
423
416
  });
424
417
 
425
418
  expect(result).toBe("#123456");
426
419
  });
427
420
 
428
- it("should return defaultColor if key value and path value are invalid", () => {
421
+ it("should return path value if path value is set, nested and is valid color", () => {
429
422
  const result = getColorFromData({
430
423
  data: entry,
431
- key: VAL.invalidColorKey,
432
- getValue: getValue(componentStyle),
433
- defaultColor,
424
+ valueFromLayout: "extensions.background_color",
434
425
  });
435
426
 
436
- expect(result).toBe(defaultColor);
427
+ expect(result).toBe("#123456");
437
428
  });
438
429
 
439
- it("should return defaultColor if path does not exist", () => {
430
+ it("should return valueFromLayout if path does not exist", () => {
440
431
  const result = getColorFromData({
441
432
  data: entry,
442
- key: VAL.invalidColorKeyPath,
443
- getValue: getValue(componentStyle),
444
- defaultColor,
433
+ valueFromLayout: "invalid_color",
445
434
  });
446
435
 
447
- expect(result).toBe(defaultColor);
436
+ expect(result).toBe("invalid_color");
448
437
  });
449
438
  });
@@ -447,30 +447,24 @@ export const toResizeMode = (
447
447
 
448
448
  type GetColorFromData = {
449
449
  data: Record<string, any>;
450
- key: string;
451
- getValue: Function;
452
- defaultColor: string;
450
+ valueFromLayout: string;
453
451
  };
454
452
 
455
453
  export const getColorFromData = ({
456
454
  data,
457
- key,
458
- getValue,
459
- defaultColor,
455
+ valueFromLayout,
460
456
  }: GetColorFromData): string => {
461
- const value = getValue(key);
462
-
463
457
  // Temporary hack to fix color validation when alpha is floating point number
464
458
  // https://github.com/dreamyguy/validate-color/issues/44
465
- if (validateColor(value.replace(".00", ""))) {
466
- return value;
459
+ if (validateColor(valueFromLayout.replace(".00", ""))) {
460
+ return valueFromLayout;
467
461
  }
468
462
 
469
- const pathValue = R.path(value.split("."), data);
463
+ const pathValue = R.path(valueFromLayout.split("."), data);
470
464
 
471
465
  if (pathValue && validateColor(pathValue)) {
472
466
  return pathValue;
473
467
  }
474
468
 
475
- return defaultColor;
469
+ return valueFromLayout;
476
470
  };
@@ -0,0 +1,11 @@
1
+ import dayjs from "dayjs";
2
+ import utc from "dayjs/plugin/utc";
3
+ import timezone from "dayjs/plugin/timezone";
4
+ import isoWeek from "dayjs/plugin/isoWeek";
5
+
6
+ // Extend dayjs with plugins
7
+ dayjs.extend(utc);
8
+ dayjs.extend(timezone);
9
+ dayjs.extend(isoWeek);
10
+
11
+ export { dayjs };
@@ -28,5 +28,6 @@ export const getAppDataHeaders = (screenId: string) => {
28
28
  "x-applicaster-signed-device-info-token": appData.signedDeviceInfoToken,
29
29
  "x-applicaster-device-make": appData.deviceMake,
30
30
  "x-applicaster-device-model": appData.deviceModel,
31
+ "x-applicaster-uuid": appData?.uuid,
31
32
  };
32
33
  };
@@ -1,4 +1,4 @@
1
- import { last, head, isNil, includes, all, pathOr } from "ramda";
1
+ import { last, head, isNil, includes, pathOr } from "ramda";
2
2
 
3
3
  const stripBrackets = (path) => {
4
4
  const withoutPrepened = last(path.split("{{"));
@@ -13,15 +13,15 @@ const hasValue = (context) => (path) => {
13
13
  return !isNil(value);
14
14
  };
15
15
 
16
- export const isEnoughDataToInflateString = (context, string): boolean => {
16
+ export const getMissingValues = (context, string): string[] => {
17
17
  if (!includes("{{", string)) {
18
- return true;
18
+ return [];
19
19
  }
20
20
 
21
21
  const SEARCH_REGEX = new RegExp(/{{.*?}}/g);
22
22
  const paths = string.toString().match(SEARCH_REGEX);
23
23
 
24
- return all(hasValue(context), paths);
24
+ return paths.filter((path) => !hasValue(context)(path));
25
25
  };
26
26
 
27
27
  export const isNonInflatedUrl = (url: string): boolean =>
File without changes
@@ -1,59 +1,59 @@
1
- import * as ReactNative from "react-native";
2
-
3
- const mocked_state = {
4
- remoteConfigurations: { localizations: {} },
5
- appData: {
6
- languageCode: "en",
7
- countryCode: "US",
8
- },
9
- };
10
-
11
- let mocked_locale = ["en-US"];
12
-
13
- jest.doMock("react-redux", () => ({
14
- useSelector: jest.fn((fn) => fn(mocked_state)),
15
- }));
1
+ import { NativeModules } from "react-native";
16
2
 
17
- const SettingsManager = {
3
+ const settingsManagerMock = {
18
4
  getConstants: jest.fn(() => ({
19
- settings: { AppleLanguages: mocked_locale },
5
+ settings: { AppleLanguages: ["en-US"] },
20
6
  })),
21
7
  };
22
8
 
23
- const QuickBrickCommunicationModule = {
9
+ const qbCommunicationModuleMock = {
24
10
  languageLocale: "en",
25
11
  countryLocale: "US",
12
+ uiLanguage: "en-US",
26
13
  };
27
14
 
28
- ReactNative.NativeModules.SettingsManager = SettingsManager;
29
-
30
- ReactNative.NativeModules.QuickBrickCommunicationModule =
31
- QuickBrickCommunicationModule;
32
-
33
- const { useIsRTL } = require("../index");
15
+ NativeModules.SettingsManager = settingsManagerMock;
16
+ NativeModules.QuickBrickCommunicationModule = qbCommunicationModuleMock;
34
17
 
35
18
  describe("localizationUtils", () => {
19
+ beforeEach(() => {
20
+ jest.resetModules();
21
+ });
22
+
36
23
  describe("when the app has a single locale", () => {
37
24
  describe("when the app has only a RTL language as locale", () => {
38
25
  beforeEach(() => {
39
- mocked_state.remoteConfigurations.localizations = {
40
- he: "http://url.com",
26
+ jest.resetModules();
27
+ const RN = require("react-native");
28
+
29
+ RN.NativeModules.QuickBrickCommunicationModule = {
30
+ languageLocale: "he",
31
+ countryLocale: "IL",
32
+ uiLanguage: "he-HE",
41
33
  };
42
34
  });
43
35
 
44
36
  it("useIsRTL returns true", () => {
37
+ const { useIsRTL } = require("../index");
45
38
  expect(useIsRTL()).toBe(true);
46
39
  });
47
40
  });
48
41
 
49
42
  describe("when the app has only a LTR language as locale", () => {
50
43
  beforeEach(() => {
51
- mocked_state.remoteConfigurations.localizations = {
52
- en: "http://url.com",
44
+ jest.resetModules();
45
+
46
+ const RN = require("react-native");
47
+
48
+ RN.NativeModules.QuickBrickCommunicationModule = {
49
+ languageLocale: "en",
50
+ countryLocale: "US",
51
+ uiLanguage: "en",
53
52
  };
54
53
  });
55
54
 
56
55
  it("useIsRTL returns false", () => {
56
+ const { useIsRTL } = require("../index");
57
57
  expect(useIsRTL()).toBe(false);
58
58
  });
59
59
  });
@@ -63,45 +63,56 @@ describe("localizationUtils", () => {
63
63
  describe("when device locale is in the app's locales", () => {
64
64
  describe("when device locale is LTR", () => {
65
65
  beforeEach(() => {
66
- mocked_state.remoteConfigurations.localizations = {
67
- he: "http://url.com",
68
- en: "http://url.com",
69
- };
66
+ jest.resetModules();
67
+
68
+ const RN = require("react-native");
70
69
 
71
- mocked_locale = ["en-US"];
70
+ RN.NativeModules.QuickBrickCommunicationModule = {
71
+ languageLocale: "en",
72
+ countryLocale: "US",
73
+ uiLanguage: "en-US",
74
+ };
72
75
  });
73
76
 
74
77
  it("useIsRTL returns false", () => {
78
+ const { useIsRTL } = require("../index");
75
79
  expect(useIsRTL()).toBe(false);
76
80
  });
77
81
  });
78
82
 
79
83
  describe("when device locale is RTL", () => {
80
84
  beforeEach(() => {
81
- mocked_state.remoteConfigurations.localizations = {
82
- he: "http://url.com",
83
- en: "http://url.com",
84
- };
85
+ jest.resetModules();
86
+ const RN = require("react-native");
85
87
 
86
- mocked_locale = ["he-HE"];
88
+ RN.NativeModules.QuickBrickCommunicationModule = {
89
+ languageLocale: "he",
90
+ countryLocale: "IL",
91
+ uiLanguage: "he",
92
+ };
87
93
  });
88
94
 
89
95
  it("useIsRTL returns true", () => {
96
+ const { useIsRTL } = require("../index");
90
97
  expect(useIsRTL()).toBe(true);
91
98
  });
92
99
  });
93
100
 
94
101
  describe("when a device has a secondary language in a supported language", () => {
95
102
  beforeEach(() => {
96
- mocked_state.remoteConfigurations.localizations = {
97
- en: "http://url.com",
98
- he: "http://url.com",
99
- };
103
+ jest.resetModules();
104
+
105
+ const RN = require("react-native");
100
106
 
101
- mocked_locale = ["es-SP", "he-HE"];
107
+ RN.NativeModules.QuickBrickCommunicationModule = {
108
+ languageLocale: "he",
109
+ countryLocale: "IL",
110
+ uiLanguage: "he",
111
+ };
102
112
  });
103
113
 
104
114
  it("useIsRTL returns true", () => {
115
+ const { useIsRTL } = require("../index");
105
116
  expect(useIsRTL()).toBe(true);
106
117
  });
107
118
  });
@@ -110,30 +121,38 @@ describe("localizationUtils", () => {
110
121
  describe("when device locale is not in the app's locales", () => {
111
122
  describe("when first app locale is LTR", () => {
112
123
  beforeEach(() => {
113
- mocked_state.remoteConfigurations.localizations = {
114
- en: "http://url.com",
115
- he: "http://url.com",
116
- };
124
+ jest.resetModules();
125
+
126
+ const RN = require("react-native");
117
127
 
118
- mocked_locale = ["fr-FR"];
128
+ RN.NativeModules.QuickBrickCommunicationModule = {
129
+ languageLocale: "fr",
130
+ countryLocale: "FR",
131
+ uiLanguage: "fr-FR",
132
+ };
119
133
  });
120
134
 
121
135
  it("useIsRTL returns false", () => {
136
+ const { useIsRTL } = require("../index");
122
137
  expect(useIsRTL()).toBe(false);
123
138
  });
124
139
  });
125
140
 
126
141
  describe("when first app locale is RTL", () => {
127
142
  beforeEach(() => {
128
- mocked_state.remoteConfigurations.localizations = {
129
- he: "http://url.com",
130
- en: "http://url.com",
131
- };
143
+ jest.resetModules();
144
+
145
+ const RN = require("react-native");
132
146
 
133
- mocked_locale = ["fr-FR"];
147
+ RN.NativeModules.QuickBrickCommunicationModule = {
148
+ languageLocale: "he",
149
+ countryLocale: "HE",
150
+ uiLanguage: "he",
151
+ };
134
152
  });
135
153
 
136
154
  it("useIsRTL returns true", () => {
155
+ const { useIsRTL } = require("../index");
137
156
  expect(useIsRTL()).toBe(true);
138
157
  });
139
158
  });
@@ -1,10 +1,9 @@
1
1
  import * as R from "ramda";
2
2
  import { ViewStyle } from "react-native";
3
- import { useSelector } from "react-redux";
4
3
 
5
- import { getDeviceLocales } from "./deviceLocales";
6
4
  import { useMemo } from "react";
7
5
  import { getUILanguage } from "../appUtils/localizationsHelper";
6
+ import { utilsLogger } from "../logger";
8
7
 
9
8
  export {
10
9
  getLocale,
@@ -45,34 +44,26 @@ export function useLocalizedStrings({ localizations }) {
45
44
  return result;
46
45
  }
47
46
 
48
- function getLocaleToUse(languageCodes, localizations) {
49
- const matchingLanguages = languageCodes.filter(
50
- R.contains(R.__, localizations)
51
- );
47
+ export function getIsRTL(): boolean {
48
+ const uiLanguage = getUILanguage();
49
+
50
+ if (!uiLanguage) {
51
+ utilsLogger.error({ message: "getUILanguage returned null or undefined" });
52
52
 
53
- if (matchingLanguages?.length) {
54
- return R.head(matchingLanguages);
53
+ return false;
55
54
  }
56
55
 
57
- return R.head(localizations);
58
- }
56
+ const [languageCode] = uiLanguage.split("-");
59
57
 
60
- export function getIsRTLFromLocalizations(localizations): boolean {
61
- const deviceLocales = getDeviceLocales() || [];
62
- const localeToUse = getLocaleToUse(deviceLocales, R.keys(localizations));
58
+ const lowerCaseLanguageCode = languageCode.toLocaleLowerCase();
63
59
 
64
- return R.contains(localeToUse, RTL_LOCALES);
65
- }
60
+ const isRTL = R.contains(lowerCaseLanguageCode, RTL_LOCALES);
66
61
 
67
- const remoteConfigLocalizationSelector = R.path([
68
- "remoteConfigurations",
69
- "localizations",
70
- ]);
62
+ return isRTL;
63
+ }
71
64
 
72
65
  export function useIsRTL(): boolean {
73
- const localizations = useSelector(remoteConfigLocalizationSelector);
74
-
75
- return getIsRTLFromLocalizations(localizations);
66
+ return getIsRTL();
76
67
  }
77
68
 
78
69
  export const applyRTLStylesIfNeeded = (
@@ -75,3 +75,7 @@ export const isLikeNumber = (value: any): boolean => {
75
75
 
76
76
  export const isZero = (value: unknown): boolean =>
77
77
  Object.is(value, -0) || Object.is(value, 0);
78
+
79
+ export const isMinusZero = (value: number) => {
80
+ return 1 / value === -Infinity;
81
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-native-utils",
3
- "version": "12.0.0-alpha.4837064762",
3
+ "version": "12.0.0-alpha.4840257570",
4
4
  "description": "Applicaster Zapp React Native utilities package",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -28,7 +28,7 @@
28
28
  },
29
29
  "homepage": "https://github.com/applicaster/quickbrick#readme",
30
30
  "dependencies": {
31
- "@applicaster/applicaster-types": "12.0.0-alpha.4837064762",
31
+ "@applicaster/applicaster-types": "12.0.0-alpha.4840257570",
32
32
  "buffer": "^5.2.1",
33
33
  "camelize": "^1.0.0",
34
34
  "create-react-context": "^0.2.3",
@@ -12,9 +12,21 @@ const {
12
12
 
13
13
  const { reactHooksLogger } = require("../../logger");
14
14
 
15
+ let mockStore;
16
+
15
17
  describe("getInflatedDataSourceUrl", () => {
16
18
  beforeEach(() => {
17
19
  reactHooksLogger.warning.mockClear();
20
+
21
+ const {
22
+ appStore,
23
+ } = require("@applicaster/zapp-react-native-redux/AppStore");
24
+
25
+ mockStore = {
26
+ getState: jest.fn().mockReturnValue({ pipesEndpoints: {} }),
27
+ };
28
+
29
+ appStore.set(mockStore);
18
30
  });
19
31
 
20
32
  it("returns the url inflated with the right params", () => {
@@ -72,7 +84,8 @@ describe("getInflatedDataSourceUrl", () => {
72
84
  expect(reactHooksLogger.warning).toHaveBeenCalledTimes(1);
73
85
 
74
86
  expect(reactHooksLogger.warning).toHaveBeenCalledWith({
75
- message: "Not enough data to fully inflate url",
87
+ message:
88
+ "Not enough data to fully inflate url: {{entry.id}} source: http://foo.com/show/seriesId={{seriesId}}",
76
89
  data: { source, contexts, mapping },
77
90
  });
78
91
  });
@@ -107,7 +120,8 @@ describe("getInflatedDataSourceUrl", () => {
107
120
  expect(reactHooksLogger.warning).toHaveBeenCalledTimes(1);
108
121
 
109
122
  expect(reactHooksLogger.warning).toHaveBeenCalledWith({
110
- message: "Not enough data to fully inflate url",
123
+ message:
124
+ "Not enough data to fully inflate url: {{entry.extensions.groupName}} source: https://zapp-1689-emmys.web.app/emmys/group-categories/{{groupName}}",
111
125
  data: { source, contexts, mapping },
112
126
  });
113
127
  });
@@ -148,7 +162,8 @@ describe("getInflatedDataSourceUrl", () => {
148
162
  expect(reactHooksLogger.warning).toHaveBeenCalledTimes(1);
149
163
 
150
164
  expect(reactHooksLogger.warning).toHaveBeenCalledWith({
151
- message: "Not enough data to fully inflate url",
165
+ message:
166
+ "Not enough data to fully inflate url: {{entry.extensions.groupName2}} source: https://zapp-1689-emmys.web.app/emmys/group-categories/{{groupName1}}?test={{groupName2}}",
152
167
  data: { source, contexts, mapping },
153
168
  });
154
169
  });
@@ -9,9 +9,15 @@ import {
9
9
 
10
10
  import { inflateString } from "../../stringUtils";
11
11
  import { useRoute } from "../navigation";
12
- import { isEnoughDataToInflateString } from "../../inflatedUrlUtils";
12
+ import { getMissingValues } from "../../inflatedUrlUtils";
13
13
  import { reactHooksLogger as logger } from "../logger";
14
14
  import { isTV } from "../../reactUtils";
15
+ import {
16
+ findEndpointForURL,
17
+ HTTP_METHODS,
18
+ } from "@applicaster/zapp-pipes-v2-client";
19
+ import { appStore } from "@applicaster/zapp-react-native-redux/AppStore";
20
+ import { ENDPOINT_TAGS } from "../../types";
15
21
 
16
22
  /**
17
23
  * will match any occurrence in a string of one or more word characters
@@ -105,24 +111,47 @@ export const getInflatedDataSourceUrl: GetInflatedDataSourceUrl = ({
105
111
  R.keys(mapping)
106
112
  );
107
113
 
108
- if (!isEnoughDataToInflateString(contexts, string)) {
109
- const message = "Not enough data to fully inflate url";
114
+ const missingValues = getMissingValues(contexts, string);
110
115
 
111
- // todo: add js only payload instead
112
- if (__DEV__) {
113
- logger.warning({
114
- message,
115
- data: { source, contexts, mapping },
116
- });
116
+ if (missingValues && missingValues.length > 0) {
117
+ const pipesEndpoints = appStore.get("pipesEndpoints");
118
+
119
+ const endpointURL = findEndpointForURL(
120
+ source,
121
+ pipesEndpoints,
122
+ HTTP_METHODS.GET
123
+ );
124
+
125
+ const tags = pipesEndpoints?.[endpointURL]?.tags;
126
+ const allowMissingKeys = tags?.includes(ENDPOINT_TAGS.allow_missing_keys);
127
+
128
+ if (!allowMissingKeys) {
129
+ const message = `Not enough data to fully inflate url: ${missingValues.join(
130
+ ","
131
+ )} source: ${source}`;
132
+
133
+ // todo: add js only payload instead
134
+ if (__DEV__) {
135
+ logger.warning({
136
+ message,
137
+ data: { source, contexts, mapping },
138
+ });
139
+ } else {
140
+ // todo: log values we've failed to resolve
141
+ logger.warning({
142
+ message,
143
+ });
144
+ }
145
+
146
+ // return null in order to prevent start fetching data with invalid url
147
+ return null;
117
148
  } else {
118
- // todo: log values we've failed to resolve
119
- logger.warning({
120
- message,
149
+ logger.debug({
150
+ message: `Ignoring missing values in url because ${
151
+ ENDPOINT_TAGS.allow_missing_keys
152
+ } endpoint tag, keys: ${missingValues.join(",")}`,
121
153
  });
122
154
  }
123
-
124
- // return null in order to prevent start fetching data with invalid url
125
- return null;
126
155
  }
127
156
 
128
157
  return inflateString({ string, data: contexts });
@@ -496,18 +496,3 @@ export const useIsScreenActive = () => {
496
496
 
497
497
  return pathname === currentRoute;
498
498
  };
499
-
500
- type Return = (prefix: string) => string;
501
-
502
- export const useUniqueRouteSuffix = (): Return => {
503
- const pathname = usePathname();
504
- const { currentRoute } = useNavigation();
505
-
506
- const suffix = `currentRoute:${currentRoute}_pathname:${pathname}`;
507
-
508
- const withUniqueRouteSuffix = React.useCallback((prefix: string) => {
509
- return `${prefix}___${suffix}`;
510
- }, []);
511
-
512
- return withUniqueRouteSuffix;
513
- };
@@ -5,7 +5,6 @@ import { useNestedNavigationContext } from "@applicaster/zapp-react-native-ui-co
5
5
  import { useNavigation } from "../navigation/useNavigation";
6
6
 
7
7
  import { ScreenContext } from "@applicaster/zapp-react-native-ui-components/Contexts/ScreenContext";
8
- import { ScreenDataContext } from "@applicaster/zapp-react-native-ui-components/Components/Layout/TV";
9
8
 
10
9
  export function useScreenContextV2() {
11
10
  return useContext(ScreenContext);
@@ -46,8 +45,8 @@ export function useSetNavbarState() {
46
45
  export function useScreenContext(): NavigationScreenData {
47
46
  const navigator = useNavigation();
48
47
 
49
- const screenData = useContext(ScreenDataContext);
50
-
48
+ const screenKey = useMemo(() => navigator?.key, [navigator]);
49
+ const screenData = useMemo(() => navigator?.data, [screenKey]);
51
50
  const modalScreenData = navigator?.modalData;
52
51
 
53
52
  const isModal = useModalNavigationContext();
@@ -7,7 +7,7 @@ export const isAnyNil = R.any(R.isNil);
7
7
 
8
8
  export const isNilOrEmpty = R.either(R.isNil, R.isEmpty);
9
9
 
10
- export const isNotEmpty = R.both(isNotNil, R.complement(R.isEmpty));
10
+ export const isNotEmpty = R.complement(R.isEmpty);
11
11
 
12
12
  export const withoutNil = R.reject(R.isNil);
13
13
 
@@ -152,3 +152,8 @@ export function objectToCamelCase(obj) {
152
152
  export function isString(value: unknown): value is string {
153
153
  return typeof value === "string" || value instanceof String;
154
154
  }
155
+
156
+ /** Checks if a value is a non-empty string */
157
+ export function isNotEmptyString(value: unknown): value is string {
158
+ return isString(value) && value.length > 0;
159
+ }
package/types.ts CHANGED
@@ -5,3 +5,7 @@ export type ManifestField<T> = {
5
5
  group?: boolean;
6
6
  fields?: ManifestField<any>[];
7
7
  };
8
+
9
+ export enum ENDPOINT_TAGS {
10
+ allow_missing_keys = "allow_missing_keys",
11
+ }