noboarding 1.0.2-beta → 1.0.3-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
  };
@@ -80,14 +101,22 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
80
101
  const [variables, setVariables] = (0, react_1.useState)(initialVariables || {});
81
102
  const apiRef = (0, react_1.useRef)(null);
82
103
  const analyticsRef = (0, react_1.useRef)(null);
83
- const userIdRef = (0, react_1.useRef)(generateUserId());
104
+ const userIdRef = (0, react_1.useRef)(null);
84
105
  const sessionIdRef = (0, react_1.useRef)(generateSessionId());
106
+ const flowIdRef = (0, react_1.useRef)(null);
85
107
  (0, react_1.useEffect)(() => {
86
- // Notify parent of the generated user ID
87
- if (onUserIdGenerated) {
88
- onUserIdGenerated(userIdRef.current);
89
- }
90
- initializeFlow();
108
+ const initialize = async () => {
109
+ // Get or create persistent user ID
110
+ const userId = await getPersistentUserId();
111
+ userIdRef.current = userId;
112
+ // Notify parent of the user ID
113
+ if (onUserIdGenerated) {
114
+ onUserIdGenerated(userId);
115
+ }
116
+ // Initialize the flow
117
+ await initializeFlow();
118
+ };
119
+ initialize();
91
120
  return () => {
92
121
  // Cleanup: flush remaining analytics
93
122
  if (analyticsRef.current) {
@@ -95,26 +124,65 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
95
124
  }
96
125
  };
97
126
  }, []);
