Subscription Paywall Screen with Image Background

Screen is created using these React Native Core Components: <StatusBar />, <SafeAreaView />, <Image />, <View />, <TouchableOpacity />, <Text />

import React from 'react';
import {
  StyleSheet,
  Dimensions,
  StatusBar,
  SafeAreaView,
  Image,
  View,
  TouchableOpacity,
  Text,
} from 'react-native';
import FeatherIcon from 'react-native-vector-icons/Feather';

const { width, height } = Dimensions.get('window');

export default function Example() {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <Image
        alt=""
        style={styles.background}
        source={{
          uri: 'https://images.unsplash.com/photo-1500916434205-0c77489c6cf7?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=987&q=80',
        }} />

      <View style={[styles.background, styles.overflow]} />

      <View style={styles.container}>
        <StatusBar barStyle="light-content" />
        <View style={styles.paywall}>
          <TouchableOpacity
            onPress={() => {
              // handle onPress
            }}
            style={styles.paywallClose}>
            <FeatherIcon color="#fff" name="x" size={30} />
          </TouchableOpacity>

          <Text style={styles.paywallBadge}>Limited time offer</Text>

          <Text style={styles.paywallTitle}>NewsApp+</Text>

          <Text style={styles.paywallMessage}>
            Don't miss out on the full story. Unlock detailed reports, special
            features, and more by subscribing to our premium service.
          </Text>

          <TouchableOpacity
            onPress={() => {
              // handle onPress
            }}>
            <View style={styles.btn}>
              <Text style={styles.btnText}>Subscribe for $3.99 / mo</Text>
            </View>
          </TouchableOpacity>

          <View style={{ marginTop: 8 }}>
            <TouchableOpacity
              onPress={() => {
                // handle onPress
              }}>
              <View style={styles.btnSecondary}>
                <Text style={styles.btnSecondaryText}>Skip for now</Text>
              </View>
            </TouchableOpacity>
          </View>
        </View>
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  background: {
    position: 'absolute',
    top: 0,
    left: 0,
    width: width,
    height: height,
  },
  overflow: {
    backgroundColor: 'rgba(0, 0, 0, 0.6)',
  },
  container: {
    paddingVertical: 6,
    paddingHorizontal: 24,
    flexGrow: 1,
    flexShrink: 1,
    flexBasis: 0,
  },
  /** Paywall */
  paywall: {
    position: 'relative',
    flexDirection: 'column',
    alignItems: 'stretch',
    justifyContent: 'flex-end',
    flexGrow: 1,
    flexShrink: 1,
    flexBasis: 0,
  },
  paywallClose: {
    alignSelf: 'flex-end',
    marginBottom: 'auto',
  },
  paywallBadge: {
    fontSize: 15,
    color: '#fff',
    fontWeight: '700',
    marginBottom: 4,
    textAlign: 'center',
  },
  paywallTitle: {
    fontSize: 42,
    textAlign: 'center',
    lineHeight: 44,
    fontWeight: '700',
    color: '#fff',
    marginBottom: 12,
  },
  paywallMessage: {
    textAlign: 'center',
    marginBottom: 36,
    fontSize: 16,
    lineHeight: 22,
    fontWeight: '500',
    color: '#fff',
  },
  /** Button */
  btn: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 8,
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderWidth: 1,
    backgroundColor: '#fff',
    borderColor: '#fff',
  },
  btnText: {
    fontSize: 18,
    lineHeight: 26,
    fontWeight: '600',
    color: '#000',
  },
  btnSecondary: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 8,
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderWidth: 1,
    backgroundColor: 'transparent',
    borderColor: 'transparent',
  },
  btnSecondaryText: {
    fontSize: 18,
    lineHeight: 26,
    fontWeight: '600',
    color: '#fff',
  },
});
import React from 'react';
import {
  StyleSheet,
  Dimensions,
  StatusBar,
  SafeAreaView,
  Image,
  View,
  TouchableOpacity,
  Text,
} from 'react-native';
import FeatherIcon from '@expo/vector-icons/Feather';

const { width, height } = Dimensions.get('window');

