Add more api calls for billing integration.

This commit is contained in:
Alex Hart 2024-08-19 15:32:45 -03:00 committed by mtang-signal
parent 26e79db057
commit 478e3a7233
3 changed files with 104 additions and 12 deletions

View file

@ -5,12 +5,22 @@
package org.thoughtcrime.securesms.billing
import android.app.Activity
/**
* Variant interface for the BillingApi.
*/
interface GooglePlayBillingApi {
fun isApiAvailable(): Boolean = false
suspend fun queryProducts() {}
suspend fun queryProducts() = Unit
/**
* Queries the user's current purchases. This enqueues a check and will
* propagate it to the normal callbacks in the api.
*/
suspend fun queryPurchases() = Unit
suspend fun launchBillingFlow(activity: Activity) = Unit
/**
* Empty implementation, to be used when play services are available but

View file

@ -1,7 +1,10 @@
package org.thoughtcrime.securesms.billing
import android.app.Activity
import android.content.Context
import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.ProductDetailsResult
import com.android.billingclient.api.PurchasesUpdatedListener
import org.signal.billing.BillingApi
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.util.RemoteConfig
@ -29,7 +32,25 @@ private class GooglePlayBillingApiImpl(context: Context) : GooglePlayBillingApi
val TAG = Log.tag(GooglePlayBillingApiImpl::class)
}
private val billingApi: BillingApi = BillingApi.getOrCreate(context)
private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
when {
billingResult.responseCode == BillingResponseCode.OK && purchases != null -> {
Log.d(TAG, "purchasesUpdatedListener: ${purchases.size} purchases.")
purchases.forEach {
// Handle purchases.
}
}
billingResult.responseCode == BillingResponseCode.USER_CANCELED -> {
// Handle user cancelled
Log.d(TAG, "purchasesUpdatedListener: User cancelled.")
}
else -> {
Log.d(TAG, "purchasesUpdatedListener: No purchases.")
}
}
}
private val billingApi: BillingApi = BillingApi.getOrCreate(context, purchasesUpdatedListener)
override fun isApiAvailable(): Boolean = billingApi.areSubscriptionsSupported()
@ -38,4 +59,15 @@ private class GooglePlayBillingApiImpl(context: Context) : GooglePlayBillingApi
Log.d(TAG, "queryProducts: ${products.billingResult.responseCode}, ${products.billingResult.debugMessage}")
}
override suspend fun queryPurchases() {
Log.d(TAG, "queryPurchases")
val purchaseResult = billingApi.queryPurchases()
purchasesUpdatedListener.onPurchasesUpdated(purchaseResult.billingResult, purchaseResult.purchasesList)
}
override suspend fun launchBillingFlow(activity: Activity) {
billingApi.launchBillingFlow(activity)
}
}

View file

@ -5,16 +5,24 @@
package org.signal.billing
import android.app.Activity
import android.content.Context
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClient.BillingResponseCode
import com.android.billingclient.api.BillingClient.ProductType
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingFlowParams
import com.android.billingclient.api.BillingFlowParams.ProductDetailsParams
import com.android.billingclient.api.BillingResult
import com.android.billingclient.api.PendingPurchasesParams
import com.android.billingclient.api.ProductDetails
import com.android.billingclient.api.ProductDetailsResult
import com.android.billingclient.api.PurchasesResult
import com.android.billingclient.api.PurchasesUpdatedListener
import com.android.billingclient.api.QueryProductDetailsParams
import com.android.billingclient.api.QueryPurchasesParams
import com.android.billingclient.api.queryProductDetails
import com.android.billingclient.api.queryPurchasesAsync
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
@ -38,7 +46,8 @@ import org.signal.core.util.logging.Log
* Care should be taken here to ensure only one instance of this exists at a time.
*/
class BillingApi private constructor(
context: Context
context: Context,
onPurchaseUpdateListener: PurchasesUpdatedListener
) {
companion object {
private val TAG = Log.tag(BillingApi::class)
@ -46,8 +55,8 @@ class BillingApi private constructor(
private var instance: BillingApi? = null
@Synchronized
fun getOrCreate(context: Context): BillingApi {
return instance ?: BillingApi(context).let {
fun getOrCreate(context: Context, onPurchaseUpdateListener: PurchasesUpdatedListener): BillingApi {
return instance ?: BillingApi(context, onPurchaseUpdateListener).let {
instance = it
it
}
@ -57,13 +66,8 @@ class BillingApi private constructor(
private val connectionState = MutableStateFlow<State>(State.Init)
private val coroutineScope = CoroutineScope(Dispatchers.Default)
private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases ->
Log.d(TAG, "purchasesUpdatedListener: ${billingResult.responseCode}")
Log.d(TAG, "purchasesUpdatedListener: Detected ${purchases?.size ?: 0} purchases.")
}
private val billingClient: BillingClient = BillingClient.newBuilder(context)
.setListener(purchasesUpdatedListener)
.setListener(onPurchaseUpdateListener)
.enablePendingPurchases(
PendingPurchasesParams.newBuilder()
.enableOneTimeProducts()
@ -88,7 +92,7 @@ class BillingApi private constructor(
val productList = listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("") // TODO [message-backups] where does the product id come from?
.setProductType(BillingClient.ProductType.SUBS)
.setProductType(ProductType.SUBS)
.build()
)
@ -103,6 +107,52 @@ class BillingApi private constructor(
}
}
suspend fun queryPurchases(): PurchasesResult {
val param = QueryPurchasesParams.newBuilder()
.setProductType(ProductType.SUBS)
.build()
return doOnConnectionReady {
billingClient.queryPurchasesAsync(param)
}
}
/**
* Launches the Google Play billing flow.
* Returns a billing result if we launched the flow, null otherwise.
*/
suspend fun launchBillingFlow(activity: Activity): BillingResult? {
val productDetails = queryProducts().productDetailsList
if (productDetails.isNullOrEmpty()) {
Log.w(TAG, "No products are available! Cancelling billing flow launch.")
return null
}
val subscriptionDetails: ProductDetails = productDetails[0]
val offerToken = subscriptionDetails.subscriptionOfferDetails?.firstOrNull()
if (offerToken == null) {
Log.w(TAG, "No offer tokens available on subscription product! Cancelling billing flow launch.")
return null
}
val productDetailParamsList = listOf(
ProductDetailsParams.newBuilder()
.setProductDetails(subscriptionDetails)
.setOfferToken(offerToken.offerToken)
.build()
)
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailParamsList)
.build()
return doOnConnectionReady {
withContext(Dispatchers.Main) {
billingClient.launchBillingFlow(activity, billingFlowParams)
}
}
}
/**
* Returns whether or not subscriptions are supported by a user's device. Lack of subscription support is generally due
* to out-of-date Google Play API