insert-affiliate-react-native-sdk 1.9.0 → 1.10.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.
@@ -80,8 +80,19 @@ const ASYNC_KEYS = {
80
80
  USER_ACCOUNT_TOKEN: '@app_user_account_token',
81
81
  IOS_OFFER_CODE: '@app_ios_offer_code',
82
82
  AFFILIATE_STORED_DATE: '@app_affiliate_stored_date',
83
+ SDK_INIT_REPORTED: '@app_sdk_init_reported',
84
+ REPORTED_AFFILIATE_ASSOCIATIONS: '@app_reported_affiliate_associations',
83
85
  };
84
86
 
87
+ // Source types for affiliate association tracking
88
+ type AffiliateAssociationSource =
89
+ | 'deep_link_ios' // iOS custom URL scheme (ia-companycode://shortcode)
90
+ | 'deep_link_android' // Android deep link with ?insertAffiliate= param
91
+ | 'install_referrer' // Android Play Store install referrer
92
+ | 'clipboard_match' // iOS clipboard UUID match from backend
93
+ | 'short_code_manual' // Developer called setShortCode()
94
+ | 'referring_link'; // Developer called setInsertAffiliateIdentifier()
95
+
85
96
  // STARTING CONTEXT IMPLEMENTATION
86
97
  export const DeepLinkIapContext = createContext<T_DEEPLINK_IAP_CONTEXT>({
87
98
  referrerLink: '',
@@ -153,6 +164,9 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
153
164
  console.log('[Insert Affiliate] [VERBOSE] Company code saved to AsyncStorage');
154
165
  console.log('[Insert Affiliate] [VERBOSE] SDK marked as initialized');
155
166
  }
167
+
168
+ // Report SDK initialization for onboarding verification (fire and forget)
169
+ reportSdkInitIfNeeded(companyCode, verboseLogging);
156
170
  } else {
157
171
  console.warn(
158
172
  '[Insert Affiliate] SDK initialized without a company code.'
@@ -389,14 +403,26 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
389
403
  return false;
390
404
  }
391
405
 
392
- // Parse the URL to extract query parameters
393
- const urlObj = new URL(url);
394
- const insertAffiliate = urlObj.searchParams.get('insertAffiliate');
395
-
406
+ // Parse the URL to extract query parameters (React Native compatible)
407
+ // URLSearchParams is not available in React Native, so parse manually
408
+ let insertAffiliate: string | null = null;
409
+ const queryIndex = url.indexOf('?');
410
+ if (queryIndex !== -1) {
411
+ const queryString = url.substring(queryIndex + 1);
412
+ const params = queryString.split('&');
413
+ for (const param of params) {
414
+ const [key, value] = param.split('=');
415
+ if (key === 'insertAffiliate' && value) {
416
+ insertAffiliate = decodeURIComponent(value);
417
+ break;
418
+ }
419
+ }
420
+ }
421
+
396
422
  if (insertAffiliate && insertAffiliate.length > 0) {
397
423
  verboseLog(`Found insertAffiliate parameter: ${insertAffiliate}`);
398
424
 
399
- await storeInsertAffiliateIdentifier({ link: insertAffiliate });
425
+ await storeInsertAffiliateIdentifier({ link: insertAffiliate, source: 'deep_link_android' });
400
426
 
401
427
  return true;
402
428
  } else {
@@ -525,7 +551,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
525
551
  // If we have insertAffiliate parameter, use it as the affiliate identifier
526
552
  if (insertAffiliate && insertAffiliate.length > 0) {
527
553
  verboseLog(`Found insertAffiliate parameter, setting as affiliate identifier: ${insertAffiliate}`);
528
- await storeInsertAffiliateIdentifier({ link: insertAffiliate });
554
+ await storeInsertAffiliateIdentifier({ link: insertAffiliate, source: 'install_referrer' });
529
555
 
530
556
  return true;
531
557
  } else {
@@ -602,7 +628,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
602
628
  }
603
629
 
604
630
  // If URL scheme is used, we can straight away store the short code as the referring link
605
- await storeInsertAffiliateIdentifier({ link: shortCode });
631
+ await storeInsertAffiliateIdentifier({ link: shortCode, source: 'deep_link_ios' });
606
632
 
607
633
  // Collect and send enhanced system info to backend
608
634
  try {
@@ -736,6 +762,96 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
736
762
  }
737
763
  };
738
764
 
765
+ // Reports a new affiliate association to the backend for tracking.
766
+ // Only reports each unique affiliateIdentifier once to prevent duplicates.
767
+ const reportAffiliateAssociationIfNeeded = async (
768
+ affiliateIdentifier: string,
769
+ source: AffiliateAssociationSource
770
+ ): Promise<void> => {
771
+ try {
772
+ const activeCompanyCode = await getActiveCompanyCode();
773
+ if (!activeCompanyCode) {
774
+ verboseLog('Cannot report affiliate association: no company code available');
775
+ return;
776
+ }
777
+
778
+ // Get the set of already-reported affiliate identifiers
779
+ const reportedAssociationsJson = await AsyncStorage.getItem(ASYNC_KEYS.REPORTED_AFFILIATE_ASSOCIATIONS);
780
+ const reportedAssociations: string[] = reportedAssociationsJson ? JSON.parse(reportedAssociationsJson) : [];
781
+
782
+ // Check if this affiliate identifier has already been reported
783
+ if (reportedAssociations.includes(affiliateIdentifier)) {
784
+ verboseLog(`Affiliate association already reported for: ${affiliateIdentifier}, skipping`);
785
+ return;
786
+ }
787
+
788
+ verboseLog(`Reporting new affiliate association: ${affiliateIdentifier} (source: ${source})`);
789
+
790
+ const response = await fetch('https://api.insertaffiliate.com/V1/onboarding/affiliate-associated', {
791
+ method: 'POST',
792
+ headers: {
793
+ 'Content-Type': 'application/json',
794
+ },
795
+ body: JSON.stringify({
796
+ companyId: activeCompanyCode,
797
+ affiliateIdentifier: affiliateIdentifier,
798
+ source: source,
799
+ timestamp: new Date().toISOString(),
800
+ }),
801
+ });
802
+
803
+ if (response.ok) {
804
+ // Add to reported set and persist
805
+ reportedAssociations.push(affiliateIdentifier);
806
+ await AsyncStorage.setItem(ASYNC_KEYS.REPORTED_AFFILIATE_ASSOCIATIONS, JSON.stringify(reportedAssociations));
807
+ verboseLog(`Affiliate association reported successfully for: ${affiliateIdentifier}`);
808
+ } else {
809
+ verboseLog(`Affiliate association report failed with status: ${response.status}`);
810
+ }
811
+ } catch (error) {
812
+ // Silently fail - this is non-critical telemetry
813
+ verboseLog(`Affiliate association report error: ${error}`);
814
+ }
815
+ };
816
+
817
+ // Reports SDK initialization to the backend for onboarding verification.
818
+ // Only reports once per install to minimize server load.
819
+ const reportSdkInitIfNeeded = async (companyCode: string, verboseLogging: boolean): Promise<void> => {
820
+ try {
821
+ // Only report once per install
822
+ const alreadyReported = await AsyncStorage.getItem(ASYNC_KEYS.SDK_INIT_REPORTED);
823
+ if (alreadyReported === 'true') {
824
+ return;
825
+ }
826
+
827
+ if (verboseLogging) {
828
+ console.log('[Insert Affiliate] Reporting SDK initialization for onboarding verification...');
829
+ }
830
+
831
+ const response = await fetch('https://api.insertaffiliate.com/V1/onboarding/sdk-init', {
832
+ method: 'POST',
833
+ headers: {
834
+ 'Content-Type': 'application/json',
835
+ },
836
+ body: JSON.stringify({ companyId: companyCode }),
837
+ });
838
+
839
+ if (response.ok) {
840
+ await AsyncStorage.setItem(ASYNC_KEYS.SDK_INIT_REPORTED, 'true');
841
+ if (verboseLogging) {
842
+ console.log('[Insert Affiliate] SDK initialization reported successfully');
843
+ }
844
+ } else if (verboseLogging) {
845
+ console.log(`[Insert Affiliate] SDK initialization report failed with status: ${response.status}`);
846
+ }
847
+ } catch (error) {
848
+ // Silently fail - this is non-critical telemetry
849
+ if (verboseLogging) {
850
+ console.log(`[Insert Affiliate] SDK initialization report error: ${error}`);
851
+ }
852
+ }
853
+ };
854
+
739
855
  // MARK: - Deep Linking Utilities
740
856
 
741
857
  // Retrieves and validates clipboard content for UUID format
@@ -1154,9 +1270,9 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1154
1270
  const matchFound = response.data.matchFound || false;
1155
1271
  if (matchFound && response.data.matched_affiliate_shortCode && response.data.matched_affiliate_shortCode.length > 0) {
1156
1272
  const matchedShortCode = response.data.matched_affiliate_shortCode;
1157
- verboseLog(`Storing Matched short code from backend: ${matchedShortCode}`);
1158
-
1159
- await storeInsertAffiliateIdentifier({ link: matchedShortCode });
1273
+ verboseLog(`Storing Matched short code from backend: ${matchedShortCode}`);
1274
+
1275
+ await storeInsertAffiliateIdentifier({ link: matchedShortCode, source: 'clipboard_match' });
1160
1276
  }
1161
1277
  }
