In-app purchases and subscription management for Flutter
This document provides a comprehensive overview of the LinkFive Purchases Flutter library, allowing you to integrate in-app purchases and subscription management functionalities into your Flutter application.
What is LinkFive Purchases?
LinkFive Purchases empowers developers to seamlessly implement and manage in-app purchases and subscriptions within their Flutter applications. It simplifies the integration process, taking care of the complexities and streamlining the experience for both developers and users.
Getting Started
- Sign Up and Obtain API Key: Visit the LinkFive website (https://app.linkfive.io/sign-up) to register and acquire your unique API key (it's free!). This key is essential for initializing the LinkFive Purchases plugin within your application.
- Installation: Add the
linkfive_purchases
package to your pubspec.yaml file and runpub get
to download and install the necessary dependencies.
Core Functionalities
Supported Purchase Types:
- Subscriptions: Offer recurring billing plans that provide users with ongoing access to premium features or content within your app.
- One-Time Purchases: Enable users to purchase a product or service permanently with a single payment.
Initialization:
- Employ the
init
method to initialize the LinkFive Purchases plugin, providing your API key as an argument. You can optionally set the logging level using thelogLevel
parameter.
await LinkFivePurchases.init("API_KEY");
Fetching Products:
- Utilize the
fetchProducts
method to retrieve a list of available products from LinkFive. This method is crucial for populating your paywall or displaying relevant subscription options to users.
await LinkFivePurchases.fetchProducts();
Purchasing Products:
- Trigger the purchase flow for a specific product using the
purchase
method. This method takes aProductDetails
object as input, representing the product the user intends to purchase.
await LinkFivePurchases.purchase(productDetails);
- BETA-function: In version 4.x we also added
purchaseFuture
which will wait for the purchase to finish and return the ActiveProducts.
await LinkFivePurchases.purchaseFuture(productDetails);
Restoring Purchases:
- The
restore
method enables users to restore previously purchased products. This ensures continued access to subscribed features upon app reinstallation or device change.
await LinkFivePurchases.restore();
- BETA-function: In version 4.x we also added
restoreFuture
which will wait for the restore to finish and return the ActiveProducts.
await LinkFivePurchases.restoreFuture();
Products Stream:
- The
products
stream continuously delivers information about the user's offering. Whenever the user opens the paywall, you want to show the data that is included in this stream.
stream = LinkFivePurchases.products.listen((products) {
// Handle products here
});
Active Products Stream:
- The
activeProducts
stream continuously delivers information about the user's active and verified products. This stream proves valuable for managing access to subscription-based features or One-time purchases within your application.
stream = LinkFivePurchases.activeProducts.listen((activeProducts) {
// Handle active products here
});
Purchase in Progress Stream
The purchaseInProgressStream
provides real-time updates on the purchase process. This stream is helpful for displaying loading indicators or disabling purchase buttons while a purchase is underway.
An example riverpod Notifier would be:
/// true -> show loading indicator / disable purchase button
/// false -> disable loading indicator / enable purchase Button
class PremiumPurchaseInProgressNotifier extends Notifier<bool> {
@override
bool build() {
final streamSub =
ref.read(billingRepositoryProvider).purchaseInProgressStream().listen((bool isPurchaseInProgress) {
state = isPurchaseInProgress;
});
ref.onDispose(() {
streamSub.cancel();
});
return false;
}
}
Subscription and One-Time Purchase Offerings
A typical riverpod notifier implementation would be:
class PremiumOfferNotifier extends Notifier<LinkFiveProducts?> {
/// fetched once whenever the user enters the paywall
Future<void> fetchOffering() async {
state = await ref.read(billingRepositoryProvider).fetchOffering();
}
void purchase(LinkFiveProductDetails productDetails) {
ref.read(billingRepositoryProvider).purchase(productDetails.productDetails);
}
void restore() {
ref.read(billingRepositoryProvider).restore();
}
@override
LinkFiveProducts? build() {
return null;
}
}
And the Widget Implementation:
class _PurchasePaywall extends ConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final premiumOffer = ref.watch(premiumOfferProvider);
if (premiumOffer == null) {
// return Page Loading Widget
}
return ListView(children: [
for (final offer in premiumOffer.productDetailList)
switch (offer.productType) {
LinkFiveProductType.OneTimePurchase => LayoutBuilder(builder: (_, _) {
// build your One Time Purchase Widget
// e.g:
// Text(offer.oneTimePurchasePrice.formattedPrice)
// and later when pressed:
// onPressed: () {
// ref.read(premiumOfferProvider.notifier).purchase(offer);
// }
}),
LinkFiveProductType.Subscription => LayoutBuilder(builder: (_, _) {
// build your Subscription Purchase Widget
// use the pricing Phases:
// for (var pricingPhase in offer.pricingPhases) {
// Text(pricingPhase.formattedPrice);
// Text(pricingPhase.billingPeriod.iso8601); // e.g.: P6M
// }
// and later when pressed:
// onPressed: () {
// ref.read(premiumOfferProvider.notifier).purchase(offer);
// }
}),
}
]
);
}
}
Active & Purchased Products
A typical riverpod notifier implementation would look like this:
class PremiumPurchaseNotifier extends Notifier<bool?> {
Future<void> _initBilling() async {
final activeProducts = await ref.read(billingRepositoryProvider).load();
state = activeProducts.isNotEmpty;
print("Billing initialized $state");
}
@override
bool? build() {
_initBilling();
final purchaseStream = ref.read(billingRepositoryProvider).purchaseStream().listen((LinkFiveActiveProducts event) {
print("Purchase Update $event");
state = event.isNotEmpty;
});
ref.onDispose(() {
purchaseStream.cancel();
});
return null;
}
}
LinkFiveActiveProducts holds all active purchases, Subscriptions and One-Time Purchases.
// LinkFiveActiveProducts
activeProducts.planList;
activeProducts.oneTimePurchaseList;
Wrap LinkFive into a repository for testing
You can wrap the Purchases implementation inside a Repository pattern or use it directly.
final billingRepositoryProvider = Provider<BillingRepository>((ref) => LinkFiveBillingRepository());
class LinkFiveBillingRepository extends BillingRepository {
@override
Future<LinkFiveActiveProducts> load() async {
return LinkFivePurchases.init(
LinkFiveKey().apiKey,
logLevel: LinkFiveLogLevel.DEBUG,
);
}
@override
Future<LinkFiveProducts?> fetchOffering() {
return LinkFivePurchases.fetchProducts();
}
@override
Future<LinkFiveActiveProducts> loadActiveProducts() async {
return LinkFivePurchases.reloadActivePlans();
}
@override
void purchase(ProductDetails productDetails) async {
await LinkFivePurchases.purchase(productDetails);
}
@override
Future<void> restore() async {
await LinkFivePurchases.restore();
}
Stream<LinkFiveActiveProducts> listenToPurchases() {
return LinkFivePurchases.activeProducts;
}
@override
Stream<LinkFiveActiveProducts> purchaseStream() {
return LinkFivePurchases.activeProducts;
}
@override
Stream<bool> purchaseInProgressStream() {
return LinkFivePurchases.purchaseInProgressStream;
}
}
ProductDetails, Pricing Phase & Google‘s new Base Plans approach
Google changed how they handle subscriptions and added Base Plans & PricingPhases to it's new data model. Unfortunately, the in_app_purchase library exposes different models depending on the platform.
We decided to combine the ProductDetails class into a simple to use class called LinkFiveProductDetails
which holds pricingPhases
for both platforms, Android & iOS.
class LinkFiveProductDetails {
/// Platform dependent Product Details such as GooglePlayProductDetails or AppStoreProductDetails
final ProductDetails productDetails;
/// Base64 encoded attributes which you can define on LinkFive
final String? attributes;
/// Converts the new Google Play & AppStore Model to a known list of pricing phases
List<PricingPhase> get pricingPhases;
}
Pricing Phase
The PricingPhase class now holds all information about the product and it's phases. An example would be a FreeTrial phase and a yearly subscription as 2 elements in the PricingPhase list.
Here are all interesting parts of the class:
class PricingPhase {
/// Represents a pricing phase, describing how a user pays at a point in time.
int get billingCycleCount;
/// Billing period for which the given price applies, specified in ISO 8601 format.
Period get billingPeriod;
/// Returns formatted price for the payment cycle, including its currency sign.
String get formattedPrice;
/// Returns the price for the payment cycle in micro-units, where 1,000,000
/// micro-units equal one unit of the currency.
int get priceAmountMicros;
/// ISO 4217 e.g. EUR, USD
String get priceCurrencyCode;
/// Recurrence of the phase
Recurrence get recurrence;
}
Period & PeriodUnit class
The Period class now holds the length of a subscription.
class Period {
final int amount;
final PeriodUnit periodUnit;
}
enum PeriodUnit {
DAYS('D'),
WEEKS('W'),
MONTH('M'),
YEARS('Y');
}
A Period of 3 months would be Period(amount: 3, periodUnit: MONTH) and a year would be Period(amount: 1, periodUnit: YEAR),
From the Period class to a readable user-friendly text
We added a intl-localization-package which uses the intl
package that can help you translate the Period into a readable text.
Here is the package on pub.dev
You can use the isoCode from the billingPeriod to get a readable String:
final translationClass = pricingPhase.billingPeriod.iso8601.fromIso8601(PaywallL10NHelper.of(context));
The translation class will output:
7 days
fromP7D
1 month
fromP1M
3 months
fromP3M
(or alsoquarterly
)1 year
fromP1Y
(or alsoyearly
)
You can submit your own translation to it's github Repository.
Easy Integration with the Paywall UI package
Integrate linkfive_purchases with package in_app_purchases_paywall_ui.
- it's working with just passing the LinkFive client to the UI library
- Automatic purchase state management
- The UI is fully customizable
- You can control the UI on our Website
Purchase Page
Success Page
Page State Management
That‘s it. Now the page will automatically offer the subscriptions to the user or if the user already bought the subscription, the paywall will show the success page.
Libraries
- client/linkfive_billing_client
- client/linkfive_billing_client_interface
- client/linkfive_billing_client_test
- client/linkfive_client
- client/linkfive_client_interface
- client/linkfive_client_test
- default/default_purchase_handler
- linkfive_purchases
- LinkFive Purchases Package
- logic/extensions/initialize_extension
- logic/linkfive_purchases
- logic/linkfive_purchases_impl
- logic/linkfive_user_management
- logic/upgrade_downgrade_purchases
- models/linkfive_active_products
- models/linkfive_one_time_purchase
- models/linkfive_plan
- models/linkfive_products
- models/linkfive_response
- models/linkfive_restore_apple_item
- models/linkfive_restore_google_item
- models/one_time_purchase_price
- models/period
- models/pricing_phase
- models/product_type
- models/recurrence
- models/requests/purchase_request_google
- models/requests/purchase_request_google_otp
- models/requests/purchase_request_pricing_phase
- store/linkfive_app_data_store
- store/linkfive_prefs
- store/linkfive_store
- util/app_store_product_details_wrapper
- util/currency_symbol