@wireai/activation 0.7.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.
- package/AGENTS.md +29 -5
- package/CHANGELOG.md +198 -6
- package/README.md +105 -10
- package/dist/analytics/index.d.mts +12 -8
- package/dist/analytics/index.d.ts +12 -8
- package/dist/analytics/index.js +525 -55
- package/dist/analytics/index.js.map +1 -1
- package/dist/analytics/index.mjs +525 -55
- 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-CxnP7gAa.d.ts → currentSession-61dcm3V-.d.ts} +19 -3
- package/dist/{currentSession-BJBB7i4-.d.mts → currentSession-Bs2JfTJ8.d.mts} +19 -3
- 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 +620 -13
- package/dist/index.d.ts +620 -13
- package/dist/index.js +1194 -369
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1015 -201
- 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-DzU-TqZz.d.ts +86 -0
- package/dist/transport-f5VVB5hH.d.mts +86 -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/analyticsFacade.ts +55 -10
- 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/context/deviceId.ts +43 -0
- package/src/device/appVersion.ts +29 -8
- package/src/device/deviceContext.ts +14 -1
- package/src/device/deviceModel.ts +106 -0
- 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 +22 -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 +77 -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,261 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* steps.ts — the pure multi-step machine behind the questionnaire popup.
|
|
3
|
+
*
|
|
4
|
+
* The gate asks ONE question per step. Skippability is per-step (`QuestionnaireStep.required`)
|
|
5
|
+
* and, after Malik's device test 2026-07-16, it is the INVERSE of what this kit shipped for a
|
|
6
|
+
* year: `opinion` (step 1) is SKIPPABLE — the old mandatory-opinion `canSend` gate is gone —
|
|
7
|
+
* and `suggestions` (the LAST step) is REQUIRED, because "i want at least something forced".
|
|
8
|
+
* The user picks WHICH thing they say; they do not get to say nothing at all.
|
|
9
|
+
*
|
|
10
|
+
* The step logic is the interesting part of the component, so it lives here as pure
|
|
11
|
+
* functions: no React, no react-native, no JSX. The gate is then a thin renderer over this
|
|
12
|
+
* state, and `steps.test.ts` can verify advance / skip / back / the required gate under
|
|
13
|
+
* node:test without mounting anything.
|
|
14
|
+
*
|
|
15
|
+
* The wire shape is NOT this module's business: it builds `QuestionnaireAnswers`, and
|
|
16
|
+
* `buildQuestionnaireSubmission` (decision.ts) still owns the POST body exactly as before.
|
|
17
|
+
*
|
|
18
|
+
* The step ORDER is the server contract's field order (opinion → improve → suggestions),
|
|
19
|
+
* so `toAnswers` can stay a plain projection and the submission never depends on how the
|
|
20
|
+
* user navigated.
|
|
21
|
+
*/
|
|
22
|
+
import type { QuestionnaireAnswers, QuestionnaireDefinition } from "./types";
|
|
23
|
+
|
|
24
|
+
/** The three answer fields, which are also the three steps. */
|
|
25
|
+
export type QuestionnaireStepKey = "opinion" | "improve" | "suggestions";
|
|
26
|
+
|
|
27
|
+
/** One question, fully resolved for rendering. */
|
|
28
|
+
export interface QuestionnaireStep {
|
|
29
|
+
/** The answer field this step fills. */
|
|
30
|
+
key: QuestionnaireStepKey;
|
|
31
|
+
/** The question shown to the user (tenant-configurable). */
|
|
32
|
+
label: string;
|
|
33
|
+
/** The greyed-out example inside the input. */
|
|
34
|
+
placeholder: string;
|
|
35
|
+
/**
|
|
36
|
+
* When true the step cannot be skipped and cannot be committed blank — the gate hides
|
|
37
|
+
* Skip and disables Send until there is non-whitespace text. Only `suggestions` (the
|
|
38
|
+
* last step) carries it; see `REQUIRED_STEPS`.
|
|
39
|
+
*/
|
|
40
|
+
required: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Which steps are mandatory. Malik, after testing on device 2026-07-16: "The any
|
|
45
|
+
* suggestions or feedback <the last step> is mandatory, i want at least something forced."
|
|
46
|
+
*
|
|
47
|
+
* Exactly one step is forced, and it is the LAST one, which is the only placement that
|
|
48
|
+
* works: the user has already been offered two cheap ways to say something and declined
|
|
49
|
+
* them, so the mandatory box lands as "then tell us anything" rather than as a toll gate
|
|
50
|
+
* in front of a popup they never asked for. Forcing step 1 (the old behaviour) taxed the
|
|
51
|
+
* arrival; forcing step 3 taxes the exit, after the cost is sunk.
|
|
52
|
+
*
|
|
53
|
+
* ⚠️ SERVER DEPENDENCY: this is the exact INVERSE of what the live server enforces today.
|
|
54
|
+
* `POST /v1/questionnaires/{id}/responses` validates `answers.opinion` with minLength=1 and
|
|
55
|
+
* 422s an empty one. Since step 1 is now skippable, a skipped step 1 produces `opinion: ""`
|
|
56
|
+
* and the server rejects the whole body — INCLUDING the suggestions text this module just
|
|
57
|
+
* forced out of the user. The transport is fire-and-forget (it never reads the response),
|
|
58
|
+
* so that rejection is SILENT: the user sees the thank-you either way. This client is
|
|
59
|
+
* therefore correct only once the server drops `opinion`'s minLength. Do not paper over it
|
|
60
|
+
* here — no placeholder opinion, no field reordering, no client-side guard.
|
|
61
|
+
*/
|
|
62
|
+
export const REQUIRED_STEPS: Record<QuestionnaireStepKey, boolean> = {
|
|
63
|
+
opinion: false,
|
|
64
|
+
improve: false,
|
|
65
|
+
suggestions: true,
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Fallback questions used when the tenant configures no labels.
|
|
70
|
+
*
|
|
71
|
+
* NOTE the copy change: the old defaults tagged `improve`/`suggestions` "(optional)".
|
|
72
|
+
* That was already wrong once step 1 stopped being mandatory, and it is wronger now that
|
|
73
|
+
* `suggestions` is the one REQUIRED step. Requiredness is carried by the CONTROLS — Skip
|
|
74
|
+
* is absent and Send is disabled on a required step — not by label text, which is tenant-
|
|
75
|
+
* configurable and would drift out of sync with `REQUIRED_STEPS` the moment it changed.
|
|
76
|
+
*/
|
|
77
|
+
export const DEFAULT_STEP_LABELS: Record<QuestionnaireStepKey, string> = {
|
|
78
|
+
opinion: "What do you think about the app?",
|
|
79
|
+
improve: "How can we improve?",
|
|
80
|
+
suggestions: "Any suggestions?",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** Per-field placeholders, carried over unchanged from the single-screen form. */
|
|
84
|
+
export const STEP_PLACEHOLDERS: Record<QuestionnaireStepKey, string> = {
|
|
85
|
+
opinion: "e.g. it feels fast and clear",
|
|
86
|
+
improve: "e.g. fewer steps to start",
|
|
87
|
+
suggestions: "e.g. a dark theme",
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/** Which way the last move went — drives the slide direction (spatial consistency). */
|
|
91
|
+
export type StepDirection = "forward" | "backward";
|
|
92
|
+
|
|
93
|
+
/** Answers captured so far, keyed by step. A skipped step records "". */
|
|
94
|
+
export type StepDrafts = Partial<Record<QuestionnaireStepKey, string>>;
|
|
95
|
+
|
|
96
|
+
/** The whole machine state. Immutable: every transition returns a new object. */
|
|
97
|
+
export interface QuestionnaireStepState {
|
|
98
|
+
/** Index into the step list. */
|
|
99
|
+
index: number;
|
|
100
|
+
/** What has been typed/skipped so far. */
|
|
101
|
+
drafts: StepDrafts;
|
|
102
|
+
/** Direction of the last transition, for the pane slide. */
|
|
103
|
+
direction: StepDirection;
|
|
104
|
+
/** True once the LAST step is answered or skipped — the gate submits and thanks. */
|
|
105
|
+
done: boolean;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the tenant's labels into the ordered step list. All three steps always render:
|
|
110
|
+
* the server contract is a fixed 3-field capture, not an arbitrary question builder.
|
|
111
|
+
*/
|
|
112
|
+
export const buildQuestionnaireSteps = (
|
|
113
|
+
questionnaire: Pick<
|
|
114
|
+
QuestionnaireDefinition,
|
|
115
|
+
"opinion_label" | "improve_label" | "suggestions_label"
|
|
116
|
+
>,
|
|
117
|
+
): QuestionnaireStep[] => [
|
|
118
|
+
{
|
|
119
|
+
key: "opinion",
|
|
120
|
+
label: questionnaire.opinion_label ?? DEFAULT_STEP_LABELS.opinion,
|
|
121
|
+
placeholder: STEP_PLACEHOLDERS.opinion,
|
|
122
|
+
required: REQUIRED_STEPS.opinion,
|
|
123
|
+
},
|
|
124
|
+
{
|
|
125
|
+
key: "improve",
|
|
126
|
+
label: questionnaire.improve_label ?? DEFAULT_STEP_LABELS.improve,
|
|
127
|
+
placeholder: STEP_PLACEHOLDERS.improve,
|
|
128
|
+
required: REQUIRED_STEPS.improve,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
key: "suggestions",
|
|
132
|
+
label: questionnaire.suggestions_label ?? DEFAULT_STEP_LABELS.suggestions,
|
|
133
|
+
placeholder: STEP_PLACEHOLDERS.suggestions,
|
|
134
|
+
required: REQUIRED_STEPS.suggestions,
|
|
135
|
+
},
|
|
136
|
+
];
|
|
137
|
+
|
|
138
|
+
/** The starting state: first question, nothing answered. */
|
|
139
|
+
export const initialStepState = (): QuestionnaireStepState => ({
|
|
140
|
+
index: 0,
|
|
141
|
+
drafts: {},
|
|
142
|
+
direction: "forward",
|
|
143
|
+
done: false,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
/** The step currently on screen, or undefined once the list is exhausted. */
|
|
147
|
+
export const currentStep = (
|
|
148
|
+
steps: readonly QuestionnaireStep[],
|
|
149
|
+
state: QuestionnaireStepState,
|
|
150
|
+
): QuestionnaireStep | undefined => steps[state.index];
|
|
151
|
+
|
|
152
|
+
/** Whether `index` is the final step (the one whose commit submits). */
|
|
153
|
+
export const isLastStep = (steps: readonly QuestionnaireStep[], index: number): boolean =>
|
|
154
|
+
index >= steps.length - 1;
|
|
155
|
+
|
|
156
|
+
/** Back navigation is available on every step except the first. */
|
|
157
|
+
export const canGoBack = (state: QuestionnaireStepState): boolean => state.index > 0;
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Whether the step on screen may be skipped — false on a required step, which is how the
|
|
161
|
+
* gate knows to render no Skip control at all rather than a Skip that silently does nothing.
|
|
162
|
+
*/
|
|
163
|
+
export const canSkipStep = (
|
|
164
|
+
steps: readonly QuestionnaireStep[],
|
|
165
|
+
state: QuestionnaireStepState,
|
|
166
|
+
): boolean => !(steps[state.index]?.required ?? false);
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Whether `value` satisfies the step on screen — i.e. whether committing it will actually
|
|
170
|
+
* move. Optional steps accept anything (including ""); a required step needs non-whitespace
|
|
171
|
+
* text, because " " is not "at least something". The gate reads this to disable Send.
|
|
172
|
+
*
|
|
173
|
+
* The SAME predicate `commitStep` enforces, so a disabled Send and a refused commit can
|
|
174
|
+
* never disagree about what counts as answered.
|
|
175
|
+
*/
|
|
176
|
+
export const canCommitStep = (
|
|
177
|
+
steps: readonly QuestionnaireStep[],
|
|
178
|
+
state: QuestionnaireStepState,
|
|
179
|
+
value: string,
|
|
180
|
+
): boolean => {
|
|
181
|
+
const step = steps[state.index];
|
|
182
|
+
if (!step) return false;
|
|
183
|
+
return !step.required || value.trim().length > 0;
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Record `value` for the CURRENT step WITHOUT navigating.
|
|
188
|
+
*
|
|
189
|
+
* This is the shared "the text on screen is now part of the answers" primitive: `commitStep`
|
|
190
|
+
* is exactly this plus the forward move, and the gate's Back does this before `goBack`. It
|
|
191
|
+
* exists because the gate's inputs are UNCONTROLLED, so the step on screen holds its text
|
|
192
|
+
* only in the component's draft ref until something folds it in here — meaning EVERY way of
|
|
193
|
+
* leaving a step has to stash first or the text is silently destroyed.
|
|
194
|
+
*/
|
|
195
|
+
export const stashDraft = (
|
|
196
|
+
steps: readonly QuestionnaireStep[],
|
|
197
|
+
state: QuestionnaireStepState,
|
|
198
|
+
value: string,
|
|
199
|
+
): QuestionnaireStepState => {
|
|
200
|
+
const step = steps[state.index];
|
|
201
|
+
if (!step) return state;
|
|
202
|
+
return { ...state, drafts: { ...state.drafts, [step.key]: value } };
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Record `value` for the CURRENT step and move on. On the last step this sets `done`
|
|
207
|
+
* (the gate reads that as "submit now") and leaves the index parked, so `toAnswers` is
|
|
208
|
+
* called with every draft present.
|
|
209
|
+
*
|
|
210
|
+
* A required step refuses a blank `value` and returns the state UNCHANGED. That is the
|
|
211
|
+
* single enforcement point for the mandatory last step: `skipStep` is defined as committing
|
|
212
|
+
* "", so it becomes a no-op there for free, and the gate's disabled Send is belt-and-braces
|
|
213
|
+
* over a machine that already cannot be walked past.
|
|
214
|
+
*/
|
|
215
|
+
export const commitStep = (
|
|
216
|
+
steps: readonly QuestionnaireStep[],
|
|
217
|
+
state: QuestionnaireStepState,
|
|
218
|
+
value: string,
|
|
219
|
+
): QuestionnaireStepState => {
|
|
220
|
+
const step = steps[state.index];
|
|
221
|
+
if (!step) return state;
|
|
222
|
+
if (!canCommitStep(steps, state, value)) return state;
|
|
223
|
+
const { drafts } = stashDraft(steps, state, value);
|
|
224
|
+
const last = isLastStep(steps, state.index);
|
|
225
|
+
return {
|
|
226
|
+
index: last ? state.index : state.index + 1,
|
|
227
|
+
drafts,
|
|
228
|
+
direction: "forward",
|
|
229
|
+
done: last,
|
|
230
|
+
};
|
|
231
|
+
};
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Skip the current step. Deliberately the SAME transition as answering with nothing —
|
|
235
|
+
* "skipped" and "left blank" are one concept, so there is one code path and no way for
|
|
236
|
+
* the two to drift. On a REQUIRED step that identity is what makes skipping impossible:
|
|
237
|
+
* committing "" is refused, so this returns the state unchanged.
|
|
238
|
+
*/
|
|
239
|
+
export const skipStep = (
|
|
240
|
+
steps: readonly QuestionnaireStep[],
|
|
241
|
+
state: QuestionnaireStepState,
|
|
242
|
+
): QuestionnaireStepState => commitStep(steps, state, "");
|
|
243
|
+
|
|
244
|
+
/** Go back one step, keeping every draft so the user's earlier text is still there. */
|
|
245
|
+
export const goBack = (state: QuestionnaireStepState): QuestionnaireStepState =>
|
|
246
|
+
state.index <= 0 ? state : { ...state, index: state.index - 1, direction: "backward" };
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Project the drafts onto the answers contract. Missing/skipped steps become "" — the
|
|
250
|
+
* exact input `buildQuestionnaireSubmission` already expects, which trims and maps the
|
|
251
|
+
* two optional fields to null. The wire shape is unchanged by the multi-step rework.
|
|
252
|
+
*
|
|
253
|
+
* `opinion` can now legitimately be "" (step 1 is skippable), which the live server still
|
|
254
|
+
* rejects with a 422 — see the REQUIRED_STEPS note above. That is a server fix, not a
|
|
255
|
+
* reason to fabricate a value here.
|
|
256
|
+
*/
|
|
257
|
+
export const toAnswers = (drafts: StepDrafts): QuestionnaireAnswers => ({
|
|
258
|
+
opinion: drafts.opinion ?? "",
|
|
259
|
+
improve: drafts.improve ?? "",
|
|
260
|
+
suggestions: drafts.suggestions ?? "",
|
|
261
|
+
});
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Shared types for the questionnaire module (the skippable pre-onboarding popup).
|
|
3
3
|
*
|
|
4
|
-
* The gate asks a short set of questions in a centered popup
|
|
5
|
-
* "what do you think about the app" (the opinion),
|
|
6
|
-
*
|
|
4
|
+
* The gate asks a short set of questions in a centered popup, ONE per step: a SKIPPABLE
|
|
5
|
+
* "what do you think about the app" (the opinion), a SKIPPABLE "help us improve", and a
|
|
6
|
+
* REQUIRED "suggestions" as the last step (Malik 2026-07-16: "i want at least something
|
|
7
|
+
* forced"). Which step is forced lives in `steps.ts` (`REQUIRED_STEPS`), not here.
|
|
8
|
+
* WHEN the gate shows is decided by local rules OR a
|
|
7
9
|
* server-provided decision (the AI seam), gated by the `questionnaire` master switch and
|
|
8
10
|
* shown at most once per user per questionnaire `id`. Dependency-free type surface.
|
|
9
11
|
*/
|
|
@@ -30,13 +32,19 @@ export interface QuestionnaireDecision {
|
|
|
30
32
|
export interface QuestionnaireDefinition {
|
|
31
33
|
/** Gate key. Seen/last-shown/session keys derive from it (`wire_questionnaire_<id>_*`). */
|
|
32
34
|
id: string;
|
|
33
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* @deprecated NOT RENDERED since 2026-07-16. The gate is one-question-per-step, so each
|
|
37
|
+
* step's own question is its heading; a title above it was a second asking-text, repeated
|
|
38
|
+
* on all three steps and briefly drawn twice mid-slide. Malik cut it on device. The field
|
|
39
|
+
* stays in the type because tenants still send it and the server contract is unchanged —
|
|
40
|
+
* dropping it from the wire is a separate, server-side decision.
|
|
41
|
+
*/
|
|
34
42
|
title?: string;
|
|
35
|
-
/** Label for the
|
|
43
|
+
/** Label for the SKIPPABLE opinion step ("what do you think about the app"). */
|
|
36
44
|
opinion_label?: string;
|
|
37
|
-
/** Label for the
|
|
45
|
+
/** Label for the SKIPPABLE "help us improve" step. */
|
|
38
46
|
improve_label?: string;
|
|
39
|
-
/** Label for the
|
|
47
|
+
/** Label for the REQUIRED "suggestions" step — the one answer the user cannot skip. */
|
|
40
48
|
suggestions_label?: string;
|
|
41
49
|
|
|
42
50
|
// ── Local fallback firing rules (used only when no server decision is passed) ──
|
|
@@ -59,13 +67,20 @@ export interface QuestionnaireDefinition {
|
|
|
59
67
|
timeoutFallbackMs?: number | null;
|
|
60
68
|
}
|
|
61
69
|
|
|
62
|
-
/**
|
|
70
|
+
/**
|
|
71
|
+
* The user's three answers.
|
|
72
|
+
*
|
|
73
|
+
* ⚠️ `opinion` is TYPED as always-present because the wire shape says so, but since
|
|
74
|
+
* 2026-07-16 its step is skippable, so "" is a legitimate value. The live server still
|
|
75
|
+
* validates it with minLength=1 and 422s an empty one — a server-side fix this client
|
|
76
|
+
* depends on. See the REQUIRED_STEPS note in steps.ts.
|
|
77
|
+
*/
|
|
63
78
|
export interface QuestionnaireAnswers {
|
|
64
|
-
/** The
|
|
79
|
+
/** The "what do you think about the app" free text. Skippable → may be "". */
|
|
65
80
|
opinion: string;
|
|
66
|
-
/** The
|
|
81
|
+
/** The "help us improve" free text. Skippable → may be "" / absent. */
|
|
67
82
|
improve?: string;
|
|
68
|
-
/** The
|
|
83
|
+
/** The "any improvements or feedback" free text. REQUIRED of the user in the gate. */
|
|
69
84
|
suggestions?: string;
|
|
70
85
|
}
|
|
71
86
|
|
|
@@ -140,7 +155,13 @@ export interface QuestionnaireGateProps {
|
|
|
140
155
|
onShown?: () => void;
|
|
141
156
|
/** Partial theme merged over the active onboarding theme. */
|
|
142
157
|
theme?: Partial<import("../theme/types").OnboardingTheme>;
|
|
143
|
-
/**
|
|
158
|
+
/**
|
|
159
|
+
* Whether the WHOLE popup can be dismissed — backdrop tap and Android hardware back.
|
|
160
|
+
* Default true (never a dead end), and the only escape from the required last step, so
|
|
161
|
+
* turning it off traps a user who has nothing to say. That path is what fires
|
|
162
|
+
* `questionnaire_dismissed`; the in-card "Not now" link it used to name is gone.
|
|
163
|
+
* Per-step Skip is separate and is driven by `steps.ts`, not by this flag.
|
|
164
|
+
*/
|
|
144
165
|
dismissible?: boolean;
|
|
145
166
|
}
|
|
146
167
|
|