@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.
- package/README.md +328 -2
- package/dist/index.d.ts +702 -0
- package/dist/index.js +1764 -0
- package/dist/index.js.map +1 -0
- package/package.json +44 -4
package/dist/index.d.ts
ADDED
|
@@ -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 };
|