noboarding 0.1.0-alpha

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.
@@ -0,0 +1,3 @@
1
+ import React from 'react';
2
+ import { OnboardingFlowProps } from './types';
3
+ export declare const OnboardingFlow: React.FC<OnboardingFlowProps>;
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.OnboardingFlow = void 0;
37
+ const react_1 = __importStar(require("react"));
38
+ const react_native_1 = require("react-native");
39
+ const api_1 = require("./api");
40
+ const analytics_1 = require("./analytics");
41
+ const variableUtils_1 = require("./variableUtils");
42
+ 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)}`;
46
+ };
47
+ const generateSessionId = () => {
48
+ return `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
49
+ };
50
+ const OnboardingFlow = ({ apiKey, onComplete, onSkip, baseUrl, initialVariables, customComponents, onUserIdGenerated, }) => {
51
+ const [loading, setLoading] = (0, react_1.useState)(true);
52
+ const [error, setError] = (0, react_1.useState)(null);
53
+ const [screens, setScreens] = (0, react_1.useState)([]);
54
+ const [currentIndex, setCurrentIndex] = (0, react_1.useState)(0);
55
+ const [collectedData, setCollectedData] = (0, react_1.useState)({});
56
+ const [variables, setVariables] = (0, react_1.useState)(initialVariables || {});
57
+ const apiRef = (0, react_1.useRef)(null);
58
+ const analyticsRef = (0, react_1.useRef)(null);
59
+ const userIdRef = (0, react_1.useRef)(generateUserId());
60
+ const sessionIdRef = (0, react_1.useRef)(generateSessionId());
61
+ (0, react_1.useEffect)(() => {
62
+ // Notify parent of the generated user ID
63
+ if (onUserIdGenerated) {
64
+ onUserIdGenerated(userIdRef.current);
65
+ }
66
+ initializeFlow();
67
+ return () => {
68
+ // Cleanup: flush remaining analytics
69
+ if (analyticsRef.current) {
70
+ analyticsRef.current.destroy();
71
+ }
72
+ };
73
+ }, []);
74
+ const initializeFlow = async () => {
75
+ try {
76
+ setLoading(true);
77
+ setError(null);
78
+ // Initialize API client
79
+ const api = new api_1.API(apiKey, baseUrl);
80
+ apiRef.current = api;
81
+ // Initialize analytics
82
+ const analytics = new analytics_1.AnalyticsManager(api, userIdRef.current, sessionIdRef.current);
83
+ analyticsRef.current = analytics;
84
+ // Track onboarding start
85
+ analytics.track('onboarding_started');
86
+ // Fetch configuration
87
+ const configResponse = await api.getConfig();
88
+ // Backward compatibility: normalize legacy 'custom_screen' (with elements) to 'noboard_screen'
89
+ const normalizedScreens = configResponse.config.screens.map(s => (Object.assign(Object.assign({}, s), { type: (s.type === 'custom_screen' && s.elements) ? 'noboard_screen' : s.type })));
90
+ setScreens(normalizedScreens);
91
+ setLoading(false);
92
+ }
93
+ catch (err) {
94
+ console.error('Failed to initialize onboarding flow:', err);
95
+ setError('Failed to load onboarding. Please try again.');
96
+ setLoading(false);
97
+ }
98
+ };
99
+ const handleNext = (data) => {
100
+ // Collect data from this screen
101
+ if (data) {
102
+ setCollectedData((prev) => (Object.assign(Object.assign({}, prev), data)));
103
+ }
104
+ // Check if this is the last screen
105
+ if (currentIndex >= screens.length - 1) {
106
+ handleComplete(data);
107
+ }
108
+ else {
109
+ setCurrentIndex((prev) => prev + 1);
110
+ }
111
+ };
112
+ const handleSkipScreen = () => {
113
+ // Move to next screen or complete
114
+ if (currentIndex >= screens.length - 1) {
115
+ handleComplete();
116
+ }
117
+ else {
118
+ setCurrentIndex((prev) => prev + 1);
119
+ }
120
+ };
121
+ const handleSetVariable = (0, react_1.useCallback)((name, value) => {
122
+ setVariables(prev => (Object.assign(Object.assign({}, prev), { [name]: value })));
123
+ }, []);
124
+ const handleComplete = async (lastScreenData) => {
125
+ const finalData = Object.assign(Object.assign(Object.assign({}, collectedData), (lastScreenData || {})), { _variables: variables });
126
+ // Track completion
127
+ if (analyticsRef.current) {
128
+ analyticsRef.current.track('onboarding_completed');
129
+ await analyticsRef.current.flush();
130
+ }
131
+ // Call developer's completion callback with collected data
132
+ onComplete(finalData);
133
+ };
134
+ const handleSkipAll = async () => {
135
+ if (analyticsRef.current) {
136
+ analyticsRef.current.track('onboarding_abandoned', {
137
+ current_screen_index: currentIndex,
138
+ });
139
+ await analyticsRef.current.flush();
140
+ }
141
+ if (onSkip) {
142
+ onSkip();
143
+ }
144
+ };
145
+ if (loading) {
146
+ return (<react_native_1.View style={styles.centerContainer}>
147
+ <react_native_1.ActivityIndicator size="large" color="#007AFF"/>
148
+ <react_native_1.Text style={styles.loadingText}>Loading...</react_native_1.Text>
149
+ </react_native_1.View>);
150
+ }
151
+ if (error) {
152
+ return (<react_native_1.View style={styles.centerContainer}>
153
+ <react_native_1.Text style={styles.errorText}>{error}</react_native_1.Text>
154
+ </react_native_1.View>);
155
+ }
156
+ if (screens.length === 0) {
157
+ return (<react_native_1.View style={styles.centerContainer}>
158
+ <react_native_1.Text style={styles.errorText}>No onboarding screens configured</react_native_1.Text>
159
+ </react_native_1.View>);
160
+ }
161
+ const currentScreen = screens[currentIndex];
162
+ // Handle noboard_screen type — render with ElementRenderer
163
+ if (currentScreen.type === 'noboard_screen' && currentScreen.elements) {
164
+ const handleElementNavigate = (destination) => {
165
+ const resolved = (0, variableUtils_1.resolveDestination)(destination, variables);
166
+ if (!resolved)
167
+ return;
168
+ if (resolved === 'next') {
169
+ handleNext();
170
+ }
171
+ else if (resolved === 'previous') {
172
+ setCurrentIndex((prev) => Math.max(0, prev - 1));
173
+ }
174
+ else {
175
+ // Navigate to specific screen by ID
176
+ const targetIndex = screens.findIndex((s) => s.id === resolved);
177
+ if (targetIndex >= 0) {
178
+ setCurrentIndex(targetIndex);
179
+ }
180
+ else {
181
+ handleNext();
182
+ }
183
+ }
184
+ };
185
+ return (<react_native_1.View style={styles.container}>
186
+ <ElementRenderer_1.ElementRenderer elements={currentScreen.elements} analytics={analyticsRef.current} screenId={currentScreen.id} onNavigate={handleElementNavigate} onDismiss={onSkip ? handleSkipAll : handleNext} variables={variables} onSetVariable={handleSetVariable}/>
187
+ </react_native_1.View>);
188
+ }
189
+ // Handle custom_screen type — developer-registered React Native components
190
+ if (currentScreen.type === 'custom_screen') {
191
+ const componentName = currentScreen.custom_component_name;
192
+ const CustomComponent = customComponents === null || customComponents === void 0 ? void 0 : customComponents[componentName || ''];
193
+ if (!CustomComponent) {
194
+ return (<react_native_1.View style={styles.centerContainer}>
195
+ <react_native_1.Text style={styles.errorText}>
196
+ Component "{componentName}" not found.
197
+ </react_native_1.Text>
198
+ <react_native_1.TouchableOpacity style={{ marginTop: 16, padding: 12 }} onPress={() => handleNext()}>
199
+ <react_native_1.Text style={{ color: '#007AFF', fontSize: 16 }}>Skip</react_native_1.Text>
200
+ </react_native_1.TouchableOpacity>
201
+ </react_native_1.View>);
202
+ }
203
+ return (<react_native_1.View style={styles.container}>
204
+ <CustomComponent analytics={analyticsRef.current} onNext={() => handleNext()} onSkip={onSkip ? handleSkipAll : undefined} data={collectedData} onDataUpdate={(newData) => setCollectedData(prev => (Object.assign(Object.assign({}, prev), newData)))}/>
205
+ </react_native_1.View>);
206
+ }
207
+ // Unknown screen type fallback
208
+ return (<react_native_1.View style={styles.centerContainer}>
209
+ <react_native_1.Text style={styles.errorText}>
210
+ Unknown screen type: "{currentScreen.type}"
211
+ </react_native_1.Text>
212
+ </react_native_1.View>);
213
+ };
214
+ exports.OnboardingFlow = OnboardingFlow;
215
+ const styles = react_native_1.StyleSheet.create({
216
+ container: {
217
+ flex: 1,
218
+ },
219
+ centerContainer: {
220
+ flex: 1,
221
+ justifyContent: 'center',
222
+ alignItems: 'center',
223
+ padding: 24,
224
+ },
225
+ loadingText: {
226
+ marginTop: 16,
227
+ fontSize: 16,
228
+ color: '#000000',
229
+ },
230
+ errorText: {
231
+ fontSize: 16,
232
+ color: '#FF3B30',
233
+ textAlign: 'center',
234
+ },
235
+ });
@@ -0,0 +1,25 @@
1
+ import { API } from './api';
2
+ export declare class AnalyticsManager {
3
+ private api;
4
+ private events;
5
+ private userId;
6
+ private sessionId;
7
+ private flushTimer;
8
+ constructor(api: API, userId: string, sessionId: string);
9
+ /**
10
+ * Track an analytics event
11
+ */
12
+ track(eventName: string, properties?: Record<string, any>): void;
13
+ /**
14
+ * Manually flush events to backend
15
+ */
16
+ flush(): Promise<void>;
17
+ /**
18
+ * Start auto-flush timer
19
+ */
20
+ private startFlushTimer;
21
+ /**
22
+ * Stop auto-flush timer and flush remaining events
23
+ */
24
+ destroy(): Promise<void>;
25
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.AnalyticsManager = void 0;
4
+ const BATCH_SIZE = 10;
5
+ const FLUSH_INTERVAL_MS = 10000; // 10 seconds
6
+ class AnalyticsManager {
7
+ constructor(api, userId, sessionId) {
8
+ this.events = [];
9
+ this.flushTimer = null;
10
+ this.api = api;
11
+ this.userId = userId;
12
+ this.sessionId = sessionId;
13
+ // Start auto-flush timer
14
+ this.startFlushTimer();
15
+ }
16
+ /**
17
+ * Track an analytics event
18
+ */
19
+ track(eventName, properties) {
20
+ const event = {
21
+ event: eventName,
22
+ user_id: this.userId,
23
+ session_id: this.sessionId,
24
+ timestamp: Date.now(),
25
+ properties: properties || {},
26
+ };
27
+ this.events.push(event);
28
+ // Auto-flush if batch size reached
29
+ if (this.events.length >= BATCH_SIZE) {
30
+ this.flush();
31
+ }
32
+ }
33
+ /**
34
+ * Manually flush events to backend
35
+ */
36
+ async flush() {
37
+ if (this.events.length === 0) {
38
+ return;
39
+ }
40
+ // Take current events and clear queue
41
+ const eventsToSend = [...this.events];
42
+ this.events = [];
43
+ try {
44
+ await this.api.trackEvents(eventsToSend);
45
+ console.log(`Flushed ${eventsToSend.length} events`);
46
+ }
47
+ catch (error) {
48
+ console.error('Failed to flush events:', error);
49
+ // Put events back in queue to retry later
50
+ this.events.unshift(...eventsToSend);
51
+ }
52
+ }
53
+ /**
54
+ * Start auto-flush timer
55
+ */
56
+ startFlushTimer() {
57
+ this.flushTimer = setInterval(() => {
58
+ this.flush();
59
+ }, FLUSH_INTERVAL_MS);
60
+ }
61
+ /**
62
+ * Stop auto-flush timer and flush remaining events
63
+ */
64
+ async destroy() {
65
+ if (this.flushTimer) {
66
+ clearInterval(this.flushTimer);
67
+ this.flushTimer = null;
68
+ }
69
+ await this.flush();
70
+ }
71
+ }
72
+ exports.AnalyticsManager = AnalyticsManager;
package/lib/api.d.ts ADDED
@@ -0,0 +1,31 @@
1
+ import { GetConfigResponse, AnalyticsEvent, TrackEventsResponse, AssignVariantResponse } from './types';
2
+ export declare class API {
3
+ private apiKey;
4
+ private baseUrl;
5
+ private supabaseAnonKey;
6
+ constructor(apiKey: string, baseUrl?: string);
7
+ /**
8
+ * Fetch onboarding configuration from API
9
+ */
10
+ getConfig(): Promise<GetConfigResponse>;
11
+ /**
12
+ * Get cached configuration
13
+ */
14
+ getCachedConfig(): Promise<GetConfigResponse | null>;
15
+ /**
16
+ * Cache configuration locally
17
+ */
18
+ private cacheConfig;
19
+ /**
20
+ * Send analytics events to backend
21
+ */
22
+ trackEvents(events: AnalyticsEvent[]): Promise<TrackEventsResponse>;
23
+ /**
24
+ * Get A/B test variant assignment
25
+ */
26
+ assignVariant(experimentId: string, userId: string): Promise<AssignVariantResponse>;
27
+ /**
28
+ * Clear cached config
29
+ */
30
+ clearCache(): Promise<void>;
31
+ }
package/lib/api.js ADDED
@@ -0,0 +1,149 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.API = void 0;
7
+ const async_storage_1 = __importDefault(require("@react-native-async-storage/async-storage"));
8
+ const CACHE_KEY = '@noboarding:config';
9
+ const CACHE_EXPIRY_MS = 60 * 60 * 1000; // 1 hour
10
+ class API {
11
+ constructor(apiKey, baseUrl) {
12
+ this.supabaseAnonKey = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6ImhobW16bXJzcHRlZ3ByZnp0cXRxIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzA3ODM1MDUsImV4cCI6MjA4NjM1OTUwNX0.otIIs3ZyWTHTnDZ1NbZolzeHjWv__wmHekxZevhKryI';
13
+ this.apiKey = apiKey;
14
+ this.baseUrl = baseUrl || 'https://hhmmzmrsptegprfztqtq.supabase.co/functions/v1';
15
+ }
16
+ /**
17
+ * Fetch onboarding configuration from API
18
+ */
19
+ async getConfig() {
20
+ try {
21
+ const response = await fetch(`${this.baseUrl}/get-config`, {
22
+ method: 'GET',
23
+ headers: {
24
+ 'Authorization': `Bearer ${this.supabaseAnonKey}`,
25
+ 'x-api-key': this.apiKey,
26
+ 'Content-Type': 'application/json',
27
+ },
28
+ });
29
+ if (!response.ok) {
30
+ throw new Error(`Failed to fetch config: ${response.status}`);
31
+ }
32
+ const data = await response.json();
33
+ // Cache the config
34
+ await this.cacheConfig(data);
35
+ return data;
36
+ }
37
+ catch (error) {
38
+ console.error('Error fetching config:', error);
39
+ // Try to return cached config as fallback
40
+ const cached = await this.getCachedConfig();
41
+ if (cached) {
42
+ console.log('Using cached config as fallback');
43
+ return cached;
44
+ }
45
+ throw error;
46
+ }
47
+ }
48
+ /**
49
+ * Get cached configuration
50
+ */
51
+ async getCachedConfig() {
52
+ try {
53
+ const cached = await async_storage_1.default.getItem(CACHE_KEY);
54
+ if (!cached) {
55
+ return null;
56
+ }
57
+ const parsedCache = JSON.parse(cached);
58
+ const now = Date.now();
59
+ // Check if cache is expired
60
+ if (now - parsedCache.timestamp > CACHE_EXPIRY_MS) {
61
+ await async_storage_1.default.removeItem(CACHE_KEY);
62
+ return null;
63
+ }
64
+ return parsedCache.config;
65
+ }
66
+ catch (error) {
67
+ console.error('Error reading cached config:', error);
68
+ return null;
69
+ }
70
+ }
71
+ /**
72
+ * Cache configuration locally
73
+ */
74
+ async cacheConfig(config) {
75
+ try {
76
+ const cacheData = {
77
+ config,
78
+ timestamp: Date.now(),
79
+ };
80
+ await async_storage_1.default.setItem(CACHE_KEY, JSON.stringify(cacheData));
81
+ }
82
+ catch (error) {
83
+ console.error('Error caching config:', error);
84
+ }
85
+ }
86
+ /**
87
+ * Send analytics events to backend
88
+ */
89
+ async trackEvents(events) {
90
+ try {
91
+ const response = await fetch(`${this.baseUrl}/track-events`, {
92
+ method: 'POST',
93
+ headers: {
94
+ 'Authorization': `Bearer ${this.supabaseAnonKey}`,
95
+ 'x-api-key': this.apiKey,
96
+ 'Content-Type': 'application/json',
97
+ },
98
+ body: JSON.stringify({ events }),
99
+ });
100
+ if (!response.ok) {
101
+ throw new Error(`Failed to track events: ${response.status}`);
102
+ }
103
+ return await response.json();
104
+ }
105
+ catch (error) {
106
+ console.error('Error tracking events:', error);
107
+ throw error;
108
+ }
109
+ }
110
+ /**
111
+ * Get A/B test variant assignment
112
+ */
113
+ async assignVariant(experimentId, userId) {
114
+ try {
115
+ const response = await fetch(`${this.baseUrl}/assign-variant`, {
116
+ method: 'POST',
117
+ headers: {
118
+ 'Authorization': `Bearer ${this.supabaseAnonKey}`,
119
+ 'x-api-key': this.apiKey,
120
+ 'Content-Type': 'application/json',
121
+ },
122
+ body: JSON.stringify({
123
+ experiment_id: experimentId,
124
+ user_id: userId,
125
+ }),
126
+ });
127
+ if (!response.ok) {
128
+ throw new Error(`Failed to assign variant: ${response.status}`);
129
+ }
130
+ return await response.json();
131
+ }
132
+ catch (error) {
133
+ console.error('Error assigning variant:', error);
134
+ throw error;
135
+ }
136
+ }
137
+ /**
138
+ * Clear cached config
139
+ */
140
+ async clearCache() {
141
+ try {
142
+ await async_storage_1.default.removeItem(CACHE_KEY);
143
+ }
144
+ catch (error) {
145
+ console.error('Error clearing cache:', error);
146
+ }
147
+ }
148
+ }
149
+ exports.API = API;
@@ -0,0 +1,13 @@
1
+ import React from 'react';
2
+ import { ElementNode, Analytics, ConditionalDestination, ConditionalRoutes } from '../types';
3
+ interface ElementRendererProps {
4
+ elements: ElementNode[];
5
+ analytics?: Analytics;
6
+ screenId?: string;
7
+ onNavigate?: (destination: string | ConditionalDestination | ConditionalRoutes) => void;
8
+ onDismiss?: () => void;
9
+ variables?: Record<string, any>;
10
+ onSetVariable?: (name: string, value: any) => void;
11
+ }
12
+ export declare const ElementRenderer: React.FC<ElementRendererProps>;
13
+ export {};