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.
- package/.claude/settings.local.json +8 -0
- package/dist/DeepLinkIapProvider.d.ts +1 -0
- package/dist/DeepLinkIapProvider.js +85 -3
- package/dist/useDeepLinkIapProvider.d.ts +1 -0
- package/dist/useDeepLinkIapProvider.js +3 -2
- package/package.json +1 -1
- package/readme.md +299 -7
- package/src/DeepLinkIapProvider.tsx +99 -4
- package/src/useDeepLinkIapProvider.tsx +4 -2
|
@@ -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
|
|
212
|
-
const isValidCharacters = /^[a-zA-Z0-
|
|
213
|
-
return isValidCharacters && referringLink.length
|
|
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
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("
|
|
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 won
|
|
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.
|
|
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
|
|
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
|
-
-
|
|
539
|
-
- Contain only **letters and
|
|
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('
|
|
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
|
|
255
|
-
const isValidCharacters = /^[a-zA-Z0-
|
|
256
|
-
return isValidCharacters && referringLink.length
|
|
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
|
|