Flutter GB In-App Purchases

A comprehensive Flutter package for handling in-app purchases using the BLoC pattern with Geekbears Stack Base. This package provides a robust, platform-agnostic solution for managing consumables, non-consumables, and subscription products across iOS and Android.

Features

  • πŸ—οΈ Clean Architecture: Built with BLoC pattern and dependency injection
  • πŸ“± Cross-Platform: Full support for iOS (StoreKit) and Android (Google Play)
  • πŸ”„ Product Types: Support for consumables, non-consumables, and subscriptions
  • πŸ›‘οΈ Transaction Verification: Built-in server-side verification with customizable endpoints
  • πŸ”„ Subscription Management: Handle subscription upgrades, downgrades, and changes
  • πŸ“Š Real-time Updates: Stream-based transaction monitoring
  • πŸ”§ Configurable: Highly customizable configuration options
  • πŸ§ͺ Testable: Built with testing in mind using dependency injection

Supported Platforms

  • βœ… iOS (StoreKit)
  • βœ… Android (Google Play)

Supported Product Types

  • Consumables: Products that can be purchased multiple times (coins, lives, etc.)
  • Non-Consumables: One-time purchases (premium features, remove ads)
  • Renewable Subscriptions: Auto-renewing subscriptions
  • Non-Renewable Subscriptions: Time-limited subscriptions

Getting Started

Installation

Add this to your package's pubspec.yaml file:

dependencies:
  flutter_gb_in_app_purchases: ^1.0.0

Prerequisites

  • Flutter SDK >= 3.35.0
  • Dart SDK >= 3.9.0
  • iOS 14.0+ / Android API 21+

iOS Setup

  1. Configure your app in App Store Connect
  2. Add your product identifiers
  3. Ensure your app is signed with a valid provisioning profile

Android Setup

  1. Configure your app in Google Play Console
  2. Add your product IDs
  3. Ensure your app is signed and uploaded to Play Console

Usage

1. Basic Setup

First, configure the dependency injection:

import 'package:flutter_gb_in_app_purchases/flutter_gb_in_app_purchases.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();
  
  // Configure dependency injection
  await configurePurchasesInjection(
    environment: AppEnvironment.production,
    purchasesConfig: InAppPurchasesConfig(
      // Optional: API endpoint to fetch product listings
      getProductsListingApiEndpoint: () => Uri.parse('https://your-api.com/products'),
      
      // Optional: Parse product listings response
      getProductsListingResponseParser: (response) {
        // Parse your API response and return PurchaseProductListing list
        return [];
      },
      
      // Optional: Transaction verification endpoint
      purchaseVerificationApiEndpoint: (transaction, params) {
        return Uri.parse('https://your-api.com/verify-purchase');
      },
      
      // Optional: Custom request mapper for verification
      purchaseVerificationRequestMapper: (request, transaction) {
        return request..body = {'transaction_id': transaction.id};
      },
      
      // Optional: Product type mapper
      productTypeMapper: (item, listings) {
        // Determine product type based on your business logic
        return ProductType.consumable;
      },
      
      // Complete purchases without verification (not recommended)
      completeWithoutVerifying: false,
    ),
  );
  
  runApp(MyApp());
}

2. Using the BLoC

Wrap your app or specific widgets with the purchases BLoC:

import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_gb_in_app_purchases/flutter_gb_in_app_purchases.dart';

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => PurchasesBloc(
        purchasesBlocConfig: PurchasesBlocConfig(
          autoInitialize: true,
          userReferenceMapper: () async => 'user_123', // Your user ID
          verificationParamBuilder: (transaction) {
            return {'user_id': 'user_123'};
          },
        ),
      ),
      child: MaterialApp(
        home: PurchaseScreen(),
      ),
    );
  }
}

3. Initialize and Get Products

class PurchaseScreen extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocBuilder<PurchasesBloc, PurchasesState>(
      builder: (context, state) {
        return Scaffold(
          appBar: AppBar(title: Text('In-App Purchases')),
          body: Column(
            children: [
              // Initialize purchases
              ElevatedButton(
                onPressed: () {
                  context.read<PurchasesBloc>().initialize();
                },
                child: Text('Initialize Purchases'),
              ),
              
              // Get store catalog
              ElevatedButton(
                onPressed: () {
                  context.read<PurchasesBloc>().add(
                    PurchasesEvent.getStoreCatalog(
                      productIds: {'premium_upgrade', 'coins_100'},
                    ),
                  );
                },
                child: Text('Load Products'),
              ),
              
              // Display products
              if (state.storeCatalog.data != null)
                Expanded(
                  child: ListView.builder(
                    itemCount: state.storeCatalog.data!.products.length,
                    itemBuilder: (context, index) {
                      final product = state.storeCatalog.data!.products[index];
                      return ListTile(
                        title: Text(product.title),
                        subtitle: Text(product.description),
                        trailing: Text(product.price),
                        onTap: () {
                          context.read<PurchasesBloc>().purchase(product);
                        },
                      );
                    },
                  ),
                ),
            ],
          ),
        );
      },
    );
  }
}

