Upgrade subscriptions to billing 4.0

This commit is contained in:
sictiru 2021-12-28 15:16:47 -08:00 committed by Samuel Clay
parent 2b5509c994
commit e0c6f96008
3 changed files with 88 additions and 52 deletions

View file

@ -32,7 +32,7 @@ dependencies {
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.1.0'
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.android.billingclient:billing:3.0.3'
implementation 'com.android.billingclient:billing:4.0.0'
implementation 'nl.dionsegijn:konfetti:1.2.2'
implementation 'com.google.android.play:core:1.10.2'
implementation "com.google.android.material:material:1.4.0"

View file

@ -7,6 +7,7 @@ import android.os.Bundle
import android.text.util.Linkify
import android.view.View
import android.widget.TextView
import androidx.lifecycle.lifecycleScope
import com.android.billingclient.api.*
import com.newsblur.R
import com.newsblur.databinding.ActivityPremiumBinding
@ -36,7 +37,7 @@ class Premium : NbActivity() {
}
override fun onBillingConnectionReady() {
subscriptionManager.getSubscriptionState()
subscriptionManager.syncSubscriptionState()
}
override fun onBillingConnectionError(message: String?) {
@ -67,8 +68,8 @@ class Premium : NbActivity() {
}
private fun setupBilling() {
subscriptionManager = SubscriptionManagerImpl(this, subscriptionManagerListener)
subscriptionManager.startBillingConnection()
subscriptionManager = SubscriptionManagerImpl(this, lifecycleScope)
subscriptionManager.startBillingConnection(subscriptionManagerListener)
}
private fun showSubscriptionDetailsError(message: String?) {

View file

@ -21,6 +21,12 @@ import com.newsblur.util.Log
import com.newsblur.util.NBScope
import com.newsblur.util.PrefsUtils
import com.newsblur.util.executeAsyncTask
import kotlinx.coroutines.CompletableDeferred
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.*
@ -31,7 +37,7 @@ interface SubscriptionManager {
* Open connection to Play Store to retrieve
* purchases and subscriptions.
*/
fun startBillingConnection()
fun startBillingConnection(listener: SubscriptionsListener? = null)
/**
* Generated subscription state by retrieve all available subscriptions
@ -39,7 +45,7 @@ interface SubscriptionManager {
*
* Subscriptions are configured via the Play Store console.
*/
fun getSubscriptionState()
fun syncSubscriptionState()
/**
* Launch the billing flow overlay for a specific subscription.
@ -53,7 +59,12 @@ interface SubscriptionManager {
*/
fun syncActiveSubscription()
fun hasActiveSubscription(): Boolean
/**
* Notify backend of active Play Store subscription.
*/
fun saveReceipt(purchase: Purchase)
suspend fun hasActiveSubscription(): Boolean
}
interface SubscriptionsListener {
@ -69,9 +80,11 @@ interface SubscriptionsListener {
class SubscriptionManagerImpl(
private val context: Context,
private val listener: SubscriptionsListener
private val scope: CoroutineScope = NBScope,
) : SubscriptionManager {
private var listener: SubscriptionsListener? = null
private val acknowledgePurchaseListener = AcknowledgePurchaseResponseListener { billingResult: BillingResult ->
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
@ -121,9 +134,9 @@ class SubscriptionManagerImpl(
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
Log.d(this, "onBillingSetupFinished OK")
listener.onBillingConnectionReady()
listener?.onBillingConnectionReady()
} else {
listener.onBillingConnectionError("Error connecting to Play Store.")
listener?.onBillingConnectionError("Error connecting to Play Store.")
}
}
@ -131,7 +144,7 @@ class SubscriptionManagerImpl(
Log.d(this, "onBillingServiceDisconnected")
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
listener.onBillingConnectionError("Error connecting to Play Store.")
listener?.onBillingConnectionError("Error connecting to Play Store.")
}
}
@ -140,13 +153,17 @@ class SubscriptionManagerImpl(
.enablePendingPurchases()
.build()
override fun startBillingConnection() {
override fun startBillingConnection(listener: SubscriptionsListener?) {
this.listener = listener
billingClient.startConnection(billingClientStateListener)
}
override fun getSubscriptionState() =
override fun syncSubscriptionState() {
scope.launch {
if (hasActiveSubscription()) syncActiveSubscription()
else syncAvailableSubscription()
}
}
override fun purchaseSubscription(activity: Activity, skuDetails: SkuDetails) {
Log.d(this, "launchBillingFlow for sku: ${skuDetails.sku}")
@ -157,30 +174,56 @@ class SubscriptionManagerImpl(
}
override fun syncActiveSubscription() {
val hasNewsBlurSubscription = PrefsUtils.getIsPremium(context)
val activePlayStoreSubscription = getActivePlayStoreSubscription()
scope.launch(Dispatchers.Default) {
val hasNewsBlurSubscription = PrefsUtils.getIsPremium(context)
val activePlayStoreSubscription = getActiveSubscriptionAsync().await()
if (hasNewsBlurSubscription || activePlayStoreSubscription != null) {
val renewalString: String? = getRenewalMessage(activePlayStoreSubscription)
listener.onActiveSubscription(renewalString)
}
if (hasNewsBlurSubscription || activePlayStoreSubscription != null) {
val renewalString: String? = getRenewalMessage(activePlayStoreSubscription)
withContext(Dispatchers.Main) {
listener?.onActiveSubscription(renewalString)
}
}
if (!hasNewsBlurSubscription && activePlayStoreSubscription != null) {
saveSubscriptionReceipt(activePlayStoreSubscription)
if (!hasNewsBlurSubscription && activePlayStoreSubscription != null) {
saveReceipt(activePlayStoreSubscription)
}
}
}
override fun hasActiveSubscription(): Boolean =
PrefsUtils.getIsPremium(context) || getActivePlayStoreSubscription() != null
override suspend fun hasActiveSubscription(): Boolean =
PrefsUtils.getIsPremium(context) || getActiveSubscriptionAsync().await() != null
private fun getActivePlayStoreSubscription(): Purchase? {
val result = billingClient.queryPurchases(BillingClient.SkuType.SUBS)
return result.purchasesList?.let {
it.find { purchase -> purchase.sku == AppConstants.PREMIUM_SKU }
override fun saveReceipt(purchase: Purchase) {
Log.d(this, "saveReceipt: ${purchase.orderId}")
val apiManager = APIManager(context)
scope.executeAsyncTask(
doInBackground = {
apiManager.saveReceipt(purchase.orderId, purchase.skus.first())
},
onPostExecute = {
if (!it.isError) {
NBSyncService.forceFeedsFolders()
FeedUtils.triggerSync(context)
}
}
)
}
private suspend fun syncAvailableSubscription() {
scope.launch(Dispatchers.Default) {
val skuDetails = getAvailableSubscriptionAsync().await()
withContext(Dispatchers.Main) {
skuDetails?.let {
Log.d(this, it.toString())
listener?.onAvailableSubscription(it)
} ?: listener?.onBillingConnectionError()
}
}
}
private fun syncAvailableSubscription() {
private fun getAvailableSubscriptionAsync(): Deferred<SkuDetails?> {
val deferred = CompletableDeferred<SkuDetails?>()
val params = SkuDetailsParams.newBuilder().apply {
// add subscription SKUs from Play Store
setSkusList(listOf(AppConstants.PREMIUM_SKU))
@ -191,16 +234,26 @@ class SubscriptionManagerImpl(
Log.d(this, "SkuDetailsResponse ${skuDetailsList.toString()}")
skuDetailsList?.let {
// Currently interested only in the premium yearly News Blur subscription.
val premiumSubscription = it.find { skuDetails ->
val skuDetails = it.find { skuDetails ->
skuDetails.sku == AppConstants.PREMIUM_SKU
}
premiumSubscription?.let { skuDetail ->
Log.d(this, skuDetail.toString())
listener.onAvailableSubscription(skuDetail)
} ?: listener.onBillingConnectionError()
}
Log.d(this, skuDetails.toString())
deferred.complete(skuDetails)
} ?: deferred.complete(null)
}
return deferred
}
private fun getActiveSubscriptionAsync(): Deferred<Purchase?> {
val deferred = CompletableDeferred<Purchase?>()
billingClient.queryPurchasesAsync(BillingClient.SkuType.SUBS) { _, purchases ->
val purchase = purchases.find { purchase -> purchase.skus.contains(AppConstants.PREMIUM_SKU) }
deferred.complete(purchase)
}
return deferred
}
private fun handlePurchase(purchase: Purchase) {
@ -219,24 +272,6 @@ class SubscriptionManagerImpl(
}
}
/**
* Notify backend of active Play Store subscription.
*/
private fun saveSubscriptionReceipt(purchase: Purchase) {
val apiManager = APIManager(context)
NBScope.executeAsyncTask(
doInBackground = {
apiManager.saveReceipt(purchase.orderId, purchase.sku)
},
onPostExecute = {
if (!it.isError) {
NBSyncService.forceFeedsFolders()
FeedUtils.triggerSync(context)
}
}
)
}
/**
* Generate subscription renewal message.
*/