@wireai/activation 0.1.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 +73 -0
- package/CHANGELOG.md +572 -0
- package/INTEGRATION_PROMPT.md +55 -0
- package/LICENSE +21 -0
- package/README.md +923 -0
- package/dist/CenteredModal-C3qQBHsA.d.mts +38 -0
- package/dist/CenteredModal-Cdgns6--.d.ts +38 -0
- package/dist/coachmarks/index.d.mts +352 -0
- package/dist/coachmarks/index.d.ts +352 -0
- package/dist/coachmarks/index.js +944 -0
- package/dist/coachmarks/index.js.map +1 -0
- package/dist/coachmarks/index.mjs +918 -0
- package/dist/coachmarks/index.mjs.map +1 -0
- package/dist/decision-CFvGY6nP.d.ts +228 -0
- package/dist/decision-Cl8OFYzu.d.mts +228 -0
- package/dist/index.d.mts +1390 -0
- package/dist/index.d.ts +1390 -0
- package/dist/index.js +3260 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +3186 -0
- package/dist/index.mjs.map +1 -0
- package/dist/questionnaire/index.d.mts +249 -0
- package/dist/questionnaire/index.d.ts +249 -0
- package/dist/questionnaire/index.js +922 -0
- package/dist/questionnaire/index.js.map +1 -0
- package/dist/questionnaire/index.mjs +906 -0
- package/dist/questionnaire/index.mjs.map +1 -0
- package/dist/reviews/index.d.mts +213 -0
- package/dist/reviews/index.d.ts +213 -0
- package/dist/reviews/index.js +1001 -0
- package/dist/reviews/index.js.map +1 -0
- package/dist/reviews/index.mjs +971 -0
- package/dist/reviews/index.mjs.map +1 -0
- package/dist/showcase/index.d.mts +119 -0
- package/dist/showcase/index.d.ts +119 -0
- package/dist/showcase/index.js +660 -0
- package/dist/showcase/index.js.map +1 -0
- package/dist/showcase/index.mjs +651 -0
- package/dist/showcase/index.mjs.map +1 -0
- package/dist/types-BKfpdZzX.d.mts +84 -0
- package/dist/types-BKfpdZzX.d.ts +84 -0
- package/dist/types-CMuOexw0.d.mts +116 -0
- package/dist/types-CMuOexw0.d.ts +116 -0
- package/dist/types-GL_hQ0TN.d.mts +46 -0
- package/dist/types-GL_hQ0TN.d.ts +46 -0
- package/llms.txt +36 -0
- package/metro/index.d.ts +32 -0
- package/metro/index.js +83 -0
- package/package.json +171 -0
- package/src/DemoOnboarding.tsx +133 -0
- package/src/OnboardingFlow.tsx +504 -0
- package/src/WireOnboarding.tsx +248 -0
- package/src/analytics/analyticsEvent.ts +69 -0
- package/src/analytics/reportClientEvent.ts +119 -0
- package/src/analytics/sendPreview.ts +73 -0
- package/src/attribution/attribution.ts +46 -0
- package/src/cards/ChipSelectCard.tsx +265 -0
- package/src/cards/InterstitialCard.tsx +299 -0
- package/src/cards/NumberStepperCard.tsx +121 -0
- package/src/cards/SelectionCard.tsx +270 -0
- package/src/cards/StatusCard.tsx +170 -0
- package/src/cards/TextInputCard.tsx +147 -0
- package/src/cards/index.ts +23 -0
- package/src/coachmarks/CoachmarkOverlayHost.tsx +44 -0
- package/src/coachmarks/CoachmarkProvider.tsx +108 -0
- package/src/coachmarks/GestureHint.tsx +331 -0
- package/src/coachmarks/SpotlightOverlay.tsx +254 -0
- package/src/coachmarks/coachmarkAnchorRegistry.ts +70 -0
- package/src/coachmarks/coachmarkOverlayStore.ts +63 -0
- package/src/coachmarks/index.ts +62 -0
- package/src/coachmarks/runtime.ts +103 -0
- package/src/coachmarks/selectTourSteps.ts +45 -0
- package/src/coachmarks/types.ts +58 -0
- package/src/coachmarks/useCoachmarkAnchor.ts +49 -0
- package/src/coachmarks/useCoachmarkTour.ts +200 -0
- package/src/components/AnimatedSparkle.tsx +101 -0
- package/src/components/Button.tsx +129 -0
- package/src/components/CardHandoff.tsx +212 -0
- package/src/components/CardLayout.tsx +103 -0
- package/src/components/CenteredModal.tsx +184 -0
- package/src/components/CompletionView.tsx +186 -0
- package/src/components/DoneBlock.tsx +37 -0
- package/src/components/ErrorBlock.tsx +42 -0
- package/src/components/Illustration.tsx +36 -0
- package/src/components/LoadingBlock.tsx +182 -0
- package/src/components/LoadingScreen.tsx +35 -0
- package/src/components/OnboardingScaffold.tsx +105 -0
- package/src/components/StepProgress.tsx +85 -0
- package/src/components/loaderChrome.ts +28 -0
- package/src/config/onboardingFlag.ts +39 -0
- package/src/config/wireConfigFromEnv.ts +52 -0
- package/src/device/deviceContext.ts +158 -0
- package/src/features/WireFeaturesProvider.tsx +81 -0
- package/src/features/cache.ts +92 -0
- package/src/features/defaults.ts +67 -0
- package/src/features/fetchWireFeatures.ts +86 -0
- package/src/features/index.ts +39 -0
- package/src/features/types.ts +61 -0
- package/src/features/useWireFeatures.ts +57 -0
- package/src/identity/userIdentity.ts +100 -0
- package/src/illustrations/defaultIllustrations.tsx +99 -0
- package/src/index.ts +159 -0
- package/src/motion/centeredModalMotion.ts +45 -0
- package/src/motion/motionSpec.ts +133 -0
- package/src/motion/useReducedMotion.ts +43 -0
- package/src/questionnaire/QuestionnaireGate.tsx +235 -0
- package/src/questionnaire/decision.ts +63 -0
- package/src/questionnaire/index.ts +54 -0
- package/src/questionnaire/runtime.ts +20 -0
- package/src/questionnaire/transport.ts +83 -0
- package/src/questionnaire/types.ts +182 -0
- package/src/questionnaire/useQuestionnaireGate.ts +159 -0
- package/src/reviews/ReviewGate.tsx +316 -0
- package/src/reviews/ReviewModal.tsx +13 -0
- package/src/reviews/decision.ts +120 -0
- package/src/reviews/equality.ts +54 -0
- package/src/reviews/expo-store-review.d.ts +16 -0
- package/src/reviews/feedbackForm.ts +41 -0
- package/src/reviews/index.ts +78 -0
- package/src/reviews/presentation.ts +29 -0
- package/src/reviews/runtime.ts +45 -0
- package/src/reviews/storeReview.ts +62 -0
- package/src/reviews/transport.ts +89 -0
- package/src/reviews/types.ts +172 -0
- package/src/reviews/useReviewGate.ts +137 -0
- package/src/session/persistedSession.ts +143 -0
- package/src/session-analytics/index.ts +18 -0
- package/src/session-analytics/reportSessionStart.ts +144 -0
- package/src/session-analytics/useSessionStart.ts +114 -0
- package/src/showcase/FeatureShowcase.tsx +233 -0
- package/src/showcase/index.ts +19 -0
- package/src/showcase/selectShowcaseSlides.ts +45 -0
- package/src/showcase/showcaseColors.ts +91 -0
- package/src/showcase/types.ts +74 -0
- package/src/theme/ThemeContext.tsx +38 -0
- package/src/theme/defaultTheme.ts +70 -0
- package/src/theme/themeFromBrand.ts +93 -0
- package/src/theme/types.ts +88 -0
- package/src/theme/typography.ts +46 -0
- package/src/types.ts +215 -0
- package/src/utils/deriveAnswers.ts +43 -0
- package/src/utils/readProgress.ts +33 -0
package/README.md
ADDED
|
@@ -0,0 +1,923 @@
|
|
|
1
|
+
# wireai-onboarding
|
|
2
|
+
|
|
3
|
+
A premium, fully-themable **drop-in onboarding kit** on top of the open-source
|
|
4
|
+
[`wireai-rn`](https://github.com/chohra-med/wireai-rn) SDK. You give it a tenant
|
|
5
|
+
config; it runs an entire AI-driven, backend-orchestrated onboarding — themed
|
|
6
|
+
cards, a progress bar, a mid-flow value screen, per-step validation, and a
|
|
7
|
+
completion recap — behind one component.
|
|
8
|
+
|
|
9
|
+
> MIT-licensed. Install from npm as [`wireai-onboarding`](https://www.npmjs.com/package/wireai-onboarding), or consume **from source** for live kit-edit (as the apps in this repo do).
|
|
10
|
+
|
|
11
|
+
<div align="center">
|
|
12
|
+
|
|
13
|
+
[](https://www.npmjs.com/package/wireai-onboarding)
|
|
14
|
+
[](LICENSE)
|
|
15
|
+
|
|
16
|
+
Created by [**Malik Chohra**](https://getwireai.com?utm_source=github&utm_medium=readme&utm_campaign=creator) · [Code Meet AI newsletter](https://codemeetai.substack.com?utm_source=github&utm_medium=readme&utm_campaign=newsletter)
|
|
17
|
+
|
|
18
|
+
Sponsored by [AI Mobile Launcher](https://aimobilelauncher.com?utm_source=github&utm_medium=readme&utm_campaign=sponsor) and [CasaInnov](https://casainnov.com?utm_source=github&utm_medium=readme&utm_campaign=sponsor)
|
|
19
|
+
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
## Where it runs (the moving parts)
|
|
23
|
+
|
|
24
|
+
| Layer | Repo / path | Role |
|
|
25
|
+
|---|---|---|
|
|
26
|
+
| **SDK (OSS)** | `wireai-rn` | `WireAIProvider` + `ComponentRenderer` + `useWireAIThread`; renders whatever components the host registers; A2A transport. |
|
|
27
|
+
| **This kit** | `getwire_ai/wireai-onboarding` | `<WireOnboarding>` — themed cards, sparkle loader, countless stepper, `InterstitialCard` value screen, animated-checkmark completion, validators. **The single onboarding UI engine.** |
|
|
28
|
+
| **Backend** | `wire-rn/examples/dynamic-onboarding/server` | Multi-tenant LangGraph engine. Per-app `product_context` + `allowed_components`; validates every emission against `KNOWN_COMPONENTS`. Live on fly dev. |
|
|
29
|
+
| **Consumers** | Production B2C mobile apps | Each hosts `<WireOnboarding>` with its own theme + illustration registry. |
|
|
30
|
+
|
|
31
|
+
The kit's peers are `react`, `react-native`, `wireai-rn`, and
|
|
32
|
+
`react-native-safe-area-context` (>=4.0.0, for safe-area insets — the host must render a
|
|
33
|
+
`SafeAreaProvider` ancestor, standard in every Expo/RN app) — **no reanimated, no SVG, no
|
|
34
|
+
icon font.** Brand artwork is supplied by the host (see [Illustrations](#illustrations)).
|
|
35
|
+
|
|
36
|
+
## How to include it in an app (end-to-end)
|
|
37
|
+
|
|
38
|
+
> Fastest path: run the **`wire-rn-integration`** Claude skill — it does all of the
|
|
39
|
+
> steps below automatically (detects your conventions, wires Metro, derives a theme,
|
|
40
|
+
> scaffolds the screen, type-checks). The manual steps, in order:
|
|
41
|
+
|
|
42
|
+
**0. Register the app on the backend → get a tenant key.** The kit talks to the
|
|
43
|
+
multi-tenant backend over A2A; the **`apiKey` is per-app and resolves the tenant
|
|
44
|
+
server-side**. Create the app in the getwireai **console** (or run the backend's
|
|
45
|
+
`register_<app>.py`) → it mints a `wai_…` key. Note the **server URL** (e.g. the fly
|
|
46
|
+
dev/prod URL). Nothing renders without these two.
|
|
47
|
+
|
|
48
|
+
**1. Install the package** (plus its peers):
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
npm install wireai-onboarding wireai-rn
|
|
52
|
+
# react + react-native are peers (already in any RN/Expo app)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
> ⚠️ **EAS / cloud builds:** install from a **git URL or a published registry**, not a
|
|
56
|
+
> local `file:../...` path *outside the app repo* — EAS only uploads the app repo, so an
|
|
57
|
+
> external `file:` path fails to resolve in the cloud build. (Local `file:` is fine for
|
|
58
|
+
> on-device dev only.)
|
|
59
|
+
|
|
60
|
+
**2. Wire Metro** (one call — pins a single React/RN/wireai-rn/zod copy; a 2nd React
|
|
61
|
+
instance crashes RN):
|
|
62
|
+
|
|
63
|
+
```js
|
|
64
|
+
const { getDefaultConfig } = require("expo/metro-config");
|
|
65
|
+
const { withWireOnboarding } = require("wireai-onboarding/metro");
|
|
66
|
+
module.exports = withWireOnboarding(getDefaultConfig(__dirname));
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**3. Config from env** (so you can flip backends without a code change):
|
|
70
|
+
|
|
71
|
+
```
|
|
72
|
+
EXPO_PUBLIC_WIREAI_API_KEY=wai_…
|
|
73
|
+
EXPO_PUBLIC_WIREAI_SERVER_URL=https://…
|
|
74
|
+
EXPO_PUBLIC_WIREAI_APP_ID=my-app
|
|
75
|
+
```
|
|
76
|
+
`wireConfigFromEnv({ appId })` reads these and returns `null` when keys are missing —
|
|
77
|
+
use that null as your gate (below).
|
|
78
|
+
|
|
79
|
+
**4. Load your brand fonts in the host.** The kit applies `theme.fonts.regular/medium/
|
|
80
|
+
bold` as **React Native `fontFamily` strings** — they only work if the host has loaded
|
|
81
|
+
those fonts (e.g. `expo-font` `useFonts`). If you don't load them, text falls back to
|
|
82
|
+
the system font. (The kit ships no fonts by design.)
|
|
83
|
+
|
|
84
|
+
**5. Theme it to match your app — including dark/light.** Either derive a palette from
|
|
85
|
+
one color (`themeFromBrand({ primary })`) or pass a full `Partial<OnboardingTheme>`
|
|
86
|
+
(colors + the font family names from step 4 + radius/spacing). To follow the app's
|
|
87
|
+
dark/light setting, build **two themes and pick by your theme state**, for example:
|
|
88
|
+
|
|
89
|
+
```tsx
|
|
90
|
+
const theme = isDarkMode ? appThemeDark : appThemeLight; // each sets fonts:{regular:'YourFontRegular',…}
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
**6. Render it + wire the continuation.** Drop `<WireOnboarding>` into your signup
|
|
94
|
+
flow (between username and the next step is typical). On `onComplete`, persist the
|
|
95
|
+
answers through your normal profile-update path, then navigate onward; on `onSkip`,
|
|
96
|
+
just navigate onward:
|
|
97
|
+
|
|
98
|
+
```tsx
|
|
99
|
+
<WireOnboarding
|
|
100
|
+
config={config}
|
|
101
|
+
theme={theme}
|
|
102
|
+
illustrations={{ ...defaultIllustrations, ...myIllustrations }}
|
|
103
|
+
validators={{ username: checkUsernameAvailable }}
|
|
104
|
+
onComplete={(r) => { persist(r.answers); goNext(); }}
|
|
105
|
+
onSkip={goNext}
|
|
106
|
+
onError={fallBackToStaticOnboarding}
|
|
107
|
+
onEvent={analytics}
|
|
108
|
+
/>
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
**7. Gate it so you can turn it on/off without a release.** Wrap the screen behind a
|
|
112
|
+
flag (env var for dev, **remote config** for production) **AND** the `config != null`
|
|
113
|
+
check, so a half-set env or a backend outage never routes a user into a dead screen —
|
|
114
|
+
fall through to your existing onboarding instead. Decide *who* sees it (e.g. new
|
|
115
|
+
signups only) in your own navigation logic.
|
|
116
|
+
|
|
117
|
+
**8. Make any new cards emittable on the backend.** Registering a card in this kit is
|
|
118
|
+
not enough — the server validates every emission. See [Backend coupling](#backend-coupling).
|
|
119
|
+
|
|
120
|
+
**No native rebuild for the kit itself** (it's pure JS). A rebuild is only needed if
|
|
121
|
+
*your* gating/flag mechanism uses a native module (e.g. Firebase Remote Config).
|
|
122
|
+
|
|
123
|
+
## Installation
|
|
124
|
+
|
|
125
|
+
The kit ships **compiled output** (`dist/`, ESM + CJS + types) for external apps,
|
|
126
|
+
**and** still resolves to `src/` for the monorepo apps that consume it from source
|
|
127
|
+
— driven by the `react-native` export condition + each app's tsconfig path alias.
|
|
128
|
+
|
|
129
|
+
### 1. External apps (install from npm)
|
|
130
|
+
|
|
131
|
+
Install the package and wire Metro with the one-call helper:
|
|
132
|
+
|
|
133
|
+
```bash
|
|
134
|
+
# from npm (public)
|
|
135
|
+
npm install wireai-onboarding
|
|
136
|
+
# …or from a tarball (see Distribution below)
|
|
137
|
+
npm install ./wireai-onboarding-0.1.3.tgz
|
|
138
|
+
# …or over SSH
|
|
139
|
+
npm install git+ssh://git@github.com/<org>/wireai-onboarding.git
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
```js
|
|
143
|
+
// metro.config.js — replaces the hand-written ~25-line block
|
|
144
|
+
const { getDefaultConfig } = require("expo/metro-config");
|
|
145
|
+
const { withWireOnboarding } = require("wireai-onboarding/metro");
|
|
146
|
+
|
|
147
|
+
module.exports = withWireOnboarding(getDefaultConfig(__dirname));
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
// 3-line minimal usage — themed from a brand color, config from env, default art
|
|
152
|
+
import { WireOnboarding, themeFromBrand, wireConfigFromEnv, defaultIllustrations } from "wireai-onboarding";
|
|
153
|
+
|
|
154
|
+
export function Onboarding() {
|
|
155
|
+
const config = wireConfigFromEnv({ appId: "my-app" }); // null if env keys missing
|
|
156
|
+
if (!config) return null; // …or render your static onboarding
|
|
157
|
+
return (
|
|
158
|
+
<WireOnboarding
|
|
159
|
+
config={config}
|
|
160
|
+
theme={themeFromBrand({ primary: "#15B0B0" })}
|
|
161
|
+
illustrations={defaultIllustrations}
|
|
162
|
+
onComplete={(r) => persist(r.answers)}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
`wireConfigFromEnv` reads `EXPO_PUBLIC_WIREAI_API_KEY` / `EXPO_PUBLIC_WIREAI_SERVER_URL`
|
|
169
|
+
(+ optional `EXPO_PUBLIC_WIREAI_APP_ID`) and returns `null` when keys are missing, so
|
|
170
|
+
you gate the AI flow on one falsy check. `themeFromBrand` derives a full palette from
|
|
171
|
+
your primary color; spread your own art over `defaultIllustrations` to override by
|
|
172
|
+
name: `illustrations={{ ...defaultIllustrations, ...myIllustrations }}`.
|
|
173
|
+
|
|
174
|
+
### 2. Source consumers (your monorepo apps)
|
|
175
|
+
|
|
176
|
+
Same helper, with `source` pointed at the kit's `src/` (adds it to `watchFolders`
|
|
177
|
+
and maps the `wireai-onboarding` specifier to source). Keep the tsconfig path alias.
|
|
178
|
+
|
|
179
|
+
```js
|
|
180
|
+
const path = require("path");
|
|
181
|
+
const { getDefaultConfig } = require("expo/metro-config");
|
|
182
|
+
const { withWireOnboarding } = require("wireai-onboarding/metro");
|
|
183
|
+
|
|
184
|
+
module.exports = withWireOnboarding(getDefaultConfig(__dirname), {
|
|
185
|
+
source: path.join(__dirname, "../../getwire_ai/wireai-onboarding/src"),
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`withWireOnboarding(config, options)` is non-destructive: it pins `react` /
|
|
190
|
+
`react-native` / `wireai-rn` / `zod` to the app's own copy (a 2nd React instance
|
|
191
|
+
crashes RN), merges with any `extraNodeModules` / `watchFolders` you already set, and
|
|
192
|
+
returns the same config. `options.appRoot` defaults to `process.cwd()`.
|
|
193
|
+
|
|
194
|
+
> The `"react-native"` export condition resolves `wireai-onboarding` to `src/index.ts`
|
|
195
|
+
> for Metro (it transforms the kit's source), while `import`/`require` resolve to the
|
|
196
|
+
> built `dist`. So both npm consumers and source consumers work; the dist build is additive.
|
|
197
|
+
|
|
198
|
+
## Distribution
|
|
199
|
+
|
|
200
|
+
`npm run pack:tarball` runs the build (via `prepack`) and produces
|
|
201
|
+
`wireai-onboarding-x.y.z.tgz` containing `dist` + `src` + `metro` + README +
|
|
202
|
+
CHANGELOG. The primary channel is **npm** (`npm install wireai-onboarding`); a
|
|
203
|
+
tarball (`npm install ./wireai-onboarding-x.y.z.tgz`) or a `git+ssh://…` install
|
|
204
|
+
work too for pre-publish or pinned builds.
|
|
205
|
+
|
|
206
|
+
## Quick start
|
|
207
|
+
|
|
208
|
+
```tsx
|
|
209
|
+
import { WireOnboarding } from "wireai-onboarding";
|
|
210
|
+
|
|
211
|
+
<WireOnboarding
|
|
212
|
+
config={{ apiKey, serverUrl, appId: "my-app" }}
|
|
213
|
+
theme={myBrandTheme}
|
|
214
|
+
illustrations={{ momentum: <MyMomentumSvg /> }}
|
|
215
|
+
validators={{ username: checkUsernameAvailable }}
|
|
216
|
+
onComplete={(result) => persist(result.answers)}
|
|
217
|
+
onSkip={skip}
|
|
218
|
+
onError={() => fallBackToStaticOnboarding()}
|
|
219
|
+
onEvent={(e) => analytics(e)}
|
|
220
|
+
/>
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### `<WireOnboarding>` props
|
|
224
|
+
|
|
225
|
+
| Prop | Type | Notes |
|
|
226
|
+
|---|---|---|
|
|
227
|
+
| `config` | `{ apiKey, serverUrl, appId, metadata?, appVersion? }` | A2A transport; the key resolves the tenant server-side. `appVersion` is host-injected (e.g. `Constants.expoConfig?.version`) and forwarded for analytics segmentation. **Required.** |
|
|
228
|
+
| `onComplete` | `(result: { answers, raw }) => void` | Fires when the user taps the terminal recap's CTA. **Required.** |
|
|
229
|
+
| `theme` | `Partial<OnboardingTheme>` | Brand colors/fonts/radius/spacing, deep-merged over a neutral default. |
|
|
230
|
+
| `illustrations` | `Record<string, ReactNode>` | App artwork for `InterstitialCard`, keyed by name. |
|
|
231
|
+
| `validators` | `Record<string, StepValidator>` | Per-step, keyed by base-question key (e.g. `username`). Blocks advance + shows an inline error. |
|
|
232
|
+
| `onSkip` | `() => void` | User skipped. |
|
|
233
|
+
| `onError` | `(err) => void` | Backend error/timeout — host owns recovery (e.g. route to a static flow). Without it, the kit shows an inline retry. |
|
|
234
|
+
| `onEvent` | `(e: OnboardingEvent) => void` | `started` / `turn` / `error` — recover per-turn analytics since the kit owns the loop. |
|
|
235
|
+
| `copy` | `Partial<OnboardingCopy>` | Localize the kit's built-in English loader/completion strings. |
|
|
236
|
+
| `approxScreens` | `number` | Approx total screens — paces the bar as `step/total` (capped, never shown). A backend `progress.total` (the screen budget, sent every turn incl. init) wins, so usually unnecessary. |
|
|
237
|
+
| `components` | `WireAIComponent[]` | Override the registered cards (defaults to `onboardingComponents`). |
|
|
238
|
+
| `startMessage` | `string` | First message to the backend. Default `"start"`. |
|
|
239
|
+
| `startTimeoutMs` | `number` | Watchdog for the first card. Default `15000`. |
|
|
240
|
+
| `storage` | `WireOnboardingStorage` | Host-injected storage (AsyncStorage-compatible `getItem/setItem/removeItem`) for **session-id persistence**: an app KILL mid-onboarding resumes the SAME backend session instead of minting a new one, so the funnel's `started` count stays honest (no phantom drops). Pass AsyncStorage as-is, or a 3-line MMKV wrapper. Omit for the previous per-mount behavior. Persists the kit's own correlation seed only — answers stay the host's job via `onComplete`. |
|
|
241
|
+
| `sessionTtlMs` | `number` | How long a persisted session id stays resumable. Default `3600000` (1h, the backend's session TTL). With `storage` only. |
|
|
242
|
+
| `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. |
|
|
243
|
+
| `userContext` | `Record<string, string \| number \| boolean>` | Host-injected, 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** (no raw emails/names); primitives only; the server caps size/keys. See [Device & user context](#device--user-context). |
|
|
244
|
+
| `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). |
|
|
245
|
+
|
|
246
|
+
## Helpers (the reusable substrate)
|
|
247
|
+
|
|
248
|
+
You rarely hand-roll config, gating, analytics, or attribution — the kit ships them:
|
|
249
|
+
|
|
250
|
+
| Helper | Use |
|
|
251
|
+
|---|---|
|
|
252
|
+
| `wireConfigFromEnv({ appId, metadata? })` | Reads `EXPO_PUBLIC_WIREAI_API_KEY`/`_SERVER_URL`/`_APP_ID` → a `config` (or `null` to gate). |
|
|
253
|
+
| `isOnboardingEnabled({ remote? })` | The one gate: transport present (apiKey + serverUrl). Optional `remote` kill-switch to disable without a release; no env flag. |
|
|
254
|
+
| `WIRE_ONBOARDING_EVENTS` + `toAnalyticsEvent(e)` | Map a kit `OnboardingEvent` to a canonical `wire_onboarding_*` `{ name, params }`; log it through your own analytics. |
|
|
255
|
+
| `attributionMetadata(a)` | Shape install/ad attribution into `{ attribution }` for `config.metadata` (forwarded into every backend request). |
|
|
256
|
+
| `reportClientEvent` / `reportClientEvents` / `makeSessionId` | Report device-only funnel events to `POST /v1/events`. `<WireOnboarding>` does this automatically: `dropped` on unmount-without-complete, and `client_fallback` when the AI flow degrades to the static fallback (so the dashboard's fallback-rate counts the whole-flow case). The host must NOT also report its own fallback. |
|
|
257
|
+
| `identifyOnboarding(opts)` / `sanitizeUserId` / `USER_ID_MAX_LENGTH` | Bind a `userId` to a session AFTER the flow (post-registration). `identifyOnboarding` resolves the session from an explicit `contextId` (captured from the `started`/`resumed` `onEvent`) or the persisted `storage`, then posts an `identify` event. `sanitizeUserId` trims/caps an id; `USER_ID_MAX_LENGTH` is the 128-char cap. See [User identity](#user-identity). |
|
|
258
|
+
| `deriveAnswers(messages)` | Deterministic `{ key: value }` from the thread (no second LLM call). |
|
|
259
|
+
| `themeFromBrand({ primary })` · `defaultIllustrations` | One-color theme; dependency-free fallback artwork. |
|
|
260
|
+
| `motionSpec` | The motion constants behind every kit animation (durations, springs, the design ease). Read-only for hosts. `setMotionDurationScale` / `getMotionDurationScale` are QA tools for the playground's animation-speed knob: never call them in production code. |
|
|
261
|
+
|
|
262
|
+
> **`onEvent`** fires `started` / `resumed` / `turn` / `error` / `retry` / `fallback` (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. 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)).
|
|
263
|
+
|
|
264
|
+
**Integrating with an AI agent?** See [`INTEGRATION_PROMPT.md`](./INTEGRATION_PROMPT.md) (copy-paste prompt for Claude Code) and [`llms.txt`](./llms.txt). Fastest of all: the `wire-rn-integration` Claude skill.
|
|
265
|
+
|
|
266
|
+
## Device & user context
|
|
267
|
+
|
|
268
|
+
Two optional, host-injected inputs let onboarding analytics segment the funnel by device and by what your app already knows about the user. The kit collects the device snapshot itself (no dependency added); everything else you pass.
|
|
269
|
+
|
|
270
|
+
- **`config.appVersion`** (string): your app version, e.g. `Constants.expoConfig?.version`. The kit reads nothing to get it, so it stays dependency-free; you inject it.
|
|
271
|
+
- **`userContext`** (`Record<string, string | number | boolean>`): non-PII context the app already has, like signup method, referral source, plan tier, or a hashed user id. Same host-injection idea as `storage`. Do not put raw emails, names, or phone numbers here; pass a hash if you need a user key. Values are primitives only, and the server caps key count/size and drops deep nesting.
|
|
272
|
+
|
|
273
|
+
Both ride the A2A session-start metadata AND every client event (`dropped`, `client_fallback`). Old servers ignore the extra fields, so it is backward compatible.
|
|
274
|
+
|
|
275
|
+
### What `collectDeviceContext()` collects
|
|
276
|
+
|
|
277
|
+
Only React Native built-ins and the standard `Intl` global, each read defensively (missing fields are omitted, it never throws):
|
|
278
|
+
|
|
279
|
+
| Field | Source |
|
|
280
|
+
|---|---|
|
|
281
|
+
| `platform` | `Platform.OS` |
|
|
282
|
+
| `osVersion` | `Platform.Version` / iOS `osVersion` / Android `Release` |
|
|
283
|
+
| `brand`, `model` | Android `Platform.constants` |
|
|
284
|
+
| `interfaceIdiom` | iOS `Platform.constants` |
|
|
285
|
+
| `formFactor` | `phone` / `tablet` (iOS idiom, else shortest screen side >= 600dp) |
|
|
286
|
+
| `screenWidth`, `screenHeight`, `screenScale` | `Dimensions.get('screen')` |
|
|
287
|
+
| `isRTL` | `I18nManager.isRTL` |
|
|
288
|
+
| `locale`, `timeZone` | `Intl.DateTimeFormat().resolvedOptions()` (guarded for Hermes) |
|
|
289
|
+
| `appVersion` | host-injected from `config.appVersion` |
|
|
290
|
+
|
|
291
|
+
### No tracking (privacy-label-neutral)
|
|
292
|
+
|
|
293
|
+
No advertising IDs (no IDFA/GAID), no `getUniqueId`, no fingerprinting APIs. Nothing here identifies a user or device uniquely, so adopting it does not change your App Privacy or Data Safety declarations, and it adds zero dependencies. On the backend the server derives a coarse country from the sent locale/timezone only, and never processes or stores IP addresses.
|
|
294
|
+
|
|
295
|
+
## User identity
|
|
296
|
+
|
|
297
|
+
`userContext` segments the funnel; **`userId`** goes one step further and lets you reconcile an onboarding session to a real user later (console sessions to your own user table, or GA4 users). It is an OPAQUE, PSEUDONYMOUS string that YOU own, your internal user id, **not** an email, name, or phone number. Pass a hash if your only key is an email. The kit trims it and caps it at `USER_ID_MAX_LENGTH` (128) chars; a longer id is truncated, never rejected. Old servers ignore the extra field, so it stays backward compatible.
|
|
298
|
+
|
|
299
|
+
Users often register during or after onboarding, so `userId` is fully optional and can arrive late. There are three binding moments:
|
|
300
|
+
|
|
301
|
+
**1. Known at mount.** Pass it as a prop and it rides the A2A session-start metadata; the server binds it when it creates the session.
|
|
302
|
+
|
|
303
|
+
```tsx
|
|
304
|
+
<WireOnboarding config={config} userId={currentUser?.id} onComplete={persist} />
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
**2. The user registers mid-session.** Change the `userId` prop and the kit emits an `identify` client event that attaches the id to the LIVE session. Nothing else to call; re-render with the new value.
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
const [userId, setUserId] = useState<string | undefined>();
|
|
311
|
+
// ...the user signs up mid-flow:
|
|
312
|
+
setUserId(newUser.id); // the kit fires `identify` and binds the running session
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
**3. The user registers after the flow.** Capture the `contextId` from the `started` (or `resumed`) `onEvent` while onboarding runs, then call `identifyOnboarding(...)` once you have the id. Completion clears the persisted session, so the captured `contextId` is the reliable handle.
|
|
316
|
+
|
|
317
|
+
```tsx
|
|
318
|
+
import { identifyOnboarding } from "wireai-onboarding";
|
|
319
|
+
|
|
320
|
+
const contextIdRef = useRef<string>();
|
|
321
|
+
|
|
322
|
+
<WireOnboarding
|
|
323
|
+
config={config}
|
|
324
|
+
onEvent={(e) => {
|
|
325
|
+
if (e.type === "started" || e.type === "resumed") contextIdRef.current = e.contextId;
|
|
326
|
+
}}
|
|
327
|
+
onComplete={persist}
|
|
328
|
+
/>;
|
|
329
|
+
|
|
330
|
+
// later, once the user registers:
|
|
331
|
+
await identifyOnboarding({ config, userId: newUser.id, contextId: contextIdRef.current });
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
If you passed `storage` to `<WireOnboarding>`, you can omit `contextId` and hand `identifyOnboarding` the same `storage` (plus `appId` or `persistKey`) and it recovers the persisted session id itself. That works while the session is still persisted (before completion clears it), so the captured-`contextId` path above is the safe one post-completion. `identifyOnboarding` is fire-and-forget and never throws; it resolves `true` when it dispatched an `identify` and `false` when it could not (no user id, no server url, or no resolvable session).
|
|
335
|
+
|
|
336
|
+
**No PII.** `userId` is length-bound only; the kit cannot detect an email for you. Keep it opaque, the same rule as `userContext`.
|
|
337
|
+
|
|
338
|
+
## Session mapping (know when a user opens the app again)
|
|
339
|
+
|
|
340
|
+
Onboarding runs once, on the first launch. Session mapping is the other half: every time the
|
|
341
|
+
user opens the app, the kit posts one standard `app.session_started` event so the backend knows
|
|
342
|
+
who came back, when, and for the how-many-th time. That single event does two jobs at once, and
|
|
343
|
+
it needs **no new server endpoint**, because it flows into the SAME event stream the console and
|
|
344
|
+
the decision engines already read.
|
|
345
|
+
|
|
346
|
+
**The contract.** Each app-open posts one event to `POST {serverUrl}/v1/events`:
|
|
347
|
+
|
|
348
|
+
```jsonc
|
|
349
|
+
{
|
|
350
|
+
"event_type": "app_event",
|
|
351
|
+
"question_key": "app.session_started", // the name a trigger matches
|
|
352
|
+
"session_id": "wire_lz1_9fk2a0", // a PER-OPEN id, NOT the onboarding contextId
|
|
353
|
+
"user_id": "u_42", // your opaque id; omitted on a pre-auth open
|
|
354
|
+
"user_context": {
|
|
355
|
+
"device_key": "dev_abc", // stable, non-PII; groups this device's opens
|
|
356
|
+
"session_count": 3, // your local open counter for this open
|
|
357
|
+
"returning": true, // derived from session_count > 1
|
|
358
|
+
"app_version": "1.4.2",
|
|
359
|
+
"platform": "ios"
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
The name is carried as `question_key` on an `app_event` (not as the `event_type`), because that
|
|
365
|
+
is the shape the server matches a trigger's `event` against and the shape it stores in the app.
|
|
366
|
+
namespace. `device_key` rides in the non-PII `user_context` bucket, where the server reads it to
|
|
367
|
+
group a device's sessions. The server fills `app_id` and `environment` from your key, so you
|
|
368
|
+
never send them.
|
|
369
|
+
|
|
370
|
+
**The per-open session id is not the onboarding contextId.** Onboarding owns one context id for
|
|
371
|
+
its one run. Session mapping fires on every open, so it mints its own fresh `session_id` each
|
|
372
|
+
time. Grouping a user or device over time is done by `user_id` and `device_key`, never by
|
|
373
|
+
`session_id`.
|
|
374
|
+
|
|
375
|
+
**Pre-auth to post-auth.** A launch before the user signs in sends `device_key` with no
|
|
376
|
+
`user_id`, so it is a valid device-only session. Once the user authenticates, later opens carry
|
|
377
|
+
both `user_id` and `device_key`, which links the earlier anonymous opens to the user. (The same
|
|
378
|
+
`device_key` also joins the onboarding session once `identifyOnboarding` or a session event
|
|
379
|
+
carries both ids.)
|
|
380
|
+
|
|
381
|
+
**Two ways to wire it. Both are first-class.**
|
|
382
|
+
|
|
383
|
+
If your app already keeps a session counter (Myelino and Morrow do), call the plain function
|
|
384
|
+
from your own "app opened" path and pass the counter value:
|
|
385
|
+
|
|
386
|
+
```tsx
|
|
387
|
+
import { reportSessionStart } from "wireai-onboarding";
|
|
388
|
+
|
|
389
|
+
// In your app-open effect (or wherever you bump your session counter):
|
|
390
|
+
reportSessionStart({
|
|
391
|
+
target: { serverUrl: config.serverUrl, apiKey: config.apiKey },
|
|
392
|
+
userId: currentUser?.id, // omit before auth
|
|
393
|
+
deviceKey, // your stable non-PII device id
|
|
394
|
+
sessionCount, // your local counter value for this open
|
|
395
|
+
appVersion, // optional
|
|
396
|
+
});
|
|
397
|
+
```
|
|
398
|
+
|
|
399
|
+
If you do NOT have a counter, use the hook. It fires on mount and again whenever the app returns
|
|
400
|
+
to the foreground after at least 30 minutes in the background (a real new open, not a quick
|
|
401
|
+
app-switch):
|
|
402
|
+
|
|
403
|
+
```tsx
|
|
404
|
+
import { useSessionStart } from "wireai-onboarding";
|
|
405
|
+
|
|
406
|
+
useSessionStart(
|
|
407
|
+
{ serverUrl: config.serverUrl, apiKey: config.apiKey, appVersion },
|
|
408
|
+
{ userId: currentUser?.id, sessionCount, deviceKey },
|
|
409
|
+
);
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
Both paths are fire-and-forget: they never throw into the UI, never block, and swallow a dead
|
|
413
|
+
endpoint. A once-per-open guard means a re-render can never double-fire the same open.
|
|
414
|
+
|
|
415
|
+
**The payoff.** Because the event lands in the shared stream, the existing decision engines act
|
|
416
|
+
on it with zero extra code:
|
|
417
|
+
|
|
418
|
+
- **Fire the questionnaire on the user's Nth session.** Set the questionnaire's `min_sessions`
|
|
419
|
+
to `N`. The server counts DISTINCT opens grouped by `device_key`, so it fires on the Nth open.
|
|
420
|
+
- **Fire on a signal within one open.** A trigger `{ event: "app.session_started", min_count: 1 }`
|
|
421
|
+
confirms the current open started before the popup shows.
|
|
422
|
+
- **Per-user retention analytics.** The `user_id` plus `session_count` plus `returning` on every
|
|
423
|
+
open is exactly the "when did this user last use the app, and how often" data the console reads.
|
|
424
|
+
|
|
425
|
+
## Testing the flow (no account) — `DemoOnboarding`
|
|
426
|
+
|
|
427
|
+
Re-running onboarding normally needs a fresh signup each time (it only shows for new
|
|
428
|
+
users). For dev/QA, drop in **`DemoOnboarding`** — an on-demand trigger that opens the
|
|
429
|
+
flow in a modal, runs it once, and closes (no loop, no account, no navigation side
|
|
430
|
+
effects). Put it behind `__DEV__` anywhere — a Settings row is typical:
|
|
431
|
+
|
|
432
|
+
```tsx
|
|
433
|
+
import { DemoOnboarding, wireConfigFromEnv } from "wireai-onboarding";
|
|
434
|
+
|
|
435
|
+
{__DEV__ && (
|
|
436
|
+
<DemoOnboarding
|
|
437
|
+
config={wireConfigFromEnv({ appId: "my-app" })} // null when Wire AI isn't configured
|
|
438
|
+
theme={myTheme}
|
|
439
|
+
illustrations={myIllustrations}
|
|
440
|
+
label="Demo onboarding (dev)"
|
|
441
|
+
/>
|
|
442
|
+
)}
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
If `config` is `null` (the app hasn't integrated Wire AI yet) it shows a "not configured"
|
|
446
|
+
hint instead of the flow. By default the demo does **not** persist — pass `onComplete` to
|
|
447
|
+
observe the captured answers, or `renderTrigger={(open) => <YourButton onPress={open} />}`
|
|
448
|
+
to use your own button.
|
|
449
|
+
|
|
450
|
+
## Cards
|
|
451
|
+
|
|
452
|
+
`onboardingComponents` registers: `TextInputCard`, `SelectionCard`,
|
|
453
|
+
`ChipSelectCard`, `NumberStepperCard`, `StatusCard` (terminal recap), and
|
|
454
|
+
**`InterstitialCard`** — a mid-flow momentum/value screen (Duolingo/Cal-AI style):
|
|
455
|
+
an illustration + a line that reflects the user's answers back + a Continue tap.
|
|
456
|
+
It is not a question; the backend emits it once mid-flow.
|
|
457
|
+
|
|
458
|
+
### Illustrations
|
|
459
|
+
|
|
460
|
+
A tenant can configure **multiple** growth images per app (name + description +
|
|
461
|
+
image URL) in the dashboard; the onboarding AI picks the one whose description best
|
|
462
|
+
fits each user, and the backend resolves the chosen name to an `imageUrl`. On the
|
|
463
|
+
device, `InterstitialCard` prefers a host-supplied `illustrations` registry node for
|
|
464
|
+
that name (so an app can ship brand SVG/Lottie the kit never imports), and otherwise
|
|
465
|
+
renders the backend `imageUrl` via RN `<Image>`. Leave a dashboard image's URL empty
|
|
466
|
+
to mean "the app provides this one by name."
|
|
467
|
+
|
|
468
|
+
```tsx
|
|
469
|
+
illustrations={{
|
|
470
|
+
"before-after": <BeforeAfterIllustration width={300} />, // your SVG component
|
|
471
|
+
momentum: <MomentumGlyph />,
|
|
472
|
+
}}
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
## Backend coupling
|
|
476
|
+
|
|
477
|
+
The backend (`wire-rn/examples/dynamic-onboarding/server`) is the source of truth
|
|
478
|
+
for what the agent may emit. To make a new card emittable you must, server-side:
|
|
479
|
+
|
|
480
|
+
1. add it to `app/wire.py` `KNOWN_COMPONENTS` + `COMPONENT_DOCS`, **and**
|
|
481
|
+
2. add its name to the tenant's `allowed_components` (the `register_*.py` script),
|
|
482
|
+
|
|
483
|
+
then `fly deploy`. Registering the card in this kit alone is **not** enough — the
|
|
484
|
+
server validates every emission and replaces unknown components with a safe
|
|
485
|
+
`StatusCard`. (`InterstitialCard` is already wired on both sides.)
|
|
486
|
+
|
|
487
|
+
## Consuming from source (Metro + tsconfig)
|
|
488
|
+
|
|
489
|
+
The monorepo apps resolve the kit from source. In each app:
|
|
490
|
+
|
|
491
|
+
- **`tsconfig.json`** → `"paths": { "wireai-onboarding": ["<rel>/wireai-onboarding/src/index.ts"], "wireai-onboarding/*": ["<rel>/wireai-onboarding/src/*"] }`
|
|
492
|
+
- **`metro.config.js`** → use the `withWireOnboarding` helper with `source` (see
|
|
493
|
+
[Installation → source consumers](#2-source-consumers-your-monorepo-apps)). It
|
|
494
|
+
watches the kit `src`, maps the `wireai-onboarding` specifier to it, and pins
|
|
495
|
+
`react` / `react-native` / `wireai-rn` / `zod` to the app's own `node_modules`
|
|
496
|
+
(a second React instance crashes RN) — the same wiring the apps spell out by hand.
|
|
497
|
+
|
|
498
|
+
Consuming apps wire this in one line via `withWireOnboarding(getDefaultConfig(__dirname))`
|
|
499
|
+
in their `metro.config.js` (npm mode); pass `{ source }` only for local source consumption.
|
|
500
|
+
|
|
501
|
+
## Conventions (for new cards)
|
|
502
|
+
|
|
503
|
+
Per `wire-rn/CONTRIBUTING.md`: `React.memo` + `useCallback`, the submitted-state
|
|
504
|
+
pattern (disable after first interaction), a Zod `propsSchema` with `.describe()`
|
|
505
|
+
on **every** field, a `description` written as an LLM routing instruction, and
|
|
506
|
+
`StyleSheet.create` (no inline styles, no `any`). `tsconfig` is `strict` +
|
|
507
|
+
`noUncheckedIndexedAccess`. Typecheck with `npm run typecheck` (or through a
|
|
508
|
+
consuming app, since the kit has no local TS install).
|
|
509
|
+
|
|
510
|
+
## Coachmarks & guided tours
|
|
511
|
+
|
|
512
|
+
The kit ships a performance-first guided-tour engine on a **subpath** so the main
|
|
513
|
+
barrel stays dependency-free — import it from `wireai-onboarding/coachmarks`. It
|
|
514
|
+
pulls in two **optional** peers only when you use it: `react-native-reanimated`
|
|
515
|
+
(UI-thread gesture + ring animation) and `expo-blur` (the frosted spotlight).
|
|
516
|
+
Source consumers get it through the existing `wireai-onboarding/*` tsconfig path
|
|
517
|
+
mapping; installed apps resolve the subpath export.
|
|
518
|
+
|
|
519
|
+
The split: **the kit owns the animation/blur/ring/measure/queue**, the **app
|
|
520
|
+
declares WHERE things anchor** (a coachmark id → an on-screen target) and **WHICH
|
|
521
|
+
tour plays**. AI-chosen ordering is a later, drop-in phase (see the feature map
|
|
522
|
+
below) — nothing to rewire.
|
|
523
|
+
|
|
524
|
+
**1. Mount the provider once, around your `NavigationContainer`.** It renders the
|
|
525
|
+
overlay host as a root sibling so a ring can paint _above_ the bottom tab bar
|
|
526
|
+
(which react-navigation draws over screen components). It takes a **synchronous**
|
|
527
|
+
storage adapter — gate reads must not be async or a ring flashes before the
|
|
528
|
+
"already seen" check resolves. An MMKV wrapper is three lines:
|
|
529
|
+
|
|
530
|
+
```tsx
|
|
531
|
+
import { CoachmarkProvider } from "wireai-onboarding/coachmarks";
|
|
532
|
+
import { storage } from "./mmkv"; // your MMKV instance
|
|
533
|
+
|
|
534
|
+
const coachmarkStorage = {
|
|
535
|
+
getItem: (k: string) => storage.getString(k) ?? null,
|
|
536
|
+
setItem: (k: string, v: string) => storage.set(k, v),
|
|
537
|
+
};
|
|
538
|
+
|
|
539
|
+
<CoachmarkProvider
|
|
540
|
+
storage={coachmarkStorage}
|
|
541
|
+
accentColor="#3BA9AE" // defaults to the onboarding theme's `primary`
|
|
542
|
+
isTestingCoachmark={__DEV__} // QA flag — see below
|
|
543
|
+
>
|
|
544
|
+
<NavigationContainer>{/* … */}</NavigationContainer>
|
|
545
|
+
</CoachmarkProvider>;
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
**2. Make any element ringable with `useCoachmarkAnchor(id)`.** Attach the ref to
|
|
549
|
+
a plain `View` wrapping the target (`collapsable={false}` so the native node
|
|
550
|
+
survives measurement). Pass `null` to register nothing (e.g. only the first row
|
|
551
|
+
of a list):
|
|
552
|
+
|
|
553
|
+
```tsx
|
|
554
|
+
import { useCoachmarkAnchor } from "wireai-onboarding/coachmarks";
|
|
555
|
+
|
|
556
|
+
const groupTab = useCoachmarkAnchor("group_tab");
|
|
557
|
+
<View ref={groupTab} collapsable={false}>{tabButton}</View>;
|
|
558
|
+
```
|
|
559
|
+
|
|
560
|
+
**3. Play an ordered tour with `useCoachmarkTour(steps, options)`.** Gating lives
|
|
561
|
+
in the kit — it reads/writes `wire_coachmark_<tourId>_seen` through the injected
|
|
562
|
+
storage, so the app keeps only its own domain gates. Steps must be a memoized
|
|
563
|
+
array; step callbacks are held in a ref (parent re-renders never re-fire a step):
|
|
564
|
+
|
|
565
|
+
```tsx
|
|
566
|
+
import { useCoachmarkTour } from "wireai-onboarding/coachmarks";
|
|
567
|
+
|
|
568
|
+
const steps = useMemo(
|
|
569
|
+
() => [
|
|
570
|
+
{ id: "feed_scroll", message: "Swipe to explore.", gesture: "swipe_up" },
|
|
571
|
+
{ id: "group_tab", message: "Your groups live here.", anchorId: "group_tab",
|
|
572
|
+
gesture: "tap", onEngage: () => navigation.navigate("Groups") },
|
|
573
|
+
],
|
|
574
|
+
[navigation],
|
|
575
|
+
);
|
|
576
|
+
|
|
577
|
+
useCoachmarkTour(steps, {
|
|
578
|
+
tourId: "home_tour", // gate key + analytics prefix
|
|
579
|
+
enabled: hasPosts, // domain gate — arm only when it makes sense
|
|
580
|
+
showOnce: true, // default; persisted via the gate
|
|
581
|
+
startDelayMs: 3000, // let passive UI land first
|
|
582
|
+
onComplete: () => {},
|
|
583
|
+
onStepShown: (id) => analytics.track("coachmark_shown", { id }),
|
|
584
|
+
onStepEngaged: (id) => analytics.track("coachmark_engaged", { id }),
|
|
585
|
+
onStepDismissed: (id) => analytics.track("coachmark_dismissed", { id }),
|
|
586
|
+
});
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
Analytics stay callback-based (no analytics dep in the kit). Reduce Motion is
|
|
590
|
+
honored (the gesture glyph renders static, no motion loop); the blur mounts only while a step is visible and
|
|
591
|
+
costs nothing at rest; only one overlay is ever active.
|
|
592
|
+
|
|
593
|
+
**Dismissal advances — it doesn't quit.** Tapping the ring or tooltip engages the step (fires `onStepEngaged`) and moves on; tapping the dimmed backdrop advances to the **next** step (fires `onStepDismissed`) instead of killing the tour, and only a backdrop tap on the **last** step ends it. A stray backdrop tap mid-tour never drops the remaining steps.
|
|
594
|
+
|
|
595
|
+
**QA replay — `isTestingCoachmark`.** Set it `true` on the provider (or call
|
|
596
|
+
`setCoachmarkTesting(true)`) and every seen-gate reads as unseen while every
|
|
597
|
+
seen-write is suppressed — so every tour _and_ showcase replays on each mount.
|
|
598
|
+
One boolean re-sees the whole surface.
|
|
599
|
+
|
|
600
|
+
### The feature map (declare it now; AI chooses from it later)
|
|
601
|
+
|
|
602
|
+
Declare your app's coachmarks as a single **catalog** — the anchor ids, the copy,
|
|
603
|
+
and where each lives. Today the whole catalog plays in declared order. Later, the
|
|
604
|
+
Wire server emits an ordered `coachmarks: string[]` id list chosen by the user's
|
|
605
|
+
captured intent; you pass it straight to `selectTourSteps(catalog, selection)` and
|
|
606
|
+
nothing else changes — same contract, AI is a drop-in:
|
|
607
|
+
|
|
608
|
+
```ts
|
|
609
|
+
import { selectTourSteps } from "wireai-onboarding/coachmarks";
|
|
610
|
+
|
|
611
|
+
// catalog = your feature map; each entry has a stable `id`
|
|
612
|
+
const catalog = [
|
|
613
|
+
{ id: "feed_scroll", message: "Swipe to explore.", gesture: "swipe_up" },
|
|
614
|
+
{ id: "post_location", message: "Tap a place to open it in Maps.", anchorId: "post_location", gesture: "tap" },
|
|
615
|
+
{ id: "group_tab", message: "Your groups live here.", anchorId: "group_tab", gesture: "tap" },
|
|
616
|
+
];
|
|
617
|
+
|
|
618
|
+
const steps = selectTourSteps(catalog); // Phase 1-2: full, declared order
|
|
619
|
+
const aiSteps = selectTourSteps(catalog, plan.coachmarks); // Phase 3: AI subset + order
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
`selectTourSteps` returns the catalog unchanged when `selection` is absent, and
|
|
623
|
+
filters + orders it by the selection ids when present (unknown ids skipped,
|
|
624
|
+
duplicates ignored).
|
|
625
|
+
|
|
626
|
+
## Feature showcase (the personalized value bridge)
|
|
627
|
+
|
|
628
|
+
`wireai-onboarding/showcase` wraps [`@blazejkustra/react-native-onboarding`](https://github.com/software-mansion-labs/react-native-onboarding)
|
|
629
|
+
(the **optional** peer) as a small deck of declarative slides. You supply a
|
|
630
|
+
`ShowcaseConfig`; the kit maps it to the package's API, bakes in the onboarding
|
|
631
|
+
theme colors, and gates once through the same storage as the tours
|
|
632
|
+
(`wire_showcase_<id>_seen`; `isTestingCoachmark` bypasses it). If already seen it
|
|
633
|
+
renders nothing and calls `onDone` from an effect.
|
|
634
|
+
|
|
635
|
+
**Recommended placement (v0.3.0+): AFTER `onComplete`, as a personalized value
|
|
636
|
+
bridge.** The user just told you what they want in `result.answers`, so the
|
|
637
|
+
terminal recap is the highest-value slot to show the 2-3 slides that answer it,
|
|
638
|
+
ordered most-relevant first ("you said you want X, here's how the app does X", the
|
|
639
|
+
Duolingo / Headspace pattern). Select them from the answers with
|
|
640
|
+
`selectShowcaseSlides`:
|
|
641
|
+
|
|
642
|
+
```tsx
|
|
643
|
+
import { FeatureShowcase, selectShowcaseSlides } from "wireai-onboarding/showcase";
|
|
644
|
+
|
|
645
|
+
// The app declares the full slide catalog once (stable ids):
|
|
646
|
+
const catalog = [
|
|
647
|
+
{ id: "feed", title: "Autoplay feed", description: "Swipe through places.", image: require("./intro/feed.png") },
|
|
648
|
+
{ id: "groups", title: "Groups", description: "Plan together.", image: require("./intro/groups.png") },
|
|
649
|
+
{ id: "saved", title: "Saved", description: "Everything you liked, in one place.", image: require("./intro/saved.png") },
|
|
650
|
+
];
|
|
651
|
+
|
|
652
|
+
// Sequence: WireOnboarding → onComplete(result) → select from answers → FeatureShowcase → app
|
|
653
|
+
<WireOnboarding
|
|
654
|
+
onComplete={(result) => {
|
|
655
|
+
persist(result.answers);
|
|
656
|
+
setSlides(selectShowcaseSlides(catalog, idsFromAnswers(result.answers)));
|
|
657
|
+
}}
|
|
658
|
+
/>;
|
|
659
|
+
|
|
660
|
+
<FeatureShowcase
|
|
661
|
+
config={{ id: "app_feature_showcase", slides }}
|
|
662
|
+
accentColor="#3BA9AE"
|
|
663
|
+
onDone={() => goToApp()}
|
|
664
|
+
/>;
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
`selectShowcaseSlides(catalog, selection?)` mirrors `selectTourSteps` exactly:
|
|
668
|
+
returns the catalog unchanged when `selection` is absent (full deck, declared
|
|
669
|
+
order), and filters + orders it by the selection ids when present (unknown ids
|
|
670
|
+
skipped, duplicates ignored). `idsFromAnswers` is the host's own pure map from its
|
|
671
|
+
answer keys/values to slide ids. Keep it to 2-3, and fall back to the full deck
|
|
672
|
+
when answers are thin.
|
|
673
|
+
|
|
674
|
+
**No `gesture` props on showcase slides.** Swipe/tap hints belong to the coachmark
|
|
675
|
+
engine, delivered just-in-time when the user is actually on the screen (the tour
|
|
676
|
+
already ships the two-finger glyph). A slide in the value bridge is a still: copy
|
|
677
|
+
plus an `image` (strongly recommended; a slide without one falls back to a
|
|
678
|
+
transparent placeholder).
|
|
679
|
+
|
|
680
|
+
Pre-onboarding placement (the deck _before_ `WireOnboarding`) is still fully
|
|
681
|
+
supported. Placement is host-controlled and the helper is additive; it's just no
|
|
682
|
+
longer the recommended shape, because a generic pre-onboarding intro contradicts
|
|
683
|
+
the "personalize from context" thesis. The once-gate
|
|
684
|
+
(`wire_showcase_<id>_seen`) works **identically** in either placement.
|
|
685
|
+
|
|
686
|
+
It reads the active `OnboardingThemeProvider` theme; pass `theme` to override just
|
|
687
|
+
the showcase, or `storage` / `isTesting` to gate independently of the provider.
|
|
688
|
+
|
|
689
|
+
**v2 server seam (future, not built).** The onboarding server's final turn will
|
|
690
|
+
later return an ordered `showcase: string[]` slide-key list chosen server-side from
|
|
691
|
+
the same answers; the host passes it straight in as the `selection` argument.
|
|
692
|
+
Same catalog, same contract, AI is a drop-in. The field is additive, so kits that
|
|
693
|
+
don't read it simply ignore it (backward-safe).
|
|
694
|
+
|
|
695
|
+
## In-app reviews (sentiment gate)
|
|
696
|
+
|
|
697
|
+
`wireai-onboarding/reviews` is the drop-in in-app review flow. It asks one neutral
|
|
698
|
+
question, then splits honestly on the rating:
|
|
699
|
+
|
|
700
|
+
- **5 stars** route to the native store review. It calls the **optional** peer
|
|
701
|
+
`expo-store-review` (`StoreReview.requestReview()` when `isAvailableAsync`), and falls
|
|
702
|
+
back to opening a configurable store URL (`{ iosAppId, androidPackage, storeUrl }`) when
|
|
703
|
+
the peer is absent. Fires `store_review_requested`.
|
|
704
|
+
- **1 to 4 stars** open a feather-light feedback form (one free-text line plus an optional
|
|
705
|
+
contact). The text is POSTed to `POST /v1/reviews`. The gate always ends on a warm
|
|
706
|
+
thank-you, never a dead end.
|
|
707
|
+
|
|
708
|
+
By default the prompt presents as a **centered popup**: a card in the middle of the screen
|
|
709
|
+
over a dimmed backdrop, with a spring/fade entrance (RN's built-in `Animated` on the native
|
|
710
|
+
driver, no reanimated). A backdrop tap (or Android back) dismisses it, counting as the same
|
|
711
|
+
resolve path `onResolved` already tracks. Pass `presentation="inline"` for the legacy bare
|
|
712
|
+
card that renders wherever you mount it. Everything is theme-aware, and `presentation="modal"`
|
|
713
|
+
is the default so no prop change is needed to adopt it.
|
|
714
|
+
|
|
715
|
+
Analytics are callback-based, the same convention as the coachmarks. Wire `onEvent` to your
|
|
716
|
+
own analytics; the four moments are `review_prompt_shown`, `review_rating_selected` (carries
|
|
717
|
+
`stars`), `store_review_requested`, and `review_feedback_submitted`. No PII rides in events.
|
|
718
|
+
Feedback text goes only in the POST body.
|
|
719
|
+
|
|
720
|
+
```tsx
|
|
721
|
+
import { ReviewGate, useReviewGate } from "wireai-onboarding/reviews";
|
|
722
|
+
|
|
723
|
+
// The gate owns WHEN to show. v1 uses the local rules below; pass a server `decision`
|
|
724
|
+
// object and it overrides them (the AI seam, see below).
|
|
725
|
+
const gate = useReviewGate({
|
|
726
|
+
config: { id: "post_onboarding", minSessions: 2, cooldownDays: 90, appVersion: "1.4.0" },
|
|
727
|
+
events, // your tracked app-event count, for the minEvents rule
|
|
728
|
+
});
|
|
729
|
+
|
|
730
|
+
return gate.visible ? (
|
|
731
|
+
<ReviewGate
|
|
732
|
+
appName="Driveline Fit"
|
|
733
|
+
store={{ iosAppId: "123456789", androidPackage: "com.driveline.fit" }}
|
|
734
|
+
target={{ serverUrl: SERVER_URL, apiKey: TENANT_KEY }}
|
|
735
|
+
sessionId={sessionId}
|
|
736
|
+
onEvent={(e) => analytics.track(e.name, { id: e.id, stars: e.stars })}
|
|
737
|
+
onShown={gate.markShown}
|
|
738
|
+
onResolved={gate.markResolved}
|
|
739
|
+
/>
|
|
740
|
+
) : null;
|
|
741
|
+
```
|
|
742
|
+
|
|
743
|
+
### The decision seam (server-controlled firing, AI later)
|
|
744
|
+
|
|
745
|
+
`useReviewGate` decides locally today, but the decision is a seam, the same idea as
|
|
746
|
+
`selectTourSteps`. Pass a `decision` object (`{ fire, reason }`) and it overrides the local
|
|
747
|
+
rules. The Wire server already computes exactly this at `GET /v1/reviews/decision`, evaluating
|
|
748
|
+
per-app trigger rules against the app's event stream, and later a learnings-driven evaluator
|
|
749
|
+
replaces the internals behind the same contract. Building the seam now is the whole cost of
|
|
750
|
+
being AI-ready.
|
|
751
|
+
|
|
752
|
+
The firing chain, in order: a server `decision` wins immediately, then after
|
|
753
|
+
`timeoutFallbackMs` the local rules take over (the timeout is a fallback only), and the
|
|
754
|
+
once-per-version gate (`wire_review_<id>_seen`) always applies locally so a server bug can
|
|
755
|
+
never spam the prompt. QA replay reuses the coachmark testing flag: `isTestingCoachmark`
|
|
756
|
+
force-shows the gate.
|
|
757
|
+
|
|
758
|
+
### Report generic app events (what the triggers evaluate on)
|
|
759
|
+
|
|
760
|
+
`reportAppEvent(target, "content_share", { sessionId, deviceKey })` reports an arbitrary
|
|
761
|
+
in-app event through the existing events transport. The server stores it in the `app.`
|
|
762
|
+
namespace and the review triggers evaluate on it. Keep the name a short stable id and any
|
|
763
|
+
`meta` small and non-PII. This is the seam that lets Wire sit on top of the app's event
|
|
764
|
+
stream over time.
|
|
765
|
+
|
|
766
|
+
### Store policy (read before you ship)
|
|
767
|
+
|
|
768
|
+
Apple's `requestReview` is quota'd (about 3 per year) and is never guaranteed to show, so
|
|
769
|
+
call it only on the honest 5-star path, never from a button labeled "rate us 5 stars". The
|
|
770
|
+
sentiment question itself stays neutral ("How's your experience?") with honest options.
|
|
771
|
+
Android's in-app review has similar quotas. The kit follows all of this by construction: the
|
|
772
|
+
5-star tap is the only path that reaches `requestStoreReview`.
|
|
773
|
+
|
|
774
|
+
### What is next
|
|
775
|
+
|
|
776
|
+
The next retention module reuses this exact server-directed shape:
|
|
777
|
+
`GET /v1/announcements/decision` returns which announcement to show per user or session
|
|
778
|
+
(AI-targeted later, for example announcing the fix for a feature a user complained about),
|
|
779
|
+
with the client once-gating per announcement id through the same storage. Same seam, no code
|
|
780
|
+
yet.
|
|
781
|
+
|
|
782
|
+
## Feature controls (per-module kill switches)
|
|
783
|
+
|
|
784
|
+
Every activation surface has a switch you can flip from the dashboard "in case of something":
|
|
785
|
+
coachmarks, showcase, onboarding mode, in-app review, and announcements (reserved). The kit
|
|
786
|
+
reads them from `GET /v1/features` (tenant key, `Cache-Control: private, max-age=300`) and gates
|
|
787
|
+
the three client surfaces accordingly. The server owns the onboarding branch (see "Onboarding
|
|
788
|
+
mode" below), so the kit needs no gating there.
|
|
789
|
+
|
|
790
|
+
**The fail-open contract (the whole point).** The kit fetches the flags once per session and
|
|
791
|
+
caches the last-known value through your `storage`. If the endpoint is unreachable, returns 401,
|
|
792
|
+
returns a 5xx, times out, or sends malformed JSON, the kit reuses its last cached value, and with
|
|
793
|
+
no cache it falls back to the all-on defaults (every module on, onboarding `ai`). A control-plane
|
|
794
|
+
outage can only ever be more permissive than the dashboard intends. **It can never dark your app.**
|
|
795
|
+
No failure path ever throws into the host.
|
|
796
|
+
|
|
797
|
+
### Zero setup is a valid setup
|
|
798
|
+
|
|
799
|
+
Every gated surface works with no flags at all. Omit the wiring and each surface stays on
|
|
800
|
+
(defaults), so a host that never adopts flags sees zero behavior change and makes zero extra
|
|
801
|
+
network calls.
|
|
802
|
+
|
|
803
|
+
### One fetch for the whole tree (optional)
|
|
804
|
+
|
|
805
|
+
Mount `WireFeaturesProvider` once near your root and every gated surface below it reads the same
|
|
806
|
+
resolved flags from context (one fetch serves all modules):
|
|
807
|
+
|
|
808
|
+
```tsx
|
|
809
|
+
import { WireFeaturesProvider } from "wireai-onboarding";
|
|
810
|
+
|
|
811
|
+
<WireFeaturesProvider config={{ serverUrl, apiKey, appId, storage }}>
|
|
812
|
+
<App />
|
|
813
|
+
</WireFeaturesProvider>;
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
Resolution order at each surface: an explicit `features` prop, then the provider context, then a
|
|
817
|
+
lazy fetch from a `featuresConfig` (serverUrl + apiKey) you pass to that surface, then the all-on
|
|
818
|
+
defaults. So you can wire flags globally (provider), per surface (`featuresConfig`), or by handing
|
|
819
|
+
already-resolved flags straight in (`features`). When you consume the built npm subpaths as
|
|
820
|
+
separate bundles, prefer the explicit `features`/`featuresConfig` props (React context is shared
|
|
821
|
+
across subpaths in the source-consumption path, not necessarily across independently-bundled
|
|
822
|
+
entries).
|
|
823
|
+
|
|
824
|
+
### Per-surface semantics
|
|
825
|
+
|
|
826
|
+
- **Coachmarks** (`CoachmarkProvider`): `coachmarks.enabled === false` makes every tour and
|
|
827
|
+
overlay `show()` request a silent no-op. Nothing paints, no error is thrown, and the
|
|
828
|
+
once-per-tour "seen" gates are **not** consumed, so re-enabling replays the tours correctly.
|
|
829
|
+
- **Showcase** (`FeatureShowcase`): `showcase.enabled === false` renders nothing and calls
|
|
830
|
+
`onDone()` from an effect, exactly as if the showcase had completed. This is the semantics
|
|
831
|
+
chosen deliberately so a disabled showcase can never strand a navigation flow that waits on a
|
|
832
|
+
screen which will not appear. It does not write the "seen" gate, so re-enabling replays it.
|
|
833
|
+
- **Reviews** (`useReviewGate` / `ReviewGate`): `review.enabled === false` means the gate never
|
|
834
|
+
fires (`visible` stays false), whatever the local rules or a server decision say. It composes as
|
|
835
|
+
an extra master switch on top of the existing decision seam.
|
|
836
|
+
|
|
837
|
+
```tsx
|
|
838
|
+
// Any surface can also take a config directly (lazy fetch), no provider required:
|
|
839
|
+
<FeatureShowcase config={showcase} onDone={next} featuresConfig={{ serverUrl, apiKey, appId, storage }} />
|
|
840
|
+
|
|
841
|
+
const gate = useReviewGate({ config: { id: "post_task" }, features }); // or pass resolved flags
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
### Onboarding mode (server-owned, no kit gating)
|
|
845
|
+
|
|
846
|
+
`onboarding.mode` is `ai` | `static` | `off`, and the server owns all three over the same A2A
|
|
847
|
+
contract, so the kit needs nothing for it:
|
|
848
|
+
|
|
849
|
+
- `ai`: the LLM-generated adaptive flow (default).
|
|
850
|
+
- `static`: the server serves a predefined scripted flow with zero LLM calls, over the same card
|
|
851
|
+
contract the kit already parses. The kit renders it exactly like the AI flow.
|
|
852
|
+
- `off`: the server returns a terminal StatusCard on the first turn, which the kit already reads
|
|
853
|
+
as completion (the user drops into the app in one tap). This folds in the legacy per-tenant
|
|
854
|
+
`onboarding_enabled` kill switch.
|
|
855
|
+
|
|
856
|
+
You can still read `onboarding.mode` from the resolved flags if you want to branch host UI, but no
|
|
857
|
+
gating is required in the kit.
|
|
858
|
+
|
|
859
|
+
## Agent tooling — MCP server + the `wire-ai` skill
|
|
860
|
+
|
|
861
|
+
Two extras in this repo help a coding agent (Claude Code, Cursor, Codex) integrate and
|
|
862
|
+
then *improve* Wire AI, not just eyeball it.
|
|
863
|
+
|
|
864
|
+
### MCP server (`mcp/`)
|
|
865
|
+
|
|
866
|
+
A local Model Context Protocol server (`@wireai/mcp`, stdio, TypeScript). It is a
|
|
867
|
+
repo-local dev tool — **not** published to npm (excluded from the package `files`
|
|
868
|
+
allowlist). Build it in place:
|
|
869
|
+
|
|
870
|
+
```bash
|
|
871
|
+
cd mcp && npm install && npm run build
|
|
872
|
+
```
|
|
873
|
+
|
|
874
|
+
Then add it to your agent client's MCP config:
|
|
875
|
+
|
|
876
|
+
```json
|
|
877
|
+
{
|
|
878
|
+
"mcpServers": {
|
|
879
|
+
"wireai": {
|
|
880
|
+
"command": "node",
|
|
881
|
+
"args": ["<abs-path>/wireai-onboarding/mcp/dist/index.js"],
|
|
882
|
+
"env": {
|
|
883
|
+
"WIREAI_SERVER_URL": "https://<your-backend>.fly.dev",
|
|
884
|
+
"WIREAI_API_KEY": "wai_<tenant-key>",
|
|
885
|
+
"WIREAI_ADMIN_KEY": "wai_admin_<operator-key>"
|
|
886
|
+
}
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
```
|
|
891
|
+
|
|
892
|
+
Tools (P1 is read-biased; the only writes are the existing register/update):
|
|
893
|
+
|
|
894
|
+
| Tool | Auth key | Does |
|
|
895
|
+
|---|---|---|
|
|
896
|
+
| `wireai_get_integration_guide` | none | the current integration recipe |
|
|
897
|
+
| `wireai_list_components` | none | the card vocabulary |
|
|
898
|
+
| `wireai_get_insights` | tenant `WIREAI_API_KEY` | **read** the funnel brief (`/v1/insights.md`), app resolved from the key; a valid "no data yet" brief when empty |
|
|
899
|
+
| `wireai_get_onboarding_config` | admin `WIREAI_ADMIN_KEY` | **read** the live generation config (context, first question, allowed components) |
|
|
900
|
+
| `wireai_register_onboarding_app` | admin `WIREAI_ADMIN_KEY` | create an app + get its key |
|
|
901
|
+
| `wireai_list_onboarding_apps` | admin `WIREAI_ADMIN_KEY` | list apps |
|
|
902
|
+
| `wireai_get_onboarding_app` | admin `WIREAI_ADMIN_KEY` | one app's raw record |
|
|
903
|
+
| `wireai_update_onboarding_app` | admin `WIREAI_ADMIN_KEY` | patch config, no release |
|
|
904
|
+
| `wireai_get_onboarding_analytics` | admin `WIREAI_ADMIN_KEY` | funnel JSON |
|
|
905
|
+
| `wireai_list_reviews` | admin `WIREAI_ADMIN_KEY` | **read** in-app reviews across tenants (stars, feedback, contact) |
|
|
906
|
+
|
|
907
|
+
Least privilege: for the read-only loop, hand the agent just `WIREAI_SERVER_URL` +
|
|
908
|
+
`WIREAI_API_KEY` (tenant) and skip the admin key. Full detail: [`mcp/README.md`](./mcp/README.md).
|
|
909
|
+
|
|
910
|
+
### `wire-ai` skill (`.claude/skills/wire-ai`)
|
|
911
|
+
|
|
912
|
+
A pure-Markdown Claude skill for *using* Wire AI easily: install the kit + wire the
|
|
913
|
+
`WireOnboarding` provider, read your insights (via the MCP or `/v1/insights.md`), and run
|
|
914
|
+
the improve loop (read insights, then adjust the app description / first question /
|
|
915
|
+
generation context). It also carries the `BOILERPLATE20` credit note. For the full
|
|
916
|
+
scaffold-the-screen integration job, the [`wire-rn-integration`](./INTEGRATION_PROMPT.md)
|
|
917
|
+
skill drives it end to end.
|
|
918
|
+
|
|
919
|
+
## More from Code Meet AI
|
|
920
|
+
|
|
921
|
+
**Open source:** [wireai-rn](https://github.com/chohra-med/wireai-rn) · [expo_boilerplate](https://github.com/chohra-med/expo_boilerplate) · [colorway-c-brand](https://github.com/chohra-med/colorway-c-brand) · [claude_design_skill](https://github.com/chohra-med/claude_design_skill)
|
|
922
|
+
**Products:** [AI Mobile Launcher](https://aimobilelauncher.com) · [AI Web Launcher](https://aiweblauncher.com) · [Wire AI](https://getwireai.com) · [CasaInnov](https://casainnov.com)
|
|
923
|
+
**Follow:** [Newsletter](https://codemeetai.substack.com) · [YouTube](https://youtube.com/@codemeetai)
|