Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[in_app_purchase] Add play country code api #5941

Merged
merged 25 commits into from
Jan 31, 2024
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
5be7c0a
Intial add of country code, no tests
reidbaker Jan 19, 2024
5d6f1cc
Add tests for getBillingConfig
reidbaker Jan 19, 2024
e19b644
Formating
reidbaker Jan 19, 2024
3f5a4e3
Manipulate data in translator class instead of impl
reidbaker Jan 24, 2024
337a7d7
Add dart side code to call getBillingConfig and conversion tests
reidbaker Jan 24, 2024
99af529
Correct null test to include empty country code, fix non null billing…
reidbaker Jan 25, 2024
fdc42cd
move buildBillingConfigMap to the class that uses it
reidbaker Jan 25, 2024
88ccd2a
Merge branch 'main' into i141627-country-code-api
reidbaker Jan 25, 2024
036e2f2
Write documentation and remove todos, remove unused import
reidbaker Jan 25, 2024
91950f3
Analyzer warnings
reidbaker Jan 25, 2024
7d4af42
Merge branch 'main' into i141627-country-code-api
reidbaker Jan 25, 2024
5cd0acb
java format warnings
reidbaker Jan 25, 2024
894473c
Version code change
reidbaker Jan 25, 2024
e33f22c
Restore in app purchase instructions in example readme from bad PR ht…
reidbaker Jan 26, 2024
1dc004b
Update packages/in_app_purchase/in_app_purchase_android/android/src/m…
reidbaker Jan 26, 2024
319ffe7
Add example app code that shows country code in UI rename addition ap…
reidbaker Jan 26, 2024
28e95ad
Remove "Demonstrates how to use" verbiage since that is confusing whi…
reidbaker Jan 29, 2024
3c263a8
Merge branch 'main' into i141627-country-code-api
reidbaker Jan 29, 2024
4cd74ec
Merge branch 'main' into i141627-country-code-api
reidbaker Jan 30, 2024
d7934fc
Remove changes to ios readme
reidbaker Jan 30, 2024
5252d6e
Readme link to signing docs and changelog verbiage update
reidbaker Jan 30, 2024
5e363fc
Restore getCountryCode tests
reidbaker Jan 30, 2024
62477c4
Use run with client since BillingConfigWrapper uses a billing result
reidbaker Jan 30, 2024
a514dc1
Merge branch 'main' into i141627-country-code-api
reidbaker Jan 30, 2024
f047aac
Use run with client since BillingConfigWrapper uses a billing result
reidbaker Jan 30, 2024
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
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
## NEXT
## 0.3.0+18

* Adds country code api for android.
reidbaker marked this conversation as resolved.
Show resolved Hide resolved
* Updates compileSdk version to 34.

