noboarding 1.0.2-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.
@@ -32,18 +32,39 @@ var __importStar = (this && this.__importStar) || (function () {
32
32
  return result;
33
33
  };
34
34
  })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
35
38
  Object.defineProperty(exports, "__esModule", { value: true });
36
39
  exports.OnboardingFlow = void 0;
37
40
  const react_1 = __importStar(require("react"));
38
41
  const react_native_1 = require("react-native");
42
+ const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
39
43
  const api_1 = require("./api");
40
44
  const analytics_1 = require("./analytics");
41
45
  const variableUtils_1 = require("./variableUtils");
42
46
  const ElementRenderer_1 = require("./components/ElementRenderer");
43
- // Generate unique IDs
44
- const generateUserId = () => {
45
- return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
47
+ const USER_ID_STORAGE_KEY = '@noboarding_user_id';
48
+ // Get or create persistent user ID
49
+ const getPersistentUserId = async () => {
50
+ try {
51
+ // Try to get existing user ID from storage
52
+ const existingUserId = await async_storage_1.default.getItem(USER_ID_STORAGE_KEY);
53
+ if (existingUserId) {
54
+ return existingUserId;
55
+ }
56
+ // Generate new user ID if none exists
57
+ const newUserId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
58
+ await async_storage_1.default.setItem(USER_ID_STORAGE_KEY, newUserId);
59
+ return newUserId;
60
+ }
61
+ catch (error) {
62
+ console.error('Failed to get/set user ID from storage:', error);
63
+ // Fallback to generating a non-persistent ID
64
+ return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
65
+ }
46
66
  };
67
+ // Generate session ID (always new per session)
47
68
  const generateSessionId = () => {
48
69
  return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
49
70
  };
@@ -75,19 +96,28 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
75
96
  const [loading, setLoading] = (0, react_1.useState)(true);
76
97
  const [error, setError] = (0, react_1.useState)(null);
77
98
  const [screens, setScreens] = (0, react_1.useState)([]);
99
+ const [assets, setAssets] = (0, react_1.useState)([]);
78
100
  const [currentIndex, setCurrentIndex] = (0, react_1.useState)(0);
79
101
  const [collectedData, setCollectedData] = (0, react_1.useState)({});
80
102
  const [variables, setVariables] = (0, react_1.useState)(initialVariables || {});
81
103
  const apiRef = (0, react_1.useRef)(null);
82
104
  const analyticsRef = (0, react_1.useRef)(null);
83
- const userIdRef = (0, react_1.useRef)(generateUserId());
105
+ const userIdRef = (0, react_1.useRef)(null);
84
106
  const sessionIdRef = (0, react_1.useRef)(generateSessionId());
107
+ const flowIdRef = (0, react_1.useRef)(null);
85
108
  (0, react_1.useEffect)(() => {
86
- // Notify parent of the generated user ID
87
- if (onUserIdGenerated) {
88
- onUserIdGenerated(userIdRef.current);
89
- }
90
- initializeFlow();
109
+ const initialize = async () => {
110
+ // Get or create persistent user ID
111
+ const userId = await getPersistentUserId();
112
+ userIdRef.current = userId;
113
+ // Notify parent of the user ID
114
+ if (onUserIdGenerated) {
115
+ onUserIdGenerated(userId);
116
+ }
117
+ // Initialize the flow
118
+ await initializeFlow();
119
+ };
120
+ initialize();
91
121
  return () => {
92
122
  // Cleanup: flush remaining analytics
93
123
  if (analyticsRef.current) {
@@ -95,26 +125,81 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
95
125
  }
96
126
  };
97
127
  }, []);
128
+ // Track screen views whenever user navigates to a different screen
129
+ (0, react_1.useEffect)(() => {
130
+ if (screens.length > 0 && analyticsRef.current && currentIndex > 0) {
131
+ const currentScreen = screens[currentIndex];
132
+ analyticsRef.current.track('screen_viewed', {
133
+ flow_id: flowIdRef.current,
134
+ screen_id: currentScreen.id,
135
+ screen_index: currentIndex,
136
+ });
137
+ }
138
+ }, [currentIndex, screens]);
98
139
  const initializeFlow = async () => {
140
+ var _a, _b, _c, _d, _e, _f, _g;
99
141
  try {
100
142
  setLoading(true);
101
143
  setError(null);
144
+ // Ensure user ID is set
145
+ if (!userIdRef.current) {
146
+ throw new Error('User ID not initialized');
147
+ }
102
148
  // Initialize API client with detected API key
103
149
  const api = new api_1.API(activeApiKey, baseUrl);
104
150
  apiRef.current = api;
105
151
  // Initialize analytics
106
152
  const analytics = new analytics_1.AnalyticsManager(api, userIdRef.current, sessionIdRef.current);
107
153
  analyticsRef.current = analytics;
108
- // Track onboarding start
109
- analytics.track('onboarding_started');
110
154
  // Fetch configuration
111
155
  const configResponse = await api.getConfig();
156
+ // Store flow_id for analytics
157
+ flowIdRef.current = configResponse.config_id;
158
+ // Store assets from config
159
+ if (configResponse.config.assets) {
160
+ setAssets(configResponse.config.assets);
161
+ }
162
+ // Handle A/B test experiment assignment
163
+ let screensToUse = configResponse.config.screens;
164
+ if (configResponse.experiments && configResponse.experiments.length > 0) {
165
+ // Assign user to the first active experiment
166
+ const experiment = configResponse.experiments[0];
167
+ try {
168
+ const assignment = await api.assignVariant(experiment.id, userIdRef.current);
169
+ console.log('🧪 A/B Test Assignment:', {
170
+ experiment_id: experiment.id,
171
+ experiment_name: experiment.name,
172
+ variant_id: assignment.variant_id,
173
+ has_variant_screens: ((_b = (_a = assignment.variant_config) === null || _a === void 0 ? void 0 : _a.screens) === null || _b === void 0 ? void 0 : _b.length) > 0,
174
+ variant_screen_count: ((_d = (_c = assignment.variant_config) === null || _c === void 0 ? void 0 : _c.screens) === null || _d === void 0 ? void 0 : _d.length) || 0,
175
+ cached: assignment.cached,
176
+ });
177
+ // Set experiment context so all events get tagged
178
+ analytics.setExperimentContext(experiment.id, assignment.variant_id);
179
+ // Use variant screens if available
180
+ if (((_f = (_e = assignment.variant_config) === null || _e === void 0 ? void 0 : _e.screens) === null || _f === void 0 ? void 0 : _f.length) > 0) {
181
+ screensToUse = assignment.variant_config.screens;
182
+ console.log('📱 Using variant screens:', assignment.variant_config.screens.length, 'screens');
183
+ }
184
+ else {
185
+ console.log('📱 Using base flow screens (variant has no screens defined)');
186
+ }
187
+ }
188
+ catch (err) {
189
+ console.warn('Failed to assign experiment variant, using default flow:', err);
190
+ }
191
+ }
112
192
  // Backward compatibility: normalize legacy 'custom_screen' (with elements) to 'noboard_screen'
113
- const normalizedScreens = configResponse.config.screens
193
+ const normalizedScreens = screensToUse
114
194
  .map(s => (Object.assign(Object.assign({}, s), { type: (s.type === 'custom_screen' && s.elements) ? 'noboard_screen' : s.type })))
115
195
  // Filter out hidden screens (dashboard show/hide feature)
116
196
  .filter(s => !s.hidden);
117
197
  setScreens(normalizedScreens);
198
+ // Track onboarding start with first screen
199
+ analytics.track('onboarding_started', {
200
+ flow_id: flowIdRef.current,
201
+ screen_id: (_g = normalizedScreens[0]) === null || _g === void 0 ? void 0 : _g.id,
202
+ });
118
203
  setLoading(false);
119
204
  }
120
205
  catch (err) {
@@ -155,18 +240,25 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
155
240
  setVariables(prev => (Object.assign(Object.assign({}, prev), { [name]: value })));
156
241
  }, []);
157
242
  const handleComplete = async (lastScreenData) => {
243
+ var _a;
158
244
  const finalData = Object.assign(Object.assign(Object.assign({}, collectedData), (lastScreenData || {})), { _variables: variables });
159
245
  // Track completion
160
246
  if (analyticsRef.current) {
161
- analyticsRef.current.track('onboarding_completed');
247
+ analyticsRef.current.track('onboarding_completed', {
248
+ flow_id: flowIdRef.current,
249
+ screen_id: (_a = screens[currentIndex]) === null || _a === void 0 ? void 0 : _a.id,
250
+ });
162
251
  await analyticsRef.current.flush();
163
252
  }
164
253
  // Call developer's completion callback with collected data
165
254
  onComplete(finalData);
166
255
  };
167
256
  const handleSkipAll = async () => {
257
+ var _a;
168
258
  if (analyticsRef.current) {
169
259
  analyticsRef.current.track('onboarding_abandoned', {
260
+ flow_id: flowIdRef.current,
261
+ screen_id: (_a = screens[currentIndex]) === null || _a === void 0 ? void 0 : _a.id,
170
262
  current_screen_index: currentIndex,
171
263
  });
172
264
  await analyticsRef.current.flush();
@@ -227,7 +319,7 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
227
319
  }
228
320
  };
229
321
  return (<react_native_1.View style={styles.container}>
230
- <ElementRenderer_1.ElementRenderer elements={currentScreen.elements} analytics={analyticsRef.current} screenId={currentScreen.id} onNavigate={handleElementNavigate} onDismiss={onSkip ? handleSkipAll : handleNext} variables={allVariables} onSetVariable={handleSetVariable}/>
322
+ <ElementRenderer_1.ElementRenderer elements={currentScreen.elements} analytics={analyticsRef.current} screenId={currentScreen.id} onNavigate={handleElementNavigate} onDismiss={onSkip ? handleSkipAll : handleNext} variables={allVariables} onSetVariable={handleSetVariable} assets={assets}/>
231
323
  </react_native_1.View>);
232
324
  }