1162
1278
 
@@ -1282,7 +1398,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1282
1398
 
1283
1399
  if (exists) {
1284
1400
  // If affiliate exists, set the Insert Affiliate Identifier
1285
- await storeInsertAffiliateIdentifier({ link: capitalisedShortCode });
1401
+ await storeInsertAffiliateIdentifier({ link: capitalisedShortCode, source: 'short_code_manual' });
1286
1402
  console.log(`[Insert Affiliate] Short code ${capitalisedShortCode} validated and stored successfully.`);
1287
1403
  return true;
1288
1404
  } else {
@@ -1457,7 +1573,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1457
1573
  if (!referringLink) {
1458
1574
  console.warn('[Insert Affiliate] Referring link is invalid.');
1459
1575
  verboseLog('Referring link is empty or invalid, storing as-is');
1460
- await storeInsertAffiliateIdentifier({ link: referringLink });
1576
+ await storeInsertAffiliateIdentifier({ link: referringLink, source: 'referring_link' });
1461
1577
  return `${referringLink}-${customerID}`;
1462
1578
  }
1463
1579
 
@@ -1481,7 +1597,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1481
1597
  '[Insert Affiliate] Referring link is already a short code.'
1482
1598
  );
1483
1599
  verboseLog('Link is already a short code, storing directly');
