@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.
Files changed (107) hide show
  1. package/dist/evaluateCondition.d.ts +6 -0
  2. package/dist/evaluateCondition.d.ts.map +1 -0
  3. package/dist/evaluateCondition.js +48 -0
  4. package/dist/evaluateCondition.js.map +1 -0
  5. package/dist/index.d.ts +3 -0
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +12 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/infra/provider/OnboardingProvider.d.ts +2 -0
  10. package/dist/infra/provider/OnboardingProvider.d.ts.map +1 -1
  11. package/dist/infra/provider/OnboardingProvider.js +8 -0
  12. package/dist/infra/provider/OnboardingProvider.js.map +1 -1
  13. package/dist/onboarding-example.d.ts +174 -0
  14. package/dist/onboarding-example.d.ts.map +1 -1
  15. package/dist/onboarding-example.js +50 -0
  16. package/dist/onboarding-example.js.map +1 -1
  17. package/dist/resolveNextStepNumber.d.ts +11 -0
  18. package/dist/resolveNextStepNumber.d.ts.map +1 -0
  19. package/dist/resolveNextStepNumber.js +39 -0
  20. package/dist/resolveNextStepNumber.js.map +1 -0
  21. package/dist/steps/Carousel/types.d.ts +29 -8
  22. package/dist/steps/Carousel/types.d.ts.map +1 -1
  23. package/dist/steps/Carousel/types.js +1 -8
  24. package/dist/steps/Carousel/types.js.map +1 -1
  25. package/dist/steps/Commitment/types.d.ts +29 -8
  26. package/dist/steps/Commitment/types.d.ts.map +1 -1
  27. package/dist/steps/Commitment/types.js +1 -8
  28. package/dist/steps/Commitment/types.js.map +1 -1
  29. package/dist/steps/ComposableScreen/elements/BaseBoxProps.d.ts +67 -0
  30. package/dist/steps/ComposableScreen/elements/BaseBoxProps.d.ts.map +1 -1
  31. package/dist/steps/ComposableScreen/elements/BaseBoxProps.js +15 -1
  32. package/dist/steps/ComposableScreen/elements/BaseBoxProps.js.map +1 -1
  33. package/dist/steps/ComposableScreen/elements/ButtonElement.d.ts +28 -1
  34. package/dist/steps/ComposableScreen/elements/ButtonElement.d.ts.map +1 -1
  35. package/dist/steps/ComposableScreen/elements/CarouselElement.d.ts +27 -0
  36. package/dist/steps/ComposableScreen/elements/CarouselElement.d.ts.map +1 -1
  37. package/dist/steps/ComposableScreen/elements/CheckboxGroupElement.d.ts +27 -0
  38. package/dist/steps/ComposableScreen/elements/CheckboxGroupElement.d.ts.map +1 -1
  39. package/dist/steps/ComposableScreen/elements/DatePickerElement.d.ts +27 -0
  40. package/dist/steps/ComposableScreen/elements/DatePickerElement.d.ts.map +1 -1
  41. package/dist/steps/ComposableScreen/elements/IconElement.d.ts +27 -0
  42. package/dist/steps/ComposableScreen/elements/IconElement.d.ts.map +1 -1
  43. package/dist/steps/ComposableScreen/elements/ImageElement.d.ts +27 -0
  44. package/dist/steps/ComposableScreen/elements/ImageElement.d.ts.map +1 -1
  45. package/dist/steps/ComposableScreen/elements/InputElement.d.ts +30 -3
  46. package/dist/steps/ComposableScreen/elements/InputElement.d.ts.map +1 -1
  47. package/dist/steps/ComposableScreen/elements/LottieElement.d.ts +27 -0
  48. package/dist/steps/ComposableScreen/elements/LottieElement.d.ts.map +1 -1
  49. package/dist/steps/ComposableScreen/elements/RadioGroupElement.d.ts +27 -0
  50. package/dist/steps/ComposableScreen/elements/RadioGroupElement.d.ts.map +1 -1
  51. package/dist/steps/ComposableScreen/elements/RiveElement.d.ts +27 -0
  52. package/dist/steps/ComposableScreen/elements/RiveElement.d.ts.map +1 -1
  53. package/dist/steps/ComposableScreen/elements/StackElement.d.ts +27 -0
  54. package/dist/steps/ComposableScreen/elements/StackElement.d.ts.map +1 -1
  55. package/dist/steps/ComposableScreen/elements/TextElement.d.ts +28 -1
  56. package/dist/steps/ComposableScreen/elements/TextElement.d.ts.map +1 -1
  57. package/dist/steps/ComposableScreen/elements/VideoElement.d.ts +27 -0
  58. package/dist/steps/ComposableScreen/elements/VideoElement.d.ts.map +1 -1
  59. package/dist/steps/ComposableScreen/types.d.ts +31 -6
  60. package/dist/steps/ComposableScreen/types.d.ts.map +1 -1
  61. package/dist/steps/ComposableScreen/types.js +3 -8
  62. package/dist/steps/ComposableScreen/types.js.map +1 -1
  63. package/dist/steps/Loader/types.d.ts +29 -8
  64. package/dist/steps/Loader/types.d.ts.map +1 -1
  65. package/dist/steps/Loader/types.js +1 -8
  66. package/dist/steps/Loader/types.js.map +1 -1
  67. package/dist/steps/MediaContent/types.d.ts +29 -8
  68. package/dist/steps/MediaContent/types.d.ts.map +1 -1
  69. package/dist/steps/MediaContent/types.js +1 -8
  70. package/dist/steps/MediaContent/types.js.map +1 -1
  71. package/dist/steps/Picker/types.d.ts +30 -8
  72. package/dist/steps/Picker/types.d.ts.map +1 -1
  73. package/dist/steps/Picker/types.js +2 -8
  74. package/dist/steps/Picker/types.js.map +1 -1
  75. package/dist/steps/Question/types.d.ts +30 -8
  76. package/dist/steps/Question/types.d.ts.map +1 -1
  77. package/dist/steps/Question/types.js +2 -8
  78. package/dist/steps/Question/types.js.map +1 -1
  79. package/dist/steps/Ratings/types.d.ts +29 -8
  80. package/dist/steps/Ratings/types.d.ts.map +1 -1
  81. package/dist/steps/Ratings/types.js +1 -8
  82. package/dist/steps/Ratings/types.js.map +1 -1
  83. package/dist/steps/common.types.d.ts +109 -0
  84. package/dist/steps/common.types.d.ts.map +1 -1
  85. package/dist/steps/common.types.js +52 -1
  86. package/dist/steps/common.types.js.map +1 -1
  87. package/dist/types.d.ts +2 -0
  88. package/dist/types.d.ts.map +1 -1
  89. package/package.json +5 -2
  90. package/src/__tests__/evaluateCondition.test.ts +167 -0
  91. package/src/__tests__/resolveNextStepNumber.test.ts +309 -0
  92. package/src/evaluateCondition.ts +50 -0
  93. package/src/index.ts +19 -0
  94. package/src/infra/provider/OnboardingProvider.tsx +11 -1
  95. package/src/onboarding-example.ts +50 -0
  96. package/src/resolveNextStepNumber.ts +41 -0
  97. package/src/steps/Carousel/types.ts +2 -9
  98. package/src/steps/Commitment/types.ts +2 -9
  99. package/src/steps/ComposableScreen/elements/BaseBoxProps.ts +42 -0
  100. package/src/steps/ComposableScreen/types.ts +4 -10
  101. package/src/steps/Loader/types.ts +2 -9
  102. package/src/steps/MediaContent/types.ts +2 -14
  103. package/src/steps/Picker/types.ts +3 -9
  104. package/src/steps/Question/types.ts +3 -9
  105. package/src/steps/Ratings/types.ts +2 -9
  106. package/src/steps/common.types.ts +72 -0
  107. 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 { ButtonSectionSchema, CustomPayloadSchema } from "../common.types";
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 = z.object({
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 { ButtonSectionSchema, CustomPayloadSchema } from "../common.types";
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 = z.object({
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 { CustomPayloadSchema } from "../common.types";
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 = z.object({
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>;