@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.
Files changed (96) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +249 -0
  3. package/android/build.gradle +50 -0
  4. package/android/src/main/AndroidManifest.xml +6 -0
  5. package/android/src/main/java/ai/mobana/sdk/MobanaModule.kt +67 -0
  6. package/android/src/main/java/ai/mobana/sdk/MobanaPackage.kt +19 -0
  7. package/app.plugin.js +274 -0
  8. package/ios/Mobana.h +11 -0
  9. package/ios/Mobana.m +20 -0
  10. package/lib/commonjs/Mobana.js +676 -0
  11. package/lib/commonjs/Mobana.js.map +1 -0
  12. package/lib/commonjs/NativeMobana.js +53 -0
  13. package/lib/commonjs/NativeMobana.js.map +1 -0
  14. package/lib/commonjs/api.js +201 -0
  15. package/lib/commonjs/api.js.map +1 -0
  16. package/lib/commonjs/bridge/index.js +19 -0
  17. package/lib/commonjs/bridge/index.js.map +1 -0
  18. package/lib/commonjs/bridge/injectBridge.js +528 -0
  19. package/lib/commonjs/bridge/injectBridge.js.map +1 -0
  20. package/lib/commonjs/components/FlowWebView.js +676 -0
  21. package/lib/commonjs/components/FlowWebView.js.map +1 -0
  22. package/lib/commonjs/components/MobanaProvider.js +275 -0
  23. package/lib/commonjs/components/MobanaProvider.js.map +1 -0
  24. package/lib/commonjs/components/index.js +20 -0
  25. package/lib/commonjs/components/index.js.map +1 -0
  26. package/lib/commonjs/device.js +49 -0
  27. package/lib/commonjs/device.js.map +1 -0
  28. package/lib/commonjs/index.js +20 -0
  29. package/lib/commonjs/index.js.map +1 -0
  30. package/lib/commonjs/package.json +1 -0
  31. package/lib/commonjs/storage.js +277 -0
  32. package/lib/commonjs/storage.js.map +1 -0
  33. package/lib/commonjs/types.js +2 -0
  34. package/lib/commonjs/types.js.map +1 -0
  35. package/lib/module/Mobana.js +673 -0
  36. package/lib/module/Mobana.js.map +1 -0
  37. package/lib/module/NativeMobana.js +49 -0
  38. package/lib/module/NativeMobana.js.map +1 -0
  39. package/lib/module/api.js +194 -0
  40. package/lib/module/api.js.map +1 -0
  41. package/lib/module/bridge/index.js +4 -0
  42. package/lib/module/bridge/index.js.map +1 -0
  43. package/lib/module/bridge/injectBridge.js +523 -0
  44. package/lib/module/bridge/injectBridge.js.map +1 -0
  45. package/lib/module/components/FlowWebView.js +672 -0
  46. package/lib/module/components/FlowWebView.js.map +1 -0
  47. package/lib/module/components/MobanaProvider.js +270 -0
  48. package/lib/module/components/MobanaProvider.js.map +1 -0
  49. package/lib/module/components/index.js +5 -0
  50. package/lib/module/components/index.js.map +1 -0
  51. package/lib/module/device.js +45 -0
  52. package/lib/module/device.js.map +1 -0
  53. package/lib/module/index.js +53 -0
  54. package/lib/module/index.js.map +1 -0
  55. package/lib/module/storage.js +257 -0
  56. package/lib/module/storage.js.map +1 -0
  57. package/lib/module/types.js +2 -0
  58. package/lib/module/types.js.map +1 -0
  59. package/lib/typescript/Mobana.d.ts +209 -0
  60. package/lib/typescript/Mobana.d.ts.map +1 -0
  61. package/lib/typescript/NativeMobana.d.ts +11 -0
  62. package/lib/typescript/NativeMobana.d.ts.map +1 -0
  63. package/lib/typescript/api.d.ts +34 -0
  64. package/lib/typescript/api.d.ts.map +1 -0
  65. package/lib/typescript/bridge/index.d.ts +3 -0
  66. package/lib/typescript/bridge/index.d.ts.map +1 -0
  67. package/lib/typescript/bridge/injectBridge.d.ts +23 -0
  68. package/lib/typescript/bridge/injectBridge.d.ts.map +1 -0
  69. package/lib/typescript/components/FlowWebView.d.ts +38 -0
  70. package/lib/typescript/components/FlowWebView.d.ts.map +1 -0
  71. package/lib/typescript/components/MobanaProvider.d.ts +65 -0
  72. package/lib/typescript/components/MobanaProvider.d.ts.map +1 -0
  73. package/lib/typescript/components/index.d.ts +5 -0
  74. package/lib/typescript/components/index.d.ts.map +1 -0
  75. package/lib/typescript/device.d.ts +6 -0
  76. package/lib/typescript/device.d.ts.map +1 -0
  77. package/lib/typescript/index.d.ts +46 -0
  78. package/lib/typescript/index.d.ts.map +1 -0
  79. package/lib/typescript/storage.d.ts +68 -0
  80. package/lib/typescript/storage.d.ts.map +1 -0
  81. package/lib/typescript/types.d.ts +298 -0
  82. package/lib/typescript/types.d.ts.map +1 -0
  83. package/mobana.podspec +19 -0
  84. package/package.json +131 -0
  85. package/src/Mobana.ts +742 -0
  86. package/src/NativeMobana.ts +61 -0
  87. package/src/api.ts +259 -0
  88. package/src/bridge/index.ts +2 -0
  89. package/src/bridge/injectBridge.ts +542 -0
  90. package/src/components/FlowWebView.tsx +826 -0
  91. package/src/components/MobanaProvider.tsx +393 -0
  92. package/src/components/index.ts +4 -0
  93. package/src/device.ts +42 -0
  94. package/src/index.ts +66 -0
  95. package/src/storage.ts +262 -0
  96. 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
+ }