@volley/vwr-loader 1.2.0 → 1.4.0

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.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Polyfills for Chrome 68+ compatibility (Samsung/LG TVs)
3
+ *
4
+ * Import this at the top of main.ts BEFORE any other code runs.
5
+ * Conservative approach: polyfill all features not available in Chrome 68.
6
+ *
7
+ * ECMAScript polyfills (via core-js):
8
+ * - globalThis (Chrome 71)
9
+ * - Array.prototype.flat/flatMap (Chrome 69)
10
+ * - Object.fromEntries (Chrome 73)
11
+ * - String.prototype.matchAll (Chrome 73)
12
+ * - Promise.allSettled (Chrome 76)
13
+ * - String.prototype.replaceAll (Chrome 85)
14
+ * - Array.prototype.at / String.prototype.at (Chrome 92)
15
+ *
16
+ * Web API polyfills:
17
+ * - focus-visible (Chrome 86) - critical for TV remote navigation
18
+ */
19
+ import "core-js/modules/es.global-this.js";
20
+ import "core-js/modules/es.array.flat.js";
21
+ import "core-js/modules/es.array.flat-map.js";
22
+ import "core-js/modules/es.object.from-entries.js";
23
+ import "core-js/modules/es.string.match-all.js";
24
+ import "core-js/modules/es.promise.all-settled.js";
25
+ import "core-js/modules/es.string.replace-all.js";
26
+ import "core-js/modules/es.array.at.js";
27
+ import "core-js/modules/es.string.at-alternative.js";
28
+ import "focus-visible";
29
+ //# sourceMappingURL=polyfills.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"polyfills.d.ts","sourceRoot":"","sources":["../src/polyfills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAGH,OAAO,mCAAmC,CAAA;AAC1C,OAAO,kCAAkC,CAAA;AACzC,OAAO,sCAAsC,CAAA;AAC7C,OAAO,2CAA2C,CAAA;AAClD,OAAO,wCAAwC,CAAA;AAC/C,OAAO,2CAA2C,CAAA;AAClD,OAAO,0CAA0C,CAAA;AACjD,OAAO,gCAAgC,CAAA;AACvC,OAAO,6CAA6C,CAAA;AAGpD,OAAO,eAAe,CAAA"}
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Polyfills for Chrome 68+ compatibility (Samsung/LG TVs)
3
+ *
4
+ * Import this at the top of main.ts BEFORE any other code runs.
5
+ * Conservative approach: polyfill all features not available in Chrome 68.
6
+ *
7
+ * ECMAScript polyfills (via core-js):
8
+ * - globalThis (Chrome 71)
9
+ * - Array.prototype.flat/flatMap (Chrome 69)
10
+ * - Object.fromEntries (Chrome 73)
11
+ * - String.prototype.matchAll (Chrome 73)
12
+ * - Promise.allSettled (Chrome 76)
13
+ * - String.prototype.replaceAll (Chrome 85)
14
+ * - Array.prototype.at / String.prototype.at (Chrome 92)
15
+ *
16
+ * Web API polyfills:
17
+ * - focus-visible (Chrome 86) - critical for TV remote navigation
18
+ */
19
+ // ECMAScript polyfills via core-js
20
+ import "core-js/modules/es.global-this.js";
21
+ import "core-js/modules/es.array.flat.js";
22
+ import "core-js/modules/es.array.flat-map.js";
23
+ import "core-js/modules/es.object.from-entries.js";
24
+ import "core-js/modules/es.string.match-all.js";
25
+ import "core-js/modules/es.promise.all-settled.js";
26
+ import "core-js/modules/es.string.replace-all.js";
27
+ import "core-js/modules/es.array.at.js";
28
+ import "core-js/modules/es.string.at-alternative.js";
29
+ // Web API polyfills
30
+ // focus-visible - Critical for TV remote focus rings
31
+ import "focus-visible";
32
+ //# sourceMappingURL=polyfills.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"polyfills.js","sourceRoot":"","sources":["../src/polyfills.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,mCAAmC;AACnC,OAAO,mCAAmC,CAAA;AAC1C,OAAO,kCAAkC,CAAA;AACzC,OAAO,sCAAsC,CAAA;AAC7C,OAAO,2CAA2C,CAAA;AAClD,OAAO,wCAAwC,CAAA;AAC/C,OAAO,2CAA2C,CAAA;AAClD,OAAO,0CAA0C,CAAA;AACjD,OAAO,gCAAgC,CAAA;AACvC,OAAO,6CAA6C,CAAA;AACpD,oBAAoB;AACpB,qDAAqD;AACrD,OAAO,eAAe,CAAA"}
package/dist/types.d.ts CHANGED
@@ -64,12 +64,16 @@ declare global {
64
64
  fetchAppInfo?: (callback: (appInfo?: {
65
65
  version?: string;
66
66
  }) => void) => void;
67
+ platformBack?: () => void;
67
68
  };
