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)
|
implementation(libs.rxdogtag)
|
||||||
|
|
||||||
"playImplementation"(project(":billing"))
|
"playImplementation"(project(":billing"))
|
||||||
|
"nightlyImplementation"(project(":billing"))
|
||||||
|
|
||||||
"spinnerImplementation"(project(":spinner"))
|
"spinnerImplementation"(project(":spinner"))
|
||||||
|
|
||||||
|
|
|
@ -7,12 +7,12 @@ import io.reactivex.rxjava3.core.Observable
|
||||||
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
import io.reactivex.rxjava3.subjects.BehaviorSubject
|
||||||
import io.reactivex.rxjava3.subjects.Subject
|
import io.reactivex.rxjava3.subjects.Subject
|
||||||
import okhttp3.OkHttpClient
|
import okhttp3.OkHttpClient
|
||||||
|
import org.signal.core.util.billing.BillingApi
|
||||||
import org.signal.core.util.concurrent.DeadlockDetector
|
import org.signal.core.util.concurrent.DeadlockDetector
|
||||||
import org.signal.core.util.resettableLazy
|
import org.signal.core.util.resettableLazy
|
||||||
import org.signal.libsignal.net.Network
|
import org.signal.libsignal.net.Network
|
||||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
|
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
|
||||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
|
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
|
||||||
import org.thoughtcrime.securesms.billing.GooglePlayBillingApi
|
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusRepository
|
import org.thoughtcrime.securesms.components.TypingStatusRepository
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender
|
import org.thoughtcrime.securesms.components.TypingStatusSender
|
||||||
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
|
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
|
||||||
|
@ -212,7 +212,7 @@ object AppDependencies {
|
||||||
}
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
val billingApi: GooglePlayBillingApi by lazy {
|
val billingApi: BillingApi by lazy {
|
||||||
provider.provideBillingApi()
|
provider.provideBillingApi()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,6 +348,6 @@ object AppDependencies {
|
||||||
fun provideClientZkReceiptOperations(signalServiceConfiguration: SignalServiceConfiguration): ClientZkReceiptOperations
|
fun provideClientZkReceiptOperations(signalServiceConfiguration: SignalServiceConfiguration): ClientZkReceiptOperations
|
||||||
fun provideScheduledMessageManager(): ScheduledMessageManager
|
fun provideScheduledMessageManager(): ScheduledMessageManager
|
||||||
fun provideLibsignalNetwork(config: SignalServiceConfiguration): Network
|
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.NonNull;
|
||||||
import androidx.annotation.VisibleForTesting;
|
import androidx.annotation.VisibleForTesting;
|
||||||
|
|
||||||
|
import org.signal.billing.BillingFactory;
|
||||||
import org.signal.core.util.ThreadUtil;
|
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.DeadlockDetector;
|
||||||
import org.signal.core.util.concurrent.SignalExecutors;
|
import org.signal.core.util.concurrent.SignalExecutors;
|
||||||
import org.signal.libsignal.net.Network;
|
import org.signal.libsignal.net.Network;
|
||||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations;
|
||||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
|
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations;
|
||||||
import org.thoughtcrime.securesms.BuildConfig;
|
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.TypingStatusRepository;
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
import org.thoughtcrime.securesms.components.TypingStatusSender;
|
||||||
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
import org.thoughtcrime.securesms.crypto.ReentrantSessionLock;
|
||||||
|
@ -460,8 +460,8 @@ public class ApplicationDependencyProvider implements AppDependencies.Provider {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public @NonNull GooglePlayBillingApi provideBillingApi() {
|
public @NonNull BillingApi provideBillingApi() {
|
||||||
return GooglePlayBillingFactory.create(context);
|
return BillingFactory.create(context, RemoteConfig.messageBackups());
|
||||||
}
|
}
|
||||||
|
|
||||||
@VisibleForTesting
|
@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 io.mockk.mockk
|
||||||
import org.mockito.Mockito
|
import org.mockito.Mockito
|
||||||
|
import org.signal.core.util.billing.BillingApi
|
||||||
import org.signal.core.util.concurrent.DeadlockDetector
|
import org.signal.core.util.concurrent.DeadlockDetector
|
||||||
import org.signal.libsignal.net.Network
|
import org.signal.libsignal.net.Network
|
||||||
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
|
import org.signal.libsignal.zkgroup.profiles.ClientZkProfileOperations
|
||||||
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
|
import org.signal.libsignal.zkgroup.receipts.ClientZkReceiptOperations
|
||||||
import org.thoughtcrime.securesms.billing.GooglePlayBillingApi
|
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusRepository
|
import org.thoughtcrime.securesms.components.TypingStatusRepository
|
||||||
import org.thoughtcrime.securesms.components.TypingStatusSender
|
import org.thoughtcrime.securesms.components.TypingStatusSender
|
||||||
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
|
import org.thoughtcrime.securesms.crypto.storage.SignalServiceDataStoreImpl
|
||||||
|
@ -199,7 +199,7 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
|
||||||
return mockk()
|
return mockk()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun provideBillingApi(): GooglePlayBillingApi {
|
override fun provideBillingApi(): BillingApi {
|
||||||
return mockk()
|
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 {
|
dependencies {
|
||||||
lintChecks(project(":lintchecks"))
|
lintChecks(project(":lintchecks"))
|
||||||
|
|
||||||
api(libs.android.billing)
|
implementation(libs.android.billing)
|
||||||
implementation(project(":core-util"))
|
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.PendingPurchasesParams
|
||||||
import com.android.billingclient.api.ProductDetails
|
import com.android.billingclient.api.ProductDetails
|
||||||
import com.android.billingclient.api.ProductDetailsResult
|
import com.android.billingclient.api.ProductDetailsResult
|
||||||
import com.android.billingclient.api.PurchasesResult
|
|
||||||
import com.android.billingclient.api.PurchasesUpdatedListener
|
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||||
import com.android.billingclient.api.QueryProductDetailsParams
|
import com.android.billingclient.api.QueryProductDetailsParams
|
||||||
import com.android.billingclient.api.QueryPurchasesParams
|
import com.android.billingclient.api.QueryPurchasesParams
|
||||||
|
@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.retry
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import org.signal.core.util.billing.BillingApi
|
||||||
import org.signal.core.util.logging.Log
|
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.
|
* Care should be taken here to ensure only one instance of this exists at a time.
|
||||||
*/
|
*/
|
||||||
class BillingApi private constructor(
|
internal class BillingApiImpl(
|
||||||
context: Context,
|
context: Context
|
||||||
onPurchaseUpdateListener: PurchasesUpdatedListener
|
) : BillingApi {
|
||||||
) {
|
|
||||||
companion object {
|
companion object {
|
||||||
private val TAG = Log.tag(BillingApi::class)
|
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 connectionState = MutableStateFlow<State>(State.Init)
|
||||||
private val coroutineScope = CoroutineScope(Dispatchers.Default)
|
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)
|
private val billingClient: BillingClient = BillingClient.newBuilder(context)
|
||||||
.setListener(onPurchaseUpdateListener)
|
.setListener(purchasesUpdatedListener)
|
||||||
.enablePendingPurchases(
|
.enablePendingPurchases(
|
||||||
PendingPurchasesParams.newBuilder()
|
PendingPurchasesParams.newBuilder()
|
||||||
.enableOneTimeProducts()
|
.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(
|
val productList = listOf(
|
||||||
QueryProductDetailsParams.Product.newBuilder()
|
QueryProductDetailsParams.Product.newBuilder()
|
||||||
.setProductId("") // TODO [message-backups] where does the product id come from?
|
.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 {
|
private suspend fun <T> doOnConnectionReady(block: suspend () -> T): T {
|
||||||
val state = connectionState
|
val state = connectionState
|
||||||
.filter { it == State.Connected || it is State.Failure }
|
.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
|
* SPDX-License-Identifier: AGPL-3.0-only
|
||||||
*/
|
*/
|
||||||
|
|
||||||
package org.thoughtcrime.securesms.billing
|
package org.signal.core.util.billing
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Variant interface for the BillingApi.
|
* Variant interface for the BillingApi.
|
||||||
*/
|
*/
|
||||||
interface GooglePlayBillingApi {
|
interface BillingApi {
|
||||||
fun isApiAvailable(): Boolean = false
|
fun isApiAvailable(): Boolean = false
|
||||||
suspend fun queryProducts() = Unit
|
suspend fun queryProducts() = Unit
|
||||||
|
|
||||||
|
@ -26,5 +26,5 @@ interface GooglePlayBillingApi {
|
||||||
* Empty implementation, to be used when play services are available but
|
* Empty implementation, to be used when play services are available but
|
||||||
* GooglePlayBillingApi is not available.
|
* GooglePlayBillingApi is not available.
|
||||||
*/
|
*/
|
||||||
object Empty : GooglePlayBillingApi
|
object Empty : BillingApi
|
||||||
}
|
}
|
Loading…
Add table
Reference in a new issue