diff --git a/app/src/main/java/org/futo/circles/subscriptions/SubscriptionManager.kt b/app/src/main/java/org/futo/circles/subscriptions/SubscriptionManager.kt index 307a1b3f8ddc99527ff6357a7058b975b5d2773c..648fb89c546a7bc23f4e3917f45fc7ded101fa07 100644 --- a/app/src/main/java/org/futo/circles/subscriptions/SubscriptionManager.kt +++ b/app/src/main/java/org/futo/circles/subscriptions/SubscriptionManager.kt @@ -1,9 +1,12 @@ package org.futo.circles.subscriptions +import org.futo.circles.extensions.Response +import org.futo.circles.model.SubscriptionListItem + interface SubscriptionManager { - suspend fun getDetails(productIds: List<String>): BillingResult<List<SubscriptionData>> + suspend fun getDetails(productIds: List<String>): Response<List<SubscriptionListItem>> - suspend fun purchaseProduct(productId: String): BillingResult<String> + suspend fun purchaseProduct(productId: String): Response<Unit> } \ No newline at end of file diff --git a/app/src/main/java/org/futo/circles/subscriptions/google/GoogleSubscriptionsManager.kt b/app/src/main/java/org/futo/circles/subscriptions/google/GoogleSubscriptionsManager.kt index d13876c797262f6da529c168aa542accac37523b..f8e8330d3260bf6a6329f4d114c142a87063682c 100644 --- a/app/src/main/java/org/futo/circles/subscriptions/google/GoogleSubscriptionsManager.kt +++ b/app/src/main/java/org/futo/circles/subscriptions/google/GoogleSubscriptionsManager.kt @@ -1,30 +1,33 @@ package org.futo.circles.subscriptions.google +import android.app.Activity import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import com.android.billingclient.api.* import com.android.billingclient.api.BillingClient.BillingResponseCode.* import kotlinx.coroutines.suspendCancellableCoroutine +import org.futo.circles.R +import org.futo.circles.extensions.Response import org.futo.circles.extensions.onBG -import org.futo.circles.subscriptions.BillingResult -import org.futo.circles.subscriptions.SubscriptionData +import org.futo.circles.model.SubscriptionListItem +import org.futo.circles.subscriptions.ItemPurchasedListener import org.futo.circles.subscriptions.SubscriptionManager import kotlin.coroutines.resume class GoogleSubscriptionsManager( - private val activity: AppCompatActivity, - private val itemPurchasedListener: GoogleItemPurchasedListener + private val activity: Activity, + private val itemPurchasedListener: ItemPurchasedListener ) : SubscriptionManager { - private val subscriptionsList = - arrayListOf("subscriptions_names_here!") private val purchasesUpdatedListener = PurchasesUpdatedListener { billingResult, purchases -> purchases?.let { if (billingResult.responseCode == OK) - itemPurchasedListener.onItemPurchased(purchases) + itemPurchasedListener.onItemPurchased( + purchases.lastOrNull()?.originalJson ?: "" + ) else itemPurchasedListener.onPurchaseFailed(billingResult.responseCode) } } @@ -36,7 +39,7 @@ class GoogleSubscriptionsManager( init { - activity.lifecycle.addObserver(object : DefaultLifecycleObserver { + (activity as? AppCompatActivity)?.lifecycle?.addObserver(object : DefaultLifecycleObserver { override fun onDestroy(owner: LifecycleOwner) { client.endConnection() super.onDestroy(owner) @@ -44,70 +47,65 @@ class GoogleSubscriptionsManager( }) } - override suspend fun getDetails() = when (val code = - client.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode) { - - OK -> queryDetails() - FEATURE_NOT_SUPPORTED -> BillingResult.FeatureNotSupported - SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE, BILLING_UNAVAILABLE -> onBG { - tryConnectAndDo { queryDetails() } + override suspend fun getDetails(productIds: List<String>): Response<List<SubscriptionListItem>> = + when (val code = + client.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode) { + OK -> queryDetails(productIds).toSubscriptionListItemsResponse(activity.applicationContext) + SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE, BILLING_UNAVAILABLE -> onBG { + tryConnectAndDo { queryDetails(productIds).toSubscriptionListItemsResponse(activity.applicationContext) } + } + else -> getErrorResponseForCode(code) } - DEVELOPER_ERROR -> BillingResult.Failure(DEVELOPER_ERROR) - else -> BillingResult.Failure(code = code) - } - override suspend fun purchaseProduct(productDetails: ProductDetails): BillingResult<String> = - when (val code = client + override suspend fun purchaseProduct(productId: String): Response<Unit> { + val detailsResponse = queryDetails(listOf(productId)) + val productDetails = + (detailsResponse as? Response.Success)?.data?.firstOrNull { it.productId == productId } + ?: return getErrorResponseForCode(ERROR) + + return when (val code = client .isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS).responseCode) { OK -> purchase(productDetails) - FEATURE_NOT_SUPPORTED -> BillingResult.FeatureNotSupported SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE, BILLING_UNAVAILABLE -> onBG { tryConnectAndDo { purchase(productDetails) } } - ITEM_ALREADY_OWNED -> BillingResult.Success("Already purchased") - ITEM_UNAVAILABLE, USER_CANCELED, ITEM_NOT_OWNED -> BillingResult.Failure( - ITEM_UNAVAILABLE - ) - DEVELOPER_ERROR -> BillingResult.Failure(DEVELOPER_ERROR) - ERROR -> BillingResult.Failure(ERROR) - else -> BillingResult.Failure(code = code) + ITEM_ALREADY_OWNED -> Response.Success(Unit) + else -> getErrorResponseForCode(code) } + } - - private suspend inline fun <T> tryConnectAndDo(action: () -> BillingResult<T>): BillingResult<T> = + private suspend inline fun <T> tryConnectAndDo(action: () -> Response<T>): Response<T> = when (val connectResult = client.tryConnect()) { - is BillingResult.Success -> if (connectResult.data) action() else BillingResult.NotConnected - is BillingResult.Failure -> connectResult - is BillingResult.FeatureNotSupported -> connectResult - is BillingResult.NotConnected -> connectResult + is Response.Success -> if (connectResult.data) action() + else getErrorResponseForCode(SERVICE_DISCONNECTED) + is Response.Error -> connectResult } - private suspend fun BillingClient.tryConnect(): BillingResult<Boolean> = + private suspend fun BillingClient.tryConnect(): Response<Boolean> = suspendCancellableCoroutine { continuation -> startConnection(object : BillingClientStateListener { override fun onBillingSetupFinished(billingResult: com.android.billingclient.api.BillingResult) { if (continuation.isCancelled || continuation.isCompleted) return continuation.resume( when (billingResult.responseCode) { - OK -> BillingResult.Success(true) - SERVICE_DISCONNECTED -> BillingResult.NotConnected - else -> BillingResult.Failure(billingResult.responseCode) + OK -> Response.Success(true) + else -> getErrorResponseForCode(billingResult.responseCode) } ) } override fun onBillingServiceDisconnected() { if (continuation.isCancelled || continuation.isCompleted) return - continuation.resume(BillingResult.NotConnected) + continuation.resume(getErrorResponseForCode(SERVICE_DISCONNECTED)) } }) } - private suspend fun queryDetails(): BillingResult<List<SubscriptionData>> { + private suspend fun queryDetails(productIds: List<String>): Response<List<ProductDetails>> { val params = QueryProductDetailsParams.newBuilder() - .setProductList(subscriptionsList.map { + .setProductList(productIds.map { QueryProductDetailsParams.Product.newBuilder() .setProductId(it) .setProductType(BillingClient.ProductType.SUBS) @@ -117,17 +115,16 @@ class GoogleSubscriptionsManager( val productDetailsResult = onBG { client.queryProductDetails(params.build()) } return if (productDetailsResult.billingResult.responseCode == OK) { - productDetailsResult.productDetailsList?.filter { subscriptionsList.contains(it.productId) } - ?.map { SubscriptionData(it) } + productDetailsResult.productDetailsList?.filter { productIds.contains(it.productId) } ?.takeIf { it.isNotEmpty() } - ?.let { BillingResult.Success(data = it) } - ?: BillingResult.Failure(code = ERROR) + ?.let { Response.Success(data = it) } + ?: getErrorResponseForCode(code = ERROR) } else { - BillingResult.Failure(code = productDetailsResult.billingResult.responseCode) + getErrorResponseForCode(productDetailsResult.billingResult.responseCode) } } - private fun purchase(productDetails: ProductDetails): BillingResult<String> { + private fun purchase(productDetails: ProductDetails): Response<Unit> { val productDetailsParamsList = listOf( BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(productDetails) @@ -140,8 +137,18 @@ class GoogleSubscriptionsManager( return when (val code = client.launchBillingFlow(activity, billingFlowParams).responseCode) { - OK -> BillingResult.Success("Started") - else -> BillingResult.Failure(code = code) + OK -> Response.Success(Unit) + else -> getErrorResponseForCode(code) } } + + private fun getErrorResponseForCode(code: Int) = when (code) { + FEATURE_NOT_SUPPORTED -> Response.Error(activity.getString(R.string.feature_not_supported)) + SERVICE_DISCONNECTED, SERVICE_UNAVAILABLE, BILLING_UNAVAILABLE -> Response.Error(activity.getString(R.string.service_unavailable)) + ITEM_UNAVAILABLE -> Response.Error(activity.getString(R.string.item_unavailable)) + USER_CANCELED -> Response.Error(activity.getString(R.string.user_canceled)) + ITEM_NOT_OWNED -> Response.Error(activity.getString(R.string.item_not_owned)) + DEVELOPER_ERROR -> Response.Error(activity.getString(R.string.developer_error)) + else -> Response.Error(activity.getString(R.string.purchase_failed_format, code)) + } } \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 07c92ff2861a2a54fdb870e235aa7e234a8fc9e3..1b3739751cea610cd50a40a4e2d089a4246ee2bd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -239,6 +239,14 @@ <string name="device_media">Device media</string> <string name="subscriptions">Subscriptions</string> <string name="wrong_signup_config">Wrong signup config!</string> + <string name="item_already_owned">Item already owned</string> + <string name="purchase_failed_format">Purchase failed with code %d</string> + <string name="feature_not_supported">Feature is not supported</string> + <string name="service_unavailable">Service unavailable</string> + <string name="item_unavailable">Item is unavailable</string> + <string name="user_canceled">User canceled</string> + <string name="item_not_owned">Item is not owned</string> + <string name="developer_error">Developer error</string> <string-array name="report_categories"> <item>@string/crude_language</item>