68
69
  tizen?: {
69
70
  application?: {
70
71
  getAppInfo: () => {
71
72
  version?: string;
72
73
  };
74
+ getCurrentApplication: () => {
75
+ exit: () => void;
76
+ };
73
77
  };
74
78
  };
75
79
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,MAAM,WAAW,oBAAqB,SAAQ,MAAM;IAChD,aAAa,CAAC,EAAE;QACZ,UAAU,EAAE,MAAM,CAAA;QAClB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;KACzB,CAAA;IACD,iBAAiB,CAAC,EAAE;QAChB,UAAU,EAAE,MAAM,CAAA;QAClB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;KACzB,CAAA;CACJ;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,MAAM;QACZ,SAAS,CAAC,EAAE;YACR,OAAO,CAAC,EAAE;gBACN,UAAU,CAAC,EAAE;oBACT,aAAa,IAAI,OAAO,CAAC;wBACrB,WAAW,CAAC,EAAE,MAAM,CAAA;wBACpB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;qBACrB,CAAC,CAAA;oBACF,YAAY,IAAI,OAAO,CAAC;wBACpB,SAAS,CAAC,EAAE,MAAM,CAAA;wBAClB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;qBACrB,CAAC,CAAA;oBACF,4BAA4B,IAAI,OAAO,CAAC;wBACpC,WAAW,CAAC,EAAE,MAAM,CAAA;qBACvB,CAAC,CAAA;oBACF,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;iBACrB,CAAA;gBACD,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;aACrB,CAAA;YACD,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;SACrB,CAAA;QACD,OAAO,CAAC,EAAE;YACN,WAAW,CAAC,EAAE;gBACV,OAAO,EAAE,MAAM,MAAM,CAAA;aACxB,CAAA;SACJ,CAAA;QACD,KAAK,CAAC,EAAE;YACJ,OAAO,CAAC,EAAE;gBACN,OAAO,EAAE,CACL,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;oBACL,MAAM,CAAC,EAAE,MAAM,CAAA;oBACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;oBACpC,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAA;oBAClD,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;wBAAE,SAAS,CAAC,EAAE,MAAM,CAAA;qBAAE,KAAK,IAAI,CAAA;oBACnD,SAAS,CAAC,EAAE,OAAO,CAAA;iBACtB,KACA;oBACD,MAAM,EAAE,MAAM,IAAI,CAAA;iBACrB,CAAA;aACJ,CAAA;YACD,YAAY,CAAC,EAAE,CACX,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE;gBAAE,OAAO,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,IAAI,KACjD,IAAI,CAAA;SACZ,CAAA;QACD,KAAK,CAAC,EAAE;YACJ,WAAW,CAAC,EAAE;gBACV,UAAU,EAAE,MAAM;oBAAE,OAAO,CAAC,EAAE,MAAM,CAAA;iBAAE,CAAA;aACzC,CAAA;SACJ,CAAA;KACJ;CACJ;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAC/B,MAAM,CAAC,EAAE,KAAK,CAAC;QACX,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;KACnB,CAAC,CAAA;IACF,WAAW,CAAC,EAAE,OAAO,CAAA;CACxB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH;;;GAGG;AACH,MAAM,WAAW,oBAAqB,SAAQ,MAAM;IAChD,aAAa,CAAC,EAAE;QACZ,UAAU,EAAE,MAAM,CAAA;QAClB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;KACzB,CAAA;IACD,iBAAiB,CAAC,EAAE;QAChB,UAAU,EAAE,MAAM,CAAA;QAClB,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,aAAa,CAAC,EAAE,MAAM,CAAA;KACzB,CAAA;CACJ;AAED;;GAEG;AACH,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,MAAM;QACZ,SAAS,CAAC,EAAE;YACR,OAAO,CAAC,EAAE;gBACN,UAAU,CAAC,EAAE;oBACT,aAAa,IAAI,OAAO,CAAC;wBACrB,WAAW,CAAC,EAAE,MAAM,CAAA;wBACpB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;qBACrB,CAAC,CAAA;oBACF,YAAY,IAAI,OAAO,CAAC;wBACpB,SAAS,CAAC,EAAE,MAAM,CAAA;wBAClB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;qBACrB,CAAC,CAAA;oBACF,4BAA4B,IAAI,OAAO,CAAC;wBACpC,WAAW,CAAC,EAAE,MAAM,CAAA;qBACvB,CAAC,CAAA;oBACF,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;iBACrB,CAAA;gBACD,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;aACrB,CAAA;YACD,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAA;SACrB,CAAA;QACD,OAAO,CAAC,EAAE;YACN,WAAW,CAAC,EAAE;gBACV,OAAO,EAAE,MAAM,MAAM,CAAA;aACxB,CAAA;SACJ,CAAA;QACD,KAAK,CAAC,EAAE;YACJ,OAAO,CAAC,EAAE;gBACN,OAAO,EAAE,CACL,GAAG,EAAE,MAAM,EACX,OAAO,EAAE;oBACL,MAAM,CAAC,EAAE,MAAM,CAAA;oBACf,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;oBACpC,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,kBAAkB,KAAK,IAAI,CAAA;oBAClD,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;wBAAE,SAAS,CAAC,EAAE,MAAM,CAAA;qBAAE,KAAK,IAAI,CAAA;oBACnD,SAAS,CAAC,EAAE,OAAO,CAAA;iBACtB,KACA;oBACD,MAAM,EAAE,MAAM,IAAI,CAAA;iBACrB,CAAA;aACJ,CAAA;YACD,YAAY,CAAC,EAAE,CACX,QAAQ,EAAE,CAAC,OAAO,CAAC,EAAE;gBAAE,OAAO,CAAC,EAAE,MAAM,CAAA;aAAE,KAAK,IAAI,KACjD,IAAI,CAAA;YACT,YAAY,CAAC,EAAE,MAAM,IAAI,CAAA;SAC5B,CAAA;QACD,KAAK,CAAC,EAAE;YACJ,WAAW,CAAC,EAAE;gBACV,UAAU,EAAE,MAAM;oBAAE,OAAO,CAAC,EAAE,MAAM,CAAA;iBAAE,CAAA;gBACtC,qBAAqB,EAAE,MAAM;oBACzB,IAAI,EAAE,MAAM,IAAI,CAAA;iBACnB,CAAA;aACJ,CAAA;SACJ,CAAA;KACJ;CACJ;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IAC/B,MAAM,CAAC,EAAE,KAAK,CAAC;QACX,MAAM,CAAC,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,MAAM,CAAA;KACnB,CAAC,CAAA;IACF,WAAW,CAAC,EAAE,OAAO,CAAA;CACxB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@volley/vwr-loader",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "description": "Vite-based VWR loader for all Volley platforms",
6
6
  "main": "dist/index.js",
