Move billing code to shared module.
This commit is contained in:
parent
4447433ffe
commit
244a81ef24
12 changed files with 136 additions and 183 deletions
|
@ -559,6 +559,7 @@ dependencies {
|
|||
implementation(libs.rxdogtag)
|
||||
|
||||
"playImplementation"(project(":billing"))
|
||||
"nightlyImplementation"(project(":billing"))
|
||||
|
||||
"spinnerImplementation"(project(":spinner"))
|
||||
|
||||
|
|
|
@ -7,12 +7,12 @@ import io.reactivex.rxjava3.core.Observable
|
|||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||
import io.reactivex.rxjava3.subjects.Subject
|
||||
import okhttp3.OkHttpClient
|
||||
import org.signal.core.util.billing.BillingApi
|
||||
import org.signal.core.util.concurrent.DeadlockDetector
|
||||
import org.signal.core.util.resettableLazy
|
||||
import org.signal.libsignal.net.Network
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
|
||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
|
||||
import org.thoughtcrime.securesms.billing.GooglePlayBillingApi
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
|
||||
|
@ -212,7 +212,7 @@ object AppDependencies {
|
|||
}
|
||||
|
||||
@JvmStatic
|
||||
val billingApi: GooglePlayBillingApi by lazy {
|
||||
val billingApi: BillingApi by lazy {
|
||||
provider.provideBillingApi()
|
||||
}
|
||||
|
||||
|
@ -348,6 +348,6 @@ object AppDependencies {
|
|||
fun provideClientZkReceiptOperations(signalServiceConfiguration: SignalServiceConfiguration): ClientZkReceiptOperations
|
||||
fun provideScheduledMessageManager(): ScheduledMessageManager
|
||||
fun provideLibsignalNetwork(config: SignalServiceConfiguration): Network
|
||||
fun provideBillingApi(): GooglePlayBillingApi
|
||||
fun provideBillingApi(): BillingApi
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,15 +8,15 @@ import android.os.HandlerThread;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import org.signal.billing.BillingFactory;
|
||||
import org.signal.core.util.ThreadUtil;
|
||||
import org.signal.core.util.billing.BillingApi;
|
||||
import org.signal.core.util.concurrent.DeadlockDetector;
|
||||
import org.signal.core.util.concurrent.SignalExecutors;
|
||||
import org.signal.libsignal.net.Network;
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
|
||||
import org.thoughtcrime.securesms.BuildConfig;
|
||||
import org.thoughtcrime.securesms.billing.GooglePlayBillingApi;
|
||||
import org.thoughtcrime.securesms.billing.GooglePlayBillingFactory;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository;
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||
|
@ -460,8 +460,8 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
|||
}
|
||||
|
||||
@Override
|
||||
public @NonNull GooglePlayBillingApi provideBillingApi() {
|
||||
return GooglePlayBillingFactory.create(context);
|
||||
public @NonNull BillingApi provideBillingApi() {
|
||||
return BillingFactory.create(context, RemoteConfig.messageBackups());
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package org.thoughtcrime.securesms.billing
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* Website builds do not support google play billing.
|
||||
*/
|
||||
object GooglePlayBillingFactory {
|
||||
@JvmStatic
|
||||
fun create(context: Context): GooglePlayBillingApi {
|
||||
return GooglePlayBillingApi.Empty
|
||||
}
|
||||
}
|
|
@ -1,73 +0,0 @@
|
|||
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
|
||||
|
||||
/**
|
||||
* Play billing factory. Returns empty implementation if message backups are not enabled.
|
||||
*/
|
||||
object GooglePlayBillingFactory {
|
||||
@JvmStatic
|
||||
fun create(context: Context): GooglePlayBillingApi {
|
||||
return if (RemoteConfig.messageBackups) {
|
||||
GooglePlayBillingApiImpl(context)
|
||||
} else {
|
||||
GooglePlayBillingApi.Empty
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play Store implementation
|
||||
*/
|
||||
private class GooglePlayBillingApiImpl(context: Context) : GooglePlayBillingApi {
|
||||
|
||||
private companion object {
|
||||
val TAG = Log.tag(GooglePlayBillingApiImpl::class)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
override suspend fun queryProducts() {
|
||||
val products: ProductDetailsResult = billingApi.queryProducts()
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
|
@ -2,11 +2,11 @@ package org.thoughtcrime.securesms.dependencies
|
|||
|
||||
import io.mockk.mockk
|
||||
import org.mockito.Mockito
|
||||
import org.signal.core.util.billing.BillingApi
|
||||
import org.signal.core.util.concurrent.DeadlockDetector
|
||||
import org.signal.libsignal.net.Network
|
||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
|
||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
|
||||
import org.thoughtcrime.securesms.billing.GooglePlayBillingApi
|
||||
import org.thoughtcrime.securesms.components.TypingStatusRepository
|
||||
import org.thoughtcrime.securesms.components.TypingStatusSender
|
||||
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
|
||||
|
@ -199,7 +199,7 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
|
|||
return mockk()
|
||||
}
|
||||
|
||||
override fun provideBillingApi(): GooglePlayBillingApi {
|
||||
override fun provideBillingApi(): BillingApi {
|
||||
return mockk()
|
||||
}
|
||||
}
|
||||
|
|
14
app/src/website/java/org/signal/billing/BillingFactory.kt
Normal file
14
app/src/website/java/org/signal/billing/BillingFactory.kt
Normal file
|
@ -0,0 +1,14 @@
|
|||
package org.signal.billing
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.billing.BillingApi
|
||||
|
||||
/**
|
||||
* Website builds do not support google play billing.
|
||||
*/
|
||||
object BillingFactory {
|
||||
@JvmStatic
|
||||
fun create(context: Context): BillingApi {
|
||||
return BillingApi.Empty
|
||||
}
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package org.thoughtcrime.securesms.billing
|
||||
|
||||
import android.content.Context
|
||||
|
||||
/**
|
||||
* Website builds do not support google play billing.
|
||||
*/
|
||||
object GooglePlayBillingFactory {
|
||||
@JvmStatic
|
||||
fun create(context: Context): GooglePlayBillingApi {
|
||||
return GooglePlayBillingApi.Empty
|
||||
}
|
||||
}
|
|
@ -9,6 +9,6 @@ android {
|
|||
dependencies {
|
||||
lintChecks(project(":lintchecks"))
|
||||
|
||||
api(libs.android.billing)
|
||||
implementation(libs.android.billing)
|
||||
implementation(project(":core-util"))
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ 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
|
||||
|
@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.retry
|
|||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.signal.core.util.billing.BillingApi
|
||||
import org.signal.core.util.logging.Log
|
||||
|
||||
/**
|
||||
|
@ -45,29 +45,37 @@ 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,
|
||||
onPurchaseUpdateListener: PurchasesUpdatedListener
|
||||
) {
|
||||
internal class BillingApiImpl(
|
||||
context: Context
|
||||
) : BillingApi {
|
||||
|
||||
companion object {
|
||||
private val TAG = Log.tag(BillingApi::class)
|
||||
|
||||
private var instance: BillingApi? = null
|
||||
|
||||
@Synchronized
|
||||
fun getOrCreate(context: Context, onPurchaseUpdateListener: PurchasesUpdatedListener): BillingApi {
|
||||
return instance ?: BillingApi(context, onPurchaseUpdateListener).let {
|
||||
instance = it
|
||||
it
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private val connectionState = MutableStateFlow<State>(State.Init)
|
||||
private val coroutineScope = CoroutineScope(Dispatchers.Default)
|
||||
|
||||
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 billingClient: BillingClient = BillingClient.newBuilder(context)
|
||||
.setListener(onPurchaseUpdateListener)
|
||||
.setListener(purchasesUpdatedListener)
|
||||
.enablePendingPurchases(
|
||||
PendingPurchasesParams.newBuilder()
|
||||
.enableOneTimeProducts()
|
||||
|
@ -88,7 +96,67 @@ class BillingApi private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
suspend fun queryProducts(): ProductDetailsResult {
|
||||
override suspend fun queryProducts() {
|
||||
val products = queryProductsInternal()
|
||||
}
|
||||
|
||||
override suspend fun queryPurchases() {
|
||||
val param = QueryPurchasesParams.newBuilder()
|
||||
.setProductType(ProductType.SUBS)
|
||||
.build()
|
||||
|
||||
val purchases = doOnConnectionReady {
|
||||
billingClient.queryPurchasesAsync(param)
|
||||
}
|
||||
|
||||
purchasesUpdatedListener.onPurchasesUpdated(purchases.billingResult, purchases.purchasesList)
|
||||
}
|
||||
|
||||
/**
|
||||
* Launches the Google Play billing flow.
|
||||
* Returns a billing result if we launched the flow, null otherwise.
|
||||
*/
|
||||
override suspend fun launchBillingFlow(activity: Activity) {
|
||||
val productDetails = queryProductsInternal().productDetailsList
|
||||
if (productDetails.isNullOrEmpty()) {
|
||||
Log.w(TAG, "No products are available! Cancelling billing flow launch.")
|
||||
return
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
val productDetailParamsList = listOf(
|
||||
ProductDetailsParams.newBuilder()
|
||||
.setProductDetails(subscriptionDetails)
|
||||
.setOfferToken(offerToken.offerToken)
|
||||
.build()
|
||||
)
|
||||
|
||||
val billingFlowParams = BillingFlowParams.newBuilder()
|
||||
.setProductDetailsParamsList(productDetailParamsList)
|
||||
.build()
|
||||
|
||||
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
|
||||
*/
|
||||
override fun isApiAvailable(): Boolean {
|
||||
return billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode == BillingResponseCode.OK
|
||||
}
|
||||
|
||||
private suspend fun queryProductsInternal(): ProductDetailsResult {
|
||||
val productList = listOf(
|
||||
QueryProductDetailsParams.Product.newBuilder()
|
||||
.setProductId("") // TODO [message-backups] where does the product id come from?
|
||||
|
@ -107,60 +175,6 @@ 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
|
||||
*/
|
||||
fun areSubscriptionsSupported(): Boolean {
|
||||
return billingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode == BillingResponseCode.OK
|
||||
}
|
||||
|
||||
private suspend fun <T> doOnConnectionReady(block: suspend () -> T): T {
|
||||
val state = connectionState
|
||||
.filter { it == State.Connected || it is State.Failure }
|
23
billing/src/main/java/org/signal/billing/BillingFactory.kt
Normal file
23
billing/src/main/java/org/signal/billing/BillingFactory.kt
Normal file
|
@ -0,0 +1,23 @@
|
|||
/*
|
||||
* Copyright 2024 Signal Messenger, LLC
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.signal.billing
|
||||
|
||||
import android.content.Context
|
||||
import org.signal.core.util.billing.BillingApi
|
||||
|
||||
/**
|
||||
* Play billing factory. Returns empty implementation if message backups are not enabled.
|
||||
*/
|
||||
object BillingFactory {
|
||||
@JvmStatic
|
||||
fun create(context: Context, isBackupsAvailable: Boolean): BillingApi {
|
||||
return if (isBackupsAvailable) {
|
||||
BillingApiImpl(context)
|
||||
} else {
|
||||
BillingApi.Empty
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,14 +3,14 @@
|
|||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
|
||||
package org.thoughtcrime.securesms.billing
|
||||
package org.signal.core.util.billing
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
/**
|
||||
* Variant interface for the BillingApi.
|
||||
*/
|
||||
interface GooglePlayBillingApi {
|
||||
interface BillingApi {
|
||||
fun isApiAvailable(): Boolean = false
|
||||
suspend fun queryProducts() = Unit
|
||||
|
||||
|
@ -26,5 +26,5 @@ interface GooglePlayBillingApi {
|
|||
* Empty implementation, to be used when play services are available but
|
||||
* GooglePlayBillingApi is not available.
|
||||
*/
|
||||
object Empty : GooglePlayBillingApi
|
||||
object Empty : BillingApi
|
||||
}
|
Loading…
Add table
Reference in a new issue