aptoide_iap 0.0.1
aptoide_iap: ^0.0.1 copied to clipboard
A Flutter plugin for Aptoide IAP on iOS. Wraps the AppCoins SDK for in-app purchases on iOS 18.0+ devices in the EU.
Aptoide IAP - Flutter Plugin #
Flutter plugin for Aptoide In-App Purchases on iOS. Seamlessly integrate with Aptoide Connect billing system for iOS 17.4+ devices in the EU.
๐ Table of Contents #
- Features
- Requirements
- Installation
- Quick Start
- Environment Configuration
- Complete API Reference
- Purchase Flow
- Error Handling
- Testing
- Best Practices
- Troubleshooting
- Documentation
- Example App
โจ Features #
- โ Simple API - Clean, intuitive methods for all IAP operations
- โ Automatic Purchase Recovery - Handles interrupted purchases automatically
- โ Built-in Verification - Purchase verification included out of the box
- โ Environment Switching - Toggle sandbox/production without rebuilding
- โ Comprehensive Logging - Emoji-prefixed logs for easy debugging
- โ Type Safety - Full Dart type safety with enums and typed returns
- โ Error Handling - Detailed error codes and messages
๐ฑ Requirements #
โ ๏ธ IMPORTANT: iOS 18.0+ is required due to AppCoins SDK 4.x using
TransactionReportingAPIs
| Requirement | Details |
|---|---|
| iOS Version | 18.0 or later (AppCoins SDK 4.x requirement) |
| Region | EU (European Union) |
| Distribution | App must be distributed via Aptoide (not App Store) |
| Testing | Physical iOS device required (simulator not supported) |
| Xcode | Latest version recommended (Xcode 16+) |
| Flutter | 3.3.0 or later |
โ ๏ธ Critical Notes:
- iOS 18.0+ is mandatory - AppCoins SDK 4.x uses
TransactionReportingAPIs - Only works on devices in the EU region
- App must be distributed through Aptoide (not App Store)
- Physical device required for testing (simulator not supported)
๐ Build Issues? See IOS_18_REQUIREMENT.md for troubleshooting.
๐ฆ Installation #
1. Add Dependency #
Add to your pubspec.yaml:
dependencies:
aptoide_iap: ^0.0.2
Then run:
flutter pub get
2. iOS Native Setup (Required) #
Complete iOS setup is required before using the plugin. Follow these steps:
Step 2.1: Add AppCoins SDK
Open your iOS project in Xcode and add the AppCoins SDK via Swift Package Manager:
- In Xcode:
FileโAdd Package Dependencies - Enter URL:
https://github.com/Catappult/appcoins-sdk-ios.git - Select version:
1.0.0or later - Add to your app target
Step 2.2: Configure Entitlements
Add keychain entitlement to ios/Runner/Runner.entitlements:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>keychain-access-groups</key>
<array>
<string>$(AppIdentifierPrefix)com.aptoide.appcoins</string>
</array>
</dict>
</plist>
Step 2.3: Update AppDelegate
Modify ios/Runner/AppDelegate.swift:
import UIKit
import Flutter
import AppCoinsSDK // Add this import
@main
@objc class AppDelegate: FlutterAppDelegate {
override func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Initialize AppCoins SDK
AppcSDK.initialize()
GeneratedPluginRegistrant.register(with: self)
return super.application(application, didFinishLaunchingWithOptions: launchOptions)
}
// Handle deep links for payment redirects
override func application(
_ app: UIApplication,
open url: URL,
options: [UIApplication.OpenURLOptionsKey : Any] = [:]
) -> Bool {
if AppcSDK.handle(redirectURL: url) {
return true
}
return super.application(app, open: url, options: options)
}
}
Step 2.4: Configure Info.plist
Add to ios/Runner/Info.plist:
<!-- URL Scheme for payment redirects -->
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleTypeRole</key>
<string>Editor</string>
<key>CFBundleURLSchemes</key>
<array>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
</array>
</dict>
</array>
<!-- Digital Goods Flag (Required for Aptoide) -->
<key>APDigitalGoodsEnabled</key>
<true/>
Step 2.5: Xcode Build Settings
For testing, configure Xcode:
- Build Settings โ Search "Marketplaces" โ Set to:
com.aptoide.ios.store - Edit Scheme โ Run โ Options โ Distribution:
com.aptoide.ios.store
๐ Detailed Setup: See SETUP_GUIDE.md for complete instructions with screenshots.
๐ Quick Start #
Minimal Example #
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:aptoide_iap/aptoide_iap.dart';
import 'package:aptoide_iap/aptoide_environment.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Initialize IAP
await AptoideIap.initialize(
environment: AptoideEnvironment.sandbox, // Use sandbox for testing
onUnfinishedPurchase: (purchase) async {
// Grant item to user
await grantItemToUser(purchase['sku']);
// Mark as finished
await AptoideIap.finishPurchase(purchase['id']);
},
);
runApp(MyApp());
}
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
home: PurchaseScreen(),
);
}
}
class PurchaseScreen extends StatefulWidget {
@override
_PurchaseScreenState createState() => _PurchaseScreenState();
}
class _PurchaseScreenState extends State<PurchaseScreen> {
List<Map<String, dynamic>> _products = [];
@override
void initState() {
super.initState();
_loadProducts();
}
Future<void> _loadProducts() async {
// Check if IAP is available
final available = await AptoideIap.isAvailable;
if (!available) {
print('IAP not available on this device');
return;
}
// Query products
final products = await AptoideIap.queryProducts([
'premium_monthly',
'coins_100',
'remove_ads',
]);
setState(() => _products = products);
}
Future<void> _purchaseProduct(String sku) async {
try {
final purchase = await AptoideIap.purchase(sku);
if (purchase['verified'] == true) {
// Purchase verified - grant item
await grantItemToUser(sku);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Purchase successful!')),
);
} else {
// Unverified - handle based on your policy
print('Purchase unverified: ${purchase['verificationError']}');
}
} on PlatformException catch (e) {
if (e.code == 'CANCELLED') {
print('User cancelled purchase');
} else {
print('Purchase failed: ${e.message}');
}
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text('In-App Purchases')),
body: ListView.builder(
itemCount: _products.length,
itemBuilder: (context, index) {
final product = _products[index];
return ListTile(
title: Text(product['title']),
subtitle: Text(product['description']),
trailing: ElevatedButton(
onPressed: () => _purchaseProduct(product['id']),
child: Text(product['priceLabel']),
),
);
},
),
);
}
}
// Your backend/database logic
Future<void> grantItemToUser(String sku) async {
// Grant the purchased item to the user
// Update your database, unlock features, etc.
print('Granting item: $sku');
}
๐ Environment Configuration #
Sandbox vs Production #
| Feature | ๐งช Sandbox | ๐ Production |
|---|---|---|
| Purpose | Testing | Live transactions |
| Charges | No real money | Real money |
| Accounts | Test accounts | Live users |
| Dashboard | Sandbox mode enabled | Production mode |
| Refunds | Automatic | Manual process |
Setting Environment #
Option 1: During Initialization (Recommended)
// For testing
await AptoideIap.initialize(
environment: AptoideEnvironment.sandbox,
onUnfinishedPurchase: handleUnfinished,
);
// For production
await AptoideIap.initialize(
environment: AptoideEnvironment.production,
onUnfinishedPurchase: handleUnfinished,
);
Option 2: Runtime Switching
// Switch to sandbox
await AptoideIap.setEnvironment(AptoideEnvironment.sandbox);
// Switch to production
await AptoideIap.setEnvironment(AptoideEnvironment.production);
โ ๏ธ Important: Always set environment BEFORE any SDK operations (products, purchases, etc.)
Option 3: Build-Based Configuration
import 'package:flutter/foundation.dart';
Future<void> initializeIAP() async {
// Automatically use sandbox in debug, production in release
final environment = kDebugMode
? AptoideEnvironment.sandbox
: AptoideEnvironment.production;
await AptoideIap.initialize(
environment: environment,
onUnfinishedPurchase: handleUnfinished,
);
}
Option 4: Environment Variables
// Run with: flutter run --dart-define=IAP_ENV=sandbox
const envString = String.fromEnvironment('IAP_ENV', defaultValue: 'production');
final environment = envString == 'sandbox'
? AptoideEnvironment.sandbox
: AptoideEnvironment.production;
await AptoideIap.initialize(environment: environment);
๐ Detailed Guide: See ENVIRONMENT_SWITCHING.md for complete documentation.
๐ Complete API Reference #
1. Initialize #
Must be called at app startup. Handles unfinished purchases automatically.
static Future<void> initialize({
AptoideEnvironment environment = AptoideEnvironment.production,
UnfinishedPurchaseCallback? onUnfinishedPurchase,
})
Parameters:
environment- SDK environment (sandbox or production). Default: productiononUnfinishedPurchase- Callback for handling interrupted purchases
Example:
await AptoideIap.initialize(
environment: AptoideEnvironment.sandbox,
onUnfinishedPurchase: (purchase) async {
print('Restoring: ${purchase['sku']}');
await grantItemToUser(purchase['sku']);
await AptoideIap.finishPurchase(purchase['id']);
},
);
2. Set Environment #
Switch between sandbox and production environments.
static Future<void> setEnvironment(AptoideEnvironment environment)
Example:
await AptoideIap.setEnvironment(AptoideEnvironment.sandbox);
โ ๏ธ Call before any SDK operations or use in initialize().
3. Check Availability #
Check if Aptoide IAP is available on the device.
static Future<bool> get isAvailable
Returns: true if SDK is available (iOS 17.4+ in EU with Aptoide distribution)
Example:
final available = await AptoideIap.isAvailable;
if (!available) {
print('IAP not available on this device');
return;
}
4. Query Products #
Fetch product details by SKU identifiers.
static Future<List<Map<String, dynamic>>> queryProducts(List<String> skus)
Parameters:
skus- List of product SKU identifiers
Returns: List of product maps with:
id(String) - Product SKUtitle(String) - Product titledescription(String) - Product descriptionprice(double) - Price valuecurrency(String) - Currency code (e.g., "EUR")priceLabel(String) - Formatted price (e.g., "โฌ9.99")
Example:
final products = await AptoideIap.queryProducts([
'premium_monthly',
'coins_100',
'remove_ads',
]);
for (final product in products) {
print('${product['title']}: ${product['priceLabel']}');
}
5. Purchase Product #
Initiate a purchase for a product.
static Future<Map<String, dynamic>> purchase(
String sku,
{String? payload}
)
Parameters:
sku- Product SKU identifierpayload- Optional custom data (e.g., user ID for backend verification)
Returns: Purchase map with:
id(String) - Purchase IDsku(String) - Product SKUstate(String) - Purchase statepayload(String) - Custom payloadorderUid(String) - Order unique IDcreated(String) - Creation timestampverified(bool) - Verification statusverificationError(String?) - Error if unverified
Throws: PlatformException with codes:
CANCELLED- User cancelled the purchasePENDING- Purchase is pendingFAILED- Purchase failed
Example:
try {
final purchase = await AptoideIap.purchase(
'premium_monthly',
payload: 'user_12345',
);
if (purchase['verified'] == true) {
await grantItemToUser(purchase['sku']);
print('Purchase successful!');
} else {
print('Unverified: ${purchase['verificationError']}');
}
} on PlatformException catch (e) {
switch (e.code) {
case 'CANCELLED':
print('User cancelled');
break;
case 'PENDING':
print('Payment pending');
break;
case 'FAILED':
print('Purchase failed: ${e.message}');
break;
}
}
6. Finish Purchase #
Mark a purchase as completed after granting the item to the user.
static Future<void> finishPurchase(String purchaseId)
Parameters:
purchaseId- The purchase ID from the purchase map
โ ๏ธ CRITICAL: Must be called after granting items. Purchases auto-refund after 24 hours if not finished.
Example:
final purchase = await AptoideIap.purchase('coins_100');
await grantItemToUser(purchase['sku']);
await AptoideIap.finishPurchase(purchase['id']); // Must call this!
7. Get Unfinished Purchases #
Retrieve all purchases that haven't been finished yet.
static Future<List<Map<String, dynamic>>> getUnfinishedPurchases()
Returns: List of purchase maps (same structure as purchase())
Example:
final unfinished = await AptoideIap.getUnfinishedPurchases();
for (final purchase in unfinished) {
await grantItemToUser(purchase['sku']);
await AptoideIap.finishPurchase(purchase['id']);
}
8. Handle Deep Links #
Handle payment redirect URLs (usually automatic).
static Future<bool> handleDeepLink(String url)
Returns: true if URL was handled by SDK
Example:
final handled = await AptoideIap.handleDeepLink(url);
9. Logging Control #
Enable or disable debug logging.
static void setLoggingEnabled(bool enabled)
Example:
// Enable for development
AptoideIap.setLoggingEnabled(true);
// Disable for production
AptoideIap.setLoggingEnabled(false);
Logs use emoji prefixes for easy scanning:
- ๐ Initialization
- ๐ Queries
- ๐ณ Purchases
- โ Success
- โ Errors
- ๐ฆ Products
- ๐งช Sandbox mode
- ๐ Production mode
๐ Purchase Flow #
Complete Purchase Flow Diagram #
1. App Startup
โโโ AptoideIap.initialize()
โโโ Check for unfinished purchases
โโโ Restore any interrupted purchases
2. User Browses Products
โโโ AptoideIap.isAvailable (check availability)
โโโ AptoideIap.queryProducts() (load products)
3. User Initiates Purchase
โโโ AptoideIap.purchase(sku)
โโโ User completes payment
โโโ SDK returns purchase result
4. Verify Purchase
โโโ Check purchase['verified'] == true
โโโ If verified: Grant item to user
โโโ If unverified: Handle based on policy
5. Finish Purchase
โโโ grantItemToUser(sku)
โโโ AptoideIap.finishPurchase(purchaseId) โ ๏ธ CRITICAL
6. Handle Errors
โโโ CANCELLED: User cancelled
โโโ PENDING: Payment processing
โโโ FAILED: Show error message
Implementation Example #
Future<void> completePurchaseFlow(String sku) async {
try {
// Step 1: Initiate purchase
print('Starting purchase for: $sku');
final purchase = await AptoideIap.purchase(sku, payload: 'user_123');
// Step 2: Verify purchase
if (purchase['verified'] != true) {
print('โ ๏ธ Purchase unverified: ${purchase['verificationError']}');
// Decide: grant anyway (risky) or reject (safe)
return;
}
// Step 3: Grant item to user
print('โ
Purchase verified, granting item...');
await grantItemToUser(sku);
// Step 4: Finish purchase (CRITICAL!)
await AptoideIap.finishPurchase(purchase['id']);
print('โ
Purchase completed successfully');
// Step 5: Update UI
showSuccessDialog();
} on PlatformException catch (e) {
// Handle errors
switch (e.code) {
case 'CANCELLED':
print('User cancelled purchase');
break;
case 'PENDING':
print('Payment is pending');
showPendingDialog();
break;
case 'FAILED':
print('Purchase failed: ${e.message}');
showErrorDialog(e.message);
break;
default:
print('Unknown error: ${e.code}');
}
} catch (e) {
print('Unexpected error: $e');
}
}
โ ๏ธ Error Handling #
Error Codes #
| Code | Meaning | Action |
|---|---|---|
CANCELLED |
User cancelled purchase | Normal flow, no action needed |
PENDING |
Payment processing | Wait for completion, show pending UI |
FAILED |
Purchase failed | Show error, allow retry |
INVALID_ARGS |
Invalid parameters | Check your code |
PRODUCT_NOT_FOUND |
Product doesn't exist | Verify SKU in dashboard |
PURCHASE_NOT_FOUND |
Purchase ID invalid | Check purchase ID |
SDK_NOT_AVAILABLE |
SDK not configured | Complete iOS setup |
SDK_ERROR |
SDK internal error | Check logs, contact support |
Comprehensive Error Handling #
Future<void> safePurchase(String sku) async {
try {
// Check availability first
if (!await AptoideIap.isAvailable) {
showError('IAP not available on this device');
return;
}
// Attempt purchase
final purchase = await AptoideIap.purchase(sku);
// Verify
if (purchase['verified'] == true) {
await grantItemToUser(sku);
await AptoideIap.finishPurchase(purchase['id']);
showSuccess('Purchase completed!');
} else {
showWarning('Purchase could not be verified');
}
} on PlatformException catch (e) {
// Handle specific errors
switch (e.code) {
case 'CANCELLED':
// User cancelled - no error message needed
break;
case 'PENDING':
showInfo('Payment is being processed');
break;
case 'FAILED':
showError('Purchase failed: ${e.message ?? "Unknown error"}');
break;
case 'PRODUCT_NOT_FOUND':
showError('Product not found. Please try again later.');
break;
case 'SDK_NOT_AVAILABLE':
showError('In-app purchases are not available');
break;
default:
showError('An error occurred: ${e.message}');
}
} catch (e) {
// Catch any unexpected errors
showError('Unexpected error: $e');
}
}
๐งช Testing #
Testing Checklist #
- โ Complete iOS native setup
- โ Configure Xcode build settings for Aptoide
- โ Enable sandbox mode in Aptoide Connect dashboard
- โ Use sandbox environment in app
- โ Test on physical iOS 17.4+ device in EU
- โ Test purchase flow
- โ Test cancellation
- โ Test unfinished purchase recovery
- โ Test environment switching
- โ Switch to production before release
Xcode Configuration for Testing #
-
Build Settings:
- Search for "Marketplaces"
- Set value to:
com.aptoide.ios.store
-
Scheme Configuration:
- Product โ Scheme โ Edit Scheme
- Run โ Options tab
- Distribution:
com.aptoide.ios.store
-
Build and Run:
flutter run --release
Aptoide Connect Dashboard #
- Log in to Aptoide Connect
- Navigate to your app
- Go to In-App Products section
- Enable Sandbox Mode for testing
- Create test products with SKUs matching your code
Testing Sandbox Purchases #
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Use sandbox for testing
await AptoideIap.initialize(
environment: AptoideEnvironment.sandbox,
onUnfinishedPurchase: handleUnfinished,
);
runApp(MyApp());
}
// Test purchase
Future<void> testPurchase() async {
try {
final purchase = await AptoideIap.purchase('test_product_sku');
print('Test purchase successful: ${purchase['sku']}');
} catch (e) {
print('Test purchase failed: $e');
}
}
Verifying Logs #
Enable logging and check for:
๐งช [AptoideIAP] Environment set to SANDBOX
๐ [AptoideIAP] Initializing Aptoide IAP...
โ
[AptoideIAP] SDK available: YES
๐๏ธ [AptoideIAP] Querying products: test_product_sku
โ
[AptoideIAP] Found 1 product(s)
๐ณ [AptoideIAP] Starting purchase: test_product_sku
โ
[AptoideIAP] Purchase verified: test_product_sku
โ Best Practices #
1. Always Initialize at Startup #
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await AptoideIap.initialize(
environment: AptoideEnvironment.sandbox,
onUnfinishedPurchase: handleUnfinished,
);
runApp(MyApp());
}
2. Always Finish Purchases #
// โ BAD - Purchase not finished
final purchase = await AptoideIap.purchase(sku);
await grantItemToUser(sku);
// Missing: finishPurchase()
// โ
GOOD - Purchase properly finished
final purchase = await AptoideIap.purchase(sku);
await grantItemToUser(sku);
await AptoideIap.finishPurchase(purchase['id']); // Critical!
3. Check Verification Status #
// โ BAD - Ignoring verification
final purchase = await AptoideIap.purchase(sku);
await grantItemToUser(sku); // Risky!
// โ
GOOD - Checking verification
final purchase = await AptoideIap.purchase(sku);
if (purchase['verified'] == true) {
await grantItemToUser(sku);
} else {
print('Unverified: ${purchase['verificationError']}');
}
4. Handle All Error Cases #
// โ
GOOD - Comprehensive error handling
try {
final purchase = await AptoideIap.purchase(sku);
// ... handle success
} on PlatformException catch (e) {
switch (e.code) {
case 'CANCELLED': // Handle cancellation
case 'PENDING': // Handle pending
case 'FAILED': // Handle failure
}
} catch (e) {
// Handle unexpected errors
}
5. Use Environment-Based Configuration #
// โ
GOOD - Automatic environment selection
import 'package:flutter/foundation.dart';
final environment = kDebugMode
? AptoideEnvironment.sandbox
: AptoideEnvironment.production;
await AptoideIap.initialize(environment: environment);
6. Disable Logging in Production #
// โ
GOOD - Conditional logging
AptoideIap.setLoggingEnabled(kDebugMode);
7. Handle Unfinished Purchases #
// โ
GOOD - Proper unfinished purchase handling
await AptoideIap.initialize(
onUnfinishedPurchase: (purchase) async {
print('Restoring: ${purchase['sku']}');
await grantItemToUser(purchase['sku']);
await AptoideIap.finishPurchase(purchase['id']);
},
);
8. Check Availability Before Operations #
// โ
GOOD - Check availability first
final available = await AptoideIap.isAvailable;
if (!available) {
showError('IAP not available');
return;
}
final products = await AptoideIap.queryProducts(skus);
๐ง Troubleshooting #
IAP Not Available #
Symptoms: isAvailable returns false
Solutions:
- โ Verify device is iOS 17.4 or later
- โ Confirm device is in EU region
- โ Ensure app is distributed via Aptoide (not App Store)
- โ
Check Xcode build settings (Marketplaces =
com.aptoide.ios.store) - โ Test on physical device (simulator not supported)
Products Not Loading #
Symptoms: queryProducts() returns empty list
Solutions:
- โ Verify product SKUs match Aptoide Connect dashboard exactly
- โ Ensure products are active in dashboard
- โ Check sandbox mode is enabled for testing
- โ Confirm AppCoins SDK is properly installed
- โ Check logs for error messages
Purchase Fails Immediately #
Symptoms: Purchase throws error without showing payment UI
Solutions:
- โ
Verify product exists:
queryProducts()first - โ Check environment is set correctly (sandbox for testing)
- โ Ensure deep link handling is configured in AppDelegate
- โ Verify URL scheme in Info.plist
- โ Check logs for specific error codes
Unfinished Purchases Not Restored #
Symptoms: Purchases not appearing in getUnfinishedPurchases()
Solutions:
- โ
Ensure
initialize()is called withonUnfinishedPurchasecallback - โ Check purchases were not already finished
- โ Verify purchases are less than 24 hours old
- โ Confirm SDK is properly initialized
Environment Not Switching #
Symptoms: Environment stays in production/sandbox
Solutions:
- โ
Call
setEnvironment()BEFORE any SDK operations - โ
Use environment parameter in
initialize() - โ Check logs for environment change confirmation
- โ Restart app after environment change
Build Errors #
Symptoms: Xcode build fails
Common Errors:
-
"Cannot find 'TransactionReporting' in scope"
- Cause: AppCoins SDK 4.x requires iOS 18.0+
- Solution:
- Ensure deployment target is iOS 18.0 in Podfile and Xcode
- Clean:
flutter clean && rm -rf ios/Pods ios/Podfile.lock - Reinstall:
cd ios && pod install - Clear DerivedData:
rm -rf ~/Library/Developer/Xcode/DerivedData
-
General Build Failures
- โ Verify AppCoins SDK is added via Swift Package Manager
- โ Check entitlements file exists and is configured
- โ
Ensure
import AppCoinsSDKin AppDelegate - โ Clean build folder: Product โ Clean Build Folder
- โ Update to latest Xcode version (16+)
Common Error Messages #
| Error | Cause | Solution |
|---|---|---|
| "SDK not available" | AppCoins SDK not installed | Add SDK via Swift Package Manager |
| "Product not found" | Invalid SKU | Check SKU in dashboard |
| "Purchase not found" | Invalid purchase ID | Verify purchase ID is correct |
| "Invalid environment" | Wrong environment string | Use AptoideEnvironment enum |
๐ Detailed Troubleshooting: See TROUBLESHOOTING.md for more solutions.
๐ Documentation #
Complete Documentation Set #
| Document | Description |
|---|---|
| README.md | This file - complete overview and API reference |
| SETUP_GUIDE.md | Detailed iOS setup with screenshots |
| IOS_18_REQUIREMENT.md | iOS 18.0 requirement and build fixes |
| FIX_IOS_BUILD.md | Detailed iOS build troubleshooting |
| ENVIRONMENT_SWITCHING.md | Complete guide to sandbox/production switching |
| QUICK_REFERENCE.md | Quick reference card for common tasks |
| TROUBLESHOOTING.md | Common issues and solutions |
| CHANGELOG.md | Version history and changes |
| MIGRATION_TO_ENVIRONMENT_SWITCHING.md | Migration guide from v0.0.1 |
| ARCHITECTURE.md | Technical architecture and data flow |
| EXAMPLE_APP_CONFIG.md | Example app configuration details |
Quick Links #
- Aptoide Connect Dashboard: https://connect.aptoide.com/
- AppCoins SDK (iOS): https://github.com/Catappult/appcoins-sdk-ios
- Flutter Documentation: https://docs.flutter.dev/
- Report Issues: GitHub Issues
๐ฑ Example App #
The plugin includes a complete example app demonstrating all features.
Running the Example #
cd example
flutter run --release
โ ๏ธ Must run on physical iOS 17.4+ device in EU region.
Example App Features #
- โ Sandbox by Default - Safe testing without real charges
- โ Environment Switcher - Toggle between sandbox/production via UI
- โ Visual Indicators - Clear display of current environment
- โ Product Listing - Query and display products
- โ Purchase Flow - Complete purchase implementation
- โ Error Handling - Comprehensive error handling examples
- โ Unfinished Purchases - Automatic recovery demonstration
Example App Configuration #
The example app uses sandbox environment by default:
AptoideEnvironment _currentEnvironment = AptoideEnvironment.sandbox;
You can switch to production using the menu in the AppBar (๐งช/๐ icon).
๐ Example Details: See EXAMPLE_APP_CONFIG.md for configuration details.
๐ฏ Common Use Cases #
Use Case 1: Premium Subscription #
Future<void> purchasePremium() async {
try {
final purchase = await AptoideIap.purchase('premium_monthly');
if (purchase['verified'] == true) {
// Update user's subscription status
await updateUserSubscription(
userId: currentUser.id,
subscriptionType: 'premium',
expiryDate: DateTime.now().add(Duration(days: 30)),
);
await AptoideIap.finishPurchase(purchase['id']);
showSuccess('Premium activated!');
}
} on PlatformException catch (e) {
handlePurchaseError(e);
}
}
Use Case 2: Consumable Items (Coins/Gems) #
Future<void> purchaseCoins(String sku, int coinAmount) async {
try {
final purchase = await AptoideIap.purchase(sku);
if (purchase['verified'] == true) {
// Add coins to user's balance
await addCoinsToUser(currentUser.id, coinAmount);
// Finish purchase (allows repurchase)
await AptoideIap.finishPurchase(purchase['id']);
showSuccess('$coinAmount coins added!');
}
} on PlatformException catch (e) {
handlePurchaseError(e);
}
}
Use Case 3: Remove Ads #
Future<void> purchaseRemoveAds() async {
try {
final purchase = await AptoideIap.purchase('remove_ads');
if (purchase['verified'] == true) {
// Set user preference
await setUserPreference('ads_removed', true);
await AptoideIap.finishPurchase(purchase['id']);
// Reload app without ads
setState(() => adsEnabled = false);
showSuccess('Ads removed!');
}
} on PlatformException catch (e) {
handlePurchaseError(e);
}
}
Use Case 4: Restore Purchases on New Device #
Future<void> restorePurchases() async {
try {
final unfinished = await AptoideIap.getUnfinishedPurchases();
if (unfinished.isEmpty) {
showInfo('No purchases to restore');
return;
}
for (final purchase in unfinished) {
// Grant items based on SKU
switch (purchase['sku']) {
case 'premium_monthly':
await restorePremiumSubscription();
break;
case 'remove_ads':
await setUserPreference('ads_removed', true);
break;
default:
print('Unknown SKU: ${purchase['sku']}');
}
await AptoideIap.finishPurchase(purchase['id']);
}
showSuccess('${unfinished.length} purchase(s) restored');
} catch (e) {
showError('Failed to restore purchases: $e');
}
}
๐ Security Best Practices #
1. Server-Side Verification #
Always verify purchases on your backend:
Future<void> securePurchase(String sku) async {
final purchase = await AptoideIap.purchase(sku, payload: userId);
// Send to your backend for verification
final verified = await verifyPurchaseOnServer(
purchaseId: purchase['id'],
orderUid: purchase['orderUid'],
payload: purchase['payload'],
);
if (verified) {
await grantItemToUser(sku);
await AptoideIap.finishPurchase(purchase['id']);
}
}
2. Use Payload for User Identification #
final purchase = await AptoideIap.purchase(
sku,
payload: jsonEncode({
'userId': currentUser.id,
'timestamp': DateTime.now().toIso8601String(),
}),
);
3. Don't Store Sensitive Data Locally #
// โ BAD - Storing purchase data locally
SharedPreferences.setString('purchase_data', jsonEncode(purchase));
// โ
GOOD - Verify and sync with backend
await syncPurchaseWithBackend(purchase);
4. Disable Logging in Production #
void main() async {
// Only log in debug mode
AptoideIap.setLoggingEnabled(kDebugMode);
await AptoideIap.initialize(
environment: kDebugMode
? AptoideEnvironment.sandbox
: AptoideEnvironment.production,
);
}
5. Handle Unverified Purchases Carefully #
if (purchase['verified'] != true) {
// Option 1: Reject (safe but may lose legitimate purchases)
showError('Purchase could not be verified');
return;
// Option 2: Grant with warning (risky but user-friendly)
await grantItemToUser(sku);
await logUnverifiedPurchase(purchase);
}
๐ Analytics Integration #
Track Purchase Events #
Future<void> purchaseWithAnalytics(String sku) async {
// Track purchase start
analytics.logEvent('purchase_started', parameters: {'sku': sku});
try {
final purchase = await AptoideIap.purchase(sku);
if (purchase['verified'] == true) {
await grantItemToUser(sku);
await AptoideIap.finishPurchase(purchase['id']);
// Track successful purchase
analytics.logEvent('purchase_completed', parameters: {
'sku': sku,
'price': purchase['price'],
'currency': purchase['currency'],
});
}
} on PlatformException catch (e) {
// Track purchase failure
analytics.logEvent('purchase_failed', parameters: {
'sku': sku,
'error_code': e.code,
'error_message': e.message,
});
}
}
๐ Production Checklist #
Before releasing your app to production:
-
iOS Setup Complete
- โ AppCoins SDK installed
- โ Entitlements configured
- โ AppDelegate updated
- โ Info.plist configured
- โ Xcode build settings correct
-
Testing Complete
- โ Tested in sandbox environment
- โ All purchase flows work
- โ Unfinished purchases restore correctly
- โ Error handling works properly
- โ Tested on physical iOS 17.4+ device
-
Code Review
- โ Environment set to production
- โ Logging disabled or conditional
- โ All purchases are finished
- โ Verification checks in place
- โ Error handling comprehensive
-
Dashboard Configuration
- โ Products created in Aptoide Connect
- โ Product SKUs match code
- โ Prices set correctly
- โ Products are active
- โ Sandbox mode disabled for production
-
Security
- โ Server-side verification implemented
- โ Payload used for user identification
- โ No sensitive data stored locally
- โ Logging disabled in production
-
Documentation
- โ User-facing purchase instructions
- โ Support documentation for IAP issues
- โ Privacy policy updated
- โ Terms of service updated
๐ Support #
Getting Help #
- Check Documentation: Review all documentation files
- Search Issues: Check GitHub Issues
- Enable Logging: Turn on logging to see detailed error messages
- Check Logs: Review console output for error details
- Create Issue: If problem persists, create a detailed issue report
Creating a Good Issue Report #
Include:
- Flutter version (
flutter --version) - iOS version
- Device model
- Plugin version
- Complete error message
- Relevant code snippet
- Steps to reproduce
Useful Resources #
- Aptoide Connect: https://connect.aptoide.com/
- AppCoins SDK: https://github.com/Catappult/appcoins-sdk-ios
- Flutter Docs: https://docs.flutter.dev/
- Aptoide Support: support@aptoide.com
๐ License #
This project is licensed under the MIT License - see the LICENSE file for details.
๐ Acknowledgments #
- Built on top of AppCoins SDK for iOS
- Designed for Aptoide Connect billing system
- Inspired by Flutter's in_app_purchase plugin
๐ Changelog #
See CHANGELOG.md for version history and changes.
Current Version: 0.0.2
Latest Changes:
- โ Added environment switching (sandbox/production)
- โ Type-safe environment enum
- โ Enhanced example app with environment toggle
- โ Comprehensive documentation
Made with โค๏ธ for Flutter developers using Aptoide