insert-affiliate-react-native-sdk 1.11.1 → 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.
- package/.claude/settings.local.json +4 -1
- package/dist/DeepLinkIapProvider.d.ts +3 -2
- package/dist/DeepLinkIapProvider.js +83 -15
- package/dist/useDeepLinkIapProvider.d.ts +2 -1
- package/dist/useDeepLinkIapProvider.js +2 -1
- package/docs/deep-linking-appsflyer.md +1 -0
- package/docs/deep-linking-branch.md +1 -0
- package/package.json +1 -1
- package/readme.md +113 -8
- package/src/DeepLinkIapProvider.tsx +97 -21
- package/src/useDeepLinkIapProvider.tsx +2 -0
|
@@ -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
|
-
|
|
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
|
-
|
|
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(`
|
|
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('
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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,6 +1865,21 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1813
1865
|
}), []);
|
|
1814
1866
|
const setInsertAffiliateIdentifierChangeCallbackHandler = (0, react_1.useCallback)((callback) => {
|
|
1815
1867
|
insertAffiliateIdentifierChangeCallbackRef.current = callback;
|
|
1868
|
+
// If callback is being set, immediately call it with the current identifier and offer code values
|
|
1869
|
+
// This ensures callbacks registered after initialization still receive the current state (including null if expired/not set)
|
|
1870
|
+
if (callback) {
|
|
1871
|
+
Promise.all([
|
|
1872
|
+
returnInsertAffiliateIdentifierImpl(),
|
|
1873
|
+
getValueFromAsync(ASYNC_KEYS.OFFER_CODE)
|
|
1874
|
+
]).then(([identifier, storedOfferCode]) => {
|
|
1875
|
+
// Verify callback is still the same (wasn't replaced during async operation)
|
|
1876
|
+
if (insertAffiliateIdentifierChangeCallbackRef.current === callback) {
|
|
1877
|
+
const offerCode = storedOfferCode && storedOfferCode.length > 0 ? storedOfferCode : null;
|
|
1878
|
+
verboseLog(`Calling callback immediately with current identifier: ${identifier}, offerCode: ${offerCode}`);
|
|
1879
|
+
callback(identifier, offerCode);
|
|
1880
|
+
}
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1816
1883
|
}, []);
|
|
1817
1884
|
const handleInsertLinks = (0, react_1.useCallback)((url) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1818
1885
|
return handleInsertLinksImplRef.current(url);
|
|
@@ -1826,6 +1893,7 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1826
1893
|
returnInsertAffiliateIdentifier,
|
|
1827
1894
|
isAffiliateAttributionValid,
|
|
1828
1895
|
getAffiliateStoredDate,
|
|
1896
|
+
getAffiliateExpiryTimestamp,
|
|
1829
1897
|
storeExpectedStoreTransaction,
|
|
1830
1898
|
returnUserAccountTokenAndStoreExpectedTransaction,
|
|
1831
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,
|
|
@@ -64,6 +64,7 @@ const DeepLinkHandler = () => {
|
|
|
64
64
|
|
|
65
65
|
if (insertAffiliateIdentifier) {
|
|
66
66
|
await Purchases.setAttributes({ "insert_affiliate": insertAffiliateIdentifier });
|
|
67
|
+
await Purchases.syncAttributesAndOfferingsIfNeeded();
|
|
67
68
|
}
|
|
68
69
|
} catch (err) {
|
|
69
70
|
console.error('Error setting affiliate identifier:', err);
|
|
@@ -50,6 +50,7 @@ const DeepLinkHandler = () => {
|
|
|
50
50
|
|
|
51
51
|
if (insertAffiliateIdentifier) {
|
|
52
52
|
await Purchases.setAttributes({ "insert_affiliate": insertAffiliateIdentifier });
|
|
53
|
+
await Purchases.syncAttributesAndOfferingsIfNeeded();
|
|
53
54
|
}
|
|
54
55
|
} catch (err) {
|
|
55
56
|
console.error('Error setting affiliate identifier:', err);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "insert-affiliate-react-native-sdk",
|
|
3
|
-
"version": "1.
|
|
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
|
|
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 {
|
|
191
|
+
const {
|
|
192
|
+
initialize,
|
|
193
|
+
isInitialized,
|
|
194
|
+
setInsertAffiliateIdentifierChangeCallback,
|
|
195
|
+
isAffiliateAttributionValid,
|
|
196
|
+
getAffiliateExpiryTimestamp
|
|
197
|
+
} = useDeepLinkIapProvider();
|
|
184
198
|
|
|
185
199
|
useEffect(() => {
|
|
186
200
|
if (!isInitialized) {
|
|
@@ -188,21 +202,72 @@ const App = () => {
|
|
|
188
202
|
}
|
|
189
203
|
}, [initialize, isInitialized]);
|
|
190
204
|
|
|
191
|
-
// Set RevenueCat
|
|
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
|
-
|
|
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
|
+
});
|
|
222
|
+
await Purchases.syncAttributesAndOfferingsIfNeeded();
|
|
196
223
|
}
|
|
197
224
|
});
|
|
198
225
|
|
|
199
226
|
return () => setInsertAffiliateIdentifierChangeCallback(null);
|
|
200
227
|
}, [setInsertAffiliateIdentifierChangeCallback]);
|
|
201
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
|
+
|
|
202
265
|
return <YourAppContent />;
|
|
203
266
|
};
|
|
204
267
|
```
|
|
205
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
|
+
|
|
206
271
|
**Step 2: Webhook Setup**
|
|
207
272
|
|
|
208
273
|
1. In RevenueCat, [create a new webhook](https://www.revenuecat.com/docs/integrations/webhooks)
|
|
@@ -226,9 +291,12 @@ const App = () => {
|
|
|
226
291
|
|
|
227
292
|
```bash
|
|
228
293
|
npm install react-native-adapty
|
|
229
|
-
cd ios && pod install && cd ..
|
|
230
294
|
```
|
|
231
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
|
+
|
|
232
300
|
Complete the [Adapty SDK installation](https://adapty.io/docs/sdk-installation-reactnative) for any additional platform-specific setup.
|
|
233
301
|
|
|
234
302
|
**Step 2: Code Setup**
|
|
@@ -509,9 +577,13 @@ const App = () => {
|
|
|
509
577
|
const { setInsertAffiliateIdentifierChangeCallback } = useDeepLinkIapProvider();
|
|
510
578
|
|
|
511
579
|
useEffect(() => {
|
|
512
|
-
setInsertAffiliateIdentifierChangeCallback(async (identifier) => {
|
|
580
|
+
setInsertAffiliateIdentifierChangeCallback(async (identifier, offerCode) => {
|
|
513
581
|
if (identifier) {
|
|
514
|
-
await Purchases.setAttributes({
|
|
582
|
+
await Purchases.setAttributes({
|
|
583
|
+
"insert_affiliate": identifier,
|
|
584
|
+
"affiliateOfferCode": offerCode || "" // For RevenueCat Targeting
|
|
585
|
+
});
|
|
586
|
+
await Purchases.syncAttributesAndOfferingsIfNeeded();
|
|
515
587
|
}
|
|
516
588
|
});
|
|
517
589
|
|
|
@@ -522,6 +594,8 @@ const App = () => {
|
|
|
522
594
|
};
|
|
523
595
|
```
|
|
524
596
|
|
|
597
|
+
> **Note:** For full RevenueCat integration with attribution timeout cleanup, see the [complete example in Option 1](#option-1-revenuecat-recommended) above.
|
|
598
|
+
|
|
525
599
|
**With Adapty:**
|
|
526
600
|
|
|
527
601
|
```javascript
|
|
@@ -671,6 +745,33 @@ Update your `ios/YourApp/AppDelegate.mm`:
|
|
|
671
745
|
}
|
|
672
746
|
```
|
|
673
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
|
+
|
|
674
775
|
**Testing Deep Links:**
|
|
675
776
|
|
|
676
777
|
```bash
|
|
@@ -928,6 +1029,10 @@ const rawIdentifier = await returnInsertAffiliateIdentifier(true);
|
|
|
928
1029
|
- iOS: Add URL scheme to Info.plist and configure associated domains
|
|
929
1030
|
- Android: Add intent filters to AndroidManifest.xml
|
|
930
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
|
+
|
|
931
1036
|
**Problem:** "No affiliate identifier found"
|
|
932
1037
|
- **Cause:** User hasn't clicked an affiliate link yet
|
|
933
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
|
-
|
|
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
|
-
|
|
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(`
|
|
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('
|
|
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
|
-
|
|
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<
|
|
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.
|
|
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.
|
|
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
|
}, []);
|
|
@@ -2127,6 +2186,22 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
2127
2186
|
|
|
2128
2187
|
const setInsertAffiliateIdentifierChangeCallbackHandler = useCallback((callback: InsertAffiliateIdentifierChangeCallback | null): void => {
|
|
2129
2188
|
insertAffiliateIdentifierChangeCallbackRef.current = callback;
|
|
2189
|
+
|
|
2190
|
+
// If callback is being set, immediately call it with the current identifier and offer code values
|
|
2191
|
+
// This ensures callbacks registered after initialization still receive the current state (including null if expired/not set)
|
|
2192
|
+
if (callback) {
|
|
2193
|
+
Promise.all([
|
|
2194
|
+
returnInsertAffiliateIdentifierImpl(),
|
|
2195
|
+
getValueFromAsync(ASYNC_KEYS.OFFER_CODE)
|
|
2196
|
+
]).then(([identifier, storedOfferCode]) => {
|
|
2197
|
+
// Verify callback is still the same (wasn't replaced during async operation)
|
|
2198
|
+
if (insertAffiliateIdentifierChangeCallbackRef.current === callback) {
|
|
2199
|
+
const offerCode = storedOfferCode && storedOfferCode.length > 0 ? storedOfferCode : null;
|
|
2200
|
+
verboseLog(`Calling callback immediately with current identifier: ${identifier}, offerCode: ${offerCode}`);
|
|
2201
|
+
callback(identifier, offerCode);
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2130
2205
|
}, []);
|
|
2131
2206
|
|
|
2132
2207
|
const handleInsertLinks = useCallback(async (url: string): Promise<boolean> => {
|
|
@@ -2144,6 +2219,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
2144
2219
|
returnInsertAffiliateIdentifier,
|
|
2145
2220
|
isAffiliateAttributionValid,
|
|
2146
2221
|
getAffiliateStoredDate,
|
|
2222
|
+
getAffiliateExpiryTimestamp,
|
|
2147
2223
|
storeExpectedStoreTransaction,
|
|
2148
2224
|
returnUserAccountTokenAndStoreExpectedTransaction,
|
|
2149
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,
|