@wireai/activation 0.8.0 → 0.9.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.
Files changed (91) hide show
  1. package/AGENTS.md +29 -5
  2. package/CHANGELOG.md +163 -6
  3. package/README.md +105 -10
  4. package/dist/analytics/index.d.mts +6 -6
  5. package/dist/analytics/index.d.ts +6 -6
  6. package/dist/analytics/index.js +447 -49
  7. package/dist/analytics/index.js.map +1 -1
  8. package/dist/analytics/index.mjs +447 -49
  9. package/dist/analytics/index.mjs.map +1 -1
  10. package/dist/coachmarks/index.d.mts +2 -2
  11. package/dist/coachmarks/index.d.ts +2 -2
  12. package/dist/coachmarks/index.js.map +1 -1
  13. package/dist/coachmarks/index.mjs.map +1 -1
  14. package/dist/{currentSession-f7LWcdWG.d.ts → currentSession-61dcm3V-.d.ts} +10 -2
  15. package/dist/{currentSession-d9CrBxwe.d.mts → currentSession-Bs2JfTJ8.d.mts} +10 -2
  16. package/dist/{decision-BzbiKwk3.d.mts → decision-Bl_M2y3r.d.mts} +1 -1
  17. package/dist/{decision-plDEOCkt.d.ts → decision-Cau5KmP6.d.ts} +1 -1
  18. package/dist/index.d.mts +558 -15
  19. package/dist/index.d.ts +558 -15
  20. package/dist/index.js +1144 -375
  21. package/dist/index.js.map +1 -1
  22. package/dist/index.mjs +969 -207
  23. package/dist/index.mjs.map +1 -1
  24. package/dist/questionnaire/index.d.mts +69 -23
  25. package/dist/questionnaire/index.d.ts +69 -23
  26. package/dist/questionnaire/index.js +643 -172
  27. package/dist/questionnaire/index.js.map +1 -1
  28. package/dist/questionnaire/index.mjs +608 -137
  29. package/dist/questionnaire/index.mjs.map +1 -1
  30. package/dist/reviews/index.d.mts +24 -12
  31. package/dist/reviews/index.d.ts +24 -12
  32. package/dist/reviews/index.js +507 -173
  33. package/dist/reviews/index.js.map +1 -1
  34. package/dist/reviews/index.mjs +462 -129
  35. package/dist/reviews/index.mjs.map +1 -1
  36. package/dist/showcase/index.d.mts +2 -2
  37. package/dist/showcase/index.d.ts +2 -2
  38. package/dist/showcase/index.js.map +1 -1
  39. package/dist/showcase/index.mjs.map +1 -1
  40. package/dist/transport-DzU-TqZz.d.ts +86 -0
  41. package/dist/transport-f5VVB5hH.d.mts +86 -0
  42. package/dist/{types-GL_hQ0TN.d.mts → types-BcmagF6K.d.mts} +1 -1
  43. package/dist/{types-GL_hQ0TN.d.ts → types-BcmagF6K.d.ts} +1 -1
  44. package/dist/{types-BhpXJGlg.d.ts → types-Buj9Lw9t.d.ts} +15 -3
  45. package/dist/{types-CMuOexw0.d.mts → types-CKFhyrMu.d.mts} +1 -1
  46. package/dist/{types-CMuOexw0.d.ts → types-CKFhyrMu.d.ts} +1 -1
  47. package/dist/{types-A6pTxIZV.d.mts → types-CNUqMK0D.d.mts} +15 -3
  48. package/metro/index.d.ts +18 -1
  49. package/metro/index.js +80 -0
  50. package/package.json +9 -1
  51. package/src/WireOnboarding.tsx +4 -0
  52. package/src/analytics/currentSession.ts +1 -1
  53. package/src/cards/CardGridSelectCard.tsx +253 -0
  54. package/src/cards/ChipSelectCard.tsx +1 -1
  55. package/src/cards/SelectionCard.tsx +15 -19
  56. package/src/cards/index.ts +13 -1
  57. package/src/cards/optionSchema.ts +65 -0
  58. package/src/coachmarks/types.ts +1 -1
  59. package/src/components/CardHandoff.tsx +39 -7
  60. package/src/components/DoneCheck.tsx +94 -0
  61. package/src/components/ModalPaneHost.tsx +141 -0
  62. package/src/components/StepDots.tsx +112 -0
  63. package/src/device/appVersion.ts +29 -8
  64. package/src/device/deviceModel.ts +21 -8
  65. package/src/features/cache.ts +1 -1
  66. package/src/haptics/expo-haptics.d.ts +31 -0
  67. package/src/haptics/haptics.ts +58 -0
  68. package/src/icons/IconRegistry.tsx +53 -0
  69. package/src/icons/WireIcon.tsx +85 -0
  70. package/src/icons/expoIcons.ts +165 -0
  71. package/src/icons/index.ts +18 -0
  72. package/src/icons/vocabulary.ts +154 -0
  73. package/src/index.ts +20 -0
  74. package/src/questionnaire/QuestionnaireGate.tsx +315 -164
  75. package/src/questionnaire/decision.ts +4 -2
  76. package/src/questionnaire/index.ts +3 -2
  77. package/src/questionnaire/steps.ts +261 -0
  78. package/src/questionnaire/types.ts +33 -12
  79. package/src/reviews/ReviewGate.tsx +250 -152
  80. package/src/reviews/index.ts +7 -3
  81. package/src/reviews/transport.ts +77 -3
  82. package/src/reviews/types.ts +13 -0
  83. package/src/session/persistedSession.ts +1 -1
  84. package/src/session-analytics/lifecycle.ts +1 -1
  85. package/src/session-analytics/useSessionStart.ts +2 -2
  86. package/src/showcase/FeatureShowcase.tsx +1 -1
  87. package/src/showcase/showcaseColors.ts +1 -1
  88. package/src/theme/mergeThemeOver.ts +27 -0
  89. package/src/types.ts +8 -0
  90. package/dist/transport-BeO_Brcu.d.mts +0 -40
  91. package/dist/transport-DLpd1v5_.d.ts +0 -40
