1import { 2 connectAsync, 3 disconnectAsync, 4 finishTransactionAsync, 5 getBillingResponseCodeAsync, 6 getProductsAsync, 7 getPurchaseHistoryAsync, 8 IAPResponseCode, 9 purchaseItemAsync, 10 setPurchaseListener, 11 IAPItemDetails, 12 InAppPurchase, 13} from 'expo-in-app-purchases'; 14import React from 'react'; 15import { 16 Button, 17 Platform, 18 SafeAreaView, 19 ScrollView, 20 StatusBar, 21 StyleSheet, 22 Text, 23 View, 24} from 'react-native'; 25 26export default class InAppPurchases extends React.Component<any, any> { 27 static navigationOptions = { 28 title: 'In-App Purchases', 29 }; 30 31 state = { 32 items: [], 33 history: [], 34 responseCode: 0, 35 }; 36 37 componentDidMount() { 38 this.prepareAsync(); 39 } 40 41 async prepareAsync() { 42 // This method must be called first to initialize listeners and billing client 43 await connectAsync(); 44 45 /* 46 These item entries are created in App Store Connect and Google Play Console respectively. 47 If you want to add more or edit their attributes you can do so there. 48 */ 49 const items = Platform.select({ 50 ios: [ 51 'dev.expo.products.premium', 52 'dev.expo.payments.updates', 53 'dev.expo.payments.adfree', 54 'dev.expo.payments.gold', 55 ], 56 android: ['gas', 'premium', 'gold_yearly', 'gold_monthly'], 57 default: [], 58 }); 59 60 // Get product details 61 const { responseCode, results } = await getProductsAsync(items); 62 if (responseCode === IAPResponseCode.OK) { 63 this.setState({ items: results }); 64 } 65 66 // Set purchase listener 67 setPurchaseListener(({ responseCode, results, errorCode }) => { 68 if (responseCode === IAPResponseCode.OK) { 69 for (const purchase of results!) { 70 console.log(`Successfully purchased ${purchase.productId}`); 71 if (!purchase.acknowledged) { 72 // `gas` is the only consumable product, the rest are subscriptions. 73 finishTransactionAsync(purchase, purchase.productId === 'gas'); 74 } 75 } 76 } else if (responseCode === IAPResponseCode.USER_CANCELED) { 77 console.log('User canceled'); 78 } else { 79 console.warn( 80 `Something went wrong with the purchase. Received response code ${responseCode} and errorCode ${errorCode}` 81 ); 82 } 83 }); 84 } 85 86 async componentWillUnmount() { 87 await disconnectAsync(); 88 } 89 90 async queryPurchaseHistory() { 91 const { responseCode, results } = await getPurchaseHistoryAsync({ useGooglePlayCache: false }); 92 if (responseCode === IAPResponseCode.OK) { 93 this.setState({ history: results }); 94 } 95 } 96 97 renderItem(item: IAPItemDetails) { 98 return ( 99 <View key={item.productId}> 100 <Text style={styles.itemTitle}>{item.title}</Text> 101 <Text>Description: {item.description}</Text> 102 <Text>Price: {item.price}</Text> 103 <Text>Currency Code: {item.priceCurrencyCode}</Text> 104 <Text>Price Amount Micros: {item.priceAmountMicros}</Text> 105 <Text>Product ID: {item.productId}</Text> 106 <Text>Type: {item.type}</Text> 107 <Text>Subscription Period: {item.subscriptionPeriod}</Text> 108 <View style={styles.buttonContainer}> 109 <Button title="Buy" onPress={() => purchaseItemAsync(item.productId)} /> 110 </View> 111 </View> 112 ); 113 } 114 115 renderHistoryRecord(record: InAppPurchase) { 116 const key = Platform.OS === 'android' ? record.purchaseToken : record.orderId; 117 return ( 118 <View key={key}> 119 <Text style={styles.itemTitle}>Product ID: {record.productId}</Text> 120 <Text>Acknowledged: {record.acknowledged ? 'True' : 'False'}</Text> 121 <Text>Purchase State: {record.purchaseState}</Text> 122 <Text>Purchase Time: {record.purchaseTime}</Text> 123 <Text>Order ID: {record.orderId}</Text> 124 {Platform.OS === 'android' ? <Text>Purchase Token: {record.purchaseToken}</Text> : null} 125 {Platform.OS === 'android' ? <Text>Package Name: {record.packageName}</Text> : null} 126 {Platform.OS === 'ios' ? <Text>Original Order ID: {record.originalOrderId}</Text> : null} 127 {Platform.OS === 'ios' ? ( 128 <Text>Original Purchase Time: {record.originalPurchaseTime}</Text> 129 ) : null} 130 {record.productId === 'gold_monthly' ? ( 131 <Button 132 title="Upgrade to yearly" 133 onPress={() => 134 purchaseItemAsync('gold_yearly', { oldPurchaseToken: record.purchaseToken }) 135 } 136 /> 137 ) : null} 138 {record.productId === 'gold_yearly' ? ( 139 <Button 140 title="Downgrade to monthly" 141 onPress={() => 142 purchaseItemAsync('gold_monthly', { oldPurchaseToken: record.purchaseToken }) 143 } 144 /> 145 ) : null} 146 </View> 147 ); 148 } 149 150 async getBillingResult() { 151 const responseCode = await getBillingResponseCodeAsync(); 152 this.setState({ responseCode }); 153 } 154 155 render() { 156 return ( 157 <ScrollView> 158 <SafeAreaView style={styles.container}> 159 <Text style={styles.titleText}>In App Store</Text> 160 </SafeAreaView> 161 {this.state.items.map((item) => this.renderItem(item))} 162 <Text style={styles.itemTitle}>History</Text> 163 <View style={styles.buttonContainer}> 164 <Button title="Query History" onPress={() => this.queryPurchaseHistory()} /> 165 </View> 166 {this.state.history.map((historyRecord) => this.renderHistoryRecord(historyRecord))} 167 <View style={styles.buttonContainer}> 168 <Button title="Update Response Code" onPress={() => this.getBillingResult()} /> 169 </View> 170 <View style={styles.container}> 171 <Text style={styles.itemTitle}>{this.state.responseCode}</Text> 172 </View> 173 </ScrollView> 174 ); 175 } 176} 177 178const styles = StyleSheet.create({ 179 container: { 180 alignItems: 'center', 181 justifyContent: 'center', 182 }, 183 titleText: { 184 fontSize: 24, 185 fontWeight: 'bold', 186 paddingTop: Platform.OS === 'android' ? StatusBar.currentHeight : 0, 187 }, 188 buttonContainer: { 189 alignItems: 'center', 190 justifyContent: 'center', 191 marginTop: 10, 192 marginBottom: 10, 193 }, 194 itemTitle: { 195 fontSize: 18, 196 fontWeight: 'bold', 197 }, 198}); 199