@volley/vwr-loader 1.0.4 → 1.0.6

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,457 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
2
+
3
+ import { type Environment, getEnvironment } from "./getEnvironment"
4
+
5
+ describe("getEnvironment", () => {
6
+ const mockLogger = {
7
+ info: vi.fn(),
8
+ warn: vi.fn(),
9
+ error: vi.fn(),
10
+ }
11
+
12
+ beforeEach(() => {
13
+ vi.clearAllMocks()
14
+ mockLogger.info.mockClear()
15
+ mockLogger.warn.mockClear()
16
+ mockLogger.error.mockClear()
17
+ // Clean up window properties
18
+ delete (window as any).Capacitor
19
+ })
20
+
21
+ afterEach(() => {
22
+ vi.restoreAllMocks()
23
+ delete (window as any).Capacitor
24
+ })
25
+
26
+ describe("Fire TV - Native Environment Detection", () => {
27
+ it("should read environment from native BuildConfig for Fire TV", async () => {
28
+ // Setup Capacitor plugin mock
29
+ Object.defineProperty(window, "Capacitor", {
30
+ value: {
31
+ Plugins: {
32
+ DeviceInfo: {
33
+ getNativeShellAppEnvironment: vi
34
+ .fn()
35
+ .mockResolvedValue({
36
+ environment: "development",
37
+ }),
38
+ },
39
+ },
40
+ },
41
+ writable: true,
42
+ configurable: true,
43
+ })
44
+
45
+ const result = await getEnvironment(
46
+ "FIRE_TV",
47
+ undefined,
48
+ mockLogger
49
+ )
50
+
51
+ expect(result.environment).toBe("dev")
52
+ expect(result.source).toBe("native")
53
+ expect(mockLogger.info).toHaveBeenCalledWith(
54
+ expect.stringContaining(
55
+ 'Environment: "dev" (source: native BuildConfig.ENVIRONMENT="development")'
56
+ )
57
+ )
58
+ })
59
+
60
+ it("should map 'production' to 'prod'", async () => {
61
+ Object.defineProperty(window, "Capacitor", {
62
+ value: {
63
+ Plugins: {
64
+ DeviceInfo: {
65
+ getNativeShellAppEnvironment: vi
66
+ .fn()
67
+ .mockResolvedValue({
68
+ environment: "production",
69
+ }),
70
+ },
71
+ },
72
+ },
73
+ writable: true,
74
+ configurable: true,
75
+ })
76
+
77
+ const result = await getEnvironment(
78
+ "FIRE_TV",
79
+ undefined,
80
+ mockLogger
81
+ )
82
+
83
+ expect(result.environment).toBe("prod")
84
+ expect(result.source).toBe("native")
85
+ })
86
+
87
+ it("should map 'staging' to 'staging'", async () => {
88
+ Object.defineProperty(window, "Capacitor", {
89
+ value: {
90
+ Plugins: {
91
+ DeviceInfo: {
92
+ getNativeShellAppEnvironment: vi
93
+ .fn()
94
+ .mockResolvedValue({
95
+ environment: "staging",
96
+ }),
97
+ },
98
+ },
99
+ },
100
+ writable: true,
101
+ configurable: true,
102
+ })
103
+
104
+ const result = await getEnvironment(
105
+ "FIRE_TV",
106
+ undefined,
107
+ mockLogger
108
+ )
109
+
110
+ expect(result.environment).toBe("staging")
111
+ expect(result.source).toBe("native")
112
+ })
113
+
114
+ it("should handle native env already in vwr format (dev)", async () => {
115
+ Object.defineProperty(window, "Capacitor", {
116
+ value: {
117
+ Plugins: {
118
+ DeviceInfo: {
119
+ getNativeShellAppEnvironment: vi
120
+ .fn()
121
+ .mockResolvedValue({
122
+ environment: "dev",
123
+ }),
124
+ },
125
+ },
126
+ },
127
+ writable: true,
128
+ configurable: true,
129
+ })
130
+
131
+ const result = await getEnvironment(
132
+ "FIRE_TV",
133
+ undefined,
134
+ mockLogger
135
+ )
136
+
137
+ expect(result.environment).toBe("dev")
138
+ expect(result.source).toBe("native")
139
+ })
140
+
141
+ it("should fallback to dev when native returns unknown value", async () => {
142
+ Object.defineProperty(window, "Capacitor", {
143
+ value: {
144
+ Plugins: {
145
+ DeviceInfo: {
146
+ getNativeShellAppEnvironment: vi
147
+ .fn()
148
+ .mockResolvedValue({
149
+ environment: "unknown-env",
150
+ }),
151
+ },
152
+ },
153
+ },
154
+ writable: true,
155
+ configurable: true,
156
+ })
157
+
158
+ const result = await getEnvironment(
159
+ "FIRE_TV",
160
+ undefined,
161
+ mockLogger
162
+ )
163
+
164
+ expect(result.environment).toBe("dev")
165
+ expect(result.source).toBe("native")
166
+ })
167
+
168
+ it("should fallback to build-time when native plugin unavailable", async () => {
169
+ // No Capacitor plugin available
170
+ const result = await getEnvironment(
171
+ "FIRE_TV",
172
+ "staging",
173
+ mockLogger
174
+ )
175
+
176
+ expect(result.environment).toBe("staging")
177
+ expect(result.source).toBe("build-time")
178
+ expect(mockLogger.warn).toHaveBeenCalledWith(
179
+ expect.stringContaining(
180
+ "Failed to read environment from native, falling back to build-time"
181
+ )
182
+ )
183
+ })
184
+
185
+ it("should fallback to build-time when native plugin throws error", async () => {
186
+ Object.defineProperty(window, "Capacitor", {
187
+ value: {
188
+ Plugins: {
189
+ DeviceInfo: {
190
+ getNativeShellAppEnvironment: vi
191
+ .fn()
192
+ .mockRejectedValue(
193
+ new Error("Native plugin error")
194
+ ),
195
+ },
196
+ },
197
+ },
198
+ writable: true,
199
+ configurable: true,
200
+ })
201
+
202
+ const result = await getEnvironment("FIRE_TV", "prod", mockLogger)
203
+
204
+ expect(result.environment).toBe("prod")
205
+ expect(result.source).toBe("build-time")
206
+ expect(mockLogger.error).toHaveBeenCalled()
207
+ expect(mockLogger.warn).toHaveBeenCalledWith(
208
+ expect.stringContaining("falling back to build-time")
209
+ )
210
+ })
211
+
212
+ it("should fallback to build-time when native returns empty", async () => {
213
+ Object.defineProperty(window, "Capacitor", {
214
+ value: {
215
+ Plugins: {
216
+ DeviceInfo: {
217
+ getNativeShellAppEnvironment: vi
218
+ .fn()
219
+ .mockResolvedValue({
220
+ environment: "",
221
+ }),
222
+ },
223
+ },
224
+ },
225
+ writable: true,
226
+ configurable: true,
227
+ })
228
+
229
+ const result = await getEnvironment("FIRE_TV", "dev", mockLogger)
230
+
231
+ expect(result.environment).toBe("dev")
232
+ expect(result.source).toBe("build-time")
233
+ expect(mockLogger.error).toHaveBeenCalledWith(
234
+ expect.stringContaining(
235
+ "DeviceInfo.getNativeShellAppEnvironment returned empty"
236
+ )
237
+ )
238
+ })
239
+
240
+ it("should fallback to default when both native and build-time fail", async () => {
241
+ // No Capacitor plugin, no build-time env
242
+ const result = await getEnvironment(
243
+ "FIRE_TV",
244
+ undefined,
245
+ mockLogger
246
+ )
247
+
248
+ expect(result.environment).toBe("dev")
249
+ expect(result.source).toBe("default")
250
+ expect(mockLogger.warn).toHaveBeenCalledWith(
251
+ expect.stringContaining(
252
+ 'Environment: "dev" (source: default fallback'
253
+ )
254
+ )
255
+ })
256
+
257
+ it("should be case-insensitive for Fire TV platform", async () => {
258
+ Object.defineProperty(window, "Capacitor", {
259
+ value: {
260
+ Plugins: {
261
+ DeviceInfo: {
262
+ getNativeShellAppEnvironment: vi
263
+ .fn()
264
+ .mockResolvedValue({
265
+ environment: "production",
266
+ }),
267
+ },
268
+ },
269
+ },
270
+ writable: true,
271
+ configurable: true,
272
+ })
273
+
274
+ const result1 = await getEnvironment(
275
+ "fire_tv",
276
+ undefined,
277
+ mockLogger
278
+ )
279
+ const result2 = await getEnvironment(
280
+ "FIRE_TV",
281
+ undefined,
282
+ mockLogger
283
+ )
284
+
285
+ expect(result1.environment).toBe("prod")
286
+ expect(result2.environment).toBe("prod")
287
+ })
288
+ })
289
+
290
+ describe("Non-Fire TV Platforms - Build-time Environment", () => {
291
+ const platforms = ["SAMSUNG_TV", "LG_TV", "MOBILE", "WEB"]
292
+
293
+ platforms.forEach((platform) => {
294
+ describe(`${platform}`, () => {
295
+ it("should use build-time environment when provided", async () => {
296
+ const result = await getEnvironment(
297
+ platform,
298
+ "staging",
299
+ mockLogger
300
+ )
301
+
302
+ expect(result.environment).toBe("staging")
303
+ expect(result.source).toBe("build-time")
304
+ expect(mockLogger.info).toHaveBeenCalledWith(
305
+ expect.stringContaining(
306
+ 'Environment: "staging" (source: build-time CLI injection)'
307
+ )
308
+ )
309
+ })
310
+
311
+ it("should use build-time 'prod' environment", async () => {
312
+ const result = await getEnvironment(
313
+ platform,
314
+ "prod",
315
+ mockLogger
316
+ )
317
+
318
+ expect(result.environment).toBe("prod")
319
+ expect(result.source).toBe("build-time")
320
+ })
321
+
322
+ it("should use build-time 'dev' environment", async () => {
323
+ const result = await getEnvironment(
324
+ platform,
325
+ "dev",
326
+ mockLogger
327
+ )
328
+
329
+ expect(result.environment).toBe("dev")
330
+ expect(result.source).toBe("build-time")
331
+ })
332
+
333
+ it("should use build-time 'local' environment", async () => {
334
+ const result = await getEnvironment(
335
+ platform,
336
+ "local",
337
+ mockLogger
338
+ )
339
+
340
+ expect(result.environment).toBe("local")
341
+ expect(result.source).toBe("build-time")
342
+ })
343
+
344
+ it("should fallback to default when build-time not provided", async () => {
345
+ const result = await getEnvironment(
346
+ platform,
347
+ undefined,
348
+ mockLogger
349
+ )
350
+
351
+ expect(result.environment).toBe("dev")
352
+ expect(result.source).toBe("default")
353
+ expect(mockLogger.warn).toHaveBeenCalledWith(
354
+ expect.stringContaining(
355
+ 'Environment: "dev" (source: default fallback'
356
+ )
357
+ )
358
+ })
359
+
360
+ it("should fallback to default when build-time is invalid", async () => {
361
+ const result = await getEnvironment(
362
+ platform,
363
+ "invalid-env",
364
+ mockLogger
365
+ )
366
+
367
+ expect(result.environment).toBe("dev")
368
+ expect(result.source).toBe("default")
369
+ })
370
+ })
371
+ })
372
+ })
373
+
374
+ describe("Environment Validation", () => {
375
+ it("should accept valid environments", async () => {
376
+ const validEnvs: Environment[] = [
377
+ "local",
378
+ "dev",
379
+ "qa",
380
+ "staging",
381
+ "prod",
382
+ ]
383
+
384
+ for (const env of validEnvs) {
385
+ const result = await getEnvironment("WEB", env, mockLogger)
386
+ expect(result.environment).toBe(env)
387
+ expect(result.source).toBe("build-time")
388
+ }
389
+ })
390
+
391
+ it("should reject invalid environments", async () => {
392
+ const invalidEnvs = [
393
+ "development",
394
+ "production",
395
+ "test",
396
+ "",
397
+ "random",
398
+ ]
399
+
400
+ for (const env of invalidEnvs) {
401
+ const result = await getEnvironment("WEB", env, mockLogger)
402
+ expect(result.environment).toBe("dev")
403
+ expect(result.source).toBe("default")
404
+ }
405
+ })
406
+ })
407
+
408
+ describe("Logging", () => {
409
+ it("should log native environment source", async () => {
410
+ Object.defineProperty(window, "Capacitor", {
411
+ value: {
412
+ Plugins: {
413
+ DeviceInfo: {
414
+ getNativeShellAppEnvironment: vi
415
+ .fn()
416
+ .mockResolvedValue({
417
+ environment: "production",
418
+ }),
419
+ },
420
+ },
421
+ },
422
+ writable: true,
423
+ configurable: true,
424
+ })
425
+
426
+ await getEnvironment("FIRE_TV", undefined, mockLogger)
427
+
428
+ expect(mockLogger.info).toHaveBeenCalledWith(
429
+ '[Shell] Environment: "prod" (source: native BuildConfig.ENVIRONMENT="production")'
430
+ )
431
+ })
432
+
433
+ it("should log build-time environment source", async () => {
434
+ await getEnvironment("SAMSUNG_TV", "staging", mockLogger)
435
+
436
+ expect(mockLogger.info).toHaveBeenCalledWith(
437
+ '[Shell] Environment: "staging" (source: build-time CLI injection)'
438
+ )
439
+ })
440
+
441
+ it("should warn when falling back to default", async () => {
442
+ await getEnvironment("LG_TV", undefined, mockLogger)
443
+
444
+ expect(mockLogger.warn).toHaveBeenCalledWith(
445
+ '[Shell] Environment: "dev" (source: default fallback - no native or build-time env found)'
446
+ )
447
+ })
448
+
449
+ it("should warn when native fails and falls back", async () => {
450
+ await getEnvironment("FIRE_TV", "prod", mockLogger)
451
+
452
+ expect(mockLogger.warn).toHaveBeenCalledWith(
453
+ "[Shell] Failed to read environment from native, falling back to build-time"
454
+ )
455
+ })
456
+ })
457
+ })
@@ -0,0 +1,140 @@
1
+ import { defaultLogger, type Logger } from "./logger"
2
+
3
+ export type Environment = "local" | "dev" | "qa" | "staging" | "prod"
4
+ export type EnvironmentSource = "native" | "build-time" | "default"
5
+
6
+ export interface EnvironmentResult {
7
+ environment: Environment
8
+ source: EnvironmentSource
9
+ }
10
+
11
+ /**
12
+ * Get the environment for the current platform.
13
+ *
14
+ * Precedence:
15
+ * 1. Native shell (Fire TV only) - reads from BuildConfig.ENVIRONMENT
16
+ * 2. Build-time injection (VITE_ENVIRONMENT) - CLI override
17
+ * 3. Default fallback ("dev")
18
+ *
19
+ * This ensures production APKs cannot accidentally run dev VWR,
20
+ * since the native Android build variant determines the environment.
21
+ *
22
+ * @param platform - Platform identifier ('FIRE_TV', 'SAMSUNG_TV', 'LG_TV', 'MOBILE', 'WEB')
23
+ * @param buildTimeEnv - Optional build-time injected environment (from CLI)
24
+ * @param logger - Optional logger for reporting
25
+ * @returns Environment result with source information for logging
26
+ */
27
+ export async function getEnvironment(
28
+ platform: string,
29
+ buildTimeEnv: string | undefined,
30
+ logger: Logger = defaultLogger
31
+ ): Promise<EnvironmentResult> {
32
+ const normalizedPlatform = platform.toUpperCase()
33
+
34
+ // For Fire TV, try to read from native first (safest - prevents env mismatch)
35
+ if (normalizedPlatform === "FIRE_TV") {
36
+ const nativeEnv = await getFireTVEnvironment(logger)
37
+ if (nativeEnv) {
38
+ const mapped = mapNativeEnvironment(nativeEnv)
39
+ logger.info(
40
+ `[Shell] Environment: "${mapped}" (source: native BuildConfig.ENVIRONMENT="${nativeEnv}")`
41
+ )
42
+ return { environment: mapped, source: "native" }
43
+ }
44
+ // Fall through to build-time if native fails
45
+ logger.warn(
46
+ "[Shell] Failed to read environment from native, falling back to build-time"
47
+ )
48
+ }
49
+
50
+ // Build-time injection (CLI --env flag)
51
+ if (buildTimeEnv) {
52
+ const normalizedBuildTimeEnv = buildTimeEnv.toLowerCase()
53
+ if (isValidEnvironment(normalizedBuildTimeEnv)) {
54
+ logger.info(
55
+ `[Shell] Environment: "${normalizedBuildTimeEnv}" (source: build-time CLI injection)`
56
+ )
57
+ return {
58
+ environment: normalizedBuildTimeEnv as Environment,
59
+ source: "build-time",
60
+ }
61
+ }
62
+ }
63
+
64
+ // Default fallback
65
+ const defaultEnv: Environment = "dev"
66
+ logger.warn(
67
+ `[Shell] Environment: "${defaultEnv}" (source: default fallback - no native or build-time env found)`
68
+ )
69
+ return { environment: defaultEnv, source: "default" }
70
+ }
71
+
72
+ /**
73
+ * Read environment from Fire TV native shell via Capacitor plugin.
74
+ */
75
+ async function getFireTVEnvironment(logger: Logger): Promise<string | null> {
76
+ try {
77
+ if (
78
+ !window.Capacitor?.Plugins?.DeviceInfo?.getNativeShellAppEnvironment
79
+ ) {
80
+ logger.warn(
81
+ "[Shell] DeviceInfo.getNativeShellAppEnvironment not available"
82
+ )
83
+ return null
84
+ }
85
+
86
+ const result =
87
+ await window.Capacitor.Plugins.DeviceInfo.getNativeShellAppEnvironment()
88
+ if (result?.environment && result.environment.trim()) {
89
+ return result.environment
90
+ } else {
91
+ logger.error(
92
+ "[Shell] DeviceInfo.getNativeShellAppEnvironment returned empty"
93
+ )
94
+ }
95
+ } catch (error) {
96
+ logger.error(
97
+ "[Shell] DeviceInfo.getNativeShellAppEnvironment failed:",
98
+ { error }
99
+ )
100
+ }
101
+ return null
102
+ }
103
+
104
+ /**
105
+ * Map native Android BuildConfig.ENVIRONMENT values to vwr-loader environment names.
106
+ *
107
+ * Android BuildConfig values:
108
+ * - "development" (debug build)
109
+ * - "staging" (staging build)
110
+ * - "production" (release build)
111
+ *
112
+ * VWR environment names:
113
+ * - "dev"
114
+ * - "staging"
115
+ * - "prod"
116
+ */
117
+ function mapNativeEnvironment(nativeEnv: string): Environment {
118
+ const normalized = nativeEnv.toLowerCase()
119
+ switch (normalized) {
120
+ case "development":
121
+ return "dev"
122
+ case "staging":
123
+ return "staging"
124
+ case "production":
125
+ return "prod"
126
+ default:
127
+ // If it's already in vwr format, use it
128
+ if (isValidEnvironment(normalized)) {
129
+ return normalized as Environment
130
+ }
131
+ return "dev"
132
+ }
133
+ }
134
+
135
+ function isValidEnvironment(env: string): boolean {
136
+ return ["local", "dev", "qa", "staging", "prod"].includes(env)
137
+ }
138
+
139
+ // Note: Window.Capacitor type is declared in getDeviceId.ts
140
+ // We just need to add the getNativeShellAppEnvironment method type there
package/src/loadVwr.ts CHANGED
@@ -3,13 +3,17 @@ import type * as VWR from "@volley/vwr"
3
3
  import { fetchAmplitudeFlags } from "./amplitudeFlagFetcher"
