productDetailsParamsList = new ArrayList<>();
+ productDetailsParamsList.add(productDetails1);
+ productDetailsParamsList.add(productDetails2);
+
+ BillingFlowParams billingFlowParams =
+ BillingFlowParams.newBuilder()
+ .setSubscriptionUpdateParams(
+ BillingFlowParams.SubscriptionUpdateParams.newBuilder()
+ .setOldPurchaseToken(purchaseTokenOfExistingSubscription)
+ .setSubscriptionReplacementMode(replacementModeForBasePlan) // Specify replacement mode for the base plan
+ .build())
+ .setProductDetailsParamsList(productDetailsParamsList)
+ .build();
+
+ billingClient.launchBillingFlow(activity, billingFlowParams);
+ // [END android_playbilling_subscription_replace_java]
+ }
+
+ public void changeSubscriptionPlanDeprecated(Activity activity, ProductDetails productDetails, int selectedOfferIndex) {
+ // [START android_playbilling_subscription_update_deprecated_java]
+ String offerToken = productDetails
+ .getSubscriptionOfferDetails().get(selectedOfferIndex)
+ .getOfferToken();
+
+ BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
+ .setProductDetailsParamsList(
+ Collections.singletonList(
+ BillingFlowParams.ProductDetailsParams.newBuilder()
+ // fetched via queryProductDetailsAsync
+ .setProductDetails(productDetails)
+ // offerToken can be found in
+ // ProductDetails=>SubscriptionOfferDetails
+ .setOfferToken(offerToken)
+ .build()))
+ .setSubscriptionUpdateParams(
+ BillingFlowParams.SubscriptionUpdateParams.newBuilder()
+ // purchaseToken can be found in Purchase#getPurchaseToken
+ .setOldPurchaseToken("old_purchase_token")
+ .setSubscriptionReplacementMode(BillingFlowParams.SubscriptionUpdateParams.ReplacementMode.CHARGE_FULL_PRICE)
+ .build())
+ .build();
+
+ BillingResult billingResult = billingClient.launchBillingFlow(activity, billingFlowParams);
+ // ...
+ // [END android_playbilling_subscription_update_deprecated_java]
+ }
+
+ public void showInAppMessages(Activity activity) {
+ // [START android_playbilling_inappmessaging_java]
+ InAppMessageParams inAppMessageParams = InAppMessageParams.newBuilder()
+ .addInAppMessageCategoryToShow(InAppMessageParams.InAppMessageCategoryId.TRANSACTIONAL)
+ .build();
+
+ billingClient.showInAppMessages(activity,
+ inAppMessageParams,
+ inAppMessageResult -> {
+ if (inAppMessageResult.getResponseCode()
+ == InAppMessageResult.InAppMessageResponseCode.NO_ACTION_NEEDED) {
+ // The flow has finished and there is no action needed from developers.
+ } else if (inAppMessageResult.getResponseCode()
+ == InAppMessageResult.InAppMessageResponseCode.SUBSCRIPTION_STATUS_UPDATED) {
+ // The subscription status changed. For example, a subscription
+ // has been recovered from a suspend state. Developers should
+ // expect the purchase token to be returned with this response
+ // code and use the purchase token with the Google Play
+ // Developer API.
+ queryPurchases(); // Re-query purchases to update UI
+ }
+ });
+ // [END android_playbilling_inappmessaging_java]
+ }
+
+
+
+
+ // A placeholder method for handlePurchase since it's referenced in multiple snippets.
+ private void handlePurchase(Purchase purchase) {
+ if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) {
+ if (!purchase.isAcknowledged()) {
+ acknowledgePurchase(purchase);
+ }
+ }
+ }
+}
diff --git a/play-billing-library/app/src/main/java/com/google/play/billing/samples/managedcatalogue/billing/BillingServiceClient.java b/play-billing-library/app/src/main/java/com/google/play/billing/samples/managedcatalogue/billing/BillingServiceClient.java
new file mode 100644
index 00000000..f81321da
--- /dev/null
+++ b/play-billing-library/app/src/main/java/com/google/play/billing/samples/managedcatalogue/billing/BillingServiceClient.java
@@ -0,0 +1,160 @@
+/* Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.play.billing.samples.managedcatalogue.billing;
+
+import android.app.Activity;
+import android.util.Log;
+import androidx.annotation.VisibleForTesting;
+import androidx.appcompat.app.AppCompatActivity;
+import com.android.billingclient.api.BillingClient;
+import com.android.billingclient.api.BillingClient.BillingResponseCode;
+import com.android.billingclient.api.BillingClientStateListener;
+import com.android.billingclient.api.BillingFlowParams;
+import com.android.billingclient.api.BillingResult;
+import com.android.billingclient.api.PendingPurchasesParams;
+import com.android.billingclient.api.ProductDetails;
+import com.android.billingclient.api.ProductDetailsResponseListener;
+import com.android.billingclient.api.QueryProductDetailsParams;
+import com.android.billingclient.api.QueryProductDetailsParams.Product;
+import com.android.billingclient.api.QueryProductDetailsResult;
+import com.google.common.collect.ImmutableList;
+import java.util.List;
+
+/**
+ * Manages interactions with the Google Play Billing Library for handling pre-orders.
+ *
+ * This class encapsulates the setup of the {@link BillingClient}, manages the connection
+ * lifecycle (including retries), queries product details, and notifies a listener.
+ */
+public class BillingServiceClient {
+
+ private static final String TAG = "BillingServiceClient";
+ private BillingClient billingClient;
+ private final AppCompatActivity activity;
+ private final BillingServiceClientListener listener;
+
+ public BillingServiceClient(AppCompatActivity activity, BillingServiceClientListener listener) {
+ this.activity = activity;
+ this.listener = listener;
+ billingClient = createBillingClient();
+ }
+
+ // Constructor for testing
+ @VisibleForTesting
+ BillingServiceClient(
+ AppCompatActivity activity,
+ BillingServiceClientListener listener,
+ BillingClient billingClient) {
+ this.activity = activity;
+ this.listener = listener;
+ this.billingClient = billingClient;
+ }
+
+ /**
+ * Starts the billing connection with Google Play. This method should be called exactly once
+ * before any other methods in this class.
+ *
+ * @param productList The list of products to query for after the connection is established.
+ */
+ public void startBillingConnection(List productList) {
+ billingClient.startConnection(
+ new BillingClientStateListener() {
+ @Override
+ public void onBillingSetupFinished(BillingResult billingResult) {
+ if (billingResult.getResponseCode() == BillingResponseCode.OK) {
+ Log.d(TAG, "Billing Client Connection Successful");
+ queryProductDetails(productList);
+ } else {
+ Log.e(TAG, "Billing Client Connection Failed: " + billingResult.getDebugMessage());
+ listener.onBillingSetupFailed(billingResult); // Propagate the error to the listener to show a message to the user.
+ }
+ }
+
+ @Override
+ public void onBillingServiceDisconnected() {
+ Log.e(TAG, "Billing Client Connection Lost");
+ listener.onBillingError("Billing Connection Lost");
+ }
+ });
+ }
+
+ /**
+ * Launches the billing flow for the product with the given offer token.
+ *
+ * @param activity The activity instance from which the billing flow will be launched.
+ * @param productDetails The product details of the product to purchase.
+ * @param offerToken The offer token of the product to purchase.
+ * @return The result of the billing flow.
+ */
+ public void launchPurchase(Activity activity, ProductDetails productDetails, String offerToken) {
+ ImmutableList productDetailsParamsList =
+ ImmutableList.of(
+ BillingFlowParams.ProductDetailsParams.newBuilder()
+ .setProductDetails(productDetails)
+ .setOfferToken(offerToken)
+ .build());
+ BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder()
+ .setProductDetailsParamsList(productDetailsParamsList)
+ .build();
+ billingClient.launchBillingFlow(activity, billingFlowParams);
+}
+
+ /**
+ * Ends the billing connection with Google Play. This method should be called when the app is
+ * closed.
+ */
+ public void endBillingConnection() {
+ if (billingClient != null && billingClient.isReady()) {
+ billingClient.endConnection();
+ billingClient = null;
+ }
+ }
+
+ private BillingClient createBillingClient() {
+ return BillingClient.newBuilder(activity)
+ .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build())
+ // For one-time products, add a listener to acknowledge the purchases. This will notify
+ // Google the purchase was processed.
+ // For client-only apps, use billingClient.acknowledgePurchase().
+ // If you have a secure backend, you must acknowledge purchases on your server using the
+ // server-side API.
+ // See https://developer.android.com/google/play/billing/security#acknowledge
+ .setListener((billingResult, purchases) -> {})
+ .enableAutoServiceReconnection()
+ .build();
+ }
+
+ private void queryProductDetails(List productList) {
+ QueryProductDetailsParams queryProductDetailsParams =
+ QueryProductDetailsParams.newBuilder().setProductList(productList).build();
+
+ billingClient.queryProductDetailsAsync(
+ queryProductDetailsParams,
+ new ProductDetailsResponseListener() {
+ @Override
+ public void onProductDetailsResponse(
+ BillingResult billingResult, QueryProductDetailsResult productDetailsResponse) {
+ if (billingResult.getResponseCode() == BillingResponseCode.OK) {
+ List productDetailsList =
+ productDetailsResponse.getProductDetailsList();
+ listener.onProductDetailsResponse(productDetailsList);
+ } else {
+ Log.e(TAG, "QueryProductDetailsAsync Failed: " + billingResult.getDebugMessage());
+ listener.onBillingError("Query Products Failed: " + billingResult.getResponseCode());
+ }
+ }
+ });
+ }
+}
diff --git a/play-billing-library/app/src/main/java/com/google/play/billing/samples/managedcatalogue/billing/BillingServiceClientListener.java b/play-billing-library/app/src/main/java/com/google/play/billing/samples/managedcatalogue/billing/BillingServiceClientListener.java
new file mode 100644
index 00000000..8c228584
--- /dev/null
+++ b/play-billing-library/app/src/main/java/com/google/play/billing/samples/managedcatalogue/billing/BillingServiceClientListener.java
@@ -0,0 +1,28 @@
+/* Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package com.google.play.billing.samples.managedcatalogue.billing;
+
+import com.android.billingclient.api.BillingResult;
+import com.android.billingclient.api.ProductDetails;
+import java.util.List;
+
+/** Listener interface for handling responses from the BillingServiceClient. */
+public interface BillingServiceClientListener {
+ void onProductDetailsResponse(List productDetailsList);
+
+ void onBillingSetupFailed(BillingResult billingResult);
+
+ void onBillingError(String errorMsg);
+}
diff --git a/play-billing-library/build.gradle b/play-billing-library/build.gradle
new file mode 100644
index 00000000..a602bdf8
--- /dev/null
+++ b/play-billing-library/build.gradle
@@ -0,0 +1,45 @@
+/* Copyright 2022 Google LLC
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * https://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+
+ repositories {
+ google()
+ mavenCentral()
+ }
+ dependencies {
+ classpath "com.android.tools.build:gradle:8.9.1"
+
+ classpath 'com.google.android.gms:strict-version-matcher-plugin:1.2.1'
+
+ // OSS plugin
+ classpath 'com.google.android.gms:oss-licenses-plugin:0.10.6'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/play-billing-library/gradle.properties b/play-billing-library/gradle.properties
new file mode 100644
index 00000000..2a19ff5b
--- /dev/null
+++ b/play-billing-library/gradle.properties
@@ -0,0 +1,20 @@
+# Project-wide Gradle settings.
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+org.gradle.jvmargs=-Xmx2048m
+# When configured, Gradle will run in incubating parallel mode.
+# This option should only be used with decoupled projects. More details, visit
+# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
+# org.gradle.parallel=true
+# AndroidX package structure to make it clearer which packages are bundled with the
+# Android operating system, and which are packaged with your app"s APK
+# https://developer.android.com/topic/libraries/support-library/androidx-rn
+android.useAndroidX=true
+# Automatically convert third-party libraries to use AndroidX
+android.enableJetifier=true
+
diff --git a/play-billing-library/gradle/wrapper/gradle-wrapper.jar b/play-billing-library/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..1b33c55b
Binary files /dev/null and b/play-billing-library/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/play-billing-library/gradle/wrapper/gradle-wrapper.properties b/play-billing-library/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..3106b43b
--- /dev/null
+++ b/play-billing-library/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,8 @@
+#Fri Aug 14 10:59:44 CDT 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+networkTimeout=10000
+validateDistributionUrl=true
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists