iap 0.1.0

  • Readme
  • Changelog
  • Example
  • Installing
  • 54

Flutter plugin for interacting with iOS StoreKit and Android Billing Library.

Build Status codecov

Work in progress.

How this plugin is different from others #

The main difference is that instead of providing unified interface for in-app purchases on iOS and Android, this plugin exposes two separate APIs.

There are several benefits to this approach:

  • We can expose complete API interfaces for both platforms, without having to look for lowest common denominator of those APIs.
  • Dart interfaces are designed to match native ones most of the time. StoreKit for iOS follows native interface in 99% of cases. BillingClient for Android is very similar as well, but also simplifies some parts of native protocol (mostly replaces listeners with Dart Futures).
  • Developers familiar with native APIs would find it easier to learn. You can simply refer to official documentation in most cases to find details about certain method of field.

All Dart code is thoroughly documented with information taken directly from Apple Developers website (for StoreKit) and Android Developers website (for BillingClient).

Note that future versions may introduce unified interfaces for specific use cases, for instance, handling of in-app subscriptions.

StoreKit (iOS) #

Plugin currently implements all native APIs except for downloads. If you are looking for this functionality consider submitting a pull request or leaving your 👍 here.

Interacting with StoreKit in Flutter is almost 100% identical to the native ObjectiveC interface.

Prerequisites

Make sure to

  • Complete Agreements, Tax and Bankings
  • Setup your products in AppStore Connect
  • Enable In-App Purchases for your app in XCode

Complete example

Checkout a complete example of interacting with StoreKit in the example app in this repo. Note that in-app purchases is a complex topic and it would be really hard to cover everything in a simple example app like this, so it is highly recommended to read official documentation on setting up in-app purchases for each platform.

Getting products

final productIds = ['my.product1', 'my.product2'];
final SKProductsResponse response = await StoreKit.instance.products(productIds);
print(response.products); // list of valid [SKProduct]s
print(response.invalidProductIdentifiers) // list of invalid IDs

App Store Receipt

// Get receipt path on device
final Uri receiptUrl = await StoreKit.instance.appStoreReceiptUrl;
// Request a refresh of receipt
await StoreKit.instance.refreshReceipt();

Handling payments and transactions

Payments and transactions are handled within SKPaymentQueue.

It is important to set an observer on this queue as early as possible after your app launch. Observer is responsible for processing all events triggered by the queue. Create an observer by extending following class:

abstract class SKPaymentTransactionObserver {
  void didUpdateTransactions(SKPaymentQueue queue, List<SKPaymentTransaction> transactions);
  void didRemoveTransactions(SKPaymentQueue queue, List<SKPaymentTransaction> transactions) {}
  void failedToRestoreCompletedTransactions(SKPaymentQueue queue, SKError error) {}
  void didRestoreCompletedTransactions(SKPaymentQueue queue) {}
  void didUpdateDownloads(SKPaymentQueue queue, List<SKDownload> downloads) {}
  void didReceiveStorePayment(SKPaymentQueue queue, SKPayment payment, SKProduct product) {}
}

See API documentation for more details on these methods.

Make sure to implement didUpdateTransactions and process all transactions according to your needs. Typical implementation should normally look like this:

void didUpdateTransactions(
    SKPaymentQueue queue, List<SKPaymentTransaction> transactions) async {
  for (final tx in transactions) {
    switch (tx.transactionState) {
      case SKPaymentTransactionState.purchased:
        // Validate transaction, unlock content, etc...
        // Make sure to call `finishTransaction` when done, otherwise
        // this transaction will be redelivered by the queue on next application
        // launch.
        await queue.finishTransaction(tx);
        break;
      case SKPaymentTransactionState.failed:
        // ...
        await queue.finishTransaction(tx);
        break;
      // ...
    }
  }
}

Before attempting to add a payment always check if the user can actually make payments:

final bool canPay = await StoreKit.instance.paymentQueue.canMakePayments();

When that's verified and you've set an observer on the queue you can add payments. For instance:

final SKProductsResponse response = await StoreKit.instance.products(['my.inapp.subscription']);
final SKProduct product = response.products.single;
final SKPayment = SKPayment.withProduct(product);
await StoreKit.instance.paymentQueue.addPayment(payment);
// ...
// Use observer to track progress of this payment...

Restoring completed transactions

await StoreKit.instance.paymentQueue.restoreCompletedTransactions();
/// Optionally implement `didRestoreCompletedTransactions` and 
/// `failedToRestoreCompletedTransactions` on observer to track
/// result of this operation.

BillingClient (Android) #