4
4
  import { ENV_DEFAULTS } from "./envDefaults"
5
5
  import { getDeviceId } from "./getDeviceId"
6
+ import { getEnvironment } from "./getEnvironment"
6
7
  import { getShellVersion } from "./getShellVersion"
7
8
  import { defaultLogger, type Logger } from "./logger"
8
9
  import type { VWRConfig, VWRConfigRequest } from "./vwrConfig"
9
10
  import { getVWRConfig, validateConfig } from "./vwrConfig"
10
11
 
11
12
  // Vite injects these at build time
12
- const ENVIRONMENT = import.meta.env.VITE_ENVIRONMENT
13
+ // VITE_ENVIRONMENT is optional for platforms that support native env reading (Fire TV)
14
+ const BUILD_TIME_ENVIRONMENT = import.meta.env.VITE_ENVIRONMENT as
15
+ | string
16
+ | undefined
13
17
  const PLATFORM = import.meta.env.VITE_PLATFORM
14
18
  const CONFIG_URL = import.meta.env.VITE_CONFIG_URL
15
19
  const CONFIG_FILE = import.meta.env.VITE_CONFIG_FILE
@@ -28,15 +32,20 @@ export type { Logger }
28
32
  * this reduces startup latency from ~2-4s to ~500ms by skipping config fetches.