@@ -1,9 +1,9 @@
1
1
  import React__default from 'react';
2
2
  import { O as OnboardingTheme } from '../types-BKfpdZzX.mjs';
3
- import { C as CoachmarkStorage } from '../types-GL_hQ0TN.mjs';
4
- import { W as WireFeatures, a as WireFeaturesConfig } from '../types-CMuOexw0.mjs';
5
- export { G as GateRules, a as GateSignals, e as evaluateGate, r as resolveRules } from '../decision-BzbiKwk3.mjs';
6
- import '../types-A6pTxIZV.mjs';
3
+ import { C as CoachmarkStorage } from '../types-BcmagF6K.mjs';
4
+ import { W as WireFeatures, a as WireFeaturesConfig } from '../types-CKFhyrMu.mjs';
5
+ export { G as GateRules, a as GateSignals, e as evaluateGate, r as resolveRules } from '../decision-Bl_M2y3r.mjs';
6
+ import '../types-CNUqMK0D.mjs';
7
7
 
8
8
  /**
9
9
  * The firing verdict - the SAME contract on both sides of the seam. `decideQuestionnaire`
@@ -24,13 +24,19 @@ interface QuestionnaireDecision {
24
24
  interface QuestionnaireDefinition {
25
25
  /** Gate key. Seen/last-shown/session keys derive from it (`wire_questionnaire_<id>_*`). */
26
26
  id: string;
27
- /** Popup heading. Defaults to a neutral prompt when absent. */
27
+ /**
28
+ * @deprecated NOT RENDERED since 2026-07-16. The gate is one-question-per-step, so each
29
+ * step's own question is its heading; a title above it was a second asking-text, repeated
30
+ * on all three steps and briefly drawn twice mid-slide. Malik cut it on device. The field
31
+ * stays in the type because tenants still send it and the server contract is unchanged —
32
+ * dropping it from the wire is a separate, server-side decision.
33
+ */
28
34
  title?: string;
29
- /** Label for the MANDATORY opinion box ("what do you think about the app"). */
35
+ /** Label for the SKIPPABLE opinion step ("what do you think about the app"). */
30
36
  opinion_label?: string;
