@mobana/react-native-sdk 0.2.10 → 0.2.11
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/lib/commonjs/Mobana.js +126 -26
- package/lib/commonjs/Mobana.js.map +1 -1
- package/lib/commonjs/api.js +60 -1
- package/lib/commonjs/api.js.map +1 -1
- package/lib/module/Mobana.js +126 -26
- package/lib/module/Mobana.js.map +1 -1
- package/lib/module/api.js +60 -1
- package/lib/module/api.js.map +1 -1
- package/lib/typescript/Mobana.d.ts +19 -2
- package/lib/typescript/Mobana.d.ts.map +1 -1
- package/lib/typescript/api.d.ts +13 -1
- package/lib/typescript/api.d.ts.map +1 -1
- package/lib/typescript/types.d.ts +43 -0
- package/lib/typescript/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/Mobana.ts +83 -29
- package/src/api.ts +75 -2
- package/src/types.ts +46 -0
package/src/Mobana.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
MobanaConfig,
|
|
3
3
|
GetAttributionOptions,
|
|
4
4
|
Attribution,
|
|
5
|
+
AttributionResult,
|
|
5
6
|
ConversionEvent,
|
|
6
7
|
FlowResult,
|
|
7
8
|
FlowOptions,
|
|
@@ -67,10 +68,14 @@ class MobanaSDK {
|
|
|
67
68
|
private config: MobanaConfig | null = null;
|
|
68
69
|
private isConfigured = false;
|
|
69
70
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
70
|
-
private attributionPromise: Promise<
|
|
71
|
+
private attributionPromise: Promise<AttributionResult<any>> | null = null;
|
|
71
72
|
// In-memory cache for attribution (faster than AsyncStorage)
|
|
72
73
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
73
74
|
private cachedAttribution: Attribution<any> | null = null;
|
|
75
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
76
|
+
private cachedAttributionResult: AttributionResult<any> | null = null;
|
|
77
|
+
// Only true after a definitive server response (matched or no_match) — never on error.
|
|
78
|
+
// This allows retry on subsequent calls when the first attempt failed.
|
|
74
79
|
private attributionChecked = false;
|
|
75
80
|
|
|
76
81
|
/**
|
|
@@ -131,6 +136,13 @@ class MobanaSDK {
|
|
|
131
136
|
|
|
132
137
|
// Flush any queued conversions when SDK is initialized
|
|
133
138
|
await this.flushConversionQueue();
|
|
139
|
+
|
|
140
|
+
// Fire attribution in the background unless explicitly disabled.
|
|
141
|
+
// Non-blocking — init() resolves immediately after this.
|
|
142
|
+
// The result is cached so any subsequent getAttribution() call returns instantly.
|
|
143
|
+
if (this.config.autoAttribute !== false) {
|
|
144
|
+
this.getAttribution().catch(() => {});
|
|
145
|
+
}
|
|
134
146
|
}
|
|
135
147
|
|
|
136
148
|
/**
|
|
@@ -193,48 +205,61 @@ class MobanaSDK {
|
|
|
193
205
|
*/
|
|
194
206
|
async getAttribution<T = Record<string, unknown>>(
|
|
195
207
|
options: GetAttributionOptions = {}
|
|
196
|
-
): Promise<
|
|
208
|
+
): Promise<AttributionResult<T>> {
|
|
197
209
|
if (!this.isConfigured || !this.config) {
|
|
198
210
|
console.warn('[Mobana] SDK not configured. Call init() first.');
|
|
199
|
-
return null;
|
|
211
|
+
return { status: 'error', attribution: null, error: { type: 'sdk_not_configured' } };
|
|
200
212
|
}
|
|
201
213
|
|
|
202
214
|
if (!this.config.enabled) {
|
|
203
215
|
if (this.config.debug) {
|
|
204
|
-
console.log('[Mobana] SDK disabled,
|
|
216
|
+
console.log('[Mobana] SDK disabled, skipping attribution');
|
|
205
217
|
}
|
|
206
|
-
return null;
|
|
218
|
+
return { status: 'error', attribution: null, error: { type: 'sdk_disabled' } };
|
|
207
219
|
}
|
|
208
220
|
|
|
209
221
|
// Return in-memory cache if available (fastest)
|
|
210
222
|
if (this.attributionChecked) {
|
|
211
|
-
return this.
|
|
223
|
+
return this.cachedAttributionResult as AttributionResult<T>;
|
|
212
224
|
}
|
|
213
225
|
|
|
214
226
|
// Check AsyncStorage cache
|
|
215
227
|
const cached = await getCachedResult<T>();
|
|
228
|
+
|
|
229
|
+
// Re-check enabled after the async read — it may have changed (e.g. GDPR opt-out)
|
|
230
|
+
if (!this.config?.enabled) {
|
|
231
|
+
return { status: 'error', attribution: null, error: { type: 'sdk_disabled' } };
|
|
232
|
+
}
|
|
233
|
+
|
|
216
234
|
if (cached) {
|
|
217
235
|
if (this.config.debug) {
|
|
218
236
|
console.log('[Mobana] Returning cached result, matched:', cached.matched);
|
|
219
237
|
}
|
|
220
|
-
|
|
238
|
+
const result: AttributionResult<T> = cached.matched && cached.attribution
|
|
239
|
+
? { status: 'matched', attribution: cached.attribution as Attribution<T> }
|
|
240
|
+
: { status: 'no_match', attribution: null };
|
|
221
241
|
this.attributionChecked = true;
|
|
222
|
-
this.cachedAttribution =
|
|
223
|
-
|
|
242
|
+
this.cachedAttribution = result.attribution;
|
|
243
|
+
this.cachedAttributionResult = result;
|
|
244
|
+
return result;
|
|
224
245
|
}
|
|
225
246
|
|
|
226
|
-
// Prevent duplicate concurrent requests
|
|
247
|
+
// Prevent duplicate concurrent requests — both callers get the same promise
|
|
227
248
|
if (this.attributionPromise) {
|
|
228
|
-
return this.attributionPromise as Promise<
|
|
249
|
+
return this.attributionPromise as Promise<AttributionResult<T>>;
|
|
229
250
|
}
|
|
230
251
|
|
|
231
252
|
this.attributionPromise = this.fetchAttribution<T>(options);
|
|
232
253
|
const result = await this.attributionPromise;
|
|
233
254
|
this.attributionPromise = null;
|
|
234
255
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
256
|
+
// Only cache definitive results. Errors (network, timeout, server) leave
|
|
257
|
+
// attributionChecked = false so the next call retries automatically.
|
|
258
|
+
if (result.status !== 'error') {
|
|
259
|
+
this.attributionChecked = true;
|
|
260
|
+
this.cachedAttribution = result.attribution;
|
|
261
|
+
this.cachedAttributionResult = result;
|
|
262
|
+
}
|
|
238
263
|
|
|
239
264
|
return result;
|
|
240
265
|
}
|
|
@@ -307,6 +332,25 @@ class MobanaSDK {
|
|
|
307
332
|
}
|
|
308
333
|
}
|
|
309
334
|
|
|
335
|
+
/**
|
|
336
|
+
* Get the install ID for this device
|
|
337
|
+
*
|
|
338
|
+
* A random UUID generated on first launch and persisted locally.
|
|
339
|
+
* Useful for GDPR data access/deletion requests — this is the identifier
|
|
340
|
+
* Mobana uses server-side to associate attribution and conversion records.
|
|
341
|
+
*
|
|
342
|
+
* @returns The install ID string
|
|
343
|
+
*
|
|
344
|
+
* @example
|
|
345
|
+
* ```typescript
|
|
346
|
+
* const installId = await Mobana.getInstallId();
|
|
347
|
+
* // Share with support for data access/deletion: support@mobana.ai
|
|
348
|
+
* ```
|
|
349
|
+
*/
|
|
350
|
+
async getInstallId(): Promise<string> {
|
|
351
|
+
return getInstallId();
|
|
352
|
+
}
|
|
353
|
+
|
|
310
354
|
/**
|
|
311
355
|
* Reset all stored attribution data
|
|
312
356
|
* Useful for testing or when user logs out
|
|
@@ -317,6 +361,7 @@ class MobanaSDK {
|
|
|
317
361
|
async reset(): Promise<void> {
|
|
318
362
|
// Clear in-memory cache
|
|
319
363
|
this.cachedAttribution = null;
|
|
364
|
+
this.cachedAttributionResult = null;
|
|
320
365
|
this.attributionChecked = false;
|
|
321
366
|
this.attributionPromise = null;
|
|
322
367
|
|
|
@@ -612,7 +657,7 @@ class MobanaSDK {
|
|
|
612
657
|
|
|
613
658
|
private async fetchAttribution<T = Record<string, unknown>>(
|
|
614
659
|
options: GetAttributionOptions
|
|
615
|
-
): Promise<
|
|
660
|
+
): Promise<AttributionResult<T>> {
|
|
616
661
|
const { timeout = DEFAULT_TIMEOUT } = options;
|
|
617
662
|
|
|
618
663
|
try {
|
|
@@ -635,8 +680,14 @@ class MobanaSDK {
|
|
|
635
680
|
}
|
|
636
681
|
}
|
|
637
682
|
|
|
683
|
+
// Re-check enabled before making the network call — the SDK may have been
|
|
684
|
+
// disabled (via setEnabled(false)) while waiting for install ID or referrer
|
|
685
|
+
if (!this.config?.enabled) {
|
|
686
|
+
return { status: 'no_match', attribution: null };
|
|
687
|
+
}
|
|
688
|
+
|
|
638
689
|
// Make API request
|
|
639
|
-
const response = await findAttribution<T>(
|
|
690
|
+
const { data: response, errorType, status } = await findAttribution<T>(
|
|
640
691
|
endpoint,
|
|
641
692
|
this.config!.appKey,
|
|
642
693
|
installId,
|
|
@@ -646,54 +697,57 @@ class MobanaSDK {
|
|
|
646
697
|
this.config?.debug ?? false
|
|
647
698
|
);
|
|
648
699
|
|
|
649
|
-
//
|
|
700
|
+
// No response — network error, timeout, or server error
|
|
650
701
|
if (!response) {
|
|
651
702
|
if (this.config?.debug) {
|
|
652
|
-
console.log('[Mobana]
|
|
703
|
+
console.log('[Mobana] Attribution request failed:', errorType);
|
|
653
704
|
}
|
|
654
|
-
return
|
|
705
|
+
return {
|
|
706
|
+
status: 'error',
|
|
707
|
+
attribution: null,
|
|
708
|
+
error: {
|
|
709
|
+
type: errorType ?? 'unknown',
|
|
710
|
+
...(status !== undefined && { status }),
|
|
711
|
+
},
|
|
712
|
+
};
|
|
655
713
|
}
|
|
656
714
|
|
|
657
|
-
// Cache the response
|
|
658
|
-
// This prevents retrying on every startup
|
|
715
|
+
// Cache the response for definitive results (matched or unmatched)
|
|
659
716
|
if (typeof response.matched === 'boolean') {
|
|
660
717
|
if (response.matched && response.attribution) {
|
|
661
|
-
// Build attribution object
|
|
662
718
|
const attribution: Attribution<T> = {
|
|
663
719
|
...response.attribution,
|
|
664
720
|
confidence: response.confidence ?? 0,
|
|
665
721
|
};
|
|
666
722
|
|
|
667
|
-
// Cache matched result
|
|
668
723
|
await setCachedResult(true, attribution);
|
|
669
724
|
|
|
670
725
|
if (this.config?.debug) {
|
|
671
726
|
console.log('[Mobana] Attribution matched:', attribution);
|
|
672
727
|
}
|
|
673
728
|
|
|
674
|
-
return attribution;
|
|
729
|
+
return { status: 'matched', attribution };
|
|
675
730
|
} else {
|
|
676
|
-
// Cache unmatched result - prevents retry on next startup
|
|
677
731
|
await setCachedResult<T>(false);
|
|
678
732
|
|
|
679
733
|
if (this.config?.debug) {
|
|
680
734
|
console.log('[Mobana] No match found (cached)');
|
|
681
735
|
}
|
|
682
736
|
|
|
683
|
-
return null;
|
|
737
|
+
return { status: 'no_match', attribution: null };
|
|
684
738
|
}
|
|
685
739
|
}
|
|
686
740
|
|
|
687
|
-
// Unexpected response format
|
|
741
|
+
// Unexpected response format — treat as transient error (don't cache, allow retry)
|
|
688
742
|
if (this.config?.debug) {
|
|
689
743
|
console.log('[Mobana] Unexpected response format');
|
|
690
744
|
}
|
|
691
|
-
return null;
|
|
745
|
+
return { status: 'error', attribution: null, error: { type: 'unknown' } };
|
|
692
746
|
} catch (error) {
|
|
693
747
|
if (this.config?.debug) {
|
|
694
748
|
console.log('[Mobana] Error fetching attribution:', error);
|
|
695
749
|
}
|
|
696
|
-
return null;
|
|
750
|
+
return { status: 'error', attribution: null, error: { type: 'unknown' } };
|
|
697
751
|
}
|
|
698
752
|
}
|
|
699
753
|
|
package/src/api.ts
CHANGED
|
@@ -70,6 +70,79 @@ async function request<T>(
|
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
/**
|
|
74
|
+
* Internal result type for requests that need to surface error details.
|
|
75
|
+
* Used by findAttribution so getAttribution() can distinguish network vs.
|
|
76
|
+
* server vs. timeout errors.
|
|
77
|
+
*/
|
|
78
|
+
interface RequestResult<U> {
|
|
79
|
+
data: U | null;
|
|
80
|
+
errorType?: 'network' | 'timeout' | 'server';
|
|
81
|
+
/** HTTP status code — only present for 'server' errorType */
|
|
82
|
+
status?: number;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Like request(), but returns a structured result with error type information
|
|
87
|
+
* instead of collapsing all failures to null.
|
|
88
|
+
*/
|
|
89
|
+
async function requestWithError<U>(
|
|
90
|
+
endpoint: string,
|
|
91
|
+
path: string,
|
|
92
|
+
body: Record<string, unknown>,
|
|
93
|
+
appKey: string,
|
|
94
|
+
timeout: number = DEFAULT_TIMEOUT,
|
|
95
|
+
debug: boolean = false
|
|
96
|
+
): Promise<RequestResult<U>> {
|
|
97
|
+
const url = `${endpoint}${path}`;
|
|
98
|
+
const controller = new AbortController();
|
|
99
|
+
const timeoutId = setTimeout(() => controller.abort(), timeout);
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
if (debug) {
|
|
103
|
+
console.log(`[Mobana] POST ${url}`, body);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const response = await fetch(url, {
|
|
107
|
+
method: 'POST',
|
|
108
|
+
headers: buildHeaders(appKey),
|
|
109
|
+
body: JSON.stringify(body),
|
|
110
|
+
signal: controller.signal,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
clearTimeout(timeoutId);
|
|
114
|
+
|
|
115
|
+
if (!response.ok) {
|
|
116
|
+
if (debug) {
|
|
117
|
+
console.log(`[Mobana] Request failed: ${response.status}`);
|
|
118
|
+
}
|
|
119
|
+
return { data: null, errorType: 'server', status: response.status };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const data = await response.json();
|
|
123
|
+
|
|
124
|
+
if (debug) {
|
|
125
|
+
console.log(`[Mobana] Response:`, data);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return { data: data as U };
|
|
129
|
+
} catch (error) {
|
|
130
|
+
clearTimeout(timeoutId);
|
|
131
|
+
|
|
132
|
+
const isTimeout = error instanceof Error && error.name === 'AbortError';
|
|
133
|
+
|
|
134
|
+
if (debug) {
|
|
135
|
+
if (isTimeout) {
|
|
136
|
+
console.log(`[Mobana] Request timed out after ${timeout}ms`);
|
|
137
|
+
} else {
|
|
138
|
+
console.log(`[Mobana] Request error:`, error);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { data: null, errorType: isTimeout ? 'timeout' : 'network' };
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
73
146
|
/**
|
|
74
147
|
* Call /find endpoint to get attribution
|
|
75
148
|
*/
|
|
@@ -81,8 +154,8 @@ export async function findAttribution<T = Record<string, unknown>>(
|
|
|
81
154
|
dacid: string | null,
|
|
82
155
|
timeout: number,
|
|
83
156
|
debug: boolean
|
|
84
|
-
): Promise<FindResponse<T
|
|
85
|
-
return
|
|
157
|
+
): Promise<RequestResult<FindResponse<T>>> {
|
|
158
|
+
return requestWithError<FindResponse<T>>(
|
|
86
159
|
endpoint,
|
|
87
160
|
'/find',
|
|
88
161
|
{
|
package/src/types.ts
CHANGED
|
@@ -32,6 +32,17 @@ export interface MobanaConfig {
|
|
|
32
32
|
* When true, logs SDK operations to console
|
|
33
33
|
*/
|
|
34
34
|
debug?: boolean;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Automatically fetch attribution when init() is called (default: true)
|
|
38
|
+
*
|
|
39
|
+
* When true, attribution is fetched in the background on init — non-blocking.
|
|
40
|
+
* The result is cached so any subsequent getAttribution() call returns instantly.
|
|
41
|
+
*
|
|
42
|
+
* Set to false to delay attribution until you explicitly call getAttribution()
|
|
43
|
+
* (e.g., to wait for GDPR consent before making any network calls).
|
|
44
|
+
*/
|
|
45
|
+
autoAttribute?: boolean;
|
|
35
46
|
}
|
|
36
47
|
|
|
37
48
|
// ============================================
|
|
@@ -300,6 +311,41 @@ export interface Attribution<T = Record<string, unknown>> {
|
|
|
300
311
|
confidence: number;
|
|
301
312
|
}
|
|
302
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Error details returned when an attribution request fails
|
|
316
|
+
*/
|
|
317
|
+
export interface AttributionError {
|
|
318
|
+
/**
|
|
319
|
+
* Type of error:
|
|
320
|
+
* - 'network' — no internet connection or request was blocked
|
|
321
|
+
* - 'timeout' — request exceeded the timeout limit
|
|
322
|
+
* - 'server' — server returned an HTTP error
|
|
323
|
+
* - 'sdk_not_configured' — SDK.init() was not called before getAttribution()
|
|
324
|
+
* - 'sdk_disabled' — SDK is disabled (enabled: false in config)
|
|
325
|
+
* - 'unknown' — unexpected error
|
|
326
|
+
*/
|
|
327
|
+
type: 'network' | 'timeout' | 'server' | 'sdk_not_configured' | 'sdk_disabled' | 'unknown';
|
|
328
|
+
/** HTTP status code (only present for 'server' type) */
|
|
329
|
+
status?: number;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Result returned by getAttribution()
|
|
334
|
+
*/
|
|
335
|
+
export interface AttributionResult<T = Record<string, unknown>> {
|
|
336
|
+
/**
|
|
337
|
+
* Attribution status:
|
|
338
|
+
* - 'matched' — attribution data found; check the attribution field
|
|
339
|
+
* - 'no_match' — no match found (organic install)
|
|
340
|
+
* - 'error' — request failed or SDK is misconfigured; check the error field for details
|
|
341
|
+
*/
|
|
342
|
+
status: 'matched' | 'no_match' | 'error';
|
|
343
|
+
/** Attribution data. Present only when status is 'matched'. */
|
|
344
|
+
attribution: Attribution<T> | null;
|
|
345
|
+
/** Error details. Present only when status is 'error'. */
|
|
346
|
+
error?: AttributionError;
|
|
347
|
+
}
|
|
348
|
+
|
|
303
349
|
/**
|
|
304
350
|
* Internal: API response from /find endpoint
|
|
305
351
|
*/
|