From 60554923b666abc10d9633511c15ce6a9de85eea Mon Sep 17 00:00:00 2001 From: sidd607 Date: Fri, 3 Jul 2026 10:57:13 +0000 Subject: [PATCH 1/3] add code snippets for User Billing --- gradle/libs.versions.toml | 2 +- .../pbl/kotlin/billingchoice/Integration.kt | 497 ++++++++++++++++++ 2 files changed, 498 insertions(+), 1 deletion(-) create mode 100644 playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index fb6b93970..9f45a5190 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -91,7 +91,7 @@ mockitoKotlin = "5.3.1" okHttp = "5.4.0" playServicesFitness = "21.3.0" playServicesWearable = "20.0.1" -playbilling = "9.0.0" +playbilling = "9.1.0" protobuf = "4.35.1" protoc = "0.10.0" protolayout = "1.4.0" diff --git a/playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt b/playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt new file mode 100644 index 000000000..75736c84b --- /dev/null +++ b/playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt @@ -0,0 +1,497 @@ +/* + * Copyright 2026 The Android Open Source Project + * + * 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.example.pbl.kotlin.billingchoice + +import android.app.Activity +import android.content.Context +import android.net.Uri +import android.widget.ImageView +import android.widget.TextView +import com.android.billingclient.api.BillingClient +import com.android.billingclient.api.BillingClient.BillingResponseCode +import com.android.billingclient.api.BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails.ChoiceScreenType +import com.android.billingclient.api.BillingProgramReportingDetailsParams.DeveloperBillingType +import com.android.billingclient.api.BillingClientStateListener +import com.android.billingclient.api.BillingFlowParams +import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams +import com.android.billingclient.api.BillingProgramReportingDetails +import com.android.billingclient.api.BillingProgramReportingDetailsListener +import com.android.billingclient.api.BillingProgramReportingDetailsParams +import com.android.billingclient.api.BillingResult +import com.android.billingclient.api.DeveloperBillingOptionParams +import com.android.billingclient.api.DeveloperProvidedBillingListener +import com.android.billingclient.api.EnableBillingProgramParams +import com.android.billingclient.api.GetBillingChoiceInfoParams +import com.android.billingclient.api.LaunchExternalLinkParams +import com.android.billingclient.api.PendingPurchasesParams +import com.android.billingclient.api.ProductDetails +import com.android.billingclient.api.PurchasesUpdatedListener +import com.android.billingclient.api.createBillingProgramReportingDetails +import com.android.billingclient.api.getBillingChoiceInfo +import com.android.billingclient.api.isBillingProgramAvailable +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +private class Integration( + private val context: Context, + private val activity: Activity, + private val billingClient: BillingClient, + private val developerProvidedBillingListener: DeveloperProvidedBillingListener, + private val purchasesUpdatedListener: PurchasesUpdatedListener +) { + + private lateinit var productDetailsParamsList: List + private lateinit var playBillingLoyaltyTextView: TextView + private lateinit var playBillingImageView: ImageView + private lateinit var yourLinkUri: Uri + private lateinit var transactionToken: String + private lateinit var productDetailsNewPlan: ProductDetails + private lateinit var offerTokenNewPlan: String + + private fun loadImage(url: String, imageView: ImageView) {} + + // Scenario 1A: Google-rendered, In-app + suspend fun startConnectionGoogleRenderedInApp() { + // [START android_playbilling_billingchoice_connect_google_rendered_in_app] + // Build the parameters to enable the Billing Choice program and assign the listener + // to handle user selection of the developer-provided billing option. + val params = EnableBillingProgramParams.newBuilder() + .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) + .setDeveloperProvidedBillingListener(developerProvidedBillingListener) + .build() + + // Build the parameters to enable support for pending purchases. + val pendingPurchasesParams = PendingPurchasesParams.newBuilder() + .enableOneTimeProducts() + .build() + + // Construct the BillingClient instance with the purchases updated listener, + // pending purchases support, and the billing choice params. + val billingClient = BillingClient.newBuilder(context) + .setListener(purchasesUpdatedListener) + .enablePendingPurchases(pendingPurchasesParams) + .enableBillingProgram(params) + .build() + + // Establish a connection to Google Play + val billingResult = suspendCancellableCoroutine { continuation -> + billingClient.startConnection(object : BillingClientStateListener { + // Called when the connection setup process completes. + override fun onBillingSetupFinished(billingResult: BillingResult) { + // Resume the coroutine and pass back the BillingResult to the caller. + continuation.resume(billingResult) + } + + // Called if the connection to the Play Store service is dropped. + // This prevents the await or suspension point from hanging indefinitely. + override fun onBillingServiceDisconnected() { + continuation.resume( + BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) + .setDebugMessage("Billing service disconnected during connection setup") + .build() + ) + } + }) + } + // [END android_playbilling_billingchoice_connect_google_rendered_in_app] + } + + suspend fun verifyAvailabilityGoogleRenderedInApp() { + // [START android_playbilling_billingchoice_verify_google_rendered_in_app] + val (billingResult, billingProgramAvailabilityDetails) = billingClient.isBillingProgramAvailable(BillingClient.BillingProgram.BILLING_CHOICE) + + if (billingResult.responseCode == BillingResponseCode.OK) { + val billingChoiceAvailabilityDetails = billingProgramAvailabilityDetails.billingChoiceAvailabilityDetails + if (billingChoiceAvailabilityDetails != null && + billingChoiceAvailabilityDetails.choiceScreenType == ChoiceScreenType.GOOGLE_RENDERED + ) { + // Billing choice is available. Query products and proceed. + + } else { + // Fallback to other available programs. + } + } else { + // Fallback to other available programs. + } + // [END android_playbilling_billingchoice_verify_google_rendered_in_app] + } + + fun launchBillingFlowGoogleRenderedInApp() { + // [START android_playbilling_billingchoice_launch_google_rendered_in_app] + val developerBillingOptionParams = DeveloperBillingOptionParams.newBuilder() + .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) + .build() + + val billingFlowParams = BillingFlowParams.newBuilder() + .setProductDetailsParamsList(productDetailsParamsList) + .enableDeveloperBillingOption(developerBillingOptionParams) + .build() + + val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams) + // [END android_playbilling_billingchoice_launch_google_rendered_in_app] + } + + // Scenario 1B: Developer-rendered, In-app + suspend fun startConnectionDeveloperRenderedInApp() { + // [START android_playbilling_billingchoice_connect_developer_rendered_in_app] + // Build the parameters to enable the Billing Choice program. + val params = EnableBillingProgramParams.newBuilder() + .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) + .build() + + // Construct the BillingClient instance with the purchases updated listener, + // pending purchases support, and the billing choice params. + val billingClient = BillingClient.newBuilder(context) + .setListener(purchasesUpdatedListener) + .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()) + .enableBillingProgram(params) + .build() + + // Establish a connection to Google Play + val billingResult = suspendCancellableCoroutine { continuation -> + billingClient.startConnection(object : BillingClientStateListener { + // Called when the connection setup process completes. + override fun onBillingSetupFinished(billingResult: BillingResult) { + // Resume the coroutine and pass back the BillingResult to the caller. + continuation.resume(billingResult) + } + + // Called if the connection to the Play Store service is dropped. + // This prevents the await or suspension point from hanging indefinitely. + override fun onBillingServiceDisconnected() { + continuation.resume( + BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) + .setDebugMessage("Billing service disconnected during connection setup") + .build() + ) + } + }) + } + // [END android_playbilling_billingchoice_connect_developer_rendered_in_app] + } + + suspend fun verifyAvailabilityDeveloperRenderedInApp() { + // [START android_playbilling_billingchoice_verify_developer_rendered_in_app] + val (billingResult, billingProgramAvailabilityDetails) = + billingClient.isBillingProgramAvailable(BillingClient.BillingProgram.BILLING_CHOICE) + + if (billingResult.responseCode == BillingResponseCode.OK) { + val billingChoiceAvailabilityDetails = + billingProgramAvailabilityDetails.billingChoiceAvailabilityDetails + + if (billingChoiceAvailabilityDetails != null && + billingChoiceAvailabilityDetails.choiceScreenType == ChoiceScreenType.DEVELOPER_RENDERED + ) { + // Billing choice is available. Query products and proceed. + // You can inspect details such as: + // - billingChoiceAvailabilityDetails.choiceScreenType + // - billingChoiceAvailabilityDetails.isExternalLinkAvailable + } else { + // Fallback to other available programs. + } + } else { + // Fallback to other available programs. + } + // [END android_playbilling_billingchoice_verify_developer_rendered_in_app] + } + + suspend fun getBillingChoiceInfoDeveloperRenderedInApp() { + // [START android_playbilling_billingchoice_info_developer_rendered_in_app] + // 1. Create the params required for the request + val params = GetBillingChoiceInfoParams.newBuilder() + .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) + .setPlayBillingChoiceImageLayout(GetBillingChoiceInfoParams.ImageLayout.RECTANGULAR_FOUR_BY_ONE) + .build() + + // 2. Call the suspend method on your billingClient instance + val (billingResult, playBillingChoiceInfo) = billingClient.getBillingChoiceInfo(params) + + if (billingResult.responseCode == BillingResponseCode.OK && playBillingChoiceInfo != null) { + // Access the URL of the image associated with the Play Billing Choice + val imageUrl = playBillingChoiceInfo.playBillingChoiceImageUrl + + // Access the Play Loyalty string information, if available + val loyaltyInfo = playBillingChoiceInfo.playBillingLoyaltyInfo + + // Populate your developer-rendered UI elements + playBillingLoyaltyTextView.text = loyaltyInfo + loadImage(imageUrl, playBillingImageView) + } else { + // Handle error scenarios + } + // [END android_playbilling_billingchoice_info_developer_rendered_in_app] + } + + suspend fun createTokenDeveloperRenderedInApp() { + // [START android_playbilling_billingchoice_create_token_developer_rendered_in_app] + // Build the parameters specifying the billing program and that the billing type is IN_APP. + val params = BillingProgramReportingDetailsParams.newBuilder() + .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) + .setDeveloperBillingType(DeveloperBillingType.IN_APP) + .build() + + // Call the suspending extension function to request the reporting details + val (billingResult, billingProgramReportingDetails) = + billingClient.createBillingProgramReportingDetails(params) + + if (billingResult.responseCode != BillingResponseCode.OK) { + // Handle failures such as retrying due to network errors. + return + } + + // Extract the transaction token from the returned reporting details + val transactionToken = billingProgramReportingDetails?.externalTransactionToken + + // Persist the external transaction token locally. Pass it to + // DeveloperBillingOptionParams when launchBillingFlow is called. + // It can also be used as part of your external website + // [END android_playbilling_billingchoice_create_token_developer_rendered_in_app] + } + + // Scenario 2A: Google-rendered, External Link + suspend fun verifyAvailabilityGoogleRenderedExternalLink() { + // [START android_playbilling_billingchoice_verify_google_rendered_external_link] + // Check the availability of the billing choice program asynchronously using coroutines + val (billingResult, billingProgramAvailabilityDetails) = + billingClient.isBillingProgramAvailable(BillingClient.BillingProgram.BILLING_CHOICE) + + // Ensure the response code is OK + if (billingResult.responseCode == BillingResponseCode.OK) { + // Retrieve the availability details specific to the billing choice program + val billingChoiceAvailabilityDetails = + billingProgramAvailabilityDetails.billingChoiceAvailabilityDetails + + // Check if billing choice is available, renders via Google Play, and external link is supported + if (billingChoiceAvailabilityDetails != null && + billingChoiceAvailabilityDetails.choiceScreenType == ChoiceScreenType.GOOGLE_RENDERED && + billingChoiceAvailabilityDetails.isExternalLinkAvailable + ) { + // Billing choice is available and external transaction links are supported. Query products and proceed. + } else { + // Fallback to other available programs. + } + } else { + // Fallback to other available programs. + } + // [END android_playbilling_billingchoice_verify_google_rendered_external_link] + } + + suspend fun createTokenGoogleRenderedExternalLink() { + // [START android_playbilling_billingchoice_create_token_google_rendered_external_link] + // Build the parameters for creating reporting details + val params = + BillingProgramReportingDetailsParams.newBuilder() + .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) + .setDeveloperBillingType(DeveloperBillingType.EXTERNAL_LINK) + .build() + + // Call the suspend function to create billing program reporting details + val (billingResult, billingProgramReportingDetails) = + billingClient.createBillingProgramReportingDetails(params) + + // Handle response failure cases + if (billingResult.responseCode != BillingResponseCode.OK) { + // Handle failures such as retrying due to network errors. + return + } + + // Retrieve the external transaction token + val transactionToken = + billingProgramReportingDetails?.externalTransactionToken + + // Persist the external transaction token locally. Pass it to + // DeveloperBillingOptionParams when launchBillingFlow is called. + // It can also be used as part of your external website + // [END android_playbilling_billingchoice_create_token_google_rendered_external_link] + } + + fun launchBillingFlowGoogleRenderedExternalLink() { + // [START android_playbilling_billingchoice_launch_google_rendered_external_link] + // Build the developer billing option parameters with the external link URI, + // the transaction token, and browser/app launch mode. + val developerBillingOptionParams = + DeveloperBillingOptionParams.newBuilder() + .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) + .setLinkUri(Uri.parse("https://www.example.com/external/purchase")) + .setExternalTransactionToken(transactionToken) + .setLaunchMode( + DeveloperBillingOptionParams.LaunchMode.LAUNCH_IN_EXTERNAL_BROWSER_OR_APP + ) + .build() + // [END android_playbilling_billingchoice_launch_google_rendered_external_link] + } + + // Scenario 2B: Developer-rendered, External Link + suspend fun verifyAvailabilityDeveloperRenderedExternalLink() { + // [START android_playbilling_billingchoice_verify_developer_rendered_external_link] + // Check the availability of the billing choice program asynchronously using a coroutine + val (billingResult, billingProgramAvailabilityDetails) = + billingClient.isBillingProgramAvailable(BillingClient.BillingProgram.BILLING_CHOICE) + + // Ensure the response code is OK + if (billingResult.responseCode == BillingResponseCode.OK) { + // Retrieve the billing choice availability details + val billingChoiceAvailabilityDetails = + billingProgramAvailabilityDetails.billingChoiceAvailabilityDetails + + // Check if billing choice details are available, choice screen is developer-rendered, + // and external transaction links are supported. + if (billingChoiceAvailabilityDetails != null && + billingChoiceAvailabilityDetails.choiceScreenType == ChoiceScreenType.DEVELOPER_RENDERED && + billingChoiceAvailabilityDetails.isExternalLinkAvailable + ) { + // Billing choice is available and external transaction links are supported. + // Query products and proceed. + } else { + // Fallback to other available programs. + } + } else { + // Fallback to other available programs. + } + // [END android_playbilling_billingchoice_verify_developer_rendered_external_link] + } + + fun launchExternalLinkDeveloperRenderedExternalLink() { + // [START android_playbilling_billingchoice_launch_external_developer_rendered_external_link] + // An activity reference from which the purchase flow will be launched. + val activity: Activity = activity + + val params = LaunchExternalLinkParams.newBuilder() + .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) + // You can pass along the external transaction token from + // BillingProgramReportingDetails as a URL parameter in the URI + .setLinkUri(yourLinkUri) + .setLinkType(LaunchExternalLinkParams.LinkType.LINK_TO_DIGITAL_CONTENT_OFFER) + .setLaunchMode( + LaunchExternalLinkParams.LaunchMode.LAUNCH_IN_EXTERNAL_BROWSER_OR_APP + ) + .build() + + // Call launchExternalLink with a callback + billingClient.launchExternalLink(activity, params) { billingResult -> + if (billingResult.responseCode == BillingResponseCode.OK) { + // Proceed with the rest of the purchase flow. If the user + // purchases an item, be sure to report the transaction to Google + // Play. + } else { + // Handle failures such as retrying due to network errors. + } + } + // [END android_playbilling_billingchoice_launch_external_developer_rendered_external_link] + } + + // Subscription Replacement + fun subsReplacementGoogleRenderedInApp(externalTransactionId: String) { + // [START android_playbilling_billingchoice_subs_replace_google_rendered_in_app] + // The external transaction ID from the current + // alternative billing subscription. + val externalTransactionId = externalTransactionId + + val developerBillingOptionParams = DeveloperBillingOptionParams.newBuilder() + .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) + .build() + + val billingFlowParams = BillingFlowParams.newBuilder() + .setProductDetailsParamsList( + listOf( + BillingFlowParams.ProductDetailsParams.newBuilder() + // Fetched using queryProductDetailsAsync. + .setProductDetails(productDetailsNewPlan) + // offerIdToken can be found in + // ProductDetails=>SubscriptionOfferDetails. + .setOfferToken(offerTokenNewPlan) + .build() + ) + ) + .setSubscriptionUpdateParams( + BillingFlowParams.SubscriptionUpdateParams.newBuilder() + .setOriginalExternalTransactionId(externalTransactionId) + .build() + ) + .enableDeveloperBillingOption(developerBillingOptionParams) + .build() + + val billingResult = billingClient.launchBillingFlow(activity, billingFlowParams) + + // When the user selects the alternative billing flow, + // the DeveloperProvidedBillingListener is triggered. + // [END android_playbilling_billingchoice_subs_replace_google_rendered_in_app] + } + + fun subsReplacementGoogleRenderedExternalLinkSample1(externalTransactionId: String) { + // [START android_playbilling_billingchoice_subs_replace_google_rendered_external_link_sample1] + val externalTransactionId = externalTransactionId + + // 1. Construct DeveloperBillingOptionParams indicating the billing program + val developerBillingOptionParams = DeveloperBillingOptionParams.newBuilder() + .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) + .build() + + // 2. Build BillingFlowParams combining DeveloperBillingOptionParams and SubscriptionUpdateParams + val billingFlowParams = BillingFlowParams.newBuilder() + .setProductDetailsParamsList( + listOf( + BillingFlowParams.ProductDetailsParams.newBuilder() + // Fetched using queryProductDetailsAsync. + .setProductDetails(productDetailsNewPlan) + // offerIdToken can be found in ProductDetails=>SubscriptionOfferDetails. + .setOfferToken(offerTokenNewPlan) + .build() + ) + ) + .setSubscriptionUpdateParams( + SubscriptionUpdateParams.newBuilder() + .setOriginalExternalTransactionId(externalTransactionId) + .build() + ) + .enableDeveloperBillingOption(developerBillingOptionParams) + .build() + // [END android_playbilling_billingchoice_subs_replace_google_rendered_external_link_sample1] + } + + fun subsReplacementGoogleRenderedExternalLinkSample2() { + // [START android_playbilling_billingchoice_subs_replace_google_rendered_external_link_sample2] + val params = + BillingProgramReportingDetailsParams.newBuilder() + .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) + .setDeveloperBillingType(DeveloperBillingType.EXTERNAL_LINK) + .build() + + billingClient.createBillingProgramReportingDetailsAsync( + params, + object : BillingProgramReportingDetailsListener { + override fun onCreateBillingProgramReportingDetailsResponse( + billingResult: BillingResult, + billingProgramReportingDetails: BillingProgramReportingDetails? + ) { + if (billingResult.responseCode != BillingResponseCode.OK) { + // Handle failures such as retrying due to network errors. + return + } + val externalTransactionToken = + billingProgramReportingDetails?.externalTransactionToken + // Persist the external transaction token locally. Pass it to + // the external website using DeveloperBillingOptionParams when + // launchBillingFlow is called. + } + } + ) + // [END android_playbilling_billingchoice_subs_replace_google_rendered_external_link_sample2] + } +} From 4a43321c3729a82ee298230489afda689008ffa5 Mon Sep 17 00:00:00 2001 From: sidd607 Date: Fri, 3 Jul 2026 11:12:19 +0000 Subject: [PATCH 2/3] added gemini suggested code reviews --- .../pbl/kotlin/billingchoice/Integration.kt | 104 ++++++++++-------- 1 file changed, 57 insertions(+), 47 deletions(-) diff --git a/playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt b/playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt index 75736c84b..bcf94468e 100644 --- a/playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt +++ b/playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt @@ -54,14 +54,6 @@ private class Integration( private val purchasesUpdatedListener: PurchasesUpdatedListener ) { - private lateinit var productDetailsParamsList: List - private lateinit var playBillingLoyaltyTextView: TextView - private lateinit var playBillingImageView: ImageView - private lateinit var yourLinkUri: Uri - private lateinit var transactionToken: String - private lateinit var productDetailsNewPlan: ProductDetails - private lateinit var offerTokenNewPlan: String - private fun loadImage(url: String, imageView: ImageView) {} // Scenario 1A: Google-rendered, In-app @@ -93,18 +85,22 @@ private class Integration( // Called when the connection setup process completes. override fun onBillingSetupFinished(billingResult: BillingResult) { // Resume the coroutine and pass back the BillingResult to the caller. - continuation.resume(billingResult) + if (continuation.isActive) { + continuation.resume(billingResult) + } } // Called if the connection to the Play Store service is dropped. // This prevents the await or suspension point from hanging indefinitely. override fun onBillingServiceDisconnected() { - continuation.resume( - BillingResult.newBuilder() - .setResponseCode(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) - .setDebugMessage("Billing service disconnected during connection setup") - .build() - ) + if (continuation.isActive) { + continuation.resume( + BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) + .setDebugMessage("Billing service disconnected during connection setup") + .build() + ) + } } }) } @@ -131,7 +127,9 @@ private class Integration( // [END android_playbilling_billingchoice_verify_google_rendered_in_app] } - fun launchBillingFlowGoogleRenderedInApp() { + fun launchBillingFlowGoogleRenderedInApp( + productDetailsParamsList: List + ) { // [START android_playbilling_billingchoice_launch_google_rendered_in_app] val developerBillingOptionParams = DeveloperBillingOptionParams.newBuilder() .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) @@ -154,11 +152,16 @@ private class Integration( .setBillingProgram(BillingClient.BillingProgram.BILLING_CHOICE) .build() + // Build the parameters to enable support for pending purchases. + val pendingPurchasesParams = PendingPurchasesParams.newBuilder() + .enableOneTimeProducts() + .build() + // Construct the BillingClient instance with the purchases updated listener, // pending purchases support, and the billing choice params. val billingClient = BillingClient.newBuilder(context) .setListener(purchasesUpdatedListener) - .enablePendingPurchases(PendingPurchasesParams.newBuilder().enableOneTimeProducts().build()) + .enablePendingPurchases(pendingPurchasesParams) .enableBillingProgram(params) .build() @@ -168,18 +171,22 @@ private class Integration( // Called when the connection setup process completes. override fun onBillingSetupFinished(billingResult: BillingResult) { // Resume the coroutine and pass back the BillingResult to the caller. - continuation.resume(billingResult) + if (continuation.isActive) { + continuation.resume(billingResult) + } } // Called if the connection to the Play Store service is dropped. // This prevents the await or suspension point from hanging indefinitely. override fun onBillingServiceDisconnected() { - continuation.resume( - BillingResult.newBuilder() - .setResponseCode(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) - .setDebugMessage("Billing service disconnected during connection setup") - .build() - ) + if (continuation.isActive) { + continuation.resume( + BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.SERVICE_DISCONNECTED) + .setDebugMessage("Billing service disconnected during connection setup") + .build() + ) + } } }) } @@ -211,7 +218,10 @@ private class Integration( // [END android_playbilling_billingchoice_verify_developer_rendered_in_app] } - suspend fun getBillingChoiceInfoDeveloperRenderedInApp() { + suspend fun getBillingChoiceInfoDeveloperRenderedInApp( + playBillingLoyaltyTextView: TextView, + playBillingImageView: ImageView + ) { // [START android_playbilling_billingchoice_info_developer_rendered_in_app] // 1. Create the params required for the request val params = GetBillingChoiceInfoParams.newBuilder() @@ -321,7 +331,7 @@ private class Integration( // [END android_playbilling_billingchoice_create_token_google_rendered_external_link] } - fun launchBillingFlowGoogleRenderedExternalLink() { + fun launchBillingFlowGoogleRenderedExternalLink(transactionToken: String) { // [START android_playbilling_billingchoice_launch_google_rendered_external_link] // Build the developer billing option parameters with the external link URI, // the transaction token, and browser/app launch mode. @@ -367,7 +377,7 @@ private class Integration( // [END android_playbilling_billingchoice_verify_developer_rendered_external_link] } - fun launchExternalLinkDeveloperRenderedExternalLink() { + fun launchExternalLinkDeveloperRenderedExternalLink(yourLinkUri: Uri) { // [START android_playbilling_billingchoice_launch_external_developer_rendered_external_link] // An activity reference from which the purchase flow will be launched. val activity: Activity = activity @@ -397,7 +407,11 @@ private class Integration( } // Subscription Replacement - fun subsReplacementGoogleRenderedInApp(externalTransactionId: String) { + fun subsReplacementGoogleRenderedInApp( + externalTransactionId: String, + productDetailsNewPlan: ProductDetails, + offerTokenNewPlan: String + ) { // [START android_playbilling_billingchoice_subs_replace_google_rendered_in_app] // The external transaction ID from the current // alternative billing subscription. @@ -434,7 +448,11 @@ private class Integration( // [END android_playbilling_billingchoice_subs_replace_google_rendered_in_app] } - fun subsReplacementGoogleRenderedExternalLinkSample1(externalTransactionId: String) { + fun subsReplacementGoogleRenderedExternalLinkSample1( + externalTransactionId: String, + productDetailsNewPlan: ProductDetails, + offerTokenNewPlan: String + ) { // [START android_playbilling_billingchoice_subs_replace_google_rendered_external_link_sample1] val externalTransactionId = externalTransactionId @@ -473,25 +491,17 @@ private class Integration( .setDeveloperBillingType(DeveloperBillingType.EXTERNAL_LINK) .build() - billingClient.createBillingProgramReportingDetailsAsync( - params, - object : BillingProgramReportingDetailsListener { - override fun onCreateBillingProgramReportingDetailsResponse( - billingResult: BillingResult, - billingProgramReportingDetails: BillingProgramReportingDetails? - ) { - if (billingResult.responseCode != BillingResponseCode.OK) { - // Handle failures such as retrying due to network errors. - return - } - val externalTransactionToken = - billingProgramReportingDetails?.externalTransactionToken - // Persist the external transaction token locally. Pass it to - // the external website using DeveloperBillingOptionParams when - // launchBillingFlow is called. - } + billingClient.createBillingProgramReportingDetailsAsync(params) { billingResult, billingProgramReportingDetails -> + if (billingResult.responseCode != BillingResponseCode.OK) { + // Handle failures such as retrying due to network errors. + return@createBillingProgramReportingDetailsAsync } - ) + val externalTransactionToken = + billingProgramReportingDetails?.externalTransactionToken + // Persist the external transaction token locally. Pass it to + // the external website using DeveloperBillingOptionParams when + // launchBillingFlow is called. + } // [END android_playbilling_billingchoice_subs_replace_google_rendered_external_link_sample2] } } From 80c112eb632b6a4d161a9ea1f946f7a71c8a8216 Mon Sep 17 00:00:00 2001 From: sidd607 <6475495+sidd607@users.noreply.github.com> Date: Fri, 3 Jul 2026 11:21:15 +0000 Subject: [PATCH 3/3] Apply Spotless --- .../com/example/pbl/kotlin/billingchoice/Integration.kt | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt b/playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt index bcf94468e..a29e6c2d2 100644 --- a/playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt +++ b/playbilling/src/main/java/com/example/pbl/kotlin/billingchoice/Integration.kt @@ -23,14 +23,12 @@ import android.widget.ImageView import android.widget.TextView import com.android.billingclient.api.BillingClient import com.android.billingclient.api.BillingClient.BillingResponseCode -import com.android.billingclient.api.BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails.ChoiceScreenType -import com.android.billingclient.api.BillingProgramReportingDetailsParams.DeveloperBillingType import com.android.billingclient.api.BillingClientStateListener import com.android.billingclient.api.BillingFlowParams import com.android.billingclient.api.BillingFlowParams.SubscriptionUpdateParams -import com.android.billingclient.api.BillingProgramReportingDetails -import com.android.billingclient.api.BillingProgramReportingDetailsListener +import com.android.billingclient.api.BillingProgramAvailabilityDetails.BillingChoiceAvailabilityDetails.ChoiceScreenType import com.android.billingclient.api.BillingProgramReportingDetailsParams +import com.android.billingclient.api.BillingProgramReportingDetailsParams.DeveloperBillingType import com.android.billingclient.api.BillingResult import com.android.billingclient.api.DeveloperBillingOptionParams import com.android.billingclient.api.DeveloperProvidedBillingListener @@ -43,8 +41,8 @@ import com.android.billingclient.api.PurchasesUpdatedListener import com.android.billingclient.api.createBillingProgramReportingDetails import com.android.billingclient.api.getBillingChoiceInfo import com.android.billingclient.api.isBillingProgramAvailable -import kotlinx.coroutines.suspendCancellableCoroutine import kotlin.coroutines.resume +import kotlinx.coroutines.suspendCancellableCoroutine private class Integration( private val context: Context, @@ -117,7 +115,6 @@ private class Integration( billingChoiceAvailabilityDetails.choiceScreenType == ChoiceScreenType.GOOGLE_RENDERED ) { // Billing choice is available. Query products and proceed. - } else { // Fallback to other available programs. }