This plugin wraps official Google Play Billing Library. Use BillingClient class as the main entry point.

Constructor of BillingClient class expects an instance of PurchaseUpdatedListener interface which looks like this:

/// Listener interface for purchase updates which happen when, for example,
/// the user buys something within the app or by initiating a purchase from
/// Google Play Store.
abstract class PurchasesUpdatedListener {
  /// Implement this method to get notifications for purchases updates.
  ///
  /// Both purchases initiated by your app and the ones initiated by Play Store
  /// will be reported here.
  void onPurchasesUpdated(int responseCode, List<Purchase> purchases);
}

Using BillingClient

To begin working with Play Billing service always start from establishing connection using startConnection method:

import 'package:iap/iap.dart';

bool _connected = false;

void main() async {
  final client = BillingClient(yourPurchaseListener);
  await client.startConnection(onDisconnect: handleDisconnect);
  _connected = true;

  // ...fetch SKUDetails, launch billing flows, query purchase history, etc

  await client.endConnection(); // Always call [endConnection] when work with this client is done.
}

void handleDisconnect() {
  // Client disconnected. Make sure to call [startConnection] next time before invoking
  // any other method of the client.
  _connected = false;
}

0.1.0 #

Initial release provides implementations of both iOS StoreKit library and Android Play Billing library.

example/lib/main.dart

// Copyright (c) 2018, Anatoly Pulyaevskiy.  Please see the AUTHORS file
// for details. All rights reserved. Use of this source code is governed by a
// BSD-style license that can be found in the LICENSE file.

// This example currently only shows some basics of using iOS StoreKit.
// Setting up in-app payments is a complex topic and cannot be covered in a
// simple example like this. Refer to official documentation for each platform
// for more details.

import 'dart:async';

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

void main() => runApp(new MyApp());

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => new _MyAppState();
}

/// Define a transaction observer responsible for processing payment updates.
/// In this example we also make this class encapsulate the whole purchase
/// flow since it leads to a bit cleaner separation of concerns.
class ExamplePurchaseProcessor extends SKPaymentTransactionObserver {
  /// Payment initiated by the user and is currently being processed.
  SKPayment _payment;
  Completer<SKPaymentTransaction> _completer;

  Future<SKPaymentTransaction> purchase(SKProduct product) {
    assert(_payment == null, 'There is already purchase in progress.');
    _payment = SKPayment.withProduct(product);
    _completer = Completer<SKPaymentTransaction>();
    StoreKit.instance.paymentQueue.addPayment(_payment);
    return _completer.future;
  }

  @override
  void didUpdateTransactions(
      SKPaymentQueue queue, List<SKPaymentTransaction> transactions) async {
    // Note that this method can be invoked by StoreKit even if there is no
    // active purchase initiated by the user (via [purchase] method), so
    // you should take this into account.
    // We only handle two states here (purchased and failed) and omit the rest
    // for brevity purposes.
    for (final tx in transactions) {
      switch (tx.transactionState) {
        case SKPaymentTransactionState.purchased:
          // Validate transaction, unlock content, etc...
          // Make sure to call `finishTransaction` when done, otherwise
          // this transaction will be redelivered by the queue on next application
          // launch.
          await queue.finishTransaction(tx);
          if (_payment == tx.payment) {
            // This transaction is related to an active purchase initiated
            // by user in UI. Signal it's been completed successfully.
            _completer.complete(tx);
            _payment = null;
            _completer = null;
          }
          break;
        case SKPaymentTransactionState.failed:
          // Purchase failed, make sure to notify the user in some way.
          await queue.finishTransaction(tx);
          if (_payment == tx.payment) {
            // This transaction is related to an active purchase as well.
            // Signal to the user that it failed. We pass the same transaction
            // object for simplicity here.
            _completer.completeError(tx);
            _payment = null;
            _completer = null;
          }
          break;
        default:
          // TODO: handle other states
          break;
      }
    }
  }
}

class _MyAppState extends State<MyApp> {
  // ID of the product we are selling.
  static const String kProductId = 'com.example.product.id';

  final ExamplePurchaseProcessor _observer = ExamplePurchaseProcessor();

  /// Whether user is allowed to make payments
  bool _canMakePayments;

  /// The product we want to provide for user to purchase.
  SKProduct _product;

  Future<SKPaymentTransaction> _purchaseFuture;

  SKPaymentTransaction _transaction;

