insert-affiliate-react-native-sdk 1.5.1 → 1.6.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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "WebFetch(domain:github.com)"
5
+ ],
6
+ "deny": []
7
+ }
8
+ }
@@ -8,6 +8,7 @@ type CustomPurchase = {
8
8
  type T_DEEPLINK_IAP_CONTEXT = {
9
9
  referrerLink: string;
10
10
  userId: string;
11
+ iOSOfferCode: string | null;
11
12
  returnInsertAffiliateIdentifier: () => Promise<string | null>;
12
13
  validatePurchaseWithIapticAPI: (jsonIapPurchase: CustomPurchase, iapticAppId: string, iapticAppName: string, iapticPublicKey: string) => Promise<boolean>;
13
14
  returnUserAccountTokenAndStoreExpectedTransaction: () => Promise<string | null>;
@@ -46,11 +46,13 @@ const ASYNC_KEYS = {
46
46
  USER_ID: '@app_user_id',
47
47
  COMPANY_CODE: '@app_company_code',
48
48
  USER_ACCOUNT_TOKEN: '@app_user_account_token',
49
+ IOS_OFFER_CODE: '@app_ios_offer_code',
49
50
  };
50
51
  // STARTING CONTEXT IMPLEMENTATION
51
52
  exports.DeepLinkIapContext = (0, react_1.createContext)({
52
53
  referrerLink: '',
53
54
  userId: '',
55
+ iOSOfferCode: null,
54
56
  returnInsertAffiliateIdentifier: () => __awaiter(void 0, void 0, void 0, function* () { return ''; }),
55
57
  validatePurchaseWithIapticAPI: (jsonIapPurchase, iapticAppId, iapticAppName, iapticPublicKey) => __awaiter(void 0, void 0, void 0, function* () { return false; }),
56
58
  returnUserAccountTokenAndStoreExpectedTransaction: () => __awaiter(void 0, void 0, void 0, function* () { return ''; }),
@@ -67,6 +69,7 @@ const DeepLinkIapProvider = ({ children, }) => {
67
69
  const [companyCode, setCompanyCode] = (0, react_1.useState)(null);
68
70
  const [isInitialized, setIsInitialized] = (0, react_1.useState)(false);
69
71
  const [verboseLogging, setVerboseLogging] = (0, react_1.useState)(false);
72
+ const [iOSOfferCode, setIOSOfferCode] = (0, react_1.useState)(null);
70
73
  // MARK: Initialize the SDK
71
74
  const initialize = (companyCode_1, ...args_1) => __awaiter(void 0, [companyCode_1, ...args_1], void 0, function* (companyCode, verboseLogging = false) {
72
75
  setVerboseLogging(verboseLogging);
@@ -106,9 +109,11 @@ const DeepLinkIapProvider = ({ children, }) => {
106
109
  const uId = yield getValueFromAsync(ASYNC_KEYS.USER_ID);
107
110
  const refLink = yield getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
108
111
  const companyCodeFromStorage = yield getValueFromAsync(ASYNC_KEYS.COMPANY_CODE);
112
+ const storedIOSOfferCode = yield getValueFromAsync(ASYNC_KEYS.IOS_OFFER_CODE);
109
113
  verboseLog(`User ID found: ${uId ? 'Yes' : 'No'}`);
110
114
  verboseLog(`Referrer link found: ${refLink ? 'Yes' : 'No'}`);
111
115
  verboseLog(`Company code found: ${companyCodeFromStorage ? 'Yes' : 'No'}`);
116
+ verboseLog(`iOS Offer Code found: ${storedIOSOfferCode ? 'Yes' : 'No'}`);
112
117
  if (uId && refLink) {
113
118
  setUserId(uId);
114
119
  setReferrerLink(refLink);
@@ -118,6 +123,10 @@ const DeepLinkIapProvider = ({ children, }) => {
118
123
  setCompanyCode(companyCodeFromStorage);
119
124
  verboseLog('Company code restored from storage');
120
125
  }
126
+ if (storedIOSOfferCode) {
127
+ setIOSOfferCode(storedIOSOfferCode);
128
+ verboseLog('iOS Offer Code restored from storage');
129
+ }
121
130
  }
122
131
  catch (error) {
123
132
  errorLog(`ERROR ~ fetchAsyncEssentials: ${error}`);
@@ -208,9 +217,9 @@ const DeepLinkIapProvider = ({ children, }) => {
208
217
  };
209
218
  // MARK: Short Codes
210
219
  const isShortCode = (referringLink) => {
211
- // Short codes are less than 10 characters
212
- const isValidCharacters = /^[a-zA-Z0-9]+$/.test(referringLink);
213
- return isValidCharacters && referringLink.length < 25 && referringLink.length > 3;
220
+ // Short codes are 3-25 characters and can include underscores
221
+ const isValidCharacters = /^[a-zA-Z0-9_]+$/.test(referringLink);
222
+ return isValidCharacters && referringLink.length >= 3 && referringLink.length <= 25;
214
223
  };
215
224
  function setShortCode(shortCode) {
216
225
  return __awaiter(this, void 0, void 0, function* () {
@@ -375,6 +384,9 @@ const DeepLinkIapProvider = ({ children, }) => {
375
384
  verboseLog(`Saving referrer link to AsyncStorage...`);
376
385
  yield saveValueInAsync(ASYNC_KEYS.REFERRER_LINK, link);
377
386
  verboseLog(`Referrer link saved to AsyncStorage successfully`);
387
+ // Automatically fetch and store offer code for any affiliate identifier
388
+ verboseLog('Attempting to fetch offer code for stored affiliate identifier...');
389
+ yield retrieveAndStoreOfferCode(link);
378
390
  });
379
391
  }
380
392
  const validatePurchaseWithIapticAPI = (jsonIapPurchase, iapticAppId, iapticAppName, iapticPublicKey) => __awaiter(void 0, void 0, void 0, function* () {
@@ -529,9 +541,79 @@ const DeepLinkIapProvider = ({ children, }) => {
529
541
  return Promise.reject(error);
530
542
  }
531
543
  });
544
+ const fetchOfferCode = (affiliateLink) => __awaiter(void 0, void 0, void 0, function* () {
545
+ try {
546
+ const activeCompanyCode = yield getActiveCompanyCode();
547
+ if (!activeCompanyCode) {
548
+ verboseLog('Cannot fetch offer code: no company code available');
549
+ return null;
550
+ }
551
+ const encodedAffiliateLink = encodeURIComponent(affiliateLink);
552
+ const url = `https://api.insertaffiliate.com/v1/affiliateReturnOfferCode/${activeCompanyCode}/${encodedAffiliateLink}`;
553
+ verboseLog(`Fetching offer code from: ${url}`);
554
+ const response = yield axios_1.default.get(url);
555
+ if (response.status === 200) {
556
+ const offerCode = response.data;
557
+ // Check for specific error strings from API
558
+ if (typeof offerCode === 'string' && (offerCode.includes("errorofferCodeNotFound") ||
559
+ offerCode.includes("errorAffiliateoffercodenotfoundinanycompany") ||
560
+ offerCode.includes("errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas") ||
561
+ offerCode.includes("Routenotfound"))) {
562
+ console.warn(`[Insert Affiliate] Offer code not found or invalid: ${offerCode}`);
563
+ verboseLog(`Offer code not found or invalid: ${offerCode}`);
564
+ return null;
565
+ }
566
+ const cleanedOfferCode = cleanOfferCode(offerCode);
567
+ verboseLog(`Successfully fetched and cleaned offer code: ${cleanedOfferCode}`);
568
+ return cleanedOfferCode;
569
+ }
570
+ else {
571
+ console.error(`[Insert Affiliate] Failed to fetch offer code. Status code: ${response.status}, Response: ${JSON.stringify(response.data)}`);
572
+ verboseLog(`Failed to fetch offer code. Status code: ${response.status}, Response: ${JSON.stringify(response.data)}`);
573
+ return null;
574
+ }
575
+ }
576
+ catch (error) {
577
+ console.error('[Insert Affiliate] Error fetching offer code:', error);
578
+ verboseLog(`Error fetching offer code: ${error}`);
579
+ return null;
580
+ }
581
+ });
582
+ const retrieveAndStoreOfferCode = (affiliateLink) => __awaiter(void 0, void 0, void 0, function* () {
583
+ try {
584
+ verboseLog(`Attempting to retrieve and store offer code for: ${affiliateLink}`);
585
+ const offerCode = yield fetchOfferCode(affiliateLink);
586
+ if (offerCode && offerCode.length > 0) {
587
+ // Store in both AsyncStorage and state
588
+ yield saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, offerCode);
589
+ setIOSOfferCode(offerCode);
590
+ verboseLog(`Successfully stored offer code: ${offerCode}`);
591
+ console.log('[Insert Affiliate] Offer code retrieved and stored successfully');
592
+ }
593
+ else {
594
+ verboseLog('No valid offer code found to store');
595
+ // Clear stored offer code if none found
596
+ yield saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, '');
597
+ setIOSOfferCode(null);
598
+ }
599
+ }
600
+ catch (error) {
601
+ console.error('[Insert Affiliate] Error retrieving and storing offer code:', error);
602
+ verboseLog(`Error in retrieveAndStoreOfferCode: ${error}`);
603
+ }
604
+ });
605
+ const removeSpecialCharacters = (offerCode) => {
606
+ // Remove special characters, keep only alphanumeric and underscores
607
+ return offerCode.replace(/[^a-zA-Z0-9_]/g, '');
608
+ };
609
+ const cleanOfferCode = (offerCode) => {
610
+ // Remove special characters, keep only alphanumeric
611
+ return removeSpecialCharacters(offerCode);
612
+ };
532
613
  return (react_1.default.createElement(exports.DeepLinkIapContext.Provider, { value: {
533
614
  referrerLink,
534
615
  userId,
616
+ iOSOfferCode,
535
617
  setShortCode,
536
618
  returnInsertAffiliateIdentifier,
537
619
  storeExpectedStoreTransaction,
@@ -12,5 +12,6 @@ declare const useDeepLinkIapProvider: () => {
12
12
  setInsertAffiliateIdentifier: (referringLink: string) => Promise<void | string>;
13
13
  initialize: (code: string | null, verboseLogging?: boolean) => Promise<void>;
14
14
  isInitialized: boolean;
15
+ iOSOfferCode: string | null;
15
16
  };
16
17
  export default useDeepLinkIapProvider;
@@ -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, trackEvent, setShortCode, setInsertAffiliateIdentifier, initialize, isInitialized } = (0, react_1.useContext)(DeepLinkIapProvider_1.DeepLinkIapContext);
6
+ const { referrerLink, userId, validatePurchaseWithIapticAPI, storeExpectedStoreTransaction, returnUserAccountTokenAndStoreExpectedTransaction, returnInsertAffiliateIdentifier, trackEvent, setShortCode, setInsertAffiliateIdentifier, initialize, isInitialized, iOSOfferCode, } = (0, react_1.useContext)(DeepLinkIapProvider_1.DeepLinkIapContext);
7
7
  return {
8
8
  referrerLink,
9
9
  userId,
@@ -15,7 +15,8 @@ const useDeepLinkIapProvider = () => {
15
15
  setShortCode,
16
16
  setInsertAffiliateIdentifier,
17
17
  initialize,
18
- isInitialized
18
+ isInitialized,
19
+ iOSOfferCode,
19
20
  };
20
21
  };
21
22
  exports.default = useDeepLinkIapProvider;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "insert-affiliate-react-native-sdk",
3
- "version": "1.5.1",
3
+ "version": "1.6.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
@@ -252,7 +252,7 @@ const Child = () => {
252
252
  <Button
253
253
  disabled={iapLoading}
254
254
  title={`Click to Buy Subscription`}
255
- onPress={() => handleBuySubscription("oneMonthSubscriptionTwo")}
255
+ onPress={() => handleBuySubscription("oneMonthSubscription")}
256
256
  />
257
257
  {iapLoading && <ActivityIndicator size={"small"} color={"black"} />}
258
258
  </View>
@@ -498,7 +498,7 @@ At this stage, we cannot guarantee that this feature is fully resistant to tampe
498
498
 
499
499
  #### Using `trackEvent`
500
500
 
501
- To track an event, use the `trackEvent` function. Make sure to set an affiliate identifier first; otherwise, event tracking wont work. Heres an example:
501
+ To track an event, use the `trackEvent` function. Make sure to set an affiliate identifier first; otherwise, event tracking won't work. Here's an example:
502
502
 
503
503
  ```javascript
504
504
  const {
@@ -520,11 +520,301 @@ const {
520
520
  />
521
521
  ```
522
522
 
523
- ### 2. Short Codes (Beta)
523
+ ### 2. Discounts for Users → Offer Codes / Dynamic Product IDs
524
+
525
+ The SDK allows you to apply dynamic modifiers to in-app purchases based on whether the app was installed via an affiliate. These modifiers can be used to swap the default product ID for a discounted or trial-based one - similar to applying an offer code.
526
+
527
+ > **Note:** Offer Codes are currently supported on **iOS only**.
528
+
529
+ #### How It Works
530
+
531
+ When a user clicks an affiliate link or enters a short code linked to an offer (set up in the **Insert Affiliate Dashboard**), the SDK auto-populates the `iOSOfferCode` field with a relevant modifier (e.g., `_oneWeekFree`). You can append this to your base product ID to dynamically display the correct subscription.
532
+
533
+
534
+ #### Basic Usage
535
+
536
+ ##### 1. Automatic Offer Code Fetching
537
+ If an affiliate short code is stored, the SDK automatically fetches and saves the associated offer code modifier.
538
+
539
+ ##### 2. Access the Offer Code Modifier
540
+ The offer code modifier is available through the context:
541
+
542
+ ```javascript
543
+ const { iOSOfferCode } = useDeepLinkIapProvider();
544
+ ```
545
+
546
+ ##### Setup Requirements
547
+
548
+ #### App Store Connect Configuration
549
+ 1. Create both a base and a promotional product:
550
+ - Base product: `oneMonthSubscription`
551
+ - Promo product: `oneMonthSubscription_oneWeekFree`
552
+ 2. Ensure **both** products are approved and available for sale.
553
+
554
+
555
+ **Product Naming Pattern:**
556
+ - Follow the pattern: `{baseProductId}{iOSOfferCode}`
557
+ - Example: `oneMonthSubscription` + `_oneWeekFree` = `oneMonthSubscription_oneWeekFree`
558
+
559
+ ---
560
+
561
+ #### RevenueCat Dashboard Configuration
562
+
563
+ #### RevenueCat Dashboard Configuration:
564
+ 1. Create separate offerings:
565
+ - Base offering: `premium_monthly`
566
+ - Modified offering: `premium_monthly_oneWeekFree`
567
+
568
+ 2. Add both product IDs under different offerings in RevenueCat.
569
+
570
+ 3. Ensure modified products follow this naming pattern: {baseProductId}_{cleanOfferCode}. e.g. premium_monthly_oneWeekFree
571
+
572
+
573
+ ### Integration Example
574
+ ```javascript
575
+ import React, { useEffect, useState } from 'react';
576
+ import { View, Button, Text } from 'react-native';
577
+ import { useDeepLinkIapProvider } from 'insert-affiliate-react-native-sdk';
578
+ import Purchases from 'react-native-purchases';
579
+
580
+ const PurchaseHandler = () => {
581
+ const { iOSOfferCode } = useDeepLinkIapProvider();
582
+ const [subscriptions, setSubscriptions] = useState([]);
583
+
584
+ const fetchSubscriptions = async () => {
585
+ const offerings = await Purchases.getOfferings();
586
+ let packagesToUse = [];
587
+
588
+ if (iOSOfferCode) {
589
+
590
+
591
+ // Construct modified product IDs from base products
592
+ const baseProducts = offerings.current.availablePackages;
593
+
594
+ for (const basePackage of baseProducts) {
595
+ const baseProductId = basePackage.product.identifier;
596
+ const modifiedProductId = `${baseProductId}_${iOSOfferCode}`;
597
+
598
+ // Search all offerings for the modified product
599
+ const allOfferings = Object.values(offerings.all);
600
+ let foundModified = false;
601
+
602
+ for (const offering of allOfferings) {
603
+ const modifiedPackage = offering.availablePackages.find(pkg =>
604
+ pkg.product.identifier === modifiedProductId
605
+ );
606
+
607
+ if (modifiedPackage) {
608
+ packagesToUse.push(modifiedPackage);
609
+ foundModified = true;
610
+ break;
611
+ }
612
+ }
613
+
614
+ // Fallback to base product if no modified version
615
+ if (!foundModified) {
616
+ packagesToUse.push(basePackage);
617
+ }
618
+ }
619
+ } else {
620
+ packagesToUse = offerings.current.availablePackages;
621
+ }
622
+
623
+ setSubscriptions(packagesToUse);
624
+ };
625
+
626
+ const handlePurchase = async (subscriptionPackage) => {
627
+ try {
628
+ await Purchases.purchasePackage(subscriptionPackage);
629
+ } catch (error) {
630
+ console.error('Purchase failed:', error);
631
+ }
632
+ };
633
+
634
+ useEffect(() => {
635
+ fetchSubscriptions();
636
+ }, [iOSOfferCode]);
637
+
638
+ return (
639
+ <View>
640
+ {subscriptions.map((pkg) => (
641
+ <Button
642
+ key={pkg.identifier}
643
+ title={`Buy: ${pkg.product.identifier}`}
644
+ onPress={() => handlePurchase(pkg)}
645
+ />
646
+ ))}
647
+ {iOSOfferCode && (
648
+ <Text>Special offer applied: {iOSOfferCode}</Text>
649
+ )}
650
+ </View>
651
+ );
652
+ };
653
+ ```
654
+ ---
655
+
656
+ #### Native Receipt Verification Example
657
+
658
+ For apps using `react-native-iap` directly:
659
+
660
+ ```javascript
661
+ import React, { useState, useEffect } from 'react';
662
+ import { View, Text, Button, Platform } from 'react-native';
663
+ import { useDeepLinkIapProvider } from 'insert-affiliate-react-native-sdk';
664
+ import {
665
+ initConnection,
666
+ getSubscriptions,
667
+ requestSubscription,
668
+ useIAP
669
+ } from 'react-native-iap';
670
+
671
+ const NativeIAPPurchaseView = () => {
672
+ const { iOSOfferCode, returnUserAccountTokenAndStoreExpectedTransaction } = useDeepLinkIapProvider();
673
+ const [availableProducts, setAvailableProducts] = useState([]);
674
+ const [loading, setLoading] = useState(false);
675
+ const { currentPurchase, connected } = useIAP();
676
+
677
+ const baseProductIdentifier = "oneMonthSubscription";
678
+
679
+ // Dynamic product identifier that includes offer code
680
+ const dynamicProductIdentifier = iOSOfferCode
681
+ ? `${baseProductIdentifier}${iOSOfferCode}` // e.g., "oneMonthSubscription_oneWeekFree"
682
+ : baseProductIdentifier;
683
+
684
+ const fetchProducts = async () => {
685
+ try {
686
+ setLoading(true);
687
+
688
+ // Try to fetch the dynamic product first
689
+ let productIds = [dynamicProductIdentifier];
690
+
691
+ // Also include base product as fallback
692
+ if (iOSOfferCode) {
693
+ productIds.push(baseProductIdentifier);
694
+ }
695
+
696
+ const products = await getSubscriptions({ skus: productIds });
697
+
698
+ // Prioritize the dynamic product if it exists
699
+ let sortedProducts = products;
700
+ if (iOSOfferCode && products.length > 1) {
701
+ sortedProducts = products.sort((a, b) =>
702
+ a.productId === dynamicProductIdentifier ? -1 : 1
703
+ );
704
+ }
705
+
706
+ setAvailableProducts(sortedProducts);
707
+ console.log(`Loaded products for: ${productIds.join(', ')}`);
708
+
709
+ } catch (error) {
710
+ try {
711
+ // Fallback logic
712
+ const baseProducts = await getSubscriptions({ skus: [baseProductIdentifier] });
713
+ setAvailableProducts(baseProducts);
714
+ } catch (fallbackError) {
715
+ console.error('Failed to fetch base products:', fallbackError);
716
+ }
717
+ } finally {
718
+ setLoading(false);
719
+ }
720
+ };
721
+
722
+ const handlePurchase = async (productId) => {
723
+ // Implement the purchase handling logic as outlined in the remaining SDK integration steps.
724
+ };
725
+
726
+ useEffect(() => {
727
+ if (connected) {
728
+ fetchProducts();
729
+ }
730
+ }, [connected, iOSOfferCode]);;
731
+
732
+ const primaryProduct = availableProducts[0];
733
+
734
+ return (
735
+ <View style={{ padding: 20 }}>
736
+ <Text style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 10 }}>
737
+ Premium Subscription
738
+ </Text>
739
+
740
+ {iOSOfferCode && (
741
+ <View style={{ backgroundColor: '#e3f2fd', padding: 10, marginBottom: 15, borderRadius: 8 }}>
742
+ <Text style={{ color: '#1976d2', fontWeight: 'bold' }}>
743
+ 🎉 Special Offer Applied: {iOSOfferCode}
744
+ </Text>
745
+ </View>
746
+ )}
747
+
748
+ {loading ? (
749
+ <Text>Loading products...</Text>
750
+ ) : primaryProduct ? (
751
+ <View>
752
+ <Text style={{ fontSize: 16, marginBottom: 5 }}>
753
+ {primaryProduct.title}
754
+ </Text>
755
+ <Text style={{ fontSize: 14, color: '#666', marginBottom: 5 }}>
756
+ Price: {primaryProduct.localizedPrice}
757
+ </Text>
758
+ <Text style={{ fontSize: 12, color: '#999', marginBottom: 15 }}>
759
+ Product ID: {primaryProduct.productId}
760
+ </Text>
761
+
762
+ <Button
763
+ title={loading ? "Processing..." : "Subscribe Now"}
764
+ onPress={() => handlePurchase(primaryProduct.productId)}
765
+ disabled={loading}
766
+ />
767
+
768
+ {primaryProduct.productId === dynamicProductIdentifier && iOSOfferCode && (
769
+ <Text style={{ fontSize: 12, color: '#4caf50', marginTop: 10 }}>
770
+ ✓ Promotional pricing applied
771
+ </Text>
772
+ )}
773
+ </View>
774
+ ) : (
775
+ <View>
776
+ <Text style={{ color: '#f44336', marginBottom: 10 }}>
777
+ Product not found: {dynamicProductIdentifier}
778
+ </Text>
779
+ <Button
780
+ title="Retry"
781
+ onPress={fetchProducts}
782
+ />
783
+ </View>
784
+ )}
785
+
786
+ {availableProducts.length > 1 && (
787
+ <View style={{ marginTop: 20 }}>
788
+ <Text style={{ fontSize: 14, fontWeight: 'bold', marginBottom: 10 }}>
789
+ Other Options:
790
+ </Text>
791
+ {availableProducts.slice(1).map((product) => (
792
+ <Button
793
+ key={product.productId}
794
+ title={`${product.title} - ${product.localizedPrice}`}
795
+ onPress={() => handlePurchase(product.productId)}
796
+ />
797
+ ))}
798
+ </View>
799
+ )}
800
+ </View>
801
+ );
802
+ };
803
+ ```
804
+
805
+ ##### Key Features of Native IAP Integration:
806
+
807
+ 1. **Dynamic Product Loading**: Automatically constructs product IDs using the offer code modifier
808
+ 2. **Fallback Strategy**: If the promotional product isn't found, falls back to the base product
809
+ 3. **Visual Feedback**: Shows users when promotional pricing is applied
810
+ 4. **Error Handling**: Graceful handling when products aren't available
811
+
812
+
813
+ ### 3. Short Codes (Beta)
524
814
 
525
815
  #### What are Short Codes?
526
816
 
527
- Short codes are unique, 10-character alphanumeric identifiers that affiliates can use to promote products or subscriptions. These codes are ideal for influencers or partners, making them easier to share than long URLs.
817
+ Short codes are unique identifiers that affiliates can use to promote products or subscriptions. These codes are ideal for influencers or partners, making them easier to share than long URLs.
528
818
 
529
819
  **Example Use Case**: An influencer promotes a subscription with the short code "JOIN12345" within their TikTok video's description. When users enter this code within your app during sign-up or before purchase, the app tracks the subscription back to the influencer for commission payouts.
530
820
 
@@ -535,10 +825,12 @@ For more information, visit the [Insert Affiliate Short Codes Documentation](htt
535
825
  Use the `setShortCode` method to associate a short code with an affiliate. This is ideal for scenarios where users enter the code via an input field, pop-up, or similar UI element.
536
826
 
537
827
  Short codes must meet the following criteria:
538
- - Exactly **10 characters long**.
539
- - Contain only **letters and numbers** (alphanumeric characters).
828
+ - Between **3-25 characters long**.
829
+ - Contain only **letters, numbers, and underscores** (alphanumeric characters and underscores).
540
830
  - Replace {{ user_entered_short_code }} with the short code the user enters through your chosen input method, i.e. an input field / pop up element
541
831
 
832
+ When a short code is set, the SDK automatically attempts to fetch and store any associated offer codes for iOS users.
833
+
542
834
  ```javascript
543
835
  import {
544
836
  DeepLinkIapProvider,
@@ -550,6 +842,6 @@ Short codes must meet the following criteria:
550
842
 
551
843
  <Button
552
844
  title={'Set Short Code'}
553
- onPress={() => setShortCode('JOIN123456')}
845
+ onPress={() => setShortCode('JOIN_123')}
554
846
  />
555
847
  ```
@@ -1,5 +1,5 @@
1
1
  import React, { createContext, useEffect, useState } from 'react';
2
- import { Platform } from 'react-native';
2
+ import { Platform, Linking } from 'react-native';
3
3
  import axios from 'axios';
4
4
  import AsyncStorage from '@react-native-async-storage/async-storage';
5
5
 
@@ -15,6 +15,7 @@ type CustomPurchase = {
15
15
  type T_DEEPLINK_IAP_CONTEXT = {
16
16
  referrerLink: string;
17
17
  userId: string;
18
+ iOSOfferCode: string | null;
18
19
  returnInsertAffiliateIdentifier: () => Promise<string | null>;
19
20
  validatePurchaseWithIapticAPI: (
20
21
  jsonIapPurchase: CustomPurchase,
@@ -57,12 +58,14 @@ const ASYNC_KEYS = {
57
58
  USER_ID: '@app_user_id',
58
59
  COMPANY_CODE: '@app_company_code',
59
60
  USER_ACCOUNT_TOKEN: '@app_user_account_token',
61
+ IOS_OFFER_CODE: '@app_ios_offer_code',
60
62
  };
61
63
 
62
64
  // STARTING CONTEXT IMPLEMENTATION
63
65
  export const DeepLinkIapContext = createContext<T_DEEPLINK_IAP_CONTEXT>({
64
66
  referrerLink: '',
65
67
  userId: '',
68
+ iOSOfferCode: null,
66
69
  returnInsertAffiliateIdentifier: async () => '',
67
70
  validatePurchaseWithIapticAPI: async (
68
71
  jsonIapPurchase: CustomPurchase,
@@ -87,6 +90,7 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
87
90
  const [companyCode, setCompanyCode] = useState<string | null>(null);
88
91
  const [isInitialized, setIsInitialized] = useState<boolean>(false);
89
92
  const [verboseLogging, setVerboseLogging] = useState<boolean>(false);
93
+ const [iOSOfferCode, setIOSOfferCode] = useState<string | null>(null);
90
94
 
91
95
  // MARK: Initialize the SDK
92
96
  const initialize = async (companyCode: string | null, verboseLogging: boolean = false): Promise<void> => {
@@ -134,10 +138,12 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
134
138
  const uId = await getValueFromAsync(ASYNC_KEYS.USER_ID);
135
139
  const refLink = await getValueFromAsync(ASYNC_KEYS.REFERRER_LINK);
136
140
  const companyCodeFromStorage = await getValueFromAsync(ASYNC_KEYS.COMPANY_CODE);
141
+ const storedIOSOfferCode = await getValueFromAsync(ASYNC_KEYS.IOS_OFFER_CODE);
137
142
 
138
143
  verboseLog(`User ID found: ${uId ? 'Yes' : 'No'}`);
139
144
  verboseLog(`Referrer link found: ${refLink ? 'Yes' : 'No'}`);
140
145
  verboseLog(`Company code found: ${companyCodeFromStorage ? 'Yes' : 'No'}`);
146
+ verboseLog(`iOS Offer Code found: ${storedIOSOfferCode ? 'Yes' : 'No'}`);
141
147
 
142
148
  if (uId && refLink) {
143
149
  setUserId(uId);
@@ -149,6 +155,11 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
149
155
  setCompanyCode(companyCodeFromStorage);
150
156
  verboseLog('Company code restored from storage');
151
157
  }
158
+
159
+ if (storedIOSOfferCode) {
160
+ setIOSOfferCode(storedIOSOfferCode);
161
+ verboseLog('iOS Offer Code restored from storage');
162
+ }
152
163
  } catch (error) {
153
164
  errorLog(`ERROR ~ fetchAsyncEssentials: ${error}`);
154
165
  verboseLog(`Error loading from AsyncStorage: ${error}`);
@@ -251,9 +262,9 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
251
262
 
252
263
  // MARK: Short Codes
253
264
  const isShortCode = (referringLink: string): boolean => {
254
- // Short codes are less than 10 characters
255
- const isValidCharacters = /^[a-zA-Z0-9]+$/.test(referringLink);
256
- return isValidCharacters && referringLink.length < 25 && referringLink.length > 3;
265
+ // Short codes are 3-25 characters and can include underscores
266
+ const isValidCharacters = /^[a-zA-Z0-9_]+$/.test(referringLink);
267
+ return isValidCharacters && referringLink.length >= 3 && referringLink.length <= 25;
257
268
  };
258
269
 
259
270
  async function setShortCode(shortCode: string): Promise<void> {
@@ -441,6 +452,10 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
441
452
  verboseLog(`Saving referrer link to AsyncStorage...`);
442
453
  await saveValueInAsync(ASYNC_KEYS.REFERRER_LINK, link);
443
454
  verboseLog(`Referrer link saved to AsyncStorage successfully`);
455
+
456
+ // Automatically fetch and store offer code for any affiliate identifier
457
+ verboseLog('Attempting to fetch offer code for stored affiliate identifier...');
458
+ await retrieveAndStoreOfferCode(link);
444
459
  }
445
460
 
446
461
  const validatePurchaseWithIapticAPI = async (
@@ -638,11 +653,91 @@ const DeepLinkIapProvider: React.FC<T_DEEPLINK_IAP_PROVIDER> = ({
638
653
  }
639
654
  };
640
655
 
656
+ const fetchOfferCode = async (affiliateLink: string): Promise<string | null> => {
657
+ try {
658
+ const activeCompanyCode = await getActiveCompanyCode();
659
+ if (!activeCompanyCode) {
660
+ verboseLog('Cannot fetch offer code: no company code available');
661
+ return null;
662
+ }
663
+
664
+ const encodedAffiliateLink = encodeURIComponent(affiliateLink);
665
+ const url = `https://api.insertaffiliate.com/v1/affiliateReturnOfferCode/${activeCompanyCode}/${encodedAffiliateLink}`;
666
+
667
+ verboseLog(`Fetching offer code from: ${url}`);
668
+
669
+ const response = await axios.get(url);
670
+
671
+ if (response.status === 200) {
672
+ const offerCode = response.data;
673
+
674
+ // Check for specific error strings from API
675
+ if (typeof offerCode === 'string' && (
676
+ offerCode.includes("errorofferCodeNotFound") ||
677
+ offerCode.includes("errorAffiliateoffercodenotfoundinanycompany") ||
678
+ offerCode.includes("errorAffiliateoffercodenotfoundinanycompanyAffiliatelinkwas") ||
679
+ offerCode.includes("Routenotfound")
680
+ )) {
681
+ console.warn(`[Insert Affiliate] Offer code not found or invalid: ${offerCode}`);
682
+ verboseLog(`Offer code not found or invalid: ${offerCode}`);
683
+ return null;
684
+ }
685
+
686
+ const cleanedOfferCode = cleanOfferCode(offerCode);
687
+ verboseLog(`Successfully fetched and cleaned offer code: ${cleanedOfferCode}`);
688
+ return cleanedOfferCode;
689
+ } else {
690
+ console.error(`[Insert Affiliate] Failed to fetch offer code. Status code: ${response.status}, Response: ${JSON.stringify(response.data)}`);
691
+ verboseLog(`Failed to fetch offer code. Status code: ${response.status}, Response: ${JSON.stringify(response.data)}`);
692
+ return null;
693
+ }
694
+ } catch (error) {
695
+ console.error('[Insert Affiliate] Error fetching offer code:', error);
696
+ verboseLog(`Error fetching offer code: ${error}`);
697
+ return null;
698
+ }
699
+ };
700
+
701
+ const retrieveAndStoreOfferCode = async (affiliateLink: string): Promise<void> => {
702
+ try {
703
+ verboseLog(`Attempting to retrieve and store offer code for: ${affiliateLink}`);
704
+
705
+ const offerCode = await fetchOfferCode(affiliateLink);
706
+
707
+ if (offerCode && offerCode.length > 0) {
708
+ // Store in both AsyncStorage and state
709
+ await saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, offerCode);
710
+ setIOSOfferCode(offerCode);
711
+ verboseLog(`Successfully stored offer code: ${offerCode}`);
712
+ console.log('[Insert Affiliate] Offer code retrieved and stored successfully');
713
+ } else {
714
+ verboseLog('No valid offer code found to store');
715
+ // Clear stored offer code if none found
716
+ await saveValueInAsync(ASYNC_KEYS.IOS_OFFER_CODE, '');
717
+ setIOSOfferCode(null);
718
+ }
719
+ } catch (error) {
720
+ console.error('[Insert Affiliate] Error retrieving and storing offer code:', error);
721
+ verboseLog(`Error in retrieveAndStoreOfferCode: ${error}`);
722
+ }
723
+ };
724
+
725
+ const removeSpecialCharacters = (offerCode: string): string => {
726
+ // Remove special characters, keep only alphanumeric and underscores
727
+ return offerCode.replace(/[^a-zA-Z0-9_]/g, '');
728
+ };
729
+
730
+ const cleanOfferCode = (offerCode: string): string => {
731
+ // Remove special characters, keep only alphanumeric
732
+ return removeSpecialCharacters(offerCode);
733
+ };
734
+
641
735
  return (
642
736
  <DeepLinkIapContext.Provider
643
737
  value={{
644
738
  referrerLink,
645
739
  userId,
740
+ iOSOfferCode,
646
741
  setShortCode,
647
742
  returnInsertAffiliateIdentifier,
648
743
  storeExpectedStoreTransaction,
@@ -13,7 +13,8 @@ const useDeepLinkIapProvider = () => {
13
13
  setShortCode,
14
14
  setInsertAffiliateIdentifier,
15
15
  initialize,
16
- isInitialized
16
+ isInitialized,
17
+ iOSOfferCode,
17
18
  } = useContext(DeepLinkIapContext);
18
19
 
19
20
  return {
@@ -27,7 +28,8 @@ const useDeepLinkIapProvider = () => {
27
28
  setShortCode,
28
29
  setInsertAffiliateIdentifier,
29
30
  initialize,
30
- isInitialized
31
+ isInitialized,
32
+ iOSOfferCode,
31
33
  };
32
34
  };
33
35