Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
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
5 changes: 5 additions & 0 deletions packages/in_app_purchase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
## 0.1.0+4

* Change the `buy` methods to return `Future<bool>` instead of `void` in order
to propagate `launchBillingFlow` failures up through `google_play_connection`.

## 0.1.0+3

* Guard against multiple onSetupFinished() calls.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,20 +44,21 @@ class AppStoreConnection implements InAppPurchaseConnection {
Future<bool> isAvailable() => SKPaymentQueueWrapper.canMakePayments();

@override
void buyNonConsumable({@required PurchaseParam purchaseParam}) {
_skPaymentQueueWrapper.addPayment(SKPaymentWrapper(
Future<bool> buyNonConsumable({@required PurchaseParam purchaseParam}) async {
await _skPaymentQueueWrapper.addPayment(SKPaymentWrapper(
productIdentifier: purchaseParam.productDetails.id,
quantity: 1,
applicationUsername: purchaseParam.applicationUserName,
simulatesAskToBuyInSandbox: purchaseParam.sandboxTesting,
requestData: null));
return true; // There's no error feedback from iOS here to return.
}

@override
void buyConsumable(
Future<bool> buyConsumable(
{@required PurchaseParam purchaseParam, bool autoConsume = true}) {
assert(autoConsume == true, 'On iOS, we should always auto consume');
buyNonConsumable(purchaseParam: purchaseParam);
return buyNonConsumable(purchaseParam: purchaseParam);
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,19 +48,20 @@ class GooglePlayConnection
}

@override
void buyNonConsumable({@required PurchaseParam purchaseParam}) {
billingClient.launchBillingFlow(
Future<bool> buyNonConsumable({@required PurchaseParam purchaseParam}) async {
BillingResponse response = await billingClient.launchBillingFlow(
sku: purchaseParam.productDetails.id,
accountId: purchaseParam.applicationUserName);
return response == BillingResponse.ok;
}

@override
void buyConsumable(
Future<bool> buyConsumable(
{@required PurchaseParam purchaseParam, bool autoConsume = true}) {
if (autoConsume) {
_productIdsToConsume.add(purchaseParam.productDetails.id);
}
buyNonConsumable(purchaseParam: purchaseParam);
return buyNonConsumable(purchaseParam: purchaseParam);
}

@override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -81,14 +81,18 @@ abstract class InAppPurchaseConnection {
/// You always need to restore all the non consumable products for user when
/// they switch their phones.
///
/// This method does not return anything. Instead, after triggering this
/// method, purchase updates will be sent to [purchaseUpdatedStream]. You
/// should [Stream.listen] to [purchaseUpdatedStream] to get [PurchaseDetails]
/// objects in different [PurchaseDetails.status] and update your UI
/// accordingly. When the [PurchaseDetails.status] is
/// [PurchaseStatus.purchased] or [PurchaseStatus.error], you should deliver
/// the content or handle the error. On iOS, you also need to call
/// [completePurchase] to finish the purchasing process.
/// This method does not return the result of the purchase. Instead, after
/// triggering this method, purchase updates will be sent to
/// [purchaseUpdatedStream]. You should [Stream.listen] to
/// [purchaseUpdatedStream] to get [PurchaseDetails] objects in different
/// [PurchaseDetails.status] and update your UI accordingly. When the
/// [PurchaseDetails.status] is [PurchaseStatus.purchased] or
/// [PurchaseStatus.error], you should deliver the content or handle the
/// error. On iOS, you also need to call [completePurchase] to finish the
/// purchasing process.
///
/// This method does return whether or not the purchase request was initially
/// sent succesfully.
///
/// Consumable items are defined differently by the different underlying
/// payment platforms, and there's no way to query for whether or not the
Expand All @@ -109,7 +113,7 @@ abstract class InAppPurchaseConnection {
/// * [queryPastPurchases], for restoring non consumable products.
///
/// Calling this method for consumable items will cause unwanted behaviors!
void buyNonConsumable({@required PurchaseParam purchaseParam});
Future<bool> buyNonConsumable({@required PurchaseParam purchaseParam});

/// Buy a consumable product.
///
Expand Down Expand Up @@ -140,14 +144,17 @@ abstract class InAppPurchaseConnection {
/// will cause user never be able to buy the same item again. Manually setting
/// this to `false` on iOS will throw an `Exception`.
///
/// This method does not return anything. Instead, after triggering this
/// method, purchase updates will be sent to [purchaseUpdatedStream]. You
/// should [Stream.listen] to [purchaseUpdatedStream] to get [PurchaseDetails]
/// objects in different [PurchaseDetails.status] and update your UI
/// accordingly. When the [PurchaseDetails.status] is
/// [PurchaseStatus.purchased] or [PurchaseStatus.error], you should deliver
/// the content or handle the error, then call [completePurchase] to finish
/// the purchasing process.
/// This method does not return the result of the purchase. Instead, after
/// triggering this method, purchase updates will be sent to
/// [purchaseUpdatedStream]. You should [Stream.listen] to
/// [purchaseUpdatedStream] to get [PurchaseDetails] objects in different
/// [PurchaseDetails.status] and update your UI accordingly. When the
/// [PurchaseDetails.status] is [PurchaseStatus.purchased] or
/// [PurchaseStatus.error], you should deliver the content or handle the
/// error, then call [completePurchase] to finish the purchasing process.
///
/// This method does return whether or not the purchase request was initially
/// sent succesfully.
///
/// See also:
///
Expand All @@ -158,7 +165,7 @@ abstract class InAppPurchaseConnection {
///
/// Calling this method for non consumable items will cause unwanted
/// behaviors!
void buyConsumable(
Future<bool> buyConsumable(
{@required PurchaseParam purchaseParam, bool autoConsume = true});

/// (App Store only) Mark that purchased content has been delivered to the
Expand Down
2 changes: 1 addition & 1 deletion packages/in_app_purchase/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: in_app_purchase
description: A Flutter plugin for in-app purchases.
author: Flutter Team <flutter-dev@googlegroups.com>
homepage: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase
version: 0.1.0+3
version: 0.1.0+4

dependencies:
async: ^2.0.8
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -204,9 +204,11 @@ void main() {
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: skuDetails.toProductDetails(),
applicationUserName: accountId);
await GooglePlayConnection.instance
final bool launchResult = await GooglePlayConnection.instance
.buyNonConsumable(purchaseParam: purchaseParam);

PurchaseDetails result = await completer.future;
expect(launchResult, isTrue);
expect(result.purchaseID, 'orderID1');
expect(result.status, PurchaseStatus.purchased);
expect(result.productID, dummySkuDetails.sku);
Expand Down Expand Up @@ -301,17 +303,48 @@ void main() {
final PurchaseParam purchaseParam = PurchaseParam(
productDetails: skuDetails.toProductDetails(),
applicationUserName: accountId);
await GooglePlayConnection.instance
final bool launchResult = await GooglePlayConnection.instance
.buyConsumable(purchaseParam: purchaseParam);

// Verify that the result has succeeded
PurchaseDetails result = await completer.future;
expect(launchResult, isTrue);
expect(result.billingClientPurchase.purchaseToken,
await consumeCompleter.future);
expect(result.status, PurchaseStatus.purchased);
expect(result.error, isNull);
});

test('buyNonConsumable propagates failures to launch the billing flow',
() async {
final BillingResponse sentCode = BillingResponse.error;
stubPlatform.addResponse(
name: launchMethodName,
value: BillingResponseConverter().toJson(sentCode));

final bool result = await GooglePlayConnection.instance.buyNonConsumable(
purchaseParam: PurchaseParam(
productDetails: dummySkuDetails.toProductDetails()));

// Verify that the failure has been converted and returned
expect(result, isFalse);
});

test('buyConsumable propagates failures to launch the billing flow',
() async {
final BillingResponse sentCode = BillingResponse.error;
stubPlatform.addResponse(
name: launchMethodName,
value: BillingResponseConverter().toJson(sentCode));

final bool result = await GooglePlayConnection.instance.buyConsumable(
purchaseParam: PurchaseParam(
productDetails: dummySkuDetails.toProductDetails()));

// Verify that the failure has been converted and returned
expect(result, isFalse);
});

test('adds consumption failures to PurchaseDetails objects', () async {
final SkuDetailsWrapper skuDetails = dummySkuDetails;
final String accountId = "hashedAccountId";
Expand Down