  @override
  void initState() {
    super.initState();
    // Always register your transaction observer with StoreKit. It is highly
    // recommended to register your observer as early as possible.
    StoreKit.instance.paymentQueue.setTransactionObserver(_observer);
    // Check if user can actually make payments.
    // Note error-handling is omitted for brevity.
    StoreKit.instance.paymentQueue
        .canMakePayments()
        .then(_handleCanMakePayments);
  }

  void _handleCanMakePayments(bool value) {
    setState(() {
      _canMakePayments = value;
      StoreKit.instance.products([kProductId]).then(_handleProducts);
    });
  }

  void _handleProducts(SKProductsResponse response) {
    // Note error-handling is omitted for brevity. If you have an issue with
    // your product it will appear in [response.invalidProductIds].
    setState(() {
      _product = response.products.single;
    });
  }

  @override
  void dispose() {
    // Don't forget to remove your observer when your app state is rebuilding.
    StoreKit.instance.paymentQueue.removeTransactionObserver();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    Widget child;
    if (_purchaseFuture != null) {
      // Payment processing
      if (_transaction == null) {
        child = CircularProgressIndicator();
      } else if (_transaction.transactionState ==
          SKPaymentTransactionState.purchased) {
        child = Text('Enjoy your product');
      } else {
        child = Text('Purchase failed: ${_transaction.transactionState}');
      }
    } else if (_canMakePayments == null) {
      // Haven't initialized yet, show loader
      child = CircularProgressIndicator();
    } else if (!_canMakePayments) {
      child = Text('Payments disabled');
    } else if (_product == null) {
      child = CircularProgressIndicator();
    } else {
      child = FlatButton(
          onPressed: _purchase, child: Text('Buy for ${_product.price}'));
    }
    return new MaterialApp(
      home: new Scaffold(
        appBar: new AppBar(
          title: const Text('In-app purchases example'),
        ),
        body: new Center(child: child),
      ),
    );
  }

  void _purchase() {
    setState(() {
      // Initiate purchase flow. From this point purchase handling must be
      // done in the transaction observer's `didUpdateTransactions` method.
      _purchaseFuture = _observer.purchase(_product);
      _purchaseFuture.then(_handlePurchase).catchError(_handlePurchaseError);
    });
  }

  void _handlePurchase(SKPaymentTransaction tx) {
    setState(() {
      _transaction = tx;
    });
  }

  void _handlePurchaseError(error) {
    if (error is SKPaymentTransaction) {
      setState(() {
        _transaction = error;
      });
    } else {
      setState(() {
        // TODO: set error state
      });
    }
  }
}

Use this package as a library

1. Depend on it

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


dependencies:
  iap: ^0.1.0

2. Install it

You can install packages from the command line:

with Flutter:


$ flutter pub get

Alternatively, your editor might support flutter pub get. Check the docs for your editor to learn more.

3. Import it

Now in your Dart code, you can use:


import 'package:iap/iap.dart';
  
Popularity:
Describes how popular the package is relative to other packages. [more]
27
Health:
Code health derived from static analysis. [more]
94
Maintenance:
Reflects how tidy and up-to-date the package is. [more]
64
Overall:
Weighted score of the above. [more]
54
Learn more about scoring.

We analyzed this package on Apr 8, 2020, and provided a score, details, and suggestions below. Analysis was completed with status completed using:

  • Dart: 2.7.1
  • pana: 0.13.6
  • Flutter: 1.12.13+hotfix.8

Health suggestions

Fix lib/src/billing_client.dart. (-6.31 points)

Analysis of lib/src/billing_client.dart reported 13 hints, including:

line 51 col 10: The class 'Future' wasn't exported from 'dart:core' until version 2.1, but this code is required to be able to run on earlier versions.

line 104 col 3: The class 'Future' wasn't exported from 'dart:core' until version 2.1, but this code is required to be able to run on earlier versions.

line 121 col 3: The class 'Future' wasn't exported from 'dart:core' until version 2.1, but this code is required to be able to run on earlier versions.

line 136 col 3: The class 'Future' wasn't exported from 'dart:core' until version 2.1, but this code is required to be able to run on earlier versions.

line 152 col 3: The class 'Future' wasn't exported from 'dart:core' until version 2.1, but this code is required to be able to run on earlier versions.

Maintenance suggestions

Package is getting outdated. (-35.89 points)

The package was last published 70 weeks ago.

Dependencies

Package Constraint Resolved Available
Direct dependencies
Dart SDK >=2.0.0-dev.68.0 <3.0.0
flutter 0.0.0
Transitive dependencies
collection 1.14.11 1.14.12
meta 1.1.8
sky_engine 0.0.99
typed_data 1.1.6
vector_math 2.0.8
Dev dependencies
flutter_test
test ^1.5.1