insert-affiliate-react-native-sdk 1.10.0 → 1.11.2
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 +8 -2
- package/dist/DeepLinkIapProvider.js +264 -151
- package/docs/deep-linking-appsflyer.md +24 -2
- package/docs/deep-linking-branch.md +24 -2
- package/package.json +1 -1
- package/readme.md +72 -22
- package/src/DeepLinkIapProvider.tsx +237 -91
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { createContext, useEffect, useState, useRef } from 'react';
|
|
1
|
+
import React, { createContext, useEffect, useState, useRef, useCallback } from 'react';
|
|
2
2
|
import { Platform, Linking, Dimensions, PixelRatio } from 'react-native';
|
|
3
3
|
import axios from 'axios';
|
|
4
4
|
import AsyncStorage from '@react-native-async-storage/async-storage';
|
|
@@ -132,52 +132,74 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
132
132
|
const [OfferCode, setOfferCode] = useState<string | null>(null);
|
|
133
133
|
const [affiliateAttributionActiveTime, setAffiliateAttributionActiveTime] = useState<number | null>(null);
|
|
134
134
|
const insertAffiliateIdentifierChangeCallbackRef = useRef<InsertAffiliateIdentifierChangeCallback | null>(null);
|
|
135
|
+
const isInitializingRef = useRef<boolean>(false);
|
|
136
|
+
|
|
137
|
+
// Refs for values that need to be current inside callbacks (to avoid stale closures)
|
|
138
|
+
const companyCodeRef = useRef<string | null>(null);
|
|
139
|
+
const verboseLoggingRef = useRef<boolean>(false);
|
|
140
|
+
|
|
141
|
+
// 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);
|
|
143
|
+
const setShortCodeImplRef = useRef<(shortCode: string) => Promise<boolean>>(null as any);
|
|
144
|
+
const getAffiliateDetailsImplRef = useRef<(affiliateCode: string) => Promise<AffiliateDetails>>(null as any);
|
|
145
|
+
const returnInsertAffiliateIdentifierImplRef = useRef<(ignoreTimeout?: boolean) => Promise<string | null>>(null as any);
|
|
146
|
+
const isAffiliateAttributionValidImplRef = useRef<() => Promise<boolean>>(null as any);
|
|
147
|
+
const getAffiliateStoredDateImplRef = useRef<() => Promise<Date | null>>(null as any);
|
|
148
|
+
const storeExpectedStoreTransactionImplRef = useRef<(purchaseToken: string) => Promise<void>>(null as any);
|
|
149
|
+
const returnUserAccountTokenAndStoreExpectedTransactionImplRef = useRef<() => Promise<string | null>>(null as any);
|
|
150
|
+
const validatePurchaseWithIapticAPIImplRef = useRef<(jsonIapPurchase: CustomPurchase, iapticAppId: string, iapticAppName: string, iapticPublicKey: string) => Promise<boolean>>(null as any);
|
|
151
|
+
const trackEventImplRef = useRef<(eventName: string) => Promise<void>>(null as any);
|
|
152
|
+
const setInsertAffiliateIdentifierImplRef = useRef<(referringLink: string) => Promise<void | string>>(null as any);
|
|
153
|
+
const handleInsertLinksImplRef = useRef<(url: string) => Promise<boolean>>(null as any);
|
|
135
154
|
|
|
136
155
|
// MARK: Initialize the SDK
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
if (affiliateAttributionActiveTime !== undefined) {
|
|
142
|
-
setAffiliateAttributionActiveTime(affiliateAttributionActiveTime);
|
|
156
|
+
const initializeImpl = async (companyCodeParam: string | null, verboseLoggingParam: boolean = false, insertLinksEnabledParam: boolean = false, insertLinksClipboardEnabledParam: boolean = false, affiliateAttributionActiveTimeParam?: number): Promise<void> => {
|
|
157
|
+
// Prevent multiple concurrent initialization attempts
|
|
158
|
+
if (isInitialized || isInitializingRef.current) {
|
|
159
|
+
return;
|
|
143
160
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
161
|
+
isInitializingRef.current = true;
|
|
162
|
+
|
|
163
|
+
setVerboseLogging(verboseLoggingParam);
|
|
164
|
+
verboseLoggingRef.current = verboseLoggingParam;
|
|
165
|
+
setInsertLinksEnabled(insertLinksEnabledParam);
|
|
166
|
+
setInsertLinksClipboardEnabled(insertLinksClipboardEnabledParam);
|
|
167
|
+
if (affiliateAttributionActiveTimeParam !== undefined) {
|
|
168
|
+
setAffiliateAttributionActiveTime(affiliateAttributionActiveTimeParam);
|
|
149
169
|
}
|
|
150
170
|
|
|
151
|
-
if (
|
|
152
|
-
console.
|
|
153
|
-
|
|
171
|
+
if (verboseLoggingParam) {
|
|
172
|
+
console.log('[Insert Affiliate] [VERBOSE] Starting SDK initialization...');
|
|
173
|
+
console.log('[Insert Affiliate] [VERBOSE] Company code provided:', companyCodeParam ? 'Yes' : 'No');
|
|
174
|
+
console.log('[Insert Affiliate] [VERBOSE] Verbose logging enabled');
|
|
154
175
|
}
|
|
155
176
|
|
|
156
|
-
if (
|
|
157
|
-
setCompanyCode(
|
|
158
|
-
|
|
177
|
+
if (companyCodeParam && companyCodeParam.trim() !== '') {
|
|
178
|
+
setCompanyCode(companyCodeParam);
|
|
179
|
+
companyCodeRef.current = companyCodeParam;
|
|
180
|
+
await saveValueInAsync(ASYNC_KEYS.COMPANY_CODE, companyCodeParam);
|
|
159
181
|
setIsInitialized(true);
|
|
160
182
|
console.log(
|
|
161
|
-
`[Insert Affiliate] SDK initialized with company code: ${
|
|
183
|
+
`[Insert Affiliate] SDK initialized with company code: ${companyCodeParam}`
|
|
162
184
|
);
|
|
163
|
-
if (
|
|
185
|
+
if (verboseLoggingParam) {
|
|
164
186
|
console.log('[Insert Affiliate] [VERBOSE] Company code saved to AsyncStorage');
|
|
165
187
|
console.log('[Insert Affiliate] [VERBOSE] SDK marked as initialized');
|
|
166
188
|
}
|
|
167
189
|
|
|
168
190
|
// Report SDK initialization for onboarding verification (fire and forget)
|
|
169
|
-
reportSdkInitIfNeeded(
|
|
191
|
+
reportSdkInitIfNeeded(companyCodeParam, verboseLoggingParam);
|
|
170
192
|
} else {
|
|
171
193
|
console.warn(
|
|
172
194
|
'[Insert Affiliate] SDK initialized without a company code.'
|
|
173
195
|
);
|
|
174
196
|
setIsInitialized(true);
|
|
175
|
-
if (
|
|
197
|
+
if (verboseLoggingParam) {
|
|
176
198
|
console.log('[Insert Affiliate] [VERBOSE] No company code provided, SDK initialized in limited mode');
|
|
177
199
|
}
|
|
178
200
|
}
|
|
179
201
|
|
|
180
|
-
if (
|
|
202
|
+
if (insertLinksEnabledParam && Platform.OS === 'ios') {
|
|
181
203
|
try {
|
|
182
204
|
const enhancedSystemInfo = await getEnhancedSystemInfo();
|
|
183
205
|
await sendSystemInfoToBackend(enhancedSystemInfo);
|
|
@@ -211,6 +233,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
211
233
|
|
|
212
234
|
if (companyCodeFromStorage) {
|
|
213
235
|
setCompanyCode(companyCodeFromStorage);
|
|
236
|
+
companyCodeRef.current = companyCodeFromStorage;
|
|
214
237
|
verboseLog('Company code restored from storage');
|
|
215
238
|
}
|
|
216
239
|
|
|
@@ -351,13 +374,6 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
351
374
|
console.log('[Insert Affiliate] SDK has been reset.');
|
|
352
375
|
};
|
|
353
376
|
|
|
354
|
-
// MARK: Callback Management
|
|
355
|
-
// Sets a callback that will be triggered whenever storeInsertAffiliateIdentifier is called
|
|
356
|
-
// The callback receives the current affiliate identifier (returnInsertAffiliateIdentifier result)
|
|
357
|
-
const setInsertAffiliateIdentifierChangeCallbackHandler = (callback: InsertAffiliateIdentifierChangeCallback | null): void => {
|
|
358
|
-
insertAffiliateIdentifierChangeCallbackRef.current = callback;
|
|
359
|
-
};
|
|
360
|
-
|
|
361
377
|
// MARK: Deep Link Handling
|
|
362
378
|
// Helper function to parse URLs in React Native compatible way
|
|
363
379
|
const parseURL = (url: string) => {
|
|
@@ -566,10 +582,10 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
566
582
|
};
|
|
567
583
|
|
|
568
584
|
// Handles Insert Links deep linking - equivalent to iOS handleInsertLinks
|
|
569
|
-
const
|
|
585
|
+
const handleInsertLinksImpl = async (url: string): Promise<boolean> => {
|
|
570
586
|
try {
|
|
571
587
|
console.log(`[Insert Affiliate] Attempting to handle URL: ${url}`);
|
|
572
|
-
|
|
588
|
+
|
|
573
589
|
if (!url || typeof url !== 'string') {
|
|
574
590
|
console.log('[Insert Affiliate] Invalid URL provided to handleInsertLinks');
|
|
575
591
|
return false;
|
|
@@ -690,14 +706,48 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
690
706
|
}
|
|
691
707
|
};
|
|
692
708
|
|
|
709
|
+
// Parse shortcode from query parameter (new format: scheme://insert-affiliate?code=SHORTCODE)
|
|
710
|
+
const parseShortCodeFromQuery = (url: string): string | null => {
|
|
711
|
+
try {
|
|
712
|
+
const queryIndex = url.indexOf('?');
|
|
713
|
+
if (queryIndex !== -1) {
|
|
714
|
+
const queryString = url.substring(queryIndex + 1);
|
|
715
|
+
const params = queryString.split('&');
|
|
716
|
+
for (const param of params) {
|
|
717
|
+
const [key, value] = param.split('=');
|
|
718
|
+
if (key === 'code' && value) {
|
|
719
|
+
return decodeURIComponent(value);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
return null;
|
|
724
|
+
} catch (error) {
|
|
725
|
+
verboseLog(`Error parsing short code from query: ${error}`);
|
|
726
|
+
return null;
|
|
727
|
+
}
|
|
728
|
+
};
|
|
729
|
+
|
|
693
730
|
const parseShortCodeFromURLString = (url: string): string | null => {
|
|
694
731
|
try {
|
|
695
|
-
//
|
|
732
|
+
// First try to extract from query parameter (new format: scheme://insert-affiliate?code=SHORTCODE)
|
|
733
|
+
const queryCode = parseShortCodeFromQuery(url);
|
|
734
|
+
if (queryCode) {
|
|
735
|
+
console.log(`[Insert Affiliate] Found short code in query parameter: ${queryCode}`);
|
|
736
|
+
return queryCode;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Fall back to path format (legacy: scheme://SHORTCODE)
|
|
696
740
|
const match = url.match(/^[^:]+:\/\/(.+)$/);
|
|
697
741
|
if (match) {
|
|
698
|
-
|
|
742
|
+
let shortCode = match[1];
|
|
699
743
|
// Remove leading slash if present
|
|
700
|
-
|
|
744
|
+
shortCode = shortCode.startsWith('/') ? shortCode.substring(1) : shortCode;
|
|
745
|
+
// If the path is 'insert-affiliate' (from new format without code param), return null
|
|
746
|
+
if (shortCode === 'insert-affiliate' || shortCode.startsWith('insert-affiliate?')) {
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
console.log(`[Insert Affiliate] Found short code in URL path (legacy format): ${shortCode}`);
|
|
750
|
+
return shortCode;
|
|
701
751
|
}
|
|
702
752
|
return null;
|
|
703
753
|
} catch (error) {
|
|
@@ -720,29 +770,30 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
720
770
|
await AsyncStorage.clear();
|
|
721
771
|
};
|
|
722
772
|
|
|
723
|
-
// Helper function to get company code from
|
|
773
|
+
// Helper function to get company code from ref or storage (uses ref to avoid stale closures)
|
|
724
774
|
const getActiveCompanyCode = async (): Promise<string | null> => {
|
|
725
775
|
verboseLog('Getting active company code...');
|
|
726
|
-
let activeCompanyCode =
|
|
727
|
-
verboseLog(`Company code in
|
|
728
|
-
|
|
776
|
+
let activeCompanyCode = companyCodeRef.current;
|
|
777
|
+
verboseLog(`Company code in ref: ${activeCompanyCode || 'empty'}`);
|
|
778
|
+
|
|
729
779
|
if (!activeCompanyCode || (activeCompanyCode.trim() === '' && activeCompanyCode !== null)) {
|
|
730
|
-
verboseLog('Company code not in
|
|
780
|
+
verboseLog('Company code not in ref, checking AsyncStorage...');
|
|
731
781
|
activeCompanyCode = await getValueFromAsync(ASYNC_KEYS.COMPANY_CODE);
|
|
732
782
|
verboseLog(`Company code in AsyncStorage: ${activeCompanyCode || 'empty'}`);
|
|
733
|
-
|
|
783
|
+
|
|
734
784
|
if (activeCompanyCode) {
|
|
735
|
-
// Update state for future use
|
|
785
|
+
// Update ref and state for future use
|
|
786
|
+
companyCodeRef.current = activeCompanyCode;
|
|
736
787
|
setCompanyCode(activeCompanyCode);
|
|
737
|
-
verboseLog('Updated React state with company code from storage');
|
|
788
|
+
verboseLog('Updated ref and React state with company code from storage');
|
|
738
789
|
}
|
|
739
790
|
}
|
|
740
791
|
return activeCompanyCode;
|
|
741
792
|
};
|
|
742
793
|
|
|
743
|
-
// Helper function for verbose logging
|
|
794
|
+
// Helper function for verbose logging (uses ref to avoid stale closures)
|
|
744
795
|
const verboseLog = (message: string) => {
|
|
745
|
-
if (
|
|
796
|
+
if (verboseLoggingRef.current) {
|
|
746
797
|
console.log(`[Insert Affiliate] [VERBOSE] ${message}`);
|
|
747
798
|
}
|
|
748
799
|
};
|
|
@@ -1340,7 +1391,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1340
1391
|
}
|
|
1341
1392
|
};
|
|
1342
1393
|
|
|
1343
|
-
const
|
|
1394
|
+
const getAffiliateDetailsImpl = async (affiliateCode: string): Promise<AffiliateDetails> => {
|
|
1344
1395
|
try {
|
|
1345
1396
|
const activeCompanyCode = await getActiveCompanyCode();
|
|
1346
1397
|
if (!activeCompanyCode) {
|
|
@@ -1385,7 +1436,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1385
1436
|
}
|
|
1386
1437
|
};
|
|
1387
1438
|
|
|
1388
|
-
async
|
|
1439
|
+
const setShortCodeImpl = async (shortCode: string): Promise<boolean> => {
|
|
1389
1440
|
console.log('[Insert Affiliate] Setting short code.');
|
|
1390
1441
|
await generateThenSetUserID();
|
|
1391
1442
|
|
|
@@ -1405,7 +1456,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1405
1456
|
console.warn(`[Insert Affiliate] Short code ${capitalisedShortCode} does not exist. Not storing.`);
|
|
1406
1457
|
return false;
|
|
1407
1458
|
}
|
|
1408
|
-
}
|
|
1459
|
+
};
|
|
1409
1460
|
|
|
1410
1461
|
async function getOrCreateUserAccountToken(): Promise<string> {
|
|
1411
1462
|
let userAccountToken = await getValueFromAsync(ASYNC_KEYS.USER_ACCOUNT_TOKEN);
|
|
@@ -1418,9 +1469,9 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1418
1469
|
return userAccountToken;
|
|
1419
1470
|
};
|
|
1420
1471
|
|
|
1421
|
-
const
|
|
1472
|
+
const returnUserAccountTokenAndStoreExpectedTransactionImpl = async (): Promise<string | null> => {
|
|
1422
1473
|
try {
|
|
1423
|
-
const shortCode = await
|
|
1474
|
+
const shortCode = await returnInsertAffiliateIdentifierImpl();
|
|
1424
1475
|
if (!shortCode) {
|
|
1425
1476
|
console.log('[Insert Affiliate] No affiliate stored - not saving expected transaction.');
|
|
1426
1477
|
return null;
|
|
@@ -1433,14 +1484,14 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1433
1484
|
console.error('[Insert Affiliate] Failed to generate user account token.');
|
|
1434
1485
|
return null;
|
|
1435
1486
|
} else {
|
|
1436
|
-
await
|
|
1487
|
+
await storeExpectedStoreTransactionImpl(userAccountToken);
|
|
1437
1488
|
return userAccountToken;
|
|
1438
1489
|
}
|
|
1439
1490
|
} catch (error) {
|
|
1440
1491
|
// Handle E_IAP_NOT_AVAILABLE error gracefully
|
|
1441
|
-
if ((error as any)?.code === 'E_IAP_NOT_AVAILABLE' ||
|
|
1492
|
+
if ((error as any)?.code === 'E_IAP_NOT_AVAILABLE' ||
|
|
1442
1493
|
(error instanceof Error && error.message.includes('E_IAP_NOT_AVAILABLE'))) {
|
|
1443
|
-
|
|
1494
|
+
|
|
1444
1495
|
if (isDevelopmentEnvironment) {
|
|
1445
1496
|
console.warn('[Insert Affiliate] IAP not available in development environment. Cannot store expected transaction.');
|
|
1446
1497
|
verboseLog('E_IAP_NOT_AVAILABLE error in returnUserAccountTokenAndStoreExpectedTransaction - gracefully handling in development');
|
|
@@ -1454,50 +1505,50 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1454
1505
|
};
|
|
1455
1506
|
|
|
1456
1507
|
// MARK: Return Insert Affiliate Identifier
|
|
1457
|
-
const
|
|
1508
|
+
const returnInsertAffiliateIdentifierImpl = async (ignoreTimeout: boolean = false): Promise<string | null> => {
|
|
1458
1509
|
try {
|
|
1459
1510
|
verboseLog(`Getting insert affiliate identifier (ignoreTimeout: ${ignoreTimeout})...`);
|
|
1460
|
-
|
|
1511
|
+
|
|
1461
1512
|
// If timeout is enabled and we're not ignoring it, check validity first
|
|
1462
1513
|
if (!ignoreTimeout && affiliateAttributionActiveTime) {
|
|
1463
|
-
const isValid = await
|
|
1514
|
+
const isValid = await isAffiliateAttributionValidImpl();
|
|
1464
1515
|
if (!isValid) {
|
|
1465
1516
|
verboseLog('Attribution has expired, returning null');
|
|
1466
1517
|
return null;
|
|
1467
1518
|
}
|
|
1468
1519
|
}
|
|
1469
|
-
|
|
1520
|
+
|
|
1470
1521
|
// Now get the actual identifier
|
|
1471
1522
|
verboseLog(`React state - referrerLink: ${referrerLink || 'empty'}, userId: ${userId || 'empty'}`);
|
|
1472
|
-
|
|
1523
|
+
|
|
1473
1524
|
// Try React state first
|
|
1474
1525
|
if (referrerLink && userId) {
|
|
1475
1526
|
const identifier = `${referrerLink}-${userId}`;
|
|
1476
1527
|
verboseLog(`Found identifier in React state: ${identifier}`);
|
|
1477
1528
|
return identifier;
|
|
1478
1529
|
}
|
|
1479
|
-
|
|
1530
|
+
|
|
1480
1531
|
verboseLog('React state empty, checking AsyncStorage...');
|
|
1481
|
-
|
|
1532
|
+
|
|
1482
1533
|
// Fallback to async storage if React state is empty
|
|
1483
1534
|
const storedLink = await getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
|
|
1484
1535
|
let storedUserId = await getValueFromAsync(ASYNC_KEYS.USER_ID);
|
|
1485
|
-
|
|
1536
|
+
|
|
1486
1537
|
verboseLog(`AsyncStorage - storedLink: ${storedLink || 'empty'}, storedUserId: ${storedUserId || 'empty'}`);
|
|
1487
|
-
|
|
1538
|
+
|
|
1488
1539
|
// If we have a stored link but no user ID, generate one now
|
|
1489
1540
|
if (storedLink && !storedUserId) {
|
|
1490
1541
|
verboseLog('Found stored link but no user ID, generating user ID now...');
|
|
1491
1542
|
storedUserId = await generateThenSetUserID();
|
|
1492
1543
|
verboseLog(`Generated user ID: ${storedUserId}`);
|
|
1493
1544
|
}
|
|
1494
|
-
|
|
1545
|
+
|
|
1495
1546
|
if (storedLink && storedUserId) {
|
|
1496
1547
|
const identifier = `${storedLink}-${storedUserId}`;
|
|
1497
1548
|
verboseLog(`Found identifier in AsyncStorage: ${identifier}`);
|
|
1498
1549
|
return identifier;
|
|
1499
1550
|
}
|
|
1500
|
-
|
|
1551
|
+
|
|
1501
1552
|
verboseLog('No affiliate identifier found in state or storage');
|
|
1502
1553
|
return null;
|
|
1503
1554
|
} catch (error) {
|
|
@@ -1507,44 +1558,44 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1507
1558
|
};
|
|
1508
1559
|
|
|
1509
1560
|
// MARK: Attribution Timeout Functions
|
|
1510
|
-
|
|
1561
|
+
|
|
1511
1562
|
// Check if the current affiliate attribution is still valid based on timeout
|
|
1512
|
-
const
|
|
1563
|
+
const isAffiliateAttributionValidImpl = async (): Promise<boolean> => {
|
|
1513
1564
|
try {
|
|
1514
1565
|
// If no timeout is set, attribution is always valid
|
|
1515
1566
|
if (!affiliateAttributionActiveTime) {
|
|
1516
1567
|
verboseLog('No attribution timeout set, attribution is valid');
|
|
1517
1568
|
return true;
|
|
1518
1569
|
}
|
|
1519
|
-
|
|
1520
|
-
const storedDate = await
|
|
1570
|
+
|
|
1571
|
+
const storedDate = await getAffiliateStoredDateImpl();
|
|
1521
1572
|
if (!storedDate) {
|
|
1522
1573
|
verboseLog('No stored date found, attribution is invalid');
|
|
1523
1574
|
return false;
|
|
1524
1575
|
}
|
|
1525
|
-
|
|
1576
|
+
|
|
1526
1577
|
const now = new Date();
|
|
1527
1578
|
const timeDifferenceSeconds = Math.floor((now.getTime() - storedDate.getTime()) / 1000);
|
|
1528
1579
|
const isValid = timeDifferenceSeconds <= affiliateAttributionActiveTime;
|
|
1529
|
-
|
|
1580
|
+
|
|
1530
1581
|
verboseLog(`Attribution timeout check: stored=${storedDate.toISOString()}, now=${now.toISOString()}, diff=${timeDifferenceSeconds}s, timeout=${affiliateAttributionActiveTime}s, valid=${isValid}`);
|
|
1531
|
-
|
|
1582
|
+
|
|
1532
1583
|
return isValid;
|
|
1533
1584
|
} catch (error) {
|
|
1534
1585
|
verboseLog(`Error checking attribution validity: ${error}`);
|
|
1535
1586
|
return false;
|
|
1536
1587
|
}
|
|
1537
1588
|
};
|
|
1538
|
-
|
|
1589
|
+
|
|
1539
1590
|
// Get the date when the affiliate identifier was stored
|
|
1540
|
-
const
|
|
1591
|
+
const getAffiliateStoredDateImpl = async (): Promise<Date | null> => {
|
|
1541
1592
|
try {
|
|
1542
1593
|
const storedDateString = await getValueFromAsync(ASYNC_KEYS.AFFILIATE_STORED_DATE);
|
|
1543
1594
|
if (!storedDateString) {
|
|
1544
1595
|
verboseLog('No affiliate stored date found');
|
|
1545
1596
|
return null;
|
|
1546
1597
|
}
|
|
1547
|
-
|
|
1598
|
+
|
|
1548
1599
|
const storedDate = new Date(storedDateString);
|
|
1549
1600
|
verboseLog(`Retrieved affiliate stored date: ${storedDate.toISOString()}`);
|
|
1550
1601
|
return storedDate;
|
|
@@ -1556,9 +1607,9 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1556
1607
|
|
|
1557
1608
|
// MARK: Insert Affiliate Identifier
|
|
1558
1609
|
|
|
1559
|
-
async
|
|
1610
|
+
const setInsertAffiliateIdentifierImpl = async (
|
|
1560
1611
|
referringLink: string
|
|
1561
|
-
): Promise<void | string> {
|
|
1612
|
+
): Promise<void | string> => {
|
|
1562
1613
|
console.log('[Insert Affiliate] Setting affiliate identifier.');
|
|
1563
1614
|
verboseLog(`Input referringLink: ${referringLink}`);
|
|
1564
1615
|
|
|
@@ -1648,7 +1699,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1648
1699
|
verboseLog(`Error in setInsertAffiliateIdentifier: ${error}`);
|
|
1649
1700
|
}
|
|
1650
1701
|
};
|
|
1651
|
-
|
|
1702
|
+
|
|
1652
1703
|
async function storeInsertAffiliateIdentifier({ link, source }: { link: string; source: AffiliateAssociationSource }) {
|
|
1653
1704
|
console.log(`[Insert Affiliate] Storing affiliate identifier: ${link} (source: ${source})`);
|
|
1654
1705
|
|
|
@@ -1677,19 +1728,19 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1677
1728
|
|
|
1678
1729
|
// Trigger callback with the current affiliate identifier
|
|
1679
1730
|
if (insertAffiliateIdentifierChangeCallbackRef.current) {
|
|
1680
|
-
const currentIdentifier = await
|
|
1731
|
+
const currentIdentifier = await returnInsertAffiliateIdentifierImpl();
|
|
1681
1732
|
verboseLog(`Triggering callback with identifier: ${currentIdentifier}`);
|
|
1682
1733
|
insertAffiliateIdentifierChangeCallbackRef.current(currentIdentifier);
|
|
1683
1734
|
}
|
|
1684
1735
|
|
|
1685
1736
|
// Report this new affiliate association to the backend (fire and forget)
|
|
1686
|
-
const fullIdentifier = await
|
|
1737
|
+
const fullIdentifier = await returnInsertAffiliateIdentifierImpl();
|
|
1687
1738
|
if (fullIdentifier) {
|
|
1688
1739
|
reportAffiliateAssociationIfNeeded(fullIdentifier, source);
|
|
1689
1740
|
}
|
|
1690
1741
|
}
|
|
1691
1742
|
|
|
1692
|
-
const
|
|
1743
|
+
const validatePurchaseWithIapticAPIImpl = async (
|
|
1693
1744
|
jsonIapPurchase: CustomPurchase,
|
|
1694
1745
|
iapticAppId: string,
|
|
1695
1746
|
iapticAppName: string,
|
|
@@ -1697,10 +1748,10 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1697
1748
|
): Promise<boolean> => {
|
|
1698
1749
|
try {
|
|
1699
1750
|
// Check for E_IAP_NOT_AVAILABLE error in development environment
|
|
1700
|
-
if ((jsonIapPurchase as any)?.error?.code === 'E_IAP_NOT_AVAILABLE' ||
|
|
1751
|
+
if ((jsonIapPurchase as any)?.error?.code === 'E_IAP_NOT_AVAILABLE' ||
|
|
1701
1752
|
(jsonIapPurchase as any)?.code === 'E_IAP_NOT_AVAILABLE' ||
|
|
1702
1753
|
(typeof jsonIapPurchase === 'string' && (jsonIapPurchase as string).includes('E_IAP_NOT_AVAILABLE'))) {
|
|
1703
|
-
|
|
1754
|
+
|
|
1704
1755
|
if (isDevelopmentEnvironment) {
|
|
1705
1756
|
console.warn('[Insert Affiliate] IAP not available in development environment. This is expected behavior.');
|
|
1706
1757
|
verboseLog('E_IAP_NOT_AVAILABLE error detected in development - gracefully handling');
|
|
@@ -1741,7 +1792,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1741
1792
|
transaction,
|
|
1742
1793
|
};
|
|
1743
1794
|
|
|
1744
|
-
let insertAffiliateIdentifier = await
|
|
1795
|
+
let insertAffiliateIdentifier = await returnInsertAffiliateIdentifierImpl();
|
|
1745
1796
|
|
|
1746
1797
|
if (insertAffiliateIdentifier) {
|
|
1747
1798
|
requestBody.additionalData = {
|
|
@@ -1769,9 +1820,9 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1769
1820
|
}
|
|
1770
1821
|
} catch (error) {
|
|
1771
1822
|
// Handle E_IAP_NOT_AVAILABLE error gracefully
|
|
1772
|
-
if ((error as any)?.code === 'E_IAP_NOT_AVAILABLE' ||
|
|
1823
|
+
if ((error as any)?.code === 'E_IAP_NOT_AVAILABLE' ||
|
|
1773
1824
|
(error instanceof Error && error.message.includes('E_IAP_NOT_AVAILABLE'))) {
|
|
1774
|
-
|
|
1825
|
+
|
|
1775
1826
|
if (isDevelopmentEnvironment) {
|
|
1776
1827
|
console.warn('[Insert Affiliate] IAP not available in development environment. SDK will continue without purchase validation.');
|
|
1777
1828
|
verboseLog('E_IAP_NOT_AVAILABLE error caught in validatePurchaseWithIapticAPI - gracefully handling in development');
|
|
@@ -1795,9 +1846,9 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1795
1846
|
}
|
|
1796
1847
|
};
|
|
1797
1848
|
|
|
1798
|
-
const
|
|
1849
|
+
const storeExpectedStoreTransactionImpl = async (purchaseToken: string): Promise<void> => {
|
|
1799
1850
|
verboseLog(`Storing expected store transaction with token: ${purchaseToken}`);
|
|
1800
|
-
|
|
1851
|
+
|
|
1801
1852
|
const activeCompanyCode = await getActiveCompanyCode();
|
|
1802
1853
|
if (!activeCompanyCode) {
|
|
1803
1854
|
console.error("[Insert Affiliate] Company code is not set. Please initialize the SDK with a valid company code.");
|
|
@@ -1805,7 +1856,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1805
1856
|
return;
|
|
1806
1857
|
}
|
|
1807
1858
|
|
|
1808
|
-
const shortCode = await
|
|
1859
|
+
const shortCode = await returnInsertAffiliateIdentifierImpl();
|
|
1809
1860
|
if (!shortCode) {
|
|
1810
1861
|
console.error("[Insert Affiliate] No affiliate identifier found. Please set one before tracking events.");
|
|
1811
1862
|
verboseLog("Cannot store transaction: no affiliate identifier available");
|
|
@@ -1851,10 +1902,10 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1851
1902
|
};
|
|
1852
1903
|
|
|
1853
1904
|
// MARK: Track Event
|
|
1854
|
-
const
|
|
1905
|
+
const trackEventImpl = async (eventName: string): Promise<void> => {
|
|
1855
1906
|
try {
|
|
1856
1907
|
verboseLog(`Tracking event: ${eventName}`);
|
|
1857
|
-
|
|
1908
|
+
|
|
1858
1909
|
const activeCompanyCode = await getActiveCompanyCode();
|
|
1859
1910
|
if (!activeCompanyCode) {
|
|
1860
1911
|
console.error("[Insert Affiliate] Company code is not set. Please initialize the SDK with a valid company code.");
|
|
@@ -1884,7 +1935,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1884
1935
|
verboseLog(`Track event payload: ${JSON.stringify(payload)}`);
|
|
1885
1936
|
|
|
1886
1937
|
verboseLog("Making API call to track event...");
|
|
1887
|
-
|
|
1938
|
+
|
|
1888
1939
|
const response = await axios.post(
|
|
1889
1940
|
'https://api.insertaffiliate.com/v1/trackEvent',
|
|
1890
1941
|
payload,
|
|
@@ -1999,6 +2050,101 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
|
|
|
1999
2050
|
return removeSpecialCharacters(offerCode);
|
|
2000
2051
|
};
|
|
2001
2052
|
|
|
2053
|
+
// ============================================================================
|
|
2054
|
+
// REF CALLBACK PATTERN: Update refs on every render for fresh closures
|
|
2055
|
+
// ============================================================================
|
|
2056
|
+
initializeImplRef.current = initializeImpl;
|
|
2057
|
+
setShortCodeImplRef.current = setShortCodeImpl;
|
|
2058
|
+
getAffiliateDetailsImplRef.current = getAffiliateDetailsImpl;
|
|
2059
|
+
returnInsertAffiliateIdentifierImplRef.current = returnInsertAffiliateIdentifierImpl;
|
|
2060
|
+
isAffiliateAttributionValidImplRef.current = isAffiliateAttributionValidImpl;
|
|
2061
|
+
getAffiliateStoredDateImplRef.current = getAffiliateStoredDateImpl;
|
|
2062
|
+
storeExpectedStoreTransactionImplRef.current = storeExpectedStoreTransactionImpl;
|
|
2063
|
+
returnUserAccountTokenAndStoreExpectedTransactionImplRef.current = returnUserAccountTokenAndStoreExpectedTransactionImpl;
|
|
2064
|
+
validatePurchaseWithIapticAPIImplRef.current = validatePurchaseWithIapticAPIImpl;
|
|
2065
|
+
trackEventImplRef.current = trackEventImpl;
|
|
2066
|
+
setInsertAffiliateIdentifierImplRef.current = setInsertAffiliateIdentifierImpl;
|
|
2067
|
+
handleInsertLinksImplRef.current = handleInsertLinksImpl;
|
|
2068
|
+
|
|
2069
|
+
// ============================================================================
|
|
2070
|
+
// STABLE WRAPPERS: useCallback with [] deps that delegate to refs
|
|
2071
|
+
// These provide stable function references that always call current implementations
|
|
2072
|
+
// ============================================================================
|
|
2073
|
+
const initialize = useCallback(async (
|
|
2074
|
+
code: string | null,
|
|
2075
|
+
verboseLogging?: boolean,
|
|
2076
|
+
insertLinksEnabled?: boolean,
|
|
2077
|
+
insertLinksClipboardEnabled?: boolean,
|
|
2078
|
+
affiliateAttributionActiveTime?: number
|
|
2079
|
+
): Promise<void> => {
|
|
2080
|
+
return initializeImplRef.current(code, verboseLogging, insertLinksEnabled, insertLinksClipboardEnabled, affiliateAttributionActiveTime);
|
|
2081
|
+
}, []);
|
|
2082
|
+
|
|
2083
|
+
const setShortCode = useCallback(async (shortCode: string): Promise<boolean> => {
|
|
2084
|
+
return setShortCodeImplRef.current(shortCode);
|
|
2085
|
+
}, []);
|
|
2086
|
+
|
|
2087
|
+
const getAffiliateDetails = useCallback(async (affiliateCode: string): Promise<AffiliateDetails> => {
|
|
2088
|
+
return getAffiliateDetailsImplRef.current(affiliateCode);
|
|
2089
|
+
}, []);
|
|
2090
|
+
|
|
2091
|
+
const returnInsertAffiliateIdentifier = useCallback(async (ignoreTimeout?: boolean): Promise<string | null> => {
|
|
2092
|
+
return returnInsertAffiliateIdentifierImplRef.current(ignoreTimeout);
|
|
2093
|
+
}, []);
|
|
2094
|
+
|
|
2095
|
+
const isAffiliateAttributionValid = useCallback(async (): Promise<boolean> => {
|
|
2096
|
+
return isAffiliateAttributionValidImplRef.current();
|
|
2097
|
+
}, []);
|
|
2098
|
+
|
|
2099
|
+
const getAffiliateStoredDate = useCallback(async (): Promise<Date | null> => {
|
|
2100
|
+
return getAffiliateStoredDateImplRef.current();
|
|
2101
|
+
}, []);
|
|
2102
|
+
|
|
2103
|
+
const storeExpectedStoreTransaction = useCallback(async (purchaseToken: string): Promise<void> => {
|
|
2104
|
+
return storeExpectedStoreTransactionImplRef.current(purchaseToken);
|
|
2105
|
+
}, []);
|
|
2106
|
+
|
|
2107
|
+
const returnUserAccountTokenAndStoreExpectedTransaction = useCallback(async (): Promise<string | null> => {
|
|
2108
|
+
return returnUserAccountTokenAndStoreExpectedTransactionImplRef.current();
|
|
2109
|
+
}, []);
|
|
2110
|
+
|
|
2111
|
+
const validatePurchaseWithIapticAPI = useCallback(async (
|
|
2112
|
+
jsonIapPurchase: CustomPurchase,
|
|
2113
|
+
iapticAppId: string,
|
|
2114
|
+
iapticAppName: string,
|
|
2115
|
+
iapticPublicKey: string
|
|
2116
|
+
): Promise<boolean> => {
|
|
2117
|
+
return validatePurchaseWithIapticAPIImplRef.current(jsonIapPurchase, iapticAppId, iapticAppName, iapticPublicKey);
|
|
2118
|
+
}, []);
|
|
2119
|
+
|
|
2120
|
+
const trackEvent = useCallback(async (eventName: string): Promise<void> => {
|
|
2121
|
+
return trackEventImplRef.current(eventName);
|
|
2122
|
+
}, []);
|
|
2123
|
+
|
|
2124
|
+
const setInsertAffiliateIdentifier = useCallback(async (referringLink: string): Promise<void | string> => {
|
|
2125
|
+
return setInsertAffiliateIdentifierImplRef.current(referringLink);
|
|
2126
|
+
}, []);
|
|
2127
|
+
|
|
2128
|
+
const setInsertAffiliateIdentifierChangeCallbackHandler = useCallback((callback: InsertAffiliateIdentifierChangeCallback | null): void => {
|
|
2129
|
+
insertAffiliateIdentifierChangeCallbackRef.current = callback;
|
|
2130
|
+
|
|
2131
|
+
// If callback is being set, immediately call it with the current identifier value
|
|
2132
|
+
// This ensures callbacks registered after initialization still receive the current state (including null if expired/not set)
|
|
2133
|
+
if (callback) {
|
|
2134
|
+
returnInsertAffiliateIdentifierImpl().then(identifier => {
|
|
2135
|
+
// Verify callback is still the same (wasn't replaced during async operation)
|
|
2136
|
+
if (insertAffiliateIdentifierChangeCallbackRef.current === callback) {
|
|
2137
|
+
verboseLog(`Calling callback immediately with current identifier: ${identifier}`);
|
|
2138
|
+
callback(identifier);
|
|
2139
|
+
}
|
|
2140
|
+
});
|
|
2141
|
+
}
|
|
2142
|
+
}, []);
|
|
2143
|
+
|
|
2144
|
+
const handleInsertLinks = useCallback(async (url: string): Promise<boolean> => {
|
|
2145
|
+
return handleInsertLinksImplRef.current(url);
|
|
2146
|
+
}, []);
|
|
2147
|
+
|
|
2002
2148
|
return (
|
|
2003
2149
|
<DeepLinkIapContext.Provider
|
|
2004
2150
|
value={{
|