@rocapine/react-native-onboarding 1.11.1 → 1.13.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/dist/evaluateCondition.d.ts +6 -0
- package/dist/evaluateCondition.d.ts.map +1 -0
- package/dist/evaluateCondition.js +48 -0
- package/dist/evaluateCondition.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +12 -1
- package/dist/index.js.map +1 -1
- package/dist/infra/provider/OnboardingProvider.d.ts +2 -0
- package/dist/infra/provider/OnboardingProvider.d.ts.map +1 -1
- package/dist/infra/provider/OnboardingProvider.js +8 -0
- package/dist/infra/provider/OnboardingProvider.js.map +1 -1
- package/dist/onboarding-example.d.ts +174 -0
- package/dist/onboarding-example.d.ts.map +1 -1
- package/dist/onboarding-example.js +50 -0
- package/dist/onboarding-example.js.map +1 -1
- package/dist/resolveNextStepNumber.d.ts +11 -0
- package/dist/resolveNextStepNumber.d.ts.map +1 -0
- package/dist/resolveNextStepNumber.js +39 -0
- package/dist/resolveNextStepNumber.js.map +1 -0
- package/dist/steps/Carousel/types.d.ts +29 -8
- package/dist/steps/Carousel/types.d.ts.map +1 -1
- package/dist/steps/Carousel/types.js +1 -8
- package/dist/steps/Carousel/types.js.map +1 -1
- package/dist/steps/Commitment/types.d.ts +29 -8
- package/dist/steps/Commitment/types.d.ts.map +1 -1
- package/dist/steps/Commitment/types.js +1 -8
- package/dist/steps/Commitment/types.js.map +1 -1
- package/dist/steps/ComposableScreen/elements/BaseBoxProps.d.ts +67 -0
- package/dist/steps/ComposableScreen/elements/BaseBoxProps.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/BaseBoxProps.js +15 -1
- package/dist/steps/ComposableScreen/elements/BaseBoxProps.js.map +1 -1
- package/dist/steps/ComposableScreen/elements/ButtonElement.d.ts +28 -1
- package/dist/steps/ComposableScreen/elements/ButtonElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/CarouselElement.d.ts +27 -0
- package/dist/steps/ComposableScreen/elements/CarouselElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/CheckboxGroupElement.d.ts +27 -0
- package/dist/steps/ComposableScreen/elements/CheckboxGroupElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/DatePickerElement.d.ts +27 -0
- package/dist/steps/ComposableScreen/elements/DatePickerElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/IconElement.d.ts +27 -0
- package/dist/steps/ComposableScreen/elements/IconElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/ImageElement.d.ts +27 -0
- package/dist/steps/ComposableScreen/elements/ImageElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/InputElement.d.ts +30 -3
- package/dist/steps/ComposableScreen/elements/InputElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/LottieElement.d.ts +27 -0
- package/dist/steps/ComposableScreen/elements/LottieElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/RadioGroupElement.d.ts +27 -0
- package/dist/steps/ComposableScreen/elements/RadioGroupElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/RiveElement.d.ts +27 -0
- package/dist/steps/ComposableScreen/elements/RiveElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/StackElement.d.ts +27 -0
- package/dist/steps/ComposableScreen/elements/StackElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/TextElement.d.ts +28 -1
- package/dist/steps/ComposableScreen/elements/TextElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/VideoElement.d.ts +27 -0
- package/dist/steps/ComposableScreen/elements/VideoElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/types.d.ts +31 -6
- package/dist/steps/ComposableScreen/types.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/types.js +3 -8
- package/dist/steps/ComposableScreen/types.js.map +1 -1
- package/dist/steps/Loader/types.d.ts +29 -8
- package/dist/steps/Loader/types.d.ts.map +1 -1
- package/dist/steps/Loader/types.js +1 -8
- package/dist/steps/Loader/types.js.map +1 -1
- package/dist/steps/MediaContent/types.d.ts +29 -8
- package/dist/steps/MediaContent/types.d.ts.map +1 -1
- package/dist/steps/MediaContent/types.js +1 -8
- package/dist/steps/MediaContent/types.js.map +1 -1
- package/dist/steps/Picker/types.d.ts +30 -8
- package/dist/steps/Picker/types.d.ts.map +1 -1
- package/dist/steps/Picker/types.js +2 -8
- package/dist/steps/Picker/types.js.map +1 -1
- package/dist/steps/Question/types.d.ts +30 -8
- package/dist/steps/Question/types.d.ts.map +1 -1
- package/dist/steps/Question/types.js +2 -8
- package/dist/steps/Question/types.js.map +1 -1
- package/dist/steps/Ratings/types.d.ts +29 -8
- package/dist/steps/Ratings/types.d.ts.map +1 -1
- package/dist/steps/Ratings/types.js +1 -8
- package/dist/steps/Ratings/types.js.map +1 -1
- package/dist/steps/common.types.d.ts +109 -0
- package/dist/steps/common.types.d.ts.map +1 -1
- package/dist/steps/common.types.js +52 -1
- package/dist/steps/common.types.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/types.d.ts.map +1 -1
- package/package.json +5 -2
- package/src/__tests__/evaluateCondition.test.ts +167 -0
- package/src/__tests__/resolveNextStepNumber.test.ts +309 -0
- package/src/evaluateCondition.ts +50 -0
- package/src/index.ts +19 -0
- package/src/infra/provider/OnboardingProvider.tsx +11 -1
- package/src/onboarding-example.ts +50 -0
- package/src/resolveNextStepNumber.ts +41 -0
- package/src/steps/Carousel/types.ts +2 -9
- package/src/steps/Commitment/types.ts +2 -9
- package/src/steps/ComposableScreen/elements/BaseBoxProps.ts +42 -0
- package/src/steps/ComposableScreen/types.ts +4 -10
- package/src/steps/Loader/types.ts +2 -9
- package/src/steps/MediaContent/types.ts +2 -14
- package/src/steps/Picker/types.ts +3 -9
- package/src/steps/Question/types.ts +3 -9
- package/src/steps/Ratings/types.ts +2 -9
- package/src/steps/common.types.ts +72 -0
- package/src/types.ts +3 -0
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { resolveNextStepNumber } from "../resolveNextStepNumber";
|
|
3
|
+
import type { BaseStepType } from "../types";
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
|
|
9
|
+
function makeStep(id: string, overrides: Partial<BaseStepType> = {}): BaseStepType {
|
|
10
|
+
return {
|
|
11
|
+
id,
|
|
12
|
+
type: "Question",
|
|
13
|
+
name: id,
|
|
14
|
+
displayProgressHeader: true,
|
|
15
|
+
nextStep: null,
|
|
16
|
+
...overrides,
|
|
17
|
+
} as BaseStepType;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const steps = [
|
|
21
|
+
makeStep("s1"),
|
|
22
|
+
makeStep("s2"),
|
|
23
|
+
makeStep("s3"),
|
|
24
|
+
makeStep("s4"),
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Linear progression (nextStep = null)
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
|
|
31
|
+
describe("linear progression", () => {
|
|
32
|
+
it("returns 2 from step 1", () => expect(resolveNextStepNumber(steps[0], {}, steps)).toBe(2));
|
|
33
|
+
it("returns 3 from step 2", () => expect(resolveNextStepNumber(steps[1], {}, steps)).toBe(3));
|
|
34
|
+
it("returns null from last step", () => expect(resolveNextStepNumber(steps[3], {}, steps)).toBe(null));
|
|
35
|
+
it("returns null when step not found in array", () => {
|
|
36
|
+
const orphan = makeStep("orphan");
|
|
37
|
+
expect(resolveNextStepNumber(orphan, {}, steps)).toBe(null);
|
|
38
|
+
});
|
|
39
|
+
it("returns null for single-step array", () => {
|
|
40
|
+
const single = [makeStep("only")];
|
|
41
|
+
expect(resolveNextStepNumber(single[0], {}, single)).toBe(null);
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Branching — condition match
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
describe("branch — first matching branch wins", () => {
|
|
50
|
+
it("routes to targetStepId of first matching branch", () => {
|
|
51
|
+
const step = makeStep("s1", {
|
|
52
|
+
nextStep: {
|
|
53
|
+
defaultTargetStepId: "s2",
|
|
54
|
+
branches: [
|
|
55
|
+
{ condition: { variable: "gender", operator: "eq", value: "female" }, targetStepId: "s4" },
|
|
56
|
+
{ condition: { variable: "gender", operator: "eq", value: "male" }, targetStepId: "s3" },
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
expect(resolveNextStepNumber(step, { gender: "male" }, steps)).toBe(3);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("first branch wins even if second also matches", () => {
|
|
64
|
+
const step = makeStep("s1", {
|
|
65
|
+
nextStep: {
|
|
66
|
+
defaultTargetStepId: "s2",
|
|
67
|
+
branches: [
|
|
68
|
+
{ condition: { variable: "age", operator: "gt", value: 10 }, targetStepId: "s3" },
|
|
69
|
+
{ condition: { variable: "age", operator: "gt", value: 5 }, targetStepId: "s4" },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
expect(resolveNextStepNumber(step, { age: 20 }, steps)).toBe(3);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("skips non-matching branch, takes second", () => {
|
|
77
|
+
const step = makeStep("s1", {
|
|
78
|
+
nextStep: {
|
|
79
|
+
defaultTargetStepId: "s2",
|
|
80
|
+
branches: [
|
|
81
|
+
{ condition: { variable: "gender", operator: "eq", value: "female" }, targetStepId: "s4" },
|
|
82
|
+
{ condition: { variable: "gender", operator: "eq", value: "male" }, targetStepId: "s3" },
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
expect(resolveNextStepNumber(step, { gender: "female" }, steps)).toBe(4);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
// Branching — unconditional branch (null condition)
|
|
92
|
+
// ---------------------------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
describe("branch — null condition (unconditional)", () => {
|
|
95
|
+
it("always matches null condition", () => {
|
|
96
|
+
const step = makeStep("s1", {
|
|
97
|
+
nextStep: {
|
|
98
|
+
defaultTargetStepId: "s2",
|
|
99
|
+
branches: [
|
|
100
|
+
{ condition: null, targetStepId: "s4" },
|
|
101
|
+
],
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
expect(resolveNextStepNumber(step, {}, steps)).toBe(4);
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("null condition after a non-matching branch acts as catch-all", () => {
|
|
108
|
+
const step = makeStep("s1", {
|
|
109
|
+
nextStep: {
|
|
110
|
+
defaultTargetStepId: "s2",
|
|
111
|
+
branches: [
|
|
112
|
+
{ condition: { variable: "age", operator: "gt", value: 100 }, targetStepId: "s3" },
|
|
113
|
+
{ condition: null, targetStepId: "s4" },
|
|
114
|
+
],
|
|
115
|
+
},
|
|
116
|
+
});
|
|
117
|
+
expect(resolveNextStepNumber(step, { age: 25 }, steps)).toBe(4);
|
|
118
|
+
});
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
// Branching — defaultTargetStepId fallback
|
|
123
|
+
// ---------------------------------------------------------------------------
|
|
124
|
+
|
|
125
|
+
describe("branch — defaultTargetStepId fallback", () => {
|
|
126
|
+
it("uses defaultTargetStepId when no branch matches", () => {
|
|
127
|
+
const step = makeStep("s1", {
|
|
128
|
+
nextStep: {
|
|
129
|
+
defaultTargetStepId: "s3",
|
|
130
|
+
branches: [
|
|
131
|
+
{ condition: { variable: "x", operator: "eq", value: "never" }, targetStepId: "s4" },
|
|
132
|
+
],
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
expect(resolveNextStepNumber(step, { x: "something_else" }, steps)).toBe(3);
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
it("defaultTargetStepId with empty branches array still resolves", () => {
|
|
139
|
+
const step = makeStep("s1", {
|
|
140
|
+
nextStep: {
|
|
141
|
+
defaultTargetStepId: "s4",
|
|
142
|
+
branches: [],
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
expect(resolveNextStepNumber(step, {}, steps)).toBe(4);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it("falls back to linear when defaultTargetStepId not in steps", () => {
|
|
149
|
+
const step = makeStep("s1", {
|
|
150
|
+
nextStep: {
|
|
151
|
+
defaultTargetStepId: "does-not-exist",
|
|
152
|
+
branches: [],
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
expect(resolveNextStepNumber(step, {}, steps)).toBe(2);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// Branching — branch targetStepId not found
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
|
|
163
|
+
describe("branch — targetStepId not found", () => {
|
|
164
|
+
it("skips branch with unknown targetStepId, tries next branch", () => {
|
|
165
|
+
const step = makeStep("s1", {
|
|
166
|
+
nextStep: {
|
|
167
|
+
defaultTargetStepId: "s2",
|
|
168
|
+
branches: [
|
|
169
|
+
{ condition: { variable: "x", operator: "eq", value: "y" }, targetStepId: "ghost" },
|
|
170
|
+
{ condition: { variable: "x", operator: "eq", value: "y" }, targetStepId: "s3" },
|
|
171
|
+
],
|
|
172
|
+
},
|
|
173
|
+
});
|
|
174
|
+
expect(resolveNextStepNumber(step, { x: "y" }, steps)).toBe(3);
|
|
175
|
+
});
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Branching — compound conditions in branches
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
describe("branch — compound conditions", () => {
|
|
183
|
+
it("AND group in branch condition", () => {
|
|
184
|
+
const step = makeStep("s1", {
|
|
185
|
+
nextStep: {
|
|
186
|
+
defaultTargetStepId: "s2",
|
|
187
|
+
branches: [
|
|
188
|
+
{
|
|
189
|
+
condition: {
|
|
190
|
+
logic: "and",
|
|
191
|
+
conditions: [
|
|
192
|
+
{ variable: "age", operator: "gte", value: 18 },
|
|
193
|
+
{ variable: "gender", operator: "eq", value: "female" },
|
|
194
|
+
],
|
|
195
|
+
},
|
|
196
|
+
targetStepId: "s4",
|
|
197
|
+
},
|
|
198
|
+
],
|
|
199
|
+
},
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
expect(resolveNextStepNumber(step, { age: 25, gender: "female" }, steps)).toBe(4);
|
|
203
|
+
expect(resolveNextStepNumber(step, { age: 15, gender: "female" }, steps)).toBe(2);
|
|
204
|
+
expect(resolveNextStepNumber(step, { age: 25, gender: "male" }, steps)).toBe(2);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("OR group in branch condition", () => {
|
|
208
|
+
const step = makeStep("s1", {
|
|
209
|
+
nextStep: {
|
|
210
|
+
defaultTargetStepId: "s2",
|
|
211
|
+
branches: [
|
|
212
|
+
{
|
|
213
|
+
condition: {
|
|
214
|
+
logic: "or",
|
|
215
|
+
conditions: [
|
|
216
|
+
{ variable: "vip", operator: "eq", value: "true" },
|
|
217
|
+
{ variable: "age", operator: "lt", value: 13 },
|
|
218
|
+
],
|
|
219
|
+
},
|
|
220
|
+
targetStepId: "s3",
|
|
221
|
+
},
|
|
222
|
+
],
|
|
223
|
+
},
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
expect(resolveNextStepNumber(step, { vip: "true", age: 25 }, steps)).toBe(3);
|
|
227
|
+
expect(resolveNextStepNumber(step, { vip: "false", age: 10 }, steps)).toBe(3);
|
|
228
|
+
expect(resolveNextStepNumber(step, { vip: "false", age: 20 }, steps)).toBe(2);
|
|
229
|
+
});
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
// Self-loop guard
|
|
234
|
+
// ---------------------------------------------------------------------------
|
|
235
|
+
|
|
236
|
+
describe("self-loop guard", () => {
|
|
237
|
+
it("skips branch where targetStepId equals currentStep.id", () => {
|
|
238
|
+
const step = makeStep("s1", {
|
|
239
|
+
nextStep: {
|
|
240
|
+
defaultTargetStepId: "s2",
|
|
241
|
+
branches: [
|
|
242
|
+
{ condition: null, targetStepId: "s1" }, // would self-loop
|
|
243
|
+
{ condition: null, targetStepId: "s3" },
|
|
244
|
+
],
|
|
245
|
+
},
|
|
246
|
+
});
|
|
247
|
+
expect(resolveNextStepNumber(step, {}, steps)).toBe(3);
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it("falls back to linear when only self-targeting branch matches", () => {
|
|
251
|
+
const step = makeStep("s1", {
|
|
252
|
+
nextStep: {
|
|
253
|
+
defaultTargetStepId: "s2",
|
|
254
|
+
branches: [
|
|
255
|
+
{ condition: null, targetStepId: "s1" }, // would self-loop, only branch
|
|
256
|
+
],
|
|
257
|
+
},
|
|
258
|
+
});
|
|
259
|
+
// defaultTargetStepId is "s2" which is valid → returns 2
|
|
260
|
+
expect(resolveNextStepNumber(step, {}, steps)).toBe(2);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("skips defaultTargetStepId when it equals currentStep.id, falls back to linear", () => {
|
|
264
|
+
const step = makeStep("s2", {
|
|
265
|
+
nextStep: {
|
|
266
|
+
defaultTargetStepId: "s2", // self-reference
|
|
267
|
+
branches: [],
|
|
268
|
+
},
|
|
269
|
+
});
|
|
270
|
+
expect(resolveNextStepNumber(step, {}, steps)).toBe(3);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("returns null when only route is self-loop and no linear next", () => {
|
|
274
|
+
const last = makeStep("s4", {
|
|
275
|
+
nextStep: {
|
|
276
|
+
defaultTargetStepId: "s4", // self-reference, and s4 is last
|
|
277
|
+
branches: [],
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
expect(resolveNextStepNumber(last, {}, steps)).toBe(null);
|
|
281
|
+
});
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// ---------------------------------------------------------------------------
|
|
285
|
+
// Edge cases
|
|
286
|
+
// ---------------------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
describe("edge cases", () => {
|
|
289
|
+
it("empty steps array returns null", () => {
|
|
290
|
+
expect(resolveNextStepNumber(makeStep("s1"), {}, [])).toBe(null);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it("empty variables object, no branch matches → linear", () => {
|
|
294
|
+
const step = makeStep("s1", {
|
|
295
|
+
nextStep: {
|
|
296
|
+
defaultTargetStepId: "s3",
|
|
297
|
+
branches: [
|
|
298
|
+
{ condition: { variable: "gender", operator: "eq", value: "male" }, targetStepId: "s4" },
|
|
299
|
+
],
|
|
300
|
+
},
|
|
301
|
+
});
|
|
302
|
+
expect(resolveNextStepNumber(step, {}, [step, makeStep("s2"), makeStep("s3"), makeStep("s4")])).toBe(3);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("steps array with one element and current step last returns null", () => {
|
|
306
|
+
const s = makeStep("only");
|
|
307
|
+
expect(resolveNextStepNumber(s, {}, [s])).toBe(null);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { LeafCondition, ConditionGroup } from "./steps/common.types";
|
|
2
|
+
|
|
3
|
+
export type Condition = LeafCondition | ConditionGroup;
|
|
4
|
+
|
|
5
|
+
export function isConditionGroup(c: Condition): c is ConditionGroup {
|
|
6
|
+
return "logic" in c && "conditions" in c;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function coerceToNumber(v: unknown): number {
|
|
10
|
+
return typeof v === "string" ? parseFloat(v) : Number(v);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function evaluateLeaf(condition: LeafCondition, variables: Record<string, unknown>): boolean {
|
|
14
|
+
const raw = variables[condition.variable];
|
|
15
|
+
const { operator, value } = condition;
|
|
16
|
+
|
|
17
|
+
switch (operator) {
|
|
18
|
+
case "eq":
|
|
19
|
+
return String(raw) === String(value);
|
|
20
|
+
case "neq":
|
|
21
|
+
return String(raw) !== String(value);
|
|
22
|
+
case "gt":
|
|
23
|
+
return coerceToNumber(raw) > coerceToNumber(value);
|
|
24
|
+
case "lt":
|
|
25
|
+
return coerceToNumber(raw) < coerceToNumber(value);
|
|
26
|
+
case "gte":
|
|
27
|
+
return coerceToNumber(raw) >= coerceToNumber(value);
|
|
28
|
+
case "lte":
|
|
29
|
+
return coerceToNumber(raw) <= coerceToNumber(value);
|
|
30
|
+
case "contains":
|
|
31
|
+
return Array.isArray(raw)
|
|
32
|
+
? raw.includes(value)
|
|
33
|
+
: String(raw).includes(String(value));
|
|
34
|
+
case "in":
|
|
35
|
+
return Array.isArray(value) ? value.includes(String(raw)) : false;
|
|
36
|
+
case "not_in":
|
|
37
|
+
return Array.isArray(value) ? !value.includes(String(raw)) : true;
|
|
38
|
+
default:
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function evaluateCondition(condition: Condition, variables: Record<string, unknown>): boolean {
|
|
44
|
+
if (isConditionGroup(condition)) {
|
|
45
|
+
return condition.logic === "and"
|
|
46
|
+
? condition.conditions.every((c) => evaluateCondition(c, variables))
|
|
47
|
+
: condition.conditions.some((c) => evaluateCondition(c, variables));
|
|
48
|
+
}
|
|
49
|
+
return evaluateLeaf(condition, variables);
|
|
50
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -4,3 +4,22 @@ export * from "./types";
|
|
|
4
4
|
export * from "./onboarding-example";
|
|
5
5
|
// Hooks and providers
|
|
6
6
|
export * from "./infra";
|
|
7
|
+
// Branching
|
|
8
|
+
export { resolveNextStepNumber } from "./resolveNextStepNumber";
|
|
9
|
+
export type {
|
|
10
|
+
LeafCondition,
|
|
11
|
+
ConditionGroup,
|
|
12
|
+
ConditionValue,
|
|
13
|
+
ConditionOperator,
|
|
14
|
+
Branch,
|
|
15
|
+
NextStep,
|
|
16
|
+
} from "./steps/common.types";
|
|
17
|
+
export {
|
|
18
|
+
BaseStepTypeSchema,
|
|
19
|
+
LeafConditionSchema,
|
|
20
|
+
ConditionGroupSchema,
|
|
21
|
+
BranchSchema,
|
|
22
|
+
NextStepSchema,
|
|
23
|
+
ConditionOperatorSchema,
|
|
24
|
+
ConditionValueSchema,
|
|
25
|
+
} from "./steps/common.types";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createContext, useState } from "react";
|
|
1
|
+
import { createContext, useCallback, useState } from "react";
|
|
2
2
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
|
3
3
|
import { OnboardingStudioClient } from "../../OnboardingStudioClient";
|
|
4
4
|
import { getOnboardingQuery } from "../queries/getOnboarding.query";
|
|
@@ -32,6 +32,10 @@ export const OnboardingProvider = ({
|
|
|
32
32
|
});
|
|
33
33
|
const [totalSteps, setTotalSteps] = useState(0);
|
|
34
34
|
const [onboarding, setOnboarding] = useState<Onboarding<OnboardingStepType> | null>(null);
|
|
35
|
+
const [variables, setVariables] = useState<Record<string, any>>({});
|
|
36
|
+
const setVariable = useCallback((name: string, value: any) => {
|
|
37
|
+
setVariables((prev) => ({ ...prev, [name]: value }));
|
|
38
|
+
}, []);
|
|
35
39
|
|
|
36
40
|
queryClient.prefetchQuery(getOnboardingQuery(client, locale, customAudienceParams, setOnboarding))
|
|
37
41
|
|
|
@@ -49,6 +53,8 @@ export const OnboardingProvider = ({
|
|
|
49
53
|
customAudienceParams,
|
|
50
54
|
onboarding,
|
|
51
55
|
setOnboarding,
|
|
56
|
+
variables,
|
|
57
|
+
setVariable,
|
|
52
58
|
}}
|
|
53
59
|
>
|
|
54
60
|
{children}
|
|
@@ -67,6 +73,8 @@ export const OnboardingProgressContext = createContext<{
|
|
|
67
73
|
customAudienceParams: Record<string, any>;
|
|
68
74
|
onboarding: Onboarding<OnboardingStepType> | null;
|
|
69
75
|
setOnboarding: (onboarding: Onboarding<OnboardingStepType>) => void;
|
|
76
|
+
variables: Record<string, any>;
|
|
77
|
+
setVariable: (name: string, value: any) => void;
|
|
70
78
|
}>({
|
|
71
79
|
activeStep: { number: 0, displayProgressHeader: false },
|
|
72
80
|
setActiveStep: () => { },
|
|
@@ -77,4 +85,6 @@ export const OnboardingProgressContext = createContext<{
|
|
|
77
85
|
customAudienceParams: {},
|
|
78
86
|
onboarding: null,
|
|
79
87
|
setOnboarding: () => { },
|
|
88
|
+
variables: {},
|
|
89
|
+
setVariable: () => { },
|
|
80
90
|
});
|
|
@@ -329,6 +329,56 @@ export const onboardingExample = {
|
|
|
329
329
|
marginVertical: 8,
|
|
330
330
|
},
|
|
331
331
|
},
|
|
332
|
+
{
|
|
333
|
+
id: "gradient-card",
|
|
334
|
+
type: "YStack",
|
|
335
|
+
props: {
|
|
336
|
+
padding: 20,
|
|
337
|
+
gap: 8,
|
|
338
|
+
borderRadius: 16,
|
|
339
|
+
overflow: "hidden",
|
|
340
|
+
marginVertical: 4,
|
|
341
|
+
backgroundGradient: {
|
|
342
|
+
type: "linear",
|
|
343
|
+
from: "topLeft",
|
|
344
|
+
to: "bottomRight",
|
|
345
|
+
stops: [
|
|
346
|
+
{ color: "#6C63FF" },
|
|
347
|
+
{ color: "#FF6584" },
|
|
348
|
+
],
|
|
349
|
+
},
|
|
350
|
+
},
|
|
351
|
+
children: [
|
|
352
|
+
{
|
|
353
|
+
id: "gradient-card-title",
|
|
354
|
+
type: "Text",
|
|
355
|
+
props: { content: "Linear gradient", fontSize: 15, fontWeight: "700", color: "#fff" },
|
|
356
|
+
},
|
|
357
|
+
{
|
|
358
|
+
id: "gradient-card-body",
|
|
359
|
+
type: "Text",
|
|
360
|
+
props: { content: "topLeft → bottomRight", fontSize: 12, color: "#fff", opacity: 0.85 },
|
|
361
|
+
},
|
|
362
|
+
],
|
|
363
|
+
},
|
|
364
|
+
{
|
|
365
|
+
id: "gradient-button",
|
|
366
|
+
type: "Button",
|
|
367
|
+
props: {
|
|
368
|
+
label: "Gradient Button",
|
|
369
|
+
variant: "filled",
|
|
370
|
+
marginVertical: 4,
|
|
371
|
+
backgroundGradient: {
|
|
372
|
+
type: "linear",
|
|
373
|
+
from: "left",
|
|
374
|
+
to: "right",
|
|
375
|
+
stops: [
|
|
376
|
+
{ color: "#FF6584", position: 0 },
|
|
377
|
+
{ color: "#6C63FF", position: 1 },
|
|
378
|
+
],
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
},
|
|
332
382
|
],
|
|
333
383
|
},
|
|
334
384
|
],
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { BaseStepType } from "./types";
|
|
2
|
+
import { evaluateCondition } from "./evaluateCondition";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Resolves the 1-indexed step number to navigate to after the current step.
|
|
6
|
+
* Returns null when the onboarding should end (no next step exists).
|
|
7
|
+
*
|
|
8
|
+
* @param currentStep - The step that just completed
|
|
9
|
+
* @param variables - Global variable store (build a merged copy before calling if you just set a variable)
|
|
10
|
+
* @param steps - Full ordered steps array
|
|
11
|
+
*/
|
|
12
|
+
export function resolveNextStepNumber(
|
|
13
|
+
currentStep: BaseStepType,
|
|
14
|
+
variables: Record<string, any>,
|
|
15
|
+
steps: BaseStepType[]
|
|
16
|
+
): number | null {
|
|
17
|
+
const linearNext = (): number | null => {
|
|
18
|
+
const idx = steps.findIndex((s) => s.id === currentStep.id);
|
|
19
|
+
if (idx === -1 || idx + 1 >= steps.length) return null;
|
|
20
|
+
return idx + 2;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const { nextStep } = currentStep;
|
|
24
|
+
|
|
25
|
+
if (nextStep == null) return linearNext();
|
|
26
|
+
|
|
27
|
+
for (const branch of nextStep.branches) {
|
|
28
|
+
if (branch.targetStepId === currentStep.id) continue;
|
|
29
|
+
if (branch.condition === null || evaluateCondition(branch.condition, variables)) {
|
|
30
|
+
const idx = steps.findIndex((s) => s.id === branch.targetStepId);
|
|
31
|
+
if (idx !== -1) return idx + 1;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (nextStep.defaultTargetStepId !== currentStep.id) {
|
|
36
|
+
const defaultIdx = steps.findIndex((s) => s.id === nextStep.defaultTargetStepId);
|
|
37
|
+
if (defaultIdx !== -1) return defaultIdx + 1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return linearNext();
|
|
41
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { BaseStepTypeSchema } from "../common.types";
|
|
3
3
|
|
|
4
4
|
export const CarouselScreenSchema = z.object({
|
|
5
5
|
mediaUrl: z.string(),
|
|
@@ -11,16 +11,9 @@ export const CarouselStepPayloadSchema = z.object({
|
|
|
11
11
|
screens: z.array(CarouselScreenSchema),
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
export const CarouselStepTypeSchema =
|
|
15
|
-
id: z.string(),
|
|
14
|
+
export const CarouselStepTypeSchema = BaseStepTypeSchema.extend({
|
|
16
15
|
type: z.literal("Carousel"),
|
|
17
|
-
name: z.string(),
|
|
18
|
-
displayProgressHeader: z.boolean(),
|
|
19
16
|
payload: CarouselStepPayloadSchema,
|
|
20
|
-
customPayload: CustomPayloadSchema,
|
|
21
|
-
continueButtonLabel: z.string().optional().default("Continue"),
|
|
22
|
-
buttonSection: ButtonSectionSchema.optional(),
|
|
23
|
-
figmaUrl: z.string().nullish(),
|
|
24
17
|
});
|
|
25
18
|
|
|
26
19
|
export type CarouselStepType = z.infer<typeof CarouselStepTypeSchema>;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { BaseStepTypeSchema } from "../common.types";
|
|
3
3
|
|
|
4
4
|
export const CommitmentItemSchema = z.object({
|
|
5
5
|
text: z.string(),
|
|
@@ -14,16 +14,9 @@ export const CommitmentStepPayloadSchema = z.object({
|
|
|
14
14
|
variant: z.enum(["signature", "simple"]).default("signature"),
|
|
15
15
|
});
|
|
16
16
|
|
|
17
|
-
export const CommitmentStepTypeSchema =
|
|
18
|
-
id: z.string(),
|
|
17
|
+
export const CommitmentStepTypeSchema = BaseStepTypeSchema.extend({
|
|
19
18
|
type: z.literal("Commitment"),
|
|
20
|
-
name: z.string(),
|
|
21
|
-
displayProgressHeader: z.boolean(),
|
|
22
19
|
payload: CommitmentStepPayloadSchema,
|
|
23
|
-
customPayload: CustomPayloadSchema,
|
|
24
|
-
continueButtonLabel: z.string().optional().default("Continue"),
|
|
25
|
-
buttonSection: ButtonSectionSchema.optional(),
|
|
26
|
-
figmaUrl: z.string().nullish(),
|
|
27
20
|
});
|
|
28
21
|
|
|
29
22
|
export type CommitmentStepType = z.infer<typeof CommitmentStepTypeSchema>;
|
|
@@ -1,5 +1,45 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
|
+
export type GradientStop = {
|
|
4
|
+
color: string;
|
|
5
|
+
position?: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type GradientEdge =
|
|
9
|
+
| "top"
|
|
10
|
+
| "bottom"
|
|
11
|
+
| "left"
|
|
12
|
+
| "right"
|
|
13
|
+
| "topLeft"
|
|
14
|
+
| "topRight"
|
|
15
|
+
| "bottomLeft"
|
|
16
|
+
| "bottomRight";
|
|
17
|
+
|
|
18
|
+
export type LinearGradientConfig = {
|
|
19
|
+
type: "linear";
|
|
20
|
+
from: GradientEdge;
|
|
21
|
+
to: GradientEdge;
|
|
22
|
+
stops: GradientStop[];
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export type GradientBackground = LinearGradientConfig;
|
|
26
|
+
|
|
27
|
+
const GradientEdgeSchema = z.enum(["top", "bottom", "left", "right", "topLeft", "topRight", "bottomLeft", "bottomRight"]);
|
|
28
|
+
|
|
29
|
+
const GradientStopSchema = z.object({
|
|
30
|
+
color: z.string(),
|
|
31
|
+
position: z.number().min(0).max(1).optional(),
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
export const GradientBackgroundSchema = z.discriminatedUnion("type", [
|
|
35
|
+
z.object({
|
|
36
|
+
type: z.literal("linear"),
|
|
37
|
+
from: GradientEdgeSchema,
|
|
38
|
+
to: GradientEdgeSchema,
|
|
39
|
+
stops: z.array(GradientStopSchema).min(2, "gradient requires at least 2 stops"),
|
|
40
|
+
}),
|
|
41
|
+
]);
|
|
42
|
+
|
|
3
43
|
export type BaseBoxProps = {
|
|
4
44
|
width?: number | string;
|
|
5
45
|
height?: number | string;
|
|
@@ -13,6 +53,7 @@ export type BaseBoxProps = {
|
|
|
13
53
|
alignSelf?: "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline";
|
|
14
54
|
opacity?: number;
|
|
15
55
|
backgroundColor?: string;
|
|
56
|
+
backgroundGradient?: GradientBackground;
|
|
16
57
|
overflow?: "hidden" | "visible" | "scroll";
|
|
17
58
|
margin?: number;
|
|
18
59
|
marginHorizontal?: number;
|
|
@@ -38,6 +79,7 @@ export const BaseBoxPropsSchema = z.object({
|
|
|
38
79
|
alignSelf: z.enum(["auto", "flex-start", "flex-end", "center", "stretch", "baseline"]).optional(),
|
|
39
80
|
opacity: z.number().min(0).max(1).optional(),
|
|
40
81
|
backgroundColor: z.string().optional(),
|
|
82
|
+
backgroundGradient: GradientBackgroundSchema.optional(),
|
|
41
83
|
overflow: z.enum(["hidden", "visible", "scroll"]).optional(),
|
|
42
84
|
margin: z.number().optional(),
|
|
43
85
|
marginHorizontal: z.number().optional(),
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { BaseStepTypeSchema } from "../common.types";
|
|
3
3
|
import { type StackElementProps, StackElementPropsSchema } from "./elements/StackElement";
|
|
4
4
|
import { type TextElementProps, TextElementPropsSchema } from "./elements/TextElement";
|
|
5
5
|
import { type ImageElementProps, ImageElementPropsSchema } from "./elements/ImageElement";
|
|
@@ -14,8 +14,8 @@ import { type CheckboxGroupElementProps, CheckboxGroupElementPropsSchema } from
|
|
|
14
14
|
import { type DatePickerElementProps, DatePickerElementPropsSchema } from "./elements/DatePickerElement";
|
|
15
15
|
import { type CarouselElementProps, CarouselElementPropsSchema } from "./elements/CarouselElement";
|
|
16
16
|
|
|
17
|
-
export type { BaseBoxProps } from "./elements/BaseBoxProps";
|
|
18
|
-
export { BaseBoxPropsSchema } from "./elements/BaseBoxProps";
|
|
17
|
+
export type { BaseBoxProps, GradientBackground, GradientEdge, GradientStop, LinearGradientConfig } from "./elements/BaseBoxProps";
|
|
18
|
+
export { BaseBoxPropsSchema, GradientBackgroundSchema } from "./elements/BaseBoxProps";
|
|
19
19
|
export type { StackElementProps } from "./elements/StackElement";
|
|
20
20
|
export type { TextElementProps } from "./elements/TextElement";
|
|
21
21
|
export type { ImageElementProps } from "./elements/ImageElement";
|
|
@@ -203,15 +203,9 @@ export const ComposableScreenStepPayloadSchema = z.object({
|
|
|
203
203
|
elements: z.array(UIElementSchema),
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
-
export const ComposableScreenStepTypeSchema =
|
|
207
|
-
id: z.string(),
|
|
206
|
+
export const ComposableScreenStepTypeSchema = BaseStepTypeSchema.extend({
|
|
208
207
|
type: z.literal("ComposableScreen"),
|
|
209
|
-
name: z.string(),
|
|
210
|
-
displayProgressHeader: z.boolean(),
|
|
211
208
|
payload: ComposableScreenStepPayloadSchema,
|
|
212
|
-
customPayload: CustomPayloadSchema,
|
|
213
|
-
continueButtonLabel: z.string().optional().default("Continue"),
|
|
214
|
-
figmaUrl: z.string().nullable(),
|
|
215
209
|
});
|
|
216
210
|
|
|
217
211
|
export type ComposableScreenStepType = z.infer<typeof ComposableScreenStepTypeSchema>;
|