verify_local_purchase 1.0.3 copy "verify_local_purchase: ^1.0.3" to clipboard
verify_local_purchase: ^1.0.3 copied to clipboard

A Flutter package for verifying in-app purchases and subscriptions locally on device with Apple App Store and Google Play Store.

example/lib/main.dart

import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:in_app_purchase/in_app_purchase.dart';
import 'package:verify_local_purchase/verify_local_purchase.dart';

void main() {
  // Initialize the verification service with your credentials
  VerifyLocalPurchase.initialize(
    appleConfig: AppleConfig(
      bundleId: 'com.example.app',
      issuerId: 'your-issuer-id-here',
      keyId: 'your-key-id-here',
      privateKey: '''-----BEGIN PRIVATE KEY-----
YOUR_PRIVATE_KEY_CONTENT_HERE
-----END PRIVATE KEY-----''',
      useSandbox: true,
    ),
    googlePlayConfig: GooglePlayConfig(
      packageName: 'com.example.app',
      serviceAccountJson: '''
{
  "type": "service_account",
  "project_id": "your-project",
  "private_key_id": "xxxxx",
  "private_key": "-----BEGIN PRIVATE KEY-----\\nYOUR_KEY\\n-----END PRIVATE KEY-----\\n",
  "client_email": "your-service-account@your-project.iam.gserviceaccount.com",
  "client_id": "xxxxx"
}''',
    ),
  );

  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'In-App Purchase Example',
      theme: ThemeData(primarySwatch: Colors.blue, useMaterial3: true),
      home: const PurchaseExamplePage(),
    );
  }
}

class PurchaseExamplePage extends StatefulWidget {
  const PurchaseExamplePage({super.key});

  @override
  State<PurchaseExamplePage> createState() => _PurchaseExamplePageState();
}

class _PurchaseExamplePageState extends State<PurchaseExamplePage> {
  final InAppPurchase _inAppPurchase = InAppPurchase.instance;
  final VerifyLocalPurchase _verifyPurchase = VerifyLocalPurchase();

  StreamSubscription<List<PurchaseDetails>>? _purchaseSubscription;
  List<ProductDetails> _products = [];
  String _statusMessage = 'Loading...';
  bool _isLoading = true;
  int _userTokens = 0;

  @override
  void initState() {
    super.initState();
    _initialize();
  }

  @override
  void dispose() {
    _purchaseSubscription?.cancel();
    super.dispose();
  }

  Future<void> _initialize() async {
    // Listen to purchase updates
    _purchaseSubscription = _inAppPurchase.purchaseStream.listen(
      _handlePurchaseUpdate,
      onError: (error) {
        debugPrint('❌ Purchase stream error: $error');
        _showMessage('Error: $error');
      },
    );

    await _loadProducts();
  }

  Future<void> _loadProducts() async {
    setState(() {
      _isLoading = true;
      _statusMessage = 'Loading products...';
    });

    try {
      // Check if in-app purchase is available
      final available = await _inAppPurchase.isAvailable();
      if (!available) {
        setState(() {
          _statusMessage = 'In-app purchases not available';
          _isLoading = false;
        });
        return;
      }

      // Load products (replace with your product IDs)
      const productIds = {'tokens_100', 'tokens_500', 'tokens_1000'};
      final response = await _inAppPurchase.queryProductDetails(productIds);

      if (response.error != null) {
        setState(() {
          _statusMessage = 'Error loading products: ${response.error}';
          _isLoading = false;
        });
        return;
      }

      setState(() {
        _products = response.productDetails;
        _statusMessage = _products.isEmpty
            ? 'No products found'
            : 'Tap a product to purchase';
        _isLoading = false;
      });

      debugPrint('📦 Loaded ${_products.length} products');
    } catch (e) {
      debugPrint('❌ Error loading products: $e');
      setState(() {
        _statusMessage = 'Error: $e';
        _isLoading = false;
      });
    }
  }

  Future<void> _buyProduct(ProductDetails product) async {
    debugPrint('🛒 Starting purchase: ${product.id}');

    setState(() {
      _statusMessage = 'Processing purchase...';
    });

    final purchaseParam = PurchaseParam(productDetails: product);
    await _inAppPurchase.buyConsumable(purchaseParam: purchaseParam);
  }

  Future<void> _handlePurchaseUpdate(
    List<PurchaseDetails> purchaseDetailsList,
  ) async {
    debugPrint('🔔 Received ${purchaseDetailsList.length} purchase updates');

    for (final purchaseDetails in purchaseDetailsList) {
      debugPrint(
        '  📦 ${purchaseDetails.productID}: ${purchaseDetails.status}',
      );

      switch (purchaseDetails.status) {
        case PurchaseStatus.pending:
          _showMessage('⏳ Purchase pending...');

        case PurchaseStatus.purchased:
        case PurchaseStatus.restored:
          await _verifyAndCompletePurchase(purchaseDetails);

        case PurchaseStatus.error:
          final errorMsg = purchaseDetails.error?.message ?? 'Unknown error';
          debugPrint('❌ Purchase error: $errorMsg');
          _showMessage('❌ Purchase failed: $errorMsg');
          if (purchaseDetails.pendingCompletePurchase) {
            await _inAppPurchase.completePurchase(purchaseDetails);
          }

        case PurchaseStatus.canceled:
          debugPrint('🚫 Purchase canceled');
          _showMessage('❌ Purchase canceled');
          if (purchaseDetails.pendingCompletePurchase) {
            await _inAppPurchase.completePurchase(purchaseDetails);
          }
      }
    }
  }

