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.
@@ -48,6 +48,7 @@ exports.ElementRenderer = void 0;
48
48
  const react_1 = __importStar(require("react"));
49
49
  const react_native_1 = require("react-native");
50
50
  const variableUtils_1 = require("../variableUtils");
51
+ const animationUtils_1 = require("../animationUtils");
51
52
  const expo_linear_gradient_1 = require("expo-linear-gradient");
52
53
  const vector_icons_1 = require("@expo/vector-icons");
53
54
  const IconSets = {
@@ -59,11 +60,26 @@ const IconSets = {
59
60
  fontawesome: vector_icons_1.FontAwesome,
60
61
  'sf-symbols': vector_icons_1.Ionicons,
61
62
  };
62
- const ElementRenderer = ({ elements, analytics, screenId, onNavigate, onDismiss, variables = {}, onSetVariable, }) => {
63
+ const ElementRenderer = ({ elements, analytics, screenId, onNavigate, onDismiss, variables = {}, onSetVariable, assets = [], }) => {
63
64
  // Track toggled element IDs for toggle actions
64
65
  const [toggledIds, setToggledIds] = (0, react_1.useState)(new Set());
65
66
  // Track selection groups: group name → selected element ID
66
67
  const [groupSelections, setGroupSelections] = (0, react_1.useState)({});
68
+ // Track text input values locally (uncontrolled)
69
+ const [inputValues, setInputValues] = (0, react_1.useState)({});
70
+ // Helper function to resolve asset: URLs to actual data URLs
71
+ const resolveAssetUrl = (url) => {
72
+ if (!url || !url.startsWith('asset:')) {
73
+ return url; // Return as-is if not an asset reference
74
+ }
75
+ const assetName = url.replace('asset:', '');
76
+ const asset = assets.find(a => a.name === assetName);
77
+ if (asset) {
78
+ return asset.data; // Return the base64 data URL
79
+ }
80
+ console.warn(`Asset not found: ${assetName}`);
81
+ return url; // Fallback to original URL
82
+ };
67
83
  const executeAction = (0, react_1.useCallback)((action, element) => {
68
84
  // Track the action
69
85
  analytics === null || analytics === void 0 ? void 0 : analytics.track('element_action', {
@@ -77,6 +93,12 @@ const ElementRenderer = ({ elements, analytics, screenId, onNavigate, onDismiss,
77
93
  if (action.variable !== undefined && onSetVariable) {
78
94
  onSetVariable(action.variable, action.value);
79
95
  }
96
+ // Also save all current text input values when any action is triggered
97
+ if (onSetVariable) {
98
+ Object.entries(inputValues).forEach(([key, value]) => {
99
+ onSetVariable(key, value);
100
+ });
101
+ }
80
102
  break;
81
103
  case 'toggle': {
82
104
  const group = action.group;
@@ -113,6 +135,12 @@ const ElementRenderer = ({ elements, analytics, screenId, onNavigate, onDismiss,
113
135
  break;
114
136
  }
115
137
  case 'navigate':
138
+ // Save all text input values before navigating
139
+ if (onSetVariable) {
140
+ Object.entries(inputValues).forEach(([key, value]) => {
141
+ onSetVariable(key, value);
142
+ });
143
+ }
116
144
  if (onNavigate && action.destination) {
117
145
  onNavigate(action.destination);
118
146
  }
@@ -129,7 +157,7 @@ const ElementRenderer = ({ elements, analytics, screenId, onNavigate, onDismiss,
129
157
  // Generic tap — analytics already tracked above
130
158
  break;
131
159
  }
132
- }, [groupSelections, onNavigate, onDismiss, analytics, screenId, onSetVariable]);
160
+ }, [groupSelections, onNavigate, onDismiss, analytics, screenId, onSetVariable, inputValues]);
133
161
  const handleAction = (0, react_1.useCallback)((element) => {
134
162
  // Execute single action (backward compatible)
135
163
  if (element.action) {
@@ -146,18 +174,35 @@ const ElementRenderer = ({ elements, analytics, screenId, onNavigate, onDismiss,
146
174
  return null;
147
175
  }
148
176
  return (<>
149
- {elements.map((element) => (<RenderNode key={element.id} element={element} toggledIds={toggledIds} groupSelections={groupSelections} onAction={handleAction} variables={variables}/>))}
177
+ {elements.map((element) => (<RenderNode key={element.id} element={element} toggledIds={toggledIds} groupSelections={groupSelections} onAction={handleAction} variables={variables} inputValues={inputValues} setInputValues={setInputValues} resolveAssetUrl={resolveAssetUrl}/>))}
150
178
  </>);
151
179
  };
152
180
  exports.ElementRenderer = ElementRenderer;
153
- const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables, onSetVariable }) => {
154
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6;
181
+ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables, onSetVariable, inputValues, setInputValues, staggerDelay = 0, resolveAssetUrl }) => {
182
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _0, _1, _2, _3, _4, _5, _6, _7, _8, _9, _10, _11, _12, _13;
155
183
  // Variable-based conditions — hide element if condition is not met
156
184
  if ((_a = element.conditions) === null || _a === void 0 ? void 0 : _a.show_if) {
157
185
  const shouldShow = (0, variableUtils_1.evaluateCondition)(element.conditions.show_if, variables);
158
186
  if (!shouldShow)
159
187
  return null;
160
188
  }
189
+ // ─── Animation State ───
190
+ const entranceValues = (0, react_1.useRef)((0, animationUtils_1.createEntranceAnimationValues)()).current;
191
+ const interactiveValue = (0, react_1.useRef)(new react_native_1.Animated.Value(1)).current;
192
+ const [hasAnimated, setHasAnimated] = (0, react_1.useState)(false);
193
+ // Start entrance animation on mount
194
+ (0, react_1.useEffect)(() => {
195
+ if (element.entrance && element.entrance.type !== 'none' && !hasAnimated) {
196
+ (0, animationUtils_1.startEntranceAnimation)(element.entrance, entranceValues, staggerDelay);
197
+ setHasAnimated(true);
198
+ }
199
+ }, [element.entrance, hasAnimated, staggerDelay]);
200
+ // Start auto-triggered interactive animations
201
+ (0, react_1.useEffect)(() => {
202
+ if (element.interactive && element.interactive.trigger === 'load' && element.interactive.type !== 'none') {
203
+ (0, animationUtils_1.startInteractiveAnimation)(element.interactive, interactiveValue);
204
+ }
205
+ }, [element.interactive]);
161
206
  const style = convertStyle(element.style || {});
162
207
  const isToggled = toggledIds.has(element.id);
163
208
  // Apply toggle visual state
@@ -192,32 +237,85 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables,
192
237
  wrapperStyle.width = style.width;
193
238
  if (style.alignSelf)
194
239
  wrapperStyle.alignSelf = style.alignSelf;
195
- return (<react_native_1.TouchableOpacity key={element.id} activeOpacity={0.7} onPress={() => onAction(element)} style={wrapperStyle}>
240
+ const handlePress = () => {
241
+ // Trigger interactive animation if configured
242
+ if (element.interactive && element.interactive.trigger === 'tap' && element.interactive.type !== 'none') {
243
+ (0, animationUtils_1.startInteractiveAnimation)(element.interactive, interactiveValue);
244
+ }
245
+ onAction(element);
246
+ };
247
+ return (<react_native_1.TouchableOpacity key={element.id} activeOpacity={0.7} onPress={handlePress} style={wrapperStyle}>
196
248
  {content}
197
249
  </react_native_1.TouchableOpacity>);
198
250
  };
199
- const childProps = { toggledIds, groupSelections, onAction, variables, onSetVariable };
251
+ // Wrapper for entrance animations
252
+ const wrapWithEntranceAnimation = (content) => {
253
+ if (!element.entrance || element.entrance.type === 'none') {
254
+ return content;
255
+ }
256
+ // Build animated style based on entrance type
257
+ const animatedStyle = {
258
+ opacity: entranceValues.opacity,
259
+ };
260
+ // Apply scale for scale animations
261
+ if (element.entrance.type === 'scaleIn') {
262
+ animatedStyle.transform = [{ scale: entranceValues.scale }];
263
+ }
264
+ // Apply translate for slide animations
265
+ else if (element.entrance.type === 'slideUp' || element.entrance.type === 'slideDown') {
266
+ animatedStyle.transform = [{ translateY: entranceValues.translateY }];
267
+ }
268
+ else if (element.entrance.type === 'slideLeft' || element.entrance.type === 'slideRight') {
269
+ animatedStyle.transform = [{ translateX: entranceValues.translateX }];
270
+ }
271
+ return <react_native_1.Animated.View style={animatedStyle}>{content}</react_native_1.Animated.View>;
272
+ };
273
+ // Wrapper for interactive animations
274
+ const wrapWithInteractiveAnimation = (content) => {
275
+ if (!element.interactive || element.interactive.type === 'none') {
276
+ return content;
277
+ }
278
+ const animatedStyle = {};
279
+ switch (element.interactive.type) {
280
+ case 'scale':
281
+ case 'pulse':
282
+ animatedStyle.transform = [{ scale: interactiveValue }];
283
+ break;
284
+ case 'shake':
285
+ animatedStyle.transform = [{ translateX: interactiveValue }];
286
+ break;
287
+ case 'bounce':
288
+ animatedStyle.transform = [{ translateY: interactiveValue }];
289
+ break;
290
+ }
291
+ return <react_native_1.Animated.View style={animatedStyle}>{content}</react_native_1.Animated.View>;
292
+ };
293
+ const childProps = { toggledIds, groupSelections, onAction, variables, onSetVariable, inputValues, setInputValues, resolveAssetUrl };
200
294
  switch (element.type) {
201
295
  // ─── Containers ───
202
296
  case 'vstack': {
297
+ const stagger = ((_f = element.entrance) === null || _f === void 0 ? void 0 : _f.stagger) || 0;
203
298
  const vstackContent = (<react_native_1.View style={[style, { flexDirection: 'column' }]}>
204
- {(_f = element.children) === null || _f === void 0 ? void 0 : _f.map((child) => (<RenderNode key={child.id} element={child} {...childProps}/>))}
299
+ {(_g = element.children) === null || _g === void 0 ? void 0 : _g.map((child, index) => (<RenderNode key={child.id} element={child} {...childProps} staggerDelay={stagger > 0 ? index * stagger : 0}/>))}
205
300
  </react_native_1.View>);
206
- return wrapWithAction(((_g = element.style) === null || _g === void 0 ? void 0 : _g.backgroundGradient)
301
+ const wrapped = wrapWithAction(((_h = element.style) === null || _h === void 0 ? void 0 : _h.backgroundGradient)
207
302
  ? wrapWithGradient(vstackContent, element.style, Object.assign(Object.assign({}, style), { flexDirection: 'column' }))
208
303
  : vstackContent);
304
+ return wrapWithInteractiveAnimation(wrapWithEntranceAnimation(wrapped));
209
305
  }
210
306
  case 'hstack': {
307
+ const stagger = ((_j = element.entrance) === null || _j === void 0 ? void 0 : _j.stagger) || 0;
211
308
  const hstackContent = (<react_native_1.View style={[style, { flexDirection: 'row' }]}>
212
- {(_h = element.children) === null || _h === void 0 ? void 0 : _h.map((child) => (<RenderNode key={child.id} element={child} {...childProps}/>))}
309
+ {(_k = element.children) === null || _k === void 0 ? void 0 : _k.map((child, index) => (<RenderNode key={child.id} element={child} {...childProps} staggerDelay={stagger > 0 ? index * stagger : 0}/>))}
213
310
  </react_native_1.View>);
214
- return wrapWithAction(((_j = element.style) === null || _j === void 0 ? void 0 : _j.backgroundGradient)
311
+ const wrapped = wrapWithAction(((_l = element.style) === null || _l === void 0 ? void 0 : _l.backgroundGradient)
215
312
  ? wrapWithGradient(hstackContent, element.style, Object.assign(Object.assign({}, style), { flexDirection: 'row' }))
216
313
  : hstackContent);
314
+ return wrapWithInteractiveAnimation(wrapWithEntranceAnimation(wrapped));
217
315
  }
218
316
  case 'zstack': {
219
317
  const zstackContent = (<react_native_1.View style={style}>
220
- {(_k = element.children) === null || _k === void 0 ? void 0 : _k.map((child, index) => {
318
+ {(_m = element.children) === null || _m === void 0 ? void 0 : _m.map((child, index) => {
221
319
  var _a;
222
320
  const childStyle = convertStyle(child.style || {});
223
321
  if (index > 0 && !((_a = child.position) === null || _a === void 0 ? void 0 : _a.type)) {
@@ -228,34 +326,85 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables,
228
326
  return <RenderNode key={child.id} element={child} {...childProps}/>;
229
327
  })}
230
328
  </react_native_1.View>);
231
- return wrapWithAction(((_l = element.style) === null || _l === void 0 ? void 0 : _l.backgroundGradient)
329
+ return wrapWithAction(((_o = element.style) === null || _o === void 0 ? void 0 : _o.backgroundGradient)
232
330
  ? wrapWithGradient(zstackContent, element.style, style)
233
331
  : zstackContent);
234
332
  }
235
333
  case 'scrollview': {
236
- const isHorizontal = ((_m = element.props) === null || _m === void 0 ? void 0 : _m.direction) === 'horizontal';
334
+ const isHorizontal = ((_p = element.props) === null || _p === void 0 ? void 0 : _p.direction) === 'horizontal';
237
335
  return (<react_native_1.ScrollView style={style} horizontal={isHorizontal} showsVerticalScrollIndicator={false} showsHorizontalScrollIndicator={false}>
238
- {isHorizontal ? (<react_native_1.View style={{ flexDirection: 'row', gap: (_o = element.style) === null || _o === void 0 ? void 0 : _o.gap }}>
239
- {(_p = element.children) === null || _p === void 0 ? void 0 : _p.map((child) => (<RenderNode key={child.id} element={child} {...childProps}/>))}
240
- </react_native_1.View>) : ((_q = element.children) === null || _q === void 0 ? void 0 : _q.map((child) => (<RenderNode key={child.id} element={child} {...childProps}/>)))}
336
+ {isHorizontal ? (<react_native_1.View style={{ flexDirection: 'row', gap: (_q = element.style) === null || _q === void 0 ? void 0 : _q.gap }}>
337
+ {(_r = element.children) === null || _r === void 0 ? void 0 : _r.map((child) => (<RenderNode key={child.id} element={child} {...childProps}/>))}
338
+ </react_native_1.View>) : ((_s = element.children) === null || _s === void 0 ? void 0 : _s.map((child) => (<RenderNode key={child.id} element={child} {...childProps}/>)))}
241
339
  </react_native_1.ScrollView>);
242
340
  }
243
341
  // ─── Content Elements ───
244
342
  case 'text': {
245
- const resolvedText = (0, variableUtils_1.resolveTemplate)(((_r = element.props) === null || _r === void 0 ? void 0 : _r.text) || '', variables);
246
- return (<react_native_1.Text style={style}>
247
- {resolvedText}
343
+ const resolvedText = (0, variableUtils_1.resolveTemplate)(((_t = element.props) === null || _t === void 0 ? void 0 : _t.text) || '', variables);
344
+ // Typewriter animation support
345
+ const [displayedText, setDisplayedText] = (0, react_1.useState)('');
346
+ const [showCursor, setShowCursor] = (0, react_1.useState)(false);
347
+ const typewriterInterval = (0, react_1.useRef)(null);
348
+ (0, react_1.useEffect)(() => {
349
+ const textAnim = element.textAnimation;
350
+ if (textAnim && textAnim.type === 'typewriter') {
351
+ const speed = textAnim.speed || 20; // chars per second
352
+ const delay = textAnim.delay || 0;
353
+ const haptic = textAnim.haptic;
354
+ let currentIndex = 0;
355
+ // Show cursor if enabled
356
+ if (textAnim.cursor) {
357
+ setShowCursor(true);
358
+ }
359
+ // Start typewriter animation after delay
360
+ const timeoutId = setTimeout(() => {
361
+ typewriterInterval.current = setInterval(() => {
362
+ if (currentIndex >= resolvedText.length) {
363
+ if (typewriterInterval.current) {
364
+ clearInterval(typewriterInterval.current);
365
+ }
366
+ // Hide cursor when done
367
+ if (textAnim.cursor) {
368
+ setShowCursor(false);
369
+ }
370
+ return;
371
+ }
372
+ setDisplayedText(resolvedText.substring(0, currentIndex + 1));
373
+ // Trigger haptic if enabled
374
+ if (haptic && haptic.enabled && (0, animationUtils_1.shouldTriggerHaptic)(currentIndex, haptic.frequency)) {
375
+ (0, animationUtils_1.triggerHaptic)(haptic.type);
376
+ }
377
+ currentIndex++;
378
+ }, 1000 / speed);
379
+ }, delay);
380
+ return () => {
381
+ clearTimeout(timeoutId);
382
+ if (typewriterInterval.current) {
383
+ clearInterval(typewriterInterval.current);
384
+ }
385
+ };
386
+ }
387
+ else {
388
+ // No typewriter - show full text immediately
389
+ setDisplayedText(resolvedText);
390
+ }
391
+ }, [resolvedText, element.textAnimation]);
392
+ const textContent = ((_u = element.textAnimation) === null || _u === void 0 ? void 0 : _u.type) === 'typewriter' ? displayedText : resolvedText;
393
+ const textElement = (<react_native_1.Text style={style}>
394
+ {textContent}
395
+ {showCursor && (<react_native_1.Text style={{ opacity: 0.5 }}>|</react_native_1.Text>)}
248
396
  </react_native_1.Text>);
397
+ return wrapWithInteractiveAnimation(wrapWithEntranceAnimation(wrapWithAction(textElement)));
249
398
  }
250
399
  case 'icon': {
251
- if ((_s = element.props) === null || _s === void 0 ? void 0 : _s.emoji) {
400
+ if ((_v = element.props) === null || _v === void 0 ? void 0 : _v.emoji) {
252
401
  return (<react_native_1.Text style={[style, { textAlign: 'center' }]}>
253
402
  {element.props.emoji}
254
403
  </react_native_1.Text>);
255
404
  }
256
405
  // Try to render a real vector icon
257
- const library = (((_t = element.props) === null || _t === void 0 ? void 0 : _t.library) || 'material').toLowerCase();
258
- const iconName = (_u = element.props) === null || _u === void 0 ? void 0 : _u.name;
406
+ const library = (((_w = element.props) === null || _w === void 0 ? void 0 : _w.library) || 'material').toLowerCase();
407
+ const iconName = (_x = element.props) === null || _x === void 0 ? void 0 : _x.name;
259
408
  const IconComponent = IconSets[library];
260
409
  if (IconComponent && iconName) {
261
410
  const iconSize = style.fontSize || 24;
@@ -275,13 +424,14 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables,
275
424
  },
276
425
  ]}>
277
426
  <react_native_1.Text style={{ fontSize: 10, color: '#666' }}>
278
- {((_v = element.props) === null || _v === void 0 ? void 0 : _v.name) || '●'}
427
+ {((_y = element.props) === null || _y === void 0 ? void 0 : _y.name) || '●'}
279
428
  </react_native_1.Text>
280
429
  </react_native_1.View>);
281
430
  }
282
431
  case 'image':
283
- if ((_w = element.props) === null || _w === void 0 ? void 0 : _w.url) {
284
- return (<react_native_1.Image source={{ uri: element.props.url }} style={[style, { resizeMode: 'cover' }]}/>);
432
+ if ((_z = element.props) === null || _z === void 0 ? void 0 : _z.url) {
433
+ const resolvedUrl = resolveAssetUrl(element.props.url);
434
+ return (<react_native_1.Image source={{ uri: resolvedUrl }} style={[style, { resizeMode: 'cover' }]}/>);
285
435
  }
286
436
  // Placeholder for images without URL
287
437
  return (<react_native_1.View style={[
@@ -293,12 +443,17 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables,
293
443
  },
294
444
  ]}>
295
445
  <react_native_1.Text style={{ fontSize: 48 }}>🖼️</react_native_1.Text>
296
- {((_x = element.props) === null || _x === void 0 ? void 0 : _x.imageDescription) && (<react_native_1.Text style={{ fontSize: 11, color: '#666', textAlign: 'center', padding: 8 }}>
446
+ {((_0 = element.props) === null || _0 === void 0 ? void 0 : _0.imageDescription) && (<react_native_1.Text style={{ fontSize: 11, color: '#666', textAlign: 'center', padding: 8 }}>
297
447
  {element.props.imageDescription}
298
448
  </react_native_1.Text>)}
299
449
  </react_native_1.View>);
300
450
  case 'video':
301
451
  // Video placeholder — actual implementation would use expo-av or react-native-video
452
+ // Resolve asset URL if present (for future implementation)
453
+ if ((_1 = element.props) === null || _1 === void 0 ? void 0 : _1.url) {
454
+ const resolvedUrl = resolveAssetUrl(element.props.url);
455
+ // TODO: Implement actual video player with resolvedUrl
456
+ }
302
457
  return (<react_native_1.View style={[
303
458
  style,
304
459
  {
@@ -308,12 +463,17 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables,
308
463
  },
309
464
  ]}>
310
465
  <react_native_1.Text style={{ fontSize: 48 }}>🎬</react_native_1.Text>
311
- {((_y = element.props) === null || _y === void 0 ? void 0 : _y.videoDescription) && (<react_native_1.Text style={{ fontSize: 11, color: '#aaa', textAlign: 'center', padding: 8 }}>
466
+ {((_2 = element.props) === null || _2 === void 0 ? void 0 : _2.videoDescription) && (<react_native_1.Text style={{ fontSize: 11, color: '#aaa', textAlign: 'center', padding: 8 }}>
312
467
  {element.props.videoDescription}
313
468
  </react_native_1.Text>)}
314
469
  </react_native_1.View>);
315
470
  case 'lottie':
316
471
  // Lottie placeholder — actual implementation would use lottie-react-native
472
+ // Resolve asset URL if present (for future implementation)
473
+ if ((_3 = element.props) === null || _3 === void 0 ? void 0 : _3.url) {
474
+ const resolvedUrl = resolveAssetUrl(element.props.url);
475
+ // TODO: Implement actual Lottie player with resolvedUrl
476
+ }
317
477
  return (<react_native_1.View style={[
318
478
  style,
319
479
  {
@@ -323,7 +483,7 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables,
323
483
  },
324
484
  ]}>
325
485
  <react_native_1.Text style={{ fontSize: 48 }}>✨</react_native_1.Text>
326
- {((_z = element.props) === null || _z === void 0 ? void 0 : _z.animationDescription) && (<react_native_1.Text style={{ fontSize: 11, color: '#666', textAlign: 'center', padding: 8 }}>
486
+ {((_4 = element.props) === null || _4 === void 0 ? void 0 : _4.animationDescription) && (<react_native_1.Text style={{ fontSize: 11, color: '#666', textAlign: 'center', padding: 8 }}>
327
487
  {element.props.animationDescription}
328
488
  </react_native_1.Text>)}
329
489
  </react_native_1.View>);
@@ -331,17 +491,17 @@ const RenderNode = ({ element, toggledIds, groupSelections, onAction, variables,
331
491
  // Only apply default border if borderWidth is not explicitly defined (including 0)
332
492
  const inputStyle = style;
333
493
  const defaultInputStyle = {};
334
- if (((_0 = element.style) === null || _0 === void 0 ? void 0 : _0.borderWidth) === undefined && ((_1 = element.style) === null || _1 === void 0 ? void 0 : _1.borderColor) === undefined) {
494
+ if (((_5 = element.style) === null || _5 === void 0 ? void 0 : _5.borderWidth) === undefined && ((_6 = element.style) === null || _6 === void 0 ? void 0 : _6.borderColor) === undefined) {
335
495
  defaultInputStyle.borderWidth = 1;
336
496
  defaultInputStyle.borderColor = '#E5E5E5';
337
497
  }
338
498
  // Get the variable name - use props.variable if specified, otherwise use element.id
339
- const variableName = ((_2 = element.props) === null || _2 === void 0 ? void 0 : _2.variable) || element.id;
340
- const currentValue = variables[variableName] || '';
341
- return (<react_native_1.TextInput style={[defaultInputStyle, inputStyle]} placeholder={((_3 = element.props) === null || _3 === void 0 ? void 0 : _3.placeholder) || 'Enter text...'} keyboardType={getKeyboardType((_4 = element.props) === null || _4 === void 0 ? void 0 : _4.type)} secureTextEntry={((_5 = element.props) === null || _5 === void 0 ? void 0 : _5.type) === 'password'} autoCapitalize={((_6 = element.props) === null || _6 === void 0 ? void 0 : _6.type) === 'email' ? 'none' : 'sentences'} value={currentValue} onChangeText={(text) => {
342
- if (onSetVariable) {
343
- onSetVariable(variableName, text);
344
- }
499
+ const variableName = ((_7 = element.props) === null || _7 === void 0 ? void 0 : _7.variable) || element.id;
500
+ // Use local state value, or fall back to variables, or empty string
501
+ const currentValue = (_9 = (_8 = inputValues[variableName]) !== null && _8 !== void 0 ? _8 : variables[variableName]) !== null && _9 !== void 0 ? _9 : '';
502
+ return (<react_native_1.TextInput style={[defaultInputStyle, inputStyle]} placeholder={((_10 = element.props) === null || _10 === void 0 ? void 0 : _10.placeholder) || 'Enter text...'} keyboardType={getKeyboardType((_11 = element.props) === null || _11 === void 0 ? void 0 : _11.type)} secureTextEntry={((_12 = element.props) === null || _12 === void 0 ? void 0 : _12.type) === 'password'} autoCapitalize={((_13 = element.props) === null || _13 === void 0 ? void 0 : _13.type) === 'email' ? 'none' : 'sentences'} value={currentValue} onChangeText={(text) => {
503
+ // Save to local state only - don't trigger parent re-render
504
+ setInputValues(prev => (Object.assign(Object.assign({}, prev), { [variableName]: text })));
345
505
  }}/>);
346
506
  }
347
507
  case 'spacer':
package/lib/types.d.ts CHANGED
@@ -7,6 +7,7 @@ export interface ScreenConfig {
7
7
  elements?: ElementNode[];
8
8
  custom_component_name?: string;
9
9
  hidden?: boolean;
10
+ transition?: ScreenTransition;
10
11
  }
11
12
  export type ElementType = 'vstack' | 'hstack' | 'zstack' | 'scrollview' | 'text' | 'image' | 'video' | 'lottie' | 'icon' | 'input' | 'spacer' | 'divider';
12
13
  export interface ElementNode {
@@ -23,6 +24,46 @@ export interface ElementNode {
23
24
  hasSelection: boolean;
24
25
  };
25
26
  conditions?: ElementConditions;
27
+ entrance?: EntranceAnimation;
28
+ interactive?: InteractiveAnimation;
29
+ textAnimation?: TextAnimation;
30
+ }
31
+ export type EntranceAnimationType = 'fadeIn' | 'slideUp' | 'slideDown' | 'slideLeft' | 'slideRight' | 'scaleIn' | 'none';
32
+ export type InteractiveAnimationType = 'scale' | 'pulse' | 'shake' | 'bounce' | 'none';
33
+ export type HapticType = 'light' | 'medium' | 'heavy' | 'success' | 'warning' | 'error';
34
+ export type HapticFrequency = 'every' | 'every-2' | 'every-3' | 'every-5';
35
+ export interface EntranceAnimation {
36
+ type: EntranceAnimationType;
37
+ duration?: number;
38
+ delay?: number;
39
+ stagger?: number;
40
+ easing?: 'linear' | 'ease-in' | 'ease-out' | 'ease-in-out' | 'spring';
41
+ }
42
+ export interface InteractiveAnimation {
43
+ type: InteractiveAnimationType;
44
+ trigger?: 'tap' | 'load';
45
+ duration?: number;
46
+ intensity?: number;
47
+ repeat?: boolean;
48
+ haptic?: boolean;
49
+ hapticType?: HapticType;
50
+ }
51
+ export interface TextAnimation {
52
+ type: 'typewriter' | 'none';
53
+ speed?: number;
54
+ delay?: number;
55
+ cursor?: boolean;
56
+ haptic?: {
57
+ enabled: boolean;
58
+ type: HapticType;
59
+ frequency: HapticFrequency;
60
+ };
61
+ }
62
+ export interface ScreenTransition {
63
+ type: 'push' | 'modal' | 'fade' | 'slide' | 'none';
64
+ direction?: 'left' | 'right' | 'up' | 'down';
65
+ duration?: number;
66
+ easing?: 'linear' | 'ease-in-out' | 'spring';
26
67
  }
27
68
  export type ComparisonOperator = 'equals' | 'not_equals' | 'greater_than' | 'less_than' | 'contains' | 'in' | 'is_empty' | 'is_not_empty';
28
69
  export interface Condition {
@@ -119,6 +160,11 @@ export interface ElementStyle {
119
160
  export interface OnboardingConfig {
120
161
  version: string;
121
162
  screens: ScreenConfig[];
163
+ assets?: Array<{
164
+ name: string;
165
+ type: string;
166
+ data: string;
167
+ }>;
122
168
  }
123
169
  export interface Experiment {
124
170
  id: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noboarding",
3
- "version": "1.0.3-beta",
3
+ "version": "1.0.6-beta",
4
4
  "description": "Expo SDK for remote onboarding flow management",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -74,6 +74,7 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
74
74
  const [loading, setLoading] = useState(true);
75
75
  const [error, setError] = useState<string | null>(null);
76
76
  const [screens, setScreens] = useState<ScreenConfig[]>([]);
77
+ const [assets, setAssets] = useState<Array<{ name: string; type: string; data: string }>>([]);
77
78
  const [currentIndex, setCurrentIndex] = useState(0);
78
79
  const [collectedData, setCollectedData] = useState<Record<string, any>>({});
79
80
  const [variables, setVariables] = useState<Record<string, any>>(initialVariables || {});
@@ -149,6 +150,11 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
149
150
  // Store flow_id for analytics
150
151
  flowIdRef.current = configResponse.config_id;
151
152
 
153
+ // Store assets from config
154
+ if (configResponse.config.assets) {
155
+ setAssets(configResponse.config.assets);
156
+ }
157
+
152
158
  // Handle A/B test experiment assignment
153
159
  let screensToUse = configResponse.config.screens;
154
160
 
@@ -161,12 +167,24 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
161
167
  userIdRef.current!
162
168
  );
163
169
 
170
+ console.log('🧪 A/B Test Assignment:', {
171
+ experiment_id: experiment.id,
172
+ experiment_name: experiment.name,
173
+ variant_id: assignment.variant_id,
174
+ has_variant_screens: assignment.variant_config?.screens?.length > 0,
175
+ variant_screen_count: assignment.variant_config?.screens?.length || 0,
176
+ cached: assignment.cached,
177
+ });
178
+
164
179
  // Set experiment context so all events get tagged
165
180
  analytics.setExperimentContext(experiment.id, assignment.variant_id);
166
181
 
167
182
  // Use variant screens if available
168
183
  if (assignment.variant_config?.screens?.length > 0) {
169
184
  screensToUse = assignment.variant_config.screens;
185
+ console.log('📱 Using variant screens:', assignment.variant_config.screens.length, 'screens');
186
+ } else {
187
+ console.log('📱 Using base flow screens (variant has no screens defined)');
170
188
  }
171
189
  } catch (err) {
172
190
  console.warn('Failed to assign experiment variant, using default flow:', err);
@@ -339,6 +357,7 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
339
357
  onDismiss={onSkip ? handleSkipAll : handleNext}
340
358
  variables={allVariables}
341
359
  onSetVariable={handleSetVariable}
360
+ assets={assets}
342
361
  />
343
362
  </View>
344
363
  );