@wireai/activation 0.1.0 → 0.2.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 +82 -40
- package/CHANGELOG.md +17 -2
- package/INTEGRATION_PROMPT.md +8 -8
- package/README.md +38 -38
- package/dist/analytics/index.d.mts +92 -0
- package/dist/analytics/index.d.ts +92 -0
- package/dist/analytics/index.js +439 -0
- package/dist/analytics/index.js.map +1 -0
- package/dist/analytics/index.mjs +426 -0
- package/dist/analytics/index.mjs.map +1 -0
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/decision-BzbiKwk3.d.mts +79 -0
- package/dist/decision-plDEOCkt.d.ts +79 -0
- package/dist/eventQueue-CxKi7Qd5.d.mts +546 -0
- package/dist/eventQueue-rV1dtJJR.d.ts +546 -0
- package/dist/index.d.mts +162 -418
- package/dist/index.d.ts +162 -418
- package/dist/index.js +407 -4
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +402 -5
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +2 -1
- package/dist/questionnaire/index.d.ts +2 -1
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +5 -40
- package/dist/reviews/index.d.ts +5 -40
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs.map +1 -1
- package/dist/transport-BeO_Brcu.d.mts +40 -0
- package/dist/transport-DLpd1v5_.d.ts +40 -0
- package/dist/{decision-Cl8OFYzu.d.mts → types-A6pTxIZV.d.mts} +1 -77
- package/dist/{decision-CFvGY6nP.d.ts → types-BhpXJGlg.d.ts} +1 -77
- package/llms.txt +7 -7
- package/metro/index.d.ts +3 -3
- package/metro/index.js +3 -3
- package/package.json +15 -1
- package/src/analytics/contextEnvelope.ts +72 -0
- package/src/analytics/eventQueue.ts +331 -0
- package/src/analytics/index.ts +47 -0
- package/src/analytics/screenTracking.ts +122 -0
- package/src/analytics/useScreenTracking.ts +48 -0
- package/src/coachmarks/index.ts +2 -2
- package/src/features/WireFeaturesProvider.tsx +1 -1
- package/src/features/index.ts +2 -2
- package/src/index.ts +18 -1
- package/src/questionnaire/index.ts +2 -2
- package/src/reviews/index.ts +2 -2
- package/src/session-analytics/index.ts +20 -0
- package/src/session-analytics/lifecycle.ts +236 -0
- package/src/session-analytics/reportSessionStart.ts +27 -7
- package/src/session-analytics/useLifecycleEvents.ts +184 -0
- package/src/showcase/index.ts +2 -2
- package/src/types.ts +1 -1
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useLifecycleEvents — the batteries-included React hook that wires the TOP-OF-FUNNEL lifecycle
|
|
3
|
+
* events in ONE place: `app.first_open` (once ever) on mount, and `app.session_started` on every
|
|
4
|
+
* real app-open, both offline-buffered through the Brief-01 event queue.
|
|
5
|
+
*
|
|
6
|
+
* ONE emitter, one guard, no double-emit: session-start goes through the EXISTING
|
|
7
|
+
* {@link reportSessionStart} emitter (routed via a `sink`), NOT a second session emitter — so a
|
|
8
|
+
* host must use EITHER this hook OR `useSessionStart`, never both (both would fire an app-open).
|
|
9
|
+
* The once-per-open guard dedupes re-renders within this hook.
|
|
10
|
+
*
|
|
11
|
+
* Firing moments mirror {@link useSessionStart} exactly:
|
|
12
|
+
* • ON MOUNT — the app opened (cold start / provider first render). `app.first_open` fires here
|
|
13
|
+
* too (once ever, gated by the persisted flag in `reportFirstOpen`).
|
|
14
|
+
* • ON FOREGROUND after a real background of at least {@link BACKGROUND_SESSION_MS} (30 min) — a
|
|
15
|
+
* new app-open, so a fresh `app.session_started` fires. A quick app-switch does NOT count.
|
|
16
|
+
*
|
|
17
|
+
* OFFLINE-FIRST by default: pass your shared {@link EventQueue}'s `enqueue` as `options.sink` to
|
|
18
|
+
* route both events through your one queue. If you pass no sink, the hook lazily creates its OWN
|
|
19
|
+
* offline queue (dedicated storage key, so it never collides with your main queue) from the config
|
|
20
|
+
* + storage. Either way lifecycle events are buffered + persisted + retried, never lost offline.
|
|
21
|
+
*
|
|
22
|
+
* Dependency policy: `AppState` + `Platform` are RN built-ins (no new dep); the device snapshot is
|
|
23
|
+
* the kit's own dependency-free `collectDeviceContext()`; the queue is the dependency-free Brief-01
|
|
24
|
+
* `createEventQueue`. The pure `reportFirstOpen` / `wireLifecycleEvents` (in lifecycle.ts) import no
|
|
25
|
+
* React — this hook is the only React surface.
|
|
26
|
+
*/
|
|
27
|
+
import { useEffect, useRef } from "react";
|
|
28
|
+
import { AppState, Platform, type AppStateStatus } from "react-native";
|
|
29
|
+
|
|
30
|
+
import { createEventQueue, type EnvelopeSource, type EventQueue } from "../analytics/eventQueue";
|
|
31
|
+
import type { ClientEvent, ClientEventTarget } from "../analytics/reportClientEvent";
|
|
32
|
+
import { collectDeviceContext } from "../device/deviceContext";
|
|
33
|
+
import type { WireOnboardingStorage } from "../session/persistedSession";
|
|
34
|
+
import { reportFirstOpen } from "./lifecycle";
|
|
35
|
+
import { reportSessionStart } from "./reportSessionStart";
|
|
36
|
+
import { BACKGROUND_SESSION_MS } from "./useSessionStart";
|
|
37
|
+
|
|
38
|
+
/** Tenant transport + host persistence for the lifecycle wiring. Same creds as `WireOnboardingConfig`. */
|
|
39
|
+
export interface LifecycleConfig {
|
|
40
|
+
/** Base server URL (same as `WireOnboardingConfig.serverUrl`); `/v1/events` is appended. */
|
|
41
|
+
serverUrl?: string;
|
|
42
|
+
/** Tenant API key; sent as `Authorization: Bearer`. */
|
|
43
|
+
apiKey?: string;
|
|
44
|
+
/** Host app version (e.g. "1.4.2"), forwarded for release segmentation. Optional. */
|
|
45
|
+
appVersion?: string;
|
|
46
|
+
/** Tenant/app id — namespaces the first-open flag AND the hook's internal queue storage key. */
|
|
47
|
+
appId?: string;
|
|
48
|
+
/** Host storage (AsyncStorage subset). Enables the persisted once-ever first-open flag AND the
|
|
49
|
+
* offline durability of the hook's internal queue. Omit it and both degrade to in-memory. */
|
|
50
|
+
storage?: WireOnboardingStorage;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Per-open identity + wiring the host supplies. All optional: a pre-auth open is device-only. */
|
|
54
|
+
export interface UseLifecycleEventsOptions {
|
|
55
|
+
/** The host's opaque pseudonymous user id (NOT PII). Omitted before the user authenticates. */
|
|
56
|
+
userId?: string;
|
|
57
|
+
/** The host's local open-counter value for this open. Drives `returning` + "Nth session". */
|
|
58
|
+
sessionCount?: number;
|
|
59
|
+
/** A stable, non-PII device id the host owns. Groups this device's sessions server-side. */
|
|
60
|
+
deviceKey?: string;
|
|
61
|
+
/** Small non-PII extras forwarded on the event `meta`. */
|
|
62
|
+
meta?: Record<string, unknown>;
|
|
63
|
+
/** Set `false` to disable firing (e.g. behind a consent gate). Default enabled. */
|
|
64
|
+
enabled?: boolean;
|
|
65
|
+
/**
|
|
66
|
+
* Explicit transport sink (e.g. an existing shared `EventQueue.enqueue`). When provided, BOTH
|
|
67
|
+
* lifecycle events route here and the hook does NOT create its own queue — pass this to share ONE
|
|
68
|
+
* offline queue across the kit's analytics (screen tracking + lifecycle).
|
|
69
|
+
*/
|
|
70
|
+
sink?: (event: ClientEvent) => void;
|
|
71
|
+
/**
|
|
72
|
+
* Context envelope (or provider) for the hook's internally-created queue. Ignored when `sink` is
|
|
73
|
+
* supplied (the host's queue owns envelope stamping).
|
|
74
|
+
*/
|
|
75
|
+
envelope?: EnvelopeSource;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Fire `app.first_open` (once ever) + `app.session_started` (per open), offline-buffered. Returns
|
|
80
|
+
* nothing — a side-effecting hook. Safe to call with inline options (read through a ref, so
|
|
81
|
+
* changing `userId`/`sessionCount` never re-fires a session).
|
|
82
|
+
*/
|
|
83
|
+
export const useLifecycleEvents = (
|
|
84
|
+
config: LifecycleConfig | undefined,
|
|
85
|
+
options: UseLifecycleEventsOptions = {},
|
|
86
|
+
): void => {
|
|
87
|
+
// Latest options/config through a ref so the mount/resume fires read fresh values without listing
|
|
88
|
+
// them as effect deps (which would re-fire a session on every prop change).
|
|
89
|
+
const latest = useRef({ config, options });
|
|
90
|
+
latest.current = { config, options };
|
|
91
|
+
|
|
92
|
+
// The resolved sink is created at most once and reused for every fire this mount.
|
|
93
|
+
const queueRef = useRef<EventQueue | undefined>(undefined);
|
|
94
|
+
|
|
95
|
+
useEffect(() => {
|
|
96
|
+
const resolveSink = (): ((event: ClientEvent) => void) | undefined => {
|
|
97
|
+
const { config: cfg, options: opts } = latest.current;
|
|
98
|
+
// Host-owned shared queue wins — one queue across the whole kit.
|
|
99
|
+
if (opts.sink) return opts.sink;
|
|
100
|
+
// No transport → no sink (events no-op rather than buffer forever against a dead target).
|
|
101
|
+
if (!cfg?.serverUrl) return undefined;
|
|
102
|
+
if (!queueRef.current) {
|
|
103
|
+
queueRef.current = createEventQueue({
|
|
104
|
+
target: { serverUrl: cfg.serverUrl, apiKey: cfg.apiKey ?? "" },
|
|
105
|
+
storage: cfg.storage,
|
|
106
|
+
// Dedicated key so the hook's internal queue never collides with a host's main queue.
|
|
107
|
+
storageKey: `wireai:evtq:lifecycle:${cfg.appId ?? "default"}`,
|
|
108
|
+
envelope: opts.envelope,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return queueRef.current.enqueue;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const targetOf = (cfg: LifecycleConfig | undefined): ClientEventTarget | undefined =>
|
|
115
|
+
cfg?.serverUrl ? { serverUrl: cfg.serverUrl, apiKey: cfg.apiKey ?? "" } : undefined;
|
|
116
|
+
|
|
117
|
+
// 1) first_open — once ever (persisted flag + in-memory latch inside reportFirstOpen).
|
|
118
|
+
{
|
|
119
|
+
const { config: cfg, options: opts } = latest.current;
|
|
120
|
+
if (opts.enabled !== false) {
|
|
121
|
+
const device = collectDeviceContext();
|
|
122
|
+
if (cfg?.appVersion && !device.appVersion) device.appVersion = cfg.appVersion;
|
|
123
|
+
reportFirstOpen({
|
|
124
|
+
target: targetOf(cfg),
|
|
125
|
+
sink: resolveSink(),
|
|
126
|
+
storage: cfg?.storage,
|
|
127
|
+
appId: cfg?.appId,
|
|
128
|
+
userId: opts.userId,
|
|
129
|
+
deviceKey: opts.deviceKey,
|
|
130
|
+
sessionCount: opts.sessionCount,
|
|
131
|
+
appVersion: cfg?.appVersion ?? device.appVersion,
|
|
132
|
+
platform: Platform.OS,
|
|
133
|
+
device,
|
|
134
|
+
meta: opts.meta,
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// 2) session_start — the EXISTING emitter routed through the sink (offline-buffered, one guard).
|
|
140
|
+
const fireSession = () => {
|
|
141
|
+
const { config: cfg, options: opts } = latest.current;
|
|
142
|
+
if (opts.enabled === false) return;
|
|
143
|
+
if (!cfg?.serverUrl && !opts.sink) return;
|
|
144
|
+
const device = collectDeviceContext();
|
|
145
|
+
if (cfg?.appVersion && !device.appVersion) device.appVersion = cfg.appVersion;
|
|
146
|
+
reportSessionStart({
|
|
147
|
+
target: targetOf(cfg),
|
|
148
|
+
sink: resolveSink(),
|
|
149
|
+
// A fresh per-open id each fire; the emitter's once-guard dedupes within the open.
|
|
150
|
+
userId: opts.userId,
|
|
151
|
+
deviceKey: opts.deviceKey,
|
|
152
|
+
sessionCount: opts.sessionCount,
|
|
153
|
+
appVersion: cfg?.appVersion ?? device.appVersion,
|
|
154
|
+
platform: Platform.OS,
|
|
155
|
+
device,
|
|
156
|
+
meta: opts.meta,
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
// Mount = an app-open.
|
|
161
|
+
fireSession();
|
|
162
|
+
|
|
163
|
+
// Foreground after a real background = a new app-open.
|
|
164
|
+
let backgroundedAt: number | null = null;
|
|
165
|
+
const onChange = (state: AppStateStatus) => {
|
|
166
|
+
if (state === "background" || state === "inactive") {
|
|
167
|
+
if (backgroundedAt == null) backgroundedAt = Date.now();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
if (state === "active") {
|
|
171
|
+
const since = backgroundedAt;
|
|
172
|
+
backgroundedAt = null;
|
|
173
|
+
if (since != null && Date.now() - since >= BACKGROUND_SESSION_MS) fireSession();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
const sub = AppState.addEventListener("change", onChange);
|
|
177
|
+
return () => {
|
|
178
|
+
// RN >= 0.65 returns a subscription with remove(); guard for older shims.
|
|
179
|
+
if (sub && typeof (sub as { remove?: () => void }).remove === "function") sub.remove();
|
|
180
|
+
};
|
|
181
|
+
// Mount-only effect: firing reads fresh values via `latest`, so no reactive deps.
|
|
182
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
183
|
+
}, []);
|
|
184
|
+
};
|
package/src/showcase/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* wireai
|
|
2
|
+
* @wireai/activation/showcase — the pre-onboarding feature showcase ("app intro").
|
|
3
3
|
*
|
|
4
4
|
* Subpath entry, kept OUT of the main barrel so the core kit stays dependency-
|
|
5
5
|
* free: importing this pulls in the optional peer
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* `react-native-reanimated`). The app supplies a declarative ShowcaseConfig; the
|
|
8
8
|
* kit bakes in the Wire theme + optional per-slide gesture hand and gates once.
|
|
9
9
|
*
|
|
10
|
-
* import { FeatureShowcase, selectShowcaseSlides } from "wireai
|
|
10
|
+
* import { FeatureShowcase, selectShowcaseSlides } from "@wireai/activation/showcase";
|
|
11
11
|
*/
|
|
12
12
|
export { FeatureShowcase } from "./FeatureShowcase";
|
|
13
13
|
export { selectShowcaseSlides } from "./selectShowcaseSlides";
|
package/src/types.ts
CHANGED