export default function Example() {
  return (
    <SafeAreaView style={{ flex: 1 }}>
      <Image
        alt=""
        style={styles.background}
        source={{
          uri: 'https://images.unsplash.com/photo-1500916434205-0c77489c6cf7?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=987&q=80',
        }} />

      <View style={[styles.background, styles.overflow]} />

      <View style={styles.container}>
        <StatusBar barStyle="light-content" />
        <View style={styles.paywall}>
          <TouchableOpacity
            onPress={() => {
              // handle onPress
            }}
            style={styles.paywallClose}>
            <FeatherIcon color="#fff" name="x" size={30} />
          </TouchableOpacity>

          <Text style={styles.paywallBadge}>Limited time offer</Text>

          <Text style={styles.paywallTitle}>NewsApp+</Text>

          <Text style={styles.paywallMessage}>
            Don't miss out on the full story. Unlock detailed reports, special
            features, and more by subscribing to our premium service.
          </Text>

          <TouchableOpacity
            onPress={() => {
              // handle onPress
            }}>
            <View style={styles.btn}>
              <Text style={styles.btnText}>Subscribe for $3.99 / mo</Text>
            </View>
          </TouchableOpacity>

          <View style={{ marginTop: 8 }}>
            <TouchableOpacity
              onPress={() => {
                // handle onPress
              }}>
              <View style={styles.btnSecondary}>
                <Text style={styles.btnSecondaryText}>Skip for now</Text>
              </View>
            </TouchableOpacity>
          </View>
        </View>
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  background: {
    position: 'absolute',
    top: 0,
    left: 0,
    width: width,
    height: height,
  },
  overflow: {
    backgroundColor: 'rgba(0, 0, 0, 0.6)',
  },
  container: {
    paddingVertical: 6,
    paddingHorizontal: 24,
    flexGrow: 1,
    flexShrink: 1,
    flexBasis: 0,
  },
  /** Paywall */
  paywall: {
    position: 'relative',
    flexDirection: 'column',
    alignItems: 'stretch',
    justifyContent: 'flex-end',
    flexGrow: 1,
    flexShrink: 1,
    flexBasis: 0,
  },
  paywallClose: {
    alignSelf: 'flex-end',
    marginBottom: 'auto',
  },
  paywallBadge: {
    fontSize: 15,
    color: '#fff',
    fontWeight: '700',
    marginBottom: 4,
    textAlign: 'center',
  },
  paywallTitle: {
    fontSize: 42,
    textAlign: 'center',
    lineHeight: 44,
    fontWeight: '700',
    color: '#fff',
    marginBottom: 12,
  },
  paywallMessage: {
    textAlign: 'center',
    marginBottom: 36,
    fontSize: 16,
    lineHeight: 22,
    fontWeight: '500',
    color: '#fff',
  },
  /** Button */
  btn: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 8,
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderWidth: 1,
    backgroundColor: '#fff',
    borderColor: '#fff',
  },
  btnText: {
    fontSize: 18,
    lineHeight: 26,
    fontWeight: '600',
    color: '#000',
  },
  btnSecondary: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 8,
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderWidth: 1,
    backgroundColor: 'transparent',
    borderColor: 'transparent',
  },
  btnSecondaryText: {
    fontSize: 18,
    lineHeight: 26,
    fontWeight: '600',
    color: '#fff',
  },
});

In-App Purchase Integration 💰

UI/UX implementation of the React Native Component with In-App Purchase integration using the `react-native-iap` package and NodeJS (Express) server.

import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
  StyleSheet,
  Dimensions,
  StatusBar,
  Alert,
  Platform,
  View,
  Text,
  TouchableOpacity,
  SafeAreaView,
  Image,
  ActivityIndicator,
} from 'react-native';
import axios from 'axios';
import {
  requestSubscription,
  getProducts,
  initConnection,
  purchaseUpdatedListener,
  purchaseErrorListener,
  flushFailedPurchasesCachedAsPendingAndroid,
  finishTransaction,
  getAvailablePurchases,
} from 'react-native-iap';
import FeatherIcon from 'react-native-vector-icons/Feather';

