noboarding 0.1.0-alpha
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/README.md +515 -0
- package/REVENUECAT_SETUP.md +756 -0
- package/SETUP_GUIDE.md +873 -0
- package/cusomte_screens.md +1964 -0
- package/lib/OnboardingFlow.d.ts +3 -0
- package/lib/OnboardingFlow.js +235 -0
- package/lib/analytics.d.ts +25 -0
- package/lib/analytics.js +72 -0
- package/lib/api.d.ts +31 -0
- package/lib/api.js +149 -0
- package/lib/components/ElementRenderer.d.ts +13 -0
- package/lib/components/ElementRenderer.js +521 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +18 -0
- package/lib/types.d.ts +185 -0
- package/lib/types.js +2 -0
- package/lib/variableUtils.d.ts +17 -0
- package/lib/variableUtils.js +118 -0
- package/logic.md +2095 -0
- package/package.json +44 -0
- package/src/OnboardingFlow.tsx +276 -0
- package/src/analytics.ts +84 -0
- package/src/api.ts +173 -0
- package/src/components/ElementRenderer.tsx +627 -0
- package/src/index.ts +32 -0
- package/src/types.ts +242 -0
- package/src/variableUtils.ts +133 -0
- package/tsconfig.json +20 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import type React from 'react';
|
|
2
|
+
|
|
3
|
+
// Screen component types
|
|
4
|
+
export type ScreenType = 'noboard_screen' | 'custom_screen';
|
|
5
|
+
|
|
6
|
+
// Screen configuration from remote
|
|
7
|
+
export interface ScreenConfig {
|
|
8
|
+
id: string;
|
|
9
|
+
type: ScreenType;
|
|
10
|
+
props: Record<string, any>;
|
|
11
|
+
// For noboard_screen type — the element tree from the AI builder
|
|
12
|
+
elements?: ElementNode[];
|
|
13
|
+
// For custom_screen type — name of the developer-registered component
|
|
14
|
+
custom_component_name?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// ─── Element Tree Types (matches dashboard primitives) ───
|
|
18
|
+
|
|
19
|
+
export type ElementType =
|
|
20
|
+
// Containers (have children)
|
|
21
|
+
| 'vstack'
|
|
22
|
+
| 'hstack'
|
|
23
|
+
| 'zstack'
|
|
24
|
+
| 'scrollview'
|
|
25
|
+
// Content (leaf elements)
|
|
26
|
+
| 'text'
|
|
27
|
+
| 'image'
|
|
28
|
+
| 'video'
|
|
29
|
+
| 'lottie'
|
|
30
|
+
| 'icon'
|
|
31
|
+
| 'input'
|
|
32
|
+
| 'spacer'
|
|
33
|
+
| 'divider';
|
|
34
|
+
|
|
35
|
+
export interface ElementNode {
|
|
36
|
+
id: string;
|
|
37
|
+
type: ElementType;
|
|
38
|
+
style: ElementStyle;
|
|
39
|
+
props: Record<string, any>;
|
|
40
|
+
children?: ElementNode[];
|
|
41
|
+
position?: ElementPosition;
|
|
42
|
+
action?: ElementAction;
|
|
43
|
+
actions?: ElementAction[]; // multi-action support (runs all in sequence)
|
|
44
|
+
visibleWhen?: { group: string; hasSelection: boolean };
|
|
45
|
+
conditions?: ElementConditions; // variable-based show/hide
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ─── Condition & Variable Types ───
|
|
49
|
+
|
|
50
|
+
export type ComparisonOperator = 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'contains' | 'in' | 'is_empty' | 'is_not_empty';
|
|
51
|
+
|
|
52
|
+
export interface Condition {
|
|
53
|
+
variable?: string;
|
|
54
|
+
operator?: ComparisonOperator;
|
|
55
|
+
value?: any;
|
|
56
|
+
all?: Condition[]; // AND — all must be true
|
|
57
|
+
any?: Condition[]; // OR — at least one must be true
|
|
58
|
+
not?: Condition; // NOT — negate
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface ElementConditions {
|
|
62
|
+
show_if?: Condition;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ConditionalDestination {
|
|
66
|
+
if: Condition;
|
|
67
|
+
then: string;
|
|
68
|
+
else?: string | ConditionalDestination;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface ConditionalRoutes {
|
|
72
|
+
routes: Array<{ condition: Condition; destination: string }>;
|
|
73
|
+
default: string;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// ─── Element Action ───
|
|
77
|
+
|
|
78
|
+
export interface ElementAction {
|
|
79
|
+
type: 'tap' | 'navigate' | 'link' | 'toggle' | 'dismiss' | 'set_variable';
|
|
80
|
+
destination?: string | ConditionalDestination | ConditionalRoutes; // screen ID, URL, or conditional
|
|
81
|
+
group?: string; // selection group name for single-select toggles
|
|
82
|
+
variable?: string; // for set_variable: variable name to set
|
|
83
|
+
value?: any; // for set_variable: value to assign
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ElementPosition {
|
|
87
|
+
type?: 'relative' | 'absolute';
|
|
88
|
+
top?: number;
|
|
89
|
+
left?: number;
|
|
90
|
+
right?: number;
|
|
91
|
+
bottom?: number;
|
|
92
|
+
centerX?: boolean;
|
|
93
|
+
centerY?: boolean;
|
|
94
|
+
zIndex?: number;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface ElementStyle {
|
|
98
|
+
// Layout
|
|
99
|
+
flex?: number;
|
|
100
|
+
flexDirection?: 'row' | 'column';
|
|
101
|
+
justifyContent?: 'flex-start' | 'center' | 'flex-end' | 'space-between' | 'space-around' | 'space-evenly';
|
|
102
|
+
alignItems?: 'flex-start' | 'center' | 'flex-end' | 'stretch';
|
|
103
|
+
alignSelf?: 'flex-start' | 'center' | 'flex-end' | 'stretch';
|
|
104
|
+
gap?: number;
|
|
105
|
+
flexWrap?: 'wrap' | 'nowrap';
|
|
106
|
+
overflow?: 'visible' | 'hidden' | 'scroll';
|
|
107
|
+
|
|
108
|
+
// Spacing
|
|
109
|
+
padding?: number;
|
|
110
|
+
paddingTop?: number;
|
|
111
|
+
paddingBottom?: number;
|
|
112
|
+
paddingLeft?: number;
|
|
113
|
+
paddingRight?: number;
|
|
114
|
+
marginTop?: number;
|
|
115
|
+
marginBottom?: number;
|
|
116
|
+
marginLeft?: number;
|
|
117
|
+
marginRight?: number;
|
|
118
|
+
|
|
119
|
+
// Size
|
|
120
|
+
width?: number | string;
|
|
121
|
+
height?: number | string;
|
|
122
|
+
maxWidth?: number;
|
|
123
|
+
minHeight?: number | string;
|
|
124
|
+
|
|
125
|
+
// Visual
|
|
126
|
+
backgroundColor?: string;
|
|
127
|
+
backgroundGradient?: {
|
|
128
|
+
type: 'linear' | 'radial';
|
|
129
|
+
angle?: number;
|
|
130
|
+
colors: Array<{ color: string; position: number }>;
|
|
131
|
+
};
|
|
132
|
+
opacity?: number;
|
|
133
|
+
borderRadius?: number;
|
|
134
|
+
borderWidth?: number;
|
|
135
|
+
borderColor?: string;
|
|
136
|
+
borderBottomWidth?: number;
|
|
137
|
+
borderBottomColor?: string;
|
|
138
|
+
|
|
139
|
+
// Shadow
|
|
140
|
+
shadowColor?: string;
|
|
141
|
+
shadowOpacity?: number;
|
|
142
|
+
shadowRadius?: number;
|
|
143
|
+
shadowOffsetX?: number;
|
|
144
|
+
shadowOffsetY?: number;
|
|
145
|
+
|
|
146
|
+
// Text
|
|
147
|
+
color?: string;
|
|
148
|
+
fontSize?: number;
|
|
149
|
+
fontWeight?: string;
|
|
150
|
+
textAlign?: 'left' | 'center' | 'right' | 'justify';
|
|
151
|
+
lineHeight?: number;
|
|
152
|
+
letterSpacing?: number;
|
|
153
|
+
textTransform?: 'none' | 'uppercase' | 'lowercase' | 'capitalize';
|
|
154
|
+
textDecorationLine?: 'none' | 'underline' | 'line-through';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Onboarding configuration from API
|
|
158
|
+
export interface OnboardingConfig {
|
|
159
|
+
version: string;
|
|
160
|
+
screens: ScreenConfig[];
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Experiment/A/B test variant
|
|
164
|
+
export interface Experiment {
|
|
165
|
+
id: string;
|
|
166
|
+
name: string;
|
|
167
|
+
variants: Variant[];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export interface Variant {
|
|
171
|
+
variant_id: string;
|
|
172
|
+
weight: number;
|
|
173
|
+
name: string;
|
|
174
|
+
screens: ScreenConfig[];
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Analytics event
|
|
178
|
+
export interface AnalyticsEvent {
|
|
179
|
+
event: string;
|
|
180
|
+
user_id: string;
|
|
181
|
+
session_id: string;
|
|
182
|
+
timestamp: number;
|
|
183
|
+
properties?: Record<string, any>;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// API response types
|
|
187
|
+
export interface GetConfigResponse {
|
|
188
|
+
config: OnboardingConfig;
|
|
189
|
+
version: string;
|
|
190
|
+
experiments: Experiment[];
|
|
191
|
+
organization_id: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export interface TrackEventsResponse {
|
|
195
|
+
success: boolean;
|
|
196
|
+
inserted: number;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
export interface AssignVariantResponse {
|
|
200
|
+
variant_id: string;
|
|
201
|
+
variant_config: {
|
|
202
|
+
screens: ScreenConfig[];
|
|
203
|
+
};
|
|
204
|
+
cached: boolean;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Component props
|
|
208
|
+
export interface BaseComponentProps {
|
|
209
|
+
id: string;
|
|
210
|
+
analytics: Analytics;
|
|
211
|
+
onNext: (data?: Record<string, any>) => void;
|
|
212
|
+
onSkip?: () => void;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Analytics class interface
|
|
216
|
+
export interface Analytics {
|
|
217
|
+
track(eventName: string, properties?: Record<string, any>): void;
|
|
218
|
+
flush(): Promise<void>;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// Props interface for developer-registered custom screen components
|
|
222
|
+
export interface CustomScreenProps {
|
|
223
|
+
analytics: {
|
|
224
|
+
track: (event: string, properties?: Record<string, any>) => void;
|
|
225
|
+
};
|
|
226
|
+
onNext: () => void;
|
|
227
|
+
onSkip?: () => void;
|
|
228
|
+
preview?: boolean;
|
|
229
|
+
data?: Record<string, any>;
|
|
230
|
+
onDataUpdate?: (data: Record<string, any>) => void;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Main SDK props
|
|
234
|
+
export interface OnboardingFlowProps {
|
|
235
|
+
apiKey: string;
|
|
236
|
+
onComplete: (data?: Record<string, any>) => void;
|
|
237
|
+
onSkip?: () => void;
|
|
238
|
+
baseUrl?: string;
|
|
239
|
+
initialVariables?: Record<string, any>; // seed the variable store
|
|
240
|
+
customComponents?: Record<string, React.ComponentType<CustomScreenProps>>;
|
|
241
|
+
onUserIdGenerated?: (userId: string) => void; // Called when user ID is generated for analytics
|
|
242
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Condition, ComparisonOperator, ConditionalDestination, ConditionalRoutes } from './types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Resolve {variable_name} placeholders in a template string.
|
|
5
|
+
* Unknown variables resolve to empty string.
|
|
6
|
+
*/
|
|
7
|
+
export function resolveTemplate(
|
|
8
|
+
template: string,
|
|
9
|
+
variables: Record<string, any>
|
|
10
|
+
): string {
|
|
11
|
+
if (!template || !template.includes('{')) return template;
|
|
12
|
+
return template.replace(/\{(\w+(?:\.\w+)*)\}/g, (_match, varName) => {
|
|
13
|
+
// Support dot notation: "user.name" → variables["user.name"] or variables.user?.name
|
|
14
|
+
let value = variables[varName];
|
|
15
|
+
if (value === undefined && varName.includes('.')) {
|
|
16
|
+
const parts = varName.split('.');
|
|
17
|
+
value = variables[parts[0]];
|
|
18
|
+
for (let i = 1; i < parts.length && value != null; i++) {
|
|
19
|
+
value = value[parts[i]];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (value === undefined || value === null) return '';
|
|
23
|
+
return String(value);
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Evaluate a single comparison against the variable store.
|
|
29
|
+
*/
|
|
30
|
+
function evaluateComparison(
|
|
31
|
+
variableName: string,
|
|
32
|
+
operator: ComparisonOperator,
|
|
33
|
+
conditionValue: any,
|
|
34
|
+
variables: Record<string, any>
|
|
35
|
+
): boolean {
|
|
36
|
+
const actual = variables[variableName];
|
|
37
|
+
|
|
38
|
+
switch (operator) {
|
|
39
|
+
case 'equals':
|
|
40
|
+
return actual === conditionValue;
|
|
41
|
+
case 'not_equals':
|
|
42
|
+
return actual !== conditionValue;
|
|
43
|
+
case 'greater_than':
|
|
44
|
+
return typeof actual === 'number' && actual > conditionValue;
|
|
45
|
+
case 'less_than':
|
|
46
|
+
return typeof actual === 'number' && actual < conditionValue;
|
|
47
|
+
case 'contains':
|
|
48
|
+
if (typeof actual === 'string') return actual.includes(conditionValue);
|
|
49
|
+
if (Array.isArray(actual)) return actual.includes(conditionValue);
|
|
50
|
+
return false;
|
|
51
|
+
case 'in':
|
|
52
|
+
return Array.isArray(conditionValue) && conditionValue.includes(actual);
|
|
53
|
+
case 'is_empty':
|
|
54
|
+
return actual === undefined || actual === null || actual === '' ||
|
|
55
|
+
(Array.isArray(actual) && actual.length === 0);
|
|
56
|
+
case 'is_not_empty':
|
|
57
|
+
return actual !== undefined && actual !== null && actual !== '' &&
|
|
58
|
+
!(Array.isArray(actual) && actual.length === 0);
|
|
59
|
+
default:
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Recursively evaluate a Condition tree against the variable store.
|
|
66
|
+
* Supports: single comparison, all (AND), any (OR), not (negation).
|
|
67
|
+
* Returns true if no valid condition structure (default: show element).
|
|
68
|
+
*/
|
|
69
|
+
export function evaluateCondition(
|
|
70
|
+
condition: Condition,
|
|
71
|
+
variables: Record<string, any>
|
|
72
|
+
): boolean {
|
|
73
|
+
if (!condition) return true;
|
|
74
|
+
|
|
75
|
+
// AND logic
|
|
76
|
+
if (condition.all) {
|
|
77
|
+
return condition.all.every(c => evaluateCondition(c, variables));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// OR logic
|
|
81
|
+
if (condition.any) {
|
|
82
|
+
return condition.any.some(c => evaluateCondition(c, variables));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Negation
|
|
86
|
+
if (condition.not) {
|
|
87
|
+
return !evaluateCondition(condition.not, variables);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Single comparison
|
|
91
|
+
if (condition.variable && condition.operator) {
|
|
92
|
+
return evaluateComparison(condition.variable, condition.operator, condition.value, variables);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// No valid condition structure — default to true
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Resolve a destination (plain string or conditional) to a concrete screen ID.
|
|
101
|
+
* Returns the string destination or undefined.
|
|
102
|
+
*/
|
|
103
|
+
export function resolveDestination(
|
|
104
|
+
destination: string | ConditionalDestination | ConditionalRoutes | undefined,
|
|
105
|
+
variables: Record<string, any>
|
|
106
|
+
): string | undefined {
|
|
107
|
+
if (!destination) return undefined;
|
|
108
|
+
|
|
109
|
+
// Plain string — backward compatible
|
|
110
|
+
if (typeof destination === 'string') return destination;
|
|
111
|
+
|
|
112
|
+
// Routes array (multi-path)
|
|
113
|
+
if ('routes' in destination) {
|
|
114
|
+
const routes = destination as ConditionalRoutes;
|
|
115
|
+
for (const route of routes.routes) {
|
|
116
|
+
if (evaluateCondition(route.condition, variables)) {
|
|
117
|
+
return route.destination;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return routes.default;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// If/then/else
|
|
124
|
+
const cond = destination as ConditionalDestination;
|
|
125
|
+
if (evaluateCondition(cond.if, variables)) {
|
|
126
|
+
return cond.then;
|
|
127
|
+
}
|
|
128
|
+
if (cond.else) {
|
|
129
|
+
if (typeof cond.else === 'string') return cond.else;
|
|
130
|
+
return resolveDestination(cond.else, variables);
|
|
131
|
+
}
|
|
132
|
+
return 'next'; // fallback
|
|
133
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2017",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2017"],
|
|
6
|
+
"jsx": "react-native",
|
|
7
|
+
"declaration": true,
|
|
8
|
+
"outDir": "./lib",
|
|
9
|
+
"rootDir": "./src",
|
|
10
|
+
"strict": true,
|
|
11
|
+
"esModuleInterop": true,
|
|
12
|
+
"skipLibCheck": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"moduleResolution": "node",
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"allowSyntheticDefaultImports": true
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*"],
|
|
19
|
+
"exclude": ["node_modules", "lib", "**/*.spec.ts"]
|
|
20
|
+
}
|