aptoide_iap 0.0.1 copy "aptoide_iap: ^0.0.1" to clipboard
aptoide_iap: ^0.0.1 copied to clipboard

PlatformiOS

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.

pub package License: MIT


๐Ÿ“‹ Table of Contents #


โœจ 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 TransactionReporting APIs

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 TransactionReporting APIs
  • 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:

  1. In Xcode: File โ†’ Add Package Dependencies
  2. Enter URL: https://github.com/Catappult/appcoins-sdk-ios.git
  3. Select version: 1.0.0 or later
  4. 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:

  1. Build Settings โ†’ Search "Marketplaces" โ†’ Set to: com.aptoide.ios.store
  2. 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 #

// 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: production
  • onUnfinishedPurchase - 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 SKU
  • title (String) - Product title
  • description (String) - Product description
  • price (double) - Price value
  • currency (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 identifier
  • payload - Optional custom data (e.g., user ID for backend verification)

Returns: Purchase map with:

  • id (String) - Purchase ID
  • sku (String) - Product SKU
  • state (String) - Purchase state
  • payload (String) - Custom payload
  • orderUid (String) - Order unique ID
  • created (String) - Creation timestamp
  • verified (bool) - Verification status
  • verificationError (String?) - Error if unverified

Throws: PlatformException with codes:

  • CANCELLED - User cancelled the purchase
  • PENDING - Purchase is pending
  • FAILED - 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']);
}

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 #

  1. Build Settings:

    • Search for "Marketplaces"
    • Set value to: com.aptoide.ios.store
  2. Scheme Configuration:

    • Product โ†’ Scheme โ†’ Edit Scheme
    • Run โ†’ Options tab
    • Distribution: com.aptoide.ios.store
  3. Build and Run:

    flutter run --release
    

Aptoide Connect Dashboard #

  1. Log in to Aptoide Connect
  2. Navigate to your app
  3. Go to In-App Products section
  4. Enable Sandbox Mode for testing
  5. 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 with onUnfinishedPurchase callback
  • โœ… 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:

  1. "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
  2. General Build Failures

    • โœ… Verify AppCoins SDK is added via Swift Package Manager
    • โœ… Check entitlements file exists and is configured
    • โœ… Ensure import AppCoinsSDK in 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

๐Ÿ“ฑ 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 #

  1. Check Documentation: Review all documentation files
  2. Search Issues: Check GitHub Issues
  3. Enable Logging: Turn on logging to see detailed error messages
  4. Check Logs: Review console output for error details
  5. 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 #


๐Ÿ“„ License #

This project is licensed under the MIT License - see the LICENSE file for details.


๐Ÿ™ Acknowledgments #


๐Ÿ“ 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

0
likes
160
points
78
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter plugin for Aptoide IAP on iOS. Wraps the AppCoins SDK for in-app purchases on iOS 18.0+ devices in the EU.

Homepage

Documentation

API reference

License

MIT (license)

Dependencies

flutter, plugin_platform_interface

More

Packages that depend on aptoide_iap

Packages that implement aptoide_iap