const { width, height } = Dimensions.get('window');

/**
 * Replace `sku` with your own value from the AppStoreConnect and PlayMarket Console
 */
const sku = Platform.select({
  ios: 'subscription__3_99',
  android: 'subscription__3_99',
});

export default function Example() {
  const [activePurchase, setActivePurchase] = useState();
  const [isLoading, setIsLoading] = useState(false);
  const [products, setProducts] = useState(null);
  const purchaseUpdateSubscription = useRef();
  const purchaseErrorSubscription = useRef();

  const getActiveSubscription = useCallback(async () => {
    try {
      setIsLoading(true);

      const purchases = await getAvailablePurchases({
        alsoPublishToEventListener: false,
        onlyIncludeActiveItems: true,
      });

      if (purchases.length) {
        setActivePurchase(purchases[0]);
      }
    } catch (err) {
      console.error(err);
    } finally {
      setIsLoading(false);
    }
  }, []);

  useEffect(() => {
    (async () => {
      try {
        await initConnection();

        await getActiveSubscription();

        // Make sure "ghost" pending payments are removed
        await flushFailedPurchasesCachedAsPendingAndroid().catch(() => {});

        purchaseUpdateSubscription.current = purchaseUpdatedListener(
          async purchase => {
            const receipt = purchase.transactionReceipt;

            if (receipt) {
              try {
                setIsLoading(true);

                const { data } = await axios.post(
                  'https://demo.withfra.me/verify-purchase',
                  {
                    receipt,
                    platform: Platform.OS,
                  },
                );

                if (data.success) {
                  await finishTransaction({ purchase, isConsumable: false });

                  Alert.alert('Success!', 'Thank you for your purchase!', [
                    { text: 'OK', onPress: () => getActiveSubscription() },
                  ]);
                }
              } catch (err) {
                const message =
                  err.response?.data?.message ??
                  err.message ??
                  'Something went wrong!';
                Alert.alert('Oops!', message);
              } finally {
                setIsLoading(false);
              }
            }
          },
        );

        purchaseErrorSubscription.current = purchaseErrorListener(error => {
          Alert.alert('Oops!', error.message);
        });

        const data = await getProducts({ skus: [sku] });
        setProducts(data);
      } catch (err) {
        const message =
          err.response?.data?.message ?? err.message ?? 'Something went wrong!';
        Alert.alert('Oops!', message);
      }
    })();

    return () => {
      // cleanup on component unmount
      purchaseUpdateSubscription.current?.remove();
      purchaseUpdateSubscription.current = null;

      purchaseErrorSubscription.current?.remove();
      purchaseErrorSubscription.current = null;
    };
  }, [getActiveSubscription]);

  const handleSubscribe = useCallback(async () => {
    try {
      await requestSubscription({ sku });
    } catch (err) {
      if (err.code !== 'E_USER_CANCELLED') {
        Alert.alert('Oops!', err.message);
      }
    }
  }, []);

  const loading = !products || isLoading;

  if (activePurchase) {
    return (
      <View style={styles.loggedIn}>
        <Text style={styles.loggedInTitle}>You're subscribed!</Text>

        <Text style={styles.loggedInText}>
          Transaction ID: {activePurchase.transactionId}
        </Text>
        <Text style={styles.loggedInText} numberOfLines={1}>
          Transaction Date:{' '}
          {new Date(activePurchase.transactionDate).toISOString()}
        </Text>
        <Text style={styles.loggedInText} numberOfLines={1}>
          Transaction Receipt: {activePurchase.transactionReceipt}
        </Text>

        <TouchableOpacity onPress={() => {}}>
          <Text style={styles.loggedInLink}>Log out</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <>
      {loading && (
        <View style={styles.activityOverflow}>
          <ActivityIndicator size="large" color="#fff" />
        </View>
      )}

      <SafeAreaView style={{ flex: 1 }}>
        <Image
          alt=""
          style={styles.background}
          source={{
            uri: 'https://images.unsplash.com/photo-1500916434205-0c77489c6cf7?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=987&q=80',
          }} />

        <View style={[styles.background, styles.overflow]} />

        <View style={styles.container}>
          <StatusBar barStyle="light-content" />
          <View style={styles.paywall}>
            <TouchableOpacity
              onPress={() => {
                // handle onPress
              }}
              style={styles.paywallClose}>
              <FeatherIcon color="#fff" name="x" size={30} />
            </TouchableOpacity>

            <Text style={styles.paywallBadge}>Limited time offer</Text>

            <Text style={styles.paywallTitle}>NewsApp+</Text>

            <Text style={styles.paywallMessage}>
              Don't miss out on the full story. Unlock detailed reports, special
              features, and more by subscribing to our premium service.
            </Text>

            <TouchableOpacity onPress={handleSubscribe}>
              <View style={styles.btn}>
                <Text style={styles.btnText}>Subscribe for $3.99 / mo</Text>
              </View>
            </TouchableOpacity>

            <View style={{ marginTop: 8 }}>
              <TouchableOpacity
                onPress={() => {
                  // handle onPress
                }}>
                <View style={styles.btnSecondary}>
                  <Text style={styles.btnSecondaryText}>Skip for now</Text>
                </View>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </SafeAreaView>
    </>
  );
}

const styles = StyleSheet.create({
  activityOverflow: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    right: 0,
    left: 0,
    zIndex: 9999,
    backgroundColor: 'rgba(0, 0, 0, 0.35)',
    alignItems: 'center',
    justifyContent: 'center',
    paddingBottom: 60,
  },
  background: {
    position: 'absolute',
    top: 0,
    left: 0,
    width: width,
    height: height,
  },
  overflow: {
    backgroundColor: 'rgba(0, 0, 0, 0.6)',
  },
  container: {
    paddingVertical: 6,
    paddingHorizontal: 24,
    flexGrow: 1,
    flexShrink: 1,
    flexBasis: 0,
  },
  loggedIn: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    paddingHorizontal: 24,
  },
  loggedInTitle: {
    fontSize: 17,
    fontWeight: '500',
    color: '#1d1d1d',
    marginBottom: 6,
  },
  loggedInText: {
    fontSize: 15,
    color: '#555',
  },
  loggedInLink: {
    fontSize: 15,
    color: '#007aff',
    textDecorationLine: 'underline',
  },
  /** Paywall */
  paywall: {
    position: 'relative',
    flexDirection: 'column',
    alignItems: 'stretch',
    justifyContent: 'flex-end',
    flexGrow: 1,
    flexShrink: 1,
    flexBasis: 0,
  },
  paywallClose: {
    alignSelf: 'flex-end',
    marginBottom: 'auto',
  },
  paywallBadge: {
    fontSize: 15,
    color: '#fff',
    fontWeight: '700',
    marginBottom: 4,
    textAlign: 'center',
  },
  paywallTitle: {
    fontSize: 42,
    textAlign: 'center',
    lineHeight: 44,
    fontWeight: '700',
    color: '#fff',
    marginBottom: 12,
  },
  paywallMessage: {
    textAlign: 'center',
    marginBottom: 36,
    fontSize: 16,
    lineHeight: 22,
    fontWeight: '500',
    color: '#fff',
  },
  /** Button */
  btn: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 8,
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderWidth: 1,
    backgroundColor: '#fff',
    borderColor: '#fff',
  },
  btnText: {
    fontSize: 18,
    lineHeight: 26,
    fontWeight: '600',
    color: '#000',
  },
  btnSecondary: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 8,
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderWidth: 1,
    backgroundColor: 'transparent',
    borderColor: 'transparent',
  },
  btnSecondaryText: {
    fontSize: 18,
    lineHeight: 26,
    fontWeight: '600',
    color: '#fff',
  },
});
import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
  StyleSheet,
  Dimensions,
  StatusBar,
  Alert,
  Platform,
  View,
  Text,
  TouchableOpacity,
  SafeAreaView,
  Image,
  ActivityIndicator,
} from 'react-native';
import axios from 'axios';
import {
  requestSubscription,
  getProducts,
  initConnection,
  purchaseUpdatedListener,
  purchaseErrorListener,
  flushFailedPurchasesCachedAsPendingAndroid,
  finishTransaction,
  getAvailablePurchases,
} from 'react-native-iap';
import FeatherIcon from '@expo/vector-icons/Feather';

