@appspacer/react-native 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,29 @@
1
+ package com.appspacer;
2
+
3
+ import androidx.annotation.NonNull;
4
+
5
+ import com.facebook.react.ReactPackage;
6
+ import com.facebook.react.bridge.NativeModule;
7
+ import com.facebook.react.bridge.ReactApplicationContext;
8
+ import com.facebook.react.uimanager.ViewManager;
9
+
10
+ import java.util.ArrayList;
11
+ import java.util.Collections;
12
+ import java.util.List;
13
+
14
+ public class AppSpacerPackage implements ReactPackage {
15
+
16
+ @NonNull
17
+ @Override
18
+ public List<NativeModule> createNativeModules(@NonNull ReactApplicationContext reactContext) {
19
+ List<NativeModule> modules = new ArrayList<>();
20
+ modules.add(new AppSpacerModule(reactContext));
21
+ return modules;
22
+ }
23
+
24
+ @NonNull
25
+ @Override
26
+ public List<ViewManager> createViewManagers(@NonNull ReactApplicationContext reactContext) {
27
+ return Collections.emptyList();
28
+ }
29
+ }
@@ -0,0 +1,116 @@
1
+ import type { AppSpacerConfig, AppSpacerUpdate, UpdateMetadata, SyncOptions } from "./types";
2
+ import { UpdateStatus } from "./types";
3
+ type StatusListener = (status: UpdateStatus, detail?: string) => void;
4
+ type ProgressListener = (progress: number) => void;
5
+ /**
6
+ * Production-grade AppSpacer OTA manager.
7
+ */
8
+ export declare class AppSpacerManager {
9
+ private config;
10
+ private status;
11
+ private listeners;
12
+ private progressListeners;
13
+ private checkTimer;
14
+ private pendingUpdate;
15
+ private appStateSubscription;
16
+ private breadcrumbAppStateSubscription;
17
+ private downloadProgressSubscription;
18
+ private sessionId;
19
+ private userMetadata;
20
+ /**
21
+ * Initialize the AppSpacer SDK.
22
+ */
23
+ init(config: AppSpacerConfig): void;
24
+ /**
25
+ * Tear down timers and listeners. Safe to call multiple times.
26
+ */
27
+ destroy(): void;
28
+ /**
29
+ * Explicitly notify the SDK that the app has successfully loaded.
30
+ * This prevents automatic rollback to the previous version.
31
+ */
32
+ notifyAppReady(): Promise<void>;
33
+ /**
34
+ * Check for updates from the AppSpacer server.
35
+ */
36
+ checkForUpdate(): Promise<AppSpacerUpdate>;
37
+ /**
38
+ * Download, verify, and prepare the update for installation.
39
+ */
40
+ downloadUpdate(): Promise<void>;
41
+ /**
42
+ * High-level sync method covering the full update lifecycle.
43
+ */
44
+ sync(options?: SyncOptions): Promise<AppSpacerUpdate>;
45
+ /**
46
+ * Remove old update directories that are no longer active.
47
+ * Keeps the current active update and the previous update for rollback.
48
+ * Returns the number of directories removed.
49
+ */
50
+ clearOldUpdates(): Promise<number>;
51
+ /**
52
+ * Get metadata about the currently installed update.
53
+ * Returns null if no OTA update is installed (running the embedded bundle).
54
+ */
55
+ getUpdateMetadata(): Promise<UpdateMetadata | null>;
56
+ /**
57
+ * Associate this session with a specific user.
58
+ * This is extremely useful for searching crashes by user ID in your dashboard.
59
+ */
60
+ setUserIdentity(userId: string, details?: object): void;
61
+ /**
62
+ * Add a breadcrumb to tracking. Breadcrumbs are included in crash reports
63
+ * to help reconstruct the events leading up to a crash.
64
+ */
65
+ addBreadcrumb(message: string, category?: string, data?: object): void;
66
+ /**
67
+ * Register a listener for download progress events.
68
+ * The callback receives a number between 0 and 1.
69
+ * Returns an unsubscribe function.
70
+ */
71
+ onDownloadProgress(listener: ProgressListener): () => void;
72
+ /**
73
+ * Report install status (installed or failed) to the backend.
74
+ */
75
+ private reportInstallStatus;
76
+ /**
77
+ * Process pending pending crash reports from native storage and send them to the backend.
78
+ */
79
+ private processCrashReports;
80
+ /**
81
+ * Get the current session ID.
82
+ */
83
+ getSessionId(): string;
84
+ /**
85
+ * Get the current update status.
86
+ */
87
+ getStatus(): UpdateStatus;
88
+ /**
89
+ * Reload the React Native bundle.
90
+ */
91
+ restartApp(): void;
92
+ onStatusChange(listener: StatusListener): () => void;
93
+ /**
94
+ * Display a native alert dialog to prompt the user about an update.
95
+ */
96
+ private showUpdateDialog;
97
+ /**
98
+ * Subscribe to native download progress events and fan out to JS listeners.
99
+ */
100
+ private subscribeToDownloadProgress;
101
+ private registerAutomaticBreadcrumbs;
102
+ /**
103
+ * Listen for app returning from background and restart to apply the installed update.
104
+ */
105
+ private waitForResumeThenRestart;
106
+ private startAutoCheck;
107
+ private stopAutoCheck;
108
+ private setStatus;
109
+ private assertConfigured;
110
+ /**
111
+ * fetch() wrapper with configurable timeout via AbortController.
112
+ */
113
+ private fetchWithTimeout;
114
+ }
115
+ export declare const AppSpacer: AppSpacerManager;
116
+ export {};
@@ -0,0 +1,546 @@
1
+ import { Platform, AppState, Alert } from "react-native";
2
+ import { getNativeModule, getEventEmitter } from "./native";
3
+ import { initAssetResolver } from "./assetResolver";
4
+ import { initCrashReporter } from "./CrashReporter";
5
+ import { UpdateStatus, InstallMode } from "./types";
6
+ /** Default fetch timeout in milliseconds */
7
+ const FETCH_TIMEOUT_MS = 15000;
8
+ /** Default API URL for AppSpacer SaaS (Development uses local machine IP for Android emulators) */
9
+ const DEFAULT_API_URL = __DEV__
10
+ ? Platform.select({
11
+ android: "http://10.0.2.2:8080/api",
12
+ ios: "http://localhost:8080/api",
13
+ default: "http://localhost:8080/api",
14
+ })
15
+ : "https://api.appspacer.com/api";
16
+ /**
17
+ * Production-grade AppSpacer OTA manager.
18
+ */
19
+ export class AppSpacerManager {
20
+ constructor() {
21
+ this.config = null;
22
+ this.status = UpdateStatus.IDLE;
23
+ this.listeners = new Set();
24
+ this.progressListeners = new Set();
25
+ this.checkTimer = null;
26
+ this.pendingUpdate = null;
27
+ this.appStateSubscription = null;
28
+ this.breadcrumbAppStateSubscription = null;
29
+ this.downloadProgressSubscription = null;
30
+ this.sessionId = "";
31
+ this.userMetadata = {};
32
+ }
33
+ /**
34
+ * Initialize the AppSpacer SDK.
35
+ */
36
+ init(config) {
37
+ // Clean up any previous initialization (safe for hot-reload)
38
+ this.destroy();
39
+ this.config = { ...config };
40
+ // Generate a unique session ID for this app launch
41
+ this.sessionId = `session-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
42
+ // Automatic rollback protection and successful boot notification
43
+ this.notifyAppReady();
44
+ // Check for pending crashes and upload them
45
+ if (config.enableCrashReporting) {
46
+ initCrashReporter(() => this.sessionId);
47
+ this.processCrashReports().catch(() => { });
48
+ this.registerAutomaticBreadcrumbs();
49
+ this.addBreadcrumb("SDK Initialized", "system");
50
+ }
51
+ // Initialize OTA asset resolver (patches Image.resolveAssetSource)
52
+ initAssetResolver().catch(() => { });
53
+ // Subscribe to native download progress events
54
+ this.subscribeToDownloadProgress();
55
+ // Start automatic checking if interval is set
56
+ if (config.checkInterval && config.checkInterval > 0) {
57
+ this.startAutoCheck(config.checkInterval);
58
+ }
59
+ }
60
+ /**
61
+ * Tear down timers and listeners. Safe to call multiple times.
62
+ */
63
+ destroy() {
64
+ this.stopAutoCheck();
65
+ if (this.appStateSubscription) {
66
+ this.appStateSubscription.remove();
67
+ this.appStateSubscription = null;
68
+ }
69
+ if (this.breadcrumbAppStateSubscription) {
70
+ this.breadcrumbAppStateSubscription.remove();
71
+ this.breadcrumbAppStateSubscription = null;
72
+ }
73
+ if (this.downloadProgressSubscription) {
74
+ this.downloadProgressSubscription.remove();
75
+ this.downloadProgressSubscription = null;
76
+ }
77
+ }
78
+ /**
79
+ * Explicitly notify the SDK that the app has successfully loaded.
80
+ * This prevents automatic rollback to the previous version.
81
+ */
82
+ async notifyAppReady() {
83
+ try {
84
+ const native = getNativeModule();
85
+ await native.markSuccess();
86
+ // If we just booted into a new version, report the successful install
87
+ const infoStr = await native.getCurrentPackageInfo();
88
+ if (infoStr) {
89
+ const info = JSON.parse(infoStr);
90
+ if (info.isPending && info.releaseId) {
91
+ await this.reportInstallStatus(info.releaseId, "installed");
92
+ // Mark as no longer pending
93
+ await native.setCurrentPackageInfo(JSON.stringify({ ...info, isPending: false }));
94
+ }
95
+ }
96
+ }
97
+ catch {
98
+ // Ignore if native not ready
99
+ }
100
+ }
101
+ /**
102
+ * Check for updates from the AppSpacer server.
103
+ */
104
+ async checkForUpdate() {
105
+ this.assertConfigured();
106
+ this.setStatus(UpdateStatus.CHECKING);
107
+ try {
108
+ const native = getNativeModule();
109
+ const currentHash = await native.getCurrentPackageHash();
110
+ const installId = await native.getInstallId();
111
+ const response = await this.fetchWithTimeout(`${DEFAULT_API_URL}/updates/check`, {
112
+ method: "POST",
113
+ headers: { "Content-Type": "application/json" },
114
+ body: JSON.stringify({
115
+ deployment_key: this.config.deploymentKey,
116
+ app_version: this.config.appVersion,
117
+ platform: Platform.OS,
118
+ package_hash: currentHash,
119
+ install_id: installId,
120
+ }),
121
+ });
122
+ if (!response.ok) {
123
+ throw new Error(`Server returned HTTP ${response.status}`);
124
+ }
125
+ const body = (await response.json());
126
+ if (!body.success) {
127
+ throw new Error("Failed to check for updates");
128
+ }
129
+ const update = {
130
+ updateAvailable: body.data.update_available,
131
+ releaseId: body.data.release_id,
132
+ packageUrl: body.data.package_url,
133
+ hash: body.data.hash,
134
+ mandatory: body.data.mandatory,
135
+ description: body.data.description,
136
+ label: body.data.label,
137
+ packageSize: body.data.package_size,
138
+ };
139
+ if (update.updateAvailable) {
140
+ this.pendingUpdate = update;
141
+ this.setStatus(UpdateStatus.UPDATE_AVAILABLE);
142
+ this.addBreadcrumb(`Update available: ${update.releaseId}`, "ota");
143
+ }
144
+ else {
145
+ this.setStatus(UpdateStatus.UP_TO_DATE);
146
+ this.addBreadcrumb("App is up to date", "ota");
147
+ }
148
+ return update;
149
+ }
150
+ catch (error) {
151
+ this.setStatus(UpdateStatus.ERROR, error.message);
152
+ throw error;
153
+ }
154
+ }
155
+ /**
156
+ * Download, verify, and prepare the update for installation.
157
+ */
158
+ async downloadUpdate() {
159
+ this.assertConfigured();
160
+ if (!this.pendingUpdate?.packageUrl || !this.pendingUpdate?.hash) {
161
+ throw new Error("No pending update. Call checkForUpdate() first.");
162
+ }
163
+ const native = getNativeModule();
164
+ const packageUrl = this.pendingUpdate.packageUrl;
165
+ const expectedHash = this.pendingUpdate.hash;
166
+ try {
167
+ this.setStatus(UpdateStatus.DOWNLOADING);
168
+ const storageDir = await native.getOtaStoragePath();
169
+ const downloadedZipPath = await native.downloadPackage(packageUrl, storageDir);
170
+ // Hash Verification
171
+ const isValid = await native.verifyHash(downloadedZipPath, expectedHash);
172
+ if (!isValid) {
173
+ // Cleanup and abort
174
+ throw new Error("Update package hash mismatch. Verification failed.");
175
+ }
176
+ this.setStatus(UpdateStatus.INSTALLING);
177
+ const unzippedDir = `${storageDir}/update_${expectedHash}`;
178
+ await native.unzipPackage(downloadedZipPath, unzippedDir);
179
+ // Final install preparation
180
+ await native.installUpdate(unzippedDir);
181
+ // Metadata with isPending flag for rollback protection
182
+ await native.setCurrentPackageInfo(JSON.stringify({
183
+ hash: expectedHash,
184
+ releaseId: this.pendingUpdate.releaseId,
185
+ description: this.pendingUpdate.description,
186
+ label: this.pendingUpdate.label,
187
+ installedAt: new Date().toISOString(),
188
+ isPending: true,
189
+ }));
190
+ this.setStatus(UpdateStatus.INSTALLED);
191
+ }
192
+ catch (error) {
193
+ // Report install failure to backend
194
+ if (this.pendingUpdate?.releaseId) {
195
+ this.reportInstallStatus(this.pendingUpdate.releaseId, "failed", error.message).catch(() => { });
196
+ }
197
+ this.setStatus(UpdateStatus.ERROR, error.message);
198
+ throw error;
199
+ }
200
+ }
201
+ /**
202
+ * High-level sync method covering the full update lifecycle.
203
+ */
204
+ async sync(options) {
205
+ const update = await this.checkForUpdate();
206
+ if (update.updateAvailable) {
207
+ const mode = update.mandatory
208
+ ? options?.mandatoryInstallMode ?? options?.installMode ?? InstallMode.IMMEDIATE
209
+ : options?.installMode ?? InstallMode.ON_NEXT_RESTART;
210
+ if (__DEV__)
211
+ console.log(`[AppSpacer] Update found: ${update.releaseId}. Mode: ${mode}. Mandatory: ${update.mandatory}`);
212
+ if (options?.updateDialog) {
213
+ const dialogOpts = typeof options.updateDialog === "boolean" ? {} : options.updateDialog;
214
+ const shouldInstall = await this.showUpdateDialog(update, dialogOpts);
215
+ if (!shouldInstall) {
216
+ return update;
217
+ }
218
+ }
219
+ await this.downloadUpdate();
220
+ if (mode === InstallMode.IMMEDIATE) {
221
+ this.restartApp();
222
+ }
223
+ else if (mode === InstallMode.ON_NEXT_RESUME) {
224
+ this.waitForResumeThenRestart();
225
+ }
226
+ }
227
+ return update;
228
+ }
229
+ /**
230
+ * Remove old update directories that are no longer active.
231
+ * Keeps the current active update and the previous update for rollback.
232
+ * Returns the number of directories removed.
233
+ */
234
+ async clearOldUpdates() {
235
+ const native = getNativeModule();
236
+ return native.clearOldUpdates();
237
+ }
238
+ /**
239
+ * Get metadata about the currently installed update.
240
+ * Returns null if no OTA update is installed (running the embedded bundle).
241
+ */
242
+ async getUpdateMetadata() {
243
+ const native = getNativeModule();
244
+ const infoStr = await native.getUpdateMetadata();
245
+ if (!infoStr)
246
+ return null;
247
+ try {
248
+ return JSON.parse(infoStr);
249
+ }
250
+ catch {
251
+ return null;
252
+ }
253
+ }
254
+ /**
255
+ * Associate this session with a specific user.
256
+ * This is extremely useful for searching crashes by user ID in your dashboard.
257
+ */
258
+ setUserIdentity(userId, details) {
259
+ this.userMetadata = { id: userId, details };
260
+ try {
261
+ const native = getNativeModule();
262
+ native.setUserIdentity(userId, JSON.stringify(details || {}));
263
+ }
264
+ catch {
265
+ // Native not ready
266
+ }
267
+ this.addBreadcrumb(`User Identity set: ${userId}`, 'user');
268
+ }
269
+ /**
270
+ * Add a breadcrumb to tracking. Breadcrumbs are included in crash reports
271
+ * to help reconstruct the events leading up to a crash.
272
+ */
273
+ addBreadcrumb(message, category, data) {
274
+ if (!this.config?.enableCrashReporting)
275
+ return;
276
+ const timestamp = new Date().toISOString();
277
+ const breadcrumb = `[${timestamp}] ${category ? `(${category}) ` : ""}${message}${data ? ` ${JSON.stringify(data)}` : ""}`;
278
+ try {
279
+ const native = getNativeModule();
280
+ native.addBreadcrumb(breadcrumb);
281
+ }
282
+ catch {
283
+ // Native not available
284
+ }
285
+ }
286
+ /**
287
+ * Register a listener for download progress events.
288
+ * The callback receives a number between 0 and 1.
289
+ * Returns an unsubscribe function.
290
+ */
291
+ onDownloadProgress(listener) {
292
+ this.progressListeners.add(listener);
293
+ return () => this.progressListeners.delete(listener);
294
+ }
295
+ /**
296
+ * Report install status (installed or failed) to the backend.
297
+ */
298
+ async reportInstallStatus(releaseId, status, errorMessage) {
299
+ if (!this.config)
300
+ return;
301
+ try {
302
+ const native = getNativeModule();
303
+ const installId = await native.getInstallId();
304
+ await this.fetchWithTimeout(`${DEFAULT_API_URL}/updates/report-status`, {
305
+ method: "POST",
306
+ headers: { "Content-Type": "application/json" },
307
+ body: JSON.stringify({
308
+ release_id: releaseId,
309
+ device_id: installId,
310
+ platform: Platform.OS,
311
+ app_version: this.config.appVersion,
312
+ status,
313
+ ...(status === "failed" && errorMessage
314
+ ? { error_message: errorMessage }
315
+ : {}),
316
+ }),
317
+ });
318
+ }
319
+ catch {
320
+ // Background reporting failure is non-fatal
321
+ }
322
+ }
323
+ /**
324
+ * Process pending pending crash reports from native storage and send them to the backend.
325
+ */
326
+ async processCrashReports() {
327
+ if (!this.config?.enableCrashReporting)
328
+ return;
329
+ try {
330
+ const native = getNativeModule();
331
+ const pendingJson = await native.getPendingCrashReports();
332
+ if (!pendingJson)
333
+ return;
334
+ const reports = JSON.parse(pendingJson);
335
+ if (__DEV__ && reports.length > 0) {
336
+ console.log(`[AppSpacer] Found ${reports.length} pending crash report(s). Uploading...`);
337
+ }
338
+ for (const report of reports) {
339
+ try {
340
+ // Attempt to parse payload if it's JSON from JS SDK
341
+ let message = 'Crash occurred';
342
+ let stack = report.payload;
343
+ let isFatal = true;
344
+ try {
345
+ const parsed = JSON.parse(report.payload);
346
+ if (parsed.message)
347
+ message = parsed.message;
348
+ if (parsed.stack)
349
+ stack = parsed.stack;
350
+ if (parsed.isFatal !== undefined)
351
+ isFatal = parsed.isFatal;
352
+ }
353
+ catch (e) {
354
+ // Native payloads might be plain text or manually formatted
355
+ }
356
+ // Fire and forget upload to crash analytics endpoint
357
+ const installId = await native.getInstallId();
358
+ const response = await this.fetchWithTimeout(`${DEFAULT_API_URL}/v1/crashes`, {
359
+ method: "POST",
360
+ headers: {
361
+ "Content-Type": "application/json",
362
+ "x-api-key": this.config.deploymentKey,
363
+ },
364
+ body: JSON.stringify({
365
+ id: report.id,
366
+ session_id: report.session_id || this.sessionId,
367
+ fatal: isFatal,
368
+ exception: {
369
+ type: report.type,
370
+ message: message,
371
+ stack_trace: stack
372
+ },
373
+ user_id: report.user_id || this.userMetadata.id,
374
+ user_metadata: report.user_metadata || this.userMetadata.details,
375
+ device: {
376
+ os: Platform.OS,
377
+ app_version: this.config.appVersion,
378
+ ...report.device || {},
379
+ },
380
+ breadcrumbs: report.breadcrumbs || [],
381
+ timestamp: report.timestamp,
382
+ }),
383
+ });
384
+ if (response.ok) {
385
+ if (__DEV__)
386
+ console.log(`[AppSpacer] Successfully uploaded crash report: ${report.id}`);
387
+ await native.deleteCrashReport(report.id);
388
+ }
389
+ else {
390
+ const body = await response.text();
391
+ console.warn(`[AppSpacer] Crash upload failed for ${report.id}. Status: ${response.status}. Body: ${body}`);
392
+ }
393
+ }
394
+ catch (e) {
395
+ console.warn(`[AppSpacer] Failed to upload crash report ${report.id}`, e);
396
+ }
397
+ }
398
+ }
399
+ catch (e) {
400
+ console.warn("[AppSpacer] Failed to process pending crashes", e);
401
+ }
402
+ }
403
+ /**
404
+ * Get the current session ID.
405
+ */
406
+ getSessionId() {
407
+ return this.sessionId;
408
+ }
409
+ /**
410
+ * Get the current update status.
411
+ */
412
+ getStatus() {
413
+ return this.status;
414
+ }
415
+ /**
416
+ * Reload the React Native bundle.
417
+ */
418
+ restartApp() {
419
+ const native = getNativeModule();
420
+ native.reloadApp();
421
+ }
422
+ onStatusChange(listener) {
423
+ this.listeners.add(listener);
424
+ return () => this.listeners.delete(listener);
425
+ }
426
+ // ─── Private helpers ───────────────────────────────────────────────
427
+ /**
428
+ * Display a native alert dialog to prompt the user about an update.
429
+ */
430
+ showUpdateDialog(update, options) {
431
+ return new Promise((resolve) => {
432
+ const title = options.title ?? "Update Available";
433
+ let message = options.message ?? (update.mandatory
434
+ ? (options.mandatoryUpdateMessage ?? "An update is available that must be installed.")
435
+ : "An update is available for this app. Would you like to install it now?");
436
+ if (options.appendReleaseDescription !== false && update.description) {
437
+ message += `\n\nRelease notes:\n${update.description}`;
438
+ }
439
+ const buttons = [];
440
+ if (!update.mandatory) {
441
+ buttons.push({
442
+ text: options.ignoreButtonLabel ?? "Ignore",
443
+ style: "cancel",
444
+ onPress: () => resolve(false),
445
+ });
446
+ }
447
+ buttons.push({
448
+ text: update.mandatory
449
+ ? (options.mandatoryContinueButtonLabel ?? "Continue")
450
+ : (options.installButtonLabel ?? "Install"),
451
+ onPress: () => resolve(true),
452
+ });
453
+ Alert.alert(title, message, buttons, { cancelable: !update.mandatory });
454
+ });
455
+ }
456
+ /**
457
+ * Subscribe to native download progress events and fan out to JS listeners.
458
+ */
459
+ subscribeToDownloadProgress() {
460
+ if (this.downloadProgressSubscription) {
461
+ this.downloadProgressSubscription.remove();
462
+ }
463
+ try {
464
+ const emitter = getEventEmitter();
465
+ this.downloadProgressSubscription = emitter.addListener("AppSpacerDownloadProgress", (event) => {
466
+ for (const listener of this.progressListeners) {
467
+ try {
468
+ listener(event.progress);
469
+ }
470
+ catch { }
471
+ }
472
+ });
473
+ }
474
+ catch {
475
+ // Event emitter not available — progress won't be reported
476
+ }
477
+ }
478
+ registerAutomaticBreadcrumbs() {
479
+ if (this.breadcrumbAppStateSubscription) {
480
+ this.breadcrumbAppStateSubscription.remove();
481
+ }
482
+ this.breadcrumbAppStateSubscription = AppState.addEventListener("change", (nextState) => {
483
+ this.addBreadcrumb(`App State changed: ${nextState}`, "system");
484
+ });
485
+ this.addBreadcrumb(`Native Platform: ${Platform.OS}`, "system");
486
+ }
487
+ /**
488
+ * Listen for app returning from background and restart to apply the installed update.
489
+ */
490
+ waitForResumeThenRestart() {
491
+ // Remove any previous subscription
492
+ if (this.appStateSubscription) {
493
+ this.appStateSubscription.remove();
494
+ this.appStateSubscription = null;
495
+ }
496
+ this.appStateSubscription = AppState.addEventListener("change", (nextState) => {
497
+ this.addBreadcrumb(`App State changed to: ${nextState}`, "system");
498
+ if (nextState === "active") {
499
+ // Clean up the listener before restarting
500
+ if (this.appStateSubscription) {
501
+ this.appStateSubscription.remove();
502
+ this.appStateSubscription = null;
503
+ }
504
+ this.restartApp();
505
+ }
506
+ });
507
+ }
508
+ startAutoCheck(intervalSeconds) {
509
+ this.stopAutoCheck();
510
+ this.checkTimer = setInterval(() => this.checkForUpdate().catch(() => { }), intervalSeconds * 1000);
511
+ }
512
+ stopAutoCheck() {
513
+ if (this.checkTimer) {
514
+ clearInterval(this.checkTimer);
515
+ this.checkTimer = null;
516
+ }
517
+ }
518
+ setStatus(status, detail) {
519
+ this.status = status;
520
+ for (const listener of this.listeners) {
521
+ try {
522
+ listener(status, detail);
523
+ }
524
+ catch { }
525
+ }
526
+ }
527
+ assertConfigured() {
528
+ if (!this.config) {
529
+ throw new Error("AppSpacer SDK not initialized. Call AppSpacer.init() first.");
530
+ }
531
+ }
532
+ /**
533
+ * fetch() wrapper with configurable timeout via AbortController.
534
+ */
535
+ async fetchWithTimeout(url, options, timeoutMs = FETCH_TIMEOUT_MS) {
536
+ const controller = new AbortController();
537
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
538
+ try {
539
+ return await fetch(url, { ...options, signal: controller.signal });
540
+ }
541
+ finally {
542
+ clearTimeout(timer);
543
+ }
544
+ }
545
+ }
546
+ export const AppSpacer = new AppSpacerManager();
@@ -0,0 +1 @@
1
+ export declare const initCrashReporter: (getSessionId: () => string) => void;