@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/src/storage.ts
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
2
|
+
import type { Attribution, CachedAttributionResult, ConversionEvent, CachedFlow } from './types';
|
|
3
|
+
|
|
4
|
+
const KEYS = {
|
|
5
|
+
INSTALL_ID: '@mobana:install_id',
|
|
6
|
+
ATTRIBUTION: '@mobana:attribution',
|
|
7
|
+
CONVERSION_QUEUE: '@mobana:conversion_queue',
|
|
8
|
+
LOCAL_DATA: '@mobana:local_data',
|
|
9
|
+
} as const;
|
|
10
|
+
|
|
11
|
+
const FLOW_CACHE_PREFIX = '@mobana:flow:';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Get or create a stable install ID (UUID)
|
|
15
|
+
* Generated once on first launch and persisted locally
|
|
16
|
+
*/
|
|
17
|
+
export async function getInstallId(): Promise<string> {
|
|
18
|
+
try {
|
|
19
|
+
let installId = await AsyncStorage.getItem(KEYS.INSTALL_ID);
|
|
20
|
+
|
|
21
|
+
if (!installId) {
|
|
22
|
+
// Generate UUID v4
|
|
23
|
+
installId = generateUUID();
|
|
24
|
+
await AsyncStorage.setItem(KEYS.INSTALL_ID, installId);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return installId;
|
|
28
|
+
} catch {
|
|
29
|
+
// If storage fails, generate a new UUID each time
|
|
30
|
+
// This is suboptimal but ensures the SDK doesn't crash
|
|
31
|
+
return generateUUID();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Get cached attribution result (includes matched: false responses)
|
|
37
|
+
*/
|
|
38
|
+
export async function getCachedResult<T = Record<string, unknown>>(): Promise<CachedAttributionResult<T> | null> {
|
|
39
|
+
try {
|
|
40
|
+
const data = await AsyncStorage.getItem(KEYS.ATTRIBUTION);
|
|
41
|
+
if (data) {
|
|
42
|
+
return JSON.parse(data) as CachedAttributionResult<T>;
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
} catch {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Store attribution result in cache (stores both matched and unmatched responses)
|
|
52
|
+
*/
|
|
53
|
+
export async function setCachedResult<T = Record<string, unknown>>(
|
|
54
|
+
matched: boolean,
|
|
55
|
+
attribution?: Attribution<T>
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
try {
|
|
58
|
+
const result: CachedAttributionResult<T> = {
|
|
59
|
+
matched,
|
|
60
|
+
attribution,
|
|
61
|
+
checkedAt: Date.now(),
|
|
62
|
+
};
|
|
63
|
+
await AsyncStorage.setItem(KEYS.ATTRIBUTION, JSON.stringify(result));
|
|
64
|
+
} catch {
|
|
65
|
+
// Silently fail - caching is not critical
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Clear all stored attribution data (for testing/reset)
|
|
71
|
+
*/
|
|
72
|
+
export async function clearAttribution(): Promise<void> {
|
|
73
|
+
try {
|
|
74
|
+
await AsyncStorage.multiRemove([KEYS.ATTRIBUTION, KEYS.INSTALL_ID]);
|
|
75
|
+
} catch {
|
|
76
|
+
// Silently fail
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Queue a conversion event for later sending (offline support)
|
|
82
|
+
*/
|
|
83
|
+
export async function queueConversion(event: ConversionEvent): Promise<void> {
|
|
84
|
+
try {
|
|
85
|
+
const queue = await getConversionQueue();
|
|
86
|
+
queue.push(event);
|
|
87
|
+
await AsyncStorage.setItem(KEYS.CONVERSION_QUEUE, JSON.stringify(queue));
|
|
88
|
+
} catch {
|
|
89
|
+
// Silently fail - we tried our best
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get all queued conversion events
|
|
95
|
+
*/
|
|
96
|
+
export async function getConversionQueue(): Promise<ConversionEvent[]> {
|
|
97
|
+
try {
|
|
98
|
+
const data = await AsyncStorage.getItem(KEYS.CONVERSION_QUEUE);
|
|
99
|
+
if (data) {
|
|
100
|
+
return JSON.parse(data) as ConversionEvent[];
|
|
101
|
+
}
|
|
102
|
+
return [];
|
|
103
|
+
} catch {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Clear the conversion queue after successful send
|
|
110
|
+
*/
|
|
111
|
+
export async function clearConversionQueue(): Promise<void> {
|
|
112
|
+
try {
|
|
113
|
+
await AsyncStorage.removeItem(KEYS.CONVERSION_QUEUE);
|
|
114
|
+
} catch {
|
|
115
|
+
// Silently fail
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Generate a UUID v4
|
|
121
|
+
* Uses crypto.getRandomValues when available (Hermes 0.73+), falls back to Math.random
|
|
122
|
+
*/
|
|
123
|
+
export function generateUUID(): string {
|
|
124
|
+
// Check if crypto.getRandomValues is available (Hermes 0.73+, modern RN)
|
|
125
|
+
const cryptoObj = typeof globalThis !== 'undefined' && (globalThis as typeof globalThis & { crypto?: { getRandomValues: (array: Uint8Array) => Uint8Array } }).crypto;
|
|
126
|
+
if (cryptoObj && typeof cryptoObj.getRandomValues === 'function') {
|
|
127
|
+
try {
|
|
128
|
+
const bytes = new Uint8Array(16);
|
|
129
|
+
cryptoObj.getRandomValues(bytes);
|
|
130
|
+
bytes[6] = (bytes[6] & 0x0f) | 0x40; // version 4
|
|
131
|
+
bytes[8] = (bytes[8] & 0x3f) | 0x80; // variant 1
|
|
132
|
+
const hex = Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
133
|
+
return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`;
|
|
134
|
+
} catch {
|
|
135
|
+
// Fall through to Math.random fallback
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Fallback for environments without crypto.getRandomValues
|
|
139
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
|
|
140
|
+
const r = (Math.random() * 16) | 0;
|
|
141
|
+
const v = c === 'x' ? r : (r & 0x3) | 0x8;
|
|
142
|
+
return v.toString(16);
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ============================================
|
|
147
|
+
// Flow Caching
|
|
148
|
+
// ============================================
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Get cached flow content by slug
|
|
152
|
+
*/
|
|
153
|
+
export async function getCachedFlow(slug: string): Promise<CachedFlow | null> {
|
|
154
|
+
try {
|
|
155
|
+
const key = `${FLOW_CACHE_PREFIX}${slug}`;
|
|
156
|
+
const data = await AsyncStorage.getItem(key);
|
|
157
|
+
if (data) {
|
|
158
|
+
return JSON.parse(data) as CachedFlow;
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
} catch {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Cache flow content
|
|
168
|
+
*/
|
|
169
|
+
export async function setCachedFlow(slug: string, flow: Omit<CachedFlow, 'cachedAt'>): Promise<void> {
|
|
170
|
+
try {
|
|
171
|
+
const key = `${FLOW_CACHE_PREFIX}${slug}`;
|
|
172
|
+
const cached: CachedFlow = {
|
|
173
|
+
...flow,
|
|
174
|
+
cachedAt: Date.now(),
|
|
175
|
+
};
|
|
176
|
+
await AsyncStorage.setItem(key, JSON.stringify(cached));
|
|
177
|
+
} catch {
|
|
178
|
+
// Silently fail - caching is not critical
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Clear cached flow by slug
|
|
184
|
+
*/
|
|
185
|
+
export async function clearCachedFlow(slug: string): Promise<void> {
|
|
186
|
+
try {
|
|
187
|
+
const key = `${FLOW_CACHE_PREFIX}${slug}`;
|
|
188
|
+
await AsyncStorage.removeItem(key);
|
|
189
|
+
} catch {
|
|
190
|
+
// Silently fail
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Clear all cached flows
|
|
196
|
+
*/
|
|
197
|
+
export async function clearAllCachedFlows(): Promise<void> {
|
|
198
|
+
try {
|
|
199
|
+
const allKeys = await AsyncStorage.getAllKeys();
|
|
200
|
+
const flowKeys = allKeys.filter(key => key.startsWith(FLOW_CACHE_PREFIX));
|
|
201
|
+
if (flowKeys.length > 0) {
|
|
202
|
+
await AsyncStorage.multiRemove(flowKeys);
|
|
203
|
+
}
|
|
204
|
+
} catch {
|
|
205
|
+
// Silently fail
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ============================================
|
|
210
|
+
// Local Data (for flow bridge)
|
|
211
|
+
// ============================================
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Get all local data
|
|
215
|
+
*/
|
|
216
|
+
export async function getAllLocalData(): Promise<Record<string, unknown>> {
|
|
217
|
+
try {
|
|
218
|
+
const data = await AsyncStorage.getItem(KEYS.LOCAL_DATA);
|
|
219
|
+
if (data) {
|
|
220
|
+
return JSON.parse(data) as Record<string, unknown>;
|
|
221
|
+
}
|
|
222
|
+
return {};
|
|
223
|
+
} catch {
|
|
224
|
+
return {};
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Store data locally on device (persists across app sessions)
|
|
230
|
+
*/
|
|
231
|
+
export async function setLocalData(key: string, value: unknown): Promise<void> {
|
|
232
|
+
try {
|
|
233
|
+
const data = await getAllLocalData();
|
|
234
|
+
data[key] = value;
|
|
235
|
+
await AsyncStorage.setItem(KEYS.LOCAL_DATA, JSON.stringify(data));
|
|
236
|
+
} catch {
|
|
237
|
+
// Silently fail
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Retrieve locally stored data
|
|
243
|
+
*/
|
|
244
|
+
export async function getLocalData(key: string): Promise<unknown> {
|
|
245
|
+
try {
|
|
246
|
+
const data = await getAllLocalData();
|
|
247
|
+
return data[key];
|
|
248
|
+
} catch {
|
|
249
|
+
return undefined;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Clear all local data
|
|
255
|
+
*/
|
|
256
|
+
export async function clearLocalData(): Promise<void> {
|
|
257
|
+
try {
|
|
258
|
+
await AsyncStorage.removeItem(KEYS.LOCAL_DATA);
|
|
259
|
+
} catch {
|
|
260
|
+
// Silently fail
|
|
261
|
+
}
|
|
262
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration options for Mobana SDK
|
|
3
|
+
*/
|
|
4
|
+
export interface MobanaConfig {
|
|
5
|
+
/**
|
|
6
|
+
* Your Mobana app ID from the dashboard (e.g., 'a1b2c3d4')
|
|
7
|
+
* Required.
|
|
8
|
+
*/
|
|
9
|
+
appId: string;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Your Mobana app key from the dashboard. Sent as X-App-Key header.
|
|
13
|
+
* Required. Regenerate from dashboard if leaked.
|
|
14
|
+
*/
|
|
15
|
+
appKey: string;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Custom endpoint URL for attribution API
|
|
19
|
+
* Optional. Use this if proxying through your own domain (e.g., 'https://myapp.com/d')
|
|
20
|
+
* If not set, defaults to https://{appId}.mobana.ai
|
|
21
|
+
*/
|
|
22
|
+
endpoint?: string;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Enable or disable the SDK (default: true)
|
|
26
|
+
* Set to false to disable all attribution tracking (e.g., for GDPR opt-out)
|
|
27
|
+
*/
|
|
28
|
+
enabled?: boolean;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Enable debug logging (default: false)
|
|
32
|
+
* When true, logs SDK operations to console
|
|
33
|
+
*/
|
|
34
|
+
debug?: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// ============================================
|
|
38
|
+
// Flow Types
|
|
39
|
+
// ============================================
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Flow content returned from the API
|
|
43
|
+
*/
|
|
44
|
+
export interface FlowConfig {
|
|
45
|
+
/** Unique version identifier (immutable) */
|
|
46
|
+
versionId: string;
|
|
47
|
+
/** HTML content of the flow */
|
|
48
|
+
html: string;
|
|
49
|
+
/** CSS styles (may be embedded in HTML) */
|
|
50
|
+
css?: string;
|
|
51
|
+
/** JavaScript code (may be embedded in HTML) */
|
|
52
|
+
js?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Result returned from startFlow()
|
|
57
|
+
*/
|
|
58
|
+
export interface FlowResult {
|
|
59
|
+
/** Whether the flow was completed (user called Mobana.complete()) */
|
|
60
|
+
completed: boolean;
|
|
61
|
+
/** Whether the flow was dismissed (user called Mobana.dismiss()) */
|
|
62
|
+
dismissed: boolean;
|
|
63
|
+
/** Error code if flow couldn't be shown */
|
|
64
|
+
error?: FlowError;
|
|
65
|
+
/** Custom data passed to Mobana.complete(data) */
|
|
66
|
+
data?: Record<string, unknown>;
|
|
67
|
+
/** Session ID for this flow presentation (use with trackConversion's flowSessionId) */
|
|
68
|
+
sessionId?: string;
|
|
69
|
+
/**
|
|
70
|
+
* Track a custom event for this flow session after the flow has closed.
|
|
71
|
+
* Useful for tracking events that happen after the flow (e.g., purchase after onboarding).
|
|
72
|
+
*
|
|
73
|
+
* @param event - Event name (e.g., 'purchase_completed', 'feature_used')
|
|
74
|
+
* @param data - Optional event data (will be stored as JSON)
|
|
75
|
+
* @returns Promise resolving to true if event was tracked successfully
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```typescript
|
|
79
|
+
* const result = await Mobana.startFlow('pre-purchase');
|
|
80
|
+
* // ... user makes purchase via Adapty ...
|
|
81
|
+
* await result.trackEvent('purchase_initiated');
|
|
82
|
+
* ```
|
|
83
|
+
*/
|
|
84
|
+
trackEvent?: (event: string, data?: Record<string, unknown>) => Promise<boolean>;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Possible flow error codes
|
|
89
|
+
*/
|
|
90
|
+
export type FlowError =
|
|
91
|
+
| 'NOT_FOUND' // Flow doesn't exist or is disabled
|
|
92
|
+
| 'PLAN_REQUIRED' // App's plan doesn't include flows
|
|
93
|
+
| 'FLOW_LIMIT_EXCEEDED' // Flow view quota exceeded (free plan hard limit)
|
|
94
|
+
| 'NETWORK_ERROR' // Network request failed
|
|
95
|
+
| 'SERVER_ERROR' // Server returned an error
|
|
96
|
+
| 'PROVIDER_NOT_MOUNTED' // MobanaProvider not in component tree
|
|
97
|
+
| 'SDK_NOT_CONFIGURED'; // SDK.init() not called
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Options for startFlow()
|
|
101
|
+
*/
|
|
102
|
+
export interface FlowOptions {
|
|
103
|
+
/**
|
|
104
|
+
* Custom parameters available in the flow via Mobana.getParams()
|
|
105
|
+
* Use this to pass context to your flow (e.g., user name, feature flags)
|
|
106
|
+
*/
|
|
107
|
+
params?: Record<string, unknown>;
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Callback when flow emits custom events via Mobana.trackEvent()
|
|
111
|
+
* Useful for analytics integration
|
|
112
|
+
*/
|
|
113
|
+
onEvent?: (event: string) => void;
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Async callback invoked when the flow calls Mobana.requestCallback(data).
|
|
117
|
+
* Allows the flow to request the app to perform an action (e.g., trigger a purchase,
|
|
118
|
+
* validate a promo code) and await the result — without closing the flow.
|
|
119
|
+
*
|
|
120
|
+
* @param data - Arbitrary data sent from the flow
|
|
121
|
+
* @returns Promise resolving to arbitrary data returned to the flow
|
|
122
|
+
*
|
|
123
|
+
* @example
|
|
124
|
+
* ```typescript
|
|
125
|
+
* const result = await Mobana.startFlow('paywall', {
|
|
126
|
+
* onCallback: async (data) => {
|
|
127
|
+
* if (data.action === 'purchase') {
|
|
128
|
+
* const purchase = await purchaseManager.buy(data.planId);
|
|
129
|
+
* return { success: purchase.success, receipt: purchase.receipt };
|
|
130
|
+
* }
|
|
131
|
+
* return { error: 'Unknown action' };
|
|
132
|
+
* },
|
|
133
|
+
* });
|
|
134
|
+
* ```
|
|
135
|
+
*/
|
|
136
|
+
onCallback?: (data: Record<string, unknown>) => Promise<Record<string, unknown>>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Cached flow content (stored in AsyncStorage)
|
|
141
|
+
*/
|
|
142
|
+
export interface CachedFlow {
|
|
143
|
+
versionId: string;
|
|
144
|
+
html: string;
|
|
145
|
+
css?: string;
|
|
146
|
+
js?: string;
|
|
147
|
+
cachedAt: number;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* API response from GET /api/flows/[slug]
|
|
152
|
+
*/
|
|
153
|
+
export interface FlowFetchResponse {
|
|
154
|
+
/** True if client's cached version is still current */
|
|
155
|
+
cached?: boolean;
|
|
156
|
+
/** Version ID (always present if not an error) */
|
|
157
|
+
versionId?: string;
|
|
158
|
+
/** HTML content (present if not cached) */
|
|
159
|
+
html?: string;
|
|
160
|
+
/** CSS content (present if not cached) */
|
|
161
|
+
css?: string;
|
|
162
|
+
/** JS content (present if not cached) */
|
|
163
|
+
js?: string;
|
|
164
|
+
/** Error code if flow unavailable */
|
|
165
|
+
error?: string;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Flow event to send to the server
|
|
170
|
+
*/
|
|
171
|
+
export interface FlowEvent {
|
|
172
|
+
installId: string;
|
|
173
|
+
versionId: string;
|
|
174
|
+
sessionId: string;
|
|
175
|
+
event: string;
|
|
176
|
+
step?: number;
|
|
177
|
+
data?: unknown;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================
|
|
181
|
+
// Bridge Types (for WebView communication)
|
|
182
|
+
// ============================================
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Haptic feedback styles
|
|
186
|
+
*/
|
|
187
|
+
export type HapticStyle =
|
|
188
|
+
| 'light'
|
|
189
|
+
| 'medium'
|
|
190
|
+
| 'heavy'
|
|
191
|
+
| 'success'
|
|
192
|
+
| 'warning'
|
|
193
|
+
| 'error'
|
|
194
|
+
| 'selection';
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Location permission status
|
|
198
|
+
*/
|
|
199
|
+
export type LocationPermissionStatus =
|
|
200
|
+
| 'granted'
|
|
201
|
+
| 'denied'
|
|
202
|
+
| 'blocked'
|
|
203
|
+
| 'unavailable'
|
|
204
|
+
| 'limited';
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* ATT (App Tracking Transparency) status (iOS only)
|
|
208
|
+
*/
|
|
209
|
+
export type ATTStatus =
|
|
210
|
+
| 'authorized'
|
|
211
|
+
| 'denied'
|
|
212
|
+
| 'not-determined'
|
|
213
|
+
| 'restricted';
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Device color scheme (light/dark mode)
|
|
217
|
+
*/
|
|
218
|
+
export type ColorScheme = 'light' | 'dark';
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Location coordinates
|
|
222
|
+
*/
|
|
223
|
+
export interface LocationCoordinates {
|
|
224
|
+
latitude: number;
|
|
225
|
+
longitude: number;
|
|
226
|
+
accuracy: number;
|
|
227
|
+
altitude: number | null;
|
|
228
|
+
altitudeAccuracy: number | null;
|
|
229
|
+
heading: number | null;
|
|
230
|
+
speed: number | null;
|
|
231
|
+
timestamp: number;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Message from WebView to native
|
|
236
|
+
*/
|
|
237
|
+
export interface BridgeMessage {
|
|
238
|
+
type: string;
|
|
239
|
+
requestId?: number;
|
|
240
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
241
|
+
payload?: any;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Options for getAttribution call
|
|
246
|
+
*/
|
|
247
|
+
export interface GetAttributionOptions {
|
|
248
|
+
/**
|
|
249
|
+
* Timeout in milliseconds for the attribution request (default: 10000)
|
|
250
|
+
*/
|
|
251
|
+
timeout?: number;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Attribution data returned from the API
|
|
256
|
+
*/
|
|
257
|
+
export interface Attribution<T = Record<string, unknown>> {
|
|
258
|
+
/**
|
|
259
|
+
* Traffic source (e.g., 'facebook', 'google', 'tiktok')
|
|
260
|
+
*/
|
|
261
|
+
utm_source?: string;
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Marketing medium (e.g., 'cpc', 'social', 'email')
|
|
265
|
+
*/
|
|
266
|
+
utm_medium?: string;
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Campaign name
|
|
270
|
+
*/
|
|
271
|
+
utm_campaign?: string;
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Ad content identifier
|
|
275
|
+
*/
|
|
276
|
+
utm_content?: string;
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Search keywords
|
|
280
|
+
*/
|
|
281
|
+
utm_term?: string;
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Referring domain that sent the user to the tracking link (e.g., 'facebook.com')
|
|
285
|
+
* Only the domain is stored (not the full URL) for privacy.
|
|
286
|
+
*/
|
|
287
|
+
referrer_domain?: string;
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Custom deeplink data passed through the attribution flow
|
|
291
|
+
* Use generics for type-safe access: getAttribution<MyDataType>()
|
|
292
|
+
*/
|
|
293
|
+
data?: T;
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Match confidence score (0.0 - 1.0)
|
|
297
|
+
* - 1.0 = Deterministic match via Android Install Referrer
|
|
298
|
+
* - < 1.0 = Probabilistic match
|
|
299
|
+
*/
|
|
300
|
+
confidence: number;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Internal: API response from /find endpoint
|
|
305
|
+
*/
|
|
306
|
+
export interface FindResponse<T = Record<string, unknown>> {
|
|
307
|
+
matched: boolean;
|
|
308
|
+
attribution?: Attribution<T>;
|
|
309
|
+
confidence?: number;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* Internal: Cached attribution result (includes matched: false responses)
|
|
314
|
+
*/
|
|
315
|
+
export interface CachedAttributionResult<T = Record<string, unknown>> {
|
|
316
|
+
matched: boolean;
|
|
317
|
+
attribution?: Attribution<T>;
|
|
318
|
+
checkedAt: number;
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Internal: Conversion event to be sent or queued
|
|
323
|
+
*/
|
|
324
|
+
export interface ConversionEvent {
|
|
325
|
+
installId: string;
|
|
326
|
+
name: string;
|
|
327
|
+
value?: number;
|
|
328
|
+
timestamp: number;
|
|
329
|
+
/** Optional flow session ID to link conversion to a specific flow presentation */
|
|
330
|
+
flowSessionId?: string;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Internal: Device info collected for attribution matching
|
|
335
|
+
*/
|
|
336
|
+
export interface DeviceInfo {
|
|
337
|
+
platform: 'ios' | 'android';
|
|
338
|
+
timezone?: string;
|
|
339
|
+
screenWidth?: number;
|
|
340
|
+
screenHeight?: number;
|
|
341
|
+
language?: string;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/**
|
|
345
|
+
* Safe area insets for the device screen
|
|
346
|
+
* Represents the areas of the screen that may be obscured by system UI
|
|
347
|
+
* (status bar, notch/dynamic island, home indicator, etc.)
|
|
348
|
+
*/
|
|
349
|
+
export interface SafeArea {
|
|
350
|
+
/** Inset from top (status bar, notch, dynamic island) in points */
|
|
351
|
+
top: number;
|
|
352
|
+
/** Inset from bottom (home indicator, navigation bar) in points */
|
|
353
|
+
bottom: number;
|
|
354
|
+
/** Inset from left (typically 0, but can be non-zero in landscape) in points */
|
|
355
|
+
left: number;
|
|
356
|
+
/** Inset from right (typically 0, but can be non-zero in landscape) in points */
|
|
357
|
+
right: number;
|
|
358
|
+
/** Full screen width in points */
|
|
359
|
+
width: number;
|
|
360
|
+
/** Full screen height in points */
|
|
361
|
+
height: number;
|
|
362
|
+
}
|