insert-affiliate-react-native-sdk 1.11.2 → 1.12.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) {
@@ -1393,6 +1409,29 @@ const DeepLinkIapProvider = ({ children, }) => {
1393
1409
  return null;
1394
1410
  }
1395
1411
  });
1412
+ // Get the timestamp when attribution expires (stored date + timeout duration in ms)
1413
+ // Returns null if no timeout is configured or no stored date exists
1414
+ const getAffiliateExpiryTimestampImpl = () => __awaiter(void 0, void 0, void 0, function* () {
1415
+ try {
1416
+ if (!affiliateAttributionActiveTime) {
1417
+ verboseLog('No attribution timeout configured, no expiry timestamp');
1418
+ return null;
1419
+ }
1420
+ const storedDate = yield getAffiliateStoredDateImpl();
1421
+ if (!storedDate) {
1422
+ verboseLog('No stored date found, cannot calculate expiry timestamp');
1423
+ return null;
1424
+ }
1425
+ // Convert timeout from seconds to milliseconds and add to stored date
1426
+ const expiryTimestamp = storedDate.getTime() + (affiliateAttributionActiveTime * 1000);
1427
+ verboseLog(`Attribution expiry timestamp: ${expiryTimestamp} (${new Date(expiryTimestamp).toISOString()})`);
1428
+ return expiryTimestamp;
1429
+ }
1430
+ catch (error) {
1431
+ verboseLog(`Error getting affiliate expiry timestamp: ${error}`);
1432
+ return null;
1433
+ }
1434
+ });
1396
1435
  // MARK: Insert Affiliate Identifier
1397
1436
  const setInsertAffiliateIdentifierImpl = (referringLink) => __awaiter(void 0, void 0, void 0, function* () {
1398
1437
  console.log('[Insert Affiliate] Setting affiliate identifier.');
@@ -1471,13 +1510,19 @@ const DeepLinkIapProvider = ({ children, }) => {
1471
1510
  });
1472
1511
  function storeInsertAffiliateIdentifier(_a) {
1473
1512
  return __awaiter(this, arguments, void 0, function* ({ link, source }) {
1474
- console.log(`[Insert Affiliate] Storing affiliate identifier: ${link} (source: ${source})`);
1513
+ verboseLog(`Storing affiliate identifier: ${link} (source: ${source})`);
1475
1514
  // Check if we're trying to store the same link (prevent duplicate storage)
1476
1515
  const existingLink = yield getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
1477
1516
  if (existingLink === link) {
1478
1517
  verboseLog(`Link ${link} is already stored, skipping duplicate storage`);
1479
1518
  return;
1480
1519
  }
1520
+ // Prevent transfer of affiliate if enabled - keep original affiliate
1521
+ verboseLog(`preventAffiliateTransfer check: enabled=${preventAffiliateTransferRef.current}, existingLink=${existingLink}, newLink=${link}`);
1522
+ if (preventAffiliateTransferRef.current && existingLink && existingLink !== link) {
1523
+ verboseLog(`Transfer blocked: existing affiliate "${existingLink}" protected from being replaced by "${link}"`);
1524
+ return;
1525
+ }
1481
1526
  verboseLog(`Updating React state with referrer link: ${link}`);
1482
1527
  setReferrerLink(link);
1483
1528
  verboseLog(`Saving referrer link to AsyncStorage...`);
@@ -1489,12 +1534,12 @@ const DeepLinkIapProvider = ({ children, }) => {
1489
1534
  verboseLog(`Referrer link saved to AsyncStorage successfully`);
1490
1535
  // Automatically fetch and store offer code for any affiliate identifier
1491
1536
  verboseLog('Attempting to fetch offer code for stored affiliate identifier...');
1492
- yield retrieveAndStoreOfferCode(link);
1493
- // Trigger callback with the current affiliate identifier
1537
+ const offerCode = yield retrieveAndStoreOfferCode(link);
1538
+ // Trigger callback with the current affiliate identifier and offer code
1494
1539
  if (insertAffiliateIdentifierChangeCallbackRef.current) {
1495
1540
  const currentIdentifier = yield returnInsertAffiliateIdentifierImpl();
1496
- verboseLog(`Triggering callback with identifier: ${currentIdentifier}`);
1497
- insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier);
1541
+ verboseLog(`Triggering callback with identifier: ${currentIdentifier}, offerCode: ${offerCode}`);
1542
+ insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier, offerCode);
1498
1543
  }
1499
1544
  // Report this new affiliate association to the backend (fire and forget)
1500
1545
  const fullIdentifier = yield returnInsertAffiliateIdentifierImpl();
@@ -1734,21 +1779,24 @@ const DeepLinkIapProvider = ({ children, }) => {
1734
1779
  const offerCode = yield fetchOfferCode(affiliateLink);
1735
1780
  if (offerCode && offerCode.length > 0) {
1736
1781
  // Store in both AsyncStorage and state
1737
- yield saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, offerCode);
1782
+ yield saveValueInAsync(ASYNC_KEYS.OFFER_CODE, offerCode);
1738
1783
  setOfferCode(offerCode);
1739
1784
  verboseLog(`Successfully stored offer code: ${offerCode}`);
1740
1785
  console.log('[Insert Affiliate] Offer code retrieved and stored successfully');
1786
+ return offerCode;
1741
1787
  }
1742
1788
  else {
1743
1789
  verboseLog('No valid offer code found to store');
1744
1790
  // Clear stored offer code if none found
1745
- yield saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, '');
1791
+ yield saveValueInAsync(ASYNC_KEYS.OFFER_CODE, '');
1746
1792
  setOfferCode(null);
1793
+ return null;
1747
1794
  }
1748
1795
  }
1749
1796
  catch (error) {
1750
1797
  console.error('[Insert Affiliate] Error retrieving and storing offer code:', error);
1751
1798
  verboseLog(`Error in retrieveAndStoreOfferCode: ${error}`);
1799
+ return null;
1752
1800
  }
1753
1801
  });
