noboarding 1.0.3-beta → 1.0.6-beta

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 CHANGED
@@ -10,16 +10,11 @@ npm install noboarding
10
10
  yarn add noboarding
11
11
  ```
12
12
 
13
- **📚 Complete Setup Guides:**
14
- - **[AI Setup](./SETUP_GUIDE.md#ai-setup)** - Copy/paste instructions for your AI coding assistant (Claude Code, Cursor, etc.)
15
- - **[Manual Setup](./SETUP_GUIDE.md#normal-setup)** - Step-by-step instructions
16
- - **[RevenueCat Integration](./REVENUECAT_SETUP.md)** - Detailed RevenueCat paywall guide
17
- - **[AI Custom Screen Guide](./AI_CUSTOM_SCREEN_GUIDE.md)** - Complete guide for building custom screens with data flow (for AI assistants)
18
-
19
13
  ## Quick Start
20
14
 
21
15
  ```typescript
22
16
  import { OnboardingFlow } from 'noboarding';
17
+ import { requestNotificationPermission, requestAppRating, signInWithApple } from './nativeHandlers';
23
18
 
24
19
  function App() {
25
20
  const [showOnboarding, setShowOnboarding] = useState(true);
@@ -42,11 +37,19 @@ function App() {
42
37
  onSkip={() => {
43
38
  setShowOnboarding(false);
44
39
  }}
40
+
45
41
  // Optional: Get the generated user ID to sync with other services
46
42
  onUserIdGenerated={(userId) => {
47
43
  console.log('User ID:', userId);
48
44
  // Use this to sync with RevenueCat, analytics, etc.
49
45
  }}
46
+
47
+ // Optional: Register native handlers for trigger_native actions
48
+ nativeHandlers={{
49
+ requestNotifications: requestNotificationPermission,
50
+ requestAppRating: requestAppRating,
51
+ signInWithApple: signInWithApple,
52
+ }}
50
53
  />
51
54
  );
52
55
  }
@@ -72,30 +75,47 @@ The SDK automatically detects your environment using React Native's `__DEV__` fl
72
75
 
73
76
  ## Screen Types
74
77
 
75
- ### Custom Screen (AI-generated)
78
+ There are **two screen types** in Noboarding:
76
79
 
77
- Screens built with the composable primitive system. The `ElementRenderer` recursively maps a JSON element tree to native React Native components (`View`, `Text`, `Image`, `ScrollView`, `TextInput`, `TouchableOpacity`).
80
+ ### 1. Noboard Screen (AI-Generated)
78
81
 
