insert-affiliate-react-native-sdk 1.6.3 → 1.7.0

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.
@@ -1,13 +1,19 @@
1
- import React, { createContext, useEffect, useState } from 'react';
2
- import { Platform, Linking } from 'react-native';
1
+ import React, { createContext, useEffect, useState, useRef } from 'react';
2
+ import { Platform, Linking, Dimensions, PixelRatio } from 'react-native';
3
3
  import axios from 'axios';
4
4
  import AsyncStorage from '@react-native-async-storage/async-storage';
5
+ import Clipboard from '@react-native-clipboard/clipboard';
6
+ import NetInfo from '@react-native-community/netinfo';
7
+ import DeviceInfo from 'react-native-device-info';
8
+ import { PlayInstallReferrer, PlayInstallReferrerInfo } from 'react-native-play-install-referrer';
5
9
 
6
10
  // TYPES USED IN THIS PROVIDER
7
11
  type T_DEEPLINK_IAP_PROVIDER = {
8
12
  children: React.ReactNode;
9
13
  };
10
14
 
15
+ export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null) => void;
16
+
11
17
  type CustomPurchase = {
12
18
  [key: string]: any; // Accept any fields to allow it to work wtih multiple IAP libraries
13
19
  };
@@ -16,7 +22,9 @@ type T_DEEPLINK_IAP_CONTEXT = {
16
22
  referrerLink: string;
17
23
  userId: string;
18
24
  OfferCode: string | null;
19
- returnInsertAffiliateIdentifier: () => Promise<string | null>;
25
+ returnInsertAffiliateIdentifier: (ignoreTimeout?: boolean) => Promise<string | null>;
26
+ isAffiliateAttributionValid: () => Promise<boolean>;
27
+ getAffiliateStoredDate: () => Promise<Date | null>;
20
28
  validatePurchaseWithIapticAPI: (
21
29
  jsonIapPurchase: CustomPurchase,
22
30
  iapticAppId: string,
@@ -32,7 +40,9 @@ type T_DEEPLINK_IAP_CONTEXT = {
32
40
  setInsertAffiliateIdentifier: (
33
41
  referringLink: string
34
42
  ) => Promise<void | string>;
35
- initialize: (code: string | null, verboseLogging?: boolean) => Promise<void>;
43
+ setInsertAffiliateIdentifierChangeCallback: (callback: InsertAffiliateIdentifierChangeCallback | null) => void;
44
+ handleInsertLinks: (url: string) => Promise<boolean>;
45
+ initialize: (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number) => Promise<void>;
36
46
  isInitialized: boolean;
37
47
  };
38
48
 
@@ -59,6 +69,7 @@ const ASYNC_KEYS = {
59
69
  COMPANY_CODE: '@app_company_code',
60
70
  USER_ACCOUNT_TOKEN: '@app_user_account_token',
61
71
  IOS_OFFER_CODE: '@app_ios_offer_code',
72
+ AFFILIATE_STORED_DATE: '@app_affiliate_stored_date',
62
73
  };
63
74
 
64
75
  // STARTING CONTEXT IMPLEMENTATION
@@ -66,7 +77,9 @@ export const DeepLinkIapContext = createContext<T_DEEPLINK_IAP_CONTEXT>({
66
77
  referrerLink: '',
67
78
  userId: '',
68
79
  OfferCode: null,
69
- returnInsertAffiliateIdentifier: async () => '',
80
+ returnInsertAffiliateIdentifier: async (ignoreTimeout?: boolean) => '',
81
+ isAffiliateAttributionValid: async () => false,
82
+ getAffiliateStoredDate: async () => null,
70
83
  validatePurchaseWithIapticAPI: async (
71
84
  jsonIapPurchase: CustomPurchase,
72
85
  iapticAppId: string,
@@ -78,23 +91,34 @@ export const DeepLinkIapContext = createContext<T_DEEPLINK_IAP_CONTEXT>({
78
91
  trackEvent: async (eventName: string) => {},
79
92
  setShortCode: async (shortCode: string) => {},
80
93
  setInsertAffiliateIdentifier: async (referringLink: string) => {},
81
- initialize: async (code: string | null, verboseLogging?: boolean) => {},
94
+ setInsertAffiliateIdentifierChangeCallback: (callback: InsertAffiliateIdentifierChangeCallback | null) => {},
95
+ handleInsertLinks: async (url: string) => false,
96
+ initialize: async (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number) => {},
82
97
  isInitialized: false,
83
98
  });
84
99
 
85
100
  const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
86
101
  children,
87
- }) => {
102
+ }) => {
88
103
  const [referrerLink, setReferrerLink] = useState<string>('');
89
104
  const [userId, setUserId] = useState<string>('');
90
105
  const [companyCode, setCompanyCode] = useState<string | null>(null);
91
106
  const [isInitialized, setIsInitialized] = useState<boolean>(false);
92
107
  const [verboseLogging, setVerboseLogging] = useState<boolean>(false);
108
+ const [insertLinksEnabled, setInsertLinksEnabled] = useState<boolean>(false);
109
+ const [insertLinksClipboardEnabled, setInsertLinksClipboardEnabled] = useState<boolean>(false);
93
110
  const [OfferCode, setOfferCode] = useState<string | null>(null);
111
+ const [affiliateAttributionActiveTime, setAffiliateAttributionActiveTime] = useState<number | null>(null);
112
+ const insertAffiliateIdentifierChangeCallbackRef = useRef<InsertAffiliateIdentifierChangeCallback | null>(null);
94
113
 
95
114
  // MARK: Initialize the SDK
96
- const initialize = async (companyCode: string | null, verboseLogging: boolean = false): Promise<void> => {
115
+ const initialize = async (companyCode: string | null, verboseLogging: boolean = false, insertLinksEnabled: boolean = false, insertLinksClipboardEnabled: boolean = false, affiliateAttributionActiveTime?: number): Promise<void> => {
97
116
  setVerboseLogging(verboseLogging);
117
+ setInsertLinksEnabled(insertLinksEnabled);
118
+ setInsertLinksClipboardEnabled(insertLinksClipboardEnabled);
119
+ if (affiliateAttributionActiveTime !== undefined) {
120
+ setAffiliateAttributionActiveTime(affiliateAttributionActiveTime);
121
+ }
98
122
 
99
123
  if (verboseLogging) {
100
124
  console.log('[Insert Affiliate] [VERBOSE] Starting SDK initialization...');
@@ -127,6 +151,15 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
127
151
  console.log('[Insert Affiliate] [VERBOSE] No company code provided, SDK initialized in limited mode');
128
152
  }
129
153
  }
154
+
155
+ if (insertLinksEnabled && Platform.OS === 'ios') {
156
+ try {
157
+ const enhancedSystemInfo = await getEnhancedSystemInfo();
158
+ await sendSystemInfoToBackend(enhancedSystemInfo);
159
+ } catch (error) {
160
+ verboseLog(`Error sending system info for clipboard check: ${error}`);
161
+ }
162
+ }
130
163
  };
131
164
 
132
165
  // EFFECT TO FETCH USER ID AND REF LINK
@@ -169,6 +202,95 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
169
202
  fetchAsyncEssentials();
170
203
  }, []);
171
204
 
205
+ // Cleanup callback on unmount
206
+ useEffect(() => {
207
+ return () => {
208
+ insertAffiliateIdentifierChangeCallbackRef.current = null;
209
+ };
210
+ }, []);
211
+
212
+ // Deep link event listeners - equivalent to iOS AppDelegate methods
213
+ useEffect(() => {
214
+ if (!isInitialized) return;
215
+
216
+ // Handle app launch with URL (equivalent to didFinishLaunchingWithOptions)
217
+ const handleInitialURL = async () => {
218
+ try {
219
+ const initialUrl = await Linking.getInitialURL();
220
+ if (initialUrl) {
221
+ verboseLog(`App launched with URL: ${initialUrl}`);
222
+ const handled = await handleDeepLink(initialUrl);
223
+ if (handled) {
224
+ verboseLog('URL was handled by Insert Affiliate SDK');
225
+ } else {
226
+ verboseLog('URL was not handled by Insert Affiliate SDK');
227
+ }
228
+ }
229
+ } catch (error) {
230
+ console.error('[Insert Affiliate] Error getting initial URL:', error);
231
+ }
232
+ };
233
+
234
+ // Handle URL opening while app is running (equivalent to open url)
235
+ const handleUrlChange = async (event: { url: string }) => {
236
+ try {
237
+ verboseLog(`URL opened while app running: ${event.url}`);
238
+ const handled = await handleDeepLink(event.url);
239
+ if (handled) {
240
+ verboseLog('URL was handled by Insert Affiliate SDK');
241
+ } else {
242
+ verboseLog('URL was not handled by Insert Affiliate SDK');
243
+ }
244
+ } catch (error) {
245
+ console.error('[Insert Affiliate] Error handling URL change:', error);
246
+ }
247
+ };
248
+
249
+ // Platform-specific deep link handler
250
+ const handleDeepLink = async (url: string): Promise<boolean> => {
251
+ try {
252
+ verboseLog(`Platform detection: Platform.OS = ${Platform.OS}`);
253
+ if (Platform.OS === 'ios') {
254
+ verboseLog('Routing to iOS handler (handleInsertLinks)');
255
+ return await handleInsertLinks(url);
256
+ } else if (Platform.OS === 'android') {
257
+ verboseLog('Routing to Android handler (handleInsertLinkAndroid)');
258
+ return await handleInsertLinkAndroid(url);
259
+ }
260
+ verboseLog(`Unrecognized platform: ${Platform.OS}`);
261
+ return false;
262
+ } catch (error) {
263
+ verboseLog(`Error handling deep link: ${error}`);
264
+ return false;
265
+ }
266
+ };
267
+
268
+ // Set up listeners
269
+ const urlListener = Linking.addEventListener('url', handleUrlChange);
270
+
271
+ // Handle initial URL
272
+ handleInitialURL();
273
+
274
+ // Cleanup
275
+ return () => {
276
+ urlListener?.remove();
277
+ };
278
+ }, [isInitialized]);
279
+
280
+ // EFFECT TO HANDLE INSTALL REFERRER ON ANDROID
281
+ useEffect(() => {
282
+
283
+ if (Platform.OS === 'android' && isInitialized && insertLinksEnabled) { verboseLog('Install referrer effect - Platform.OS is android, isInitialized is true, and insertLinksEnabled is true');
284
+ // Ensure user ID is generated before processing install referrer
285
+ const initializeAndCapture = async () => {
286
+ await generateThenSetUserID();
287
+ verboseLog('Install referrer effect - Generating user ID and capturing install referrer');
288
+ captureInstallReferrer();
289
+ };
290
+ initializeAndCapture();
291
+ }
292
+ }, [isInitialized, insertLinksEnabled]);
293
+
172
294
  async function generateThenSetUserID() {
173
295
  verboseLog('Getting or generating user ID...');
174
296
  let userId = await getValueFromAsync(ASYNC_KEYS.USER_ID);
@@ -204,6 +326,349 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
204
326
  console.log('[Insert Affiliate] SDK has been reset.');
205
327
  };
206
328
 
329
+ // MARK: Callback Management
330
+ // Sets a callback that will be triggered whenever storeInsertAffiliateIdentifier is called
331
+ // The callback receives the current affiliate identifier (returnInsertAffiliateIdentifier result)
332
+ const setInsertAffiliateIdentifierChangeCallbackHandler = (callback: InsertAffiliateIdentifierChangeCallback | null): void => {
333
+ insertAffiliateIdentifierChangeCallbackRef.current = callback;
334
+ };
335
+
336
+ // MARK: Deep Link Handling
337
+ // Helper function to parse URLs in React Native compatible way
338
+ const parseURL = (url: string) => {
339
+ try {
340
+ // Extract protocol
341
+ const protocolMatch = url.match(/^([^:]+):/);
342
+ const protocol = protocolMatch ? protocolMatch[1] + ':' : '';
343
+
344
+ // Extract hostname for https URLs
345
+ let hostname = '';
346
+ if (protocol === 'https:' || protocol === 'http:') {
347
+ const hostnameMatch = url.match(/^https?:\/\/([^\/]+)/);
348
+ hostname = hostnameMatch ? hostnameMatch[1] : '';
349
+ }
350
+
351
+ return {
352
+ protocol,
353
+ hostname,
354
+ href: url
355
+ };
356
+ } catch (error) {
357
+ return {
358
+ protocol: '',
359
+ hostname: '',
360
+ href: url
361
+ };
362
+ }
363
+ };
364
+
365
+ // Handles Android deep links with insertAffiliate parameter
366
+ const handleInsertLinkAndroid = async (url: string): Promise<boolean> => {
367
+ try {
368
+ // Check if deep links are enabled
369
+ if (!insertLinksEnabled) {
370
+ verboseLog('Deep links are disabled, not handling Android URL');
371
+ return false;
372
+ }
373
+
374
+ verboseLog(`Processing Android deep link: ${url}`);
375
+
376
+ if (!url || typeof url !== 'string') {
377
+ verboseLog('Invalid URL provided to handleInsertLinkAndroid');
378
+ return false;
379
+ }
380
+
381
+ // Parse the URL to extract query parameters
382
+ const urlObj = new URL(url);
383
+ const insertAffiliate = urlObj.searchParams.get('insertAffiliate');
384
+
385
+ if (insertAffiliate && insertAffiliate.length > 0) {
386
+ verboseLog(`Found insertAffiliate parameter: ${insertAffiliate}`);
387
+
388
+ await storeInsertAffiliateIdentifier({ link: insertAffiliate });
389
+
390
+ return true;
391
+ } else {
392
+ verboseLog('No insertAffiliate parameter found in Android deep link');
393
+ return false;
394
+ }
395
+ } catch (error) {
396
+ verboseLog(`Error handling Android deep link: ${error}`);
397
+ return false;
398
+ }
399
+ };
400
+
401
+ // MARK: Play Install Referrer
402
+ /**
403
+ * Captures install referrer data from Google Play Store
404
+ * This method automatically extracts referral parameters and processes them
405
+ */
406
+ const captureInstallReferrer = async (retryCount: number = 0): Promise<boolean> => {
407
+ try {
408
+ // Check if deep links are enabled
409
+ if (!insertLinksEnabled) {
410
+ verboseLog('Deep links are disabled, not processing install referrer');
411
+ return false;
412
+ }
413
+
414
+ // Check if we're on Android
415
+ if (Platform.OS !== 'android') {
416
+ verboseLog('Install referrer is only available on Android');
417
+ return false;
418
+ }
419
+
420
+ verboseLog(`Starting install referrer capture... (attempt ${retryCount + 1})`);
421
+
422
+ // Convert callback-based API to Promise with timeout
423
+ const referrerData = await new Promise<PlayInstallReferrerInfo | null>((resolve, reject) => {
424
+ const timeout = setTimeout(() => {
425
+ reject(new Error('Install referrer request timed out'));
426
+ }, 10000); // 10 second timeout
427
+
428
+ PlayInstallReferrer.getInstallReferrerInfo((info, error) => {
429
+ clearTimeout(timeout);
430
+ if (error) {
431
+ reject(error);
432
+ } else {
433
+ resolve(info);
434
+ }
435
+ });
436
+ });
437
+
438
+ if (referrerData && referrerData.installReferrer) {
439
+ verboseLog(`Raw install referrer data: ${referrerData.installReferrer}`);
440
+
441
+ const success = await processInstallReferrerData(referrerData.installReferrer);
442
+
443
+ if (success) {
444
+ verboseLog('Install referrer processed successfully');
445
+ return true;
446
+ } else {
447
+ verboseLog('No insertAffiliate parameter found in install referrer');
448
+ return false;
449
+ }
450
+ } else {
451
+ verboseLog('No install referrer data found');
452
+ return false;
453
+ }
454
+
455
+ } catch (error) {
456
+ const errorMessage = error instanceof Error ? error.message : String(error);
457
+ verboseLog(`Error capturing install referrer (attempt ${retryCount + 1}): ${errorMessage}`);
458
+
459
+ // Check if this is a retryable error and we haven't exceeded max retries
460
+ const isRetryableError = errorMessage.includes('SERVICE_UNAVAILABLE') ||
461
+ errorMessage.includes('DEVELOPER_ERROR') ||
462
+ errorMessage.includes('timed out') ||
463
+ errorMessage.includes('SERVICE_DISCONNECTED');
464
+
465
+ const maxRetries = 3;
466
+ const retryDelay = Math.min(1000 * Math.pow(2, retryCount), 10000); // Exponential backoff, max 10s
467
+
468
+ if (isRetryableError && retryCount < maxRetries) {
469
+ verboseLog(`Retrying install referrer capture in ${retryDelay}ms...`);
470
+
471
+ // Schedule retry
472
+ setTimeout(() => {
473
+ captureInstallReferrer(retryCount + 1);
474
+ }, retryDelay);
475
+
476
+ return false;
477
+ } else {
478
+ verboseLog(`Install referrer capture failed after ${retryCount + 1} attempts`);
479
+ return false;
480
+ }
481
+ }
482
+ };
483
+
484
+ /**
485
+ * Processes the raw install referrer data and extracts insertAffiliate parameter
486
+ * @param rawReferrer The raw referrer string from Play Store
487
+ */
488
+ const processInstallReferrerData = async (rawReferrer: string): Promise<boolean> => {
489
+ try {
490
+ verboseLog('Processing install referrer data...');
491
+
492
+ if (!rawReferrer || rawReferrer.length === 0) {
493
+ verboseLog('No referrer data provided');
494
+ return false;
495
+ }
496
+
497
+ verboseLog(`Raw referrer data: ${rawReferrer}`);
498
+
499
+ // Parse the referrer string directly for insertAffiliate parameter
500
+ let insertAffiliate: string | null = null;
501
+
502
+ if (rawReferrer.includes('insertAffiliate=')) {
503
+ const params = rawReferrer.split('&');
504
+ for (const param of params) {
505
+ if (param.startsWith('insertAffiliate=')) {
506
+ insertAffiliate = param.substring('insertAffiliate='.length);
507
+ break;
508
+ }
509
+ }
510
+ }
511
+
512
+ verboseLog(`Extracted insertAffiliate parameter: ${insertAffiliate}`);
513
+
514
+ // If we have insertAffiliate parameter, use it as the affiliate identifier
515
+ if (insertAffiliate && insertAffiliate.length > 0) {
516
+ verboseLog(`Found insertAffiliate parameter, setting as affiliate identifier: ${insertAffiliate}`);
517
+ await storeInsertAffiliateIdentifier({ link: insertAffiliate });
518
+
519
+ return true;
520
+ } else {
521
+ verboseLog('No insertAffiliate parameter found in referrer data');
522
+ return false;
523
+ }
524
+
525
+ } catch (error) {
526
+ verboseLog(`Error processing install referrer data: ${error}`);
527
+ return false;
528
+ }
529
+ };
530
+
531
+ // Handles Insert Links deep linking - equivalent to iOS handleInsertLinks
532
+ const handleInsertLinks = async (url: string): Promise<boolean> => {
533
+ try {
534
+ console.log(`[Insert Affiliate] Attempting to handle URL: ${url}`);
535
+
536
+ if (!url || typeof url !== 'string') {
537
+ console.log('[Insert Affiliate] Invalid URL provided to handleInsertLinks');
538
+ return false;
539
+ }
540
+
541
+ // Check if deep links are enabled synchronously
542
+ if (!insertLinksEnabled) {
543
+ console.log('[Insert Affiliate] Deep links are disabled, not handling URL');
544
+ return false;
545
+ }
546
+
547
+ const urlObj = parseURL(url);
548
+
549
+ // Handle custom URL schemes (ia-companycode://shortcode)
550
+ if (urlObj.protocol && urlObj.protocol.startsWith('ia-')) {
551
+ return await handleCustomURLScheme(url, urlObj.protocol);
552
+ }
553
+
554
+ // Handle universal links (https://insertaffiliate.link/V1/companycode/shortcode)
555
+ // if (urlObj.protocol === 'https:' && urlObj.hostname?.includes('insertaffiliate.link')) {
556
+ // return await handleUniversalLink(urlObj);
557
+ // }
558
+
559
+ return false;
560
+ } catch (error) {
561
+ console.error('[Insert Affiliate] Error handling Insert Link:', error);
562
+ verboseLog(`Error in handleInsertLinks: ${error}`);
563
+ return false;
564
+ }
565
+ };
566
+
567
+ // Handle custom URL schemes like ia-companycode://shortcode
568
+ const handleCustomURLScheme = async (url: string, protocol: string): Promise<boolean> => {
569
+ try {
570
+ const scheme = protocol.replace(':', '');
571
+
572
+ if (!scheme.startsWith('ia-')) {
573
+ return false;
574
+ }
575
+
576
+ // Extract company code from scheme (remove "ia-" prefix)
577
+ const companyCode = scheme.substring(3);
578
+
579
+ const shortCode = parseShortCodeFromURLString(url);
580
+ if (!shortCode) {
581
+ console.log(`[Insert Affiliate] Failed to parse short code from deep link: ${url}`);
582
+ return false;
583
+ }
584
+
585
+ console.log(`[Insert Affiliate] Custom URL scheme detected - Company: ${companyCode}, Short code: ${shortCode}`);
586
+
587
+ // Validate company code matches initialized one
588
+ const activeCompanyCode = await getActiveCompanyCode();
589
+ if (activeCompanyCode && companyCode.toLowerCase() !== activeCompanyCode.toLowerCase()) {
590
+ console.log(`[Insert Affiliate] Warning: URL company code (${companyCode}) doesn't match initialized company code (${activeCompanyCode})`);
591
+ }
592
+
593
+ // If URL scheme is used, we can straight away store the short code as the referring link
594
+ await storeInsertAffiliateIdentifier({ link: shortCode });
595
+
596
+ // Collect and send enhanced system info to backend
597
+ try {
598
+ const enhancedSystemInfo = await getEnhancedSystemInfo();
599
+ await sendSystemInfoToBackend(enhancedSystemInfo);
600
+ } catch (error) {
601
+ verboseLog(`Error sending system info for deep link: ${error}`);
602
+ }
603
+
604
+ return true;
605
+ } catch (error) {
606
+ console.error('[Insert Affiliate] Error handling custom URL scheme:', error);
607
+ return false;
608
+ }
609
+ };
610
+
611
+ // Handle universal links like https://insertaffiliate.link/V1/companycode/shortcode
612
+ // const handleUniversalLink = async (url: URL): Promise<boolean> => {
613
+ // try {
614
+ // const pathComponents = url.pathname.split('/').filter(segment => segment.length > 0);
615
+
616
+ // // Expected format: /V1/companycode/shortcode
617
+ // if (pathComponents.length < 3 || pathComponents[0] !== 'V1') {
618
+ // console.log(`[Insert Affiliate] Invalid universal link format: ${url.href}`);
619
+ // return false;
620
+ // }
621
+
622
+ // const companyCode = pathComponents[1];
623
+ // const shortCode = pathComponents[2];
624
+
625
+ // console.log(`[Insert Affiliate] Universal link detected - Company: ${companyCode}, Short code: ${shortCode}`);
626
+
627
+ // // Validate company code matches initialized one
628
+ // const activeCompanyCode = await getActiveCompanyCode();
629
+ // if (activeCompanyCode && companyCode.toLowerCase() !== activeCompanyCode.toLowerCase()) {
630
+ // console.log(`[Insert Affiliate] Warning: URL company code (${companyCode}) doesn't match initialized company code (${activeCompanyCode})`);
631
+ // }
632
+
633
+ // // Process the affiliate attribution
634
+ // await storeInsertAffiliateIdentifier({ link: shortCode });
635
+
636
+
637
+ // return true;
638
+ // } catch (error) {
639
+ // console.error('[Insert Affiliate] Error handling universal link:', error);
640
+ // return false;
641
+ // }
642
+ // };
643
+
644
+ // Parse short code from URL
645
+ const parseShortCodeFromURL = (url: URL): string | null => {
646
+ try {
647
+ // For custom schemes like ia-companycode://shortcode, everything after :// is the short code
648
+ // Remove leading slash from pathname
649
+ return url.pathname.startsWith('/') ? url.pathname.substring(1) : url.pathname;
650
+ } catch (error) {
651
+ verboseLog(`Error parsing short code from URL: ${error}`);
652
+ return null;
653
+ }
654
+ };
655
+
656
+ const parseShortCodeFromURLString = (url: string): string | null => {
657
+ try {
658
+ // For custom schemes like ia-companycode://shortcode, everything after :// is the short code
659
+ const match = url.match(/^[^:]+:\/\/(.+)$/);
660
+ if (match) {
661
+ const shortCode = match[1];
662
+ // Remove leading slash if present
663
+ return shortCode.startsWith('/') ? shortCode.substring(1) : shortCode;
664
+ }
665
+ return null;
666
+ } catch (error) {
667
+ verboseLog(`Error parsing short code from URL string: ${error}`);
668
+ return null;
669
+ }
670
+ };
671
+
207
672
  // Helper funciton Storage / Retrieval
208
673
  const saveValueInAsync = async (key: string, value: string) => {
209
674
  await AsyncStorage.setItem(key, value);
@@ -260,6 +725,445 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
260
725
  }
261
726
  };