1754
1802
  const removeSpecialCharacters = (offerCode) => {
@@ -1768,6 +1816,7 @@ const DeepLinkIapProvider = ({ children, }) => {
1768
1816
  returnInsertAffiliateIdentifierImplRef.current = returnInsertAffiliateIdentifierImpl;
1769
1817
  isAffiliateAttributionValidImplRef.current = isAffiliateAttributionValidImpl;
1770
1818
  getAffiliateStoredDateImplRef.current = getAffiliateStoredDateImpl;
1819
+ getAffiliateExpiryTimestampImplRef.current = getAffiliateExpiryTimestampImpl;
1771
1820
  storeExpectedStoreTransactionImplRef.current = storeExpectedStoreTransactionImpl;
1772
1821
  returnUserAccountTokenAndStoreExpectedTransactionImplRef.current = returnUserAccountTokenAndStoreExpectedTransactionImpl;
1773
1822
  validatePurchaseWithIapticAPIImplRef.current = validatePurchaseWithIapticAPIImpl;
@@ -1778,8 +1827,8 @@ const DeepLinkIapProvider = ({ children, }) => {
1778
1827
  // STABLE WRAPPERS: useCallback with [] deps that delegate to refs
1779
1828
  // These provide stable function references that always call current implementations
1780
1829
  // ============================================================================
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);
1830
+ const initialize = (0, react_1.useCallback)((code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime, preventAffiliateTransfer) => __awaiter(void 0, void 0, void 0, function* () {
1831
+ return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime, preventAffiliateTransfer);
1783
1832
  }), []);
1784
1833
  const setShortCode = (0, react_1.useCallback)((shortCode) => __awaiter(void 0, void 0, void 0, function* () {
1785
1834
  return setShortCodeImplRef.current(shortCode);
@@ -1796,6 +1845,9 @@ const DeepLinkIapProvider = ({ children, }) => {
1796
1845
  const getAffiliateStoredDate = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
1797
1846
  return getAffiliateStoredDateImplRef.current();
1798
1847
  }), []);
1848
+ const getAffiliateExpiryTimestamp = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
1849
+ return getAffiliateExpiryTimestampImplRef.current();
1850
+ }), []);
1799
1851
  const storeExpectedStoreTransaction = (0, react_1.useCallback)((purchaseToken) => __awaiter(void 0, void 0, void 0, function* () {
1800
1852
  return storeExpectedStoreTransactionImplRef.current(purchaseToken);
1801
1853
  }), []);
