easy_iap 0.2.0 copy "easy_iap: ^0.2.0" to clipboard
easy_iap: ^0.2.0 copied to clipboard

Simple and elegant in-app purchase SDK for Flutter. Unified API for iOS App Store and Google Play Store with functional patterns (fold, when) and multi-language support.

example/lib/main.dart

/// Easy IAP Example
///
/// This example demonstrates the core features of the easy_iap package.
/// Copy and use directly in your project.
///
/// ## Key Features
/// - Simple initialization with EasyIap.init()
/// - IapBuilder for reactive UI based on ownership
/// - fold/when patterns for result handling
/// - Smart error handling with retry support
library;

import 'package:flutter/material.dart';
import 'package:easy_iap/easy_iap.dart';

// -----------------------------------------------------------------------------
// Configuration
// -----------------------------------------------------------------------------

/// Demo mode: When true, uses MockEasyIap for UI testing without real purchases
const kDemoMode = true;

/// EasyIap instance (initialized in main)
late final dynamic iap;

// -----------------------------------------------------------------------------
// Main
// -----------------------------------------------------------------------------

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  if (kDemoMode) {
    // Demo mode: Use MockEasyIap for testing
    final mockIap = MockEasyIap(
      products: [
        IapProduct.subscription('pro_monthly'),
        IapProduct.subscription('pro_yearly'),
        IapProduct.nonConsumable('remove_ads'),
        IapProduct.consumable('gems_100'),
      ],
      delay: const Duration(milliseconds: 800),
    );
    await mockIap.initialize();
    iap = mockIap;
  } else {
    // Production: Use real EasyIap
    // final realIap = await EasyIap.init(
    //   products: [
    //     IapProduct.subscription('pro_monthly'),
    //     IapProduct.subscription('pro_yearly'),
    //     IapProduct.nonConsumable('remove_ads'),
    //     IapProduct.consumable('gems_100'),
    //   ],
    // );
    // iap = realIap;
  }

  runApp(const App());
}

// -----------------------------------------------------------------------------
// App
// -----------------------------------------------------------------------------

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Easy IAP Example',
      debugShowCheckedModeBanner: false,
      theme: ThemeData(
        colorScheme: ColorScheme.fromSeed(seedColor: Colors.indigo),
        useMaterial3: true,
      ),
      home: const HomeScreen(),
    );
  }
}

// -----------------------------------------------------------------------------
// Home Screen
// -----------------------------------------------------------------------------

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

  @override
  State<HomeScreen> createState() => _HomeScreenState();
}