const { width, height } = Dimensions.get('window');

/**
 * Replace `sku` with your own value from the AppStoreConnect and PlayMarket Console
 */
const sku = Platform.select({
  ios: 'subscription__3_99',
  android: 'subscription__3_99',
});

export default function Example() {
  const [activePurchase, setActivePurchase] = useState();
  const [isLoading, setIsLoading] = useState(false);
  const [products, setProducts] = useState(null);
  const purchaseUpdateSubscription = useRef();
  const purchaseErrorSubscription = useRef();

  const getActiveSubscription = useCallback(async () => {
    try {
      setIsLoading(true);

      const purchases = await getAvailablePurchases({
        alsoPublishToEventListener: false,
        onlyIncludeActiveItems: true,
      });

      if (purchases.length) {
        setActivePurchase(purchases[0]);
      }
    } catch (err) {
      console.error(err);
    } finally {
      setIsLoading(false);
    }
  }, []);

  useEffect(() => {
    (async () => {
      try {
        await initConnection();

        await getActiveSubscription();

        // Make sure "ghost" pending payments are removed
        await flushFailedPurchasesCachedAsPendingAndroid().catch(() => {});

        purchaseUpdateSubscription.current = purchaseUpdatedListener(
          async purchase => {
            const receipt = purchase.transactionReceipt;

            if (receipt) {
              try {
                setIsLoading(true);

                const { data } = await axios.post(
                  'https://demo.withfra.me/verify-purchase',
                  {
                    receipt,
                    platform: Platform.OS,
                  },
                );

                if (data.success) {
                  await finishTransaction({ purchase, isConsumable: false });

                  Alert.alert('Success!', 'Thank you for your purchase!', [
                    { text: 'OK', onPress: () => getActiveSubscription() },
                  ]);
                }
              } catch (err) {
                const message =
                  err.response?.data?.message ??
                  err.message ??
                  'Something went wrong!';
                Alert.alert('Oops!', message);
              } finally {
                setIsLoading(false);
              }
            }
          },
        );

        purchaseErrorSubscription.current = purchaseErrorListener(error => {
          Alert.alert('Oops!', error.message);
        });

        const data = await getProducts({ skus: [sku] });
        setProducts(data);
      } catch (err) {
        const message =
          err.response?.data?.message ?? err.message ?? 'Something went wrong!';
        Alert.alert('Oops!', message);
      }
    })();

    return () => {
      // cleanup on component unmount
      purchaseUpdateSubscription.current?.remove();
      purchaseUpdateSubscription.current = null;

      purchaseErrorSubscription.current?.remove();
      purchaseErrorSubscription.current = null;
    };
  }, [getActiveSubscription]);

  const handleSubscribe = useCallback(async () => {
    try {
      await requestSubscription({ sku });
    } catch (err) {
      if (err.code !== 'E_USER_CANCELLED') {
        Alert.alert('Oops!', err.message);
      }
    }
  }, []);

  const loading = !products || isLoading;

  if (activePurchase) {
    return (
      <View style={styles.loggedIn}>
        <Text style={styles.loggedInTitle}>You're subscribed!</Text>

        <Text style={styles.loggedInText}>
          Transaction ID: {activePurchase.transactionId}
        </Text>
        <Text style={styles.loggedInText} numberOfLines={1}>
          Transaction Date:{' '}
          {new Date(activePurchase.transactionDate).toISOString()}
        </Text>
        <Text style={styles.loggedInText} numberOfLines={1}>
          Transaction Receipt: {activePurchase.transactionReceipt}
        </Text>

        <TouchableOpacity onPress={() => {}}>
          <Text style={styles.loggedInLink}>Log out</Text>
        </TouchableOpacity>
      </View>
    );
  }

  return (
    <>
      {loading && (
        <View style={styles.activityOverflow}>
          <ActivityIndicator size="large" color="#fff" />
        </View>
      )}

      <SafeAreaView style={{ flex: 1 }}>
        <Image
          alt=""
          style={styles.background}
          source={{
            uri: 'https://images.unsplash.com/photo-1500916434205-0c77489c6cf7?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=987&q=80',
          }} />

        <View style={[styles.background, styles.overflow]} />

        <View style={styles.container}>
          <StatusBar barStyle="light-content" />
          <View style={styles.paywall}>
            <TouchableOpacity
              onPress={() => {
                // handle onPress
              }}
              style={styles.paywallClose}>
              <FeatherIcon color="#fff" name="x" size={30} />
            </TouchableOpacity>

            <Text style={styles.paywallBadge}>Limited time offer</Text>

            <Text style={styles.paywallTitle}>NewsApp+</Text>

            <Text style={styles.paywallMessage}>
              Don't miss out on the full story. Unlock detailed reports, special
              features, and more by subscribing to our premium service.
            </Text>

            <TouchableOpacity onPress={handleSubscribe}>
              <View style={styles.btn}>
                <Text style={styles.btnText}>Subscribe for $3.99 / mo</Text>
              </View>
            </TouchableOpacity>

            <View style={{ marginTop: 8 }}>
              <TouchableOpacity
                onPress={() => {
                  // handle onPress
                }}>
                <View style={styles.btnSecondary}>
                  <Text style={styles.btnSecondaryText}>Skip for now</Text>
                </View>
              </TouchableOpacity>
            </View>
          </View>
        </View>
      </SafeAreaView>
    </>
  );
}

