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 6 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
2 changes: 1 addition & 1 deletion packages/in_app_purchase/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
## 0.3.2
## 0.3.0

* Migrate the `Google Play Library` to 2.0.3.
* Introduce a new class `BillingResultWrapper` which contains a detailed result of a BillingClient operation.
Expand Down
6 changes: 3 additions & 3 deletions packages/in_app_purchase/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,15 @@ for (PurchaseDetails purchase in response.pastPurchases) {
}
```

Note that the App Store does not have any APIs for querying consummable
products, and Google Play considers consummable products to no longer be owned
Note that the App Store does not have any APIs for querying consumable
products, and Google Play considers consumable products to no longer be owned
once they're marked as consumed and fails to return them here. For restoring
these across devices you'll need to persist them on your own server and query
that as well.

### Making a purchase

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -513,7 +513,7 @@ public void consumeAsync() {
}

@Test
public void acknowledgetPurchase() {
public void acknowledgePurchase() {
establishConnectedBillingClient(null, null);
ArgumentCaptor<BillingResult> resultCaptor = ArgumentCaptor.forClass(BillingResult.class);
BillingResult billingResult =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,16 @@ public void fromBillingResult() throws JSONException {
assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage());
}

@Test
public void fromBillingResult_dubugMessageNull() throws JSONException {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
public void fromBillingResult_dubugMessageNull() throws JSONException {
public void fromBillingResult_debugMessageNull() throws JSONException {

BillingResult newBillingResult =
BillingResult.newBuilder().setResponseCode(BillingClient.BillingResponseCode.OK).build();
Map<String, Object> billingResultMap = Translator.fromBillingResult(newBillingResult);

assertEquals(billingResultMap.get("responseCode"), newBillingResult.getResponseCode());
assertEquals(billingResultMap.get("debugMessage"), newBillingResult.getDebugMessage());
}

private void assertSerialized(SkuDetails expected, Map<String, Object> serialized) {
assertEquals(expected.getDescription(), serialized.get("description"));
assertEquals(expected.getFreeTrialPeriod(), serialized.get("freeTrialPeriod"));
Expand Down
2 changes: 2 additions & 0 deletions packages/in_app_purchase/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -366,12 +366,14 @@ class _MyAppState extends State<MyApp> {
} else {
if (purchaseDetails.status == PurchaseStatus.error) {
handleError(purchaseDetails.error);
return;
Copy link
Contributor

@malsabbagh malsabbagh Jan 21, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also on iOS completePurchase() needs to be called for status == PurchaseStatus.error. With this change this will be broken.

Please refer to completePurchase documentation.

/// the purchase needs to be completed if the [PurchaseDetails.status] is [PurchaseStatus.error].

} else if (purchaseDetails.status == PurchaseStatus.purchased) {
bool valid = await _verifyPurchase(purchaseDetails);
if (valid) {
deliverProduct(purchaseDetails);
} else {
_handleInvalidPurchase(purchaseDetails);
return;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am guessing this will have the same impact as line 374.

}
}
if (Platform.isAndroid) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -206,47 +206,51 @@ class BillingClient {
/// Consuming can only be done on an item that's owned, and as a result of consumption, the user will no longer own it.
/// Consumption is done asynchronously. The method returns a Future containing a [BillingResultWrapper].
///
/// The `params` must not be null.
/// The `purchaseToken` must not be null.
/// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null.
///
/// This wraps [`BillingClient#consumeAsync(String, ConsumeResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#consumeAsync(java.lang.String,%20com.android.billingclient.api.ConsumeResponseListener))
Future<BillingResultWrapper> consumeAsync(ConsumeParams params) async {
assert(params != null);
Future<BillingResultWrapper> consumeAsync(String purchaseToken,
{String developerPayload}) async {
assert(purchaseToken != null);
return BillingResultWrapper.fromJson(await channel
.invokeMapMethod<String, dynamic>(
'BillingClient#consumeAsync(String, ConsumeResponseListener)',
<String, String>{
'purchaseToken': params.purchaseToken,
'developerPayload': params.developerPayload,
'purchaseToken': purchaseToken,
'developerPayload': developerPayload,
}));
}

/// Acknowledge an In-App purchase.
/// Acknowledge an in-app purchase.
///
/// The developer is required to acknowledge that they have granted entitlement for all in-app purchases.
/// The developer must acknowledge all in-app purchases after they have been granted to the user.
/// If this doesn't happen within three days of the purchase, the purchase will be refunded.
///
/// Warning! The acknowledgement has to be happen within the 3 days of the purchase.
/// Failure to do so will result the purchase to be refunded.
/// Consumables are already implicitly acknowledged by calls to [consumeAsync] and
/// do not need to be explicitly acknowledged by using this method.
/// However this method can be called for them in order to explicitly acknowledge them if desired.
///
/// For consumable items, calling [consumeAsync] acts as an implicit acknowledgement. This method can also
/// be called for explicitly acknowledging a consumable purchase.
/// Be sure to only acknowledge a purchase after it has been granted to the user.
/// [PurchaseWrapper.purchaseState] should be [PurchaseStateWrapper.purchased] and
/// the purchase should be validated. See [Verify a purchase](https://developer.android.com/google/play/billing/billing_library_overview#Verify) on verifying purchases.
///
/// Be sure to only acknowledge a purchase when the [PurchaseWrapper.purchaseState] is [PurchaseStateWrapper.purchased].
///
/// Please refer to https://developer.android.com/google/play/billing/billing_library_overview#acknowledge for more
/// Please refer to [acknowledge](https://developer.android.com/google/play/billing/billing_library_overview#acknowledge) for more
/// details.
///
/// The `params` must not be null.
/// The `purchaseToken` must not be null.
/// The `developerPayload` is the developer data associated with the purchase to be consumed, it defaults to null.
///
/// This wraps [`BillingClient#acknowledgePurchase(String, AcknowledgePurchaseResponseListener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.html#acknowledgePurchase(com.android.billingclient.api.AcknowledgePurchaseParams,%20com.android.billingclient.api.AcknowledgePurchaseResponseListener))
Future<BillingResultWrapper> acknowledgePurchase(
AcknowledgeParams params) async {
assert(params != null);
Future<BillingResultWrapper> acknowledgePurchase(String purchaseToken,
{String developerPayload}) async {
assert(purchaseToken != null);
return BillingResultWrapper.fromJson(await channel.invokeMapMethod<String,
dynamic>(
'BillingClient#(AcknowledgePurchaseParams params, (AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)',
<String, String>{
'purchaseToken': params.purchaseToken,
'developerPayload': params.developerPayload,
'purchaseToken': purchaseToken,
'developerPayload': developerPayload,
}));
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,8 @@ import 'package:json_annotation/json_annotation.dart';
part 'enum_converters.g.dart';

/// Serializer for [BillingResponse].
///
/// Use these in `@JsonSerializable()` classes by annotating them with
/// `@BillingResponseConverter()`.
// Use these in `@JsonSerializable()` classes by annotating them with
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Why deleting the paragraph breaks here and below? I think they probably made sense as-is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

// `@BillingResponseConverter()`.
class BillingResponseConverter implements JsonConverter<BillingResponse, int> {
const BillingResponseConverter();

Expand All @@ -24,9 +23,8 @@ class BillingResponseConverter implements JsonConverter<BillingResponse, int> {
}

/// Serializer for [SkuType].
///
/// Use these in `@JsonSerializable()` classes by annotating them with
/// `@SkuTypeConverter()`.
// Use these in `@JsonSerializable()` classes by annotating them with
// `@SkuTypeConverter()`.
class SkuTypeConverter implements JsonConverter<SkuType, String> {
const SkuTypeConverter();

Expand All @@ -47,9 +45,8 @@ class _SerializedEnums {
}

/// Serializer for [PurchaseStateWrapper].
///
/// Use these in `@JsonSerializable()` classes by annotating them with
/// `@PurchaseStateConverter()`.
// Use these in `@JsonSerializable()` classes by annotating them with
// `@PurchaseStateConverter()`.
class PurchaseStateConverter
implements JsonConverter<PurchaseStateWrapper, int> {
const PurchaseStateConverter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -109,9 +109,15 @@ class PurchaseWrapper {
final String developerPayload;

/// Whether the purchase has been acknowledged.
///
/// A successful purchase has to be acknowledged within 3 days after the purchase via [BillingClient.acknowledgePurchase].
/// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases.
final bool isAcknowledged;

/// The state of purchase.
/// Determines the current state of the purchase.
///
/// [BillingClient.acknowledgePurchase] should only be called when the `purchaseState` is [PurchaseStateWrapper.purchased].
/// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases.
final PurchaseStateWrapper purchaseState;
}

Expand Down Expand Up @@ -264,57 +270,33 @@ class PurchasesHistoryResult {
final List<PurchaseHistoryRecordWrapper> purchaseHistoryRecordList;
}

/// The parameter object used when consuming a purchase.
///
/// See also [BillingClient.consumeAsync] for consuming a purchase.
class ConsumeParams {
/// Constructs the [ConsumeParams].
///
/// The `purchaseToken` must not be null.
/// The default value of `developerPayload` is null.
ConsumeParams({@required this.purchaseToken, this.developerPayload = null});

/// The developer data associated with the purchase to be consumed.
///
/// Defaults to null.
final String developerPayload;

/// The token that identifies the purchase to be consumed.
final String purchaseToken;
}

/// The parameter object used when acknowledge a purchase.
///
/// See also [BillingClient.acknowledgePurchase] for acknowledging a purchase.
class AcknowledgeParams {
/// Constructs the [AcknowledgeParams].
///
/// The `purchaseToken` must not be null.
/// The default value of `developerPayload` is null.
AcknowledgeParams(
{@required this.purchaseToken, this.developerPayload = null});

/// The developer data associated with the purchase to be acknowledged.
///
/// Defaults to null.
final String developerPayload;

/// The token that identifies the purchase to be acknowledged.
final String purchaseToken;
}

/// Possible state of a [PurchaseWrapper].
///
/// Wraps
/// [`BillingClient.api.Purchase.PurchaseState`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState.html).
/// * See also: [PurchaseWrapper].
enum PurchaseStateWrapper {
/// The state is unspecified.
///
/// No actions on the [PurchaseWrapper] should be performed on this state.
/// This is a catch-all. It should never be returned by the Play Billing Library.
@JsonValue(0)
unspecified_state,

/// The user has completed the purchase process.
///
/// The production should be delivered and then the purchase should be acknowledged.
/// * See also [BillingClient.acknowledgePurchase] for more details on acknowledging purchases.
@JsonValue(1)
purchased,

/// The user has started the purchase process.
///
/// The user should follow the instructions that were given to them by the Play
/// Billing Library to complete the purchase.
///
/// You can also choose to remind the user to complete the purchase if you detected a
/// [PurchaseWrapper] is still in the `pending` state in the future while calling [BillingClient.queryPurchases].
@JsonValue(2)
pending,
}
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class SkuDetailsResponseWrapper {
int get hashCode => hashValues(billingResult, skuDetailsList);
}

/// Params containing the response code and the debug message from In-app Billing API response.
/// Params containing the response code and the debug message from the Play Billing API response.
@JsonSerializable()
@BillingResponseConverter()
class BillingResultWrapper {
Expand All @@ -190,10 +190,10 @@ class BillingResultWrapper {
factory BillingResultWrapper.fromJson(Map map) =>
_$BillingResultWrapperFromJson(map);

/// Response code returned in In-app Billing API calls.
/// Response code returned in the Play Billing API calls.
final BillingResponse responseCode;

/// Debug message returned in In-app Billing API calls.
/// Debug message returned in the Play Billing API calls.
///
/// This message uses an en-US locale and should not be shown to users.
final String debugMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ import 'package:flutter/widgets.dart';
import 'package:in_app_purchase/src/billing_client_wrappers/enum_converters.dart';
import 'package:in_app_purchase/src/in_app_purchase/purchase_details.dart';
import '../../billing_client_wrappers.dart';
import '../../billing_client_wrappers.dart';
import '../../billing_client_wrappers.dart';
import '../../billing_client_wrappers.dart';
import 'in_app_purchase_connection.dart';
import 'product_details.dart';

Expand Down Expand Up @@ -72,20 +69,21 @@ class GooglePlayConnection

@override
Future<BillingResultWrapper> completePurchase(PurchaseDetails purchase,
{String developerPayload}) {
AcknowledgeParams params = AcknowledgeParams(
purchaseToken: purchase.verificationData.serverVerificationData,
{String developerPayload}) async {
if (purchase.billingClientPurchase.isAcknowledged) {
return BillingResultWrapper(responseCode: BillingResponse.ok);
}
return await billingClient.acknowledgePurchase(
purchase.verificationData.serverVerificationData,
developerPayload: developerPayload);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a nice thing to have for the developer in case they accidentally call this twice, but I would rather just defer to the underlying SDK at all times here instead of trying to special case based off of what we think the state of this purchase really is. I don't want to risk introducing any bugs based on our state being somehow out of sync with the reality on the platform.

return billingClient.acknowledgePurchase(params);
}

@override
Future<BillingResultWrapper> consumePurchase(PurchaseDetails purchase,
{String developerPayload}) {
ConsumeParams params = ConsumeParams(
purchaseToken: purchase.verificationData.serverVerificationData,
return billingClient.consumeAsync(
purchase.verificationData.serverVerificationData,
developerPayload: developerPayload);
return billingClient.consumeAsync(params);
}

@override
Expand Down Expand Up @@ -258,9 +256,8 @@ class GooglePlayConnection
}
final List<Future<PurchaseDetails>> purchases =
resultWrapper.purchasesList.map((PurchaseWrapper purchase) {
return _maybeAutoConsumePurchase(PurchaseDetails.fromPurchase(purchase)
..status = _buildPurchaseStatus(purchase.purchaseState)
..error = error);
return _maybeAutoConsumePurchase(
PurchaseDetails.fromPurchase(purchase)..error = error);
}).toList();
if (!purchases.isEmpty) {
return Future.wait(purchases);
Expand Down Expand Up @@ -288,21 +285,16 @@ class GooglePlayConnection
await instance.consumePurchase(purchaseDetails);
final BillingResponse consumedResponse = billingResult.responseCode;
if (consumedResponse != BillingResponse.ok) {
purchaseDetails.status = PurchaseStatus.error;
purchaseDetails.error = IAPError(
source: IAPSource.GooglePlay,
code: kConsumptionFailedErrorCode,
message: consumedResponse.toString(),
details: billingResult.debugMessage,
);
}
purchaseDetails.status = _buildPurchaseStatus(
purchaseDetails.billingClientPurchase.purchaseState);
_productIdsToConsume.remove(purchaseDetails.productID);

return purchaseDetails;
}

static PurchaseStatus _buildPurchaseStatus(PurchaseStateWrapper state) {
return PurchaseStateConverter().toPurchaseStatus(state);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ abstract class InAppPurchaseConnection {
/// purchasing process.
///
/// This method does return whether or not the purchase request was initially
/// sent succesfully.
/// sent successfully.
///
/// Consumable items are defined differently by the different underlying
/// payment platforms, and there's no way to query for whether or not the
Expand Down Expand Up @@ -174,16 +174,20 @@ abstract class InAppPurchaseConnection {
/// user.
///
/// You are responsible for completing every [PurchaseDetails] whose
/// [PurchaseDetails.status] is [PurchaseStatus.purchased].
/// Additionally, the purchase needs to be completed if the [PurchaseDetails.status]
/// [[PurchaseStatus.error].
/// [PurchaseDetails.status] is [PurchaseStatus.purchased]. Additionally on iOS,
/// the purchase needs to be completed if the [PurchaseDetails.status] is [PurchaseStatus.error].
/// Completing a [PurchaseStatus.pending] purchase will cause an exception.
/// For convenience, [PurchaseDetails.pendingCompletePurchase] indicates if a purchase is pending for completion.
///
/// The method returns a [BillingResultWrapper] to indicate a detailed status of the complete process.
/// If the result contains [BillingResponse.error] or [BillingResponse.serviceUnavailable], the developer should try
/// to complete the purchase via this method again, or retry the [completePurchase] it at a later time.
/// If the result indicates other errors, there might be some issue with
/// the app's code. The developer is responsible to fix the issue.
///
/// Warning!Fail to call this method within 3 days of the purchase will result a refund on Android.
/// Warning!Failure to call this method and get a successful response within 3 days of the purchase will result a refund on Android.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit:

Suggested change
/// Warning!Failure to call this method and get a successful response within 3 days of the purchase will result a refund on Android.
/// Warning! Failure to call this method and get a successful response within 3 days of the purchase will result a refund on Android.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

/// The [consumePurchase] acts as an implicit [completePurchase] on Android.
///
/// The optional parameter `developerPayload` only works on Android.
Future<BillingResultWrapper> completePurchase(PurchaseDetails purchase,
{String developerPayload = null});
Expand Down
Loading