@sharib/geopulse-react-native 0.3.3 → 0.3.4
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 +37 -31
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +11 -11
package/README.md
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
# @sharib/geopulse-react-native
|
|
2
2
|
|
|
3
|
+
> **🚧 Not live for public sign-up yet.** Thanks for installing this early! The admin portal
|
|
4
|
+
> (where you create a project and get the `apiKey`/`applicationId` below) isn't open to public
|
|
5
|
+
> sign-up yet, so `initialize()` will fail for most people right now. This isn't a bug on your
|
|
6
|
+
> end, and there's nothing to fix. We're actively finishing this up, so check back soon. If you
|
|
7
|
+
> need access now, reach out to the GeoPulse team directly.
|
|
8
|
+
|
|
3
9
|
On-device geofence detection and event sync for React Native / Expo apps. Fetches an
|
|
4
10
|
application's configured geofences from GeoPulse, watches location (real device or the
|
|
5
11
|
built-in simulator), and reports `geofence.enter` / `geofence.exit` events to the GeoPulse
|
|
6
|
-
backend
|
|
12
|
+
backend, with a persistent, backoff-retried offline queue so a flaky connection never loses
|
|
7
13
|
an event.
|
|
8
14
|
|
|
9
15
|
## Installation
|
|
@@ -16,7 +22,7 @@ npm install @sharib/geopulse-react-native
|
|
|
16
22
|
|
|
17
23
|
Get `apiKey` and `applicationId` from the admin portal's `/sdk` page (the application created
|
|
18
24
|
automatically for you during onboarding is named "Playground"). The SDK connects to the hosted
|
|
19
|
-
GeoPulse backend automatically
|
|
25
|
+
GeoPulse backend automatically, with nothing else to configure:
|
|
20
26
|
|
|
21
27
|
```ts
|
|
22
28
|
import { GeoPulse } from "@sharib/geopulse-react-native";
|
|
@@ -28,9 +34,9 @@ await GeoPulse.initialize({
|
|
|
28
34
|
```
|
|
29
35
|
|
|
30
36
|
`initialize()` fetches the application's currently active geofences from the backend
|
|
31
|
-
(`getProjectConfig`)
|
|
37
|
+
(`getProjectConfig`). This is the "SDK receives project configuration" step. It throws if any
|
|
32
38
|
of the three fields above are missing, or if the backend call fails (bad/revoked SDK key,
|
|
33
|
-
network error, etc.)
|
|
39
|
+
network error, etc.). Check `GeoPulse.getStatus()` (`"error"`) if you need to react to that.
|
|
34
40
|
|
|
35
41
|
## Start / stop
|
|
36
42
|
|
|
@@ -53,7 +59,7 @@ GeoPulse.on("geofence.exit", (event) => console.log("exited", event));
|
|
|
53
59
|
GeoPulse.off("geofence.enter", onEnter);
|
|
54
60
|
```
|
|
55
61
|
|
|
56
|
-
An event fires at most once per actual state transition
|
|
62
|
+
An event fires at most once per actual state transition: moving around *inside* a geofence
|
|
57
63
|
never re-fires `geofence.enter`, and a geofence that's still active in your config never fires
|
|
58
64
|
`geofence.exit` just because you didn't move (see `src/geofencing/state-machine.ts` in this
|
|
59
65
|
package's source for the exact transition rules this guarantees).
|
|
@@ -65,17 +71,17 @@ associated Experience (a banner/card/message):
|
|
|
65
71
|
|
|
66
72
|
```ts
|
|
67
73
|
GeoPulse.on("experience.received", (payload) => {
|
|
68
|
-
// payload: ExperienceDeliveryPayload
|
|
74
|
+
// payload: ExperienceDeliveryPayload: { deliveryId, campaignId, experience: {...}, event: {...} }
|
|
69
75
|
showBanner(payload.experience);
|
|
70
76
|
});
|
|
71
77
|
```
|
|
72
78
|
|
|
73
79
|
No push notifications involved: the SDK polls its own device's pending experiences on a timer
|
|
74
80
|
(`GeoPulseConfig.experiencePollIntervalMs`, defaults to 30s) over the same connection it already
|
|
75
|
-
uses for config/ingestion, and acknowledges each one automatically once it's fired the event
|
|
81
|
+
uses for config/ingestion, and acknowledges each one automatically once it's fired the event, so
|
|
76
82
|
you never need to call anything to mark it "seen." The SDK also never re-fires the same
|
|
77
83
|
`deliveryId` twice. There's no `getActiveExperiences()`: `on("experience.received")` is the
|
|
78
|
-
whole surface
|
|
84
|
+
whole surface: the SDK transports and notifies, your app owns the UI.
|
|
79
85
|
|
|
80
86
|
## Status / diagnostics
|
|
81
87
|
|
|
@@ -87,9 +93,9 @@ without duplicating SDK-internal state:
|
|
|
87
93
|
GeoPulse.getStatus(); // "idle" | "initializing" | "initialized" | "running" | "stopped" | "error"
|
|
88
94
|
await GeoPulse.getPermissionStatus(); // "granted" | "denied" | "undetermined"
|
|
89
95
|
await GeoPulse.getDiagnostics(); // rich snapshot: network status, config status, pending events, last sync, etc.
|
|
90
|
-
GeoPulse.getApplicationId(); // string | null
|
|
91
|
-
GeoPulse.getDeviceId(); // string | null
|
|
92
|
-
// getDiagnostics() also includes `locationError: string | null
|
|
96
|
+
GeoPulse.getApplicationId(); // string | null, resolved server-side, not just an echo of what you passed in
|
|
97
|
+
GeoPulse.getDeviceId(); // string | null, persisted across app restarts
|
|
98
|
+
// getDiagnostics() also includes `locationError: string | null`, set if the location provider
|
|
93
99
|
// failed to start (e.g. OS location services off).
|
|
94
100
|
GeoPulse.getConfiguredGeofences(); // the geofences fetched by initialize()
|
|
95
101
|
GeoPulse.getLastKnownLocation(); // most recent location update, or null
|
|
@@ -106,11 +112,11 @@ GeoPulse.simulator.setLocation({ latitude: 33.4484, longitude: -112.074 });
|
|
|
106
112
|
```
|
|
107
113
|
|
|
108
114
|
Each call runs through the exact same geofence engine and event pipeline as a real device
|
|
109
|
-
update would
|
|
115
|
+
update would. The simulator is a location *source*, not a shortcut that fakes an event
|
|
110
116
|
directly.
|
|
111
117
|
|
|
112
118
|
**The simulator is inert in production builds.** `GeoPulse.simulator.setLocation(...)` is a
|
|
113
|
-
no-op (with a console warning) once `__DEV__` is false
|
|
119
|
+
no-op (with a console warning) once `__DEV__` is false. Release builds run on
|
|
114
120
|
`DeviceLocationProvider` (real `expo-location`) instead, automatically, with no code change
|
|
115
121
|
required. If you need real device location *during development* instead of the simulator,
|
|
116
122
|
construct your own instance rather than using the `GeoPulse` singleton:
|
|
@@ -126,65 +132,65 @@ const sdk = new GeoPulseSdk({
|
|
|
126
132
|
|
|
127
133
|
## Permissions
|
|
128
134
|
|
|
129
|
-
This SDK requests **foreground location only
|
|
135
|
+
This SDK requests **foreground location only**, never background location. Concretely:
|
|
130
136
|
|
|
131
137
|
- **iOS**: `NSLocationWhenInUseUsageDescription` must be set in your app's `app.json` (or
|
|
132
138
|
`Info.plist` directly for a bare RN app); `expo-location`'s config plugin sets this from the
|
|
133
139
|
`locationWhenInUsePermission` option. No `NSLocationAlwaysAndWhenInUseUsageDescription` is
|
|
134
|
-
requested
|
|
140
|
+
requested, so the SDK will never prompt for "Always" access.
|
|
135
141
|
- **Android**: `ACCESS_FINE_LOCATION` (and `ACCESS_COARSE_LOCATION` as a fallback) in the
|
|
136
142
|
manifest, via your app's `app.json`'s `android.permissions`. No `ACCESS_BACKGROUND_LOCATION`.
|
|
137
143
|
- **Foreground-only limitation**: location updates (and therefore geofence detection) only run
|
|
138
|
-
while the app is in the foreground. Backgrounding the app pauses detection
|
|
144
|
+
while the app is in the foreground. Backgrounding the app pauses detection: an enter/exit
|
|
139
145
|
that happens while the app is backgrounded will not be caught until the app is foregrounded
|
|
140
146
|
again and a new location update arrives. This is a deliberate scope boundary for this phase,
|
|
141
|
-
not an oversight
|
|
147
|
+
not an oversight. Background geofencing needs a materially different architecture (a native
|
|
142
148
|
background task, plus battery/OS-throttling tradeoffs) and is planned for a later phase. Do
|
|
143
149
|
not present this SDK as providing production-grade background geofencing.
|
|
144
150
|
- **Battery**: `DeviceLocationProvider` uses `Location.Accuracy.Balanced` with a 10m distance
|
|
145
|
-
filter and a 5s minimum interval
|
|
151
|
+
filter and a 5s minimum interval. That's a reasonable foreground default, not tuned for
|
|
146
152
|
high-precision or high-frequency tracking.
|
|
147
153
|
|
|
148
154
|
## Troubleshooting
|
|
149
155
|
|
|
150
|
-
- **`initialize()` rejects with a fetch/network error
|
|
156
|
+
- **`initialize()` rejects with a fetch/network error**: check `functionsBaseUrl` is reachable
|
|
151
157
|
from the device (an emulator URL like `127.0.0.1` is not reachable from a physical device or
|
|
152
|
-
even always from a simulator
|
|
153
|
-
- **`start()` rejects with "Location permission not granted"
|
|
158
|
+
even always from a simulator, so use your machine's LAN IP or a deployed URL instead).
|
|
159
|
+
- **`start()` rejects with "Location permission not granted"**: the user denied the OS
|
|
154
160
|
permission prompt (real device), or `PermissionProvider.request()` didn't resolve
|
|
155
|
-
`"granted"` (simulator mode always grants
|
|
156
|
-
- **No `geofence.enter` ever fires in the simulator
|
|
161
|
+
`"granted"` (simulator mode always grants; this only happens with `DeviceLocationProvider`).
|
|
162
|
+
- **No `geofence.enter` ever fires in the simulator**: confirm `GeoPulse.getConfiguredGeofences()`
|
|
157
163
|
isn't empty; if it is, no geofence exists yet for this application's project in the admin
|
|
158
164
|
portal, or `initialize()` was called before the SDK key's application had one created.
|
|
159
|
-
- **Events show up in `getPendingEventCount()` but never seem to send
|
|
165
|
+
- **Events show up in `getPendingEventCount()` but never seem to send**: each queued event has
|
|
160
166
|
its own exponential-backoff schedule; a recently-failed event isn't retried again immediately.
|
|
161
167
|
Retries also fire as soon as the network monitor reports connectivity restored, so this should
|
|
162
|
-
resolve quickly once you're actually back online
|
|
168
|
+
resolve quickly once you're actually back online. If it doesn't, check `functionsBaseUrl` is
|
|
163
169
|
reachable at all.
|
|
164
170
|
|
|
165
171
|
## Internal architecture
|
|
166
172
|
|
|
167
173
|
```
|
|
168
174
|
src/
|
|
169
|
-
core/ SDK lifecycle + diagnostics (GeoPulseSdk), Logger
|
|
175
|
+
core/ SDK lifecycle + diagnostics (GeoPulseSdk), Logger. The only module that wires everything else together
|
|
170
176
|
config/ GeoPulseConfig type, device-id persistence, config caching
|
|
171
177
|
location/ LocationProvider abstraction: SimulatedLocationProvider (default in dev) + DeviceLocationProvider (expo-location)
|
|
172
|
-
geofencing/ GeofenceStateMachine
|
|
178
|
+
geofencing/ GeofenceStateMachine: haversine distance + INSIDE/OUTSIDE transition tracking
|
|
173
179
|
events/ Event payload construction + id generation
|
|
174
180
|
storage/ Offline pending-event queue (InMemoryEventQueueStore for tests, AsyncStorageEventQueueStore for the real app)
|
|
175
181
|
sync/ SyncManager (send-or-queue, backoff retry), centralized retry policy, NetworkMonitor
|
|
176
182
|
permissions/ PermissionProvider abstraction: SimulatedPermissionProvider (default in dev) + ExpoPermissionProvider
|
|
177
|
-
firebase/ ApiClient
|
|
178
|
-
index.ts Public API (GeoPulse)
|
|
183
|
+
firebase/ ApiClient: plain-fetch HTTPS client for the Cloud Functions endpoints (no Firebase SDK on-device)
|
|
184
|
+
index.ts Public API (GeoPulse). Wires the real RN adapters; everything else is injectable for testing
|
|
179
185
|
```
|
|
180
186
|
|
|
181
187
|
Every module except `location/device-location-provider.ts`,
|
|
182
188
|
`permissions/expo-permission-provider.ts`, `storage/async-storage-store.ts`,
|
|
183
189
|
`config/async-storage-key-value-store.ts`, and `sync/netinfo-network-monitor.ts` has zero React
|
|
184
|
-
Native dependencies and runs under plain Node/Vitest
|
|
190
|
+
Native dependencies and runs under plain Node/Vitest. Those five are the only files that
|
|
185
191
|
import an RN-only package, and nothing in `core/`, `geofencing/`, `events/`, `sync/`
|
|
186
192
|
(excluding the one file above), or `firebase/` imports them (see the comments at the top of
|
|
187
193
|
`storage/index.ts`, `location/index.ts`, `config/index.ts`, `permissions/index.ts`, and
|
|
188
194
|
`sync/index.ts`). That split is what makes `GeoPulseSdk` and the geofence engine unit-testable
|
|
189
|
-
without a device, simulator, or Metro bundler
|
|
195
|
+
without a device, simulator, or Metro bundler. Run `pnpm test` from this package's directory
|
|
190
196
|
(55 tests as of this phase).
|
package/dist/index.js
CHANGED
|
@@ -60,7 +60,7 @@ var AsyncStorageKeyValueStore = class {
|
|
|
60
60
|
|
|
61
61
|
// src/config/types.ts
|
|
62
62
|
var DEFAULT_BACKEND_URL = "https://geofends.onrender.com";
|
|
63
|
-
var SDK_VERSION = "0.3.
|
|
63
|
+
var SDK_VERSION = "0.3.4";
|
|
64
64
|
|
|
65
65
|
// src/config/key-value-store.ts
|
|
66
66
|
var InMemoryKeyValueStore = class {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/index.ts","../src/config/async-storage-key-value-store.ts","../src/config/types.ts","../src/config/key-value-store.ts","../src/events/uuid.ts","../src/config/device-id.ts","../src/config/config-cache.ts","../src/protocol.ts","../src/firebase/api-client.ts","../src/events/factory.ts","../src/geofencing/state-machine.ts","../src/location/simulated-location-provider.ts","../src/permissions/simulated-permission-provider.ts","../src/sync/retry-policy.ts","../src/storage/memory-store.ts","../src/storage/queue.ts","../src/sync/network-monitor.ts","../src/sync/sync-manager.ts","../src/core/logger.ts","../src/core/geo-pulse-sdk.ts","../src/location/device-location-provider.ts","../src/permissions/expo-permission-provider.ts","../src/storage/async-storage-store.ts","../src/sync/netinfo-network-monitor.ts","../src/rendering/experience-components.tsx"],"sourcesContent":["import { Platform } from \"react-native\";\nimport type { ExperienceDeliveryPayload, GeofenceEventType, LocationEventPayload } from \"./protocol\";\n\nimport { AsyncStorageKeyValueStore } from \"./config/async-storage-key-value-store\";\nimport type { GeoPulseConfig } from \"./config/types\";\nimport { GeoPulseSdk } from \"./core/geo-pulse-sdk\";\nimport { DeviceLocationProvider } from \"./location/device-location-provider\";\nimport { SimulatedLocationProvider } from \"./location/simulated-location-provider\";\nimport { ExpoPermissionProvider } from \"./permissions/expo-permission-provider\";\nimport { SimulatedPermissionProvider } from \"./permissions/simulated-permission-provider\";\nimport { AsyncStorageEventQueueStore } from \"./storage/async-storage-store\";\nimport { PendingEventQueue } from \"./storage/queue\";\nimport { NetInfoNetworkMonitor } from \"./sync/netinfo-network-monitor\";\n\nconst isDev = typeof __DEV__ === \"undefined\" ? true : __DEV__;\n\n/**\n * Opt-in escape hatch for testing real GPS *during development* without\n * switching to a production build — set `EXPO_PUBLIC_GEOPULSE_USE_REAL_LOCATION=true`\n * in the host app's `.env` (Expo statically inlines `EXPO_PUBLIC_*` at\n * bundle time, including inside this workspace package, so this works\n * without the SDK needing any new config field). Defaults to false: dev\n * still means \"simulator\" unless a developer explicitly asks otherwise.\n */\nconst useRealLocationInDev = process.env.EXPO_PUBLIC_GEOPULSE_USE_REAL_LOCATION === \"true\";\nconst useSimulatedProviders = isDev && !useRealLocationInDev;\n\nfunction getPlatform(): \"ios\" | \"android\" | \"web\" | \"unknown\" {\n switch (Platform.OS) {\n case \"ios\":\n case \"android\":\n case \"web\":\n return Platform.OS;\n default:\n return \"unknown\";\n }\n}\n\n// Shared by device-id and config-cache — both are just distinctly-keyed\n// entries in AsyncStorage, so one instance is enough.\nconst persistentKeyValueStore = new AsyncStorageKeyValueStore();\n\n/**\n * Dev builds default to the simulated location + permission providers (see\n * the \"Simulator\" section of this package's README) so the whole\n * location -> geofence -> event flow works with zero device setup. A\n * release build — or a dev build with `EXPO_PUBLIC_GEOPULSE_USE_REAL_LOCATION=true`\n * set (see above) — uses real device location and real OS permission\n * prompts instead.\n */\nconst sdk = new GeoPulseSdk({\n locationProvider: useSimulatedProviders ? new SimulatedLocationProvider() : new DeviceLocationProvider(),\n permissionProvider: useSimulatedProviders ? new SimulatedPermissionProvider() : new ExpoPermissionProvider(),\n queue: new PendingEventQueue(new AsyncStorageEventQueueStore()),\n deviceIdStore: persistentKeyValueStore,\n configCacheStore: persistentKeyValueStore,\n networkMonitor: new NetInfoNetworkMonitor(),\n getPlatform,\n debug: isDev,\n});\n\n/** `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. */\ntype OnOff = {\n (eventName: GeofenceEventType, callback: (event: LocationEventPayload) => void): void;\n (eventName: \"experience.received\", callback: (payload: ExperienceDeliveryPayload) => void): void;\n};\n\nexport const GeoPulse = {\n initialize: (config: GeoPulseConfig) => sdk.initialize(config),\n start: () => sdk.start(),\n stop: () => sdk.stop(),\n getStatus: () => sdk.getStatus(),\n getPermissionStatus: () => sdk.getPermissionStatus(),\n on: sdk.on.bind(sdk) as OnOff,\n off: sdk.off.bind(sdk) as OnOff,\n\n // Small additions beyond the core 7-method API, for building a status UI\n // around the SDK (see apps/playground) — not part of the minimal public\n // surface the spec calls out, but plain read-only accessors for state the\n // SDK already tracks internally.\n getConfiguredGeofences: () => sdk.getConfiguredGeofences(),\n getApplicationId: () => sdk.getApplicationId(),\n getLastKnownLocation: () => sdk.getLastKnownLocation(),\n getPendingEventCount: () => sdk.getPendingEventCount(),\n getDeviceId: () => sdk.getDeviceId(),\n /** Rich developer-diagnostics snapshot — see core/types.ts's DiagnosticsSnapshot doc. */\n getDiagnostics: () => sdk.getDiagnostics(),\n\n /** Development-only. No-ops (with a console warning) in a production build — see GeoPulseSdk.simulateLocation. */\n simulator: {\n setLocation: (coords: { latitude: number; longitude: number }) => sdk.simulateLocation(coords),\n },\n};\n\n// Advanced/testing escape hatch — construct your own SDK instance instead\n// of using the `GeoPulse` singleton above (e.g. to force real device\n// location during development). Most consumers should just use `GeoPulse`.\nexport { ApplicationMismatchError, GeoPulseSdk } from \"./core/geo-pulse-sdk\";\nexport { DeviceLocationProvider } from \"./location/device-location-provider\";\nexport { SimulatedLocationProvider } from \"./location/simulated-location-provider\";\nexport { ExpoPermissionProvider } from \"./permissions/expo-permission-provider\";\nexport { SimulatedPermissionProvider } from \"./permissions/simulated-permission-provider\";\nexport { NetInfoNetworkMonitor } from \"./sync/netinfo-network-monitor\";\nexport { ManualNetworkMonitor } from \"./sync/network-monitor\";\n\nexport type { GeoPulseConfig } from \"./config/types\";\nexport type { ConfigurationStatus, DiagnosticsSnapshot, SdkStatus } from \"./core/types\";\nexport type { LocationProvider, LocationUpdate } from \"./location/types\";\nexport type { PermissionProvider, PermissionStatus } from \"./permissions/types\";\nexport type { NetworkMonitor, NetworkStatus } from \"./sync/network-monitor\";\nexport type { ExperienceDeliveryPayload, GeofenceEventType, LocationEventPayload, SdkGeofenceConfig } from \"./protocol\";\nexport { BannerExperience, CardExperience, ExperienceView, MessageExperience } from \"./rendering\";\n","import AsyncStorage from \"@react-native-async-storage/async-storage\";\n\nimport type { KeyValueStore } from \"./key-value-store\";\n\n/** Real persistence for the RN app. Isolated in its own file — see storage/async-storage-store.ts's comment for why. */\nexport class AsyncStorageKeyValueStore implements KeyValueStore {\n async getItem(key: string): Promise<string | null> {\n return AsyncStorage.getItem(key);\n }\n\n async setItem(key: string, value: string): Promise<void> {\n await AsyncStorage.setItem(key, value);\n }\n}\n","export interface GeoPulseConfig {\n apiKey: string;\n applicationId: string;\n /**\n * Internal override for the GeoPulse backend URL — only for GeoPulse's own\n * local development (e.g. apps/playground against a local backend).\n * Customers never set this; it defaults to `DEFAULT_BACKEND_URL`.\n */\n functionsBaseUrl?: string;\n /** How often to retry queued (previously failed) events, in milliseconds. Defaults to 30s. */\n retryIntervalMs?: number;\n /** How often to poll for pending campaign experiences (Phase 5B), in milliseconds. Defaults to 30s, mirroring `retryIntervalMs`. A host app that wants faster demo feedback (see apps/playground) can pass a smaller value. */\n experiencePollIntervalMs?: number;\n /** 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. */\n debug?: boolean;\n}\n\n/** The hosted GeoPulse backend every customer app talks to. */\nexport const DEFAULT_BACKEND_URL = \"https://geofends.onrender.com\";\n\nexport const SDK_VERSION = \"0.3.3\";\n","/**\n * Minimal persistent key-value abstraction, shared by device-id.ts and\n * config-cache.ts — deliberately smaller than `storage/EventQueueStore`\n * (which manages a whole collection) since these two only ever need \"get\n * one string\" / \"set one string\".\n */\nexport interface KeyValueStore {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n}\n\nexport class InMemoryKeyValueStore implements KeyValueStore {\n private readonly data = new Map<string, string>();\n\n async getItem(key: string): Promise<string | null> {\n return this.data.get(key) ?? null;\n }\n\n async setItem(key: string, value: string): Promise<void> {\n this.data.set(key, value);\n }\n}\n","/**\n * RFC 4122 v4 UUID for event and device ids. Uses the platform CSPRNG\n * (`crypto.getRandomValues`, available in Hermes >= 0.12 and Node) when\n * present, falling back to `Math.random()` only on runtimes without it.\n * These ids aren't secrets, but a device id scopes which pending\n * experiences a request can fetch, so it shouldn't be predictable.\n */\nexport function generateEventId(): string {\n const bytes = new Uint8Array(16);\n const cryptoApi = (globalThis as { crypto?: { getRandomValues?: (array: Uint8Array) => Uint8Array } }).crypto;\n if (cryptoApi?.getRandomValues) {\n cryptoApi.getRandomValues(bytes);\n } else {\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;\n bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;\n const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n","import { generateEventId } from \"../events/uuid\";\nimport type { KeyValueStore } from \"./key-value-store\";\n\nconst DEVICE_ID_STORAGE_KEY = \"@geopulse/device-id\";\n\n/**\n * A stable, random, non-identifying installation id — generated once per\n * install and persisted thereafter, not derived from any hardware\n * identifier (spec: \"do not attempt invasive device fingerprinting\").\n * `generateEventId` is just a generic UUID v4 generator despite its name\n * (see events/uuid.ts); reused here rather than duplicated.\n */\nexport async function getOrCreateDeviceId(store: KeyValueStore): Promise<string> {\n const existing = await store.getItem(DEVICE_ID_STORAGE_KEY);\n if (existing) {\n return existing;\n }\n const deviceId = generateEventId();\n await store.setItem(DEVICE_ID_STORAGE_KEY, deviceId);\n return deviceId;\n}\n","import type { SdkProjectConfig } from \"../protocol\";\n\nimport type { KeyValueStore } from \"./key-value-store\";\n\nconst CONFIG_CACHE_STORAGE_KEY = \"@geopulse/config-cache\";\n\ninterface CachedConfig {\n config: SdkProjectConfig;\n fetchedAt: string;\n}\n\n/**\n * Persists the last successfully fetched `SdkProjectConfig` so\n * `initialize()` can still succeed — using stale-but-valid geofences —\n * when `getProjectConfig` is unreachable (spec: \"If the device temporarily\n * loses network access, the SDK should be able to continue using the last\n * valid configuration\"). There is no expiry: a geofence list that's a few\n * hours old is still far more useful than refusing to start at all, and\n * the next successful fetch overwrites it regardless of age.\n */\nexport class ConfigCache {\n constructor(private readonly store: KeyValueStore) {}\n\n async save(config: SdkProjectConfig): Promise<void> {\n const cached: CachedConfig = { config, fetchedAt: new Date().toISOString() };\n await this.store.setItem(CONFIG_CACHE_STORAGE_KEY, JSON.stringify(cached));\n }\n\n async load(): Promise<CachedConfig | null> {\n const raw = await this.store.getItem(CONFIG_CACHE_STORAGE_KEY);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as CachedConfig;\n } catch {\n return null;\n }\n }\n}\n","/**\n * The SDK's entire wire contract with the GeoPulse backend, copied from\n * `@geopulse/shared` rather than depending on it at runtime. `@geopulse/shared`\n * is a monorepo-internal package (also used by apps/admin-web and functions/)\n * that carries the *whole* platform's schemas — organizations, webhooks,\n * campaigns, auth forms, and more. None of that belongs inside a published,\n * on-device SDK: bundling the real package pulled in all of it (verified via\n * `npm pack` on the published tarball — dist/index.js contained the admin\n * portal's signup/login validation, webhook, and campaign schemas). This file\n * is the deliberately small subset the SDK actually touches. Keep it in sync\n * with the corresponding types in packages/shared/src/ by hand if the wire\n * contract changes — `src/core/geo-pulse-sdk.test.ts` and\n * `src/firebase/api-client.test.ts` still validate against the real\n * `@geopulse/shared` package (a devDependency, never published) precisely so\n * drift between this copy and the canonical schema gets caught.\n */\n\nexport const SDK_KEY_HEADER = \"x-geopulse-sdk-key\";\n\n/** The configured application id, sent with every request so the backend can reject a key bound to another application. */\nexport const APPLICATION_ID_HEADER = \"x-geopulse-application-id\";\n\nexport const SDK_ENDPOINTS = {\n GET_PROJECT_CONFIG: \"getProjectConfig\",\n INGEST_LOCATION_EVENT: \"ingestLocationEvent\",\n GET_PENDING_EXPERIENCES: \"getPendingExperiences\",\n ACKNOWLEDGE_EXPERIENCE_DELIVERY: \"acknowledgeExperienceDelivery\",\n} as const;\n\nconst EARTH_RADIUS_METERS = 6_371_000;\n\nfunction toRadians(degrees: number): number {\n return (degrees * Math.PI) / 180;\n}\n\n/** Haversine great-circle distance in meters — see packages/shared/src/utils/geo.ts for the full derivation comment. */\nexport function calculateDistanceMeters(\n from: { latitude: number; longitude: number },\n to: { latitude: number; longitude: number },\n): number {\n const lat1 = toRadians(from.latitude);\n const lat2 = toRadians(to.latitude);\n const deltaLat = toRadians(to.latitude - from.latitude);\n const deltaLng = toRadians(to.longitude - from.longitude);\n\n const a =\n Math.sin(deltaLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLng / 2) ** 2;\n const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n\n return EARTH_RADIUS_METERS * c;\n}\n\nexport const GEOFENCE_EVENT_TYPES = [\"geofence.enter\", \"geofence.exit\"] as const;\nexport type GeofenceEventType = (typeof GEOFENCE_EVENT_TYPES)[number];\n\nexport const DEVICE_PLATFORMS = [\"ios\", \"android\", \"web\", \"unknown\"] as const;\nexport type DevicePlatform = (typeof DEVICE_PLATFORMS)[number];\n\nexport const EXPERIENCE_TYPES = [\"banner\", \"card\", \"message\"] as const;\nexport type ExperienceType = (typeof EXPERIENCE_TYPES)[number];\n\nexport const ANCHOR_POINTS = [\n \"top-left\",\n \"top-center\",\n \"top-right\",\n \"center-left\",\n \"center\",\n \"center-right\",\n \"bottom-left\",\n \"bottom-center\",\n \"bottom-right\",\n] as const;\nexport type AnchorPoint = (typeof ANCHOR_POINTS)[number];\n\nexport interface AnchoredPosition {\n anchor: AnchorPoint;\n offsetX?: number | null;\n offsetY?: number | null;\n}\n\nexport interface ContainerStyle {\n backgroundColor?: string | null;\n borderColor?: string | null;\n borderWidth?: number | null;\n borderRadius?: number | null;\n padding?: number | null;\n backgroundImageUrl?: string | null;\n backgroundImageFit?: \"cover\" | \"contain\" | \"stretch\";\n widthMode?: \"fill\" | \"fixed\";\n width?: number | null;\n heightMode?: \"auto\" | \"fixed\";\n height?: number | null;\n}\n\nexport interface TextStyle {\n visible?: boolean;\n color?: string | null;\n fontSize?: number | null;\n fontWeight?: \"normal\" | \"bold\";\n}\n\nexport interface ButtonStyle {\n visible?: boolean;\n backgroundColor?: string | null;\n textColor?: string | null;\n borderRadius?: number | null;\n position?: AnchoredPosition | null;\n}\n\nexport interface ExperienceStyleV2 {\n version: 2;\n container?: ContainerStyle;\n title?: TextStyle;\n body?: TextStyle;\n button?: ButtonStyle;\n}\n\nexport interface ExperienceStyleLegacy {\n backgroundColor: string | null;\n textColor: string | null;\n accentColor: string | null;\n}\n\nexport type ExperienceStyle = ExperienceStyleV2 | ExperienceStyleLegacy;\n\nexport function isLegacyExperienceStyle(style: ExperienceStyle): style is ExperienceStyleLegacy {\n return !(\"version\" in style);\n}\n\nexport interface ResolvedPosition {\n top: number | null;\n bottom: number | null;\n left: number | null;\n right: number | null;\n}\n\nexport interface ResolvedContainerStyle {\n backgroundColor: string;\n borderColor: string | null;\n borderWidth: number;\n borderRadius: number;\n padding: number;\n backgroundImageUrl: string | null;\n backgroundImageFit: \"cover\" | \"contain\" | \"stretch\";\n width: \"fill\" | number;\n height: \"auto\" | number;\n}\n\nexport interface ResolvedTextStyle {\n visible: boolean;\n color: string;\n fontSize: number;\n fontWeight: \"normal\" | \"bold\";\n}\n\nexport interface ResolvedButtonStyle {\n visible: boolean;\n backgroundColor: string;\n textColor: string;\n borderRadius: number;\n position: ResolvedPosition;\n}\n\nexport interface ResolvedExperienceStyle {\n container: ResolvedContainerStyle;\n title: ResolvedTextStyle;\n body: ResolvedTextStyle;\n button: ResolvedButtonStyle;\n}\n\nconst HEX_COLOR_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\n/** dp — a generous cap so a malformed/huge offset can't push an element fully off-screen. */\nconst OFFSET_LIMIT = 500;\n\nconst ANCHOR_BASE_POSITIONS: Record<AnchorPoint, ResolvedPosition> = {\n \"top-left\": { top: 0, left: 0, bottom: null, right: null },\n \"top-center\": { top: 0, left: null, bottom: null, right: null },\n \"top-right\": { top: 0, left: null, bottom: null, right: 0 },\n \"center-left\": { top: null, left: 0, bottom: null, right: null },\n center: { top: null, left: null, bottom: null, right: null },\n \"center-right\": { top: null, left: null, bottom: null, right: 0 },\n \"bottom-left\": { top: null, left: 0, bottom: 0, right: null },\n \"bottom-center\": { top: null, left: null, bottom: 0, right: null },\n \"bottom-right\": { top: null, left: null, bottom: 0, right: 0 },\n};\n\nconst DEFAULT_STYLES: Record<ExperienceType, ResolvedExperienceStyle> = {\n card: {\n container: { backgroundColor: \"#ffffff\", borderColor: null, borderWidth: 0, borderRadius: 16, padding: 20, backgroundImageUrl: null, backgroundImageFit: \"cover\", width: \"fill\", height: \"auto\" },\n title: { visible: true, color: \"#111827\", fontSize: 18, fontWeight: \"bold\" },\n body: { visible: true, color: \"#374151\", fontSize: 14, fontWeight: \"normal\" },\n button: { visible: true, backgroundColor: \"#2563eb\", textColor: \"#ffffff\", borderRadius: 8, position: { top: null, bottom: 16, left: 16, right: null } },\n },\n banner: {\n container: { backgroundColor: \"#111827\", borderColor: null, borderWidth: 0, borderRadius: 0, padding: 12, backgroundImageUrl: null, backgroundImageFit: \"cover\", width: \"fill\", height: \"auto\" },\n title: { visible: true, color: \"#ffffff\", fontSize: 15, fontWeight: \"bold\" },\n body: { visible: true, color: \"#e5e7eb\", fontSize: 13, fontWeight: \"normal\" },\n button: { visible: false, backgroundColor: \"#2563eb\", textColor: \"#ffffff\", borderRadius: 6, position: { top: null, bottom: null, left: null, right: 12 } },\n },\n message: {\n container: { backgroundColor: \"#1f2937\", borderColor: null, borderWidth: 0, borderRadius: 12, padding: 16, backgroundImageUrl: null, backgroundImageFit: \"cover\", width: \"fill\", height: \"auto\" },\n title: { visible: false, color: \"#ffffff\", fontSize: 16, fontWeight: \"bold\" },\n body: { visible: true, color: \"#ffffff\", fontSize: 14, fontWeight: \"normal\" },\n button: { visible: false, backgroundColor: \"#2563eb\", textColor: \"#ffffff\", borderRadius: 8, position: { top: null, bottom: 16, left: null, right: 16 } },\n },\n};\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\nfunction resolveColor(value: string | null | undefined, fallback: string): string {\n return typeof value === \"string\" && HEX_COLOR_PATTERN.test(value) ? value : fallback;\n}\n\nfunction resolveOptionalColor(value: string | null | undefined, fallback: string | null): string | null {\n return typeof value === \"string\" && HEX_COLOR_PATTERN.test(value) ? value : fallback;\n}\n\nfunction resolveNonNegativeNumber(value: number | null | undefined, fallback: number): number {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : fallback;\n}\n\nfunction resolvePosition(position: AnchoredPosition | null | undefined, fallback: ResolvedPosition): ResolvedPosition {\n if (!position || !ANCHOR_POINTS.includes(position.anchor)) return fallback;\n const offsetX = typeof position.offsetX === \"number\" && Number.isFinite(position.offsetX) ? clamp(position.offsetX, -OFFSET_LIMIT, OFFSET_LIMIT) : 0;\n const offsetY = typeof position.offsetY === \"number\" && Number.isFinite(position.offsetY) ? clamp(position.offsetY, -OFFSET_LIMIT, OFFSET_LIMIT) : 0;\n const base = ANCHOR_BASE_POSITIONS[position.anchor];\n return {\n top: base.top === null ? null : base.top + offsetY,\n bottom: base.bottom === null ? null : base.bottom + offsetY,\n left: base.left === null ? null : base.left + offsetX,\n right: base.right === null ? null : base.right + offsetX,\n };\n}\n\nfunction resolveWidth(mode: \"fill\" | \"fixed\" | undefined, value: number | null | undefined, fallback: \"fill\" | number): \"fill\" | number {\n if (mode === \"fixed\") {\n return typeof value === \"number\" && Number.isFinite(value) && value > 0 ? value : fallback;\n }\n if (mode === \"fill\") return \"fill\";\n return fallback;\n}\n\nfunction resolveHeight(mode: \"auto\" | \"fixed\" | undefined, value: number | null | undefined, fallback: \"auto\" | number): \"auto\" | number {\n if (mode === \"fixed\") {\n return typeof value === \"number\" && Number.isFinite(value) && value > 0 ? value : fallback;\n }\n if (mode === \"auto\") return \"auto\";\n return fallback;\n}\n\nfunction resolveContainer(custom: ContainerStyle | undefined, fallback: ResolvedContainerStyle): ResolvedContainerStyle {\n return {\n backgroundColor: resolveColor(custom?.backgroundColor, fallback.backgroundColor),\n borderColor: resolveOptionalColor(custom?.borderColor, fallback.borderColor),\n borderWidth: resolveNonNegativeNumber(custom?.borderWidth, fallback.borderWidth),\n borderRadius: resolveNonNegativeNumber(custom?.borderRadius, fallback.borderRadius),\n padding: resolveNonNegativeNumber(custom?.padding, fallback.padding),\n backgroundImageUrl: typeof custom?.backgroundImageUrl === \"string\" && custom.backgroundImageUrl.length > 0 ? custom.backgroundImageUrl : fallback.backgroundImageUrl,\n backgroundImageFit: custom?.backgroundImageFit ?? fallback.backgroundImageFit,\n width: resolveWidth(custom?.widthMode, custom?.width, fallback.width),\n height: resolveHeight(custom?.heightMode, custom?.height, fallback.height),\n };\n}\n\nfunction resolveText(custom: TextStyle | undefined, fallback: ResolvedTextStyle): ResolvedTextStyle {\n return {\n visible: custom?.visible ?? fallback.visible,\n color: resolveColor(custom?.color, fallback.color),\n fontSize: resolveNonNegativeNumber(custom?.fontSize, fallback.fontSize),\n fontWeight: custom?.fontWeight ?? fallback.fontWeight,\n };\n}\n\nfunction resolveButton(custom: ButtonStyle | undefined, fallback: ResolvedButtonStyle): ResolvedButtonStyle {\n return {\n visible: custom?.visible ?? fallback.visible,\n backgroundColor: resolveColor(custom?.backgroundColor, fallback.backgroundColor),\n textColor: resolveColor(custom?.textColor, fallback.textColor),\n borderRadius: resolveNonNegativeNumber(custom?.borderRadius, fallback.borderRadius),\n position: resolvePosition(custom?.position, fallback.position),\n };\n}\n\n/** Maps today's flat 3-color shape onto the v2 groups it corresponds to. */\nfunction upgradeLegacyStyle(legacy: ExperienceStyleLegacy): ExperienceStyleV2 {\n return {\n version: 2,\n container: { backgroundColor: legacy.backgroundColor },\n title: { color: legacy.textColor },\n body: { color: legacy.textColor },\n button: { backgroundColor: legacy.accentColor },\n };\n}\n\n/**\n * Cascades a type's built-in default under a campaign's custom style,\n * upgrading today's legacy 3-color shape on the fly, and resolves 9-point\n * anchor positions to concrete offsets. Never throws — any malformed or\n * missing field silently falls back to the type's default.\n */\nexport function resolveExperienceStyle(type: ExperienceType, custom: ExperienceStyle | null | undefined): ResolvedExperienceStyle {\n const fallback = DEFAULT_STYLES[type] ?? DEFAULT_STYLES.card;\n if (!custom) return fallback;\n const v2 = isLegacyExperienceStyle(custom) ? upgradeLegacyStyle(custom) : custom;\n return {\n container: resolveContainer(v2.container, fallback.container),\n title: resolveText(v2.title, fallback.title),\n body: resolveText(v2.body, fallback.body),\n button: resolveButton(v2.button, fallback.button),\n };\n}\n\nexport interface GeofenceCenter {\n latitude: number;\n longitude: number;\n}\n\nexport interface SdkGeofenceConfig {\n id: string;\n name: string;\n locationId: string;\n locationName: string;\n center: GeofenceCenter;\n radiusMeters: number;\n}\n\nexport interface SdkProjectConfig {\n applicationId: string;\n projectId: string;\n applicationEnabled: boolean;\n configVersion: string;\n geofences: SdkGeofenceConfig[];\n}\n\nexport interface LocationEventPayload {\n id: string;\n type: GeofenceEventType;\n projectId: string;\n applicationId: string;\n geofenceId: string;\n locationId: string;\n deviceId: string;\n timestamp: string;\n latitude: number;\n longitude: number;\n metadata: Record<string, string>;\n sdkVersion: string;\n attempts: number;\n}\n\nexport interface ExperienceDeliveryPayload {\n type: \"location.experience\";\n deliveryId: string;\n campaignId: string;\n experience: {\n id: string;\n type: ExperienceType;\n title: string;\n body: string;\n imageUrl: string | null;\n cardNavigationUrl: string | null;\n ctaLabel: string | null;\n ctaUrl: string | null;\n style: ExperienceStyle | null;\n };\n event: {\n id: string;\n type: GeofenceEventType;\n geofenceId: string;\n timestamp: string;\n };\n}\n","import { APPLICATION_ID_HEADER, SDK_ENDPOINTS, SDK_KEY_HEADER } from \"../protocol\";\nimport type { DevicePlatform, ExperienceDeliveryPayload, LocationEventPayload, SdkProjectConfig } from \"../protocol\";\n\nexport interface DeviceIdentity {\n deviceId: string;\n platform: DevicePlatform;\n sdkVersion: string;\n}\n\n/**\n * Without this, a request to an unresponsive/hung backend (e.g. a local\n * emulator that was restarted mid-request, or a real network stall) would\n * hang forever — `fetch` has no default timeout. That leaves an event\n * stuck `pending` indefinitely: `SyncManager.attemptDelivery`'s retry/backoff\n * logic only runs once a request actually *fails*, so a request that never\n * settles at all never gets retried either. Aborting after this long\n * converts \"hung forever\" into \"failed now,\" which the existing queue\n * already knows how to handle.\n */\nconst REQUEST_TIMEOUT_MS = 15_000;\n\nasync function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/**\n * A non-2xx response already throws a clear, endpoint-named error below —\n * but `fetch` itself rejecting (offline, DNS failure, or `fetchWithTimeout`'s\n * own abort) previously propagated as a bare, unattributed error like\n * \"Aborted\" or \"Network request failed\", with nothing indicating which SDK\n * call it came from (Phase 7 SDK audit finding). This wraps that case with\n * the same \"which call failed\" context the HTTP-error path already has,\n * without changing what the underlying error actually was — `error.message`\n * is still included verbatim.\n */\nasync function fetchOrThrow(callName: string, url: string, init: RequestInit): Promise<Response> {\n try {\n return await fetchWithTimeout(url, init);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`${callName} request failed: ${message}`);\n }\n}\n\n/**\n * Named `firebase/` per this project's spec (\"communication with GeoPulse\n * backend\"), but deliberately does NOT use the Firebase client SDK — it\n * talks to the Cloud Functions HTTPS endpoints over plain `fetch` (built\n * into both React Native's JS runtime and Node 18+, so this file is\n * testable without any RN runtime at all). That keeps the SDK's dependency\n * footprint small and means device requests carry only the app-scoped SDK\n * key, never a Firebase credential of any kind.\n */\nexport class ApiClient {\n constructor(\n private readonly baseUrl: string,\n private readonly sdkKey: string,\n /** Optional so existing callers keep working; when set, the backend rejects a key bound to a different application. */\n private readonly applicationId?: string,\n ) {}\n\n private headers(json = false): Record<string, string> {\n return {\n [SDK_KEY_HEADER]: this.sdkKey,\n ...(this.applicationId ? { [APPLICATION_ID_HEADER]: this.applicationId } : {}),\n ...(json ? { \"Content-Type\": \"application/json\" } : {}),\n };\n }\n\n /**\n * `identity` doubles as this device's registration/heartbeat (see\n * functions/src/events/get-project-config.ts) — sent as query params\n * since GET requests carry no body.\n */\n async getProjectConfig(identity: DeviceIdentity): Promise<SdkProjectConfig> {\n const query = new URLSearchParams({\n deviceId: identity.deviceId,\n platform: identity.platform,\n sdkVersion: identity.sdkVersion,\n });\n const response = await fetchOrThrow(\"getProjectConfig\", `${this.baseUrl}/${SDK_ENDPOINTS.GET_PROJECT_CONFIG}?${query.toString()}`, {\n method: \"GET\",\n headers: this.headers(),\n });\n if (!response.ok) {\n throw new Error(`getProjectConfig failed with status ${response.status}`);\n }\n return (await response.json()) as SdkProjectConfig;\n }\n\n async ingestEvent(payload: LocationEventPayload): Promise<{ id: string; deduplicated: boolean }> {\n const response = await fetchOrThrow(\"ingestLocationEvent\", `${this.baseUrl}/${SDK_ENDPOINTS.INGEST_LOCATION_EVENT}`, {\n method: \"POST\",\n headers: this.headers(true),\n body: JSON.stringify(payload),\n });\n if (!response.ok) {\n throw new Error(`ingestLocationEvent failed with status ${response.status}`);\n }\n return (await response.json()) as { id: string; deduplicated: boolean };\n }\n\n /** Polled by GeoPulseSdk's experience-polling loop — see core/geo-pulse-sdk.ts. */\n async getPendingExperiences(deviceId: string): Promise<ExperienceDeliveryPayload[]> {\n const query = new URLSearchParams({ deviceId });\n const response = await fetchOrThrow(\"getPendingExperiences\", `${this.baseUrl}/${SDK_ENDPOINTS.GET_PENDING_EXPERIENCES}?${query.toString()}`, {\n method: \"GET\",\n headers: this.headers(),\n });\n if (!response.ok) {\n throw new Error(`getPendingExperiences failed with status ${response.status}`);\n }\n const body = (await response.json()) as { experiences: ExperienceDeliveryPayload[] };\n return body.experiences;\n }\n\n /** Confirms a delivery was surfaced via `experience.received`, so it stops appearing in future polls. */\n async acknowledgeExperienceDelivery(deliveryId: string): Promise<void> {\n const response = await fetchOrThrow(\"acknowledgeExperienceDelivery\", `${this.baseUrl}/${SDK_ENDPOINTS.ACKNOWLEDGE_EXPERIENCE_DELIVERY}`, {\n method: \"POST\",\n headers: this.headers(true),\n body: JSON.stringify({ deliveryId }),\n });\n if (!response.ok) {\n throw new Error(`acknowledgeExperienceDelivery failed with status ${response.status}`);\n }\n }\n}\n","import type { GeofenceEventType, LocationEventPayload, SdkGeofenceConfig } from \"../protocol\";\n\nimport { SDK_VERSION } from \"../config/types\";\nimport { generateEventId } from \"./uuid\";\n\nexport function createEventPayload(params: {\n type: GeofenceEventType;\n projectId: string;\n applicationId: string;\n geofence: SdkGeofenceConfig;\n location: { latitude: number; longitude: number };\n deviceId: string;\n metadata?: Record<string, string>;\n}): LocationEventPayload {\n return {\n id: generateEventId(),\n type: params.type,\n projectId: params.projectId,\n applicationId: params.applicationId,\n geofenceId: params.geofence.id,\n locationId: params.geofence.locationId,\n deviceId: params.deviceId,\n timestamp: new Date().toISOString(),\n latitude: params.location.latitude,\n longitude: params.location.longitude,\n metadata: params.metadata ?? {},\n sdkVersion: SDK_VERSION,\n // First-try attempt count; SyncManager overwrites this with the real\n // count if the event ends up going through the retry queue (see\n // sync/sync-manager.ts#retryPending) — that's what makes the\n // \"Attempts\" column in the admin's Event Explorer meaningful.\n attempts: 1,\n };\n}\n","import { calculateDistanceMeters } from \"../protocol\";\nimport type { GeofenceEventType, SdkGeofenceConfig } from \"../protocol\";\n\nexport type GeofenceMembership = \"INSIDE\" | \"OUTSIDE\";\n\nexport interface GeofenceTransition {\n geofence: SdkGeofenceConfig;\n type: GeofenceEventType;\n}\n\n/**\n * Tracks INSIDE/OUTSIDE membership per geofence and emits a transition only\n * when membership actually changes:\n *\n * OUTSIDE -> INSIDE = geofence.enter\n * INSIDE -> OUTSIDE = geofence.exit\n * INSIDE -> INSIDE = nothing\n * OUTSIDE -> OUTSIDE = nothing\n *\n * A geofence with no prior recorded state is treated as OUTSIDE (so the\n * very first location update that lands inside a geofence correctly fires\n * one `geofence.enter`, not silence). State survives across repeated\n * `evaluate()` calls precisely so a location update that keeps the device\n * inside the same geofence produces no duplicate ENTER — that's the whole\n * point of this class existing instead of a stateless distance check.\n */\nexport class GeofenceStateMachine {\n private readonly membershipByGeofenceId = new Map<string, GeofenceMembership>();\n\n evaluate(location: { latitude: number; longitude: number }, geofences: SdkGeofenceConfig[]): GeofenceTransition[] {\n const transitions: GeofenceTransition[] = [];\n const activeIds = new Set<string>();\n\n for (const geofence of geofences) {\n activeIds.add(geofence.id);\n\n const distance = calculateDistanceMeters(location, geofence.center);\n const nextMembership: GeofenceMembership = distance <= geofence.radiusMeters ? \"INSIDE\" : \"OUTSIDE\";\n const previousMembership = this.membershipByGeofenceId.get(geofence.id) ?? \"OUTSIDE\";\n\n if (previousMembership !== nextMembership) {\n transitions.push({\n geofence,\n type: nextMembership === \"INSIDE\" ? \"geofence.enter\" : \"geofence.exit\",\n });\n }\n\n this.membershipByGeofenceId.set(geofence.id, nextMembership);\n }\n\n // Drop bookkeeping for geofences no longer in the active config (e.g.\n // deleted/disabled since the last `getProjectConfig` fetch) so it can't\n // silently grow forever or produce a stale transition if the same\n // geofence id ever reappears later.\n for (const id of this.membershipByGeofenceId.keys()) {\n if (!activeIds.has(id)) {\n this.membershipByGeofenceId.delete(id);\n }\n }\n\n return transitions;\n }\n\n getMembership(geofenceId: string): GeofenceMembership {\n return this.membershipByGeofenceId.get(geofenceId) ?? \"OUTSIDE\";\n }\n\n reset(): void {\n this.membershipByGeofenceId.clear();\n }\n}\n","import type { LocationProvider, LocationUpdate } from \"./types\";\n\n/**\n * The mandatory simulation provider (spec section 7) — lets the geofence\n * flow be exercised end to end without physically travelling anywhere.\n * `setLocation` is the playground simulator screen's only way to move the\n * \"user\"; everything downstream (geofence engine, event generation, sync)\n * runs exactly as it would for a real device update, because both\n * providers speak the same `LocationProvider` interface.\n */\nexport class SimulatedLocationProvider implements LocationProvider {\n private onUpdate: ((location: LocationUpdate) => void) | null = null;\n private currentLocation: LocationUpdate | null = null;\n\n start(onUpdate: (location: LocationUpdate) => void): void {\n this.onUpdate = onUpdate;\n // If a location was already set before `start()` (e.g. the simulator\n // screen was positioned before pressing \"Start\"), replay it immediately\n // rather than waiting for the next `setLocation` call.\n if (this.currentLocation) {\n onUpdate(this.currentLocation);\n }\n }\n\n stop(): void {\n this.onUpdate = null;\n }\n\n setLocation(coords: { latitude: number; longitude: number }): void {\n const location: LocationUpdate = { ...coords, timestamp: new Date().toISOString() };\n this.currentLocation = location;\n this.onUpdate?.(location);\n }\n\n getCurrentLocation(): LocationUpdate | null {\n return this.currentLocation;\n }\n}\n","import type { PermissionProvider, PermissionStatus } from \"./types\";\n\n/**\n * Used with the simulated location provider (see ../location) — a\n * simulated user obviously isn't gated by the OS location permission\n * dialog, so this always reports \"granted\" rather than requiring a real\n * device's permission state to test the geofence flow end to end.\n */\nexport class SimulatedPermissionProvider implements PermissionProvider {\n async getStatus(): Promise<PermissionStatus> {\n return \"granted\";\n }\n\n async request(): Promise<PermissionStatus> {\n return \"granted\";\n }\n}\n","/**\n * Centralized so retry behavior can be tuned in one place instead of\n * scattered magic numbers — see spec section 4 (\"Do not hard-code these\n * values throughout the codebase\").\n */\nexport const RETRY_POLICY = {\n /** Attempts beyond this stop increasing the delay — it plateaus at MAX_RETRY_DELAY_MS, but retrying never stops entirely (the SDK must never give up on an event; see storage/queue.ts). */\n MAX_RETRIES: 6,\n BASE_RETRY_DELAY_MS: 5_000,\n MAX_RETRY_DELAY_MS: 5 * 60_000,\n} as const;\n\n/**\n * Exponential backoff with a ceiling: attempt 1 -> ~5s, 2 -> ~10s, 3 -> ~20s,\n * ... capped at MAX_RETRY_DELAY_MS once `attempts` passes MAX_RETRIES. A\n * small jitter (±20%) avoids every queued event on a device — or every\n * device that lost connectivity at the same moment — retrying in lockstep.\n */\nexport function computeRetryDelayMs(attempts: number): number {\n const cappedAttempts = Math.min(attempts, RETRY_POLICY.MAX_RETRIES);\n const exponential = RETRY_POLICY.BASE_RETRY_DELAY_MS * 2 ** Math.max(0, cappedAttempts - 1);\n const delay = Math.min(exponential, RETRY_POLICY.MAX_RETRY_DELAY_MS);\n const jitter = delay * 0.2 * (Math.random() * 2 - 1);\n return Math.round(delay + jitter);\n}\n","import type { EventQueueStore, QueuedEvent } from \"./types\";\n\n/** Default store — also what unit tests use, since it needs no RN runtime. */\nexport class InMemoryEventQueueStore implements EventQueueStore {\n private readonly eventsById = new Map<string, QueuedEvent>();\n\n async getAll(): Promise<QueuedEvent[]> {\n return [...this.eventsById.values()];\n }\n\n async save(event: QueuedEvent): Promise<void> {\n this.eventsById.set(event.eventId, event);\n }\n\n async remove(eventId: string): Promise<void> {\n this.eventsById.delete(eventId);\n }\n}\n","import type { LocationEventPayload } from \"../protocol\";\n\nimport { computeRetryDelayMs } from \"../sync/retry-policy\";\nimport { InMemoryEventQueueStore } from \"./memory-store\";\nimport type { EventQueueStore, QueuedEvent } from \"./types\";\n\n/**\n * The local pending-event queue (spec: \"Offline queue\" / \"reliable SDK\n * event queue\"). An event is only ever in this queue between a failed send\n * attempt and a successful retry — there's no \"delivered\" status because a\n * delivered event is simply removed (`markSent`), which is also what makes\n * retrying an already-sent event harmless: it's not in the queue to retry.\n */\nexport class PendingEventQueue {\n constructor(private readonly store: EventQueueStore = new InMemoryEventQueueStore()) {}\n\n async enqueue(payload: LocationEventPayload): Promise<void> {\n await this.store.save({\n eventId: payload.id,\n type: payload.type,\n applicationId: payload.applicationId,\n deviceId: payload.deviceId,\n locationId: payload.locationId,\n geofenceId: payload.geofenceId,\n timestamp: payload.timestamp,\n payload,\n attempts: 0,\n status: \"pending\",\n createdAt: new Date().toISOString(),\n lastAttemptAt: null,\n nextAttemptAt: null,\n });\n }\n\n /** Every queued event, regardless of retry timing — used for diagnostics/pending counts. */\n async listPending(): Promise<QueuedEvent[]> {\n return this.store.getAll();\n }\n\n /** Only events whose backoff window has elapsed (or that have never been attempted) — used by SyncManager.retryPending(). */\n async listReadyForRetry(now: Date = new Date()): Promise<QueuedEvent[]> {\n const all = await this.store.getAll();\n return all.filter(\n (event) => event.status !== \"sending\" && (!event.nextAttemptAt || new Date(event.nextAttemptAt) <= now),\n );\n }\n\n /** Marks an event as in-flight so a concurrent retry pass won't also pick it up. */\n async markSending(eventId: string): Promise<void> {\n const existing = await this.get(eventId);\n if (!existing) return;\n await this.store.save({ ...existing, status: \"sending\" });\n }\n\n async markAttemptFailed(eventId: string): Promise<void> {\n const existing = await this.get(eventId);\n if (!existing) return;\n const attempts = existing.attempts + 1;\n const now = new Date();\n await this.store.save({\n ...existing,\n status: \"failed\",\n attempts,\n lastAttemptAt: now.toISOString(),\n nextAttemptAt: new Date(now.getTime() + computeRetryDelayMs(attempts)).toISOString(),\n });\n }\n\n async markSent(eventId: string): Promise<void> {\n await this.store.remove(eventId);\n }\n\n private async get(eventId: string): Promise<QueuedEvent | undefined> {\n return (await this.store.getAll()).find((event) => event.eventId === eventId);\n }\n}\n","export type NetworkStatus = \"online\" | \"offline\" | \"unknown\";\n\nexport interface NetworkMonitor {\n getStatus(): NetworkStatus;\n /** Calls `listener` whenever connectivity changes. Returns an unsubscribe function. */\n subscribe(listener: (status: NetworkStatus) => void): () => void;\n}\n\n/**\n * Default/test implementation — starts \"online\" (optimistic default: if we\n * can't observe connectivity, assume the network call itself is the\n * ultimate source of truth rather than blocking on a monitor). Tests (and\n * the Node/non-RN environment generally) drive it explicitly via\n * `setStatus`; see `sync/netinfo-network-monitor.ts` for the real RN\n * implementation, kept in its own file for the same reason\n * `storage/async-storage-store.ts` is — see that file's comment.\n */\nexport class ManualNetworkMonitor implements NetworkMonitor {\n private status: NetworkStatus = \"online\";\n private readonly listeners = new Set<(status: NetworkStatus) => void>();\n\n getStatus(): NetworkStatus {\n return this.status;\n }\n\n setStatus(status: NetworkStatus): void {\n if (status === this.status) return;\n this.status = status;\n this.listeners.forEach((listener) => listener(status));\n }\n\n subscribe(listener: (status: NetworkStatus) => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n}\n","import type { LocationEventPayload } from \"../protocol\";\n\nimport type { ApiClient } from \"../firebase/api-client\";\nimport type { Logger } from \"../core/logger\";\nimport type { PendingEventQueue } from \"../storage/queue\";\n\nconst DEFAULT_RETRY_INTERVAL_MS = 30_000;\n\n/**\n * Only the one method SyncManager actually calls — `Pick` rather than the\n * full `ApiClient` class specifically so tests can pass a plain object\n * (`{ ingestEvent: vi.fn() }`) instead of a real ApiClient instance. A\n * class's private fields make it nominally typed, so a plain object\n * wouldn't satisfy `ApiClient` itself even with matching public methods.\n */\ntype EventIngestionClient = Pick<ApiClient, \"ingestEvent\">;\n\nexport interface SyncResult {\n attempted: number;\n delivered: number;\n failed: number;\n}\n\n/**\n * Send-or-queue plus backoff-aware retry (spec: \"Event retry system\").\n * `retryPending()` only attempts events whose backoff window has elapsed\n * (see PendingEventQueue.listReadyForRetry/RETRY_POLICY) — it's still safe\n * to call as often as you like (on a timer, on reconnect, or manually);\n * events not yet due just get skipped this pass. A successful resend of an\n * already-delivered event is also a no-op server-side, since\n * `ingestLocationEvent` dedupes on the event's own id — so calling this\n * concurrently with an in-flight `sendOrQueue` is never harmful, only\n * occasionally redundant.\n */\nexport class SyncManager {\n private retryTimer: ReturnType<typeof setInterval> | null = null;\n private lastSyncAt: string | null = null;\n private lastUploadResult: \"success\" | \"failure\" | null = null;\n\n constructor(\n private readonly apiClient: EventIngestionClient,\n private readonly queue: PendingEventQueue,\n private readonly retryIntervalMs: number = DEFAULT_RETRY_INTERVAL_MS,\n private readonly logger?: Logger,\n ) {}\n\n /**\n * Write-ahead: the event is durably queued *before* any network attempt is\n * made, so an app kill mid-request can never lose it (spec: \"true\n * write-ahead event persistence\" — a prior version of this method tried\n * the network first and only queued on failure, which meant an event in\n * flight when the process died was gone for good). `attemptDelivery` then\n * runs the exact same success/failure bookkeeping a queued retry uses, so\n * there is only one code path for \"what happens when an upload settles.\"\n */\n async sendOrQueue(payload: LocationEventPayload): Promise<void> {\n await this.queue.enqueue(payload);\n await this.attemptDelivery(payload.id, payload);\n }\n\n /**\n * Attempts queued events. By default only ones whose backoff window has\n * elapsed (the timer-driven path, `startAutoRetry` — the whole point of\n * backoff is to not hammer the backend on a fixed sweep). Pass\n * `{ force: true }` to ignore that window and attempt every non-in-flight\n * queued event regardless — connectivity just being confirmed restored\n * (see `GeoPulseSdk.start`'s network-monitor subscription) is a stronger\n * signal than \"some fixed delay has passed,\" and a device that was\n * offline for a while shouldn't have to wait out a stale backoff timer\n * once it's actually back online.\n */\n async retryPending(options: { force?: boolean } = {}): Promise<SyncResult> {\n const ready = options.force\n ? (await this.queue.listPending()).filter((event) => event.status !== \"sending\")\n : await this.queue.listReadyForRetry();\n const result: SyncResult = { attempted: ready.length, delivered: 0, failed: 0 };\n\n for (const event of ready) {\n const attemptNumber = event.attempts + 1;\n // Reflects the true attempt count server-side (spec: Event Explorer's \"Attempts\" column) — the\n // payload as originally generated always says 1, since it doesn't yet know it'll be retried.\n const delivered = await this.attemptDelivery(event.eventId, { ...event.payload, attempts: attemptNumber });\n if (delivered) {\n result.delivered++;\n } else {\n result.failed++;\n }\n }\n\n return result;\n }\n\n /**\n * The single place that actually talks to the backend: marks the queued\n * record \"sending\" (so a concurrent retry pass skips it), attempts the\n * upload, and resolves it to either removed-from-queue (success — a\n * `deduplicated: true` response still counts, since it means the backend\n * already has this event id, which is exactly the outcome a retry is\n * supposed to produce) or a scheduled retry (failure). Returns whether it\n * was delivered, purely so `retryPending` can tally its `SyncResult`.\n */\n private async attemptDelivery(eventId: string, payload: LocationEventPayload): Promise<boolean> {\n await this.queue.markSending(eventId);\n this.logger?.log(\"Event upload started\", { eventId, attempt: payload.attempts });\n try {\n await this.apiClient.ingestEvent(payload);\n await this.queue.markSent(eventId);\n this.lastSyncAt = new Date().toISOString();\n this.lastUploadResult = \"success\";\n this.logger?.log(\"Event upload succeeded\", { eventId });\n return true;\n } catch (error) {\n await this.queue.markAttemptFailed(eventId);\n this.lastUploadResult = \"failure\";\n this.logger?.warn(\"Event upload failed — retry scheduled\", { eventId, error: String(error) });\n return false;\n }\n }\n\n startAutoRetry(): void {\n if (this.retryTimer) return;\n this.retryTimer = setInterval(() => {\n void this.retryPending();\n }, this.retryIntervalMs);\n }\n\n stopAutoRetry(): void {\n if (this.retryTimer) {\n clearInterval(this.retryTimer);\n this.retryTimer = null;\n }\n }\n\n getLastSyncAt(): string | null {\n return this.lastSyncAt;\n }\n\n getLastUploadResult(): \"success\" | \"failure\" | null {\n return this.lastUploadResult;\n }\n}\n","/**\n * Structured, easy-to-disable debug logging (spec section 22). Every call\n * site passes a short lifecycle label matching the examples in this\n * package's README (\"Configuration synced\", \"Event queued\", ...) plus\n * optional structured details — never the SDK key, never a raw event\n * payload body, never anything from `GeoPulseConfig` beyond `applicationId`.\n */\nexport class Logger {\n constructor(private enabled: boolean) {}\n\n setEnabled(enabled: boolean): void {\n this.enabled = enabled;\n }\n\n log(message: string, details?: Record<string, unknown>): void {\n if (!this.enabled) return;\n if (details) {\n console.log(`[GeoPulse] ${message}`, details);\n } else {\n console.log(`[GeoPulse] ${message}`);\n }\n }\n\n warn(message: string, details?: Record<string, unknown>): void {\n if (!this.enabled) return;\n if (details) {\n console.warn(`[GeoPulse] ${message}`, details);\n } else {\n console.warn(`[GeoPulse] ${message}`);\n }\n }\n}\n","import type {\n DevicePlatform,\n ExperienceDeliveryPayload,\n GeofenceEventType,\n LocationEventPayload,\n SdkGeofenceConfig,\n SdkProjectConfig,\n} from \"../protocol\";\n\nimport {\n ConfigCache,\n DEFAULT_BACKEND_URL,\n InMemoryKeyValueStore,\n getOrCreateDeviceId,\n SDK_VERSION,\n type GeoPulseConfig,\n type KeyValueStore,\n} from \"../config\";\nimport { ApiClient } from \"../firebase/api-client\";\nimport { createEventPayload } from \"../events/factory\";\nimport { GeofenceStateMachine } from \"../geofencing/state-machine\";\nimport type { LocationProvider, LocationUpdate } from \"../location/types\";\nimport { SimulatedLocationProvider } from \"../location/simulated-location-provider\";\nimport type { PermissionProvider, PermissionStatus } from \"../permissions/types\";\nimport { SimulatedPermissionProvider } from \"../permissions/simulated-permission-provider\";\nimport { PendingEventQueue } from \"../storage/queue\";\nimport { ManualNetworkMonitor, type NetworkMonitor } from \"../sync/network-monitor\";\nimport { SyncManager } from \"../sync/sync-manager\";\nimport { Logger } from \"./logger\";\nimport type { ConfigurationStatus, DiagnosticsSnapshot, SdkStatus } from \"./types\";\n\ntype EventListener = (event: LocationEventPayload) => void;\ntype ExperienceListener = (payload: ExperienceDeliveryPayload) => void;\n\n/** Mirrors SyncManager's own default retry interval — see sync/sync-manager.ts's DEFAULT_RETRY_INTERVAL_MS. A host app that wants faster demo feedback (see apps/playground) can override via GeoPulseConfig.experiencePollIntervalMs. */\n/** The SDK key resolved to a different application than `config.applicationId`. */\nexport class ApplicationMismatchError extends Error {\n constructor() {\n super(\"GeoPulse: the SDK key belongs to a different application than config.applicationId — check both values\");\n this.name = \"ApplicationMismatchError\";\n }\n}\n\nconst DEFAULT_EXPERIENCE_POLL_INTERVAL_MS = 30_000;\n/** Floor for any caller-supplied interval (retryIntervalMs/experiencePollIntervalMs) — a config of `0` or a negative number would otherwise turn into a tight, runaway retry/poll loop (Phase 7 SDK audit finding). */\nconst MIN_TIMER_INTERVAL_MS = 1_000;\n\n/** `undefined` passes through untouched (lets the caller's own default apply); a defined-but-too-small value is floored instead of silently creating a runaway timer. */\nfunction clampIntervalMs(value: number | undefined): number | undefined {\n if (value === undefined) {\n return undefined;\n }\n if (!Number.isFinite(value) || value < MIN_TIMER_INTERVAL_MS) {\n return MIN_TIMER_INTERVAL_MS;\n }\n return value;\n}\n\n/**\n * Everything the SDK talks to outside of itself, injectable so\n * `GeoPulseSdk` (this whole lifecycle) is unit-testable without an RN\n * runtime — tests construct it with fakes; `src/index.ts` (the real public\n * entrypoint) constructs the default instance with the real RN adapters.\n */\nexport interface GeoPulseSdkDependencies {\n locationProvider?: LocationProvider;\n permissionProvider?: PermissionProvider;\n queue?: PendingEventQueue;\n createApiClient?: (config: GeoPulseConfig) => ApiClient;\n deviceIdStore?: KeyValueStore;\n configCacheStore?: KeyValueStore;\n networkMonitor?: NetworkMonitor;\n /** Defaults to \"unknown\" — src/index.ts supplies the real `Platform.OS` so this file stays RN-free. */\n getPlatform?: () => DevicePlatform;\n /** Overrides GeoPulseConfig.debug — mainly for tests that want to assert on log output. */\n debug?: boolean;\n}\n\nexport class GeoPulseSdk {\n private status: SdkStatus = \"idle\";\n private config: GeoPulseConfig | null = null;\n private apiClient: ApiClient | null = null;\n private syncManager: SyncManager | null = null;\n private resolvedProjectId: string | null = null;\n private resolvedApplicationId: string | null = null;\n private applicationEnabled = true;\n private configVersion: string | null = null;\n private configurationStatus: ConfigurationStatus = \"unsynced\";\n private geofences: SdkGeofenceConfig[] = [];\n private lastKnownLocation: LocationUpdate | null = null;\n private lastEvent: LocationEventPayload | null = null;\n private deviceId: string | null = null;\n private locationError: string | null = null;\n private unsubscribeNetwork: (() => void) | null = null;\n private readonly stateMachine = new GeofenceStateMachine();\n private readonly listeners = new Map<GeofenceEventType, Set<EventListener>>();\n private readonly experienceListeners = new Set<ExperienceListener>();\n /** 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. */\n private readonly seenExperienceDeliveryIds = new Set<string>();\n private experiencePollTimer: ReturnType<typeof setInterval> | null = null;\n private experiencesReceivedCount = 0;\n private readonly logger: Logger;\n\n private readonly locationProvider: LocationProvider;\n private readonly permissionProvider: PermissionProvider;\n private readonly queue: PendingEventQueue;\n private readonly buildApiClient: (config: GeoPulseConfig) => ApiClient;\n private readonly deviceIdStore: KeyValueStore;\n private readonly configCache: ConfigCache;\n private readonly networkMonitor: NetworkMonitor;\n private readonly getPlatform: () => DevicePlatform;\n private experiencePollIntervalMs = DEFAULT_EXPERIENCE_POLL_INTERVAL_MS;\n\n constructor(dependencies: GeoPulseSdkDependencies = {}) {\n this.locationProvider = dependencies.locationProvider ?? new SimulatedLocationProvider();\n this.permissionProvider = dependencies.permissionProvider ?? new SimulatedPermissionProvider();\n this.queue = dependencies.queue ?? new PendingEventQueue();\n this.buildApiClient =\n dependencies.createApiClient ?? ((config) => new ApiClient(config.functionsBaseUrl || DEFAULT_BACKEND_URL, config.apiKey, config.applicationId));\n this.deviceIdStore = dependencies.deviceIdStore ?? new InMemoryKeyValueStore();\n this.configCache = new ConfigCache(dependencies.configCacheStore ?? new InMemoryKeyValueStore());\n this.networkMonitor = dependencies.networkMonitor ?? new ManualNetworkMonitor();\n this.getPlatform = dependencies.getPlatform ?? (() => \"unknown\");\n this.logger = new Logger(dependencies.debug ?? false);\n }\n\n /**\n * Resolves this device's persistent id, then fetches (or falls back to\n * cached) project configuration. Must succeed — fresh or cached — before\n * `start()`; if neither is available (first-ever run with no network),\n * this rethrows and `getStatus()` reports \"error\".\n */\n async initialize(config: GeoPulseConfig): Promise<void> {\n // Without this guard, calling initialize() while already \"running\" replaced\n // this.syncManager with a fresh instance, orphaning the old one's auto-retry timer (it kept\n // firing forever with no reference left to stop it) while location callbacks from the\n // still-running locationProvider could race against the new config being applied — a real\n // bug found in Phase 7's SDK audit, not a hypothetical. Re-initializing a live SDK has no\n // safe, well-defined meaning short of a full stop() first.\n if (this.status === \"running\") {\n throw new Error(\"GeoPulse.initialize() cannot be called while already running — call GeoPulse.stop() first\");\n }\n if (this.status === \"initializing\") {\n throw new Error(\"GeoPulse.initialize() is already in progress — wait for the first call to resolve\");\n }\n if (!config.apiKey || !config.applicationId) {\n throw new Error(\"GeoPulse.initialize requires both apiKey and applicationId\");\n }\n\n if (config.debug !== undefined) {\n this.logger.setEnabled(config.debug);\n }\n this.logger.log(\"GeoPulse.initialize()\", { applicationId: config.applicationId });\n\n this.status = \"initializing\";\n this.config = config;\n this.experiencePollIntervalMs = clampIntervalMs(config.experiencePollIntervalMs) ?? DEFAULT_EXPERIENCE_POLL_INTERVAL_MS;\n this.apiClient = this.buildApiClient(config);\n this.syncManager = new SyncManager(this.apiClient, this.queue, clampIntervalMs(config.retryIntervalMs), this.logger);\n this.deviceId = await getOrCreateDeviceId(this.deviceIdStore);\n\n try {\n const projectConfig = await this.apiClient.getProjectConfig({\n deviceId: this.deviceId,\n platform: this.getPlatform(),\n sdkVersion: SDK_VERSION,\n });\n // The key must belong to the application the integrator configured; never adopt another one.\n if (projectConfig.applicationId !== config.applicationId) {\n throw new ApplicationMismatchError();\n }\n this.applyProjectConfig(projectConfig);\n this.configurationStatus = \"synced\";\n await this.configCache.save(projectConfig);\n this.status = \"initialized\";\n this.logger.log(\"Configuration synced\", { geofenceCount: projectConfig.geofences.length, configVersion: projectConfig.configVersion });\n } catch (error) {\n if (error instanceof ApplicationMismatchError) {\n this.configurationStatus = \"unavailable\";\n this.status = \"error\";\n throw error;\n }\n const cached = await this.configCache.load();\n if (cached && cached.config.applicationId === config.applicationId) {\n this.applyProjectConfig(cached.config);\n this.configurationStatus = \"cached\";\n this.status = \"initialized\";\n this.logger.warn(\"getProjectConfig failed — using cached configuration\", { fetchedAt: cached.fetchedAt });\n return;\n }\n this.configurationStatus = \"unavailable\";\n this.status = \"error\";\n throw error;\n }\n }\n\n async start(): Promise<void> {\n if (this.status === \"running\") {\n // Previously fell into the same generic \"called before initialize succeeded\" message below\n // even though initialize DID succeed and the SDK IS running — factually wrong and would\n // send a developer debugging a double-start() down the wrong path (Phase 7 SDK audit).\n throw new Error(\"GeoPulse.start() called while already running — call GeoPulse.stop() first if you want to restart\");\n }\n if (this.status !== \"initialized\" && this.status !== \"stopped\") {\n throw new Error(\"GeoPulse.start called before GeoPulse.initialize succeeded\");\n }\n if (!this.applicationEnabled) {\n this.status = \"error\";\n throw new Error(\"This application has been disabled in the GeoPulse admin portal\");\n }\n\n const permission = await this.permissionProvider.request();\n if (permission !== \"granted\") {\n this.status = \"error\";\n throw new Error(`Location permission not granted (status: ${permission})`);\n }\n\n this.locationError = null;\n await this.locationProvider.start(\n (location) => {\n void this.handleLocationUpdate(location);\n },\n (error) => {\n this.locationError = error.message;\n this.logger.warn(\"Location provider failed to start\", { error: error.message });\n },\n );\n if (this.locationError) {\n // The provider caught its own failure and reported it via onError rather than rejecting\n // start() directly (see LocationProvider.start's doc) — surfaced here as a normal rejection\n // so callers get the same \"start() throws on failure\" contract as the permission-denied case.\n this.status = \"error\";\n throw new Error(`GeoPulse failed to start location tracking: ${this.locationError}`);\n }\n\n this.unsubscribeNetwork = this.networkMonitor.subscribe((networkStatus) => {\n if (networkStatus === \"online\") {\n this.logger.log(\"Network available — retrying queued events\");\n void this.syncManager?.retryPending({ force: true });\n }\n });\n\n this.syncManager?.startAutoRetry();\n this.startExperiencePolling();\n this.status = \"running\";\n this.logger.log(\"GeoPulse.start()\");\n }\n\n stop(): void {\n this.locationProvider.stop();\n this.syncManager?.stopAutoRetry();\n this.stopExperiencePolling();\n this.unsubscribeNetwork?.();\n this.unsubscribeNetwork = null;\n this.status = \"stopped\";\n this.logger.log(\"GeoPulse.stop()\");\n }\n\n getStatus(): SdkStatus {\n return this.status;\n }\n\n async getPermissionStatus(): Promise<PermissionStatus> {\n return this.permissionProvider.getStatus();\n }\n\n on(eventName: GeofenceEventType, callback: EventListener): void;\n on(eventName: \"experience.received\", callback: ExperienceListener): void;\n on(eventName: GeofenceEventType | \"experience.received\", callback: EventListener | ExperienceListener): void {\n if (eventName === \"experience.received\") {\n this.experienceListeners.add(callback as ExperienceListener);\n return;\n }\n if (!this.listeners.has(eventName)) {\n this.listeners.set(eventName, new Set());\n }\n this.listeners.get(eventName)?.add(callback as EventListener);\n }\n\n off(eventName: GeofenceEventType, callback: EventListener): void;\n off(eventName: \"experience.received\", callback: ExperienceListener): void;\n off(eventName: GeofenceEventType | \"experience.received\", callback: EventListener | ExperienceListener): void {\n if (eventName === \"experience.received\") {\n this.experienceListeners.delete(callback as ExperienceListener);\n return;\n }\n this.listeners.get(eventName)?.delete(callback as EventListener);\n }\n\n // --- Small read-only accessors beyond the core 7-method API, so a host\n // app (see apps/playground) can build a status UI around the SDK without\n // it needing to duplicate state the SDK already tracks internally. ---\n\n getConfiguredGeofences(): SdkGeofenceConfig[] {\n return this.geofences;\n }\n\n /** The server-resolved application id (from `getProjectConfig`), or null before `initialize()` completes. */\n getApplicationId(): string | null {\n return this.resolvedApplicationId;\n }\n\n getLastKnownLocation(): LocationUpdate | null {\n return this.lastKnownLocation;\n }\n\n async getPendingEventCount(): Promise<number> {\n return (await this.queue.listPending()).length;\n }\n\n /** Null until `initialize()` resolves it — persisted across app restarts (see config/device-id.ts). */\n getDeviceId(): string | null {\n return this.deviceId;\n }\n\n /**\n * The rich developer-diagnostics snapshot (spec section 10/11) — a\n * superset of `getStatus()`, not a replacement for it (see core/types.ts's\n * `DiagnosticsSnapshot` doc for why these are two separate methods).\n */\n async getDiagnostics(): Promise<DiagnosticsSnapshot> {\n const pending = await this.queue.listPending();\n const permissionStatus = this.status === \"idle\" || this.status === \"initializing\" ? \"unknown\" : await this.permissionProvider.getStatus();\n\n return {\n initialized: this.status !== \"idle\" && this.status !== \"initializing\",\n running: this.status === \"running\",\n locationAvailable: this.lastKnownLocation !== null,\n permissionStatus,\n networkStatus: this.networkMonitor.getStatus(),\n configurationStatus: this.configurationStatus,\n configVersion: this.configVersion,\n pendingEvents: pending.length,\n totalRetryAttempts: pending.reduce((sum, event) => sum + event.attempts, 0),\n locationError: this.locationError,\n lastEventAt: this.lastEvent?.timestamp ?? null,\n lastEventType: this.lastEvent?.type ?? null,\n lastSyncAt: this.syncManager?.getLastSyncAt() ?? null,\n lastUploadResult: this.syncManager?.getLastUploadResult() ?? null,\n deviceId: this.deviceId,\n sdkVersion: SDK_VERSION,\n experiencesReceivedCount: this.experiencesReceivedCount,\n };\n }\n\n /**\n * Only functional when the SDK is running the simulated location\n * provider (the default outside of production builds — see\n * src/index.ts) and gated by `__DEV__` so it's inert in a production\n * bundle even if a caller somehow held a reference to it.\n */\n simulateLocation(coords: { latitude: number; longitude: number }): void {\n const isDev = typeof __DEV__ === \"undefined\" ? true : __DEV__;\n if (!isDev) {\n console.warn(\"GeoPulse.simulator is unavailable in production builds.\");\n return;\n }\n if (!(this.locationProvider instanceof SimulatedLocationProvider)) {\n console.warn(\n \"GeoPulse.simulator.setLocation has no effect: the SDK is not running the simulated location provider.\",\n );\n return;\n }\n this.locationProvider.setLocation(coords);\n }\n\n /**\n * The Phase 5B delivery mechanism: poll immediately on `start()` (so a\n * demo doesn't wait a full interval for the first check), then on a\n * timer. A poll failure (network blip, backend down) is logged and\n * swallowed — it must never crash the SDK or the host app (spec section\n * 13), and the next tick tries again on its own.\n */\n private startExperiencePolling(): void {\n void this.pollForExperiences();\n this.experiencePollTimer = setInterval(() => {\n void this.pollForExperiences();\n }, this.experiencePollIntervalMs);\n }\n\n private stopExperiencePolling(): void {\n if (this.experiencePollTimer) {\n clearInterval(this.experiencePollTimer);\n this.experiencePollTimer = null;\n }\n }\n\n private async pollForExperiences(): Promise<void> {\n if (!this.apiClient || !this.deviceId) {\n return;\n }\n try {\n const experiences = await this.apiClient.getPendingExperiences(this.deviceId);\n for (const experience of experiences) {\n // Backend is the source of truth (spec section 14) — this is defense in depth against\n // observing the same still-\"pending\" delivery twice across two polls before the ack lands.\n if (this.seenExperienceDeliveryIds.has(experience.deliveryId)) {\n continue;\n }\n this.seenExperienceDeliveryIds.add(experience.deliveryId);\n this.experiencesReceivedCount += 1;\n this.logger.log(\"Experience received\", { deliveryId: experience.deliveryId, campaignId: experience.campaignId });\n this.experienceListeners.forEach((listener) => listener(experience));\n\n await this.apiClient.acknowledgeExperienceDelivery(experience.deliveryId).catch((error) => {\n this.logger.warn(\"Failed to acknowledge experience delivery\", {\n deliveryId: experience.deliveryId,\n error: error instanceof Error ? error.message : String(error),\n });\n });\n }\n } catch (error) {\n this.logger.warn(\"Polling for experiences failed\", { error: error instanceof Error ? error.message : String(error) });\n }\n }\n\n private applyProjectConfig(projectConfig: SdkProjectConfig): void {\n this.resolvedProjectId = projectConfig.projectId;\n this.resolvedApplicationId = projectConfig.applicationId;\n this.applicationEnabled = projectConfig.applicationEnabled;\n this.configVersion = projectConfig.configVersion;\n this.geofences = projectConfig.geofences;\n }\n\n private async handleLocationUpdate(location: LocationUpdate): Promise<void> {\n this.lastKnownLocation = location;\n if (!this.resolvedProjectId || !this.resolvedApplicationId || !this.syncManager || !this.deviceId) {\n return;\n }\n\n const transitions = this.stateMachine.evaluate(location, this.geofences);\n for (const transition of transitions) {\n this.logger.log(\"Geofence state changed\", { geofenceId: transition.geofence.id, type: transition.type });\n\n const event = createEventPayload({\n type: transition.type,\n projectId: this.resolvedProjectId,\n applicationId: this.resolvedApplicationId,\n geofence: transition.geofence,\n location,\n deviceId: this.deviceId,\n });\n this.lastEvent = event;\n this.logger.log(\"Event generated\", { eventId: event.id, type: event.type });\n\n this.listeners.get(transition.type)?.forEach((listener) => listener(event));\n await this.syncManager.sendOrQueue(event);\n }\n }\n}\n","import * as Location from \"expo-location\";\n\nimport type { LocationProvider, LocationUpdate } from \"./types\";\n\n/**\n * Real, foreground-only device location via expo-location. Does not\n * request or use background location — see this package's README for the\n * platform permission requirements and why background tracking is\n * explicitly out of scope for this phase.\n */\nexport class DeviceLocationProvider implements LocationProvider {\n private subscription: Location.LocationSubscription | null = null;\n\n /**\n * `watchPositionAsync` rejects if location services are off at the OS\n * level (distinct from the app's own permission grant, which is checked\n * before this ever runs) — caught here and reported via `onError` instead\n * of left to reject this method's own promise, so a caller that doesn't\n * wrap `GeoPulse.start()` in a try/catch still can't end up with an\n * unhandled rejection from deep inside a native module.\n */\n async start(onUpdate: (location: LocationUpdate) => void, onError?: (error: Error) => void): Promise<void> {\n try {\n this.subscription = await Location.watchPositionAsync(\n { accuracy: Location.Accuracy.Balanced, timeInterval: 5000, distanceInterval: 10 },\n (position) => {\n onUpdate({\n latitude: position.coords.latitude,\n longitude: position.coords.longitude,\n timestamp: new Date(position.timestamp).toISOString(),\n });\n },\n );\n } catch (error) {\n onError?.(error instanceof Error ? error : new Error(String(error)));\n }\n }\n\n stop(): void {\n this.subscription?.remove();\n this.subscription = null;\n }\n}\n","import * as Location from \"expo-location\";\n\nimport type { PermissionProvider, PermissionStatus } from \"./types\";\n\nfunction mapExpoStatus(status: Location.PermissionStatus): PermissionStatus {\n switch (status) {\n case Location.PermissionStatus.GRANTED:\n return \"granted\";\n case Location.PermissionStatus.DENIED:\n return \"denied\";\n default:\n return \"undetermined\";\n }\n}\n\n/**\n * Foreground-only — see this package's README (\"Permissions\" section) for\n * why background permission is explicitly out of scope for this phase.\n * Requests only the foreground permission; never touches\n * `requestBackgroundPermissionsAsync`.\n */\nexport class ExpoPermissionProvider implements PermissionProvider {\n async getStatus(): Promise<PermissionStatus> {\n const { status } = await Location.getForegroundPermissionsAsync();\n return mapExpoStatus(status);\n }\n\n async request(): Promise<PermissionStatus> {\n const { status } = await Location.requestForegroundPermissionsAsync();\n return mapExpoStatus(status);\n }\n}\n","import AsyncStorage from \"@react-native-async-storage/async-storage\";\n\nimport type { EventQueueStore, QueuedEvent } from \"./types\";\n\nconst STORAGE_KEY = \"@geopulse/pending-events\";\n\n/**\n * Persists the pending-event queue across app restarts using AsyncStorage —\n * required for the offline queue to actually survive the app being killed\n * before connectivity returns, not just a network blip while it's running.\n * Single-JSON-blob storage rather than one key per event: the expected\n * queue size (events waiting on a flaky connection) is small enough that\n * read/write-modify-write on one key is simpler and fine.\n */\nexport class AsyncStorageEventQueueStore implements EventQueueStore {\n async getAll(): Promise<QueuedEvent[]> {\n const raw = await AsyncStorage.getItem(STORAGE_KEY);\n if (!raw) return [];\n try {\n return JSON.parse(raw) as QueuedEvent[];\n } catch {\n return [];\n }\n }\n\n async save(event: QueuedEvent): Promise<void> {\n const all = await this.getAll();\n const next = [...all.filter((existing) => existing.eventId !== event.eventId), event];\n await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));\n }\n\n async remove(eventId: string): Promise<void> {\n const all = await this.getAll();\n await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(all.filter((existing) => existing.eventId !== eventId)));\n }\n}\n","import NetInfo from \"@react-native-community/netinfo\";\n\nimport type { NetworkMonitor, NetworkStatus } from \"./network-monitor\";\n\nfunction toStatus(isConnected: boolean | null): NetworkStatus {\n if (isConnected === null) return \"unknown\";\n return isConnected ? \"online\" : \"offline\";\n}\n\n/** Real connectivity detection for the RN app. Isolated in its own file — see storage/async-storage-store.ts's comment for why. */\nexport class NetInfoNetworkMonitor implements NetworkMonitor {\n private status: NetworkStatus = \"unknown\";\n\n constructor() {\n NetInfo.fetch().then((state) => {\n this.status = toStatus(state.isConnected);\n });\n }\n\n getStatus(): NetworkStatus {\n return this.status;\n }\n\n subscribe(listener: (status: NetworkStatus) => void): () => void {\n return NetInfo.addEventListener((state) => {\n this.status = toStatus(state.isConnected);\n listener(this.status);\n });\n }\n}\n","import React from \"react\";\nimport { Image, Linking, Text, TouchableOpacity, View } from \"react-native\";\n\nimport { resolveExperienceStyle, type ExperienceDeliveryPayload, type ResolvedExperienceStyle } from \"../protocol\";\n\ntype ExperienceContent = ExperienceDeliveryPayload[\"experience\"];\n\n/**\n * Experience URLs come from the backend. Only web URLs are opened: schemes\n * like `intent:`, `tel:`, `file:` or another app's deep links could trigger\n * actions outside this app (audit M-2).\n */\nexport function isSafeExternalUrl(url: string | null | undefined): url is string {\n return typeof url === \"string\" && /^https?:\\/\\/[^\\s]+$/i.test(url);\n}\n\nfunction openUrl(url: string | null): void {\n if (!isSafeExternalUrl(url)) return;\n // Linking.openURL hands off to the OS/system browser — no WebView, no\n // in-app script execution, matching the existing playground component's\n // rule this replaces (see apps/playground/src/components/ExperienceCard.tsx).\n Linking.openURL(url).catch(() => {\n // Swallow — an unreachable/malformed URL must never crash the host app.\n });\n}\n\nfunction ExperienceButton({ style, ctaLabel, ctaUrl }: { style: ResolvedExperienceStyle; ctaLabel: string | null; ctaUrl: string | null }) {\n if (!style.button.visible || !ctaLabel || !ctaUrl) return null;\n const { position } = style.button;\n return (\n <TouchableOpacity\n onPress={() => openUrl(ctaUrl)}\n style={{\n position: \"absolute\",\n top: position.top ?? undefined,\n bottom: position.bottom ?? undefined,\n left: position.left ?? undefined,\n right: position.right ?? undefined,\n backgroundColor: style.button.backgroundColor,\n borderRadius: style.button.borderRadius,\n paddingVertical: 10,\n paddingHorizontal: 16,\n }}\n >\n <Text style={{ color: style.button.textColor, fontWeight: \"600\" }}>{ctaLabel}</Text>\n </TouchableOpacity>\n );\n}\n\n/** Shared by all three experience types — full-bleed background image (if any) behind title/body/button, everything else laid out per-type by its caller. */\nfunction ExperienceContainer({ content, style, children }: { content: ExperienceContent; style: ResolvedExperienceStyle; children: React.ReactNode }): React.JSX.Element {\n const containerStyle = {\n backgroundColor: style.container.backgroundColor,\n borderColor: style.container.borderColor ?? undefined,\n borderWidth: style.container.borderWidth,\n borderRadius: style.container.borderRadius,\n padding: style.container.padding,\n overflow: \"hidden\" as const,\n width: style.container.width === \"fill\" ? (\"100%\" as const) : style.container.width,\n // \"auto\" means no explicit height at all — RN sizes a View to its content by default, so\n // omitting the property (rather than passing \"auto\", which RN's height type doesn't accept)\n // is what actually produces that behavior.\n // The button is absolutely positioned, so it takes no layout space — without a floor, an\n // \"auto\" container shrinks to title+body and the button overlaps them. 160 matches the\n // admin-web preview (experience-preview.tsx) so the device renders what the portal shows.\n ...(style.container.height === \"auto\" ? { minHeight: 160 } : { height: style.container.height }),\n };\n const backgroundImage = isSafeExternalUrl(style.container.backgroundImageUrl) ? (\n <Image\n source={{ uri: style.container.backgroundImageUrl }}\n resizeMode={style.container.backgroundImageFit}\n style={{ position: \"absolute\", top: 0, left: 0, right: 0, bottom: 0 }}\n />\n ) : null;\n\n // TouchableOpacity and View have different prop shapes, so this branches on\n // two full JSX returns rather than picking a dynamic component variable —\n // the latter can't be given a declaration-emittable type.\n if (content.cardNavigationUrl) {\n const cardNavigationUrl = content.cardNavigationUrl;\n return (\n <TouchableOpacity testID=\"experience-container\" onPress={() => openUrl(cardNavigationUrl)} style={containerStyle}>\n {backgroundImage}\n {children}\n </TouchableOpacity>\n );\n }\n return (\n <View testID=\"experience-container\" style={containerStyle}>\n {backgroundImage}\n {children}\n </View>\n );\n}\n\nfunction ExperienceText({ visible, text, color, fontSize, fontWeight }: { visible: boolean; text: string; color: string; fontSize: number; fontWeight: \"normal\" | \"bold\" }) {\n if (!visible) return null;\n return <Text style={{ color, fontSize, fontWeight }}>{text}</Text>;\n}\n\nexport function CardExperience({ content }: { content: ExperienceContent }): React.JSX.Element {\n const style = resolveExperienceStyle(\"card\", content.style);\n return (\n <ExperienceContainer content={content} style={style}>\n <ExperienceText visible={style.title.visible} text={content.title} color={style.title.color} fontSize={style.title.fontSize} fontWeight={style.title.fontWeight} />\n <ExperienceText visible={style.body.visible} text={content.body} color={style.body.color} fontSize={style.body.fontSize} fontWeight={style.body.fontWeight} />\n <ExperienceButton style={style} ctaLabel={content.ctaLabel} ctaUrl={content.ctaUrl} />\n </ExperienceContainer>\n );\n}\n\nexport function BannerExperience({ content }: { content: ExperienceContent }): React.JSX.Element {\n const style = resolveExperienceStyle(\"banner\", content.style);\n return (\n <ExperienceContainer content={content} style={style}>\n <ExperienceText visible={style.title.visible} text={content.title} color={style.title.color} fontSize={style.title.fontSize} fontWeight={style.title.fontWeight} />\n <ExperienceText visible={style.body.visible} text={content.body} color={style.body.color} fontSize={style.body.fontSize} fontWeight={style.body.fontWeight} />\n <ExperienceButton style={style} ctaLabel={content.ctaLabel} ctaUrl={content.ctaUrl} />\n </ExperienceContainer>\n );\n}\n\nexport function MessageExperience({ content }: { content: ExperienceContent }): React.JSX.Element {\n const style = resolveExperienceStyle(\"message\", content.style);\n return (\n <ExperienceContainer content={content} style={style}>\n <ExperienceText visible={style.title.visible} text={content.title} color={style.title.color} fontSize={style.title.fontSize} fontWeight={style.title.fontWeight} />\n <ExperienceText visible={style.body.visible} text={content.body} color={style.body.color} fontSize={style.body.fontSize} fontWeight={style.body.fontWeight} />\n <ExperienceButton style={style} ctaLabel={content.ctaLabel} ctaUrl={content.ctaUrl} />\n </ExperienceContainer>\n );\n}\n\n/** Picks the right renderer by `experience.type` — the one component most host apps need. */\nexport function ExperienceView({ experience }: { experience: ExperienceContent }): React.JSX.Element {\n switch (experience.type) {\n case \"banner\":\n return <BannerExperience content={experience} />;\n case \"message\":\n return <MessageExperience content={experience} />;\n case \"card\":\n default:\n return <CardExperience content={experience} />;\n }\n}\n"],"mappings":"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","names":["import_react_native","AsyncStorage","isDev","Location","import_async_storage","AsyncStorage","NetInfo","React"]}
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/config/async-storage-key-value-store.ts","../src/config/types.ts","../src/config/key-value-store.ts","../src/events/uuid.ts","../src/config/device-id.ts","../src/config/config-cache.ts","../src/protocol.ts","../src/firebase/api-client.ts","../src/events/factory.ts","../src/geofencing/state-machine.ts","../src/location/simulated-location-provider.ts","../src/permissions/simulated-permission-provider.ts","../src/sync/retry-policy.ts","../src/storage/memory-store.ts","../src/storage/queue.ts","../src/sync/network-monitor.ts","../src/sync/sync-manager.ts","../src/core/logger.ts","../src/core/geo-pulse-sdk.ts","../src/location/device-location-provider.ts","../src/permissions/expo-permission-provider.ts","../src/storage/async-storage-store.ts","../src/sync/netinfo-network-monitor.ts","../src/rendering/experience-components.tsx"],"sourcesContent":["import { Platform } from \"react-native\";\nimport type { ExperienceDeliveryPayload, GeofenceEventType, LocationEventPayload } from \"./protocol\";\n\nimport { AsyncStorageKeyValueStore } from \"./config/async-storage-key-value-store\";\nimport type { GeoPulseConfig } from \"./config/types\";\nimport { GeoPulseSdk } from \"./core/geo-pulse-sdk\";\nimport { DeviceLocationProvider } from \"./location/device-location-provider\";\nimport { SimulatedLocationProvider } from \"./location/simulated-location-provider\";\nimport { ExpoPermissionProvider } from \"./permissions/expo-permission-provider\";\nimport { SimulatedPermissionProvider } from \"./permissions/simulated-permission-provider\";\nimport { AsyncStorageEventQueueStore } from \"./storage/async-storage-store\";\nimport { PendingEventQueue } from \"./storage/queue\";\nimport { NetInfoNetworkMonitor } from \"./sync/netinfo-network-monitor\";\n\nconst isDev = typeof __DEV__ === \"undefined\" ? true : __DEV__;\n\n/**\n * Opt-in escape hatch for testing real GPS *during development* without\n * switching to a production build — set `EXPO_PUBLIC_GEOPULSE_USE_REAL_LOCATION=true`\n * in the host app's `.env` (Expo statically inlines `EXPO_PUBLIC_*` at\n * bundle time, including inside this workspace package, so this works\n * without the SDK needing any new config field). Defaults to false: dev\n * still means \"simulator\" unless a developer explicitly asks otherwise.\n */\nconst useRealLocationInDev = process.env.EXPO_PUBLIC_GEOPULSE_USE_REAL_LOCATION === \"true\";\nconst useSimulatedProviders = isDev && !useRealLocationInDev;\n\nfunction getPlatform(): \"ios\" | \"android\" | \"web\" | \"unknown\" {\n switch (Platform.OS) {\n case \"ios\":\n case \"android\":\n case \"web\":\n return Platform.OS;\n default:\n return \"unknown\";\n }\n}\n\n// Shared by device-id and config-cache — both are just distinctly-keyed\n// entries in AsyncStorage, so one instance is enough.\nconst persistentKeyValueStore = new AsyncStorageKeyValueStore();\n\n/**\n * Dev builds default to the simulated location + permission providers (see\n * the \"Simulator\" section of this package's README) so the whole\n * location -> geofence -> event flow works with zero device setup. A\n * release build — or a dev build with `EXPO_PUBLIC_GEOPULSE_USE_REAL_LOCATION=true`\n * set (see above) — uses real device location and real OS permission\n * prompts instead.\n */\nconst sdk = new GeoPulseSdk({\n locationProvider: useSimulatedProviders ? new SimulatedLocationProvider() : new DeviceLocationProvider(),\n permissionProvider: useSimulatedProviders ? new SimulatedPermissionProvider() : new ExpoPermissionProvider(),\n queue: new PendingEventQueue(new AsyncStorageEventQueueStore()),\n deviceIdStore: persistentKeyValueStore,\n configCacheStore: persistentKeyValueStore,\n networkMonitor: new NetInfoNetworkMonitor(),\n getPlatform,\n debug: isDev,\n});\n\n/** `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. */\ntype OnOff = {\n (eventName: GeofenceEventType, callback: (event: LocationEventPayload) => void): void;\n (eventName: \"experience.received\", callback: (payload: ExperienceDeliveryPayload) => void): void;\n};\n\nexport const GeoPulse = {\n initialize: (config: GeoPulseConfig) => sdk.initialize(config),\n start: () => sdk.start(),\n stop: () => sdk.stop(),\n getStatus: () => sdk.getStatus(),\n getPermissionStatus: () => sdk.getPermissionStatus(),\n on: sdk.on.bind(sdk) as OnOff,\n off: sdk.off.bind(sdk) as OnOff,\n\n // Small additions beyond the core 7-method API, for building a status UI\n // around the SDK (see apps/playground) — not part of the minimal public\n // surface the spec calls out, but plain read-only accessors for state the\n // SDK already tracks internally.\n getConfiguredGeofences: () => sdk.getConfiguredGeofences(),\n getApplicationId: () => sdk.getApplicationId(),\n getLastKnownLocation: () => sdk.getLastKnownLocation(),\n getPendingEventCount: () => sdk.getPendingEventCount(),\n getDeviceId: () => sdk.getDeviceId(),\n /** Rich developer-diagnostics snapshot — see core/types.ts's DiagnosticsSnapshot doc. */\n getDiagnostics: () => sdk.getDiagnostics(),\n\n /** Development-only. No-ops (with a console warning) in a production build — see GeoPulseSdk.simulateLocation. */\n simulator: {\n setLocation: (coords: { latitude: number; longitude: number }) => sdk.simulateLocation(coords),\n },\n};\n\n// Advanced/testing escape hatch — construct your own SDK instance instead\n// of using the `GeoPulse` singleton above (e.g. to force real device\n// location during development). Most consumers should just use `GeoPulse`.\nexport { ApplicationMismatchError, GeoPulseSdk } from \"./core/geo-pulse-sdk\";\nexport { DeviceLocationProvider } from \"./location/device-location-provider\";\nexport { SimulatedLocationProvider } from \"./location/simulated-location-provider\";\nexport { ExpoPermissionProvider } from \"./permissions/expo-permission-provider\";\nexport { SimulatedPermissionProvider } from \"./permissions/simulated-permission-provider\";\nexport { NetInfoNetworkMonitor } from \"./sync/netinfo-network-monitor\";\nexport { ManualNetworkMonitor } from \"./sync/network-monitor\";\n\nexport type { GeoPulseConfig } from \"./config/types\";\nexport type { ConfigurationStatus, DiagnosticsSnapshot, SdkStatus } from \"./core/types\";\nexport type { LocationProvider, LocationUpdate } from \"./location/types\";\nexport type { PermissionProvider, PermissionStatus } from \"./permissions/types\";\nexport type { NetworkMonitor, NetworkStatus } from \"./sync/network-monitor\";\nexport type { ExperienceDeliveryPayload, GeofenceEventType, LocationEventPayload, SdkGeofenceConfig } from \"./protocol\";\nexport { BannerExperience, CardExperience, ExperienceView, MessageExperience } from \"./rendering\";\n","import AsyncStorage from \"@react-native-async-storage/async-storage\";\n\nimport type { KeyValueStore } from \"./key-value-store\";\n\n/** Real persistence for the RN app. Isolated in its own file — see storage/async-storage-store.ts's comment for why. */\nexport class AsyncStorageKeyValueStore implements KeyValueStore {\n async getItem(key: string): Promise<string | null> {\n return AsyncStorage.getItem(key);\n }\n\n async setItem(key: string, value: string): Promise<void> {\n await AsyncStorage.setItem(key, value);\n }\n}\n","export interface GeoPulseConfig {\n apiKey: string;\n applicationId: string;\n /**\n * Internal override for the GeoPulse backend URL — only for GeoPulse's own\n * local development (e.g. apps/playground against a local backend).\n * Customers never set this; it defaults to `DEFAULT_BACKEND_URL`.\n */\n functionsBaseUrl?: string;\n /** How often to retry queued (previously failed) events, in milliseconds. Defaults to 30s. */\n retryIntervalMs?: number;\n /** How often to poll for pending campaign experiences (Phase 5B), in milliseconds. Defaults to 30s, mirroring `retryIntervalMs`. A host app that wants faster demo feedback (see apps/playground) can pass a smaller value. */\n experiencePollIntervalMs?: number;\n /** 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. */\n debug?: boolean;\n}\n\n/** The hosted GeoPulse backend every customer app talks to. */\nexport const DEFAULT_BACKEND_URL = \"https://geofends.onrender.com\";\n\nexport const SDK_VERSION = \"0.3.4\";\n","/**\n * Minimal persistent key-value abstraction, shared by device-id.ts and\n * config-cache.ts — deliberately smaller than `storage/EventQueueStore`\n * (which manages a whole collection) since these two only ever need \"get\n * one string\" / \"set one string\".\n */\nexport interface KeyValueStore {\n getItem(key: string): Promise<string | null>;\n setItem(key: string, value: string): Promise<void>;\n}\n\nexport class InMemoryKeyValueStore implements KeyValueStore {\n private readonly data = new Map<string, string>();\n\n async getItem(key: string): Promise<string | null> {\n return this.data.get(key) ?? null;\n }\n\n async setItem(key: string, value: string): Promise<void> {\n this.data.set(key, value);\n }\n}\n","/**\n * RFC 4122 v4 UUID for event and device ids. Uses the platform CSPRNG\n * (`crypto.getRandomValues`, available in Hermes >= 0.12 and Node) when\n * present, falling back to `Math.random()` only on runtimes without it.\n * These ids aren't secrets, but a device id scopes which pending\n * experiences a request can fetch, so it shouldn't be predictable.\n */\nexport function generateEventId(): string {\n const bytes = new Uint8Array(16);\n const cryptoApi = (globalThis as { crypto?: { getRandomValues?: (array: Uint8Array) => Uint8Array } }).crypto;\n if (cryptoApi?.getRandomValues) {\n cryptoApi.getRandomValues(bytes);\n } else {\n for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);\n }\n bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;\n bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;\n const hex = [...bytes].map((byte) => byte.toString(16).padStart(2, \"0\")).join(\"\");\n return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;\n}\n","import { generateEventId } from \"../events/uuid\";\nimport type { KeyValueStore } from \"./key-value-store\";\n\nconst DEVICE_ID_STORAGE_KEY = \"@geopulse/device-id\";\n\n/**\n * A stable, random, non-identifying installation id — generated once per\n * install and persisted thereafter, not derived from any hardware\n * identifier (spec: \"do not attempt invasive device fingerprinting\").\n * `generateEventId` is just a generic UUID v4 generator despite its name\n * (see events/uuid.ts); reused here rather than duplicated.\n */\nexport async function getOrCreateDeviceId(store: KeyValueStore): Promise<string> {\n const existing = await store.getItem(DEVICE_ID_STORAGE_KEY);\n if (existing) {\n return existing;\n }\n const deviceId = generateEventId();\n await store.setItem(DEVICE_ID_STORAGE_KEY, deviceId);\n return deviceId;\n}\n","import type { SdkProjectConfig } from \"../protocol\";\n\nimport type { KeyValueStore } from \"./key-value-store\";\n\nconst CONFIG_CACHE_STORAGE_KEY = \"@geopulse/config-cache\";\n\ninterface CachedConfig {\n config: SdkProjectConfig;\n fetchedAt: string;\n}\n\n/**\n * Persists the last successfully fetched `SdkProjectConfig` so\n * `initialize()` can still succeed — using stale-but-valid geofences —\n * when `getProjectConfig` is unreachable (spec: \"If the device temporarily\n * loses network access, the SDK should be able to continue using the last\n * valid configuration\"). There is no expiry: a geofence list that's a few\n * hours old is still far more useful than refusing to start at all, and\n * the next successful fetch overwrites it regardless of age.\n */\nexport class ConfigCache {\n constructor(private readonly store: KeyValueStore) {}\n\n async save(config: SdkProjectConfig): Promise<void> {\n const cached: CachedConfig = { config, fetchedAt: new Date().toISOString() };\n await this.store.setItem(CONFIG_CACHE_STORAGE_KEY, JSON.stringify(cached));\n }\n\n async load(): Promise<CachedConfig | null> {\n const raw = await this.store.getItem(CONFIG_CACHE_STORAGE_KEY);\n if (!raw) return null;\n try {\n return JSON.parse(raw) as CachedConfig;\n } catch {\n return null;\n }\n }\n}\n","/**\n * The SDK's entire wire contract with the GeoPulse backend, copied from\n * `@geopulse/shared` rather than depending on it at runtime. `@geopulse/shared`\n * is a monorepo-internal package (also used by apps/admin-web and functions/)\n * that carries the *whole* platform's schemas — organizations, webhooks,\n * campaigns, auth forms, and more. None of that belongs inside a published,\n * on-device SDK: bundling the real package pulled in all of it (verified via\n * `npm pack` on the published tarball — dist/index.js contained the admin\n * portal's signup/login validation, webhook, and campaign schemas). This file\n * is the deliberately small subset the SDK actually touches. Keep it in sync\n * with the corresponding types in packages/shared/src/ by hand if the wire\n * contract changes — `src/core/geo-pulse-sdk.test.ts` and\n * `src/firebase/api-client.test.ts` still validate against the real\n * `@geopulse/shared` package (a devDependency, never published) precisely so\n * drift between this copy and the canonical schema gets caught.\n */\n\nexport const SDK_KEY_HEADER = \"x-geopulse-sdk-key\";\n\n/** The configured application id, sent with every request so the backend can reject a key bound to another application. */\nexport const APPLICATION_ID_HEADER = \"x-geopulse-application-id\";\n\nexport const SDK_ENDPOINTS = {\n GET_PROJECT_CONFIG: \"getProjectConfig\",\n INGEST_LOCATION_EVENT: \"ingestLocationEvent\",\n GET_PENDING_EXPERIENCES: \"getPendingExperiences\",\n ACKNOWLEDGE_EXPERIENCE_DELIVERY: \"acknowledgeExperienceDelivery\",\n} as const;\n\nconst EARTH_RADIUS_METERS = 6_371_000;\n\nfunction toRadians(degrees: number): number {\n return (degrees * Math.PI) / 180;\n}\n\n/** Haversine great-circle distance in meters — see packages/shared/src/utils/geo.ts for the full derivation comment. */\nexport function calculateDistanceMeters(\n from: { latitude: number; longitude: number },\n to: { latitude: number; longitude: number },\n): number {\n const lat1 = toRadians(from.latitude);\n const lat2 = toRadians(to.latitude);\n const deltaLat = toRadians(to.latitude - from.latitude);\n const deltaLng = toRadians(to.longitude - from.longitude);\n\n const a =\n Math.sin(deltaLat / 2) ** 2 + Math.cos(lat1) * Math.cos(lat2) * Math.sin(deltaLng / 2) ** 2;\n const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));\n\n return EARTH_RADIUS_METERS * c;\n}\n\nexport const GEOFENCE_EVENT_TYPES = [\"geofence.enter\", \"geofence.exit\"] as const;\nexport type GeofenceEventType = (typeof GEOFENCE_EVENT_TYPES)[number];\n\nexport const DEVICE_PLATFORMS = [\"ios\", \"android\", \"web\", \"unknown\"] as const;\nexport type DevicePlatform = (typeof DEVICE_PLATFORMS)[number];\n\nexport const EXPERIENCE_TYPES = [\"banner\", \"card\", \"message\"] as const;\nexport type ExperienceType = (typeof EXPERIENCE_TYPES)[number];\n\nexport const ANCHOR_POINTS = [\n \"top-left\",\n \"top-center\",\n \"top-right\",\n \"center-left\",\n \"center\",\n \"center-right\",\n \"bottom-left\",\n \"bottom-center\",\n \"bottom-right\",\n] as const;\nexport type AnchorPoint = (typeof ANCHOR_POINTS)[number];\n\nexport interface AnchoredPosition {\n anchor: AnchorPoint;\n offsetX?: number | null;\n offsetY?: number | null;\n}\n\nexport interface ContainerStyle {\n backgroundColor?: string | null;\n borderColor?: string | null;\n borderWidth?: number | null;\n borderRadius?: number | null;\n padding?: number | null;\n backgroundImageUrl?: string | null;\n backgroundImageFit?: \"cover\" | \"contain\" | \"stretch\";\n widthMode?: \"fill\" | \"fixed\";\n width?: number | null;\n heightMode?: \"auto\" | \"fixed\";\n height?: number | null;\n}\n\nexport interface TextStyle {\n visible?: boolean;\n color?: string | null;\n fontSize?: number | null;\n fontWeight?: \"normal\" | \"bold\";\n}\n\nexport interface ButtonStyle {\n visible?: boolean;\n backgroundColor?: string | null;\n textColor?: string | null;\n borderRadius?: number | null;\n position?: AnchoredPosition | null;\n}\n\nexport interface ExperienceStyleV2 {\n version: 2;\n container?: ContainerStyle;\n title?: TextStyle;\n body?: TextStyle;\n button?: ButtonStyle;\n}\n\nexport interface ExperienceStyleLegacy {\n backgroundColor: string | null;\n textColor: string | null;\n accentColor: string | null;\n}\n\nexport type ExperienceStyle = ExperienceStyleV2 | ExperienceStyleLegacy;\n\nexport function isLegacyExperienceStyle(style: ExperienceStyle): style is ExperienceStyleLegacy {\n return !(\"version\" in style);\n}\n\nexport interface ResolvedPosition {\n top: number | null;\n bottom: number | null;\n left: number | null;\n right: number | null;\n}\n\nexport interface ResolvedContainerStyle {\n backgroundColor: string;\n borderColor: string | null;\n borderWidth: number;\n borderRadius: number;\n padding: number;\n backgroundImageUrl: string | null;\n backgroundImageFit: \"cover\" | \"contain\" | \"stretch\";\n width: \"fill\" | number;\n height: \"auto\" | number;\n}\n\nexport interface ResolvedTextStyle {\n visible: boolean;\n color: string;\n fontSize: number;\n fontWeight: \"normal\" | \"bold\";\n}\n\nexport interface ResolvedButtonStyle {\n visible: boolean;\n backgroundColor: string;\n textColor: string;\n borderRadius: number;\n position: ResolvedPosition;\n}\n\nexport interface ResolvedExperienceStyle {\n container: ResolvedContainerStyle;\n title: ResolvedTextStyle;\n body: ResolvedTextStyle;\n button: ResolvedButtonStyle;\n}\n\nconst HEX_COLOR_PATTERN = /^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/;\n/** dp — a generous cap so a malformed/huge offset can't push an element fully off-screen. */\nconst OFFSET_LIMIT = 500;\n\nconst ANCHOR_BASE_POSITIONS: Record<AnchorPoint, ResolvedPosition> = {\n \"top-left\": { top: 0, left: 0, bottom: null, right: null },\n \"top-center\": { top: 0, left: null, bottom: null, right: null },\n \"top-right\": { top: 0, left: null, bottom: null, right: 0 },\n \"center-left\": { top: null, left: 0, bottom: null, right: null },\n center: { top: null, left: null, bottom: null, right: null },\n \"center-right\": { top: null, left: null, bottom: null, right: 0 },\n \"bottom-left\": { top: null, left: 0, bottom: 0, right: null },\n \"bottom-center\": { top: null, left: null, bottom: 0, right: null },\n \"bottom-right\": { top: null, left: null, bottom: 0, right: 0 },\n};\n\nconst DEFAULT_STYLES: Record<ExperienceType, ResolvedExperienceStyle> = {\n card: {\n container: { backgroundColor: \"#ffffff\", borderColor: null, borderWidth: 0, borderRadius: 16, padding: 20, backgroundImageUrl: null, backgroundImageFit: \"cover\", width: \"fill\", height: \"auto\" },\n title: { visible: true, color: \"#111827\", fontSize: 18, fontWeight: \"bold\" },\n body: { visible: true, color: \"#374151\", fontSize: 14, fontWeight: \"normal\" },\n button: { visible: true, backgroundColor: \"#2563eb\", textColor: \"#ffffff\", borderRadius: 8, position: { top: null, bottom: 16, left: 16, right: null } },\n },\n banner: {\n container: { backgroundColor: \"#111827\", borderColor: null, borderWidth: 0, borderRadius: 0, padding: 12, backgroundImageUrl: null, backgroundImageFit: \"cover\", width: \"fill\", height: \"auto\" },\n title: { visible: true, color: \"#ffffff\", fontSize: 15, fontWeight: \"bold\" },\n body: { visible: true, color: \"#e5e7eb\", fontSize: 13, fontWeight: \"normal\" },\n button: { visible: false, backgroundColor: \"#2563eb\", textColor: \"#ffffff\", borderRadius: 6, position: { top: null, bottom: null, left: null, right: 12 } },\n },\n message: {\n container: { backgroundColor: \"#1f2937\", borderColor: null, borderWidth: 0, borderRadius: 12, padding: 16, backgroundImageUrl: null, backgroundImageFit: \"cover\", width: \"fill\", height: \"auto\" },\n title: { visible: false, color: \"#ffffff\", fontSize: 16, fontWeight: \"bold\" },\n body: { visible: true, color: \"#ffffff\", fontSize: 14, fontWeight: \"normal\" },\n button: { visible: false, backgroundColor: \"#2563eb\", textColor: \"#ffffff\", borderRadius: 8, position: { top: null, bottom: 16, left: null, right: 16 } },\n },\n};\n\nfunction clamp(value: number, min: number, max: number): number {\n return Math.min(max, Math.max(min, value));\n}\n\nfunction resolveColor(value: string | null | undefined, fallback: string): string {\n return typeof value === \"string\" && HEX_COLOR_PATTERN.test(value) ? value : fallback;\n}\n\nfunction resolveOptionalColor(value: string | null | undefined, fallback: string | null): string | null {\n return typeof value === \"string\" && HEX_COLOR_PATTERN.test(value) ? value : fallback;\n}\n\nfunction resolveNonNegativeNumber(value: number | null | undefined, fallback: number): number {\n return typeof value === \"number\" && Number.isFinite(value) && value >= 0 ? value : fallback;\n}\n\nfunction resolvePosition(position: AnchoredPosition | null | undefined, fallback: ResolvedPosition): ResolvedPosition {\n if (!position || !ANCHOR_POINTS.includes(position.anchor)) return fallback;\n const offsetX = typeof position.offsetX === \"number\" && Number.isFinite(position.offsetX) ? clamp(position.offsetX, -OFFSET_LIMIT, OFFSET_LIMIT) : 0;\n const offsetY = typeof position.offsetY === \"number\" && Number.isFinite(position.offsetY) ? clamp(position.offsetY, -OFFSET_LIMIT, OFFSET_LIMIT) : 0;\n const base = ANCHOR_BASE_POSITIONS[position.anchor];\n return {\n top: base.top === null ? null : base.top + offsetY,\n bottom: base.bottom === null ? null : base.bottom + offsetY,\n left: base.left === null ? null : base.left + offsetX,\n right: base.right === null ? null : base.right + offsetX,\n };\n}\n\nfunction resolveWidth(mode: \"fill\" | \"fixed\" | undefined, value: number | null | undefined, fallback: \"fill\" | number): \"fill\" | number {\n if (mode === \"fixed\") {\n return typeof value === \"number\" && Number.isFinite(value) && value > 0 ? value : fallback;\n }\n if (mode === \"fill\") return \"fill\";\n return fallback;\n}\n\nfunction resolveHeight(mode: \"auto\" | \"fixed\" | undefined, value: number | null | undefined, fallback: \"auto\" | number): \"auto\" | number {\n if (mode === \"fixed\") {\n return typeof value === \"number\" && Number.isFinite(value) && value > 0 ? value : fallback;\n }\n if (mode === \"auto\") return \"auto\";\n return fallback;\n}\n\nfunction resolveContainer(custom: ContainerStyle | undefined, fallback: ResolvedContainerStyle): ResolvedContainerStyle {\n return {\n backgroundColor: resolveColor(custom?.backgroundColor, fallback.backgroundColor),\n borderColor: resolveOptionalColor(custom?.borderColor, fallback.borderColor),\n borderWidth: resolveNonNegativeNumber(custom?.borderWidth, fallback.borderWidth),\n borderRadius: resolveNonNegativeNumber(custom?.borderRadius, fallback.borderRadius),\n padding: resolveNonNegativeNumber(custom?.padding, fallback.padding),\n backgroundImageUrl: typeof custom?.backgroundImageUrl === \"string\" && custom.backgroundImageUrl.length > 0 ? custom.backgroundImageUrl : fallback.backgroundImageUrl,\n backgroundImageFit: custom?.backgroundImageFit ?? fallback.backgroundImageFit,\n width: resolveWidth(custom?.widthMode, custom?.width, fallback.width),\n height: resolveHeight(custom?.heightMode, custom?.height, fallback.height),\n };\n}\n\nfunction resolveText(custom: TextStyle | undefined, fallback: ResolvedTextStyle): ResolvedTextStyle {\n return {\n visible: custom?.visible ?? fallback.visible,\n color: resolveColor(custom?.color, fallback.color),\n fontSize: resolveNonNegativeNumber(custom?.fontSize, fallback.fontSize),\n fontWeight: custom?.fontWeight ?? fallback.fontWeight,\n };\n}\n\nfunction resolveButton(custom: ButtonStyle | undefined, fallback: ResolvedButtonStyle): ResolvedButtonStyle {\n return {\n visible: custom?.visible ?? fallback.visible,\n backgroundColor: resolveColor(custom?.backgroundColor, fallback.backgroundColor),\n textColor: resolveColor(custom?.textColor, fallback.textColor),\n borderRadius: resolveNonNegativeNumber(custom?.borderRadius, fallback.borderRadius),\n position: resolvePosition(custom?.position, fallback.position),\n };\n}\n\n/** Maps today's flat 3-color shape onto the v2 groups it corresponds to. */\nfunction upgradeLegacyStyle(legacy: ExperienceStyleLegacy): ExperienceStyleV2 {\n return {\n version: 2,\n container: { backgroundColor: legacy.backgroundColor },\n title: { color: legacy.textColor },\n body: { color: legacy.textColor },\n button: { backgroundColor: legacy.accentColor },\n };\n}\n\n/**\n * Cascades a type's built-in default under a campaign's custom style,\n * upgrading today's legacy 3-color shape on the fly, and resolves 9-point\n * anchor positions to concrete offsets. Never throws — any malformed or\n * missing field silently falls back to the type's default.\n */\nexport function resolveExperienceStyle(type: ExperienceType, custom: ExperienceStyle | null | undefined): ResolvedExperienceStyle {\n const fallback = DEFAULT_STYLES[type] ?? DEFAULT_STYLES.card;\n if (!custom) return fallback;\n const v2 = isLegacyExperienceStyle(custom) ? upgradeLegacyStyle(custom) : custom;\n return {\n container: resolveContainer(v2.container, fallback.container),\n title: resolveText(v2.title, fallback.title),\n body: resolveText(v2.body, fallback.body),\n button: resolveButton(v2.button, fallback.button),\n };\n}\n\nexport interface GeofenceCenter {\n latitude: number;\n longitude: number;\n}\n\nexport interface SdkGeofenceConfig {\n id: string;\n name: string;\n locationId: string;\n locationName: string;\n center: GeofenceCenter;\n radiusMeters: number;\n}\n\nexport interface SdkProjectConfig {\n applicationId: string;\n projectId: string;\n applicationEnabled: boolean;\n configVersion: string;\n geofences: SdkGeofenceConfig[];\n}\n\nexport interface LocationEventPayload {\n id: string;\n type: GeofenceEventType;\n projectId: string;\n applicationId: string;\n geofenceId: string;\n locationId: string;\n deviceId: string;\n timestamp: string;\n latitude: number;\n longitude: number;\n metadata: Record<string, string>;\n sdkVersion: string;\n attempts: number;\n}\n\nexport interface ExperienceDeliveryPayload {\n type: \"location.experience\";\n deliveryId: string;\n campaignId: string;\n experience: {\n id: string;\n type: ExperienceType;\n title: string;\n body: string;\n imageUrl: string | null;\n cardNavigationUrl: string | null;\n ctaLabel: string | null;\n ctaUrl: string | null;\n style: ExperienceStyle | null;\n };\n event: {\n id: string;\n type: GeofenceEventType;\n geofenceId: string;\n timestamp: string;\n };\n}\n","import { APPLICATION_ID_HEADER, SDK_ENDPOINTS, SDK_KEY_HEADER } from \"../protocol\";\nimport type { DevicePlatform, ExperienceDeliveryPayload, LocationEventPayload, SdkProjectConfig } from \"../protocol\";\n\nexport interface DeviceIdentity {\n deviceId: string;\n platform: DevicePlatform;\n sdkVersion: string;\n}\n\n/**\n * Without this, a request to an unresponsive/hung backend (e.g. a local\n * emulator that was restarted mid-request, or a real network stall) would\n * hang forever — `fetch` has no default timeout. That leaves an event\n * stuck `pending` indefinitely: `SyncManager.attemptDelivery`'s retry/backoff\n * logic only runs once a request actually *fails*, so a request that never\n * settles at all never gets retried either. Aborting after this long\n * converts \"hung forever\" into \"failed now,\" which the existing queue\n * already knows how to handle.\n */\nconst REQUEST_TIMEOUT_MS = 15_000;\n\nasync function fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {\n const controller = new AbortController();\n const timeout = setTimeout(() => controller.abort(), REQUEST_TIMEOUT_MS);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } finally {\n clearTimeout(timeout);\n }\n}\n\n/**\n * A non-2xx response already throws a clear, endpoint-named error below —\n * but `fetch` itself rejecting (offline, DNS failure, or `fetchWithTimeout`'s\n * own abort) previously propagated as a bare, unattributed error like\n * \"Aborted\" or \"Network request failed\", with nothing indicating which SDK\n * call it came from (Phase 7 SDK audit finding). This wraps that case with\n * the same \"which call failed\" context the HTTP-error path already has,\n * without changing what the underlying error actually was — `error.message`\n * is still included verbatim.\n */\nasync function fetchOrThrow(callName: string, url: string, init: RequestInit): Promise<Response> {\n try {\n return await fetchWithTimeout(url, init);\n } catch (error) {\n const message = error instanceof Error ? error.message : String(error);\n throw new Error(`${callName} request failed: ${message}`);\n }\n}\n\n/**\n * Named `firebase/` per this project's spec (\"communication with GeoPulse\n * backend\"), but deliberately does NOT use the Firebase client SDK — it\n * talks to the Cloud Functions HTTPS endpoints over plain `fetch` (built\n * into both React Native's JS runtime and Node 18+, so this file is\n * testable without any RN runtime at all). That keeps the SDK's dependency\n * footprint small and means device requests carry only the app-scoped SDK\n * key, never a Firebase credential of any kind.\n */\nexport class ApiClient {\n constructor(\n private readonly baseUrl: string,\n private readonly sdkKey: string,\n /** Optional so existing callers keep working; when set, the backend rejects a key bound to a different application. */\n private readonly applicationId?: string,\n ) {}\n\n private headers(json = false): Record<string, string> {\n return {\n [SDK_KEY_HEADER]: this.sdkKey,\n ...(this.applicationId ? { [APPLICATION_ID_HEADER]: this.applicationId } : {}),\n ...(json ? { \"Content-Type\": \"application/json\" } : {}),\n };\n }\n\n /**\n * `identity` doubles as this device's registration/heartbeat (see\n * functions/src/events/get-project-config.ts) — sent as query params\n * since GET requests carry no body.\n */\n async getProjectConfig(identity: DeviceIdentity): Promise<SdkProjectConfig> {\n const query = new URLSearchParams({\n deviceId: identity.deviceId,\n platform: identity.platform,\n sdkVersion: identity.sdkVersion,\n });\n const response = await fetchOrThrow(\"getProjectConfig\", `${this.baseUrl}/${SDK_ENDPOINTS.GET_PROJECT_CONFIG}?${query.toString()}`, {\n method: \"GET\",\n headers: this.headers(),\n });\n if (!response.ok) {\n throw new Error(`getProjectConfig failed with status ${response.status}`);\n }\n return (await response.json()) as SdkProjectConfig;\n }\n\n async ingestEvent(payload: LocationEventPayload): Promise<{ id: string; deduplicated: boolean }> {\n const response = await fetchOrThrow(\"ingestLocationEvent\", `${this.baseUrl}/${SDK_ENDPOINTS.INGEST_LOCATION_EVENT}`, {\n method: \"POST\",\n headers: this.headers(true),\n body: JSON.stringify(payload),\n });\n if (!response.ok) {\n throw new Error(`ingestLocationEvent failed with status ${response.status}`);\n }\n return (await response.json()) as { id: string; deduplicated: boolean };\n }\n\n /** Polled by GeoPulseSdk's experience-polling loop — see core/geo-pulse-sdk.ts. */\n async getPendingExperiences(deviceId: string): Promise<ExperienceDeliveryPayload[]> {\n const query = new URLSearchParams({ deviceId });\n const response = await fetchOrThrow(\"getPendingExperiences\", `${this.baseUrl}/${SDK_ENDPOINTS.GET_PENDING_EXPERIENCES}?${query.toString()}`, {\n method: \"GET\",\n headers: this.headers(),\n });\n if (!response.ok) {\n throw new Error(`getPendingExperiences failed with status ${response.status}`);\n }\n const body = (await response.json()) as { experiences: ExperienceDeliveryPayload[] };\n return body.experiences;\n }\n\n /** Confirms a delivery was surfaced via `experience.received`, so it stops appearing in future polls. */\n async acknowledgeExperienceDelivery(deliveryId: string): Promise<void> {\n const response = await fetchOrThrow(\"acknowledgeExperienceDelivery\", `${this.baseUrl}/${SDK_ENDPOINTS.ACKNOWLEDGE_EXPERIENCE_DELIVERY}`, {\n method: \"POST\",\n headers: this.headers(true),\n body: JSON.stringify({ deliveryId }),\n });\n if (!response.ok) {\n throw new Error(`acknowledgeExperienceDelivery failed with status ${response.status}`);\n }\n }\n}\n","import type { GeofenceEventType, LocationEventPayload, SdkGeofenceConfig } from \"../protocol\";\n\nimport { SDK_VERSION } from \"../config/types\";\nimport { generateEventId } from \"./uuid\";\n\nexport function createEventPayload(params: {\n type: GeofenceEventType;\n projectId: string;\n applicationId: string;\n geofence: SdkGeofenceConfig;\n location: { latitude: number; longitude: number };\n deviceId: string;\n metadata?: Record<string, string>;\n}): LocationEventPayload {\n return {\n id: generateEventId(),\n type: params.type,\n projectId: params.projectId,\n applicationId: params.applicationId,\n geofenceId: params.geofence.id,\n locationId: params.geofence.locationId,\n deviceId: params.deviceId,\n timestamp: new Date().toISOString(),\n latitude: params.location.latitude,\n longitude: params.location.longitude,\n metadata: params.metadata ?? {},\n sdkVersion: SDK_VERSION,\n // First-try attempt count; SyncManager overwrites this with the real\n // count if the event ends up going through the retry queue (see\n // sync/sync-manager.ts#retryPending) — that's what makes the\n // \"Attempts\" column in the admin's Event Explorer meaningful.\n attempts: 1,\n };\n}\n","import { calculateDistanceMeters } from \"../protocol\";\nimport type { GeofenceEventType, SdkGeofenceConfig } from \"../protocol\";\n\nexport type GeofenceMembership = \"INSIDE\" | \"OUTSIDE\";\n\nexport interface GeofenceTransition {\n geofence: SdkGeofenceConfig;\n type: GeofenceEventType;\n}\n\n/**\n * Tracks INSIDE/OUTSIDE membership per geofence and emits a transition only\n * when membership actually changes:\n *\n * OUTSIDE -> INSIDE = geofence.enter\n * INSIDE -> OUTSIDE = geofence.exit\n * INSIDE -> INSIDE = nothing\n * OUTSIDE -> OUTSIDE = nothing\n *\n * A geofence with no prior recorded state is treated as OUTSIDE (so the\n * very first location update that lands inside a geofence correctly fires\n * one `geofence.enter`, not silence). State survives across repeated\n * `evaluate()` calls precisely so a location update that keeps the device\n * inside the same geofence produces no duplicate ENTER — that's the whole\n * point of this class existing instead of a stateless distance check.\n */\nexport class GeofenceStateMachine {\n private readonly membershipByGeofenceId = new Map<string, GeofenceMembership>();\n\n evaluate(location: { latitude: number; longitude: number }, geofences: SdkGeofenceConfig[]): GeofenceTransition[] {\n const transitions: GeofenceTransition[] = [];\n const activeIds = new Set<string>();\n\n for (const geofence of geofences) {\n activeIds.add(geofence.id);\n\n const distance = calculateDistanceMeters(location, geofence.center);\n const nextMembership: GeofenceMembership = distance <= geofence.radiusMeters ? \"INSIDE\" : \"OUTSIDE\";\n const previousMembership = this.membershipByGeofenceId.get(geofence.id) ?? \"OUTSIDE\";\n\n if (previousMembership !== nextMembership) {\n transitions.push({\n geofence,\n type: nextMembership === \"INSIDE\" ? \"geofence.enter\" : \"geofence.exit\",\n });\n }\n\n this.membershipByGeofenceId.set(geofence.id, nextMembership);\n }\n\n // Drop bookkeeping for geofences no longer in the active config (e.g.\n // deleted/disabled since the last `getProjectConfig` fetch) so it can't\n // silently grow forever or produce a stale transition if the same\n // geofence id ever reappears later.\n for (const id of this.membershipByGeofenceId.keys()) {\n if (!activeIds.has(id)) {\n this.membershipByGeofenceId.delete(id);\n }\n }\n\n return transitions;\n }\n\n getMembership(geofenceId: string): GeofenceMembership {\n return this.membershipByGeofenceId.get(geofenceId) ?? \"OUTSIDE\";\n }\n\n reset(): void {\n this.membershipByGeofenceId.clear();\n }\n}\n","import type { LocationProvider, LocationUpdate } from \"./types\";\n\n/**\n * The mandatory simulation provider (spec section 7) — lets the geofence\n * flow be exercised end to end without physically travelling anywhere.\n * `setLocation` is the playground simulator screen's only way to move the\n * \"user\"; everything downstream (geofence engine, event generation, sync)\n * runs exactly as it would for a real device update, because both\n * providers speak the same `LocationProvider` interface.\n */\nexport class SimulatedLocationProvider implements LocationProvider {\n private onUpdate: ((location: LocationUpdate) => void) | null = null;\n private currentLocation: LocationUpdate | null = null;\n\n start(onUpdate: (location: LocationUpdate) => void): void {\n this.onUpdate = onUpdate;\n // If a location was already set before `start()` (e.g. the simulator\n // screen was positioned before pressing \"Start\"), replay it immediately\n // rather than waiting for the next `setLocation` call.\n if (this.currentLocation) {\n onUpdate(this.currentLocation);\n }\n }\n\n stop(): void {\n this.onUpdate = null;\n }\n\n setLocation(coords: { latitude: number; longitude: number }): void {\n const location: LocationUpdate = { ...coords, timestamp: new Date().toISOString() };\n this.currentLocation = location;\n this.onUpdate?.(location);\n }\n\n getCurrentLocation(): LocationUpdate | null {\n return this.currentLocation;\n }\n}\n","import type { PermissionProvider, PermissionStatus } from \"./types\";\n\n/**\n * Used with the simulated location provider (see ../location) — a\n * simulated user obviously isn't gated by the OS location permission\n * dialog, so this always reports \"granted\" rather than requiring a real\n * device's permission state to test the geofence flow end to end.\n */\nexport class SimulatedPermissionProvider implements PermissionProvider {\n async getStatus(): Promise<PermissionStatus> {\n return \"granted\";\n }\n\n async request(): Promise<PermissionStatus> {\n return \"granted\";\n }\n}\n","/**\n * Centralized so retry behavior can be tuned in one place instead of\n * scattered magic numbers — see spec section 4 (\"Do not hard-code these\n * values throughout the codebase\").\n */\nexport const RETRY_POLICY = {\n /** Attempts beyond this stop increasing the delay — it plateaus at MAX_RETRY_DELAY_MS, but retrying never stops entirely (the SDK must never give up on an event; see storage/queue.ts). */\n MAX_RETRIES: 6,\n BASE_RETRY_DELAY_MS: 5_000,\n MAX_RETRY_DELAY_MS: 5 * 60_000,\n} as const;\n\n/**\n * Exponential backoff with a ceiling: attempt 1 -> ~5s, 2 -> ~10s, 3 -> ~20s,\n * ... capped at MAX_RETRY_DELAY_MS once `attempts` passes MAX_RETRIES. A\n * small jitter (±20%) avoids every queued event on a device — or every\n * device that lost connectivity at the same moment — retrying in lockstep.\n */\nexport function computeRetryDelayMs(attempts: number): number {\n const cappedAttempts = Math.min(attempts, RETRY_POLICY.MAX_RETRIES);\n const exponential = RETRY_POLICY.BASE_RETRY_DELAY_MS * 2 ** Math.max(0, cappedAttempts - 1);\n const delay = Math.min(exponential, RETRY_POLICY.MAX_RETRY_DELAY_MS);\n const jitter = delay * 0.2 * (Math.random() * 2 - 1);\n return Math.round(delay + jitter);\n}\n","import type { EventQueueStore, QueuedEvent } from \"./types\";\n\n/** Default store — also what unit tests use, since it needs no RN runtime. */\nexport class InMemoryEventQueueStore implements EventQueueStore {\n private readonly eventsById = new Map<string, QueuedEvent>();\n\n async getAll(): Promise<QueuedEvent[]> {\n return [...this.eventsById.values()];\n }\n\n async save(event: QueuedEvent): Promise<void> {\n this.eventsById.set(event.eventId, event);\n }\n\n async remove(eventId: string): Promise<void> {\n this.eventsById.delete(eventId);\n }\n}\n","import type { LocationEventPayload } from \"../protocol\";\n\nimport { computeRetryDelayMs } from \"../sync/retry-policy\";\nimport { InMemoryEventQueueStore } from \"./memory-store\";\nimport type { EventQueueStore, QueuedEvent } from \"./types\";\n\n/**\n * The local pending-event queue (spec: \"Offline queue\" / \"reliable SDK\n * event queue\"). An event is only ever in this queue between a failed send\n * attempt and a successful retry — there's no \"delivered\" status because a\n * delivered event is simply removed (`markSent`), which is also what makes\n * retrying an already-sent event harmless: it's not in the queue to retry.\n */\nexport class PendingEventQueue {\n constructor(private readonly store: EventQueueStore = new InMemoryEventQueueStore()) {}\n\n async enqueue(payload: LocationEventPayload): Promise<void> {\n await this.store.save({\n eventId: payload.id,\n type: payload.type,\n applicationId: payload.applicationId,\n deviceId: payload.deviceId,\n locationId: payload.locationId,\n geofenceId: payload.geofenceId,\n timestamp: payload.timestamp,\n payload,\n attempts: 0,\n status: \"pending\",\n createdAt: new Date().toISOString(),\n lastAttemptAt: null,\n nextAttemptAt: null,\n });\n }\n\n /** Every queued event, regardless of retry timing — used for diagnostics/pending counts. */\n async listPending(): Promise<QueuedEvent[]> {\n return this.store.getAll();\n }\n\n /** Only events whose backoff window has elapsed (or that have never been attempted) — used by SyncManager.retryPending(). */\n async listReadyForRetry(now: Date = new Date()): Promise<QueuedEvent[]> {\n const all = await this.store.getAll();\n return all.filter(\n (event) => event.status !== \"sending\" && (!event.nextAttemptAt || new Date(event.nextAttemptAt) <= now),\n );\n }\n\n /** Marks an event as in-flight so a concurrent retry pass won't also pick it up. */\n async markSending(eventId: string): Promise<void> {\n const existing = await this.get(eventId);\n if (!existing) return;\n await this.store.save({ ...existing, status: \"sending\" });\n }\n\n async markAttemptFailed(eventId: string): Promise<void> {\n const existing = await this.get(eventId);\n if (!existing) return;\n const attempts = existing.attempts + 1;\n const now = new Date();\n await this.store.save({\n ...existing,\n status: \"failed\",\n attempts,\n lastAttemptAt: now.toISOString(),\n nextAttemptAt: new Date(now.getTime() + computeRetryDelayMs(attempts)).toISOString(),\n });\n }\n\n async markSent(eventId: string): Promise<void> {\n await this.store.remove(eventId);\n }\n\n private async get(eventId: string): Promise<QueuedEvent | undefined> {\n return (await this.store.getAll()).find((event) => event.eventId === eventId);\n }\n}\n","export type NetworkStatus = \"online\" | \"offline\" | \"unknown\";\n\nexport interface NetworkMonitor {\n getStatus(): NetworkStatus;\n /** Calls `listener` whenever connectivity changes. Returns an unsubscribe function. */\n subscribe(listener: (status: NetworkStatus) => void): () => void;\n}\n\n/**\n * Default/test implementation — starts \"online\" (optimistic default: if we\n * can't observe connectivity, assume the network call itself is the\n * ultimate source of truth rather than blocking on a monitor). Tests (and\n * the Node/non-RN environment generally) drive it explicitly via\n * `setStatus`; see `sync/netinfo-network-monitor.ts` for the real RN\n * implementation, kept in its own file for the same reason\n * `storage/async-storage-store.ts` is — see that file's comment.\n */\nexport class ManualNetworkMonitor implements NetworkMonitor {\n private status: NetworkStatus = \"online\";\n private readonly listeners = new Set<(status: NetworkStatus) => void>();\n\n getStatus(): NetworkStatus {\n return this.status;\n }\n\n setStatus(status: NetworkStatus): void {\n if (status === this.status) return;\n this.status = status;\n this.listeners.forEach((listener) => listener(status));\n }\n\n subscribe(listener: (status: NetworkStatus) => void): () => void {\n this.listeners.add(listener);\n return () => this.listeners.delete(listener);\n }\n}\n","import type { LocationEventPayload } from \"../protocol\";\n\nimport type { ApiClient } from \"../firebase/api-client\";\nimport type { Logger } from \"../core/logger\";\nimport type { PendingEventQueue } from \"../storage/queue\";\n\nconst DEFAULT_RETRY_INTERVAL_MS = 30_000;\n\n/**\n * Only the one method SyncManager actually calls — `Pick` rather than the\n * full `ApiClient` class specifically so tests can pass a plain object\n * (`{ ingestEvent: vi.fn() }`) instead of a real ApiClient instance. A\n * class's private fields make it nominally typed, so a plain object\n * wouldn't satisfy `ApiClient` itself even with matching public methods.\n */\ntype EventIngestionClient = Pick<ApiClient, \"ingestEvent\">;\n\nexport interface SyncResult {\n attempted: number;\n delivered: number;\n failed: number;\n}\n\n/**\n * Send-or-queue plus backoff-aware retry (spec: \"Event retry system\").\n * `retryPending()` only attempts events whose backoff window has elapsed\n * (see PendingEventQueue.listReadyForRetry/RETRY_POLICY) — it's still safe\n * to call as often as you like (on a timer, on reconnect, or manually);\n * events not yet due just get skipped this pass. A successful resend of an\n * already-delivered event is also a no-op server-side, since\n * `ingestLocationEvent` dedupes on the event's own id — so calling this\n * concurrently with an in-flight `sendOrQueue` is never harmful, only\n * occasionally redundant.\n */\nexport class SyncManager {\n private retryTimer: ReturnType<typeof setInterval> | null = null;\n private lastSyncAt: string | null = null;\n private lastUploadResult: \"success\" | \"failure\" | null = null;\n\n constructor(\n private readonly apiClient: EventIngestionClient,\n private readonly queue: PendingEventQueue,\n private readonly retryIntervalMs: number = DEFAULT_RETRY_INTERVAL_MS,\n private readonly logger?: Logger,\n ) {}\n\n /**\n * Write-ahead: the event is durably queued *before* any network attempt is\n * made, so an app kill mid-request can never lose it (spec: \"true\n * write-ahead event persistence\" — a prior version of this method tried\n * the network first and only queued on failure, which meant an event in\n * flight when the process died was gone for good). `attemptDelivery` then\n * runs the exact same success/failure bookkeeping a queued retry uses, so\n * there is only one code path for \"what happens when an upload settles.\"\n */\n async sendOrQueue(payload: LocationEventPayload): Promise<void> {\n await this.queue.enqueue(payload);\n await this.attemptDelivery(payload.id, payload);\n }\n\n /**\n * Attempts queued events. By default only ones whose backoff window has\n * elapsed (the timer-driven path, `startAutoRetry` — the whole point of\n * backoff is to not hammer the backend on a fixed sweep). Pass\n * `{ force: true }` to ignore that window and attempt every non-in-flight\n * queued event regardless — connectivity just being confirmed restored\n * (see `GeoPulseSdk.start`'s network-monitor subscription) is a stronger\n * signal than \"some fixed delay has passed,\" and a device that was\n * offline for a while shouldn't have to wait out a stale backoff timer\n * once it's actually back online.\n */\n async retryPending(options: { force?: boolean } = {}): Promise<SyncResult> {\n const ready = options.force\n ? (await this.queue.listPending()).filter((event) => event.status !== \"sending\")\n : await this.queue.listReadyForRetry();\n const result: SyncResult = { attempted: ready.length, delivered: 0, failed: 0 };\n\n for (const event of ready) {\n const attemptNumber = event.attempts + 1;\n // Reflects the true attempt count server-side (spec: Event Explorer's \"Attempts\" column) — the\n // payload as originally generated always says 1, since it doesn't yet know it'll be retried.\n const delivered = await this.attemptDelivery(event.eventId, { ...event.payload, attempts: attemptNumber });\n if (delivered) {\n result.delivered++;\n } else {\n result.failed++;\n }\n }\n\n return result;\n }\n\n /**\n * The single place that actually talks to the backend: marks the queued\n * record \"sending\" (so a concurrent retry pass skips it), attempts the\n * upload, and resolves it to either removed-from-queue (success — a\n * `deduplicated: true` response still counts, since it means the backend\n * already has this event id, which is exactly the outcome a retry is\n * supposed to produce) or a scheduled retry (failure). Returns whether it\n * was delivered, purely so `retryPending` can tally its `SyncResult`.\n */\n private async attemptDelivery(eventId: string, payload: LocationEventPayload): Promise<boolean> {\n await this.queue.markSending(eventId);\n this.logger?.log(\"Event upload started\", { eventId, attempt: payload.attempts });\n try {\n await this.apiClient.ingestEvent(payload);\n await this.queue.markSent(eventId);\n this.lastSyncAt = new Date().toISOString();\n this.lastUploadResult = \"success\";\n this.logger?.log(\"Event upload succeeded\", { eventId });\n return true;\n } catch (error) {\n await this.queue.markAttemptFailed(eventId);\n this.lastUploadResult = \"failure\";\n this.logger?.warn(\"Event upload failed — retry scheduled\", { eventId, error: String(error) });\n return false;\n }\n }\n\n startAutoRetry(): void {\n if (this.retryTimer) return;\n this.retryTimer = setInterval(() => {\n void this.retryPending();\n }, this.retryIntervalMs);\n }\n\n stopAutoRetry(): void {\n if (this.retryTimer) {\n clearInterval(this.retryTimer);\n this.retryTimer = null;\n }\n }\n\n getLastSyncAt(): string | null {\n return this.lastSyncAt;\n }\n\n getLastUploadResult(): \"success\" | \"failure\" | null {\n return this.lastUploadResult;\n }\n}\n","/**\n * Structured, easy-to-disable debug logging (spec section 22). Every call\n * site passes a short lifecycle label matching the examples in this\n * package's README (\"Configuration synced\", \"Event queued\", ...) plus\n * optional structured details — never the SDK key, never a raw event\n * payload body, never anything from `GeoPulseConfig` beyond `applicationId`.\n */\nexport class Logger {\n constructor(private enabled: boolean) {}\n\n setEnabled(enabled: boolean): void {\n this.enabled = enabled;\n }\n\n log(message: string, details?: Record<string, unknown>): void {\n if (!this.enabled) return;\n if (details) {\n console.log(`[GeoPulse] ${message}`, details);\n } else {\n console.log(`[GeoPulse] ${message}`);\n }\n }\n\n warn(message: string, details?: Record<string, unknown>): void {\n if (!this.enabled) return;\n if (details) {\n console.warn(`[GeoPulse] ${message}`, details);\n } else {\n console.warn(`[GeoPulse] ${message}`);\n }\n }\n}\n","import type {\n DevicePlatform,\n ExperienceDeliveryPayload,\n GeofenceEventType,\n LocationEventPayload,\n SdkGeofenceConfig,\n SdkProjectConfig,\n} from \"../protocol\";\n\nimport {\n ConfigCache,\n DEFAULT_BACKEND_URL,\n InMemoryKeyValueStore,\n getOrCreateDeviceId,\n SDK_VERSION,\n type GeoPulseConfig,\n type KeyValueStore,\n} from \"../config\";\nimport { ApiClient } from \"../firebase/api-client\";\nimport { createEventPayload } from \"../events/factory\";\nimport { GeofenceStateMachine } from \"../geofencing/state-machine\";\nimport type { LocationProvider, LocationUpdate } from \"../location/types\";\nimport { SimulatedLocationProvider } from \"../location/simulated-location-provider\";\nimport type { PermissionProvider, PermissionStatus } from \"../permissions/types\";\nimport { SimulatedPermissionProvider } from \"../permissions/simulated-permission-provider\";\nimport { PendingEventQueue } from \"../storage/queue\";\nimport { ManualNetworkMonitor, type NetworkMonitor } from \"../sync/network-monitor\";\nimport { SyncManager } from \"../sync/sync-manager\";\nimport { Logger } from \"./logger\";\nimport type { ConfigurationStatus, DiagnosticsSnapshot, SdkStatus } from \"./types\";\n\ntype EventListener = (event: LocationEventPayload) => void;\ntype ExperienceListener = (payload: ExperienceDeliveryPayload) => void;\n\n/** Mirrors SyncManager's own default retry interval — see sync/sync-manager.ts's DEFAULT_RETRY_INTERVAL_MS. A host app that wants faster demo feedback (see apps/playground) can override via GeoPulseConfig.experiencePollIntervalMs. */\n/** The SDK key resolved to a different application than `config.applicationId`. */\nexport class ApplicationMismatchError extends Error {\n constructor() {\n super(\"GeoPulse: the SDK key belongs to a different application than config.applicationId — check both values\");\n this.name = \"ApplicationMismatchError\";\n }\n}\n\nconst DEFAULT_EXPERIENCE_POLL_INTERVAL_MS = 30_000;\n/** Floor for any caller-supplied interval (retryIntervalMs/experiencePollIntervalMs) — a config of `0` or a negative number would otherwise turn into a tight, runaway retry/poll loop (Phase 7 SDK audit finding). */\nconst MIN_TIMER_INTERVAL_MS = 1_000;\n\n/** `undefined` passes through untouched (lets the caller's own default apply); a defined-but-too-small value is floored instead of silently creating a runaway timer. */\nfunction clampIntervalMs(value: number | undefined): number | undefined {\n if (value === undefined) {\n return undefined;\n }\n if (!Number.isFinite(value) || value < MIN_TIMER_INTERVAL_MS) {\n return MIN_TIMER_INTERVAL_MS;\n }\n return value;\n}\n\n/**\n * Everything the SDK talks to outside of itself, injectable so\n * `GeoPulseSdk` (this whole lifecycle) is unit-testable without an RN\n * runtime — tests construct it with fakes; `src/index.ts` (the real public\n * entrypoint) constructs the default instance with the real RN adapters.\n */\nexport interface GeoPulseSdkDependencies {\n locationProvider?: LocationProvider;\n permissionProvider?: PermissionProvider;\n queue?: PendingEventQueue;\n createApiClient?: (config: GeoPulseConfig) => ApiClient;\n deviceIdStore?: KeyValueStore;\n configCacheStore?: KeyValueStore;\n networkMonitor?: NetworkMonitor;\n /** Defaults to \"unknown\" — src/index.ts supplies the real `Platform.OS` so this file stays RN-free. */\n getPlatform?: () => DevicePlatform;\n /** Overrides GeoPulseConfig.debug — mainly for tests that want to assert on log output. */\n debug?: boolean;\n}\n\nexport class GeoPulseSdk {\n private status: SdkStatus = \"idle\";\n private config: GeoPulseConfig | null = null;\n private apiClient: ApiClient | null = null;\n private syncManager: SyncManager | null = null;\n private resolvedProjectId: string | null = null;\n private resolvedApplicationId: string | null = null;\n private applicationEnabled = true;\n private configVersion: string | null = null;\n private configurationStatus: ConfigurationStatus = \"unsynced\";\n private geofences: SdkGeofenceConfig[] = [];\n private lastKnownLocation: LocationUpdate | null = null;\n private lastEvent: LocationEventPayload | null = null;\n private deviceId: string | null = null;\n private locationError: string | null = null;\n private unsubscribeNetwork: (() => void) | null = null;\n private readonly stateMachine = new GeofenceStateMachine();\n private readonly listeners = new Map<GeofenceEventType, Set<EventListener>>();\n private readonly experienceListeners = new Set<ExperienceListener>();\n /** 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. */\n private readonly seenExperienceDeliveryIds = new Set<string>();\n private experiencePollTimer: ReturnType<typeof setInterval> | null = null;\n private experiencesReceivedCount = 0;\n private readonly logger: Logger;\n\n private readonly locationProvider: LocationProvider;\n private readonly permissionProvider: PermissionProvider;\n private readonly queue: PendingEventQueue;\n private readonly buildApiClient: (config: GeoPulseConfig) => ApiClient;\n private readonly deviceIdStore: KeyValueStore;\n private readonly configCache: ConfigCache;\n private readonly networkMonitor: NetworkMonitor;\n private readonly getPlatform: () => DevicePlatform;\n private experiencePollIntervalMs = DEFAULT_EXPERIENCE_POLL_INTERVAL_MS;\n\n constructor(dependencies: GeoPulseSdkDependencies = {}) {\n this.locationProvider = dependencies.locationProvider ?? new SimulatedLocationProvider();\n this.permissionProvider = dependencies.permissionProvider ?? new SimulatedPermissionProvider();\n this.queue = dependencies.queue ?? new PendingEventQueue();\n this.buildApiClient =\n dependencies.createApiClient ?? ((config) => new ApiClient(config.functionsBaseUrl || DEFAULT_BACKEND_URL, config.apiKey, config.applicationId));\n this.deviceIdStore = dependencies.deviceIdStore ?? new InMemoryKeyValueStore();\n this.configCache = new ConfigCache(dependencies.configCacheStore ?? new InMemoryKeyValueStore());\n this.networkMonitor = dependencies.networkMonitor ?? new ManualNetworkMonitor();\n this.getPlatform = dependencies.getPlatform ?? (() => \"unknown\");\n this.logger = new Logger(dependencies.debug ?? false);\n }\n\n /**\n * Resolves this device's persistent id, then fetches (or falls back to\n * cached) project configuration. Must succeed — fresh or cached — before\n * `start()`; if neither is available (first-ever run with no network),\n * this rethrows and `getStatus()` reports \"error\".\n */\n async initialize(config: GeoPulseConfig): Promise<void> {\n // Without this guard, calling initialize() while already \"running\" replaced\n // this.syncManager with a fresh instance, orphaning the old one's auto-retry timer (it kept\n // firing forever with no reference left to stop it) while location callbacks from the\n // still-running locationProvider could race against the new config being applied — a real\n // bug found in Phase 7's SDK audit, not a hypothetical. Re-initializing a live SDK has no\n // safe, well-defined meaning short of a full stop() first.\n if (this.status === \"running\") {\n throw new Error(\"GeoPulse.initialize() cannot be called while already running — call GeoPulse.stop() first\");\n }\n if (this.status === \"initializing\") {\n throw new Error(\"GeoPulse.initialize() is already in progress — wait for the first call to resolve\");\n }\n if (!config.apiKey || !config.applicationId) {\n throw new Error(\"GeoPulse.initialize requires both apiKey and applicationId\");\n }\n\n if (config.debug !== undefined) {\n this.logger.setEnabled(config.debug);\n }\n this.logger.log(\"GeoPulse.initialize()\", { applicationId: config.applicationId });\n\n this.status = \"initializing\";\n this.config = config;\n this.experiencePollIntervalMs = clampIntervalMs(config.experiencePollIntervalMs) ?? DEFAULT_EXPERIENCE_POLL_INTERVAL_MS;\n this.apiClient = this.buildApiClient(config);\n this.syncManager = new SyncManager(this.apiClient, this.queue, clampIntervalMs(config.retryIntervalMs), this.logger);\n this.deviceId = await getOrCreateDeviceId(this.deviceIdStore);\n\n try {\n const projectConfig = await this.apiClient.getProjectConfig({\n deviceId: this.deviceId,\n platform: this.getPlatform(),\n sdkVersion: SDK_VERSION,\n });\n // The key must belong to the application the integrator configured; never adopt another one.\n if (projectConfig.applicationId !== config.applicationId) {\n throw new ApplicationMismatchError();\n }\n this.applyProjectConfig(projectConfig);\n this.configurationStatus = \"synced\";\n await this.configCache.save(projectConfig);\n this.status = \"initialized\";\n this.logger.log(\"Configuration synced\", { geofenceCount: projectConfig.geofences.length, configVersion: projectConfig.configVersion });\n } catch (error) {\n if (error instanceof ApplicationMismatchError) {\n this.configurationStatus = \"unavailable\";\n this.status = \"error\";\n throw error;\n }\n const cached = await this.configCache.load();\n if (cached && cached.config.applicationId === config.applicationId) {\n this.applyProjectConfig(cached.config);\n this.configurationStatus = \"cached\";\n this.status = \"initialized\";\n this.logger.warn(\"getProjectConfig failed — using cached configuration\", { fetchedAt: cached.fetchedAt });\n return;\n }\n this.configurationStatus = \"unavailable\";\n this.status = \"error\";\n throw error;\n }\n }\n\n async start(): Promise<void> {\n if (this.status === \"running\") {\n // Previously fell into the same generic \"called before initialize succeeded\" message below\n // even though initialize DID succeed and the SDK IS running — factually wrong and would\n // send a developer debugging a double-start() down the wrong path (Phase 7 SDK audit).\n throw new Error(\"GeoPulse.start() called while already running — call GeoPulse.stop() first if you want to restart\");\n }\n if (this.status !== \"initialized\" && this.status !== \"stopped\") {\n throw new Error(\"GeoPulse.start called before GeoPulse.initialize succeeded\");\n }\n if (!this.applicationEnabled) {\n this.status = \"error\";\n throw new Error(\"This application has been disabled in the GeoPulse admin portal\");\n }\n\n const permission = await this.permissionProvider.request();\n if (permission !== \"granted\") {\n this.status = \"error\";\n throw new Error(`Location permission not granted (status: ${permission})`);\n }\n\n this.locationError = null;\n await this.locationProvider.start(\n (location) => {\n void this.handleLocationUpdate(location);\n },\n (error) => {\n this.locationError = error.message;\n this.logger.warn(\"Location provider failed to start\", { error: error.message });\n },\n );\n if (this.locationError) {\n // The provider caught its own failure and reported it via onError rather than rejecting\n // start() directly (see LocationProvider.start's doc) — surfaced here as a normal rejection\n // so callers get the same \"start() throws on failure\" contract as the permission-denied case.\n this.status = \"error\";\n throw new Error(`GeoPulse failed to start location tracking: ${this.locationError}`);\n }\n\n this.unsubscribeNetwork = this.networkMonitor.subscribe((networkStatus) => {\n if (networkStatus === \"online\") {\n this.logger.log(\"Network available — retrying queued events\");\n void this.syncManager?.retryPending({ force: true });\n }\n });\n\n this.syncManager?.startAutoRetry();\n this.startExperiencePolling();\n this.status = \"running\";\n this.logger.log(\"GeoPulse.start()\");\n }\n\n stop(): void {\n this.locationProvider.stop();\n this.syncManager?.stopAutoRetry();\n this.stopExperiencePolling();\n this.unsubscribeNetwork?.();\n this.unsubscribeNetwork = null;\n this.status = \"stopped\";\n this.logger.log(\"GeoPulse.stop()\");\n }\n\n getStatus(): SdkStatus {\n return this.status;\n }\n\n async getPermissionStatus(): Promise<PermissionStatus> {\n return this.permissionProvider.getStatus();\n }\n\n on(eventName: GeofenceEventType, callback: EventListener): void;\n on(eventName: \"experience.received\", callback: ExperienceListener): void;\n on(eventName: GeofenceEventType | \"experience.received\", callback: EventListener | ExperienceListener): void {\n if (eventName === \"experience.received\") {\n this.experienceListeners.add(callback as ExperienceListener);\n return;\n }\n if (!this.listeners.has(eventName)) {\n this.listeners.set(eventName, new Set());\n }\n this.listeners.get(eventName)?.add(callback as EventListener);\n }\n\n off(eventName: GeofenceEventType, callback: EventListener): void;\n off(eventName: \"experience.received\", callback: ExperienceListener): void;\n off(eventName: GeofenceEventType | \"experience.received\", callback: EventListener | ExperienceListener): void {\n if (eventName === \"experience.received\") {\n this.experienceListeners.delete(callback as ExperienceListener);\n return;\n }\n this.listeners.get(eventName)?.delete(callback as EventListener);\n }\n\n // --- Small read-only accessors beyond the core 7-method API, so a host\n // app (see apps/playground) can build a status UI around the SDK without\n // it needing to duplicate state the SDK already tracks internally. ---\n\n getConfiguredGeofences(): SdkGeofenceConfig[] {\n return this.geofences;\n }\n\n /** The server-resolved application id (from `getProjectConfig`), or null before `initialize()` completes. */\n getApplicationId(): string | null {\n return this.resolvedApplicationId;\n }\n\n getLastKnownLocation(): LocationUpdate | null {\n return this.lastKnownLocation;\n }\n\n async getPendingEventCount(): Promise<number> {\n return (await this.queue.listPending()).length;\n }\n\n /** Null until `initialize()` resolves it — persisted across app restarts (see config/device-id.ts). */\n getDeviceId(): string | null {\n return this.deviceId;\n }\n\n /**\n * The rich developer-diagnostics snapshot (spec section 10/11) — a\n * superset of `getStatus()`, not a replacement for it (see core/types.ts's\n * `DiagnosticsSnapshot` doc for why these are two separate methods).\n */\n async getDiagnostics(): Promise<DiagnosticsSnapshot> {\n const pending = await this.queue.listPending();\n const permissionStatus = this.status === \"idle\" || this.status === \"initializing\" ? \"unknown\" : await this.permissionProvider.getStatus();\n\n return {\n initialized: this.status !== \"idle\" && this.status !== \"initializing\",\n running: this.status === \"running\",\n locationAvailable: this.lastKnownLocation !== null,\n permissionStatus,\n networkStatus: this.networkMonitor.getStatus(),\n configurationStatus: this.configurationStatus,\n configVersion: this.configVersion,\n pendingEvents: pending.length,\n totalRetryAttempts: pending.reduce((sum, event) => sum + event.attempts, 0),\n locationError: this.locationError,\n lastEventAt: this.lastEvent?.timestamp ?? null,\n lastEventType: this.lastEvent?.type ?? null,\n lastSyncAt: this.syncManager?.getLastSyncAt() ?? null,\n lastUploadResult: this.syncManager?.getLastUploadResult() ?? null,\n deviceId: this.deviceId,\n sdkVersion: SDK_VERSION,\n experiencesReceivedCount: this.experiencesReceivedCount,\n };\n }\n\n /**\n * Only functional when the SDK is running the simulated location\n * provider (the default outside of production builds — see\n * src/index.ts) and gated by `__DEV__` so it's inert in a production\n * bundle even if a caller somehow held a reference to it.\n */\n simulateLocation(coords: { latitude: number; longitude: number }): void {\n const isDev = typeof __DEV__ === \"undefined\" ? true : __DEV__;\n if (!isDev) {\n console.warn(\"GeoPulse.simulator is unavailable in production builds.\");\n return;\n }\n if (!(this.locationProvider instanceof SimulatedLocationProvider)) {\n console.warn(\n \"GeoPulse.simulator.setLocation has no effect: the SDK is not running the simulated location provider.\",\n );\n return;\n }\n this.locationProvider.setLocation(coords);\n }\n\n /**\n * The Phase 5B delivery mechanism: poll immediately on `start()` (so a\n * demo doesn't wait a full interval for the first check), then on a\n * timer. A poll failure (network blip, backend down) is logged and\n * swallowed — it must never crash the SDK or the host app (spec section\n * 13), and the next tick tries again on its own.\n */\n private startExperiencePolling(): void {\n void this.pollForExperiences();\n this.experiencePollTimer = setInterval(() => {\n void this.pollForExperiences();\n }, this.experiencePollIntervalMs);\n }\n\n private stopExperiencePolling(): void {\n if (this.experiencePollTimer) {\n clearInterval(this.experiencePollTimer);\n this.experiencePollTimer = null;\n }\n }\n\n private async pollForExperiences(): Promise<void> {\n if (!this.apiClient || !this.deviceId) {\n return;\n }\n try {\n const experiences = await this.apiClient.getPendingExperiences(this.deviceId);\n for (const experience of experiences) {\n // Backend is the source of truth (spec section 14) — this is defense in depth against\n // observing the same still-\"pending\" delivery twice across two polls before the ack lands.\n if (this.seenExperienceDeliveryIds.has(experience.deliveryId)) {\n continue;\n }\n this.seenExperienceDeliveryIds.add(experience.deliveryId);\n this.experiencesReceivedCount += 1;\n this.logger.log(\"Experience received\", { deliveryId: experience.deliveryId, campaignId: experience.campaignId });\n this.experienceListeners.forEach((listener) => listener(experience));\n\n await this.apiClient.acknowledgeExperienceDelivery(experience.deliveryId).catch((error) => {\n this.logger.warn(\"Failed to acknowledge experience delivery\", {\n deliveryId: experience.deliveryId,\n error: error instanceof Error ? error.message : String(error),\n });\n });\n }\n } catch (error) {\n this.logger.warn(\"Polling for experiences failed\", { error: error instanceof Error ? error.message : String(error) });\n }\n }\n\n private applyProjectConfig(projectConfig: SdkProjectConfig): void {\n this.resolvedProjectId = projectConfig.projectId;\n this.resolvedApplicationId = projectConfig.applicationId;\n this.applicationEnabled = projectConfig.applicationEnabled;\n this.configVersion = projectConfig.configVersion;\n this.geofences = projectConfig.geofences;\n }\n\n private async handleLocationUpdate(location: LocationUpdate): Promise<void> {\n this.lastKnownLocation = location;\n if (!this.resolvedProjectId || !this.resolvedApplicationId || !this.syncManager || !this.deviceId) {\n return;\n }\n\n const transitions = this.stateMachine.evaluate(location, this.geofences);\n for (const transition of transitions) {\n this.logger.log(\"Geofence state changed\", { geofenceId: transition.geofence.id, type: transition.type });\n\n const event = createEventPayload({\n type: transition.type,\n projectId: this.resolvedProjectId,\n applicationId: this.resolvedApplicationId,\n geofence: transition.geofence,\n location,\n deviceId: this.deviceId,\n });\n this.lastEvent = event;\n this.logger.log(\"Event generated\", { eventId: event.id, type: event.type });\n\n this.listeners.get(transition.type)?.forEach((listener) => listener(event));\n await this.syncManager.sendOrQueue(event);\n }\n }\n}\n","import * as Location from \"expo-location\";\n\nimport type { LocationProvider, LocationUpdate } from \"./types\";\n\n/**\n * Real, foreground-only device location via expo-location. Does not\n * request or use background location — see this package's README for the\n * platform permission requirements and why background tracking is\n * explicitly out of scope for this phase.\n */\nexport class DeviceLocationProvider implements LocationProvider {\n private subscription: Location.LocationSubscription | null = null;\n\n /**\n * `watchPositionAsync` rejects if location services are off at the OS\n * level (distinct from the app's own permission grant, which is checked\n * before this ever runs) — caught here and reported via `onError` instead\n * of left to reject this method's own promise, so a caller that doesn't\n * wrap `GeoPulse.start()` in a try/catch still can't end up with an\n * unhandled rejection from deep inside a native module.\n */\n async start(onUpdate: (location: LocationUpdate) => void, onError?: (error: Error) => void): Promise<void> {\n try {\n this.subscription = await Location.watchPositionAsync(\n { accuracy: Location.Accuracy.Balanced, timeInterval: 5000, distanceInterval: 10 },\n (position) => {\n onUpdate({\n latitude: position.coords.latitude,\n longitude: position.coords.longitude,\n timestamp: new Date(position.timestamp).toISOString(),\n });\n },\n );\n } catch (error) {\n onError?.(error instanceof Error ? error : new Error(String(error)));\n }\n }\n\n stop(): void {\n this.subscription?.remove();\n this.subscription = null;\n }\n}\n","import * as Location from \"expo-location\";\n\nimport type { PermissionProvider, PermissionStatus } from \"./types\";\n\nfunction mapExpoStatus(status: Location.PermissionStatus): PermissionStatus {\n switch (status) {\n case Location.PermissionStatus.GRANTED:\n return \"granted\";\n case Location.PermissionStatus.DENIED:\n return \"denied\";\n default:\n return \"undetermined\";\n }\n}\n\n/**\n * Foreground-only — see this package's README (\"Permissions\" section) for\n * why background permission is explicitly out of scope for this phase.\n * Requests only the foreground permission; never touches\n * `requestBackgroundPermissionsAsync`.\n */\nexport class ExpoPermissionProvider implements PermissionProvider {\n async getStatus(): Promise<PermissionStatus> {\n const { status } = await Location.getForegroundPermissionsAsync();\n return mapExpoStatus(status);\n }\n\n async request(): Promise<PermissionStatus> {\n const { status } = await Location.requestForegroundPermissionsAsync();\n return mapExpoStatus(status);\n }\n}\n","import AsyncStorage from \"@react-native-async-storage/async-storage\";\n\nimport type { EventQueueStore, QueuedEvent } from \"./types\";\n\nconst STORAGE_KEY = \"@geopulse/pending-events\";\n\n/**\n * Persists the pending-event queue across app restarts using AsyncStorage —\n * required for the offline queue to actually survive the app being killed\n * before connectivity returns, not just a network blip while it's running.\n * Single-JSON-blob storage rather than one key per event: the expected\n * queue size (events waiting on a flaky connection) is small enough that\n * read/write-modify-write on one key is simpler and fine.\n */\nexport class AsyncStorageEventQueueStore implements EventQueueStore {\n async getAll(): Promise<QueuedEvent[]> {\n const raw = await AsyncStorage.getItem(STORAGE_KEY);\n if (!raw) return [];\n try {\n return JSON.parse(raw) as QueuedEvent[];\n } catch {\n return [];\n }\n }\n\n async save(event: QueuedEvent): Promise<void> {\n const all = await this.getAll();\n const next = [...all.filter((existing) => existing.eventId !== event.eventId), event];\n await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(next));\n }\n\n async remove(eventId: string): Promise<void> {\n const all = await this.getAll();\n await AsyncStorage.setItem(STORAGE_KEY, JSON.stringify(all.filter((existing) => existing.eventId !== eventId)));\n }\n}\n","import NetInfo from \"@react-native-community/netinfo\";\n\nimport type { NetworkMonitor, NetworkStatus } from \"./network-monitor\";\n\nfunction toStatus(isConnected: boolean | null): NetworkStatus {\n if (isConnected === null) return \"unknown\";\n return isConnected ? \"online\" : \"offline\";\n}\n\n/** Real connectivity detection for the RN app. Isolated in its own file — see storage/async-storage-store.ts's comment for why. */\nexport class NetInfoNetworkMonitor implements NetworkMonitor {\n private status: NetworkStatus = \"unknown\";\n\n constructor() {\n NetInfo.fetch().then((state) => {\n this.status = toStatus(state.isConnected);\n });\n }\n\n getStatus(): NetworkStatus {\n return this.status;\n }\n\n subscribe(listener: (status: NetworkStatus) => void): () => void {\n return NetInfo.addEventListener((state) => {\n this.status = toStatus(state.isConnected);\n listener(this.status);\n });\n }\n}\n","import React from \"react\";\nimport { Image, Linking, Text, TouchableOpacity, View } from \"react-native\";\n\nimport { resolveExperienceStyle, type ExperienceDeliveryPayload, type ResolvedExperienceStyle } from \"../protocol\";\n\ntype ExperienceContent = ExperienceDeliveryPayload[\"experience\"];\n\n/**\n * Experience URLs come from the backend. Only web URLs are opened: schemes\n * like `intent:`, `tel:`, `file:` or another app's deep links could trigger\n * actions outside this app (audit M-2).\n */\nexport function isSafeExternalUrl(url: string | null | undefined): url is string {\n return typeof url === \"string\" && /^https?:\\/\\/[^\\s]+$/i.test(url);\n}\n\nfunction openUrl(url: string | null): void {\n if (!isSafeExternalUrl(url)) return;\n // Linking.openURL hands off to the OS/system browser — no WebView, no\n // in-app script execution, matching the existing playground component's\n // rule this replaces (see apps/playground/src/components/ExperienceCard.tsx).\n Linking.openURL(url).catch(() => {\n // Swallow — an unreachable/malformed URL must never crash the host app.\n });\n}\n\nfunction ExperienceButton({ style, ctaLabel, ctaUrl }: { style: ResolvedExperienceStyle; ctaLabel: string | null; ctaUrl: string | null }) {\n if (!style.button.visible || !ctaLabel || !ctaUrl) return null;\n const { position } = style.button;\n return (\n <TouchableOpacity\n onPress={() => openUrl(ctaUrl)}\n style={{\n position: \"absolute\",\n top: position.top ?? undefined,\n bottom: position.bottom ?? undefined,\n left: position.left ?? undefined,\n right: position.right ?? undefined,\n backgroundColor: style.button.backgroundColor,\n borderRadius: style.button.borderRadius,\n paddingVertical: 10,\n paddingHorizontal: 16,\n }}\n >\n <Text style={{ color: style.button.textColor, fontWeight: \"600\" }}>{ctaLabel}</Text>\n </TouchableOpacity>\n );\n}\n\n/** Shared by all three experience types — full-bleed background image (if any) behind title/body/button, everything else laid out per-type by its caller. */\nfunction ExperienceContainer({ content, style, children }: { content: ExperienceContent; style: ResolvedExperienceStyle; children: React.ReactNode }): React.JSX.Element {\n const containerStyle = {\n backgroundColor: style.container.backgroundColor,\n borderColor: style.container.borderColor ?? undefined,\n borderWidth: style.container.borderWidth,\n borderRadius: style.container.borderRadius,\n padding: style.container.padding,\n overflow: \"hidden\" as const,\n width: style.container.width === \"fill\" ? (\"100%\" as const) : style.container.width,\n // \"auto\" means no explicit height at all — RN sizes a View to its content by default, so\n // omitting the property (rather than passing \"auto\", which RN's height type doesn't accept)\n // is what actually produces that behavior.\n // The button is absolutely positioned, so it takes no layout space — without a floor, an\n // \"auto\" container shrinks to title+body and the button overlaps them. 160 matches the\n // admin-web preview (experience-preview.tsx) so the device renders what the portal shows.\n ...(style.container.height === \"auto\" ? { minHeight: 160 } : { height: style.container.height }),\n };\n const backgroundImage = isSafeExternalUrl(style.container.backgroundImageUrl) ? (\n <Image\n source={{ uri: style.container.backgroundImageUrl }}\n resizeMode={style.container.backgroundImageFit}\n style={{ position: \"absolute\", top: 0, left: 0, right: 0, bottom: 0 }}\n />\n ) : null;\n\n // TouchableOpacity and View have different prop shapes, so this branches on\n // two full JSX returns rather than picking a dynamic component variable —\n // the latter can't be given a declaration-emittable type.\n if (content.cardNavigationUrl) {\n const cardNavigationUrl = content.cardNavigationUrl;\n return (\n <TouchableOpacity testID=\"experience-container\" onPress={() => openUrl(cardNavigationUrl)} style={containerStyle}>\n {backgroundImage}\n {children}\n </TouchableOpacity>\n );\n }\n return (\n <View testID=\"experience-container\" style={containerStyle}>\n {backgroundImage}\n {children}\n </View>\n );\n}\n\nfunction ExperienceText({ visible, text, color, fontSize, fontWeight }: { visible: boolean; text: string; color: string; fontSize: number; fontWeight: \"normal\" | \"bold\" }) {\n if (!visible) return null;\n return <Text style={{ color, fontSize, fontWeight }}>{text}</Text>;\n}\n\nexport function CardExperience({ content }: { content: ExperienceContent }): React.JSX.Element {\n const style = resolveExperienceStyle(\"card\", content.style);\n return (\n <ExperienceContainer content={content} style={style}>\n <ExperienceText visible={style.title.visible} text={content.title} color={style.title.color} fontSize={style.title.fontSize} fontWeight={style.title.fontWeight} />\n <ExperienceText visible={style.body.visible} text={content.body} color={style.body.color} fontSize={style.body.fontSize} fontWeight={style.body.fontWeight} />\n <ExperienceButton style={style} ctaLabel={content.ctaLabel} ctaUrl={content.ctaUrl} />\n </ExperienceContainer>\n );\n}\n\nexport function BannerExperience({ content }: { content: ExperienceContent }): React.JSX.Element {\n const style = resolveExperienceStyle(\"banner\", content.style);\n return (\n <ExperienceContainer content={content} style={style}>\n <ExperienceText visible={style.title.visible} text={content.title} color={style.title.color} fontSize={style.title.fontSize} fontWeight={style.title.fontWeight} />\n <ExperienceText visible={style.body.visible} text={content.body} color={style.body.color} fontSize={style.body.fontSize} fontWeight={style.body.fontWeight} />\n <ExperienceButton style={style} ctaLabel={content.ctaLabel} ctaUrl={content.ctaUrl} />\n </ExperienceContainer>\n );\n}\n\nexport function MessageExperience({ content }: { content: ExperienceContent }): React.JSX.Element {\n const style = resolveExperienceStyle(\"message\", content.style);\n return (\n <ExperienceContainer content={content} style={style}>\n <ExperienceText visible={style.title.visible} text={content.title} color={style.title.color} fontSize={style.title.fontSize} fontWeight={style.title.fontWeight} />\n <ExperienceText visible={style.body.visible} text={content.body} color={style.body.color} fontSize={style.body.fontSize} fontWeight={style.body.fontWeight} />\n <ExperienceButton style={style} ctaLabel={content.ctaLabel} ctaUrl={content.ctaUrl} />\n </ExperienceContainer>\n );\n}\n\n/** Picks the right renderer by `experience.type` — the one component most host apps need. */\nexport function ExperienceView({ experience }: { experience: ExperienceContent }): React.JSX.Element {\n switch (experience.type) {\n case \"banner\":\n return <BannerExperience content={experience} />;\n case \"message\":\n return <MessageExperience content={experience} />;\n case \"card\":\n default:\n return <CardExperience content={experience} />;\n }\n}\n"],"mappings":"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","names":["import_react_native","AsyncStorage","isDev","Location","import_async_storage","AsyncStorage","NetInfo","React"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sharib/geopulse-react-native",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.4",
|
|
4
4
|
"description": "GeoPulse React Native SDK — on-device geofence detection and event sync.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -14,30 +14,30 @@
|
|
|
14
14
|
"files": [
|
|
15
15
|
"dist"
|
|
16
16
|
],
|
|
17
|
-
"scripts": {
|
|
18
|
-
"build": "tsup",
|
|
19
|
-
"lint": "eslint src --ext .ts",
|
|
20
|
-
"type-check": "tsc --noEmit",
|
|
21
|
-
"test": "vitest run",
|
|
22
|
-
"clean": "rm -rf dist"
|
|
23
|
-
},
|
|
24
17
|
"dependencies": {
|
|
25
18
|
"@react-native-async-storage/async-storage": "2.2.0",
|
|
26
19
|
"@react-native-community/netinfo": "12.0.1",
|
|
27
20
|
"expo-location": "~57.0.20"
|
|
28
21
|
},
|
|
29
22
|
"devDependencies": {
|
|
30
|
-
"@geopulse/shared": "workspace:*",
|
|
31
23
|
"@types/node": "^20.16.11",
|
|
32
24
|
"@types/react": "~19.2.18",
|
|
33
25
|
"react": "19.2.3",
|
|
34
26
|
"react-native": "0.86.3",
|
|
35
27
|
"tsup": "^8.3.5",
|
|
36
28
|
"typescript": "^5.6.3",
|
|
37
|
-
"vitest": "^2.1.2"
|
|
29
|
+
"vitest": "^2.1.2",
|
|
30
|
+
"@geopulse/shared": "0.1.0"
|
|
38
31
|
},
|
|
39
32
|
"peerDependencies": {
|
|
40
33
|
"react": "19.2.3",
|
|
41
34
|
"react-native": "0.86.3"
|
|
35
|
+
},
|
|
36
|
+
"scripts": {
|
|
37
|
+
"build": "tsup",
|
|
38
|
+
"lint": "eslint src --ext .ts",
|
|
39
|
+
"type-check": "tsc --noEmit",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"clean": "rm -rf dist"
|
|
42
42
|
}
|
|
43
|
-
}
|
|
43
|
+
}
|