insert-affiliate-react-native-sdk 1.11.2 → 1.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/settings.local.json +4 -1
- package/dist/DeepLinkIapProvider.d.ts +3 -2
- package/dist/DeepLinkIapProvider.js +81 -21
- package/dist/useDeepLinkIapProvider.d.ts +2 -1
- package/dist/useDeepLinkIapProvider.js +2 -1
- package/package.json +1 -1
- package/readme.md +134 -8
- package/src/DeepLinkIapProvider.tsx +96 -28
- 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) {
|
|
@@ -1166,7 +1182,7 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1166
1182
|
const isValidCharacters = /^[a-zA-Z0-9_]+$/.test(referringLink);
|
|
1167
1183
|
return isValidCharacters && referringLink.length >= 3 && referringLink.length <= 25;
|
|
1168
1184
|
};
|
|
1169
|
-
const checkAffiliateExists = (
|
|
1185
|
+
const checkAffiliateExists = (affiliateCode_1, ...args_1) => __awaiter(void 0, [affiliateCode_1, ...args_1], void 0, function* (affiliateCode, trackUsage = false) {
|
|
1170
1186
|
try {
|
|
1171
1187
|
const activeCompanyCode = yield getActiveCompanyCode();
|
|
1172
1188
|
if (!activeCompanyCode) {
|
|
@@ -1178,6 +1194,9 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1178
1194
|
companyId: activeCompanyCode,
|
|
1179
1195
|
affiliateCode: affiliateCode
|
|
1180
1196
|
};
|
|
1197
|
+
if (trackUsage) {
|
|
1198
|
+
payload.trackUsage = true;
|
|
1199
|
+
}
|
|
1181
1200
|
verboseLog(`Checking if affiliate exists: ${affiliateCode}`);
|
|
1182
1201
|
const response = yield axios_1.default.post(url, payload, {
|
|
1183
1202
|
headers: {
|
|
@@ -1251,7 +1270,7 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1251
1270
|
const capitalisedShortCode = shortCode.toUpperCase();
|
|
1252
1271
|
isShortCode(capitalisedShortCode);
|
|
1253
1272
|
// Check if the affiliate exists before storing
|
|
1254
|
-
const exists = yield checkAffiliateExists(capitalisedShortCode);
|
|
1273
|
+
const exists = yield checkAffiliateExists(capitalisedShortCode, true);
|
|
1255
1274
|
if (exists) {
|
|
1256
1275
|
// If affiliate exists, set the Insert Affiliate Identifier
|
|
1257
1276
|
yield storeInsertAffiliateIdentifier({ link: capitalisedShortCode, source: 'short_code_manual' });
|
|
@@ -1393,6 +1412,29 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1393
1412
|
return null;
|
|
1394
1413
|
}
|
|
1395
1414
|
});
|
|
1415
|
+
// Get the timestamp when attribution expires (stored date + timeout duration in ms)
|
|
1416
|
+
// Returns null if no timeout is configured or no stored date exists
|
|
1417
|
+
const getAffiliateExpiryTimestampImpl = () => __awaiter(void 0, void 0, void 0, function* () {
|
|
1418
|
+
try {
|
|
1419
|
+
if (!affiliateAttributionActiveTime) {
|
|
1420
|
+
verboseLog('No attribution timeout configured, no expiry timestamp');
|
|
1421
|
+
return null;
|
|
1422
|
+
}
|
|
1423
|
+
const storedDate = yield getAffiliateStoredDateImpl();
|
|
1424
|
+
if (!storedDate) {
|
|
1425
|
+
verboseLog('No stored date found, cannot calculate expiry timestamp');
|
|
1426
|
+
return null;
|
|
1427
|
+
}
|
|
1428
|
+
// Convert timeout from seconds to milliseconds and add to stored date
|
|
1429
|
+
const expiryTimestamp = storedDate.getTime() + (affiliateAttributionActiveTime * 1000);
|
|
1430
|
+
verboseLog(`Attribution expiry timestamp: ${expiryTimestamp} (${new Date(expiryTimestamp).toISOString()})`);
|
|
1431
|
+
return expiryTimestamp;
|
|
1432
|
+
}
|
|
1433
|
+
catch (error) {
|
|
1434
|
+
verboseLog(`Error getting affiliate expiry timestamp: ${error}`);
|
|
1435
|
+
return null;
|
|
1436
|
+
}
|
|
1437
|
+
});
|
|
1396
1438
|
// MARK: Insert Affiliate Identifier
|
|
1397
1439
|
const setInsertAffiliateIdentifierImpl = (referringLink) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1398
1440
|
console.log('[Insert Affiliate] Setting affiliate identifier.');
|
|
@@ -1471,13 +1513,19 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1471
1513
|
});
|
|
1472
1514
|
function storeInsertAffiliateIdentifier(_a) {
|
|
1473
1515
|
return __awaiter(this, arguments, void 0, function* ({ link, source }) {
|
|
1474
|
-
|
|
1516
|
+
verboseLog(`Storing affiliate identifier: ${link} (source: ${source})`);
|
|
1475
1517
|
// Check if we're trying to store the same link (prevent duplicate storage)
|
|
1476
1518
|
const existingLink = yield getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
|
|
1477
1519
|
if (existingLink === link) {
|
|
1478
1520
|
verboseLog(`Link ${link} is already stored, skipping duplicate storage`);
|
|
1479
1521
|
return;
|
|
1480
1522
|
}
|
|
1523
|
+
// Prevent transfer of affiliate if enabled - keep original affiliate
|
|
1524
|
+
verboseLog(`preventAffiliateTransfer check: enabled=${preventAffiliateTransferRef.current}, existingLink=${existingLink}, newLink=${link}`);
|
|
1525
|
+
if (preventAffiliateTransferRef.current && existingLink && existingLink !== link) {
|
|
1526
|
+
verboseLog(`Transfer blocked: existing affiliate "${existingLink}" protected from being replaced by "${link}"`);
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1481
1529
|
verboseLog(`Updating React state with referrer link: ${link}`);
|
|
1482
1530
|
setReferrerLink(link);
|
|
1483
1531
|
verboseLog(`Saving referrer link to AsyncStorage...`);
|
|
@@ -1489,12 +1537,12 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1489
1537
|
verboseLog(`Referrer link saved to AsyncStorage successfully`);
|
|
1490
1538
|
// Automatically fetch and store offer code for any affiliate identifier
|
|
1491
1539
|
verboseLog('Attempting to fetch offer code for stored affiliate identifier...');
|
|
1492
|
-
yield retrieveAndStoreOfferCode(link);
|
|
1493
|
-
// Trigger callback with the current affiliate identifier
|
|
1540
|
+
const offerCode = yield retrieveAndStoreOfferCode(link);
|
|
1541
|
+
// Trigger callback with the current affiliate identifier and offer code
|
|
1494
1542
|
if (insertAffiliateIdentifierChangeCallbackRef.current) {
|
|
1495
1543
|
const currentIdentifier = yield returnInsertAffiliateIdentifierImpl();
|
|
1496
|
-
verboseLog(`Triggering callback with identifier: ${currentIdentifier}`);
|
|
1497
|
-
insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier);
|
|
1544
|
+
verboseLog(`Triggering callback with identifier: ${currentIdentifier}, offerCode: ${offerCode}`);
|
|
1545
|
+
insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier, offerCode);
|
|
1498
1546
|
}
|
|
1499
1547
|
// Report this new affiliate association to the backend (fire and forget)
|
|
1500
1548
|
const fullIdentifier = yield returnInsertAffiliateIdentifierImpl();
|
|
@@ -1734,21 +1782,24 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1734
1782
|
const offerCode = yield fetchOfferCode(affiliateLink);
|
|
1735
1783
|
if (offerCode && offerCode.length > 0) {
|
|
1736
1784
|
// Store in both AsyncStorage and state
|
|
1737
|
-
yield saveValueInAsync(ASYNC_KEYS.
|
|
1785
|
+
yield saveValueInAsync(ASYNC_KEYS.OFFER_CODE, offerCode);
|
|
1738
1786
|
setOfferCode(offerCode);
|
|
1739
1787
|
verboseLog(`Successfully stored offer code: ${offerCode}`);
|
|
1740
1788
|
console.log('[Insert Affiliate] Offer code retrieved and stored successfully');
|
|
1789
|
+
return offerCode;
|
|
1741
1790
|
}
|
|
1742
1791
|
else {
|
|
1743
1792
|
verboseLog('No valid offer code found to store');
|
|
1744
1793
|
// Clear stored offer code if none found
|
|
1745
|
-
yield saveValueInAsync(ASYNC_KEYS.
|
|
1794
|
+
yield saveValueInAsync(ASYNC_KEYS.OFFER_CODE, '');
|
|
1746
1795
|
setOfferCode(null);
|
|
1796
|
+
return null;
|
|
1747
1797
|
}
|
|
1748
1798
|
}
|
|
1749
1799
|
catch (error) {
|
|
1750
1800
|
console.error('[Insert Affiliate] Error retrieving and storing offer code:', error);
|
|
1751
1801
|
verboseLog(`Error in retrieveAndStoreOfferCode: ${error}`);
|
|
1802
|
+
return null;
|
|
1752
1803
|
}
|
|
1753
1804
|
});
|
|
1754
1805
|
const removeSpecialCharacters = (offerCode) => {
|
|
@@ -1768,6 +1819,7 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1768
1819
|
returnInsertAffiliateIdentifierImplRef.current = returnInsertAffiliateIdentifierImpl;
|
|
1769
1820
|
isAffiliateAttributionValidImplRef.current = isAffiliateAttributionValidImpl;
|
|
1770
1821
|
getAffiliateStoredDateImplRef.current = getAffiliateStoredDateImpl;
|
|
1822
|
+
getAffiliateExpiryTimestampImplRef.current = getAffiliateExpiryTimestampImpl;
|
|
1771
1823
|
storeExpectedStoreTransactionImplRef.current = storeExpectedStoreTransactionImpl;
|
|
1772
1824
|
returnUserAccountTokenAndStoreExpectedTransactionImplRef.current = returnUserAccountTokenAndStoreExpectedTransactionImpl;
|
|
1773
1825
|
validatePurchaseWithIapticAPIImplRef.current = validatePurchaseWithIapticAPIImpl;
|
|
@@ -1778,8 +1830,8 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1778
1830
|
// STABLE WRAPPERS: useCallback with [] deps that delegate to refs
|
|
1779
1831
|
// These provide stable function references that always call current implementations
|
|
1780
1832
|
// ============================================================================
|
|
1781
|
-
const initialize = (0, react_1.useCallback)((code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1782
|
-
return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime);
|
|
1833
|
+
const initialize = (0, react_1.useCallback)((code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime, preventAffiliateTransfer) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1834
|
+
return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime, preventAffiliateTransfer);
|
|
1783
1835
|
}), []);
|
|
1784
1836
|
const setShortCode = (0, react_1.useCallback)((shortCode) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1785
1837
|
return setShortCodeImplRef.current(shortCode);
|
|
@@ -1796,6 +1848,9 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1796
1848
|
const getAffiliateStoredDate = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1797
1849
|
return getAffiliateStoredDateImplRef.current();
|
|
1798
1850
|
}), []);
|
|
1851
|
+
const getAffiliateExpiryTimestamp = (0, react_1.useCallback)(() => __awaiter(void 0, void 0, void 0, function* () {
|
|
1852
|
+
return getAffiliateExpiryTimestampImplRef.current();
|
|
1853
|
+
}), []);
|
|
1799
1854
|
const storeExpectedStoreTransaction = (0, react_1.useCallback)((purchaseToken) => __awaiter(void 0, void 0, void 0, function* () {
|
|
1800
1855
|
return storeExpectedStoreTransactionImplRef.current(purchaseToken);
|
|
1801
1856
|
}), []);
|
|
@@ -1813,14 +1868,18 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1813
1868
|
}), []);
|
|
1814
1869
|
const setInsertAffiliateIdentifierChangeCallbackHandler = (0, react_1.useCallback)((callback) => {
|
|
1815
1870
|
insertAffiliateIdentifierChangeCallbackRef.current = callback;
|
|
1816
|
-
// If callback is being set, immediately call it with the current identifier
|
|
1871
|
+
// If callback is being set, immediately call it with the current identifier and offer code values
|
|
1817
1872
|
// This ensures callbacks registered after initialization still receive the current state (including null if expired/not set)
|
|
1818
1873
|
if (callback) {
|
|
1819
|
-
|
|
1874
|
+
Promise.all([
|
|
1875
|
+
returnInsertAffiliateIdentifierImpl(),
|
|
1876
|
+
getValueFromAsync(ASYNC_KEYS.OFFER_CODE)
|
|
1877
|
+
]).then(([identifier, storedOfferCode]) => {
|
|
1820
1878
|
// Verify callback is still the same (wasn't replaced during async operation)
|
|
1821
1879
|
if (insertAffiliateIdentifierChangeCallbackRef.current === callback) {
|
|
1822
|
-
|
|
1823
|
-
callback
|
|
1880
|
+
const offerCode = storedOfferCode && storedOfferCode.length > 0 ? storedOfferCode : null;
|
|
1881
|
+
verboseLog(`Calling callback immediately with current identifier: ${identifier}, offerCode: ${offerCode}`);
|
|
1882
|
+
callback(identifier, offerCode);
|
|
1824
1883
|
}
|
|
1825
1884
|
});
|
|
1826
1885
|
}
|
|
@@ -1837,6 +1896,7 @@ const DeepLinkIapProvider = ({ children, }) => {
|
|
|
1837
1896
|
returnInsertAffiliateIdentifier,
|
|
1838
1897
|
isAffiliateAttributionValid,
|
|
1839
1898
|
getAffiliateStoredDate,
|
|
1899
|
+
getAffiliateExpiryTimestamp,
|
|
1840
1900
|
storeExpectedStoreTransaction,
|
|
1841
1901
|
returnUserAccountTokenAndStoreExpectedTransaction,
|
|
1842
1902
|
validatePurchaseWithIapticAPI,
|
|
@@ -9,13 +9,14 @@ declare const useDeepLinkIapProvider: () => {
|
|
|
9
9
|
returnInsertAffiliateIdentifier: (ignoreTimeout?: boolean) => Promise<string | null>;
|
|
10
10
|
isAffiliateAttributionValid: () => Promise<boolean>;
|
|
11
11
|
getAffiliateStoredDate: () => Promise<Date | null>;
|
|
12
|
+
getAffiliateExpiryTimestamp: () => Promise<number | null>;
|
|
12
13
|
trackEvent: (eventName: string) => Promise<void>;
|
|
13
14
|
setShortCode: (shortCode: string) => Promise<boolean>;
|
|
14
15
|
getAffiliateDetails: (affiliateCode: string) => Promise<import("./DeepLinkIapProvider").AffiliateDetails>;
|
|
15
16
|
setInsertAffiliateIdentifier: (referringLink: string) => Promise<void | string>;
|
|
16
17
|
setInsertAffiliateIdentifierChangeCallback: (callback: import("./DeepLinkIapProvider").InsertAffiliateIdentifierChangeCallback | null) => void;
|
|
17
18
|
handleInsertLinks: (url: string) => Promise<boolean>;
|
|
18
|
-
initialize: (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number) => Promise<void>;
|
|
19
|
+
initialize: (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number, preventAffiliateTransfer?: boolean) => Promise<void>;
|
|
19
20
|
isInitialized: boolean;
|
|
20
21
|
OfferCode: string | null;
|
|
21
22
|
};
|
|
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
const react_1 = require("react");
|
|
4
4
|
const DeepLinkIapProvider_1 = require("./DeepLinkIapProvider");
|
|
5
5
|
const useDeepLinkIapProvider = () => {
|
|
6
|
-
const { referrerLink, userId, validatePurchaseWithIapticAPI, storeExpectedStoreTransaction, returnUserAccountTokenAndStoreExpectedTransaction, returnInsertAffiliateIdentifier, isAffiliateAttributionValid, getAffiliateStoredDate, trackEvent, setShortCode, getAffiliateDetails, setInsertAffiliateIdentifier, setInsertAffiliateIdentifierChangeCallback, handleInsertLinks, initialize, isInitialized, OfferCode, } = (0, react_1.useContext)(DeepLinkIapProvider_1.DeepLinkIapContext);
|
|
6
|
+
const { referrerLink, userId, validatePurchaseWithIapticAPI, storeExpectedStoreTransaction, returnUserAccountTokenAndStoreExpectedTransaction, returnInsertAffiliateIdentifier, isAffiliateAttributionValid, getAffiliateStoredDate, getAffiliateExpiryTimestamp, trackEvent, setShortCode, getAffiliateDetails, setInsertAffiliateIdentifier, setInsertAffiliateIdentifierChangeCallback, handleInsertLinks, initialize, isInitialized, OfferCode, } = (0, react_1.useContext)(DeepLinkIapProvider_1.DeepLinkIapContext);
|
|
7
7
|
return {
|
|
8
8
|
referrerLink,
|
|
9
9
|
userId,
|
|
@@ -13,6 +13,7 @@ const useDeepLinkIapProvider = () => {
|
|
|
13
13
|
returnInsertAffiliateIdentifier,
|
|
14
14
|
isAffiliateAttributionValid,
|
|
15
15
|
getAffiliateStoredDate,
|
|
16
|
+
getAffiliateExpiryTimestamp,
|
|
16
17
|
trackEvent,
|
|
17
18
|
setShortCode,
|
|
18
19
|
getAffiliateDetails,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "insert-affiliate-react-native-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "A package for connecting with the Insert Affiliate Platform to add app based affiliate marketing.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
package/readme.md
CHANGED
|
@@ -46,10 +46,13 @@ npm install @react-native-async-storage/async-storage @react-native-clipboard/cl
|
|
|
46
46
|
|
|
47
47
|
**Step 3:** Install iOS pods (iOS only)
|
|
48
48
|
|
|
49
|
+
For **bare React Native** projects:
|
|
49
50
|
```bash
|
|
50
51
|
cd ios && pod install && cd ..
|
|
51
52
|
```
|
|
52
53
|
|
|
54
|
+
For **Expo managed workflow**: Skip this step - pods are installed automatically when you run `npx expo prebuild` or `npx expo run:ios`.
|
|
55
|
+
|
|
53
56
|
### Your First Integration
|
|
54
57
|
|
|
55
58
|
**In `index.js`** - Wrap your app with the provider:
|
|
@@ -139,7 +142,8 @@ initialize(
|
|
|
139
142
|
true, // verboseLogging - Enable for debugging (disable in production)
|
|
140
143
|
true, // insertLinksEnabled - Enable Insert Links (Insert Affiliate's built-in deep linking)
|
|
141
144
|
true, // insertLinksClipboardEnabled - Enable clipboard attribution (triggers permission prompt)
|
|
142
|
-
604800
|
|
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,11 +202,23 @@ 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
|
+
});
|
|
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({
|
|
582
|
+
await Purchases.setAttributes({
|
|
583
|
+
"insert_affiliate": identifier,
|
|
584
|
+
"affiliateOfferCode": offerCode || "" // For RevenueCat Targeting
|
|
585
|
+
});
|
|
516
586
|
await Purchases.syncAttributesAndOfferingsIfNeeded();
|
|
517
587
|
}
|
|
518
588
|
});
|
|
@@ -524,6 +594,8 @@ const App = () => {
|
|
|
524
594
|
};
|
|
525
595
|
```
|
|
526
596
|
|
|
597
|
+
> **Note:** For full RevenueCat integration with attribution timeout cleanup, see the [complete example in Option 1](#option-1-revenuecat-recommended) above.
|
|
598
|
+
|
|
527
599
|
**With Adapty:**
|
|
528
600
|
|
|
529
601
|
```javascript
|
|
@@ -673,6 +745,33 @@ Update your `ios/YourApp/AppDelegate.mm`:
|
|
|
673
745
|
}
|
|
674
746
|
```
|
|
675
747
|
|
|
748
|
+
**Step 4: Expo Router Setup (If Using Expo Router)**
|
|
749
|
+
|
|
750
|
+
If you're using Expo Router, deep links will cause a "This screen does not exist" error because both the Insert Affiliate SDK and Expo Router try to handle the incoming URL. The SDK correctly processes the affiliate attribution, but Expo Router simultaneously attempts to navigate to the URL path (e.g., `/insert-affiliate`), which doesn't exist as a route.
|
|
751
|
+
|
|
752
|
+
To fix this, create `app/+native-intent.tsx` to intercept Insert Affiliate URLs before Expo Router processes them:
|
|
753
|
+
|
|
754
|
+
```tsx
|
|
755
|
+
// app/+native-intent.tsx
|
|
756
|
+
// Tell Expo Router to skip navigation for Insert Affiliate URLs
|
|
757
|
+
// The SDK handles these via native Linking - we just prevent router errors
|
|
758
|
+
|
|
759
|
+
export function redirectSystemPath({ path }: { path: string }): string | null {
|
|
760
|
+
// Skip navigation for Insert Affiliate deep links
|
|
761
|
+
if (path.includes('insert-affiliate') || path.includes('insertAffiliate')) {
|
|
762
|
+
return null; // SDK handles it via Linking API
|
|
763
|
+
}
|
|
764
|
+
return path; // Let Expo Router handle all other URLs normally
|
|
765
|
+
}
|
|
766
|
+
```
|
|
767
|
+
|
|
768
|
+
This ensures:
|
|
769
|
+
- The Insert Affiliate SDK still receives and processes the URL via the native Linking API
|
|
770
|
+
- Expo Router ignores the URL and doesn't attempt navigation
|
|
771
|
+
- No "screen not found" errors
|
|
772
|
+
|
|
773
|
+
See the [Expo Router +native-intent docs](https://docs.expo.dev/router/reference/native-intent/) for more details.
|
|
774
|
+
|
|
676
775
|
**Testing Deep Links:**
|
|
677
776
|
|
|
678
777
|
```bash
|
|
@@ -912,6 +1011,29 @@ const rawIdentifier = await returnInsertAffiliateIdentifier(true);
|
|
|
912
1011
|
|
|
913
1012
|
</details>
|
|
914
1013
|
|
|
1014
|
+
### Prevent Affiliate Transfer
|
|
1015
|
+
|
|
1016
|
+
By default, clicking a new affiliate link will overwrite any existing attribution. Enable `preventAffiliateTransfer` to lock the first affiliate:
|
|
1017
|
+
|
|
1018
|
+
```javascript
|
|
1019
|
+
initialize(
|
|
1020
|
+
"YOUR_COMPANY_CODE",
|
|
1021
|
+
false, // verboseLogging
|
|
1022
|
+
false, // insertLinksEnabled
|
|
1023
|
+
false, // insertLinksClipboardEnabled
|
|
1024
|
+
604800, // affiliateAttributionActiveTime (7 days)
|
|
1025
|
+
true // preventAffiliateTransfer - locks first affiliate
|
|
1026
|
+
);
|
|
1027
|
+
```
|
|
1028
|
+
|
|
1029
|
+
**How it works:**
|
|
1030
|
+
- When enabled, once a user is attributed to an affiliate, that attribution is locked
|
|
1031
|
+
- New affiliate links will not overwrite the existing attribution
|
|
1032
|
+
- The callback still fires with the existing affiliate data (not the new one)
|
|
1033
|
+
- Useful for preventing "affiliate stealing" where users click competitor links
|
|
1034
|
+
|
|
1035
|
+
Learn more: [Prevent Affiliate Transfer Documentation](https://docs.insertaffiliate.com/prevent-affiliate-transfer)
|
|
1036
|
+
|
|
915
1037
|
---
|
|
916
1038
|
|
|
917
1039
|
## 🔍 Troubleshooting
|
|
@@ -930,6 +1052,10 @@ const rawIdentifier = await returnInsertAffiliateIdentifier(true);
|
|
|
930
1052
|
- iOS: Add URL scheme to Info.plist and configure associated domains
|
|
931
1053
|
- Android: Add intent filters to AndroidManifest.xml
|
|
932
1054
|
|
|
1055
|
+
**Problem:** "This screen does not exist" error with Expo Router
|
|
1056
|
+
- **Cause:** Both Insert Affiliate SDK and Expo Router are trying to handle the same URL
|
|
1057
|
+
- **Solution:** Create `app/+native-intent.tsx` to intercept Insert Affiliate URLs before Expo Router processes them. See [Expo Router Setup](#option-1-insert-links-simplest) in the Insert Links section.
|
|
1058
|
+
|
|
933
1059
|
**Problem:** "No affiliate identifier found"
|
|
934
1060
|
- **Cause:** User hasn't clicked an affiliate link yet
|
|
935
1061
|
- **Solution:** Test with simulator/emulator:
|
|
@@ -15,7 +15,7 @@ type T_DEEPLINK_IAP_PROVIDER = {
|
|
|
15
15
|
children: React.ReactNode;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
-
export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null) => void;
|
|
18
|
+
export type InsertAffiliateIdentifierChangeCallback = (identifier: string | null, offerCode: string | null) => void;
|
|
19
19
|
|
|
20
20
|
export type AffiliateDetails = {
|
|
21
21
|
affiliateName: string;
|
|
@@ -34,6 +34,7 @@ type T_DEEPLINK_IAP_CONTEXT = {
|
|
|
34
34
|
returnInsertAffiliateIdentifier: (ignoreTimeout?: boolean) => Promise<string | null>;
|
|
35
35
|
isAffiliateAttributionValid: () => Promise<boolean>;
|
|
36
36
|
getAffiliateStoredDate: () => Promise<Date | null>;
|
|
37
|
+
getAffiliateExpiryTimestamp: () => Promise<number | null>;
|
|
37
38
|
validatePurchaseWithIapticAPI: (
|
|
38
39
|
jsonIapPurchase: CustomPurchase,
|
|
39
40
|
iapticAppId: string,
|
|
@@ -52,7 +53,7 @@ type T_DEEPLINK_IAP_CONTEXT = {
|
|
|
52
53
|
) => Promise<void | string>;
|
|
53
54
|
setInsertAffiliateIdentifierChangeCallback: (callback: InsertAffiliateIdentifierChangeCallback | null) => void;
|
|
54
55
|
handleInsertLinks: (url: string) => Promise<boolean>;
|
|
55
|
-
initialize: (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number) => Promise<void>;
|
|
56
|
+
initialize: (code: string | null, verboseLogging?: boolean, insertLinksEnabled?: boolean, insertLinksClipboardEnabled?: boolean, affiliateAttributionActiveTime?: number, preventAffiliateTransfer?: boolean) => Promise<void>;
|
|
56
57
|
isInitialized: boolean;
|
|
57
58
|
};
|
|
58
59
|
|
|
@@ -78,7 +79,7 @@ const ASYNC_KEYS = {
|
|
|
78
79
|
USER_ID: '@app_user_id',
|
|
79
80
|
COMPANY_CODE: '@app_company_code',
|
|
80
81
|
USER_ACCOUNT_TOKEN: '@app_user_account_token',
|
|
81
|
-
|
|
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}`);
|
|
@@ -1349,7 +1367,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1349
1367
|
return isValidCharacters && referringLink.length >= 3 && referringLink.length <= 25;
|
|
1350
1368
|
};
|
|
1351
1369
|
|
|
1352
|
-
const checkAffiliateExists = async (affiliateCode: string): Promise<boolean> => {
|
|
1370
|
+
const checkAffiliateExists = async (affiliateCode: string, trackUsage: boolean = false): Promise<boolean> => {
|
|
1353
1371
|
try {
|
|
1354
1372
|
const activeCompanyCode = await getActiveCompanyCode();
|
|
1355
1373
|
if (!activeCompanyCode) {
|
|
@@ -1358,11 +1376,15 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1358
1376
|
}
|
|
1359
1377
|
|
|
1360
1378
|
const url = 'https://api.insertaffiliate.com/V1/checkAffiliateExists';
|
|
1361
|
-
const payload = {
|
|
1379
|
+
const payload: Record<string, any> = {
|
|
1362
1380
|
companyId: activeCompanyCode,
|
|
1363
1381
|
affiliateCode: affiliateCode
|
|
1364
1382
|
};
|
|
1365
1383
|
|
|
1384
|
+
if (trackUsage) {
|
|
1385
|
+
payload.trackUsage = true;
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1366
1388
|
verboseLog(`Checking if affiliate exists: ${affiliateCode}`);
|
|
1367
1389
|
|
|
1368
1390
|
const response = await axios.post(url, payload, {
|
|
@@ -1445,7 +1467,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1445
1467
|
isShortCode(capitalisedShortCode);
|
|
1446
1468
|
|
|
1447
1469
|
// Check if the affiliate exists before storing
|
|
1448
|
-
const exists = await checkAffiliateExists(capitalisedShortCode);
|
|
1470
|
+
const exists = await checkAffiliateExists(capitalisedShortCode, true);
|
|
1449
1471
|
|
|
1450
1472
|
if (exists) {
|
|
1451
1473
|
// If affiliate exists, set the Insert Affiliate Identifier
|
|
@@ -1605,6 +1627,31 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1605
1627
|
}
|
|
1606
1628
|
};
|
|
1607
1629
|
|
|
1630
|
+
// Get the timestamp when attribution expires (stored date + timeout duration in ms)
|
|
1631
|
+
// Returns null if no timeout is configured or no stored date exists
|
|
1632
|
+
const getAffiliateExpiryTimestampImpl = async (): Promise<number | null> => {
|
|
1633
|
+
try {
|
|
1634
|
+
if (!affiliateAttributionActiveTime) {
|
|
1635
|
+
verboseLog('No attribution timeout configured, no expiry timestamp');
|
|
1636
|
+
return null;
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
const storedDate = await getAffiliateStoredDateImpl();
|
|
1640
|
+
if (!storedDate) {
|
|
1641
|
+
verboseLog('No stored date found, cannot calculate expiry timestamp');
|
|
1642
|
+
return null;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
// Convert timeout from seconds to milliseconds and add to stored date
|
|
1646
|
+
const expiryTimestamp = storedDate.getTime() + (affiliateAttributionActiveTime * 1000);
|
|
1647
|
+
verboseLog(`Attribution expiry timestamp: ${expiryTimestamp} (${new Date(expiryTimestamp).toISOString()})`);
|
|
1648
|
+
return expiryTimestamp;
|
|
1649
|
+
} catch (error) {
|
|
1650
|
+
verboseLog(`Error getting affiliate expiry timestamp: ${error}`);
|
|
1651
|
+
return null;
|
|
1652
|
+
}
|
|
1653
|
+
};
|
|
1654
|
+
|
|
1608
1655
|
// MARK: Insert Affiliate Identifier
|
|
1609
1656
|
|
|
1610
1657
|
const setInsertAffiliateIdentifierImpl = async (
|
|
@@ -1701,7 +1748,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1701
1748
|
};
|
|
1702
1749
|
|
|
1703
1750
|
async function storeInsertAffiliateIdentifier({ link, source }: { link: string; source: AffiliateAssociationSource }) {
|
|
1704
|
-
|
|
1751
|
+
verboseLog(`Storing affiliate identifier: ${link} (source: ${source})`);
|
|
1705
1752
|
|
|
1706
1753
|
// Check if we're trying to store the same link (prevent duplicate storage)
|
|
1707
1754
|
const existingLink = await getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
|
|
@@ -1710,6 +1757,13 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1710
1757
|
return;
|
|
1711
1758
|
}
|
|
1712
1759
|
|
|
1760
|
+
// Prevent transfer of affiliate if enabled - keep original affiliate
|
|
1761
|
+
verboseLog(`preventAffiliateTransfer check: enabled=${preventAffiliateTransferRef.current}, existingLink=${existingLink}, newLink=${link}`);
|
|
1762
|
+
if (preventAffiliateTransferRef.current && existingLink && existingLink !== link) {
|
|
1763
|
+
verboseLog(`Transfer blocked: existing affiliate "${existingLink}" protected from being replaced by "${link}"`);
|
|
1764
|
+
return;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1713
1767
|
verboseLog(`Updating React state with referrer link: ${link}`);
|
|
1714
1768
|
setReferrerLink(link);
|
|
1715
1769
|
verboseLog(`Saving referrer link to AsyncStorage...`);
|
|
@@ -1724,13 +1778,13 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1724
1778
|
|
|
1725
1779
|
// Automatically fetch and store offer code for any affiliate identifier
|
|
1726
1780
|
verboseLog('Attempting to fetch offer code for stored affiliate identifier...');
|
|
1727
|
-
await retrieveAndStoreOfferCode(link);
|
|
1781
|
+
const offerCode = await retrieveAndStoreOfferCode(link);
|
|
1728
1782
|
|
|
1729
|
-
// Trigger callback with the current affiliate identifier
|
|
1783
|
+
// Trigger callback with the current affiliate identifier and offer code
|
|
1730
1784
|
if (insertAffiliateIdentifierChangeCallbackRef.current) {
|
|
1731
1785
|
const currentIdentifier = await returnInsertAffiliateIdentifierImpl();
|
|
1732
|
-
verboseLog(`Triggering callback with identifier: ${currentIdentifier}`);
|
|
1733
|
-
insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier);
|
|
1786
|
+
verboseLog(`Triggering callback with identifier: ${currentIdentifier}, offerCode: ${offerCode}`);
|
|
1787
|
+
insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier, offerCode);
|
|
1734
1788
|
}
|
|
1735
1789
|
|
|
1736
1790
|
// Report this new affiliate association to the backend (fire and forget)
|
|
@@ -2016,27 +2070,30 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
2016
2070
|
}
|
|
2017
2071
|
};
|
|
2018
2072
|
|
|
2019
|
-
const retrieveAndStoreOfferCode = async (affiliateLink: string): Promise<
|
|
2073
|
+
const retrieveAndStoreOfferCode = async (affiliateLink: string): Promise<string | null> => {
|
|
2020
2074
|
try {
|
|
2021
2075
|
verboseLog(`Attempting to retrieve and store offer code for: ${affiliateLink}`);
|
|
2022
|
-
|
|
2076
|
+
|
|
2023
2077
|
const offerCode = await fetchOfferCode(affiliateLink);
|
|
2024
|
-
|
|
2078
|
+
|
|
2025
2079
|
if (offerCode && offerCode.length > 0) {
|
|
2026
2080
|
// Store in both AsyncStorage and state
|
|
2027
|
-
await saveValueInAsync(ASYNC_KEYS.
|
|
2081
|
+
await saveValueInAsync(ASYNC_KEYS.OFFER_CODE, offerCode);
|
|
2028
2082
|
setOfferCode(offerCode);
|
|
2029
2083
|
verboseLog(`Successfully stored offer code: ${offerCode}`);
|
|
2030
2084
|
console.log('[Insert Affiliate] Offer code retrieved and stored successfully');
|
|
2085
|
+
return offerCode;
|
|
2031
2086
|
} else {
|
|
2032
2087
|
verboseLog('No valid offer code found to store');
|
|
2033
2088
|
// Clear stored offer code if none found
|
|
2034
|
-
await saveValueInAsync(ASYNC_KEYS.
|
|
2089
|
+
await saveValueInAsync(ASYNC_KEYS.OFFER_CODE, '');
|
|
2035
2090
|
setOfferCode(null);
|
|
2091
|
+
return null;
|
|
2036
2092
|
}
|
|
2037
2093
|
} catch (error) {
|
|
2038
2094
|
console.error('[Insert Affiliate] Error retrieving and storing offer code:', error);
|
|
2039
2095
|
verboseLog(`Error in retrieveAndStoreOfferCode: ${error}`);
|
|
2096
|
+
return null;
|
|
2040
2097
|
}
|
|
2041
2098
|
};
|
|
2042
2099
|
|
|
@@ -2059,6 +2116,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
2059
2116
|
returnInsertAffiliateIdentifierImplRef.current = returnInsertAffiliateIdentifierImpl;
|
|
2060
2117
|
isAffiliateAttributionValidImplRef.current = isAffiliateAttributionValidImpl;
|
|
2061
2118
|
getAffiliateStoredDateImplRef.current = getAffiliateStoredDateImpl;
|
|
2119
|
+
getAffiliateExpiryTimestampImplRef.current = getAffiliateExpiryTimestampImpl;
|
|
2062
2120
|
storeExpectedStoreTransactionImplRef.current = storeExpectedStoreTransactionImpl;
|
|
2063
2121
|
returnUserAccountTokenAndStoreExpectedTransactionImplRef.current = returnUserAccountTokenAndStoreExpectedTransactionImpl;
|
|
2064
2122
|
validatePurchaseWithIapticAPIImplRef.current = validatePurchaseWithIapticAPIImpl;
|
|
@@ -2075,9 +2133,10 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
2075
2133
|
verboseLogging?: boolean,
|
|
2076
2134
|
insertLinksEnabled?: boolean,
|
|
2077
2135
|
insertLinksClipboardEnabled?: boolean,
|
|
2078
|
-
affiliateAttributionActiveTime?: number
|
|
2136
|
+
affiliateAttributionActiveTime?: number,
|
|
2137
|
+
preventAffiliateTransfer?: boolean
|
|
2079
2138
|
): Promise<void> => {
|
|
2080
|
-
return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime);
|
|
2139
|
+
return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime, preventAffiliateTransfer);
|
|
2081
2140
|
}, []);
|
|
2082
2141
|
|
|
2083
2142
|
const setShortCode = useCallback(async (shortCode: string): Promise<boolean> => {
|
|
@@ -2100,6 +2159,10 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
2100
2159
|
return getAffiliateStoredDateImplRef.current();
|
|
2101
2160
|
}, []);
|
|
2102
2161
|
|
|
2162
|
+
const getAffiliateExpiryTimestamp = useCallback(async (): Promise<number | null> => {
|
|
2163
|
+
return getAffiliateExpiryTimestampImplRef.current();
|
|
2164
|
+
}, []);
|
|
2165
|
+
|
|
2103
2166
|
const storeExpectedStoreTransaction = useCallback(async (purchaseToken: string): Promise<void> => {
|
|
2104
2167
|
return storeExpectedStoreTransactionImplRef.current(purchaseToken);
|
|
2105
2168
|
}, []);
|
|
@@ -2128,14 +2191,18 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
2128
2191
|
const setInsertAffiliateIdentifierChangeCallbackHandler = useCallback((callback: InsertAffiliateIdentifierChangeCallback | null): void => {
|
|
2129
2192
|
insertAffiliateIdentifierChangeCallbackRef.current = callback;
|
|
2130
2193
|
|
|
2131
|
-
// If callback is being set, immediately call it with the current identifier
|
|
2194
|
+
// If callback is being set, immediately call it with the current identifier and offer code values
|
|
2132
2195
|
// This ensures callbacks registered after initialization still receive the current state (including null if expired/not set)
|
|
2133
2196
|
if (callback) {
|
|
2134
|
-
|
|
2197
|
+
Promise.all([
|
|
2198
|
+
returnInsertAffiliateIdentifierImpl(),
|
|
2199
|
+
getValueFromAsync(ASYNC_KEYS.OFFER_CODE)
|
|
2200
|
+
]).then(([identifier, storedOfferCode]) => {
|
|
2135
2201
|
// Verify callback is still the same (wasn't replaced during async operation)
|
|
2136
2202
|
if (insertAffiliateIdentifierChangeCallbackRef.current === callback) {
|
|
2137
|
-
|
|
2138
|
-
callback
|
|
2203
|
+
const offerCode = storedOfferCode && storedOfferCode.length > 0 ? storedOfferCode : null;
|
|
2204
|
+
verboseLog(`Calling callback immediately with current identifier: ${identifier}, offerCode: ${offerCode}`);
|
|
2205
|
+
callback(identifier, offerCode);
|
|
2139
2206
|
}
|
|
2140
2207
|
});
|
|
2141
2208
|
}
|
|
@@ -2156,6 +2223,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
2156
2223
|
returnInsertAffiliateIdentifier,
|
|
2157
2224
|
isAffiliateAttributionValid,
|
|
2158
2225
|
getAffiliateStoredDate,
|
|
2226
|
+
getAffiliateExpiryTimestamp,
|
|
2159
2227
|
storeExpectedStoreTransaction,
|
|
2160
2228
|
returnUserAccountTokenAndStoreExpectedTransaction,
|
|
2161
2229
|
validatePurchaseWithIapticAPI,
|
|
@@ -11,6 +11,7 @@ const useDeepLinkIapProvider = () => {
|
|
|
11
11
|
returnInsertAffiliateIdentifier,
|
|
12
12
|
isAffiliateAttributionValid,
|
|
13
13
|
getAffiliateStoredDate,
|
|
14
|
+
getAffiliateExpiryTimestamp,
|
|
14
15
|
trackEvent,
|
|
15
16
|
setShortCode,
|
|
16
17
|
getAffiliateDetails,
|
|
@@ -31,6 +32,7 @@ const useDeepLinkIapProvider = () => {
|
|
|
31
32
|
returnInsertAffiliateIdentifier,
|
|
32
33
|
isAffiliateAttributionValid,
|
|
33
34
|
getAffiliateStoredDate,
|
|
35
|
+
getAffiliateExpiryTimestamp,
|
|
34
36
|
trackEvent,
|
|
35
37
|
setShortCode,
|
|
36
38
|
getAffiliateDetails,
|