Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

[in_app_purchase] Implementation of the app facing package #3877

Merged
merged 12 commits into from
May 12, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions packages/in_app_purchase/in_app_purchase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,35 @@
## 0.6.0

As part of implementing federated architecture and making the interface compatible for other platforms this version contains the following **breaking changes**:

* Changes to the platform agnostic interface:
* If you used `InAppPurchaseConnection.instance` to access generic In App Purchase APIs, please use `InAppPurchase.instance` instead;
* The `InAppPurchaseConnection.purchaseUpdatedStream` has been renamed to `InAppPurchase.purchaseStream`;
* The `InAppPurchaseConnection.queryPastPurchases` method has been removed. Instead, you should use `InAppPurchase.restorePurchases`. This method emits each restored purchase on the `InAppPurchase.purchaseStream`, the `PurchaseDetails` object will be marked with a `status` of `PurchaseStatus.restored`;
* The `InAppPurchase.completePurchase` method no longer returns an instance `BillingWrapperResult` class (which was Android specific). Instead it will return a completed `Future` if the method executed successfully, in case of errors it will complete with an `InAppPurchaseException` describing the error.
* Android specific changes:
* The Android specific `InAppPurchaseConnection.consumePurchase` and `InAppPurchaseConnection.enablePendingPurchases` methods have been removed from the platform agnostic interface and moved to the Android specific `InAppPurchaseAndroidPlatformAddition` class:
* `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases` is a static method that should be called when initializing your App. Access the method like this: `InAppPurchaseAndroidPlatformAddition.enablePendingPurchases()` (make sure to add the following import: `import 'package:in_app_purchase_android/in_app_purchase_android.dart';`);
* To use the `InAppPurchaseAndroidPlatformAddition.consumePurchase` method, acquire an instance using the `InAppPurchase.getPlatformAddition` method. For example:
```dart
// Acquire the InAppPurchaseAndroidPlatformAddition instance.
InAppPurchaseAndroidPlatformAddition androidAddition = InAppPurchase.instance.getPlatformAddition<InAppPurchaseAndroidPlatformAddition>();
// Consume an Android purchase.
BillingResultWrapper billingResult = await androidAddition.consumePurchase(purchase);
```
* The [billing_client_wrappers](https://pub.dev/documentation/in_app_purchase_android/latest/billing_client_wrappers/billing_client_wrappers-library.html) have been moved into the [in_app_purchase_android](https://pub.dev/packages/in_app_purchase_android) package. They are still available through the [in_app_purchase](https://pub.dev/packages/in_app_purchase) plugin but to use them it is necessary to import the correct package when using them: `import 'package:in_app_purchase_android/billing_client_wrappers.dart';`;
* iOS specific changes:
* The iOS specific methods `InAppPurchaseConnection.presentCodeRedemptionSheet` and `InAppPurchaseConnection.refreshPurchaseVerificationData` methods have been removed from the platform agnostic interface and moved into the iOS specific `InAppPurchaseIosPlatformAddition` class. To use them acquire an instance through the `InAppPurchase.getPlatformAddition` method like so:
```dart
// Acquire the InAppPurchaseIosPlatformAddition instance.
InAppPurchaseIosPlatformAddition iosAddition = InAppPurchase.instance.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
// Present the code redemption sheet.
await iosAddition.presentCodeRedemptionSheet();
// Refresh purchase verification data.
PurchaseVerificationData? verificationData = await iosAddition.refreshPurchaseVerificationData();
```
* The [store_kit_wrappers](https://pub.dev/documentation/in_app_purchase_ios/latest/store_kit_wrappers/store_kit_wrappers-library.html) have been moved into the [in_app_purchase_ios](https://pub.dev/packages/in_app_purchase_ios) package. They are still available in the [in_app_purchase](https://pub.dev/packages/in_app_purchase) plugin, but to use them it is necessary to import the correct package when using them: `import 'package:in_app_purchase_ios/store_kit_wrappers.dart';`;

## 0.5.2

* Added `rawPrice` and `currencyCode` to the ProductDetails model.
Expand Down
96 changes: 44 additions & 52 deletions packages/in_app_purchase/in_app_purchase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,8 @@ can start using the plugin. Two basic options are available:
1. A generic, idiomatic Flutter API: [in_app_purchase](https://pub.dev/documentation/in_app_purchase/latest/in_app_purchase/in_app_purchase-library.html).
This API supports most use cases for loading and making purchases.

2. Platform-specific Dart APIs: [store_kit_wrappers](https://pub.dev/documentation/in_app_purchase/latest/store_kit_wrappers/store_kit_wrappers-library.html)
and [billing_client_wrappers](https://pub.dev/documentation/in_app_purchase/latest/billing_client_wrappers/billing_client_wrappers-library.html).
2. Platform-specific Dart APIs: [store_kit_wrappers](https://pub.dev/documentation/in_app_purchase_ios/latest/store_kit_wrappers/store_kit_wrappers-library.html)
and [billing_client_wrappers](https://pub.dev/documentation/in_app_purchase_android/latest/billing_client_wrappers/billing_client_wrappers-library.html).
These APIs expose platform-specific behavior and allow for more fine-tuned
control when needed. However, if you use one of these APIs, your
purchase-handling logic is significantly different for the different
Expand All @@ -70,13 +70,17 @@ This section has examples of code for the following tasks:
The following initialization code is required for Google Play:

```dart
// Import `in_app_purchase_android.dart` to be able to access the
// `InAppPurchaseAndroidPlatformAddition` class.
import 'package:in_app_purchase_android/in_app_purchase_android.dart';

void main() {
// Inform the plugin that this app supports pending purchases on Android.
// An error will occur on Android if you access the plugin `instance`
// without this call.
//
// On iOS this is a no-op.
InAppPurchaseConnection.enablePendingPurchases();
if (defaultTargetPlatform == TargetPlatform.android) {
InAppPurchaseAndroidPlatformAddition.enablePendingPurchases();
}
runApp(MyApp());
}
```
Expand All @@ -98,7 +102,7 @@ class _MyAppState extends State<MyApp> {
@override
void initState() {
final Stream purchaseUpdated =
InAppPurchaseConnection.instance.purchaseUpdatedStream;
InAppPurchase.instance.purchaseStream;
_subscription = purchaseUpdated.listen((purchaseDetailsList) {
_listenToPurchaseUpdated(purchaseDetailsList);
}, onDone: () {
Expand Down Expand Up @@ -126,7 +130,8 @@ void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
} else {
if (purchaseDetails.status == PurchaseStatus.error) {
_handleError(purchaseDetails.error!);
} else if (purchaseDetails.status == PurchaseStatus.purchased) {
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {
bool valid = await _verifyPurchase(purchaseDetails);
if (valid) {
_deliverProduct(purchaseDetails);
Expand All @@ -136,7 +141,7 @@ void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
}
}
if (purchaseDetails.pendingCompletePurchase) {
await InAppPurchaseConnection.instance
await InAppPurchase.instance
.completePurchase(purchaseDetails);
}
}
Expand All @@ -147,7 +152,7 @@ void _listenToPurchaseUpdated(List<PurchaseDetails> purchaseDetailsList) {
### Connecting to the underlying store

```dart
final bool available = await InAppPurchaseConnection.instance.isAvailable();
final bool available = await InAppPurchase.instance.isAvailable();
if (!available) {
// The store cannot be reached or accessed. Update the UI accordingly.
}
Expand All @@ -160,38 +165,25 @@ if (!available) {
// `Set<String> _kIds = <String>['product1', 'product2'].toSet()`.
const Set<String> _kIds = <String>{'product1', 'product2'};
final ProductDetailsResponse response =
await InAppPurchaseConnection.instance.queryProductDetails(_kIds);
await InAppPurchase.instance.queryProductDetails(_kIds);
if (response.notFoundIDs.isNotEmpty) {
// Handle the error.
}
List<ProductDetails> products = response.productDetails;
```

### Loading previous purchases
### Restoring previous purchases

In the following example, implement `_verifyPurchase` so that it verifies the
purchase following the best practices for each underlying store:
Restored purchases will be emitted on the `InAppPurchase.purchaseStream`, make
sure to validate restored purchases following the best practices for each
underlying store:

* [Verifying App Store purchases](https://developer.apple.com/documentation/storekit/in-app_purchase/validating_receipts_with_the_app_store)
* [Verifying Google Play purchases](https://developer.android.com/google/play/billing/security#verify)


```dart
final QueryPurchaseDetailsResponse response =
await InAppPurchaseConnection.instance.queryPastPurchases();
if (response.error != null) {
// Handle the error.
}
for (PurchaseDetails purchase in response.pastPurchases) {
// Verify the purchase following best practices for each underlying store.
_verifyPurchase(purchase);
// Deliver the purchase to the user in your app.
_deliverPurchase(purchase);
if (purchase.pendingCompletePurchase) {
// Mark that you've delivered the purchase. This is mandatory.
InAppPurchaseConnection.instance.completePurchase(purchase);
}
}
await InAppPurchase.instance.restorePurchases();
```

Note that the App Store does not have any APIs for querying consumable
Expand All @@ -203,39 +195,39 @@ that as well.
### Making a purchase

Both underlying stores handle consumable and non-consumable products differently. If
you're using `InAppPurchaseConnection`, you need to make a distinction here and
you're using `InAppPurchase`, you need to make a distinction here and
call the right purchase method for each type.

```dart
final ProductDetails productDetails = ... // Saved earlier from queryPastPurchases().
final ProductDetails productDetails = ... // Saved earlier from queryProductDetails().
final PurchaseParam purchaseParam = PurchaseParam(productDetails: productDetails);
if (_isConsumable(productDetails)) {
InAppPurchaseConnection.instance.buyConsumable(purchaseParam: purchaseParam);
InAppPurchase.instance.buyConsumable(purchaseParam: purchaseParam);
} else {
InAppPurchaseConnection.instance.buyNonConsumable(purchaseParam: purchaseParam);
InAppPurchase.instance.buyNonConsumable(purchaseParam: purchaseParam);
}
// From here the purchase flow will be handled by the underlying store.
// Updates will be delivered to the `InAppPurchaseConnection.instance.purchaseUpdatedStream`.
// Updates will be delivered to the `InAppPurchase.instance.purchaseStream`.
```

### Completing a purchase

The `InAppPurchaseConnection.purchaseUpdatedStream` will send purchase updates after
you initiate the purchase flow using `InAppPurchaseConnection.buyConsumable` or `InAppPurchaseConnection.buyNonConsumable`.
After delivering the content to the user, call
`InAppPurchaseConnection.completePurchase` to tell the App Store and
Google Play that the purchase has been finished.
The `InAppPurchase.purchaseStream` will send purchase updates after
you initiate the purchase flow using `InAppPurchase.buyConsumable`
or `InAppPurchase.buyNonConsumable`. After delivering the content to
the user, call `InAppPurchase.completePurchase` to tell the App Store
and Google Play that the purchase has been finished.

> **Warning:** Failure to call `InAppPurchaseConnection.completePurchase` and
> **Warning:** Failure to call `InAppPurchase.completePurchase` and
> get a successful response within 3 days of the purchase will result a refund.

### Upgrading or downgrading an existing in-app subscription

To upgrade/downgrade an existing in-app subscription in Google Play,
you need to provide an instance of `ChangeSubscriptionParam` with the old
`PurchaseDetails` that the user needs to migrate from, and an optional
`ProrationMode` with the `PurchaseParam` object while calling
`InAppPurchaseConnection.buyNonConsumable`.
`ProrationMode` with the `GooglePlayPurchaseParam` object while calling
`InAppPurchase.buyNonConsumable`.

The App Store does not require this because it provides a subscription
grouping mechanism. Each subscription you offer must be assigned to a
Expand All @@ -246,12 +238,12 @@ users from accidentally purchasing multiple subscriptions. Refer to the

```dart
final PurchaseDetails oldPurchaseDetails = ...;
PurchaseParam purchaseParam = PurchaseParam(
PurchaseParam purchaseParam = GooglePlayPurchaseParam(
productDetails: productDetails,
changeSubscriptionParam: ChangeSubscriptionParam(
oldPurchaseDetails: oldPurchaseDetails,
prorationMode: ProrationMode.immediateWithTimeProration));
InAppPurchaseConnection.instance
InAppPurchase.instance
.buyNonConsumable(purchaseParam: purchaseParam);
```

Expand All @@ -262,18 +254,18 @@ codes that you've set up in App Store Connect. For more information on
redeeming offer codes, see [Implementing Offer Codes in Your App](https://developer.apple.com/documentation/storekit/in-app_purchase/subscriptions_and_offers/implementing_offer_codes_in_your_app).

```dart
InAppPurchaseConnection.instance.presentCodeRedemptionSheet();
InAppPurchaseIosPlatformAddition iosPlatformAddition =
InAppPurchase.getPlatformAddition<InAppPurchaseIosPlatformAddition>();
iosPlatformAddition.presentCodeRedemptionSheet();
```

## Contributing to this plugin
> **note:** The `InAppPurchaseIosPlatformAddition` is defined in the `in_app_purchase_ios.dart`
> file so you need to import it into the file you will be using `InAppPurchaseIosPlatformAddition`:
> ```dart
> import 'package:in_app_purchase_ios/in_app_purchase_ios.dart';
> ```

This plugin uses
[json_serializable](https://pub.dev/packages/json_serializable) for the
many data structs passed between the underlying platform layers and Dart. After
editing any of the serialized data structs, rebuild the serializers by running
`flutter packages pub run build_runner build --delete-conflicting-outputs`.
`flutter packages pub run build_runner watch --delete-conflicting-outputs` will
watch the filesystem for changes.
## Contributing to this plugin

If you would like to contribute to the plugin, check out our
[contribution guide](https://github.com/flutter/plugins/blob/master/CONTRIBUTING.md).
48 changes: 0 additions & 48 deletions packages/in_app_purchase/in_app_purchase/android/build.gradle

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Loading