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 1 commit
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
4 changes: 4 additions & 0 deletions packages/in_app_purchase/in_app_purchase_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## 0.1.1

* Added support to request a list of active subscriptions and non-consumed one-time purchases on Android, through the `InAppPurchaseAndroidPlatformAddition.queryPastPurchases` method.

## 0.1.0

* Initial open-source release.
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/services.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';

import '../billing_client_wrappers.dart';
import 'types/types.dart';

/// Contains InApp Purchase features that are only available on PlayStore.
class InAppPurchaseAndroidPlatformAddition
Expand Down Expand Up @@ -52,4 +55,84 @@ class InAppPurchaseAndroidPlatformAddition
return _billingClient
.consumeAsync(purchase.verificationData.serverVerificationData);
}

/// Query all previous purchases.
///
/// The `applicationUserName` should match whatever was sent in the initial
/// `PurchaseParam`, if anything. If no `applicationUserName` was specified in
/// the initial `PurchaseParam`, use `null`.
///
/// This does not return consumed products. If you want to restore unused
/// consumable products, you need to persist consumable product information
/// for your user on your own server.
///
/// See also:
///
/// * [refreshPurchaseVerificationData], for reloading failed
/// [PurchaseDetails.verificationData].
Future<QueryPurchaseDetailsResponse> queryPastPurchases(
{String? applicationUserName}) async {
List<PurchasesResultWrapper> responses;
PlatformException? exception;
try {
responses = await Future.wait([
_billingClient.queryPurchases(SkuType.inapp),
_billingClient.queryPurchases(SkuType.subs)
]);
} on PlatformException catch (e) {
exception = e;
responses = [
PurchasesResultWrapper(
responseCode: BillingResponse.error,
purchasesList: [],
billingResult: BillingResultWrapper(
responseCode: BillingResponse.error,
debugMessage: e.details.toString(),
),
),
PurchasesResultWrapper(
responseCode: BillingResponse.error,
purchasesList: [],
billingResult: BillingResultWrapper(
responseCode: BillingResponse.error,
debugMessage: e.details.toString(),
),
)
];
}

Set errorCodeSet = responses
.where((PurchasesResultWrapper response) =>
response.responseCode != BillingResponse.ok)
.map((PurchasesResultWrapper response) =>
response.responseCode.toString())
.toSet();

String errorMessage =
errorCodeSet.isNotEmpty ? errorCodeSet.join(', ') : '';

List<GooglePlayPurchaseDetails> pastPurchases =
responses.expand((PurchasesResultWrapper response) {
return response.purchasesList;
}).map((PurchaseWrapper purchaseWrapper) {
return GooglePlayPurchaseDetails.fromPurchase(purchaseWrapper);
}).toList();

IAPError? error;
if (exception != null) {
error = IAPError(
source: kIAPSource,
code: exception.code,
message: exception.message ?? '',
details: exception.details);
} else if (errorMessage.isNotEmpty) {
error = IAPError(
source: kIAPSource,
code: kRestoredPurchaseErrorCode,
message: errorMessage);
}

return QueryPurchaseDetailsResponse(
pastPurchases: pastPurchases, error: error);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright 2013 The Flutter 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 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';

import 'types.dart';

/// The response object for fetching the past purchases.
///
/// An instance of this class is returned in [InAppPurchaseConnection.queryPastPurchases].
class QueryPurchaseDetailsResponse {
/// Creates a new [QueryPurchaseDetailsResponse] object with the provider information.
QueryPurchaseDetailsResponse({required this.pastPurchases, this.error});

/// A list of successfully fetched past purchases.
///
/// If there are no past purchases, or there is an [error] fetching past purchases,
/// this variable is an empty List.
/// You should verify the purchase data using [PurchaseDetails.verificationData] before using the [PurchaseDetails] object.
final List<GooglePlayPurchaseDetails> pastPurchases;

/// The error when fetching past purchases.
///
/// If the fetch is successful, the value is `null`.
final IAPError? error;
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ export 'change_subscription_param.dart';
export 'google_play_product_details.dart';
export 'google_play_purchase_details.dart';
export 'google_play_purchase_param.dart';
export 'query_purchase_details_response.dart';
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: in_app_purchase_android
description: An implementation for the Android platform of the Flutter `in_app_purchase` plugin. This uses the Android BillingClient APIs.
repository: https://github.com/flutter/plugins/tree/master/packages/in_app_purchase/in_app_purchase_android
version: 0.1.0
version: 0.1.1

flutter:
plugin:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart' as widgets;
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
import 'package:in_app_purchase_android/in_app_purchase_android.dart';
import 'package:in_app_purchase_android/src/billing_client_wrappers/enum_converters.dart';
import 'package:in_app_purchase_android/src/channel.dart';
import 'package:in_app_purchase_android/src/in_app_purchase_android_platform_addition.dart';

Expand Down Expand Up @@ -61,4 +63,82 @@ void main() {
expect(billingResultWrapper, equals(expectedBillingResult));
});
});