31
- /** Label for the first OPTIONAL box ("help us improve"). */
37
+ /** Label for the SKIPPABLE "help us improve" step. */
32
38
  improve_label?: string;
33
- /** Label for the second OPTIONAL box ("suggestions"). */
39
+ /** Label for the REQUIRED "suggestions" step — the one answer the user cannot skip. */
34
40
  suggestions_label?: string;
35
41
  /** Master local switch. Default true. */
36
42
  enabled?: boolean;
@@ -50,13 +56,20 @@ interface QuestionnaireDefinition {
50
56
  */
51
57
  timeoutFallbackMs?: number | null;
52
58
  }
53
- /** The user's three answers. `opinion` is the only mandatory one. */
59
+ /**
60
+ * The user's three answers.
61
+ *
62
+ * ⚠️ `opinion` is TYPED as always-present because the wire shape says so, but since
63
+ * 2026-07-16 its step is skippable, so "" is a legitimate value. The live server still
64
+ * validates it with minLength=1 and 422s an empty one — a server-side fix this client
65
+ * depends on. See the REQUIRED_STEPS note in steps.ts.
66
+ */
54
67
  interface QuestionnaireAnswers {
55
- /** The mandatory "what do you think about the app" free text. */
68
+ /** The "what do you think about the app" free text. Skippable → may be "". */
56
69
  opinion: string;
57
- /** The optional "help us improve" free text. */
70
+ /** The "help us improve" free text. Skippable → may be "" / absent. */
58
71
  improve?: string;
59
- /** The optional "suggestions" free text. */
72
+ /** The "any improvements or feedback" free text. REQUIRED of the user in the gate. */
60
73
  suggestions?: string;
61
74
  }
62
75
  /** Non-PII correlation extras sent with a submission (the `meta` bucket in the POST body). */
@@ -121,7 +134,13 @@ interface QuestionnaireGateProps {
121
134
  onShown?: () => void;
122
135
  /** Partial theme merged over the active onboarding theme. */
123
136
  theme?: Partial<OnboardingTheme>;
124
- /** Show a Skip/Not now control. Default true (always skippable, never a dead end). */
137
+ /**
138
+ * Whether the WHOLE popup can be dismissed — backdrop tap and Android hardware back.
139
+ * Default true (never a dead end), and the only escape from the required last step, so
140
+ * turning it off traps a user who has nothing to say. That path is what fires
141
+ * `questionnaire_dismissed`; the in-card "Not now" link it used to name is gone.
142
+ * Per-step Skip is separate and is driven by `steps.ts`, not by this flag.
143
+ */
125
144
  dismissible?: boolean;
126
145
  }
127
146
  /** Options for `useQuestionnaireGate` - the WHEN decision. */
@@ -161,15 +180,40 @@ interface QuestionnaireGateController {
161
180
  }
162
181
 
