@wireai/activation 0.8.0 → 0.9.1
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 +29 -5
- package/CHANGELOG.md +202 -6
- package/README.md +105 -10
- package/dist/analytics/index.d.mts +6 -6
- package/dist/analytics/index.d.ts +6 -6
- package/dist/analytics/index.js +447 -49
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +447 -49
- package/dist/analytics/index.mjs.map +1 -1
- package/dist/coachmarks/index.d.mts +2 -2
- package/dist/coachmarks/index.d.ts +2 -2
- package/dist/coachmarks/index.js.map +1 -1
- package/dist/coachmarks/index.mjs.map +1 -1
- package/dist/{currentSession-f7LWcdWG.d.ts → currentSession-61dcm3V-.d.ts} +10 -2
- package/dist/{currentSession-d9CrBxwe.d.mts → currentSession-Bs2JfTJ8.d.mts} +10 -2
- package/dist/{decision-BzbiKwk3.d.mts → decision-Bl_M2y3r.d.mts} +1 -1
- package/dist/{decision-plDEOCkt.d.ts → decision-Cau5KmP6.d.ts} +1 -1
- package/dist/index.d.mts +558 -15
- package/dist/index.d.ts +558 -15
- package/dist/index.js +1144 -375
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +969 -207
- package/dist/index.mjs.map +1 -1
- package/dist/questionnaire/index.d.mts +69 -23
- package/dist/questionnaire/index.d.ts +69 -23
- package/dist/questionnaire/index.js +643 -172
- package/dist/questionnaire/index.js.map +1 -1
- package/dist/questionnaire/index.mjs +608 -137
- package/dist/questionnaire/index.mjs.map +1 -1
- package/dist/reviews/index.d.mts +24 -12
- package/dist/reviews/index.d.ts +24 -12
- package/dist/reviews/index.js +507 -173
- package/dist/reviews/index.js.map +1 -1
- package/dist/reviews/index.mjs +462 -129
- package/dist/reviews/index.mjs.map +1 -1
- package/dist/showcase/index.d.mts +2 -2
- package/dist/showcase/index.d.ts +2 -2
- package/dist/showcase/index.js.map +1 -1
- package/dist/showcase/index.mjs.map +1 -1
- package/dist/transport-B31G0Cib.d.ts +128 -0
- package/dist/transport-Bzb-bcB2.d.mts +128 -0
- package/dist/{types-GL_hQ0TN.d.mts → types-BcmagF6K.d.mts} +1 -1
- package/dist/{types-GL_hQ0TN.d.ts → types-BcmagF6K.d.ts} +1 -1
- package/dist/{types-BhpXJGlg.d.ts → types-Buj9Lw9t.d.ts} +15 -3
- package/dist/{types-CMuOexw0.d.mts → types-CKFhyrMu.d.mts} +1 -1
- package/dist/{types-CMuOexw0.d.ts → types-CKFhyrMu.d.ts} +1 -1
- package/dist/{types-A6pTxIZV.d.mts → types-CNUqMK0D.d.mts} +15 -3
- package/metro/index.d.ts +18 -1
- package/metro/index.js +80 -0
- package/package.json +9 -1
- package/src/WireOnboarding.tsx +4 -0
- package/src/analytics/currentSession.ts +1 -1
- package/src/cards/CardGridSelectCard.tsx +253 -0
- package/src/cards/ChipSelectCard.tsx +1 -1
- package/src/cards/SelectionCard.tsx +15 -19
- package/src/cards/index.ts +13 -1
- package/src/cards/optionSchema.ts +65 -0
- package/src/coachmarks/types.ts +1 -1
- package/src/components/CardHandoff.tsx +39 -7
- package/src/components/DoneCheck.tsx +94 -0
- package/src/components/ModalPaneHost.tsx +141 -0
- package/src/components/StepDots.tsx +112 -0
- package/src/device/appVersion.ts +29 -8
- package/src/device/deviceModel.ts +21 -8
- package/src/features/cache.ts +1 -1
- package/src/haptics/expo-haptics.d.ts +31 -0
- package/src/haptics/haptics.ts +58 -0
- package/src/icons/IconRegistry.tsx +53 -0
- package/src/icons/WireIcon.tsx +85 -0
- package/src/icons/expoIcons.ts +165 -0
- package/src/icons/index.ts +18 -0
- package/src/icons/vocabulary.ts +154 -0
- package/src/index.ts +20 -0
- package/src/questionnaire/QuestionnaireGate.tsx +315 -164
- package/src/questionnaire/decision.ts +4 -2
- package/src/questionnaire/index.ts +3 -2
- package/src/questionnaire/steps.ts +261 -0
- package/src/questionnaire/types.ts +33 -12
- package/src/reviews/ReviewGate.tsx +250 -152
- package/src/reviews/index.ts +7 -3
- package/src/reviews/transport.ts +119 -3
- package/src/reviews/types.ts +13 -0
- package/src/session/persistedSession.ts +1 -1
- package/src/session-analytics/lifecycle.ts +1 -1
- package/src/session-analytics/useSessionStart.ts +2 -2
- package/src/showcase/FeatureShowcase.tsx +1 -1
- package/src/showcase/showcaseColors.ts +1 -1
- package/src/theme/mergeThemeOver.ts +27 -0
- package/src/types.ts +8 -0
- package/dist/transport-BeO_Brcu.d.mts +0 -40
- package/dist/transport-DLpd1v5_.d.ts +0 -40
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { k as ReviewTarget, g as ReviewDecisionResponse, j as ReviewSubmission } from './types-CNUqMK0D.mjs';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* transport.ts — kit → Wire server requests for the review module, all fire-and-forget
|
|
5
|
+
* (analytics/reviews must never break the app). Mirrors analytics/reportClientEvent: a
|
|
6
|
+
* thin fetch wrapper, Bearer tenant key, swallow every error.
|
|
7
|
+
*
|
|
8
|
+
* • submitReview → POST {serverUrl}/v1/reviews (the review row)
|
|
9
|
+
* • fetchReviewDecision → GET {serverUrl}/v1/reviews/decision (best-effort, the AI seam)
|
|
10
|
+
* • reportAppEvent → POST {serverUrl}/v1/events (generic app.* namespace)
|
|
11
|
+
*
|
|
12
|
+
* `reportAppEvent` is the strategic extension: it lets a host report arbitrary in-app
|
|
13
|
+
* events through the SAME transport (stored server-side as event_type='app_event',
|
|
14
|
+
* question_key=<name>), which is what the backend review-firing rules evaluate on — and
|
|
15
|
+
* it seeds the broader app-analytics stream. Keep payloads minimal + non-PII.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* POST a review (the 1-4 feedback path). Fire-and-forget: a missing target, a build error,
|
|
20
|
+
* a missing `fetch`, or a network failure is swallowed and the call returns immediately.
|
|
21
|
+
*/
|
|
22
|
+
declare const submitReview: (target: ReviewTarget | undefined, review: ReviewSubmission) => void;
|
|
23
|
+
/**
|
|
24
|
+
* Options for the best-effort review decision fetch.
|
|
25
|
+
*
|
|
26
|
+
* ── WHAT THE DEPLOYED SERVER ACTUALLY READS (verified 2026-07-17) ────────────────────────
|
|
27
|
+
*
|
|
28
|
+
* `GET /v1/reviews/decision` declares exactly two query params — `session_id` and
|
|
29
|
+
* `device_key` — plus the `Authorization` header. That is the whole wire. Verified against
|
|
30
|
+
* the deployed OpenAPI schema, not against intent.
|
|
31
|
+
*
|
|
32
|
+
* This type is CLOSED on purpose. Hosts that hand-rolled this fetch invented `user_id` and
|
|
33
|
+
* `session_count` query params believing "the server ignores what it doesn't read, so passing
|
|
34
|
+
* it is always safe". Both are no-ops: the route declares neither. `session_count` is real, but
|
|
35
|
+
* only on the questionnaire POST body — which is precisely how it copy-pasted its way into a
|
|
36
|
+
* reviews call site and sat there doing nothing. They cost a wire lie — a call site that reads as though
|
|
37
|
+
* identity and a session counter reach the firing brain when neither does. So they are not
|
|
38
|
+
* offered here. Pass identity as `deviceKey`; the session count the server reasons about is the
|
|
39
|
+
* one IT derives from the event stream keyed by `deviceKey`, not one the client asserts.
|
|
40
|
+
*
|
|
41
|
+
* The client-side session floor is a LOCAL rule, not a wire param: use `ReviewConfig.minSessions`
|
|
42
|
+
* (evaluated by `useReviewGate` against the kit's own `wire_review_<id>_sessions` counter).
|
|
43
|
+
*/
|
|
44
|
+
interface FetchReviewDecisionOptions {
|
|
45
|
+
/**
|
|
46
|
+
* The onboarding session id, when there IS one. OPTIONAL on purpose: the review gate lives on
|
|
47
|
+
* the home feed, where a user legitimately has no onboarding session. `deviceKey` is the real
|
|
48
|
+
* identity for this call. (The server relaxed `session_id` to optional on 2026-07-16 and the
|
|
49
|
+
* relaxation IS deployed — the route no longer 422s without it. Permissive wire: do NOT make
|
|
50
|
+
* this required here.)
|
|
51
|
+
*/
|
|
52
|
+
sessionId?: string;
|
|
53
|
+
/**
|
|
54
|
+
* A stable, non-PII device id — THE identity for this call. The decision endpoint reads it for
|
|
55
|
+
* cooldown + min-sessions, and it is the key the server groups a device's events under. A host
|
|
56
|
+
* whose own identity is a user id passes that id here rather than reaching for a `user_id`
|
|
57
|
+
* param the route does not declare.
|
|
58
|
+
*/
|
|
59
|
+
deviceKey?: string;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Best-effort fetch of the SERVER's review firing decision. The mirror of
|
|
63
|
+
* `fetchQuestionnaireDecision`, and the primitive whose absence caused a live incident.
|
|
64
|
+
*
|
|
65
|
+
* ── WHY THIS LIVES IN THE KIT (Malik, 2026-07-16) ────────────────────────────────────────
|
|
66
|
+
*
|
|
67
|
+
* The questionnaire module has always had its decision fetch; reviews never did. So a host
|
|
68
|
+
* hand-rolled one, and got it subtly wrong: its helper collapsed an explicit `{fire:false}`
|
|
69
|
+
* into `undefined`. `decideReview` is `(local, decision) => decision ?? local`, so `undefined`
|
|
70
|
+
* means "the server has no opinion, use the local rules" — and the local timer fired. The
|
|
71
|
+
* server could therefore only ever turn review prompts ON, never OFF. A real user was asked to
|
|
72
|
+
* rate the app ~3 minutes into their FIRST session, having seen nothing yet, and left 1 star.
|
|
73
|
+
*
|
|
74
|
+
* The bug was not that the host was careless. It was that the kit made every host invent this.
|
|
75
|
+
* So the primitive moves here and hosts keep a thin call site.
|
|
76
|
+
*
|
|
77
|
+
* ── THE CONTRACT THAT MATTERS ────────────────────────────────────────────────────────────
|
|
78
|
+
*
|
|
79
|
+
* • 2xx → the FULL `{fire, reason, arm}`, INCLUDING `fire:false`. Never collapse a false
|
|
80
|
+
* into null. That collapse IS the bug: a false must reach `decideReview` intact so
|
|
81
|
+
* it can override the local rules and keep the gate shut.
|
|
82
|
+
* • else → null, and ONLY then. Null means "the server genuinely has no opinion", which is
|
|
83
|
+
* the one case where falling back to local rules is correct.
|
|
84
|
+
*
|
|
85
|
+
* Never throws: unreachable, non-2xx, bad JSON, or a missing `fetch` all resolve to null. The
|
|
86
|
+
* kit does not call this internally; a host awaits it and passes the result straight to
|
|
87
|
+
* `useReviewGate({ decision })`.
|
|
88
|
+
*
|
|
89
|
+
* ── DON'T RACE THIS AGAINST A LOCAL TIMER ────────────────────────────────────────────────
|
|
90
|
+
*
|
|
91
|
+
* The return type is `ReviewDecisionResponse`, which carries `arm` alongside `{fire, reason}`.
|
|
92
|
+
* Hand the WHOLE object to the gate and echo `arm` into the submission's `meta.firing_arm`;
|
|
93
|
+
* narrowing it to `{fire, reason}` on the way through silently kills per-arm attribution
|
|
94
|
+
* across a reweighting of the experiment.
|
|
95
|
+
*
|
|
96
|
+
* A host that starts its own dwell timer in parallel with this fetch has built a race a slow
|
|
97
|
+
* server loses: the timer fires, the local rules show the prompt, and the `{fire:false}` still
|
|
98
|
+
* in flight arrives too late to stop it. Do not hand-roll that. `useReviewGate` already owns
|
|
99
|
+
* the wait — set `ReviewConfig.timeoutFallbackMs` and the local rules stay parked until either
|
|
100
|
+
* the decision lands or the window expires, whichever comes first.
|
|
101
|
+
*
|
|
102
|
+
* const decision = await fetchReviewDecision(target, { deviceKey });
|
|
103
|
+
* const gate = useReviewGate({
|
|
104
|
+
* config: { id: "home", minSessions: 2, timeoutFallbackMs: 3000 },
|
|
105
|
+
* decision: decision ?? undefined, // pass it whole — keep `arm`
|
|
106
|
+
* storage,
|
|
107
|
+
* });
|
|
108
|
+
*/
|
|
109
|
+
declare const fetchReviewDecision: (target: ReviewTarget | undefined, options?: FetchReviewDecisionOptions) => Promise<ReviewDecisionResponse | null>;
|
|
110
|
+
/** Options for a reported app event. `deviceKey` groups a device's sessions server-side. */
|
|
111
|
+
interface ReportAppEventOptions {
|
|
112
|
+
/** The onboarding/session id to correlate with, when known. */
|
|
113
|
+
sessionId?: string;
|
|
114
|
+
/** A stable, non-PII device id — the review-decision endpoint reads it for min-sessions. */
|
|
115
|
+
deviceKey?: string;
|
|
116
|
+
/** Small non-PII extras. */
|
|
117
|
+
meta?: Record<string, unknown>;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Report a generic in-app event through the existing events transport. Stored server-side
|
|
121
|
+
* as `event_type='app_event'`, `question_key=<name>`. Fire-and-forget. Keep `name` a short
|
|
122
|
+
* stable identifier and `meta` small + non-PII.
|
|
123
|
+
*
|
|
124
|
+
* reportAppEvent(target, "content_share", { sessionId, deviceKey });
|
|
125
|
+
*/
|
|
126
|
+
declare const reportAppEvent: (target: ReviewTarget | undefined, name: string, options?: ReportAppEventOptions) => void;
|
|
127
|
+
|
|
128
|
+
export { type FetchReviewDecisionOptions as F, type ReportAppEventOptions as R, fetchReviewDecision as f, reportAppEvent as r, submitReview as s };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared types for the coachmark / guided-tour overlay engine.
|
|
3
3
|
*
|
|
4
|
-
* Lifted from
|
|
4
|
+
* Lifted from a host app engine and made kit-generic: the app supplies the
|
|
5
5
|
* anchors + catalog + copy, the kit owns the animation/measure/queue mechanics.
|
|
6
6
|
* Dependency-free on purpose so the type surface can be imported anywhere.
|
|
7
7
|
*/
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared types for the coachmark / guided-tour overlay engine.
|
|
3
3
|
*
|
|
4
|
-
* Lifted from
|
|
4
|
+
* Lifted from a host app engine and made kit-generic: the app supplies the
|
|
5
5
|
* anchors + catalog + copy, the kit owns the animation/measure/queue mechanics.
|
|
6
6
|
* Dependency-free on purpose so the type surface can be imported anywhere.
|
|
7
7
|
*/
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { O as OnboardingTheme } from './types-BKfpdZzX.js';
|
|
2
|
-
import { C as CoachmarkStorage } from './types-
|
|
3
|
-
import { W as WireFeatures, a as WireFeaturesConfig } from './types-
|
|
2
|
+
import { C as CoachmarkStorage } from './types-BcmagF6K.js';
|
|
3
|
+
import { W as WireFeatures, a as WireFeaturesConfig } from './types-CKFhyrMu.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The firing verdict — the SAME contract on both sides of the seam. `decideReview`
|
|
@@ -11,6 +11,18 @@ interface ReviewDecision {
|
|
|
11
11
|
fire: boolean;
|
|
12
12
|
reason?: string;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The `GET /v1/reviews/decision` response: `ReviewDecision` plus the additive `arm`.
|
|
16
|
+
*
|
|
17
|
+
* `arm` is the firing experiment's sticky assignment for this device/session. Echo it back in
|
|
18
|
+
* the submission's `meta.firing_arm` and the server keeps it verbatim (`_stamp_firing_arm`:
|
|
19
|
+
* "an arm the kit already echoed is left as-is"); omit it and the server recomputes from the
|
|
20
|
+
* sticky hash. Echoing is what makes per-arm attribution survive a later reweighting of the
|
|
21
|
+
* experiment, so a host running one should pass it through.
|
|
22
|
+
*/
|
|
23
|
+
interface ReviewDecisionResponse extends ReviewDecision {
|
|
24
|
+
arm?: string | null;
|
|
25
|
+
}
|
|
14
26
|
/** Where to route a rating: 5 stars → the store, 1-4 → the feedback form. */
|
|
15
27
|
type RatingRoute = "store" | "feedback";
|
|
16
28
|
/**
|
|
@@ -149,4 +161,4 @@ interface ReviewGateController {
|
|
|
149
161
|
markResolved: () => void;
|
|
150
162
|
}
|
|
151
163
|
|
|
152
|
-
export type { ReviewGateProps as R, UseReviewGateOptions as U, ReviewPresentation as a, ReviewGateController as b, ReviewDecision as c, ReviewStoreConfig as d, RatingRoute as e, ReviewConfig as f,
|
|
164
|
+
export type { ReviewGateProps as R, UseReviewGateOptions as U, ReviewPresentation as a, ReviewGateController as b, ReviewDecision as c, ReviewStoreConfig as d, RatingRoute as e, ReviewConfig as f, ReviewDecisionResponse as g, ReviewGateEvent as h, ReviewGateEventName as i, ReviewSubmission as j, ReviewTarget as k };
|
|
@@ -13,7 +13,7 @@ type WireOnboardingStorage = {
|
|
|
13
13
|
* this can't be resumed server-side anyway, so the client mints fresh past it.
|
|
14
14
|
*/
|
|
15
15
|
declare const DEFAULT_SESSION_TTL_MS = 3600000;
|
|
16
|
-
/** Storage key for an app's cached session, e.g. `wireai:session:
|
|
16
|
+
/** Storage key for an app's cached session, e.g. `wireai:session:acme`. */
|
|
17
17
|
declare const sessionStorageKey: (appId: string) => string;
|
|
18
18
|
/** Resolved session for a mount: the id to use + whether it was restored from storage. */
|
|
19
19
|
type LoadedSession = {
|
|
@@ -13,7 +13,7 @@ type WireOnboardingStorage = {
|
|
|
13
13
|
* this can't be resumed server-side anyway, so the client mints fresh past it.
|
|
14
14
|
*/
|
|
15
15
|
declare const DEFAULT_SESSION_TTL_MS = 3600000;
|
|
16
|
-
/** Storage key for an app's cached session, e.g. `wireai:session:
|
|
16
|
+
/** Storage key for an app's cached session, e.g. `wireai:session:acme`. */
|
|
17
17
|
declare const sessionStorageKey: (appId: string) => string;
|
|
18
18
|
/** Resolved session for a mount: the id to use + whether it was restored from storage. */
|
|
19
19
|
type LoadedSession = {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { O as OnboardingTheme } from './types-BKfpdZzX.mjs';
|
|
2
|
-
import { C as CoachmarkStorage } from './types-
|
|
3
|
-
import { W as WireFeatures, a as WireFeaturesConfig } from './types-
|
|
2
|
+
import { C as CoachmarkStorage } from './types-BcmagF6K.mjs';
|
|
3
|
+
import { W as WireFeatures, a as WireFeaturesConfig } from './types-CKFhyrMu.mjs';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* The firing verdict — the SAME contract on both sides of the seam. `decideReview`
|
|
@@ -11,6 +11,18 @@ interface ReviewDecision {
|
|
|
11
11
|
fire: boolean;
|
|
12
12
|
reason?: string;
|
|
13
13
|
}
|
|
14
|
+
/**
|
|
15
|
+
* The `GET /v1/reviews/decision` response: `ReviewDecision` plus the additive `arm`.
|
|
16
|
+
*
|
|
17
|
+
* `arm` is the firing experiment's sticky assignment for this device/session. Echo it back in
|
|
18
|
+
* the submission's `meta.firing_arm` and the server keeps it verbatim (`_stamp_firing_arm`:
|
|
19
|
+
* "an arm the kit already echoed is left as-is"); omit it and the server recomputes from the
|
|
20
|
+
* sticky hash. Echoing is what makes per-arm attribution survive a later reweighting of the
|
|
21
|
+
* experiment, so a host running one should pass it through.
|
|
22
|
+
*/
|
|
23
|
+
interface ReviewDecisionResponse extends ReviewDecision {
|
|
24
|
+
arm?: string | null;
|
|
25
|
+
}
|
|
14
26
|
/** Where to route a rating: 5 stars → the store, 1-4 → the feedback form. */
|
|
15
27
|
type RatingRoute = "store" | "feedback";
|
|
16
28
|
/**
|
|
@@ -149,4 +161,4 @@ interface ReviewGateController {
|
|
|
149
161
|
markResolved: () => void;
|
|
150
162
|
}
|
|
151
163
|
|
|
152
|
-
export type { ReviewGateProps as R, UseReviewGateOptions as U, ReviewPresentation as a, ReviewGateController as b, ReviewDecision as c, ReviewStoreConfig as d, RatingRoute as e, ReviewConfig as f,
|
|
164
|
+
export type { ReviewGateProps as R, UseReviewGateOptions as U, ReviewPresentation as a, ReviewGateController as b, ReviewDecision as c, ReviewStoreConfig as d, RatingRoute as e, ReviewConfig as f, ReviewDecisionResponse as g, ReviewGateEvent as h, ReviewGateEventName as i, ReviewSubmission as j, ReviewTarget as k };
|
package/metro/index.d.ts
CHANGED
|
@@ -16,12 +16,20 @@ export interface WithWireOnboardingOptions {
|
|
|
16
16
|
* When omitted, the kit resolves from the installed package.
|
|
17
17
|
*/
|
|
18
18
|
source?: string;
|
|
19
|
+
/**
|
|
20
|
+
* Extra specifiers to treat as optional, i.e. resolve to an EMPTY module instead of
|
|
21
|
+
* failing the build when they are not installed. Added to the kit's own
|
|
22
|
+
* `OPTIONAL_MODULES` list, never replacing it.
|
|
23
|
+
*/
|
|
24
|
+
optionalModules?: string[];
|
|
19
25
|
}
|
|
20
26
|
|
|
21
27
|
/**
|
|
22
28
|
* Non-destructively wire the @wireai/activation kit into an existing Metro config:
|
|
23
29
|
* pins single-instance deps to the app's copy, optionally maps the kit specifier
|
|
24
|
-
* to source + watches it
|
|
30
|
+
* to source + watches it, and makes the kit's optional modules genuinely optional
|
|
31
|
+
* (an absent one resolves to an empty module instead of breaking the build).
|
|
32
|
+
* Mutates and returns the same config object.
|
|
25
33
|
*/
|
|
26
34
|
export function withWireOnboarding<TConfig extends Record<string, any>>(
|
|
27
35
|
config: TConfig,
|
|
@@ -30,3 +38,12 @@ export function withWireOnboarding<TConfig extends Record<string, any>>(
|
|
|
30
38
|
|
|
31
39
|
/** The deps pinned to the app's single copy. */
|
|
32
40
|
export const SINGLE_INSTANCE_DEPS: readonly ['react', 'react-native', 'wireai-rn', 'zod'];
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The modules the kit reads through a guarded require and can live without. When one of
|
|
44
|
+
* these does not resolve, `withWireOnboarding` maps it to an empty module so the host
|
|
45
|
+
* still bundles and the feature degrades (icon → null, analytics field omitted, native
|
|
46
|
+
* review → store-URL fallback). Modules the kit imports statically are deliberately NOT
|
|
47
|
+
* in this list — stubbing those would hide a real build error.
|
|
48
|
+
*/
|
|
49
|
+
export const OPTIONAL_MODULES: readonly string[];
|
package/metro/index.js
CHANGED
|
@@ -26,6 +26,8 @@
|
|
|
26
26
|
* @param {string} [options.source] Absolute path to the kit's `src/` dir. When
|
|
27
27
|
* set, the kit specifier maps here (source consumption) and it's added to
|
|
28
28
|
* watchFolders; when omitted, the kit resolves from the installed package.
|
|
29
|
+
* @param {string[]} [options.optionalModules] Extra specifiers to treat as
|
|
30
|
+
* optional (see OPTIONAL_MODULES). Added to the kit's list, never replacing it.
|
|
29
31
|
* @returns {object} The same config object, mutated in place and returned.
|
|
30
32
|
*/
|
|
31
33
|
'use strict';
|
|
@@ -35,6 +37,60 @@ const path = require('path');
|
|
|
35
37
|
/** Deps that MUST resolve to the app's single copy (a 2nd React instance crashes RN). */
|
|
36
38
|
const SINGLE_INSTANCE_DEPS = ['react', 'react-native', 'wireai-rn', 'zod'];
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Modules the kit reads through a GUARDED require/import and is designed to live without.
|
|
42
|
+
*
|
|
43
|
+
* ── HOW OPTIONAL DEPS ACTUALLY WORK UNDER METRO (verified, not folklore) ────────────────
|
|
44
|
+
*
|
|
45
|
+
* Metro has NO runtime module resolution: every specifier is resolved when the bundle is BUILT.
|
|
46
|
+
* But it DOES have a first-class optional-dependency mechanism, and it needs two things:
|
|
47
|
+
*
|
|
48
|
+
* 1. a STRING-LITERAL specifier — Metro's collector only matches `require("literal")` /
|
|
49
|
+
* `import("literal")`. A variable specifier collects NOTHING, so the module never enters
|
|
50
|
+
* the bundle at all (that was the 0.8.0 bug; see src/icons/expoIcons.ts), and
|
|
51
|
+
* 2. the call sitting inside a TRY/CATCH — that is literally how Metro decides a dependency
|
|
52
|
+
* is optional (`isOptional`), and it only counts when the transformer has
|
|
53
|
+
* `allowOptionalDependencies` enabled. Expo's `getDefaultConfig` enables it; plain
|
|
54
|
+
* `metro-config` defaults it to FALSE.
|
|
55
|
+
*
|
|
56
|
+
* When both hold and the module is absent, Metro puts `null` in the module's dependencyMap, and
|
|
57
|
+
* `metroRequire(null, "name")` throws "Cannot find module" — synchronously, from metroRequire
|
|
58
|
+
* itself, so the caller's own catch swallows it. Verified in a real bundle: haptics' dependencyMap
|
|
59
|
+
* is `[355,null,1300]` with `expo-haptics` uninstalled. So DO NOT "simplify" a try/catch away from
|
|
60
|
+
* around a guarded require: the try/catch is load-bearing, and removing it turns an absent peer
|
|
61
|
+
* into a hard build failure.
|
|
62
|
+
*
|
|
63
|
+
* ── WHY THIS LIST STILL EXISTS ─────────────────────────────────────────────────────────
|
|
64
|
+
*
|
|
65
|
+
* Mechanism (2) is off by default in bare Metro (`allowOptionalDependencies: false`), and a host
|
|
66
|
+
* can turn it off. There, an absent optional module fails the BUILD with "Unable to resolve
|
|
67
|
+
* module". So `resolveRequest` below maps an absent module on this list to an EMPTY module, which
|
|
68
|
+
* lands on each caller's existing "peer absent" degradation path (no exports on the namespace →
|
|
69
|
+
* undefined → icon renders null / analytics field omitted / native review → store-URL fallback).
|
|
70
|
+
* It is a safety net that makes optionality independent of the host's Metro settings — not the
|
|
71
|
+
* only thing holding it up.
|
|
72
|
+
*
|
|
73
|
+
* ONLY modules read through a guarded require/import belong here. A module the kit imports
|
|
74
|
+
* STATICALLY at module scope (react-native-reanimated, expo-blur,
|
|
75
|
+
* @blazejkustra/react-native-onboarding) must NOT be stubbed: an empty module would turn a loud
|
|
76
|
+
* build-time "Unable to resolve module" into a silent `undefined` component that crashes at
|
|
77
|
+
* render, which is strictly worse. `test/canary/metroResolution.test.tsx` keeps this list in sync
|
|
78
|
+
* with the source by asking Metro which deps it marks optional.
|
|
79
|
+
*/
|
|
80
|
+
const OPTIONAL_MODULES = [
|
|
81
|
+
// src/icons/expoIcons.ts — icon families; absent → WireIcon renders null.
|
|
82
|
+
'@expo/vector-icons',
|
|
83
|
+
// src/device/appVersion.ts — analytics `by_app_version`; absent → field omitted.
|
|
84
|
+
'expo-constants',
|
|
85
|
+
'expo-application',
|
|
86
|
+
// src/device/deviceModel.ts — analytics iOS `by_model`; absent → field omitted.
|
|
87
|
+
'expo-device',
|
|
88
|
+
// src/reviews/storeReview.ts — native review prompt; absent → store-URL fallback.
|
|
89
|
+
'expo-store-review',
|
|
90
|
+
// src/haptics/haptics.ts — haptic feedback; absent → silent no-op.
|
|
91
|
+
'expo-haptics',
|
|
92
|
+
];
|
|
93
|
+
|
|
38
94
|
function withWireOnboarding(config, options) {
|
|
39
95
|
const opts = options || {};
|
|
40
96
|
const appRoot = opts.appRoot || process.cwd();
|
|
@@ -67,6 +123,29 @@ function withWireOnboarding(config, options) {
|
|
|
67
123
|
...existingExtra,
|
|
68
124
|
};
|
|
69
125
|
|
|
126
|
+
// ── resolveRequest: make the optional modules genuinely optional ────────────
|
|
127
|
+
// Non-destructive by construction: the app's own resolveRequest (or Metro's default) is asked
|
|
128
|
+
// FIRST and keeps full authority. We only act on the failure of an OPTIONAL specifier, turning
|
|
129
|
+
// "Unable to resolve module" into an empty module so a host that never installed the peer still
|
|
130
|
+
// bundles. A non-optional module's failure is rethrown untouched — a missing `react` must stay
|
|
131
|
+
// a hard, loud build error.
|
|
132
|
+
const optional = new Set([...OPTIONAL_MODULES, ...(opts.optionalModules || [])]);
|
|
133
|
+
const upstream = resolver.resolveRequest;
|
|
134
|
+
|
|
135
|
+
resolver.resolveRequest = (context, moduleName, platform) => {
|
|
136
|
+
const resolve = upstream || context.resolveRequest;
|
|
137
|
+
try {
|
|
138
|
+
return resolve(context, moduleName, platform);
|
|
139
|
+
} catch (error) {
|
|
140
|
+
// Match the package root and its subpaths ('@expo/vector-icons/build/Icons').
|
|
141
|
+
const isOptional = [...optional].some(
|
|
142
|
+
(name) => moduleName === name || moduleName.startsWith(name + '/'),
|
|
143
|
+
);
|
|
144
|
+
if (!isOptional) throw error;
|
|
145
|
+
return { type: 'empty' };
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
70
149
|
// ── watchFolders: add the kit source so Metro reads + HMRs it ───────────────
|
|
71
150
|
if (source) {
|
|
72
151
|
const existingWatch = Array.isArray(config.watchFolders) ? config.watchFolders : [];
|
|
@@ -81,3 +160,4 @@ function withWireOnboarding(config, options) {
|
|
|
81
160
|
module.exports = { withWireOnboarding };
|
|
82
161
|
module.exports.withWireOnboarding = withWireOnboarding;
|
|
83
162
|
module.exports.SINGLE_INSTANCE_DEPS = SINGLE_INSTANCE_DEPS;
|
|
163
|
+
module.exports.OPTIONAL_MODULES = OPTIONAL_MODULES;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wireai/activation",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Premium, fully-themable drop-in AI onboarding kit for React Native / Expo, on top of the open-source wireai-rn SDK.",
|
|
6
6
|
"author": "Malik Chohra <malik@getwireai.com>",
|
|
@@ -143,7 +143,9 @@
|
|
|
143
143
|
},
|
|
144
144
|
"peerDependencies": {
|
|
145
145
|
"@blazejkustra/react-native-onboarding": ">=1",
|
|
146
|
+
"@expo/vector-icons": ">=14",
|
|
146
147
|
"expo-blur": ">=13",
|
|
148
|
+
"expo-haptics": ">=13",
|
|
147
149
|
"expo-store-review": ">=6",
|
|
148
150
|
"react": ">=18.0.0",
|
|
149
151
|
"react-native": ">=0.73.0",
|
|
@@ -156,12 +158,18 @@
|
|
|
156
158
|
"zod": {
|
|
157
159
|
"optional": false
|
|
158
160
|
},
|
|
161
|
+
"@expo/vector-icons": {
|
|
162
|
+
"optional": true
|
|
163
|
+
},
|
|
159
164
|
"react-native-reanimated": {
|
|
160
165
|
"optional": true
|
|
161
166
|
},
|
|
162
167
|
"expo-blur": {
|
|
163
168
|
"optional": true
|
|
164
169
|
},
|
|
170
|
+
"expo-haptics": {
|
|
171
|
+
"optional": true
|
|
172
|
+
},
|
|
165
173
|
"expo-store-review": {
|
|
166
174
|
"optional": true
|
|
167
175
|
},
|
package/src/WireOnboarding.tsx
CHANGED
|
@@ -16,6 +16,7 @@ import { WireAIProvider, type LocalLLMConfig } from "wireai-rn";
|
|
|
16
16
|
import { OnboardingThemeProvider } from "./theme/ThemeContext";
|
|
17
17
|
import { IllustrationProvider } from "./components/Illustration";
|
|
18
18
|
import { defaultIllustrations } from "./illustrations/defaultIllustrations";
|
|
19
|
+
import { IconRegistryProvider } from "./icons/IconRegistry";
|
|
19
20
|
import { LoadingScreen } from "./components/LoadingScreen";
|
|
20
21
|
import { OnboardingFlow, DEFAULT_COPY } from "./OnboardingFlow";
|
|
21
22
|
import { onboardingComponents } from "./cards";
|
|
@@ -36,6 +37,7 @@ export const WireOnboarding: React.FC<WireOnboardingProps> = ({
|
|
|
36
37
|
theme,
|
|
37
38
|
components,
|
|
38
39
|
illustrations,
|
|
40
|
+
icons,
|
|
39
41
|
validators,
|
|
40
42
|
onComplete,
|
|
41
43
|
onSkip,
|
|
@@ -229,6 +231,7 @@ export const WireOnboarding: React.FC<WireOnboardingProps> = ({
|
|
|
229
231
|
return (
|
|
230
232
|
<OnboardingThemeProvider theme={theme}>
|
|
231
233
|
<IllustrationProvider registry={{ ...defaultIllustrations, ...illustrations }}>
|
|
234
|
+
<IconRegistryProvider registry={icons}>
|
|
232
235
|
<WireAIProvider llm={llm} components={cards}>
|
|
233
236
|
<OnboardingFlow
|
|
234
237
|
validators={validators}
|
|
@@ -248,6 +251,7 @@ export const WireOnboarding: React.FC<WireOnboardingProps> = ({
|
|
|
248
251
|
clientContext={clientContext}
|
|
249
252
|
/>
|
|
250
253
|
</WireAIProvider>
|
|
254
|
+
</IconRegistryProvider>
|
|
251
255
|
</IllustrationProvider>
|
|
252
256
|
</OnboardingThemeProvider>
|
|
253
257
|
);
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* post `app.session_started` with it — so the SERVER knows that id. But other client paths
|
|
7
7
|
* (`identify`, host `app_event`s through the analytics façade) used to reference a DIFFERENT id
|
|
8
8
|
* (a frozen per-instance id), which the server had never seen, so it back-filled a synthetic
|
|
9
|
-
* `session_started` — inflating session counts (the
|
|
9
|
+
* `session_started` — inflating session counts (the "phantom-session" bug).
|
|
10
10
|
*
|
|
11
11
|
* This registry is the single seam that lets those paths reuse the LIVE per-open session id the
|
|
12
12
|
* server already ingested. `reportSessionStart` writes the current id here on every open; the façade
|