262
727
 
728
+ // MARK: - Deep Linking Utilities
729
+
730
+ // Retrieves and validates clipboard content for UUID format
731
+ const getClipboardUUID = async (): Promise<string | null> => {
732
+ // Check if clipboard access is enabled
733
+ if (!insertLinksClipboardEnabled) {
734
+ return null;
735
+ }
736
+
737
+ verboseLog('Getting clipboard UUID');
738
+
739
+ try {
740
+ const clipboardString = await Clipboard.getString();
741
+
742
+ if (!clipboardString) {
743
+ verboseLog('No clipboard string found or access denied');
744
+ return null;
745
+ }
746
+
747
+ const trimmedString = clipboardString.trim();
748
+
749
+ if (isValidUUID(trimmedString)) {
750
+ verboseLog(`Valid clipboard UUID found: ${trimmedString}`);
751
+ return trimmedString;
752
+ }
753
+
754
+ verboseLog(`Invalid clipboard UUID found: ${trimmedString}`);
755
+ return null;
756
+ } catch (error) {
757
+ verboseLog(`Clipboard access error: ${error}`);
758
+ return null;
759
+ }
760
+ };
761
+
762
+ // Validates if a string is a properly formatted UUID (36 characters)
763
+ const isValidUUID = (string: string): boolean => {
764
+ if (string.length !== 36) return false;
765
+
766
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
767
+ return uuidRegex.test(string);
768
+ };
769
+
770
+ // MARK: - System Info Collection
771
+
772
+ // Gets network connection type and interface information
773
+ const getNetworkInfo = async (): Promise<{[key: string]: any}> => {
774
+ try {
775
+ const connectionInfo = {
776
+ connectionType: 'unknown',
777
+ interfaceTypes: [] as string[],
778
+ isExpensive: false,
779
+ isConstrained: false,
780
+ status: 'disconnected',
781
+ availableInterfaces: [] as string[]
782
+ };
783
+
784
+ try {
785
+ // Use NetInfo to get accurate network information
786
+ const netInfo = await NetInfo.fetch();
787
+
788
+ connectionInfo.status = netInfo.isConnected ? 'connected' : 'disconnected';
789
+ connectionInfo.connectionType = netInfo.type || 'unknown';
790
+ connectionInfo.isExpensive = netInfo.isInternetReachable === false ? true : false;
791
+ connectionInfo.isConstrained = false; // NetInfo doesn't provide this directly
792
+
793
+ // Map NetInfo types to our interface format
794
+ if (netInfo.type) {
795
+ connectionInfo.interfaceTypes = [netInfo.type];
796
+ connectionInfo.availableInterfaces = [netInfo.type];
797
+ }
798
+
799
+ // Additional details if available
800
+ if (netInfo.details && 'isConnectionExpensive' in netInfo.details) {
801
+ connectionInfo.isExpensive = netInfo.details.isConnectionExpensive || false;
802
+ }
803
+
804
+ } catch (error) {
805
+ verboseLog(`Network info fetch failed: ${error}`);
806
+ // Fallback to basic connectivity test
807
+ try {
808
+ const response = await fetch('https://www.google.com/favicon.ico', {
809
+ method: 'HEAD'
810
+ });
811
+ if (response.ok) {
812
+ connectionInfo.status = 'connected';
813
+ }
814
+ } catch (fetchError) {
815
+ verboseLog(`Fallback connectivity test failed: ${fetchError}`);
816
+ }
817
+ }
818
+
819
+ return connectionInfo;
820
+ } catch (error) {
821
+ verboseLog(`Error getting network info: ${error}`);
822
+ return {
823
+ connectionType: 'unknown',
824
+ interfaceTypes: [],
825
+ isExpensive: false,
826
+ isConstrained: false,
827
+ status: 'disconnected',
828
+ availableInterfaces: []
829
+ };
830
+ }
831
+ };
832
+
833
+
834
+ const getNetworkPathInfo = async (): Promise<{[key: string]: any}> => {
835
+ try {
836
+ const netInfo = await NetInfo.fetch();
837
+
838
+ // Default values - only set to true when proven
839
+ let supportsIPv4 = false;
840
+ let supportsIPv6 = false;
841
+ let supportsDNS = false;
842
+ let hasUnsatisfiedGateway = false;
843
+ let gatewayCount = 0;
844
+ let gateways: string[] = [];
845
+ let interfaceDetails: Array<{[key: string]: any}> = [];
846
+
847
+ if (netInfo.details && netInfo.isConnected) {
848
+ supportsIPv4 = true;
849
+
850
+ // IPv6 support based on interface type (following Swift logic)
851
+ if (netInfo.type === 'wifi' || netInfo.type === 'cellular' || netInfo.type === 'ethernet') {
852
+ supportsIPv6 = true;
853
+ } else {
854
+ supportsIPv6 = false;
855
+ }
856
+
857
+ supportsDNS = netInfo.isInternetReachable === true;
858
+
859
+ // Get interface details from NetInfo
860
+ if (netInfo.details && 'isConnectionExpensive' in netInfo.details) {
861
+ // This is a cellular connection
862
+ interfaceDetails.push({
863
+ name: 'cellular',
864
+ index: 0,
865
+ type: 'cellular'
866
+ });
867
+ } else if (netInfo.type === 'wifi') {
868
+ interfaceDetails.push({
869
+ name: 'en0',
870
+ index: 0,
871
+ type: 'wifi'
872
+ });
873
+ } else if (netInfo.type === 'ethernet') {
874
+ interfaceDetails.push({
875
+ name: 'en0',
876
+ index: 0,
877
+ type: 'wiredEthernet'
878
+ });
879
+ }
880
+
881
+ gatewayCount = interfaceDetails.length;
882
+ hasUnsatisfiedGateway = gatewayCount === 0;
883
+
884
+ // For React Native, we can't easily get actual gateway IPs
885
+ // but we can indicate if we have network connectivity
886
+ if (netInfo.isConnected) {
887
+ gateways = ['default']; // Placeholder since we can't get actual gateway IPs
888
+ }
889
+ }
890
+
891
+ // Fallback if NetInfo doesn't provide enough details
892
+ if (interfaceDetails.length === 0) {
893
+ interfaceDetails = [{
894
+ name: 'en0',
895
+ index: 0,
896
+ type: netInfo.type || 'unknown'
897
+ }];
898
+ gatewayCount = 1;
899
+ hasUnsatisfiedGateway = false;
900
+ gateways = ['default'];
901
+ }
902
+
903
+ return {
904
+ supportsIPv4,
905
+ supportsIPv6,
906
+ supportsDNS,
907
+ hasUnsatisfiedGateway,
908
+ gatewayCount,
909
+ gateways,
910
+ interfaceDetails
911
+ };
912
+
913
+ } catch (error) {
914
+ verboseLog(`Error getting network path info: ${error}`);
915
+
916
+ // Fallback to basic defaults if NetInfo fails
917
+ return {
918
+ supportsIPv4: true,
919
+ supportsIPv6: false,
920
+ supportsDNS: true,
921
+ hasUnsatisfiedGateway: false,
922
+ gatewayCount: 1,
923
+ gateways: ['default'],
924
+ interfaceDetails: [{
925
+ name: 'en0',
926
+ index: 0,
927
+ type: 'unknown'
928
+ }]
929
+ };
930
+ }
931
+ };
932
+
933
+ // Collects basic system information for deep linking (non-identifying data only)
934
+ const getSystemInfo = async (): Promise<{[key: string]: any}> => {
935
+ const systemInfo: {[key: string]: any} = {};
936
+
937
+ try {
938
+ systemInfo.systemName = await DeviceInfo.getSystemName();
939
+ systemInfo.systemVersion = await DeviceInfo.getSystemVersion();
940
+ systemInfo.model = await DeviceInfo.getModel();
941
+ systemInfo.localizedModel = await DeviceInfo.getModel();
942
+ systemInfo.isPhysicalDevice = !(await DeviceInfo.isEmulator());
943
+ systemInfo.bundleId = await DeviceInfo.getBundleId();
944
+
945
+ // Map device type to more readable format
946
+ const deviceType = await DeviceInfo.getDeviceType();
947
+ systemInfo.deviceType = deviceType === 'Handset' ? 'mobile' : deviceType;
948
+ } catch (error) {
949
+ verboseLog(`Error getting device info: ${error}`);
950
+ // Fallback to basic platform detection
951
+ systemInfo.systemName = 'iOS';
952
+ systemInfo.systemVersion = Platform.Version.toString();
953
+ systemInfo.model = 'iPhone';
954
+ systemInfo.localizedModel = systemInfo.model;
955
+ systemInfo.isPhysicalDevice = true; // Assume physical device if we can't detect
956
+ systemInfo.bundleId = 'null'; // Fallback if we can't get bundle ID
957
+ systemInfo.deviceType = 'unknown';
958
+ }
959
+
960
+ if (verboseLogging) {
961
+ console.log('[Insert Affiliate] system info:', systemInfo);
962
+ }
963
+
964
+ return systemInfo;
965
+ };
966
+
967
+ const getEnhancedSystemInfo = async (): Promise<{[key: string]: any}> => {
968
+ verboseLog('Collecting enhanced system information...');
969
+
970
+ let systemInfo = await getSystemInfo();
971
+
972
+ verboseLog(`System info: ${JSON.stringify(systemInfo)}`);
973
+
974
+ try {
975
+ // Add timestamp
976
+ const now = new Date();
977
+ systemInfo.requestTime = now.toISOString();
978
+ systemInfo.requestTimestamp = Math.floor(now.getTime());
979
+
980
+ // Add user agent style information
981
+ const systemName = systemInfo.systemName;
982
+ const systemVersion = systemInfo.systemVersion;
983
+ const model = systemInfo.model;
984
+
985
+ systemInfo.userAgent = `${model}; ${systemName} ${systemVersion}`;
986
+
987
+ // Add screen dimensions and device pixel ratio (matching exact field names)
988
+ const { width, height } = Dimensions.get('window');
989
+ const pixelRatio = PixelRatio.get();
990
+
991
+ systemInfo.screenWidth = Math.floor(width);
992
+ systemInfo.screenHeight = Math.floor(height);
993
+ systemInfo.screenAvailWidth = Math.floor(width);
994
+ systemInfo.screenAvailHeight = Math.floor(height);
995
+ systemInfo.devicePixelRatio = pixelRatio;
996
+ systemInfo.screenColorDepth = 24;
997
+ systemInfo.screenPixelDepth = 24;
998
+
999
+
1000
+ try {
1001
+ systemInfo.hardwareConcurrency = await DeviceInfo.getTotalMemory() / (1024 * 1024 * 1024); // Convert to GB
1002
+ } catch (error) {
1003
+ systemInfo.hardwareConcurrency = 4; // Fallback assumption
1004
+ }
1005
+ systemInfo.maxTouchPoints = 5; // Default for mobile devices
1006
+
1007
+ // Add screen dimensions (native mobile naming)
1008
+ systemInfo.screenInnerWidth = Math.floor(width);
1009
+ systemInfo.screenInnerHeight = Math.floor(height);
1010
+ systemInfo.screenOuterWidth = Math.floor(width);
1011
+ systemInfo.screenOuterHeight = Math.floor(height);
1012
+
1013
+ // Add clipboard UUID if available
1014
+ const clipboardUUID = await getClipboardUUID();
1015
+ if (clipboardUUID) {
1016
+ systemInfo.clipboardID = clipboardUUID;
1017
+ verboseLog(`Found valid clipboard UUID: ${clipboardUUID}`);
1018
+ } else {
1019
+ if (insertLinksClipboardEnabled) {
1020
+ verboseLog('Clipboard UUID not available - it may require NSPasteboardGeneralUseDescription in Info.plist');
1021
+ } else {
1022
+ verboseLog('Clipboard access is disabled - it may require NSPasteboardGeneralUseDescription in Info.plist');
1023
+ }
1024
+ }
1025
+
1026
+ // Add language information using Intl API
1027
+ try {
1028
+ // Get locale with region information
1029
+ const locale = Intl.DateTimeFormat().resolvedOptions().locale;
1030
+ const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1031
+
1032
+ // Try to get more specific locale information
1033
+ let bestLocale = locale;
1034
+
1035
+ // If the locale doesn't have region info, try to infer from timezone
1036
+ if (!locale.includes('-') && timeZone) {
1037
+ try {
1038
+ // Create a locale-specific date formatter to get region
1039
+ const regionLocale = new Intl.DateTimeFormat(undefined, {
1040
+ timeZone: timeZone
1041
+ }).resolvedOptions().locale;
1042
+
1043
+ if (regionLocale && regionLocale.includes('-')) {
1044
+ bestLocale = regionLocale;
1045
+ }
1046
+ } catch (e) {
1047
+ // Fallback to original locale
1048
+ }
1049
+ }
1050
+
1051
+ // Try navigator.language as fallback for better region detection
1052
+ if (!bestLocale.includes('-') && typeof navigator !== 'undefined' && navigator.language) {
1053
+ bestLocale = navigator.language;
1054
+ }
1055
+
1056
+ const parts = bestLocale.split('-');
1057
+ systemInfo.language = parts[0] || 'en';
1058
+ systemInfo.country = parts[1] || null; // Set to null instead of defaulting to 'US'
1059
+ systemInfo.languages = [bestLocale, parts[0] || 'en'];
1060
+ } catch (error) {
1061
+ verboseLog(`Error getting device locale: ${error}`);
1062
+ // Fallback to defaults
1063
+ systemInfo.language = 'en';
1064
+ systemInfo.country = null; // Set to null instead of defaulting to 'US'
1065
+ systemInfo.languages = ['en'];
1066
+ }
1067
+
1068
+ // Add timezone info (matching exact field names)
1069
+ const timezoneOffset = new Date().getTimezoneOffset();
1070
+ systemInfo.timezoneOffset = -timezoneOffset;
1071
+ systemInfo.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
1072
+
1073
+ // Add browser and platform info (matching exact field names)
1074
+ systemInfo.browserVersion = systemInfo.systemVersion;
1075
+ systemInfo.platform = systemInfo.systemName;
1076
+ systemInfo.os = systemInfo.systemName;
1077
+ systemInfo.osVersion = systemInfo.systemVersion;
1078
+
1079
+ // Add network connection info
1080
+ verboseLog('Getting network info');
1081
+
1082
+ const networkInfo = await getNetworkInfo();
1083
+ const pathInfo = await getNetworkPathInfo();
1084
+
1085
+ verboseLog(`Network info: ${JSON.stringify(networkInfo)}`);
1086
+ verboseLog(`Network path info: ${JSON.stringify(pathInfo)}`);
1087
+
1088
+ systemInfo.networkInfo = networkInfo;
1089
+ systemInfo.networkPath = pathInfo;
1090
+
1091
+ // Update connection info with real data
1092
+ const connection: {[key: string]: any} = {};
1093
+ connection.type = networkInfo.connectionType || 'unknown';
1094
+ connection.isExpensive = networkInfo.isExpensive || false;
1095
+ connection.isConstrained = networkInfo.isConstrained || false;
1096
+ connection.status = networkInfo.status || 'unknown';
1097
+ connection.interfaces = networkInfo.availableInterfaces || [];
1098
+ connection.supportsIPv4 = pathInfo.supportsIPv4 || true;
1099
+ connection.supportsIPv6 = pathInfo.supportsIPv6 || false;
1100
+ connection.supportsDNS = pathInfo.supportsDNS || true;
1101
+
1102
+ // Keep legacy fields for compatibility
1103
+ connection.downlink = networkInfo.connectionType === 'wifi' ? 100 : 10;
1104
+ connection.effectiveType = networkInfo.connectionType === 'wifi' ? '4g' : '3g';
1105
+ connection.rtt = networkInfo.connectionType === 'wifi' ? 20 : 100;
1106
+ connection.saveData = networkInfo.isConstrained || false;
1107
+
1108
+ systemInfo.connection = connection;
1109
+
1110
+ verboseLog(`Enhanced system info collected: ${JSON.stringify(systemInfo)}`);
1111
+
1112
+ return systemInfo;
1113
+ } catch (error) {
1114
+ verboseLog(`Error collecting enhanced system info: ${error}`);
1115
+ return systemInfo;
1116
+ }
1117
+ };
1118
+
1119
+ // Sends enhanced system info to the backend API for deep link event tracking
1120
+ const sendSystemInfoToBackend = async (systemInfo: {[key: string]: any}): Promise<void> => {
1121
+ if (verboseLogging) {
1122
+ console.log('[Insert Affiliate] Sending system info to backend...');
1123
+ }
1124
+
1125
+ try {
1126
+ const apiUrlString = 'https://insertaffiliate.link/V1/appDeepLinkEvents';
1127
+
1128
+ verboseLog(`Sending request to: ${apiUrlString}`);
1129
+
1130
+ const response = await axios.post(apiUrlString, systemInfo, {
1131
+ headers: {
1132
+ 'Content-Type': 'application/json',
1133
+ },
1134
+ });
1135
+
1136
+ verboseLog(`System info response status: ${response.status}`);
1137
+ if (response.data) {
1138
+ verboseLog(`System info response: ${JSON.stringify(response.data)}`);
1139
+ }
1140
+
1141
+ // Try to parse backend response and persist matched short code if present
1142
+ if (response.data && typeof response.data === 'object') {
1143
+ const matchFound = response.data.matchFound || false;
1144
+ if (matchFound && response.data.matched_affiliate_shortCode && response.data.matched_affiliate_shortCode.length > 0) {
1145
+ const matchedShortCode = response.data.matched_affiliate_shortCode;
1146
+ verboseLog(`Storing Matched short code from backend: ${matchedShortCode}`);
1147
+
1148
+ await storeInsertAffiliateIdentifier({ link: matchedShortCode });
1149
+ }
1150
+ }
1151
+
1152
+ // Check for a successful response
1153
+ if (response.status >= 200 && response.status <= 299) {
1154
+ verboseLog('System info sent successfully');
1155
+ } else {
1156
+ verboseLog(`Failed to send system info with status code: ${response.status}`);
1157
+ if (response.data) {
1158
+ verboseLog(`Error response: ${JSON.stringify(response.data)}`);
1159
+ }
1160
+ }
1161
+ } catch (error) {
1162
+ verboseLog(`Error sending system info: ${error}`);
1163
+ verboseLog(`Network error sending system info: ${error}`);
1164
+ }
1165
+ };
1166
+
263
1167
  // MARK: Short Codes
