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