Skip to content

Commit

Permalink
Move billing code to shared module.
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-signal authored and mtang-signal committed Aug 22, 2024
1 parent 4447433 commit 244a81e
Show file tree
Hide file tree
Showing 12 changed files with 105 additions and 152 deletions.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -559,6 +559,7 @@ dependencies {
implementation(libs.rxdogtag)

"playImplementation"(project(":billing"))
"nightlyImplementation"(project(":billing"))

"spinnerImplementation"(project(":spinner"))

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -212,7 +212,7 @@ object AppDependencies {
}

@JvmStatic
val billingApi: GooglePlayBillingApi by lazy {
val billingApi: BillingApi by lazy {
provider.provideBillingApi()
}

Expand Down Expand Up @@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,15 @@
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;
Expand Down Expand Up @@ -460,8 +460,8 @@ public WebSocketConnection createUnidentifiedWebSocket() {
}

@Override
public @NonNull GooglePlayBillingApi provideBillingApi() {
return GooglePlayBillingFactory.create(context);
public @NonNull BillingApi provideBillingApi() {
return BillingFactory.create(context, RemoteConfig.messageBackups());
}

@VisibleForTesting
Expand Down

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -199,7 +199,7 @@ class MockApplicationDependencyProvider : AppDependencies.Provider {
return mockk()
}

override fun provideBillingApi(): GooglePlayBillingApi {
override fun provideBillingApi(): BillingApi {
return mockk()
}
}
14 changes: 14 additions & 0 deletions app/src/website/java/org/signal/billing/BillingFactory.kt
Original file line number Diff line number Diff line change
@@ -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
}
}

This file was deleted.

2 changes: 1 addition & 1 deletion billing/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,6 @@ android {
dependencies {
lintChecks(project(":lintchecks"))

api(libs.android.billing)
implementation(libs.android.billing)
implementation(project(":core-util"))
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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

/**
Expand All @@ -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
private val connectionState = MutableStateFlow<State>(State.Init)
private val coroutineScope = CoroutineScope(Dispatchers.Default)

@Synchronized
fun getOrCreate(context: Context, onPurchaseUpdateListener: PurchasesUpdatedListener): BillingApi {
return instance ?: BillingApi(context, onPurchaseUpdateListener).let {
instance = it
it
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 connectionState = MutableStateFlow<State>(State.Init)
private val coroutineScope = CoroutineScope(Dispatchers.Default)

private val billingClient: BillingClient = BillingClient.newBuilder(context)
.setListener(onPurchaseUpdateListener)
.setListener(purchasesUpdatedListener)
.enablePendingPurchases(
PendingPurchasesParams.newBuilder()
.enableOneTimeProducts()
Expand All @@ -88,51 +96,38 @@ class BillingApi private constructor(
}
}

suspend fun queryProducts(): ProductDetailsResult {
val productList = listOf(
QueryProductDetailsParams.Product.newBuilder()
.setProductId("") // TODO [message-backups] where does the product id come from?
.setProductType(ProductType.SUBS)
.build()
)

val params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()

return withContext(Dispatchers.IO) {
doOnConnectionReady {
billingClient.queryProductDetails(params)
}
}
override suspend fun queryProducts() {
val products = queryProductsInternal()
}

suspend fun queryPurchases(): PurchasesResult {
override suspend fun queryPurchases() {
val param = QueryPurchasesParams.newBuilder()
.setProductType(ProductType.SUBS)
.build()

return doOnConnectionReady {
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.
*/
suspend fun launchBillingFlow(activity: Activity): BillingResult? {
val productDetails = queryProducts().productDetailsList
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 null
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 null
return
}

val productDetailParamsList = listOf(
Expand All @@ -146,7 +141,7 @@ class BillingApi private constructor(
.setProductDetailsParamsList(productDetailParamsList)
.build()

return doOnConnectionReady {
doOnConnectionReady {
withContext(Dispatchers.Main) {
billingClient.launchBillingFlow(activity, billingFlowParams)
}
Expand All @@ -157,10 +152,29 @@ class BillingApi private constructor(
* 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 {
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?
.setProductType(ProductType.SUBS)
.build()
)

val params = QueryProductDetailsParams.newBuilder()
.setProductList(productList)
.build()

return withContext(Dispatchers.IO) {
doOnConnectionReady {
billingClient.queryProductDetails(params)
}
}
}

private suspend fun <T> doOnConnectionReady(block: suspend () -> T): T {
val state = connectionState
.filter { it == State.Connected || it is State.Failure }
Expand Down
Loading

0 comments on commit 244a81e

Please sign in to comment.