group('queryPastPurchase', () {
group('queryPurchaseDetails', () {
const String queryMethodName = 'BillingClient#queryPurchases(String)';
test('handles error', () async {
const String debugMessage = 'dummy message';
final BillingResponse responseCode = BillingResponse.developerError;
final BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: responseCode, debugMessage: debugMessage);

stubPlatform
.addResponse(name: queryMethodName, value: <dynamic, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': BillingResponseConverter().toJson(responseCode),
'purchasesList': <Map<String, dynamic>>[]
});
final QueryPurchaseDetailsResponse response =
await iapAndroidPlatformAddition.queryPastPurchases();
expect(response.pastPurchases, isEmpty);
expect(response.error, isNotNull);
expect(
response.error!.message, BillingResponse.developerError.toString());
expect(response.error!.source, kIAPSource);
});

test('returns SkuDetailsResponseWrapper', () async {
const String debugMessage = 'dummy message';
final BillingResponse responseCode = BillingResponse.ok;
final BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: responseCode, debugMessage: debugMessage);

stubPlatform
.addResponse(name: queryMethodName, value: <String, dynamic>{
'billingResult': buildBillingResultMap(expectedBillingResult),
'responseCode': BillingResponseConverter().toJson(responseCode),
'purchasesList': <Map<String, dynamic>>[
buildPurchaseMap(dummyPurchase),
]
});

// Since queryPastPurchases makes 2 platform method calls (one for each SkuType), the result will contain 2 dummyWrapper instead
// of 1.
final QueryPurchaseDetailsResponse response =
await iapAndroidPlatformAddition.queryPastPurchases();
expect(response.error, isNull);
expect(response.pastPurchases.first.purchaseID, dummyPurchase.orderId);
});

test('should store platform exception in the response', () async {
const String debugMessage = 'dummy message';

final BillingResponse responseCode = BillingResponse.developerError;
final BillingResultWrapper expectedBillingResult = BillingResultWrapper(
responseCode: responseCode, debugMessage: debugMessage);
stubPlatform.addResponse(
name: queryMethodName,
value: <dynamic, dynamic>{
'responseCode': BillingResponseConverter().toJson(responseCode),
'billingResult': buildBillingResultMap(expectedBillingResult),
'purchasesList': <Map<String, dynamic>>[]
},
additionalStepBeforeReturn: (_) {
throw PlatformException(
code: 'error_code',
message: 'error_message',
details: {'info': 'error_info'},
);
});
final QueryPurchaseDetailsResponse response =
await iapAndroidPlatformAddition.queryPastPurchases();
expect(response.pastPurchases, isEmpty);
expect(response.error, isNotNull);
expect(response.error!.code, 'error_code');
expect(response.error!.message, 'error_message');
expect(response.error!.details, {'info': 'error_info'});
});
});
});
}