@damn-coding/geopulse-react-native 0.0.0-stage → 0.5.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.
@@ -0,0 +1,702 @@
1
+ import React from 'react';
2
+
3
+ declare const GEOFENCE_EVENT_TYPES: readonly ["geofence.enter", "geofence.exit"];
4
+ type GeofenceEventType = (typeof GEOFENCE_EVENT_TYPES)[number];
5
+ declare const DEVICE_PLATFORMS: readonly ["ios", "android", "web", "unknown"];
6
+ type DevicePlatform = (typeof DEVICE_PLATFORMS)[number];
7
+ declare const EXPERIENCE_TYPES: readonly ["banner", "card", "message", "alert"];
8
+ type ExperienceType = (typeof EXPERIENCE_TYPES)[number];
9
+ declare const ANCHOR_POINTS: readonly ["top-left", "top-center", "top-right", "center-left", "center", "center-right", "bottom-left", "bottom-center", "bottom-right"];
10
+ type AnchorPoint = (typeof ANCHOR_POINTS)[number];
11
+ interface AnchoredPosition {
12
+ anchor: AnchorPoint;
13
+ offsetX?: number | null;
14
+ offsetY?: number | null;
15
+ }
16
+ interface ContainerStyle {
17
+ backgroundColor?: string | null;
18
+ borderColor?: string | null;
19
+ borderWidth?: number | null;
20
+ borderRadius?: number | null;
21
+ padding?: number | null;
22
+ backgroundImageUrl?: string | null;
23
+ backgroundImageFit?: "cover" | "contain" | "stretch";
24
+ widthMode?: "fill" | "fixed";
25
+ width?: number | null;
26
+ heightMode?: "auto" | "fixed";
27
+ height?: number | null;
28
+ }
29
+ interface TextStyle {
30
+ visible?: boolean;
31
+ color?: string | null;
32
+ fontSize?: number | null;
33
+ fontWeight?: "normal" | "bold";
34
+ }
35
+ interface ButtonStyle {
36
+ visible?: boolean;
37
+ backgroundColor?: string | null;
38
+ textColor?: string | null;
39
+ borderRadius?: number | null;
40
+ position?: AnchoredPosition | null;
41
+ }
42
+ interface ExperienceStyleV2 {
43
+ version: 2;
44
+ container?: ContainerStyle;
45
+ title?: TextStyle;
46
+ body?: TextStyle;
47
+ button?: ButtonStyle;
48
+ }
49
+ interface ExperienceStyleLegacy {
50
+ backgroundColor: string | null;
51
+ textColor: string | null;
52
+ accentColor: string | null;
53
+ }
54
+ type ExperienceStyle = ExperienceStyleV2 | ExperienceStyleLegacy;
55
+ interface GeofenceCenter {
56
+ latitude: number;
57
+ longitude: number;
58
+ }
59
+ interface SdkGeofenceConfig {
60
+ id: string;
61
+ name: string;
62
+ locationId: string;
63
+ locationName: string;
64
+ center: GeofenceCenter;
65
+ radiusMeters: number;
66
+ }
67
+ interface SdkProjectConfig {
68
+ applicationId: string;
69
+ projectId: string;
70
+ applicationEnabled: boolean;
71
+ configVersion: string;
72
+ geofences: SdkGeofenceConfig[];
73
+ }
74
+ interface LocationEventPayload {
75
+ id: string;
76
+ type: GeofenceEventType;
77
+ projectId: string;
78
+ applicationId: string;
79
+ geofenceId: string;
80
+ locationId: string;
81
+ deviceId: string;
82
+ timestamp: string;
83
+ latitude: number;
84
+ longitude: number;
85
+ metadata: Record<string, string>;
86
+ sdkVersion: string;
87
+ attempts: number;
88
+ }
89
+ interface ExperienceDeliveryPayload {
90
+ type: "location.experience";
91
+ deliveryId: string;
92
+ campaignId: string;
93
+ /** Changes when the experience's content is edited; absent on older backends. */
94
+ revision?: number;
95
+ /** Set by the SDK, never the backend: true when this replaces content the user was already shown for the same delivery. */
96
+ isUpdate?: boolean;
97
+ experience: {
98
+ id: string;
99
+ type: ExperienceType;
100
+ title: string;
101
+ body: string;
102
+ imageUrl: string | null;
103
+ cardNavigationUrl: string | null;
104
+ ctaLabel: string | null;
105
+ ctaUrl: string | null;
106
+ style: ExperienceStyle | null;
107
+ };
108
+ event: {
109
+ id: string;
110
+ type: GeofenceEventType;
111
+ geofenceId: string;
112
+ timestamp: string;
113
+ };
114
+ }
115
+ /** A delivery the user was already shown that the app must now remove (its campaign was deleted or disabled). */
116
+ interface RevokedExperience {
117
+ deliveryId: string;
118
+ campaignId: string;
119
+ }
120
+ /** Response body of `getPendingExperiences`; `revoked` is absent on older backends. */
121
+ interface PendingExperiencesResponse {
122
+ experiences: ExperienceDeliveryPayload[];
123
+ revoked?: RevokedExperience[];
124
+ }
125
+
126
+ interface GeoPulseConfig {
127
+ apiKey: string;
128
+ applicationId: string;
129
+ /**
130
+ * Internal override for the GeoPulse backend URL — only for GeoPulse's own
131
+ * local development (e.g. apps/playground against a local backend).
132
+ * Customers never set this; it defaults to `DEFAULT_BACKEND_URL`.
133
+ */
134
+ functionsBaseUrl?: string;
135
+ /** How often to retry queued (previously failed) events, in milliseconds. Defaults to 30s. */
136
+ retryIntervalMs?: number;
137
+ /**
138
+ * Fallback interval for polling pending campaign experiences, in milliseconds. Defaults to 15 minutes.
139
+ * Experiences are normally fetched right after an event the backend says created one, once on
140
+ * `start()`, and when the server pushes a change to a phone inside a geofence, so this only catches anything missed. Lower it only if you need that safety net faster.
141
+ */
142
+ experiencePollIntervalMs?: number;
143
+ /** Enables the lifecycle/event debug log lines described in this package's README. Defaults to `__DEV__`. Never logs the SDK key or event payload bodies. */
144
+ debug?: boolean;
145
+ }
146
+
147
+ /**
148
+ * Minimal persistent key-value abstraction, shared by device-id.ts and
149
+ * config-cache.ts — deliberately smaller than `storage/EventQueueStore`
150
+ * (which manages a whole collection) since these two only ever need "get
151
+ * one string" / "set one string".
152
+ */
153
+ interface KeyValueStore {
154
+ getItem(key: string): Promise<string | null>;
155
+ setItem(key: string, value: string): Promise<void>;
156
+ }
157
+
158
+ interface DeviceIdentity {
159
+ deviceId: string;
160
+ platform: DevicePlatform;
161
+ sdkVersion: string;
162
+ }
163
+ interface IngestResult {
164
+ id: string;
165
+ deduplicated: boolean;
166
+ /** False when the backend created no experience for this event, so polling for one would find nothing. Absent on older backends — treat as "maybe". */
167
+ experiencesPending?: boolean;
168
+ }
169
+ /**
170
+ * Named `firebase/` per this project's spec ("communication with GeoPulse
171
+ * backend"), but deliberately does NOT use the Firebase client SDK — it
172
+ * talks to the Cloud Functions HTTPS endpoints over plain `fetch` (built
173
+ * into both React Native's JS runtime and Node 18+, so this file is
174
+ * testable without any RN runtime at all). That keeps the SDK's dependency
175
+ * footprint small and means device requests carry only the app-scoped SDK
176
+ * key, never a Firebase credential of any kind.
177
+ */
178
+ declare class ApiClient {
179
+ private readonly baseUrl;
180
+ private readonly sdkKey;
181
+ /** Optional so existing callers keep working; when set, the backend rejects a key bound to a different application. */
182
+ private readonly applicationId?;
183
+ constructor(baseUrl: string, sdkKey: string,
184
+ /** Optional so existing callers keep working; when set, the backend rejects a key bound to a different application. */
185
+ applicationId?: string | undefined);
186
+ private headers;
187
+ /**
188
+ * `identity` doubles as this device's registration/heartbeat (see
189
+ * functions/src/events/get-project-config.ts) — sent as query params
190
+ * since GET requests carry no body.
191
+ */
192
+ getProjectConfig(identity: DeviceIdentity): Promise<SdkProjectConfig>;
193
+ ingestEvent(payload: LocationEventPayload): Promise<IngestResult>;
194
+ /** Polled by GeoPulseSdk's experience-polling loop — see core/geo-pulse-sdk.ts. */
195
+ getPendingExperiences(deviceId: string): Promise<Required<PendingExperiencesResponse>>;
196
+ /** Confirms a delivery was surfaced via `experience.received`, so it stops appearing in future polls. */
197
+ acknowledgeExperienceDelivery(deliveryId: string): Promise<void>;
198
+ }
199
+
200
+ interface StateMachineOptions {
201
+ /** How far past the edge a position must be before an exit counts (GPS jitter at the boundary). Default 20 m, or the fix's accuracy if larger. */
202
+ exitBufferMeters?: number;
203
+ /** Fixes less accurate than this are ignored: they cannot tell inside from outside. Default 100 m. */
204
+ maxAccuracyMeters?: number;
205
+ /** Minimum time between two transitions of the same geofence. Default 2 minutes. */
206
+ cooldownMs?: number;
207
+ now?: () => number;
208
+ }
209
+
210
+ /**
211
+ * Structured, easy-to-disable debug logging (spec section 22). Every call
212
+ * site passes a short lifecycle label matching the examples in this
213
+ * package's README ("Configuration synced", "Event queued", ...) plus
214
+ * optional structured details — never the SDK key, never a raw event
215
+ * payload body, never anything from `GeoPulseConfig` beyond `applicationId`.
216
+ */
217
+ declare class Logger {
218
+ private enabled;
219
+ constructor(enabled: boolean);
220
+ setEnabled(enabled: boolean): void;
221
+ log(message: string, details?: Record<string, unknown>): void;
222
+ warn(message: string, details?: Record<string, unknown>): void;
223
+ }
224
+
225
+ /** The part of a WebSocket the client uses; React Native's built-in `WebSocket` satisfies it, and tests pass a fake. */
226
+ interface RealtimeSocket {
227
+ onopen: ((event: unknown) => void) | null;
228
+ onmessage: ((event: {
229
+ data: unknown;
230
+ }) => void) | null;
231
+ onclose: ((event: {
232
+ code: number;
233
+ }) => void) | null;
234
+ onerror: ((event: unknown) => void) | null;
235
+ send(data: string): void;
236
+ close(code?: number, reason?: string): void;
237
+ }
238
+ interface RealtimeOptions {
239
+ /** Backend base URL (https://...). The socket goes to the same host over wss://. */
240
+ baseUrl: string;
241
+ sdkKey: string;
242
+ applicationId: string;
243
+ deviceId: string;
244
+ /** The server says something changed for this device: fetch it. */
245
+ onChanged: () => void;
246
+ /** The server accepted the connection. Anything changed while we were disconnected is fetched now. */
247
+ onConnected: () => void;
248
+ createSocket?: (url: string) => RealtimeSocket;
249
+ logger?: Logger;
250
+ }
251
+ /** What the host app (index.ts) hands to GeoPulseSdk: just connect and disconnect. */
252
+ interface RealtimeConnection {
253
+ connect(): void;
254
+ disconnect(): void;
255
+ isConnected(): boolean;
256
+ }
257
+
258
+ interface LocationUpdate {
259
+ latitude: number;
260
+ longitude: number;
261
+ timestamp: string;
262
+ /** Horizontal accuracy radius in meters, when the provider knows it. A big value means the fix cannot be trusted near a geofence edge. */
263
+ accuracy?: number;
264
+ }
265
+ /**
266
+ * The SDK's own abstraction over "something that produces location
267
+ * updates" — deliberately small (start/stop) so both the mandatory
268
+ * simulator and a real device provider implement it identically, and the
269
+ * core lifecycle (../core) never needs to know which one it's driving.
270
+ */
271
+ interface LocationProvider {
272
+ /**
273
+ * `onError` covers a provider that fails to start at all (e.g. a real
274
+ * device with location services turned off) — implementations must catch
275
+ * their own internal failures and report them here rather than letting an
276
+ * exception escape `start()` unhandled (see `DeviceLocationProvider` for
277
+ * why: an uncaught rejection from a native module is exactly the kind of
278
+ * thing that shouldn't be able to crash the host app).
279
+ */
280
+ start(onUpdate: (location: LocationUpdate) => void, onError?: (error: Error) => void): Promise<void> | void;
281
+ stop(): void;
282
+ }
283
+
284
+ type PermissionStatus = "granted" | "denied" | "undetermined";
285
+ interface PermissionProvider {
286
+ getStatus(): Promise<PermissionStatus>;
287
+ request(): Promise<PermissionStatus>;
288
+ }
289
+
290
+ /**
291
+ * "sending" is a short-lived marker set immediately before an upload
292
+ * attempt and cleared right after (success removes the record entirely;
293
+ * failure moves it back to "pending"/"failed") — it exists specifically so
294
+ * two overlapping retry passes (e.g. a manual retry racing the timer) don't
295
+ * both attempt the same event at once. There is no "delivered" status
296
+ * stored here: a delivered event is removed from the queue, not
297
+ * transitioned — see PendingEventQueue's class doc for why.
298
+ */
299
+ declare const QUEUED_EVENT_STATUSES: readonly ["pending", "sending", "failed"];
300
+ type QueuedEventStatus = (typeof QUEUED_EVENT_STATUSES)[number];
301
+ interface QueuedEvent {
302
+ eventId: string;
303
+ /** Flattened copies of the corresponding `payload` fields, purely so diagnostics/inspection don't need to unwrap `payload` for the common case. */
304
+ type: GeofenceEventType;
305
+ applicationId: string;
306
+ deviceId: string;
307
+ locationId: string;
308
+ geofenceId: string;
309
+ timestamp: string;
310
+ payload: LocationEventPayload;
311
+ attempts: number;
312
+ status: QueuedEventStatus;
313
+ createdAt: string;
314
+ lastAttemptAt: string | null;
315
+ /** Backoff scheduling — retryPending() skips any event whose nextAttemptAt is still in the future. Null means "ready now" (never attempted, or immediately retryable). */
316
+ nextAttemptAt: string | null;
317
+ }
318
+ /**
319
+ * Storage abstraction so the queue's retry/dedupe logic (storage/queue.ts)
320
+ * is testable in plain Node — `InMemoryEventQueueStore` for tests and as a
321
+ * safe default, `AsyncStorageEventQueueStore` for the real RN app. Kept
322
+ * intentionally minimal (get/save/remove over the whole set) so a future
323
+ * SQLite-backed store is a drop-in replacement without touching
324
+ * `PendingEventQueue`'s logic — see this package's README.
325
+ */
326
+ interface EventQueueStore {
327
+ getAll(): Promise<QueuedEvent[]>;
328
+ save(event: QueuedEvent): Promise<void>;
329
+ remove(eventId: string): Promise<void>;
330
+ }
331
+
332
+ /**
333
+ * The local pending-event queue (spec: "Offline queue" / "reliable SDK
334
+ * event queue"). An event is only ever in this queue between a failed send
335
+ * attempt and a successful retry — there's no "delivered" status because a
336
+ * delivered event is simply removed (`markSent`), which is also what makes
337
+ * retrying an already-sent event harmless: it's not in the queue to retry.
338
+ *
339
+ * Every operation runs one at a time. The store keeps the whole queue in one value, so each change is read, modify,
340
+ * write; two overlapping ones (a retry finishing while a new event is queued) would otherwise overwrite each other and
341
+ * lose an event.
342
+ */
343
+ declare class PendingEventQueue {
344
+ private readonly store;
345
+ private chain;
346
+ constructor(store?: EventQueueStore);
347
+ private serialized;
348
+ enqueue(payload: LocationEventPayload): Promise<void>;
349
+ /** Every queued event, regardless of retry timing — used for diagnostics/pending counts. */
350
+ listPending(): Promise<QueuedEvent[]>;
351
+ /** Only events whose backoff window has elapsed (or that have never been attempted) — used by SyncManager.retryPending(). Drops events too old to ever be accepted. */
352
+ listReadyForRetry(now?: Date): Promise<QueuedEvent[]>;
353
+ /** Marks an event as in-flight so a concurrent retry pass won't also pick it up. */
354
+ markSending(eventId: string): Promise<void>;
355
+ /** `minDelayMs` lets a 429's Retry-After push the next attempt later than the normal backoff. */
356
+ markAttemptFailed(eventId: string, minDelayMs?: number): Promise<void>;
357
+ markSent(eventId: string): Promise<void>;
358
+ private get;
359
+ /** Keeps the queue within MAX_QUEUE_SIZE by dropping the oldest events that are not being sent right now. */
360
+ private trim;
361
+ }
362
+
363
+ type NetworkStatus = "online" | "offline" | "unknown";
364
+ interface NetworkMonitor {
365
+ getStatus(): NetworkStatus;
366
+ /** Calls `listener` whenever connectivity changes. Returns an unsubscribe function. */
367
+ subscribe(listener: (status: NetworkStatus) => void): () => void;
368
+ }
369
+ /**
370
+ * Default/test implementation — starts "online" (optimistic default: if we
371
+ * can't observe connectivity, assume the network call itself is the
372
+ * ultimate source of truth rather than blocking on a monitor). Tests (and
373
+ * the Node/non-RN environment generally) drive it explicitly via
374
+ * `setStatus`; see `sync/netinfo-network-monitor.ts` for the real RN
375
+ * implementation, kept in its own file for the same reason
376
+ * `storage/async-storage-store.ts` is — see that file's comment.
377
+ */
378
+ declare class ManualNetworkMonitor implements NetworkMonitor {
379
+ private status;
380
+ private readonly listeners;
381
+ getStatus(): NetworkStatus;
382
+ setStatus(status: NetworkStatus): void;
383
+ subscribe(listener: (status: NetworkStatus) => void): () => void;
384
+ }
385
+
386
+ type SdkStatus = "idle" | "initializing" | "initialized" | "running" | "stopped" | "error";
387
+ type ConfigurationStatus = "unsynced" | "synced" | "cached" | "unavailable";
388
+ /**
389
+ * `GeoPulse.getDiagnostics()` — a superset of `getStatus()`, purpose-built
390
+ * for a developer-facing status screen (spec section 10/11) rather than
391
+ * for the SDK's own internal decisions, which is why this is a separate
392
+ * method instead of changing what `getStatus()` returns: the existing
393
+ * playground code (and any Phase 2 integration) already depends on
394
+ * `getStatus()` returning the plain `SdkStatus` string.
395
+ */
396
+ interface DiagnosticsSnapshot {
397
+ initialized: boolean;
398
+ running: boolean;
399
+ locationAvailable: boolean;
400
+ permissionStatus: PermissionStatus | "unknown";
401
+ networkStatus: "online" | "offline" | "unknown";
402
+ configurationStatus: ConfigurationStatus;
403
+ configVersion: string | null;
404
+ pendingEvents: number;
405
+ /** Sum of `attempts` across every currently-queued event — a rough "how much retrying is backed up" signal for a diagnostics screen. */
406
+ totalRetryAttempts: number;
407
+ /** Set when the location provider failed to start (e.g. OS location services disabled) — see LocationProvider's `onError`. Null when location started cleanly or hasn't been attempted yet. */
408
+ locationError: string | null;
409
+ lastEventAt: string | null;
410
+ lastEventType: GeofenceEventType | null;
411
+ lastSyncAt: string | null;
412
+ lastUploadResult: "success" | "failure" | null;
413
+ deviceId: string | null;
414
+ sdkVersion: string;
415
+ /** Count of distinct campaign experiences received via `experience.received` this session (Phase 5B). Resets on app restart, not persisted — purely a live "is the pipeline working" signal for a status screen. */
416
+ experiencesReceivedCount: number;
417
+ }
418
+
419
+ type EventListener = (event: LocationEventPayload) => void;
420
+ type ExperienceListener = (payload: ExperienceDeliveryPayload) => void;
421
+ type ExperienceRemovedListener = (payload: RevokedExperience) => void;
422
+ /** The SDK key resolved to a different application than `config.applicationId`. */
423
+ declare class ApplicationMismatchError extends Error {
424
+ constructor();
425
+ }
426
+ /**
427
+ * Everything the SDK talks to outside of itself, injectable so
428
+ * `GeoPulseSdk` (this whole lifecycle) is unit-testable without an RN
429
+ * runtime — tests construct it with fakes; `src/index.ts` (the real public
430
+ * entrypoint) constructs the default instance with the real RN adapters.
431
+ */
432
+ interface GeoPulseSdkDependencies {
433
+ locationProvider?: LocationProvider;
434
+ permissionProvider?: PermissionProvider;
435
+ queue?: PendingEventQueue;
436
+ createApiClient?: (config: GeoPulseConfig) => ApiClient;
437
+ deviceIdStore?: KeyValueStore;
438
+ configCacheStore?: KeyValueStore;
439
+ networkMonitor?: NetworkMonitor;
440
+ /** Defaults to "unknown" — src/index.ts supplies the real `Platform.OS` so this file stays RN-free. */
441
+ getPlatform?: () => DevicePlatform;
442
+ /** Overrides GeoPulseConfig.debug — mainly for tests that want to assert on log output. */
443
+ debug?: boolean;
444
+ /** Where the geofence state machine keeps its memory across restarts. Defaults to memory only (lost on restart). */
445
+ stateStore?: KeyValueStore;
446
+ /** Tuning for tests (cooldown, exit buffer, clock). */
447
+ stateMachineOptions?: StateMachineOptions;
448
+ /**
449
+ * Builds the push connection used while the user is inside a geofence. Not set by default: the core has no
450
+ * WebSocket dependency, and src/index.ts supplies the real one.
451
+ */
452
+ createRealtimeClient?: (options: RealtimeOptions) => RealtimeConnection;
453
+ }
454
+ declare class GeoPulseSdk {
455
+ private status;
456
+ private config;
457
+ private apiClient;
458
+ private syncManager;
459
+ private resolvedProjectId;
460
+ private resolvedApplicationId;
461
+ private applicationEnabled;
462
+ private configVersion;
463
+ private configurationStatus;
464
+ private geofences;
465
+ private lastKnownLocation;
466
+ private lastEvent;
467
+ private deviceId;
468
+ private locationError;
469
+ private unsubscribeNetwork;
470
+ private readonly stateMachine;
471
+ private readonly geofenceStateStore;
472
+ private geofenceStateRestored;
473
+ private realtime;
474
+ private readonly createRealtime;
475
+ private lastConfigFetchAt;
476
+ private configRefreshInFlight;
477
+ private readonly listeners;
478
+ private readonly experienceListeners;
479
+ /** Client-side duplicate protection (spec section 14) — belt-and-suspenders alongside the server-side ack; a delivery already acked but re-observed before the ack lands must not fire `experience.received` twice. */
480
+ private readonly seenExperienceRevisions;
481
+ private readonly experienceRemovedListeners;
482
+ private readonly removedDeliveryIds;
483
+ private experiencePollTimer;
484
+ private experiencePollInFlight;
485
+ /** Set while a poll that was refused with "too many requests" waits to be tried again. */
486
+ private pollRetryTimer;
487
+ /** Set when a poll was asked for while one was already running; that one may have queried before the new delivery existed. */
488
+ private experiencePollRequested;
489
+ private experiencesReceivedCount;
490
+ private readonly logger;
491
+ private readonly locationProvider;
492
+ private readonly permissionProvider;
493
+ private readonly queue;
494
+ private readonly buildApiClient;
495
+ private readonly deviceIdStore;
496
+ private readonly configCache;
497
+ private readonly networkMonitor;
498
+ private readonly getPlatform;
499
+ private experiencePollIntervalMs;
500
+ constructor(dependencies?: GeoPulseSdkDependencies);
501
+ /**
502
+ * Resolves this device's persistent id, then fetches (or falls back to
503
+ * cached) project configuration. Must succeed — fresh or cached — before
504
+ * `start()`; if neither is available (first-ever run with no network),
505
+ * this rethrows and `getStatus()` reports "error".
506
+ */
507
+ initialize(config: GeoPulseConfig): Promise<void>;
508
+ start(): Promise<void>;
509
+ stop(): void;
510
+ getStatus(): SdkStatus;
511
+ getPermissionStatus(): Promise<PermissionStatus>;
512
+ on(eventName: GeofenceEventType, callback: EventListener): void;
513
+ on(eventName: "experience.received", callback: ExperienceListener): void;
514
+ on(eventName: "experience.removed", callback: ExperienceRemovedListener): void;
515
+ off(eventName: GeofenceEventType, callback: EventListener): void;
516
+ off(eventName: "experience.received", callback: ExperienceListener): void;
517
+ off(eventName: "experience.removed", callback: ExperienceRemovedListener): void;
518
+ getConfiguredGeofences(): SdkGeofenceConfig[];
519
+ /** The server-resolved application id (from `getProjectConfig`), or null before `initialize()` completes. */
520
+ getApplicationId(): string | null;
521
+ getLastKnownLocation(): LocationUpdate | null;
522
+ getPendingEventCount(): Promise<number>;
523
+ /** Null until `initialize()` resolves it — persisted across app restarts (see config/device-id.ts). */
524
+ getDeviceId(): string | null;
525
+ /**
526
+ * The rich developer-diagnostics snapshot (spec section 10/11) — a
527
+ * superset of `getStatus()`, not a replacement for it (see core/types.ts's
528
+ * `DiagnosticsSnapshot` doc for why these are two separate methods).
529
+ */
530
+ getDiagnostics(): Promise<DiagnosticsSnapshot>;
531
+ /**
532
+ * Only functional when the SDK is running the simulated location
533
+ * provider (the default outside of production builds — see
534
+ * src/index.ts) and gated by `__DEV__` so it's inert in a production
535
+ * bundle even if a caller somehow held a reference to it.
536
+ */
537
+ simulateLocation(coords: {
538
+ latitude: number;
539
+ longitude: number;
540
+ }): void;
541
+ /**
542
+ * The Phase 5B delivery mechanism: poll immediately on `start()` (so a
543
+ * demo doesn't wait a full interval for the first check), then on a
544
+ * timer. A poll failure (network blip, backend down) is logged and
545
+ * swallowed — it must never crash the SDK or the host app (spec section
546
+ * 13), and the next tick tries again on its own.
547
+ */
548
+ private startExperiencePolling;
549
+ private stopExperiencePolling;
550
+ /**
551
+ * The backend says in its ingest response whether the event created an experience for this
552
+ * device. Poll right away if so (or if it doesn't say — an older backend), skip if it says
553
+ * there is nothing. Does nothing unless polling is active, so a stopped SDK never polls.
554
+ */
555
+ private handleEventDelivered;
556
+ /** One poll at a time: a second request while one is running makes it run once more afterwards, rather than overlapping. */
557
+ private pollForExperiences;
558
+ private pollForExperiencesOnce;
559
+ private schedulePollRetry;
560
+ /**
561
+ * Re-reads the geofence list when it is an hour old, so a geofence an admin added, moved, resized or removed reaches a phone
562
+ * that keeps the app open (otherwise only a restart picks it up). Runs from the poll timer, so it costs no timer of its own.
563
+ * A failure keeps the list we have. A disabled application stops the SDK.
564
+ */
565
+ private refreshConfigIfStale;
566
+ /** Keeps the push connection open exactly while the user is inside a geofence. */
567
+ private syncRealtime;
568
+ private persistGeofenceState;
569
+ private applyProjectConfig;
570
+ private handleLocationUpdate;
571
+ }
572
+
573
+ /**
574
+ * Real, foreground-only device location via expo-location. Does not
575
+ * request or use background location — see this package's README for the
576
+ * platform permission requirements and why background tracking is
577
+ * explicitly out of scope for this phase.
578
+ */
579
+ declare class DeviceLocationProvider implements LocationProvider {
580
+ private subscription;
581
+ /**
582
+ * `watchPositionAsync` rejects if location services are off at the OS
583
+ * level (distinct from the app's own permission grant, which is checked
584
+ * before this ever runs) — caught here and reported via `onError` instead
585
+ * of left to reject this method's own promise, so a caller that doesn't
586
+ * wrap `GeoPulse.start()` in a try/catch still can't end up with an
587
+ * unhandled rejection from deep inside a native module.
588
+ */
589
+ start(onUpdate: (location: LocationUpdate) => void, onError?: (error: Error) => void): Promise<void>;
590
+ stop(): void;
591
+ }
592
+
593
+ /**
594
+ * The mandatory simulation provider (spec section 7) — lets the geofence
595
+ * flow be exercised end to end without physically travelling anywhere.
596
+ * `setLocation` is the playground simulator screen's only way to move the
597
+ * "user"; everything downstream (geofence engine, event generation, sync)
598
+ * runs exactly as it would for a real device update, because both
599
+ * providers speak the same `LocationProvider` interface.
600
+ */
601
+ declare class SimulatedLocationProvider implements LocationProvider {
602
+ private onUpdate;
603
+ private currentLocation;
604
+ start(onUpdate: (location: LocationUpdate) => void): void;
605
+ stop(): void;
606
+ setLocation(coords: {
607
+ latitude: number;
608
+ longitude: number;
609
+ }): void;
610
+ getCurrentLocation(): LocationUpdate | null;
611
+ }
612
+
613
+ /**
614
+ * Foreground-only — see this package's README ("Permissions" section) for
615
+ * why background permission is explicitly out of scope for this phase.
616
+ * Requests only the foreground permission; never touches
617
+ * `requestBackgroundPermissionsAsync`.
618
+ */
619
+ declare class ExpoPermissionProvider implements PermissionProvider {
620
+ getStatus(): Promise<PermissionStatus>;
621
+ request(): Promise<PermissionStatus>;
622
+ }
623
+
624
+ /**
625
+ * Used with the simulated location provider (see ../location) — a
626
+ * simulated user obviously isn't gated by the OS location permission
627
+ * dialog, so this always reports "granted" rather than requiring a real
628
+ * device's permission state to test the geofence flow end to end.
629
+ */
630
+ declare class SimulatedPermissionProvider implements PermissionProvider {
631
+ getStatus(): Promise<PermissionStatus>;
632
+ request(): Promise<PermissionStatus>;
633
+ }
634
+
635
+ /** Real connectivity detection for the RN app. Isolated in its own file — see storage/async-storage-store.ts's comment for why. */
636
+ declare class NetInfoNetworkMonitor implements NetworkMonitor {
637
+ private status;
638
+ constructor();
639
+ getStatus(): NetworkStatus;
640
+ subscribe(listener: (status: NetworkStatus) => void): () => void;
641
+ }
642
+
643
+ type ExperienceContent = ExperienceDeliveryPayload["experience"];
644
+ declare function CardExperience({ content }: {
645
+ content: ExperienceContent;
646
+ }): React.JSX.Element;
647
+ declare function BannerExperience({ content }: {
648
+ content: ExperienceContent;
649
+ }): React.JSX.Element;
650
+ /**
651
+ * The popup type. This component only draws the content, the same as the others; showing it over the
652
+ * screen is the host app's job (put it in a React Native `Modal`, see the README), because only the app
653
+ * knows what else is on screen. Use `isAlertExperience` to decide popup versus inline.
654
+ */
655
+ declare function AlertExperience({ content }: {
656
+ content: ExperienceContent;
657
+ }): React.JSX.Element;
658
+ /** True for the one experience type meant to interrupt the user (a popup). Banner, card and message belong inline in a screen. */
659
+ declare function isAlertExperience(payload: {
660
+ experience: {
661
+ type: string;
662
+ };
663
+ }): boolean;
664
+ declare function MessageExperience({ content }: {
665
+ content: ExperienceContent;
666
+ }): React.JSX.Element;
667
+ /** Picks the right renderer by `experience.type` — the one component most host apps need. */
668
+ declare function ExperienceView({ experience }: {
669
+ experience: ExperienceContent;
670
+ }): React.JSX.Element;
671
+
672
+ /** `sdk.on`/`sdk.off` are already overloaded this way (see core/geo-pulse-sdk.ts) — this just re-states the same public shape for the plain-object `GeoPulse` singleton below. */
673
+ type OnOff = {
674
+ (eventName: GeofenceEventType, callback: (event: LocationEventPayload) => void): void;
675
+ (eventName: "experience.received", callback: (payload: ExperienceDeliveryPayload) => void): void;
676
+ (eventName: "experience.removed", callback: (payload: RevokedExperience) => void): void;
677
+ };
678
+ declare const GeoPulse: {
679
+ initialize: (config: GeoPulseConfig) => Promise<void>;
680
+ start: () => Promise<void>;
681
+ stop: () => void;
682
+ getStatus: () => SdkStatus;
683
+ getPermissionStatus: () => Promise<PermissionStatus>;
684
+ on: OnOff;
685
+ off: OnOff;
686
+ getConfiguredGeofences: () => SdkGeofenceConfig[];
687
+ getApplicationId: () => string | null;
688
+ getLastKnownLocation: () => LocationUpdate | null;
689
+ getPendingEventCount: () => Promise<number>;
690
+ getDeviceId: () => string | null;
691
+ /** Rich developer-diagnostics snapshot — see core/types.ts's DiagnosticsSnapshot doc. */
692
+ getDiagnostics: () => Promise<DiagnosticsSnapshot>;
693
+ /** Development-only. No-ops (with a console warning) in a production build — see GeoPulseSdk.simulateLocation. */
694
+ simulator: {
695
+ setLocation: (coords: {
696
+ latitude: number;
697
+ longitude: number;
698
+ }) => void;
699
+ };
700
+ };
701
+
702
+ export { AlertExperience, ApplicationMismatchError, BannerExperience, CardExperience, type ConfigurationStatus, DeviceLocationProvider, type DiagnosticsSnapshot, type ExperienceDeliveryPayload, ExperienceView, ExpoPermissionProvider, GeoPulse, type GeoPulseConfig, GeoPulseSdk, type GeofenceEventType, type LocationEventPayload, type LocationProvider, type LocationUpdate, ManualNetworkMonitor, MessageExperience, NetInfoNetworkMonitor, type NetworkMonitor, type NetworkStatus, type PermissionProvider, type PermissionStatus, type RevokedExperience, type SdkGeofenceConfig, type SdkStatus, SimulatedLocationProvider, SimulatedPermissionProvider, isAlertExperience };