79
- ```typescript
80
- // A custom screen in your config looks like:
82
+ Screens built with the **composable primitive system**. The AI generates these screens in the dashboard, and the `ElementRenderer` recursively maps the JSON element tree to native React Native components (`View`, `Text`, `Image`, `ScrollView`, `TextInput`, `TouchableOpacity`).
83
+
84
+ **✅ Fully updateable over-the-air** — change UI, text, colors, layout without app updates.
85
+
86
+ ```json
81
87
  {
82
88
  "id": "welcome",
83
- "type": "custom_screen",
84
- "props": {},
89
+ "type": "noboard_screen",
85
90
  "elements": [
86
91
  {
87
92
  "id": "root",
88
93
  "type": "vstack",
89
94
  "style": { "width": "100%", "height": "100%", "padding": 24 },
90
95
  "children": [
91
- { "id": "title", "type": "text", "props": { "text": "Welcome!" }, "style": { "fontSize": 32, "fontWeight": "700" } },
96
+ {
97
+ "id": "title",
98
+ "type": "text",
99
+ "props": { "text": "Welcome!" },
100
+ "style": { "fontSize": 32, "fontWeight": "700" }
101
+ },
92
102
  { "id": "spacer", "type": "spacer" },
93
103
  {
94
104
  "id": "cta",
95
105
  "type": "hstack",
96
- "style": { "backgroundColor": "#000", "borderRadius": 12, "padding": 16, "justifyContent": "center" },
106
+ "style": {
107
+ "backgroundColor": "#000",
108
+ "borderRadius": 12,
109
+ "padding": 16,
110
+ "justifyContent": "center"
111
+ },
97
112
  "children": [
98
- { "id": "cta_text", "type": "text", "props": { "text": "Get Started" }, "style": { "color": "#fff", "fontSize": 16 } }
113
+ {
114
+ "id": "cta_text",
115
+ "type": "text",
116
+ "props": { "text": "Get Started" },
117
+ "style": { "color": "#fff", "fontSize": 16 }
118
+ }
99
119
  ],
100
120
  "action": { "type": "navigate", "destination": "next" }
101
121
  }
@@ -105,62 +125,16 @@ Screens built with the composable primitive system. The `ElementRenderer` recurs
105
125
  }
106
126
  ```
107
127
 
108
- ### Pre-built Components
109
-
110
- - **WelcomeScreen** — Image + title + subtitle + CTA button
111
- - **TextInput** — Form for collecting user data (name, email, etc.)
112
- - **SocialLogin** — Apple/Google/Facebook authentication buttons
113
-
114
- ## Composable Primitives
115
-
116
- The element tree uses these building blocks:
117
-
118
- **Containers** (have `children` array):
119
- - `vstack` — vertical flex column
120
- - `hstack` — horizontal flex row
121
- - `zstack` — layered/overlapping elements
122
- - `scrollview` — scrollable container
123
-
124
- **Content** (leaf elements with `props`):
125
- - `text` — text content (`props.text`)
126
- - `image` — image (`props.url`, `props.slotNumber`)
127
- - `video` — video placeholder (`props.url`)
128
- - `lottie` — Lottie animation (`props.url`)
129
- - `icon` — emoji (`props.emoji`) or named icon (`props.name`, `props.library`)
130
- - `input` — text field (`props.placeholder`, `props.type`)
131
- - `spacer` — flexible empty space
132
- - `divider` — horizontal line
128
+ ### 2. Custom Screen (Developer-Registered Components)
133
129
 
134
- ## Actions
130
+ React Native components you write and register with the SDK. Used for advanced native features that can't be represented as JSON element trees (camera, biometrics, complex native SDKs, custom animations).
135
131
 
136
- Any container can have an `action` to make it interactive:
137
-
138
- ```typescript
139
- action: {
140
- type: 'tap' | 'navigate' | 'link' | 'toggle' | 'dismiss',
141
- destination?: string // URL for link, screen ID for navigate
142
- }
143
- ```
144
-
145
- | Action | Behavior |
146
- |--------|----------|
147
- | `tap` | Generic tap handler |
148
- | `navigate` | Go to `"next"`, `"previous"`, or a specific screen ID |
149
- | `link` | Open URL via `Linking.openURL` |
150
- | `toggle` | Toggle selected/unselected state (visual border change) |
151
- | `dismiss` | Dismiss current screen or flow |
152
-
153
- ## Custom Screens (Developer-Registered Components)
154
-
155
- For advanced use cases requiring native features (camera, payments, biometrics) or third-party SDKs, you can create custom React Native components and register them with the SDK.
156
-
157
- ### Creating a Custom Screen
158
-
159
- 1. **Create your component** with the required props:
132
+ **❌ Code NOT updateable over-the-air** requires app update to change component logic.
133
+ **✅ Flow control updateable** — can add/remove/reorder these screens in dashboard without app updates.
160
134
 
161
135
  ```typescript
162
136
  // screens/PaywallScreen.tsx
163
- import React, { useEffect } from 'react';
137
+ import React from 'react';
164
138
  import { View, Text, Button } from 'react-native';
165
139
  import type { CustomScreenProps } from 'noboarding';
166
140
 
@@ -172,25 +146,6 @@ export const PaywallScreen: React.FC<CustomScreenProps> = ({
172
146
  data,
173
147
  onDataUpdate,
174
148
  }) => {
175
- useEffect(() => {
176
- analytics.track('paywall_viewed', {
177
- screen_id: 'paywall',
178
- });
179
- }, []);
180
-
181
- const handlePurchase = () => {
182
- analytics.track('paywall_conversion', {
183
- package: 'premium_monthly',
184
- });
185
-
186
- onDataUpdate?.({
187
- premium: true,
188
- purchase_date: new Date().toISOString(),
189
- });
190
-
191
- onNext();
192
- };
193
-
194
149
  // Preview mode for dashboard
195
150
  if (preview) {
196
151
  return (
@@ -199,9 +154,6 @@ export const PaywallScreen: React.FC<CustomScreenProps> = ({
199
154
  <Text style={{ fontSize: 24, fontWeight: 'bold', marginVertical: 20 }}>
200
155
  Paywall Preview
201
156
  </Text>
202
- <Text style={{ color: '#666', marginBottom: 20 }}>
203
- (Real paywall only works in app)
204
- </Text>
205
157
  <Button title="Continue" onPress={onNext} />
206
158
  </View>
207
159
  );
@@ -212,262 +164,308 @@ export const PaywallScreen: React.FC<CustomScreenProps> = ({
212
164
  <Text style={{ fontSize: 28, fontWeight: 'bold', marginBottom: 20 }}>
213
165
  Unlock Premium
214
166
  </Text>
215
- <Button title="Subscribe - $9.99/month" onPress={handlePurchase} />
216
- {onSkip && (
217
- <Button title="Maybe Later" onPress={onSkip} color="#666" />
218
- )}
167
+ <Button title="Subscribe - $9.99/month" onPress={() => {
168
+ analytics.track('paywall_conversion');
169
+ onDataUpdate?.({ premium: true });
170
+ onNext();
171
+ }} />
172
+ {onSkip && <Button title="Maybe Later" onPress={onSkip} color="#666" />}
219
173
  </View>
220
174
  );
221
175
  };
176
+
177
+ // Register in your app
178
+ <OnboardingFlow
179
+ customComponents={{
180
+ PaywallScreen: PaywallScreen,
181
+ }}
182
+ ...
183
+ />
222
184
  ```
223
185
 
224
- 2. **Register the component** in your app:
186
+ Then add to your flow in the dashboard by clicking "Add Custom Screen" and entering the component name `PaywallScreen`.
225
187
 
226
- ```typescript
227
- import { OnboardingFlow } from 'noboarding';
228
- import { PaywallScreen } from './screens/PaywallScreen';
188
+ ## Composable Primitives
229
189
 
230
- function App() {
231
- return (
232
- <OnboardingFlow
233
- apiKey="sk_live_your_api_key_here"
234
- customComponents={{
235
- PaywallScreen: PaywallScreen, // Register here
236
- }}
237
- onComplete={(userData) => {
238
- console.log('User data:', userData);
239
- // userData includes data from custom screens
240
- }}
241
- />
242
- );
190
+ Noboard screens are built from a small set of building blocks:
191
+
192
+ ### Containers (have `children` array)
193
+ - `vstack` — vertical flex column
194
+ - `hstack` — horizontal flex row
195
+ - `zstack` layered/overlapping elements
196
+ - `scrollview` — scrollable container
197
+
198
+ ### Content (leaf elements with `props`)
199
+ - `text` text content (`props.text`)
200
+ - `image` — image (`props.url`, `props.imageDescription`)
201
+ - `video` — video placeholder (`props.videoDescription`)
202
+ - `lottie` — Lottie animation (`props.animationDescription`)
203
+ - `icon` — emoji (`props.emoji`) or named icon (`props.name`, `props.library`)
204
+ - `input` — text field (`props.placeholder`, `props.type`, `props.variable`)
205
+ - `spacer` — flexible empty space
206
+ - `divider` — horizontal line
207
+
208
+ **Note:** There are no dedicated `button`, `checkbox`, or `card` elements. Complex components are composed from stacks with actions attached.
209
+
210
+ ## Actions
211
+
212
+ Any container can have an `action` or `actions` array to make it interactive:
213
+
214
+ ```typescript
215
+ action: {
216
+ type: 'tap' | 'navigate' | 'link' | 'toggle' | 'dismiss' | 'set_variable' | 'trigger_native',
217
+ destination?: string, // For navigate/link
218
+ variable?: string, // For set_variable
219
+ value?: any, // For set_variable
220
+ handlerName?: string, // For trigger_native
221
+ handlerParams?: Record<string, any> // For trigger_native
243
222
  }
244
223
  ```
245
224
 
246
- 3. **Add to your flow** in the dashboard:
247
- - Click "Add Custom Screen"
248
- - Enter component name: `PaywallScreen`
249
- - Position in flow
225
+ ### Action Types
250
226
 
251
- ### CustomScreenProps Interface
227
+ | Action | Behavior | Use Case |
228
+ |--------|----------|----------|
229
+ | `tap` | Generic tap handler | Analytics tracking |
230
+ | `navigate` | Go to `"next"`, `"previous"`, or a specific screen ID | Flow navigation |
231
+ | `link` | Open URL via `Linking.openURL` | External links |
232
+ | `toggle` | Toggle selected/unselected state (visual border change) | Single/multi-select options |
233
+ | `dismiss` | Dismiss current screen or flow | Exit/skip |
234
+ | `set_variable` | Store a value in the variable store | Save form data, selections |
235
+ | `trigger_native` | **NEW:** Call registered native handler | Permissions, auth, ratings, native features |
252
236
 
253
- ```typescript
254
- interface CustomScreenProps {
255
- analytics: {
256
- track: (event: string, properties?: Record<string, any>) => void;
257
- };
258
- onNext: () => void;
259
- onBack?: () => void; // Navigate to previous screen (undefined on first screen)
260
- onSkip?: () => void;
261
- preview?: boolean; // True when rendering in dashboard preview
262
- data?: Record<string, any>; // Previously collected user data
263
- onDataUpdate?: (data: Record<string, any>) => void; // Update collected data
237
+ ### Multiple Actions
238
+
239
+ Elements can have multiple actions that execute in sequence:
240
+
241
+ ```json
242
+ {
243
+ "type": "hstack",
244
+ "actions": [
245
+ { "type": "set_variable", "variable": "selected_plan", "value": "premium" },
246
+ { "type": "navigate", "destination": "next" }
247
+ ]
264
248
  }
265
249
  ```
266
250
 
267
- ### RevenueCat Integration Example
251
+ ## Native Handlers (trigger_native Action)
252
+
253
+ **The best of both worlds:** Over-the-air updateable UI that triggers native code compiled into your app.
254
+
255
+ ### Why Use trigger_native?
256
+
257
+ For native features like notifications, authentication, app ratings, camera access, or biometrics:
258
+ - ✅ **UI fully updateable** — Change button text, colors, position via dashboard
259
+ - ✅ **Native code stays in app** — Logic never changes, no app updates needed
260
+ - ✅ **Flow control updateable** — Add/remove/reorder via dashboard
261
+ - ✅ **Works for all native features** — Any native API or SDK
268
262
 
269
- Here's a complete example integrating RevenueCat paywalls:
263
+ ### How It Works
264
+
265
+ 1. **Write native handler functions** in your app (one-time setup)
266
+ 2. **Register handlers** with `OnboardingFlow`
267
+ 3. **AI generates buttons** in dashboard that trigger these handlers
268
+ 4. **Update button UI remotely** without app updates
269
+
270
+ ### Example: Notification Permissions
271
+
272
+ **Step 1:** Create the native handler
270
273
 
271
274
  ```typescript
272
- // screens/RevenueCatPaywall.tsx
273
- import React, { useState, useEffect } from 'react';
274
- import { View, Text, ActivityIndicator, Alert } from 'react-native';
275
- import Purchases, { PurchasesOffering } from 'react-native-purchases';
276
- import type { CustomScreenProps } from 'noboarding';
275
+ // nativeHandlers.ts
276
+ import * as Notifications from 'expo-notifications';
277
277
 
278
- export const RevenueCatPaywall: React.FC<CustomScreenProps> = ({
279
- analytics,
280
- onNext,
281
- onSkip,
282
- preview,
283
- onDataUpdate,
284
- }) => {
285
- const [offering, setOffering] = useState<PurchasesOffering | null>(null);
286
- const [loading, setLoading] = useState(true);
287
-
288
- useEffect(() => {
289
- analytics.track('paywall_viewed');
290
- loadOffering();
291
- }, []);
292
-
293
- const loadOffering = async () => {
294
- try {
295
- const offerings = await Purchases.getOfferings();
296
- setOffering(offerings.current);
297
-
298
- analytics.track('paywall_loaded', {
299
- offering_id: offerings.current?.identifier,
300
- packages_count: offerings.current?.availablePackages.length,
301
- });
302
- } catch (error: any) {
303
- analytics.track('paywall_error', { error: error.message });
304
- } finally {
305
- setLoading(false);
306
- }
307
- };
278
+ export const requestNotificationPermission = async () => {
279
+ const { status: existingStatus } = await Notifications.getPermissionsAsync();
280
+
281
+ if (existingStatus !== 'granted') {
282
+ const { status } = await Notifications.requestPermissionsAsync();
283
+ return { granted: status === 'granted', status };
284
+ }
285
+
286
+ return { granted: true, status: existingStatus };
287
+ };
288
+ ```
289
+
290
+ **Step 2:** Register with SDK
291
+
292
+ ```typescript
293
+ import { OnboardingFlow } from 'noboarding';
294
+ import { requestNotificationPermission } from './nativeHandlers';
295
+
296
+ <OnboardingFlow
297
+ testKey="nb_test_..."
298
+ nativeHandlers={{
299
+ requestNotifications: requestNotificationPermission,
300
+ }}
301
+ onComplete={(data) => console.log(data)}
302
+ />
303
+ ```
304
+
305
+ **Step 3:** AI generates the button in dashboard
306
+
307
+ In the dashboard AI Chat, say: "Create a button that says 'Enable Notifications' and triggers the `requestNotifications` handler"
308
+
309
+ The AI generates:
308
310
 
309
- const handlePurchase = async (packageId: string) => {
310
- try {
311
- analytics.track('paywall_purchase_started', { package: packageId });
312
-
313
- const pkg = offering?.availablePackages.find(p => p.identifier === packageId);
314
- if (!pkg) return;
315
-
316
- const { customerInfo } = await Purchases.purchasePackage(pkg);
317
- const isPremium = customerInfo.entitlements.active['premium'] !== undefined;
318
-
319
- if (isPremium) {
320
- analytics.track('paywall_conversion', {
321
- package: packageId,
322
- price: pkg.product.priceString,
323
- });
324
-
325
- onDataUpdate?.({
326
- premium: true,
327
- package: packageId,
328
- purchase_timestamp: new Date().toISOString(),
329
- });
330
-
331
- Alert.alert('Welcome to Premium!', '', [
332
- { text: 'Continue', onPress: onNext }
333
- ]);
334
- }
335
- } catch (error: any) {
336
- const cancelled = error.userCancelled;
337
-
338
- analytics.track('paywall_purchase_failed', {
339
- package: packageId,
340
- cancelled,
341
- });
342
-
343
- if (!cancelled) {
344
- Alert.alert('Purchase Failed', 'Please try again.');
345
- }
311
+ ```json
312
+ {
313
+ "type": "hstack",
314
+ "style": { "backgroundColor": "#007AFF", "padding": 16, "borderRadius": 12 },
315
+ "children": [
316
+ {
317
+ "type": "text",
318
+ "props": { "text": "Enable Notifications" },
319
+ "style": { "color": "#fff", "fontSize": 16, "fontWeight": "600" }
346
320
  }
347
- };
321
+ ],
322
+ "actions": [
323
+ {
324
+ "type": "trigger_native",
325
+ "handlerName": "requestNotifications",
326
+ "variable": "notification_result"
327
+ },
328
+ { "type": "navigate", "destination": "next" }
329
+ ]
330
+ }
331
+ ```
348
332
 
349
- const handleSkip = () => {
350
- analytics.track('paywall_dismissed');
351
- onSkip?.() || onNext();
352
- };
333
+ **Step 4:** Update button UI remotely
353
334
 
354
- // Preview mode
355
- if (preview) {
356
- return (
357
- <View style={{ padding: 20, alignItems: 'center' }}>
358
- <Text style={{ fontSize: 64, marginBottom: 20 }}>💎</Text>
359
- <Text style={{ fontSize: 24, fontWeight: 'bold' }}>
360
- RevenueCat Paywall
361
- </Text>
362
- <Text style={{ color: '#999', marginTop: 8 }}>
363
- (Preview - real paywall in app)
364
- </Text>
365
- </View>
366
- );
367
- }
335
+ Change the button text, colors, position in the dashboard — no app update needed!
368
336
 
369
- if (loading) {
370
- return (
371
- <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
372
- <ActivityIndicator size="large" />
373
- </View>
374
- );
375
- }
337
+ ### More Examples
376
338
 
377
- return (
378
- <View style={{ flex: 1, padding: 20 }}>
379
- <Text style={{ fontSize: 32, fontWeight: 'bold', marginBottom: 20 }}>
380
- Unlock Premium
381
- </Text>
339
+ #### App Store Rating
382
340
 
383
- {offering?.availablePackages.map(pkg => (
384
- <TouchableOpacity
385
- key={pkg.identifier}
386
- onPress={() => handlePurchase(pkg.identifier)}
387
- style={{
388
- backgroundColor: '#007AFF',
389
- padding: 16,
390
- borderRadius: 12,
391
- marginBottom: 12,
392
- }}
393
- >
394
- <Text style={{ color: '#FFF', fontSize: 18, fontWeight: 'bold' }}>
395
- {pkg.product.title} - {pkg.product.priceString}
396
- </Text>
397
- </TouchableOpacity>
398
- ))}
399
-
400
- <Button title="Maybe Later" onPress={handleSkip} color="#666" />
401
- </View>
402
- );
341
+ ```typescript
342
+ // nativeHandlers.ts
343
+ import * as StoreReview from 'expo-store-review';
344
+
345
+ export const requestAppRating = async () => {
346
+ const isAvailable = await StoreReview.isAvailableAsync();
347
+ if (isAvailable) {
348
+ await StoreReview.requestReview();
349
+ return { prompted: true };
350
+ }
351
+ return { prompted: false, reason: 'not_available' };
403
352
  };
404
353
  ```
405
354
 
406
- **Setup:**
407
-
408
- 1. Install RevenueCat:
409
- ```bash
410
- npm install react-native-purchases
355
+ ```typescript
356
+ <OnboardingFlow
357
+ nativeHandlers={{
358
+ requestAppRating: requestAppRating,
359
+ }}
360
+ />
411
361
  ```
412
362
 
413
- 2. Configure RevenueCat in your app initialization:
363
+ #### Apple Sign-In
364
+
414
365
  ```typescript
415
- // App.tsx
416
- import Purchases from 'react-native-purchases';
417
-
418
- useEffect(() => {
419
- Purchases.configure({
420
- apiKey: Platform.OS === 'ios'
421
- ? 'appl_YOUR_KEY'
422
- : 'goog_YOUR_KEY',
423
- });
424
- }, []);
366
+ // nativeHandlers.ts
367
+ import * as AppleAuthentication from 'expo-apple-authentication';
368
+
369
+ export const signInWithApple = async () => {
370
+ try {
371
+ const credential = await AppleAuthentication.signInAsync({
372
+ requestedScopes: [
373
+ AppleAuthentication.AppleAuthenticationScope.FULL_NAME,
374
+ AppleAuthentication.AppleAuthenticationScope.EMAIL,
375
+ ],
376
+ });
377
+ return {
378
+ success: true,
379
+ userId: credential.user,
380
+ email: credential.email,
381
+ fullName: credential.fullName,
382
+ };
383
+ } catch (error) {
384
+ return { success: false, error: error.code };
385
+ }
386
+ };
425
387
  ```
426
388
 
427
- 3. Register and use in onboarding:
428
389
  ```typescript
429
390
  <OnboardingFlow
430
- customComponents={{
431
- RevenueCatPaywall: RevenueCatPaywall,
391
+ nativeHandlers={{
392
+ signInWithApple: signInWithApple,
432
393
  }}
433
394
  />
434
395
  ```
435
396
 
436
- 4. Set up webhooks (see below) to track conversions server-side
397
+ ### Handler Return Values
437
398
 
438
- ### Best Practices
399
+ Handlers can return data that gets saved to the variable store:
439
400
 
440
- - ✅ Always implement `preview` mode for dashboard compatibility
441
- - ✅ Track key events with `analytics.track()`
442
- - ✅ Use `onDataUpdate()` to save data from custom screens
443
- - ✅ Handle errors gracefully with user-friendly messages
444
- - ✅ Call `onNext()` when screen is complete
401
+ ```json
402
+ {
403
+ "type": "trigger_native",
404
+ "handlerName": "requestNotifications",
405
+ "variable": "notification_result"
406
+ }
407
+ ```
445
408
 
446
- For more details, see [AI Custom Screen Guide](./AI_CUSTOM_SCREEN_GUIDE.md).
409
+ The returned value is automatically stored in `variables.notification_result` and can be:
410
+ - Used in conditional navigation
411
+ - Referenced in text templates: `{notification_result.status}`
412
+ - Passed to `onComplete` callback
447
413
 
448
- ## API
414
+ ### Passing Parameters
449
415
 
450
- ### OnboardingFlow Props
416
+ Send configuration to handlers:
417
+
418
+ ```json
419
+ {
420
+ "type": "trigger_native",
421
+ "handlerName": "trackEvent",
422
+ "handlerParams": {
423
+ "eventName": "button_clicked",
424
+ "category": "onboarding"
425
+ }
426
+ }
427
+ ```
428
+
429
+ ```typescript
430
+ export const trackEvent = async (params) => {
431
+ await analytics.track(params.eventName, { category: params.category });
432
+ };
433
+ ```
434
+
435
+ ## CustomScreenProps Interface
436
+
437
+ For developer-registered custom screens:
438
+
439
+ ```typescript
440
+ interface CustomScreenProps {
441
+ analytics: {
442
+ track: (event: string, properties?: Record<string, any>) => void;
443
+ };
444
+ onNext: () => void;
445
+ onBack?: () => void; // Navigate to previous screen (undefined on first screen)
446
+ onSkip?: () => void;
447
+ preview?: boolean; // True when rendering in dashboard preview
448
+ data?: Record<string, any>; // Previously collected user data
449
+ onDataUpdate?: (data: Record<string, any>) => void; // Update collected data
450
+ }
451
+ ```
452
+
453
+ ## OnboardingFlow Props
451
454
 
452
455
  | Prop | Type | Required | Description |
453
456
  |------|------|----------|-------------|
454
- | `apiKey` | `string` | Yes | Your API key from the dashboard |
457
+ | `testKey` | `string` | No* | Test API key (`nb_test_...`) for development |
458
+ | `productionKey` | `string` | No* | Production API key (`nb_live_...`) for production |
459
+ | `apiKey` | `string` | No* | Legacy single key (backwards compatible) |
455
460
  | `onComplete` | `(data?) => void` | Yes | Called when user completes onboarding |
456
461
  | `onSkip` | `() => void` | No | Called when user skips onboarding |
457
462
  | `baseUrl` | `string` | No | Custom API base URL |
458
463
  | `customComponents` | `Record<string, Component>` | No | Developer-registered custom screen components |
464
+ | `nativeHandlers` | `Record<string, Function>` | No | Native handler functions for `trigger_native` actions |
459
465
  | `initialVariables` | `Record<string, any>` | No | Initial values for the variable store |
460
- | `onUserIdGenerated` | `(userId: string) => void` | No | Called when SDK generates user ID (use to sync with RevenueCat, analytics, etc.) |
461
-
462
- ### ElementRenderer Props
466
+ | `onUserIdGenerated` | `(userId: string) => void` | No | Called when SDK generates user ID |
463
467
 
464
- | Prop | Type | Required | Description |
465
- |------|------|----------|-------------|
466
- | `elements` | `ElementNode[]` | Yes | The element tree to render |
467
- | `analytics` | `AnalyticsManager` | Yes | Analytics manager for tracking |
468
- | `screenId` | `string` | Yes | Current screen ID for analytics |
469
- | `onNavigate` | `(destination: string) => void` | Yes | Navigation handler |
470
- | `onDismiss` | `() => void` | Yes | Dismiss handler |
468
+ *At least one key is required: either `apiKey`, or both `testKey` and `productionKey`
471
469
 
472
470
  ## Auto-Tracked Events
473
471
 
@@ -485,6 +483,64 @@ The SDK automatically tracks:
485
483
  - `onboarding_abandoned`
486
484
  - `element_action` — tracks every action with element ID, action type, and screen ID
487
485
 
486
+ ## Variables & Templating
487
+
488
+ Variables store data collected during onboarding:
489
+
490
+ ### Setting Variables
491
+
492
+ ```json
493
+ {
494
+ "type": "input",
495
+ "props": { "placeholder": "Enter your name", "variable": "user_name" }
496
+ }
497
+ ```
498
+
499
+ ```json
500
+ {
501
+ "type": "hstack",
502
+ "action": {
503
+ "type": "set_variable",
504
+ "variable": "selected_plan",
505
+ "value": "premium"
506
+ }
507
+ }
508
+ ```
509
+
510
+ ### Using Variables in Text
511
+
512
+ ```json
513
+ {
514
+ "type": "text",
515
+ "props": { "text": "Welcome back, {user_name}!" }
516
+ }
517
+ ```
518
+
519
+ ### Conditional Navigation
520
+
521
+ ```json
522
+ {
523
+ "type": "navigate",
524
+ "destination": {
525
+ "if": { "variable": "selected_plan", "operator": "equals", "value": "premium" },
526
+ "then": "payment_screen",
527
+ "else": "free_trial_screen"
528
+ }
529
+ }
530
+ ```
531
+
532
+ ### Conditional Visibility
533
+
534
+ ```json
535
+ {
536
+ "type": "text",
537
+ "props": { "text": "Premium features unlocked!" },
538
+ "conditions": {
539
+ "show_if": { "variable": "premium", "operator": "equals", "value": true }
540
+ }
541
+ }
542
+ ```
543
+
488
544
  ## Exports
489
545
 
490
546
  ```typescript
@@ -505,6 +561,7 @@ import type {
505
561
  ElementStyle,
506
562
  ElementPosition,
507
563
  AnalyticsEvent,
564
+ CustomScreenProps,
508
565
  } from 'noboarding';
509
566
 
510
567
  // Utilities
@@ -515,42 +572,20 @@ import { API, AnalyticsManager } from 'noboarding';
515
572
 
516
573
  ### Building the SDK
517
574
 
518
- The TestApp imports the SDK from the compiled `lib/` directory (`"main": "lib/index.js"`), not from `src/` directly. After making any changes to files in `sdk/src/`, you must rebuild before testing:
575
+ The TestApp imports the SDK from the compiled `lib/` directory, not from `src/` directly. After making changes to `sdk/src/`, you must rebuild:
519
576
 
520
577
  ```bash
521
578
  cd sdk
522
579
  npm run build
523
580
  ```
524
581
 
525
- Then restart the TestApp. If you skip this step, the TestApp will still be running the old compiled code and your changes won't take effect.
582
+ Then restart the TestApp.
526
583
 
527
584
  ### Dashboard Preview Integration
528
585
 
529
- The dashboard uses **local copies** of SDK source files for the preview feature. When you modify SDK source files, they need to be synced to the dashboard.
530
-
531
- **Why copies?** Next.js/Turbopack doesn't support importing from external directories with the react-native-web setup, so the dashboard maintains local copies in `dashboard/lib/sdk/`.
532
-
533
- #### Files That Need Syncing
586
+ The dashboard uses **local copies** of SDK source files for the preview feature. When you modify SDK source files, sync them to the dashboard:
534
587
 
535
- When you modify these SDK files:
536
- - `src/types.ts` → Auto-synced to `dashboard/lib/sdk/types.ts`
537
- - `src/variableUtils.ts` → Auto-synced to `dashboard/lib/sdk/variableUtils.ts`
538
- - `src/components/ElementRenderer.tsx` → ⚠️ **NOT auto-synced** (dashboard has web-specific modifications)
539
-
540
- **ElementRenderer Special Case:**
541
-
542
- The dashboard copy of `ElementRenderer.tsx` has web-specific modifications for icon support:
543
- - Uses `react-icons` instead of `@expo/vector-icons`
544
- - Renders real icons in preview (Feather, Material, Ionicons, FontAwesome)
545
- - Gradients fall back to solid colors
546
-
547
- **If you modify ElementRenderer.tsx significantly:**
548
- 1. Run `npm run sync:full` from project root to copy it
549
- 2. Manually re-add web icon imports and logic (check git diff to see what changed)
550
-
551
- #### Syncing Methods
552
-
553
- **Manual sync (run when needed):**
588
+ **Manual sync:**
554
589
  ```bash
555
590
  # From project root
556
591
  npm run sync
@@ -562,11 +597,9 @@ npm run sync
562
597
  npm run sync:watch
563
598
  ```
564
599
 
565
- This watches SDK files and automatically copies changes to the dashboard when you save.
566
-
567
600
  **Full development mode:**
568
601
  ```bash
569
- # From project root - starts dashboard + auto-sync
602
+ # From project root
570
603
  npm run dev
571
604
  ```
572
605
 
@@ -575,11 +608,11 @@ This command:
575
608
  2. Starts file watcher for auto-sync
576
609
  3. Starts dashboard dev server
577
610
 
578
- #### Important Notes
611
+ #### Files That Need Syncing
579
612
 
580
- - ⚠️ **Dashboard preview uses copies** - Changes to SDK files won't appear in dashboard preview until synced
581
- - **Mobile app uses npm package** - TestApp uses the built SDK from `lib/`, requires `npm run build`
582
- - 🔄 **Keep in sync** - Run `npm run sync:watch` while developing SDK to keep dashboard preview accurate
613
+ - `src/types.ts` Auto-synced to `dashboard/lib/sdk/types.ts`
614
+ - `src/variableUtils.ts` Auto-synced to `dashboard/lib/sdk/variableUtils.ts`
615
+ - `src/components/ElementRenderer.tsx` ⚠️ **NOT auto-synced** (dashboard has web-specific modifications)
583
616
 
584
617
  ## Requirements
585
618