noboarding 1.0.3-beta → 1.0.7
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/ANIMATIONS.md +446 -0
- package/README.md +356 -323
- package/lib/OnboardingFlow.js +21 -4
- package/lib/animationUtils.d.ts +19 -0
- package/lib/animationUtils.js +252 -0
- package/lib/components/ElementRenderer.d.ts +5 -0
- package/lib/components/ElementRenderer.js +231 -40
- package/lib/types.d.ts +46 -0
- package/package.json +4 -2
- package/src/OnboardingFlow.tsx +19 -0
- package/src/animationUtils.ts +292 -0
- package/src/components/ElementRenderer.tsx +287 -22
- package/src/types.ts +51 -0
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
|
-
|
|
78
|
+
There are **two screen types** in Noboarding:
|
|
76
79
|
|
|
77
|
-
|
|
80
|
+
### 1. Noboard Screen (AI-Generated)
|
|
78
81
|
|
|
79
|
-
|
|
80
|
-
|
|
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": "
|
|
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
|
-
{
|
|
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": {
|
|
106
|
+
"style": {
|
|
107
|
+
"backgroundColor": "#000",
|
|
108
|
+
"borderRadius": 12,
|
|
109
|
+
"padding": 16,
|
|
110
|
+
"justifyContent": "center"
|
|
111
|
+
},
|
|
97
112
|
"children": [
|
|
98
|
-
{
|
|
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
|
-
###
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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={
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
186
|
+
Then add to your flow in the dashboard by clicking "Add Custom Screen" and entering the component name `PaywallScreen`.
|
|
225
187
|
|
|
226
|
-
|
|
227
|
-
import { OnboardingFlow } from 'noboarding';
|
|
228
|
-
import { PaywallScreen } from './screens/PaywallScreen';
|
|
188
|
+
## Composable Primitives
|
|
229
189
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
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
|
-
|
|
247
|
-
- Click "Add Custom Screen"
|
|
248
|
-
- Enter component name: `PaywallScreen`
|
|
249
|
-
- Position in flow
|
|
225
|
+
### Action Types
|
|
250
226
|
|
|
251
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
273
|
-
import
|
|
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
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
350
|
-
analytics.track('paywall_dismissed');
|
|
351
|
-
onSkip?.() || onNext();
|
|
352
|
-
};
|
|
333
|
+
**Step 4:** Update button UI remotely
|
|
353
334
|
|
|
354
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
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
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
355
|
+
```typescript
|
|
356
|
+
<OnboardingFlow
|
|
357
|
+
nativeHandlers={{
|
|
358
|
+
requestAppRating: requestAppRating,
|
|
359
|
+
}}
|
|
360
|
+
/>
|
|
411
361
|
```
|
|
412
362
|
|
|
413
|
-
|
|
363
|
+
#### Apple Sign-In
|
|
364
|
+
|
|
414
365
|
```typescript
|
|
415
|
-
//
|
|
416
|
-
import
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
431
|
-
|
|
391
|
+
nativeHandlers={{
|
|
392
|
+
signInWithApple: signInWithApple,
|
|
432
393
|
}}
|
|
433
394
|
/>
|
|
434
395
|
```
|
|
435
396
|
|
|
436
|
-
|
|
397
|
+
### Handler Return Values
|
|
437
398
|
|
|
438
|
-
|
|
399
|
+
Handlers can return data that gets saved to the variable store:
|
|
439
400
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
401
|
+
```json
|
|
402
|
+
{
|
|
403
|
+
"type": "trigger_native",
|
|
404
|
+
"handlerName": "requestNotifications",
|
|
405
|
+
"variable": "notification_result"
|
|
406
|
+
}
|
|
407
|
+
```
|
|
445
408
|
|
|
446
|
-
|
|
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
|
-
|
|
414
|
+
### Passing Parameters
|
|
449
415
|
|
|
450
|
-
|
|
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
|
-
| `
|
|
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
|
|
461
|
-
|
|
462
|
-
### ElementRenderer Props
|
|
466
|
+
| `onUserIdGenerated` | `(userId: string) => void` | No | Called when SDK generates user ID |
|
|
463
467
|
|
|
464
|
-
|
|
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
|
|
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.
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
####
|
|
611
|
+
#### Files That Need Syncing
|
|
579
612
|
|
|
580
|
-
-
|
|
581
|
-
-
|
|
582
|
-
-
|
|
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
|
|