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 13 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 @@ -61,7 +63,8 @@ static final class MethodNames {
static final String ACKNOWLEDGE_PURCHASE =
"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_CONNECTION_STATE = "BillingClient#getConnectionState()";;
reidbaker marked this conversation as resolved.
Show resolved Hide resolved
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 @@ -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);
}

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 @@ -146,4 +146,15 @@ class InAppPurchaseAndroidPlatformAddition
(BillingClient client) => client.isFeatureSupported(feature),
);
}

/// Returns Play billing country code based on ISO-3166-1 alpha2 format.
///
/// See: https://developer.android.com/reference/com/android/billingclient/api/BillingConfig
/// See: https://unicode.org/cldr/charts/latest/supplemental/territory_containment_un_m_49.html
Future<String> getBillingConfig() async {
return _billingClientManager.runWithClientNonRetryable(
Copy link
Member

Choose a reason for hiding this comment

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

Should this return a BillingConfigWrapper and then be also be retryable, as it returns a subclass of HasBillingResponse?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I wanted to keep the exposed api as small as possible so I thought the country code was the right level to expose. I didnt even think about if it should be retryable I just mirrored the other commands. Let me look at what that does and make a call.

Copy link
Member

Choose a reason for hiding this comment

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

I think this comment still applies

(BillingClient client) async =>
(await client.getBillingConfig()).countryCode,
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,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/packages/tree/main/packages/in_app_purchase/in_app_purchase_android
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
version: 0.3.0+17
version: 0.3.0+18

environment:
sdk: ">=3.0.0 <4.0.0"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
import 'package:in_app_purchase_android/src/billing_client_wrappers/billing_config_wrapper.dart';
import 'package:in_app_purchase_android/src/channel.dart';

import '../stub_in_app_purchase_platform.dart';
Expand Down Expand Up @@ -641,6 +642,47 @@ void main() {
expect(arguments['feature'], equals('subscriptions'));
});
});

group('billingConfig', () {
const String billingConfigMethodName = 'BillingClient#getBillingConfig()';
test('billingConfig returns object', () async {
const BillingConfigWrapper expected = BillingConfigWrapper(
countryCode: 'US',
responseCode: BillingResponse.ok,
debugMessage: '');
stubPlatform.addResponse(
name: billingConfigMethodName,
value: buildBillingConfigMap(expected),
);
final BillingConfigWrapper result =
await billingClient.getBillingConfig();
expect(result.countryCode, 'US');
expect(result, expected);
});

test('handles method channel returning null', () async {
stubPlatform.addResponse(
name: billingConfigMethodName,
);
final BillingConfigWrapper result =
await billingClient.getBillingConfig();
expect(
result,
equals(const BillingConfigWrapper(
responseCode: BillingResponse.error,
debugMessage: kInvalidBillingConfigErrorMessage,
)));
});
});
}

Map<String, dynamic> buildBillingConfigMap(BillingConfigWrapper original) {
return <String, dynamic>{
'responseCode':
const BillingResponseConverter().toJson(original.responseCode),
'debugMessage': original.debugMessage,
'countryCode': original.countryCode,
};
}

/// This allows a value of type T or T? to be treated as a value of type T?.
Expand Down