127
+ // Track screen views whenever user navigates to a different screen
128
+ (0, react_1.useEffect)(() => {
129
+ if (screens.length > 0 && analyticsRef.current && currentIndex > 0) {
130
+ const currentScreen = screens[currentIndex];
131
+ analyticsRef.current.track('screen_viewed', {
132
+ flow_id: flowIdRef.current,
133
+ screen_id: currentScreen.id,
134
+ screen_index: currentIndex,
135
+ });
136
+ }
137
+ }, [currentIndex, screens]);
98
138
  const initializeFlow = async () => {
139
+ var _a, _b, _c;
99
140
  try {
100
141
  setLoading(true);
101
142
  setError(null);
143
+ // Ensure user ID is set
144
+ if (!userIdRef.current) {
145
+ throw new Error('User ID not initialized');
146
+ }
102
147
  // Initialize API client with detected API key
103
148
  const api = new api_1.API(activeApiKey, baseUrl);
104
149
  apiRef.current = api;
105
150
  // Initialize analytics
106
151
  const analytics = new analytics_1.AnalyticsManager(api, userIdRef.current, sessionIdRef.current);
107
152
  analyticsRef.current = analytics;
108
- // Track onboarding start
109
- analytics.track('onboarding_started');
110
153
  // Fetch configuration
111
154
  const configResponse = await api.getConfig();
155
+ // Store flow_id for analytics
156
+ flowIdRef.current = configResponse.config_id;
157
+ // Handle A/B test experiment assignment
158
+ let screensToUse = configResponse.config.screens;
159
+ if (configResponse.experiments && configResponse.experiments.length > 0) {
160
+ // Assign user to the first active experiment
161
+ const experiment = configResponse.experiments[0];
162
+ try {
163
+ const assignment = await api.assignVariant(experiment.id, userIdRef.current);
164
+ // Set experiment context so all events get tagged
165
+ analytics.setExperimentContext(experiment.id, assignment.variant_id);
166
+ // Use variant screens if available
167
+ if (((_b = (_a = assignment.variant_config) === null || _a === void 0 ? void 0 : _a.screens) === null || _b === void 0 ? void 0 : _b.length) > 0) {
168
+ screensToUse = assignment.variant_config.screens;
169
+ }
170
+ }
171
+ catch (err) {
172
+ console.warn('Failed to assign experiment variant, using default flow:', err);
173
+ }
174
+ }
112
175
  // Backward compatibility: normalize legacy 'custom_screen' (with elements) to 'noboard_screen'
113
- const normalizedScreens = configResponse.config.screens
176
+ const normalizedScreens = screensToUse
114
177
  .map(s => (Object.assign(Object.assign({}, s), { type: (s.type === 'custom_screen' && s.elements) ? 'noboard_screen' : s.type })))
115
178
  // Filter out hidden screens (dashboard show/hide feature)
116
179
  .filter(s => !s.hidden);
117
180
  setScreens(normalizedScreens);
181
+ // Track onboarding start with first screen
182
+ analytics.track('onboarding_started', {
183
+ flow_id: flowIdRef.current,
184
+ screen_id: (_c = normalizedScreens[0]) === null || _c === void 0 ? void 0 : _c.id,
185
+ });
118
186
  setLoading(false);
119
187
  }
120
188
  catch (err) {
@@ -155,18 +223,25 @@ const OnboardingFlow = ({ apiKey, testKey, productionKey, onComplete, onSkip, ba
155
223
  setVariables(prev => (Object.assign(Object.assign({}, prev), { [name]: value })));
156
224
  }, []);
157
225
  const handleComplete = async (lastScreenData) => {
226
+ var _a;
158
227
  const finalData = Object.assign(Object.assign(Object.assign({}, collectedData), (lastScreenData || {})), { _variables: variables });
159
228
  // Track completion
160
229
  if (analyticsRef.current) {
161
- analyticsRef.current.track('onboarding_completed');
230
+ analyticsRef.current.track('onboarding_completed', {
231
+ flow_id: flowIdRef.current,
232
+ screen_id: (_a = screens[currentIndex]) === null || _a === void 0 ? void 0 : _a.id,
233
+ });
162
234
  await analyticsRef.current.flush();
163
235
  }
164
236
  // Call developer's completion callback with collected data
165
237
  onComplete(finalData);
166
238
  };
167
239
  const handleSkipAll = async () => {
240
+ var _a;
168
241
  if (analyticsRef.current) {
169
242
  analyticsRef.current.track('onboarding_abandoned', {
243
+ flow_id: flowIdRef.current,
244
+ screen_id: (_a = screens[currentIndex]) === null || _a === void 0 ? void 0 : _a.id,
170
245
  current_screen_index: currentIndex,
171
246
  });
172
247
  await analyticsRef.current.flush();
@@ -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
package/lib/types.d.ts CHANGED
@@ -141,6 +141,7 @@ export interface AnalyticsEvent {
141
141
  export interface GetConfigResponse {
142
142
  config: OnboardingConfig;
143
143
  version: string;
144
+ config_id: string | null;
144
145
  experiments: Experiment[];
145
146
  organization_id: string;
146
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "noboarding",
3
- "version": "1.0.2-beta",
3
+ "version": "1.0.3-beta",
4
4
  "description": "Expo SDK for remote onboarding flow management",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -1,16 +1,36 @@
1
1
  import React, { useState, useEffect, useRef, useCallback } from 'react';
2
2
  import { View, ActivityIndicator, StyleSheet, Text, TouchableOpacity } from 'react-native';
3
+ import AsyncStorage from '@react-native-async-storage/async-storage';
3
4
  import { API } from './api';
4
5
  import { AnalyticsManager } from './analytics';
5
6
  import { OnboardingFlowProps, ScreenConfig, ConditionalDestination, ConditionalRoutes } from './types';
6
7
  import { resolveDestination } from './variableUtils';
7
8
  import { ElementRenderer } from './components/ElementRenderer';
8
9
 
9
- // Generate unique IDs
10
- const generateUserId = (): string => {
11
- return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
10
+ const USER_ID_STORAGE_KEY = '@noboarding_user_id';
11
+
12
+ // Get or create persistent user ID
13
+ const getPersistentUserId = async (): Promise<string> => {
14
+ try {
15
+ // Try to get existing user ID from storage
16
+ const existingUserId = await AsyncStorage.getItem(USER_ID_STORAGE_KEY);
17
+
18
+ if (existingUserId) {
19
+ return existingUserId;
20
+ }
21
+
22
+ // Generate new user ID if none exists
23
+ const newUserId = `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
24
+ await AsyncStorage.setItem(USER_ID_STORAGE_KEY, newUserId);
25
+ return newUserId;
26
+ } catch (error) {
27
+ console.error('Failed to get/set user ID from storage:', error);
28
+ // Fallback to generating a non-persistent ID
29
+ return `user_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
30
+ }
12
31
  };
13
32
 
33
+ // Generate session ID (always new per session)
14
34
  const generateSessionId = (): string => {
15
35
  return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
16
36
  };
@@ -60,16 +80,26 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
60
80
 
61
81
  const apiRef = useRef<API | null>(null);
62
82
  const analyticsRef = useRef<AnalyticsManager | null>(null);
63
- const userIdRef = useRef<string>(generateUserId());
83
+ const userIdRef = useRef<string | null>(null);
64
84
  const sessionIdRef = useRef<string>(generateSessionId());
85
+ const flowIdRef = useRef<string | null>(null);
65
86
 
66
87
  useEffect(() => {
67
- // Notify parent of the generated user ID
68
- if (onUserIdGenerated) {
69
- onUserIdGenerated(userIdRef.current);
70
- }
88
+ const initialize = async () => {
89
+ // Get or create persistent user ID
90
+ const userId = await getPersistentUserId();
91
+ userIdRef.current = userId;
92
+
93
+ // Notify parent of the user ID
94
+ if (onUserIdGenerated) {
95
+ onUserIdGenerated(userId);
96
+ }
71
97
 
72
- initializeFlow();
98
+ // Initialize the flow
99
+ await initializeFlow();
100
+ };
101
+
102
+ initialize();
73
103
 
74
104
  return () => {
75
105
  // Cleanup: flush remaining analytics
@@ -79,11 +109,28 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
79
109
  };
80
110
  }, []);
81
111
 
112
+ // Track screen views whenever user navigates to a different screen
113
+ useEffect(() => {
114
+ if (screens.length > 0 && analyticsRef.current && currentIndex > 0) {
115
+ const currentScreen = screens[currentIndex];
116
+ analyticsRef.current.track('screen_viewed', {
117
+ flow_id: flowIdRef.current,
118
+ screen_id: currentScreen.id,
119
+ screen_index: currentIndex,
120
+ });
121
+ }
122
+ }, [currentIndex, screens]);
123
+
82
124
  const initializeFlow = async () => {
83
125
  try {
84
126
  setLoading(true);
85
127
  setError(null);
86
128
 
129
+ // Ensure user ID is set
130
+ if (!userIdRef.current) {
131
+ throw new Error('User ID not initialized');
132
+ }
133
+
87
134
  // Initialize API client with detected API key
88
135
  const api = new API(activeApiKey, baseUrl);
89
136
  apiRef.current = api;
@@ -96,13 +143,38 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
96
143
  );
97
144
  analyticsRef.current = analytics;
98
145
 
99
- // Track onboarding start
100
- analytics.track('onboarding_started');
101
-
102
146
  // Fetch configuration
103
147
  const configResponse = await api.getConfig();
148
+
149
+ // Store flow_id for analytics
150
+ flowIdRef.current = configResponse.config_id;
151
+
152
+ // Handle A/B test experiment assignment
153
+ let screensToUse = configResponse.config.screens;
154
+
155
+ if (configResponse.experiments && configResponse.experiments.length > 0) {
156
+ // Assign user to the first active experiment
157
+ const experiment = configResponse.experiments[0];
158
+ try {
159
+ const assignment = await api.assignVariant(
160
+ experiment.id,
161
+ userIdRef.current!
162
+ );
163
+
164
+ // Set experiment context so all events get tagged
165
+ analytics.setExperimentContext(experiment.id, assignment.variant_id);
166
+
167
+ // Use variant screens if available
168
+ if (assignment.variant_config?.screens?.length > 0) {
169
+ screensToUse = assignment.variant_config.screens;
170
+ }
171
+ } catch (err) {
172
+ console.warn('Failed to assign experiment variant, using default flow:', err);
173
+ }
174
+ }
175
+
104
176
  // Backward compatibility: normalize legacy 'custom_screen' (with elements) to 'noboard_screen'
105
- const normalizedScreens = configResponse.config.screens
177
+ const normalizedScreens = screensToUse
106
178
  .map(s => ({
107
179
  ...s,
108
180
  type: (s.type === ('custom_screen' as any) && s.elements) ? 'noboard_screen' as ScreenConfig['type'] : s.type,
@@ -111,6 +183,12 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
111
183
  .filter(s => !s.hidden);
112
184
  setScreens(normalizedScreens);
113
185
 
186
+ // Track onboarding start with first screen
187
+ analytics.track('onboarding_started', {
188
+ flow_id: flowIdRef.current,
189
+ screen_id: normalizedScreens[0]?.id,
190
+ });
191
+
114
192
  setLoading(false);
115
193
  } catch (err) {
116
194
  console.error('Failed to initialize onboarding flow:', err);
@@ -162,7 +240,10 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
162
240
 
163
241
  // Track completion
164
242
  if (analyticsRef.current) {
165
- analyticsRef.current.track('onboarding_completed');
243
+ analyticsRef.current.track('onboarding_completed', {
244
+ flow_id: flowIdRef.current,
245
+ screen_id: screens[currentIndex]?.id,
246
+ });
166
247
  await analyticsRef.current.flush();
167
248
  }
168
249
 
@@ -173,6 +254,8 @@ export const OnboardingFlow: React.FC<OnboardingFlowProps> = ({
173
254
  const handleSkipAll = async () => {
174
255
  if (analyticsRef.current) {
175
256
  analyticsRef.current.track('onboarding_abandoned', {
257
+ flow_id: flowIdRef.current,
258
+ screen_id: screens[currentIndex]?.id,
176
259
  current_screen_index: currentIndex,
177
260
  });
178
261
  await analyticsRef.current.flush();
package/src/analytics.ts CHANGED
@@ -10,6 +10,8 @@ export class AnalyticsManager {
10
10
  private userId: string;
11
11
  private sessionId: string;
12
12
  private flushTimer: NodeJS.Timeout | null = null;
13
+ private experimentId: string | null = null;
14
+ private variantId: string | null = null;
13
15
 
14
16
  constructor(api: API, userId: string, sessionId: string) {
15
17
  this.api = api;
@@ -20,16 +22,36 @@ export class AnalyticsManager {
20
22
  this.startFlushTimer();
21
23
  }
22
24
 
25
+ /**
26
+ * Set experiment context — all subsequent events will be tagged
27
+ */
28
+ setExperimentContext(experimentId: string, variantId: string): void {
29
+ this.experimentId = experimentId;
30
+ this.variantId = variantId;
31
+ }
32
+
23
33
  /**
24
34
  * Track an analytics event
25
35
  */
26
36
  track(eventName: string, properties?: Record<string, any>): void {
37
+ const mergedProperties: Record<string, any> = {
38
+ ...(properties || {}),
39
+ };
40
+
41
+ // Auto-inject experiment context if set
42
+ if (this.experimentId) {
43
+ mergedProperties.experiment_id = this.experimentId;
44
+ }
45
+ if (this.variantId) {
46
+ mergedProperties.variant_id = this.variantId;
47
+ }
48
+
27
49
  const event: AnalyticsEvent = {
28
50
  event: eventName,
29
51
  user_id: this.userId,
30
52
  session_id: this.sessionId,
31
53
  timestamp: Date.now(),
32
- properties: properties || {},
54
+ properties: mergedProperties,
33
55
  };
34
56
 
35
57
  this.events.push(event);
package/src/types.ts CHANGED
@@ -189,6 +189,7 @@ export interface AnalyticsEvent {
189
189
  export interface GetConfigResponse {
190
190
  config: OnboardingConfig;
191
191
  version: string;
192
+ config_id: string | null;
192
193
  experiments: Experiment[];
193
194
  organization_id: string;
194
195
  }