@@ -1813,14 +1865,18 @@ const DeepLinkIapProvider = ({ children, }) => {
1813
1865
  }), []);
1814
1866
  const setInsertAffiliateIdentifierChangeCallbackHandler = (0, react_1.useCallback)((callback) => {
1815
1867
  insertAffiliateIdentifierChangeCallbackRef.current = callback;
1816
- // If callback is being set, immediately call it with the current identifier value
1868
+ // If callback is being set, immediately call it with the current identifier and offer code values
1817
1869
  // This ensures callbacks registered after initialization still receive the current state (including null if expired/not set)
1818
1870
  if (callback) {
1819
- returnInsertAffiliateIdentifierImpl().then(identifier => {
1871
+ Promise.all([
1872
+ returnInsertAffiliateIdentifierImpl(),
1873
+ getValueFromAsync(ASYNC_KEYS.OFFER_CODE)
1874
+ ]).then(([identifier, storedOfferCode]) => {
1820
1875
  // Verify callback is still the same (wasn't replaced during async operation)
1821
1876
  if (insertAffiliateIdentifierChangeCallbackRef.current === callback) {
1822
- verboseLog(`Calling callback immediately with current identifier: ${identifier}`);
1823
- callback(identifier);
1877
+ const offerCode = storedOfferCode && storedOfferCode.length > 0 ? storedOfferCode : null;
1878
+ verboseLog(`Calling callback immediately with current identifier: ${identifier}, offerCode: ${offerCode}`);
1879
+ callback(identifier, offerCode);
1824
1880
  }
1825
1881
  });
1826
1882
  }
@@ -1837,6 +1893,7 @@ const DeepLinkIapProvider = ({ children, }) => {
1837
1893
  returnInsertAffiliateIdentifier,
1838
1894
  isAffiliateAttributionValid,
1839
1895
  getAffiliateStoredDate,
1896
+ getAffiliateExpiryTimestamp,
1840
1897
  storeExpectedStoreTransaction,
1841
1898
  returnUserAccountTokenAndStoreExpectedTransaction,
1842
1899
  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.12.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
@@ -930,6 +1029,10 @@ const rawIdentifier = await returnInsertAffiliateIdentifier(true);
930
1029
  - iOS: Add URL scheme to Info.plist and configure associated domains
931
1030
  - Android: Add intent filters to AndroidManifest.xml
932
1031
 
1032
+ **Problem:** "This screen does not exist" error with Expo Router
1033
+ - **Cause:** Both Insert Affiliate SDK and Expo Router are trying to handle the same URL
1034
+ - **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.
1035
+
933
1036
  **Problem:** "No affiliate identifier found"
934
1037
  - **Cause:** User hasn't clicked an affiliate link yet
935
1038
  - **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}`);
@@ -1605,6 +1623,31 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1605
1623
  }
1606
1624
  };
1607
1625
 
1626
+ // Get the timestamp when attribution expires (stored date + timeout duration in ms)
1627
+ // Returns null if no timeout is configured or no stored date exists
1628
+ const getAffiliateExpiryTimestampImpl = async (): Promise<number | null> => {
1629
+ try {
1630
+ if (!affiliateAttributionActiveTime) {
1631
+ verboseLog('No attribution timeout configured, no expiry timestamp');
1632
+ return null;
1633
+ }
1634
+
1635
+ const storedDate = await getAffiliateStoredDateImpl();
1636
+ if (!storedDate) {
1637
+ verboseLog('No stored date found, cannot calculate expiry timestamp');
1638
+ return null;
1639
+ }
1640
+
1641
+ // Convert timeout from seconds to milliseconds and add to stored date
1642
+ const expiryTimestamp = storedDate.getTime() + (affiliateAttributionActiveTime * 1000);
1643
+ verboseLog(`Attribution expiry timestamp: ${expiryTimestamp} (${new Date(expiryTimestamp).toISOString()})`);
1644
+ return expiryTimestamp;
1645
+ } catch (error) {
1646
+ verboseLog(`Error getting affiliate expiry timestamp: ${error}`);
1647
+ return null;
1648
+ }
1649
+ };
1650
+
1608
1651
  // MARK: Insert Affiliate Identifier
1609
1652
 
1610
1653
  const setInsertAffiliateIdentifierImpl = async (
@@ -1701,7 +1744,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1701
1744
  };
1702
1745
 
1703
1746
  async function storeInsertAffiliateIdentifier({ link, source }: { link: string; source: AffiliateAssociationSource }) {
1704
- console.log(`[Insert Affiliate] Storing affiliate identifier: ${link} (source: ${source})`);
1747
+ verboseLog(`Storing affiliate identifier: ${link} (source: ${source})`);
1705
1748
 
1706
1749
  // Check if we're trying to store the same link (prevent duplicate storage)
1707
1750
  const existingLink = await getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
@@ -1710,6 +1753,13 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1710
1753
  return;
1711
1754
  }
1712
1755
 
1756
+ // Prevent transfer of affiliate if enabled - keep original affiliate
1757
+ verboseLog(`preventAffiliateTransfer check: enabled=${preventAffiliateTransferRef.current}, existingLink=${existingLink}, newLink=${link}`);
1758
+ if (preventAffiliateTransferRef.current && existingLink && existingLink !== link) {
1759
+ verboseLog(`Transfer blocked: existing affiliate "${existingLink}" protected from being replaced by "${link}"`);
1760
+ return;
1761
+ }
1762
+
1713
1763
  verboseLog(`Updating React state with referrer link: ${link}`);
1714
1764
  setReferrerLink(link);
1715
1765
  verboseLog(`Saving referrer link to AsyncStorage...`);
@@ -1724,13 +1774,13 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
1724
1774
 
1725
1775
  // Automatically fetch and store offer code for any affiliate identifier
1726
1776
  verboseLog('Attempting to fetch offer code for stored affiliate identifier...');
1727
- await retrieveAndStoreOfferCode(link);
1777
+ const offerCode = await retrieveAndStoreOfferCode(link);
1728
1778
 
1729
- // Trigger callback with the current affiliate identifier
1779
+ // Trigger callback with the current affiliate identifier and offer code
1730
1780
  if (insertAffiliateIdentifierChangeCallbackRef.current) {
1731
1781
  const currentIdentifier = await returnInsertAffiliateIdentifierImpl();
1732
- verboseLog(`Triggering callback with identifier: ${currentIdentifier}`);
1733
- insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier);
1782
+ verboseLog(`Triggering callback with identifier: ${currentIdentifier}, offerCode: ${offerCode}`);
1783
+ insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier, offerCode);
1734
1784
  }
1735
1785
 
1736
1786
  // Report this new affiliate association to the backend (fire and forget)
@@ -2016,27 +2066,30 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2016
2066
  }
2017
2067
  };
2018
2068
 
2019
- const retrieveAndStoreOfferCode = async (affiliateLink: string): Promise<void> => {
2069
+ const retrieveAndStoreOfferCode = async (affiliateLink: string): Promise<string | null> => {
2020
2070
  try {
2021
2071
  verboseLog(`Attempting to retrieve and store offer code for: ${affiliateLink}`);
2022
-
2072
+
2023
2073
  const offerCode = await fetchOfferCode(affiliateLink);
2024
-
2074
+
2025
2075
  if (offerCode && offerCode.length > 0) {
2026
2076
  // Store in both AsyncStorage and state
2027
- await saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, offerCode);
2077
+ await saveValueInAsync(ASYNC_KEYS.OFFER_CODE, offerCode);
2028
2078
  setOfferCode(offerCode);
2029
2079
  verboseLog(`Successfully stored offer code: ${offerCode}`);
2030
2080
  console.log('[Insert Affiliate] Offer code retrieved and stored successfully');
2081
+ return offerCode;
2031
2082
  } else {
2032
2083
  verboseLog('No valid offer code found to store');
2033
2084
  // Clear stored offer code if none found
2034
- await saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, '');
2085
+ await saveValueInAsync(ASYNC_KEYS.OFFER_CODE, '');
2035
2086
  setOfferCode(null);
2087
+ return null;
2036
2088
  }
2037
2089
  } catch (error) {
2038
2090
  console.error('[Insert Affiliate] Error retrieving and storing offer code:', error);
2039
2091
  verboseLog(`Error in retrieveAndStoreOfferCode: ${error}`);
2092
+ return null;
2040
2093
  }
2041
2094
  };
2042
2095
 
@@ -2059,6 +2112,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2059
2112
  returnInsertAffiliateIdentifierImplRef.current = returnInsertAffiliateIdentifierImpl;
2060
2113
  isAffiliateAttributionValidImplRef.current = isAffiliateAttributionValidImpl;
2061
2114
  getAffiliateStoredDateImplRef.current = getAffiliateStoredDateImpl;
2115
+ getAffiliateExpiryTimestampImplRef.current = getAffiliateExpiryTimestampImpl;
2062
2116
  storeExpectedStoreTransactionImplRef.current = storeExpectedStoreTransactionImpl;
2063
2117
  returnUserAccountTokenAndStoreExpectedTransactionImplRef.current = returnUserAccountTokenAndStoreExpectedTransactionImpl;
2064
2118
  validatePurchaseWithIapticAPIImplRef.current = validatePurchaseWithIapticAPIImpl;
@@ -2075,9 +2129,10 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2075
2129
  verboseLogging?: boolean,
2076
2130
  insertLinksEnabled?: boolean,
2077
2131
  insertLinksClipboardEnabled?: boolean,
2078
- affiliateAttributionActiveTime?: number
2132
+ affiliateAttributionActiveTime?: number,
2133
+ preventAffiliateTransfer?: boolean
2079
2134
  ): Promise<void> => {
2080
- return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime);
2135
+ return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime, preventAffiliateTransfer);
2081
2136
  }, []);
2082
2137
 
2083
2138
  const setShortCode = useCallback(async (shortCode: string): Promise<boolean> => {
@@ -2100,6 +2155,10 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2100
2155
  return getAffiliateStoredDateImplRef.current();
2101
2156
  }, []);
2102
2157
 
2158
+ const getAffiliateExpiryTimestamp = useCallback(async (): Promise<number | null> => {
2159
+ return getAffiliateExpiryTimestampImplRef.current();
2160
+ }, []);
2161
+
2103
2162
  const storeExpectedStoreTransaction = useCallback(async (purchaseToken: string): Promise<void> => {
2104
2163
  return storeExpectedStoreTransactionImplRef.current(purchaseToken);
2105
2164
  }, []);
@@ -2128,14 +2187,18 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2128
2187
  const setInsertAffiliateIdentifierChangeCallbackHandler = useCallback((callback: InsertAffiliateIdentifierChangeCallback | null): void => {
2129
2188
  insertAffiliateIdentifierChangeCallbackRef.current = callback;
2130
2189
 
2131
- // If callback is being set, immediately call it with the current identifier value
2190
+ // If callback is being set, immediately call it with the current identifier and offer code values
2132
2191
  // This ensures callbacks registered after initialization still receive the current state (including null if expired/not set)
2133
2192
  if (callback) {
2134
- returnInsertAffiliateIdentifierImpl().then(identifier => {
2193
+ Promise.all([
2194
+ returnInsertAffiliateIdentifierImpl(),
2195
+ getValueFromAsync(ASYNC_KEYS.OFFER_CODE)
2196
+ ]).then(([identifier, storedOfferCode]) => {
2135
2197
  // Verify callback is still the same (wasn't replaced during async operation)
2136
2198
  if (insertAffiliateIdentifierChangeCallbackRef.current === callback) {
2137
- verboseLog(`Calling callback immediately with current identifier: ${identifier}`);
2138
- callback(identifier);
2199
+ const offerCode = storedOfferCode && storedOfferCode.length > 0 ? storedOfferCode : null;
2200
+ verboseLog(`Calling callback immediately with current identifier: ${identifier}, offerCode: ${offerCode}`);
2201
+ callback(identifier, offerCode);
2139
2202
  }
2140
2203
  });
2141
2204
  }
@@ -2156,6 +2219,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
2156
2219
  returnInsertAffiliateIdentifier,
2157
2220
  isAffiliateAttributionValid,
2158
2221
  getAffiliateStoredDate,
2222
+ getAffiliateExpiryTimestamp,
2159
2223
  storeExpectedStoreTransaction,
2160
2224
  returnUserAccountTokenAndStoreExpectedTransaction,
2161
2225
  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,