const styles = StyleSheet.create({
  activityOverflow: {
    position: 'absolute',
    top: 0,
    bottom: 0,
    right: 0,
    left: 0,
    zIndex: 9999,
    backgroundColor: 'rgba(0, 0, 0, 0.35)',
    alignItems: 'center',
    justifyContent: 'center',
    paddingBottom: 60,
  },
  background: {
    position: 'absolute',
    top: 0,
    left: 0,
    width: width,
    height: height,
  },
  overflow: {
    backgroundColor: 'rgba(0, 0, 0, 0.6)',
  },
  container: {
    paddingVertical: 6,
    paddingHorizontal: 24,
    flexGrow: 1,
    flexShrink: 1,
    flexBasis: 0,
  },
  loggedIn: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    paddingHorizontal: 24,
  },
  loggedInTitle: {
    fontSize: 17,
    fontWeight: '500',
    color: '#1d1d1d',
    marginBottom: 6,
  },
  loggedInText: {
    fontSize: 15,
    color: '#555',
  },
  loggedInLink: {
    fontSize: 15,
    color: '#007aff',
    textDecorationLine: 'underline',
  },
  /** Paywall */
  paywall: {
    position: 'relative',
    flexDirection: 'column',
    alignItems: 'stretch',
    justifyContent: 'flex-end',
    flexGrow: 1,
    flexShrink: 1,
    flexBasis: 0,
  },
  paywallClose: {
    alignSelf: 'flex-end',
    marginBottom: 'auto',
  },
  paywallBadge: {
    fontSize: 15,
    color: '#fff',
    fontWeight: '700',
    marginBottom: 4,
    textAlign: 'center',
  },
  paywallTitle: {
    fontSize: 42,
    textAlign: 'center',
    lineHeight: 44,
    fontWeight: '700',
    color: '#fff',
    marginBottom: 12,
  },
  paywallMessage: {
    textAlign: 'center',
    marginBottom: 36,
    fontSize: 16,
    lineHeight: 22,
    fontWeight: '500',
    color: '#fff',
  },
  /** Button */
  btn: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 8,
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderWidth: 1,
    backgroundColor: '#fff',
    borderColor: '#fff',
  },
  btnText: {
    fontSize: 18,
    lineHeight: 26,
    fontWeight: '600',
    color: '#000',
  },
  btnSecondary: {
    flexDirection: 'row',
    alignItems: 'center',
    justifyContent: 'center',
    borderRadius: 8,
    paddingVertical: 10,
    paddingHorizontal: 20,
    borderWidth: 1,
    backgroundColor: 'transparent',
    borderColor: 'transparent',
  },
  btnSecondaryText: {
    fontSize: 18,
    lineHeight: 26,
    fontWeight: '600',
    color: '#fff',
  },
});
const express = require('express');
const axios = require('axios');
const {google} = require('googleapis');