233
325
  // Handle custom_screen type — developer-registered React Native components
@@ -5,7 +5,13 @@ export declare class AnalyticsManager {
5
5
  private userId;
6
6
  private sessionId;
7
7
  private flushTimer;
8
+ private experimentId;
9
+ private variantId;
8
10
  constructor(api: API, userId: string, sessionId: string);
11
+ /**
12
+ * Set experiment context — all subsequent events will be tagged
13
+ */
14
+ setExperimentContext(experimentId: string, variantId: string): void;
9
15
  /**
10
16
  * Track an analytics event
11
17
  */
package/lib/analytics.js CHANGED
@@ -7,22 +7,39 @@ class AnalyticsManager {
7
7
  constructor(api, userId, sessionId) {
8
8
  this.events = [];
9
9
  this.flushTimer = null;
10
+ this.experimentId = null;
11
+ this.variantId = null;
10
12
  this.api = api;
11
13
  this.userId = userId;
12
14
  this.sessionId = sessionId;
13
15
  // Start auto-flush timer
14
16
  this.startFlushTimer();
15
17
  }
18
+ /**
19
+ * Set experiment context — all subsequent events will be tagged
20
+ */
21
+ setExperimentContext(experimentId, variantId) {
22
+ this.experimentId = experimentId;
23
+ this.variantId = variantId;
24
+ }
16
25
  /**
17
26
  * Track an analytics event
18
27
  */
19
28
  track(eventName, properties) {
29
+ const mergedProperties = Object.assign({}, (properties || {}));
30
+ // Auto-inject experiment context if set
31
+ if (this.experimentId) {
32
+ mergedProperties.experiment_id = this.experimentId;
33
+ }
34
+ if (this.variantId) {
35
+ mergedProperties.variant_id = this.variantId;
36
+ }
20
37
  const event = {
21
38
  event: eventName,
22
39
  user_id: this.userId,
23
40
  session_id: this.sessionId,
24
41
  timestamp: Date.now(),
25
- properties: properties || {},
42
+ properties: mergedProperties,
26
43
  };
27
44
  this.events.push(event);
28
45
  // Auto-flush if batch size reached
@@ -0,0 +1,19 @@
1
+ import { Animated } from 'react-native';
2
+ import type { EntranceAnimation, InteractiveAnimation, HapticType } from './types';
3
+ export declare const triggerHaptic: (type?: HapticType) => void;
4
+ export declare const getEasing: (easingType?: string) => import("react-native").EasingFunction;
5
+ export interface EntranceAnimationValues {
6
+ opacity: Animated.Value;
7
+ translateY: Animated.Value;
8
+ translateX: Animated.Value;
9
+ scale: Animated.Value;
10
+ }
11
+ export declare const createEntranceAnimationValues: () => EntranceAnimationValues;
12
+ export declare const startEntranceAnimation: (config: EntranceAnimation, values: EntranceAnimationValues, delay?: number) => void;
13
+ export declare const startInteractiveAnimation: (config: InteractiveAnimation, animatedValue: Animated.Value) => void;
14
+ export interface TypewriterState {
15
+ displayedText: string;
16
+ currentIndex: number;
17
+ isComplete: boolean;
18
+ }
19
+ export declare const shouldTriggerHaptic: (index: number, frequency: string) => boolean;
@@ -0,0 +1,252 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.shouldTriggerHaptic = exports.startInteractiveAnimation = exports.startEntranceAnimation = exports.createEntranceAnimationValues = exports.getEasing = exports.triggerHaptic = void 0;
4
+ const react_native_1 = require("react-native");
5
+ // Lazy load haptics to avoid errors if not installed
6
+ let Haptics = null;
7
+ try {
8
+ Haptics = require('expo-haptics');
9
+ }
10
+ catch (e) {
11
+ console.warn('expo-haptics not installed, haptic feedback will be disabled');
12
+ }
13
+ // ─── Haptic Feedback Helper ───
14
+ const triggerHaptic = (type = 'light') => {
15
+ if (!Haptics) {
16
+ // Silently skip if expo-haptics not available
17
+ return;
18
+ }
19
+ try {
20
+ switch (type) {
21
+ case 'light':
22
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
23
+ break;
24
+ case 'medium':
25
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
26
+ break;
27
+ case 'heavy':
28
+ Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Heavy);
29
+ break;
30
+ case 'success':
31
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
32
+ break;
33
+ case 'warning':
34
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning);
35
+ break;
36
+ case 'error':
37
+ Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
38
+ break;
39
+ }
40
+ }
41
+ catch (error) {
42
+ console.warn('Haptic feedback failed:', error);
43
+ }
44
+ };
45
+ exports.triggerHaptic = triggerHaptic;
46
+ // ─── Easing Function Mapper ───
47
+ const getEasing = (easingType) => {
48
+ switch (easingType) {
49
+ case 'linear':
50
+ return react_native_1.Easing.linear;
51
+ case 'ease-in':
52
+ return react_native_1.Easing.in(react_native_1.Easing.ease);
53
+ case 'ease-out':
54
+ return react_native_1.Easing.out(react_native_1.Easing.ease);
55
+ case 'ease-in-out':
56
+ return react_native_1.Easing.inOut(react_native_1.Easing.ease);
57
+ case 'spring':
58
+ return react_native_1.Easing.elastic(1);
59
+ default:
60
+ return react_native_1.Easing.inOut(react_native_1.Easing.ease);
61
+ }
62
+ };
63
+ exports.getEasing = getEasing;
64
+ const createEntranceAnimationValues = () => ({
65
+ opacity: new react_native_1.Animated.Value(0),
66
+ translateY: new react_native_1.Animated.Value(0),
67
+ translateX: new react_native_1.Animated.Value(0),
68
+ scale: new react_native_1.Animated.Value(1),
69
+ });
70
+ exports.createEntranceAnimationValues = createEntranceAnimationValues;
71
+ const startEntranceAnimation = (config, values, delay = 0) => {
72
+ const duration = config.duration || 400;
73
+ const totalDelay = (config.delay || 0) + delay;
74
+ const easing = (0, exports.getEasing)(config.easing);
75
+ // Set initial values based on animation type
76
+ switch (config.type) {
77
+ case 'fadeIn':
78
+ values.opacity.setValue(0);
79
+ break;
80
+ case 'slideUp':
81
+ values.opacity.setValue(0);
82
+ values.translateY.setValue(30);
83
+ break;
84
+ case 'slideDown':
85
+ values.opacity.setValue(0);
86
+ values.translateY.setValue(-30);
87
+ break;
88
+ case 'slideLeft':
89
+ values.opacity.setValue(0);
90
+ values.translateX.setValue(30);
91
+ break;
92
+ case 'slideRight':
93
+ values.opacity.setValue(0);
94
+ values.translateX.setValue(-30);
95
+ break;
96
+ case 'scaleIn':
97
+ values.opacity.setValue(0);
98
+ values.scale.setValue(0.8);
99
+ break;
100
+ case 'none':
101
+ values.opacity.setValue(1);
102
+ return;
103
+ }
104
+ // Animate to final values
105
+ react_native_1.Animated.parallel([
106
+ react_native_1.Animated.timing(values.opacity, {
107
+ toValue: 1,
108
+ duration,
109
+ delay: totalDelay,
110
+ easing,
111
+ useNativeDriver: true,
112
+ }),
113
+ react_native_1.Animated.timing(values.translateY, {
114
+ toValue: 0,
115
+ duration,
116
+ delay: totalDelay,
117
+ easing,
118
+ useNativeDriver: true,
119
+ }),
120
+ react_native_1.Animated.timing(values.translateX, {
121
+ toValue: 0,
122
+ duration,
123
+ delay: totalDelay,
124
+ easing,
125
+ useNativeDriver: true,
126
+ }),
127
+ react_native_1.Animated.timing(values.scale, {
128
+ toValue: 1,
129
+ duration,
130
+ delay: totalDelay,
131
+ easing,
132
+ useNativeDriver: true,
133
+ }),
134
+ ]).start();
135
+ };
136
+ exports.startEntranceAnimation = startEntranceAnimation;
137
+ // ─── Interactive Animations ───
138
+ const startInteractiveAnimation = (config, animatedValue) => {
139
+ const duration = config.duration || 200;
140
+ const intensity = config.intensity || (config.type === 'scale' ? 0.95 : 10);
141
+ // Trigger haptic if enabled
142
+ if (config.haptic && config.hapticType) {
143
+ (0, exports.triggerHaptic)(config.hapticType);
144
+ }
145
+ switch (config.type) {
146
+ case 'scale':
147
+ react_native_1.Animated.sequence([
148
+ react_native_1.Animated.timing(animatedValue, {
149
+ toValue: intensity,
150
+ duration: duration / 2,
151
+ easing: react_native_1.Easing.out(react_native_1.Easing.ease),
152
+ useNativeDriver: true,
153
+ }),
154
+ react_native_1.Animated.timing(animatedValue, {
155
+ toValue: 1,
156
+ duration: duration / 2,
157
+ easing: react_native_1.Easing.in(react_native_1.Easing.ease),
158
+ useNativeDriver: true,
159
+ }),
160
+ ]).start();
161
+ break;
162
+ case 'pulse':
163
+ const pulseSequence = react_native_1.Animated.sequence([
164
+ react_native_1.Animated.timing(animatedValue, {
165
+ toValue: 1.05,
166
+ duration: duration,
167
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
168
+ useNativeDriver: true,
169
+ }),
170
+ react_native_1.Animated.timing(animatedValue, {
171
+ toValue: 1,
172
+ duration: duration,
173
+ easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
174
+ useNativeDriver: true,
175
+ }),
176
+ ]);
177
+ if (config.repeat) {
178
+ react_native_1.Animated.loop(pulseSequence).start();
179
+ }
180
+ else {
181
+ pulseSequence.start();
182
+ }
183
+ break;
184
+ case 'shake':
185
+ react_native_1.Animated.sequence([
186
+ react_native_1.Animated.timing(animatedValue, {
187
+ toValue: intensity,
188
+ duration: duration / 8,
189
+ useNativeDriver: true,
190
+ }),
191
+ react_native_1.Animated.timing(animatedValue, {
192
+ toValue: -intensity,
193
+ duration: duration / 4,
194
+ useNativeDriver: true,
195
+ }),
196
+ react_native_1.Animated.timing(animatedValue, {
197
+ toValue: intensity,
198
+ duration: duration / 4,
199
+ useNativeDriver: true,
200
+ }),
201
+ react_native_1.Animated.timing(animatedValue, {
202
+ toValue: -intensity,
203
+ duration: duration / 4,
204
+ useNativeDriver: true,
205
+ }),
206
+ react_native_1.Animated.timing(animatedValue, {
207
+ toValue: 0,
208
+ duration: duration / 8,
209
+ useNativeDriver: true,
210
+ }),
211
+ ]).start();
212
+ break;
213
+ case 'bounce':
214
+ react_native_1.Animated.sequence([
215
+ react_native_1.Animated.timing(animatedValue, {
216
+ toValue: -20,
217
+ duration: duration / 3,
218
+ easing: react_native_1.Easing.out(react_native_1.Easing.ease),
219
+ useNativeDriver: true,
220
+ }),
221
+ react_native_1.Animated.timing(animatedValue, {
222
+ toValue: 5,
223
+ duration: duration / 3,
224
+ easing: react_native_1.Easing.in(react_native_1.Easing.ease),
225
+ useNativeDriver: true,
226
+ }),
227
+ react_native_1.Animated.timing(animatedValue, {
228
+ toValue: 0,
229
+ duration: duration / 3,
230
+ easing: react_native_1.Easing.out(react_native_1.Easing.ease),
231
+ useNativeDriver: true,
232
+ }),
233
+ ]).start();
234
+ break;
235
+ }
236
+ };
237
+ exports.startInteractiveAnimation = startInteractiveAnimation;
238
+ const shouldTriggerHaptic = (index, frequency) => {
239
+ switch (frequency) {
240
+ case 'every':
241
+ return true;
242
+ case 'every-2':
243
+ return index % 2 === 0;
244
+ case 'every-3':
245
+ return index % 3 === 0;
246
+ case 'every-5':
247
+ return index % 5 === 0;
248
+ default:
249
+ return false;
250
+ }
251
+ };
252
+ exports.shouldTriggerHaptic = shouldTriggerHaptic;
@@ -8,6 +8,11 @@ interface ElementRendererProps {
8
8
  onDismiss?: () => void;
9
9
  variables?: Record<string, any>;
10
10
  onSetVariable?: (name: string, value: any) => void;
11
+ assets?: Array<{
12
+ name: string;
13
+ type: string;
14
+ data: string;
15
+ }>;
11
16
  }
12
17
  export declare const ElementRenderer: React.FC<ElementRendererProps>;
13
18
  export {};