264
1168
  const isShortCode = (referringLink: string): boolean => {
265
1169
  // Short codes are 3-25 characters and can include underscores
@@ -315,10 +1219,20 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
315
1219
  };
316
1220
 
317
1221
  // MARK: Return Insert Affiliate Identifier
318
- // Instead of just reading React state
319
- const returnInsertAffiliateIdentifier = async (): Promise<string | null> => {
1222
+ const returnInsertAffiliateIdentifier = async (ignoreTimeout: boolean = false): Promise<string | null> => {
320
1223
  try {
321
- verboseLog('Getting insert affiliate identifier...');
1224
+ verboseLog(`Getting insert affiliate identifier (ignoreTimeout: ${ignoreTimeout})...`);
1225
+
1226
+ // If timeout is enabled and we're not ignoring it, check validity first
1227
+ if (!ignoreTimeout && affiliateAttributionActiveTime) {
1228
+ const isValid = await isAffiliateAttributionValid();
1229
+ if (!isValid) {
1230
+ verboseLog('Attribution has expired, returning null');
1231
+ return null;
1232
+ }
1233
+ }
1234
+
1235
+ // Now get the actual identifier
322
1236
  verboseLog(`React state - referrerLink: ${referrerLink || 'empty'}, userId: ${userId || 'empty'}`);
323
1237
 
324
1238
  // Try React state first
@@ -332,10 +1246,17 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
332
1246
 
333
1247
  // Fallback to async storage if React state is empty
334
1248
  const storedLink = await getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
335
- const storedUserId = await getValueFromAsync(ASYNC_KEYS.USER_ID);
1249
+ let storedUserId = await getValueFromAsync(ASYNC_KEYS.USER_ID);
336
1250
 
337
1251
  verboseLog(`AsyncStorage - storedLink: ${storedLink || 'empty'}, storedUserId: ${storedUserId || 'empty'}`);
338
1252
 
1253
+ // If we have a stored link but no user ID, generate one now
1254
+ if (storedLink && !storedUserId) {
1255
+ verboseLog('Found stored link but no user ID, generating user ID now...');
1256
+ storedUserId = await generateThenSetUserID();
1257
+ verboseLog(`Generated user ID: ${storedUserId}`);
1258
+ }
1259
+
339
1260
  if (storedLink && storedUserId) {
340
1261
  const identifier = `${storedLink}-${storedUserId}`;
341
1262
  verboseLog(`Found identifier in AsyncStorage: ${identifier}`);
@@ -350,6 +1271,54 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
350
1271
  }
351
1272
  };
352
1273
 
1274
+ // MARK: Attribution Timeout Functions
1275
+
1276
+ // Check if the current affiliate attribution is still valid based on timeout
1277
+ const isAffiliateAttributionValid = async (): Promise<boolean> => {
1278
+ try {
1279
+ // If no timeout is set, attribution is always valid
1280
+ if (!affiliateAttributionActiveTime) {
1281
+ verboseLog('No attribution timeout set, attribution is valid');
1282
+ return true;
1283
+ }
1284
+
1285
+ const storedDate = await getAffiliateStoredDate();
1286
+ if (!storedDate) {
1287
+ verboseLog('No stored date found, attribution is invalid');
1288
+ return false;
1289
+ }
1290
+
1291
+ const now = new Date();
1292
+ const timeDifferenceSeconds = Math.floor((now.getTime() - storedDate.getTime()) / 1000);
1293
+ const isValid = timeDifferenceSeconds <= affiliateAttributionActiveTime;
1294
+
1295
+ verboseLog(`Attribution timeout check: stored=${storedDate.toISOString()}, now=${now.toISOString()}, diff=${timeDifferenceSeconds}s, timeout=${affiliateAttributionActiveTime}s, valid=${isValid}`);
1296
+
1297
+ return isValid;
1298
+ } catch (error) {
1299
+ verboseLog(`Error checking attribution validity: ${error}`);
1300
+ return false;
1301
+ }
1302
+ };
1303
+
1304
+ // Get the date when the affiliate identifier was stored
1305
+ const getAffiliateStoredDate = async (): Promise<Date | null> => {
1306
+ try {
1307
+ const storedDateString = await getValueFromAsync(ASYNC_KEYS.AFFILIATE_STORED_DATE);
1308
+ if (!storedDateString) {
1309
+ verboseLog('No affiliate stored date found');
1310
+ return null;
1311
+ }
1312
+
1313
+ const storedDate = new Date(storedDateString);
1314
+ verboseLog(`Retrieved affiliate stored date: ${storedDate.toISOString()}`);
1315
+ return storedDate;
1316
+ } catch (error) {
1317
+ verboseLog(`Error getting affiliate stored date: ${error}`);
1318
+ return null;
1319
+ }
1320
+ };
1321
+
353
1322
  // MARK: Insert Affiliate Identifier
354
1323
 
355
1324
  async function setInsertAffiliateIdentifier(
@@ -447,15 +1416,36 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
447
1416
 
448
1417
  async function storeInsertAffiliateIdentifier({ link }: { link: string }) {
449
1418
  console.log(`[Insert Affiliate] Storing affiliate identifier: ${link}`);
1419
+
1420
+ // Check if we're trying to store the same link (prevent duplicate storage)
1421
+ const existingLink = await getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
1422
+ if (existingLink === link) {
1423
+ verboseLog(`Link ${link} is already stored, skipping duplicate storage`);
1424
+ return;
1425
+ }
1426
+
450
1427
  verboseLog(`Updating React state with referrer link: ${link}`);
451
1428
  setReferrerLink(link);
452
1429
  verboseLog(`Saving referrer link to AsyncStorage...`);
453
1430
  await saveValueInAsync(ASYNC_KEYS.REFERRER_LINK, link);
1431
+
1432
+ // Store the current date/time when the affiliate identifier is stored
1433
+ const currentDate = new Date().toISOString();
1434
+ verboseLog(`Saving affiliate stored date: ${currentDate}`);
1435
+ await saveValueInAsync(ASYNC_KEYS.AFFILIATE_STORED_DATE, currentDate);
1436
+
454
1437
  verboseLog(`Referrer link saved to AsyncStorage successfully`);
455
1438
 
456
1439
  // Automatically fetch and store offer code for any affiliate identifier
457
1440
  verboseLog('Attempting to fetch offer code for stored affiliate identifier...');
458
1441
  await retrieveAndStoreOfferCode(link);
1442
+
1443
+ // Trigger callback with the current affiliate identifier
1444
+ if (insertAffiliateIdentifierChangeCallbackRef.current) {
1445
+ const currentIdentifier = await returnInsertAffiliateIdentifier();
1446
+ verboseLog(`Triggering callback with identifier: ${currentIdentifier}`);
1447
+ insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier);
1448
+ }
459
1449
  }
460
1450
 
461
1451
  const validatePurchaseWithIapticAPI = async (
@@ -749,11 +1739,15 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
749
1739
  OfferCode,
750
1740
  setShortCode,
751
1741
  returnInsertAffiliateIdentifier,
1742
+ isAffiliateAttributionValid,
1743
+ getAffiliateStoredDate,
752
1744
  storeExpectedStoreTransaction,
753
1745
  returnUserAccountTokenAndStoreExpectedTransaction,
754
1746
  validatePurchaseWithIapticAPI,
755
1747
  trackEvent,
756
1748
  setInsertAffiliateIdentifier,
1749
+ setInsertAffiliateIdentifierChangeCallback: setInsertAffiliateIdentifierChangeCallbackHandler,
1750
+ handleInsertLinks,
757
1751
  initialize,
758
1752
  isInitialized,
759
1753
  }}