1484
- await storeInsertAffiliateIdentifier({ link: referringLink });
1600
+ await storeInsertAffiliateIdentifier({ link: referringLink, source: 'referring_link' });
1485
1601
  return `${referringLink}-${customerID}`;
1486
1602
  }
1487
1603
 
@@ -1494,7 +1610,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1494
1610
  if (!encodedAffiliateLink) {
1495
1611
  console.error('[Insert Affiliate] Failed to encode affiliate link.');
1496
1612
  verboseLog('Failed to encode link, storing original');
1497
- await storeInsertAffiliateIdentifier({ link: referringLink });
1613
+ await storeInsertAffiliateIdentifier({ link: referringLink, source: 'referring_link' });
1498
1614
  return `${referringLink}-${customerID}`;
1499
1615
  }
1500
1616
 
@@ -1517,14 +1633,14 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1517
1633
  console.log('[Insert Affiliate] Short link received:', shortLink);
1518
1634
  verboseLog(`Successfully converted to short link: ${shortLink}`);
1519
1635
  verboseLog('Storing short link to AsyncStorage...');
1520
- await storeInsertAffiliateIdentifier({ link: shortLink });
1636
+ await storeInsertAffiliateIdentifier({ link: shortLink, source: 'referring_link' });
1521
1637
  verboseLog('Short link stored successfully');
