Skip to main content

Lifecycle

Understanding the lifecycle of in-app purchase connections and how to properly manage them is crucial for a robust implementation.

Lifecycle Diagram

Lifecycle Overview

The diagram above illustrates the complete lifecycle of in-app purchases in expo-iap:

  1. Initialization: Connection to the store is established
  2. Product Fetching: Available products are retrieved from the store
  3. Purchase Request: User initiates a purchase
  4. Purchase Processing: The store processes the payment
  5. Purchase Update: Your app receives purchase notifications
  6. Validation: Receipt validation (should be done server-side)
  7. Transaction Finishing: Complete the transaction to finalize the purchase
  8. Connection Management: Proper cleanup when needed

Connection Management with useIAP

The useIAP hook automatically manages the connection lifecycle for you, but it's important to understand what happens under the hood.

Automatic Connection

When you use the useIAP hook, it automatically:

  1. Initializes the connection to the store
  2. Sets up purchase listeners
  3. Manages connection state
  4. Cleans up when the component unmounts
import {useIAP} from 'expo-iap';

export default function App() {
const {connected, products, getProducts} = useIAP();

useEffect(() => {
// Connection is automatically established
if (connected) {
console.log('Connected to store');
// You can now safely call store methods
getProducts({skus: ['product1', 'product2']});
}
}, [connected, getProducts]);

return <YourAppContent />;
}

Connection States

The connection can be in several states:

  • Disconnected: Initial state, no connection to store
  • Connecting: Attempting to establish connection
  • Connected: Successfully connected, ready for operations
  • Error: Connection failed
const {connected, connectionError} = useIAP();

if (connectionError) {
return <ErrorView error={connectionError} />;
}

if (!connected) {
return <LoadingView message="Connecting to store..." />;
}

return <StoreView />;

Manual Connection Management

If you need more control over the connection lifecycle, you can use the low-level methods:

import {initConnection, endConnection} from 'expo-iap';

class StoreManager {
async initialize() {
try {
await initConnection();
console.log('Store connection initialized');

// Set up purchase listeners
this.setupPurchaseListeners();
} catch (error) {
console.error('Failed to initialize store connection:', error);
}
}

async cleanup() {
try {
// Remove listeners
this.removePurchaseListeners();

// End connection
await endConnection();
console.log('Store connection ended');
} catch (error) {
console.error('Failed to end store connection:', error);
}
}

setupPurchaseListeners() {
// Set up purchase update and error listeners
}

removePurchaseListeners() {
// Remove listeners to prevent memory leaks
}
}

Component Lifecycle Integration

Class Components

import React, {Component} from 'react';
import {
initConnection,
endConnection,
purchaseUpdatedListener,
purchaseErrorListener,
} from 'expo-iap';

class StoreComponent extends Component {
purchaseUpdateSubscription = null;
purchaseErrorSubscription = null;

async componentDidMount() {
try {
await initConnection();

// Set up purchase listeners
this.purchaseUpdateSubscription = purchaseUpdatedListener((purchase) => {
this.handlePurchaseUpdate(purchase);
});

this.purchaseErrorSubscription = purchaseErrorListener((error) => {
this.handlePurchaseError(error);
});
} catch (error) {
console.error('Failed to initialize:', error);
}
}

componentWillUnmount() {
// Clean up listeners
if (this.purchaseUpdateSubscription) {
this.purchaseUpdateSubscription.remove();
}

if (this.purchaseErrorSubscription) {
this.purchaseErrorSubscription.remove();
}

// End connection
endConnection();
}

handlePurchaseUpdate = (purchase) => {
// Handle purchase updates
};

handlePurchaseError = (error) => {
// Handle purchase errors
};

render() {
return <div>Your store UI</div>;
}
}

Functional Components

import React, {useEffect, useRef} from 'react';
import {
initConnection,
endConnection,
purchaseUpdatedListener,
purchaseErrorListener,
} from 'expo-iap';

export default function StoreComponent() {
const listenersRef = useRef([]);

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

// Set up listeners
const purchaseUpdateSubscription = purchaseUpdatedListener(
(purchase) => {
handlePurchaseUpdate(purchase);
},
);

const purchaseErrorSubscription = purchaseErrorListener((error) => {
handlePurchaseError(error);
});

// Store references for cleanup
listenersRef.current = [
purchaseUpdateSubscription,
purchaseErrorSubscription,
];
} catch (error) {
console.error('Failed to setup store:', error);
}
};

setupStore();

// Cleanup function
return () => {
// Remove listeners
listenersRef.current.forEach((subscription) => {
if (subscription && subscription.remove) {
subscription.remove();
}
});

// End connection
endConnection();
};
}, []);

const handlePurchaseUpdate = (purchase) => {
// Handle purchase updates
};

const handlePurchaseError = (error) => {
// Handle purchase errors
};

return <div>Your store UI</div>;
}

Best Practices