class _HomeScreenState extends State<HomeScreen> {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: const Text('Easy IAP'),
        actions: [
          IconButton(
            onPressed: _showSubscriptionStatus,
            icon: const Icon(Icons.card_membership),
            tooltip: 'Subscription Status',
          ),
        ],
      ),
      body: ListView(
        padding: const EdgeInsets.all(16),
        children: [
          // Demo mode indicator
          if (kDemoMode)
            Container(
              padding: const EdgeInsets.all(12),
              margin: const EdgeInsets.only(bottom: 16),
              decoration: BoxDecoration(
                color: Colors.orange.shade50,
                borderRadius: BorderRadius.circular(8),
                border: Border.all(color: Colors.orange.shade200),
              ),
              child: Row(
                children: [
                  Icon(Icons.science, color: Colors.orange.shade700),
                  const SizedBox(width: 12),
                  Expanded(
                    child: Text(
                      'Demo Mode - Using MockEasyIap',
                      style: TextStyle(color: Colors.orange.shade700),
                    ),
                  ),
                ],
              ),
            ),

          // Subscriptions Section
          _SectionHeader(title: 'Subscriptions'),
          _ProductCard(
            productId: 'pro_monthly',
            title: 'Pro Monthly',
            description: 'Unlock all features',
            price: '\$9.99/month',
            icon: Icons.star,
            color: Colors.indigo,
          ),
          _ProductCard(
            productId: 'pro_yearly',
            title: 'Pro Yearly',
            description: 'Save 40% with annual plan',
            price: '\$69.99/year',
            icon: Icons.workspace_premium,
            color: Colors.purple,
            badge: 'BEST VALUE',
          ),

          const SizedBox(height: 24),

          // One-time Purchases Section
          _SectionHeader(title: 'One-time Purchases'),
          _ProductCard(
            productId: 'remove_ads',
            title: 'Remove Ads',
            description: 'Enjoy ad-free experience forever',
            price: '\$4.99',
            icon: Icons.block,
            color: Colors.green,
          ),

          const SizedBox(height: 24),

          // Consumables Section
          _SectionHeader(title: 'Consumables'),
          _ProductCard(
            productId: 'gems_100',
            title: '100 Gems',
            description: 'Use gems to unlock special content',
            price: '\$0.99',
            icon: Icons.diamond,
            color: Colors.cyan,
            isConsumable: true,
          ),

          const SizedBox(height: 24),

          // Restore Purchases
          Center(
            child: TextButton.icon(
              onPressed: _restorePurchases,
              icon: const Icon(Icons.restore),
              label: const Text('Restore Purchases'),
            ),
          ),

          const SizedBox(height: 32),
        ],
      ),
    );
  }

  void _showSubscriptionStatus() {
    final hasSubscription = iap.hasActiveSubscription;
    final ownedProducts = iap.ownedProductIds;

    showDialog(
      context: context,
      builder: (context) => AlertDialog(
        title: const Text('Purchase Status'),
        content: Column(
          mainAxisSize: MainAxisSize.min,
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            _statusRow(
              'Subscription',
              hasSubscription ? 'Active' : 'None',
              hasSubscription ? Colors.green : Colors.grey,
            ),
            const Divider(),
            const Text(
              'Owned Products:',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            if (ownedProducts.isEmpty)
              const Text('No products owned',
                  style: TextStyle(color: Colors.grey))
            else
              ...ownedProducts.map((id) => Padding(
                    padding: const EdgeInsets.symmetric(vertical: 2),
                    child: Row(
                      children: [
                        const Icon(Icons.check_circle,
                            size: 16, color: Colors.green),
                        const SizedBox(width: 8),
                        Text(id),
                      ],
                    ),
                  )),
          ],
        ),
        actions: [
          TextButton(
            onPressed: () => Navigator.pop(context),
            child: const Text('Close'),
          ),
        ],
      ),
    );
  }

  Widget _statusRow(String label, String value, Color color) {
    return Padding(
      padding: const EdgeInsets.symmetric(vertical: 4),
      child: Row(
        mainAxisAlignment: MainAxisAlignment.spaceBetween,
        children: [
          Text(label),
          Container(
            padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
            decoration: BoxDecoration(
              color: color.withValues(alpha: 0.1),
              borderRadius: BorderRadius.circular(4),
            ),
            child: Text(
              value,
              style: TextStyle(color: color, fontWeight: FontWeight.bold),
            ),
          ),
        ],
      ),
    );
  }

  Future<void> _restorePurchases() async {
    showDialog(
      context: context,
      barrierDismissible: false,
      builder: (context) => const AlertDialog(
        content: Row(
          children: [
            CircularProgressIndicator(),
            SizedBox(width: 16),
            Text('Restoring purchases...'),
          ],
        ),
      ),
    );

    final receipts = await iap.restore();

    if (mounted) {
      Navigator.pop(context);

      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(
            receipts.isEmpty
                ? 'No purchases to restore'
                : 'Restored ${receipts.length} purchase(s)',
          ),
        ),
      );

      setState(() {}); // Refresh UI
    }
  }
}

// -----------------------------------------------------------------------------
// Section Header
// -----------------------------------------------------------------------------

class _SectionHeader extends StatelessWidget {
  const _SectionHeader({required this.title});

  final String title;

