Skip to content

Commit

Permalink
[in_app_purchase] Adds Dart BillingClient APIs for loading purchases (f…
Browse files Browse the repository at this point in the history
…lutter#1286)

1. Wires the Purchase data class and serializes in to Dart.
2. BillingClient now required a callback for responding to Purchase
   updates to be passed in at construction time.
3. Exposes `BillingClient#queryPurchases` and
   `BillingClient#queryPurchaseHistory`.

For now GooglePlayConnection passes in a lambda that does nothing to
`BillingClient`. This will need to be fixed when all the collections are
updated.
  • Loading branch information
Michael Klimushyn authored Mar 7, 2019
1 parent 5bea35b commit 6d7b459
Show file tree
Hide file tree
Showing 11 changed files with 416 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ static HashMap<String, Object> fromPurchase(Purchase purchase) {
info.put("signature", purchase.getSignature());
info.put("sku", purchase.getSku());
info.put("isAutoRenewing", purchase.isAutoRenewing());
info.put("originalJson", purchase.getOriginalJson());
return info;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ private void assertSerialized(Purchase expected, Map<String, Object> serialized)
assertEquals(expected.getPurchaseTime(), serialized.get("purchaseTime"));
assertEquals(expected.getPurchaseToken(), serialized.get("purchaseToken"));
assertEquals(expected.getSignature(), serialized.get("signature"));
assertEquals(expected.getOriginalJson(), serialized.get("originalJson"));
assertEquals(expected.getSku(), serialized.get("sku"));
}
}
1 change: 1 addition & 0 deletions packages/in_app_purchase/lib/billing_client_wrappers.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
// found in the LICENSE file.

export 'src/billing_client_wrappers/billing_client_wrapper.dart';
export 'src/billing_client_wrappers/purchase_wrapper.dart';
export 'src/billing_client_wrappers/sku_details_wrapper.dart';
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,33 @@ import 'package:flutter/services.dart';
import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
import '../channel.dart';
import 'purchase_wrapper.dart';
import 'sku_details_wrapper.dart';
import 'enum_converters.dart';

const String _kOnPurchasesUpdated =
'PurchasesUpdatedListener#onPurchasesUpdated(int, List<Purchase>)';
const String _kOnBillingServiceDisconnected =
'BillingClientStateListener#onBillingServiceDisconnected()';

/// Callback triggered by Play in response to purchase activity.
///
/// This callback is triggered in response to all purchase activity while an
/// instance of `BillingClient` is active. This includes purchases initiated by
/// the app ([BillingClient.launchBillingFlow]) as well as purchases made in
/// Play itself while this app is open.
///
/// This does not provide any hooks for purchases made in the past. See
/// [BillingClient.queryPurchases] and [BillingClient.queryPurchaseHistory].
///
/// All purchase information should also be verified manually, with your server
/// if at all possible. See ["Verify a
/// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify).
///
/// Wraps a
/// [`PurchasesUpdatedListener`](https://developer.android.com/reference/com/android/billingclient/api/PurchasesUpdatedListener.html).
typedef void PurchasesUpdatedListener(PurchasesResultWrapper purchasesResult);