1522
1638
  return `${shortLink}-${customerID}`;
1523
1639
  } else {
1524
1640
  console.warn('[Insert Affiliate] Unexpected response format.');
1525
1641
  verboseLog(`Unexpected API response. Status: ${response.status}, Data: ${JSON.stringify(response.data)}`);
1526
1642
  verboseLog('Storing original link as fallback');
1527
- await storeInsertAffiliateIdentifier({ link: referringLink });
1643
+ await storeInsertAffiliateIdentifier({ link: referringLink, source: 'referring_link' });
1528
1644
  return `${referringLink}-${customerID}`;
1529
1645
  }
1530
1646
  } catch (error) {
@@ -1533,38 +1649,44 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1533
1649
  }
1534
1650
  };
1535
1651
 
1536
- async function storeInsertAffiliateIdentifier({ link }: { link: string }) {
1537
- console.log(`[Insert Affiliate] Storing affiliate identifier: ${link}`);
1538
-
1652
+ async function storeInsertAffiliateIdentifier({ link, source }: { link: string; source: AffiliateAssociationSource }) {
1653
+ console.log(`[Insert Affiliate] Storing affiliate identifier: ${link} (source: ${source})`);
1654
+
1539
1655
  // Check if we're trying to store the same link (prevent duplicate storage)
1540
1656
  const existingLink = await getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
1541
1657
  if (existingLink === link) {
1542
1658
  verboseLog(`Link ${link} is already stored, skipping duplicate storage`);
1543
1659
  return;
1544
1660
  }
1545
-
1661
+
1546
1662
  verboseLog(`Updating React state with referrer link: ${link}`);
1547
1663
  setReferrerLink(link);
1548
1664
  verboseLog(`Saving referrer link to AsyncStorage...`);
1549
1665
  await saveValueInAsync(ASYNC_KEYS.REFERRER_LINK, link);
1550
-
1666
+
1551
1667
  // Store the current date/time when the affiliate identifier is stored
1552
1668
  const currentDate = new Date().toISOString();
1553
1669
  verboseLog(`Saving affiliate stored date: ${currentDate}`);
1554
1670
  await saveValueInAsync(ASYNC_KEYS.AFFILIATE_STORED_DATE, currentDate);
1555
-
1671
+
1556
1672
  verboseLog(`Referrer link saved to AsyncStorage successfully`);
1557
-
1673
+
1558
1674
  // Automatically fetch and store offer code for any affiliate identifier
1559
1675
  verboseLog('Attempting to fetch offer code for stored affiliate identifier...');
1560
1676
  await retrieveAndStoreOfferCode(link);
1561
-
1677
+
1562
1678
  // Trigger callback with the current affiliate identifier
1563
1679
  if (insertAffiliateIdentifierChangeCallbackRef.current) {
1564
1680
  const currentIdentifier = await returnInsertAffiliateIdentifier();
1565
1681
  verboseLog(`Triggering callback with identifier: ${currentIdentifier}`);
1566
1682
  insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier);
1567
1683
  }
1684
+
1685
+ // Report this new affiliate association to the backend (fire and forget)
1686
+ const fullIdentifier = await returnInsertAffiliateIdentifier();
1687
+ if (fullIdentifier) {
1688
+ reportAffiliateAssociationIfNeeded(fullIdentifier, source);
1689
+ }
1568
1690
  }
1569
1691
 
1570
1692
  const validatePurchaseWithIapticAPI = async (