  Future<void> _verifyAndCompletePurchase(
    PurchaseDetails purchaseDetails,
  ) async {
    try {
      // Get token for verification (iOS uses transactionId, Android uses purchaseToken)
      String verificationToken;
      if (Platform.isIOS) {
        verificationToken = purchaseDetails.purchaseID ?? '';
      } else {
        verificationToken =
            purchaseDetails.verificationData.serverVerificationData;
      }

      if (verificationToken.isEmpty) {
        debugPrint('⚠️ Empty verification token');
        _showMessage('❌ Invalid purchase data');
        if (purchaseDetails.pendingCompletePurchase) {
          await _inAppPurchase.completePurchase(purchaseDetails);
        }
        return;
      }

      debugPrint('🔐 Verifying purchase...');
      _showMessage('🔐 Verifying purchase...');

      // Verify the purchase locally
      final isValid = await _verifyPurchase.verifyPurchase(verificationToken);

      if (!isValid) {
        debugPrint('❌ Purchase verification failed');
        _showMessage('❌ Purchase verification failed');
        if (purchaseDetails.pendingCompletePurchase) {
          await _inAppPurchase.completePurchase(purchaseDetails);
        }
        return;
      }

      debugPrint('✅ Purchase verified successfully');

      // Extract tokens from product description
      // In a real app, you'd get this from your backend
      final tokens = _extractTokensFromDescription(
        _products
            .firstWhere((p) => p.id == purchaseDetails.productID)
            .description,
      );

      // Add tokens to user (in a real app, this would be done on your backend)
      setState(() {
        _userTokens += tokens;
      });

      // Complete the purchase
      if (purchaseDetails.pendingCompletePurchase) {
        await _inAppPurchase.completePurchase(purchaseDetails);
      }

      _showMessage('✅ Purchase successful! Added $tokens tokens');
      debugPrint('✅ Purchase completed. Total tokens: $_userTokens');
    } catch (e) {
      debugPrint('❌ Error processing purchase: $e');
      _showMessage('❌ Error: $e');
    }
  }

  int _extractTokensFromDescription(String description) {
    // Simple extraction - in production, use proper product metadata
    final match = RegExp(
      r'(\d+)\s*tokens?',
      caseSensitive: false,
    ).firstMatch(description);
    return match != null ? int.parse(match.group(1)!) : 100;
  }

  void _showMessage(String message) {
    setState(() {
      _statusMessage = message;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('In-App Purchase Example'),
        elevation: 2,
      ),
      body: Column(
        children: [
          // User Balance
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(20),
            color: Colors.blue.shade50,
            child: Column(
              children: [
                const Icon(
                  Icons.account_balance_wallet,
                  size: 48,
                  color: Colors.blue,
                ),
                const SizedBox(height: 8),
                Text(
                  '$_userTokens Tokens',
                  style: Theme.of(context).textTheme.headlineMedium?.copyWith(
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ],
            ),
          ),

          // Status Message
          Container(
            width: double.infinity,
            padding: const EdgeInsets.all(16),
            color: _statusMessage.contains('✅')
                ? Colors.green.shade50
                : _statusMessage.contains('❌')
                ? Colors.red.shade50
                : Colors.grey.shade100,
            child: Text(
              _statusMessage,
              textAlign: TextAlign.center,
              style: const TextStyle(fontSize: 16),
            ),
          ),

          // Products List
          Expanded(
            child: _isLoading
                ? const Center(child: CircularProgressIndicator())
                : _products.isEmpty
                ? Center(
                    child: Column(
                      mainAxisAlignment: MainAxisAlignment.center,
                      children: [
                        const Icon(
                          Icons.shopping_cart_outlined,
                          size: 64,
                          color: Colors.grey,
                        ),
                        const SizedBox(height: 16),
                        Text(
                          _statusMessage,
                          style: const TextStyle(
                            fontSize: 18,
                            color: Colors.grey,
                          ),
                        ),
                      ],
                    ),
                  )
                : ListView.builder(
                    padding: const EdgeInsets.all(16),
                    itemCount: _products.length,
                    itemBuilder: (context, index) {
                      final product = _products[index];
                      return Card(
                        margin: const EdgeInsets.only(bottom: 12),
                        child: ListTile(
                          contentPadding: const EdgeInsets.all(16),
                          leading: CircleAvatar(
                            backgroundColor: Colors.blue,
                            child: Text(
                              '${index + 1}',
                              style: const TextStyle(color: Colors.white),
                            ),
                          ),
                          title: Text(
                            product.title,
                            style: const TextStyle(
                              fontWeight: FontWeight.bold,
                              fontSize: 18,
                            ),
                          ),
                          subtitle: Padding(
                            padding: const EdgeInsets.only(top: 8),
                            child: Text(product.description),
                          ),
                          trailing: ElevatedButton(
                            onPressed: () => _buyProduct(product),
                            child: Text(
                              product.price,
                              style: const TextStyle(fontSize: 16),
                            ),
                          ),
                        ),
                      );
                    },
                  ),
          ),
        ],
      ),
    );
  }
}
2
likes
150
points
37
downloads

Publisher

unverified uploader

Weekly Downloads

A Flutter package for verifying in-app purchases and subscriptions locally on device with Apple App Store and Google Play Store.

Repository (GitHub)
View/report issues

Topics

#in-app-purchase #app-store #google-play #subscription #verification

Documentation

API reference

License

MIT (license)

Dependencies

app_store_server_sdk, flutter, googleapis_auth, plugin_platform_interface

More

Packages that depend on verify_local_purchase

Packages that implement verify_local_purchase