@rocapine/react-native-onboarding 1.10.0 → 1.12.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 +109 -15
- package/dist/onboarding-example.d.ts.map +1 -1
- package/dist/onboarding-example.js +44 -1
- 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 +28 -6
- package/dist/steps/ComposableScreen/elements/BaseBoxProps.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/BaseBoxProps.js +11 -2
- package/dist/steps/ComposableScreen/elements/BaseBoxProps.js.map +1 -1
- package/dist/steps/ComposableScreen/elements/ButtonElement.d.ts +22 -10
- package/dist/steps/ComposableScreen/elements/ButtonElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/ButtonElement.js +0 -1
- package/dist/steps/ComposableScreen/elements/ButtonElement.js.map +1 -1
- package/dist/steps/ComposableScreen/elements/CarouselElement.d.ts +67 -0
- package/dist/steps/ComposableScreen/elements/CarouselElement.d.ts.map +1 -0
- package/dist/steps/ComposableScreen/elements/CarouselElement.js +19 -0
- package/dist/steps/ComposableScreen/elements/CarouselElement.js.map +1 -0
- package/dist/steps/ComposableScreen/elements/CheckboxGroupElement.d.ts +17 -4
- package/dist/steps/ComposableScreen/elements/CheckboxGroupElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/DatePickerElement.d.ts +17 -4
- package/dist/steps/ComposableScreen/elements/DatePickerElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/IconElement.d.ts +16 -4
- package/dist/steps/ComposableScreen/elements/IconElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/ImageElement.d.ts +17 -4
- package/dist/steps/ComposableScreen/elements/ImageElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/InputElement.d.ts +24 -6
- package/dist/steps/ComposableScreen/elements/InputElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/InputElement.js +3 -0
- package/dist/steps/ComposableScreen/elements/InputElement.js.map +1 -1
- package/dist/steps/ComposableScreen/elements/LottieElement.d.ts +17 -4
- package/dist/steps/ComposableScreen/elements/LottieElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/RadioGroupElement.d.ts +17 -4
- package/dist/steps/ComposableScreen/elements/RadioGroupElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/RiveElement.d.ts +19 -6
- package/dist/steps/ComposableScreen/elements/RiveElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/RiveElement.js +1 -1
- package/dist/steps/ComposableScreen/elements/StackElement.d.ts +31 -50
- package/dist/steps/ComposableScreen/elements/StackElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/StackElement.js +2 -22
- package/dist/steps/ComposableScreen/elements/StackElement.js.map +1 -1
- package/dist/steps/ComposableScreen/elements/TextElement.d.ts +35 -23
- package/dist/steps/ComposableScreen/elements/TextElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/elements/TextElement.js +2 -12
- package/dist/steps/ComposableScreen/elements/TextElement.js.map +1 -1
- package/dist/steps/ComposableScreen/elements/VideoElement.d.ts +17 -4
- package/dist/steps/ComposableScreen/elements/VideoElement.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/types.d.ts +37 -4
- package/dist/steps/ComposableScreen/types.d.ts.map +1 -1
- package/dist/steps/ComposableScreen/types.js +9 -7
- 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 +44 -1
- 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 +22 -4
- package/src/steps/ComposableScreen/elements/ButtonElement.ts +0 -2
- package/src/steps/ComposableScreen/elements/CarouselElement.ts +30 -0
- package/src/steps/ComposableScreen/elements/InputElement.ts +6 -0
- package/src/steps/ComposableScreen/elements/RiveElement.ts +2 -2
- package/src/steps/ComposableScreen/elements/StackElement.ts +3 -44
- package/src/steps/ComposableScreen/elements/TextElement.ts +3 -24
- package/src/steps/ComposableScreen/types.ts +18 -8
- 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
|
});
|
|
@@ -116,7 +116,7 @@ export const onboardingExample = {
|
|
|
116
116
|
props: {
|
|
117
117
|
url: "https://cdn.rive.app/animations/vehicles.riv",
|
|
118
118
|
height: 180,
|
|
119
|
-
|
|
119
|
+
autoPlay: true,
|
|
120
120
|
fit: "Contain",
|
|
121
121
|
},
|
|
122
122
|
},
|
|
@@ -277,6 +277,49 @@ export const onboardingExample = {
|
|
|
277
277
|
marginVertical: 4,
|
|
278
278
|
},
|
|
279
279
|
},
|
|
280
|
+
{
|
|
281
|
+
id: "hero-carousel",
|
|
282
|
+
type: "Carousel",
|
|
283
|
+
props: {
|
|
284
|
+
carouselType: "parallax",
|
|
285
|
+
autoPlay: true,
|
|
286
|
+
autoPlayInterval: 3000,
|
|
287
|
+
loop: true,
|
|
288
|
+
showDots: true,
|
|
289
|
+
height: 220,
|
|
290
|
+
borderRadius: 16,
|
|
291
|
+
marginVertical: 8,
|
|
292
|
+
},
|
|
293
|
+
children: [
|
|
294
|
+
{
|
|
295
|
+
id: "carousel-slide-1",
|
|
296
|
+
type: "Image",
|
|
297
|
+
props: {
|
|
298
|
+
url: "https://picsum.photos/400/220?random=10",
|
|
299
|
+
height: 220,
|
|
300
|
+
resizeMode: "cover",
|
|
301
|
+
},
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
id: "carousel-slide-2",
|
|
305
|
+
type: "Image",
|
|
306
|
+
props: {
|
|
307
|
+
url: "https://picsum.photos/400/220?random=11",
|
|
308
|
+
height: 220,
|
|
309
|
+
resizeMode: "cover",
|
|
310
|
+
},
|
|
311
|
+
},
|
|
312
|
+
{
|
|
313
|
+
id: "carousel-slide-3",
|
|
314
|
+
type: "Image",
|
|
315
|
+
props: {
|
|
316
|
+
url: "https://picsum.photos/400/220?random=12",
|
|
317
|
+
height: 220,
|
|
318
|
+
resizeMode: "cover",
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
},
|
|
280
323
|
{
|
|
281
324
|
id: "hero-button",
|
|
282
325
|
type: "Button",
|
|
@@ -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,10 +1,19 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
|
|
3
3
|
export type BaseBoxProps = {
|
|
4
|
-
width?: number;
|
|
5
|
-
height?: number;
|
|
4
|
+
width?: number | string;
|
|
5
|
+
height?: number | string;
|
|
6
|
+
minWidth?: number;
|
|
7
|
+
maxWidth?: number;
|
|
8
|
+
minHeight?: number;
|
|
9
|
+
maxHeight?: number;
|
|
10
|
+
flex?: number;
|
|
11
|
+
flexShrink?: number;
|
|
12
|
+
flexGrow?: number;
|
|
6
13
|
alignSelf?: "auto" | "flex-start" | "flex-end" | "center" | "stretch" | "baseline";
|
|
7
14
|
opacity?: number;
|
|
15
|
+
backgroundColor?: string;
|
|
16
|
+
overflow?: "hidden" | "visible" | "scroll";
|
|
8
17
|
margin?: number;
|
|
9
18
|
marginHorizontal?: number;
|
|
10
19
|
marginVertical?: number;
|
|
@@ -17,10 +26,19 @@ export type BaseBoxProps = {
|
|
|
17
26
|
};
|
|
18
27
|
|
|
19
28
|
export const BaseBoxPropsSchema = z.object({
|
|
20
|
-
width: z.number().min(0).optional(),
|
|
21
|
-
height: z.number().min(0).optional(),
|
|
29
|
+
width: z.union([z.number().min(0), z.string()]).optional(),
|
|
30
|
+
height: z.union([z.number().min(0), z.string()]).optional(),
|
|
31
|
+
minWidth: z.number().min(0).optional(),
|
|
32
|
+
maxWidth: z.number().min(0).optional(),
|
|
33
|
+
minHeight: z.number().min(0).optional(),
|
|
34
|
+
maxHeight: z.number().min(0).optional(),
|
|
35
|
+
flex: z.number().min(0).optional(),
|
|
36
|
+
flexShrink: z.number().min(0).optional(),
|
|
37
|
+
flexGrow: z.number().min(0).optional(),
|
|
22
38
|
alignSelf: z.enum(["auto", "flex-start", "flex-end", "center", "stretch", "baseline"]).optional(),
|
|
23
39
|
opacity: z.number().min(0).max(1).optional(),
|
|
40
|
+
backgroundColor: z.string().optional(),
|
|
41
|
+
overflow: z.enum(["hidden", "visible", "scroll"]).optional(),
|
|
24
42
|
margin: z.number().optional(),
|
|
25
43
|
marginHorizontal: z.number().optional(),
|
|
26
44
|
marginVertical: z.number().optional(),
|
|
@@ -11,7 +11,6 @@ export type ButtonElementProps = BaseBoxProps & {
|
|
|
11
11
|
fontWeight?: string;
|
|
12
12
|
fontFamily?: string;
|
|
13
13
|
textAlign?: "left" | "center" | "right";
|
|
14
|
-
alignSelf?: "auto" | "flex-start" | "center" | "flex-end" | "stretch";
|
|
15
14
|
};
|
|
16
15
|
|
|
17
16
|
export const ButtonElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
@@ -24,5 +23,4 @@ export const ButtonElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
|
24
23
|
fontWeight: z.string().optional(),
|
|
25
24
|
fontFamily: z.string().optional(),
|
|
26
25
|
textAlign: z.enum(["left", "center", "right"]).optional(),
|
|
27
|
-
alignSelf: z.enum(["auto", "flex-start", "center", "flex-end", "stretch"]).optional(),
|
|
28
26
|
});
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { BaseBoxProps, BaseBoxPropsSchema } from "./BaseBoxProps";
|
|
3
|
+
|
|
4
|
+
export type CarouselElementProps = BaseBoxProps & {
|
|
5
|
+
carouselType?: "left-align" | "normal" | "parallax" | "stack";
|
|
6
|
+
autoPlay?: boolean;
|
|
7
|
+
autoPlayInterval?: number;
|
|
8
|
+
loop?: boolean;
|
|
9
|
+
showDots?: boolean;
|
|
10
|
+
dotColor?: string;
|
|
11
|
+
activeDotColor?: string;
|
|
12
|
+
dotWidth?: number;
|
|
13
|
+
dotHeight?: number;
|
|
14
|
+
dotsGap?: number;
|
|
15
|
+
dotsMarginTop?: number;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const CarouselElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
19
|
+
carouselType: z.enum(["left-align", "normal", "parallax", "stack"]).optional().default("normal"),
|
|
20
|
+
autoPlay: z.boolean().optional().default(false),
|
|
21
|
+
autoPlayInterval: z.number().nonnegative().optional().default(3000),
|
|
22
|
+
loop: z.boolean().optional().default(true),
|
|
23
|
+
showDots: z.boolean().optional().default(true),
|
|
24
|
+
dotColor: z.string().optional(),
|
|
25
|
+
activeDotColor: z.string().optional(),
|
|
26
|
+
dotWidth: z.number().nonnegative().optional().default(20),
|
|
27
|
+
dotHeight: z.number().nonnegative().optional().default(4),
|
|
28
|
+
dotsGap: z.number().nonnegative().optional().default(8),
|
|
29
|
+
dotsMarginTop: z.number().optional().default(12),
|
|
30
|
+
});
|
|
@@ -17,6 +17,9 @@ export type InputElementProps = BaseBoxProps & {
|
|
|
17
17
|
backgroundColor?: string;
|
|
18
18
|
fontSize?: number;
|
|
19
19
|
fontWeight?: string;
|
|
20
|
+
fontFamily?: string;
|
|
21
|
+
lineHeight?: number;
|
|
22
|
+
letterSpacing?: number;
|
|
20
23
|
textAlign?: "left" | "center" | "right";
|
|
21
24
|
placeholderColor?: string;
|
|
22
25
|
};
|
|
@@ -37,6 +40,9 @@ export const InputElementPropsSchema = BaseBoxPropsSchema.extend({
|
|
|
37
40
|
backgroundColor: z.string().optional(),
|
|
38
41
|
fontSize: z.number().optional(),
|
|
39
42
|
fontWeight: z.string().optional(),
|
|
43
|
+
fontFamily: z.string().optional(),
|
|
44
|
+
lineHeight: z.number().optional(),
|
|
45
|
+
letterSpacing: z.number().optional(),
|
|
40
46
|
textAlign: z.enum(["left", "center", "right"]).optional(),
|
|
41
47
|
placeholderColor: z.string().optional(),
|
|
42
48
|
});
|