/// This class can be used directly instead of [InAppPurchaseConnection] to call
/// Play-specific billing APIs.
///
Expand All @@ -26,8 +47,10 @@ const String _kOnBillingServiceDisconnected =
/// some minor changes to account for language differences. Callbacks have been
/// converted to futures where appropriate.
class BillingClient {
BillingClient() {
BillingClient(PurchasesUpdatedListener onPurchasesUpdated) {
assert(onPurchasesUpdated != null);
channel.setMethodCallHandler(_callHandler);
_callbacks[_kOnPurchasesUpdated] = [onPurchasesUpdated];
}

// Occasionally methods in the native layer require a Dart callback to be
Expand Down Expand Up @@ -111,8 +134,8 @@ class BillingClient {
/// to complete the transaction there.
///
/// This method returns a [BillingResponse] representing the initial attempt
/// to show the Google Play purchase screen.
/// TODO(mklim, flutter/flutter#26326): Expose onPurchasesUpdated() result.
/// to show the Google Play billing flow. Actual purchase updates are
/// delivered via the [PurchasesUpdatedListener].
///
/// This method calls through to
/// [`BillingClient#launchBillingFlow`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#launchbillingflow).
Expand All @@ -134,8 +157,52 @@ class BillingClient {
arguments));
}

/// Fetches recent purchases for the given [SkuType].
///
/// This only fetches whatever purchase history Play happens to have cached
/// in memory.
///
/// All purchase information should also be verified manually, with your server
/// if at all possible. See ["Verify a
/// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify).
///
/// This wraps [`BillingClient#queryPurchases(String
/// skutype)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchases).
Future<PurchasesResultWrapper> queryPurchases(SkuType skuType) async {
assert(skuType != null);
return PurchasesResultWrapper.fromJson(await channel.invokeMapMethod(
'BillingClient#queryPurchases(String)',
<String, dynamic>{'skuType': SkuTypeConverter().toJson(skuType)}));
}

/// Fetches purchase history for the given [SkuType].
///
/// This makes a network request via Play and returns the most recent purchase
/// for each [SkuDetailsWrapper] of the given [SkuType].
///
/// All purchase information should also be verified manually, with your
/// server if at all possible. See ["Verify a
/// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify).
///
/// This wraps [`BillingClient#queryPurchaseHistoryAsync(String skuType,
/// PurchaseHistoryResponseListener
/// listener)`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient#querypurchasehistoryasync).
Future<PurchasesResultWrapper> queryPurchaseHistory(SkuType skuType) async {
assert(skuType != null);
return PurchasesResultWrapper.fromJson(await channel.invokeMapMethod(
'BillingClient#queryPurchaseHistoryAsync(String, PurchaseHistoryResponseListener)',
<String, dynamic>{'skuType': SkuTypeConverter().toJson(skuType)}));
}

Future<void> _callHandler(MethodCall call) async {
switch (call.method) {
case _kOnPurchasesUpdated:
// The purchases updated listener is a singleton.
assert(_callbacks[_kOnPurchasesUpdated].length == 1);
final PurchasesUpdatedListener listener =
_callbacks[_kOnPurchasesUpdated].first;
listener(PurchasesResultWrapper.fromJson(call.arguments));
break;
case _kOnBillingServiceDisconnected:
final int handle = call.arguments['handle'];
await _callbacks[_kOnBillingServiceDisconnected][handle]();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// Copyright 2019 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

import 'dart:ui' show hashValues;
import 'package:flutter/foundation.dart';
import 'package:json_annotation/json_annotation.dart';
import 'enum_converters.dart';
import 'billing_client_wrapper.dart';

// WARNING: Changes to `@JsonSerializable` classes need to be reflected in the
// below generated file. Run `flutter packages pub run build_runner watch` to
// rebuild and watch for further changes.
part 'purchase_wrapper.g.dart';

/// Data structure reprenting a succesful purchase.
///
/// All purchase information should also be verified manually, with your
/// server if at all possible. See ["Verify a
/// purchase"](https://developer.android.com/google/play/billing/billing_library_overview#Verify).
///
/// This wraps [`com.android.billlingclient.api.Purchase`](https://developer.android.com/reference/com/android/billingclient/api/Purchase)
@JsonSerializable()
class PurchaseWrapper {
@visibleForTesting
PurchaseWrapper({
@required this.orderId,
@required this.packageName,
@required this.purchaseTime,
@required this.purchaseToken,
@required this.signature,
@required this.sku,
@required this.isAutoRenewing,
@required this.originalJson,
});

factory PurchaseWrapper.fromJson(Map map) => _$PurchaseWrapperFromJson(map);

@override
bool operator ==(Object other) {
if (identical(other, this)) return true;
if (other.runtimeType != runtimeType) return false;
final PurchaseWrapper typedOther = other;
return typedOther.orderId == orderId &&
typedOther.packageName == packageName &&
typedOther.purchaseTime == purchaseTime &&
typedOther.purchaseToken == purchaseToken &&
typedOther.signature == signature &&
typedOther.sku == sku &&
typedOther.isAutoRenewing == isAutoRenewing &&
typedOther.originalJson == originalJson;
}

@override
int get hashCode => hashValues(orderId, packageName, purchaseTime,
purchaseToken, signature, sku, isAutoRenewing, originalJson);

/// The unique ID for this purchase. Corresponds to the Google Payments order
/// ID.
final String orderId;

/// The package name the purchase was made from.
final String packageName;

/// When the purchase was made, as an epoch timestamp.
final int purchaseTime;

/// A unique ID for a given [SkuDetailsWrapper], user, and purchase.
final String purchaseToken;

/// Signature of purchase data, signed with the developer's private key. Uses
/// RSASSA-PKCS1-v1_5.
final String signature;

/// The product ID of this purchase.
final String sku;

/// True for subscriptions that renew automatically. Does not apply to
/// [SkuType.inapp] products.
///
/// For [SkuType.subs] this means that the subscription is canceled when it is
/// false.
final bool isAutoRenewing;

/// Details about this purchase, in JSON.
///
/// This can be used verify a purchase. See ["Verify a purchase on a
/// device"](https://developer.android.com/google/play/billing/billing_library_overview#Verify-purchase-device).
/// Note though that verifying a purchase locally is inherently insecure (see
/// the article for more details).
final String originalJson;
}

/// A data struct representing the result of a transaction.
///
/// Contains a potentially empty list of [PurchaseWrapper]s and a
/// [BillingResponse] to signify the overall state of the transaction.
///
/// Wraps [`com.android.billingclient.api.Purchase.PurchasesResult`](https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchasesResult).
@JsonSerializable()
@BillingResponseConverter()
class PurchasesResultWrapper {
PurchasesResultWrapper(
{@required BillingResponse this.responseCode,
@required List<PurchaseWrapper> this.purchasesList});

factory PurchasesResultWrapper.fromJson(Map map) =>
_$PurchasesResultWrapperFromJson(map);

@override
bool operator ==(Object other) {
if (identical(other, this)) return true;
if (other.runtimeType != runtimeType) return false;
final PurchasesResultWrapper typedOther = other;
return typedOther.responseCode == responseCode &&
typedOther.purchasesList == purchasesList;
}

@override
int get hashCode => hashValues(responseCode, purchasesList);

/// The status of the operation.
///
/// This can represent either the status of the "query purchase history" half
/// of the operation and the "user made purchases" transaction itself.
final BillingResponse responseCode;

/// The list of succesful purchases made in this transaction.
///
/// May be empty, especially if [responseCode] is not [BillingResponse.ok].
final List<PurchaseWrapper> purchasesList;
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,10 @@ import 'product_details.dart';
class GooglePlayConnection
with WidgetsBindingObserver
implements InAppPurchaseConnection {
GooglePlayConnection._() : _billingClient = BillingClient() {
GooglePlayConnection._()
: _billingClient = BillingClient((PurchasesResultWrapper _) {
// TODO(mklim): wire this in to the generic interface
}) {
_readyFuture = _connect();
WidgetsBinding.instance.addObserver(this);
}
Expand Down
Loading

0 comments on commit 6d7b459

Please sign in to comment.