@wireai/activation 0.14.3 → 0.16.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 +96 -21
- package/CHANGELOG.md +780 -0
- package/INTEGRATION_PROMPT.md +61 -23
- package/README.md +110 -31
- package/dist/analytics/index.d.mts +35 -13
- package/dist/analytics/index.d.ts +35 -13
- package/dist/analytics/index.js +288 -127
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +288 -127
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/coachmarks/index.d.mts +15 -1
- package/dist/coachmarks/index.d.ts +15 -1
- package/dist/coachmarks/index.js +120 -22
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs +120 -22
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{currentSession-CUvTOchb.d.mts → currentSession-Bz7G6lno.d.mts} +35 -39
- package/dist/{currentSession-CW_5Mq4O.d.ts → currentSession-z-CZ55ad.d.ts} +35 -39
- package/dist/{decision-Bgo17oH7.d.mts → decision-3vWLuBlO.d.ts} +11 -2
- package/dist/{decision-Bkh_LigV.d.ts → decision-yBj2AyPW.d.mts} +11 -2
- package/dist/index.d.mts +5 -2
- package/dist/index.d.ts +5 -2
- package/dist/index.js +125 -36
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +125 -36
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +3 -16
- package/dist/questionnaire/index.d.ts +3 -16
- package/dist/questionnaire/index.js +202 -46
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +203 -47
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +10 -6
- package/dist/reviews/index.d.ts +10 -6
- package/dist/reviews/index.js +269 -52
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +270 -53
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.d.mts +1 -1
- package/dist/showcase/index.d.ts +1 -1
- package/dist/showcase/index.js +106 -20
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs +107 -21
- package/dist/showcase/index.mjs.map +1 -1
- package/dist/{transport-j5gFfJhK.d.mts → transport-s5QxA-ci.d.mts} +18 -13
- package/dist/{transport-B_0SgCBe.d.ts → transport-xqqSFqxs.d.ts} +18 -13
- package/dist/{types-Cju-1_jT.d.mts → types-Byx306Kv.d.mts} +25 -10
- package/dist/{types-BcmagF6K.d.mts → types-D_0B0yay.d.mts} +7 -2
- package/dist/{types-BcmagF6K.d.ts → types-D_0B0yay.d.ts} +7 -2
- package/dist/{types-h2BZvl1t.d.ts → types-tdATL5z0.d.ts} +25 -10
- package/llms.txt +9 -9
- package/package.json +6 -9
- package/src/WireOnboarding.tsx +4 -1
- package/src/analytics/currentSession.ts +141 -4
- package/src/analytics/index.ts +6 -1
- package/src/analytics/reportClientEvent.ts +19 -10
- package/src/analytics/useAnalytics.ts +74 -15
- package/src/analytics/wireDoctor.ts +152 -7
- package/src/coachmarks/CoachmarkProvider.tsx +26 -5
- package/src/coachmarks/runtime.ts +156 -2
- package/src/coachmarks/types.ts +7 -2
- package/src/coachmarks/useCoachmarkTour.ts +51 -1
- package/src/context/deviceId.ts +72 -6
- package/src/features/WireFeaturesProvider.tsx +72 -12
- package/src/features/fetchWireFeatures.ts +49 -11
- package/src/features/useWireFeatures.ts +39 -3
- package/src/identity/identityRecord.ts +15 -2
- package/src/questionnaire/QuestionnaireGate.tsx +40 -1
- package/src/questionnaire/transport.ts +22 -8
- package/src/questionnaire/useQuestionnaireGate.ts +58 -7
- package/src/reviews/ReviewGate.tsx +125 -37
- package/src/reviews/decision.ts +11 -1
- package/src/reviews/idempotency.ts +109 -0
- package/src/reviews/index.ts +4 -2
- package/src/reviews/runtime.ts +44 -13
- package/src/reviews/transport.ts +39 -20
- package/src/reviews/types.ts +7 -0
- package/src/reviews/useReviewGate.ts +57 -7
- package/src/session-analytics/lifecycle.ts +16 -0
- package/src/session-analytics/useLifecycleEvents.ts +30 -2
- package/src/session-analytics/useSessionStart.ts +22 -2
- package/src/showcase/FeatureShowcase.tsx +50 -3
- package/src/types.ts +15 -8
- package/src/utils/submitResult.ts +17 -9
- package/src/utils/withDeadline.ts +70 -0
package/INTEGRATION_PROMPT.md
CHANGED
|
@@ -3,12 +3,9 @@
|
|
|
3
3
|
Copy everything in the block below and paste it into Claude Code (or any coding agent) **from the
|
|
4
4
|
root of your React Native / Expo app**. Fill in the two values at the top first.
|
|
5
5
|
|
|
6
|
-
> Fastest path instead of this prompt: run the **`wire-rn-integration`** Claude skill, the same job
|
|
7
|
-
> fully automated. Use this prompt when you don't have that skill installed.
|
|
8
|
-
|
|
9
6
|
---
|
|
10
7
|
|
|
11
|
-
```
|
|
8
|
+
```text
|
|
12
9
|
You are integrating the `@wireai/activation` package into THIS React Native / Expo app, a drop-in
|
|
13
10
|
AI onboarding flow. Do it end to end, match my app's existing conventions, and leave it
|
|
14
11
|
type-checking green. Read the package's README.md, llms.txt, and AGENTS.md (in node_modules/@wireai/activation
|
|
@@ -16,14 +13,20 @@ after install) before writing code.
|
|
|
16
13
|
|
|
17
14
|
MY VALUES:
|
|
18
15
|
- WIREAI_SERVER_URL: <paste the backend URL, e.g. https://...fly.dev>
|
|
19
|
-
- WIREAI_API_KEY: <paste the wai_… app key from
|
|
16
|
+
- WIREAI_API_KEY: <paste the wai_… app key from your app's Integrate tab in the console:
|
|
17
|
+
sign up at https://getwireai.com/signup (app name + email, then
|
|
18
|
+
confirm by email and set a password: that step is what creates the app
|
|
19
|
+
and its key), then copy the key from
|
|
20
|
+
https://getwireai.com/console/<app-id>/integrate. The server URL is the
|
|
21
|
+
serverUrl line of the init snippet on that same tab, NOT the "Server
|
|
22
|
+
URL" in that tab's Add MCP dialog, which is a different host>
|
|
20
23
|
- APP_ID: <paste my app id, e.g. my-app>
|
|
21
24
|
|
|
22
25
|
STEPS (do them in order, stop and ask if a convention is ambiguous):
|
|
23
26
|
1. Detect my conventions: navigation lib + where the signup/first-run flow lives, my env-var
|
|
24
27
|
typing, my profile-update / persistence path, my theme tokens (colors/fonts), my path aliases.
|
|
25
|
-
2. Install:
|
|
26
|
-
package manager (yarn/npm/pnpm, detect it).
|
|
28
|
+
2. Install: `npm install @wireai/activation wireai-rn zod react-native-safe-area-context` (react +
|
|
29
|
+
react-native are already peers). Use my package manager (yarn/npm/pnpm, detect it).
|
|
27
30
|
3. Metro: set `module.exports = withWireOnboarding(getDefaultConfig(__dirname))` from
|
|
28
31
|
`@wireai/activation/metro` in metro.config.js (preserve my existing config). This pins one copy
|
|
29
32
|
of react/react-native/wireai-rn/zod; do NOT skip it.
|
|
@@ -31,9 +34,11 @@ STEPS (do them in order, stop and ask if a convention is ambiguous):
|
|
|
31
34
|
to my env files and my env typing.
|
|
32
35
|
5. Theme: build a theme from my brand with `themeFromBrand({ primary: "<my brand color>" })` or a
|
|
33
36
|
Partial<OnboardingTheme>; match my dark/light.
|
|
34
|
-
6. Screen: scaffold an onboarding screen
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
6. Screen: scaffold an onboarding screen. Build the config ONCE and gate on it first, because
|
|
38
|
+
`wireConfigFromEnv` returns `null` when the env is unset while `config` is a required non-null
|
|
39
|
+
prop: `const config = wireConfigFromEnv({ appId: APP_ID }); if (!config) return <MyExistingStaticOnboarding/>;`
|
|
40
|
+
Then render
|
|
41
|
+
`<WireOnboarding config={config} theme={...} illustrations={...} onComplete={persist} fallbackFlow={<MyExistingStaticOnboarding/>} onEvent={logEvent} />`.
|
|
37
42
|
- Gate entry with `isOnboardingEnabled({ remote })` (wire my remote-config boolean if I have one).
|
|
38
43
|
- `persist`: write ALL of `result.answers` (use `deriveAnswers(result.raw)` to fill gaps) through
|
|
39
44
|
my normal profile-update path. Do NOT re-ask anything a dedicated screen already owns.
|
|
@@ -48,9 +53,17 @@ STEPS (do them in order, stop and ask if a convention is ambiguous):
|
|
|
48
53
|
- Analytics: log via `toAnalyticsEvent(e)` → my logger, using the canonical `wire_onboarding_*` names.
|
|
49
54
|
7. Navigation: mount the screen in my first-run/signup flow; branch to the static flow when the gate
|
|
50
55
|
is off or config is missing.
|
|
51
|
-
8. Lifecycle (do NOT skip): mount
|
|
52
|
-
|
|
53
|
-
|
|
56
|
+
8. Lifecycle (do NOT skip): mount
|
|
57
|
+
`useLifecycleEvents({ serverUrl, apiKey, appId, storage }, { deviceKey, sessionCount, userId })`
|
|
58
|
+
ONCE at my app root, above the navigator, before anything else touches analytics.
|
|
59
|
+
`sessionCount` is MINE to own: a counter I persist and increment once per app open, starting at
|
|
60
|
+
1 on the very first launch. Hardcoding `1` compiles, ships, and pins every user at `returning:
|
|
61
|
+
false` forever with nothing reporting an error. If I have no such counter yet, create one
|
|
62
|
+
(read, increment, write) in the same storage I pass above, or say so instead of inventing a
|
|
63
|
+
constant. That first
|
|
64
|
+
argument is a `LifecycleConfig` built from explicit fields, NOT the config from step 6:
|
|
65
|
+
`wireConfigFromEnv` can return `null`, and `storage` is not a field of `WireOnboardingConfig`.
|
|
66
|
+
It is the only path that emits `app.first_open`, it registers the per-open
|
|
54
67
|
session id every other surface correlates to, and with storage it stamps the kit's persisted auto
|
|
55
68
|
`device_key` on `app.session_started`, which is what the server counts `min_sessions` from. If my
|
|
56
69
|
storage adapter throws or rejects, the kit treats it as no storage and stamps no auto key at all
|
|
@@ -62,11 +75,26 @@ STEPS (do them in order, stop and ask if a convention is ambiguous):
|
|
|
62
75
|
`journal_done`. On my home/gate screen call
|
|
63
76
|
`const { track, sessionId, revalidation } = useWireActivation({ serverUrl: WIREAI_SERVER_URL, apiKey: WIREAI_API_KEY, appId: APP_ID, deviceKey })`
|
|
64
77
|
from `@wireai/activation`, fetch the gate decision in a `useEffect` keyed on `[revalidation]`
|
|
65
|
-
(`fetchReviewDecision({ serverUrl, apiKey }, { sessionId, deviceKey })`
|
|
66
|
-
`
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
78
|
+
(`fetchReviewDecision({ serverUrl, apiKey }, { sessionId, deviceKey })`), and at the action site
|
|
79
|
+
call `await track("journal_done")`. Do NOT hand-roll the session id or the POST; `wire.track`
|
|
80
|
+
owns both and revalidates the gate on a successful 2xx. Questionnaires use
|
|
81
|
+
`fetchQuestionnaireDecision` + `useQuestionnaireGate` the same way, with the same three rules:
|
|
82
|
+
- THE GATE NEEDS SYNCHRONOUS STORAGE. Pass `storage` to the hook (or mount `CoachmarkProvider`
|
|
83
|
+
with one above it) as a `CoachmarkStorage`, whose `getItem(key)` returns `string | null` and
|
|
84
|
+
NOT a Promise. This is a different shape from `<WireOnboarding storage>`, which takes the
|
|
85
|
+
AsyncStorage-style adapter. Without it the app-open counter is pinned at 1 forever, the
|
|
86
|
+
fail-closed `minSessions` default of 2 can never be met, and the gate silently never fires;
|
|
87
|
+
an ASYNC adapter is detected and substituted with a fail-closed store, same outcome. Use my
|
|
88
|
+
app's existing synchronous store (MMKV or similar). If I have none, say so.
|
|
89
|
+
- THE HOOK'S `config` IS THE GATE'S CONFIG, not the kit transport config from step 6: it needs
|
|
90
|
+
an `id`. Write `useReviewGate({ config: { id: "post_win" }, decision: served ?? undefined, storage: gateStorage })`.
|
|
91
|
+
The `?? undefined` is not optional either: the fetch resolves `ReviewDecisionResponse | null`
|
|
92
|
+
and the option is `decision?: ReviewDecision`, so the raw result does not type-check.
|
|
93
|
+
- WIRE THE COMPONENT, not just the hook. `<ReviewGate>` has no `gate` prop. Render it when
|
|
94
|
+
`gate.visible` and pass `onShown={gate.markShown}` and `onResolved={gate.markResolved}`.
|
|
95
|
+
Those two write the cooldown timestamp and the once-per-version seen gate; leave them out and
|
|
96
|
+
it compiles, shows, and re-prompts the same user on every open. The full wiring example is the
|
|
97
|
+
"Wiring `<ReviewGate>`" block in AGENTS.md.
|
|
70
98
|
10. RevenueCat (only if my app sells subscriptions through `react-native-purchases`): wire my paywall
|
|
71
99
|
to the same event stream with `createRevenueCatBridge({ analytics, entitlementId: "<my
|
|
72
100
|
entitlement, e.g. pro>" })` from `@wireai/activation`. Do NOT add or import
|
|
@@ -75,17 +103,27 @@ STEPS (do them in order, stop and ask if a convention is ambiguous):
|
|
|
75
103
|
hand-rolled paywall analytics calls with `paywallShown` / `checkoutStarted` / `purchaseCompleted`
|
|
76
104
|
/ `purchaseFailed` / `purchasesRestored`, and call `syncPlanTier(customerInfo)` once at launch.
|
|
77
105
|
THE JOIN KEY: pass the SAME device key to the analytics instance AND to onboarding via
|
|
78
|
-
`<WireOnboarding userContext={activationJoinContext(deviceKey)} />`, because purchases join to
|
|
106
|
+
`<WireOnboarding userContext={activationJoinContext(deviceKey)} ... />`, because purchases join to
|
|
79
107
|
onboarding on `user_context.device_key` and never on `session_id` (those are separate id spaces
|
|
80
108
|
and intersecting them returns zero rows). If my app has no single stable device key yet, say so
|
|
81
109
|
instead of inventing one.
|
|
82
110
|
11. Verify: type-check (and lint the changed files); test the flag-off + backend-error paths. Then
|
|
83
111
|
prove the events wiring actually works, because `/v1/events` answers 200 for a batch it discards:
|
|
84
112
|
behind an `if (__DEV__)` guard, call
|
|
85
|
-
`wireDoctor({ target: { serverUrl: WIREAI_SERVER_URL, apiKey: WIREAI_API_KEY }, storage
|
|
86
|
-
`@wireai/activation/analytics` once at app root and
|
|
87
|
-
`ok`. A failing `round_trip` names the field the
|
|
88
|
-
dev-gated, once the report is green.
|
|
113
|
+
`wireDoctor({ target: { serverUrl: WIREAI_SERVER_URL, apiKey: WIREAI_API_KEY }, storage, join: {
|
|
114
|
+
appId: WIREAI_APP_ID, userContext } })` from `@wireai/activation/analytics` once at app root and
|
|
115
|
+
log the report. Every check must come back `ok`. A failing `round_trip` names the field the
|
|
116
|
+
server refused. Remove the call, or leave it dev-gated, once the report is green.
|
|
117
|
+
PASS `join`. Without it the `join_key` check reports NOT EVALUATED and fails the report on
|
|
118
|
+
purpose: a report that never looked at the join has not earned a green.
|
|
119
|
+
READ THE SCOPE OF THAT GREEN. `wireDoctor` runs five checks: `target` (the shape of the url +
|
|
120
|
+
key), `reachability`, `storage` (writability), `round_trip` (one probe event the server
|
|
121
|
+
acknowledges as written), and `join_key` (this session will carry a `user_context.device_key`,
|
|
122
|
+
yours or the kit's auto-injected one, step 7). It still does NOT check that the lifecycle hook
|
|
123
|
+
is mounted, and it does NOT check `min_sessions`. So an integration that skipped step 8's
|
|
124
|
+
lifecycle mount passes every check green while its `activated` funnel stays permanently at zero.
|
|
125
|
+
A green doctor means "events reach the server and can be joined"; it does not mean "the funnel
|
|
126
|
+
works". Verify step 8 by reading the code, not by reading the report.
|
|
89
127
|
|
|
90
128
|
Report back: the files you changed (path:line), the typecheck result, and anything you couldn't
|
|
91
129
|
infer about my conventions.
|
package/README.md
CHANGED
|
@@ -41,20 +41,53 @@ library? Pass your own nodes through the `icons` prop.
|
|
|
41
41
|
|
|
42
42
|
## How to include it in an app (end-to-end)
|
|
43
43
|
|
|
44
|
-
> Fastest path:
|
|
45
|
-
>
|
|
46
|
-
> scaffolds the screen, type-checks). The
|
|
44
|
+
> Fastest path: paste [`INTEGRATION_PROMPT.md`](./INTEGRATION_PROMPT.md) into your coding
|
|
45
|
+
> agent — it ships in this package and does all of the steps below automatically (detects
|
|
46
|
+
> your conventions, wires Metro, derives a theme, scaffolds the screen, type-checks). The
|
|
47
|
+
> manual steps, in order:
|
|
47
48
|
|
|
48
|
-
**0.
|
|
49
|
+
**0. Get your `apiKey` and `serverUrl` from the console.** The kit talks to the
|
|
49
50
|
multi-tenant backend over A2A; the **`apiKey` is per-app and resolves the tenant
|
|
50
|
-
server-side**.
|
|
51
|
-
|
|
52
|
-
|
|
51
|
+
server-side**. Nothing renders without both values. Two stops in the browser with an
|
|
52
|
+
email round trip between them:
|
|
53
|
+
|
|
54
|
+
1. **Create an account** → **<https://getwireai.com/signup>**. You give your app's name
|
|
55
|
+
and an email; the confirmation link is where you choose a password. That one step
|
|
56
|
+
creates the account, your **first app** and its `wai_…` key, and lands you inside the
|
|
57
|
+
console already signed in.
|
|
58
|
+
2. **Copy both values** from that app's **Integrate** tab
|
|
59
|
+
(`https://getwireai.com/console/<your-app-id>/integrate`). The **API key** is a copy
|
|
60
|
+
field; the **server URL** is the `serverUrl` line of the ready-made init snippet on
|
|
61
|
+
that same tab. The key stays readable there on every visit, so there is nothing to
|
|
62
|
+
catch as it goes past.
|
|
63
|
+
|
|
64
|
+
> ⚠️ **Two different things are called "Server URL" in that console.** The one you want is
|
|
65
|
+
> the `serverUrl` in the init snippet. The **Add MCP** dialog on the same tab also shows a
|
|
66
|
+
> copy field labelled "Server URL", and that is the **MCP endpoint on a different host** —
|
|
67
|
+
> it is for pointing an AI client at your account, and it is *not* a valid
|
|
68
|
+
> `EXPO_PUBLIC_WIREAI_SERVER_URL`.
|
|
69
|
+
|
|
70
|
+
Later: sign back in at **<https://getwireai.com/login>**, and register a second app at
|
|
71
|
+
**<https://getwireai.com/console/register>** if you need one — signup already made the
|
|
72
|
+
first.
|
|
73
|
+
|
|
74
|
+
> **Keys are per environment.** A key's label *is* its environment (`dev` / `prod` /
|
|
75
|
+
> `staging` / custom), so use a different key in dev than in production and your analytics
|
|
76
|
+
> split by that label. The **Keys and environments** section of the same **Integrate** tab
|
|
77
|
+
> lists your keys and can **revoke** one. Issuing a new key is **not self-serve yet**, so
|
|
78
|
+
> ask for an extra environment via the console's **Support** section.
|
|
79
|
+
|
|
80
|
+
> ⚠️ **The `apiKey` is a tenant key, not a login.** It resolves your app server-side and is
|
|
81
|
+
> *expected to ship inside your app bundle* — the documented path below puts it in
|
|
82
|
+
> `EXPO_PUBLIC_WIREAI_API_KEY`, which Metro inlines at build time. It cannot authenticate a
|
|
83
|
+
> console session; an app key presented on a console route is refused. Do still read it from
|
|
84
|
+
> an env var rather than a committed literal, so a build can point at a different
|
|
85
|
+
> environment's key without a code change.
|
|
53
86
|
|
|
54
87
|
**1. Install the package** (plus its peers):
|
|
55
88
|
|
|
56
89
|
```bash
|
|
57
|
-
npm install @wireai/activation wireai-rn
|
|
90
|
+
npm install @wireai/activation wireai-rn zod react-native-safe-area-context
|
|
58
91
|
# react + react-native are peers (already in any RN/Expo app)
|
|
59
92
|
```
|
|
60
93
|
|
|
@@ -74,7 +107,7 @@ module.exports = withWireOnboarding(getDefaultConfig(__dirname));
|
|
|
74
107
|
|
|
75
108
|
**3. Config from env** (so you can flip backends without a code change):
|
|
76
109
|
|
|
77
|
-
```
|
|
110
|
+
```env
|
|
78
111
|
EXPO_PUBLIC_WIREAI_API_KEY=wai_…
|
|
79
112
|
EXPO_PUBLIC_WIREAI_SERVER_URL=https://…
|
|
80
113
|
EXPO_PUBLIC_WIREAI_APP_ID=my-app
|
|
@@ -107,6 +140,8 @@ just navigate onward:
|
|
|
107
140
|
theme={theme}
|
|
108
141
|
illustrations={{ ...defaultIllustrations, ...myIllustrations }}
|
|
109
142
|
validators={{ username: checkUsernameAvailable }}
|
|
143
|
+
storage={storage}
|
|
144
|
+
fallbackFlow={<MyExistingStaticOnboarding />}
|
|
110
145
|
onComplete={(r) => { persist(r.answers); goNext(); }}
|
|
111
146
|
onSkip={goNext}
|
|
112
147
|
onError={fallBackToStaticOnboarding}
|
|
@@ -138,11 +173,11 @@ Install the package and wire Metro with the one-call helper:
|
|
|
138
173
|
|
|
139
174
|
```bash
|
|
140
175
|
# from npm (public)
|
|
141
|
-
npm install @wireai/activation
|
|
142
|
-
# …or
|
|
143
|
-
npm
|
|
144
|
-
|
|
145
|
-
npm install git+ssh://git@github.com/<org
|
|
176
|
+
npm install @wireai/activation wireai-rn zod react-native-safe-area-context
|
|
177
|
+
# …or, for a pre-publish or pinned build only (see Distribution below):
|
|
178
|
+
# `x.y.z` is a placeholder — use the version `npm run pack:tarball` actually produced.
|
|
179
|
+
npm install ./wireai-activation-x.y.z.tgz
|
|
180
|
+
npm install git+ssh://git@github.com/<org>/<repo>.git
|
|
146
181
|
```
|
|
147
182
|
|
|
148
183
|
```js
|
|
@@ -154,7 +189,9 @@ module.exports = withWireOnboarding(getDefaultConfig(__dirname));
|
|
|
154
189
|
```
|
|
155
190
|
|
|
156
191
|
```tsx
|
|
157
|
-
//
|
|
192
|
+
// Minimal usage. "Minimal" still carries `fallbackFlow` and a join key, because omitting
|
|
193
|
+
// either fails SILENTLY: no fallback dead-ends the user on a backend error, and no join key
|
|
194
|
+
// leaves the `activated` funnel reading zero forever with no error anywhere.
|
|
158
195
|
import { WireOnboarding, themeFromBrand, wireConfigFromEnv, defaultIllustrations } from "@wireai/activation";
|
|
159
196
|
|
|
160
197
|
export function Onboarding() {
|
|
@@ -165,7 +202,10 @@ export function Onboarding() {
|
|
|
165
202
|
config={config}
|
|
166
203
|
theme={themeFromBrand({ primary: "#15B0B0" })}
|
|
167
204
|
illustrations={defaultIllustrations}
|
|
205
|
+
storage={storage}
|
|
206
|
+
fallbackFlow={<MyExistingStaticOnboarding />}
|
|
168
207
|
onComplete={(r) => persist(r.answers)}
|
|
208
|
+
onSkip={goNext}
|
|
169
209
|
/>
|
|
170
210
|
);
|
|
171
211
|
}
|
|
@@ -204,21 +244,23 @@ returns the same config. `options.appRoot` defaults to `process.cwd()`.
|
|
|
204
244
|
## Distribution
|
|
205
245
|
|
|
206
246
|
`npm run pack:tarball` runs the build (via `prepack`) and produces
|
|
207
|
-
|
|
208
|
-
CHANGELOG. The primary channel is **npm** (
|
|
209
|
-
tarball (`npm install
|
|
247
|
+
`wireai-activation-x.y.z.tgz` containing `dist` + `src` + `metro` + README +
|
|
248
|
+
CHANGELOG. The primary channel is **npm** (the install line above); a
|
|
249
|
+
tarball (`npm install ./wireai-activation-x.y.z.tgz`) or a `git+ssh://…` install
|
|
210
250
|
work too for pre-publish or pinned builds.
|
|
211
251
|
|
|
212
252
|
## Quick start
|
|
213
253
|
|
|
214
254
|
```tsx
|
|
215
|
-
import { WireOnboarding } from "@wireai/activation";
|
|
255
|
+
import { WireOnboarding, activationJoinContext } from "@wireai/activation";
|
|
216
256
|
|
|
217
257
|
<WireOnboarding
|
|
218
258
|
config={{ apiKey, serverUrl, appId: "my-app" }}
|
|
219
259
|
theme={myBrandTheme}
|
|
220
260
|
illustrations={{ momentum: <MyMomentumSvg /> }}
|
|
221
261
|
validators={{ username: checkUsernameAvailable }}
|
|
262
|
+
userContext={activationJoinContext(deviceKey)}
|
|
263
|
+
fallbackFlow={<MyExistingStaticOnboarding />}
|
|
222
264
|
onComplete={(result) => persist(result.answers)}
|
|
223
265
|
onSkip={skip}
|
|
224
266
|
onError={() => fallBackToStaticOnboarding()}
|
|
@@ -252,7 +294,7 @@ import { WireOnboarding } from "@wireai/activation";
|
|
|
252
294
|
| `persistKey` | `string` | Override the storage key (default `wireai:session:<appId>`). Scope per-user if one device can onboard multiple accounts mid-flow. With `storage` only. |
|
|
253
295
|
| `retainSessionOnComplete` | `boolean` | Opt IN: keep the persisted session seed alive across completion. A multi-stage signup that re-enters onboarding (typically a `key=` remount) then resumes the SAME session instead of minting a fresh `metadata.sessionId`, which the backend adopts as a second `contextId` and double-counts as a `session_started`. Omitted or `false` clears the seed on completion, the legacy single-stage behavior, so leaving it unset changes nothing. With `true`, freshness comes from `sessionTtlMs` plus an explicit new-run signal (scope a new `persistKey`, e.g. a per-signup id). With `storage` only. |
|
|
254
296
|
| `userContext` | `Record<string, string \| number \| boolean>` | **This is where the join key goes.** `user_context.device_key` is the only thing that joins this onboarding session to the app's later events, so pass `userContext={activationJoinContext(deviceKey)}`. If your app owns no device id, pass a working `storage` and leave this prop alone: the kit injects its own key, and only after it has confirmed the key actually persists. ⛔ Do not hand-build that key with `activationJoinContext(resolveAutoDeviceKey({ appId, storage }))`, because `resolveAutoDeviceKey` is synchronous by contract (it hands back a fresh mint and adopts the persisted id a storage read later) and it cannot tell you whether the id survives the launch at all. Since 0.12.2, omitting it no longer empties the funnel silently: with `storage`, the kit injects its own key (see [The join key](#the-join-key-device_key-never-session_id) and `autoJoinKey` below). Second job: any other non-PII context the app already knows (signup method, referral, plan, a HASHED user id), forwarded on session metadata + client events for funnel segmentation. **No PII on THIS prop** (no raw emails/names); primitives only; the server caps size/keys. See [Device & user context](#device--user-context). ⚠️ Distinct from the analytics/activation **`WireUserContext`** object (`createAnalytics` / `useWireActivation`), which additionally accepts an opt-in raw `userEmail` — see [Rich user context & PII](#rich-user-context--pii). |
|
|
255
|
-
| `autoJoinKey` | `boolean` | Opt OUT of the automatic join key. Default `true`. When this prop is left alone, `storage` is present **and actually persists** (0.13.0: a `storage` adapter that rejects or throws is treated exactly like no `storage` — the kit declines and says so, because a key it cannot persist is a different key on every launch, which corrupts `min_sessions` rather than merely leaving the join empty), and `userContext` carries no `device_key`, the kit injects its own per-install key (the same id `createAnalytics` / `createWireActivation` mint and persist), so the `activated` funnel joins with no host wiring. Pass `autoJoinKey={false}` if you genuinely want an UNLINKED onboarding session: that restores the pre-0.12.2 behavior exactly, and the dev warning fires again. It never overrides a `device_key` you
|
|
297
|
+
| `autoJoinKey` | `boolean` | Opt OUT of the automatic join key. Default `true`. When this prop is left alone, `storage` is present **and actually persists** (0.13.0: a `storage` adapter that rejects or throws is treated exactly like no `storage` — the kit declines and says so, because a key it cannot persist is a different key on every launch, which corrupts `min_sessions` rather than merely leaving the join empty), and `userContext` carries no `device_key`, the kit injects its own per-install key (the same id `createAnalytics` / `createWireActivation` mint and persist), so the `activated` funnel joins with no host wiring. Pass `autoJoinKey={false}` if you genuinely want an UNLINKED onboarding session: that restores the pre-0.12.2 behavior exactly, and the dev warning fires again. It never overrides a `device_key` you actually supplied, and it cannot inject without `storage` (see [The join key](#the-join-key-device_key-never-session_id)). The kit counts yours as supplied only when it is a string with at least one non-whitespace character, so a non-string or an all-whitespace `device_key` is treated as absent and the auto key is merged over it. |
|
|
256
298
|
| `userId` | `string` | Your own OPAQUE user id, so onboarding sessions reconcile to real users later (console sessions to your user table / GA4 users). Optional and supports **late binding**: present at mount it rides the session-start metadata; if it changes mid-session (the user just registered) the kit emits an `identify` event; available only after the flow, use `identifyOnboarding(...)`. **No PII** (not an email/name/phone); trimmed and capped at 128 chars. See [User identity](#user-identity). |
|
|
257
299
|
|
|
258
300
|
## Helpers (the reusable substrate)
|
|
@@ -273,7 +315,7 @@ You rarely hand-roll config, gating, analytics, or attribution — the kit ships
|
|
|
273
315
|
|
|
274
316
|
> **`onEvent`** fires `started` / `resumed` / `turn` / `error` / `retry` / `fallback` / `permission` (the kit owns the loop, so this is how you recover per-turn analytics). `resumed` fires INSTEAD of `started` when a persisted session was restored (see the `storage` prop) — don't count both as flow starts. Since 0.13.6 the `turn` event also carries `variant`, the experiment arm the backend assigned, from the first card that declares one onwards; that is how you read the arm DURING the flow rather than waiting for `onComplete`. The key is absent when no experiment is running, and the kit never attaches the arm to an analytics event, so arm-segmented funnels are the host's job. Completion is signalled via `onComplete`, not `onEvent` — log `WIRE_ONBOARDING_EVENTS.completed` there. `started` and `resumed` also carry a `contextId` (the A2A session id): capture it if you might bind a `userId` after the flow finishes (see [User identity](#user-identity)).
|
|
275
317
|
|
|
276
|
-
**Integrating with an AI agent?** See [`INTEGRATION_PROMPT.md`](./INTEGRATION_PROMPT.md) (copy-paste prompt for Claude Code) and [`llms.txt`](./llms.txt).
|
|
318
|
+
**Integrating with an AI agent?** See [`INTEGRATION_PROMPT.md`](./INTEGRATION_PROMPT.md) (copy-paste prompt for Claude Code) and [`llms.txt`](./llms.txt).
|
|
277
319
|
|
|
278
320
|
## Device & user context
|
|
279
321
|
|
|
@@ -331,7 +373,7 @@ setUserId(newUser.id); // the kit fires `identify` and binds the running session
|
|
|
331
373
|
```tsx
|
|
332
374
|
import { identifyOnboarding } from "@wireai/activation";
|
|
333
375
|
|
|
334
|
-
const contextIdRef = useRef<string>();
|
|
376
|
+
const contextIdRef = useRef<string | undefined>(undefined);
|
|
335
377
|
|
|
336
378
|
<WireOnboarding
|
|
337
379
|
config={config}
|
|
@@ -378,6 +420,8 @@ await clearUserContext({ storage, appId }); // the wire/activation path (then re
|
|
|
378
420
|
|
|
379
421
|
**3. The auto `device_key` is a per-install identifier.** When you supply no `deviceKey`, the analytics and activation surfaces auto-mint a stable per-install `device_key`, persist it via `storage` (`wireai:analytics:deviceKey:<appId>`), and stamp it on every event so the server's review/questionnaire gating and A/B stickiness work out of the box. It carries no hardware id, no IDFA/GAID, and cannot be joined across apps (the privacy category of a first-party cookie), but it IS a persistent per-install id. If you adopt these surfaces, declare it in your App Privacy / Data Safety accordingly. Supply your own `deviceKey` to override it.
|
|
380
422
|
|
|
423
|
+
**The review gate is on that list too, even if it is the only surface you mount.** Since 0.15.0 `<ReviewGate>` stamps `meta.device_key` onto its `POST /v1/reviews` body when you passed none. That id is the unit the server scopes the review's idempotency key to, and a post without it gets the key discarded. The gate adopts whatever per-install id the analytics and activation surfaces already registered in this process, so it never opens a second id space. On a reviews-only host, where nothing else registered one, it mints its own instead. `<ReviewGate>` takes no `storage` prop, so that minted id is process-scoped: a fresh value on each launch, never persisted, rather than a stable per-install one. Either way it leaves the device inside a review payload. Declare it. Pass your own non-blank `meta.device_key` and it is forwarded verbatim instead.
|
|
424
|
+
|
|
381
425
|
### Do not pass `sessionId` to `createAnalytics`
|
|
382
426
|
|
|
383
427
|
`CreateAnalyticsConfig.sessionId` is an opt-out knob, not a default. Set it and **every** event that
|
|
@@ -387,6 +431,7 @@ following the live per-open session the kit registers from `app.session_started`
|
|
|
387
431
|
collapse onto a single device-scoped id: one "first open" for the life of the install, however many
|
|
388
432
|
times the user comes back. The kit warns about it in dev builds.
|
|
389
433
|
|
|
434
|
+
<!-- doc-check: skip - two ALTERNATIVE call shapes shown side by side; one file cannot declare the same const twice -->
|
|
390
435
|
```tsx
|
|
391
436
|
const analytics = createAnalytics({ serverUrl, apiKey, storage, appId }); // ✅ follows each app-open
|
|
392
437
|
const analytics = createAnalytics({ serverUrl, apiKey, sessionId: myId }); // ⚠️ frozen for good
|
|
@@ -569,6 +614,7 @@ that name (so an app can ship brand SVG/Lottie the kit never imports), and other
|
|
|
569
614
|
renders the backend `imageUrl` via RN `<Image>`. Leave a dashboard image's URL empty
|
|
570
615
|
to mean "the app provides this one by name."
|
|
571
616
|
|
|
617
|
+
<!-- doc-check: skip - a single JSX ATTRIBUTE, not a statement; there is no expression form for it -->
|
|
572
618
|
```tsx
|
|
573
619
|
illustrations={{
|
|
574
620
|
"before-after": <BeforeAfterIllustration width={300} />, // your SVG component
|
|
@@ -611,6 +657,7 @@ waiting on every shipped app to update.
|
|
|
611
657
|
native rebuild (most Expo apps already ship it). Skip it and the cards render fine without them.
|
|
612
658
|
Or skip it and bring your own brand nodes, keyed by the same vocabulary names:
|
|
613
659
|
|
|
660
|
+
<!-- doc-check: skip - a single JSX ATTRIBUTE, not a statement; there is no expression form for it -->
|
|
614
661
|
```tsx
|
|
615
662
|
icons={{
|
|
616
663
|
instagram: <BrandInstagram />, // override one name with your own mark
|
|
@@ -680,6 +727,7 @@ An `afterCard` the flow never reaches **clamps to `"beforeEnd"`** instead of sil
|
|
|
680
727
|
|
|
681
728
|
The kit ships English good enough to ship. Override any single line and the rest of the default stays:
|
|
682
729
|
|
|
730
|
+
<!-- doc-check: skip - one OBJECT PROPERTY of a permission screen, not a statement -->
|
|
683
731
|
```tsx
|
|
684
732
|
copy: { title: "Never miss a session", primaryLabel: "Turn on reminders" }
|
|
685
733
|
```
|
|
@@ -705,7 +753,15 @@ The gap between `wire_permission_screen_shown` and `wire_permission_primer_accep
|
|
|
705
753
|
## Backend coupling
|
|
706
754
|
|
|
707
755
|
The backend (`wire-rn/examples/dynamic-onboarding/server`) is the source of truth
|
|
708
|
-
for what the agent may emit.
|
|
756
|
+
for what the agent may emit.
|
|
757
|
+
|
|
758
|
+
> ℹ️ **This section is a VENDOR operation, not something you can do from your app.** Both
|
|
759
|
+
> steps below run against the hosted backend and end in a deploy, so if you need a card
|
|
760
|
+
> that is not in the list above, ask via the console's **Support** section rather than
|
|
761
|
+
> trying to perform them. It is documented here because it explains *why* an unknown card
|
|
762
|
+
> is never emitted, which is otherwise a confusing silence.
|
|
763
|
+
|
|
764
|
+
To make a new card emittable you must, server-side:
|
|
709
765
|
|
|
710
766
|
1. add it to `app/wire.py` `KNOWN_COMPONENTS` + `COMPONENT_DOCS`, **and**
|
|
711
767
|
2. add its name to the tenant's `allowed_components` (the `register_*.py` script),
|
|
@@ -743,12 +799,14 @@ useEffect(() => {
|
|
|
743
799
|
wireDoctor({
|
|
744
800
|
target: { serverUrl: DRIVELINE_SERVER_URL, apiKey: DRIVELINE_API_KEY },
|
|
745
801
|
storage: AsyncStorage,
|
|
802
|
+
// The same values you pass <WireOnboarding>. Omit `join` and the join_key check FAILS.
|
|
803
|
+
join: { appId: DRIVELINE_APP_ID, userContext },
|
|
746
804
|
}).then((report) => console.log(report.ok, report.checks));
|
|
747
805
|
}, []);
|
|
748
806
|
```
|
|
749
807
|
|
|
750
808
|
It resolves `{ ok, checks: [{ name, ok, detail }] }` and **never throws**, whatever the target, the
|
|
751
|
-
storage adapter or the network does.
|
|
809
|
+
storage adapter or the network does. Five independent checks:
|
|
752
810
|
|
|
753
811
|
| `name` | What it proves |
|
|
754
812
|
|---|---|
|
|
@@ -756,9 +814,23 @@ storage adapter or the network does. Four independent checks:
|
|
|
756
814
|
| `reachability` | `GET /v1/events/contract` answers, so the URL really points at a Wire server. |
|
|
757
815
|
| `storage` | The offline queue can persist, via a write / read / delete probe on its own key. Your pending backlog is never read, overwritten or deleted. |
|
|
758
816
|
| `round_trip` | One synthetic event survives real server validation. It goes out with `dry_run`, so nothing is written and no metric moves. The verdict is `written === 1 && skipped === 0`, read from the ack body, because the status code cannot tell you. |
|
|
817
|
+
| `join_key` | The events will be **joinable**: this session will carry a `user_context.device_key`, either the one you supplied or the one the kit auto-injects. Needs `join`. |
|
|
759
818
|
|
|
760
819
|
A `ts`-shaped drift shows up as `skipped=1` carrying the server's own `field: ts`.
|
|
761
820
|
|
|
821
|
+
**Why `join_key` is its own check.** The first four all answer one question — *does an event leave
|
|
822
|
+
and get accepted?* None of them answers *can it be joined?* `user_context.device_key` is the only
|
|
823
|
+
thing that stitches an onboarding session to everything the app reports later, so an integration
|
|
824
|
+
without one writes every event, passes all four, and still reports a permanent **zero** in the
|
|
825
|
+
`activated` funnel. Two real consumers shipped exactly that.
|
|
826
|
+
|
|
827
|
+
**Omitting `join` fails the check** rather than skipping it, for the same reason a release build
|
|
828
|
+
reports `dev_only` as a failure: a report that never looked at the join has not earned a green.
|
|
829
|
+
|
|
830
|
+
`join_key` is **strictly read-only** — one `getItem`, plus pure predicates. It never mints a key,
|
|
831
|
+
never writes the identity registry and never persists anything, so running the doctor cannot itself
|
|
832
|
+
create the split-identity defect it exists to detect.
|
|
833
|
+
|
|
762
834
|
**Dev-only by contract.** The `__DEV__` guard is the first statement in the function, so a release
|
|
763
835
|
build does nothing at all: no network call, no storage write. In that case the report says it was
|
|
764
836
|
skipped rather than reporting a pass it never earned. A host that never imports `wireDoctor` does not
|
|
@@ -818,8 +890,10 @@ below) — nothing to rewire.
|
|
|
818
890
|
**1. Mount the provider once, around your `NavigationContainer`.** It renders the
|
|
819
891
|
overlay host as a root sibling so a ring can paint _above_ the bottom tab bar
|
|
820
892
|
(which react-navigation draws over screen components). It takes a **synchronous**
|
|
821
|
-
storage adapter — gate reads
|
|
822
|
-
|
|
893
|
+
storage adapter — gate reads resolve during render. An async adapter (e.g.
|
|
894
|
+
AsyncStorage handed straight in) is caught by a probe and the gates FAIL CLOSED:
|
|
895
|
+
coachmarks, showcases and the review/questionnaire gates all stay silent, and a
|
|
896
|
+
dev-build warning names the fix. An MMKV wrapper is three lines:
|
|
823
897
|
|
|
824
898
|
```tsx
|
|
825
899
|
import { CoachmarkProvider } from "@wireai/activation/coachmarks";
|
|
@@ -857,9 +931,9 @@ storage, so the app keeps only its own domain gates. Steps must be a memoized
|
|
|
857
931
|
array; step callbacks are held in a ref (parent re-renders never re-fire a step):
|
|
858
932
|
|
|
859
933
|
```tsx
|
|
860
|
-
import { useCoachmarkTour } from "@wireai/activation/coachmarks";
|
|
934
|
+
import { useCoachmarkTour, type CoachmarkStep } from "@wireai/activation/coachmarks";
|
|
861
935
|
|
|
862
|
-
const steps = useMemo(
|
|
936
|
+
const steps = useMemo<CoachmarkStep[]>(
|
|
863
937
|
() => [
|
|
864
938
|
{ id: "feed_scroll", message: "Swipe to explore.", gesture: "swipe_up" },
|
|
865
939
|
{ id: "group_tab", message: "Your groups live here.", anchorId: "group_tab",
|
|
@@ -951,6 +1025,7 @@ const catalog = [
|
|
|
951
1025
|
|
|
952
1026
|
// Sequence: WireOnboarding → onComplete(result) → select from answers → FeatureShowcase → app
|
|
953
1027
|
<WireOnboarding
|
|
1028
|
+
config={config}
|
|
954
1029
|
onComplete={(result) => {
|
|
955
1030
|
persist(result.answers);
|
|
956
1031
|
setSlides(selectShowcaseSlides(catalog, idsFromAnswers(result.answers)));
|
|
@@ -1029,6 +1104,7 @@ own analytics; the four moments are `review_prompt_shown`, `review_rating_select
|
|
|
1029
1104
|
`stars`), `store_review_requested`, and `review_feedback_submitted`. No PII rides in events.
|
|
1030
1105
|
Feedback text goes only in the POST body.
|
|
1031
1106
|
|
|
1107
|
+
<!-- doc-check: skip - a COMPONENT BODY fragment: the `return` needs an enclosing component, which the surrounding prose supplies -->
|
|
1032
1108
|
```tsx
|
|
1033
1109
|
import { ReviewGate, useReviewGate } from "@wireai/activation/reviews";
|
|
1034
1110
|
|
|
@@ -1070,7 +1146,10 @@ import { fetchReviewDecision, useReviewGate } from "@wireai/activation/reviews";
|
|
|
1070
1146
|
// so deviceKey is the identity that matters. sessionId is optional.
|
|
1071
1147
|
const decision = await fetchReviewDecision(target, { deviceKey });
|
|
1072
1148
|
|
|
1073
|
-
|
|
1149
|
+
// `config` here is the GATE's config (it has an `id`), not the kit transport config, and
|
|
1150
|
+
// `gateStorage` is a SYNCHRONOUS store (`getItem`/`setItem` returning values, not promises) —
|
|
1151
|
+
// the same shape `CoachmarkProvider` takes, not the async adapter `<WireOnboarding storage>` wants.
|
|
1152
|
+
const gate = useReviewGate({ config: { id: "post_onboarding" }, decision: decision ?? undefined, storage: gateStorage });
|
|
1074
1153
|
```
|
|
1075
1154
|
|
|
1076
1155
|
It returns the full `{ fire, reason, arm }` on a 2xx and `null` on anything else. That
|
|
@@ -1166,7 +1245,7 @@ useEffect(() => {
|
|
|
1166
1245
|
fetchReviewDecision(target, { sessionId, deviceKey }).then((d) => setDecision(d ?? undefined));
|
|
1167
1246
|
}, [revalidation]);
|
|
1168
1247
|
|
|
1169
|
-
const gate = useReviewGate({ config: { id: "home", minSessions: 2 }, decision, storage });
|
|
1248
|
+
const gate = useReviewGate({ config: { id: "home", minSessions: 2 }, decision, storage: gateStorage });
|
|
1170
1249
|
|
|
1171
1250
|
// …elsewhere, on the triggering action. Awaiting it means the re-fetch sees the event:
|
|
1172
1251
|
await track("journal_done"); // POST stored → revalidation bumps → effect re-fetches → gate can fire
|
|
@@ -1245,7 +1324,7 @@ Skip the onboarding half and the purchase events are still valid on their own, t
|
|
|
1245
1324
|
|
|
1246
1325
|
Three rules govern it:
|
|
1247
1326
|
|
|
1248
|
-
- **Your key always wins.**
|
|
1327
|
+
- **Your key always wins.** A `device_key` you pass is forwarded verbatim, untrimmed, and nothing is merged over it — as long as it is a string with at least one non-whitespace character, which is the kit's entire test for "the host supplied one" (the trim is used only to decide presence). A non-string (the `userContext` type also allows numbers and booleans) or an all-whitespace value is not a usable join key: it is treated as absent and the auto key is merged over it. Existing integrations that pass a real key see a byte-identical payload.
|
|
1249
1328
|
- **It needs `storage`.** Without persistence the kit's id is minted fresh on every launch, and a per-launch key is worse than no key: the server counts `min_sessions` by distinct app-opens grouped on `device_key`, so it corrupts the counter instead of leaving the join empty. With no `storage` the kit declines to inject and warns in dev.
|
|
1250
1329
|
- **You can opt out.** `autoJoinKey={false}` restores the pre-0.12.2 behavior for a host that genuinely wants an unlinked session. The dev warning fires again there, naming the opt-out as the reason nothing was injected.
|
|
1251
1330
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
|
-
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-
|
|
2
|
-
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-
|
|
3
|
-
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-
|
|
1
|
+
export { R as ReportAppEventOptions, r as reportAppEvent } from '../transport-s5QxA-ci.mjs';
|
|
2
|
+
import { C as ClientEventTarget, W as WireUserContext, E as EventQueueOptions } from '../currentSession-Bz7G6lno.mjs';
|
|
3
|
+
export { A as AUTO_DEVICE_ID_PREFIX, a as AnalyticsEvent, b as ClearUserContextOptions, c as ClientEvent, d as ClientEventType, e as ContextEnvelope, f as ContextEnvelopeInput, D as DeviceKeyStorage, g as EnvelopeSource, h as EventQueue, R as ResolveAutoDeviceKeyOptions, i as WIRE_ONBOARDING_EVENTS, j as WireOnboardingEventName, k as analyticsUserIdStorageKey, l as buildContextEnvelope, m as clearPiiFromContext, n as clearUserContext, o as createEventQueue, p as deviceIdStorageKey, q as ensureCurrentSessionId, r as getCurrentSessionId, s as looksLikeEmail, t as makeSessionId, u as reportClientEvent, v as reportClientEventAwait, w as reportClientEvents, x as reportClientEventsAwait, y as resetAutoDeviceKeys, z as resetCurrentSessionId, B as resetEventQueueKeys, F as resolveAutoDeviceKey, G as setCurrentSessionId, H as toAnalyticsEvent } from '../currentSession-Bz7G6lno.mjs';
|
|
4
4
|
import { W as WireOnboardingStorage } from '../types-BpwiRpA8.mjs';
|
|
5
|
-
import '../types-
|
|
5
|
+
import '../types-Byx306Kv.mjs';
|
|
6
6
|
import '../types-BKfpdZzX.mjs';
|
|
7
|
-
import '../types-
|
|
7
|
+
import '../types-D_0B0yay.mjs';
|
|
8
8
|
import 'react';
|
|
9
9
|
import 'wireai-rn';
|
|
10
10
|
import 'react-native';
|
|
@@ -105,13 +105,18 @@ declare const useScreenTracking: (navigationRef: NavigationRefLike | undefined,
|
|
|
105
105
|
* numeric `ts` did exactly that through 0.13.0. This turns that class of loss from something you
|
|
106
106
|
* discover in a funnel report weeks later into something the first run tells you.
|
|
107
107
|
*
|
|
108
|
-
* SHAPE:
|
|
108
|
+
* SHAPE: five independent checks, each a `{name, ok, detail}` unit that can be read (and tested)
|
|
109
109
|
* without the others. The report is data, never a thrown error and never a side effect on the host:
|
|
110
110
|
*
|
|
111
111
|
* 1. `target` : is there a server URL and a key, and do they look like a key and a URL?
|
|
112
112
|
* 2. `reachability`: is the server actually there? (`GET /v1/events/contract`, public and cheap)
|
|
113
113
|
* 3. `storage` : can the offline queue persist? (a write / read / delete probe)
|
|
114
114
|
* 4. `round_trip` : does a REAL event survive REAL server validation? (a `dry_run` POST)
|
|
115
|
+
* 5. `join_key` : will those events be JOINABLE? (read-only; needs `join`)
|
|
116
|
+
*
|
|
117
|
+
* Checks 1-4 all answer "does an event leave and get accepted". NONE of them answers "can it be
|
|
118
|
+
* joined", and an integration with no `user_context.device_key` writes every event, passes every
|
|
119
|
+
* one of those four, and still reports a permanent ZERO in the activated funnel. That is check 5.
|
|
115
120
|
*
|
|
116
121
|
* NEVER THROWS, under any input, any network condition, or any hostile response object. A doctor
|
|
117
122
|
* that can crash the screen it is diagnosing is worse than no doctor.
|
|
@@ -131,6 +136,9 @@ declare const useScreenTracking: (navigationRef: NavigationRefLike | undefined,
|
|
|
131
136
|
* const report = await wireDoctor({
|
|
132
137
|
* target: { serverUrl: "https://api.example.com", apiKey: DRIVELINE_KEY },
|
|
133
138
|
* storage: AsyncStorage,
|
|
139
|
+
* // The same values you hand <WireOnboarding>. Omit `join` and the join_key check FAILS,
|
|
140
|
+
* // because a report that never looked at the join cannot honestly read green.
|
|
141
|
+
* join: { appId: WIRE_APP_ID, userContext },
|
|
134
142
|
* });
|
|
135
143
|
* console.log(report.ok, report.checks);
|
|
136
144
|
*/
|
|
@@ -138,7 +146,8 @@ declare const useScreenTracking: (navigationRef: NavigationRefLike | undefined,
|
|
|
138
146
|
/** One diagnosis. `name` is stable and machine-readable; `detail` is for a human reading a console. */
|
|
139
147
|
type WireDoctorCheck = {
|
|
140
148
|
/**
|
|
141
|
-
* Stable id: `target` | `reachability` | `storage` | `round_trip` | `
|
|
149
|
+
* Stable id: `target` | `reachability` | `storage` | `round_trip` | `join_key` | `dev_only` |
|
|
150
|
+
* `internal_error`.
|
|
142
151
|
*
|
|
143
152
|
* `dev_only` means ONE thing and only that thing: `__DEV__` is unset or false, so nothing ran.
|
|
144
153
|
* `internal_error` is the separate catch-all for a failure that got past every check's own
|
|
@@ -155,6 +164,19 @@ type WireDoctorReport = {
|
|
|
155
164
|
ok: boolean;
|
|
156
165
|
checks: WireDoctorCheck[];
|
|
157
166
|
};
|
|
167
|
+
/**
|
|
168
|
+
* What the join check needs: the same three values the host hands `<WireOnboarding>`. Pass the very
|
|
169
|
+
* props you pass the component — anything reconstructed here would diagnose a DIFFERENT integration
|
|
170
|
+
* than the one that ships.
|
|
171
|
+
*/
|
|
172
|
+
type WireDoctorJoinTarget = {
|
|
173
|
+
/** The same `config.appId`. It namespaces the persisted auto key, so the wrong one reads the wrong slot. */
|
|
174
|
+
appId?: string;
|
|
175
|
+
/** The same `userContext` prop. Only `device_key` is read; nothing else is inspected or reported. */
|
|
176
|
+
userContext?: Record<string, string | number | boolean>;
|
|
177
|
+
/** The same `autoJoinKey` prop. `false` is the documented opt-out from the kit's auto-join. */
|
|
178
|
+
autoJoinKey?: boolean;
|
|
179
|
+
};
|
|
158
180
|
/** Input for {@link wireDoctor}. `storage` is optional: without it the queue runs in-memory only. */
|
|
159
181
|
type WireDoctorOptions = {
|
|
160
182
|
/** The same `{serverUrl, apiKey}` the kit is configured with. */
|
|
@@ -164,6 +186,11 @@ type WireDoctorOptions = {
|
|
|
164
186
|
* check reports the DEGRADED in-memory mode rather than failing.
|
|
165
187
|
*/
|
|
166
188
|
storage?: WireOnboardingStorage;
|
|
189
|
+
/**
|
|
190
|
+
* The join-key inputs. OMITTING THIS FAILS THE `join_key` CHECK — deliberately, and see
|
|
191
|
+
* {@link checkJoinKey} for why a skipped join check may not read as a pass.
|
|
192
|
+
*/
|
|
193
|
+
join?: WireDoctorJoinTarget;
|
|
167
194
|
};
|
|
168
195
|
/**
|
|
169
196
|
* Run the full diagnosis. Resolves a report; NEVER throws and NEVER rejects.
|
|
@@ -283,11 +310,6 @@ type Analytics = {
|
|
|
283
310
|
*/
|
|
284
311
|
declare const createAnalytics: (config: CreateAnalyticsConfig, options?: AnalyticsOptions) => Analytics;
|
|
285
312
|
|
|
286
|
-
/**
|
|
287
|
-
* Build a per-mount analytics instance. `config`/`options` are read once at first render (the
|
|
288
|
-
* instance is stable for the component's lifetime, held in a ref). Returns the {@link Analytics}
|
|
289
|
-
* surface so the component can `track` / `screen` / `identify` and drive `notifyOnline` on reconnect.
|
|
290
|
-
*/
|
|
291
313
|
declare const useAnalytics: (config: CreateAnalyticsConfig, options?: AnalyticsOptions) => Analytics;
|
|
292
314
|
|
|
293
|
-
export { type Analytics, type AnalyticsOptions, type AnalyticsProps, ClientEventTarget, type CreateAnalyticsConfig, EventQueueOptions, type NavigationRefLike, type NavigationRouteLike, type NavigationStateLike, type ScreenTracker, type ScreenTrackerOptions, type WireDoctorCheck, type WireDoctorOptions, type WireDoctorReport, createAnalytics, createScreenTracker, getActiveRouteName, screenTrackingHandler, useAnalytics, useScreenTracking, wireDoctor };
|
|
315
|
+
export { type Analytics, type AnalyticsOptions, type AnalyticsProps, ClientEventTarget, type CreateAnalyticsConfig, EventQueueOptions, type NavigationRefLike, type NavigationRouteLike, type NavigationStateLike, type ScreenTracker, type ScreenTrackerOptions, type WireDoctorCheck, type WireDoctorJoinTarget, type WireDoctorOptions, type WireDoctorReport, createAnalytics, createScreenTracker, getActiveRouteName, screenTrackingHandler, useAnalytics, useScreenTracking, wireDoctor };
|