@sharib/geopulse-react-native 0.3.4 → 0.4.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 +110 -15
- package/dist/index.d.ts +144 -11
- package/dist/index.js +522 -76
- package/dist/index.js.map +1 -1
- package/package.json +20 -17
package/README.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# @sharib/geopulse-react-native
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
7
|
-
>
|
|
3
|
+
> **Beta.** GeoPulse and this SDK are in beta: the API may change before 1.0, and there is no
|
|
4
|
+
> uptime guarantee yet. Questions, bugs and feature requests:
|
|
5
|
+
> [GitHub issues](https://github.com/sharibkhan1/geofends/issues). The Terms of Service and
|
|
6
|
+
> Privacy Policy are in the admin portal at `/terms` and `/privacy`; see also
|
|
7
|
+
> [Privacy and store disclosures](#privacy-and-store-disclosures) below.
|
|
8
8
|
|
|
9
9
|
On-device geofence detection and event sync for React Native / Expo apps. Fetches an
|
|
10
10
|
application's configured geofences from GeoPulse, watches location (real device or the
|
|
@@ -12,12 +12,35 @@ built-in simulator), and reports `geofence.enter` / `geofence.exit` events to th
|
|
|
12
12
|
backend, with a persistent, backoff-retried offline queue so a flaky connection never loses
|
|
13
13
|
an event.
|
|
14
14
|
|
|
15
|
+
## Requirements
|
|
16
|
+
|
|
17
|
+
- **React Native ≥ 0.73 and React ≥ 18** (peer dependencies; developed and tested against React
|
|
18
|
+
Native 0.86 / React 19.2 only, so please report problems on older versions).
|
|
19
|
+
- **Expo, or a bare React Native app with Expo modules.** Location comes from `expo-location`,
|
|
20
|
+
which needs the Expo modules runtime. In an Expo app (including Expo Go and development
|
|
21
|
+
builds) that is already there. In a bare React Native app, run `npx install-expo-modules`
|
|
22
|
+
first.
|
|
23
|
+
- Native modules the SDK relies on: `expo-location`, `@react-native-async-storage/async-storage`
|
|
24
|
+
and `@react-native-community/netinfo`. They are dependencies of the SDK, so installing the SDK
|
|
25
|
+
installs them (see Installation). Rebuild your native app after adding the SDK.
|
|
26
|
+
- Location permission (foreground only) declared in your app config. See [Permissions](#permissions).
|
|
27
|
+
- A GeoPulse account: create a project and an application in the admin portal to get your
|
|
28
|
+
`apiKey` and `applicationId`.
|
|
29
|
+
|
|
15
30
|
## Installation
|
|
16
31
|
|
|
17
32
|
```bash
|
|
18
33
|
npm install @sharib/geopulse-react-native
|
|
19
34
|
```
|
|
20
35
|
|
|
36
|
+
That is the only install command: the SDK brings the packages it needs. Two cases need one extra step:
|
|
37
|
+
|
|
38
|
+
- If `npx expo-doctor` says one of those packages is not the version your Expo SDK expects (npm installs
|
|
39
|
+
the newest allowed version when your app has none yet), run `npx expo install --fix`.
|
|
40
|
+
- In a bare React Native app without Expo, run `npx install-expo-modules` first. React Native's own
|
|
41
|
+
autolinking only links packages your app lists itself, so if a native module is reported as missing, add
|
|
42
|
+
it with `npx expo install <package>`.
|
|
43
|
+
|
|
21
44
|
## Initialization
|
|
22
45
|
|
|
23
46
|
Get `apiKey` and `applicationId` from the admin portal's `/sdk` page (the application created
|
|
@@ -34,8 +57,8 @@ await GeoPulse.initialize({
|
|
|
34
57
|
```
|
|
35
58
|
|
|
36
59
|
`initialize()` fetches the application's currently active geofences from the backend
|
|
37
|
-
(`getProjectConfig`). This is the "SDK receives project configuration" step. It throws if
|
|
38
|
-
|
|
60
|
+
(`getProjectConfig`). This is the "SDK receives project configuration" step. It throws if
|
|
61
|
+
`apiKey` or `applicationId` is missing, or if the backend call fails (bad/revoked SDK key,
|
|
39
62
|
network error, etc.). Check `GeoPulse.getStatus()` (`"error"`) if you need to react to that.
|
|
40
63
|
|
|
41
64
|
## Start / stop
|
|
@@ -71,17 +94,44 @@ associated Experience (a banner/card/message):
|
|
|
71
94
|
|
|
72
95
|
```ts
|
|
73
96
|
GeoPulse.on("experience.received", (payload) => {
|
|
74
|
-
// payload: ExperienceDeliveryPayload: { deliveryId, campaignId, experience: {...}, event: {...} }
|
|
97
|
+
// payload: ExperienceDeliveryPayload: { deliveryId, campaignId, revision, isUpdate, experience: {...}, event: {...} }
|
|
75
98
|
showBanner(payload.experience);
|
|
76
99
|
});
|
|
100
|
+
|
|
101
|
+
GeoPulse.on("experience.removed", ({ deliveryId, campaignId }) => {
|
|
102
|
+
hideBanner(deliveryId);
|
|
103
|
+
});
|
|
77
104
|
```
|
|
78
105
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
106
|
+
- `isUpdate` is `true` when an admin edited an experience the user was already shown: show the new
|
|
107
|
+
content in place of the old (use `deliveryId` to find it).
|
|
108
|
+
- `experience.removed` fires when an admin deleted or disabled the campaign while the user is still
|
|
109
|
+
inside the location: remove what you showed.
|
|
110
|
+
|
|
111
|
+
No push notifications are involved. The SDK fetches its own device's pending experiences over the
|
|
112
|
+
connection it already uses for config/ingestion: once on `start()`, right after an event the backend
|
|
113
|
+
says created one, and on a slow fallback timer (`GeoPulseConfig.experiencePollIntervalMs`, default 15
|
|
114
|
+
minutes). While the user is **inside a geofence with the app open**, the SDK also keeps one WebSocket
|
|
115
|
+
open so the server can tell it the moment an admin changes something that affects them; it closes when
|
|
116
|
+
they leave. Idle devices send no repeated requests. The SDK acknowledges each experience
|
|
117
|
+
automatically, so you never need to call anything to mark it "seen", and it never re-fires the same
|
|
118
|
+
`deliveryId` and `revision` twice. There's no `getActiveExperiences()`: the two events above are the
|
|
119
|
+
whole surface: the SDK transports and notifies, your app owns the UI. How long each kind of admin
|
|
120
|
+
change takes to arrive is in the portal's Setup Guide ("Why changes take a moment").
|
|
121
|
+
|
|
122
|
+
## How often the SDK talks to the server
|
|
123
|
+
|
|
124
|
+
It only sends an event when the user **enters or leaves** a geofence, never while they stand still.
|
|
125
|
+
To keep call volume (and your bill) low, the SDK holds itself back:
|
|
126
|
+
|
|
127
|
+
- one event per geofence per 2 minutes, at most 3 uploads a minute, whatever the user does;
|
|
128
|
+
- leaving only counts once the user is clearly outside (about 20 m past the edge, or more if the GPS fix
|
|
129
|
+
is less accurate); GPS fixes worse than 100 m are ignored;
|
|
130
|
+
- the state is saved, so reopening the app while still inside does not report a second "enter";
|
|
131
|
+
- when the server answers "too many requests" the SDK waits as long as it is told; a request the
|
|
132
|
+
server will never accept (for example a deleted geofence) is dropped instead of retried forever;
|
|
133
|
+
- the offline queue holds at most 100 events and drops anything older than 24 hours;
|
|
134
|
+
- the geofence list is re-read about once an hour while the app is open, and on every launch.
|
|
85
135
|
|
|
86
136
|
## Status / diagnostics
|
|
87
137
|
|
|
@@ -112,7 +162,10 @@ GeoPulse.simulator.setLocation({ latitude: 33.4484, longitude: -112.074 });
|
|
|
112
162
|
```
|
|
113
163
|
|
|
114
164
|
Each call runs through the exact same geofence engine and event pipeline as a real device
|
|
115
|
-
update would. The simulator is
|
|
165
|
+
update would. The one difference: while the simulator is running, the 2-minute per-geofence cooldown is off and
|
|
166
|
+
the saved "inside" state is not kept across reloads, so you can enter and exit as quickly as you like and
|
|
167
|
+
an "enter" fires again after a reload. (The backend still applies its own limits, for example a webhook rule
|
|
168
|
+
fires at most once per 2 minutes for the same device and geofence.) The simulator is a location *source*, not a shortcut that fakes an event
|
|
116
169
|
directly.
|
|
117
170
|
|
|
118
171
|
**The simulator is inert in production builds.** `GeoPulse.simulator.setLocation(...)` is a
|
|
@@ -134,6 +187,21 @@ const sdk = new GeoPulseSdk({
|
|
|
134
187
|
|
|
135
188
|
This SDK requests **foreground location only**, never background location. Concretely:
|
|
136
189
|
|
|
190
|
+
Minimal Expo `app.json` that covers both platforms:
|
|
191
|
+
|
|
192
|
+
```json
|
|
193
|
+
{
|
|
194
|
+
"expo": {
|
|
195
|
+
"plugins": [
|
|
196
|
+
["expo-location", { "locationWhenInUsePermission": "This app uses your location to detect nearby places." }]
|
|
197
|
+
],
|
|
198
|
+
"android": { "permissions": ["ACCESS_COARSE_LOCATION", "ACCESS_FINE_LOCATION"] }
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
Use your own wording for the permission text: it is what your users see, and the stores review it.
|
|
204
|
+
|
|
137
205
|
- **iOS**: `NSLocationWhenInUseUsageDescription` must be set in your app's `app.json` (or
|
|
138
206
|
`Info.plist` directly for a bare RN app); `expo-location`'s config plugin sets this from the
|
|
139
207
|
`locationWhenInUsePermission` option. No `NSLocationAlwaysAndWhenInUseUsageDescription` is
|
|
@@ -151,6 +219,33 @@ This SDK requests **foreground location only**, never background location. Concr
|
|
|
151
219
|
filter and a 5s minimum interval. That's a reasonable foreground default, not tuned for
|
|
152
220
|
high-precision or high-frequency tracking.
|
|
153
221
|
|
|
222
|
+
## Privacy and store disclosures
|
|
223
|
+
|
|
224
|
+
Your app sends its end users' location data to GeoPulse, so **you** must disclose that to them and
|
|
225
|
+
to the app stores. What the SDK sends:
|
|
226
|
+
|
|
227
|
+
- **When:** only when a user enters or exits one of your geofences, while the app is in the
|
|
228
|
+
foreground. Continuous location is processed on the device and is not uploaded.
|
|
229
|
+
- **What:** the user's precise coordinates and a timestamp at that moment, the geofence and
|
|
230
|
+
location ids, your application id, a random device id the SDK generates and stores on the
|
|
231
|
+
device (not an advertising id), the platform (`ios`/`android`) and the SDK version.
|
|
232
|
+
- **Where it goes:** your GeoPulse project. It is visible to the members of your organization in
|
|
233
|
+
the portal and is forwarded to any webhook endpoints you configure.
|
|
234
|
+
|
|
235
|
+
What that means for you:
|
|
236
|
+
|
|
237
|
+
- Your **privacy policy** must say you collect precise location and a device identifier, why, and
|
|
238
|
+
who processes it (GeoPulse acts as your service provider).
|
|
239
|
+
- **Apple App Privacy ("nutrition label"):** declare *Precise Location* and *Device ID*, collected
|
|
240
|
+
for app functionality and/or analytics, according to how you use them.
|
|
241
|
+
- **Google Play Data safety form:** declare *Approximate and Precise location* and *Device or other
|
|
242
|
+
IDs* as collected, with the purposes that apply to your app.
|
|
243
|
+
- Show a clear, honest permission prompt text (see [Permissions](#permissions)), and ask for
|
|
244
|
+
consent where the law of your users' region requires it.
|
|
245
|
+
|
|
246
|
+
This section is guidance, not legal advice: the store forms are your responsibility, so check
|
|
247
|
+
them against how your app really behaves.
|
|
248
|
+
|
|
154
249
|
## Troubleshooting
|
|
155
250
|
|
|
156
251
|
- **`initialize()` rejects with a fetch/network error**: check `functionsBaseUrl` is reachable
|
package/dist/index.d.ts
CHANGED
|
@@ -90,6 +90,10 @@ interface ExperienceDeliveryPayload {
|
|
|
90
90
|
type: "location.experience";
|
|
91
91
|
deliveryId: string;
|
|
92
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;
|
|
93
97
|
experience: {
|
|
94
98
|
id: string;
|
|
95
99
|
type: ExperienceType;
|
|
@@ -108,6 +112,16 @@ interface ExperienceDeliveryPayload {
|
|
|
108
112
|
timestamp: string;
|
|
109
113
|
};
|
|
110
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
|
+
}
|
|
111
125
|
|
|
112
126
|
interface GeoPulseConfig {
|
|
113
127
|
apiKey: string;
|
|
@@ -120,7 +134,11 @@ interface GeoPulseConfig {
|
|
|
120
134
|
functionsBaseUrl?: string;
|
|
121
135
|
/** How often to retry queued (previously failed) events, in milliseconds. Defaults to 30s. */
|
|
122
136
|
retryIntervalMs?: number;
|
|
123
|
-
/**
|
|
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
|
+
*/
|
|
124
142
|
experiencePollIntervalMs?: number;
|
|
125
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. */
|
|
126
144
|
debug?: boolean;
|
|
@@ -142,6 +160,12 @@ interface DeviceIdentity {
|
|
|
142
160
|
platform: DevicePlatform;
|
|
143
161
|
sdkVersion: string;
|
|
144
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
|
+
}
|
|
145
169
|
/**
|
|
146
170
|
* Named `firebase/` per this project's spec ("communication with GeoPulse
|
|
147
171
|
* backend"), but deliberately does NOT use the Firebase client SDK — it
|
|
@@ -166,20 +190,77 @@ declare class ApiClient {
|
|
|
166
190
|
* since GET requests carry no body.
|
|
167
191
|
*/
|
|
168
192
|
getProjectConfig(identity: DeviceIdentity): Promise<SdkProjectConfig>;
|
|
169
|
-
ingestEvent(payload: LocationEventPayload): Promise<
|
|
170
|
-
id: string;
|
|
171
|
-
deduplicated: boolean;
|
|
172
|
-
}>;
|
|
193
|
+
ingestEvent(payload: LocationEventPayload): Promise<IngestResult>;
|
|
173
194
|
/** Polled by GeoPulseSdk's experience-polling loop — see core/geo-pulse-sdk.ts. */
|
|
174
|
-
getPendingExperiences(deviceId: string): Promise<
|
|
195
|
+
getPendingExperiences(deviceId: string): Promise<Required<PendingExperiencesResponse>>;
|
|
175
196
|
/** Confirms a delivery was surfaced via `experience.received`, so it stops appearing in future polls. */
|
|
176
197
|
acknowledgeExperienceDelivery(deliveryId: string): Promise<void>;
|
|
177
198
|
}
|
|
178
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
|
+
|
|
179
258
|
interface LocationUpdate {
|
|
180
259
|
latitude: number;
|
|
181
260
|
longitude: number;
|
|
182
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;
|
|
183
264
|
}
|
|
184
265
|
/**
|
|
185
266
|
* The SDK's own abstraction over "something that produces location
|
|
@@ -254,20 +335,29 @@ interface EventQueueStore {
|
|
|
254
335
|
* attempt and a successful retry — there's no "delivered" status because a
|
|
255
336
|
* delivered event is simply removed (`markSent`), which is also what makes
|
|
256
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.
|
|
257
342
|
*/
|
|
258
343
|
declare class PendingEventQueue {
|
|
259
344
|
private readonly store;
|
|
345
|
+
private chain;
|
|
260
346
|
constructor(store?: EventQueueStore);
|
|
347
|
+
private serialized;
|
|
261
348
|
enqueue(payload: LocationEventPayload): Promise<void>;
|
|
262
349
|
/** Every queued event, regardless of retry timing — used for diagnostics/pending counts. */
|
|
263
350
|
listPending(): Promise<QueuedEvent[]>;
|
|
264
|
-
/** Only events whose backoff window has elapsed (or that have never been attempted) — used by SyncManager.retryPending(). */
|
|
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. */
|
|
265
352
|
listReadyForRetry(now?: Date): Promise<QueuedEvent[]>;
|
|
266
353
|
/** Marks an event as in-flight so a concurrent retry pass won't also pick it up. */
|
|
267
354
|
markSending(eventId: string): Promise<void>;
|
|
268
|
-
|
|
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>;
|
|
269
357
|
markSent(eventId: string): Promise<void>;
|
|
270
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;
|
|
271
361
|
}
|
|
272
362
|
|
|
273
363
|
type NetworkStatus = "online" | "offline" | "unknown";
|
|
@@ -328,7 +418,7 @@ interface DiagnosticsSnapshot {
|
|
|
328
418
|
|
|
329
419
|
type EventListener = (event: LocationEventPayload) => void;
|
|
330
420
|
type ExperienceListener = (payload: ExperienceDeliveryPayload) => void;
|
|
331
|
-
|
|
421
|
+
type ExperienceRemovedListener = (payload: RevokedExperience) => void;
|
|
332
422
|
/** The SDK key resolved to a different application than `config.applicationId`. */
|
|
333
423
|
declare class ApplicationMismatchError extends Error {
|
|
334
424
|
constructor();
|
|
@@ -351,6 +441,15 @@ interface GeoPulseSdkDependencies {
|
|
|
351
441
|
getPlatform?: () => DevicePlatform;
|
|
352
442
|
/** Overrides GeoPulseConfig.debug — mainly for tests that want to assert on log output. */
|
|
353
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;
|
|
354
453
|
}
|
|
355
454
|
declare class GeoPulseSdk {
|
|
356
455
|
private status;
|
|
@@ -369,11 +468,24 @@ declare class GeoPulseSdk {
|
|
|
369
468
|
private locationError;
|
|
370
469
|
private unsubscribeNetwork;
|
|
371
470
|
private readonly stateMachine;
|
|
471
|
+
private readonly geofenceStateStore;
|
|
472
|
+
private geofenceStateRestored;
|
|
473
|
+
private realtime;
|
|
474
|
+
private readonly createRealtime;
|
|
475
|
+
private lastConfigFetchAt;
|
|
476
|
+
private configRefreshInFlight;
|
|
372
477
|
private readonly listeners;
|
|
373
478
|
private readonly experienceListeners;
|
|
374
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. */
|
|
375
|
-
private readonly
|
|
480
|
+
private readonly seenExperienceRevisions;
|
|
481
|
+
private readonly experienceRemovedListeners;
|
|
482
|
+
private readonly removedDeliveryIds;
|
|
376
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;
|
|
377
489
|
private experiencesReceivedCount;
|
|
378
490
|
private readonly logger;
|
|
379
491
|
private readonly locationProvider;
|
|
@@ -399,8 +511,10 @@ declare class GeoPulseSdk {
|
|
|
399
511
|
getPermissionStatus(): Promise<PermissionStatus>;
|
|
400
512
|
on(eventName: GeofenceEventType, callback: EventListener): void;
|
|
401
513
|
on(eventName: "experience.received", callback: ExperienceListener): void;
|
|
514
|
+
on(eventName: "experience.removed", callback: ExperienceRemovedListener): void;
|
|
402
515
|
off(eventName: GeofenceEventType, callback: EventListener): void;
|
|
403
516
|
off(eventName: "experience.received", callback: ExperienceListener): void;
|
|
517
|
+
off(eventName: "experience.removed", callback: ExperienceRemovedListener): void;
|
|
404
518
|
getConfiguredGeofences(): SdkGeofenceConfig[];
|
|
405
519
|
/** The server-resolved application id (from `getProjectConfig`), or null before `initialize()` completes. */
|
|
406
520
|
getApplicationId(): string | null;
|
|
@@ -433,7 +547,25 @@ declare class GeoPulseSdk {
|
|
|
433
547
|
*/
|
|
434
548
|
private startExperiencePolling;
|
|
435
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. */
|
|
436
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;
|
|
437
569
|
private applyProjectConfig;
|
|
438
570
|
private handleLocationUpdate;
|
|
439
571
|
}
|
|
@@ -527,6 +659,7 @@ declare function ExperienceView({ experience }: {
|
|
|
527
659
|
type OnOff = {
|
|
528
660
|
(eventName: GeofenceEventType, callback: (event: LocationEventPayload) => void): void;
|
|
529
661
|
(eventName: "experience.received", callback: (payload: ExperienceDeliveryPayload) => void): void;
|
|
662
|
+
(eventName: "experience.removed", callback: (payload: RevokedExperience) => void): void;
|
|
530
663
|
};
|
|
531
664
|
declare const GeoPulse: {
|
|
532
665
|
initialize: (config: GeoPulseConfig) => Promise<void>;
|
|
@@ -552,4 +685,4 @@ declare const GeoPulse: {
|
|
|
552
685
|
};
|
|
553
686
|
};
|
|
554
687
|
|
|
555
|
-
export { 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 SdkGeofenceConfig, type SdkStatus, SimulatedLocationProvider, SimulatedPermissionProvider };
|
|
688
|
+
export { 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 };
|