@teardown/react-native 1.2.39 → 2.0.1

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.
Files changed (87) hide show
  1. package/README.md +89 -7
  2. package/docs/01-getting-started.mdx +147 -0
  3. package/docs/02-core-concepts.mdx +188 -0
  4. package/docs/03-identity.mdx +301 -0
  5. package/docs/04-force-updates.mdx +339 -0
  6. package/docs/05-device-info.mdx +324 -0
  7. package/docs/06-logging.mdx +345 -0
  8. package/docs/06-storage.mdx +349 -0
  9. package/docs/07-api-reference.mdx +472 -0
  10. package/docs/07-logging.mdx +345 -0
  11. package/docs/08-api-reference.mdx +472 -0
  12. package/docs/08-hooks-reference.mdx +476 -0
  13. package/docs/09-advanced.mdx +563 -0
  14. package/docs/09-hooks-reference.mdx +476 -0
  15. package/docs/10-advanced.mdx +563 -0
  16. package/package.json +65 -47
  17. package/src/clients/api/api.client.ts +80 -0
  18. package/src/clients/api/index.ts +1 -0
  19. package/src/clients/device/device.adpater-interface.ts +57 -0
  20. package/src/clients/device/device.client.test.ts +190 -0
  21. package/src/clients/device/device.client.ts +69 -0
  22. package/src/clients/device/expo-adapter.ts +90 -0
  23. package/src/clients/device/index.ts +4 -0
  24. package/src/clients/force-update/force-update.client.test.ts +295 -0
  25. package/src/clients/force-update/force-update.client.ts +224 -0
  26. package/src/clients/force-update/index.ts +1 -0
  27. package/src/clients/identity/identity.client.test.ts +454 -0
  28. package/src/clients/identity/identity.client.ts +255 -0
  29. package/src/clients/identity/index.ts +1 -0
  30. package/src/clients/logging/index.ts +1 -0
  31. package/src/clients/logging/logging.client.ts +92 -0
  32. package/src/clients/notifications/notifications.client.ts +10 -0
  33. package/src/clients/storage/index.ts +1 -0
  34. package/src/clients/storage/mmkv-adapter.ts +23 -0
  35. package/src/clients/storage/storage.client.ts +75 -0
  36. package/src/clients/utils/index.ts +1 -0
  37. package/src/clients/utils/utils.client.ts +75 -0
  38. package/src/components/ui/button.tsx +0 -0
  39. package/src/components/ui/input.tsx +0 -0
  40. package/src/contexts/index.ts +1 -0
  41. package/src/contexts/teardown.context.ts +17 -0
  42. package/src/exports/expo.ts +1 -0
  43. package/src/exports/index.ts +16 -0
  44. package/src/exports/mmkv.ts +1 -0
  45. package/src/hooks/use-force-update.ts +38 -0
  46. package/src/hooks/use-session.ts +26 -0
  47. package/src/providers/teardown.provider.tsx +28 -0
  48. package/src/teardown.core.ts +76 -0
  49. package/dist/components/index.d.ts +0 -1
  50. package/dist/components/index.js +0 -3
  51. package/dist/components/index.js.map +0 -1
  52. package/dist/components/teardown-logo.d.ts +0 -4
  53. package/dist/components/teardown-logo.js +0 -35
  54. package/dist/components/teardown-logo.js.map +0 -1
  55. package/dist/containers/index.d.ts +0 -1
  56. package/dist/containers/index.js +0 -18
  57. package/dist/containers/index.js.map +0 -1
  58. package/dist/containers/teardown.container.d.ts +0 -8
  59. package/dist/containers/teardown.container.js +0 -26
  60. package/dist/containers/teardown.container.js.map +0 -1
  61. package/dist/index.d.ts +0 -2
  62. package/dist/index.js +0 -22
  63. package/dist/index.js.map +0 -1
  64. package/dist/plugins/http.plugin.d.ts +0 -23
  65. package/dist/plugins/http.plugin.js +0 -145
  66. package/dist/plugins/http.plugin.js.map +0 -1
  67. package/dist/plugins/index.d.ts +0 -2
  68. package/dist/plugins/index.js +0 -20
  69. package/dist/plugins/index.js.map +0 -1
  70. package/dist/plugins/logging.plugin.d.ts +0 -9
  71. package/dist/plugins/logging.plugin.js +0 -36
  72. package/dist/plugins/logging.plugin.js.map +0 -1
  73. package/dist/plugins/websocket.plugin.d.ts +0 -1
  74. package/dist/plugins/websocket.plugin.js +0 -108
  75. package/dist/plugins/websocket.plugin.js.map +0 -1
  76. package/dist/services/index.d.ts +0 -1
  77. package/dist/services/index.js +0 -18
  78. package/dist/services/index.js.map +0 -1
  79. package/dist/services/teardown.service.d.ts +0 -10
  80. package/dist/services/teardown.service.js +0 -22
  81. package/dist/services/teardown.service.js.map +0 -1
  82. package/dist/teardown.client.d.ts +0 -41
  83. package/dist/teardown.client.js +0 -60
  84. package/dist/teardown.client.js.map +0 -1
  85. package/dist/utils/log.d.ts +0 -5
  86. package/dist/utils/log.js +0 -9
  87. package/dist/utils/log.js.map +0 -1
