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.
- package/README.md +515 -0
- package/REVENUECAT_SETUP.md +756 -0
- package/SETUP_GUIDE.md +873 -0
- package/cusomte_screens.md +1964 -0
- package/lib/OnboardingFlow.d.ts +3 -0
- package/lib/OnboardingFlow.js +235 -0
- package/lib/analytics.d.ts +25 -0
- package/lib/analytics.js +72 -0
- package/lib/api.d.ts +31 -0
- package/lib/api.js +149 -0
- package/lib/components/ElementRenderer.d.ts +13 -0
- package/lib/components/ElementRenderer.js +521 -0
- package/lib/index.d.ts +6 -0
- package/lib/index.js +18 -0
- package/lib/types.d.ts +185 -0
- package/lib/types.js +2 -0
- package/lib/variableUtils.d.ts +17 -0
- package/lib/variableUtils.js +118 -0
- package/logic.md +2095 -0
- package/package.json +44 -0
- package/src/OnboardingFlow.tsx +276 -0
- package/src/analytics.ts +84 -0
- package/src/api.ts +173 -0
- package/src/components/ElementRenderer.tsx +627 -0
- package/src/index.ts +32 -0
- package/src/types.ts +242 -0
- package/src/variableUtils.ts +133 -0
- package/tsconfig.json +20 -0
|
@@ -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
|
+
}
|
package/lib/analytics.js
ADDED
|
@@ -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 {};
|