const app = express();
app.use(express.json());

/**
 * For demo purchases we will always approve receipts without checking with Google / Apple.
 * 
 * Note: Make sure to remove this flag in your production code! 
 */
const IS_DEVELOPMENT = true;

/**
 * APPLE
 * 
 * Follow this guide to generate your secret key:
 *    https://developer.apple.com/help/app-store-connect/configure-in-app-purchase-settings/generate-a-shared-secret-to-verify-receipts
 */
const ITUNES_PASSWORD = 'YOUR_ITUNES_PASSWORD';

const verifyTransactionWithItunes = async (receipt) => {
  try {
    const {data: production} = await axios.post('https://buy.itunes.apple.com/verifyReceipt', {
      'receipt-data': receipt,
      password: ITUNES_PASSWORD,
    })

    // successful transaction
    if (production.status === 0) {
      return true;
    }

    // According to Apple, status `21007` is returned when the purchase is made in the sandbox envorinment.
    if (production.status === 21007) {
      const {data: sandbox} = await axios.post('https://sandbox.itunes.apple.com/verifyReceipt', {
        'receipt-data': receipt,
        password: ITUNES_PASSWORD,
      })

      // successful transaction
      if (sandbox.status === 0) {
        return true;
      }
    }
  } catch (err) {
    console.error(err);
  }

  return false;
}


