@applicaster/zapp-react-dom-app 15.0.0-alpha.3752284136 → 15.0.0-alpha.3842304615

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.
@@ -1,8 +1,21 @@
1
1
  import React from "react";
2
2
  import { render } from "@testing-library/react-native";
3
+
3
4
  import { withBackToTopActionHOC } from "../withBackToTopAction";
5
+ import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils";
6
+ import { useSubscriberFor } from "@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor";
7
+ import {
8
+ useCurrentScreenIsHome,
9
+ useCurrentScreenIsRoot,
10
+ useCurrentScreenIsTabs,
11
+ useIsStandaloneFullscreen,
12
+ } from "../hooks";
13
+
14
+ import {
15
+ useCurrentScreenIsHook,
16
+ useCurrentScreenIsStartupHook,
17
+ } from "@applicaster/zapp-react-native-utils/reactHooks/screen";
4
18
 
5
- // Mock focusManager and hook
6
19
  jest.mock("@applicaster/zapp-react-native-utils/appUtils", () => ({
7
20
  focusManager: {
8
21
  isFocusOnMenu: jest.fn(),
@@ -11,26 +24,17 @@ jest.mock("@applicaster/zapp-react-native-utils/appUtils", () => ({
11
24
  },
12
25
  }));
13
26
 
14
- jest.mock("../hooks", () => ({
15
- useCurrentScreenIsHome: jest.fn(),
16
- useCurrentScreenIsRoot: jest.fn(),
17
- useCurrentScreenIsTabs: jest.fn(),
18
- }));
19
-
20
- import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils";
21
- import {
22
- useCurrentScreenIsHome,
23
- useCurrentScreenIsRoot,
24
- useCurrentScreenIsTabs,
25
- } from "../hooks";
27
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor");
28
+ jest.mock("../hooks");
29
+ jest.mock("@applicaster/zapp-react-native-utils/reactHooks/screen");
26
30
 
27
- jest.mock("@applicaster/zapp-react-native-utils/reactHooks", () => ({
28
- useNavigation: jest.fn(),
29
- }));
31
+ const mockUseSubscriberFor = useSubscriberFor as jest.Mock;
30
32
 
31
- import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
33
+ const setupFocusManager = (overrides: Partial<typeof focusManager>) => {
34
+ Object.assign(focusManager, overrides);
35
+ };
32
36
 
33
- describe("withBackToTopActionHOC", () => {
37
+ describe("withBackToTopActionHOC - backToTopAction", () => {
34
38
  let receivedProps: any;
35
39
 
36
40
  const BaseComponent = (props) => {
@@ -43,141 +47,119 @@ describe("withBackToTopActionHOC", () => {
43
47
 
44
48
  beforeEach(() => {
45
49
  jest.clearAllMocks();
50
+
51
+ mockUseSubscriberFor.mockImplementation(() => jest.fn());
52
+
53
+ (useCurrentScreenIsHome as jest.Mock).mockReturnValue(false);
54
+ (useCurrentScreenIsRoot as jest.Mock).mockReturnValue(false);
55
+ (useCurrentScreenIsTabs as jest.Mock).mockReturnValue(false);
56
+ (useIsStandaloneFullscreen as jest.Mock).mockReturnValue(false);
57
+ (useCurrentScreenIsHook as jest.Mock).mockReturnValue(false);
58
+ (useCurrentScreenIsStartupHook as jest.Mock).mockReturnValue(false);
59
+
60
+ setupFocusManager({
61
+ isFocusOnMenu: () => false,
62
+ isFocusOnContent: () => false,
63
+ isTabsScreenContentFocused: () => false,
64
+ });
46
65
  });
47
66
 
48
- it("returns PLATFORM_BACK when on root screen, and startup hooks", () => {
67
+ it("returns PLATFORM_BACK for startup hook", () => {
49
68
  expect.assertions(1);
50
69
 
51
- (useCurrentScreenIsRoot as jest.Mock).mockReturnValue(true);
52
-
53
- (useNavigation as jest.Mock).mockReturnValue({
54
- startUpHooks: ["some_startup_hook"],
55
- });
70
+ (useCurrentScreenIsStartupHook as jest.Mock).mockReturnValue(true);
56
71
 
57
72
  render(<Wrapped />);
58
73
 
59
74
  expect(receivedProps.backToTopAction()).toBe("PLATFORM_BACK");
60
75
  });
61
76
 
62
- it("returns PLATFORM_BACK when on root screen, focus on menu, and is home", () => {
77
+ it("returns GO_BACK_FROM_HOOK when isHook", () => {
63
78
  expect.assertions(1);
64
79
 
65
- (useCurrentScreenIsRoot as jest.Mock).mockReturnValue(true);
66
- (useCurrentScreenIsHome as jest.Mock).mockReturnValue(true);
67
- (useCurrentScreenIsTabs as jest.Mock).mockReturnValue(false);
80
+ (useCurrentScreenIsHook as jest.Mock).mockReturnValue(true);
68
81
 
69
- (useNavigation as jest.Mock).mockReturnValue({
70
- startUpHooks: [],
71
- });
82
+ render(<Wrapped />);
83
+
84
+ expect(receivedProps.backToTopAction()).toBe("GO_BACK_FROM_HOOK");
85
+ });
86
+
87
+ it("returns GO_HOME for standalone full screen", () => {
88
+ expect.assertions(1);
72
89
 
73
- (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(true);
74
- (focusManager.isFocusOnContent as jest.Mock).mockReturnValue(false);
90
+ (useIsStandaloneFullscreen as jest.Mock).mockReturnValue(true);
75
91
 
76
92
  render(<Wrapped />);
77
93
 
78
- expect(receivedProps.backToTopAction()).toBe("PLATFORM_BACK");
94
+ expect(receivedProps.backToTopAction()).toBe("GO_HOME");
79
95
  });
80
96
 
81
- it("returns GO_HOME when on root screen, focus on menu, and not home", () => {
97
+ it("returns PLATFORM_BACK when root + menu focus + home", () => {
82
98
  expect.assertions(1);
83
99
 
84
100
  (useCurrentScreenIsRoot as jest.Mock).mockReturnValue(true);
85
- (useCurrentScreenIsHome as jest.Mock).mockReturnValue(false);
86
- (useCurrentScreenIsTabs as jest.Mock).mockReturnValue(false);
101
+ (useCurrentScreenIsHome as jest.Mock).mockReturnValue(true);
87
102
 
88
- (useNavigation as jest.Mock).mockReturnValue({
89
- startUpHooks: [],
103
+ setupFocusManager({
104
+ isFocusOnMenu: () => true,
90
105
  });
91
106
 
92
- (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(true);
93
- (focusManager.isFocusOnContent as jest.Mock).mockReturnValue(false);
94
-
95
107
  render(<Wrapped />);
96
- expect(receivedProps.backToTopAction()).toBe("GO_HOME");
108
+
109
+ expect(receivedProps.backToTopAction()).toBe("PLATFORM_BACK");
97
110
  });
98
111
 
99
- it("returns FOCUS_ON_SELECTED_TOP_MENU_ITEM when on root screen and focus on content without tabs_screen", () => {
112
+ it("returns GO_HOME when root + menu focus + NOT home", () => {
113
+ expect.assertions(1);
114
+
100
115
  (useCurrentScreenIsRoot as jest.Mock).mockReturnValue(true);
101
116
  (useCurrentScreenIsHome as jest.Mock).mockReturnValue(false);
102
- (useCurrentScreenIsTabs as jest.Mock).mockReturnValue(false);
103
117
 
104
- (useNavigation as jest.Mock).mockReturnValue({
105
- startUpHooks: [],
118
+ setupFocusManager({
119
+ isFocusOnMenu: () => true,
106
120
  });
107
121
 
108
- (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(false);
109
- (focusManager.isFocusOnContent as jest.Mock).mockReturnValue(true);
110
-
111
122
  render(<Wrapped />);
112
123
 
113
- expect(receivedProps.backToTopAction()).toBe(
114
- "FOCUS_ON_SELECTED_TOP_MENU_ITEM"
115
- );
124
+ expect(receivedProps.backToTopAction()).toBe("GO_HOME");
116
125
  });
117
126
 
118
- it("returns FOCUS_ON_SELECTED_TAB_ITEM when on root screen and focus on content with tabs_screen", () => {
127
+ it("returns FOCUS_ON_SELECTED_TAB_ITEM when tabs content focused", () => {
128
+ expect.assertions(1);
129
+
119
130
  (useCurrentScreenIsRoot as jest.Mock).mockReturnValue(true);
120
- (useCurrentScreenIsHome as jest.Mock).mockReturnValue(false);
121
131
  (useCurrentScreenIsTabs as jest.Mock).mockReturnValue(true);
122
132
 
123
- (useNavigation as jest.Mock).mockReturnValue({
124
- startUpHooks: [],
133
+ setupFocusManager({
134
+ isTabsScreenContentFocused: () => true,
125
135
  });
126
136
 
127
- (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(false);
128
- (focusManager.isFocusOnContent as jest.Mock).mockReturnValue(true);
129
-
130
- (focusManager.isTabsScreenContentFocused as jest.Mock).mockReturnValue(
131
- true
132
- );
133
-
134
137
  render(<Wrapped />);
135
138
 
136
139
  expect(receivedProps.backToTopAction()).toBe("FOCUS_ON_SELECTED_TAB_ITEM");
137
140
  });
138
141
 
139
- it("returns GO_BACK in default case", () => {
142
+ it("returns FOCUS_ON_SELECTED_TOP_MENU_ITEM when content focused", () => {
140
143
  expect.assertions(1);
141
144
 
142
- (useCurrentScreenIsRoot as jest.Mock).mockReturnValue(false);
143
- (useCurrentScreenIsHome as jest.Mock).mockReturnValue(false);
144
- (useCurrentScreenIsTabs as jest.Mock).mockReturnValue(false);
145
+ (useCurrentScreenIsRoot as jest.Mock).mockReturnValue(true);
145
146
 
146
- (useNavigation as jest.Mock).mockReturnValue({
147
- startUpHooks: [],
147
+ setupFocusManager({
148
+ isFocusOnContent: () => true,
148
149
  });
149
150
 
150
- (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(false);
151
- (focusManager.isFocusOnContent as jest.Mock).mockReturnValue(false);
151
+ render(<Wrapped />);
152
152
 
153
- (focusManager.isTabsScreenContentFocused as jest.Mock).mockReturnValue(
154
- false
153
+ expect(receivedProps.backToTopAction()).toBe(
154
+ "FOCUS_ON_SELECTED_TOP_MENU_ITEM"
155
155
  );
156
-
157
- render(<Wrapped />);
158
- expect(receivedProps.backToTopAction()).toBe("GO_BACK");
159
156
  });
160
157
 
161
- it("returns FORCE_GO_BACK in hook case", () => {
158
+ it("returns default GO_BACK", () => {
162
159
  expect.assertions(1);
163
160
 
164
- (useCurrentScreenIsRoot as jest.Mock).mockReturnValue(false);
165
- (useCurrentScreenIsHome as jest.Mock).mockReturnValue(false);
166
- (useCurrentScreenIsTabs as jest.Mock).mockReturnValue(false);
167
-
168
- (useNavigation as jest.Mock).mockReturnValue({
169
- startUpHooks: [],
170
- currentRoute: "/hooks/some_route",
171
- });
172
-
173
- (focusManager.isFocusOnMenu as jest.Mock).mockReturnValue(false);
174
- (focusManager.isFocusOnContent as jest.Mock).mockReturnValue(false);
175
-
176
- (focusManager.isTabsScreenContentFocused as jest.Mock).mockReturnValue(
177
- false
178
- );
179
-
180
161
  render(<Wrapped />);
181
- expect(receivedProps.backToTopAction()).toBe("FORCE_GO_BACK");
162
+
163
+ expect(receivedProps.backToTopAction()).toBe("GO_BACK");
182
164
  });
183
165
  });
@@ -4,6 +4,7 @@ import {
4
4
  useRivers,
5
5
  } from "@applicaster/zapp-react-native-utils/reactHooks/state";
6
6
  import { last } from "@applicaster/zapp-react-native-utils/utils";
7
+ import { toBooleanWithDefaultFalse } from "@applicaster/zapp-react-native-utils/booleanUtils";
7
8
 
8
9
  export const useCurrentScreenIsHome = (): boolean => {
9
10
  const navigator = useNavigation();
@@ -32,3 +33,12 @@ export const useCurrentScreenIsTabs = (): boolean => {
32
33
 
33
34
  return river?.type === "tabs_screen";
34
35
  };
36
+
37
+ export const useIsStandaloneFullscreen = (): boolean => {
38
+ const navigator = useNavigation();
39
+
40
+ return toBooleanWithDefaultFalse(
41
+ !navigator?.canGoBack() &&
42
+ navigator?.screenData?.general?.allow_screen_plugin_presentation
43
+ );
44
+ };
@@ -1,4 +1 @@
1
- export {
2
- withBackToTopActionHOC,
3
- type BackToTopAction,
4
- } from "./withBackToTopAction";
1
+ export { withBackToTopActionHOC } from "./withBackToTopAction";
@@ -1,5 +1,4 @@
1
1
  import * as React from "react";
2
- import { isFilledArray } from "@applicaster/zapp-react-native-utils/arrayUtils";
3
2
  import { focusManager } from "@applicaster/zapp-react-native-utils/appUtils";
4
3
 
5
4
  import { useSubscriberFor } from "@applicaster/zapp-react-native-utils/reactHooks/useSubscriberFor";
@@ -9,18 +8,13 @@ import {
9
8
  useCurrentScreenIsHome,
10
9
  useCurrentScreenIsRoot,
11
10
  useCurrentScreenIsTabs,
11
+ useIsStandaloneFullscreen,
12
12
  } from "./hooks";
13
- import { useNavigation } from "@applicaster/zapp-react-native-utils/reactHooks";
14
13
 
15
- export type BACK_TO_TOP_ACTION =
16
- | "PLATFORM_BACK"
17
- | "GO_HOME"
18
- | "GO_BACK"
19
- | "FOCUS_ON_SELECTED_TOP_MENU_ITEM"
20
- | "FORCE_GO_BACK"
21
- | "FOCUS_ON_SELECTED_TAB_ITEM";
22
-
23
- export type BackToTopAction = () => BACK_TO_TOP_ACTION;
14
+ import {
15
+ useCurrentScreenIsHook,
16
+ useCurrentScreenIsStartupHook,
17
+ } from "@applicaster/zapp-react-native-utils/reactHooks/screen";
24
18
 
25
19
  export function withBackToTopActionHOC(Component) {
26
20
  return function WrappedComponent(props) {
@@ -28,11 +22,11 @@ export function withBackToTopActionHOC(Component) {
28
22
  const isRoot = useCurrentScreenIsRoot();
29
23
  const isTabsScreen = useCurrentScreenIsTabs();
30
24
 
31
- const navigator = useNavigation();
25
+ const isStartUpHook = useCurrentScreenIsStartupHook();
32
26
 
33
- const isStartUpHook = isFilledArray(navigator.startUpHooks);
27
+ const isHook = useCurrentScreenIsHook();
34
28
 
35
- const isHook = navigator.currentRoute?.includes("hook");
29
+ const isStandaloneFullscreen = useIsStandaloneFullscreen();
36
30
 
37
31
  const emitFocusOnSelectedTab = useSubscriberFor(
38
32
  QBUIComponentEvents.focusOnSelectedTab
@@ -42,11 +36,20 @@ export function withBackToTopActionHOC(Component) {
42
36
  QBUIComponentEvents.focusOnSelectedTopMenuItem
43
37
  );
44
38
 
45
- const backToTopAction = React.useCallback((): BACK_TO_TOP_ACTION => {
46
- if (isRoot && isStartUpHook) {
39
+ const backToTopAction = React.useCallback((): BackToTopAction => {
40
+ if (isStartUpHook) {
47
41
  return "PLATFORM_BACK";
48
42
  }
49
43
 
44
+ if (isHook) {
45
+ return "GO_BACK_FROM_HOOK";
46
+ }
47
+
48
+ // Handling case where is not top menu visible
49
+ if (isStandaloneFullscreen) {
50
+ return "GO_HOME";
51
+ }
52
+
50
53
  if (isRoot && focusManager.isFocusOnMenu() && isHome) {
51
54
  return "PLATFORM_BACK";
52
55
  }
@@ -63,13 +66,16 @@ export function withBackToTopActionHOC(Component) {
63
66
  return "FOCUS_ON_SELECTED_TOP_MENU_ITEM";
64
67
  }
65
68
 
66
- if (isHook) {
67
- return "FORCE_GO_BACK";
68
- }
69
-
70
69
  // default
71
70
  return "GO_BACK";
72
- }, [isHome, isRoot, isTabsScreen, isStartUpHook, isHook]);
71
+ }, [
72
+ isHome,
73
+ isRoot,
74
+ isTabsScreen,
75
+ isStartUpHook,
76
+ isHook,
77
+ isStandaloneFullscreen,
78
+ ]);
73
79
 
74
80
  return (
75
81
  <Component
@@ -54,7 +54,7 @@ import { OnScreenKeyboard } from "@applicaster/zapp-react-dom-ui-components/Comp
54
54
  import { KeyboardLongPressManager } from "@applicaster/zapp-react-dom-ui-components/Utils/KeyboardLongPressManager";
55
55
  import { KeyInputHandler } from "@applicaster/zapp-react-native-utils/appUtils/keyInputHandler/KeyInputHandler";
56
56
  import { getHomeRiver } from "@applicaster/zapp-react-native-utils/reactHooks/state";
57
- import { withBackToTopActionHOC, BackToTopAction } from "./hoc";
57
+ import { withBackToTopActionHOC } from "./hoc";
58
58
 
59
59
  const { withXray } = getXray();
60
60
  const globalAny: any = global;
@@ -76,7 +76,7 @@ type Props = {
76
76
  rivers: {};
77
77
  xray: XRayContext;
78
78
  confirmDialog: typeof confirmationDialogStore;
79
- backToTopAction: BackToTopAction;
79
+ backToTopAction: () => BackToTopAction;
80
80
  emitFocusOnSelectedTopMenuItem: Callback;
81
81
  emitFocusOnSelectedTab: Callback;
82
82
  };
@@ -461,7 +461,7 @@ class InteractionManagerClass extends React.Component<Props, State> {
461
461
  }
462
462
  }
463
463
 
464
- if (action === "FORCE_GO_BACK") {
464
+ if (action === "GO_BACK_FROM_HOOK") {
465
465
  log_info(action);
466
466
 
467
467
  const fallbackToHome = false;
@@ -259,6 +259,11 @@ export const getDeviceData = async () => {
259
259
  try {
260
260
  let deviceData = {};
261
261
 
262
+ const deviceDimensions = {
263
+ deviceWidth: window?.screen?.width,
264
+ deviceHeight: window?.screen?.height,
265
+ };
266
+
262
267
  if (isLgPlatform()) {
263
268
  const [webOSInfo, webOSConnectionInfo] = await Promise.all([
264
269
  getWebOSInfo(),
@@ -270,6 +275,7 @@ export const getDeviceData = async () => {
270
275
  ...webOSInfo,
271
276
  ...webOSConnectionInfo,
272
277
  device_type: getDeviceType(),
278
+ ...deviceDimensions,
273
279
  };
274
280
  } else if (isSamsungPlatform()) {
275
281
  const [tizenInfo, tizenConnectionInfo] = await Promise.all([
@@ -282,6 +288,7 @@ export const getDeviceData = async () => {
282
288
  ...tizenInfo,
283
289
  ...tizenConnectionInfo,
284
290
  device_type: getDeviceType(),
291
+ ...deviceDimensions,
285
292
  };
286
293
  } else if (isVizioPlatform()) {
287
294
  deviceData = {
@@ -289,8 +296,7 @@ export const getDeviceData = async () => {
289
296
  platform: "vizio",
290
297
  deviceMake: "Vizio",
291
298
  deviceType: "tv",
292
- deviceWidth: window.innerWidth,
293
- deviceHeight: window.innerHeight,
299
+ ...deviceDimensions,
294
300
  };
295
301
 
296
302
  if (hasVizioAPIs()) {
@@ -109,7 +109,7 @@ describe("QuickBrickCommunicationModule Polyfill", () => {
109
109
  });
110
110
  });
111
111
 
112
- it("should fallback to default language if browser / device locale (en-UK) does not match list of supported languages", () => {
112
+ it("should use partial match en language when browser / device locale (en-UK) does not exactly match list of supported languages", () => {
113
113
  // localizations: ["en", "en-US", "fr", "fr-FR"], device_language: en, device_region: UK -> ui: en, language: en, region: UK
114
114
 
115
115
  global.window.navigator.language = "en-UK";
@@ -118,8 +118,22 @@ describe("QuickBrickCommunicationModule Polyfill", () => {
118
118
  expect(result).toEqual({
119
119
  uiLanguage: "en",
120
120
  languageCode: "en",
121
- countryLocale: "",
122
- languageLocale: "en",
121
+ countryLocale: "UK",
122
+ languageLocale: "en-UK",
123
+ });
124
+ });
125
+
126
+ it("should partial match fr language when browser / device locale (fr-CA) does not exact match list of supported languages", () => {
127
+ // localizations: ["en", "en-US", "fr", "fr-FR"], device_language: fr, device_region: CA -> ui: fr, language: fr, region: CA
128
+
129
+ global.window.navigator.language = "fr-CA";
130
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
131
+
132
+ expect(result).toEqual({
133
+ uiLanguage: "fr",
134
+ languageCode: "fr",
135
+ countryLocale: "CA",
136
+ languageLocale: "fr-CA",
123
137
  });
124
138
  });
125
139
 
@@ -190,6 +204,312 @@ describe("QuickBrickCommunicationModule Polyfill", () => {
190
204
  languageLocale: "fr-FR",
191
205
  });
192
206
  });
207
+
208
+ it("should not attempt to use user-selected language if it is improperly stored as undefined, null, or empty string", () => {
209
+ mockGetAppData.mockReturnValue(
210
+ getMockAppData({
211
+ languages: ["en", "es", "fr", "en-UK", "en-GB", "ar"],
212
+ })
213
+ );
214
+
215
+ global.window.navigator.language = "fr-CA";
216
+
217
+ // Test with undefined
218
+ window.localStorage.getItem.mockReturnValue("undefined");
219
+ let result = QuickBrickModule.getMatchingLanguageFromBuildData();
220
+
221
+ expect(result).toEqual({
222
+ uiLanguage: "fr",
223
+ languageCode: "fr",
224
+ countryLocale: "CA",
225
+ languageLocale: "fr-CA",
226
+ });
227
+
228
+ // Test with null
229
+ window.localStorage.getItem.mockReturnValue("null");
230
+ result = QuickBrickModule.getMatchingLanguageFromBuildData();
231
+
232
+ expect(result).toEqual({
233
+ uiLanguage: "fr",
234
+ languageCode: "fr",
235
+ countryLocale: "CA",
236
+ languageLocale: "fr-CA",
237
+ });
238
+
239
+ // Test with empty string
240
+ window.localStorage.getItem.mockReturnValue("");
241
+ result = QuickBrickModule.getMatchingLanguageFromBuildData();
242
+
243
+ expect(result).toEqual({
244
+ uiLanguage: "fr",
245
+ languageCode: "fr",
246
+ countryLocale: "CA",
247
+ languageLocale: "fr-CA",
248
+ });
249
+ });
250
+
251
+ describe("user-selected language scenarios", () => {
252
+ it("should use exact match user-selected language when available, ignoring browser locale", () => {
253
+ window.localStorage.getItem.mockReturnValue("fr-FR");
254
+ global.window.navigator.language = "en-US";
255
+
256
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
257
+
258
+ expect(result).toEqual({
259
+ uiLanguage: "fr-FR",
260
+ languageCode: "fr",
261
+ countryLocale: "FR",
262
+ languageLocale: "fr-FR",
263
+ });
264
+ });
265
+
266
+ it("should use base language user-selected when available (single language code)", () => {
267
+ window.localStorage.getItem.mockReturnValue("en");
268
+ global.window.navigator.language = "fr-FR";
269
+
270
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
271
+
272
+ expect(result).toEqual({
273
+ uiLanguage: "en",
274
+ languageCode: "en",
275
+ countryLocale: "",
276
+ languageLocale: "en",
277
+ });
278
+ });
279
+
280
+ it("should use base language fr user-selected when available", () => {
281
+ window.localStorage.getItem.mockReturnValue("fr");
282
+ global.window.navigator.language = "en-US";
283
+
284
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
285
+
286
+ expect(result).toEqual({
287
+ uiLanguage: "fr",
288
+ languageCode: "fr",
289
+ countryLocale: "",
290
+ languageLocale: "fr",
291
+ });
292
+ });
293
+
294
+ it("should fallback to browser exact match when user-selected language not available", () => {
295
+ window.localStorage.getItem.mockReturnValue("de-DE");
296
+ global.window.navigator.language = "fr-FR";
297
+
298
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
299
+
300
+ expect(result).toEqual({
301
+ uiLanguage: "fr-FR",
302
+ languageCode: "fr",
303
+ countryLocale: "FR",
304
+ languageLocale: "fr-FR",
305
+ });
306
+ });
307
+
308
+ it("should fallback to browser partial match when user-selected language not available", () => {
309
+ window.localStorage.getItem.mockReturnValue("de-DE");
310
+ global.window.navigator.language = "fr-CA";
311
+
312
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
313
+
314
+ expect(result).toEqual({
315
+ uiLanguage: "fr",
316
+ languageCode: "fr",
317
+ countryLocale: "CA",
318
+ languageLocale: "fr-CA",
319
+ });
320
+ });
321
+
322
+ it("should fallback to default language when both user-selected and browser locales not available", () => {
323
+ window.localStorage.getItem.mockReturnValue("de-DE");
324
+ global.window.navigator.language = "ja-JP";
325
+
326
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
327
+
328
+ expect(result).toEqual({
329
+ uiLanguage: "en",
330
+ languageCode: "en",
331
+ countryLocale: "",
332
+ languageLocale: "en",
333
+ });
334
+ });
335
+ });
336
+
337
+ describe("browser/device locale scenarios", () => {
338
+ beforeEach(() => {
339
+ window.localStorage.getItem.mockReturnValue(null);
340
+ });
341
+
342
+ it("should use exact match browser locale when no user selection", () => {
343
+ global.window.navigator.language = "en-US";
344
+
345
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
346
+
347
+ expect(result).toEqual({
348
+ uiLanguage: "en-US",
349
+ languageCode: "en",
350
+ countryLocale: "US",
351
+ languageLocale: "en-US",
352
+ });
353
+ });
354
+
355
+ it("should use partial match browser locale when exact match not available", () => {
356
+ global.window.navigator.language = "en-AU";
357
+
358
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
359
+
360
+ expect(result).toEqual({
361
+ uiLanguage: "en",
362
+ languageCode: "en",
363
+ countryLocale: "AU",
364
+ languageLocale: "en-AU",
365
+ });
366
+ });
367
+
368
+ it("should fallback to default language when browser locale not available at all", () => {
369
+ global.window.navigator.language = "de-DE";
370
+
371
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
372
+
373
+ expect(result).toEqual({
374
+ uiLanguage: "en",
375
+ languageCode: "en",
376
+ countryLocale: "",
377
+ languageLocale: "en",
378
+ });
379
+ });
380
+
381
+ it("should handle browser locale without country code (base language only)", () => {
382
+ global.window.navigator.language = "fr";
383
+
384
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
385
+
386
+ expect(result).toEqual({
387
+ uiLanguage: "fr",
388
+ languageCode: "fr",
389
+ countryLocale: "",
390
+ languageLocale: "fr",
391
+ });
392
+ });
393
+
394
+ it("should handle browser locale without country code when only regional variants available", () => {
395
+ mockGetAppData.mockReturnValue(
396
+ getMockAppData({ languages: ["en-US", "en-GB", "fr-FR", "fr-CA"] })
397
+ );
398
+
399
+ jest.resetModules();
400
+ QuickBrickModule = require("../index");
401
+
402
+ global.window.navigator.language = "fr";
403
+
404
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
405
+
406
+ expect(result).toEqual({
407
+ uiLanguage: "fr-FR",
408
+ languageCode: "fr",
409
+ countryLocale: "",
410
+ languageLocale: "fr",
411
+ });
412
+ });
413
+ });
414
+
415
+ describe("edge cases", () => {
416
+ beforeEach(() => {
417
+ window.localStorage.getItem.mockReturnValue(null);
418
+ });
419
+
420
+ it("should fallback to locale when browser language is undefined", () => {
421
+ global.window.navigator.language = undefined;
422
+
423
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
424
+
425
+ expect(result).toEqual({
426
+ uiLanguage: "en",
427
+ languageCode: "en",
428
+ countryLocale: "",
429
+ languageLocale: "en",
430
+ });
431
+ });
432
+
433
+ it("should prefer exact match over partial match when both are available", () => {
434
+ mockGetAppData.mockReturnValue(
435
+ getMockAppData({ languages: ["en", "en-US", "en-GB", "fr"] })
436
+ );
437
+
438
+ jest.resetModules();
439
+ QuickBrickModule = require("../index");
440
+
441
+ global.window.navigator.language = "en-GB";
442
+
443
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
444
+
445
+ expect(result).toEqual({
446
+ uiLanguage: "en-GB",
447
+ languageCode: "en",
448
+ countryLocale: "GB",
449
+ languageLocale: "en-GB",
450
+ });
451
+ });
452
+
453
+ it("should use first partial match when multiple base languages could match", () => {
454
+ mockGetAppData.mockReturnValue(
455
+ getMockAppData({ languages: ["en-US", "en-GB", "fr-FR", "fr-CA"] })
456
+ );
457
+
458
+ jest.resetModules();
459
+ QuickBrickModule = require("../index");
460
+
461
+ global.window.navigator.language = "en-AU";
462
+
463
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
464
+
465
+ expect(result).toEqual({
466
+ uiLanguage: "en-US",
467
+ languageCode: "en",
468
+ countryLocale: "AU",
469
+ languageLocale: "en-AU",
470
+ });
471
+ });
472
+
473
+ it("should handle complex regional variants like zh-Hans-CN", () => {
474
+ mockGetAppData.mockReturnValue(
475
+ getMockAppData({ languages: ["en", "zh-CN", "zh-TW"] })
476
+ );
477
+
478
+ jest.resetModules();
479
+ QuickBrickModule = require("../index");
480
+
481
+ global.window.navigator.language = "zh-CN";
482
+
483
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
484
+
485
+ expect(result).toEqual({
486
+ uiLanguage: "zh-CN",
487
+ languageCode: "zh",
488
+ countryLocale: "CN",
489
+ languageLocale: "zh-CN",
490
+ });
491
+ });
492
+
493
+ it("should fallback to first language when supported languages list is populated but browser locale cannot be parsed", () => {
494
+ mockGetAppData.mockReturnValue(
495
+ getMockAppData({ languages: ["ar", "he", "fa"] })
496
+ );
497
+
498
+ jest.resetModules();
499
+ QuickBrickModule = require("../index");
500
+
501
+ global.window.navigator.language = "en-US";
502
+
503
+ const result = QuickBrickModule.getMatchingLanguageFromBuildData();
504
+
505
+ expect(result).toEqual({
506
+ uiLanguage: "ar",
507
+ languageCode: "ar",
508
+ countryLocale: "",
509
+ languageLocale: "ar",
510
+ });
511
+ });
512
+ });
193
513
  });
194
514
 
195
515
  describe("setAppLanguage", () => {
@@ -13,6 +13,27 @@ const { log_error, log_debug, log_warning } = createLogger({
13
13
 
14
14
  const QuickBrickEventEmitter = {};
15
15
 
16
+ const getUserSelectedLanguage = () => {
17
+ const userSelectedLanguage = window.localStorage.getItem(
18
+ "applicaster.v2_::_uiLanguage"
19
+ );
20
+
21
+ if (
22
+ userSelectedLanguage === "null" ||
23
+ userSelectedLanguage === "undefined" ||
24
+ userSelectedLanguage === ""
25
+ ) {
26
+ log_warning(
27
+ "Invalid user selected language found in localStorage:",
28
+ userSelectedLanguage
29
+ );
30
+
31
+ return null;
32
+ }
33
+
34
+ return userSelectedLanguage;
35
+ };
36
+
16
37
  export const QuickBrickEvents = (function (eventEmitter) {
17
38
  let listeners = [];
18
39
 
@@ -122,9 +143,12 @@ export const getMatchingLanguageFromBuildData = () => {
122
143
  const browserLocale = getBrowserLocale();
123
144
 
124
145
  if (!Array.isArray(languages)) {
125
- log_error("Missing required 'languages' from the build time data.", {
126
- buildTimeData,
127
- });
146
+ log_error(
147
+ "Missing required 'languages' in build time data; using locale as UI language.",
148
+ {
149
+ buildTimeData,
150
+ }
151
+ );
128
152
 
129
153
  return {
130
154
  uiLanguage: locale,
@@ -134,9 +158,7 @@ export const getMatchingLanguageFromBuildData = () => {
134
158
  };
135
159
  }
136
160
 
137
- const userSelectedLanguage = window.localStorage.getItem(
138
- "applicaster.v2_::_uiLanguage"
139
- );
161
+ const userSelectedLanguage = getUserSelectedLanguage();
140
162
 
141
163
  if (userSelectedLanguage) {
142
164
  if (languages.find((lang) => lang === userSelectedLanguage)) {
@@ -152,7 +174,6 @@ export const getMatchingLanguageFromBuildData = () => {
152
174
 
153
175
  log_debug(
154
176
  "User selected language is not present in available localizations, selecting language using browser or device locale",
155
-
156
177
  { userSelectedLanguage, languages }
157
178
  );
158
179
  }
@@ -172,21 +193,35 @@ export const getMatchingLanguageFromBuildData = () => {
172
193
  }
173
194
 
174
195
  // Exact match
175
- const [browserLanguageCode, browserCountryLocale] = browserLocale.split("-");
196
+ const [browserBaseLanguage, browserCountryLocale] = browserLocale.split("-");
176
197
  const browserLocaleMatch = languages.find((lang) => lang === browserLocale);
177
198
 
178
199
  if (browserLocaleMatch) {
179
200
  return {
180
201
  uiLanguage: browserLocaleMatch,
181
- languageCode: browserLanguageCode,
202
+ languageCode: browserBaseLanguage,
182
203
  countryLocale: browserCountryLocale || "",
183
204
  languageLocale: browserLocaleMatch,
184
205
  };
185
206
  }
186
207
 
208
+ // Partial match: try to match the base language (e.g., "fr" from "fr-CA")
209
+ const partialMatch = languages.find((language) =>
210
+ language.startsWith(browserBaseLanguage)
211
+ );
212
+
213
+ if (partialMatch) {
214
+ return {
215
+ uiLanguage: partialMatch,
216
+ languageCode: browserBaseLanguage,
217
+ countryLocale: browserCountryLocale || "",
218
+ languageLocale: browserLocale,
219
+ };
220
+ }
221
+
187
222
  // If there is no user-selected language, or the user-selected language is not found in the list of supported languages,
188
223
  // and the browser locale is also not found in the list of supported languages,
189
- // then fallback to the default language from the list of supported languages, while preserving the country locale from the browser.
224
+ // then fallback to the first (default) language from the list of supported languages.
190
225
  const defaultLanguage = languages[0];
191
226
 
192
227
  if (defaultLanguage) {
package/main.css CHANGED
@@ -1,7 +1,7 @@
1
1
  html,
2
2
  body {
3
3
  width: 100vw;
4
- height: 100vw;
4
+ height: 100vh;
5
5
  max-width: 1920px;
6
6
  max-height: 1080px;
7
7
  overflow: hidden;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@applicaster/zapp-react-dom-app",
3
- "version": "15.0.0-alpha.3752284136",
3
+ "version": "15.0.0-alpha.3842304615",
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": "15.0.0-alpha.3752284136",
26
- "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.3752284136",
27
- "@applicaster/zapp-react-native-redux": "15.0.0-alpha.3752284136",
28
- "@applicaster/zapp-react-native-ui-components": "15.0.0-alpha.3752284136",
29
- "@applicaster/zapp-react-native-utils": "15.0.0-alpha.3752284136",
25
+ "@applicaster/zapp-react-dom-ui-components": "15.0.0-alpha.3842304615",
26
+ "@applicaster/zapp-react-native-bridge": "15.0.0-alpha.3842304615",
27
+ "@applicaster/zapp-react-native-redux": "15.0.0-alpha.3842304615",
28
+ "@applicaster/zapp-react-native-ui-components": "15.0.0-alpha.3842304615",
29
+ "@applicaster/zapp-react-native-utils": "15.0.0-alpha.3842304615",
30
30
  "abortcontroller-polyfill": "^1.7.5",
31
31
  "typeface-montserrat": "^0.0.54",
32
32
  "video.js": "7.14.3",