@@ -0,0 +1,295 @@
1
+ import { describe, test, expect, beforeEach, mock } from "bun:test";
2
+ import { EventEmitter } from "eventemitter3";
3
+ import { ForceUpdateClient, IdentifyVersionStatusEnum } from "./force-update.client";
4
+
5
+ // Must mock react-native BEFORE any imports that use it
6
+ const mockAppStateListeners: ((state: string) => void)[] = [];
7
+ mock.module("react-native", () => ({
8
+ AppState: {
9
+ addEventListener: (_event: string, handler: (state: string) => void) => {
10
+ mockAppStateListeners.push(handler);
11
+ return { remove: () => mockAppStateListeners.splice(mockAppStateListeners.indexOf(handler), 1) };
12
+ },
13
+ },
14
+ }));
15
+
16
+ type IdentifyState = import("../identity").IdentifyState;
17
+ type IdentifyStateChangeEvents = import("../identity").IdentifyStateChangeEvents;
18
+ type VersionStatus = import("./force-update.client").VersionStatus;
19
+
20
+ function createMockIdentityClient() {
21
+ const emitter = new EventEmitter<IdentifyStateChangeEvents>();
22
+ let identifyCallCount = 0;
23
+ let nextIdentifyResult: { success: boolean; data?: { version_info: { status: IdentifyVersionStatusEnum } } } = {
24
+ success: true,
25
+ data: { version_info: { status: IdentifyVersionStatusEnum.UP_TO_DATE } },
26
+ };
27
+
28
+ return {
29
+ emitter,
30
+ onIdentifyStateChange: (listener: (state: IdentifyState) => void) => {
31
+ emitter.addListener("IDENTIFY_STATE_CHANGED", listener);
32
+ return () => emitter.removeListener("IDENTIFY_STATE_CHANGED", listener);
33
+ },
34
+ identify: async () => {
35
+ identifyCallCount++;
36
+ emitter.emit("IDENTIFY_STATE_CHANGED", { type: "identifying" });
37
+ emitter.emit("IDENTIFY_STATE_CHANGED", {
38
+ type: "identified",
39
+ session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
40
+ version_info: { status: nextIdentifyResult.data?.version_info.status ?? IdentifyVersionStatusEnum.UP_TO_DATE, update: null },
41
+ });
42
+ return nextIdentifyResult;
43
+ },
44
+ getIdentifyCallCount: () => identifyCallCount,
45
+ setNextIdentifyResult: (result: typeof nextIdentifyResult) => {
46
+ nextIdentifyResult = result;
47
+ },
48
+ };
49
+ }
50
+
51
+ function createMockLoggingClient() {
52
+ return {
53
+ createLogger: () => ({
54
+ info: () => { },
55
+ warn: () => { },
56
+ error: () => { },
57
+ debug: () => { },
58
+ }),
59
+ };
60
+ }
61
+
62
+ function createMockStorageClient() {
63
+ const storage = new Map<string, string>();
64
+ return {
65
+ createStorage: () => ({
66
+ getItem: (key: string) => storage.get(key) ?? null,
67
+ setItem: (key: string, value: string) => storage.set(key, value),
68
+ removeItem: (key: string) => storage.delete(key),
69
+ }),
70
+ };
71
+ }
72
+
73
+ describe("ForceUpdateClient", () => {
74
+ beforeEach(() => {
75
+ mockAppStateListeners.length = 0;
76
+ });
77
+
78
+ describe("updateFromVersionStatus single emission", () => {
79
+ test("emits VERSION_STATUS_CHANGED exactly once per identify cycle", async () => {
80
+ const mockIdentity = createMockIdentityClient();
81
+ const mockLogging = createMockLoggingClient();
82
+ const mockStorage = createMockStorageClient();
83
+
84
+ const client = new ForceUpdateClient(
85
+ mockLogging as never,
86
+ mockStorage as never,
87
+ mockIdentity as never,
88
+ { throttleMs: 0, checkCooldownMs: 0 }
89
+ );
90
+
91
+ const statusChanges: VersionStatus[] = [];
92
+ client.onVersionStatusChange((status) => statusChanges.push(status));
93
+
94
+ // Trigger identify via state change
95
+ mockIdentity.emitter.emit("IDENTIFY_STATE_CHANGED", { type: "identifying" });
96
+ mockIdentity.emitter.emit("IDENTIFY_STATE_CHANGED", {
97
+ type: "identified",
98
+ session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
99
+ version_info: { status: IdentifyVersionStatusEnum.UPDATE_AVAILABLE, update: null },
100
+ });
101
+
102
+ // Should have: checking (from identifying) + update_available (from identified)
103
+ expect(statusChanges).toHaveLength(2);
104
+ expect(statusChanges[0]).toEqual({ type: "checking" });
105
+ expect(statusChanges[1]).toEqual({ type: "update_available" });
106
+
107
+ client.shutdown();
108
+ });
109
+
110
+ test("foreground check triggers only one status update via subscription", async () => {
111
+ const mockIdentity = createMockIdentityClient();
112
+ const mockLogging = createMockLoggingClient();
113
+ const mockStorage = createMockStorageClient();
114
+
115
+ mockIdentity.setNextIdentifyResult({
116
+ success: true,
117
+ data: { version_info: { status: IdentifyVersionStatusEnum.UPDATE_REQUIRED } },
118
+ });
119
+
120
+ const client = new ForceUpdateClient(
121
+ mockLogging as never,
122
+ mockStorage as never,
123
+ mockIdentity as never,
124
+ { throttleMs: 0, checkCooldownMs: 0 }
125
+ );
126
+
127
+ const statusChanges: VersionStatus[] = [];
128
+ client.onVersionStatusChange((status) => statusChanges.push(status));
129
+
130
+ // Simulate app coming to foreground
131
+ const foregroundHandler = mockAppStateListeners[0];
132
+ expect(foregroundHandler).toBeDefined();
133
+
134
+ await foregroundHandler("active");
135
+
136
+ // Wait for async identify to complete
137
+ await new Promise((r) => setTimeout(r, 10));
138
+
139
+ // Should have: checking + update_required (NOT duplicated)
140
+ expect(statusChanges).toHaveLength(2);
141
+ expect(statusChanges[0]).toEqual({ type: "checking" });
142
+ expect(statusChanges[1]).toEqual({ type: "update_required" });
143
+
144
+ client.shutdown();
145
+ });
146
+ });
147
+
148
+ describe("cleanup on shutdown", () => {
149
+ test("removes all listeners on shutdown", () => {
150
+ const mockIdentity = createMockIdentityClient();
151
+ const mockLogging = createMockLoggingClient();
152
+ const mockStorage = createMockStorageClient();
153
+
154
+ const client = new ForceUpdateClient(
155
+ mockLogging as never,
156
+ mockStorage as never,
157
+ mockIdentity as never
158
+ );
159
+
160
+ const statusChanges: VersionStatus[] = [];
161
+ client.onVersionStatusChange((status) => statusChanges.push(status));
162
+
163
+ client.shutdown();
164
+
165
+ // After shutdown, emitting should not trigger listener
166
+ mockIdentity.emitter.emit("IDENTIFY_STATE_CHANGED", {
167
+ type: "identified",
168
+ session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
169
+ version_info: { status: IdentifyVersionStatusEnum.UPDATE_AVAILABLE, update: null },
170
+ });
171
+
172
+ // No new status changes after shutdown
173
+ expect(statusChanges).toHaveLength(0);
174
+ });
175
+
176
+ test("removes AppState listener on shutdown", () => {
177
+ const mockIdentity = createMockIdentityClient();
178
+ const mockLogging = createMockLoggingClient();
179
+ const mockStorage = createMockStorageClient();
180
+
181
+ const client = new ForceUpdateClient(
182
+ mockLogging as never,
183
+ mockStorage as never,
184
+ mockIdentity as never
185
+ );
186
+
187
+ expect(mockAppStateListeners).toHaveLength(1);
188
+
189
+ client.shutdown();
190
+
191
+ expect(mockAppStateListeners).toHaveLength(0);
192
+ });
193
+ });
194
+
195
+ describe("throttle and cooldown", () => {
196
+ test("throttle prevents rapid foreground checks", async () => {
197
+ const mockIdentity = createMockIdentityClient();
198
+ const mockLogging = createMockLoggingClient();
199
+ const mockStorage = createMockStorageClient();
200
+
201
+ const client = new ForceUpdateClient(
202
+ mockLogging as never,
203
+ mockStorage as never,
204
+ mockIdentity as never,
205
+ { throttleMs: 1000, checkCooldownMs: 0 }
206
+ );
207
+
208
+ const foregroundHandler = mockAppStateListeners[0];
209
+
210
+ // First foreground
211
+ await foregroundHandler("active");
212
+ await new Promise((r) => setTimeout(r, 10));
213
+
214
+ const callsAfterFirst = mockIdentity.getIdentifyCallCount();
215
+
216
+ // Second foreground immediately (within throttle window)
217
+ await foregroundHandler("active");
218
+ await new Promise((r) => setTimeout(r, 10));
219
+
220
+ const callsAfterSecond = mockIdentity.getIdentifyCallCount();
221
+
222
+ // Should only have one identify call due to throttle
223
+ expect(callsAfterFirst).toBe(1);
224
+ expect(callsAfterSecond).toBe(1);
225
+
226
+ client.shutdown();
227
+ });
228
+
229
+ test("cooldown prevents redundant checks after recent success", async () => {
230
+ const mockIdentity = createMockIdentityClient();
231
+ const mockLogging = createMockLoggingClient();
232
+ const mockStorage = createMockStorageClient();
233
+
234
+ const client = new ForceUpdateClient(
235
+ mockLogging as never,
236
+ mockStorage as never,
237
+ mockIdentity as never,
238
+ { throttleMs: 0, checkCooldownMs: 5000 }
239
+ );
240
+
241
+ const foregroundHandler = mockAppStateListeners[0];
242
+
243
+ // First foreground - should trigger check
244
+ await foregroundHandler("active");
245
+ await new Promise((r) => setTimeout(r, 10));
246
+
247
+ const callsAfterFirst = mockIdentity.getIdentifyCallCount();
248
+
249
+ // Second foreground (within cooldown window)
250
+ await foregroundHandler("active");
251
+ await new Promise((r) => setTimeout(r, 10));
252
+
253
+ const callsAfterSecond = mockIdentity.getIdentifyCallCount();
254
+
255
+ // Only first call should have triggered identify (cooldown blocks second)
256
+ expect(callsAfterFirst).toBe(1);
257
+ expect(callsAfterSecond).toBe(1);
258
+
259
+ client.shutdown();
260
+ });
261
+ });
262
+
263
+ describe("version status mapping", () => {
264
+ test.each([
265
+ [IdentifyVersionStatusEnum.UP_TO_DATE, "up_to_date"],
266
+ [IdentifyVersionStatusEnum.UPDATE_AVAILABLE, "update_available"],
267
+ [IdentifyVersionStatusEnum.UPDATE_REQUIRED, "update_required"],
268
+ [IdentifyVersionStatusEnum.UPDATE_RECOMMENDED, "update_recommended"],
269
+ [IdentifyVersionStatusEnum.DISABLED, "disabled"],
270
+ ])("maps %s to %s", (apiStatus, expectedType) => {
271
+ const mockIdentity = createMockIdentityClient();
272
+ const mockLogging = createMockLoggingClient();
273
+ const mockStorage = createMockStorageClient();
274
+
275
+ const client = new ForceUpdateClient(
276
+ mockLogging as never,
277
+ mockStorage as never,
278
+ mockIdentity as never
279
+ );
280
+
281
+ const statusChanges: VersionStatus[] = [];
282
+ client.onVersionStatusChange((status) => statusChanges.push(status));
283
+
284
+ mockIdentity.emitter.emit("IDENTIFY_STATE_CHANGED", {
285
+ type: "identified",
286
+ session: { session_id: "s1", device_id: "d1", persona_id: "p1", token: "t1" },
287
+ version_info: { status: apiStatus, update: null },
288
+ });
289
+
290
+ expect(statusChanges[statusChanges.length - 1]?.type).toBe(expectedType);
291
+
292
+ client.shutdown();
293
+ });
294
+ });
295
+ });
@@ -0,0 +1,224 @@
1
+ import { EventEmitter } from "eventemitter3";
2
+ import { AppState, type AppStateStatus, type NativeEventSubscription } from "react-native";
3
+ import { z } from "zod";
4
+ import type { IdentityClient } from "../identity";
5
+ import type { Logger, LoggingClient } from "../logging";
6
+ import type { StorageClient, SupportedStorage } from "../storage";
7
+
8
+ // TODO: sort out why importing these enuims from schemas is not working - @teardown/schemas
9
+ export enum IdentifyVersionStatusEnum {
10
+ /**
11
+ * A new version is available
12
+ */
13
+ UPDATE_AVAILABLE = "UPDATE_AVAILABLE",
14
+ /**
15
+ * An update is recommended
16
+ */
17
+ UPDATE_RECOMMENDED = "UPDATE_RECOMMENDED",
18
+ /**
19
+ * An update is required
20
+ */
21
+ UPDATE_REQUIRED = "UPDATE_REQUIRED",
22
+ /**
23
+ * The current version is valid & up to date
24
+ */
25
+ UP_TO_DATE = "UP_TO_DATE",
26
+ /**
27
+ * The version or build has been disabled
28
+ */
29
+ DISABLED = "DISABLED",
30
+ }
31
+
32
+
33
+ export const InitializingVersionStatusSchema = z.object({ type: z.literal("initializing") });
34
+ export const CheckingVersionStatusSchema = z.object({ type: z.literal("checking") });
35
+ export const UpToDateVersionStatusSchema = z.object({ type: z.literal("up_to_date") });
36
+ export const UpdateAvailableVersionStatusSchema = z.object({ type: z.literal("update_available") });
37
+ export const UpdateRecommendedVersionStatusSchema = z.object({ type: z.literal("update_recommended") });
38
+ export const UpdateRequiredVersionStatusSchema = z.object({ type: z.literal("update_required") });
39
+ export const DisabledVersionStatusSchema = z.object({ type: z.literal("disabled") });
40
+ /**
41
+ * The version status schema.
42
+ * - "initializing" - The version status is initializing.
43
+ * - "checking" - The version status is being checked.
44
+ * - "up_to_date" - The version is up to date.
45
+ * - "update_available" - The version is available for update.
46
+ * - "update_required" - The version is required for update.
47
+ */
48
+ export const VersionStatusSchema = z.discriminatedUnion("type", [
49
+ InitializingVersionStatusSchema,
50
+ CheckingVersionStatusSchema,
51
+ UpToDateVersionStatusSchema,
52
+ UpdateAvailableVersionStatusSchema,
53
+ UpdateRecommendedVersionStatusSchema,
54
+ UpdateRequiredVersionStatusSchema,
55
+ DisabledVersionStatusSchema,
56
+ ]);
57
+
58
+ export type InitializingVersionStatus = z.infer<typeof InitializingVersionStatusSchema>;
59
+ export type CheckingVersionStatus = z.infer<typeof CheckingVersionStatusSchema>;
60
+ export type UpToDateVersionStatus = z.infer<typeof UpToDateVersionStatusSchema>;
61
+ export type UpdateAvailableVersionStatus = z.infer<typeof UpdateAvailableVersionStatusSchema>;
62
+ export type UpdateRecommendedVersionStatus = z.infer<typeof UpdateRecommendedVersionStatusSchema>;
63
+ export type UpdateRequiredVersionStatus = z.infer<typeof UpdateRequiredVersionStatusSchema>;
64
+ export type DisabledVersionStatus = z.infer<typeof DisabledVersionStatusSchema>;
65
+ export type VersionStatus = z.infer<typeof VersionStatusSchema>;
66
+
67
+ export type VersionStatusChangeEvents = {
68
+ VERSION_STATUS_CHANGED: (status: VersionStatus) => void;
69
+ };
70
+
71
+ export type ForceUpdateClientOptions = {
72
+ /** Min ms between foreground checks (default: 30000) */
73
+ throttleMs?: number;
74
+ /** Min ms since last successful check before re-checking (default: 300000 = 5min) */
75
+ checkCooldownMs?: number;
76
+ };
77
+
78
+ const DEFAULT_OPTIONS: Required<ForceUpdateClientOptions> = {
79
+ throttleMs: 30_000, // 30 seconds
80
+ checkCooldownMs: 300_000, // 5 minutes
81
+ };
82
+
83
+ export const VERSION_STATUS_STORAGE_KEY = "VERSION_STATUS";
84
+
85
+ export class ForceUpdateClient {
86
+ private emitter = new EventEmitter<VersionStatusChangeEvents>();
87
+ private versionStatus: VersionStatus;
88
+ private unsubscribe: (() => void) | null = null;
89
+ private appStateSubscription: NativeEventSubscription | null = null;
90
+ private lastCheckTime: number | null = null;
91
+ private lastForegroundTime: number | null = null;
92
+
93
+ private readonly logger: Logger;
94
+ private readonly storage: SupportedStorage;
95
+ private readonly options: Required<ForceUpdateClientOptions>;
96
+
97
+ constructor(
98
+ logging: LoggingClient,
99
+ storage: StorageClient,
100
+ private readonly identity: IdentityClient,
101
+ options: ForceUpdateClientOptions = {}
102
+ ) {
103
+ this.logger = logging.createLogger({ name: "ForceUpdateClient" });
104
+ this.storage = storage.createStorage("version");
105
+ this.options = { ...DEFAULT_OPTIONS, ...options };
106
+ this.versionStatus = this.getVersionStatusFromStorage();
107
+ this.subscribeToIdentity();
108
+ this.subscribeToAppState();
109
+ }
110
+
111
+ private getVersionStatusFromStorage(): VersionStatus {
112
+ const stored = this.storage.getItem(VERSION_STATUS_STORAGE_KEY);
113
+ if (stored == null) {
114
+ return InitializingVersionStatusSchema.parse({ type: "initializing" });
115
+ }
116
+
117
+ return VersionStatusSchema.parse(JSON.parse(stored));
118
+ }
119
+
120
+ private saveVersionStatusToStorage(status: VersionStatus): void {
121
+ this.storage.setItem(VERSION_STATUS_STORAGE_KEY, JSON.stringify(status));
122
+ }
123
+
124
+ private subscribeToIdentity() {
125
+ this.unsubscribe = this.identity.onIdentifyStateChange((state) => {
126
+ if (state.type === "identifying") {
127
+ this.setVersionStatus({ type: "checking" });
128
+ } else if (state.type === "identified") {
129
+ this.updateFromVersionStatus(state.version_info.status ?? IdentifyVersionStatusEnum.UP_TO_DATE);
130
+ }
131
+ });
132
+ }
133
+
134
+ private updateFromVersionStatus(status?: IdentifyVersionStatusEnum) {
135
+ if (!status) {
136
+ this.setVersionStatus({ type: "up_to_date" });
137
+ return;
138
+ }
139
+
140
+ switch (status) {
141
+ case "UPDATE_AVAILABLE":
142
+ this.setVersionStatus({ type: "update_available" });
143
+ break;
144
+ case "UPDATE_RECOMMENDED":
145
+ this.setVersionStatus({ type: "update_recommended" });
146
+ break;
147
+ case "UPDATE_REQUIRED":
148
+ this.setVersionStatus({ type: "update_required" });
149
+ break;
150
+ case "UP_TO_DATE":
151
+ this.setVersionStatus({ type: "up_to_date" });
152
+ break;
153
+ case "DISABLED":
154
+ this.setVersionStatus({ type: "disabled" });
155
+ break;
156
+ default:
157
+ this.setVersionStatus({ type: "up_to_date" });
158
+ }
159
+ }
160
+
161
+ private subscribeToAppState() {
162
+ this.appStateSubscription = AppState.addEventListener("change", this.handleAppStateChange);
163
+ }
164
+
165
+ private handleAppStateChange = (nextState: AppStateStatus) => {
166
+ if (nextState === "active") {
167
+ const now = Date.now();
168
+ const throttleOk = !this.lastForegroundTime || now - this.lastForegroundTime >= this.options.throttleMs;
169
+ const cooldownOk = !this.lastCheckTime || now - this.lastCheckTime >= this.options.checkCooldownMs;
170
+
171
+ this.lastForegroundTime = now;
172
+
173
+ if (throttleOk && cooldownOk) {
174
+ this.checkVersionOnForeground();
175
+ }
176
+ }
177
+ };
178
+
179
+ private async checkVersionOnForeground() {
180
+ this.logger.info("Checking version status on foreground");
181
+ const result = await this.identity.identify();
182
+
183
+ if (!result) {
184
+ this.logger.info("Skipping version check - not identified");
185
+ return;
186
+ }
187
+
188
+ if (result.success) {
189
+ this.lastCheckTime = Date.now();
190
+ // Version status is handled by subscribeToIdentity() listener
191
+ }
192
+ }
193
+
194
+ public onVersionStatusChange(listener: (status: VersionStatus) => void) {
195
+ this.emitter.addListener("VERSION_STATUS_CHANGED", listener);
196
+ return () => {
197
+ this.emitter.removeListener("VERSION_STATUS_CHANGED", listener);
198
+ };
199
+ }
200
+
201
+ public getVersionStatus(): VersionStatus {
202
+ return this.versionStatus;
203
+ }
204
+
205
+ private setVersionStatus(newStatus: VersionStatus) {
206
+ this.logger.info(`Version status changing: ${this.versionStatus.type} -> ${newStatus.type}`);
207
+ this.versionStatus = newStatus;
208
+ this.saveVersionStatusToStorage(newStatus);
209
+ this.emitter.emit("VERSION_STATUS_CHANGED", newStatus);
210
+ }
211
+
212
+ public shutdown() {
213
+ if (this.unsubscribe) {
214
+ this.unsubscribe();
215
+ this.unsubscribe = null;
216
+ }
217
+ if (this.appStateSubscription) {
218
+ this.appStateSubscription.remove();
219
+ this.appStateSubscription = null;
220
+ }
221
+ this.emitter.removeAllListeners("VERSION_STATUS_CHANGED");
222
+ }
223
+
224
+ }
@@ -0,0 +1 @@
1
+ export * from "./force-update.client";