@teardown/react-native 2.0.23 → 2.0.27
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/docs/adapters/device/basic.mdx +59 -0
- package/docs/adapters/device/device-info.mdx +76 -0
- package/docs/adapters/device/expo.mdx +61 -0
- package/docs/adapters/device/index.mdx +102 -0
- package/docs/adapters/device/meta.json +4 -0
- package/docs/adapters/index.mdx +96 -0
- package/docs/adapters/meta.json +4 -0
- package/docs/adapters/notifications/expo.mdx +127 -0
- package/docs/adapters/notifications/firebase.mdx +142 -0
- package/docs/adapters/notifications/index.mdx +100 -0
- package/docs/adapters/notifications/meta.json +4 -0
- package/docs/adapters/notifications/wix.mdx +140 -0
- package/docs/adapters/storage/async-storage.mdx +95 -0
- package/docs/adapters/storage/index.mdx +93 -0
- package/docs/adapters/storage/meta.json +4 -0
- package/docs/adapters/storage/mmkv.mdx +86 -0
- package/docs/advanced.mdx +280 -0
- package/docs/api-reference.mdx +241 -0
- package/docs/core-concepts.mdx +158 -0
- package/docs/force-updates.mdx +185 -0
- package/docs/getting-started.mdx +156 -0
- package/docs/hooks-reference.mdx +232 -0
- package/docs/identity.mdx +171 -0
- package/docs/index.mdx +61 -0
- package/docs/logging.mdx +144 -0
- package/docs/meta.json +14 -0
- package/package.json +49 -31
- package/src/clients/api/index.ts +1 -1
- package/src/clients/device/adapters/basic.adapter.ts +57 -66
- package/src/clients/device/adapters/device-info.adapter.ts +21 -28
- package/src/clients/device/adapters/device.adpater-interface.ts +1 -8
- package/src/clients/device/adapters/expo.adapter.ts +33 -40
- package/src/clients/device/device.client.test.ts +20 -35
- package/src/clients/device/device.client.ts +0 -3
- package/src/clients/force-update/force-update.client.test.ts +69 -23
- package/src/clients/force-update/force-update.client.ts +42 -59
- package/src/clients/identity/identity.client.test.ts +22 -14
- package/src/clients/identity/identity.client.ts +1 -1
- package/src/clients/identity/index.ts +1 -1
- package/src/clients/logging/index.ts +1 -1
- package/src/clients/notifications/adapters/expo-notifications.adapter.ts +105 -0
- package/src/clients/notifications/adapters/firebase-messaging.adapter.ts +87 -0
- package/src/clients/notifications/adapters/notifications.adapter-interface.ts +112 -0
- package/src/clients/notifications/adapters/wix-notifications.adapter.ts +183 -0
- package/src/clients/notifications/index.ts +2 -0
- package/src/clients/notifications/notifications.client.ts +214 -3
- package/src/clients/storage/adapters/async-storage.adapter.ts +2 -6
- package/src/clients/storage/adapters/storage-adapters.test.ts +2 -7
- package/src/clients/storage/adapters/storage.adpater-interface.ts +1 -5
- package/src/clients/utils/index.ts +1 -1
- package/src/clients/utils/utils.client.ts +1 -1
- package/src/exports/adapters/async-storage.ts +1 -1
- package/src/exports/adapters/expo.ts +1 -1
- package/src/exports/expo.ts +2 -0
- package/src/exports/firebase.ts +1 -0
- package/src/exports/index.ts +6 -9
- package/src/exports/wix.ts +1 -0
- package/src/hooks/use-force-update.ts +31 -34
- package/src/index.ts +1 -0
- package/src/teardown.core.ts +0 -2
- package/src/.DS_Store +0 -0
|
@@ -1,36 +1,29 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ApplicationInfo,
|
|
3
|
-
HardwareInfo,
|
|
4
|
-
OSInfo
|
|
5
|
-
} from "@teardown/schemas";
|
|
1
|
+
import type { ApplicationInfo, HardwareInfo, OSInfo } from "@teardown/schemas";
|
|
6
2
|
import { Platform } from "react-native";
|
|
7
3
|
import DeviceInfo from "react-native-device-info";
|
|
8
4
|
import { DeviceInfoAdapter as BaseInfoAdapterInterface } from "./device.adpater-interface";
|
|
9
5
|
|
|
10
6
|
export class DeviceInfoAdapter extends BaseInfoAdapterInterface {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
),
|
|
18
|
-
};
|
|
19
|
-
}
|
|
7
|
+
get applicationInfo(): ApplicationInfo {
|
|
8
|
+
return {
|
|
9
|
+
version: DeviceInfo.getVersion() ?? "0.0.0",
|
|
10
|
+
build_number: Number.parseInt(DeviceInfo.getBuildNumber() ?? "0", 10),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
20
13
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
14
|
+
get hardwareInfo(): HardwareInfo {
|
|
15
|
+
return {
|
|
16
|
+
device_name: DeviceInfo.getDeviceNameSync() ?? "Unknown Device",
|
|
17
|
+
device_brand: DeviceInfo.getBrand() ?? "Unknown Brand",
|
|
18
|
+
device_type: DeviceInfo.getDeviceType?.() ?? "unknown",
|
|
19
|
+
};
|
|
20
|
+
}
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
22
|
+
get osInfo(): OSInfo {
|
|
23
|
+
return {
|
|
24
|
+
platform: this.mapPlatform(Platform.OS),
|
|
25
|
+
name: DeviceInfo.getSystemName() ?? Platform.OS,
|
|
26
|
+
version: DeviceInfo.getSystemVersion() ?? "0.0.0",
|
|
27
|
+
};
|
|
28
|
+
}
|
|
36
29
|
}
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ApplicationInfo,
|
|
3
|
-
DeviceInfo,
|
|
4
|
-
HardwareInfo,
|
|
5
|
-
OSInfo
|
|
6
|
-
} from "@teardown/schemas";
|
|
1
|
+
import type { ApplicationInfo, DeviceInfo, HardwareInfo, OSInfo } from "@teardown/schemas";
|
|
7
2
|
import type { Platform } from "react-native";
|
|
8
3
|
import { DevicePlatformEnum } from "../device.client";
|
|
9
4
|
|
|
@@ -37,7 +32,6 @@ export abstract class DeviceInfoAdapter {
|
|
|
37
32
|
application: this.applicationInfo,
|
|
38
33
|
hardware: this.hardwareInfo,
|
|
39
34
|
os: this.osInfo,
|
|
40
|
-
notifications: null,
|
|
41
35
|
update: null,
|
|
42
36
|
});
|
|
43
37
|
}
|
|
@@ -61,5 +55,4 @@ export abstract class DeviceInfoAdapter {
|
|
|
61
55
|
return DevicePlatformEnum.UNKNOWN;
|
|
62
56
|
}
|
|
63
57
|
}
|
|
64
|
-
|
|
65
58
|
}
|
|
@@ -1,8 +1,4 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ApplicationInfo,
|
|
3
|
-
HardwareInfo,
|
|
4
|
-
OSInfo
|
|
5
|
-
} from "@teardown/schemas";
|
|
1
|
+
import type { ApplicationInfo, HardwareInfo, OSInfo } from "@teardown/schemas";
|
|
6
2
|
import * as Application from "expo-application";
|
|
7
3
|
import * as Device from "expo-device";
|
|
8
4
|
import { Platform } from "react-native";
|
|
@@ -13,44 +9,41 @@ import { DeviceInfoAdapter } from "./device.adpater-interface";
|
|
|
13
9
|
* Maps expo-device DeviceType to a string representation
|
|
14
10
|
*/
|
|
15
11
|
function mapDeviceType(deviceType: Device.DeviceType | null): string {
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
12
|
+
switch (deviceType) {
|
|
13
|
+
case Device.DeviceType.PHONE:
|
|
14
|
+
return "phone";
|
|
15
|
+
case Device.DeviceType.TABLET:
|
|
16
|
+
return "tablet";
|
|
17
|
+
case Device.DeviceType.DESKTOP:
|
|
18
|
+
return "desktop";
|
|
19
|
+
case Device.DeviceType.TV:
|
|
20
|
+
return "tv";
|
|
21
|
+
default:
|
|
22
|
+
return "unknown";
|
|
23
|
+
}
|
|
28
24
|
}
|
|
29
25
|
|
|
30
26
|
export class ExpoDeviceAdapter extends DeviceInfoAdapter {
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
),
|
|
38
|
-
};
|
|
39
|
-
}
|
|
27
|
+
get applicationInfo(): ApplicationInfo {
|
|
28
|
+
return {
|
|
29
|
+
version: Application.nativeApplicationVersion ?? "0.0.0",
|
|
30
|
+
build_number: Number.parseInt(Application.nativeBuildVersion ?? "0", 10),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
40
33
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
34
|
+
get hardwareInfo(): HardwareInfo {
|
|
35
|
+
return {
|
|
36
|
+
device_name: Device.deviceName ?? "Unknown Device",
|
|
37
|
+
device_brand: Device.brand ?? "Unknown Brand",
|
|
38
|
+
device_type: mapDeviceType(Device.deviceType),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
48
41
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
42
|
+
get osInfo(): OSInfo {
|
|
43
|
+
return {
|
|
44
|
+
platform: this.mapPlatform(Platform.OS),
|
|
45
|
+
name: Device.osName ?? Platform.OS,
|
|
46
|
+
version: Device.osVersion ?? "0.0.0",
|
|
47
|
+
};
|
|
48
|
+
}
|
|
56
49
|
}
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import type { DeviceInfo } from "@teardown/schemas";
|
|
2
1
|
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { DeviceInfo } from "@teardown/schemas";
|
|
3
3
|
import type { DeviceInfoAdapter } from "./adapters/device.adpater-interface";
|
|
4
4
|
import { DeviceClient } from "./device.client";
|
|
5
5
|
|
|
6
6
|
function createMockLoggingClient() {
|
|
7
7
|
return {
|
|
8
8
|
createLogger: () => ({
|
|
9
|
-
info: () => {
|
|
10
|
-
warn: () => {
|
|
11
|
-
error: () => {
|
|
12
|
-
debug: () => {
|
|
9
|
+
info: () => {},
|
|
10
|
+
warn: () => {},
|
|
11
|
+
error: () => {},
|
|
12
|
+
debug: () => {},
|
|
13
13
|
}),
|
|
14
14
|
};
|
|
15
15
|
}
|
|
@@ -70,12 +70,9 @@ describe("DeviceClient", () => {
|
|
|
70
70
|
const mockUtils = createMockUtilsClient();
|
|
71
71
|
const mockAdapter = createMockDeviceAdapter();
|
|
72
72
|
|
|
73
|
-
const client = new DeviceClient(
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
mockStorage as never,
|
|
77
|
-
{ adapter: mockAdapter }
|
|
78
|
-
);
|
|
73
|
+
const client = new DeviceClient(mockLogging as never, mockUtils as never, mockStorage as never, {
|
|
74
|
+
adapter: mockAdapter,
|
|
75
|
+
});
|
|
79
76
|
|
|
80
77
|
const deviceId = await client.getDeviceId();
|
|
81
78
|
|
|
@@ -92,12 +89,9 @@ describe("DeviceClient", () => {
|
|
|
92
89
|
// Pre-populate storage with a device ID
|
|
93
90
|
mockStorage.getStorage().set("deviceId", "existing-device-id");
|
|
94
91
|
|
|
95
|
-
const client = new DeviceClient(
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
mockStorage as never,
|
|
99
|
-
{ adapter: mockAdapter }
|
|
100
|
-
);
|
|
92
|
+
const client = new DeviceClient(mockLogging as never, mockUtils as never, mockStorage as never, {
|
|
93
|
+
adapter: mockAdapter,
|
|
94
|
+
});
|
|
101
95
|
|
|
102
96
|
const deviceId = await client.getDeviceId();
|
|
103
97
|
|
|
@@ -114,12 +108,9 @@ describe("DeviceClient", () => {
|
|
|
114
108
|
|
|
115
109
|
mockStorage.getStorage().set("deviceId", "consistent-id");
|
|
116
110
|
|
|
117
|
-
const client = new DeviceClient(
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
mockStorage as never,
|
|
121
|
-
{ adapter: mockAdapter }
|
|
122
|
-
);
|
|
111
|
+
const client = new DeviceClient(mockLogging as never, mockUtils as never, mockStorage as never, {
|
|
112
|
+
adapter: mockAdapter,
|
|
113
|
+
});
|
|
123
114
|
|
|
124
115
|
const id1 = await client.getDeviceId();
|
|
125
116
|
const id2 = await client.getDeviceId();
|
|
@@ -138,12 +129,9 @@ describe("DeviceClient", () => {
|
|
|
138
129
|
const mockUtils = createMockUtilsClient();
|
|
139
130
|
const mockAdapter = createMockDeviceAdapter();
|
|
140
131
|
|
|
141
|
-
const client = new DeviceClient(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
mockStorage as never,
|
|
145
|
-
{ adapter: mockAdapter }
|
|
146
|
-
);
|
|
132
|
+
const client = new DeviceClient(mockLogging as never, mockUtils as never, mockStorage as never, {
|
|
133
|
+
adapter: mockAdapter,
|
|
134
|
+
});
|
|
147
135
|
|
|
148
136
|
const deviceInfo = await client.getDeviceInfo();
|
|
149
137
|
|
|
@@ -174,12 +162,9 @@ describe("DeviceClient", () => {
|
|
|
174
162
|
},
|
|
175
163
|
} as DeviceInfoAdapter;
|
|
176
164
|
|
|
177
|
-
const client = new DeviceClient(
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
mockStorage as never,
|
|
181
|
-
{ adapter: customAdapter }
|
|
182
|
-
);
|
|
165
|
+
const client = new DeviceClient(mockLogging as never, mockUtils as never, mockStorage as never, {
|
|
166
|
+
adapter: customAdapter,
|
|
167
|
+
});
|
|
183
168
|
|
|
184
169
|
const deviceInfo = await client.getDeviceInfo();
|
|
185
170
|
|
|
@@ -51,7 +51,6 @@ export class DeviceClient {
|
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
async getDeviceId(): Promise<string> {
|
|
54
|
-
|
|
55
54
|
this.logger.debug("Getting device ID");
|
|
56
55
|
const deviceId = this.storage.getItem("deviceId");
|
|
57
56
|
this.logger.debug(`Device ID found in storage: ${deviceId}`);
|
|
@@ -68,6 +67,4 @@ export class DeviceClient {
|
|
|
68
67
|
async getDeviceInfo(): Promise<DeviceInfo> {
|
|
69
68
|
return this.options.adapter.getDeviceInfo();
|
|
70
69
|
}
|
|
71
|
-
|
|
72
|
-
|
|
73
70
|
}
|
|
@@ -259,7 +259,7 @@ describe("ForceUpdateClient", () => {
|
|
|
259
259
|
});
|
|
260
260
|
});
|
|
261
261
|
|
|
262
|
-
describe("
|
|
262
|
+
describe("checkIntervalMs", () => {
|
|
263
263
|
test("skips version check when identify returns null", async () => {
|
|
264
264
|
const mockIdentity = createMockIdentityClient();
|
|
265
265
|
const mockLogging = createMockLoggingClient();
|
|
@@ -268,8 +268,7 @@ describe("ForceUpdateClient", () => {
|
|
|
268
268
|
mockIdentity.setNextIdentifyResult(null);
|
|
269
269
|
|
|
270
270
|
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
271
|
-
|
|
272
|
-
checkCooldownMs: 0,
|
|
271
|
+
checkIntervalMs: 0,
|
|
273
272
|
});
|
|
274
273
|
client.initialize();
|
|
275
274
|
|
|
@@ -287,14 +286,13 @@ describe("ForceUpdateClient", () => {
|
|
|
287
286
|
client.shutdown();
|
|
288
287
|
});
|
|
289
288
|
|
|
290
|
-
test("
|
|
289
|
+
test("checkIntervalMs -1 disables version checking entirely", async () => {
|
|
291
290
|
const mockIdentity = createMockIdentityClient();
|
|
292
291
|
const mockLogging = createMockLoggingClient();
|
|
293
292
|
const mockStorage = createMockStorageClient();
|
|
294
293
|
|
|
295
294
|
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
296
|
-
|
|
297
|
-
checkCooldownMs: -1,
|
|
295
|
+
checkIntervalMs: -1,
|
|
298
296
|
});
|
|
299
297
|
client.initialize();
|
|
300
298
|
|
|
@@ -310,47 +308,46 @@ describe("ForceUpdateClient", () => {
|
|
|
310
308
|
client.shutdown();
|
|
311
309
|
});
|
|
312
310
|
|
|
313
|
-
test("
|
|
311
|
+
test("interval prevents checks too soon after successful check", async () => {
|
|
314
312
|
const mockIdentity = createMockIdentityClient();
|
|
315
313
|
const mockLogging = createMockLoggingClient();
|
|
316
314
|
const mockStorage = createMockStorageClient();
|
|
317
315
|
|
|
318
316
|
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
319
|
-
checkOnForeground:
|
|
320
|
-
|
|
321
|
-
checkCooldownMs: 0,
|
|
317
|
+
checkOnForeground: true,
|
|
318
|
+
checkIntervalMs: 100_000,
|
|
322
319
|
});
|
|
323
320
|
client.initialize();
|
|
324
321
|
|
|
325
322
|
const foregroundHandler = mockAppStateListeners[0];
|
|
326
323
|
|
|
327
|
-
// First foreground
|
|
324
|
+
// First foreground - should trigger check
|
|
328
325
|
await foregroundHandler("active");
|
|
329
326
|
await new Promise((r) => setTimeout(r, 10));
|
|
330
327
|
|
|
331
328
|
const callsAfterFirst = mockIdentity.getIdentifyCallCount();
|
|
332
329
|
|
|
333
|
-
// Second foreground immediately (within
|
|
330
|
+
// Second foreground immediately (within interval window)
|
|
334
331
|
await foregroundHandler("active");
|
|
335
332
|
await new Promise((r) => setTimeout(r, 10));
|
|
336
333
|
|
|
337
334
|
const callsAfterSecond = mockIdentity.getIdentifyCallCount();
|
|
338
335
|
|
|
339
|
-
// Only first call should have triggered identify (
|
|
336
|
+
// Only first call should have triggered identify (interval blocks second)
|
|
340
337
|
expect(callsAfterFirst).toBe(1);
|
|
341
338
|
expect(callsAfterSecond).toBe(1);
|
|
342
339
|
|
|
343
340
|
client.shutdown();
|
|
344
341
|
});
|
|
345
342
|
|
|
346
|
-
test("checkOnForeground: true
|
|
343
|
+
test("checkOnForeground: true respects interval", async () => {
|
|
347
344
|
const mockIdentity = createMockIdentityClient();
|
|
348
345
|
const mockLogging = createMockLoggingClient();
|
|
349
346
|
const mockStorage = createMockStorageClient();
|
|
350
347
|
|
|
351
348
|
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
352
349
|
checkOnForeground: true,
|
|
353
|
-
|
|
350
|
+
checkIntervalMs: 100_000,
|
|
354
351
|
});
|
|
355
352
|
client.initialize();
|
|
356
353
|
|
|
@@ -362,28 +359,77 @@ describe("ForceUpdateClient", () => {
|
|
|
362
359
|
|
|
363
360
|
const callsAfterFirst = mockIdentity.getIdentifyCallCount();
|
|
364
361
|
|
|
365
|
-
// Second foreground immediately (within
|
|
362
|
+
// Second foreground immediately (within interval window)
|
|
366
363
|
await foregroundHandler("active");
|
|
367
364
|
await new Promise((r) => setTimeout(r, 10));
|
|
368
365
|
|
|
369
366
|
const callsAfterSecond = mockIdentity.getIdentifyCallCount();
|
|
370
367
|
|
|
371
|
-
//
|
|
368
|
+
// Only first should trigger - interval blocks second even with checkOnForeground: true
|
|
372
369
|
expect(callsAfterFirst).toBe(1);
|
|
373
|
-
expect(callsAfterSecond).toBe(
|
|
370
|
+
expect(callsAfterSecond).toBe(1);
|
|
374
371
|
|
|
375
372
|
client.shutdown();
|
|
376
373
|
});
|
|
377
374
|
|
|
378
|
-
test("
|
|
375
|
+
test("checkOnForeground: false disables foreground checking entirely", async () => {
|
|
379
376
|
const mockIdentity = createMockIdentityClient();
|
|
380
377
|
const mockLogging = createMockLoggingClient();
|
|
381
378
|
const mockStorage = createMockStorageClient();
|
|
382
379
|
|
|
383
380
|
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
384
381
|
checkOnForeground: false,
|
|
385
|
-
|
|
386
|
-
|
|
382
|
+
checkIntervalMs: 0, // Would normally allow every check
|
|
383
|
+
});
|
|
384
|
+
client.initialize();
|
|
385
|
+
|
|
386
|
+
const foregroundHandler = mockAppStateListeners[0];
|
|
387
|
+
|
|
388
|
+
// Foreground should not trigger check when checkOnForeground is false
|
|
389
|
+
await foregroundHandler("active");
|
|
390
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
391
|
+
|
|
392
|
+
expect(mockIdentity.getIdentifyCallCount()).toBe(0);
|
|
393
|
+
|
|
394
|
+
client.shutdown();
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
test("checkIntervalMs 0 checks on every foreground", async () => {
|
|
398
|
+
const mockIdentity = createMockIdentityClient();
|
|
399
|
+
const mockLogging = createMockLoggingClient();
|
|
400
|
+
const mockStorage = createMockStorageClient();
|
|
401
|
+
|
|
402
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
403
|
+
checkOnForeground: true,
|
|
404
|
+
checkIntervalMs: 0,
|
|
405
|
+
});
|
|
406
|
+
client.initialize();
|
|
407
|
+
|
|
408
|
+
const foregroundHandler = mockAppStateListeners[0];
|
|
409
|
+
|
|
410
|
+
// First foreground
|
|
411
|
+
await foregroundHandler("active");
|
|
412
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
413
|
+
|
|
414
|
+
// Second foreground immediately
|
|
415
|
+
await foregroundHandler("active");
|
|
416
|
+
await new Promise((r) => setTimeout(r, 10));
|
|
417
|
+
|
|
418
|
+
// Both should trigger since interval is 0
|
|
419
|
+
expect(mockIdentity.getIdentifyCallCount()).toBe(2);
|
|
420
|
+
|
|
421
|
+
client.shutdown();
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("values below 30s are clamped to 30s minimum", async () => {
|
|
425
|
+
const mockIdentity = createMockIdentityClient();
|
|
426
|
+
const mockLogging = createMockLoggingClient();
|
|
427
|
+
const mockStorage = createMockStorageClient();
|
|
428
|
+
|
|
429
|
+
// Set interval to 10ms (below 30s minimum)
|
|
430
|
+
const client = new ForceUpdateClient(mockLogging as never, mockStorage as never, mockIdentity as never, {
|
|
431
|
+
checkOnForeground: true,
|
|
432
|
+
checkIntervalMs: 10,
|
|
387
433
|
});
|
|
388
434
|
client.initialize();
|
|
389
435
|
|
|
@@ -395,13 +441,13 @@ describe("ForceUpdateClient", () => {
|
|
|
395
441
|
|
|
396
442
|
const callsAfterFirst = mockIdentity.getIdentifyCallCount();
|
|
397
443
|
|
|
398
|
-
// Second foreground immediately
|
|
444
|
+
// Second foreground immediately - should be blocked by 30s minimum
|
|
399
445
|
await foregroundHandler("active");
|
|
400
446
|
await new Promise((r) => setTimeout(r, 10));
|
|
401
447
|
|
|
402
448
|
const callsAfterSecond = mockIdentity.getIdentifyCallCount();
|
|
403
449
|
|
|
404
|
-
// Only first call should have triggered
|
|
450
|
+
// Only first call should have triggered (30s minimum enforced)
|
|
405
451
|
expect(callsAfterFirst).toBe(1);
|
|
406
452
|
expect(callsAfterSecond).toBe(1);
|
|
407
453
|
|
|
@@ -69,41 +69,30 @@ export interface VersionStatusChangeEvents {
|
|
|
69
69
|
|
|
70
70
|
export type ForceUpdateClientOptions = {
|
|
71
71
|
/**
|
|
72
|
-
* Minimum time (ms) between
|
|
73
|
-
* Measured from the last time the app came to foreground.
|
|
74
|
-
* Prevents checking when user quickly switches apps back and forth.
|
|
75
|
-
* Default: 30000 (30 seconds)
|
|
76
|
-
*
|
|
77
|
-
* Special values:
|
|
78
|
-
* - -1: Disable throttling, check on every foreground (respects checkCooldownMs)
|
|
79
|
-
*
|
|
80
|
-
* Example: If throttleMs is 30s and user backgrounds then foregrounds the app
|
|
81
|
-
* twice within 20s, only the first transition triggers a check.
|
|
82
|
-
*/
|
|
83
|
-
throttleMs?: number;
|
|
84
|
-
/**
|
|
85
|
-
* Minimum time (ms) since the last successful version check before checking again.
|
|
86
|
-
* Measured from when the last check completed successfully (not when it started).
|
|
87
|
-
* Prevents unnecessary API calls after we already have fresh version data.
|
|
72
|
+
* Minimum time (ms) between version checks.
|
|
88
73
|
* Default: 300000 (5 minutes)
|
|
89
74
|
*
|
|
75
|
+
* Values below 30 seconds are clamped to 30 seconds to prevent excessive API calls.
|
|
76
|
+
*
|
|
90
77
|
* Special values:
|
|
91
|
-
* - 0:
|
|
92
|
-
* - -1: Disable
|
|
78
|
+
* - 0: Check on every foreground (no interval)
|
|
79
|
+
* - -1: Disable automatic version checking entirely
|
|
93
80
|
*
|
|
94
|
-
* Example: If
|
|
81
|
+
* Example: If checkIntervalMs is 5min and a check completes at 12:00pm,
|
|
95
82
|
* no new checks occur until 12:05pm, even if user foregrounds the app multiple times.
|
|
96
83
|
*/
|
|
97
|
-
|
|
98
|
-
/**
|
|
84
|
+
checkIntervalMs?: number;
|
|
85
|
+
/** Check version when app comes to foreground, respecting checkIntervalMs (default: true) */
|
|
99
86
|
checkOnForeground?: boolean;
|
|
100
87
|
/** If true, check version even when not identified by using anonymous device identification (default: false) */
|
|
101
88
|
identifyAnonymousDevice?: boolean;
|
|
102
89
|
};
|
|
103
90
|
|
|
91
|
+
/** Hard minimum interval between checks to prevent excessive API calls */
|
|
92
|
+
const MIN_CHECK_INTERVAL_MS = 30_000; // 30 seconds
|
|
93
|
+
|
|
104
94
|
const DEFAULT_OPTIONS: Required<ForceUpdateClientOptions> = {
|
|
105
|
-
|
|
106
|
-
checkCooldownMs: 300_000, // 5 minutes
|
|
95
|
+
checkIntervalMs: 300_000, // 5 minutes
|
|
107
96
|
checkOnForeground: true,
|
|
108
97
|
identifyAnonymousDevice: false,
|
|
109
98
|
};
|
|
@@ -116,7 +105,6 @@ export class ForceUpdateClient {
|
|
|
116
105
|
private unsubscribe: (() => void) | null = null;
|
|
117
106
|
private appStateSubscription: NativeEventSubscription | null = null;
|
|
118
107
|
private lastCheckTime: number | null = null;
|
|
119
|
-
private lastForegroundTime: number | null = null;
|
|
120
108
|
private initialized = false;
|
|
121
109
|
|
|
122
110
|
private readonly logger: Logger;
|
|
@@ -132,7 +120,6 @@ export class ForceUpdateClient {
|
|
|
132
120
|
this.logger = logging.createLogger({ name: "ForceUpdateClient" });
|
|
133
121
|
this.storage = storage.createStorage("version");
|
|
134
122
|
this.options = { ...DEFAULT_OPTIONS, ...options };
|
|
135
|
-
// Don't initialize here - defer to initialize()
|
|
136
123
|
}
|
|
137
124
|
|
|
138
125
|
initialize(): void {
|
|
@@ -142,7 +129,6 @@ export class ForceUpdateClient {
|
|
|
142
129
|
}
|
|
143
130
|
this.initialized = true;
|
|
144
131
|
|
|
145
|
-
// Load from storage, subscribe to events, and sync with current identity state
|
|
146
132
|
this.versionStatus = this.getVersionStatusFromStorage();
|
|
147
133
|
this.logger.debug(`Initialized with version status: ${this.versionStatus.type}`);
|
|
148
134
|
this.subscribeToIdentity();
|
|
@@ -164,25 +150,30 @@ export class ForceUpdateClient {
|
|
|
164
150
|
}
|
|
165
151
|
|
|
166
152
|
private getVersionStatusFromStorage(): VersionStatus {
|
|
167
|
-
|
|
153
|
+
try {
|
|
154
|
+
const stored = this.storage.getItem(VERSION_STATUS_STORAGE_KEY);
|
|
168
155
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
156
|
+
if (stored == null) {
|
|
157
|
+
this.logger.debug("No stored version status, returning initializing");
|
|
158
|
+
return InitializingVersionStatusSchema.parse({ type: "initializing" });
|
|
159
|
+
}
|
|
173
160
|
|
|
174
|
-
|
|
175
|
-
|
|
161
|
+
const parsed = VersionStatusSchema.parse(JSON.parse(stored));
|
|
162
|
+
this.logger.debug(`Parsed version status from storage: ${parsed.type}`);
|
|
163
|
+
|
|
164
|
+
// "checking" and "initializing" are transient states - if we restore them, reset to initializing
|
|
165
|
+
// This can happen if the app was killed during a version check
|
|
166
|
+
if (parsed.type === "checking" || parsed.type === "initializing") {
|
|
167
|
+
this.logger.debug(`Found stale '${parsed.type}' state in storage, resetting to initializing`);
|
|
168
|
+
this.storage.removeItem(VERSION_STATUS_STORAGE_KEY);
|
|
169
|
+
return InitializingVersionStatusSchema.parse({ type: "initializing" });
|
|
170
|
+
}
|
|
176
171
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
this.logger.debug(`Found stale '${parsed.type}' state in storage, resetting to initializing`);
|
|
181
|
-
this.storage.removeItem(VERSION_STATUS_STORAGE_KEY);
|
|
172
|
+
return parsed;
|
|
173
|
+
} catch (error) {
|
|
174
|
+
this.logger.debugError("Error getting version status from storage", { error });
|
|
182
175
|
return InitializingVersionStatusSchema.parse({ type: "initializing" });
|
|
183
176
|
}
|
|
184
|
-
|
|
185
|
-
return parsed;
|
|
186
177
|
}
|
|
187
178
|
|
|
188
179
|
private saveVersionStatusToStorage(status: VersionStatus): void {
|
|
@@ -239,30 +230,22 @@ export class ForceUpdateClient {
|
|
|
239
230
|
if (nextState === "active") {
|
|
240
231
|
this.logger.debug("App state changed to active");
|
|
241
232
|
|
|
242
|
-
// If
|
|
243
|
-
if (this.options.
|
|
244
|
-
this.logger.debug("Version checking disabled (
|
|
233
|
+
// If checkIntervalMs is -1, disable checking entirely
|
|
234
|
+
if (this.options.checkIntervalMs === -1) {
|
|
235
|
+
this.logger.debug("Version checking disabled (checkIntervalMs = -1)");
|
|
245
236
|
return;
|
|
246
237
|
}
|
|
247
238
|
|
|
248
239
|
const now = Date.now();
|
|
249
240
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
// Otherwise, check if enough time has passed since last successful check
|
|
259
|
-
const cooldownOk =
|
|
260
|
-
this.options.checkCooldownMs === 0 ||
|
|
261
|
-
!this.lastCheckTime ||
|
|
262
|
-
now - this.lastCheckTime >= this.options.checkCooldownMs;
|
|
263
|
-
|
|
264
|
-
if (this.options.checkOnForeground || (throttleOk && cooldownOk)) {
|
|
265
|
-
this.lastForegroundTime = now;
|
|
241
|
+
// Calculate effective interval (clamp to minimum unless 0 for "always check")
|
|
242
|
+
const effectiveInterval =
|
|
243
|
+
this.options.checkIntervalMs === 0 ? 0 : Math.max(this.options.checkIntervalMs, MIN_CHECK_INTERVAL_MS);
|
|
244
|
+
|
|
245
|
+
// Check if enough time has passed since last successful check
|
|
246
|
+
const canCheck = effectiveInterval === 0 || !this.lastCheckTime || now - this.lastCheckTime >= effectiveInterval;
|
|
247
|
+
|
|
248
|
+
if (this.options.checkOnForeground && canCheck) {
|
|
266
249
|
this.checkVersionOnForeground();
|
|
267
250
|
}
|
|
268
251
|
}
|