insert-affiliate-react-native-sdk 1.8.0 → 1.10.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/dist/DeepLinkIapProvider.d.ts +7 -1
- package/dist/DeepLinkIapProvider.js +209 -17
- package/dist/index.d.ts +1 -1
- package/dist/useDeepLinkIapProvider.d.ts +2 -1
- package/dist/useDeepLinkIapProvider.js +2 -1
- package/docs/deep-linking-appsflyer.md +364 -0
- package/docs/deep-linking-branch.md +330 -0
- package/docs/dynamic-offer-codes.md +369 -0
- package/package.json +1 -1
- package/readme.md +600 -982
- package/src/DeepLinkIapProvider.tsx +256 -28
- package/src/index.ts +1 -1
- package/src/useDeepLinkIapProvider.tsx +2 -0
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
# Dynamic Offer Codes Complete Guide
|
|
2
|
+
|
|
3
|
+
Automatically apply discounts or trials when users come from specific affiliates using offer code modifiers.
|
|
4
|
+
|
|
5
|
+
## How It Works
|
|
6
|
+
|
|
7
|
+
When someone clicks an affiliate link or enters a short code linked to an offer (set up in the Insert Affiliate Dashboard), the SDK fills in `OfferCode` with the right modifier (like `_oneWeekFree`). You can then add this to your regular product ID to load the correct version of the subscription in your app.
|
|
8
|
+
|
|
9
|
+
## Setup in Insert Affiliate Dashboard
|
|
10
|
+
|
|
11
|
+
1. Go to [app.insertaffiliate.com/affiliates](https://app.insertaffiliate.com/affiliates)
|
|
12
|
+
2. Select the affiliate you want to configure
|
|
13
|
+
3. Click "View" to access the affiliate's settings
|
|
14
|
+
4. Assign an **iOS IAP Modifier** to the affiliate (e.g., `_oneWeekFree`, `_threeMonthsFree`)
|
|
15
|
+
5. Assign an **Android IAP Modifier** to the affiliate (e.g., `-oneweekfree`, `-threemonthsfree`)
|
|
16
|
+
6. Save the settings
|
|
17
|
+
|
|
18
|
+
Once configured, when users click that affiliate's links or enter their short codes, your app will automatically receive the modifier and can load the appropriate discounted product.
|
|
19
|
+
|
|
20
|
+
## Setup in App Store Connect (iOS)
|
|
21
|
+
|
|
22
|
+
Make sure you have created the corresponding subscription products in App Store Connect:
|
|
23
|
+
- Your base subscription (e.g., `oneMonthSubscription`)
|
|
24
|
+
- Promotional offer variants (e.g., `oneMonthSubscription_oneWeekFree`)
|
|
25
|
+
|
|
26
|
+
Both must be configured and published to at least TestFlight for testing.
|
|
27
|
+
|
|
28
|
+
## Setup in Google Play Console (Android)
|
|
29
|
+
|
|
30
|
+
There are multiple ways you can configure your products:
|
|
31
|
+
|
|
32
|
+
1. **Multiple Products Approach**: Create both a base and a promotional product:
|
|
33
|
+
- Base product: `oneMonthSubscription`
|
|
34
|
+
- Promo product: `oneMonthSubscription-oneweekfree`
|
|
35
|
+
|
|
36
|
+
2. **Single Product with Multiple Base Plans**: Create one product with multiple base plans, one with an offer attached
|
|
37
|
+
|
|
38
|
+
3. **Developer Triggered Offers**: Have one base product and apply the offer through developer-triggered offers
|
|
39
|
+
|
|
40
|
+
4. **Base Product with Intro Offers**: Have one base product that includes an introductory offer
|
|
41
|
+
|
|
42
|
+
**Important:** If using the Multiple Products Approach, ensure both products are activated and generate a release to at least Internal Testing.
|
|
43
|
+
|
|
44
|
+
## Implementation Examples
|
|
45
|
+
|
|
46
|
+
### RevenueCat Example
|
|
47
|
+
|
|
48
|
+
```javascript
|
|
49
|
+
import React, { useEffect, useState } from 'react';
|
|
50
|
+
import { View, Button, Text } from 'react-native';
|
|
51
|
+
import { useDeepLinkIapProvider } from 'insert-affiliate-react-native-sdk';
|
|
52
|
+
import Purchases from 'react-native-purchases';
|
|
53
|
+
|
|
54
|
+
const PurchaseHandler = () => {
|
|
55
|
+
const { OfferCode } = useDeepLinkIapProvider();
|
|
56
|
+
const [subscriptions, setSubscriptions] = useState([]);
|
|
57
|
+
|
|
58
|
+
const fetchSubscriptions = async () => {
|
|
59
|
+
const offerings = await Purchases.getOfferings();
|
|
60
|
+
let packagesToUse = [];
|
|
61
|
+
|
|
62
|
+
if (OfferCode) {
|
|
63
|
+
// Construct modified product IDs from base products
|
|
64
|
+
const baseProducts = offerings.current.availablePackages;
|
|
65
|
+
|
|
66
|
+
for (const basePackage of baseProducts) {
|
|
67
|
+
const baseProductId = basePackage.product.identifier;
|
|
68
|
+
const modifiedProductId = `${baseProductId}_${OfferCode}`;
|
|
69
|
+
|
|
70
|
+
// Search all offerings for the modified product
|
|
71
|
+
const allOfferings = Object.values(offerings.all);
|
|
72
|
+
let foundModified = false;
|
|
73
|
+
|
|
74
|
+
for (const offering of allOfferings) {
|
|
75
|
+
const modifiedPackage = offering.availablePackages.find(pkg =>
|
|
76
|
+
pkg.product.identifier === modifiedProductId
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
if (modifiedPackage) {
|
|
80
|
+
packagesToUse.push(modifiedPackage);
|
|
81
|
+
foundModified = true;
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Fallback to base product if no modified version
|
|
87
|
+
if (!foundModified) {
|
|
88
|
+
packagesToUse.push(basePackage);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
packagesToUse = offerings.current.availablePackages;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
setSubscriptions(packagesToUse);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const handlePurchase = async (subscriptionPackage) => {
|
|
99
|
+
try {
|
|
100
|
+
await Purchases.purchasePackage(subscriptionPackage);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error('Purchase failed:', error);
|
|
103
|
+
}
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
useEffect(() => {
|
|
107
|
+
fetchSubscriptions();
|
|
108
|
+
}, [OfferCode]);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<View>
|
|
112
|
+
{subscriptions.map((pkg) => (
|
|
113
|
+
<Button
|
|
114
|
+
key={pkg.identifier}
|
|
115
|
+
title={`Buy: ${pkg.product.identifier}`}
|
|
116
|
+
onPress={() => handlePurchase(pkg)}
|
|
117
|
+
/>
|
|
118
|
+
))}
|
|
119
|
+
{OfferCode && (
|
|
120
|
+
<Text>Special offer applied: {OfferCode}</Text>
|
|
121
|
+
)}
|
|
122
|
+
</View>
|
|
123
|
+
);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
export default PurchaseHandler;
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### RevenueCat Dashboard Configuration
|
|
130
|
+
|
|
131
|
+
1. Create separate offerings:
|
|
132
|
+
- Base offering: `premium_monthly`
|
|
133
|
+
- Modified offering: `premium_monthly_oneWeekFree`
|
|
134
|
+
|
|
135
|
+
2. Add both product IDs under different offerings in RevenueCat
|
|
136
|
+
|
|
137
|
+
3. Ensure modified products follow this naming pattern: `{baseProductId}_{cleanOfferCode}`
|
|
138
|
+
|
|
139
|
+
### Native react-native-iap Example
|
|
140
|
+
|
|
141
|
+
For apps using `react-native-iap` directly:
|
|
142
|
+
|
|
143
|
+
```javascript
|
|
144
|
+
import React, { useState, useEffect } from 'react';
|
|
145
|
+
import { View, Text, Button, Platform } from 'react-native';
|
|
146
|
+
import { useDeepLinkIapProvider } from 'insert-affiliate-react-native-sdk';
|
|
147
|
+
import {
|
|
148
|
+
initConnection,
|
|
149
|
+
getSubscriptions,
|
|
150
|
+
requestSubscription,
|
|
151
|
+
useIAP
|
|
152
|
+
} from 'react-native-iap';
|
|
153
|
+
|
|
154
|
+
const NativeIAPPurchaseView = () => {
|
|
155
|
+
const { OfferCode, returnUserAccountTokenAndStoreExpectedTransaction } = useDeepLinkIapProvider();
|
|
156
|
+
const [availableProducts, setAvailableProducts] = useState([]);
|
|
157
|
+
const [loading, setLoading] = useState(false);
|
|
158
|
+
const { currentPurchase, connected } = useIAP();
|
|
159
|
+
|
|
160
|
+
const baseProductIdentifier = "oneMonthSubscription";
|
|
161
|
+
|
|
162
|
+
// Dynamic product identifier that includes offer code
|
|
163
|
+
const dynamicProductIdentifier = OfferCode
|
|
164
|
+
? `${baseProductIdentifier}${OfferCode}` // e.g., "oneMonthSubscription_oneWeekFree"
|
|
165
|
+
: baseProductIdentifier;
|
|
166
|
+
|
|
167
|
+
const fetchProducts = async () => {
|
|
168
|
+
try {
|
|
169
|
+
setLoading(true);
|
|
170
|
+
|
|
171
|
+
// Try to fetch the dynamic product first
|
|
172
|
+
let productIds = [dynamicProductIdentifier];
|
|
173
|
+
|
|
174
|
+
// Also include base product as fallback
|
|
175
|
+
if (OfferCode) {
|
|
176
|
+
productIds.push(baseProductIdentifier);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const products = await getSubscriptions({ skus: productIds });
|
|
180
|
+
|
|
181
|
+
// Prioritize the dynamic product if it exists
|
|
182
|
+
let sortedProducts = products;
|
|
183
|
+
if (OfferCode && products.length > 1) {
|
|
184
|
+
sortedProducts = products.sort((a, b) =>
|
|
185
|
+
a.productId === dynamicProductIdentifier ? -1 : 1
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
setAvailableProducts(sortedProducts);
|
|
190
|
+
console.log(`Loaded products for: ${productIds.join(', ')}`);
|
|
191
|
+
|
|
192
|
+
} catch (error) {
|
|
193
|
+
try {
|
|
194
|
+
// Fallback logic
|
|
195
|
+
const baseProducts = await getSubscriptions({ skus: [baseProductIdentifier] });
|
|
196
|
+
setAvailableProducts(baseProducts);
|
|
197
|
+
} catch (fallbackError) {
|
|
198
|
+
console.error('Failed to fetch base products:', fallbackError);
|
|
199
|
+
}
|
|
200
|
+
} finally {
|
|
201
|
+
setLoading(false);
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
|
|
205
|
+
const handlePurchase = async (productId) => {
|
|
206
|
+
try {
|
|
207
|
+
let appAccountToken = null;
|
|
208
|
+
|
|
209
|
+
// For iOS App Store Direct integration
|
|
210
|
+
if (Platform.OS === 'ios') {
|
|
211
|
+
appAccountToken = await returnUserAccountTokenAndStoreExpectedTransaction();
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
await requestSubscription({
|
|
215
|
+
sku: productId,
|
|
216
|
+
...(appAccountToken ? { applicationUsername: appAccountToken } : {}),
|
|
217
|
+
});
|
|
218
|
+
} catch (error) {
|
|
219
|
+
console.error('Purchase error:', error);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
|
|
223
|
+
useEffect(() => {
|
|
224
|
+
if (connected) {
|
|
225
|
+
fetchProducts();
|
|
226
|
+
}
|
|
227
|
+
}, [connected, OfferCode]);
|
|
228
|
+
|
|
229
|
+
const primaryProduct = availableProducts[0];
|
|
230
|
+
|
|
231
|
+
return (
|
|
232
|
+
<View style={{ padding: 20 }}>
|
|
233
|
+
<Text style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 10 }}>
|
|
234
|
+
Premium Subscription
|
|
235
|
+
</Text>
|
|
236
|
+
|
|
237
|
+
{OfferCode && (
|
|
238
|
+
<View style={{ backgroundColor: '#e3f2fd', padding: 10, marginBottom: 15, borderRadius: 8 }}>
|
|
239
|
+
<Text style={{ color: '#1976d2', fontWeight: 'bold' }}>
|
|
240
|
+
Special Offer Applied: {OfferCode}
|
|
241
|
+
</Text>
|
|
242
|
+
</View>
|
|
243
|
+
)}
|
|
244
|
+
|
|
245
|
+
{loading ? (
|
|
246
|
+
<Text>Loading products...</Text>
|
|
247
|
+
) : primaryProduct ? (
|
|
248
|
+
<View>
|
|
249
|
+
<Text style={{ fontSize: 16, marginBottom: 5 }}>
|
|
250
|
+
{primaryProduct.title}
|
|
251
|
+
</Text>
|
|
252
|
+
<Text style={{ fontSize: 14, color: '#666', marginBottom: 5 }}>
|
|
253
|
+
Price: {primaryProduct.localizedPrice}
|
|
254
|
+
</Text>
|
|
255
|
+
<Text style={{ fontSize: 12, color: '#999', marginBottom: 15 }}>
|
|
256
|
+
Product ID: {primaryProduct.productId}
|
|
257
|
+
</Text>
|
|
258
|
+
|
|
259
|
+
<Button
|
|
260
|
+
title={loading ? "Processing..." : "Subscribe Now"}
|
|
261
|
+
onPress={() => handlePurchase(primaryProduct.productId)}
|
|
262
|
+
disabled={loading}
|
|
263
|
+
/>
|
|
264
|
+
|
|
265
|
+
{primaryProduct.productId === dynamicProductIdentifier && OfferCode && (
|
|
266
|
+
<Text style={{ fontSize: 12, color: '#4caf50', marginTop: 10 }}>
|
|
267
|
+
Promotional pricing applied
|
|
268
|
+
</Text>
|
|
269
|
+
)}
|
|
270
|
+
</View>
|
|
271
|
+
) : (
|
|
272
|
+
<View>
|
|
273
|
+
<Text style={{ color: '#f44336', marginBottom: 10 }}>
|
|
274
|
+
Product not found: {dynamicProductIdentifier}
|
|
275
|
+
</Text>
|
|
276
|
+
<Button
|
|
277
|
+
title="Retry"
|
|
278
|
+
onPress={fetchProducts}
|
|
279
|
+
/>
|
|
280
|
+
</View>
|
|
281
|
+
)}
|
|
282
|
+
|
|
283
|
+
{availableProducts.length > 1 && (
|
|
284
|
+
<View style={{ marginTop: 20 }}>
|
|
285
|
+
<Text style={{ fontSize: 14, fontWeight: 'bold', marginBottom: 10 }}>
|
|
286
|
+
Other Options:
|
|
287
|
+
</Text>
|
|
288
|
+
{availableProducts.slice(1).map((product) => (
|
|
289
|
+
<Button
|
|
290
|
+
key={product.productId}
|
|
291
|
+
title={`${product.title} - ${product.localizedPrice}`}
|
|
292
|
+
onPress={() => handlePurchase(product.productId)}
|
|
293
|
+
/>
|
|
294
|
+
))}
|
|
295
|
+
</View>
|
|
296
|
+
)}
|
|
297
|
+
</View>
|
|
298
|
+
);
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
export default NativeIAPPurchaseView;
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
## Key Features
|
|
305
|
+
|
|
306
|
+
1. **Dynamic Product Loading**: Automatically constructs product IDs using the offer code modifier
|
|
307
|
+
2. **Fallback Strategy**: If the promotional product isn't found, falls back to the base product
|
|
308
|
+
3. **Visual Feedback**: Shows users when promotional pricing is applied
|
|
309
|
+
4. **Cross-Platform**: Works on both iOS and Android with appropriate product naming
|
|
310
|
+
|
|
311
|
+
## Example Product Identifiers
|
|
312
|
+
|
|
313
|
+
**iOS (App Store Connect):**
|
|
314
|
+
- Base product: `oneMonthSubscription`
|
|
315
|
+
- With introductory discount: `oneMonthSubscription_oneWeekFree`
|
|
316
|
+
- With different offer: `oneMonthSubscription_threeMonthsFree`
|
|
317
|
+
|
|
318
|
+
**Android (Google Play Console):**
|
|
319
|
+
- Base product: `onemonthsubscription`
|
|
320
|
+
- With introductory discount: `onemonthsubscription-oneweekfree`
|
|
321
|
+
- With different offer: `onemonthsubscription-threemonthsfree`
|
|
322
|
+
|
|
323
|
+
## Best Practices
|
|
324
|
+
|
|
325
|
+
1. **Call in Purchase Views**: Always implement this logic in views where users can make purchases
|
|
326
|
+
2. **Handle Both Cases**: Ensure your app works whether an offer code is present or not
|
|
327
|
+
3. **Fallback**: Have a fallback to your base product if the dynamic product isn't found
|
|
328
|
+
4. **Platform-Specific Naming**: Use underscores (`_`) for iOS modifiers and hyphens (`-`) for Android modifiers
|
|
329
|
+
|
|
330
|
+
## Testing
|
|
331
|
+
|
|
332
|
+
1. **Set up test affiliate** with offer code modifier in Insert Affiliate dashboard
|
|
333
|
+
2. **Click test affiliate link** or enter short code
|
|
334
|
+
3. **Verify offer code** is stored:
|
|
335
|
+
```javascript
|
|
336
|
+
const { OfferCode } = useDeepLinkIapProvider();
|
|
337
|
+
console.log('Offer code:', OfferCode);
|
|
338
|
+
```
|
|
339
|
+
4. **Check dynamic product ID** is constructed correctly
|
|
340
|
+
5. **Complete test purchase** to verify correct product is purchased
|
|
341
|
+
|
|
342
|
+
## Troubleshooting
|
|
343
|
+
|
|
344
|
+
**Problem:** Offer code is null
|
|
345
|
+
- **Solution:** Ensure affiliate has offer code modifier configured in dashboard
|
|
346
|
+
- Verify user clicked affiliate link or entered short code before checking
|
|
347
|
+
|
|
348
|
+
**Problem:** Promotional product not found
|
|
349
|
+
- **Solution:** Verify promotional product exists in App Store Connect / Google Play Console
|
|
350
|
+
- Check product ID matches exactly (including the modifier)
|
|
351
|
+
- Ensure product is published to at least TestFlight (iOS) or Internal Testing (Android)
|
|
352
|
+
|
|
353
|
+
**Problem:** Always showing base product instead of promotional
|
|
354
|
+
- **Solution:** Ensure offer code is retrieved before fetching products
|
|
355
|
+
- Check that `OfferCode` is not null/undefined
|
|
356
|
+
- Verify the dynamic product identifier is correct
|
|
357
|
+
|
|
358
|
+
**Problem:** Purchase tracking not working with promotional product
|
|
359
|
+
- **Solution:** For App Store Direct, ensure you're using `returnUserAccountTokenAndStoreExpectedTransaction()`
|
|
360
|
+
- For RevenueCat/Apphud, verify `insert_affiliate` attribute is set correctly
|
|
361
|
+
|
|
362
|
+
## Next Steps
|
|
363
|
+
|
|
364
|
+
- Configure offer code modifiers for high-value affiliates
|
|
365
|
+
- Create promotional products in App Store Connect and Google Play Console
|
|
366
|
+
- Test the complete flow from link click to purchase
|
|
367
|
+
- Monitor affiliate performance in Insert Affiliate dashboard
|
|
368
|
+
|
|
369
|
+
[Back to Main README](../readme.md)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "insert-affiliate-react-native-sdk",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.10.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",
|