Skip to content

Commit d6fdd82

Browse files
authored
Support fetching and redeeming win-back offers on custom paywall (#1134)
1 parent bb11891 commit d6fdd82

File tree

9 files changed

+673
-15
lines changed

9 files changed

+673
-15
lines changed

apitesters/purchases.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,3 +344,25 @@ async function checkSyncObserverModeAmazonPurchase(
344344
price
345345
);
346346
}
347+
348+
async function checkFetchAndPurchaseWinBackOffersForProduct(
349+
product: PurchasesStoreProduct
350+
): Promise<MakePurchaseResult> {
351+
const offers = await Purchases.getEligibleWinBackOffersForProduct(product);
352+
353+
if (!offers || offers.length < 1) {
354+
throw new Error("No eligible win-back offers available for the product.");
355+
}
356+
return await Purchases.purchaseProductWithWinBackOffer(product, offers[0]);
357+
}
358+
359+
async function checkFetchAndPurchaseWinBackOffersForPackage(
360+
aPackage: PurchasesPackage
361+
): Promise<MakePurchaseResult> {
362+
const offers = await Purchases.getEligibleWinBackOffersForPackage(aPackage);
363+
364+
if (!offers || offers.length < 1) {
365+
throw new Error("No eligible win-back offers available for the package.");
366+
}
367+
return await Purchases.purchasePackageWithWinBackOffer(aPackage, offers[0]);
368+
}

examples/purchaseTesterTypescript/App.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import CustomerInfoScreen from './app/screens/CustomerInfoScreen';
2121
import OfferingDetailScreen from './app/screens/OfferingDetailScreen';
2222
import PaywallScreen from './app/screens/PaywallScreen';
2323
import FooterPaywallScreen from "./app/screens/FooterPaywallScreen";
24+
import WinBackTestingScreen from "./app/screens/WinBackTestingScreen";
2425

2526
import APIKeys from './app/APIKeys';
2627
import { SafeAreaView } from 'react-native-safe-area-context';
@@ -80,6 +81,7 @@ const App = () => {
8081
<Stack.Screen name="OfferingDetail" component={OfferingDetailScreen} />
8182
<Stack.Screen name="Paywall" component={PaywallScreen} />
8283
<Stack.Screen name="FooterPaywall" component={FooterPaywallScreen} />
84+
<Stack.Screen name="WinBackTesting" component={WinBackTestingScreen} />
8385
</Stack.Navigator>
8486
</NavigationContainer>
8587
);

examples/purchaseTesterTypescript/app/RootStackParamList.tsx

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@ type RootStackParamList = {
44
Home: undefined;
55
CustomerInfo: {appUserID: String | null, customerInfo: CustomerInfo | null};
66
OfferingDetail: {offering: PurchasesOffering | null};
7-
Paywall: {offering: PurchasesOffering | null, fontFamily?: string | null};
8-
FooterPaywall: {offering: PurchasesOffering | null, fontFamily?: string | null};
7+
WinBackTesting: {};
8+
Paywall: {offering: PurchasesOffering | null; fontFamily?: string | null};
9+
FooterPaywall: {
10+
offering: PurchasesOffering | null;
11+
fontFamily?: string | null;
12+
};
913
};
1014

1115
export default RootStackParamList;