✅ Do:

  1. Use useIAP hook: Simplifies lifecycle management
  2. Initialize early: Connect to store as early as possible in app lifecycle
  3. Handle connection states: Provide feedback to users about connection status
  4. Clean up properly: Always remove listeners and end connections
// ✅ Good: Using useIAP hook
function MyApp() {
const {connected, products, getProducts} = useIAP();

useEffect(() => {
if (connected) {
getProducts({skus: productIds});
}
}, [connected]);

return <AppContent />;
}

❌ Don't:

  1. Initialize and end repeatedly: Don't call initConnection/endConnection for every operation
  2. Ignore connection state: Don't attempt store operations when disconnected
  3. Forget cleanup: Always clean up listeners to prevent memory leaks
// ❌ Bad: Initializing for every operation
const badPurchaseFlow = async (productId) => {
await initConnection(); // Don't do this
await requestPurchase({sku: productId});
await endConnection(); // Don't do this
};

// ✅ Good: Use existing connection
const goodPurchaseFlow = async (productId) => {
if (connected) {
await requestPurchase({sku: productId});
}
};

Purchase Flow Best Practices

Receipt Validation and Security

  1. Server-side receipt validation is recommended: For production apps, it's highly recommended to validate receipts on your secure server before granting access to content or features. See Apple's receipt validation guide and Google Play's verification guide.

  2. Finish transactions after validation: Always call finishTransaction after successfully validating a purchase on your server. Failing to do so will cause the purchase to remain in a pending state and may trigger repeated purchase prompts.

  3. Never trust client-side data: Always validate receipts server-side before granting premium content or features.

Purchase State Management

  1. Handle all purchase states: Including pending, failed, restored, and cancelled purchases. Each state requires different handling logic.

  2. Handle pending purchases: Some purchases may require approval (e.g., parental consent) and remain in pending state for extended periods.

  3. Restore purchases properly: Implement purchase restoration for non-consumable products and subscriptions. This is required by app store guidelines.

Error Handling and User Experience

  1. Implement comprehensive error handling: Provide meaningful feedback to users for different error scenarios including network errors, cancelled purchases, and validation failures.

  2. Graceful degradation: Your app should work even if purchases fail or the store is unavailable. Don't block core functionality.

  3. User feedback: Keep users informed about purchase status with appropriate loading states and success/error messages.

Testing and Development

  1. Test thoroughly: Use real devices and official test accounts. In-app purchases don't work in simulators/emulators.

  2. Monitor purchase flow: Log important events for debugging, but never log sensitive information like receipts or tokens.

  3. Check server-side validation libraries: Consider using open-source libraries like node-app-store-receipt-verify for iOS or google-play-billing-validator for Android.

Common Pitfalls and Solutions

Transaction Management Issues

Not finishing transactions:

// Wrong - forgetting to finish transaction
const handlePurchase = async (purchase) => {
await validateReceipt(purchase);
// Missing: finishTransaction call
};

Always finish transactions after validation:

// Correct - always finish transaction
const handlePurchase = async (purchase) => {
const isValid = await validateReceipt(purchase);
if (isValid) {
await finishTransaction({purchase, isConsumable: true});
}
};

Security Issues

Trusting client-side validation:

// Wrong - never trust client-side validation alone
const handlePurchase = async (purchase) => {
// This is not secure for production
grantPremiumFeature();
};

Always validate server-side:

// Correct - validate on secure server
const handlePurchase = async (purchase) => {
const isValid = await yourAPI.validateReceipt(purchase.transactionReceipt);
if (isValid) {
grantPremiumFeature();
await finishTransaction({purchase});
}
};

Development and Testing Issues

Testing on simulators: In-app purchases only work on real devices with proper app store configurations.

Ignoring error codes: Different errors require different handling strategies.

Proper error handling:

// Correct - handle different error types appropriately
const handlePurchaseError = (error) => {
switch (error.code) {
case 'E_USER_CANCELLED':
// Silent - user intentionally cancelled
break;
case 'E_NETWORK_ERROR':
showRetryDialog();
break;
case 'E_ITEM_UNAVAILABLE':
showUnavailableMessage();
break;
default:
showGenericErrorMessage();
break;
}
};

App Lifecycle Issues

Not handling app crashes: Purchases can complete after app restart, so always check for pending purchases on app launch.

Handle background purchases:

// Correct - check for purchases on app launch
useEffect(() => {
const checkPendingPurchases = async () => {
if (connected) {
// Check for any purchases that completed while app was closed
const purchases = await getAvailablePurchases();
for (const purchase of purchases) {
await processPurchase(purchase);
}
}
};

checkPendingPurchases();
}, [connected]);

Connection Management Issues

Initializing connection repeatedly:

// Wrong - don't initialize for every operation
const purchaseProduct = async (sku) => {
await initConnection(); // Don't do this
await requestPurchase({sku});
await endConnection(); // Don't do this
};

Maintain single connection:

// Correct - use existing connection
const purchaseProduct = async (sku) => {
if (connected) {
await requestPurchase({sku});
} else {
console.error('Store not connected');
}
};

Next Steps