@mobana/react-native-sdk 0.2.10
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/LICENSE +21 -0
- package/README.md +249 -0
- package/android/build.gradle +50 -0
- package/android/src/main/AndroidManifest.xml +6 -0
- package/android/src/main/java/ai/mobana/sdk/MobanaModule.kt +67 -0
- package/android/src/main/java/ai/mobana/sdk/MobanaPackage.kt +19 -0
- package/app.plugin.js +274 -0
- package/ios/Mobana.h +11 -0
- package/ios/Mobana.m +20 -0
- package/lib/commonjs/Mobana.js +676 -0
- package/lib/commonjs/Mobana.js.map +1 -0
- package/lib/commonjs/NativeMobana.js +53 -0
- package/lib/commonjs/NativeMobana.js.map +1 -0
- package/lib/commonjs/api.js +201 -0
- package/lib/commonjs/api.js.map +1 -0
- package/lib/commonjs/bridge/index.js +19 -0
- package/lib/commonjs/bridge/index.js.map +1 -0
- package/lib/commonjs/bridge/injectBridge.js +528 -0
- package/lib/commonjs/bridge/injectBridge.js.map +1 -0
- package/lib/commonjs/components/FlowWebView.js +676 -0
- package/lib/commonjs/components/FlowWebView.js.map +1 -0
- package/lib/commonjs/components/MobanaProvider.js +275 -0
- package/lib/commonjs/components/MobanaProvider.js.map +1 -0
- package/lib/commonjs/components/index.js +20 -0
- package/lib/commonjs/components/index.js.map +1 -0
- package/lib/commonjs/device.js +49 -0
- package/lib/commonjs/device.js.map +1 -0
- package/lib/commonjs/index.js +20 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/storage.js +277 -0
- package/lib/commonjs/storage.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/module/Mobana.js +673 -0
- package/lib/module/Mobana.js.map +1 -0
- package/lib/module/NativeMobana.js +49 -0
- package/lib/module/NativeMobana.js.map +1 -0
- package/lib/module/api.js +194 -0
- package/lib/module/api.js.map +1 -0
- package/lib/module/bridge/index.js +4 -0
- package/lib/module/bridge/index.js.map +1 -0
- package/lib/module/bridge/injectBridge.js +523 -0
- package/lib/module/bridge/injectBridge.js.map +1 -0
- package/lib/module/components/FlowWebView.js +672 -0
- package/lib/module/components/FlowWebView.js.map +1 -0
- package/lib/module/components/MobanaProvider.js +270 -0
- package/lib/module/components/MobanaProvider.js.map +1 -0
- package/lib/module/components/index.js +5 -0
- package/lib/module/components/index.js.map +1 -0
- package/lib/module/device.js +45 -0
- package/lib/module/device.js.map +1 -0
- package/lib/module/index.js +53 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/storage.js +257 -0
- package/lib/module/storage.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/Mobana.d.ts +209 -0
- package/lib/typescript/Mobana.d.ts.map +1 -0
- package/lib/typescript/NativeMobana.d.ts +11 -0
- package/lib/typescript/NativeMobana.d.ts.map +1 -0
- package/lib/typescript/api.d.ts +34 -0
- package/lib/typescript/api.d.ts.map +1 -0
- package/lib/typescript/bridge/index.d.ts +3 -0
- package/lib/typescript/bridge/index.d.ts.map +1 -0
- package/lib/typescript/bridge/injectBridge.d.ts +23 -0
- package/lib/typescript/bridge/injectBridge.d.ts.map +1 -0
- package/lib/typescript/components/FlowWebView.d.ts +38 -0
- package/lib/typescript/components/FlowWebView.d.ts.map +1 -0
- package/lib/typescript/components/MobanaProvider.d.ts +65 -0
- package/lib/typescript/components/MobanaProvider.d.ts.map +1 -0
- package/lib/typescript/components/index.d.ts +5 -0
- package/lib/typescript/components/index.d.ts.map +1 -0
- package/lib/typescript/device.d.ts +6 -0
- package/lib/typescript/device.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +46 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/storage.d.ts +68 -0
- package/lib/typescript/storage.d.ts.map +1 -0
- package/lib/typescript/types.d.ts +298 -0
- package/lib/typescript/types.d.ts.map +1 -0
- package/mobana.podspec +19 -0
- package/package.json +131 -0
- package/src/Mobana.ts +742 -0
- package/src/NativeMobana.ts +61 -0
- package/src/api.ts +259 -0
- package/src/bridge/index.ts +2 -0
- package/src/bridge/injectBridge.ts +542 -0
- package/src/components/FlowWebView.tsx +826 -0
- package/src/components/MobanaProvider.tsx +393 -0
- package/src/components/index.ts +4 -0
- package/src/device.ts +42 -0
- package/src/index.ts +66 -0
- package/src/storage.ts +262 -0
- package/src/types.ts +362 -0
package/ios/Mobana.h
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Mobana native module for iOS
|
|
5
|
+
*
|
|
6
|
+
* Note: iOS does not have Install Referrer like Android.
|
|
7
|
+
* This module exists for API parity but getInstallReferrer always returns nil.
|
|
8
|
+
*/
|
|
9
|
+
@interface Mobana : NSObject <RCTBridgeModule>
|
|
10
|
+
|
|
11
|
+
@end
|
package/ios/Mobana.m
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#import "Mobana.h"
|
|
2
|
+
|
|
3
|
+
@implementation Mobana
|
|
4
|
+
|
|
5
|
+
RCT_EXPORT_MODULE()
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Get Install Referrer - not available on iOS, always returns nil
|
|
9
|
+
*
|
|
10
|
+
* iOS does not have an equivalent to Android's Install Referrer API.
|
|
11
|
+
* Attribution on iOS relies on probabilistic matching.
|
|
12
|
+
*/
|
|
13
|
+
RCT_EXPORT_METHOD(getInstallReferrer:(RCTPromiseResolveBlock)resolve
|
|
14
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
15
|
+
{
|
|
16
|
+
// iOS doesn't have Install Referrer, return nil
|
|
17
|
+
resolve(nil);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
@end
|
|
@@ -0,0 +1,676 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", {
|
|
4
|
+
value: true
|
|
5
|
+
});
|
|
6
|
+
exports.MobanaSDK = exports.Mobana = void 0;
|
|
7
|
+
var _storage = require("./storage");
|
|
8
|
+
var _api = require("./api");
|
|
9
|
+
var _device = require("./device");
|
|
10
|
+
var _NativeMobana = require("./NativeMobana");
|
|
11
|
+
var _MobanaProvider = require("./components/MobanaProvider");
|
|
12
|
+
const DEFAULT_ENDPOINT = 'https://{appId}.mobana.ai';
|
|
13
|
+
const DEFAULT_TIMEOUT = 10000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Mobana SDK for React Native
|
|
17
|
+
*
|
|
18
|
+
* Simple, privacy-focused mobile app attribution, conversions, and remote flows.
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```typescript
|
|
22
|
+
* import { Mobana, MobanaProvider } from '@mobana/react-native-sdk';
|
|
23
|
+
*
|
|
24
|
+
* // Wrap your app with the provider (in App.tsx)
|
|
25
|
+
* function App() {
|
|
26
|
+
* return (
|
|
27
|
+
* <MobanaProvider>
|
|
28
|
+
* <YourApp />
|
|
29
|
+
* </MobanaProvider>
|
|
30
|
+
* );
|
|
31
|
+
* }
|
|
32
|
+
*
|
|
33
|
+
* // Initialize once on app start
|
|
34
|
+
* await Mobana.init({ appId: 'a1b2c3d4' });
|
|
35
|
+
*
|
|
36
|
+
* // Get attribution (handles caching, retries, Android Install Referrer)
|
|
37
|
+
* const attribution = await Mobana.getAttribution();
|
|
38
|
+
*
|
|
39
|
+
* // Track conversions
|
|
40
|
+
* Mobana.trackConversion('signup');
|
|
41
|
+
* Mobana.trackConversion('purchase', 49.99);
|
|
42
|
+
*
|
|
43
|
+
* // Show a flow
|
|
44
|
+
* const result = await Mobana.startFlow('onboarding');
|
|
45
|
+
* if (result.completed) {
|
|
46
|
+
* console.log('User completed onboarding!', result.data);
|
|
47
|
+
* }
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
class MobanaSDK {
|
|
51
|
+
config = null;
|
|
52
|
+
isConfigured = false;
|
|
53
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
54
|
+
attributionPromise = null;
|
|
55
|
+
// In-memory cache for attribution (faster than AsyncStorage)
|
|
56
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
57
|
+
cachedAttribution = null;
|
|
58
|
+
attributionChecked = false;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Initialize the SDK with your app settings
|
|
62
|
+
* Must be called before any other SDK methods
|
|
63
|
+
*
|
|
64
|
+
* @param config - Configuration options (appId is required)
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* ```typescript
|
|
68
|
+
* // Basic initialization
|
|
69
|
+
* await Mobana.init({ appId: 'a1b2c3d4' });
|
|
70
|
+
*
|
|
71
|
+
* // With custom endpoint (for domain proxying)
|
|
72
|
+
* await Mobana.init({
|
|
73
|
+
* appId: 'a1b2c3d4',
|
|
74
|
+
* endpoint: 'https://myapp.com/d',
|
|
75
|
+
* });
|
|
76
|
+
*
|
|
77
|
+
* // With all options
|
|
78
|
+
* await Mobana.init({
|
|
79
|
+
* appId: 'a1b2c3d4',
|
|
80
|
+
* endpoint: 'https://myapp.com/d', // Optional
|
|
81
|
+
* enabled: userHasConsented, // Optional, default: true
|
|
82
|
+
* debug: __DEV__, // Optional, default: false
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*/
|
|
86
|
+
async init(config) {
|
|
87
|
+
if (!config.appId) {
|
|
88
|
+
console.warn('[Mobana] appId is required');
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!config.appKey) {
|
|
92
|
+
console.warn('[Mobana] appKey is required');
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
this.config = {
|
|
96
|
+
enabled: true,
|
|
97
|
+
debug: false,
|
|
98
|
+
...config
|
|
99
|
+
};
|
|
100
|
+
this.isConfigured = true;
|
|
101
|
+
|
|
102
|
+
// Eagerly generate/retrieve the install ID so it's ready before
|
|
103
|
+
// the first attribution or conversion call.
|
|
104
|
+
const installId = await (0, _storage.getInstallId)();
|
|
105
|
+
if (this.config.debug) {
|
|
106
|
+
console.log('[Mobana] Initialized:', {
|
|
107
|
+
appId: this.config.appId,
|
|
108
|
+
endpoint: this.config.endpoint,
|
|
109
|
+
enabled: this.config.enabled,
|
|
110
|
+
installId
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Flush any queued conversions when SDK is initialized
|
|
115
|
+
await this.flushConversionQueue();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Enable or disable the SDK dynamically
|
|
120
|
+
* Useful for GDPR consent flows
|
|
121
|
+
*
|
|
122
|
+
* @param enabled - Whether the SDK should be enabled
|
|
123
|
+
*/
|
|
124
|
+
setEnabled(enabled) {
|
|
125
|
+
if (!this.config) {
|
|
126
|
+
console.warn('[Mobana] SDK not configured. Call init() first.');
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this.config.enabled = enabled;
|
|
130
|
+
if (this.config.debug) {
|
|
131
|
+
console.log(`[Mobana] ${enabled ? 'Enabled' : 'Disabled'}`);
|
|
132
|
+
}
|
|
133
|
+
if (enabled) {
|
|
134
|
+
this.flushConversionQueue();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get attribution data for this install
|
|
140
|
+
*
|
|
141
|
+
* Returns cached result if available, otherwise fetches from server.
|
|
142
|
+
* Never throws - returns null on error or no match.
|
|
143
|
+
*
|
|
144
|
+
* @param options - Optional settings for the attribution request
|
|
145
|
+
* @returns Attribution data or null if not available
|
|
146
|
+
*
|
|
147
|
+
* @example
|
|
148
|
+
* ```typescript
|
|
149
|
+
* const attribution = await Mobana.getAttribution();
|
|
150
|
+
*
|
|
151
|
+
* if (attribution) {
|
|
152
|
+
* YourAnalyticsProvider.track('App Installed', {
|
|
153
|
+
* source: attribution.utm_source,
|
|
154
|
+
* campaign: attribution.utm_campaign,
|
|
155
|
+
* });
|
|
156
|
+
*
|
|
157
|
+
* if (attribution.data?.promo) {
|
|
158
|
+
* applyPromoCode(attribution.data.promo);
|
|
159
|
+
* }
|
|
160
|
+
* }
|
|
161
|
+
* ```
|
|
162
|
+
*
|
|
163
|
+
* @example
|
|
164
|
+
* // With TypeScript generics for typed data
|
|
165
|
+
* interface MyDeeplinkData {
|
|
166
|
+
* promo?: string;
|
|
167
|
+
* referrer?: string;
|
|
168
|
+
* }
|
|
169
|
+
*
|
|
170
|
+
* const attribution = await Mobana.getAttribution<MyDeeplinkData>();
|
|
171
|
+
* // attribution.data is now typed as MyDeeplinkData
|
|
172
|
+
*/
|
|
173
|
+
async getAttribution(options = {}) {
|
|
174
|
+
if (!this.isConfigured || !this.config) {
|
|
175
|
+
console.warn('[Mobana] SDK not configured. Call init() first.');
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
if (!this.config.enabled) {
|
|
179
|
+
if (this.config.debug) {
|
|
180
|
+
console.log('[Mobana] SDK disabled, returning null');
|
|
181
|
+
}
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Return in-memory cache if available (fastest)
|
|
186
|
+
if (this.attributionChecked) {
|
|
187
|
+
return this.cachedAttribution;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check AsyncStorage cache
|
|
191
|
+
const cached = await (0, _storage.getCachedResult)();
|
|
192
|
+
if (cached) {
|
|
193
|
+
if (this.config.debug) {
|
|
194
|
+
console.log('[Mobana] Returning cached result, matched:', cached.matched);
|
|
195
|
+
}
|
|
196
|
+
// Update in-memory cache
|
|
197
|
+
this.attributionChecked = true;
|
|
198
|
+
this.cachedAttribution = cached.matched ? cached.attribution ?? null : null;
|
|
199
|
+
return this.cachedAttribution;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Prevent duplicate concurrent requests
|
|
203
|
+
if (this.attributionPromise) {
|
|
204
|
+
return this.attributionPromise;
|
|
205
|
+
}
|
|
206
|
+
this.attributionPromise = this.fetchAttribution(options);
|
|
207
|
+
const result = await this.attributionPromise;
|
|
208
|
+
this.attributionPromise = null;
|
|
209
|
+
|
|
210
|
+
// Update in-memory cache
|
|
211
|
+
this.attributionChecked = true;
|
|
212
|
+
this.cachedAttribution = result;
|
|
213
|
+
return result;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Track a conversion event
|
|
218
|
+
*
|
|
219
|
+
* Conversions are linked to the original attribution via installId.
|
|
220
|
+
* If offline, conversions are queued and sent when back online.
|
|
221
|
+
* Never throws - silently handles errors.
|
|
222
|
+
*
|
|
223
|
+
* @param name - Conversion name (must be configured in app settings)
|
|
224
|
+
* @param value - Optional monetary value
|
|
225
|
+
* @param flowSessionId - Optional flow session ID to link conversion to a specific flow presentation
|
|
226
|
+
*
|
|
227
|
+
* @example
|
|
228
|
+
* ```typescript
|
|
229
|
+
* // Simple conversion
|
|
230
|
+
* Mobana.trackConversion('signup');
|
|
231
|
+
*
|
|
232
|
+
* // Conversion with value
|
|
233
|
+
* Mobana.trackConversion('purchase', 49.99);
|
|
234
|
+
*
|
|
235
|
+
* // Conversion linked to a flow session
|
|
236
|
+
* const result = await Mobana.startFlow('pre-purchase');
|
|
237
|
+
* // ... user makes purchase via paywall ...
|
|
238
|
+
* await Mobana.trackConversion('purchase', 49.99, result.sessionId);
|
|
239
|
+
* ```
|
|
240
|
+
*/
|
|
241
|
+
async trackConversion(name, value, flowSessionId) {
|
|
242
|
+
if (!this.isConfigured || !this.config) {
|
|
243
|
+
if (this.config?.debug) {
|
|
244
|
+
console.log('[Mobana] SDK not configured, skipping conversion');
|
|
245
|
+
}
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (!this.config.enabled) {
|
|
249
|
+
if (this.config.debug) {
|
|
250
|
+
console.log('[Mobana] SDK disabled, skipping conversion');
|
|
251
|
+
}
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const installId = await (0, _storage.getInstallId)();
|
|
255
|
+
const event = {
|
|
256
|
+
installId,
|
|
257
|
+
name,
|
|
258
|
+
value,
|
|
259
|
+
timestamp: Date.now(),
|
|
260
|
+
flowSessionId
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
// Ensure Install record exists on server (created on first getAttribution call).
|
|
264
|
+
// This is fast after first call — returns from in-memory cache without network.
|
|
265
|
+
// We don't care about the result; conversions work for organic installs too.
|
|
266
|
+
await this.getAttribution();
|
|
267
|
+
|
|
268
|
+
// Try to send immediately
|
|
269
|
+
const success = await this.sendConversion(event);
|
|
270
|
+
if (!success) {
|
|
271
|
+
// Queue for later if failed (offline, etc.)
|
|
272
|
+
await (0, _storage.queueConversion)(event);
|
|
273
|
+
if (this.config.debug) {
|
|
274
|
+
console.log('[Mobana] Conversion queued for later');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Reset all stored attribution data
|
|
281
|
+
* Useful for testing or when user logs out
|
|
282
|
+
*
|
|
283
|
+
* Note: This generates a new installId, so subsequent attributions
|
|
284
|
+
* will be treated as a new install.
|
|
285
|
+
*/
|
|
286
|
+
async reset() {
|
|
287
|
+
// Clear in-memory cache
|
|
288
|
+
this.cachedAttribution = null;
|
|
289
|
+
this.attributionChecked = false;
|
|
290
|
+
this.attributionPromise = null;
|
|
291
|
+
|
|
292
|
+
// Clear persistent storage
|
|
293
|
+
await (0, _storage.clearAttribution)();
|
|
294
|
+
await (0, _storage.clearConversionQueue)();
|
|
295
|
+
await (0, _storage.clearAllCachedFlows)();
|
|
296
|
+
await (0, _storage.clearLocalData)();
|
|
297
|
+
if (this.config?.debug) {
|
|
298
|
+
console.log('[Mobana] Reset complete');
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ============================================
|
|
303
|
+
// Flows
|
|
304
|
+
// ============================================
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Start and display a flow
|
|
308
|
+
*
|
|
309
|
+
* Fetches the flow content (or uses cache) and presents it in a full-screen modal.
|
|
310
|
+
* The promise resolves when the user completes or dismisses the flow.
|
|
311
|
+
*
|
|
312
|
+
* Requires MobanaProvider to be mounted in your app.
|
|
313
|
+
*
|
|
314
|
+
* @param slug - Flow identifier (from dashboard)
|
|
315
|
+
* @param options - Optional flow configuration
|
|
316
|
+
* @returns Flow result with completion status and optional data
|
|
317
|
+
*
|
|
318
|
+
* @example
|
|
319
|
+
* ```typescript
|
|
320
|
+
* // Basic usage
|
|
321
|
+
* const result = await Mobana.startFlow('onboarding');
|
|
322
|
+
*
|
|
323
|
+
* if (result.completed) {
|
|
324
|
+
* console.log('Onboarding completed!', result.data);
|
|
325
|
+
* } else if (result.error) {
|
|
326
|
+
* console.log('Flow error:', result.error);
|
|
327
|
+
* }
|
|
328
|
+
*
|
|
329
|
+
* // With custom parameters
|
|
330
|
+
* const result = await Mobana.startFlow('welcome', {
|
|
331
|
+
* params: { userName: 'John', isPremium: true },
|
|
332
|
+
* onEvent: (name) => {
|
|
333
|
+
* analytics.track(name);
|
|
334
|
+
* },
|
|
335
|
+
* });
|
|
336
|
+
* ```
|
|
337
|
+
*/
|
|
338
|
+
async startFlow(slug, options) {
|
|
339
|
+
// Check if SDK is configured
|
|
340
|
+
if (!this.isConfigured || !this.config) {
|
|
341
|
+
console.warn('[Mobana] SDK not configured. Call init() first.');
|
|
342
|
+
return {
|
|
343
|
+
completed: false,
|
|
344
|
+
dismissed: true,
|
|
345
|
+
error: 'SDK_NOT_CONFIGURED'
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Check if SDK is enabled
|
|
350
|
+
if (!this.config.enabled) {
|
|
351
|
+
if (this.config.debug) {
|
|
352
|
+
console.log('[Mobana] SDK disabled, cannot start flow');
|
|
353
|
+
}
|
|
354
|
+
return {
|
|
355
|
+
completed: false,
|
|
356
|
+
dismissed: true,
|
|
357
|
+
error: 'SDK_NOT_CONFIGURED'
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Check if provider is mounted
|
|
362
|
+
const flowContext = (0, _MobanaProvider.getGlobalFlowContext)();
|
|
363
|
+
if (!flowContext?.isProviderMounted) {
|
|
364
|
+
console.warn('[Mobana] startFlow() called but MobanaProvider is not mounted. ' + 'Wrap your app with <MobanaProvider> to enable flows.');
|
|
365
|
+
return {
|
|
366
|
+
completed: false,
|
|
367
|
+
dismissed: true,
|
|
368
|
+
error: 'PROVIDER_NOT_MOUNTED'
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
try {
|
|
372
|
+
const endpoint = this.getEndpoint();
|
|
373
|
+
const installId = await (0, _storage.getInstallId)();
|
|
374
|
+
|
|
375
|
+
// Ensure attribution is loaded (for passing to flow context).
|
|
376
|
+
// Fast after first call — returns from in-memory cache without network.
|
|
377
|
+
// We don't fail if attribution isn't matched; flows work for organic installs too.
|
|
378
|
+
await this.getAttribution();
|
|
379
|
+
|
|
380
|
+
// Check cache for this flow
|
|
381
|
+
const cached = await (0, _storage.getCachedFlow)(slug);
|
|
382
|
+
if (this.config.debug) {
|
|
383
|
+
console.log(`[Mobana] Starting flow: ${slug}`, {
|
|
384
|
+
hasCached: !!cached,
|
|
385
|
+
cachedVersionId: cached?.versionId
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Fetch flow from server (with cache validation)
|
|
390
|
+
const response = await (0, _api.fetchFlow)(endpoint, this.config.appKey, slug, installId, cached?.versionId, DEFAULT_TIMEOUT, this.config.debug);
|
|
391
|
+
|
|
392
|
+
// Handle network error
|
|
393
|
+
if (!response) {
|
|
394
|
+
// If we have a cached version, use it
|
|
395
|
+
if (cached) {
|
|
396
|
+
if (this.config.debug) {
|
|
397
|
+
console.log('[Mobana] Network error, using cached flow');
|
|
398
|
+
}
|
|
399
|
+
return this.presentFlowToUser(flowContext, {
|
|
400
|
+
slug,
|
|
401
|
+
config: cached,
|
|
402
|
+
installId,
|
|
403
|
+
endpoint,
|
|
404
|
+
appKey: this.config.appKey,
|
|
405
|
+
options
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
return {
|
|
409
|
+
completed: false,
|
|
410
|
+
dismissed: true,
|
|
411
|
+
error: 'NETWORK_ERROR'
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Handle server errors
|
|
416
|
+
if (response.error) {
|
|
417
|
+
if (this.config.debug) {
|
|
418
|
+
console.log(`[Mobana] Flow error: ${response.error}`);
|
|
419
|
+
}
|
|
420
|
+
return {
|
|
421
|
+
completed: false,
|
|
422
|
+
dismissed: true,
|
|
423
|
+
error: response.error
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Determine flow content to use
|
|
428
|
+
let flowConfig;
|
|
429
|
+
if (response.cached && cached) {
|
|
430
|
+
// Server confirmed our cached version is current
|
|
431
|
+
flowConfig = cached;
|
|
432
|
+
if (this.config.debug) {
|
|
433
|
+
console.log('[Mobana] Using cached flow (validated)');
|
|
434
|
+
}
|
|
435
|
+
} else if (response.versionId && response.html) {
|
|
436
|
+
// New content from server
|
|
437
|
+
flowConfig = {
|
|
438
|
+
versionId: response.versionId,
|
|
439
|
+
html: response.html,
|
|
440
|
+
css: response.css,
|
|
441
|
+
js: response.js
|
|
442
|
+
};
|
|
443
|
+
// Cache for next time
|
|
444
|
+
await (0, _storage.setCachedFlow)(slug, flowConfig);
|
|
445
|
+
if (this.config.debug) {
|
|
446
|
+
console.log('[Mobana] Using fresh flow, cached for next time');
|
|
447
|
+
}
|
|
448
|
+
} else {
|
|
449
|
+
// Unexpected response
|
|
450
|
+
if (this.config.debug) {
|
|
451
|
+
console.log('[Mobana] Unexpected flow response');
|
|
452
|
+
}
|
|
453
|
+
return {
|
|
454
|
+
completed: false,
|
|
455
|
+
dismissed: true,
|
|
456
|
+
error: 'SERVER_ERROR'
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Present the flow
|
|
461
|
+
return this.presentFlowToUser(flowContext, {
|
|
462
|
+
slug,
|
|
463
|
+
config: flowConfig,
|
|
464
|
+
installId,
|
|
465
|
+
endpoint,
|
|
466
|
+
appKey: this.config.appKey,
|
|
467
|
+
options
|
|
468
|
+
});
|
|
469
|
+
} catch (error) {
|
|
470
|
+
if (this.config.debug) {
|
|
471
|
+
console.log('[Mobana] Error starting flow:', error);
|
|
472
|
+
}
|
|
473
|
+
return {
|
|
474
|
+
completed: false,
|
|
475
|
+
dismissed: true,
|
|
476
|
+
error: 'SERVER_ERROR'
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Prefetch a flow for faster display later
|
|
483
|
+
*
|
|
484
|
+
* Downloads and caches the flow content without displaying it.
|
|
485
|
+
* Call this ahead of time if you know a flow will be shown soon.
|
|
486
|
+
*
|
|
487
|
+
* @param slug - Flow identifier (from dashboard)
|
|
488
|
+
*
|
|
489
|
+
* @example
|
|
490
|
+
* ```typescript
|
|
491
|
+
* // Prefetch during app startup
|
|
492
|
+
* Mobana.prefetchFlow('onboarding');
|
|
493
|
+
*
|
|
494
|
+
* // Later, when ready to show (will be instant if prefetched)
|
|
495
|
+
* const result = await Mobana.startFlow('onboarding');
|
|
496
|
+
* ```
|
|
497
|
+
*/
|
|
498
|
+
async prefetchFlow(slug) {
|
|
499
|
+
if (!this.isConfigured || !this.config) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
if (!this.config.enabled) {
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
try {
|
|
506
|
+
const endpoint = this.getEndpoint();
|
|
507
|
+
const installId = await (0, _storage.getInstallId)();
|
|
508
|
+
const cached = await (0, _storage.getCachedFlow)(slug);
|
|
509
|
+
if (this.config.debug) {
|
|
510
|
+
console.log(`[Mobana] Prefetching flow: ${slug}`);
|
|
511
|
+
}
|
|
512
|
+
const response = await (0, _api.fetchFlow)(endpoint, this.config.appKey, slug, installId, cached?.versionId, DEFAULT_TIMEOUT, this.config.debug);
|
|
513
|
+
if (response && !response.error && !response.cached && response.versionId && response.html) {
|
|
514
|
+
// Cache the new content
|
|
515
|
+
await (0, _storage.setCachedFlow)(slug, {
|
|
516
|
+
versionId: response.versionId,
|
|
517
|
+
html: response.html,
|
|
518
|
+
css: response.css,
|
|
519
|
+
js: response.js
|
|
520
|
+
});
|
|
521
|
+
if (this.config.debug) {
|
|
522
|
+
console.log(`[Mobana] Flow "${slug}" prefetched and cached`);
|
|
523
|
+
}
|
|
524
|
+
} else if (response?.cached) {
|
|
525
|
+
if (this.config.debug) {
|
|
526
|
+
console.log(`[Mobana] Flow "${slug}" already cached and current`);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} catch (error) {
|
|
530
|
+
if (this.config.debug) {
|
|
531
|
+
console.log('[Mobana] Error prefetching flow:', error);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* Present a flow to the user via the provider
|
|
538
|
+
*/
|
|
539
|
+
presentFlowToUser(flowContext, params) {
|
|
540
|
+
return new Promise(resolve => {
|
|
541
|
+
flowContext.presentFlow({
|
|
542
|
+
slug: params.slug,
|
|
543
|
+
config: params.config,
|
|
544
|
+
installId: params.installId,
|
|
545
|
+
endpoint: params.endpoint,
|
|
546
|
+
appKey: params.appKey,
|
|
547
|
+
attribution: this.cachedAttribution,
|
|
548
|
+
options: params.options,
|
|
549
|
+
resolve,
|
|
550
|
+
debug: this.config?.debug
|
|
551
|
+
});
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// ============================================
|
|
556
|
+
// Private methods
|
|
557
|
+
// ============================================
|
|
558
|
+
|
|
559
|
+
getEndpoint() {
|
|
560
|
+
if (this.config?.endpoint) {
|
|
561
|
+
// Remove trailing slash
|
|
562
|
+
return this.config.endpoint.replace(/\/$/, '');
|
|
563
|
+
}
|
|
564
|
+
if (this.config?.appId) {
|
|
565
|
+
return DEFAULT_ENDPOINT.replace('{appId}', this.config.appId);
|
|
566
|
+
}
|
|
567
|
+
throw new Error('No endpoint configured');
|
|
568
|
+
}
|
|
569
|
+
async fetchAttribution(options) {
|
|
570
|
+
const {
|
|
571
|
+
timeout = DEFAULT_TIMEOUT
|
|
572
|
+
} = options;
|
|
573
|
+
try {
|
|
574
|
+
const endpoint = this.getEndpoint();
|
|
575
|
+
const installId = await (0, _storage.getInstallId)();
|
|
576
|
+
const deviceInfo = (0, _device.getDeviceInfo)();
|
|
577
|
+
if (this.config?.debug) {
|
|
578
|
+
console.log('[Mobana] Fetching attribution...');
|
|
579
|
+
console.log('[Mobana] Device info:', deviceInfo);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Get Android Install Referrer for deterministic attribution
|
|
583
|
+
let dacid = null;
|
|
584
|
+
if (deviceInfo.platform === 'android') {
|
|
585
|
+
dacid = await (0, _NativeMobana.getInstallReferrer)();
|
|
586
|
+
if (this.config?.debug) {
|
|
587
|
+
console.log('[Mobana] Install Referrer dacid:', dacid || '(not available)');
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
// Make API request
|
|
592
|
+
const response = await (0, _api.findAttribution)(endpoint, this.config.appKey, installId, deviceInfo, dacid, timeout, this.config?.debug ?? false);
|
|
593
|
+
|
|
594
|
+
// If no response (network error, timeout), don't cache - allow retry
|
|
595
|
+
if (!response) {
|
|
596
|
+
if (this.config?.debug) {
|
|
597
|
+
console.log('[Mobana] No response from server');
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// Cache the response if server returned a valid response with matched key
|
|
603
|
+
// This prevents retrying on every startup
|
|
604
|
+
if (typeof response.matched === 'boolean') {
|
|
605
|
+
if (response.matched && response.attribution) {
|
|
606
|
+
// Build attribution object
|
|
607
|
+
const attribution = {
|
|
608
|
+
...response.attribution,
|
|
609
|
+
confidence: response.confidence ?? 0
|
|
610
|
+
};
|
|
611
|
+
|
|
612
|
+
// Cache matched result
|
|
613
|
+
await (0, _storage.setCachedResult)(true, attribution);
|
|
614
|
+
if (this.config?.debug) {
|
|
615
|
+
console.log('[Mobana] Attribution matched:', attribution);
|
|
616
|
+
}
|
|
617
|
+
return attribution;
|
|
618
|
+
} else {
|
|
619
|
+
// Cache unmatched result - prevents retry on next startup
|
|
620
|
+
await (0, _storage.setCachedResult)(false);
|
|
621
|
+
if (this.config?.debug) {
|
|
622
|
+
console.log('[Mobana] No match found (cached)');
|
|
623
|
+
}
|
|
624
|
+
return null;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Unexpected response format
|
|
629
|
+
if (this.config?.debug) {
|
|
630
|
+
console.log('[Mobana] Unexpected response format');
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
633
|
+
} catch (error) {
|
|
634
|
+
if (this.config?.debug) {
|
|
635
|
+
console.log('[Mobana] Error fetching attribution:', error);
|
|
636
|
+
}
|
|
637
|
+
return null;
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async sendConversion(event) {
|
|
641
|
+
try {
|
|
642
|
+
const endpoint = this.getEndpoint();
|
|
643
|
+
return await (0, _api.trackConversionApi)(endpoint, this.config.appKey, event, this.config?.debug ?? false);
|
|
644
|
+
} catch {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
async flushConversionQueue() {
|
|
649
|
+
if (!this.config?.enabled) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
const queue = await (0, _storage.getConversionQueue)();
|
|
653
|
+
if (queue.length === 0) {
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
if (this.config?.debug) {
|
|
657
|
+
console.log(`[Mobana] Flushing ${queue.length} queued conversions`);
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Send all queued conversions
|
|
661
|
+
const results = await Promise.all(queue.map(event => this.sendConversion(event)));
|
|
662
|
+
|
|
663
|
+
// Clear the queue, then re-queue only the failures (avoids duplicate sends)
|
|
664
|
+
await (0, _storage.clearConversionQueue)();
|
|
665
|
+
const failed = queue.filter((_, i) => !results[i]);
|
|
666
|
+
for (const event of failed) {
|
|
667
|
+
await (0, _storage.queueConversion)(event);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
// Export class for testing (create fresh instances without shared state)
|
|
673
|
+
exports.MobanaSDK = MobanaSDK;
|
|
674
|
+
// Export singleton instance
|
|
675
|
+
const Mobana = exports.Mobana = new MobanaSDK();
|
|
676
|
+
//# sourceMappingURL=Mobana.js.map
|