4. Handle Purchase States

class PurchaseListener extends StatelessWidget {
  final Widget child;
  
  const PurchaseListener({Key? key, required this.child}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return BlocListener<PurchasesBloc, PurchasesState>(
      listener: (context, state) {
        // Handle purchase completion
        if (state.isPurchasing.data == true) {
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Purchase completed!')),
          );
        }
        
        // Handle errors
        if (state.storeCatalog.data?.isLeft() == true) {
          final failure = state.storeCatalog.data?.fold(
            (failure) => failure,
            (success) => null,
          );
          ScaffoldMessenger.of(context).showSnackBar(
            SnackBar(content: Text('Error: ${failure?.message}')),
          );
        }
      },
      child: child,
    );
  }
}

5. Restore Purchases

// Restore purchases (typically called from a settings screen)
context.read<PurchasesBloc>().restorePurchases();

6. Subscription Changes (Android)

// Change subscription (upgrade/downgrade)
context.read<PurchasesBloc>().changeSubscription(newProduct);

Configuration Options

InAppPurchasesConfig

Property Type Description
getProductsListingApiEndpoint Uri Function()? API endpoint to fetch product listings
getProductsListingResponseParser List<PurchaseProductListing> Function(String)? Parse product listings API response
purchaseVerificationApiEndpoint Uri Function(Transaction, Map<String, dynamic>?)? Transaction verification endpoint
purchaseVerificationRequestMapper BaseRequest Function(Request, Transaction)? Customize verification request
completeWithoutVerifying bool Complete purchases without server verification (default: false)
productTypeMapper ProductItemTypeMapper? Map products to their types
logger Logger? Custom logger instance

PurchasesBlocConfig

Property Type Description
userReferenceMapper UserReferenceMapper? Function to get current user ID
verificationParamBuilder VerificationParamBuilder? Build verification parameters
autoInitialize bool Auto-initialize on bloc creation (default: false)
logEvents bool Enable event logging (default: false)
verifyDebounced Duration Debounce time for verification (default: 3s)
transactionCompleteDebounced Duration Debounce time for completion (default: 3s)

API Integration

Product Listings API

Your API should return product information in this format:

{
  "products": [
    {
      "product_id": "premium_upgrade",
      "title": "Premium Upgrade",
      "description": "Unlock premium features",
      "price": "4.99",
      "type": "non-consumable"
    }
  ]
}

Transaction Verification API

Your verification endpoint will receive transaction details and should return verification results.

State Management

The package provides comprehensive state management through BLoC:

  • Initialization: Track initialization status
  • Product Loading: Monitor catalog loading states
  • Purchase States: Track individual purchase progress
  • Transaction Updates: Real-time transaction monitoring
  • Verification Status: Track server-side verification

Error Handling

The package provides detailed error handling for common scenarios:

  • Network failures
  • Invalid products
  • Purchase failures
  • Verification failures
  • Platform-specific errors

Testing

The package is built with testing in mind. Use the mock configuration for testing:

final mockConfig = InAppPurchasesConfig.mock();

Dependencies

This package depends on:

  • flutter_gb_stack_base: Geekbears base utilities
  • in_app_purchase: Core in-app purchase functionality
  • flutter_bloc: State management
  • dartz: Functional programming utilities
  • built_value: Immutable data structures
  • rxdart: Reactive extensions

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

License

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

Support

For support, please open an issue in the GitLab repository or contact the Geekbears team.

Changelog

See CHANGELOG.md for a list of changes and version history.

Libraries

application/application
application/bloc/bloc
application/bloc/containers/containers
application/bloc/containers/store_purchases_catalog_builder/store_purchases_catalog_builder_bloc
application/bloc/shared/purchases/purchases_bloc
application/bloc/shared/shared
config/config
config/purchases_bloc_config
config/purchases_config
dependency_injection
domain/domain
domain/entities/catalog_product
domain/entities/entities
domain/entities/product_item
domain/entities/purchase_product_listing
domain/entities/shopping_cart
domain/entities/transaction
domain/entities/transaction_verification
domain/services/purchase_service
domain/services/services
flutter_gb_in_app_purchases
infrastructure/facade/facade
infrastructure/facade/in_app_purchases_facade
infrastructure/infrastructure
infrastructure/models/catalog_product_model
infrastructure/models/models
infrastructure/models/product_item_model
infrastructure/models/purchase_product_listing_model
infrastructure/models/serializers
infrastructure/models/shopping_cart_model
infrastructure/models/transaction_model
infrastructure/models/transaction_verification_model
infrastructure/services/purchase_service_impl
infrastructure/services/services
presentation/containers/containers
presentation/containers/purchase_bloc_event_listener_container
presentation/containers/purchase_bloc_provider_container
presentation/containers/purchase_store_catalog_builder_container
presentation/presentation
utils/enums/enums
utils/enums/product_type
utils/enums/store_platform
utils/enums/transaction_status
utils/failures/failures
utils/logger
utils/utils