29
33
  *
30
34
  * Flow:
31
- * 1. Get deviceId, shellVersion (fast, local)
35
+ * 1. Get deviceId, shellVersion, environment (fast, local/native)
32
36
  * 2. Check vwr-enabled flag (single request, ~500ms)
33
37
  * 3. If OFF → throw immediately (no config fetches)
34
38
  * 4. If ON → fetch config, load VWR module, initialize
35
39
  *
40
+ * Environment resolution:
41
+ * - Fire TV: reads from native BuildConfig.ENVIRONMENT (prevents env mismatch)
42
+ * - Other platforms: uses build-time injected VITE_ENVIRONMENT
43
+ * - Fallback: "dev"
44
+ *
36
45
  * @param logger - Optional logger, defaults to console
37
46
  */
38
47
  export const loadVwr = async (logger: Logger = defaultLogger) => {
39
- if (!ENVIRONMENT || !PLATFORM || !CONFIG_URL || !CONFIG_FILE) {
48
+ if (!PLATFORM || !CONFIG_URL || !CONFIG_FILE) {
40
49
  throw new Error("[Shell] Build config not injected properly")
41
50
  }
42
51
 
@@ -48,6 +57,14 @@ export const loadVwr = async (logger: Logger = defaultLogger) => {
48
57
  }
49
58
  const shellVersion = await getShellVersion(PLATFORM, logger)
50
59
 
60
+ // Get environment from native (Fire TV) or build-time injection
61
+ // This ensures prod APKs cannot accidentally run dev VWR
62
+ const { environment: ENVIRONMENT } = await getEnvironment(
63
+ PLATFORM,
64
+ BUILD_TIME_ENVIRONMENT,
65
+ logger
66
+ )
67
+
51
68
  // Get amplitude key: injected at build time OR from envDefaults
52
69
  // Precedence: build-time injection > envDefaults
53
70
  let amplitudeKey = AMPLITUDE_KEY
@@ -68,7 +85,8 @@ export const loadVwr = async (logger: Logger = defaultLogger) => {
68
85
  apiKey: amplitudeKey,
69
86
  timeout: 2000,
70
87
  },
71
- shellVersion
88
+ shellVersion,
89
+ logger
72
90
  )
73
91
 
74
92
  logger.info("[Shell] Flags fetched", { flags })
@@ -90,8 +90,8 @@ describe("vwrConfig", async () => {
90
90
  expect(config.hubUrl).toBe("https://game-clients.volley.tv/hub")
91
91
  expect(config.launchUrl).toBeUndefined() // Optional, not set by default
92
92
  expect(config.trustedDomains).toEqual([
93
- "https://game-clients.volley.tv/hub",
94
- "https://vwr.volley.tv/v1/latest/vwr.js",
93
+ "https://game-clients.volley.tv",
94
+ "https://vwr.volley.tv",
95
95
  ])
96
96
  })
97
97