β Previous: Step 3 - Configuration | Next: Step 5 - Managing Entitlements β
4# Step 4: In-App Purchase Service Integration
For this tutorial, we'll put our application logic in a separate class called AppService
.
This class will handle the initialization of the IAP system, and provide methods for checking user entitlements and initiating purchases.
- Inside the
src
folder, create a new file namedAppService.ts
- Let's add the required imports and define our empty
AppService
class for now.
import { Platform, Alert, ToastAndroid } from 'react-native';
import { IapticError, IapticSeverity, IapticOffer, IapticRN } from 'react-native-iaptic';
import { Config } from './Config';
export class AppService {
DEBUG = true;
showAlert(title: string, message: string, debug: string = '') {
if (this.DEBUG) {
Alert.alert(title, message + '\n\n' + debug);
}
else {
Alert.alert(title, message);
}
}
log(message: string, severity: IapticSeverity = IapticSeverity.INFO) {
console.log(`${new Date().toISOString()} [${severity}] ${message}`);
}
/**
* Creates a new AppService instance
* @param setEntitlements - Callback function to update UI with current entitlements
* This function is called whenever entitlements change
*/
constructor(private setEntitlements: (entitlements: string[]) => void) {
}
}
Constructor: The setEntitlement function passed to the constructor will let our AppService update the list of entitlements stored in a React state object.
- Constructor: Takes a callback function to update entitlements in the app's UI
- showAlert(): Displays alerts when needed
- log(): Helper function for logging with severity levels using emojis for visual clarity
Add the following code to your App.tsx file within your main component function:
import React, { useEffect, useState, useRef } from 'react';
import { StyleSheet, SafeAreaView, TouchableOpacity, Text, ScrollView } from 'react-native';
// Add this import
import { AppService } from './src/AppService';
// Create a stable reference to the iapServiceInstance outside the component
let iapServiceInstance: AppService | null = null;
function App(): React.JSX.Element {
// Our interface will reflect the entitlements of the user, so we store them in the state
const [entitlements, setEntitlements] = useState<string[]>([]);
// Initialize the iapService, containing our app logic
const iapService = useRef<AppService>(
iapServiceInstance || (iapServiceInstance = new AppService(setEntitlements))
).current;
return (
// ...
What This Code Does:
-
const [entitlements, setEntitlements] = useState<string[]>([]):
- Creates a state variable to track user's purchase entitlements (e.g., 'premium', 'pro', 'basic')
- These entitlements will determine which features the user can access
- The state variable is used to refresh the UI when entitlements change
- In large apps, this would be part of your app state.
-
const iapService = useRef<AppService>(...).current:
- Creates a singleton instance of
AppService
that persists between renders - Passes the
setEntitlements
function so the service can update the UI when purchases change - Uses singleton pattern with
iapServiceInstance
to ensure only one IAP service exists
- Creates a singleton instance of
Now we'll add a function to initialize the IAP system at startup.
export class AppService {
// ... existing code ...
/**
* Called when the app starts up.
* Initializes the IAP system and sets up event listeners
*
* @returns a destructor function that should be called when the app is closed
* to properly clean up IAP resources
*/
onAppStartup() {
this.log('onAppStartup');
this.initializeIaptic();
// Return cleanup function that React will call when component unmounts
return () => {
IapticRN.destroy();
}
}
async initializeIaptic() {}
}
onAppStartup()
will initialize the IAP system and provide cleanup when the app closes.
To make sure this is called at startup, we'll add this to our App.tsx file:
function App(): React.JSX.Element {
// ... existing code ...
// const iapService = ...;
// One-time initialization with proper cleanup
useEffect(() => iapService.onAppStartup(), []);
// ...
What this code does:
- Initializes the IAP service when the app starts
- The empty dependency array
[]
ensures this only runs once - Handles connecting to the app store and loading existing purchases
In our AppService class, let's implement the initializeIaptic()
function.
async initializeIaptic() {
try {
// Set up listener for subscription changes (renewals, cancellations, etc.)
IapticRN.addEventListener('subscription.updated', (reason, purchase) => {
this.log('π Subscription updated: ' + reason + ' for ' + JSON.stringify(purchase));
// Update UI with new entitlements after subscription change
this.setEntitlements(IapticRN.listEntitlements());
});
// Initialize IAP system with app-specific configuration
await IapticRN.initialize(Config.iaptic);
// Set a username to associate purchases with this user
// This will associate purchases with this user id
IapticRN.setApplicationUsername('iaptic-rn-demo-user');
// Update UI with initial entitlements (if any existing purchases)
this.setEntitlements(IapticRN.listEntitlements());
}
catch (err: any) {
this.log('Error initializing iaptic: ' + err.message);
if (err instanceof IapticError) {
this.showAlert('Error', err.localizedMessage, err.debugMessage);
}
else {
this.showAlert('Error', err.message);
}
}
}
What this code does:
- Configures event listeners for subscription changes with detailed logging
- Logs subscription updates with purchase information
- Initializes the IAP system with the configuration from Config.ts
- Sets a username for tracking purchases across sessions
- Updates the UI with current entitlements using the callback function
- Implements comprehensive error handling with type checking
- Shows user-friendly alerts with localized messages for different error types
Here's the complete AppService class:
import { Platform, Alert, ToastAndroid } from 'react-native';
import { IapticError, IapticSeverity, IapticOffer, IapticRN } from 'react-native-iaptic';
import { Config } from './Config';
/**
* AppService - Core service for managing in-app purchases
*
* This class handles the initialization of the IAP system,
* tracks subscription states, and provides methods for
* checking user entitlements and handling purchases.
*/
export class AppService {
DEBUG = true;
showAlert(title: string, message: string, debug: string = '') {
if (this.DEBUG) {
Alert.alert(title, message + '\n\n' + debug);
}
else {
Alert.alert(title, message);
}
}
log(message: string, severity: IapticSeverity = IapticSeverity.INFO) {
const SYMBOLS = ['π‘', 'π', 'β'];
console.log(`${new Date().toISOString()} ${SYMBOLS[severity]} ${message}`);
}
/**
* Creates a new AppService instance
* @param setEntitlements - Callback function to update UI with current entitlements
* This function is called whenever entitlements change
*/
constructor(private setEntitlements: (entitlements: string[]) => void) {
}
/**
* Called when the app starts up.
* Initializes the IAP system and sets up event listeners
*
* @returns a destructor function that should be called when the app is closed
* to properly clean up IAP resources
*/
onAppStartup() {
this.log('onAppStartup');
this.initializeIaptic();
// Return cleanup function that React will call when component unmounts
return () => {
IapticRN.destroy();
}
}
async initializeIaptic() {
try {
// Set up listener for subscription changes (renewals, cancellations, etc.)
IapticRN.addEventListener('subscription.updated', (reason, purchase) => {
this.log('π Subscription updated: ' + reason + ' for ' + JSON.stringify(purchase));
// Update UI with new entitlements after subscription change
this.setEntitlements(IapticRN.listEntitlements());
});
// Initialize IAP system with app-specific configuration
await IapticRN.initialize(Config.iaptic);
// Set a username to associate purchases with this user
// This helps with purchase restoration and user identification
IapticRN.setApplicationUsername('iaptic-rn-demo-user');
// Update UI with initial entitlements (if any existing purchases)
this.setEntitlements(IapticRN.listEntitlements());
}
catch (err: any) {
this.log('Error initializing iaptic: ' + err.message);
if (err instanceof IapticError) {
this.showAlert('Error', err.localizedMessage, err.debugMessage);
}
else {
this.showAlert('Error', err.message);
}
}
}
}
Here's the complete App.tsx class:
import React, { useEffect, useState, useRef } from 'react';
import { StyleSheet, SafeAreaView, TouchableOpacity, Text, ScrollView } from 'react-native';
import { AppService } from './src/AppService';
// Create stable references outside component
let iapServiceInstance: AppService | null = null;
function App(): React.JSX.Element {
// Our interface will reflect the entitlements of the user, so we store them in the state
const [entitlements, setEntitlements] = useState<string[]>([]);
// Initialize the iapService, containing our app logic
const iapService = useRef(
iapServiceInstance || (iapServiceInstance = new AppService(setEntitlements))
).current;
// One-time initialization with proper cleanup
useEffect(() => iapService.onAppStartup(), []);
return (
<SafeAreaView style={styles.container}>
<ScrollView style={styles.scrollView} contentContainerStyle={styles.productsContainer}>
<Text style={styles.subscriptionText}>Subscription</Text>
{/* A feature that will only be available if the user has any subscription */}
{/* Currently non-functional - will be connected to IAP service in later steps */}
<TouchableOpacity
onPress={() => console.log('Basic access button pressed')}
style={styles.button}
>
<Text style={styles.buttonText}>Basic Access: Locked</Text>
</TouchableOpacity>
{/* A feature that will only be available if the user has a premium subscription */}
{/* Currently non-functional - will be connected to IAP service in later steps */}
<TouchableOpacity
onPress={() => console.log('Premium access button pressed')}
style={styles.button}
>
<Text style={styles.buttonText}>Premium Access: Locked</Text>
</TouchableOpacity>
{/* Subscription management button */}
{/* Currently non-functional - will be connected to IAP service in later steps */}
<TouchableOpacity
style={[
styles.button,
styles.subscriptionButton,
]}
onPress={() => console.log('Subscribe button pressed')}
>
<Text style={styles.buttonText}>Subscribe To Unlock</Text>
</TouchableOpacity>
</ScrollView>
{/* In a later step, we'll add the IapticSubscriptionView component here */}
</SafeAreaView>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
scrollView: {
flex: 1,
},
productsContainer: {
padding: 10,
gap: 10,
paddingBottom: 20,
},
button: {
backgroundColor: '#007AFF',
padding: 15,
borderRadius: 8,
alignItems: 'center',
},
buttonDisabled: {
opacity: 0.6,
},
buttonText: {
color: 'white',
fontSize: 16,
},
restoreText: {
textAlign: 'center',
marginTop: 20,
marginBottom: 10,
color: '#666',
},
subscriptionButton: {
marginTop: 20,
backgroundColor: '#5856D6',
},
subscriptionText: {
fontSize: 18,
fontWeight: 'bold',
marginBottom: 10,
color: '#333',
},
});
export default App;
Note: This code requires the Config.ts
file with your IAP configuration, which we created in the previous step.
At this point you can run the app on your device. It should initialize the In-app purchase service through loading the products and purchases. But for now you can not make new purchases.
This step helps make sure that the service and configuration are well set up before going further.
Now that we have created our service layer and understood how to integrate it with our App component, in the next step we'll complete the UI integration by updating our feature buttons to use the AppService methods for checking entitlements and showing the subscription view.
β Previous: Step 3 - Configuration | Next: Step 5 - Managing Entitlements β