  @override
  Widget build(BuildContext context) {
    return Padding(
      padding: const EdgeInsets.only(bottom: 12),
      child: Text(
        title,
        style: Theme.of(context).textTheme.titleMedium?.copyWith(
              fontWeight: FontWeight.bold,
              color: Colors.grey.shade700,
            ),
      ),
    );
  }
}

// -----------------------------------------------------------------------------
// Product Card
// -----------------------------------------------------------------------------

class _ProductCard extends StatefulWidget {
  const _ProductCard({
    required this.productId,
    required this.title,
    required this.description,
    required this.price,
    required this.icon,
    required this.color,
    this.badge,
    this.isConsumable = false,
  });

  final String productId;
  final String title;
  final String description;
  final String price;
  final IconData icon;
  final Color color;
  final String? badge;
  final bool isConsumable;

  @override
  State<_ProductCard> createState() => _ProductCardState();
}

class _ProductCardState extends State<_ProductCard> {
  bool _loading = false;

  bool get _isOwned => iap.isOwned(widget.productId);

  Future<void> _purchase() async {
    setState(() => _loading = true);

    final result = await iap.purchase(widget.productId);

    if (mounted) {
      setState(() => _loading = false);

      // Using when pattern for success/cancelled/failure handling
      result.when(
        success: (receipt) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(
              content: Text('Purchased: ${receipt.product.id}'),
              backgroundColor: Colors.green,
            ),
          );
        },
        cancelled: () {
          // User cancelled - usually ignore
        },
        failure: (failure) => _handleFailure(failure),
      );
    }
  }

  void _handleFailure(IapFailure failure) {
    if (failure.shouldIgnore) return;

    if (failure.canRetry) {
      // Show retry dialog for network errors
      showDialog(
        context: context,
        builder: (context) => AlertDialog(
          title: const Text('Purchase Failed'),
          content: Column(
            mainAxisSize: MainAxisSize.min,
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Text(failure.displayMessage),
              const SizedBox(height: 12),
              Text(
                'Please check your connection and try again.',
                style: TextStyle(color: Colors.grey.shade600, fontSize: 13),
              ),
            ],
          ),
          actions: [
            TextButton(
              onPressed: () => Navigator.pop(context),
              child: const Text('Cancel'),
            ),
            FilledButton(
              onPressed: () {
                Navigator.pop(context);
                _purchase();
              },
              child: const Text('Retry'),
            ),
          ],
        ),
      );
    } else if (failure.requiresUserAction) {
      // Payment settings issue
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(failure.displayMessage),
          backgroundColor: Colors.orange,
          action: SnackBarAction(
            label: 'Settings',
            textColor: Colors.white,
            onPressed: () {
              // Open payment settings
            },
          ),
        ),
      );
    } else {
      // Other errors
      ScaffoldMessenger.of(context).showSnackBar(
        SnackBar(
          content: Text(failure.displayMessage),
          backgroundColor: Colors.red,
        ),
      );
    }
  }

  @override
  Widget build(BuildContext context) {
    final isOwned = _isOwned;

    return Card(
      margin: const EdgeInsets.only(bottom: 12),
      clipBehavior: Clip.antiAlias,
      child: Stack(
        children: [
          Padding(
            padding: const EdgeInsets.all(16),
            child: Row(
              children: [
                // Icon
                Container(
                  width: 56,
                  height: 56,
                  decoration: BoxDecoration(
                    color: widget.color.withValues(alpha: 0.1),
                    borderRadius: BorderRadius.circular(12),
                  ),
                  child: Icon(widget.icon, color: widget.color, size: 28),
                ),
                const SizedBox(width: 16),

                // Title & Description
                Expanded(
                  child: Column(
                    crossAxisAlignment: CrossAxisAlignment.start,
                    children: [
                      Text(
                        widget.title,
                        style: const TextStyle(
                          fontWeight: FontWeight.bold,
                          fontSize: 16,
                        ),
                      ),
                      const SizedBox(height: 4),
                      Text(
                        widget.description,
                        style: TextStyle(
                          color: Colors.grey.shade600,
                          fontSize: 13,
                        ),
                      ),
                    ],
                  ),
                ),

                // Price & Button
                Column(
                  crossAxisAlignment: CrossAxisAlignment.end,
                  children: [
                    if (!isOwned || widget.isConsumable)
                      Text(
                        widget.price,
                        style: TextStyle(
                          fontWeight: FontWeight.bold,
                          color: widget.color,
                        ),
                      ),
                    const SizedBox(height: 8),
                    if (isOwned && !widget.isConsumable)
                      Container(
                        padding: const EdgeInsets.symmetric(
                          horizontal: 12,
                          vertical: 6,
                        ),
                        decoration: BoxDecoration(
                          color: Colors.green.shade50,
                          borderRadius: BorderRadius.circular(20),
                          border: Border.all(color: Colors.green.shade200),
                        ),
                        child: Row(
                          mainAxisSize: MainAxisSize.min,
                          children: [
                            Icon(
                              Icons.check_circle,
                              size: 16,
                              color: Colors.green.shade700,
                            ),
                            const SizedBox(width: 4),
                            Text(
                              'Owned',
                              style: TextStyle(
                                color: Colors.green.shade700,
                                fontWeight: FontWeight.w500,
                                fontSize: 13,
                              ),
                            ),
                          ],
                        ),
                      )
                    else
                      SizedBox(
                        height: 36,
                        child: FilledButton(
                          onPressed: _loading ? null : _purchase,
                          style: FilledButton.styleFrom(
                            backgroundColor: widget.color,
                            padding: const EdgeInsets.symmetric(horizontal: 16),
                          ),
                          child: _loading
                              ? const SizedBox(
                                  width: 20,
                                  height: 20,
                                  child: CircularProgressIndicator(
                                    strokeWidth: 2,
                                    color: Colors.white,
                                  ),
                                )
                              : const Text('Buy'),
                        ),
                      ),
                  ],
                ),
              ],
            ),
          ),

          // Badge
          if (widget.badge != null)
            Positioned(
              top: 0,
              right: 0,
              child: Container(
                padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
                decoration: BoxDecoration(
                  color: widget.color,
                  borderRadius: const BorderRadius.only(
                    bottomLeft: Radius.circular(8),
                  ),
                ),
                child: Text(
                  widget.badge!,
                  style: const TextStyle(
                    color: Colors.white,
                    fontSize: 10,
                    fontWeight: FontWeight.bold,
                  ),
                ),
              ),
            ),
        ],
      ),
    );
  }
}