/**
 * GOOGLE
 * 
 * Obtain `CLIENT_EMAIL` and `PRIVATE_KEY` values from the service account JSON file.
 */
const CLIENT_EMAIL = 'YOUR_CLIENT_EMAIL'
const PRIVATE_KEY = 'YOUR_PRIVATE_KEY'

const PACKAGE_NAME = 'YOUR_APP_PACKAGE_NAME';
const PRODUCT_ID = 'YOUR_PRODUCT_ID'

const auth = new google.auth.JWT(
  CLIENT_EMAIL,
  null,
  PRIVATE_KEY,
  ['https://www.googleapis.com/auth/androidpublisher']
)
const androidApi = google.androidpublisher({
  version: 'v3',
  auth,
});

const verifyTransactionWithPlayMarket = async (receipt) => {
  try{
    const { purchaseToken } = JSON.parse(receipt)

    await androidApi.purchases.products.acknowledge({
      packageName: PACKAGE_NAME,
      productId: PRODUCT_ID,
      token: purchaseToken
    })

    return true
  } catch(err) {
    console.error(err)
  }

  return false;
}

app.post('/verify-purchase', async (req, res) => {
  const {receipt, platform} = req.body;

  if (!receipt) {
    return res.status(400).json({ success: false, message: '`receipt` is required.' });
  }

  if (!['ios', 'android'].includes(platform)) {
    return res.status(400).json({ success: false, message: '`platform` has to be either `ios` or `android`.' });
  }

  // For demo purchases we will always approve receipts without checking with Google / Apple.
  if (!IS_DEVELOPMENT) {
    const verifiedPurchase = platform === 'ios'
      ? await verifyTransactionWithItunes(receipt)
      : await verifyTransactionWithPlayMarket(receipt);
  
    if (!verifiedPurchase) {
      return res.status(400).json({ success: false, message: 'Looks like we could not verify your purchase, please try again.' });
    }
  }

  // Purchase is successful, update user's entity to reflect it or deliver items the user has paid for..
  await new Promise(resolve => setTimeout(resolve, 4000))

  return res.json({ success: true });
})


app.listen(4242, () => {
  console.log('Express server is running on port 4242.');
})

Dependencies

Before getting started, make sure you have all the necessary dependencies for this component. Follow the steps below to install any missing dependencies.

Explore more components

Over 100 Ready to Use React Native Components for all your needs.

Never miss a beat

Stay in the loop with our latest updates and offers - no spam, just the good stuff!