## 0.3.0+17
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ android {
if (project.android.hasProperty("namespace")) {
namespace 'io.flutter.plugins.inapppurchase'
}

compileSdk 34

defaultConfig {
minSdkVersion 16
minSdk 19
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
Expand Down Expand Up @@ -61,7 +62,7 @@ dependencies {
// org.jetbrains.kotlin:kotlin-bom artifact purpose is to align kotlin stdlib and related code versions.
// See: https://youtrack.jetbrains.com/issue/KT-55297/kotlin-stdlib-should-declare-constraints-on-kotlin-stdlib-jdk8-and-kotlin-stdlib-jdk7
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.8.22"))
implementation 'com.android.billingclient:billing:6.0.1'
implementation 'com.android.billingclient:billing:6.1.0'
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.json:json:20231013'
testImplementation 'org.mockito:mockito-core:5.4.0'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

package io.flutter.plugins.inapppurchase;

import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig;
import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult;
import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList;
import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList;
Expand All @@ -25,6 +26,7 @@
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.GetBillingConfigParams;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.QueryProductDetailsParams;
import com.android.billingclient.api.QueryProductDetailsParams.Product;
Expand Down Expand Up @@ -62,6 +64,7 @@ static final class MethodNames {
"BillingClient#acknowledgePurchase(AcknowledgePurchaseParams, AcknowledgePurchaseResponseListener)";
static final String IS_FEATURE_SUPPORTED = "BillingClient#isFeatureSupported(String)";
static final String GET_CONNECTION_STATE = "BillingClient#getConnectionState()";
static final String GET_BILLING_CONFIG = "BillingClient#getBillingConfig()";

private MethodNames() {}
}
Expand Down Expand Up @@ -184,11 +187,22 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result
case MethodNames.GET_CONNECTION_STATE:
getConnectionState(result);
break;
case MethodNames.GET_BILLING_CONFIG:
getBillingConfig(result);
break;
default:
result.notImplemented();
}
}

private void getBillingConfig(final MethodChannel.Result result) {
billingClient.getBillingConfigAsync(
GetBillingConfigParams.newBuilder().build(),
(billingResult, billingConfig) -> {
result.success(fromBillingConfig(billingResult, billingConfig));
});
}

private void endConnection(final MethodChannel.Result result) {
endBillingClientConnection();
result.success(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import com.android.billingclient.api.AccountIdentifiers;
import com.android.billingclient.api.BillingConfig;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.Purchase;
Expand Down Expand Up @@ -231,6 +232,14 @@ static HashMap<String, Object> fromBillingResult(BillingResult billingResult) {
return info;
}

/** Converter from {@link BillingResult} and {@link BillingConfig} to map. */
static HashMap<String, Object> fromBillingConfig(
BillingResult result, BillingConfig billingConfig) {
HashMap<String, Object> info = fromBillingResult(result);
info.put("countryCode", billingConfig.getCountryCode());
return info;
}

/**
* Gets the symbol of for the given currency code for the default {@link Locale.Category#DISPLAY
* DISPLAY} locale. For example, for the US Dollar, the symbol is "$" if the default locale is the
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.ACKNOWLEDGE_PURCHASE;
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.CONSUME_PURCHASE_ASYNC;
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.END_CONNECTION;
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.GET_BILLING_CONFIG;
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.IS_FEATURE_SUPPORTED;
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.IS_READY;
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.LAUNCH_BILLING_FLOW;
Expand All @@ -16,6 +17,7 @@
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC;
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.START_CONNECTION;
import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED;
import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig;
import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult;
import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList;
import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList;
Expand Down Expand Up @@ -46,10 +48,13 @@
import com.android.billingclient.api.AcknowledgePurchaseResponseListener;
import com.android.billingclient.api.BillingClient;
import com.android.billingclient.api.BillingClientStateListener;
import com.android.billingclient.api.BillingConfig;
import com.android.billingclient.api.BillingConfigResponseListener;
import com.android.billingclient.api.BillingFlowParams;
import com.android.billingclient.api.BillingResult;
import com.android.billingclient.api.ConsumeParams;
import com.android.billingclient.api.ConsumeResponseListener;
import com.android.billingclient.api.GetBillingConfigParams;
import com.android.billingclient.api.ProductDetails;
import com.android.billingclient.api.ProductDetailsResponseListener;
import com.android.billingclient.api.Purchase;
Expand Down Expand Up @@ -90,6 +95,7 @@ public class MethodCallHandlerTest {
@Mock Context context;
@Mock ActivityPluginBinding mockActivityPluginBinding;
@Captor ArgumentCaptor<HashMap<String, Object>> resultCaptor;
@Mock BillingConfig mockBillingConfig;

@Before
public void setUp() {
Expand Down Expand Up @@ -185,6 +191,35 @@ public void startConnection_multipleCalls() {
verify(result, times(1)).success(any());
}

@Test
public void getBillingConfigSuccess() {
mockStartConnection();
ArgumentCaptor<GetBillingConfigParams> paramsCaptor =
ArgumentCaptor.forClass(GetBillingConfigParams.class);
ArgumentCaptor<BillingConfigResponseListener> listenerCaptor =
ArgumentCaptor.forClass(BillingConfigResponseListener.class);
MethodCall billingCall = new MethodCall(GET_BILLING_CONFIG, null);
methodChannelHandler.onMethodCall(billingCall, mock(Result.class));
BillingResult billingResult =
BillingResult.newBuilder()
.setResponseCode(100)
.setDebugMessage("dummy debug message")
.build();
final String expectedCountryCode = "US";
final HashMap<String, Object> expectedResult = fromBillingResult(billingResult);
expectedResult.put("countryCode", expectedCountryCode);

when(mockBillingConfig.getCountryCode()).thenReturn(expectedCountryCode);
doNothing()
.when(mockBillingClient)
.getBillingConfigAsync(paramsCaptor.capture(), listenerCaptor.capture());

methodChannelHandler.onMethodCall(billingCall, result);
listenerCaptor.getValue().onBillingConfigResponse(billingResult, mockBillingConfig);

verify(result, times(1)).success(fromBillingConfig(billingResult, mockBillingConfig));
}

@Test
public void endConnection() {
// Set up a connected BillingClient instance
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,57 @@ package.

Unless you are making changes to this implementation package, this example is
very unlikely to be relevant.

# In App Purchase Example

### Preparation

There's a significant amount of setup required for testing in-app purchases
successfully, including registering new app IDs and store entries to use for
testing in the Play Developer Console. Google Play requires developers to
configure an app with in-app items for purchase to call their in-app-purchase
APIs. The Google Play Store has extensive documentation on how to do this, and
we've also included a high level guide below.

* [Google Play Billing Overview](https://developer.android.com/google/play/billing/billing_overview)

### Android

1. Create a new app in the [Play Developer
Console](https://play.google.com/apps/publish/) (PDC).

2. Sign up for a merchant's account in the PDC.

3. Create IAPs in the PDC available for purchase in the app. The example assumes
the following SKU IDs exist:

- `consumable`: A managed product.
- `upgrade`: A managed product.
- `subscription_silver`: A lower level subscription.
- `subscription_gold`: A higher level subscription.

Make sure that all of the products are set to `ACTIVE`.

4. Update `APP_ID` in `example/android/app/build.gradle` to match your package
ID in the PDC.

5. Create an `example/android/keystore.properties` file with all your signing
information. `keystore.example.properties` exists as an example to follow.
It's impossible to use any of the `BillingClient` APIs from an unsigned APK.
See
[here](https://developer.android.com/studio/publish/app-signing#secure-shared-keystore)
and [here](https://developer.android.com/studio/publish/app-signing#sign-apk)
for more information.
reidbaker marked this conversation as resolved.
Show resolved Hide resolved

6. Build a signed apk. `flutter build apk` will work for this, the gradle files
in this project have been configured to sign even debug builds.

7. Upload the signed APK from step 6 to the PDC, and publish that to the alpha
test channel. Add your test account as an approved tester. The
`BillingClient` APIs won't work unless the app has been fully published to
the alpha channel and is being used by an authorized test account. See
[here](https://support.google.com/googleplay/android-developer/answer/3131213)
for more info.

8. Sign in to the test device with the test account from step #7. Then use
`flutter run` to install the app to the device and test like normal.
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ class _MyAppState extends State<_MyApp> {
List<ProductDetails> _products = <ProductDetails>[];
List<PurchaseDetails> _purchases = <PurchaseDetails>[];
List<String> _consumables = <String>[];
String _countryCode = '';
bool _isAvailable = false;
bool _purchasePending = false;
bool _loading = true;
Expand Down Expand Up @@ -228,6 +229,11 @@ class _MyAppState extends State<_MyApp> {
'This app needs special configuration to run. Please see example/README.md for instructions.')));
}

productList.add(ListTile(
title: Text('User Country Code',
style: TextStyle(color: ThemeData.light().colorScheme.error)),
subtitle: Text(_countryCode)));

// This loading previous purchases code is just a demo. Please do not use this as it is.
// In your app you should always verify the purchase data using the `verificationData` inside the [PurchaseDetails] object before trusting it.
// We recommend that you use your own server to verify the purchase data.
Expand Down Expand Up @@ -346,6 +352,12 @@ class _MyAppState extends State<_MyApp> {
});
}

Future<void> deliverCountryCode(String countryCode) async {
setState(() {
_countryCode = countryCode;
});
}

Future<void> deliverProduct(PurchaseDetails purchaseDetails) async {
// IMPORTANT!! Always verify purchase details before delivering the product.
if (purchaseDetails.productID == _kConsumableId) {
Expand Down Expand Up @@ -385,6 +397,10 @@ class _MyAppState extends State<_MyApp> {
if (purchaseDetails.status == PurchaseStatus.pending) {
showPendingUI();
} else {
final InAppPurchaseAndroidPlatformAddition addition =
InAppPurchasePlatformAddition.instance!
as InAppPurchaseAndroidPlatformAddition;
unawaited(deliverCountryCode(await addition.getCountryCode()));
if (purchaseDetails.status == PurchaseStatus.error) {
handleError(purchaseDetails.error!);
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
Expand All @@ -399,10 +415,6 @@ class _MyAppState extends State<_MyApp> {
}

if (!_kAutoConsume && purchaseDetails.productID == _kConsumableId) {
final InAppPurchaseAndroidPlatformAddition addition =
InAppPurchasePlatformAddition.instance!
as InAppPurchaseAndroidPlatformAddition;

await addition.consumePurchase(purchaseDetails);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import 'package:json_annotation/json_annotation.dart';

import '../../billing_client_wrappers.dart';
import '../channel.dart';
import 'billing_config_wrapper.dart';

part 'billing_client_wrapper.g.dart';

Expand Down Expand Up @@ -324,6 +325,21 @@ class BillingClient {
return result ?? false;
}

/// BillingConfig method channel string identifier.
reidbaker marked this conversation as resolved.
Show resolved Hide resolved
//
// Must match the value of GET_BILLING_CONFIG in
// ../../../android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java
@visibleForTesting
final String getBillingConfigMethodString =
'BillingClient#getBillingConfig()';

/// Fetches billing config info into a [BillingConfigWrapper] object.
Future<BillingConfigWrapper> getBillingConfig() async {
return BillingConfigWrapper.fromJson((await channel
.invokeMapMethod<String, dynamic>(getBillingConfigMethodString)) ??
<String, dynamic>{});
}

/// The method call handler for [channel].
@visibleForTesting
Future<void> callHandler(MethodCall call) async {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// 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:flutter/material.dart';
import 'package:json_annotation/json_annotation.dart';

import '../../billing_client_wrappers.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 'billing_config_wrapper.g.dart';

/// The error message shown when the map represents billing config is invalid from method channel.
///
/// This usually indicates a serious underlining code issue in the plugin.
@visibleForTesting
const String kInvalidBillingConfigErrorMessage =
'Invalid billing config map from method channel.';

/// Params containing the response code and the debug message from the Play Billing API response.
@JsonSerializable()
@BillingResponseConverter()
@immutable
class BillingConfigWrapper implements HasBillingResponse {
/// Constructs the object with [responseCode] and [debugMessage].
const BillingConfigWrapper(
{required this.responseCode, this.debugMessage, this.countryCode = ''});

/// Constructs an instance of this from a key value map of data.
///
/// The map needs to have named string keys with values matching the names and
/// types of all of the members on this class.
factory BillingConfigWrapper.fromJson(Map<String, dynamic>? map) {
if (map == null || map.isEmpty) {
return const BillingConfigWrapper(
responseCode: BillingResponse.error,
debugMessage: kInvalidBillingConfigErrorMessage,
);
}
return _$BillingConfigWrapperFromJson(map);
}

/// Response code returned in the Play Billing API calls.
@override
final BillingResponse responseCode;

/// Debug message returned in the Play Billing API calls.
///
/// Defaults to `null`.
/// This message uses an en-US locale and should not be shown to users.
@JsonKey(defaultValue: '')
final String? debugMessage;

/// https://developer.android.com/reference/com/android/billingclient/api/BillingConfig#getCountryCode()
@JsonKey(defaultValue: '')
final String countryCode;

@override
bool operator ==(Object other) {
if (other.runtimeType != runtimeType) {
return false;
}

return other is BillingConfigWrapper &&
other.responseCode == responseCode &&
other.debugMessage == debugMessage &&
other.countryCode == countryCode;
}

@override
int get hashCode => Object.hash(responseCode, debugMessage, countryCode);
}
Loading