@@ -36,7 +36,9 @@
36
36
  "@datadog/browser-rum": "^6.25.2",
37
37
  "@volley/logger": "^1.4.1",
38
38
  "commander": "^12.0.0",
39
+ "core-js": "^3.45.0",
39
40
  "dotenv": "^16.0.0",
41
+ "focus-visible": "^5.2.1",
40
42
  "fs-extra": "^11.0.0",
41
43
  "uuid": "^11.1.0",
42
44
  "vite": "^5.0.0"
@@ -0,0 +1,179 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
+
3
+ import { exitIfBackNavigation, setupLGBackButtonHandler } from "./exitHandler"
4
+
5
+ describe("exitHandler", () => {
6
+ beforeEach(() => {
7
+ vi.clearAllMocks()
8
+ })
9
+
10
+ afterEach(() => {
11
+ vi.restoreAllMocks()
12
+ })
13
+
14
+ describe("exitIfBackNavigation", () => {
15
+ it("should not exit when navigation type is not back_forward", () => {
16
+ vi.spyOn(performance, "getEntriesByType").mockReturnValue([
17
+ { type: "navigate" } as PerformanceNavigationTiming,
18
+ ])
19
+
20
+ const mockExit = vi.fn()
21
+ window.tizen = {
22
+ application: {
23
+ getAppInfo: () => ({ version: "1.0.0" }),
24
+ getCurrentApplication: () => ({ exit: mockExit }),
25
+ },
26
+ }
27
+
28
+ exitIfBackNavigation("SAMSUNG_TV")
29
+
30
+ expect(mockExit).not.toHaveBeenCalled()
31
+ })
32
+
33
+ it("should exit Samsung app when back_forward detected", () => {
34
+ vi.spyOn(performance, "getEntriesByType").mockReturnValue([
35
+ { type: "back_forward" } as PerformanceNavigationTiming,
36
+ ])
37
+
38
+ const mockExit = vi.fn()
39
+ window.tizen = {
40
+ application: {
41
+ getAppInfo: () => ({ version: "1.0.0" }),
42
+ getCurrentApplication: () => ({ exit: mockExit }),
43
+ },
44
+ }
45
+
46
+ exitIfBackNavigation("SAMSUNG_TV")
47
+
48
+ expect(mockExit).toHaveBeenCalled()
49
+ })
50
+
51
+ it("should close window for LG app when back_forward detected", () => {
52
+ vi.spyOn(performance, "getEntriesByType").mockReturnValue([
53
+ { type: "back_forward" } as PerformanceNavigationTiming,
54
+ ])
55
+
56
+ const mockClose = vi.fn()
57
+ vi.spyOn(window, "close").mockImplementation(mockClose)
58
+
59
+ exitIfBackNavigation("LG_TV")
60
+
61
+ expect(mockClose).toHaveBeenCalled()
62
+ })
63
+
64
+ it("should not exit when no navigation entries exist", () => {
65
+ vi.spyOn(performance, "getEntriesByType").mockReturnValue([])
66
+
67
+ const mockClose = vi.fn()
68
+ vi.spyOn(window, "close").mockImplementation(mockClose)
69
+
70
+ exitIfBackNavigation("LG_TV")
71
+
72
+ expect(mockClose).not.toHaveBeenCalled()
73
+ })
74
+
75
+ it("should handle errors gracefully", () => {
76
+ vi.spyOn(performance, "getEntriesByType").mockImplementation(() => {
77
+ throw new Error("Performance API not available")
78
+ })
79
+
80
+ // Should not throw
81
+ expect(() => exitIfBackNavigation("SAMSUNG_TV")).not.toThrow()
82
+ })
83
+
84
+ it("should handle platforms without specific exit behavior", () => {
85
+ vi.spyOn(performance, "getEntriesByType").mockReturnValue([
86
+ { type: "back_forward" } as PerformanceNavigationTiming,
87
+ ])
88
+
89
+ // Fire TV doesn't have platform-specific exit - just logs
90
+ expect(() => exitIfBackNavigation("FIRE_TV")).not.toThrow()
91
+ })
92
+ })
93
+
94
+ describe("setupLGBackButtonHandler", () => {
95
+ const LG_BACK_KEYCODE = 461
96
+
97
+ it("should call webOS.platformBack when back button is pressed", () => {
98
+ const mockPlatformBack = vi.fn()
99
+ window.webOS = { platformBack: mockPlatformBack }
100
+
101
+ const cleanup = setupLGBackButtonHandler()
102
+
103
+ // Simulate LG back button press
104
+ const event = new KeyboardEvent("keydown", {
105
+ keyCode: LG_BACK_KEYCODE,
106
+ })
107
+ document.dispatchEvent(event)
108
+
109
+ expect(mockPlatformBack).toHaveBeenCalled()
110
+
111
+ cleanup()
112
+ })
113
+
114
+ it("should fall back to window.close when platformBack is not available", () => {
115
+ window.webOS = {} // No platformBack
116
+ const mockClose = vi.fn()
117
+ vi.spyOn(window, "close").mockImplementation(mockClose)
118
+
119
+ const cleanup = setupLGBackButtonHandler()
120
+
121
+ const event = new KeyboardEvent("keydown", {
122
+ keyCode: LG_BACK_KEYCODE,
123
+ })
124
+ document.dispatchEvent(event)
125
+
126
+ expect(mockClose).toHaveBeenCalled()
127
+
128
+ cleanup()
129
+ })
130
+
131
+ it("should not trigger on other key presses", () => {
132
+ const mockPlatformBack = vi.fn()
133
+ window.webOS = { platformBack: mockPlatformBack }
134
+
135
+ const cleanup = setupLGBackButtonHandler()
136
+
137
+ // Simulate a different key press (Enter = 13)
138
+ const event = new KeyboardEvent("keydown", { keyCode: 13 })
139
+ document.dispatchEvent(event)
140
+
141
+ expect(mockPlatformBack).not.toHaveBeenCalled()
142
+
143
+ cleanup()
144
+ })
145
+
146
+ it("should remove event listener when cleanup is called", () => {
147
+ const mockPlatformBack = vi.fn()
148
+ window.webOS = { platformBack: mockPlatformBack }
149
+
150
+ const cleanup = setupLGBackButtonHandler()
151
+ cleanup()
152
+
153
+ // Simulate back button press after cleanup
154
+ const event = new KeyboardEvent("keydown", {
155
+ keyCode: LG_BACK_KEYCODE,
156
+ })
157
+ document.dispatchEvent(event)
158
+
159
+ expect(mockPlatformBack).not.toHaveBeenCalled()
160
+ })
161
+
162
+ it("should handle missing webOS object gracefully", () => {
163
+ window.webOS = undefined
164
+ const mockClose = vi.fn()
165
+ vi.spyOn(window, "close").mockImplementation(mockClose)
166
+
167
+ const cleanup = setupLGBackButtonHandler()
168
+
169
+ const event = new KeyboardEvent("keydown", {
170
+ keyCode: LG_BACK_KEYCODE,
171
+ })
172
+ document.dispatchEvent(event)
173
+
174
+ expect(mockClose).toHaveBeenCalled()
175
+
176
+ cleanup()
177
+ })
178
+ })
179
+ })
@@ -0,0 +1,125 @@
1
+ /**
2
+ * Back button / exit handling for vwr-loader.
3
+ *
4
+ * This is a safety net for when VWR fails to load and user presses back.
5
+ * When VWR loads successfully, it handles exit behavior itself.
6
+ */
7
+
8
+ import { logger } from "./logger"
9
+
10
+ /** LG webOS back button keycode */
11
+ const LG_BACK_KEYCODE = 461
12
+
13
+ /**
14
+ * Exit the app if the page was loaded via back/forward navigation.
15
+ *
16
+ * This is a safety net for Samsung/LG TVs: if VWR failed to load and user
17
+ * pressed back, they'd land on the loader again. This detects that and exits.
18
+ *
19
+ * Note: On LG TV with disableBackHistoryAPI: true, this won't trigger
20
+ * because the platform doesn't automatically navigate back. Use
21
+ * setupLGBackButtonHandler() for LG back button support.
22
+ */
23
+ export function exitIfBackNavigation(platform: Platform): void {
24
+ try {
25
+ const navEntries = performance.getEntriesByType("navigation")
26
+ const navEntry = navEntries[0] as
27
+ | PerformanceNavigationTiming
28
+ | undefined
29
+
30
+ if (navEntry?.type === "back_forward") {
31
+ logger.info(
32
+ { platform, navigationType: navEntry.type },
33
+ "[Shell] Back navigation detected, exiting app"
34
+ )
35
+ exitApp(platform)
36
+ }
37
+ } catch (error) {
38
+ // Performance API may not be available on all platforms
39
+ logger.warn({ error }, "[Shell] Could not check navigation type")
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Set up LG back button (keycode 461) handler.
45
+ *
46
+ * On LG TV with disableBackHistoryAPI: true in appinfo.json, the platform
47
+ * doesn't automatically handle the back button. This sets up a keydown
48
+ * listener to call webOS.platformBack() which shows the system exit popup
49
+ * on webOS 6.0+ or launches Home on older versions.
50
+ *
51
+ * Once VWR loads successfully, call the returned cleanup function - VWR/PSDK
52
+ * will take over back button handling with its own custom modal.
53
+ *
54
+ * @returns Cleanup function to remove the listener
55
+ */
56
+ export function setupLGBackButtonHandler(): () => void {
57
+ const handleKeyDown = (event: KeyboardEvent): void => {
58
+ if (event.keyCode === LG_BACK_KEYCODE) {
59
+ logger.info(
60
+ "[Shell] LG back button pressed, triggering platform exit"
61
+ )
62
+
63
+ if (window.webOS?.platformBack) {
64
+ window.webOS.platformBack()
65
+ } else {
66
+ logger.warn(
67
+ "[Shell] webOS.platformBack not available, using window.close"
68
+ )
69
+ window.close()
70
+ }
71
+ }
72
+ }
73
+
74
+ document.addEventListener("keydown", handleKeyDown)
75
+ logger.info("[Shell] LG back button handler registered")
76
+
77
+ return () => {
78
+ document.removeEventListener("keydown", handleKeyDown)
79
+ logger.info("[Shell] LG back button handler removed")
80
+ }
81
+ }
82
+
83
+ /**
84
+ * Exit the app using platform-specific methods.
85
+ *
86
+ * @param platform - Platform identifier
87
+ */
88
+ function exitApp(platform: Platform): void {
89
+ switch (platform) {
90
+ case "SAMSUNG_TV":
91
+ exitSamsungApp()
92
+ break
93
+ case "LG_TV":
94
+ exitLGAppViaBackNavigation()
95
+ break
96
+ // Fire TV and mobile apps handle exit at the native layer
97
+ // Web doesn't have a meaningful "exit" concept
98
+ default:
99
+ logger.info({ platform }, "[Shell] No platform-specific exit")
100
+ }
101
+ }
102
+
103
+ function exitSamsungApp(): void {
104
+ try {
105
+ if (window.tizen?.application?.getCurrentApplication) {
106
+ window.tizen.application.getCurrentApplication().exit()
107
+ } else {
108
+ logger.warn("[Shell] Tizen exit API not available")
109
+ }
110
+ } catch (error) {
111
+ logger.error({ error }, "[Shell] Failed to exit Samsung app")
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Exit LG app when back navigation is detected.
117
+ * Uses window.close() for immediate exit since user already navigated back.
118
+ */
119
+ function exitLGAppViaBackNavigation(): void {
120
+ try {
121
+ window.close()
122
+ } catch (error) {
123
+ logger.error({ error }, "[Shell] Failed to exit LG app")
124
+ }
125
+ }
package/src/loadVwr.ts CHANGED
@@ -298,6 +298,7 @@ async function initializeVwr(rum: DatadogRum | undefined) {
298
298
  await vwr.init(
299
299
  {
300
300
  hubUrl: vwrConfig.hubUrl,
301
+ launchUrl: vwrConfig.launchUrl,
301
302
  platform: PLATFORM,
302
303
  stage: ENVIRONMENT,
303
304
  appVersion: shellVersion,
package/src/main.test.ts CHANGED
@@ -70,7 +70,7 @@ describe("main.ts fallback behavior", () => {
70
70
  })
71
71
 
72
72
  expect(mockLocationHref).toHaveBeenCalledWith(
73
- "https://hub.example.com/?volley_platform=FIRE_TV"
73
+ "https://hub.example.com/?volley_platform=FIRE_TV&version=unknown"
74
74
  )
75
75
  expect(logger.info).toHaveBeenCalledWith(
76
76
  expect.objectContaining({
@@ -117,7 +117,7 @@ describe("main.ts fallback behavior", () => {
117
117
  })
118
118
 
119
119
  expect(mockLocationHref).toHaveBeenCalledWith(
120
- "https://hub.example.com/?volley_platform=FIRE_TV"
120
+ "https://hub.example.com/?volley_platform=FIRE_TV&version=unknown"
121
121
  )
122
122
  })
123
123
 
@@ -134,7 +134,7 @@ describe("main.ts fallback behavior", () => {
134
134
  })
135
135
 
136
136
  expect(mockLocationHref).toHaveBeenCalledWith(
137
- "https://hub.example.com/?volley_platform=FIRE_TV"
137
+ "https://hub.example.com/?volley_platform=FIRE_TV&version=unknown"
138
138
  )
139
139
  })
140
140
 
@@ -150,7 +150,7 @@ describe("main.ts fallback behavior", () => {
150
150
  // Verify error was logged (exact format depends on error type)
151
151
  expect(logger.error).toHaveBeenCalled()
152
152
  expect(mockLocationHref).toHaveBeenCalledWith(
153
- "https://hub.example.com/?volley_platform=FIRE_TV"
153
+ "https://hub.example.com/?volley_platform=FIRE_TV&version=unknown"
154
154
  )
155
155
  })
156
156
  })
package/src/main.ts CHANGED
@@ -1,5 +1,9 @@
1
+ // Load polyfills FIRST before any other code
2
+ import "./polyfills"
3
+
1
4
  import { initRumGlobal } from "./datadog"
2
5
  import { InitializationError, isVWRInitializationError } from "./errors"
6
+ import { exitIfBackNavigation, setupLGBackButtonHandler } from "./exitHandler"
3
7
  import { loadVwr } from "./loadVwr"
4
8
  import { logger } from "./logger"
5
9
 
@@ -42,6 +46,10 @@ function handleInitializationError(error: unknown) {
42
46
  }
43
47
 
44
48
  async function init() {
49
+ // Exit app if user pressed back - safety net for Samsung/LG TVs
50
+ // when VWR fails to load and user navigates back to loader
51
+ exitIfBackNavigation(PLATFORM)
52
+
45
53
  // Initialize Datadog RUM early (if credentials provided)
46
54
  // For mobile webviews with WebViewTracking.enable(), events are
47
55
  // automatically forwarded to the mobile app's RUM session
@@ -53,9 +61,21 @@ async function init() {
53
61
  service: "vwr-loader",
54
62
  })
55
63
 
64
+ // Set up LG back button handler while loader is active.
65
+ // On LG with disableBackHistoryAPI: true, the platform doesn't handle back automatically.
66
+ // Once VWR loads, it will take over with PSDK's custom exit modal.
67
+ let cleanupLGBackHandler: (() => void) | undefined
68
+ if (PLATFORM === "LG_TV") {
69
+ cleanupLGBackHandler = setupLGBackButtonHandler()
70
+ }
71
+
56
72
  try {
57
73
  await loadVwr(rum)
74
+ // VWR loaded successfully - remove our back handler, PSDK takes over
75
+ cleanupLGBackHandler?.()
58
76
  } catch (error) {
77
+ // Keep LG back handler active during fallback - it will handle exit
78
+ // if user presses back on the Hub page and navigates back here
59
79
  try {
60
80
  handleInitializationError(error)
61
81
  } finally {
@@ -79,6 +99,10 @@ async function init() {
79
99
  } else {
80
100
  fallbackUrl = new URL(HUB_URL)
81
101
  fallbackUrl.searchParams.set("volley_platform", PLATFORM)
102
+ fallbackUrl.searchParams.set(
103
+ "version",
104
+ SHELL_VERSION ?? "unknown"
105
+ )
82
106
 
83
107
  logger.info(
84
108
  {
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Polyfills for Chrome 68+ compatibility (Samsung/LG TVs)
3
+ *
4
+ * Import this at the top of main.ts BEFORE any other code runs.
5
+ * Conservative approach: polyfill all features not available in Chrome 68.
6
+ *
7
+ * ECMAScript polyfills (via core-js):
8
+ * - globalThis (Chrome 71)
9
+ * - Array.prototype.flat/flatMap (Chrome 69)
10
+ * - Object.fromEntries (Chrome 73)
11
+ * - String.prototype.matchAll (Chrome 73)
12
+ * - Promise.allSettled (Chrome 76)
13
+ * - String.prototype.replaceAll (Chrome 85)
14
+ * - Array.prototype.at / String.prototype.at (Chrome 92)
15
+ *
16
+ * Web API polyfills:
17
+ * - focus-visible (Chrome 86) - critical for TV remote navigation
18
+ */
19
+
20
+ // ECMAScript polyfills via core-js
21
+ import "core-js/modules/es.global-this.js"
22
+ import "core-js/modules/es.array.flat.js"
23
+ import "core-js/modules/es.array.flat-map.js"
24
+ import "core-js/modules/es.object.from-entries.js"
25
+ import "core-js/modules/es.string.match-all.js"
26
+ import "core-js/modules/es.promise.all-settled.js"
27
+ import "core-js/modules/es.string.replace-all.js"
28
+ import "core-js/modules/es.array.at.js"
29
+ import "core-js/modules/es.string.at-alternative.js"
30
+ // Web API polyfills
31
+ // focus-visible - Critical for TV remote focus rings
32
+ import "focus-visible"
package/src/types.ts CHANGED
@@ -67,10 +67,14 @@ declare global {
67
67
  fetchAppInfo?: (
68
68
  callback: (appInfo?: { version?: string }) => void
69
69
  ) => void
70
+ platformBack?: () => void
70
71
  }
71
72
  tizen?: {
72
73
  application?: {
73
74
  getAppInfo: () => { version?: string }
75
+ getCurrentApplication: () => {
76
+ exit: () => void
77
+ }
74
78
  }
75
79
  }
76
80
  }
package/src/vite-env.d.ts CHANGED
@@ -1,10 +1,22 @@
1
+ /**
2
+ * Supported platform identifiers.
3
+ * These must be uppercase - invalid values will cause build-time TypeScript errors.
4
+ */
5
+ type Platform =
6
+ | "FIRE_TV"
7
+ | "SAMSUNG_TV"
8
+ | "LG_TV"
9
+ | "ANDROID_MOBILE"
10
+ | "IOS_MOBILE"
11
+ | "WEB"
12
+
1
13
  interface ImportMetaEnv {
2
14
  readonly VITE_HUB_URL: string
3
15
  readonly VITE_VWR_URL: string
4
16
  readonly VITE_LAUNCH_URL: string
5
17
  readonly VITE_AMPLITUDE_DEPLOYMENT_KEY: string
6
18
  readonly VITE_ENVIRONMENT: string
7
- readonly VITE_PLATFORM: string
19
+ readonly VITE_PLATFORM: Platform
8
20
  readonly VITE_SHELL_VERSION: string
9
21
  readonly VITE_CONFIG_URL: string
10
22
  readonly VITE_CONFIG_FILE: string