@wireai/activation 0.13.0 → 0.13.3
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 +7 -4
- package/CHANGELOG.md +341 -4
- package/README.md +98 -2
- package/dist/analytics/index.d.mts +4 -4
- package/dist/analytics/index.d.ts +4 -4
- package/dist/analytics/index.js +164 -471
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +162 -469
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/coachmarks/index.d.mts +14 -1
- package/dist/coachmarks/index.d.ts +14 -1
- package/dist/coachmarks/index.js +58 -14
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs +58 -14
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{currentSession-_GynvhzT.d.mts → currentSession-BoWtr3Jp.d.mts} +299 -14
- package/dist/{currentSession-D7zabMXK.d.ts → currentSession-CCOMlaQ2.d.ts} +299 -14
- package/dist/{decision-Bl_M2y3r.d.mts → decision-Cjw0jbEj.d.mts} +1 -1
- package/dist/{decision-Cau5KmP6.d.ts → decision-GfpXAsk_.d.ts} +1 -1
- package/dist/index.d.mts +218 -4
- package/dist/index.d.ts +218 -4
- package/dist/index.js +1104 -888
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +887 -692
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +42 -10
- package/dist/questionnaire/index.d.ts +42 -10
- package/dist/questionnaire/index.js +45 -13
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +45 -13
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +4 -4
- package/dist/reviews/index.d.ts +4 -4
- package/dist/reviews/index.js +39 -8
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +39 -8
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.js +7 -2
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs +7 -2
- package/dist/showcase/index.mjs.map +1 -1
- package/dist/{transport-DsRe4epC.d.ts → transport-CefdERPs.d.mts} +44 -5
- package/dist/{transport-CF_eHwzC.d.mts → transport-DFuPyCoJ.d.ts} +44 -5
- package/dist/{types-Buj9Lw9t.d.ts → types-UVYt9BJe.d.ts} +41 -1
- package/dist/{types-CNUqMK0D.d.mts → types-l2tfg23P.d.mts} +41 -1
- package/package.json +4 -1
- package/src/OnboardingFlow.tsx +175 -15
- package/src/WireOnboarding.tsx +70 -8
- package/src/activation/wireActivation.ts +3 -3
- package/src/analytics/analyticsEvent.ts +16 -1
- package/src/analytics/analyticsFacade.ts +2 -2
- package/src/analytics/eventQueue.ts +85 -20
- package/src/analytics/reportClientEvent.ts +68 -12
- package/src/cards/CardGridSelectCard.tsx +1 -1
- package/src/cards/ChipSelectCard.tsx +2 -2
- package/src/cards/PermissionCard.tsx +438 -0
- package/src/cards/SelectionCard.tsx +1 -1
- package/src/cards/index.ts +7 -0
- package/src/coachmarks/runtime.ts +62 -2
- package/src/coachmarks/useCoachmarkTour.ts +62 -14
- package/src/context/deviceId.ts +4 -4
- package/src/features/WireFeaturesProvider.tsx +4 -1
- package/src/identity/userIdentity.ts +1 -1
- package/src/illustrations/defaultIllustrations.tsx +44 -3
- package/src/index.ts +38 -0
- package/src/permissions/index.ts +64 -0
- package/src/permissions/permissionCopy.ts +87 -0
- package/src/permissions/permissionEvents.ts +76 -0
- package/src/permissions/permissionMemory.ts +88 -0
- package/src/permissions/placement.ts +88 -0
- package/src/permissions/types.ts +131 -0
- package/src/questionnaire/QuestionnaireGate.tsx +90 -10
- package/src/questionnaire/index.ts +9 -1
- package/src/questionnaire/transport.ts +46 -11
- package/src/reviews/ReviewGate.tsx +61 -6
- package/src/reviews/index.ts +10 -1
- package/src/reviews/transport.ts +58 -12
- package/src/session/persistedSession.ts +10 -3
- package/src/session-analytics/useLifecycleEvents.ts +1 -1
- package/src/theme/ThemeContext.tsx +42 -1
- package/src/types.ts +66 -4
- package/src/utils/submitResult.ts +39 -0
|
@@ -3,12 +3,53 @@
|
|
|
3
3
|
* themed card via `useOnboardingTheme()`. WHY a context rather than prop-drilling:
|
|
4
4
|
* the cards are mounted by the SDK's ComponentRenderer (not by us), so we can't
|
|
5
5
|
* pass them props directly — they must read the theme from context.
|
|
6
|
+
*
|
|
7
|
+
* ── WHY THE CONTEXT LIVES ON A `Symbol.for` SLOT ───────────────────────────────────────────────
|
|
8
|
+
*
|
|
9
|
+
* It was a plain `createContext(...)`, and tsup inlines this module into FIVE dist bundles
|
|
10
|
+
* (`.`, `./reviews`, `./questionnaire`, `./coachmarks`, `./showcase`) — so each one carried its own
|
|
11
|
+
* context instance. `OnboardingThemeProvider` is exported ONLY from the root barrel, so on any
|
|
12
|
+
* `dist` resolution path (plain RN CLI, `unstable_enablePackageExports: false`, node, jest, SSR) a
|
|
13
|
+
* host that wrapped its tree in `<OnboardingThemeProvider>` and rendered `<ReviewGate>` from
|
|
14
|
+
* `@wireai/activation/reviews` was writing one context and reading another. Nothing crashed and
|
|
15
|
+
* nothing warned: `useOnboardingTheme`'s `ctx ?? mergeTheme()` fallback quietly served the NEUTRAL
|
|
16
|
+
* default theme, so the tenant's brand simply did not apply to the gate. On-device this was masked
|
|
17
|
+
* only because Metro's `react-native` export condition collapses every subpath back to this one
|
|
18
|
+
* `src/` file — the same "bundler accident, not a guarantee" the `currentSession` header names, and
|
|
19
|
+
* the same root cause as the optional-peers bug 0.13.3 fixed.
|
|
20
|
+
*
|
|
21
|
+
* The bundler-agnostic fix, exactly as `coachmarks/runtime`, `analytics/currentSession`,
|
|
22
|
+
* `context/deviceId` and `features/WireFeaturesProvider` already do it: park the ONE context on a
|
|
23
|
+
* well-known `globalThis` slot keyed by `Symbol.for(...)`. `Symbol.for` uses the runtime-global
|
|
24
|
+
* symbol registry, so every inlined copy of this module resolves the SAME symbol, finds the slot
|
|
25
|
+
* already filled, and reuses that instance — one context identity however many copies exist.
|
|
26
|
+
* `globalThis` is present and identical in Hermes/React Native, Node and SSR (we never touch
|
|
27
|
+
* `window`), so this is safe on every host. `test/canary/themeContextIdentity.test.tsx` measures it
|
|
28
|
+
* in the BUILT artifact, because the defect is invisible in source.
|
|
6
29
|
*/
|
|
7
30
|
import React, { createContext, useContext, useMemo } from "react";
|
|
8
31
|
import { mergeTheme } from "./defaultTheme";
|
|
9
32
|
import type { OnboardingTheme } from "./types";
|
|
10
33
|
|
|
11
|
-
|
|
34
|
+
/**
|
|
35
|
+
* Well-known key into the runtime-global symbol registry. `Symbol.for` (NOT a plain `Symbol()`) is
|
|
36
|
+
* what makes this cross-bundle: it returns the SAME symbol for the same string across every copy of
|
|
37
|
+
* this module, so duplicated inlined copies all address one slot.
|
|
38
|
+
*/
|
|
39
|
+
const THEME_CONTEXT_SLOT: unique symbol = Symbol.for("@wireai/activation:themeContext");
|
|
40
|
+
|
|
41
|
+
type GlobalWithThemeContext = typeof globalThis & {
|
|
42
|
+
[THEME_CONTEXT_SLOT]?: React.Context<OnboardingTheme | null>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const themeGlobal = globalThis as GlobalWithThemeContext;
|
|
46
|
+
|
|
47
|
+
// First copy to load creates the context; every later copy adopts it. Never overwrite an existing
|
|
48
|
+
// slot — a second `createContext` here would re-split the identity this guard exists to keep whole.
|
|
49
|
+
if (!themeGlobal[THEME_CONTEXT_SLOT]) {
|
|
50
|
+
themeGlobal[THEME_CONTEXT_SLOT] = createContext<OnboardingTheme | null>(null);
|
|
51
|
+
}
|
|
52
|
+
const OnboardingThemeContext = themeGlobal[THEME_CONTEXT_SLOT] as React.Context<OnboardingTheme | null>;
|
|
12
53
|
|
|
13
54
|
export type OnboardingThemeProviderProps = {
|
|
14
55
|
/** Partial overrides merged over the neutral default theme. */
|
package/src/types.ts
CHANGED
|
@@ -4,6 +4,12 @@
|
|
|
4
4
|
import type { Message } from "wireai-rn";
|
|
5
5
|
import type { OnboardingTheme } from "./theme/types";
|
|
6
6
|
import type { WireOnboardingStorage } from "./session/persistedSession";
|
|
7
|
+
import type {
|
|
8
|
+
PermissionScreenConfig,
|
|
9
|
+
PermissionStage,
|
|
10
|
+
WirePermissionKind,
|
|
11
|
+
WirePermissionStatus,
|
|
12
|
+
} from "./permissions/types";
|
|
7
13
|
|
|
8
14
|
export type { OnboardingTheme } from "./theme/types";
|
|
9
15
|
export type { WireOnboardingStorage } from "./session/persistedSession";
|
|
@@ -65,6 +71,10 @@ export type OnboardingResult = {
|
|
|
65
71
|
* - `fallback`: retries are exhausted; the kit degraded to the static `fallbackFlow`
|
|
66
72
|
* (or handed off to `onError`). This is the client-side mirror of the
|
|
67
73
|
* backend's `llm_fallback` reliability event.
|
|
74
|
+
* - `permission`: an injected permission screen moved (`shown` / `accepted` / `granted` /
|
|
75
|
+
* `denied` / `skipped` / `settings`). Carries NO funnel weight: a permission screen
|
|
76
|
+
* is not a question, so it never appears in `answers` and never gates completion.
|
|
77
|
+
* `toAnalyticsEvent` maps it to the canonical `wire_permission_*` name.
|
|
68
78
|
*/
|
|
69
79
|
export type OnboardingEvent =
|
|
70
80
|
| { type: "started"; contextId: string }
|
|
@@ -72,7 +82,14 @@ export type OnboardingEvent =
|
|
|
72
82
|
| { type: "turn"; step: number; component?: string }
|
|
73
83
|
| { type: "error"; reason: "backend" | "timeout" }
|
|
74
84
|
| { type: "retry"; reason: "backend" | "timeout"; attempt: number }
|
|
75
|
-
| { type: "fallback"; reason: "backend" | "timeout" }
|
|
85
|
+
| { type: "fallback"; reason: "backend" | "timeout" }
|
|
86
|
+
| {
|
|
87
|
+
type: "permission";
|
|
88
|
+
permission: WirePermissionKind;
|
|
89
|
+
stage: PermissionStage;
|
|
90
|
+
/** What the OS actually answered, when it answered. Absent on `shown` / `accepted`. */
|
|
91
|
+
status?: WirePermissionStatus;
|
|
92
|
+
};
|
|
76
93
|
|
|
77
94
|
/**
|
|
78
95
|
* Copy overrides for the kit's built-in (English) strings, so a host can localize
|
|
@@ -118,6 +135,48 @@ export type WireOnboardingProps = {
|
|
|
118
135
|
icons?: Record<string, import("react").ReactNode>;
|
|
119
136
|
/** Per-step validators keyed by base-question key, e.g. `{ username: checkUsername }`. */
|
|
120
137
|
validators?: Record<string, StepValidator>;
|
|
138
|
+
/**
|
|
139
|
+
* PERMISSION SCREENS injected into the server-driven flow at a position you choose.
|
|
140
|
+
*
|
|
141
|
+
* The screen explains why the app wants the permission and asks the OS **only** on the primary
|
|
142
|
+
* tap. That priming pattern is not decoration: iOS grants an app exactly ONE native notification
|
|
143
|
+
* prompt for its whole lifetime, and firing it on mount spends it on a user who was told nothing.
|
|
144
|
+
* "Maybe later" advances the flow with the prompt still unspent.
|
|
145
|
+
*
|
|
146
|
+
* ```tsx
|
|
147
|
+
* import * as Notifications from "expo-notifications";
|
|
148
|
+
*
|
|
149
|
+
* <WireOnboarding
|
|
150
|
+
* permissionScreens={[
|
|
151
|
+
* {
|
|
152
|
+
* permission: "notifications",
|
|
153
|
+
* placement: "beforeEnd",
|
|
154
|
+
* request: async () => {
|
|
155
|
+
* const { status, canAskAgain } = await Notifications.requestPermissionsAsync();
|
|
156
|
+
* return status === "granted" ? "granted" : canAskAgain ? "denied" : "blocked";
|
|
157
|
+
* },
|
|
158
|
+
* getStatus: async () => {
|
|
159
|
+
* const { status, canAskAgain } = await Notifications.getPermissionsAsync();
|
|
160
|
+
* return status === "granted" ? "granted" : canAskAgain ? "denied" : "blocked";
|
|
161
|
+
* },
|
|
162
|
+
* onResult: (_p, outcome) => { if (outcome === "granted") scheduleFirstReminder(); },
|
|
163
|
+
* },
|
|
164
|
+
* ]}
|
|
165
|
+
* />
|
|
166
|
+
* ```
|
|
167
|
+
*
|
|
168
|
+
* The kit adds NO dependency for this: it imports no `expo-notifications`, no
|
|
169
|
+
* `react-native-permissions`, nothing native. You inject `request`, exactly the way you hand the
|
|
170
|
+
* RevenueCat bridge real RevenueCat objects.
|
|
171
|
+
*
|
|
172
|
+
* A permission screen is NOT a question. It sends nothing to the backend, never enters the
|
|
173
|
+
* thread, mints no `key` / `slot_id`, and never appears in `onComplete`'s `answers`. Completion
|
|
174
|
+
* never blocks on a grant: grant, deny, skip and blocked all continue the flow. Each screen is
|
|
175
|
+
* shown at most once per session, and with `storage` that survives an app kill (a resumed session
|
|
176
|
+
* does not re-ask). Analytics ride `onEvent` (`type: "permission"`) and the canonical
|
|
177
|
+
* `wire_permission_*` events, stamped with the same `device_key` as the rest of the funnel.
|
|
178
|
+
*/
|
|
179
|
+
permissionScreens?: PermissionScreenConfig[];
|
|
121
180
|
/** Fired once the flow reaches its terminal StatusCard. */
|
|
122
181
|
onComplete: (result: OnboardingResult) => void;
|
|
123
182
|
/**
|
|
@@ -281,9 +340,12 @@ export type OnboardingProgress = {
|
|
|
281
340
|
* `answers.interests` silently becomes `answers.what_are_you_into_v2`, with no error anywhere. A
|
|
282
341
|
* slot is the question's identity independent of its wording.
|
|
283
342
|
*
|
|
284
|
-
* FULLY ADDITIVE AND
|
|
285
|
-
*
|
|
286
|
-
*
|
|
343
|
+
* FULLY ADDITIVE, AND LIVE SINCE 2026-07-28. The deployed server sends it on
|
|
344
|
+
* `progress` for every AI-GENERATED question, as `adaptive_<n>` 1-based over adaptive answers, and
|
|
345
|
+
* for a CONFIGURED question only when the tenant set one. Where the tenant set none the field is
|
|
346
|
+
* simply absent. Every fallback is PER-CARD, so a thread that mixes slotted and unslotted cards
|
|
347
|
+
* (the real shape during a rollout) keys each one correctly, and a backend or tenant that never
|
|
348
|
+
* sends it produces byte-identical behaviour to 0.12.2.
|
|
287
349
|
*/
|
|
288
350
|
slot_id?: string;
|
|
289
351
|
/** Whether the CURRENT screen may be skipped (backend-marked; default false → no Skip shown). */
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* submitResult — the ONE delivery verdict every user-payload POST in this kit reports back.
|
|
3
|
+
*
|
|
4
|
+
* WHY IT IS SHARED, AND WHY IT IS NOT A BOOLEAN. Two surfaces post a payload the user typed —
|
|
5
|
+
* `submitReview` (`reviews/transport.ts`) and `submitQuestionnaireResponse`
|
|
6
|
+
* (`questionnaire/transport.ts`) — and NEITHER has a persisted queue behind it (unlike
|
|
7
|
+
* `analytics/eventQueue`, with its `maxSize`/`maxRetries`/backoff). Both sit behind a permanent
|
|
8
|
+
* once-gate: the host writes `wire_review_<id>_seen` / `wire_questionnaire_<id>_<ver>_seen` when the
|
|
9
|
+
* gate resolves, so a payload that dies in the socket is lost AND the user is never asked again.
|
|
10
|
+
* Reading the fate of the request is the only thing that lets a caller keep an undelivered payload
|
|
11
|
+
* recoverable, so the two surfaces must answer the same question in the same words.
|
|
12
|
+
*
|
|
13
|
+
* • `accepted` — the server answered 2xx. Stored.
|
|
14
|
+
* • `rejected` — the server ANSWERED, non-2xx. It was reached; it either stored the payload and
|
|
15
|
+
* lost the ack (a gateway 502 after the write commits) or deliberately refused it.
|
|
16
|
+
* Either way the server owns the outcome and a re-post risks a SECOND row.
|
|
17
|
+
* • `unsent` — nothing came back at all: no target, a missing/throwing `fetch`, a rejected
|
|
18
|
+
* request. **This is the ONLY outcome a caller may retry on.**
|
|
19
|
+
*
|
|
20
|
+
* ── WHY THREE VALUES AND NOT TWO (pinned here so the defect cannot move surface) ───────────────
|
|
21
|
+
*
|
|
22
|
+
* `submitReview` was a boolean for exactly one unpublished release, and the boolean WAS the bug.
|
|
23
|
+
* `false` meant both "nothing reached the server" and "the server answered non-2xx", and the caller
|
|
24
|
+
* treats `false` as "still owed" and re-posts. But the server mints its own row id
|
|
25
|
+
* (`create_review` / `_new_id()`), and neither `CreateReviewRequest` nor the questionnaire response
|
|
26
|
+
* body carries a client id, so there is NO idempotency key on the wire: a 502 returned AFTER the
|
|
27
|
+
* insert commits means the re-post writes a SECOND row. A response of any status proves the server
|
|
28
|
+
* was reached, and that is a different question from whether it liked the payload.
|
|
29
|
+
*
|
|
30
|
+
* ── THE RESIDUAL, STATED HONESTLY ─────────────────────────────────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* `unsent` is not proof the server never got it. A connection dropped after the request was written
|
|
33
|
+
* — or after the row committed — surfaces as a thrown/rejected `fetch`, exactly like an offline
|
|
34
|
+
* device. Retrying only on `unsent` is therefore SAFER, not SAFE. Closing that window needs a
|
|
35
|
+
* CLIENT-MINTED IDEMPOTENCY KEY the server upserts on, which is a server change and not something
|
|
36
|
+
* the kit can fake. Until it exists, prefer losing a row over inventing one: a lost detractor is a
|
|
37
|
+
* gap in the data, a duplicated one is a lie in the data.
|
|
38
|
+
*/
|
|
39
|
+
export type SubmitResult = "accepted" | "rejected" | "unsent";
|