@volley/vwr-loader 1.0.5 → 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.
- package/README.md +21 -0
- package/dist/amplitudeFlagFetcher.d.ts +4 -2
- package/dist/amplitudeFlagFetcher.d.ts.map +1 -1
- package/dist/amplitudeFlagFetcher.js +26 -19
- package/dist/amplitudeFlagFetcher.js.map +1 -1
- package/dist/cli.js +234 -0
- package/dist/cli.js.map +1 -0
- package/dist/envDefaults.d.ts +3 -0
- package/dist/envDefaults.d.ts.map +1 -1
- package/dist/envDefaults.js +26 -4
- package/dist/envDefaults.js.map +1 -1
- package/dist/getDeviceId.d.ts +2 -10
- package/dist/getDeviceId.d.ts.map +1 -1
- package/dist/getDeviceId.js +31 -18
- package/dist/getDeviceId.js.map +1 -1
- package/dist/getEnvironment.d.ts +1 -1
- package/dist/getEnvironment.d.ts.map +1 -1
- package/dist/getEnvironment.js +10 -7
- package/dist/getEnvironment.js.map +1 -1
- package/dist/getShellVersion.d.ts +2 -2
- package/dist/getShellVersion.d.ts.map +1 -1
- package/dist/getShellVersion.js +5 -3
- package/dist/getShellVersion.js.map +1 -1
- package/dist/index.html +1 -1
- package/dist/loadVwr.d.ts.map +1 -1
- package/dist/loadVwr.js +8 -6
- package/dist/loadVwr.js.map +1 -1
- package/dist/main.js +1 -1
- package/dist/main.js.map +1 -1
- package/dist/vwrConfig.d.ts +2 -0
- package/dist/vwrConfig.d.ts.map +1 -1
- package/dist/vwrConfig.js +148 -119
- package/dist/vwrConfig.js.map +1 -1
- package/package.json +16 -5
- package/src/amplitudeFlagFetcher.test.ts +36 -59
- package/src/amplitudeFlagFetcher.ts +31 -22
- package/src/envDefaults.ts +33 -4
- package/src/getDeviceId.test.ts +79 -24
- package/src/getDeviceId.ts +47 -22
- package/src/getEnvironment.test.ts +457 -0
- package/src/getEnvironment.ts +12 -9
- package/src/getShellVersion.test.ts +1 -1
- package/src/getShellVersion.ts +7 -3
- package/src/loadVwr.ts +13 -6
- package/src/main.ts +3 -1
- package/src/vite-env.d.ts +3 -0
- package/src/vwrConfig.test.ts +2 -2
- package/src/vwrConfig.ts +202 -199
- package/eslint.config.mjs +0 -23
- package/scripts/build.js +0 -2
- package/scripts/build.ts +0 -207
- package/tsconfig.eslint.json +0 -16
- package/tsconfig.json +0 -17
- package/vite.config.ts +0 -24
- package/vitest.config.ts +0 -8
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@volley/vwr-loader",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Vite-based VWR loader for all Volley platforms",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -13,31 +13,42 @@
|
|
|
13
13
|
"./envDefaults": "./dist/envDefaults.js"
|
|
14
14
|
},
|
|
15
15
|
"bin": {
|
|
16
|
-
"build-loader": "./
|
|
16
|
+
"build-loader": "./dist/cli.js"
|
|
17
17
|
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"src"
|
|
21
|
+
],
|
|
18
22
|
"scripts": {
|
|
19
|
-
"build": "tsc && vite build",
|
|
23
|
+
"build": "tsc && vite build && vite build -c vite.cli.config.ts && chmod +x dist/cli.js",
|
|
24
|
+
"build:cli": "vite build -c vite.cli.config.ts",
|
|
20
25
|
"build:loader": "vite build",
|
|
21
26
|
"typecheck": "tsc --noEmit",
|
|
22
27
|
"test": "vitest run",
|
|
23
28
|
"test:watch": "vitest",
|
|
24
29
|
"format": "eslint . --fix",
|
|
25
|
-
"lint": "eslint ."
|
|
30
|
+
"lint": "eslint .",
|
|
31
|
+
"semantic-release": "semantic-release"
|
|
26
32
|
},
|
|
27
33
|
"dependencies": {
|
|
28
34
|
"commander": "^12.0.0",
|
|
29
35
|
"dotenv": "^16.0.0",
|
|
30
36
|
"fs-extra": "^11.0.0",
|
|
31
|
-
"tsx": "^4.7.0",
|
|
32
37
|
"vite": "^5.0.0"
|
|
33
38
|
},
|
|
34
39
|
"devDependencies": {
|
|
40
|
+
"tsx": "^4.7.0",
|
|
35
41
|
"@platform/eslint-config": "workspace:*",
|
|
42
|
+
"@semantic-release/git": "^10.0.1",
|
|
43
|
+
"@semantic-release/npm": "^12.0.1",
|
|
36
44
|
"@types/fs-extra": "^11.0.0",
|
|
37
45
|
"@types/node": "^20.0.0",
|
|
46
|
+
"@volley/vwr": "workspace:*",
|
|
38
47
|
"eslint": "^9.25.1",
|
|
39
48
|
"jsdom": "^24.1.3",
|
|
40
49
|
"prettier": "~3.0.3",
|
|
50
|
+
"semantic-release": "^24.2.3",
|
|
51
|
+
"semantic-release-monorepo": "^8.0.2",
|
|
41
52
|
"typescript": "^5.0.0",
|
|
42
53
|
"vitest": "^1.0.0"
|
|
43
54
|
}
|
|
@@ -1,26 +1,16 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"
|
|
2
2
|
|
|
3
3
|
import { fetchAmplitudeFlags } from "./amplitudeFlagFetcher"
|
|
4
|
-
import * as getShellVersionModule from "./getShellVersion"
|
|
5
4
|
|
|
6
5
|
describe("fetchAmplitudeFlags", () => {
|
|
7
6
|
const mockFetch = vi.fn()
|
|
8
|
-
const originalLocation = window.location
|
|
9
7
|
|
|
10
8
|
beforeEach(() => {
|
|
11
9
|
global.fetch = mockFetch
|
|
12
10
|
vi.clearAllMocks()
|
|
13
|
-
|
|
14
|
-
// Mock window.location for getShellVersion
|
|
15
|
-
delete (window as any).location
|
|
16
|
-
window.location = {
|
|
17
|
-
...originalLocation,
|
|
18
|
-
href: "http://localhost:3000/?version=1.0.0",
|
|
19
|
-
} as Location
|
|
20
11
|
})
|
|
21
12
|
|
|
22
13
|
afterEach(() => {
|
|
23
|
-
window.location = originalLocation
|
|
24
14
|
vi.restoreAllMocks()
|
|
25
15
|
})
|
|
26
16
|
|
|
@@ -36,9 +26,12 @@ describe("fetchAmplitudeFlags", () => {
|
|
|
36
26
|
}),
|
|
37
27
|
})
|
|
38
28
|
|
|
39
|
-
const result = await fetchAmplitudeFlags(
|
|
40
|
-
|
|
41
|
-
|
|
29
|
+
const result = await fetchAmplitudeFlags(
|
|
30
|
+
"device-123",
|
|
31
|
+
"SAMSUNG_TV",
|
|
32
|
+
{ apiKey: "test-key" },
|
|
33
|
+
"1.0.0"
|
|
34
|
+
)
|
|
42
35
|
|
|
43
36
|
expect(result).toEqual({
|
|
44
37
|
"vwr-enabled": true,
|
|
@@ -98,45 +91,17 @@ describe("fetchAmplitudeFlags", () => {
|
|
|
98
91
|
})
|
|
99
92
|
})
|
|
100
93
|
|
|
101
|
-
it("auto-fetches shell version when not provided", async () => {
|
|
102
|
-
const getShellVersionSpy = vi.spyOn(
|
|
103
|
-
getShellVersionModule,
|
|
104
|
-
"getShellVersion"
|
|
105
|
-
)
|
|
106
|
-
|
|
107
|
-
mockFetch.mockResolvedValueOnce({
|
|
108
|
-
ok: true,
|
|
109
|
-
json: async () => ({
|
|
110
|
-
"vwr-enabled": {
|
|
111
|
-
payload: {
|
|
112
|
-
"vwr-enabled": false,
|
|
113
|
-
},
|
|
114
|
-
},
|
|
115
|
-
}),
|
|
116
|
-
})
|
|
117
|
-
|
|
118
|
-
await fetchAmplitudeFlags("device-456", "LG_TV", {
|
|
119
|
-
apiKey: "test-key",
|
|
120
|
-
})
|
|
121
|
-
|
|
122
|
-
expect(getShellVersionSpy).toHaveBeenCalledWith("LG_TV")
|
|
123
|
-
|
|
124
|
-
const callArgs = mockFetch.mock.calls[0]
|
|
125
|
-
const url = new URL(callArgs[0])
|
|
126
|
-
const context = JSON.parse(url.searchParams.get("context") || "{}")
|
|
127
|
-
|
|
128
|
-
expect(context.user_properties).toHaveProperty("nativeShellAppVersion")
|
|
129
|
-
})
|
|
130
|
-
|
|
131
94
|
it("returns safe defaults on timeout", async () => {
|
|
132
95
|
mockFetch.mockImplementationOnce(
|
|
133
96
|
() => new Promise((resolve) => setTimeout(resolve, 3000))
|
|
134
97
|
)
|
|
135
98
|
|
|
136
|
-
const result = await fetchAmplitudeFlags(
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
99
|
+
const result = await fetchAmplitudeFlags(
|
|
100
|
+
"device-123",
|
|
101
|
+
"FIRE_TV",
|
|
102
|
+
{ apiKey: "test-key", timeout: 100 },
|
|
103
|
+
"1.0.0"
|
|
104
|
+
)
|
|
140
105
|
|
|
141
106
|
expect(result).toEqual({
|
|
142
107
|
"vwr-enabled": false,
|
|
@@ -149,9 +114,12 @@ describe("fetchAmplitudeFlags", () => {
|
|
|
149
114
|
status: 500,
|
|
150
115
|
})
|
|
151
116
|
|
|
152
|
-
const result = await fetchAmplitudeFlags(
|
|
153
|
-
|
|
154
|
-
|
|
117
|
+
const result = await fetchAmplitudeFlags(
|
|
118
|
+
"device-123",
|
|
119
|
+
"MOBILE",
|
|
120
|
+
{ apiKey: "test-key" },
|
|
121
|
+
"1.0.0"
|
|
122
|
+
)
|
|
155
123
|
|
|
156
124
|
expect(result).toEqual({
|
|
157
125
|
"vwr-enabled": false,
|
|
@@ -161,9 +129,12 @@ describe("fetchAmplitudeFlags", () => {
|
|
|
161
129
|
it("returns safe defaults on network error", async () => {
|
|
162
130
|
mockFetch.mockRejectedValueOnce(new Error("Network error"))
|
|
163
131
|
|
|
164
|
-
const result = await fetchAmplitudeFlags(
|
|
165
|
-
|
|
166
|
-
|
|
132
|
+
const result = await fetchAmplitudeFlags(
|
|
133
|
+
"device-123",
|
|
134
|
+
"MOBILE",
|
|
135
|
+
{ apiKey: "test-key" },
|
|
136
|
+
"1.0.0"
|
|
137
|
+
)
|
|
167
138
|
|
|
168
139
|
expect(result).toEqual({
|
|
169
140
|
"vwr-enabled": false,
|
|
@@ -176,9 +147,12 @@ describe("fetchAmplitudeFlags", () => {
|
|
|
176
147
|
json: async () => ({}),
|
|
177
148
|
})
|
|
178
149
|
|
|
179
|
-
const result = await fetchAmplitudeFlags(
|
|
180
|
-
|
|
181
|
-
|
|
150
|
+
const result = await fetchAmplitudeFlags(
|
|
151
|
+
"device-123",
|
|
152
|
+
"LG_TV",
|
|
153
|
+
{ apiKey: "test-key" },
|
|
154
|
+
"1.0.0"
|
|
155
|
+
)
|
|
182
156
|
|
|
183
157
|
expect(result).toEqual({
|
|
184
158
|
"vwr-enabled": false,
|
|
@@ -193,9 +167,12 @@ describe("fetchAmplitudeFlags", () => {
|
|
|
193
167
|
}),
|
|
194
168
|
})
|
|
195
169
|
|
|
196
|
-
const result = await fetchAmplitudeFlags(
|
|
197
|
-
|
|
198
|
-
|
|
170
|
+
const result = await fetchAmplitudeFlags(
|
|
171
|
+
"device-123",
|
|
172
|
+
"LG_TV",
|
|
173
|
+
{ apiKey: "test-key" },
|
|
174
|
+
"1.0.0"
|
|
175
|
+
)
|
|
199
176
|
|
|
200
177
|
expect(result).toEqual({
|
|
201
178
|
"vwr-enabled": false,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { defaultLogger, type Logger } from "./logger"
|
|
2
2
|
|
|
3
3
|
export interface AmplitudeConfig {
|
|
4
4
|
apiKey: string
|
|
@@ -20,14 +20,16 @@ export interface FlagResult {
|
|
|
20
20
|
* @param deviceId - Unique device identifier
|
|
21
21
|
* @param platform - Platform name (samsung, lg, firetv, ios, android)
|
|
22
22
|
* @param config - Amplitude configuration
|
|
23
|
-
* @param shellVersion -
|
|
23
|
+
* @param shellVersion - Shell version for flag targeting
|
|
24
|
+
* @param logger - Optional logger, defaults to console
|
|
24
25
|
* @returns Flag values with safe defaults on error
|
|
25
26
|
*/
|
|
26
27
|
export async function fetchAmplitudeFlags(
|
|
27
28
|
deviceId: string,
|
|
28
29
|
platform: string,
|
|
29
30
|
config: AmplitudeConfig,
|
|
30
|
-
shellVersion
|
|
31
|
+
shellVersion: string,
|
|
32
|
+
logger: Logger = defaultLogger
|
|
31
33
|
): Promise<FlagResult> {
|
|
32
34
|
const {
|
|
33
35
|
apiKey,
|
|
@@ -35,28 +37,26 @@ export async function fetchAmplitudeFlags(
|
|
|
35
37
|
timeout = 2000,
|
|
36
38
|
} = config
|
|
37
39
|
|
|
40
|
+
// Build context with user_properties
|
|
41
|
+
const context = {
|
|
42
|
+
user_properties: {
|
|
43
|
+
platformEnum: platform,
|
|
44
|
+
nativeShellAppVersion: shellVersion,
|
|
45
|
+
},
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Build query parameters
|
|
49
|
+
const params = new URLSearchParams({
|
|
50
|
+
user_id: deviceId,
|
|
51
|
+
flag_keys: "vwr-enabled",
|
|
52
|
+
context: JSON.stringify(context),
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
// Start timeout immediately before the fetch call for accurate timing
|
|
38
56
|
const controller = new AbortController()
|
|
39
57
|
const timeoutId = setTimeout(() => controller.abort(), timeout)
|
|
40
58
|
|
|
41
59
|
try {
|
|
42
|
-
// Get shell version if not provided
|
|
43
|
-
const version = shellVersion ?? (await getShellVersion(platform))
|
|
44
|
-
|
|
45
|
-
// Build context with user_properties
|
|
46
|
-
const context = {
|
|
47
|
-
user_properties: {
|
|
48
|
-
platformEnum: platform,
|
|
49
|
-
nativeShellAppVersion: version,
|
|
50
|
-
},
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
// Build query parameters
|
|
54
|
-
const params = new URLSearchParams({
|
|
55
|
-
user_id: deviceId,
|
|
56
|
-
flag_keys: "vwr-enabled",
|
|
57
|
-
context: JSON.stringify(context),
|
|
58
|
-
})
|
|
59
|
-
|
|
60
60
|
const response = await fetch(`${apiUrl}?${params}`, {
|
|
61
61
|
method: "GET",
|
|
62
62
|
headers: {
|
|
@@ -78,7 +78,16 @@ export async function fetchAmplitudeFlags(
|
|
|
78
78
|
data["vwr-enabled"]?.payload?.["vwr-enabled"] ?? false,
|
|
79
79
|
}
|
|
80
80
|
} catch (error) {
|
|
81
|
-
|
|
81
|
+
clearTimeout(timeoutId)
|
|
82
|
+
|
|
83
|
+
// Provide clear warning message for abort vs other errors
|
|
84
|
+
if (error instanceof Error && error.name === "AbortError") {
|
|
85
|
+
logger.warn(
|
|
86
|
+
`[Shell] Amplitude flag fetch aborted because it took longer than ${timeout}ms`
|
|
87
|
+
)
|
|
88
|
+
} else {
|
|
89
|
+
logger.warn("[Shell] Amplitude flag fetch failed:", { error })
|
|
90
|
+
}
|
|
82
91
|
|
|
83
92
|
// Return safe defaults - runs legacy path
|
|
84
93
|
return {
|
package/src/envDefaults.ts
CHANGED
|
@@ -3,7 +3,10 @@ export interface EnvConfig {
|
|
|
3
3
|
vwrUrl: string
|
|
4
4
|
configUrl: string
|
|
5
5
|
configFile: string
|
|
6
|
+
platformApiUrl: string
|
|
7
|
+
platformAuthApiUrl: string
|
|
6
8
|
amplitudeKey: string // Used for amplitude flag fetch, not part of VWRConfig
|
|
9
|
+
trustedOrigins?: string[]
|
|
7
10
|
}
|
|
8
11
|
|
|
9
12
|
const CONFIG_URL_DEFAULT = "https://vwr.volley.tv/config/"
|
|
@@ -15,22 +18,36 @@ const prodConfig: EnvConfig = {
|
|
|
15
18
|
vwrUrl: `https://vwr.volley.tv/${VWR_URL_PATH_DEFAULT}`,
|
|
16
19
|
configUrl: CONFIG_URL_DEFAULT,
|
|
17
20
|
configFile: CONFIG_FILE_DEFAULT,
|
|
21
|
+
platformApiUrl: "https://platform.volley-services.net",
|
|
22
|
+
platformAuthApiUrl: "https://auth.volley.tv",
|
|
18
23
|
amplitudeKey: "",
|
|
19
24
|
}
|
|
20
25
|
|
|
21
26
|
export const ENV_DEFAULTS: Record<string, EnvConfig> = {
|
|
22
27
|
local: {
|
|
23
|
-
hubUrl: "http://localhost:5173",
|
|
24
|
-
vwrUrl: "http://localhost:5174/vwr.js",
|
|
25
|
-
configUrl:
|
|
26
|
-
|
|
28
|
+
hubUrl: import.meta.env.VITE_HUB_URL || "http://localhost:5173",
|
|
29
|
+
vwrUrl: import.meta.env.VITE_VWR_URL || "http://localhost:5174/vwr.js",
|
|
30
|
+
configUrl:
|
|
31
|
+
import.meta.env.VITE_CONFIG_URL || "http://localhost:5174/config/",
|
|
32
|
+
configFile: import.meta.env.VITE_CONFIG_FILE || CONFIG_FILE_DEFAULT,
|
|
33
|
+
platformApiUrl:
|
|
34
|
+
import.meta.env.VITE_PLATFORM_API_URL ||
|
|
35
|
+
"https://platform-dev.volley-services.net",
|
|
36
|
+
platformAuthApiUrl:
|
|
37
|
+
import.meta.env.VITE_PLATFORM_AUTH_API_URL ||
|
|
38
|
+
"https://auth-dev.volley.tv",
|
|
27
39
|
amplitudeKey: "client-uJJVW3zKPC1G9kqPhUumLnZN6eaY42iQ",
|
|
40
|
+
trustedOrigins: import.meta.env.VITE_TRUSTED_ORIGINS?.split(",").filter(
|
|
41
|
+
Boolean
|
|
42
|
+
),
|
|
28
43
|
},
|
|
29
44
|
dev: {
|
|
30
45
|
hubUrl: "https://game-clients-dev.volley.tv/hub",
|
|
31
46
|
vwrUrl: `https://vwr.volley.tv/dev/${VWR_URL_PATH_DEFAULT}`,
|
|
32
47
|
configUrl: CONFIG_URL_DEFAULT,
|
|
33
48
|
configFile: CONFIG_FILE_DEFAULT,
|
|
49
|
+
platformApiUrl: "https://platform-dev.volley-services.net",
|
|
50
|
+
platformAuthApiUrl: "https://auth-dev.volley.tv",
|
|
34
51
|
amplitudeKey: "client-uJJVW3zKPC1G9kqPhUumLnZN6eaY42iQ",
|
|
35
52
|
},
|
|
36
53
|
staging: {
|
|
@@ -38,8 +55,20 @@ export const ENV_DEFAULTS: Record<string, EnvConfig> = {
|
|
|
38
55
|
vwrUrl: `https://vwr.volley.tv/staging/${VWR_URL_PATH_DEFAULT}`,
|
|
39
56
|
configUrl: CONFIG_URL_DEFAULT,
|
|
40
57
|
configFile: CONFIG_FILE_DEFAULT,
|
|
58
|
+
platformApiUrl: "https://platform-staging.volley-services.net",
|
|
59
|
+
platformAuthApiUrl: "https://auth-staging.volley.tv",
|
|
41
60
|
amplitudeKey: "",
|
|
42
61
|
},
|
|
43
62
|
prod: prodConfig,
|
|
44
63
|
production: prodConfig, // Alias for prod
|
|
64
|
+
qa: {
|
|
65
|
+
// QA uses staging infrastructure
|
|
66
|
+
hubUrl: "https://game-clients-staging.volley.tv/hub",
|
|
67
|
+
vwrUrl: `https://vwr.volley.tv/staging/${VWR_URL_PATH_DEFAULT}`,
|
|
68
|
+
configUrl: CONFIG_URL_DEFAULT,
|
|
69
|
+
configFile: CONFIG_FILE_DEFAULT,
|
|
70
|
+
platformApiUrl: "https://platform-staging.volley-services.net",
|
|
71
|
+
platformAuthApiUrl: "https://auth-staging.volley.tv",
|
|
72
|
+
amplitudeKey: "",
|
|
73
|
+
},
|
|
45
74
|
}
|
package/src/getDeviceId.test.ts
CHANGED
|
@@ -6,8 +6,9 @@ describe("getDeviceId", () => {
|
|
|
6
6
|
beforeEach(() => {
|
|
7
7
|
vi.clearAllMocks()
|
|
8
8
|
// Clean up window properties
|
|
9
|
-
delete (window as any).
|
|
10
|
-
delete (window as any).
|
|
9
|
+
delete (window as any).Capacitor
|
|
10
|
+
delete (window as any).iosAppContext
|
|
11
|
+
delete (window as any).androidAppContext
|
|
11
12
|
delete (window as any).webapis
|
|
12
13
|
delete (window as any).webOSDev
|
|
13
14
|
delete (window as any).webOS
|
|
@@ -15,21 +16,26 @@ describe("getDeviceId", () => {
|
|
|
15
16
|
|
|
16
17
|
afterEach(() => {
|
|
17
18
|
vi.restoreAllMocks()
|
|
18
|
-
delete (window as any).
|
|
19
|
-
delete (window as any).
|
|
19
|
+
delete (window as any).Capacitor
|
|
20
|
+
delete (window as any).iosAppContext
|
|
21
|
+
delete (window as any).androidAppContext
|
|
20
22
|
delete (window as any).webapis
|
|
21
23
|
delete (window as any).webOSDev
|
|
22
24
|
delete (window as any).webOS
|
|
23
25
|
})
|
|
24
26
|
|
|
25
27
|
describe("FIRE_TV platform", () => {
|
|
26
|
-
it("returns device ID from DeviceInfo.getAndroidId when available", async () => {
|
|
28
|
+
it("returns device ID from Capacitor DeviceInfo.getAndroidId when available", async () => {
|
|
27
29
|
const mockAndroidId = "firetv-android-id-123"
|
|
28
|
-
Object.defineProperty(window, "
|
|
30
|
+
Object.defineProperty(window, "Capacitor", {
|
|
29
31
|
value: {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
32
|
+
Plugins: {
|
|
33
|
+
DeviceInfo: {
|
|
34
|
+
getAndroidId: vi.fn().mockResolvedValue({
|
|
35
|
+
androidId: mockAndroidId,
|
|
36
|
+
}),
|
|
37
|
+
},
|
|
38
|
+
},
|
|
33
39
|
},
|
|
34
40
|
writable: true,
|
|
35
41
|
configurable: true,
|
|
@@ -40,11 +46,17 @@ describe("getDeviceId", () => {
|
|
|
40
46
|
})
|
|
41
47
|
|
|
42
48
|
it("returns null when DeviceInfo throws error", async () => {
|
|
43
|
-
Object.defineProperty(window, "
|
|
49
|
+
Object.defineProperty(window, "Capacitor", {
|
|
44
50
|
value: {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
Plugins: {
|
|
52
|
+
DeviceInfo: {
|
|
53
|
+
getAndroidId: vi
|
|
54
|
+
.fn()
|
|
55
|
+
.mockRejectedValue(
|
|
56
|
+
new Error("DeviceInfo error")
|
|
57
|
+
),
|
|
58
|
+
},
|
|
59
|
+
},
|
|
48
60
|
},
|
|
49
61
|
writable: true,
|
|
50
62
|
configurable: true,
|
|
@@ -55,9 +67,15 @@ describe("getDeviceId", () => {
|
|
|
55
67
|
})
|
|
56
68
|
|
|
57
69
|
it("returns null when DeviceInfo returns empty", async () => {
|
|
58
|
-
Object.defineProperty(window, "
|
|
70
|
+
Object.defineProperty(window, "Capacitor", {
|
|
59
71
|
value: {
|
|
60
|
-
|
|
72
|
+
Plugins: {
|
|
73
|
+
DeviceInfo: {
|
|
74
|
+
getAndroidId: vi
|
|
75
|
+
.fn()
|
|
76
|
+
.mockResolvedValue({ androidId: "" }),
|
|
77
|
+
},
|
|
78
|
+
},
|
|
61
79
|
},
|
|
62
80
|
writable: true,
|
|
63
81
|
configurable: true,
|
|
@@ -148,11 +166,25 @@ describe("getDeviceId", () => {
|
|
|
148
166
|
})
|
|
149
167
|
|
|
150
168
|
describe("MOBILE platform", () => {
|
|
151
|
-
it("returns device ID from
|
|
152
|
-
const mockDeviceId = "
|
|
153
|
-
Object.defineProperty(window, "
|
|
169
|
+
it("returns device ID from iosAppContext when available", async () => {
|
|
170
|
+
const mockDeviceId = "ios-device-123"
|
|
171
|
+
Object.defineProperty(window, "iosAppContext", {
|
|
172
|
+
value: {
|
|
173
|
+
phoneDeviceId: mockDeviceId,
|
|
174
|
+
},
|
|
175
|
+
writable: true,
|
|
176
|
+
configurable: true,
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const result = await getDeviceId("MOBILE")
|
|
180
|
+
expect(result).toBe(mockDeviceId)
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it("returns device ID from androidAppContext when available", async () => {
|
|
184
|
+
const mockDeviceId = "android-device-123"
|
|
185
|
+
Object.defineProperty(window, "androidAppContext", {
|
|
154
186
|
value: {
|
|
155
|
-
|
|
187
|
+
phoneDeviceId: mockDeviceId,
|
|
156
188
|
},
|
|
157
189
|
writable: true,
|
|
158
190
|
configurable: true,
|
|
@@ -162,12 +194,35 @@ describe("getDeviceId", () => {
|
|
|
162
194
|
expect(result).toBe(mockDeviceId)
|
|
163
195
|
})
|
|
164
196
|
|
|
165
|
-
it("
|
|
166
|
-
Object.defineProperty(window, "
|
|
197
|
+
it("prefers iosAppContext over androidAppContext", async () => {
|
|
198
|
+
Object.defineProperty(window, "iosAppContext", {
|
|
199
|
+
value: {
|
|
200
|
+
phoneDeviceId: "ios-id",
|
|
201
|
+
},
|
|
202
|
+
writable: true,
|
|
203
|
+
configurable: true,
|
|
204
|
+
})
|
|
205
|
+
Object.defineProperty(window, "androidAppContext", {
|
|
206
|
+
value: {
|
|
207
|
+
phoneDeviceId: "android-id",
|
|
208
|
+
},
|
|
209
|
+
writable: true,
|
|
210
|
+
configurable: true,
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
const result = await getDeviceId("MOBILE")
|
|
214
|
+
expect(result).toBe("ios-id")
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
it("returns null when no app context is available", async () => {
|
|
218
|
+
const result = await getDeviceId("MOBILE")
|
|
219
|
+
expect(result).toBeNull()
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
it("returns null when phoneDeviceId is empty", async () => {
|
|
223
|
+
Object.defineProperty(window, "iosAppContext", {
|
|
167
224
|
value: {
|
|
168
|
-
|
|
169
|
-
.fn()
|
|
170
|
-
.mockRejectedValue(new Error("Native error")),
|
|
225
|
+
phoneDeviceId: "",
|
|
171
226
|
},
|
|
172
227
|
writable: true,
|
|
173
228
|
configurable: true,
|
package/src/getDeviceId.ts
CHANGED
|
@@ -7,12 +7,12 @@ import { defaultLogger, type Logger } from "./logger"
|
|
|
7
7
|
*
|
|
8
8
|
* Platform handling:
|
|
9
9
|
* - FireTV: DeviceInfo (Capacitor plugin)
|
|
10
|
-
* - Android/iOS:
|
|
10
|
+
* - Android/iOS: window.iosAppContext / window.androidAppContext (phoneDeviceId)
|
|
11
11
|
* - Samsung TV: webapis
|
|
12
12
|
* - LG TV: webOS Luna service
|
|
13
13
|
* - Web: localStorage (with generated UUID fallback)
|
|
14
14
|
*
|
|
15
|
-
* @param platform - Platform identifier ('FIRE_TV', 'SAMSUNG_TV', 'LG_TV', '
|
|
15
|
+
* @param platform - Platform identifier ('FIRE_TV', 'SAMSUNG_TV', 'LG_TV', 'ANDROID_MOBILE', 'IOS_MOBILE', 'WEB')
|
|
16
16
|
* @param logger - Optional logger for error reporting. Defaults to defaultLogger.
|
|
17
17
|
* @returns A promise that resolves to a unique device identifier string, or null if retrieval fails
|
|
18
18
|
*
|
|
@@ -36,7 +36,9 @@ export async function getDeviceId(
|
|
|
36
36
|
return await getSamsungDeviceId(logger)
|
|
37
37
|
case "LG_TV":
|
|
38
38
|
return await getLGDeviceId(logger)
|
|
39
|
-
case "
|
|
39
|
+
case "ANDROID_MOBILE":
|
|
40
|
+
case "IOS_MOBILE":
|
|
41
|
+
case "MOBILE": // Legacy/Generic
|
|
40
42
|
return await getMobileDeviceId(logger)
|
|
41
43
|
case "WEB":
|
|
42
44
|
return getLocalStorageDeviceId()
|
|
@@ -84,24 +86,40 @@ async function getFireTVDeviceId(logger: Logger): Promise<string | null> {
|
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
/**
|
|
87
|
-
* Attempt to get device ID from
|
|
89
|
+
* Attempt to get device ID from mobile app context (Android/iOS)
|
|
90
|
+
*
|
|
91
|
+
* iOS exposes phoneDeviceId via window.iosAppContext
|
|
92
|
+
* Android exposes phoneDeviceId via window.androidAppContext (also check window.top for iframes)
|
|
88
93
|
*/
|
|
89
|
-
|
|
94
|
+
function getMobileDeviceId(logger: Logger): string | null {
|
|
95
|
+
// Try iOS first
|
|
96
|
+
const iosId = (window as ExtendedWindow).iosAppContext?.phoneDeviceId
|
|
97
|
+
if (iosId && iosId.trim()) {
|
|
98
|
+
return iosId
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Try Android (check both window and window.top for iframe scenarios)
|
|
102
|
+
const androidContext = (window as ExtendedWindow).androidAppContext
|
|
90
103
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
104
|
+
const androidId =
|
|
105
|
+
androidContext?.phoneDeviceId ??
|
|
106
|
+
(window.top as ExtendedWindow | null)?.androidAppContext
|
|
107
|
+
?.phoneDeviceId
|
|
108
|
+
if (androidId && androidId.trim()) {
|
|
109
|
+
return androidId
|
|
93
110
|
}
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
logger.error("NativeBridge.getDeviceId returned empty")
|
|
111
|
+
} catch {
|
|
112
|
+
// window.top access can throw if cross-origin
|
|
113
|
+
const androidId = androidContext?.phoneDeviceId
|
|
114
|
+
if (androidId && androidId.trim()) {
|
|
115
|
+
return androidId
|
|
100
116
|
}
|
|
101
|
-
} catch (error) {
|
|
102
|
-
const message = "NativeBridge.getDeviceId failed:"
|
|
103
|
-
logger.error(message, { error })
|
|
104
117
|
}
|
|
118
|
+
|
|
119
|
+
logger.error("[VWR:MobileDeviceId] phoneDeviceId not found", {
|
|
120
|
+
iosAppContext: !!(window as ExtendedWindow).iosAppContext,
|
|
121
|
+
androidAppContext: !!androidContext,
|
|
122
|
+
})
|
|
105
123
|
return null
|
|
106
124
|
}
|
|
107
125
|
|
|
@@ -209,18 +227,25 @@ interface LGDeviceIdResponse {
|
|
|
209
227
|
}>
|
|
210
228
|
}
|
|
211
229
|
|
|
230
|
+
/**
|
|
231
|
+
* Extended window interface for mobile app context
|
|
232
|
+
* Matches the types from @volley/sdk ExtendedWindow.ts
|
|
233
|
+
*/
|
|
234
|
+
interface ExtendedWindow extends Window {
|
|
235
|
+
iosAppContext?: {
|
|
236
|
+
phoneDeviceId: string
|
|
237
|
+
}
|
|
238
|
+
androidAppContext?: {
|
|
239
|
+
phoneDeviceId: string
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
212
243
|
// Type augmentation for platform-specific APIs
|
|
213
244
|
// Note: Capacitor types are declared in getShellVersion.ts with index signatures
|
|
214
245
|
// to allow extensibility. Additional methods used here are accessed via
|
|
215
246
|
// the index signatures [key: string]: any
|
|
216
247
|
declare global {
|
|
217
248
|
interface Window {
|
|
218
|
-
DeviceInfo?: {
|
|
219
|
-
getAndroidId: () => Promise<{ androidId: string }>
|
|
220
|
-
}
|
|
221
|
-
NativeBridge?: {
|
|
222
|
-
getDeviceId: () => Promise<string>
|
|
223
|
-
}
|
|
224
249
|
webapis?: {
|
|
225
250
|
productinfo?: {
|
|
226
251
|
getDuid: () => string
|