@wireai/activation 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +51 -0
- package/CHANGELOG.md +111 -1
- package/INTEGRATION_PROMPT.md +13 -1
- package/README.md +106 -4
- package/dist/analytics/index.d.mts +35 -6
- package/dist/analytics/index.d.ts +35 -6
- package/dist/analytics/index.js +222 -94
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +214 -95
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/{currentSession-D0Vq7_VE.d.ts → currentSession-D6RiVtc8.d.ts} +187 -29
- package/dist/{currentSession-DdDkprpM.d.mts → currentSession-DsSDHqor.d.mts} +187 -29
- package/dist/index.d.mts +236 -82
- package/dist/index.d.ts +236 -82
- package/dist/index.js +330 -60
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +314 -61
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +1 -1
- package/dist/questionnaire/index.d.ts +1 -1
- package/dist/questionnaire/index.js +59 -8
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +59 -8
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +2 -2
- package/dist/reviews/index.d.ts +2 -2
- package/dist/reviews/index.js +97 -17
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +97 -17
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/{transport-Bzb-bcB2.d.mts → transport-CF_eHwzC.d.mts} +15 -16
- package/dist/{transport-B31G0Cib.d.ts → transport-DsRe4epC.d.ts} +15 -16
- package/llms.txt +9 -0
- package/package.json +1 -1
- package/src/activation/useWireActivation.ts +12 -1
- package/src/activation/wireActivation.ts +36 -24
- package/src/analytics/analyticsFacade.ts +113 -29
- package/src/analytics/currentSession.ts +83 -0
- package/src/analytics/eventQueue.ts +20 -11
- package/src/analytics/index.ts +29 -1
- package/src/analytics/reportClientEvent.ts +50 -2
- package/src/analytics/screenTracking.ts +6 -1
- package/src/analytics/useAnalytics.ts +22 -1
- package/src/context/deviceId.ts +109 -0
- package/src/context/userContext.ts +73 -0
- package/src/identity/userIdentity.ts +10 -0
- package/src/index.ts +50 -2
- package/src/questionnaire/runtime.ts +12 -2
- package/src/questionnaire/transport.ts +5 -1
- package/src/questionnaire/useQuestionnaireGate.ts +9 -7
- package/src/revenuecat/index.ts +55 -0
- package/src/revenuecat/purchaseEvents.ts +167 -0
- package/src/revenuecat/revenueCatBridge.ts +221 -0
- package/src/revenuecat/types.ts +95 -0
- package/src/reviews/decision.ts +8 -1
- package/src/reviews/runtime.ts +92 -1
- package/src/reviews/transport.ts +27 -10
- package/src/reviews/useReviewGate.ts +12 -7
- package/src/session-analytics/lifecycle.ts +15 -13
- package/src/session-analytics/reportSessionStart.ts +15 -4
- package/src/session-analytics/useLifecycleEvents.ts +68 -28
package/src/reviews/transport.ts
CHANGED
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
* question_key=<name>), which is what the backend review-firing rules evaluate on — and
|
|
13
13
|
* it seeds the broader app-analytics stream. Keep payloads minimal + non-PII.
|
|
14
14
|
*/
|
|
15
|
+
import { ensureCurrentSessionId } from "../analytics/currentSession";
|
|
16
|
+
import { buildEventsRequest, type ClientEvent } from "../analytics/reportClientEvent";
|
|
15
17
|
import type { ReviewDecisionResponse, ReviewSubmission, ReviewTarget } from "./types";
|
|
16
18
|
|
|
17
19
|
/**
|
|
@@ -156,7 +158,11 @@ export const fetchReviewDecision = async (
|
|
|
156
158
|
|
|
157
159
|
/** Options for a reported app event. `deviceKey` groups a device's sessions server-side. */
|
|
158
160
|
export interface ReportAppEventOptions {
|
|
159
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* The onboarding/session id to correlate with, when known. Optional: when omitted the event
|
|
163
|
+
* still carries the CURRENT per-open session id (`ensureCurrentSessionId()`), because an event
|
|
164
|
+
* with no `session_id` is dropped server-side behind a 200. Pass one only to override.
|
|
165
|
+
*/
|
|
160
166
|
sessionId?: string;
|
|
161
167
|
/** A stable, non-PII device id — the review-decision endpoint reads it for min-sessions. */
|
|
162
168
|
deviceKey?: string;
|
|
@@ -170,6 +176,16 @@ export interface ReportAppEventOptions {
|
|
|
170
176
|
* stable identifier and `meta` small + non-PII.
|
|
171
177
|
*
|
|
172
178
|
* reportAppEvent(target, "content_share", { sessionId, deviceKey });
|
|
179
|
+
*
|
|
180
|
+
* ── `session_id` IS NON-NEGOTIABLE ON THE WIRE ───────────────────────────────────────────
|
|
181
|
+
* The server's event model declares `session_id` required + non-empty, and `POST /v1/events`
|
|
182
|
+
* validates per event inside a try/except that counts the failure as `skipped` and STILL returns
|
|
183
|
+
* HTTP 200. An event sent without a `session_id` is therefore accepted and discarded, and a
|
|
184
|
+
* fire-and-forget caller never finds out. This used to be reachable through the ordinary API:
|
|
185
|
+
* `options.sessionId` was optional, so a host calling `reportAppEvent(target, "screen", { deviceKey })`
|
|
186
|
+
* posted every screen view into that hole. So the id is no longer conditional — an explicit
|
|
187
|
+
* `sessionId` wins, otherwise the CURRENT per-open id is used (minted + registered if no app-open
|
|
188
|
+
* has been registered yet).
|
|
173
189
|
*/
|
|
174
190
|
export const reportAppEvent = (
|
|
175
191
|
target: ReviewTarget | undefined,
|
|
@@ -178,25 +194,26 @@ export const reportAppEvent = (
|
|
|
178
194
|
): void => {
|
|
179
195
|
if (!target?.serverUrl || !name) return;
|
|
180
196
|
try {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
197
|
+
// An explicit id wins; a missing OR BLANK one falls back to the current per-open id. A bare
|
|
198
|
+
// `??` would let `sessionId: ""` through, and the server rejects an empty string exactly like
|
|
199
|
+
// a missing key (`min_length=1`), so the blank case has to fall back too.
|
|
200
|
+
const supplied = options.sessionId ?? "";
|
|
184
201
|
const event: Record<string, unknown> = {
|
|
185
202
|
event_type: "app_event",
|
|
186
203
|
question_key: name,
|
|
204
|
+
session_id: supplied.trim().length > 0 ? supplied : ensureCurrentSessionId(),
|
|
187
205
|
};
|
|
188
|
-
if (options.sessionId) event.session_id = options.sessionId;
|
|
189
206
|
// device_key rides in the non-PII user_context bucket the server sanitizes; the
|
|
190
207
|
// review-decision endpoint reads it to group a device's sessions.
|
|
191
208
|
if (options.deviceKey) event.user_context = { device_key: options.deviceKey };
|
|
192
209
|
if (options.meta && Object.keys(options.meta).length > 0) {
|
|
193
210
|
event.meta = JSON.stringify(options.meta);
|
|
194
211
|
}
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
212
|
+
// Route through the ONE canonical /v1/events builder (url + headers + body) instead of
|
|
213
|
+
// re-describing the endpoint here; still fire-and-forget.
|
|
214
|
+
const req = buildEventsRequest(target, [event as unknown as ClientEvent]);
|
|
215
|
+
if (!req) return;
|
|
216
|
+
void fetch(req.url, req.init).catch(() => {
|
|
200
217
|
/* best-effort, swallow */
|
|
201
218
|
});
|
|
202
219
|
} catch {
|
|
@@ -19,10 +19,12 @@ import { useResolvedFeatures } from "../features/WireFeaturesProvider";
|
|
|
19
19
|
import { decideReview, evaluateGate, resolveRules } from "./decision";
|
|
20
20
|
import { sameDecision, shallowEqual } from "./equality";
|
|
21
21
|
import {
|
|
22
|
+
bumpSessionCount,
|
|
22
23
|
readInt,
|
|
23
24
|
resolveStorage,
|
|
24
25
|
reviewLastShownKey,
|
|
25
26
|
reviewSeenKey,
|
|
27
|
+
reviewSessionOpenKey,
|
|
26
28
|
reviewSessionsKey,
|
|
27
29
|
writeInt,
|
|
28
30
|
} from "./runtime";
|
|
@@ -70,14 +72,17 @@ export const useReviewGate = ({
|
|
|
70
72
|
const seenKey = reviewSeenKey(config.id, config.oncePerVersion === false ? undefined : config.appVersion);
|
|
71
73
|
const lastKey = reviewLastShownKey(config.id);
|
|
72
74
|
const sessionsKey = reviewSessionsKey(config.id);
|
|
75
|
+
const sessionOpenKey = reviewSessionOpenKey(config.id);
|
|
73
76
|
|
|
74
|
-
// Read (and bump) the
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
77
|
+
// Read (and bump) the app-open counter. IDEMPOTENT per app-open, NOT per mount: `bumpSessionCount`
|
|
78
|
+
// keys off the live per-open session id (or a per-process id when no host wired the lifecycle
|
|
79
|
+
// events), so a remount, a navigation return, or React StrictMode's dev double-invoke of this
|
|
80
|
+
// initializer all read the same number back instead of inflating it. Before this, `sessions`
|
|
81
|
+
// counted mounts, so the fail-closed `minSessions: 2` default could be satisfied inside the user's
|
|
82
|
+
// very first app open — the exact scenario it was added to prevent.
|
|
83
|
+
const sessions = useState(() =>
|
|
84
|
+
bumpSessionCount(resolveStorage(storage), sessionsKey, sessionOpenKey),
|
|
85
|
+
)[0];
|
|
81
86
|
|
|
82
87
|
// Gate the local rules behind an optional client-side timeout, so a reachable server
|
|
83
88
|
// gets a window to answer first. A present `decision` bypasses the wait entirely.
|
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
* FIRE-AND-FORGET: like every analytics path in the kit, nothing here throws into the UI, awaits
|
|
35
35
|
* in the caller, or hangs the app — storage reads race a short timeout, writes swallow errors.
|
|
36
36
|
*/
|
|
37
|
-
import { makeSessionId, type ClientEvent, type ClientEventTarget } from "../analytics/reportClientEvent";
|
|
37
|
+
import { buildEventsRequest, makeSessionId, type ClientEvent, type ClientEventTarget } from "../analytics/reportClientEvent";
|
|
38
38
|
import type { DeviceContext } from "../device/deviceContext";
|
|
39
39
|
import { sanitizeUserId } from "../identity/userIdentity";
|
|
40
40
|
import type { WireOnboardingStorage } from "../session/persistedSession";
|
|
@@ -141,16 +141,11 @@ const routeLifecycleEvent = (event: Record<string, unknown>, opts: LifecycleEven
|
|
|
141
141
|
opts.sink(event as ClientEvent);
|
|
142
142
|
return;
|
|
143
143
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
void fetch(url, {
|
|
150
|
-
method: "POST",
|
|
151
|
-
headers,
|
|
152
|
-
body: JSON.stringify({ events: [event] }),
|
|
153
|
-
}).catch(() => {
|
|
144
|
+
// Route the direct-POST fallback through the ONE canonical /v1/events builder (url + headers +
|
|
145
|
+
// body) instead of re-describing the endpoint here; still fire-and-forget.
|
|
146
|
+
const req = buildEventsRequest(opts.target, [event as unknown as ClientEvent]);
|
|
147
|
+
if (!req) return;
|
|
148
|
+
void fetch(req.url, req.init).catch(() => {
|
|
154
149
|
// Network/transport error — analytics is best-effort, swallow.
|
|
155
150
|
});
|
|
156
151
|
} catch {
|
|
@@ -217,11 +212,18 @@ export interface WireLifecycleOptions extends ReportFirstOpenOptions {}
|
|
|
217
212
|
* Route both through the same `sink` (the offline queue) to buffer them. Fire-and-forget.
|
|
218
213
|
*/
|
|
219
214
|
export const wireLifecycleEvents = (opts: WireLifecycleOptions): void => {
|
|
220
|
-
reportFirstOpen
|
|
215
|
+
// ONE per-open id for BOTH events when the caller supplies none. Without this, `reportFirstOpen`
|
|
216
|
+
// fell through to `buildLifecycleEvent`'s own `makeSessionId()` while `reportSessionStart` minted a
|
|
217
|
+
// DIFFERENT one — so `app.first_open` carried a session id the server never saw a `session_started`
|
|
218
|
+
// for and back-filled into a phantom session, inflating session counts. `useLifecycleEvents` already
|
|
219
|
+
// pinned this (it mints `mountOpenSessionId`); the React-free entry point did not, so the identical
|
|
220
|
+
// bug was still reachable from the documented non-hook path.
|
|
221
|
+
const sessionId = opts.sessionId ?? makeSessionId();
|
|
222
|
+
reportFirstOpen({ ...opts, sessionId });
|
|
221
223
|
reportSessionStart({
|
|
222
224
|
target: opts.target,
|
|
223
225
|
sink: opts.sink,
|
|
224
|
-
sessionId
|
|
226
|
+
sessionId,
|
|
225
227
|
userId: opts.userId,
|
|
226
228
|
deviceKey: opts.deviceKey,
|
|
227
229
|
sessionCount: opts.sessionCount,
|
|
@@ -32,7 +32,12 @@
|
|
|
32
32
|
* never be able to break the app.
|
|
33
33
|
*/
|
|
34
34
|
import { setCurrentSessionId } from "../analytics/currentSession";
|
|
35
|
-
import {
|
|
35
|
+
import {
|
|
36
|
+
makeSessionId,
|
|
37
|
+
warnOnSkippedEvents,
|
|
38
|
+
type ClientEvent,
|
|
39
|
+
type ClientEventTarget,
|
|
40
|
+
} from "../analytics/reportClientEvent";
|
|
36
41
|
import type { DeviceContext } from "../device/deviceContext";
|
|
37
42
|
import { sanitizeUserId } from "../identity/userIdentity";
|
|
38
43
|
|
|
@@ -162,9 +167,15 @@ export const reportSessionStart = (opts: ReportSessionStartOptions): void => {
|
|
|
162
167
|
method: "POST",
|
|
163
168
|
headers,
|
|
164
169
|
body: JSON.stringify({ events: [event] }),
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
|
|
170
|
+
})
|
|
171
|
+
.then((res) => {
|
|
172
|
+
// A 200 can still carry `skipped:N` — the server took the request and threw the event away.
|
|
173
|
+
// Log-only; this path has nothing to retry either way.
|
|
174
|
+
warnOnSkippedEvents(res);
|
|
175
|
+
})
|
|
176
|
+
.catch(() => {
|
|
177
|
+
// Network/transport error — analytics is best-effort, swallow.
|
|
178
|
+
});
|
|
168
179
|
} catch {
|
|
169
180
|
// URL construction, JSON serialization, a throwing sink, or a missing fetch — swallow.
|
|
170
181
|
}
|
|
@@ -28,7 +28,8 @@ import { useEffect, useRef } from "react";
|
|
|
28
28
|
import { AppState, Platform, type AppStateStatus } from "react-native";
|
|
29
29
|
|
|
30
30
|
import { createEventQueue, type EnvelopeSource, type EventQueue } from "../analytics/eventQueue";
|
|
31
|
-
import type
|
|
31
|
+
import { makeSessionId, type ClientEvent, type ClientEventTarget } from "../analytics/reportClientEvent";
|
|
32
|
+
import { resolveAutoDeviceKey } from "../context/deviceId";
|
|
32
33
|
import { collectDeviceContext } from "../device/deviceContext";
|
|
33
34
|
import type { WireOnboardingStorage } from "../session/persistedSession";
|
|
34
35
|
import { reportFirstOpen } from "./lifecycle";
|
|
@@ -114,7 +115,70 @@ export const useLifecycleEvents = (
|
|
|
114
115
|
const targetOf = (cfg: LifecycleConfig | undefined): ClientEventTarget | undefined =>
|
|
115
116
|
cfg?.serverUrl ? { serverUrl: cfg.serverUrl, apiKey: cfg.apiKey ?? "" } : undefined;
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
/**
|
|
119
|
+
* The `device_key` these lifecycle events ride under. A host-supplied id always wins.
|
|
120
|
+
*
|
|
121
|
+
* WHY THE FALLBACK EXISTS: `min_sessions` (the review / questionnaire firing rule, and the
|
|
122
|
+
* "fire on the user's Nth session" recipe in the README) is computed SERVER-SIDE by counting
|
|
123
|
+
* distinct `app.session_started` events grouped by `user_context.device_key`. A host that took
|
|
124
|
+
* the batteries-included path and passed no `deviceKey` emitted those events with NO device key
|
|
125
|
+
* at all — while its `createAnalytics` / `createWireActivation` events carried an auto-minted
|
|
126
|
+
* one. Two disjoint identity spaces again: the counter the rule reads could never increase, so
|
|
127
|
+
* `min_sessions` was structurally unsatisfiable and the gate never fired from the server side.
|
|
128
|
+
*
|
|
129
|
+
* ONLY WITH `storage`: the auto id is per-INSTALL only when it can be persisted. With no
|
|
130
|
+
* storage it would be per-LAUNCH, which would make every open look like a brand-new device and
|
|
131
|
+
* corrupt `min_sessions` in the other direction. So no storage → no fallback, same as before.
|
|
132
|
+
*/
|
|
133
|
+
const resolveDeviceKey = (
|
|
134
|
+
cfg: LifecycleConfig | undefined,
|
|
135
|
+
opts: UseLifecycleEventsOptions,
|
|
136
|
+
): string | undefined => {
|
|
137
|
+
const host = typeof opts.deviceKey === "string" && opts.deviceKey.trim() ? opts.deviceKey : undefined;
|
|
138
|
+
if (host) return host;
|
|
139
|
+
if (!cfg?.storage) return undefined;
|
|
140
|
+
return resolveAutoDeviceKey({ appId: cfg.appId, storage: cfg.storage });
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
// ONE per-open session id for the MOUNT open, shared by first_open AND session_started below.
|
|
144
|
+
// WHY: `first_open` used to default to its OWN fresh `makeSessionId()` (via `buildLifecycleEvent`)
|
|
145
|
+
// while `session_started` minted a different one — so every install's `first_open` carried a
|
|
146
|
+
// session id the server never got a `session_started` for (a PHANTOM session the server
|
|
147
|
+
// back-filled). Minting one id here and handing it to both makes them agree, so the server sees
|
|
148
|
+
// `first_open` under the same session it saw `session_started` for. A later foreground open
|
|
149
|
+
// (below) mints its OWN fresh id — a new open is a new session.
|
|
150
|
+
const mountOpenSessionId = makeSessionId();
|
|
151
|
+
|
|
152
|
+
// session_start — the EXISTING emitter routed through the sink (offline-buffered, one guard).
|
|
153
|
+
// Accepts the per-open id so the mount open reuses `mountOpenSessionId`; omit it and the emitter
|
|
154
|
+
// mints a fresh id (a genuinely new open). Either way the once-guard dedupes within the open.
|
|
155
|
+
const fireSession = (sessionId?: string) => {
|
|
156
|
+
const { config: cfg, options: opts } = latest.current;
|
|
157
|
+
if (opts.enabled === false) return;
|
|
158
|
+
if (!cfg?.serverUrl && !opts.sink) return;
|
|
159
|
+
const device = collectDeviceContext();
|
|
160
|
+
// `device.appVersion` is auto-detected best-effort; an explicit host version always wins.
|
|
161
|
+
if (cfg?.appVersion) device.appVersion = cfg.appVersion;
|
|
162
|
+
reportSessionStart({
|
|
163
|
+
target: targetOf(cfg),
|
|
164
|
+
sink: resolveSink(),
|
|
165
|
+
sessionId,
|
|
166
|
+
userId: opts.userId,
|
|
167
|
+
deviceKey: resolveDeviceKey(cfg, opts),
|
|
168
|
+
sessionCount: opts.sessionCount,
|
|
169
|
+
appVersion: cfg?.appVersion ?? device.appVersion,
|
|
170
|
+
platform: Platform.OS,
|
|
171
|
+
device,
|
|
172
|
+
meta: opts.meta,
|
|
173
|
+
});
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
// 1) session_start FIRST (mount = an app-open), so the server has ingested `session_started`
|
|
177
|
+
// for `mountOpenSessionId` before first_open references the same id.
|
|
178
|
+
fireSession(mountOpenSessionId);
|
|
179
|
+
|
|
180
|
+
// 2) first_open — once ever (persisted flag + in-memory latch inside reportFirstOpen), pinned to
|
|
181
|
+
// the SAME per-open id so it is never a phantom session.
|
|
118
182
|
{
|
|
119
183
|
const { config: cfg, options: opts } = latest.current;
|
|
120
184
|
if (opts.enabled !== false) {
|
|
@@ -124,10 +188,11 @@ export const useLifecycleEvents = (
|
|
|
124
188
|
reportFirstOpen({
|
|
125
189
|
target: targetOf(cfg),
|
|
126
190
|
sink: resolveSink(),
|
|
191
|
+
sessionId: mountOpenSessionId,
|
|
127
192
|
storage: cfg?.storage,
|
|
128
193
|
appId: cfg?.appId,
|
|
129
194
|
userId: opts.userId,
|
|
130
|
-
deviceKey: opts
|
|
195
|
+
deviceKey: resolveDeviceKey(cfg, opts),
|
|
131
196
|
sessionCount: opts.sessionCount,
|
|
132
197
|
appVersion: cfg?.appVersion ?? device.appVersion,
|
|
133
198
|
platform: Platform.OS,
|
|
@@ -137,31 +202,6 @@ export const useLifecycleEvents = (
|
|
|
137
202
|
}
|
|
138
203
|
}
|
|
139
204
|
|
|
140
|
-
// 2) session_start — the EXISTING emitter routed through the sink (offline-buffered, one guard).
|
|
141
|
-
const fireSession = () => {
|
|
142
|
-
const { config: cfg, options: opts } = latest.current;
|
|
143
|
-
if (opts.enabled === false) return;
|
|
144
|
-
if (!cfg?.serverUrl && !opts.sink) return;
|
|
145
|
-
const device = collectDeviceContext();
|
|
146
|
-
// `device.appVersion` is auto-detected best-effort; an explicit host version always wins.
|
|
147
|
-
if (cfg?.appVersion) device.appVersion = cfg.appVersion;
|
|
148
|
-
reportSessionStart({
|
|
149
|
-
target: targetOf(cfg),
|
|
150
|
-
sink: resolveSink(),
|
|
151
|
-
// A fresh per-open id each fire; the emitter's once-guard dedupes within the open.
|
|
152
|
-
userId: opts.userId,
|
|
153
|
-
deviceKey: opts.deviceKey,
|
|
154
|
-
sessionCount: opts.sessionCount,
|
|
155
|
-
appVersion: cfg?.appVersion ?? device.appVersion,
|
|
156
|
-
platform: Platform.OS,
|
|
157
|
-
device,
|
|
158
|
-
meta: opts.meta,
|
|
159
|
-
});
|
|
160
|
-
};
|
|
161
|
-
|
|
162
|
-
// Mount = an app-open.
|
|
163
|
-
fireSession();
|
|
164
|
-
|
|
165
205
|
// Foreground after a real background = a new app-open.
|
|
166
206
|
let backgroundedAt: number | null = null;
|
|
167
207
|
const onChange = (state: AppStateStatus) => {
|