mirror of
https://github.com/flutter/packages.git
synced 2025-07-01 23:51:55 +08:00
[in_app_purchase_android] Add UserChoiceBilling mode. (#6162)
Add UserChoiceBilling billing mode option. Fixes flutter/flutter/issues/143004 Left in draft until: This does not have an End to end working example with play integration. I am currently stuck at the server side play integration part.
This commit is contained in:
@ -1,5 +1,6 @@
|
|||||||
## NEXT
|
## 0.3.2
|
||||||
|
|
||||||
|
* Adds UserChoiceBilling APIs to platform addition.
|
||||||
* Updates minimum supported SDK version to Flutter 3.13/Dart 3.1.
|
* Updates minimum supported SDK version to Flutter 3.13/Dart 3.1.
|
||||||
|
|
||||||
## 0.3.1
|
## 0.3.1
|
||||||
|
@ -6,7 +6,9 @@ package io.flutter.plugins.inapppurchase;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import com.android.billingclient.api.BillingClient;
|
import com.android.billingclient.api.BillingClient;
|
||||||
|
import com.android.billingclient.api.UserChoiceBillingListener;
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
|
|
||||||
/** Responsible for creating a {@link BillingClient} object. */
|
/** Responsible for creating a {@link BillingClient} object. */
|
||||||
@ -22,5 +24,8 @@ interface BillingClientFactory {
|
|||||||
* @return The {@link BillingClient} object that is created.
|
* @return The {@link BillingClient} object that is created.
|
||||||
*/
|
*/
|
||||||
BillingClient createBillingClient(
|
BillingClient createBillingClient(
|
||||||
@NonNull Context context, @NonNull MethodChannel channel, int billingChoiceMode);
|
@NonNull Context context,
|
||||||
|
@NonNull MethodChannel channel,
|
||||||
|
int billingChoiceMode,
|
||||||
|
@Nullable UserChoiceBillingListener userChoiceBillingListener);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,10 @@ package io.flutter.plugins.inapppurchase;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import com.android.billingclient.api.BillingClient;
|
import com.android.billingclient.api.BillingClient;
|
||||||
|
import com.android.billingclient.api.UserChoiceBillingListener;
|
||||||
|
import io.flutter.Log;
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
import io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.BillingChoiceMode;
|
import io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.BillingChoiceMode;
|
||||||
|
|
||||||
@ -15,11 +18,34 @@ final class BillingClientFactoryImpl implements BillingClientFactory {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public BillingClient createBillingClient(
|
public BillingClient createBillingClient(
|
||||||
@NonNull Context context, @NonNull MethodChannel channel, int billingChoiceMode) {
|
@NonNull Context context,
|
||||||
|
@NonNull MethodChannel channel,
|
||||||
|
int billingChoiceMode,
|
||||||
|
@Nullable UserChoiceBillingListener userChoiceBillingListener) {
|
||||||
BillingClient.Builder builder = BillingClient.newBuilder(context).enablePendingPurchases();
|
BillingClient.Builder builder = BillingClient.newBuilder(context).enablePendingPurchases();
|
||||||
if (billingChoiceMode == BillingChoiceMode.ALTERNATIVE_BILLING_ONLY) {
|
switch (billingChoiceMode) {
|
||||||
// https://developer.android.com/google/play/billing/alternative/alternative-billing-without-user-choice-in-app
|
case BillingChoiceMode.ALTERNATIVE_BILLING_ONLY:
|
||||||
builder.enableAlternativeBillingOnly();
|
// https://developer.android.com/google/play/billing/alternative/alternative-billing-without-user-choice-in-app
|
||||||
|
builder.enableAlternativeBillingOnly();
|
||||||
|
break;
|
||||||
|
case BillingChoiceMode.USER_CHOICE_BILLING:
|
||||||
|
if (userChoiceBillingListener != null) {
|
||||||
|
// https://developer.android.com/google/play/billing/alternative/alternative-billing-with-user-choice-in-app
|
||||||
|
builder.enableUserChoiceBilling(userChoiceBillingListener);
|
||||||
|
} else {
|
||||||
|
Log.e(
|
||||||
|
"BillingClientFactoryImpl",
|
||||||
|
"userChoiceBillingListener null when USER_CHOICE_BILLING set. Defaulting to PLAY_BILLING_ONLY");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case BillingChoiceMode.PLAY_BILLING_ONLY:
|
||||||
|
// Do nothing.
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
Log.e(
|
||||||
|
"BillingClientFactoryImpl",
|
||||||
|
"Unknown BillingChoiceMode " + billingChoiceMode + ", Defaulting to PLAY_BILLING_ONLY");
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return builder.setListener(new PluginPurchaseListener(channel)).build();
|
return builder.setListener(new PluginPurchaseListener(channel)).build();
|
||||||
}
|
}
|
||||||
|
@ -10,6 +10,7 @@ import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult;
|
|||||||
import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList;
|
import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList;
|
||||||
import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList;
|
import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList;
|
||||||
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;
|
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;
|
||||||
|
import static io.flutter.plugins.inapppurchase.Translator.fromUserChoiceDetails;
|
||||||
import static io.flutter.plugins.inapppurchase.Translator.toProductList;
|
import static io.flutter.plugins.inapppurchase.Translator.toProductList;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
@ -33,6 +34,7 @@ import com.android.billingclient.api.QueryProductDetailsParams;
|
|||||||
import com.android.billingclient.api.QueryProductDetailsParams.Product;
|
import com.android.billingclient.api.QueryProductDetailsParams.Product;
|
||||||
import com.android.billingclient.api.QueryPurchaseHistoryParams;
|
import com.android.billingclient.api.QueryPurchaseHistoryParams;
|
||||||
import com.android.billingclient.api.QueryPurchasesParams;
|
import com.android.billingclient.api.QueryPurchasesParams;
|
||||||
|
import com.android.billingclient.api.UserChoiceBillingListener;
|
||||||
import io.flutter.plugin.common.MethodCall;
|
import io.flutter.plugin.common.MethodCall;
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@ -72,6 +74,8 @@ class MethodCallHandlerImpl
|
|||||||
"BillingClient#createAlternativeBillingOnlyReportingDetails()";
|
"BillingClient#createAlternativeBillingOnlyReportingDetails()";
|
||||||
static final String SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG =
|
static final String SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG =
|
||||||
"BillingClient#showAlternativeBillingOnlyInformationDialog()";
|
"BillingClient#showAlternativeBillingOnlyInformationDialog()";
|
||||||
|
static final String USER_SELECTED_ALTERNATIVE_BILLING =
|
||||||
|
"UserChoiceBillingListener#userSelectedAlternativeBilling(UserChoiceDetails)";
|
||||||
|
|
||||||
private MethodNames() {}
|
private MethodNames() {}
|
||||||
}
|
}
|
||||||
@ -94,6 +98,7 @@ class MethodCallHandlerImpl
|
|||||||
static final class BillingChoiceMode {
|
static final class BillingChoiceMode {
|
||||||
static final int PLAY_BILLING_ONLY = 0;
|
static final int PLAY_BILLING_ONLY = 0;
|
||||||
static final int ALTERNATIVE_BILLING_ONLY = 1;
|
static final int ALTERNATIVE_BILLING_ONLY = 1;
|
||||||
|
static final int USER_CHOICE_BILLING = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new
|
// TODO(gmackall): Replace uses of deprecated ProrationMode enum values with new
|
||||||
@ -507,9 +512,10 @@ class MethodCallHandlerImpl
|
|||||||
private void startConnection(
|
private void startConnection(
|
||||||
final int handle, final MethodChannel.Result result, int billingChoiceMode) {
|
final int handle, final MethodChannel.Result result, int billingChoiceMode) {
|
||||||
if (billingClient == null) {
|
if (billingClient == null) {
|
||||||
|
UserChoiceBillingListener listener = getUserChoiceBillingListener(billingChoiceMode);
|
||||||
billingClient =
|
billingClient =
|
||||||
billingClientFactory.createBillingClient(
|
billingClientFactory.createBillingClient(
|
||||||
applicationContext, methodChannel, billingChoiceMode);
|
applicationContext, methodChannel, billingChoiceMode, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
billingClient.startConnection(
|
billingClient.startConnection(
|
||||||
@ -537,6 +543,19 @@ class MethodCallHandlerImpl
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private UserChoiceBillingListener getUserChoiceBillingListener(int billingChoiceMode) {
|
||||||
|
UserChoiceBillingListener listener = null;
|
||||||
|
if (billingChoiceMode == BillingChoiceMode.USER_CHOICE_BILLING) {
|
||||||
|
listener =
|
||||||
|
userChoiceDetails -> {
|
||||||
|
final Map<String, Object> arguments = fromUserChoiceDetails(userChoiceDetails);
|
||||||
|
methodChannel.invokeMethod(MethodNames.USER_SELECTED_ALTERNATIVE_BILLING, arguments);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return listener;
|
||||||
|
}
|
||||||
|
|
||||||
private void acknowledgePurchase(String purchaseToken, final MethodChannel.Result result) {
|
private void acknowledgePurchase(String purchaseToken, final MethodChannel.Result result) {
|
||||||
if (billingClientError(result)) {
|
if (billingClientError(result)) {
|
||||||
return;
|
return;
|
||||||
|
@ -14,6 +14,8 @@ import com.android.billingclient.api.ProductDetails;
|
|||||||
import com.android.billingclient.api.Purchase;
|
import com.android.billingclient.api.Purchase;
|
||||||
import com.android.billingclient.api.PurchaseHistoryRecord;
|
import com.android.billingclient.api.PurchaseHistoryRecord;
|
||||||
import com.android.billingclient.api.QueryProductDetailsParams;
|
import com.android.billingclient.api.QueryProductDetailsParams;
|
||||||
|
import com.android.billingclient.api.UserChoiceDetails;
|
||||||
|
import com.android.billingclient.api.UserChoiceDetails.Product;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.Currency;
|
import java.util.Currency;
|
||||||
@ -233,6 +235,34 @@ import java.util.Map;
|
|||||||
return info;
|
return info;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static HashMap<String, Object> fromUserChoiceDetails(UserChoiceDetails userChoiceDetails) {
|
||||||
|
HashMap<String, Object> info = new HashMap<>();
|
||||||
|
info.put("externalTransactionToken", userChoiceDetails.getExternalTransactionToken());
|
||||||
|
info.put("originalExternalTransactionId", userChoiceDetails.getOriginalExternalTransactionId());
|
||||||
|
info.put("products", fromProductsList(userChoiceDetails.getProducts()));
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
|
static List<HashMap<String, Object>> fromProductsList(List<Product> productsList) {
|
||||||
|
if (productsList.isEmpty()) {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
ArrayList<HashMap<String, Object>> output = new ArrayList<>();
|
||||||
|
for (Product product : productsList) {
|
||||||
|
output.add(fromProduct(product));
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
static HashMap<String, Object> fromProduct(Product product) {
|
||||||
|
HashMap<String, Object> info = new HashMap<>();
|
||||||
|
info.put("id", product.getId());
|
||||||
|
info.put("offerToken", product.getOfferToken());
|
||||||
|
info.put("productType", product.getType());
|
||||||
|
|
||||||
|
return info;
|
||||||
|
}
|
||||||
|
|
||||||
/** Converter from {@link BillingResult} and {@link BillingConfig} to map. */
|
/** Converter from {@link BillingResult} and {@link BillingConfig} to map. */
|
||||||
static HashMap<String, Object> fromBillingConfig(
|
static HashMap<String, Object> fromBillingConfig(
|
||||||
BillingResult result, BillingConfig billingConfig) {
|
BillingResult result, BillingConfig billingConfig) {
|
||||||
|
@ -20,6 +20,7 @@ import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames
|
|||||||
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC;
|
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.QUERY_PURCHASE_HISTORY_ASYNC;
|
||||||
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG;
|
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.SHOW_ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG;
|
||||||
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.START_CONNECTION;
|
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.START_CONNECTION;
|
||||||
|
import static io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodNames.USER_SELECTED_ALTERNATIVE_BILLING;
|
||||||
import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED;
|
import static io.flutter.plugins.inapppurchase.PluginPurchaseListener.ON_PURCHASES_UPDATED;
|
||||||
import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails;
|
import static io.flutter.plugins.inapppurchase.Translator.fromAlternativeBillingOnlyReportingDetails;
|
||||||
import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig;
|
import static io.flutter.plugins.inapppurchase.Translator.fromBillingConfig;
|
||||||
@ -27,6 +28,7 @@ import static io.flutter.plugins.inapppurchase.Translator.fromBillingResult;
|
|||||||
import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList;
|
import static io.flutter.plugins.inapppurchase.Translator.fromProductDetailsList;
|
||||||
import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList;
|
import static io.flutter.plugins.inapppurchase.Translator.fromPurchaseHistoryRecordList;
|
||||||
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;
|
import static io.flutter.plugins.inapppurchase.Translator.fromPurchasesList;
|
||||||
|
import static io.flutter.plugins.inapppurchase.Translator.fromUserChoiceDetails;
|
||||||
import static java.util.Arrays.asList;
|
import static java.util.Arrays.asList;
|
||||||
import static java.util.Collections.singletonList;
|
import static java.util.Collections.singletonList;
|
||||||
import static java.util.Collections.unmodifiableList;
|
import static java.util.Collections.unmodifiableList;
|
||||||
@ -73,6 +75,8 @@ import com.android.billingclient.api.PurchasesResponseListener;
|
|||||||
import com.android.billingclient.api.QueryProductDetailsParams;
|
import com.android.billingclient.api.QueryProductDetailsParams;
|
||||||
import com.android.billingclient.api.QueryPurchaseHistoryParams;
|
import com.android.billingclient.api.QueryPurchaseHistoryParams;
|
||||||
import com.android.billingclient.api.QueryPurchasesParams;
|
import com.android.billingclient.api.QueryPurchasesParams;
|
||||||
|
import com.android.billingclient.api.UserChoiceBillingListener;
|
||||||
|
import com.android.billingclient.api.UserChoiceDetails;
|
||||||
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
|
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
|
||||||
import io.flutter.plugin.common.MethodCall;
|
import io.flutter.plugin.common.MethodCall;
|
||||||
import io.flutter.plugin.common.MethodChannel;
|
import io.flutter.plugin.common.MethodChannel;
|
||||||
@ -82,6 +86,7 @@ import io.flutter.plugins.inapppurchase.MethodCallHandlerImpl.MethodArgs;
|
|||||||
import java.lang.reflect.Constructor;
|
import java.lang.reflect.Constructor;
|
||||||
import java.lang.reflect.InvocationTargetException;
|
import java.lang.reflect.InvocationTargetException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
@ -92,6 +97,7 @@ import org.junit.Test;
|
|||||||
import org.mockito.ArgumentCaptor;
|
import org.mockito.ArgumentCaptor;
|
||||||
import org.mockito.Captor;
|
import org.mockito.Captor;
|
||||||
import org.mockito.Mock;
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.Mockito;
|
||||||
import org.mockito.MockitoAnnotations;
|
import org.mockito.MockitoAnnotations;
|
||||||
import org.mockito.Spy;
|
import org.mockito.Spy;
|
||||||
import org.mockito.stubbing.Answer;
|
import org.mockito.stubbing.Answer;
|
||||||
@ -107,15 +113,23 @@ public class MethodCallHandlerTest {
|
|||||||
@Mock ActivityPluginBinding mockActivityPluginBinding;
|
@Mock ActivityPluginBinding mockActivityPluginBinding;
|
||||||
@Captor ArgumentCaptor<HashMap<String, Object>> resultCaptor;
|
@Captor ArgumentCaptor<HashMap<String, Object>> resultCaptor;
|
||||||
|
|
||||||
|
private final int DEFAULT_HANDLE = 1;
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
public void setUp() {
|
public void setUp() {
|
||||||
MockitoAnnotations.openMocks(this);
|
MockitoAnnotations.openMocks(this);
|
||||||
// Use the same client no matter if alternative billing is enabled or not.
|
// Use the same client no matter if alternative billing is enabled or not.
|
||||||
when(factory.createBillingClient(
|
when(factory.createBillingClient(
|
||||||
context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY))
|
context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY, null))
|
||||||
.thenReturn(mockBillingClient);
|
.thenReturn(mockBillingClient);
|
||||||
when(factory.createBillingClient(
|
when(factory.createBillingClient(
|
||||||
context, mockMethodChannel, BillingChoiceMode.ALTERNATIVE_BILLING_ONLY))
|
context, mockMethodChannel, BillingChoiceMode.ALTERNATIVE_BILLING_ONLY, null))
|
||||||
|
.thenReturn(mockBillingClient);
|
||||||
|
when(factory.createBillingClient(
|
||||||
|
any(Context.class),
|
||||||
|
any(MethodChannel.class),
|
||||||
|
eq(BillingChoiceMode.USER_CHOICE_BILLING),
|
||||||
|
any(UserChoiceBillingListener.class)))
|
||||||
.thenReturn(mockBillingClient);
|
.thenReturn(mockBillingClient);
|
||||||
methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockMethodChannel, factory);
|
methodChannelHandler = new MethodCallHandlerImpl(activity, context, mockMethodChannel, factory);
|
||||||
when(mockActivityPluginBinding.getActivity()).thenReturn(activity);
|
when(mockActivityPluginBinding.getActivity()).thenReturn(activity);
|
||||||
@ -164,7 +178,7 @@ public class MethodCallHandlerTest {
|
|||||||
mockStartConnection(BillingChoiceMode.PLAY_BILLING_ONLY);
|
mockStartConnection(BillingChoiceMode.PLAY_BILLING_ONLY);
|
||||||
verify(result, never()).success(any());
|
verify(result, never()).success(any());
|
||||||
verify(factory, times(1))
|
verify(factory, times(1))
|
||||||
.createBillingClient(context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY);
|
.createBillingClient(context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY, null);
|
||||||
|
|
||||||
BillingResult billingResult =
|
BillingResult billingResult =
|
||||||
BillingResult.newBuilder()
|
BillingResult.newBuilder()
|
||||||
@ -183,7 +197,7 @@ public class MethodCallHandlerTest {
|
|||||||
verify(result, never()).success(any());
|
verify(result, never()).success(any());
|
||||||
verify(factory, times(1))
|
verify(factory, times(1))
|
||||||
.createBillingClient(
|
.createBillingClient(
|
||||||
context, mockMethodChannel, BillingChoiceMode.ALTERNATIVE_BILLING_ONLY);
|
context, mockMethodChannel, BillingChoiceMode.ALTERNATIVE_BILLING_ONLY, null);
|
||||||
|
|
||||||
BillingResult billingResult =
|
BillingResult billingResult =
|
||||||
BillingResult.newBuilder()
|
BillingResult.newBuilder()
|
||||||
@ -209,7 +223,7 @@ public class MethodCallHandlerTest {
|
|||||||
methodChannelHandler.onMethodCall(call, result);
|
methodChannelHandler.onMethodCall(call, result);
|
||||||
verify(result, never()).success(any());
|
verify(result, never()).success(any());
|
||||||
verify(factory, times(1))
|
verify(factory, times(1))
|
||||||
.createBillingClient(context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY);
|
.createBillingClient(context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY, null);
|
||||||
|
|
||||||
BillingResult billingResult =
|
BillingResult billingResult =
|
||||||
BillingResult.newBuilder()
|
BillingResult.newBuilder()
|
||||||
@ -221,6 +235,106 @@ public class MethodCallHandlerTest {
|
|||||||
verify(result, times(1)).success(fromBillingResult(billingResult));
|
verify(result, times(1)).success(fromBillingResult(billingResult));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void startConnectionUserChoiceBilling() {
|
||||||
|
ArgumentCaptor<BillingClientStateListener> captor =
|
||||||
|
mockStartConnection(BillingChoiceMode.USER_CHOICE_BILLING);
|
||||||
|
ArgumentCaptor<UserChoiceBillingListener> billingCaptor =
|
||||||
|
ArgumentCaptor.forClass(UserChoiceBillingListener.class);
|
||||||
|
verify(result, never()).success(any());
|
||||||
|
verify(factory, times(1))
|
||||||
|
.createBillingClient(
|
||||||
|
any(Context.class),
|
||||||
|
any(MethodChannel.class),
|
||||||
|
eq(BillingChoiceMode.USER_CHOICE_BILLING),
|
||||||
|
billingCaptor.capture());
|
||||||
|
|
||||||
|
BillingResult billingResult =
|
||||||
|
BillingResult.newBuilder()
|
||||||
|
.setResponseCode(100)
|
||||||
|
.setDebugMessage("dummy debug message")
|
||||||
|
.build();
|
||||||
|
captor.getValue().onBillingSetupFinished(billingResult);
|
||||||
|
|
||||||
|
verify(result, times(1)).success(fromBillingResult(billingResult));
|
||||||
|
UserChoiceDetails details = mock(UserChoiceDetails.class);
|
||||||
|
final String externalTransactionToken = "someLongTokenId1234";
|
||||||
|
final String originalTransactionId = "originalTransactionId123456";
|
||||||
|
when(details.getExternalTransactionToken()).thenReturn(externalTransactionToken);
|
||||||
|
when(details.getOriginalExternalTransactionId()).thenReturn(originalTransactionId);
|
||||||
|
when(details.getProducts()).thenReturn(Collections.emptyList());
|
||||||
|
billingCaptor.getValue().userSelectedAlternativeBilling(details);
|
||||||
|
|
||||||
|
verify(mockMethodChannel, times(1))
|
||||||
|
.invokeMethod(USER_SELECTED_ALTERNATIVE_BILLING, fromUserChoiceDetails(details));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void userChoiceBillingOnSecondConnection() {
|
||||||
|
// First connection.
|
||||||
|
ArgumentCaptor<BillingClientStateListener> captor1 =
|
||||||
|
mockStartConnection(BillingChoiceMode.PLAY_BILLING_ONLY);
|
||||||
|
verify(result, never()).success(any());
|
||||||
|
verify(factory, times(1))
|
||||||
|
.createBillingClient(context, mockMethodChannel, BillingChoiceMode.PLAY_BILLING_ONLY, null);
|
||||||
|
|
||||||
|
BillingResult billingResult1 =
|
||||||
|
BillingResult.newBuilder()
|
||||||
|
.setResponseCode(100)
|
||||||
|
.setDebugMessage("dummy debug message")
|
||||||
|
.build();
|
||||||
|
final BillingClientStateListener stateListener = captor1.getValue();
|
||||||
|
stateListener.onBillingSetupFinished(billingResult1);
|
||||||
|
verify(result, times(1)).success(fromBillingResult(billingResult1));
|
||||||
|
Mockito.reset(result, mockMethodChannel, mockBillingClient);
|
||||||
|
|
||||||
|
// Disconnect
|
||||||
|
MethodCall disconnectCall = new MethodCall(END_CONNECTION, null);
|
||||||
|
methodChannelHandler.onMethodCall(disconnectCall, result);
|
||||||
|
|
||||||
|
// Verify that the client is disconnected and that the OnDisconnect callback has
|
||||||
|
// been triggered
|
||||||
|
verify(result, times(1)).success(any());
|
||||||
|
verify(mockBillingClient, times(1)).endConnection();
|
||||||
|
stateListener.onBillingServiceDisconnected();
|
||||||
|
Map<String, Integer> expectedInvocation = new HashMap<>();
|
||||||
|
expectedInvocation.put("handle", DEFAULT_HANDLE);
|
||||||
|
verify(mockMethodChannel, times(1)).invokeMethod(ON_DISCONNECT, expectedInvocation);
|
||||||
|
Mockito.reset(result, mockMethodChannel, mockBillingClient);
|
||||||
|
|
||||||
|
// Second connection.
|
||||||
|
ArgumentCaptor<BillingClientStateListener> captor2 =
|
||||||
|
mockStartConnection(BillingChoiceMode.USER_CHOICE_BILLING);
|
||||||
|
ArgumentCaptor<UserChoiceBillingListener> billingCaptor =
|
||||||
|
ArgumentCaptor.forClass(UserChoiceBillingListener.class);
|
||||||
|
verify(result, never()).success(any());
|
||||||
|
verify(factory, times(1))
|
||||||
|
.createBillingClient(
|
||||||
|
any(Context.class),
|
||||||
|
any(MethodChannel.class),
|
||||||
|
eq(BillingChoiceMode.USER_CHOICE_BILLING),
|
||||||
|
billingCaptor.capture());
|
||||||
|
|
||||||
|
BillingResult billingResult2 =
|
||||||
|
BillingResult.newBuilder()
|
||||||
|
.setResponseCode(100)
|
||||||
|
.setDebugMessage("dummy debug message")
|
||||||
|
.build();
|
||||||
|
captor2.getValue().onBillingSetupFinished(billingResult2);
|
||||||
|
|
||||||
|
verify(result, times(1)).success(fromBillingResult(billingResult2));
|
||||||
|
UserChoiceDetails details = mock(UserChoiceDetails.class);
|
||||||
|
final String externalTransactionToken = "someLongTokenId1234";
|
||||||
|
final String originalTransactionId = "originalTransactionId123456";
|
||||||
|
when(details.getExternalTransactionToken()).thenReturn(externalTransactionToken);
|
||||||
|
when(details.getOriginalExternalTransactionId()).thenReturn(originalTransactionId);
|
||||||
|
when(details.getProducts()).thenReturn(Collections.emptyList());
|
||||||
|
billingCaptor.getValue().userSelectedAlternativeBilling(details);
|
||||||
|
|
||||||
|
verify(mockMethodChannel, times(1))
|
||||||
|
.invokeMethod(USER_SELECTED_ALTERNATIVE_BILLING, fromUserChoiceDetails(details));
|
||||||
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void startConnection_multipleCalls() {
|
public void startConnection_multipleCalls() {
|
||||||
Map<String, Object> arguments = new HashMap<>();
|
Map<String, Object> arguments = new HashMap<>();
|
||||||
@ -1071,7 +1185,7 @@ public class MethodCallHandlerTest {
|
|||||||
*/
|
*/
|
||||||
private ArgumentCaptor<BillingClientStateListener> mockStartConnection(int billingChoiceMode) {
|
private ArgumentCaptor<BillingClientStateListener> mockStartConnection(int billingChoiceMode) {
|
||||||
Map<String, Object> arguments = new HashMap<>();
|
Map<String, Object> arguments = new HashMap<>();
|
||||||
arguments.put(MethodArgs.HANDLE, 1);
|
arguments.put(MethodArgs.HANDLE, DEFAULT_HANDLE);
|
||||||
arguments.put(MethodArgs.BILLING_CHOICE_MODE, billingChoiceMode);
|
arguments.put(MethodArgs.BILLING_CHOICE_MODE, billingChoiceMode);
|
||||||
MethodCall call = new MethodCall(START_CONNECTION, arguments);
|
MethodCall call = new MethodCall(START_CONNECTION, arguments);
|
||||||
ArgumentCaptor<BillingClientStateListener> captor =
|
ArgumentCaptor<BillingClientStateListener> captor =
|
||||||
|
@ -27,7 +27,8 @@ void main() {
|
|||||||
late final BillingClient billingClient;
|
late final BillingClient billingClient;
|
||||||
|
|
||||||
setUpAll(() {
|
setUpAll(() {
|
||||||
billingClient = BillingClient((PurchasesResultWrapper _) {});
|
billingClient = BillingClient(
|
||||||
|
(PurchasesResultWrapper _) {}, (UserChoiceDetailsWrapper _) {});
|
||||||
});
|
});
|
||||||
|
|
||||||
testWidgets('BillingClient.acknowledgePurchase',
|
testWidgets('BillingClient.acknowledgePurchase',
|
||||||
|
@ -44,6 +44,7 @@ class _MyAppState extends State<_MyApp> {
|
|||||||
final InAppPurchasePlatform _inAppPurchasePlatform =
|
final InAppPurchasePlatform _inAppPurchasePlatform =
|
||||||
InAppPurchasePlatform.instance;
|
InAppPurchasePlatform.instance;
|
||||||
late StreamSubscription<List<PurchaseDetails>> _subscription;
|
late StreamSubscription<List<PurchaseDetails>> _subscription;
|
||||||
|
late StreamSubscription<GooglePlayUserChoiceDetails> _userChoiceDetailsStream;
|
||||||
List<String> _notFoundIds = <String>[];
|
List<String> _notFoundIds = <String>[];
|
||||||
List<ProductDetails> _products = <ProductDetails>[];
|
List<ProductDetails> _products = <ProductDetails>[];
|
||||||
List<PurchaseDetails> _purchases = <PurchaseDetails>[];
|
List<PurchaseDetails> _purchases = <PurchaseDetails>[];
|
||||||
@ -56,6 +57,7 @@ class _MyAppState extends State<_MyApp> {
|
|||||||
bool _purchasePending = false;
|
bool _purchasePending = false;
|
||||||
bool _loading = true;
|
bool _loading = true;
|
||||||
String? _queryProductError;
|
String? _queryProductError;
|
||||||
|
final List<String> _userChoiceDetailsList = <String>[];
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@ -70,6 +72,19 @@ class _MyAppState extends State<_MyApp> {
|
|||||||
// handle error here.
|
// handle error here.
|
||||||
});
|
});
|
||||||
initStoreInfo();
|
initStoreInfo();
|
||||||
|
final InAppPurchaseAndroidPlatformAddition addition =
|
||||||
|
InAppPurchasePlatformAddition.instance!
|
||||||
|
as InAppPurchaseAndroidPlatformAddition;
|
||||||
|
final Stream<GooglePlayUserChoiceDetails> userChoiceDetailsUpdated =
|
||||||
|
addition.userChoiceDetailsStream;
|
||||||
|
_userChoiceDetailsStream =
|
||||||
|
userChoiceDetailsUpdated.listen((GooglePlayUserChoiceDetails details) {
|
||||||
|
deliverUserChoiceDetails(details);
|
||||||
|
}, onDone: () {
|
||||||
|
_userChoiceDetailsStream.cancel();
|
||||||
|
}, onError: (Object error) {
|
||||||
|
// handle error here.
|
||||||
|
});
|
||||||
super.initState();
|
super.initState();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,6 +149,8 @@ class _MyAppState extends State<_MyApp> {
|
|||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_subscription.cancel();
|
_subscription.cancel();
|
||||||
|
_userChoiceDetailsStream.cancel();
|
||||||
|
_userChoiceDetailsList.clear();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -149,6 +166,7 @@ class _MyAppState extends State<_MyApp> {
|
|||||||
_buildConsumableBox(),
|
_buildConsumableBox(),
|
||||||
const _FeatureCard(),
|
const _FeatureCard(),
|
||||||
_buildFetchButtons(),
|
_buildFetchButtons(),
|
||||||
|
_buildUserChoiceDetailsDisplay(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@ -326,6 +344,26 @@ class _MyAppState extends State<_MyApp> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Card _buildUserChoiceDetailsDisplay() {
|
||||||
|
const ListTile header = ListTile(title: Text('UserChoiceDetails'));
|
||||||
|
final List<Widget> entries = <ListTile>[];
|
||||||
|
for (final String item in _userChoiceDetailsList) {
|
||||||
|
entries.add(ListTile(
|
||||||
|
title: Text(item,
|
||||||
|
style: TextStyle(color: ThemeData.light().colorScheme.primary)),
|
||||||
|
subtitle: Text(_countryCode)));
|
||||||
|
}
|
||||||
|
return Card(
|
||||||
|
child: Column(
|
||||||
|
children: <Widget>[
|
||||||
|
header,
|
||||||
|
const Divider(),
|
||||||
|
...entries,
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Card _buildProductList() {
|
Card _buildProductList() {
|
||||||
if (_loading) {
|
if (_loading) {
|
||||||
return const Card(
|
return const Card(
|
||||||
@ -537,6 +575,15 @@ class _MyAppState extends State<_MyApp> {
|
|||||||
// handle invalid purchase here if _verifyPurchase` failed.
|
// handle invalid purchase here if _verifyPurchase` failed.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> deliverUserChoiceDetails(
|
||||||
|
GooglePlayUserChoiceDetails details) async {
|
||||||
|
final String detailDescription =
|
||||||
|
'${details.externalTransactionToken}, ${details.originalExternalTransactionId}, ${details.products.length}';
|
||||||
|
setState(() {
|
||||||
|
_userChoiceDetailsList.add(detailDescription);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _listenToPurchaseUpdated(
|
Future<void> _listenToPurchaseUpdated(
|
||||||
List<PurchaseDetails> purchaseDetailsList) async {
|
List<PurchaseDetails> purchaseDetailsList) async {
|
||||||
for (final PurchaseDetails purchaseDetails in purchaseDetailsList) {
|
for (final PurchaseDetails purchaseDetails in purchaseDetailsList) {
|
||||||
|
@ -11,3 +11,4 @@ export 'src/billing_client_wrappers/product_details_wrapper.dart';
|
|||||||
export 'src/billing_client_wrappers/product_wrapper.dart';
|
export 'src/billing_client_wrappers/product_wrapper.dart';
|
||||||
export 'src/billing_client_wrappers/purchase_wrapper.dart';
|
export 'src/billing_client_wrappers/purchase_wrapper.dart';
|
||||||
export 'src/billing_client_wrappers/subscription_offer_details_wrapper.dart';
|
export 'src/billing_client_wrappers/subscription_offer_details_wrapper.dart';
|
||||||
|
export 'src/billing_client_wrappers/user_choice_details_wrapper.dart';
|
||||||
|
@ -8,6 +8,7 @@ import 'package:flutter/widgets.dart';
|
|||||||
|
|
||||||
import 'billing_client_wrapper.dart';
|
import 'billing_client_wrapper.dart';
|
||||||
import 'purchase_wrapper.dart';
|
import 'purchase_wrapper.dart';
|
||||||
|
import 'user_choice_details_wrapper.dart';
|
||||||
|
|
||||||
/// Abstraction of result of [BillingClient] operation that includes
|
/// Abstraction of result of [BillingClient] operation that includes
|
||||||
/// a [BillingResponse].
|
/// a [BillingResponse].
|
||||||
@ -37,6 +38,13 @@ class BillingClientManager {
|
|||||||
_connect();
|
_connect();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stream of `userSelectedAlternativeBilling` events from the [BillingClient].
|
||||||
|
///
|
||||||
|
/// This is a broadcast stream, so it can be listened to multiple times.
|
||||||
|
/// A "done" event will be sent after [dispose] is called.
|
||||||
|
late final Stream<UserChoiceDetailsWrapper> userChoiceDetailsStream =
|
||||||
|
_userChoiceAlternativeBillingController.stream;
|
||||||
|
|
||||||
/// Stream of `onPurchasesUpdated` events from the [BillingClient].
|
/// Stream of `onPurchasesUpdated` events from the [BillingClient].
|
||||||
///
|
///
|
||||||
/// This is a broadcast stream, so it can be listened to multiple times.
|
/// This is a broadcast stream, so it can be listened to multiple times.
|
||||||
@ -49,10 +57,14 @@ class BillingClientManager {
|
|||||||
/// In order to access the [BillingClient], use [runWithClient]
|
/// In order to access the [BillingClient], use [runWithClient]
|
||||||
/// and [runWithClientNonRetryable] methods.
|
/// and [runWithClientNonRetryable] methods.
|
||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
late final BillingClient client = BillingClient(_onPurchasesUpdated);
|
late final BillingClient client =
|
||||||
|
BillingClient(_onPurchasesUpdated, onUserChoiceAlternativeBilling);
|
||||||
|
|
||||||
final StreamController<PurchasesResultWrapper> _purchasesUpdatedController =
|
final StreamController<PurchasesResultWrapper> _purchasesUpdatedController =
|
||||||
StreamController<PurchasesResultWrapper>.broadcast();
|
StreamController<PurchasesResultWrapper>.broadcast();
|
||||||
|
final StreamController<UserChoiceDetailsWrapper>
|
||||||
|
_userChoiceAlternativeBillingController =
|
||||||
|
StreamController<UserChoiceDetailsWrapper>.broadcast();
|
||||||
|
|
||||||
BillingChoiceMode _billingChoiceMode;
|
BillingChoiceMode _billingChoiceMode;
|
||||||
bool _isConnecting = false;
|
bool _isConnecting = false;
|
||||||
@ -113,12 +125,14 @@ class BillingClientManager {
|
|||||||
/// After calling [dispose]:
|
/// After calling [dispose]:
|
||||||
/// - Further connection attempts will not be made.
|
/// - Further connection attempts will not be made.
|
||||||
/// - [purchasesUpdatedStream] will be closed.
|
/// - [purchasesUpdatedStream] will be closed.
|
||||||
|
/// - [userChoiceDetailsStream] will be closed.
|
||||||
/// - Calls to [runWithClient] and [runWithClientNonRetryable] will throw.
|
/// - Calls to [runWithClient] and [runWithClientNonRetryable] will throw.
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_debugAssertNotDisposed();
|
_debugAssertNotDisposed();
|
||||||
_isDisposed = true;
|
_isDisposed = true;
|
||||||
client.endConnection();
|
client.endConnection();
|
||||||
_purchasesUpdatedController.close();
|
_purchasesUpdatedController.close();
|
||||||
|
_userChoiceAlternativeBillingController.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ends connection to [BillingClient] and reconnects with [billingChoiceMode].
|
/// Ends connection to [BillingClient] and reconnects with [billingChoiceMode].
|
||||||
@ -168,4 +182,14 @@ class BillingClientManager {
|
|||||||
'called dispose() on a BillingClientManager, it can no longer be used.',
|
'called dispose() on a BillingClientManager, it can no longer be used.',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Callback passed to [BillingClient] to use when customer chooses
|
||||||
|
/// alternative billing.
|
||||||
|
@visibleForTesting
|
||||||
|
void onUserChoiceAlternativeBilling(UserChoiceDetailsWrapper event) {
|
||||||
|
if (_isDisposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_userChoiceAlternativeBillingController.add(event);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,12 @@ part 'billing_client_wrapper.g.dart';
|
|||||||
@visibleForTesting
|
@visibleForTesting
|
||||||
const String kOnPurchasesUpdated =
|
const String kOnPurchasesUpdated =
|
||||||
'PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List<Purchase>)';
|
'PurchasesUpdatedListener#onPurchasesUpdated(BillingResult, List<Purchase>)';
|
||||||
|
|
||||||
|
/// Method identifier for the userSelectedAlternativeBilling method channel method.
|
||||||
|
@visibleForTesting
|
||||||
|
const String kUserSelectedAlternativeBilling =
|
||||||
|
'UserChoiceBillingListener#userSelectedAlternativeBilling(UserChoiceDetails)';
|
||||||
|
|
||||||
const String _kOnBillingServiceDisconnected =
|
const String _kOnBillingServiceDisconnected =
|
||||||
'BillingClientStateListener#onBillingServiceDisconnected()';
|
'BillingClientStateListener#onBillingServiceDisconnected()';
|
||||||
|
|
||||||
@ -40,6 +46,10 @@ const String _kOnBillingServiceDisconnected =
|
|||||||
typedef PurchasesUpdatedListener = void Function(
|
typedef PurchasesUpdatedListener = void Function(
|
||||||
PurchasesResultWrapper purchasesResult);
|
PurchasesResultWrapper purchasesResult);
|
||||||
|
|
||||||
|
/// Wraps a [UserChoiceBillingListener](https://developer.android.com/reference/com/android/billingclient/api/UserChoiceBillingListener)
|
||||||
|
typedef UserSelectedAlternativeBillingListener = void Function(
|
||||||
|
UserChoiceDetailsWrapper userChoiceDetailsWrapper);
|
||||||
|
|
||||||
/// This class can be used directly instead of [InAppPurchaseConnection] to call
|
/// This class can be used directly instead of [InAppPurchaseConnection] to call
|
||||||
/// Play-specific billing APIs.
|
/// Play-specific billing APIs.
|
||||||
///
|
///
|
||||||
@ -60,11 +70,16 @@ typedef PurchasesUpdatedListener = void Function(
|
|||||||
/// transparently.
|
/// transparently.
|
||||||
class BillingClient {
|
class BillingClient {
|
||||||
/// Creates a billing client.
|
/// Creates a billing client.
|
||||||
BillingClient(PurchasesUpdatedListener onPurchasesUpdated) {
|
BillingClient(PurchasesUpdatedListener onPurchasesUpdated,
|
||||||
|
UserSelectedAlternativeBillingListener? alternativeBillingListener) {
|
||||||
channel.setMethodCallHandler(callHandler);
|
channel.setMethodCallHandler(callHandler);
|
||||||
_callbacks[kOnPurchasesUpdated] = <PurchasesUpdatedListener>[
|
_callbacks[kOnPurchasesUpdated] = <PurchasesUpdatedListener>[
|
||||||
onPurchasesUpdated
|
onPurchasesUpdated
|
||||||
];
|
];
|
||||||
|
_callbacks[kUserSelectedAlternativeBilling] = alternativeBillingListener ==
|
||||||
|
null
|
||||||
|
? <UserSelectedAlternativeBillingListener>[]
|
||||||
|
: <UserSelectedAlternativeBillingListener>[alternativeBillingListener];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Occasionally methods in the native layer require a Dart callback to be
|
// Occasionally methods in the native layer require a Dart callback to be
|
||||||
@ -114,7 +129,8 @@ class BillingClient {
|
|||||||
BillingChoiceMode.playBillingOnly}) async {
|
BillingChoiceMode.playBillingOnly}) async {
|
||||||
final List<Function> disconnectCallbacks =
|
final List<Function> disconnectCallbacks =
|
||||||
_callbacks[_kOnBillingServiceDisconnected] ??= <Function>[];
|
_callbacks[_kOnBillingServiceDisconnected] ??= <Function>[];
|
||||||
disconnectCallbacks.add(onBillingServiceDisconnected);
|
_callbacks[_kOnBillingServiceDisconnected]
|
||||||
|
?.add(onBillingServiceDisconnected);
|
||||||
return BillingResultWrapper.fromJson((await channel
|
return BillingResultWrapper.fromJson((await channel
|
||||||
.invokeMapMethod<String, dynamic>(
|
.invokeMapMethod<String, dynamic>(
|
||||||
'BillingClient#startConnection(BillingClientStateListener)',
|
'BillingClient#startConnection(BillingClientStateListener)',
|
||||||
@ -412,6 +428,15 @@ class BillingClient {
|
|||||||
_callbacks[_kOnBillingServiceDisconnected]!
|
_callbacks[_kOnBillingServiceDisconnected]!
|
||||||
.cast<OnBillingServiceDisconnected>();
|
.cast<OnBillingServiceDisconnected>();
|
||||||
onDisconnected[handle]();
|
onDisconnected[handle]();
|
||||||
|
case kUserSelectedAlternativeBilling:
|
||||||
|
if (_callbacks[kUserSelectedAlternativeBilling]!.isNotEmpty) {
|
||||||
|
final UserSelectedAlternativeBillingListener listener =
|
||||||
|
_callbacks[kUserSelectedAlternativeBilling]!.first
|
||||||
|
as UserSelectedAlternativeBillingListener;
|
||||||
|
listener(UserChoiceDetailsWrapper.fromJson(
|
||||||
|
(call.arguments as Map<dynamic, dynamic>)
|
||||||
|
.cast<String, dynamic>()));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -443,7 +468,7 @@ enum BillingResponse {
|
|||||||
@JsonValue(-2)
|
@JsonValue(-2)
|
||||||
featureNotSupported,
|
featureNotSupported,
|
||||||
|
|
||||||
/// The play Store service is not connected now - potentially transient state.
|
/// The Play Store service is not connected now - potentially transient state.
|
||||||
@JsonValue(-1)
|
@JsonValue(-1)
|
||||||
serviceDisconnected,
|
serviceDisconnected,
|
||||||
|
|
||||||
@ -490,8 +515,8 @@ enum BillingResponse {
|
|||||||
|
|
||||||
/// Plugin concept to cover billing modes.
|
/// Plugin concept to cover billing modes.
|
||||||
///
|
///
|
||||||
/// [playBillingOnly] (google play billing only).
|
/// [playBillingOnly] (google Play billing only).
|
||||||
/// [alternativeBillingOnly] (app provided billing with reporting to play).
|
/// [alternativeBillingOnly] (app provided billing with reporting to Play).
|
||||||
@JsonEnum(alwaysCreate: true)
|
@JsonEnum(alwaysCreate: true)
|
||||||
enum BillingChoiceMode {
|
enum BillingChoiceMode {
|
||||||
// WARNING: Changes to this class need to be reflected in our generated code.
|
// WARNING: Changes to this class need to be reflected in our generated code.
|
||||||
@ -500,13 +525,17 @@ enum BillingChoiceMode {
|
|||||||
// Values must match what is used in
|
// Values must match what is used in
|
||||||
// in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java
|
// in_app_purchase_android/android/src/main/java/io/flutter/plugins/inapppurchase/MethodCallHandlerImpl.java
|
||||||
|
|
||||||
/// Billing through google play. Default state.
|
/// Billing through google Play. Default state.
|
||||||
@JsonValue(0)
|
@JsonValue(0)
|
||||||
playBillingOnly,
|
playBillingOnly,
|
||||||
|
|
||||||
/// Billing through app provided flow.
|
/// Billing through app provided flow.
|
||||||
@JsonValue(1)
|
@JsonValue(1)
|
||||||
alternativeBillingOnly,
|
alternativeBillingOnly,
|
||||||
|
|
||||||
|
/// Users can choose Play billing or alternative billing.
|
||||||
|
@JsonValue(2)
|
||||||
|
userChoiceBilling,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Serializer for [BillingChoiceMode].
|
/// Serializer for [BillingChoiceMode].
|
||||||
|
@ -25,6 +25,7 @@ const _$BillingResponseEnumMap = {
|
|||||||
const _$BillingChoiceModeEnumMap = {
|
const _$BillingChoiceModeEnumMap = {
|
||||||
BillingChoiceMode.playBillingOnly: 0,
|
BillingChoiceMode.playBillingOnly: 0,
|
||||||
BillingChoiceMode.alternativeBillingOnly: 1,
|
BillingChoiceMode.alternativeBillingOnly: 1,
|
||||||
|
BillingChoiceMode.userChoiceBilling: 2,
|
||||||
};
|
};
|
||||||
|
|
||||||
const _$ProductTypeEnumMap = {
|
const _$ProductTypeEnumMap = {
|
||||||
|
@ -0,0 +1,131 @@
|
|||||||
|
// 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/foundation.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 'user_choice_details_wrapper.g.dart';
|
||||||
|
|
||||||
|
/// This wraps [`com.android.billingclient.api.UserChoiceDetails`](https://developer.android.com/reference/com/android/billingclient/api/UserChoiceDetails)
|
||||||
|
// See https://docs.flutter.dev/data-and-backend/serialization/json#generating-code-for-nested-classes
|
||||||
|
// for explination for why this uses explicitToJson.
|
||||||
|
@JsonSerializable(createToJson: true, explicitToJson: true)
|
||||||
|
@immutable
|
||||||
|
class UserChoiceDetailsWrapper {
|
||||||
|
/// Creates a purchase wrapper with the given purchase details.
|
||||||
|
@visibleForTesting
|
||||||
|
const UserChoiceDetailsWrapper({
|
||||||
|
required this.originalExternalTransactionId,
|
||||||
|
required this.externalTransactionToken,
|
||||||
|
required this.products,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Factory for creating a [UserChoiceDetailsWrapper] from a [Map] with
|
||||||
|
/// the user choice details.
|
||||||
|
factory UserChoiceDetailsWrapper.fromJson(Map<String, dynamic> map) =>
|
||||||
|
_$UserChoiceDetailsWrapperFromJson(map);
|
||||||
|
|
||||||
|
/// Creates a JSON representation of this product.
|
||||||
|
Map<String, dynamic> toJson() => _$UserChoiceDetailsWrapperToJson(this);
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(other, this)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return other is UserChoiceDetailsWrapper &&
|
||||||
|
other.originalExternalTransactionId == originalExternalTransactionId &&
|
||||||
|
other.externalTransactionToken == externalTransactionToken &&
|
||||||
|
listEquals(other.products, products);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
originalExternalTransactionId,
|
||||||
|
externalTransactionToken,
|
||||||
|
products.hashCode,
|
||||||
|
);
|
||||||
|
|
||||||
|
/// Returns the external transaction Id of the originating subscription, if
|
||||||
|
/// the purchase is a subscription upgrade/downgrade.
|
||||||
|
@JsonKey(defaultValue: '')
|
||||||
|
final String originalExternalTransactionId;
|
||||||
|
|
||||||
|
/// Returns a token that represents the user's prospective purchase via
|
||||||
|
/// user choice alternative billing.
|
||||||
|
@JsonKey(defaultValue: '')
|
||||||
|
final String externalTransactionToken;
|
||||||
|
|
||||||
|
/// Returns a list of [UserChoiceDetailsProductWrapper] to be purchased in
|
||||||
|
/// the user choice alternative billing flow.
|
||||||
|
@JsonKey(defaultValue: <UserChoiceDetailsProductWrapper>[])
|
||||||
|
final List<UserChoiceDetailsProductWrapper> products;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data structure representing a UserChoiceDetails product.
|
||||||
|
///
|
||||||
|
/// This wraps [`com.android.billingclient.api.UserChoiceDetails.Product`](https://developer.android.com/reference/com/android/billingclient/api/UserChoiceDetails.Product)
|
||||||
|
//
|
||||||
|
// See https://docs.flutter.dev/data-and-backend/serialization/json#generating-code-for-nested-classes
|
||||||
|
// for explination for why this uses explicitToJson.
|
||||||
|
@JsonSerializable(createToJson: true, explicitToJson: true)
|
||||||
|
@ProductTypeConverter()
|
||||||
|
@immutable
|
||||||
|
class UserChoiceDetailsProductWrapper {
|
||||||
|
/// Creates a [UserChoiceDetailsProductWrapper] with the given record details.
|
||||||
|
@visibleForTesting
|
||||||
|
const UserChoiceDetailsProductWrapper({
|
||||||
|
required this.id,
|
||||||
|
required this.offerToken,
|
||||||
|
required this.productType,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Factory for creating a [UserChoiceDetailsProductWrapper] from a [Map] with the record details.
|
||||||
|
factory UserChoiceDetailsProductWrapper.fromJson(Map<String, dynamic> map) =>
|
||||||
|
_$UserChoiceDetailsProductWrapperFromJson(map);
|
||||||
|
|
||||||
|
/// Creates a JSON representation of this product.
|
||||||
|
Map<String, dynamic> toJson() =>
|
||||||
|
_$UserChoiceDetailsProductWrapperToJson(this);
|
||||||
|
|
||||||
|
/// Returns the id of the product being purchased.
|
||||||
|
@JsonKey(defaultValue: '')
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// Returns the offer token that was passed in launchBillingFlow to purchase the product.
|
||||||
|
@JsonKey(defaultValue: '')
|
||||||
|
final String offerToken;
|
||||||
|
|
||||||
|
/// Returns the [ProductType] of the product being purchased.
|
||||||
|
final ProductType productType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(other, this)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return other is UserChoiceDetailsProductWrapper &&
|
||||||
|
other.id == id &&
|
||||||
|
other.offerToken == offerToken &&
|
||||||
|
other.productType == productType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
id,
|
||||||
|
offerToken,
|
||||||
|
productType,
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,45 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'user_choice_details_wrapper.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
UserChoiceDetailsWrapper _$UserChoiceDetailsWrapperFromJson(Map json) =>
|
||||||
|
UserChoiceDetailsWrapper(
|
||||||
|
originalExternalTransactionId:
|
||||||
|
json['originalExternalTransactionId'] as String? ?? '',
|
||||||
|
externalTransactionToken:
|
||||||
|
json['externalTransactionToken'] as String? ?? '',
|
||||||
|
products: (json['products'] as List<dynamic>?)
|
||||||
|
?.map((e) => UserChoiceDetailsProductWrapper.fromJson(
|
||||||
|
Map<String, dynamic>.from(e as Map)))
|
||||||
|
.toList() ??
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$UserChoiceDetailsWrapperToJson(
|
||||||
|
UserChoiceDetailsWrapper instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'originalExternalTransactionId': instance.originalExternalTransactionId,
|
||||||
|
'externalTransactionToken': instance.externalTransactionToken,
|
||||||
|
'products': instance.products.map((e) => e.toJson()).toList(),
|
||||||
|
};
|
||||||
|
|
||||||
|
UserChoiceDetailsProductWrapper _$UserChoiceDetailsProductWrapperFromJson(
|
||||||
|
Map json) =>
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: json['id'] as String? ?? '',
|
||||||
|
offerToken: json['offerToken'] as String? ?? '',
|
||||||
|
productType:
|
||||||
|
const ProductTypeConverter().fromJson(json['productType'] as String?),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$UserChoiceDetailsProductWrapperToJson(
|
||||||
|
UserChoiceDetailsProductWrapper instance) =>
|
||||||
|
<String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'offerToken': instance.offerToken,
|
||||||
|
'productType': const ProductTypeConverter().toJson(instance.productType),
|
||||||
|
};
|
@ -2,19 +2,34 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
|
import 'package:in_app_purchase_platform_interface/in_app_purchase_platform_interface.dart';
|
||||||
|
|
||||||
import '../billing_client_wrappers.dart';
|
import '../billing_client_wrappers.dart';
|
||||||
import '../in_app_purchase_android.dart';
|
import '../in_app_purchase_android.dart';
|
||||||
import 'billing_client_wrappers/billing_config_wrapper.dart';
|
import 'billing_client_wrappers/billing_config_wrapper.dart';
|
||||||
|
import 'types/translator.dart';
|
||||||
|
|
||||||
/// Contains InApp Purchase features that are only available on PlayStore.
|
/// Contains InApp Purchase features that are only available on PlayStore.
|
||||||
class InAppPurchaseAndroidPlatformAddition
|
class InAppPurchaseAndroidPlatformAddition
|
||||||
extends InAppPurchasePlatformAddition {
|
extends InAppPurchasePlatformAddition {
|
||||||
/// Creates a [InAppPurchaseAndroidPlatformAddition] which uses the supplied
|
/// Creates a [InAppPurchaseAndroidPlatformAddition] which uses the supplied
|
||||||
/// `BillingClientManager` to provide Android specific features.
|
/// `BillingClientManager` to provide Android specific features.
|
||||||
InAppPurchaseAndroidPlatformAddition(this._billingClientManager);
|
InAppPurchaseAndroidPlatformAddition(this._billingClientManager) {
|
||||||
|
_billingClientManager.userChoiceDetailsStream
|
||||||
|
.map(Translator.convertToUserChoiceDetails)
|
||||||
|
.listen(_userChoiceDetailsStreamController.add);
|
||||||
|
}
|
||||||
|
|
||||||
|
final StreamController<GooglePlayUserChoiceDetails>
|
||||||
|
_userChoiceDetailsStreamController =
|
||||||
|
StreamController<GooglePlayUserChoiceDetails>.broadcast();
|
||||||
|
|
||||||
|
/// [GooglePlayUserChoiceDetails] emits each time user selects alternative billing.
|
||||||
|
late final Stream<GooglePlayUserChoiceDetails> userChoiceDetailsStream =
|
||||||
|
_userChoiceDetailsStreamController.stream;
|
||||||
|
|
||||||
/// Whether pending purchase is enabled.
|
/// Whether pending purchase is enabled.
|
||||||
///
|
///
|
||||||
|
@ -0,0 +1,101 @@
|
|||||||
|
// 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/foundation.dart';
|
||||||
|
|
||||||
|
/// Data structure representing a UserChoiceDetails.
|
||||||
|
///
|
||||||
|
/// This wraps [`com.android.billingclient.api.UserChoiceDetails`](https://developer.android.com/reference/com/android/billingclient/api/UserChoiceDetails)
|
||||||
|
@immutable
|
||||||
|
class GooglePlayUserChoiceDetails {
|
||||||
|
/// Creates a new Google Play specific user choice billing details object with
|
||||||
|
/// the provided details.
|
||||||
|
const GooglePlayUserChoiceDetails({
|
||||||
|
required this.originalExternalTransactionId,
|
||||||
|
required this.externalTransactionToken,
|
||||||
|
required this.products,
|
||||||
|
});
|
||||||
|
|
||||||
|
/// Returns the external transaction Id of the originating subscription, if
|
||||||
|
/// the purchase is a subscription upgrade/downgrade.
|
||||||
|
final String originalExternalTransactionId;
|
||||||
|
|
||||||
|
/// Returns a token that represents the user's prospective purchase via
|
||||||
|
/// user choice alternative billing.
|
||||||
|
final String externalTransactionToken;
|
||||||
|
|
||||||
|
/// Returns a list of [GooglePlayUserChoiceDetailsProduct] to be purchased in
|
||||||
|
/// the user choice alternative billing flow.
|
||||||
|
final List<GooglePlayUserChoiceDetailsProduct> products;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(other, this)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return other is GooglePlayUserChoiceDetails &&
|
||||||
|
other.originalExternalTransactionId == originalExternalTransactionId &&
|
||||||
|
other.externalTransactionToken == externalTransactionToken &&
|
||||||
|
listEquals(other.products, products);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
originalExternalTransactionId,
|
||||||
|
externalTransactionToken,
|
||||||
|
products.hashCode,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Data structure representing a UserChoiceDetails product.
|
||||||
|
///
|
||||||
|
/// This wraps [`com.android.billingclient.api.UserChoiceDetails.Product`](https://developer.android.com/reference/com/android/billingclient/api/UserChoiceDetails.Product)
|
||||||
|
@immutable
|
||||||
|
class GooglePlayUserChoiceDetailsProduct {
|
||||||
|
/// Creates UserChoiceDetailsProduct.
|
||||||
|
const GooglePlayUserChoiceDetailsProduct(
|
||||||
|
{required this.id, required this.offerToken, required this.productType});
|
||||||
|
|
||||||
|
/// Returns the id of the product being purchased.
|
||||||
|
final String id;
|
||||||
|
|
||||||
|
/// Returns the offer token that was passed in launchBillingFlow to purchase the product.
|
||||||
|
final String offerToken;
|
||||||
|
|
||||||
|
/// Returns the [GooglePlayProductType] of the product being purchased.
|
||||||
|
final GooglePlayProductType productType;
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool operator ==(Object other) {
|
||||||
|
if (identical(other, this)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (other.runtimeType != runtimeType) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return other is GooglePlayUserChoiceDetailsProduct &&
|
||||||
|
other.id == id &&
|
||||||
|
other.offerToken == offerToken &&
|
||||||
|
other.productType == productType;
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
int get hashCode => Object.hash(
|
||||||
|
id,
|
||||||
|
offerToken,
|
||||||
|
productType,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// This wraps [`com.android.billingclient.api.BillingClient.ProductType`](https://developer.android.com/reference/com/android/billingclient/api/BillingClient.ProductType)
|
||||||
|
enum GooglePlayProductType {
|
||||||
|
/// A Product type for Android apps in-app products.
|
||||||
|
inapp,
|
||||||
|
|
||||||
|
/// A Product type for Android apps subscriptions.
|
||||||
|
subs
|
||||||
|
}
|
@ -0,0 +1,47 @@
|
|||||||
|
// 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/foundation.dart';
|
||||||
|
|
||||||
|
import '../../billing_client_wrappers.dart';
|
||||||
|
import 'google_play_user_choice_details.dart';
|
||||||
|
|
||||||
|
/// Class used to convert cross process object into api expose objects.
|
||||||
|
class Translator {
|
||||||
|
Translator._();
|
||||||
|
|
||||||
|
/// Converts from [UserChoiceDetailsWrapper] to [GooglePlayUserChoiceDetails].
|
||||||
|
static GooglePlayUserChoiceDetails convertToUserChoiceDetails(
|
||||||
|
UserChoiceDetailsWrapper detailsWrapper) {
|
||||||
|
return GooglePlayUserChoiceDetails(
|
||||||
|
originalExternalTransactionId:
|
||||||
|
detailsWrapper.originalExternalTransactionId,
|
||||||
|
externalTransactionToken: detailsWrapper.externalTransactionToken,
|
||||||
|
products: detailsWrapper.products
|
||||||
|
.map((UserChoiceDetailsProductWrapper e) =>
|
||||||
|
convertToUserChoiceDetailsProduct(e))
|
||||||
|
.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts from [UserChoiceDetailsProductWrapper] to [GooglePlayUserChoiceDetailsProduct].
|
||||||
|
@visibleForTesting
|
||||||
|
static GooglePlayUserChoiceDetailsProduct convertToUserChoiceDetailsProduct(
|
||||||
|
UserChoiceDetailsProductWrapper productWrapper) {
|
||||||
|
return GooglePlayUserChoiceDetailsProduct(
|
||||||
|
id: productWrapper.id,
|
||||||
|
offerToken: productWrapper.offerToken,
|
||||||
|
productType: convertToPlayProductType(productWrapper.productType));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Coverts from [ProductType] to [GooglePlayProductType].
|
||||||
|
@visibleForTesting
|
||||||
|
static GooglePlayProductType convertToPlayProductType(ProductType type) {
|
||||||
|
switch (type) {
|
||||||
|
case ProductType.inapp:
|
||||||
|
return GooglePlayProductType.inapp;
|
||||||
|
case ProductType.subs:
|
||||||
|
return GooglePlayProductType.subs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,4 +6,5 @@ export 'change_subscription_param.dart';
|
|||||||
export 'google_play_product_details.dart';
|
export 'google_play_product_details.dart';
|
||||||
export 'google_play_purchase_details.dart';
|
export 'google_play_purchase_details.dart';
|
||||||
export 'google_play_purchase_param.dart';
|
export 'google_play_purchase_param.dart';
|
||||||
|
export 'google_play_user_choice_details.dart';
|
||||||
export 'query_purchase_details_response.dart';
|
export 'query_purchase_details_response.dart';
|
||||||
|
@ -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.
|
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
|
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
|
issue_tracker: https://github.com/flutter/flutter/issues?q=is%3Aissue+is%3Aopen+label%3A%22p%3A+in_app_purchase%22
|
||||||
version: 0.3.1
|
version: 0.3.2
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.1.0
|
sdk: ^3.1.0
|
||||||
|
@ -138,5 +138,32 @@ void main() {
|
|||||||
expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1));
|
expect(stubPlatform.countPreviousCalls(startConnectionCall), equals(1));
|
||||||
expect(stubPlatform.countPreviousCalls(endConnectionCall), equals(1));
|
expect(stubPlatform.countPreviousCalls(endConnectionCall), equals(1));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test(
|
||||||
|
'Emits UserChoiceDetailsWrapper when onUserChoiceAlternativeBilling is called',
|
||||||
|
() async {
|
||||||
|
connectedCompleter.complete();
|
||||||
|
// Ensures all asynchronous connected code finishes.
|
||||||
|
await manager.runWithClientNonRetryable((_) async {});
|
||||||
|
|
||||||
|
const UserChoiceDetailsWrapper expected = UserChoiceDetailsWrapper(
|
||||||
|
originalExternalTransactionId: 'TransactionId',
|
||||||
|
externalTransactionToken: 'TransactionToken',
|
||||||
|
products: <UserChoiceDetailsProductWrapper>[
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: 'id1',
|
||||||
|
offerToken: 'offerToken1',
|
||||||
|
productType: ProductType.inapp),
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: 'id2',
|
||||||
|
offerToken: 'offerToken2',
|
||||||
|
productType: ProductType.inapp),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
final Future<UserChoiceDetailsWrapper> detailsFuture =
|
||||||
|
manager.userChoiceDetailsStream.first;
|
||||||
|
manager.onUserChoiceAlternativeBilling(expected);
|
||||||
|
expect(await detailsFuture, expected);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// Use of this source code is governed by a BSD-style license that can be
|
// Use of this source code is governed by a BSD-style license that can be
|
||||||
// found in the LICENSE file.
|
// found in the LICENSE file.
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_test/flutter_test.dart';
|
import 'package:flutter_test/flutter_test.dart';
|
||||||
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
|
import 'package:in_app_purchase_android/billing_client_wrappers.dart';
|
||||||
@ -37,7 +39,8 @@ void main() {
|
|||||||
.setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler));
|
.setMockMethodCallHandler(channel, stubPlatform.fakeMethodCallHandler));
|
||||||
|
|
||||||
setUp(() {
|
setUp(() {
|
||||||
billingClient = BillingClient((PurchasesResultWrapper _) {});
|
billingClient = BillingClient(
|
||||||
|
(PurchasesResultWrapper _) {}, (UserChoiceDetailsWrapper _) {});
|
||||||
stubPlatform.reset();
|
stubPlatform.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -114,7 +117,7 @@ void main() {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('passes billingChoiceMode when set', () async {
|
test('passes billingChoiceMode alternativeBillingOnly when set', () async {
|
||||||
const String debugMessage = 'dummy message';
|
const String debugMessage = 'dummy message';
|
||||||
const BillingResponse responseCode = BillingResponse.developerError;
|
const BillingResponse responseCode = BillingResponse.developerError;
|
||||||
stubPlatform.addResponse(
|
stubPlatform.addResponse(
|
||||||
@ -136,6 +139,91 @@ void main() {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('passes billingChoiceMode userChoiceBilling when set', () async {
|
||||||
|
const String debugMessage = 'dummy message';
|
||||||
|
const BillingResponse responseCode = BillingResponse.ok;
|
||||||
|
stubPlatform.addResponse(
|
||||||
|
name: methodName,
|
||||||
|
value: <String, dynamic>{
|
||||||
|
'responseCode': const BillingResponseConverter().toJson(responseCode),
|
||||||
|
'debugMessage': debugMessage,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
final Completer<UserChoiceDetailsWrapper> completer =
|
||||||
|
Completer<UserChoiceDetailsWrapper>();
|
||||||
|
|
||||||
|
billingClient = BillingClient((PurchasesResultWrapper _) {},
|
||||||
|
(UserChoiceDetailsWrapper details) => completer.complete(details));
|
||||||
|
stubPlatform.reset();
|
||||||
|
await billingClient.startConnection(
|
||||||
|
onBillingServiceDisconnected: () {},
|
||||||
|
billingChoiceMode: BillingChoiceMode.userChoiceBilling);
|
||||||
|
final MethodCall call = stubPlatform.previousCallMatching(methodName);
|
||||||
|
expect(
|
||||||
|
call.arguments,
|
||||||
|
equals(<dynamic, dynamic>{
|
||||||
|
'handle': 0,
|
||||||
|
'billingChoiceMode': 2,
|
||||||
|
}));
|
||||||
|
const UserChoiceDetailsWrapper expected = UserChoiceDetailsWrapper(
|
||||||
|
originalExternalTransactionId: 'TransactionId',
|
||||||
|
externalTransactionToken: 'TransactionToken',
|
||||||
|
products: <UserChoiceDetailsProductWrapper>[
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: 'id1',
|
||||||
|
offerToken: 'offerToken1',
|
||||||
|
productType: ProductType.inapp),
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: 'id2',
|
||||||
|
offerToken: 'offerToken2',
|
||||||
|
productType: ProductType.inapp),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
await billingClient.callHandler(
|
||||||
|
MethodCall(kUserSelectedAlternativeBilling, expected.toJson()));
|
||||||
|
expect(completer.isCompleted, isTrue);
|
||||||
|
expect(await completer.future, expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('UserChoiceDetailsWrapper searilization check', () async {
|
||||||
|
// Test ensures that changes to UserChoiceDetailsWrapper#toJson are
|
||||||
|
// compatible with code in Translator.java.
|
||||||
|
const String transactionIdKey = 'originalExternalTransactionId';
|
||||||
|
const String transactionTokenKey = 'externalTransactionToken';
|
||||||
|
const String productsKey = 'products';
|
||||||
|
const String productIdKey = 'id';
|
||||||
|
const String productOfferTokenKey = 'offerToken';
|
||||||
|
const String productTypeKey = 'productType';
|
||||||
|
|
||||||
|
const UserChoiceDetailsProductWrapper expectedProduct1 =
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: 'id1',
|
||||||
|
offerToken: 'offerToken1',
|
||||||
|
productType: ProductType.inapp);
|
||||||
|
const UserChoiceDetailsProductWrapper expectedProduct2 =
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: 'id2',
|
||||||
|
offerToken: 'offerToken2',
|
||||||
|
productType: ProductType.inapp);
|
||||||
|
const UserChoiceDetailsWrapper expected = UserChoiceDetailsWrapper(
|
||||||
|
originalExternalTransactionId: 'TransactionId',
|
||||||
|
externalTransactionToken: 'TransactionToken',
|
||||||
|
products: <UserChoiceDetailsProductWrapper>[
|
||||||
|
expectedProduct1,
|
||||||
|
expectedProduct2,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
final Map<String, dynamic> detailsJson = expected.toJson();
|
||||||
|
expect(detailsJson.keys, contains(transactionIdKey));
|
||||||
|
expect(detailsJson.keys, contains(transactionTokenKey));
|
||||||
|
expect(detailsJson.keys, contains(productsKey));
|
||||||
|
|
||||||
|
final Map<String, dynamic> productJson = expectedProduct1.toJson();
|
||||||
|
expect(productJson, contains(productIdKey));
|
||||||
|
expect(productJson, contains(productOfferTokenKey));
|
||||||
|
expect(productJson, contains(productTypeKey));
|
||||||
|
});
|
||||||
|
|
||||||
test('handles method channel returning null', () async {
|
test('handles method channel returning null', () async {
|
||||||
stubPlatform.addResponse(
|
stubPlatform.addResponse(
|
||||||
name: methodName,
|
name: methodName,
|
||||||
|
@ -9,6 +9,7 @@ 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/in_app_purchase_android.dart';
|
||||||
import 'package:in_app_purchase_android/src/billing_client_wrappers/billing_config_wrapper.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 'package:in_app_purchase_android/src/channel.dart';
|
||||||
|
import 'package:in_app_purchase_android/src/types/translator.dart';
|
||||||
|
|
||||||
import 'billing_client_wrappers/billing_client_wrapper_test.dart';
|
import 'billing_client_wrappers/billing_client_wrapper_test.dart';
|
||||||
import 'billing_client_wrappers/purchase_wrapper_test.dart';
|
import 'billing_client_wrappers/purchase_wrapper_test.dart';
|
||||||
@ -281,4 +282,28 @@ void main() {
|
|||||||
expect(arguments['feature'], equals('subscriptions'));
|
expect(arguments['feature'], equals('subscriptions'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
group('userChoiceDetails', () {
|
||||||
|
test('called', () async {
|
||||||
|
final Future<GooglePlayUserChoiceDetails> futureDetails =
|
||||||
|
iapAndroidPlatformAddition.userChoiceDetailsStream.first;
|
||||||
|
const UserChoiceDetailsWrapper expected = UserChoiceDetailsWrapper(
|
||||||
|
originalExternalTransactionId: 'TransactionId',
|
||||||
|
externalTransactionToken: 'TransactionToken',
|
||||||
|
products: <UserChoiceDetailsProductWrapper>[
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: 'id1',
|
||||||
|
offerToken: 'offerToken1',
|
||||||
|
productType: ProductType.inapp),
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: 'id2',
|
||||||
|
offerToken: 'offerToken2',
|
||||||
|
productType: ProductType.inapp),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
manager.onUserChoiceAlternativeBilling(expected);
|
||||||
|
expect(
|
||||||
|
await futureDetails, Translator.convertToUserChoiceDetails(expected));
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,71 @@
|
|||||||
|
// 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_android/billing_client_wrappers.dart';
|
||||||
|
import 'package:in_app_purchase_android/src/types/google_play_user_choice_details.dart';
|
||||||
|
import 'package:in_app_purchase_android/src/types/translator.dart';
|
||||||
|
import 'package:test/test.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
group('Translator ', () {
|
||||||
|
test('convertToPlayProductType', () {
|
||||||
|
expect(Translator.convertToPlayProductType(ProductType.inapp),
|
||||||
|
GooglePlayProductType.inapp);
|
||||||
|
expect(Translator.convertToPlayProductType(ProductType.subs),
|
||||||
|
GooglePlayProductType.subs);
|
||||||
|
expect(GooglePlayProductType.values.length, ProductType.values.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('convertToUserChoiceDetailsProduct', () {
|
||||||
|
const GooglePlayUserChoiceDetailsProduct expected =
|
||||||
|
GooglePlayUserChoiceDetailsProduct(
|
||||||
|
id: 'id',
|
||||||
|
offerToken: 'offerToken',
|
||||||
|
productType: GooglePlayProductType.inapp);
|
||||||
|
expect(
|
||||||
|
Translator.convertToUserChoiceDetailsProduct(
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: expected.id,
|
||||||
|
offerToken: expected.offerToken,
|
||||||
|
productType: ProductType.inapp)),
|
||||||
|
expected);
|
||||||
|
});
|
||||||
|
test('convertToUserChoiceDetailsProduct', () {
|
||||||
|
const GooglePlayUserChoiceDetailsProduct expectedProduct1 =
|
||||||
|
GooglePlayUserChoiceDetailsProduct(
|
||||||
|
id: 'id1',
|
||||||
|
offerToken: 'offerToken1',
|
||||||
|
productType: GooglePlayProductType.inapp);
|
||||||
|
const GooglePlayUserChoiceDetailsProduct expectedProduct2 =
|
||||||
|
GooglePlayUserChoiceDetailsProduct(
|
||||||
|
id: 'id2',
|
||||||
|
offerToken: 'offerToken2',
|
||||||
|
productType: GooglePlayProductType.subs);
|
||||||
|
const GooglePlayUserChoiceDetails expected = GooglePlayUserChoiceDetails(
|
||||||
|
originalExternalTransactionId: 'originalExternalTransactionId',
|
||||||
|
externalTransactionToken: 'externalTransactionToken',
|
||||||
|
products: <GooglePlayUserChoiceDetailsProduct>[
|
||||||
|
expectedProduct1,
|
||||||
|
expectedProduct2
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(
|
||||||
|
Translator.convertToUserChoiceDetails(UserChoiceDetailsWrapper(
|
||||||
|
originalExternalTransactionId:
|
||||||
|
expected.originalExternalTransactionId,
|
||||||
|
externalTransactionToken: expected.externalTransactionToken,
|
||||||
|
products: <UserChoiceDetailsProductWrapper>[
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: expectedProduct1.id,
|
||||||
|
offerToken: expectedProduct1.offerToken,
|
||||||
|
productType: ProductType.inapp),
|
||||||
|
UserChoiceDetailsProductWrapper(
|
||||||
|
id: expectedProduct2.id,
|
||||||
|
offerToken: expectedProduct2.offerToken,
|
||||||
|
productType: ProductType.subs),
|
||||||
|
])),
|
||||||
|
expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
Reference in New Issue
Block a user