insert-affiliate-react-native-sdk 1.11.2 → 1.13.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.
@@ -10,7 +10,10 @@
10
10
  "Bash(adb shell:*)",
11
11
  "Bash(adb shell am start:*)",
12
12
  "Bash(git checkout:*)",
13
- "Bash(git pull:*)"
13
+ "Bash(git pull:*)",
14
+ "Bash(ssh root@46.101.83.25 \"pm2 logs --lines 200\")",
15
+ "Bash(ssh:*)",
16
+ "Bash(gh api:*)"
14
17
  ],
15
18
  "deny": []
16
19
  }
@@ -2,7 +2,7 @@ import React from 'react';
2
2
  type T_DEEPLINK_IAP_PROVIDER = {
3
3
  children: React.ReactNode;
4
4
  };
5
- export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null) => void;
5
+ export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null, offerCode: string | null) => void;
6
6
  export type AffiliateDetails = {
7
7
  affiliateName: string;
8
8
  affiliateShortCode: string;
@@ -18,6 +18,7 @@ type T_DEEPLINK_IAP_CONTEXT = {
18
18
  returnInsertAffiliateIdentifier: (ignoreTimeout?: boolean) => Promise<string | null>;
19
19
  isAffiliateAttributionValid: () => Promise<boolean>;
20
20
  getAffiliateStoredDate: () => Promise<Date | null>;
21
+ getAffiliateExpiryTimestamp: () => Promise<number | null>;
21
22
  validatePurchaseWithIapticAPI: (jsonIapPurchase: CustomPurchase, iapticAppId: string, iapticAppName: string, iapticPublicKey: string) => Promise<boolean>;
22
23
  returnUserAccountTokenAndStoreExpectedTransaction: () => Promise<string | null>;
23
24
  storeExpectedStoreTransaction: (purchaseToken: string) => Promise<void>;
@@ -27,7 +28,7 @@ type T_DEEPLINK_IAP_CONTEXT = {
27
28
  setInsertAffiliateIdentifier: (referringLink: string) => Promise<void | string>;
28
29
  setInsertAffiliateIdentifierChangeCallback: (callback: InsertAffiliateIdentifierChangeCallback | null) => void;
29
30
  handleInsertLinks: (url: string) => Promise<boolean>;
30
- initialize: (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number) => Promise<void>;
31
+ initialize: (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number, preventAffiliateTransfer?: boolean) => Promise<void>;
31
32
  isInitialized: boolean;
32
33
  };
33
34
  export declare const DeepLinkIapContext: React.Context<T_DEEPLINK_IAP_CONTEXT>;
@@ -52,7 +52,7 @@ const ASYNC_KEYS = {
52
52
  USER_ID: '@app_user_id',
53
53
  COMPANY_CODE: '@app_company_code',
54
54
  USER_ACCOUNT_TOKEN: '@app_user_account_token',
55
- IOS_OFFER_CODE: '@app_ios_offer_code',
55
+ OFFER_CODE: '@app_offer_code',
56
56
  AFFILIATE_STORED_DATE: '@app_affiliate_stored_date',
57
57
  SDK_INIT_REPORTED: '@app_sdk_init_reported',
58
58
  REPORTED_AFFILIATE_ASSOCIATIONS: '@app_reported_affiliate_associations',
@@ -65,6 +65,7 @@ exports.DeepLinkIapContext = (0, react_1.createContext)({
65
65
  returnInsertAffiliateIdentifier: (ignoreTimeout) => __awaiter(void 0, void 0, void 0, function* () { return ''; }),
66
66
  isAffiliateAttributionValid: () => __awaiter(void 0, void 0, void 0, function* () { return false; }),
67
67
  getAffiliateStoredDate: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
68
+ getAffiliateExpiryTimestamp: () => __awaiter(void 0, void 0, void 0, function* () { return null; }),
68
69
  validatePurchaseWithIapticAPI: (jsonIapPurchase, iapticAppId, iapticAppName, iapticPublicKey) => __awaiter(void 0, void 0, void 0, function* () { return false; }),
69
70
  returnUserAccountTokenAndStoreExpectedTransaction: () => __awaiter(void 0, void 0, void 0, function* () { return ''; }),
70
71
  storeExpectedStoreTransaction: (purchaseToken) => __awaiter(void 0, void 0, void 0, function* () { }),
@@ -74,7 +75,7 @@ exports.DeepLinkIapContext = (0, react_1.createContext)({
74
75
  setInsertAffiliateIdentifier: (referringLink) => __awaiter(void 0, void 0, void 0, function* () { }),
75
76
  setInsertAffiliateIdentifierChangeCallback: (callback) => { },
76
77
  handleInsertLinks: (url) => __awaiter(void 0, void 0, void 0, function* () { return false; }),
77
- initialize: (code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime) => __awaiter(void 0, void 0, void 0, function* () { }),
78
+ initialize: (code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime, preventAffiliateTransfer) => __awaiter(void 0, void 0, void 0, function* () { }),
78
79
  isInitialized: false,
79
80
  });
80
81
  const DeepLinkIapProvider = ({ children, }) => {
@@ -87,11 +88,13 @@ const DeepLinkIapProvider = ({ children, }) => {
87
88
  const [insertLinksClipboardEnabled, setInsertLinksClipboardEnabled] = (0, react_1.useState)(false);
88
89
  const [OfferCode, setOfferCode] = (0, react_1.useState)(null);
89
90
  const [affiliateAttributionActiveTime, setAffiliateAttributionActiveTime] = (0, react_1.useState)(null);
91
+ const [preventAffiliateTransfer, setPreventAffiliateTransfer] = (0, react_1.useState)(false);
90
92
  const insertAffiliateIdentifierChangeCallbackRef = (0, react_1.useRef)(null);
91
93
  const isInitializingRef = (0, react_1.useRef)(false);
92
94
  // Refs for values that need to be current inside callbacks (to avoid stale closures)
93
95
  const companyCodeRef = (0, react_1.useRef)(null);
94
96
  const verboseLoggingRef = (0, react_1.useRef)(false);
97
+ const preventAffiliateTransferRef = (0, react_1.useRef)(false);
95
98
  // Refs for implementation functions (ref callback pattern for stable + fresh)
96
99
  const initializeImplRef = (0, react_1.useRef)(null);
97
100
  const setShortCodeImplRef = (0, react_1.useRef)(null);
@@ -99,6 +102,7 @@ const DeepLinkIapProvider = ({ children, }) => {
99
102
  const returnInsertAffiliateIdentifierImplRef = (0, react_1.useRef)(null);
100
103
  const isAffiliateAttributionValidImplRef = (0, react_1.useRef)(null);
101
104
  const getAffiliateStoredDateImplRef = (0, react_1.useRef)(null);
105
+ const getAffiliateExpiryTimestampImplRef = (0, react_1.useRef)(null);
102
106
  const storeExpectedStoreTransactionImplRef = (0, react_1.useRef)(null);
103
107
  const returnUserAccountTokenAndStoreExpectedTransactionImplRef = (0, react_1.useRef)(null);
104
108
  const validatePurchaseWithIapticAPIImplRef = (0, react_1.useRef)(null);
@@ -106,7 +110,7 @@ const DeepLinkIapProvider = ({ children, }) => {
106
110
  const setInsertAffiliateIdentifierImplRef = (0, react_1.useRef)(null);
107
111
  const handleInsertLinksImplRef = (0, react_1.useRef)(null);
108
112
  // MARK: Initialize the SDK
109
- const initializeImpl = (companyCodeParam_1, ...args_1) => __awaiter(void 0, [companyCodeParam_1, ...args_1], void 0, function* (companyCodeParam, verboseLoggingParam = false, insertLinksEnabledParam = false, insertLinksClipboardEnabledParam = false, affiliateAttributionActiveTimeParam) {
113
+ const initializeImpl = (companyCodeParam_1, ...args_1) => __awaiter(void 0, [companyCodeParam_1, ...args_1], void 0, function* (companyCodeParam, verboseLoggingParam = false, insertLinksEnabledParam = false, insertLinksClipboardEnabledParam = false, affiliateAttributionActiveTimeParam, preventAffiliateTransferParam = false) {
110
114
  // Prevent multiple concurrent initialization attempts
111
115
  if (isInitialized || isInitializingRef.current) {
112
116
  return;
@@ -119,6 +123,8 @@ const DeepLinkIapProvider = ({ children, }) => {
119
123
  if (affiliateAttributionActiveTimeParam !== undefined) {
120
124
  setAffiliateAttributionActiveTime(affiliateAttributionActiveTimeParam);
121
125
  }
126
+ setPreventAffiliateTransfer(preventAffiliateTransferParam);
127
+ preventAffiliateTransferRef.current = preventAffiliateTransferParam;
122
128
  if (verboseLoggingParam) {
123
129
  console.log('[Insert Affiliate] [VERBOSE] Starting SDK initialization...');
124
130
  console.log('[Insert Affiliate] [VERBOSE] Company code provided:', companyCodeParam ? 'Yes' : 'No');
@@ -163,11 +169,21 @@ const DeepLinkIapProvider = ({ children, }) => {
163
169
  const uId = yield getValueFromAsync(ASYNC_KEYS.USER_ID);
164
170
  const refLink = yield getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
165
171
  const companyCodeFromStorage = yield getValueFromAsync(ASYNC_KEYS.COMPANY_CODE);
166
- const storedOfferCode = yield getValueFromAsync(ASYNC_KEYS.IOS_OFFER_CODE);
172
+ // Migration: check new key first, fall back to legacy iOS key
173
+ let storedOfferCode = yield getValueFromAsync(ASYNC_KEYS.OFFER_CODE);
174
+ if (!storedOfferCode) {
175
+ const legacyOfferCode = yield getValueFromAsync('@app_ios_offer_code');
176
+ if (legacyOfferCode) {
177
+ storedOfferCode = legacyOfferCode;
178
+ // Migrate to new key
179
+ yield saveValueInAsync(ASYNC_KEYS.OFFER_CODE, legacyOfferCode);
180
+ verboseLog('Migrated offer code from legacy iOS key to new key');
181
+ }
182
+ }
167
183
  verboseLog(`User ID found: ${uId ? 'Yes' : 'No'}`);
168
184
  verboseLog(`Referrer link found: ${refLink ? 'Yes' : 'No'}`);
169
185
  verboseLog(`Company code found: ${companyCodeFromStorage ? 'Yes' : 'No'}`);
170
- verboseLog(`iOS Offer Code found: ${storedOfferCode ? 'Yes' : 'No'}`);
186
+ verboseLog(`Offer Code found: ${storedOfferCode ? 'Yes' : 'No'}`);
171
187
  if (uId && refLink) {
172
188
  setUserId(uId);
173
189
  setReferrerLink(refLink);
@@ -180,7 +196,7 @@ const DeepLinkIapProvider = ({ children, }) => {
180
196
  }
181
197
  if (storedOfferCode) {
182
198
  setOfferCode(storedOfferCode);
183
- verboseLog('iOS Offer Code restored from storage');
199
+ verboseLog('Offer Code restored from storage');
184
200
  }
185
201
  }
186
202
  catch (error) {
@@ -1166,7 +1182,7 @@ const DeepLinkIapProvider = ({ children, }) => {
1166
1182
  const isValidCharacters = /^[a-zA-Z0-9_]+$/.test(referringLink);
1167
1183
  return isValidCharacters && referringLink.length >= 3 && referringLink.length <= 25;
1168
1184
  };
1169
- const checkAffiliateExists = (affiliateCode) => __awaiter(void 0, void 0, void 0, function* () {
1185
+ const checkAffiliateExists = (affiliateCode_1, ...args_1) => __awaiter(void 0, [affiliateCode_1, ...args_1], void 0, function* (affiliateCode, trackUsage = false) {
1170
1186
  try {
1171
1187
  const activeCompanyCode = yield getActiveCompanyCode();
1172
1188
  if (!activeCompanyCode) {
@@ -1178,6 +1194,9 @@ const DeepLinkIapProvider = ({ children, }) => {
1178
1194
  companyId: activeCompanyCode,
1179
1195
  affiliateCode: affiliateCode
1180
1196
  };
1197
+ if (trackUsage) {
1198
+ payload.trackUsage = true;
1199
+ }
1181
1200
  verboseLog(`Checking if affiliate exists: ${affiliateCode}`);
1182
1201
  const response = yield axios_1.default.post(url, payload, {
1183
1202
  headers: {
@@ -1251,7 +1270,7 @@ const DeepLinkIapProvider = ({ children, }) => {
1251
1270
  const capitalisedShortCode = shortCode.toUpperCase();
1252
1271
  isShortCode(capitalisedShortCode);
1253
1272
  // Check if the affiliate exists before storing
1254
- const exists = yield checkAffiliateExists(capitalisedShortCode);
1273
+ const exists = yield checkAffiliateExists(capitalisedShortCode, true);
1255
1274
  if (exists) {
1256
1275
  // If affiliate exists, set the Insert Affiliate Identifier
1257
1276
  yield storeInsertAffiliateIdentifier({ link: capitalisedShortCode, source: 'short_code_manual' });
@@ -1393,6 +1412,29 @@ const DeepLinkIapProvider = ({ children, }) => {
1393
1412
  return null;
1394
1413
  }
1395
1414
  });
1415
+ // Get the timestamp when attribution expires (stored date + timeout duration in ms)
1416
+ // Returns null if no timeout is configured or no stored date exists
1417
+ const getAffiliateExpiryTimestampImpl = () => __awaiter(void 0, void 0, void 0, function* () {
1418
+ try {
1419
+ if (!affiliateAttributionActiveTime) {
1420
+ verboseLog('No attribution timeout configured, no expiry timestamp');
1421
+ return null;
1422
+ }
1423
+ const storedDate = yield getAffiliateStoredDateImpl();
1424
+ if (!storedDate) {
1425
+ verboseLog('No stored date found, cannot calculate expiry timestamp');
1426
+ return null;
1427
+ }
1428
+ // Convert timeout from seconds to milliseconds and add to stored date
1429
+ const expiryTimestamp = storedDate.getTime() + (affiliateAttributionActiveTime * 1000);
1430
+ verboseLog(`Attribution expiry timestamp: ${expiryTimestamp} (${new Date(expiryTimestamp).toISOString()})`);
1431
+ return expiryTimestamp;
1432
+ }
1433
+ catch (error) {
1434
+ verboseLog(`Error getting affiliate expiry timestamp: ${error}`);
1435
+ return null;
1436
+ }
1437
+ });
1396
1438
  // MARK: Insert Affiliate Identifier
1397
1439
  const setInsertAffiliateIdentifierImpl = (referringLink) => __awaiter(void 0, void 0, void 0, function* () {
1398
1440
  console.log('[Insert Affiliate] Setting affiliate identifier.');
@@ -1471,13 +1513,19 @@ const DeepLinkIapProvider = ({ children, }) => {
1471
1513
  });
1472
1514
  function storeInsertAffiliateIdentifier(_a) {
1473
1515
  return __awaiter(this, arguments, void 0, function* ({ link, source }) {
1474
- console.log(`[Insert Affiliate] Storing affiliate identifier: ${link} (source: ${source})`);
1516
+ verboseLog(`Storing affiliate identifier: ${link} (source: ${source})`);
1475
1517
  // Check if we're trying to store the same link (prevent duplicate storage)
1476
1518
  const existingLink = yield getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
1477
1519
  if (existingLink === link) {
1478
1520
  verboseLog(`Link ${link} is already stored, skipping duplicate storage`);
1479
1521
  return;
1480
1522
  }
1523
+ // Prevent transfer of affiliate if enabled - keep original affiliate
1524
+ verboseLog(`preventAffiliateTransfer check: enabled=${preventAffiliateTransferRef.current}, existingLink=${existingLink}, newLink=${link}`);
1525
+ if (preventAffiliateTransferRef.current && existingLink && existingLink !== link) {
1526
+ verboseLog(`Transfer blocked: existing affiliate "${existingLink}" protected from being replaced by "${link}"`);
1527
+ return;
1528
+ }
1481
1529
  verboseLog(`Updating React state with referrer link: ${link}`);
1482
1530
  setReferrerLink(link);
1483
1531
  verboseLog(`Saving referrer link to AsyncStorage...`);
@@ -1489,12 +1537,12 @@ const DeepLinkIapProvider = ({ children, }) => {
1489
1537
  verboseLog(`Referrer link saved to AsyncStorage successfully`);
1490
1538
  // Automatically fetch and store offer code for any affiliate identifier
1491
1539
  verboseLog('Attempting to fetch offer code for stored affiliate identifier...');
1492
- yield retrieveAndStoreOfferCode(link);
1493
- // Trigger callback with the current affiliate identifier
1540
+ const offerCode = yield retrieveAndStoreOfferCode(link);
1541
+ // Trigger callback with the current affiliate identifier and offer code
1494
1542
  if (insertAffiliateIdentifierChangeCallbackRef.current) {
1495
1543
  const currentIdentifier = yield returnInsertAffiliateIdentifierImpl();
1496
- verboseLog(`Triggering callback with identifier: ${currentIdentifier}`);
1497
- insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier);
1544
+ verboseLog(`Triggering callback with identifier: ${currentIdentifier}, offerCode: ${offerCode}`);
1545
+ insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier, offerCode);
1498
1546
  }
1499
1547
  // Report this new affiliate association to the backend (fire and forget)
1500
1548
  const fullIdentifier = yield returnInsertAffiliateIdentifierImpl();
@@ -1734,21 +1782,24 @@ const DeepLinkIapProvider = ({ children, }) => {
1734
1782
  const offerCode = yield fetchOfferCode(affiliateLink);
1735
1783
  if (offerCode && offerCode.length > 0) {
1736
1784
  // Store in both AsyncStorage and state
1737
- yield saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, offerCode);
1785
+ yield saveValueInAsync(ASYNC_KEYS.OFFER_CODE, offerCode);
1738
1786
  setOfferCode(offerCode);
1739
1787
  verboseLog(`Successfully stored offer code: ${offerCode}`);
1740
1788
  console.log('[Insert Affiliate] Offer code retrieved and stored successfully');
1789
+ return offerCode;
1741
1790
  }
1742
1791
  else {
1743
1792
  verboseLog('No valid offer code found to store');
1744
1793
  // Clear stored offer code if none found
1745
- yield saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, '');
1794
+ yield saveValueInAsync(ASYNC_KEYS.OFFER_CODE, '');
1746
1795
  setOfferCode(null);
1796
+ return null;
1747
1797
  }
1748
1798
  }
1749
1799
  catch (error) {
1750
1800
  console.error('[Insert Affiliate] Error retrieving and storing offer code:', error);
1751
1801
  verboseLog(`Error in retrieveAndStoreOfferCode: ${error}`);
1802
+ return null;
1752
1803
  }
1753
1804
  });
1754
1805
  const removeSpecialCharacters = (offerCode) => {
@@ -1768,6 +1819,7 @@ const DeepLinkIapProvider = ({ children, }) => {
1768
1819
  returnInsertAffiliateIdentifierImplRef.current = returnInsertAffiliateIdentifierImpl;
1769
1820
  isAffiliateAttributionValidImplRef.current = isAffiliateAttributionValidImpl;
1770
1821
  getAffiliateStoredDateImplRef.current = getAffiliateStoredDateImpl;
1822
+ getAffiliateExpiryTimestampImplRef.current = getAffiliateExpiryTimestampImpl;
1771
1823
  storeExpectedStoreTransactionImplRef.current = storeExpectedStoreTransactionImpl;
1772
1824
  returnUserAccountTokenAndStoreExpectedTransactionImplRef.current = returnUserAccountTokenAndStoreExpectedTransactionImpl;
1773
1825
  validatePurchaseWithIapticAPIImplRef.current = validatePurchaseWithIapticAPIImpl;
@@ -1778,8 +1830,8 @@ const DeepLinkIapProvider = ({ children, }) => {
1778
1830
  // STABLE WRAPPERS: useCallback with [] deps that delegate to refs
1779
1831
  // These provide stable function references that always call current implementations
1780
1832
  // ============================================================================
1781
- const initialize = (0, react_1.useCallback)((code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime) => __awaiter(void 0, void 0, void 0, function* () {
1782
- return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime);
1833
+ const initialize = (0, react_1.useCallback)((code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime, preventAffiliateTransfer) => __awaiter(void 0, void 0, void 0, function* () {
1834
+ return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime, preventAffiliateTransfer);
1783
1835
  }), []);
1784
1836
  const setShortCode = (0, react_1.useCallback)((shortCode) => __awaiter(void 0, void 0, void 0, function* () {
1785
1837
  return setShortCodeImplRef.current(shortCode);
@@ -1796,6 +1848,9 @@ const DeepLinkIapProvider = ({ children, }) => {
1796
1848
  const getAffiliateStoredDate = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
1797
1849
  return getAffiliateStoredDateImplRef.current();
1798
1850
  }), []);
1851
+ const getAffiliateExpiryTimestamp = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
1852
+ return getAffiliateExpiryTimestampImplRef.current();
1853
+ }), []);
1799
1854
  const storeExpectedStoreTransaction = (0, react_1.useCallback)((purchaseToken) => __awaiter(void 0, void 0, void 0, function* () {
1800
1855
  return storeExpectedStoreTransactionImplRef.current(purchaseToken);
1801
1856
  }), []);
@@ -1813,14 +1868,18 @@ const DeepLinkIapProvider = ({ children, }) => {
1813
1868
  }), []);
1814
1869
  const setInsertAffiliateIdentifierChangeCallbackHandler = (0, react_1.useCallback)((callback) => {
1815
1870
  insertAffiliateIdentifierChangeCallbackRef.current = callback;
1816
- // If callback is being set, immediately call it with the current identifier value
1871
+ // If callback is being set, immediately call it with the current identifier and offer code values
1817
1872
  // This ensures callbacks registered after initialization still receive the current state (including null if expired/not set)
1818
1873
  if (callback) {
1819
- returnInsertAffiliateIdentifierImpl().then(identifier => {
1874
+ Promise.all([
1875
+ returnInsertAffiliateIdentifierImpl(),
1876
+ getValueFromAsync(ASYNC_KEYS.OFFER_CODE)
1877
+ ]).then(([identifier, storedOfferCode]) => {
1820
1878
  // Verify callback is still the same (wasn't replaced during async operation)
1821
1879
  if (insertAffiliateIdentifierChangeCallbackRef.current === callback) {
1822
- verboseLog(`Calling callback immediately with current identifier: ${identifier}`);
1823
- callback(identifier);
1880
+ const offerCode = storedOfferCode && storedOfferCode.length > 0 ? storedOfferCode : null;
1881
+ verboseLog(`Calling callback immediately with current identifier: ${identifier}, offerCode: ${offerCode}`);
1882
+ callback(identifier, offerCode);
1824
1883
  }
1825
1884
  });
1826
1885
  }
@@ -1837,6 +1896,7 @@ const DeepLinkIapProvider = ({ children, }) => {
1837
1896
  returnInsertAffiliateIdentifier,
1838
1897
  isAffiliateAttributionValid,
1839
1898
  getAffiliateStoredDate,
1899
+ getAffiliateExpiryTimestamp,
1840
1900
  storeExpectedStoreTransaction,
1841
1901
  returnUserAccountTokenAndStoreExpectedTransaction,
1842
1902
  validatePurchaseWithIapticAPI,
@@ -9,13 +9,14 @@ declare const useDeepLinkIapProvider: () => {
9
9
  returnInsertAffiliateIdentifier: (ignoreTimeout?: boolean) => Promise<string | null>;
10
10
  isAffiliateAttributionValid: () => Promise<boolean>;
11
11
  getAffiliateStoredDate: () => Promise<Date | null>;
12
+ getAffiliateExpiryTimestamp: () => Promise<number | null>;
12
13
  trackEvent: (eventName: string) => Promise<void>;
13
14
  setShortCode: (shortCode: string) => Promise<boolean>;
14
15
  getAffiliateDetails: (affiliateCode: string) => Promise<import("./DeepLinkIapProvider").AffiliateDetails>;
15
16
  setInsertAffiliateIdentifier: (referringLink: string) => Promise<void | string>;
16
17
  setInsertAffiliateIdentifierChangeCallback: (callback: import("./DeepLinkIapProvider").InsertAffiliateIdentifierChangeCallback | null) => void;
17
18
  handleInsertLinks: (url: string) => Promise<boolean>;
18
- initialize: (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number) => Promise<void>;
19
+ initialize: (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number, preventAffiliateTransfer?: boolean) => Promise<void>;
19
20
  isInitialized: boolean;
20
21
  OfferCode: string | null;
21
22
  };
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  const react_1 = require("react");
4
4
  const DeepLinkIapProvider_1 = require("./DeepLinkIapProvider");
5
5
  const useDeepLinkIapProvider = () => {
6
- const { referrerLink, userId, validatePurchaseWithIapticAPI, storeExpectedStoreTransaction, returnUserAccountTokenAndStoreExpectedTransaction, returnInsertAffiliateIdentifier, isAffiliateAttributionValid, getAffiliateStoredDate, trackEvent, setShortCode, getAffiliateDetails, setInsertAffiliateIdentifier, setInsertAffiliateIdentifierChangeCallback, handleInsertLinks, initialize, isInitialized, OfferCode, } = (0, react_1.useContext)(DeepLinkIapProvider_1.DeepLinkIapContext);
6
+ const { referrerLink, userId, validatePurchaseWithIapticAPI, storeExpectedStoreTransaction, returnUserAccountTokenAndStoreExpectedTransaction, returnInsertAffiliateIdentifier, isAffiliateAttributionValid, getAffiliateStoredDate, getAffiliateExpiryTimestamp, trackEvent, setShortCode, getAffiliateDetails, setInsertAffiliateIdentifier, setInsertAffiliateIdentifierChangeCallback, handleInsertLinks, initialize, isInitialized, OfferCode, } = (0, react_1.useContext)(DeepLinkIapProvider_1.DeepLinkIapContext);
7
7
  return {
8
8
  referrerLink,
9
9
  userId,
@@ -13,6 +13,7 @@ const useDeepLinkIapProvider = () => {
13
13
  returnInsertAffiliateIdentifier,
14
14
  isAffiliateAttributionValid,
15
15
  getAffiliateStoredDate,
16
+ getAffiliateExpiryTimestamp,
16
17
  trackEvent,
17
18
  setShortCode,
18
19
  getAffiliateDetails,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insert-affiliate-react-native-sdk",
3
- "version": "1.11.2",
3
+ "version": "1.13.0",
4
4
  "description": "A package for connecting with the Insert Affiliate Platform to add app based affiliate marketing.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
package/readme.md CHANGED
@@ -46,10 +46,13 @@ npm install @react-native-async-storage/async-storage @react-native-clipboard/cl
46
46
 
47
47
  **Step 3:** Install iOS pods (iOS only)
48
48
 
49
+ For **bare React Native** projects:
49
50
  ```bash
50
51
  cd ios && pod install && cd ..
51
52
  ```
52
53
 
54
+ For **Expo managed workflow**: Skip this step - pods are installed automatically when you run `npx expo prebuild` or `npx expo run:ios`.
55
+
53
56
  ### Your First Integration
54
57
 
55
58
  **In `index.js`** - Wrap your app with the provider:
@@ -139,7 +142,8 @@ initialize(
139
142
  true, // verboseLogging - Enable for debugging (disable in production)
140
143
  true, // insertLinksEnabled - Enable Insert Links (Insert Affiliate's built-in deep linking)
141
144
  true, // insertLinksClipboardEnabled - Enable clipboard attribution (triggers permission prompt)
142
- 604800 // affiliateAttributionActiveTime - 7 days attribution timeout in seconds
145
+ 604800, // affiliateAttributionActiveTime - 7 days attribution timeout in seconds
146
+ true // preventAffiliateTransfer - Protect original affiliate from being replaced
143
147
  );
144
148
  ```
145
149
 
@@ -150,6 +154,9 @@ initialize(
150
154
  - Improves attribution accuracy when deep linking fails
151
155
  - iOS will show a permission prompt: "[Your App] would like to paste from [App Name]"
152
156
  - `affiliateAttributionActiveTime`: How long affiliate attribution lasts in seconds (omit for no timeout)
157
+ - `preventAffiliateTransfer`: When `true`, prevents a new affiliate link from overwriting an existing affiliate attribution (defaults to `false`)
158
+ - Use this to ensure the first affiliate who acquired the user always gets credit
159
+ - New affiliate links will be silently ignored if the user already has an affiliate
153
160
 
154
161
  </details>
155
162
 
@@ -176,11 +183,18 @@ Complete the [RevenueCat SDK installation](https://www.revenuecat.com/docs/getti
176
183
 
177
184
  ```javascript
178
185
  import React, { useEffect } from 'react';
186
+ import { AppState } from 'react-native';
179
187
  import Purchases from 'react-native-purchases';
180
188
  import { useDeepLinkIapProvider } from 'insert-affiliate-react-native-sdk';
181
189
 
182
190
  const App = () => {
183
- const { initialize, isInitialized, setInsertAffiliateIdentifierChangeCallback } = useDeepLinkIapProvider();
191
+ const {
192
+ initialize,
193
+ isInitialized,
194
+ setInsertAffiliateIdentifierChangeCallback,
195
+ isAffiliateAttributionValid,
196
+ getAffiliateExpiryTimestamp
197
+ } = useDeepLinkIapProvider();
184
198
 
185
199
  useEffect(() => {
186
200
  if (!isInitialized) {
@@ -188,11 +202,23 @@ const App = () => {
188
202
  }
189
203
  }, [initialize, isInitialized]);
190
204
 
191
- // Set RevenueCat attribute when affiliate identifier changes
205
+ // Set RevenueCat attributes when affiliate identifier changes
206
+ // Note: Use preventAffiliateTransfer in initialize() to block affiliate changes in the SDK
192
207
  useEffect(() => {
193
- setInsertAffiliateIdentifierChangeCallback(async (identifier) => {
208
+ setInsertAffiliateIdentifierChangeCallback(async (identifier, offerCode) => {
194
209
  if (identifier) {
195
- await Purchases.setAttributes({ "insert_affiliate": identifier });
210
+ // OPTIONAL: Prevent attribution for existing subscribers
211
+ // Uncomment to ensure affiliates only earn from users they actually brought:
212
+ // const customerInfo = await Purchases.getCustomerInfo();
213
+ // const hasActiveEntitlement = Object.keys(customerInfo.entitlements.active).length > 0;
214
+ // if (hasActiveEntitlement) return; // User already subscribed, don't attribute
215
+
216
+ // Sync affiliate to RevenueCat
217
+ await Purchases.setAttributes({
218
+ "insert_affiliate": identifier,
219
+ "affiliateOfferCode": offerCode || "", // For RevenueCat Targeting
220
+ "insert_timedout": "" // Clear timeout flag on new attribution
221
+ });
196
222
  await Purchases.syncAttributesAndOfferingsIfNeeded();
197
223
  }
198
224
  });
@@ -200,10 +226,48 @@ const App = () => {
200
226
  return () => setInsertAffiliateIdentifierChangeCallback(null);
201
227
  }, [setInsertAffiliateIdentifierChangeCallback]);
202
228
 
229
+ // Handle expired attribution - clear offer code and set timeout timestamp
230
+ // NOTE: insert_affiliate is preserved for renewal webhook attribution
231
+ // insert_timedout stores the TIMESTAMP when attribution expired (storedDate + timeout)
232
+ // Webhook compares this against purchase dates to determine if purchases should be attributed:
233
+ // - Purchases made BEFORE timeout → attributed (initial + all renewals)
234
+ // - Purchases made AFTER timeout → not attributed (initial + all renewals)
235
+ useEffect(() => {
236
+ if (!isInitialized) return;
237
+
238
+ const clearExpiredAffiliation = async () => {
239
+ const isValid = await isAffiliateAttributionValid();
240
+ if (!isValid) {
241
+ const expiryTimestamp = await getAffiliateExpiryTimestamp();
242
+ if (!expiryTimestamp) return; // No timeout configured
243
+
244
+ await Purchases.setAttributes({
245
+ "affiliateOfferCode": "",
246
+ "insert_timedout": expiryTimestamp.toString()
247
+ });
248
+ await Purchases.syncAttributesAndOfferingsIfNeeded();
249
+ }
250
+ };
251
+
252
+ // Check on app initialization
253
+ clearExpiredAffiliation();
254
+
255
+ // Check when app returns to foreground
256
+ const subscription = AppState.addEventListener('change', (state) => {
257
+ if (state === 'active') {
258
+ clearExpiredAffiliation();
259
+ }
260
+ });
261
+
262
+ return () => subscription?.remove();
263
+ }, [isInitialized, isAffiliateAttributionValid, getAffiliateExpiryTimestamp]);
264
+
203
265
  return <YourAppContent />;
204
266
  };
205
267
  ```
206
268
 
269
+ > **RevenueCat Targeting:** Use the `affiliateOfferCode` attribute in your [RevenueCat Targeting rules](https://www.revenuecat.com/docs/tools/targeting) to show different offerings to affiliates (e.g., extended free trials). Set a rule like `affiliateOfferCode is any of ["oneWeekFree", "twoWeeksFree"]` to target users with specific offer codes.
270
+
207
271
  **Step 2: Webhook Setup**
208
272
 
209
273
  1. In RevenueCat, [create a new webhook](https://www.revenuecat.com/docs/integrations/webhooks)
@@ -227,9 +291,12 @@ const App = () => {
227
291
 
228
292
  ```bash
229
293
  npm install react-native-adapty
230
- cd ios && pod install && cd ..
231
294
  ```
232
295
 
296
+ For **bare React Native**: Run `cd ios && pod install && cd ..`
297
+
298
+ For **Expo managed workflow**: Pods install automatically with `npx expo prebuild` or `npx expo run:ios`.
299
+
233
300
  Complete the [Adapty SDK installation](https://adapty.io/docs/sdk-installation-reactnative) for any additional platform-specific setup.
234
301
 
235
302
  **Step 2: Code Setup**
@@ -510,9 +577,12 @@ const App = () => {
510
577
  const { setInsertAffiliateIdentifierChangeCallback } = useDeepLinkIapProvider();
511
578
 
512
579
  useEffect(() => {
513
- setInsertAffiliateIdentifierChangeCallback(async (identifier) => {
580
+ setInsertAffiliateIdentifierChangeCallback(async (identifier, offerCode) => {
514
581
  if (identifier) {
515
- await Purchases.setAttributes({ "insert_affiliate": identifier });
582
+ await Purchases.setAttributes({
583
+ "insert_affiliate": identifier,
584
+ "affiliateOfferCode": offerCode || "" // For RevenueCat Targeting
585
+ });
516
586
  await Purchases.syncAttributesAndOfferingsIfNeeded();
517
587
  }
518
588
  });
@@ -524,6 +594,8 @@ const App = () => {
524
594
  };
525
595
  ```
526
596
 
597
+ > **Note:** For full RevenueCat integration with attribution timeout cleanup, see the [complete example in Option 1](#option-1-revenuecat-recommended) above.
598
+
527
599
  **With Adapty:**
528
600
 
529
601
  ```javascript
@@ -673,6 +745,33 @@ Update your `ios/YourApp/AppDelegate.mm`:
673
745
  }
674
746
  ```
675
747
 
748
+ **Step 4: Expo Router Setup (If Using Expo Router)**
749
+
750
+ If you're using Expo Router, deep links will cause a "This screen does not exist" error because both the Insert Affiliate SDK and Expo Router try to handle the incoming URL. The SDK correctly processes the affiliate attribution, but Expo Router simultaneously attempts to navigate to the URL path (e.g., `/insert-affiliate`), which doesn't exist as a route.
751
+
752
+ To fix this, create `app/+native-intent.tsx` to intercept Insert Affiliate URLs before Expo Router processes them:
753
+
754
+ ```tsx
755
+ // app/+native-intent.tsx
756
+ // Tell Expo Router to skip navigation for Insert Affiliate URLs
757
+ // The SDK handles these via native Linking - we just prevent router errors
758
+
759
+ export function redirectSystemPath({ path }: { path: string }): string | null {
760
+ // Skip navigation for Insert Affiliate deep links
761
+ if (path.includes('insert-affiliate') || path.includes('insertAffiliate')) {
762
+ return null; // SDK handles it via Linking API
763
+ }
764
+ return path; // Let Expo Router handle all other URLs normally
765
+ }
766
+ ```
767
+
768
+ This ensures:
769
+ - The Insert Affiliate SDK still receives and processes the URL via the native Linking API
770
+ - Expo Router ignores the URL and doesn't attempt navigation
771
+ - No "screen not found" errors
772
+
773
+ See the [Expo Router +native-intent docs](https://docs.expo.dev/router/reference/native-intent/) for more details.
774
+
676
775
  **Testing Deep Links:**
677
776
 
678
777
  ```bash
@@ -912,6 +1011,29 @@ const rawIdentifier = await returnInsertAffiliateIdentifier(true);
912
1011
 
913
1012
  </details>
914
1013
 
1014
+ ### Prevent Affiliate Transfer
1015
+
1016
+ By default, clicking a new affiliate link will overwrite any existing attribution. Enable `preventAffiliateTransfer` to lock the first affiliate:
1017
+
1018
+ ```javascript
1019
+ initialize(
1020
+ "YOUR_COMPANY_CODE",
1021
+ false, // verboseLogging
1022
+ false, // insertLinksEnabled
1023
+ false, // insertLinksClipboardEnabled
1024
+ 604800, // affiliateAttributionActiveTime (7 days)
1025
+ true // preventAffiliateTransfer - locks first affiliate
1026
+ );
1027
+ ```
1028
+
1029
+ **How it works:**
1030
+ - When enabled, once a user is attributed to an affiliate, that attribution is locked
1031
+ - New affiliate links will not overwrite the existing attribution
1032
+ - The callback still fires with the existing affiliate data (not the new one)
1033
+ - Useful for preventing "affiliate stealing" where users click competitor links
1034
+
1035
+ Learn more: [Prevent Affiliate Transfer Documentation](https://docs.insertaffiliate.com/prevent-affiliate-transfer)
1036
+
915
1037
  ---
916
1038
 
917
1039
  ## 🔍 Troubleshooting
@@ -930,6 +1052,10 @@ const rawIdentifier = await returnInsertAffiliateIdentifier(true);
930
1052
  - iOS: Add URL scheme to Info.plist and configure associated domains
931
1053
  - Android: Add intent filters to AndroidManifest.xml
932
1054
 
1055
+ **Problem:** "This screen does not exist" error with Expo Router
1056
+ - **Cause:** Both Insert Affiliate SDK and Expo Router are trying to handle the same URL
1057
+ - **Solution:** Create `app/+native-intent.tsx` to intercept Insert Affiliate URLs before Expo Router processes them. See [Expo Router Setup](#option-1-insert-links-simplest) in the Insert Links section.
1058
+
933
1059
  **Problem:** "No affiliate identifier found"
934
1060
  - **Cause:** User hasn't clicked an affiliate link yet
935
1061
  - **Solution:** Test with simulator/emulator:
@@ -15,7 +15,7 @@ type T_DEEPLINK_IAP_PROVIDER = {
15
15
  children: React.ReactNode;
16
16
  };
17
17
 
18
- export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null) => void;
18
+ export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null, offerCode: string | null) => void;
19
19
 
20
20
  export type AffiliateDetails = {
21
21
  affiliateName: string;
@@ -34,6 +34,7 @@ type T_DEEPLINK_IAP_CONTEXT = {
34
34
  returnInsertAffiliateIdentifier: (ignoreTimeout?: boolean) => Promise<string | null>;
35
35
  isAffiliateAttributionValid: () => Promise<boolean>;
36
36
  getAffiliateStoredDate: () => Promise<Date | null>;
37
+ getAffiliateExpiryTimestamp: () => Promise<number | null>;
37
38
  validatePurchaseWithIapticAPI: (
38
39
  jsonIapPurchase: CustomPurchase,
39
40
  iapticAppId: string,
@@ -52,7 +53,7 @@ type T_DEEPLINK_IAP_CONTEXT = {
52
53
  ) => Promise<void | string>;
53
54
  setInsertAffiliateIdentifierChangeCallback: (callback: InsertAffiliateIdentifierChangeCallback | null) => void;
54
55
  handleInsertLinks: (url: string) => Promise<boolean>;
55
- initialize: (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number) => Promise<void>;
56
+ initialize: (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number, preventAffiliateTransfer?: boolean) => Promise<void>;
56
57
  isInitialized: boolean;
57
58
  };
58
59
 
@@ -78,7 +79,7 @@ const ASYNC_KEYS = {
78
79
  USER_ID: '@app_user_id',
79
80
  COMPANY_CODE: '@app_company_code',
80
81
  USER_ACCOUNT_TOKEN: '@app_user_account_token',
81
- IOS_OFFER_CODE: '@app_ios_offer_code',
82
+ OFFER_CODE: '@app_offer_code',
82
83
  AFFILIATE_STORED_DATE: '@app_affiliate_stored_date',
83
84
  SDK_INIT_REPORTED: '@app_sdk_init_reported',
84
85
  REPORTED_AFFILIATE_ASSOCIATIONS: '@app_reported_affiliate_associations',
@@ -101,6 +102,7 @@ export const DeepLinkIapContext = createContext<T_DEEPLINK_IAP_CONTEXT>({
101
102
  returnInsertAffiliateIdentifier: async (ignoreTimeout?: boolean) => '',
102
103
  isAffiliateAttributionValid: async () => false,
103
104
  getAffiliateStoredDate: async () => null,
105
+ getAffiliateExpiryTimestamp: async () => null,
104
106
  validatePurchaseWithIapticAPI: async (
105
107
  jsonIapPurchase: CustomPurchase,
106
108
  iapticAppId: string,
@@ -115,7 +117,7 @@ export const DeepLinkIapContext = createContext<T_DEEPLINK_IAP_CONTEXT>({
115
117
  setInsertAffiliateIdentifier: async (referringLink: string) => {},
116
118
  setInsertAffiliateIdentifierChangeCallback: (callback: InsertAffiliateIdentifierChangeCallback | null) => {},
117
119
  handleInsertLinks: async (url: string) => false,
118
- initialize: async (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number) => {},
120
+ initialize: async (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number, preventAffiliateTransfer?: boolean) => {},
119
121
  isInitialized: false,
120
122
  });
121
123
 
@@ -131,20 +133,23 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
131
133
  const [insertLinksClipboardEnabled, setInsertLinksClipboardEnabled] = useState<boolean>(false);
132
134
  const [OfferCode, setOfferCode] = useState<string | null>(null);
133
135
  const [affiliateAttributionActiveTime, setAffiliateAttributionActiveTime] = useState<number | null>(null);
136
+ const [preventAffiliateTransfer, setPreventAffiliateTransfer] = useState<boolean>(false);
134
137
  const insertAffiliateIdentifierChangeCallbackRef = useRef<InsertAffiliateIdentifierChangeCallback | null>(null);
135
138
  const isInitializingRef = useRef<boolean>(false);
136
139
 
137
140
  // Refs for values that need to be current inside callbacks (to avoid stale closures)
138
141
  const companyCodeRef = useRef<string | null>(null);
139
142
  const verboseLoggingRef = useRef<boolean>(false);
143
+ const preventAffiliateTransferRef = useRef<boolean>(false);
140
144
 
141
145
  // Refs for implementation functions (ref callback pattern for stable + fresh)
142
- const initializeImplRef = useRef<(code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number) => Promise<void>>(null as any);
146
+ const initializeImplRef = useRef<(code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number, preventAffiliateTransfer?: boolean) => Promise<void>>(null as any);
143
147
  const setShortCodeImplRef = useRef<(shortCode: string) => Promise<boolean>>(null as any);
144
148
  const getAffiliateDetailsImplRef = useRef<(affiliateCode: string) => Promise<AffiliateDetails>>(null as any);
145
149
  const returnInsertAffiliateIdentifierImplRef = useRef<(ignoreTimeout?: boolean) => Promise<string | null>>(null as any);
146
150
  const isAffiliateAttributionValidImplRef = useRef<() => Promise<boolean>>(null as any);
147
151
  const getAffiliateStoredDateImplRef = useRef<() => Promise<Date | null>>(null as any);
152
+ const getAffiliateExpiryTimestampImplRef = useRef<() => Promise<number | null>>(null as any);
148
153
  const storeExpectedStoreTransactionImplRef = useRef<(purchaseToken: string) => Promise<void>>(null as any);
149
154
  const returnUserAccountTokenAndStoreExpectedTransactionImplRef = useRef<() => Promise<string | null>>(null as any);
150
155
  const validatePurchaseWithIapticAPIImplRef = useRef<(jsonIapPurchase: CustomPurchase, iapticAppId: string, iapticAppName: string, iapticPublicKey: string) => Promise<boolean>>(null as any);
@@ -153,7 +158,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
153
158
  const handleInsertLinksImplRef = useRef<(url: string) => Promise<boolean>>(null as any);
154
159
 
155
160
  // MARK: Initialize the SDK
156
- const initializeImpl = async (companyCodeParam: string | null, verboseLoggingParam: boolean = false, insertLinksEnabledParam: boolean = false, insertLinksClipboardEnabledParam: boolean = false, affiliateAttributionActiveTimeParam?: number): Promise<void> => {
161
+ const initializeImpl = async (companyCodeParam: string | null, verboseLoggingParam: boolean = false, insertLinksEnabledParam: boolean = false, insertLinksClipboardEnabledParam: boolean = false, affiliateAttributionActiveTimeParam?: number, preventAffiliateTransferParam: boolean = false): Promise<void> => {
157
162
  // Prevent multiple concurrent initialization attempts
158
163
  if (isInitialized || isInitializingRef.current) {
159
164
  return;
@@ -167,6 +172,8 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
167
172
  if (affiliateAttributionActiveTimeParam !== undefined) {
168
173
  setAffiliateAttributionActiveTime(affiliateAttributionActiveTimeParam);
169
174
  }
175
+ setPreventAffiliateTransfer(preventAffiliateTransferParam);
176
+ preventAffiliateTransferRef.current = preventAffiliateTransferParam;
170
177
 
171
178
  if (verboseLoggingParam) {
172
179
  console.log('[Insert Affiliate] [VERBOSE] Starting SDK initialization...');
@@ -218,12 +225,23 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
218
225
  const uId = await getValueFromAsync(ASYNC_KEYS.USER_ID);
219
226
  const refLink = await getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
220
227
  const companyCodeFromStorage = await getValueFromAsync(ASYNC_KEYS.COMPANY_CODE);
221
- const storedOfferCode = await getValueFromAsync(ASYNC_KEYS.IOS_OFFER_CODE);
228
+
229
+ // Migration: check new key first, fall back to legacy iOS key
230
+ let storedOfferCode = await getValueFromAsync(ASYNC_KEYS.OFFER_CODE);
231
+ if (!storedOfferCode) {
232
+ const legacyOfferCode = await getValueFromAsync('@app_ios_offer_code');
233
+ if (legacyOfferCode) {
234
+ storedOfferCode = legacyOfferCode;
235
+ // Migrate to new key
236
+ await saveValueInAsync(ASYNC_KEYS.OFFER_CODE, legacyOfferCode);
237
+ verboseLog('Migrated offer code from legacy iOS key to new key');
238
+ }
239
+ }
222
240
 
223
241
  verboseLog(`User ID found: ${uId ? 'Yes' : 'No'}`);
224
242
  verboseLog(`Referrer link found: ${refLink ? 'Yes' : 'No'}`);
225
243
  verboseLog(`Company code found: ${companyCodeFromStorage ? 'Yes' : 'No'}`);
226
- verboseLog(`iOS Offer Code found: ${storedOfferCode ? 'Yes' : 'No'}`);
244
+ verboseLog(`Offer Code found: ${storedOfferCode ? 'Yes' : 'No'}`);
227
245
 
228
246
  if (uId && refLink) {
229
247
  setUserId(uId);
@@ -239,7 +257,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
239
257
 
240
258
  if (storedOfferCode) {
241
259
  setOfferCode(storedOfferCode);
242
- verboseLog('iOS Offer Code restored from storage');
260
+ verboseLog('Offer Code restored from storage');
243
261
  }
244
262
  } catch (error) {
245
263
  errorLog(`ERROR ~ fetchAsyncEssentials: ${error}`);
@@ -1349,7 +1367,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1349
1367
  return isValidCharacters && referringLink.length >= 3 && referringLink.length <= 25;
1350
1368
  };
1351
1369
 
1352
- const checkAffiliateExists = async (affiliateCode: string): Promise<boolean> => {
1370
+ const checkAffiliateExists = async (affiliateCode: string, trackUsage: boolean = false): Promise<boolean> => {
1353
1371
  try {
1354
1372
  const activeCompanyCode = await getActiveCompanyCode();
1355
1373
  if (!activeCompanyCode) {
@@ -1358,11 +1376,15 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1358
1376
  }
1359
1377
 
1360
1378
  const url = 'https://api.insertaffiliate.com/V1/checkAffiliateExists';
1361
- const payload = {
1379
+ const payload: Record<string, any> = {
1362
1380
  companyId: activeCompanyCode,
1363
1381
  affiliateCode: affiliateCode
1364
1382
  };
1365
1383
 
1384
+ if (trackUsage) {
1385
+ payload.trackUsage = true;
1386
+ }
1387
+
1366
1388
  verboseLog(`Checking if affiliate exists: ${affiliateCode}`);
1367
1389
 
1368
1390
  const response = await axios.post(url, payload, {
@@ -1445,7 +1467,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1445
1467
  isShortCode(capitalisedShortCode);
1446
1468
 
1447
1469
  // Check if the affiliate exists before storing
1448
- const exists = await checkAffiliateExists(capitalisedShortCode);
1470
+ const exists = await checkAffiliateExists(capitalisedShortCode, true);
1449
1471
 
1450
1472
  if (exists) {
1451
1473
  // If affiliate exists, set the Insert Affiliate Identifier
@@ -1605,6 +1627,31 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1605
1627
  }
1606
1628
  };
1607
1629
 
1630
+ // Get the timestamp when attribution expires (stored date + timeout duration in ms)
1631
+ // Returns null if no timeout is configured or no stored date exists
1632
+ const getAffiliateExpiryTimestampImpl = async (): Promise<number | null> => {
1633
+ try {
1634
+ if (!affiliateAttributionActiveTime) {
1635
+ verboseLog('No attribution timeout configured, no expiry timestamp');
1636
+ return null;
1637
+ }
1638
+
1639
+ const storedDate = await getAffiliateStoredDateImpl();
1640
+ if (!storedDate) {
1641
+ verboseLog('No stored date found, cannot calculate expiry timestamp');
1642
+ return null;
1643
+ }
1644
+
1645
+ // Convert timeout from seconds to milliseconds and add to stored date
1646
+ const expiryTimestamp = storedDate.getTime() + (affiliateAttributionActiveTime * 1000);
1647
+ verboseLog(`Attribution expiry timestamp: ${expiryTimestamp} (${new Date(expiryTimestamp).toISOString()})`);
1648
+ return expiryTimestamp;
1649
+ } catch (error) {
1650
+ verboseLog(`Error getting affiliate expiry timestamp: ${error}`);
1651
+ return null;
1652
+ }
1653
+ };
1654
+
1608
1655
  // MARK: Insert Affiliate Identifier
1609
1656
 
1610
1657
  const setInsertAffiliateIdentifierImpl = async (
@@ -1701,7 +1748,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1701
1748
  };
1702
1749
 
1703
1750
  async function storeInsertAffiliateIdentifier({ link, source }: { link: string; source: AffiliateAssociationSource }) {
1704
- console.log(`[Insert Affiliate] Storing affiliate identifier: ${link} (source: ${source})`);
1751
+ verboseLog(`Storing affiliate identifier: ${link} (source: ${source})`);
1705
1752
 
1706
1753
  // Check if we're trying to store the same link (prevent duplicate storage)
1707
1754
  const existingLink = await getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
@@ -1710,6 +1757,13 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1710
1757
  return;
1711
1758
  }
1712
1759
 
1760
+ // Prevent transfer of affiliate if enabled - keep original affiliate
1761
+ verboseLog(`preventAffiliateTransfer check: enabled=${preventAffiliateTransferRef.current}, existingLink=${existingLink}, newLink=${link}`);
1762
+ if (preventAffiliateTransferRef.current && existingLink && existingLink !== link) {
1763
+ verboseLog(`Transfer blocked: existing affiliate "${existingLink}" protected from being replaced by "${link}"`);
1764
+ return;
1765
+ }
1766
+
1713
1767
  verboseLog(`Updating React state with referrer link: ${link}`);
1714
1768
  setReferrerLink(link);
1715
1769
  verboseLog(`Saving referrer link to AsyncStorage...`);
@@ -1724,13 +1778,13 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1724
1778
 
1725
1779
  // Automatically fetch and store offer code for any affiliate identifier
1726
1780
  verboseLog('Attempting to fetch offer code for stored affiliate identifier...');
1727
- await retrieveAndStoreOfferCode(link);
1781
+ const offerCode = await retrieveAndStoreOfferCode(link);
1728
1782
 
1729
- // Trigger callback with the current affiliate identifier
1783
+ // Trigger callback with the current affiliate identifier and offer code
1730
1784
  if (insertAffiliateIdentifierChangeCallbackRef.current) {
1731
1785
  const currentIdentifier = await returnInsertAffiliateIdentifierImpl();
1732
- verboseLog(`Triggering callback with identifier: ${currentIdentifier}`);
1733
- insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier);
1786
+ verboseLog(`Triggering callback with identifier: ${currentIdentifier}, offerCode: ${offerCode}`);
1787
+ insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier, offerCode);
1734
1788
  }
1735
1789
 
1736
1790
  // Report this new affiliate association to the backend (fire and forget)
@@ -2016,27 +2070,30 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2016
2070
  }
2017
2071
  };
2018
2072
 
2019
- const retrieveAndStoreOfferCode = async (affiliateLink: string): Promise<void> => {
2073
+ const retrieveAndStoreOfferCode = async (affiliateLink: string): Promise<string | null> => {
2020
2074
  try {
2021
2075
  verboseLog(`Attempting to retrieve and store offer code for: ${affiliateLink}`);
2022
-
2076
+
2023
2077
  const offerCode = await fetchOfferCode(affiliateLink);
2024
-
2078
+
2025
2079
  if (offerCode && offerCode.length > 0) {
2026
2080
  // Store in both AsyncStorage and state
2027
- await saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, offerCode);
2081
+ await saveValueInAsync(ASYNC_KEYS.OFFER_CODE, offerCode);
2028
2082
  setOfferCode(offerCode);
2029
2083
  verboseLog(`Successfully stored offer code: ${offerCode}`);
2030
2084
  console.log('[Insert Affiliate] Offer code retrieved and stored successfully');
2085
+ return offerCode;
2031
2086
  } else {
2032
2087
  verboseLog('No valid offer code found to store');
2033
2088
  // Clear stored offer code if none found
2034
- await saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, '');
2089
+ await saveValueInAsync(ASYNC_KEYS.OFFER_CODE, '');
2035
2090
  setOfferCode(null);
2091
+ return null;
2036
2092
  }
2037
2093
  } catch (error) {
2038
2094
  console.error('[Insert Affiliate] Error retrieving and storing offer code:', error);
2039
2095
  verboseLog(`Error in retrieveAndStoreOfferCode: ${error}`);
2096
+ return null;
2040
2097
  }
2041
2098
  };
2042
2099
 
@@ -2059,6 +2116,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2059
2116
  returnInsertAffiliateIdentifierImplRef.current = returnInsertAffiliateIdentifierImpl;
2060
2117
  isAffiliateAttributionValidImplRef.current = isAffiliateAttributionValidImpl;
2061
2118
  getAffiliateStoredDateImplRef.current = getAffiliateStoredDateImpl;
2119
+ getAffiliateExpiryTimestampImplRef.current = getAffiliateExpiryTimestampImpl;
2062
2120
  storeExpectedStoreTransactionImplRef.current = storeExpectedStoreTransactionImpl;
2063
2121
  returnUserAccountTokenAndStoreExpectedTransactionImplRef.current = returnUserAccountTokenAndStoreExpectedTransactionImpl;
2064
2122
  validatePurchaseWithIapticAPIImplRef.current = validatePurchaseWithIapticAPIImpl;
@@ -2075,9 +2133,10 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2075
2133
  verboseLogging?: boolean,
2076
2134
  insertLinksEnabled?: boolean,
2077
2135
  insertLinksClipboardEnabled?: boolean,
2078
- affiliateAttributionActiveTime?: number
2136
+ affiliateAttributionActiveTime?: number,
2137
+ preventAffiliateTransfer?: boolean
2079
2138
  ): Promise<void> => {
2080
- return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime);
2139
+ return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime, preventAffiliateTransfer);
2081
2140
  }, []);
2082
2141
 
2083
2142
  const setShortCode = useCallback(async (shortCode: string): Promise<boolean> => {
@@ -2100,6 +2159,10 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2100
2159
  return getAffiliateStoredDateImplRef.current();
2101
2160
  }, []);
2102
2161
 
2162
+ const getAffiliateExpiryTimestamp = useCallback(async (): Promise<number | null> => {
2163
+ return getAffiliateExpiryTimestampImplRef.current();
2164
+ }, []);
2165
+
2103
2166
  const storeExpectedStoreTransaction = useCallback(async (purchaseToken: string): Promise<void> => {
2104
2167
  return storeExpectedStoreTransactionImplRef.current(purchaseToken);
2105
2168
  }, []);
@@ -2128,14 +2191,18 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2128
2191
  const setInsertAffiliateIdentifierChangeCallbackHandler = useCallback((callback: InsertAffiliateIdentifierChangeCallback | null): void => {
2129
2192
  insertAffiliateIdentifierChangeCallbackRef.current = callback;
2130
2193
 
2131
- // If callback is being set, immediately call it with the current identifier value
2194
+ // If callback is being set, immediately call it with the current identifier and offer code values
2132
2195
  // This ensures callbacks registered after initialization still receive the current state (including null if expired/not set)
2133
2196
  if (callback) {
2134
- returnInsertAffiliateIdentifierImpl().then(identifier => {
2197
+ Promise.all([
2198
+ returnInsertAffiliateIdentifierImpl(),
2199
+ getValueFromAsync(ASYNC_KEYS.OFFER_CODE)
2200
+ ]).then(([identifier, storedOfferCode]) => {
2135
2201
  // Verify callback is still the same (wasn't replaced during async operation)
2136
2202
  if (insertAffiliateIdentifierChangeCallbackRef.current === callback) {
2137
- verboseLog(`Calling callback immediately with current identifier: ${identifier}`);
2138
- callback(identifier);
2203
+ const offerCode = storedOfferCode && storedOfferCode.length > 0 ? storedOfferCode : null;
2204
+ verboseLog(`Calling callback immediately with current identifier: ${identifier}, offerCode: ${offerCode}`);
2205
+ callback(identifier, offerCode);
2139
2206
  }
2140
2207
  });
2141
2208
  }
@@ -2156,6 +2223,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2156
2223
  returnInsertAffiliateIdentifier,
2157
2224
  isAffiliateAttributionValid,
2158
2225
  getAffiliateStoredDate,
2226
+ getAffiliateExpiryTimestamp,
2159
2227
  storeExpectedStoreTransaction,
2160
2228
  returnUserAccountTokenAndStoreExpectedTransaction,
2161
2229
  validatePurchaseWithIapticAPI,
@@ -11,6 +11,7 @@ const useDeepLinkIapProvider = () => {
11
11
  returnInsertAffiliateIdentifier,
12
12
  isAffiliateAttributionValid,
13
13
  getAffiliateStoredDate,
14
+ getAffiliateExpiryTimestamp,
14
15
  trackEvent,
15
16
  setShortCode,
16
17
  getAffiliateDetails,
@@ -31,6 +32,7 @@ const useDeepLinkIapProvider = () => {
31
32
  returnInsertAffiliateIdentifier,
32
33
  isAffiliateAttributionValid,
33
34
  getAffiliateStoredDate,
35
+ getAffiliateExpiryTimestamp,
34
36
  trackEvent,
35
37
  setShortCode,
36
38
  getAffiliateDetails,