examples/purchaseTesterTypescript/app/screens/HomeScreen.tsx

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,14 @@ const HomeScreen: React.FC<Props> = ({navigation}) => {
292292
<Text style={styles.otherActions}>Present paywall</Text>
293293
</TouchableOpacity>
294294
</View>
295+
296+
<Divider />
297+
<View>
298+
<TouchableOpacity
299+
onPress={() => navigation.navigate('WinBackTesting', {})}>
300+
<Text style={styles.otherActions}>Win-Back Offer Testing</Text>
301+
</TouchableOpacity>
302+
</View>
295303
</ScrollView>
296304
</SafeAreaView>
297305
);
Lines changed: 315 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,315 @@
1+
import React, {useEffect, useState} from 'react';
2+
import {
3+
Button,
4+
ScrollView,
5+
StyleSheet,
6+
Text,
7+
TouchableOpacity,
8+
View,
9+
} from 'react-native';
10+
import {Colors} from 'react-native/Libraries/NewAppScreen';
11+
import Purchases, {
12+
PurchasesPackage,
13+
PurchasesStoreProduct,
14+
PurchasesWinBackOffer,
15+
} from 'react-native-purchases';
16+
import {NativeStackScreenProps} from '@react-navigation/native-stack';
17+
import RootStackParamList from '../RootStackParamList';
18+
19+
type Props = NativeStackScreenProps<RootStackParamList, 'WinBackTesting'>;
20+
21+
const WinBackTestingScreen: React.FC = () => {
22+
const [product, setProduct] = useState<PurchasesStoreProduct>();
23+
const [productWinBackOffers, setProductWinBackOffers] = useState<
24+
PurchasesWinBackOffer[]
25+
>([]);
26+
const [aPackage, setPackage] = useState<PurchasesPackage>();
27+
const [packageWinBackOffers, setPackageWinBackOffers] = useState<
28+
PurchasesWinBackOffer[]
29+
>([]);
30+
const [error, setError] = useState<string | null>(null);
31+
32+
const fetchProduct = async () => {
33+
try {
34+
const products = await Purchases.getProducts(['winbackTesting']);
35+
if (products.length > 0) {
36+
setProduct(products[0]);
37+
} else {
38+
setError('No products available');
39+
}
40+
} catch (err) {
41+
setError('Failed to fetch products: ' + (err as Error).message);
42+
}
43+
};
44+
45+
const purchaseProduct = async (product: PurchasesStoreProduct) => {
46+
try {
47+
const purchaseResult = await Purchases.purchaseStoreProduct(product);
48+
console.log('Purchase successful:', purchaseResult);
49+
} catch (err) {
50+
console.error('Purchase failed:', err);
51+
}
52+
};
53+
54+
const fetchEligibleWinBackOffersForProduct = async (
55+
product: PurchasesStoreProduct,
56+
) => {
57+
try {
58+
const offers = await Purchases.getEligibleWinBackOffersForProduct(
59+
product,
60+
);
61+
if (offers) {
62+
setProductWinBackOffers(offers); // Store win-back offers
63+
} else {
64+
setProductWinBackOffers([]); // Clear if no offers
65+
}
66+
} catch (err) {
67+
console.error('Error fetching win-back offers:', err);
68+
setProductWinBackOffers([]);
69+
}
70+
};
71+
72+
const fetchEligibleWinBackOffersForPackage = async (
73+
aPackage: PurchasesPackage,
74+
) => {
75+
try {
76+
const offers = await Purchases.getEligibleWinBackOffersForPackage(
77+
aPackage,
78+
);
79+
if (offers) {
80+
setPackageWinBackOffers(offers); // Store win-back offers
81+
} else {
82+
setPackageWinBackOffers([]); // Clear if no offers
83+
}
84+
} catch (err) {
85+
console.error('Error fetching win-back offers:', err);
86+
setPackageWinBackOffers([]);
87+
}
88+
};
89+
90+
const purchaseWinBackOfferForProduct = async (
91+
product: PurchasesStoreProduct,
92+
offer: PurchasesWinBackOffer,
93+
) => {
94+
try {
95+
const result = await Purchases.purchaseProductWithWinBackOffer(
96+
product,
97+
offer,
98+
);
99+
console.log('Win-Back Offer purchase successful:', result);
100+
} catch (err) {
101+
console.error('Win-Back Offer purchase failed:', err);
102+
}
103+
};
104+
105+
const fetchPackage = async () => {
106+
const currentOffering = (await Purchases.getOfferings()).current;
107+
const monthlyPackage = currentOffering?.availablePackages.find(
108+
pkg => pkg.identifier === '$rc_monthly',
109+
);
110+
111+
if (monthlyPackage) {
112+
setPackage(monthlyPackage);
113+
}
114+
};
115+
116+
const purchasePackage = async (aPackage: PurchasesPackage) => {
117+
try {
118+
const purchaseResult = await Purchases.purchasePackage(aPackage);
119+
console.log('Purchase successful:', purchaseResult);
120+
} catch (err) {
121+
console.error('Purchase failed:', err);
122+
}
123+
};
124+
125+
const purchaseWinBackOfferForPackage = async (
126+
aPackage: PurchasesPackage,
127+
offer: PurchasesWinBackOffer,
128+
) => {
129+
try {
130+
const result = await Purchases.purchasePackageWithWinBackOffer(
131+
aPackage,
132+
offer,
133+
);
134+
console.log('Win-Back Offer purchase successful:', result);
135+
} catch (err) {
136+
console.error('Win-Back Offer purchase failed:', err);
137+
}
138+
};
139+
140+
return (
141+
<ScrollView>
142+
<Text>
143+
Use this screen to fetch eligible win-back offers, purchase products
144+
without a win-back offer, and purchase products with an eligible
145+
win-back offer.
146+
</Text>
147+
<Text>
148+
This test relies on products and offers defined in the SKConfig file, so
149+
be sure to launch the PurchaseTester app from Xcode with the SKConfig
150+
file configured.
151+
</Text>
152+
153+
<Button title="Fetch Product" onPress={fetchProduct} />
154+
155+
{error && <Text style={{color: 'red'}}>{error}</Text>}
156+
157+
{product && (
158+
<View style={styles.productContainer}>
159+
<Text style={styles.productTitle}>{product.title}</Text>
160+
<Text>{product.description}</Text>
161+
<Text>{product.priceString}</Text>
162+
<TouchableOpacity
163+
style={styles.purchaseButton}
164+
onPress={() => purchaseProduct(product)}>
165+
<Text style={styles.purchaseButtonText}>Purchase</Text>
166+
</TouchableOpacity>
167+
168+
<TouchableOpacity
169+
style={styles.getEligibleWinBackOffersButton}
170+
onPress={() => fetchEligibleWinBackOffersForProduct(product)}>
171+
<Text style={styles.purchaseButtonText}>
172+
Fetch Eligible Win-Back Offers for this Product
173+
</Text>
174+
</TouchableOpacity>
175+
176+
{productWinBackOffers.length > 0 && (
177+
<View style={styles.winBackContainer}>
178+
<Text style={styles.winBackTitle}>
179+
Win-Back Offers for Product:
180+
</Text>
181+
{productWinBackOffers.map((offer, index) => (
182+
<View key={index} style={styles.winBackOffer}>
183+
<Text>Identifier: {offer.identifier}</Text>
184+
<Text>Price: {offer.priceString}</Text>
185+
<Text>Cycles: {offer.cycles}</Text>
186+
<Text>Period: {offer.period}</Text>
187+
<Text>Period Unit: {offer.periodUnit}</Text>
188+
<Text>
189+
Period Number of Units: {offer.periodNumberOfUnits}
190+
</Text>
191+
192+
<TouchableOpacity
193+
style={styles.purchaseWinBackButton}
194+
onPress={() =>
195+
purchaseWinBackOfferForProduct(product, offer)
196+
}>
197+
<Text style={styles.purchaseWinBackButtonText}>
198+
Purchase Win-Back Offer
199+
</Text>
200+
</TouchableOpacity>
201+
</View>
202+
))}
203+
</View>
204+
)}
205+
</View>
206+
)}
207+
208+
<Button title="Fetch Package" onPress={fetchPackage} />
209+
{aPackage && (
210+
<View style={styles.productContainer}>
211+
<Text style={styles.productTitle}>{aPackage.identifier}</Text>
212+
<Text>{aPackage.product.description}</Text>
213+
<Text>{aPackage.product.priceString}</Text>
214+
215+
<TouchableOpacity
216+
style={styles.purchaseButton}
217+
onPress={() => purchasePackage(aPackage)}>
218+
<Text style={styles.purchaseButtonText}>Purchase</Text>
219+
</TouchableOpacity>
220+
221+
<TouchableOpacity
222+
style={styles.getEligibleWinBackOffersButton}
223+
onPress={() => fetchEligibleWinBackOffersForPackage(aPackage)}>
224+
<Text style={styles.purchaseButtonText}>
225+
Fetch Eligible Win-Back Offers for this Package
226+
</Text>
227+
</TouchableOpacity>
228+
229+
{packageWinBackOffers.map((offer, index) => (
230+
<View key={index} style={styles.winBackOffer}>
231+
<Text>Identifier: {offer.identifier}</Text>
232+
<Text>Price: {offer.priceString}</Text>
233+
<Text>Cycles: {offer.cycles}</Text>
234+
<Text>Period: {offer.period}</Text>
235+
<Text>Period Unit: {offer.periodUnit}</Text>
236+
<Text>Period Number of Units: {offer.periodNumberOfUnits}</Text>
237+
238+
<TouchableOpacity
239+
style={styles.purchaseWinBackButton}
240+
onPress={() => purchaseWinBackOfferForPackage(aPackage, offer)}>
241+
<Text style={styles.purchaseWinBackButtonText}>
242+
Purchase Win-Back Offer
243+
</Text>
244+
</TouchableOpacity>
245+
</View>
246+
))}
247+
</View>
248+
)}
249+
</ScrollView>
250+
);
251+
};
252+
253+
const styles = StyleSheet.create({
254+
productContainer: {
255+
marginTop: 16,
256+
padding: 16,
257+
borderColor: '#ccc',
258+
borderWidth: 1,
259+
borderRadius: 8,
260+
},
261+
productTitle: {
262+
fontSize: 18,
263+
fontWeight: 'bold',
264+
},
265+
purchaseButton: {
266+
marginTop: 10,
267+
backgroundColor: 'lightcoral',
268+
paddingVertical: 10,
269+
paddingHorizontal: 20,
270+
borderRadius: 8,
271+
alignItems: 'center',
272+
},
273+
getEligibleWinBackOffersButton: {
274+
marginTop: 10,
275+
backgroundColor: 'blue',
276+
paddingVertical: 10,
277+
paddingHorizontal: 20,
278+
borderRadius: 8,
279+
alignItems: 'center',
280+
},
281+
purchaseButtonText: {
282+
color: 'white',
283+
fontWeight: 'bold',
284+
},
285+
winBackContainer: {
286+
marginTop: 16,
287+
padding: 16,
288+
borderColor: '#cccccc',
289+
borderWidth: 1,
290+
borderRadius: 8,
291+
backgroundColor: '#f9f9f9',
292+
},
293+
winBackTitle: {
294+
fontSize: 16,
295+
fontWeight: 'bold',
296+
marginBottom: 8,
297+
},
298+
winBackOffer: {
299+
marginBottom: 10,
300+
},
301+
purchaseWinBackButton: {
302+
marginTop: 10,
303+
backgroundColor: 'green',
304+
paddingVertical: 10,
305+
paddingHorizontal: 20,
306+
borderRadius: 8,
307+
alignItems: 'center',
308+
},
309+
purchaseWinBackButtonText: {
310+
color: 'white',
311+
fontWeight: 'bold',
312+
},
313+
});
314+
315+
export default WinBackTestingScreen;

examples/purchaseTesterTypescript/ios/PurchaseTester.xcodeproj/xcshareddata/xcschemes/PurchaseTester.xcscheme

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,9 @@
6060
ReferencedContainer = "container:PurchaseTester.xcodeproj">
6161
</BuildableReference>
6262
</BuildableProductRunnable>
63+
<StoreKitConfigurationFileReference
64+
identifier = "../RevenueCast_PurchaseTesterTypescriptConfiguration.storekit">
65+
</StoreKitConfigurationFileReference>
6366
</LaunchAction>
6467
<ProfileAction
6568
buildConfiguration = "Release"

0 commit comments

Comments
 (0)