// -----------------------------------------------------------------------------
// IapBuilder Example (commented)
// -----------------------------------------------------------------------------

/*
/// Example: Using IapBuilder for reactive UI
///
/// ```dart
/// IapBuilder(
///   iap: iap,
///   productId: 'remove_ads',
///   owned: () => const AdFreeContent(),
///   notOwned: () => const ContentWithAds(),
///   loading: () => const LoadingSpinner(),
/// )
/// ```
///
/// Example: Using SubscriptionBuilder
///
/// ```dart
/// SubscriptionBuilder(
///   iap: iap,
///   subscribed: (subscriptions) => PremiumContent(subscriptions),
///   notSubscribed: () => const UpgradePrompt(),
/// )
/// ```
///
/// Example: Using PurchaseListener
///
/// ```dart
/// PurchaseListener(
///   iap: iap,
///   onPurchase: (receipt) {
///     analytics.logPurchase(receipt);
///     showSuccessAnimation();
///   },
///   child: const YourContent(),
/// )
/// ```
*/
1
likes
150
points
137
downloads

Publisher

unverified uploader

Weekly Downloads

Simple and elegant in-app purchase SDK for Flutter. Unified API for iOS App Store and Google Play Store with functional patterns (fold, when) and multi-language support.

Repository (GitHub)
View/report issues
Contributing

Documentation

Documentation
API reference

License

MIT (license)

Dependencies

collection, flutter, in_app_purchase, in_app_purchase_android, in_app_purchase_storekit, intl

More

Packages that depend on easy_iap