163
182
  /**
164
- * QuestionnaireGate - the skippable pre-onboarding questionnaire popup.
183
+ * QuestionnaireGate - the multi-step questionnaire popup.
184
+ *
185
+ * ONE question per step. Steps 1-2 are skippable; the LAST step is REQUIRED (Malik's
186
+ * device test 2026-07-16: "i want at least something forced"). The user chooses which
187
+ * thing they tell us, not whether they tell us anything. `steps.ts` owns that rule and
188
+ * every transition as pure functions, unit-tested under node:test without mounting React;
189
+ * this component is a thin renderer over that machine.
190
+ *
191
+ * The CHROME (back icon + step dots) is a static header, deliberately NOT part of the
192
+ * sliding pane - see the four notes below. Only the question, its box and its controls move.
193
+ *
194
+ * - No title. The gate used to render `questionnaire.title` as a heading ON EVERY STEP,
195
+ * directly above the step's own question. Two asking-texts stacked, one of them the same
196
+ * string three times, and mid-slide the outgoing and incoming panes each drew their own
197
+ * copy so it was briefly on screen TWICE. Malik saw it on the last step, where the
198
+ * forced question most needs the focus. The question IS the heading now; it renders once.
199
+ * - No "Question 2 of 3" counter. `StepDots` says it without spending a line next to the
200
+ * question, which is what the user is supposed to be reading.
201
+ * - Back is a top-left icon, not a link in the button stack, and it is simply absent on
202
+ * step 1 rather than present-but-dead.
203
+ * - No "Not now". Per-step Skip is the only in-card opt-out. `questionnaire_dismissed`
204
+ * moved to the modal's dismiss path (backdrop tap / Android back) - see `handleDismissed`.
205
+ *
206
+ * The wire shape is UNCHANGED. `buildQuestionnaireSubmission` still builds the exact same
207
+ * `POST /v1/questionnaires/{id}/responses` body from the same three answers; a skipped
208
+ * step is simply an empty answer, which is what the builder already handled.
209
+ *
210
+ * ⚠️ This gate now REQUIRES the server to accept an empty `answers.opinion` - step 1 is
211
+ * skippable and the live endpoint still 422s a blank opinion, silently (the transport is
212
+ * fire-and-forget). See the REQUIRED_STEPS note in steps.ts. Server fix, not a client one.
165
213
  *
166
- * A centered popup (over the shared `CenteredModal` shell) with three inputs:
167
- * a MANDATORY multiline "what do you think about the app" box (~4 lines), and
168
- * two OPTIONAL smaller boxes ("help us improve" + "suggestions").
169
- * Send is disabled ONLY until the opinion box has text; the two optional boxes never gate it.
170
- * The gate is ALWAYS skippable (a "Not now"/"Skip" link plays the popup exit), so it is never
171
- * a dead end. On send it builds the contract body and fire-and-forgets it to the Wire server,
172
- * then shows a warm thank-you until the user taps the backdrop to close.
214
+ * Motion: `ModalPaneHost` slides step→step (and mirrors the slide on Back) and resizes the
215
+ * card; `StepDots` fills; `DoneCheck` plays the submit payoff. All shared, all reduce-motion
216
+ * aware. Haptics are lazy + optional (`haptics/haptics.ts`) and silently absent otherwise.
173
217
  *
174
218
  * The form NEVER asks for identity (no name / email / contact field): the host already
175
219
  * identifies the user via `sessionId` + `meta` (user_id, device_key, session_count), which is
@@ -187,8 +231,10 @@ declare const useQuestionnaireGate: ({ config: configProp, decision: decisionPro
187
231
  */
188
232
  declare const decideQuestionnaire: (local: QuestionnaireDecision, decision?: QuestionnaireDecision) => QuestionnaireDecision;
189
233
  /**
190
- * Build the `POST /v1/questionnaires/{id}/responses` body. `opinion` is the only mandatory
191
- * answer (trimmed); `improve`/`suggestions` are trimmed and sent as `null` when empty. The
234
+ * Build the `POST /v1/questionnaires/{id}/responses` body. `opinion` is always sent as a
235
+ * string (trimmed, possibly "" now that its step is skippable the live server still
236
+ * rejects that, see steps.ts REQUIRED_STEPS); `improve`/`suggestions` are trimmed and sent
237
+ * as `null` when empty. The
192
238
  * `meta` bucket carries the non-PII correlation fields (user_id / device_key / session_count),
193
239
  * each defaulting to null. Free text lives ONLY here, never in analytics events.
194
240
  */
@@ -1,9 +1,9 @@
1
1
  import React__default from 'react';
2
2
  import { O as OnboardingTheme } from '../types-BKfpdZzX.js';
3
- import { C as CoachmarkStorage } from '../types-GL_hQ0TN.js';
4
- import { W as WireFeatures, a as WireFeaturesConfig } from '../types-CMuOexw0.js';
5
- export { G as GateRules, a as GateSignals, e as evaluateGate, r as resolveRules } from '../decision-plDEOCkt.js';
6
- import '../types-BhpXJGlg.js';
3
+ import { C as CoachmarkStorage } from '../types-BcmagF6K.js';
4
+ import { W as WireFeatures, a as WireFeaturesConfig } from '../types-CKFhyrMu.js';
5
+ export { G as GateRules, a as GateSignals, e as evaluateGate, r as resolveRules } from '../decision-Cau5KmP6.js';
6
+ import '../types-Buj9Lw9t.js';
7
7
 
