@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/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<Attribution<any> | null> | null = null;
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<Attribution<T> | null> {
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, returning null');
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.cachedAttribution as Attribution<T> | null;
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
- // Update in-memory cache
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 = cached.matched ? (cached.attribution ?? null) : null;
223
- return this.cachedAttribution as Attribution<T> | null;
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<Attribution<T> | null>;
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
- // Update in-memory cache
236
- this.attributionChecked = true;
237
- this.cachedAttribution = result;
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<Attribution<T> | null> {
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
- // If no response (network error, timeout), don't cache - allow retry
700
+ // No response network error, timeout, or server error
650
701
  if (!response) {
651
702
  if (this.config?.debug) {
652
- console.log('[Mobana] No response from server');
703
+ console.log('[Mobana] Attribution request failed:', errorType);
653
704
  }
654
- return null;
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 if server returned a valid response with matched key
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> | null> {
85
- return request<FindResponse<T>>(
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
  */