pulse-updates 1.1.1 → 1.2.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.
- package/README.md +50 -10
- package/SECURITY.md +42 -0
- package/android/src/main/java/app/pulse/updates/PulseController.kt +6 -2
- package/android/src/main/java/app/pulse/updates/PulseUpdatesModule.kt +25 -0
- package/app.plugin.js +47 -0
- package/ios/PulseUpdates/PulseController.swift +7 -2
- package/ios/PulseUpdates/PulseUpdates.m +7 -0
- package/ios/PulseUpdates/PulseUpdates.swift +22 -0
- package/lib/commonjs/NativePulseUpdates.js.map +1 -1
- package/lib/commonjs/PulseUpdates.js +36 -0
- package/lib/commonjs/PulseUpdates.js.map +1 -1
- package/lib/commonjs/config.js +175 -21
- package/lib/commonjs/config.js.map +1 -1
- package/lib/commonjs/decisions.js +128 -0
- package/lib/commonjs/decisions.js.map +1 -0
- package/lib/commonjs/index.js +12 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/init.js +153 -13
- package/lib/commonjs/init.js.map +1 -1
- package/lib/commonjs/track.js +154 -12
- package/lib/commonjs/track.js.map +1 -1
- package/lib/commonjs/usePulseUpdates.js +21 -16
- package/lib/commonjs/usePulseUpdates.js.map +1 -1
- package/lib/module/NativePulseUpdates.js.map +1 -1
- package/lib/module/PulseUpdates.js +33 -0
- package/lib/module/PulseUpdates.js.map +1 -1
- package/lib/module/config.js +174 -21
- package/lib/module/config.js.map +1 -1
- package/lib/module/decisions.js +122 -0
- package/lib/module/decisions.js.map +1 -0
- package/lib/module/index.js +1 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/init.js +154 -15
- package/lib/module/init.js.map +1 -1
- package/lib/module/track.js +152 -12
- package/lib/module/track.js.map +1 -1
- package/lib/module/usePulseUpdates.js +21 -16
- package/lib/module/usePulseUpdates.js.map +1 -1
- package/lib/typescript/NativePulseUpdates.d.ts +1 -0
- package/lib/typescript/NativePulseUpdates.d.ts.map +1 -1
- package/lib/typescript/PulseUpdates.d.ts +20 -0
- package/lib/typescript/PulseUpdates.d.ts.map +1 -1
- package/lib/typescript/config.d.ts +22 -0
- package/lib/typescript/config.d.ts.map +1 -1
- package/lib/typescript/decisions.d.ts +55 -0
- package/lib/typescript/decisions.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +1 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/init.d.ts +51 -4
- package/lib/typescript/init.d.ts.map +1 -1
- package/lib/typescript/track.d.ts +29 -1
- package/lib/typescript/track.d.ts.map +1 -1
- package/lib/typescript/usePulseUpdates.d.ts.map +1 -1
- package/logo.png +0 -0
- package/package.json +14 -3
- package/scripts/publish.mjs +68 -3
- package/src/NativePulseUpdates.ts +1 -0
- package/src/PulseUpdates.ts +54 -0
- package/src/config.ts +234 -21
- package/src/decisions.ts +179 -0
- package/src/index.ts +1 -0
- package/src/init.ts +227 -12
- package/src/track.ts +191 -13
- package/src/usePulseUpdates.ts +21 -16
package/src/track.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* everybody sees.
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
import type { ConfigContext } from './config';
|
|
23
|
+
import type { ConfigContext, ConfigStorage } from './config';
|
|
24
24
|
|
|
25
25
|
export interface TrackOptions {
|
|
26
26
|
/**
|
|
@@ -43,9 +43,35 @@ export interface TrackOptions {
|
|
|
43
43
|
/** Events held before the oldest are dropped. Default 500. */
|
|
44
44
|
maxQueue?: number;
|
|
45
45
|
|
|
46
|
+
/** Persists the outbox across process death. Use the same storage passed to initPulse. */
|
|
47
|
+
storage?: ConfigStorage;
|
|
48
|
+
|
|
49
|
+
/** App-scoped persistence key. initPulse supplies one automatically. */
|
|
50
|
+
storageKey?: string;
|
|
51
|
+
|
|
52
|
+
/** Per-request timeout. Default 10s. */
|
|
53
|
+
timeoutMs?: number;
|
|
54
|
+
|
|
55
|
+
/** Maximum delivery attempts before an event is dropped. Default 10. */
|
|
56
|
+
maxAttempts?: number;
|
|
57
|
+
|
|
58
|
+
/** Return false until analytics consent exists. No event is retained before consent. */
|
|
59
|
+
hasConsent?: () => boolean;
|
|
60
|
+
|
|
61
|
+
/** Only these property keys may leave the process. */
|
|
62
|
+
propertyAllowlist?: readonly string[];
|
|
63
|
+
|
|
64
|
+
/** These allowed properties are replaced with "[REDACTED]". */
|
|
65
|
+
redactProperties?: readonly string[];
|
|
66
|
+
|
|
67
|
+
/** Foreground/background lifecycle used for a best-effort final flush. */
|
|
68
|
+
appState?: { addEventListener: (type: 'change', handler: (state: string) => void) => { remove: () => void } };
|
|
69
|
+
|
|
46
70
|
/** Called with the event keys the server did not recognise. */
|
|
47
71
|
onIgnored?: (events: string[]) => void;
|
|
48
72
|
|
|
73
|
+
onDropped?: (count: number, reason: 'queue-full' | 'max-attempts' | 'no-consent') => void;
|
|
74
|
+
|
|
49
75
|
onError?: (error: unknown) => void;
|
|
50
76
|
}
|
|
51
77
|
|
|
@@ -56,19 +82,27 @@ export interface TrackedEventInput {
|
|
|
56
82
|
}
|
|
57
83
|
|
|
58
84
|
interface QueuedEvent {
|
|
85
|
+
id: string;
|
|
59
86
|
event: string;
|
|
60
87
|
time: string;
|
|
61
88
|
props: Record<string, string>;
|
|
89
|
+
attempts: number;
|
|
62
90
|
}
|
|
63
91
|
|
|
64
92
|
const DEFAULT_FLUSH_MS = 10_000;
|
|
65
93
|
const DEFAULT_MAX_BATCH = 100;
|
|
66
94
|
const DEFAULT_MAX_QUEUE = 500;
|
|
95
|
+
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
96
|
+
const DEFAULT_MAX_ATTEMPTS = 10;
|
|
97
|
+
const DEFAULT_STORAGE_KEY = 'pulse.events.v1';
|
|
67
98
|
|
|
68
99
|
let options: TrackOptions | null = null;
|
|
69
100
|
let queue: QueuedEvent[] = [];
|
|
70
101
|
let timer: ReturnType<typeof setInterval> | null = null;
|
|
71
102
|
let sending = false;
|
|
103
|
+
let nextAttemptAt = 0;
|
|
104
|
+
let lifecycleSubscription: { remove: () => void } | null = null;
|
|
105
|
+
let trackingGeneration = 0;
|
|
72
106
|
|
|
73
107
|
/** Turns a config url into the track url; leaves an explicit track url alone. */
|
|
74
108
|
function trackUrl(url: string): string {
|
|
@@ -79,8 +113,14 @@ function trackUrl(url: string): string {
|
|
|
79
113
|
}
|
|
80
114
|
|
|
81
115
|
export function configureTracking(opts: TrackOptions): void {
|
|
116
|
+
disposeTracking();
|
|
82
117
|
options = opts;
|
|
118
|
+
queue = readQueue(opts);
|
|
119
|
+
nextAttemptAt = 0;
|
|
83
120
|
startTimer();
|
|
121
|
+
lifecycleSubscription = opts.appState?.addEventListener('change', (state) => {
|
|
122
|
+
if (state !== 'active') void flushEvents();
|
|
123
|
+
}) ?? null;
|
|
84
124
|
}
|
|
85
125
|
|
|
86
126
|
/**
|
|
@@ -90,17 +130,29 @@ export function configureTracking(opts: TrackOptions): void {
|
|
|
90
130
|
*/
|
|
91
131
|
export function track(event: string, props?: TrackedEventInput['props'], time?: Date): void {
|
|
92
132
|
if (!options || !event) return;
|
|
133
|
+
if (options.hasConsent?.() === false) {
|
|
134
|
+
options.onDropped?.(1, 'no-consent');
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
93
137
|
|
|
94
138
|
const flat: Record<string, string> = {};
|
|
139
|
+
const allow = options.propertyAllowlist ? new Set(options.propertyAllowlist) : null;
|
|
140
|
+
const redact = new Set(options.redactProperties ?? []);
|
|
95
141
|
for (const [key, value] of Object.entries(props ?? {})) {
|
|
96
142
|
if (value === null || value === undefined) continue;
|
|
97
|
-
|
|
143
|
+
if (allow && !allow.has(key)) continue;
|
|
144
|
+
flat[key] = redact.has(key) ? '[REDACTED]' : (typeof value === 'string' ? value : String(value));
|
|
98
145
|
}
|
|
99
146
|
|
|
100
|
-
queue.push({ event, time: (time ?? new Date()).toISOString(), props: flat });
|
|
147
|
+
queue.push({ id: randomEventId(), event, time: (time ?? new Date()).toISOString(), props: flat, attempts: 0 });
|
|
101
148
|
|
|
102
149
|
const maxQueue = options.maxQueue ?? DEFAULT_MAX_QUEUE;
|
|
103
|
-
if (queue.length > maxQueue)
|
|
150
|
+
if (queue.length > maxQueue) {
|
|
151
|
+
const dropped = queue.length - maxQueue;
|
|
152
|
+
queue = queue.slice(dropped);
|
|
153
|
+
options.onDropped?.(dropped, 'queue-full');
|
|
154
|
+
}
|
|
155
|
+
persistQueue();
|
|
104
156
|
|
|
105
157
|
if (queue.length >= (options.maxBatch ?? DEFAULT_MAX_BATCH)) void flushEvents();
|
|
106
158
|
}
|
|
@@ -113,9 +165,10 @@ export function track(event: string, props?: TrackedEventInput['props'], time?:
|
|
|
113
165
|
* after launch, when the first events of a session are the ones a funnel needs most.
|
|
114
166
|
*/
|
|
115
167
|
export async function flushEvents(): Promise<number> {
|
|
116
|
-
if (!options || sending || queue.length === 0) return 0;
|
|
168
|
+
if (!options || sending || queue.length === 0 || Date.now() < nextAttemptAt) return 0;
|
|
117
169
|
|
|
118
170
|
const opts = options;
|
|
171
|
+
const requestGeneration = trackingGeneration;
|
|
119
172
|
const ctx = opts.getContext?.() ?? {};
|
|
120
173
|
|
|
121
174
|
// Without a device id the server cannot put the event in an arm, and an empty id
|
|
@@ -125,8 +178,14 @@ export async function flushEvents(): Promise<number> {
|
|
|
125
178
|
|
|
126
179
|
const batch = queue.slice(0, opts.maxBatch ?? DEFAULT_MAX_BATCH);
|
|
127
180
|
queue = queue.slice(batch.length);
|
|
181
|
+
persistQueue();
|
|
128
182
|
sending = true;
|
|
129
183
|
|
|
184
|
+
const controller = typeof AbortController !== 'undefined' ? new AbortController() : null;
|
|
185
|
+
const timeout = controller
|
|
186
|
+
? setTimeout(() => controller.abort(), opts.timeoutMs ?? DEFAULT_TIMEOUT_MS)
|
|
187
|
+
: null;
|
|
188
|
+
|
|
130
189
|
try {
|
|
131
190
|
const response = await fetch(trackUrl(opts.url), {
|
|
132
191
|
method: 'POST',
|
|
@@ -136,26 +195,36 @@ export async function flushEvents(): Promise<number> {
|
|
|
136
195
|
userId: ctx.userId,
|
|
137
196
|
platform: ctx.platform,
|
|
138
197
|
appVersion: ctx.appVersion,
|
|
139
|
-
events: batch.map((e) => ({ event: e.event, time: e.time, props: e.props })),
|
|
198
|
+
events: batch.map((e) => ({ id: e.id, event: e.event, time: e.time, props: e.props })),
|
|
140
199
|
}),
|
|
200
|
+
signal: controller?.signal,
|
|
141
201
|
});
|
|
142
202
|
|
|
143
203
|
if (!response.ok) {
|
|
144
204
|
// Rate limited or briefly down: keep the events, try on the next tick.
|
|
145
|
-
|
|
205
|
+
if (requestGeneration === trackingGeneration && options === opts) {
|
|
206
|
+
requeueWithBackoff(batch, response.status);
|
|
207
|
+
} else {
|
|
208
|
+
restoreStaleBatch(opts, batch, response.status);
|
|
209
|
+
}
|
|
146
210
|
if (response.status !== 429) opts.onError?.(new Error(`Pulse track failed: ${response.status}`));
|
|
147
211
|
return 0;
|
|
148
212
|
}
|
|
149
213
|
|
|
150
214
|
const body = (await response.json()) as { accepted?: number; ignored?: string[] };
|
|
215
|
+
if (requestGeneration !== trackingGeneration || options !== opts) return body.accepted ?? 0;
|
|
151
216
|
if (body.ignored && body.ignored.length > 0) opts.onIgnored?.(body.ignored);
|
|
217
|
+
nextAttemptAt = 0;
|
|
218
|
+
persistQueue();
|
|
152
219
|
return body.accepted ?? 0;
|
|
153
220
|
} catch (error) {
|
|
154
|
-
|
|
221
|
+
if (requestGeneration === trackingGeneration && options === opts) requeueWithBackoff(batch);
|
|
222
|
+
else restoreStaleBatch(opts, batch);
|
|
155
223
|
opts.onError?.(error);
|
|
156
224
|
return 0;
|
|
157
225
|
} finally {
|
|
158
|
-
|
|
226
|
+
if (timeout) clearTimeout(timeout);
|
|
227
|
+
if (requestGeneration === trackingGeneration) sending = false;
|
|
159
228
|
}
|
|
160
229
|
}
|
|
161
230
|
|
|
@@ -164,14 +233,32 @@ export function pendingEventCount(): number {
|
|
|
164
233
|
return queue.length;
|
|
165
234
|
}
|
|
166
235
|
|
|
236
|
+
export function getTrackingInfo(): {
|
|
237
|
+
queueDepth: number;
|
|
238
|
+
sending: boolean;
|
|
239
|
+
nextAttemptAt: number;
|
|
240
|
+
} {
|
|
241
|
+
return { queueDepth: queue.length, sending, nextAttemptAt };
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/** Stop timers and listeners while preserving the durable outbox. */
|
|
245
|
+
export function disposeTracking(): void {
|
|
246
|
+
trackingGeneration += 1;
|
|
247
|
+
if (timer) clearInterval(timer);
|
|
248
|
+
timer = null;
|
|
249
|
+
lifecycleSubscription?.remove();
|
|
250
|
+
lifecycleSubscription = null;
|
|
251
|
+
persistQueue();
|
|
252
|
+
options = null;
|
|
253
|
+
sending = false;
|
|
254
|
+
}
|
|
255
|
+
|
|
167
256
|
export function stopTracking(): void {
|
|
168
|
-
|
|
169
|
-
clearInterval(timer);
|
|
170
|
-
timer = null;
|
|
171
|
-
}
|
|
257
|
+
disposeTracking();
|
|
172
258
|
options = null;
|
|
173
259
|
queue = [];
|
|
174
260
|
sending = false;
|
|
261
|
+
nextAttemptAt = 0;
|
|
175
262
|
}
|
|
176
263
|
|
|
177
264
|
function startTimer(): void {
|
|
@@ -182,3 +269,94 @@ function startTimer(): void {
|
|
|
182
269
|
// and a CLI importing this should still be able to exit.
|
|
183
270
|
(timer as unknown as { unref?: () => void }).unref?.();
|
|
184
271
|
}
|
|
272
|
+
|
|
273
|
+
function requeueWithBackoff(batch: QueuedEvent[], status?: number): void {
|
|
274
|
+
if (!options) return;
|
|
275
|
+
const maxAttempts = options.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
276
|
+
const retryable: QueuedEvent[] = [];
|
|
277
|
+
let dropped = 0;
|
|
278
|
+
for (const event of batch) {
|
|
279
|
+
const next = { ...event, attempts: event.attempts + 1 };
|
|
280
|
+
if (next.attempts >= maxAttempts && status !== 429) dropped++;
|
|
281
|
+
else retryable.push(next);
|
|
282
|
+
}
|
|
283
|
+
queue = [...retryable, ...queue];
|
|
284
|
+
if (dropped > 0) options.onDropped?.(dropped, 'max-attempts');
|
|
285
|
+
const attempts = retryable.reduce((max, event) => Math.max(max, event.attempts), 1);
|
|
286
|
+
const base = Math.min(60_000, 1_000 * 2 ** Math.min(attempts - 1, 6));
|
|
287
|
+
nextAttemptAt = Date.now() + base + Math.floor(Math.random() * Math.max(1, base / 4));
|
|
288
|
+
persistQueue();
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* An old request may fail after another app has been configured. Its rows belong in
|
|
293
|
+
* the old app's outbox, never in the new app's in-memory queue. When the same app was
|
|
294
|
+
* merely reconfigured, merge them back into the live queue so it need not restart to
|
|
295
|
+
* see them; otherwise persist directly under the captured app-scoped key.
|
|
296
|
+
*/
|
|
297
|
+
function restoreStaleBatch(opts: TrackOptions, batch: QueuedEvent[], status?: number): void {
|
|
298
|
+
const maxAttempts = opts.maxAttempts ?? DEFAULT_MAX_ATTEMPTS;
|
|
299
|
+
const retryable: QueuedEvent[] = [];
|
|
300
|
+
let dropped = 0;
|
|
301
|
+
for (const event of batch) {
|
|
302
|
+
const next = { ...event, attempts: event.attempts + 1 };
|
|
303
|
+
if (next.attempts >= maxAttempts && status !== 429) dropped++;
|
|
304
|
+
else retryable.push(next);
|
|
305
|
+
}
|
|
306
|
+
if (dropped > 0) opts.onDropped?.(dropped, 'max-attempts');
|
|
307
|
+
if (retryable.length === 0) return;
|
|
308
|
+
|
|
309
|
+
const currentOptions = options;
|
|
310
|
+
const sameOutbox = currentOptions?.storage === opts.storage &&
|
|
311
|
+
currentOptions !== null && storageKey(currentOptions) === storageKey(opts);
|
|
312
|
+
if (sameOutbox) {
|
|
313
|
+
queue = [...retryable, ...queue].slice(-(options?.maxQueue ?? DEFAULT_MAX_QUEUE));
|
|
314
|
+
persistQueue();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
if (!opts.storage) return;
|
|
319
|
+
try {
|
|
320
|
+
const stored = readQueue(opts);
|
|
321
|
+
const restored = [...retryable, ...stored].slice(-(opts.maxQueue ?? DEFAULT_MAX_QUEUE));
|
|
322
|
+
opts.storage.set(storageKey(opts), JSON.stringify(restored));
|
|
323
|
+
} catch {
|
|
324
|
+
// Best effort only; a telemetry recovery must not affect the new app session.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function storageKey(opts: TrackOptions): string {
|
|
329
|
+
return opts.storageKey?.trim() || DEFAULT_STORAGE_KEY;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function readQueue(opts: TrackOptions): QueuedEvent[] {
|
|
333
|
+
if (!opts.storage) return [];
|
|
334
|
+
try {
|
|
335
|
+
const raw = opts.storage.getString(storageKey(opts));
|
|
336
|
+
if (!raw) return [];
|
|
337
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
338
|
+
if (!Array.isArray(parsed)) return [];
|
|
339
|
+
return parsed.filter((item): item is QueuedEvent => Boolean(
|
|
340
|
+
item && typeof item === 'object' && typeof item.id === 'string' &&
|
|
341
|
+
typeof item.event === 'string' && typeof item.time === 'string' &&
|
|
342
|
+
typeof item.props === 'object' && typeof item.attempts === 'number',
|
|
343
|
+
)).slice(-(opts.maxQueue ?? DEFAULT_MAX_QUEUE));
|
|
344
|
+
} catch {
|
|
345
|
+
return [];
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function persistQueue(): void {
|
|
350
|
+
if (!options?.storage) return;
|
|
351
|
+
try {
|
|
352
|
+
options.storage.set(storageKey(options), JSON.stringify(queue));
|
|
353
|
+
} catch {
|
|
354
|
+
// Telemetry persistence must never become an app failure.
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function randomEventId(): string {
|
|
359
|
+
const cryptoLike = globalThis.crypto as { randomUUID?: () => string } | undefined;
|
|
360
|
+
if (typeof cryptoLike?.randomUUID === 'function') return cryptoLike.randomUUID();
|
|
361
|
+
return `evt-${Date.now()}-${Math.random().toString(36).slice(2, 14)}`;
|
|
362
|
+
}
|
package/src/usePulseUpdates.ts
CHANGED
|
@@ -27,6 +27,7 @@ export interface UsePulseUpdatesResult {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export function usePulseUpdates(): UsePulseUpdatesResult {
|
|
30
|
+
const [nativeState, setNativeState] = useState(PulseUpdates.getUpdatesState);
|
|
30
31
|
const [isChecking, setIsChecking] = useState(false);
|
|
31
32
|
const [isDownloading, setIsDownloading] = useState(false);
|
|
32
33
|
const [availableUpdate, setAvailableUpdate] = useState<PulseManifest | null>(null);
|
|
@@ -35,11 +36,15 @@ export function usePulseUpdates(): UsePulseUpdatesResult {
|
|
|
35
36
|
|
|
36
37
|
// Refresh state on mount to get latest values after configure
|
|
37
38
|
useEffect(() => {
|
|
38
|
-
PulseUpdates.
|
|
39
|
+
const unsubscribe = PulseUpdates.onUpdatesStateChange(() => {
|
|
40
|
+
setNativeState(PulseUpdates.getUpdatesState());
|
|
41
|
+
});
|
|
42
|
+
void PulseUpdates.refreshStateAsync();
|
|
43
|
+
return unsubscribe;
|
|
39
44
|
}, []);
|
|
40
45
|
|
|
41
46
|
useEffect(() => {
|
|
42
|
-
if (!
|
|
47
|
+
if (!nativeState.isEnabled) return;
|
|
43
48
|
|
|
44
49
|
const subscription = PulseUpdates.addUpdateListener((event) => {
|
|
45
50
|
switch (event.type) {
|
|
@@ -59,10 +64,10 @@ export function usePulseUpdates(): UsePulseUpdatesResult {
|
|
|
59
64
|
});
|
|
60
65
|
|
|
61
66
|
return () => subscription.remove();
|
|
62
|
-
}, []);
|
|
67
|
+
}, [nativeState.isEnabled]);
|
|
63
68
|
|
|
64
69
|
const checkForUpdate = useCallback(async (): Promise<UpdateCheckResult> => {
|
|
65
|
-
if (!
|
|
70
|
+
if (!nativeState.isEnabled) {
|
|
66
71
|
return { isAvailable: false };
|
|
67
72
|
}
|
|
68
73
|
|
|
@@ -84,10 +89,10 @@ export function usePulseUpdates(): UsePulseUpdatesResult {
|
|
|
84
89
|
} finally {
|
|
85
90
|
setIsChecking(false);
|
|
86
91
|
}
|
|
87
|
-
}, []);
|
|
92
|
+
}, [nativeState.isEnabled]);
|
|
88
93
|
|
|
89
94
|
const downloadUpdate = useCallback(async (): Promise<UpdateFetchResult> => {
|
|
90
|
-
if (!
|
|
95
|
+
if (!nativeState.isEnabled) {
|
|
91
96
|
return { isNew: false };
|
|
92
97
|
}
|
|
93
98
|
|
|
@@ -107,23 +112,23 @@ export function usePulseUpdates(): UsePulseUpdatesResult {
|
|
|
107
112
|
} finally {
|
|
108
113
|
setIsDownloading(false);
|
|
109
114
|
}
|
|
110
|
-
}, []);
|
|
115
|
+
}, [nativeState.isEnabled]);
|
|
111
116
|
|
|
112
117
|
const reload = useCallback(async (): Promise<void> => {
|
|
113
|
-
if (!
|
|
118
|
+
if (!nativeState.isEnabled) return;
|
|
114
119
|
await PulseUpdates.reloadAsync();
|
|
115
|
-
}, []);
|
|
120
|
+
}, [nativeState.isEnabled]);
|
|
116
121
|
|
|
117
122
|
return {
|
|
118
|
-
isEnabled:
|
|
123
|
+
isEnabled: nativeState.isEnabled,
|
|
119
124
|
isChecking,
|
|
120
125
|
isDownloading,
|
|
121
|
-
updateId:
|
|
122
|
-
runtimeVersion:
|
|
123
|
-
channel:
|
|
124
|
-
manifest:
|
|
125
|
-
isEmbeddedLaunch:
|
|
126
|
-
createdAt:
|
|
126
|
+
updateId: nativeState.updateId,
|
|
127
|
+
runtimeVersion: nativeState.runtimeVersion,
|
|
128
|
+
channel: nativeState.channel,
|
|
129
|
+
manifest: nativeState.manifest,
|
|
130
|
+
isEmbeddedLaunch: nativeState.isEmbeddedLaunch,
|
|
131
|
+
createdAt: nativeState.createdAt,
|
|
127
132
|
build: PulseUpdates.getUpdateBuild(),
|
|
128
133
|
availableUpdate,
|
|
129
134
|
downloadedUpdate,
|