8
8
  /**
9
9
  * The firing verdict - the SAME contract on both sides of the seam. `decideQuestionnaire`
@@ -24,13 +24,19 @@ interface QuestionnaireDecision {
24
24
  interface QuestionnaireDefinition {
25
25
  /** Gate key. Seen/last-shown/session keys derive from it (`wire_questionnaire_<id>_*`). */
26
26
  id: string;
27
- /** Popup heading. Defaults to a neutral prompt when absent. */
27
+ /**
28
+ * @deprecated NOT RENDERED since 2026-07-16. The gate is one-question-per-step, so each
29
+ * step's own question is its heading; a title above it was a second asking-text, repeated
30
+ * on all three steps and briefly drawn twice mid-slide. Malik cut it on device. The field
31
+ * stays in the type because tenants still send it and the server contract is unchanged —
32
+ * dropping it from the wire is a separate, server-side decision.
33
+ */
28
34
  title?: string;
29
- /** Label for the MANDATORY opinion box ("what do you think about the app"). */
35
+ /** Label for the SKIPPABLE opinion step ("what do you think about the app"). */
30
36
  opinion_label?: string;
31
- /** Label for the first OPTIONAL box ("help us improve"). */
37
+ /** Label for the SKIPPABLE "help us improve" step. */
32
38
  improve_label?: string;
33
- /** Label for the second OPTIONAL box ("suggestions"). */
39
+ /** Label for the REQUIRED "suggestions" step — the one answer the user cannot skip. */
34
40
  suggestions_label?: string;
35
41
  /** Master local switch. Default true. */
36
42
  enabled?: boolean;
@@ -50,13 +56,20 @@ interface QuestionnaireDefinition {
50
56
  */
51
57
  timeoutFallbackMs?: number | null;
52
58
  }
53
- /** The user's three answers. `opinion` is the only mandatory one. */
59
+ /**
60
+ * The user's three answers.
61
+ *
62
+ * ⚠️ `opinion` is TYPED as always-present because the wire shape says so, but since
63
+ * 2026-07-16 its step is skippable, so "" is a legitimate value. The live server still
64
+ * validates it with minLength=1 and 422s an empty one — a server-side fix this client
65
+ * depends on. See the REQUIRED_STEPS note in steps.ts.
66
+ */
54
67
  interface QuestionnaireAnswers {
55
- /** The mandatory "what do you think about the app" free text. */
68
+ /** The "what do you think about the app" free text. Skippable → may be "". */
56
69
  opinion: string;
57
- /** The optional "help us improve" free text. */
70
+ /** The "help us improve" free text. Skippable → may be "" / absent. */
58
71
  improve?: string;
59
- /** The optional "suggestions" free text. */
72
+ /** The "any improvements or feedback" free text. REQUIRED of the user in the gate. */
60
73
  suggestions?: string;
61
74
  }
62
75
  /** Non-PII correlation extras sent with a submission (the `meta` bucket in the POST body). */
@@ -121,7 +134,13 @@ interface QuestionnaireGateProps {
121
134
  onShown?: () => void;
122
135
  /** Partial theme merged over the active onboarding theme. */
123
136
  theme?: Partial<OnboardingTheme>;
124
- /** Show a Skip/Not now control. Default true (always skippable, never a dead end). */
137
+ /**
138
+ * Whether the WHOLE popup can be dismissed — backdrop tap and Android hardware back.
139
+ * Default true (never a dead end), and the only escape from the required last step, so
140
+ * turning it off traps a user who has nothing to say. That path is what fires
141
+ * `questionnaire_dismissed`; the in-card "Not now" link it used to name is gone.
142
+ * Per-step Skip is separate and is driven by `steps.ts`, not by this flag.
143
+ */
125
144
  dismissible?: boolean;
126
145
  }
127
146
  /** Options for `useQuestionnaireGate` - the WHEN decision. */
@@ -161,15 +180,40 @@ interface QuestionnaireGateController {
161
180
  }
162
181
 
