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/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
+ }