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.
@@ -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 initialize = async (companyCode: string | null, verboseLogging: boolean = false, insertLinksEnabled: boolean = false, insertLinksClipboardEnabled: boolean = false, affiliateAttributionActiveTime?: number): Promise<void> => {
138
- setVerboseLogging(verboseLogging);
139
- setInsertLinksEnabled(insertLinksEnabled);
140
- setInsertLinksClipboardEnabled(insertLinksClipboardEnabled);
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
- if (verboseLogging) {
146
- console.log('[Insert Affiliate] [VERBOSE] Starting SDK initialization...');
147
- console.log('[Insert Affiliate] [VERBOSE] Company code provided:', companyCode ? 'Yes' : 'No');
148
- console.log('[Insert Affiliate] [VERBOSE] Verbose logging enabled');
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 (isInitialized) {
152
- console.error('[Insert Affiliate] SDK is already initialized.');
153
- return;
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 (companyCode && companyCode.trim() !== '') {
157
- setCompanyCode(companyCode);
158
- await saveValueInAsync(ASYNC_KEYS.COMPANY_CODE, companyCode);
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: ${companyCode}`
183
+ `[Insert Affiliate] SDK initialized with company code: ${companyCodeParam}`
162
184
  );
163
- if (verboseLogging) {
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(companyCode, verboseLogging);
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 (verboseLogging) {
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 (insertLinksEnabled && Platform.OS === 'ios') {
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 handleInsertLinks = async (url: string): Promise<boolean> => {
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
- // For custom schemes like ia-companycode://shortcode, everything after :// is the short code
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
- const shortCode = match[1];
742
+ let shortCode = match[1];
699
743
  // Remove leading slash if present
700
- return shortCode.startsWith('/') ? shortCode.substring(1) : shortCode;
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 state or storage
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 = companyCode;
727
- verboseLog(`Company code in React state: ${activeCompanyCode || 'empty'}`);
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 state, checking AsyncStorage...');
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 (verboseLogging) {
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 getAffiliateDetails = async (affiliateCode: string): Promise<AffiliateDetails> => {
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 function setShortCode(shortCode: string): Promise<boolean> {
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 returnUserAccountTokenAndStoreExpectedTransaction = async (): Promise<string | null> => {
1472
+ const returnUserAccountTokenAndStoreExpectedTransactionImpl = async (): Promise<string | null> => {
1422
1473
  try {
1423
- const shortCode = await returnInsertAffiliateIdentifier();
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 storeExpectedStoreTransaction(userAccountToken);
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 returnInsertAffiliateIdentifier = async (ignoreTimeout: boolean = false): Promise<string | null> => {
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 isAffiliateAttributionValid();
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 isAffiliateAttributionValid = async (): Promise<boolean> => {
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 getAffiliateStoredDate();
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 getAffiliateStoredDate = async (): Promise<Date | null> => {
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 function setInsertAffiliateIdentifier(
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 returnInsertAffiliateIdentifier();
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 returnInsertAffiliateIdentifier();
1737
+ const fullIdentifier = await returnInsertAffiliateIdentifierImpl();
1687
1738
  if (fullIdentifier) {
1688
1739
  reportAffiliateAssociationIfNeeded(fullIdentifier, source);
1689
1740
  }
1690
1741
  }
1691
1742
 
1692
- const validatePurchaseWithIapticAPI = async (
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 returnInsertAffiliateIdentifier();
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 storeExpectedStoreTransaction = async (purchaseToken: string): Promise<void> => {
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 returnInsertAffiliateIdentifier();
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 trackEvent = async (eventName: string): Promise<void> => {
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={{