163
182
  /**
164
- * QuestionnaireGate - the skippable pre-onboarding questionnaire popup.
183
+ * QuestionnaireGate - the multi-step questionnaire popup.
184
+ *
185
+ * ONE question per step. Steps 1-2 are skippable; the LAST step is REQUIRED (Malik's
186
+ * device test 2026-07-16: "i want at least something forced"). The user chooses which
187
+ * thing they tell us, not whether they tell us anything. `steps.ts` owns that rule and
188
+ * every transition as pure functions, unit-tested under node:test without mounting React;
189
+ * this component is a thin renderer over that machine.
190
+ *
191
+ * The CHROME (back icon + step dots) is a static header, deliberately NOT part of the
192
+ * sliding pane - see the four notes below. Only the question, its box and its controls move.
193
+ *
194
+ * - No title. The gate used to render `questionnaire.title` as a heading ON EVERY STEP,
195
+ * directly above the step's own question. Two asking-texts stacked, one of them the same
196
+ * string three times, and mid-slide the outgoing and incoming panes each drew their own
197
+ * copy so it was briefly on screen TWICE. Malik saw it on the last step, where the
198
+ * forced question most needs the focus. The question IS the heading now; it renders once.
199
+ * - No "Question 2 of 3" counter. `StepDots` says it without spending a line next to the
200
+ * question, which is what the user is supposed to be reading.
201
+ * - Back is a top-left icon, not a link in the button stack, and it is simply absent on
202
+ * step 1 rather than present-but-dead.
203
+ * - No "Not now". Per-step Skip is the only in-card opt-out. `questionnaire_dismissed`
204
+ * moved to the modal's dismiss path (backdrop tap / Android back) - see `handleDismissed`.
205
+ *
206
+ * The wire shape is UNCHANGED. `buildQuestionnaireSubmission` still builds the exact same
207
+ * `POST /v1/questionnaires/{id}/responses` body from the same three answers; a skipped
208
+ * step is simply an empty answer, which is what the builder already handled.
209
+ *
210
+ * ⚠️ This gate now REQUIRES the server to accept an empty `answers.opinion` - step 1 is
211
+ * skippable and the live endpoint still 422s a blank opinion, silently (the transport is
212
+ * fire-and-forget). See the REQUIRED_STEPS note in steps.ts. Server fix, not a client one.
165
213
  *
166
- * A centered popup (over the shared `CenteredModal` shell) with three inputs:
167
- * a MANDATORY multiline "what do you think about the app" box (~4 lines), and
168
- * two OPTIONAL smaller boxes ("help us improve" + "suggestions").
169
- * Send is disabled ONLY until the opinion box has text; the two optional boxes never gate it.
170
- * The gate is ALWAYS skippable (a "Not now"/"Skip" link plays the popup exit), so it is never
171
- * a dead end. On send it builds the contract body and fire-and-forgets it to the Wire server,
172
- * then shows a warm thank-you until the user taps the backdrop to close.
214
+ * Motion: `ModalPaneHost` slides step→step (and mirrors the slide on Back) and resizes the
215
+ * card; `StepDots` fills; `DoneCheck` plays the submit payoff. All shared, all reduce-motion
216
+ * aware. Haptics are lazy + optional (`haptics/haptics.ts`) and silently absent otherwise.
173
217
  *
174
218
  * The form NEVER asks for identity (no name / email / contact field): the host already
175
219
  * identifies the user via `sessionId` + `meta` (user_id, device_key, session_count), which is
@@ -187,8 +231,10 @@ declare const useQuestionnaireGate: ({ config: configProp, decision: decisionPro
187
231
  */
188
232
  declare const decideQuestionnaire: (local: QuestionnaireDecision, decision?: QuestionnaireDecision) => QuestionnaireDecision;
189
233
  /**
190
- * Build the `POST /v1/questionnaires/{id}/responses` body. `opinion` is the only mandatory
191
- * answer (trimmed); `improve`/`suggestions` are trimmed and sent as `null` when empty. The
234
+ * Build the `POST /v1/questionnaires/{id}/responses` body. `opinion` is always sent as a
235
+ * string (trimmed, possibly "" now that its step is skippable the live server still
236
+ * rejects that, see steps.ts REQUIRED_STEPS); `improve`/`suggestions` are trimmed and sent
237
+ * as `null` when empty. The
192
238
  * `meta` bucket carries the non-PII correlation fields (user_id / device_key / session_count),
193
239
  * each defaulting to null. Free text lives ONLY here, never in analytics events.
194
240
  */