mirror of
https://github.com/samuelclay/NewsBlur.git
synced 2025-09-18 21:50:56 +00:00
Subscription manager to handle billing and subscription syncs between Play Store and NewsBlur.
This commit is contained in:
parent
7329f54d62
commit
5a17dec799
3 changed files with 304 additions and 188 deletions
|
@ -4,90 +4,43 @@ import android.graphics.Color
|
|||
import android.graphics.Paint
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
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
|
||||
import com.newsblur.network.APIManager
|
||||
import com.newsblur.service.NBSyncService
|
||||
import com.newsblur.subscription.SubscriptionManager
|
||||
import com.newsblur.subscription.SubscriptionManagerImpl
|
||||
import com.newsblur.subscription.SubscriptionsListener
|
||||
import com.newsblur.util.*
|
||||
import nl.dionsegijn.konfetti.emitters.StreamEmitter
|
||||
import nl.dionsegijn.konfetti.models.Shape.Circle
|
||||
import nl.dionsegijn.konfetti.models.Shape.Square
|
||||
import nl.dionsegijn.konfetti.models.Size
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class Premium : NbActivity() {
|
||||
|
||||
private lateinit var binding: ActivityPremiumBinding
|
||||
private lateinit var billingClient: BillingClient
|
||||
private lateinit var subscriptionManager: SubscriptionManager
|
||||
|
||||
private var subscriptionDetails: SkuDetails? = null
|
||||
private var purchasedSubscription: Purchase? = null
|
||||
private val subscriptionManagerListener = object : SubscriptionsListener {
|
||||
|
||||
private val acknowledgePurchaseResponseListener = AcknowledgePurchaseResponseListener { billingResult: BillingResult ->
|
||||
when (billingResult.responseCode) {
|
||||
BillingClient.BillingResponseCode.OK -> {
|
||||
Log.d(this@Premium.localClassName, "acknowledgePurchaseResponseListener OK")
|
||||
verifyUserSubscriptionStatus()
|
||||
}
|
||||
BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> {
|
||||
// Billing API version is not supported for the type requested.
|
||||
Log.d(this@Premium.localClassName, "acknowledgePurchaseResponseListener BILLING_UNAVAILABLE")
|
||||
}
|
||||
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> {
|
||||
// Network connection is down.
|
||||
Log.d(this@Premium.localClassName, "acknowledgePurchaseResponseListener SERVICE_UNAVAILABLE")
|
||||
}
|
||||
else -> {
|
||||
// Handle any other error codes.
|
||||
Log.d(this@Premium.localClassName, "acknowledgePurchaseResponseListener ERROR - message: " + billingResult.debugMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
private val purchaseUpdateListener = PurchasesUpdatedListener { billingResult: BillingResult, purchases: List<Purchase>? ->
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
|
||||
Log.d(this@Premium.localClassName, "purchaseUpdateListener OK")
|
||||
for (purchase in purchases) {
|
||||
handlePurchase(purchase)
|
||||
}
|
||||
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
|
||||
// Handle an error caused by a user cancelling the purchase flow.
|
||||
Log.d(this@Premium.localClassName, "purchaseUpdateListener USER_CANCELLED")
|
||||
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
|
||||
// Billing API version is not supported for the type requested.
|
||||
Log.d(this@Premium.localClassName, "purchaseUpdateListener BILLING_UNAVAILABLE")
|
||||
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE) {
|
||||
// Network connection is down.
|
||||
Log.d(this@Premium.localClassName, "purchaseUpdateListener SERVICE_UNAVAILABLE")
|
||||
} else {
|
||||
// Handle any other error codes.
|
||||
Log.d(this@Premium.localClassName, "purchaseUpdateListener ERROR - message: " + billingResult.debugMessage)
|
||||
}
|
||||
}
|
||||
private val billingClientStateListener: BillingClientStateListener = object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
// The BillingClient is ready. You can query purchases here.
|
||||
Log.d(this@Premium.localClassName, "onBillingSetupFinished OK")
|
||||
retrievePlayStoreSubscriptions()
|
||||
verifyUserSubscriptionStatus()
|
||||
} else {
|
||||
showSubscriptionDetailsError()
|
||||
}
|
||||
override fun onActiveSubscription(renewalMessage: String?) {
|
||||
showActiveSubscriptionDetails(renewalMessage)
|
||||
}
|
||||
|
||||
override fun onBillingServiceDisconnected() {
|
||||
Log.d(this@Premium.localClassName, "onBillingServiceDisconnected")
|
||||
// Try to restart the connection on the next request to
|
||||
// Google Play by calling the startConnection() method.
|
||||
showSubscriptionDetailsError()
|
||||
override fun onAvailableSubscription(skuDetails: SkuDetails) {
|
||||
showAvailableSubscriptionDetails(skuDetails)
|
||||
}
|
||||
|
||||
override fun onBillingConnectionReady() {
|
||||
subscriptionManager.getSubscriptionState()
|
||||
}
|
||||
|
||||
override fun onBillingConnectionError(message: String?) {
|
||||
showSubscriptionDetailsError(message)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -96,7 +49,7 @@ class Premium : NbActivity() {
|
|||
binding = ActivityPremiumBinding.inflate(layoutInflater)
|
||||
setContentView(binding.root)
|
||||
setupUI()
|
||||
setupBillingClient()
|
||||
setupBilling()
|
||||
}
|
||||
|
||||
private fun setupUI() {
|
||||
|
@ -113,129 +66,45 @@ class Premium : NbActivity() {
|
|||
FeedUtils.iconLoader!!.displayImage(AppConstants.SHILOH_PHOTO_URL, binding.imgShiloh)
|
||||
}
|
||||
|
||||
private fun setupBillingClient() {
|
||||
billingClient = BillingClient.newBuilder(this)
|
||||
.setListener(purchaseUpdateListener)
|
||||
.enablePendingPurchases()
|
||||
.build()
|
||||
billingClient.startConnection(billingClientStateListener)
|
||||
private fun setupBilling() {
|
||||
subscriptionManager = SubscriptionManagerImpl(this, subscriptionManagerListener)
|
||||
subscriptionManager.startBillingConnection()
|
||||
}
|
||||
|
||||
private fun verifyUserSubscriptionStatus() {
|
||||
val hasNewsBlurSubscription = PrefsUtils.getIsPremium(this)
|
||||
var playStoreSubscription: Purchase? = null
|
||||
val result = billingClient.queryPurchases(BillingClient.SkuType.SUBS)
|
||||
if (result.purchasesList != null) {
|
||||
for (purchase in result.purchasesList!!) {
|
||||
if (purchase.sku == AppConstants.PREMIUM_SKU) {
|
||||
playStoreSubscription = purchase
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hasNewsBlurSubscription || playStoreSubscription != null) {
|
||||
binding.containerGoingPremium.visibility = View.GONE
|
||||
binding.containerGonePremium.visibility = View.VISIBLE
|
||||
val expirationTimeMs = PrefsUtils.getPremiumExpire(this)
|
||||
var renewalString: String? = null
|
||||
if (expirationTimeMs == 0L) {
|
||||
renewalString = getString(R.string.premium_subscription_no_expiration)
|
||||
} else if (expirationTimeMs > 0) {
|
||||
// date constructor expects ms
|
||||
val expirationDate = Date(expirationTimeMs * 1000)
|
||||
val dateFormat: DateFormat = SimpleDateFormat("EEE, MMMM d, yyyy", Locale.getDefault())
|
||||
dateFormat.timeZone = TimeZone.getDefault()
|
||||
renewalString = getString(R.string.premium_subscription_renewal, dateFormat.format(expirationDate))
|
||||
if (playStoreSubscription != null && !playStoreSubscription.isAutoRenewing) {
|
||||
renewalString = getString(R.string.premium_subscription_expiration, dateFormat.format(expirationDate))
|
||||
}
|
||||
}
|
||||
if (!TextUtils.isEmpty(renewalString)) {
|
||||
binding.textSubscriptionRenewal.text = renewalString
|
||||
binding.textSubscriptionRenewal.visibility = View.VISIBLE
|
||||
}
|
||||
showConfetti()
|
||||
}
|
||||
if (!hasNewsBlurSubscription && playStoreSubscription != null) {
|
||||
purchasedSubscription = playStoreSubscription
|
||||
notifyNewsBlurOfSubscription()
|
||||
}
|
||||
}
|
||||
|
||||
private fun retrievePlayStoreSubscriptions() {
|
||||
val skuList: MutableList<String> = ArrayList(1)
|
||||
// add sub SKUs from Play Store
|
||||
skuList.add(AppConstants.PREMIUM_SKU)
|
||||
val params = SkuDetailsParams.newBuilder()
|
||||
params.setSkusList(skuList).setType(BillingClient.SkuType.SUBS)
|
||||
billingClient.querySkuDetailsAsync(params.build()) { _: BillingResult?, skuDetailsList: List<SkuDetails>? ->
|
||||
Log.d(this@Premium.localClassName, "SkuDetailsResponse")
|
||||
processSkuDetailsList(skuDetailsList)
|
||||
}
|
||||
}
|
||||
|
||||
private fun processSkuDetailsList(skuDetailsList: List<SkuDetails>?) {
|
||||
if (skuDetailsList != null) {
|
||||
for (skuDetails in skuDetailsList) {
|
||||
if (skuDetails.sku == AppConstants.PREMIUM_SKU) {
|
||||
Log.d(this@Premium.localClassName, "Sku detail: " + skuDetails.title + " | " + skuDetails.description + " | " + skuDetails.price + " | " + skuDetails.sku)
|
||||
subscriptionDetails = skuDetails
|
||||
}
|
||||
}
|
||||
}
|
||||
if (subscriptionDetails != null) {
|
||||
showSubscriptionDetails()
|
||||
} else {
|
||||
showSubscriptionDetailsError()
|
||||
}
|
||||
}
|
||||
|
||||
private fun showSubscriptionDetailsError() {
|
||||
binding.textLoading.setText(R.string.premium_subscription_details_error)
|
||||
private fun showSubscriptionDetailsError(message: String?) {
|
||||
binding.textLoading.text = message ?: getString(R.string.premium_subscription_details_error)
|
||||
binding.textLoading.visibility = View.VISIBLE
|
||||
binding.containerSub.visibility = View.GONE
|
||||
}
|
||||
|
||||
private fun showSubscriptionDetails() {
|
||||
val price = (subscriptionDetails!!.priceAmountMicros / 1000f / 1000f).toDouble()
|
||||
val currency = Currency.getInstance(subscriptionDetails!!.priceCurrencyCode)
|
||||
private fun showAvailableSubscriptionDetails(skuDetails: SkuDetails) {
|
||||
val price = (skuDetails.priceAmountMicros / 1000f / 1000f).toDouble()
|
||||
val currency = Currency.getInstance(skuDetails.priceCurrencyCode)
|
||||
val currencySymbol = currency.getSymbol(Locale.getDefault())
|
||||
val pricingText = StringBuilder()
|
||||
pricingText.append(subscriptionDetails!!.price)
|
||||
pricingText.append(skuDetails.price)
|
||||
pricingText.append(" per year (")
|
||||
pricingText.append(currencySymbol)
|
||||
pricingText.append(String.format(Locale.getDefault(), "%.2f", price / 12))
|
||||
pricingText.append("/month)")
|
||||
binding.textSubTitle.text = subscriptionDetails!!.title
|
||||
binding.textSubTitle.text = skuDetails.title
|
||||
binding.textSubPrice.text = pricingText
|
||||
binding.textLoading.visibility = View.GONE
|
||||
binding.containerSub.visibility = View.VISIBLE
|
||||
binding.containerSub.setOnClickListener { launchBillingFlow(subscriptionDetails!!) }
|
||||
}
|
||||
|
||||
private fun launchBillingFlow(skuDetails: SkuDetails) {
|
||||
Log.d(this@Premium.localClassName, "launchBillingFlow for sku: " + skuDetails.sku)
|
||||
val billingFlowParams = BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(skuDetails)
|
||||
.build()
|
||||
billingClient.launchBillingFlow(this, billingFlowParams)
|
||||
}
|
||||
|
||||
private fun handlePurchase(purchase: Purchase) {
|
||||
Log.d(this@Premium.localClassName, "handlePurchase: " + purchase.orderId)
|
||||
purchasedSubscription = purchase
|
||||
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED && purchase.isAcknowledged) {
|
||||
verifyUserSubscriptionStatus()
|
||||
} else if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged) {
|
||||
// need to acknowledge first time sub otherwise it will void
|
||||
Log.d(this@Premium.localClassName, "acknowledge purchase: " + purchase.orderId)
|
||||
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
|
||||
.setPurchaseToken(purchase.purchaseToken)
|
||||
.build()
|
||||
billingClient.acknowledgePurchase(acknowledgePurchaseParams, acknowledgePurchaseResponseListener)
|
||||
binding.containerSub.setOnClickListener {
|
||||
subscriptionManager.purchaseSubscription(this, skuDetails)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showConfetti() {
|
||||
private fun showActiveSubscriptionDetails(renewalMessage: String?) {
|
||||
binding.containerGoingPremium.visibility = View.GONE
|
||||
binding.containerGonePremium.visibility = View.VISIBLE
|
||||
|
||||
if (!renewalMessage.isNullOrEmpty()) {
|
||||
binding.textSubscriptionRenewal.text = renewalMessage
|
||||
binding.textSubscriptionRenewal.visibility = View.VISIBLE
|
||||
}
|
||||
|
||||
binding.konfetti.build()
|
||||
.addColors(Color.YELLOW, Color.GREEN, Color.MAGENTA, Color.BLUE, Color.CYAN, Color.RED)
|
||||
.setDirection(90.0)
|
||||
|
@ -246,22 +115,4 @@ class Premium : NbActivity() {
|
|||
.setPosition(0f, binding.konfetti.width + 0f, -50f, -20f)
|
||||
.streamFor(100, StreamEmitter.INDEFINITE)
|
||||
}
|
||||
|
||||
private fun notifyNewsBlurOfSubscription() {
|
||||
if (purchasedSubscription != null) {
|
||||
val apiManager = APIManager(this)
|
||||
lifecycleScope.executeAsyncTask(
|
||||
doInBackground = {
|
||||
apiManager.saveReceipt(purchasedSubscription!!.orderId, purchasedSubscription!!.sku)
|
||||
},
|
||||
onPostExecute = {
|
||||
if (!it.isError) {
|
||||
NBSyncService.forceFeedsFolders()
|
||||
triggerSync()
|
||||
}
|
||||
finish()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,6 +8,7 @@ import android.os.Parcelable;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
|
|
@ -0,0 +1,264 @@
|
|||
package com.newsblur.subscription
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import com.android.billingclient.api.AcknowledgePurchaseParams
|
||||
import com.android.billingclient.api.AcknowledgePurchaseResponseListener
|
||||
import com.android.billingclient.api.BillingClient
|
||||
import com.android.billingclient.api.BillingClientStateListener
|
||||
import com.android.billingclient.api.BillingFlowParams
|
||||
import com.android.billingclient.api.BillingResult
|
||||
import com.android.billingclient.api.Purchase
|
||||
import com.android.billingclient.api.PurchasesUpdatedListener
|
||||
import com.android.billingclient.api.SkuDetails
|
||||
import com.android.billingclient.api.SkuDetailsParams
|
||||
import com.newsblur.R
|
||||
import com.newsblur.network.APIManager
|
||||
import com.newsblur.service.NBSyncService
|
||||
import com.newsblur.util.AppConstants
|
||||
import com.newsblur.util.FeedUtils
|
||||
import com.newsblur.util.Log
|
||||
import com.newsblur.util.NBScope
|
||||
import com.newsblur.util.PrefsUtils
|
||||
import com.newsblur.util.executeAsyncTask
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
interface SubscriptionManager {
|
||||
|
||||
/**
|
||||
* Open connection to Play Store to retrieve
|
||||
* purchases and subscriptions.
|
||||
*/
|
||||
fun startBillingConnection()
|
||||
|
||||
/**
|
||||
* Generated subscription state by retrieve all available subscriptions
|
||||
* and checking whether the user has an active subscription.
|
||||
*
|
||||
* Subscriptions are configured via the Play Store console.
|
||||
*/
|
||||
fun getSubscriptionState()
|
||||
|
||||
/**
|
||||
* Launch the billing flow overlay for a specific subscription.
|
||||
* @param activity Activity on which the billing overlay will be displayed.
|
||||
* @param skuDetails Subscription details for the intended purchases.
|
||||
*/
|
||||
fun purchaseSubscription(activity: Activity, skuDetails: SkuDetails)
|
||||
|
||||
/**
|
||||
* Sync subscription state between NewsBlur and Play Store.
|
||||
*/
|
||||
fun syncActiveSubscription()
|
||||
|
||||
fun hasActiveSubscription(): Boolean
|
||||
}
|
||||
|
||||
interface SubscriptionsListener {
|
||||
|
||||
fun onActiveSubscription(renewalMessage: String?)
|
||||
|
||||
fun onAvailableSubscription(skuDetails: SkuDetails)
|
||||
|
||||
fun onBillingConnectionReady()
|
||||
|
||||
fun onBillingConnectionError(message: String? = null)
|
||||
}
|
||||
|
||||
class SubscriptionManagerImpl(
|
||||
private val context: Context,
|
||||
private val listener: SubscriptionsListener
|
||||
) : SubscriptionManager {
|
||||
|
||||
private val acknowledgePurchaseListener = AcknowledgePurchaseResponseListener { billingResult: BillingResult ->
|
||||
when (billingResult.responseCode) {
|
||||
BillingClient.BillingResponseCode.OK -> {
|
||||
Log.d(this, "acknowledgePurchaseResponseListener OK")
|
||||
syncActiveSubscription()
|
||||
}
|
||||
BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> {
|
||||
// Billing API version is not supported for the type requested.
|
||||
Log.d(this, "acknowledgePurchaseResponseListener BILLING_UNAVAILABLE")
|
||||
}
|
||||
BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE -> {
|
||||
// Network connection is down.
|
||||
Log.d(this, "acknowledgePurchaseResponseListener SERVICE_UNAVAILABLE")
|
||||
}
|
||||
else -> {
|
||||
// Handle any other error codes.
|
||||
Log.d(this, "acknowledgePurchaseResponseListener ERROR - message: " + billingResult.debugMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Billing client listener triggered after every user purchase intent.
|
||||
*/
|
||||
private val purchaseUpdateListener = PurchasesUpdatedListener { billingResult: BillingResult, purchases: List<Purchase>? ->
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK && purchases != null) {
|
||||
Log.d(this, "purchaseUpdateListener OK")
|
||||
for (purchase in purchases) {
|
||||
handlePurchase(purchase)
|
||||
}
|
||||
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.USER_CANCELED) {
|
||||
// Handle an error caused by a user cancelling the purchase flow.
|
||||
Log.d(this, "purchaseUpdateListener USER_CANCELLED")
|
||||
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE) {
|
||||
// Billing API version is not supported for the type requested.
|
||||
Log.d(this, "purchaseUpdateListener BILLING_UNAVAILABLE")
|
||||
} else if (billingResult.responseCode == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE) {
|
||||
// Network connection is down.
|
||||
Log.d(this, "purchaseUpdateListener SERVICE_UNAVAILABLE")
|
||||
} else {
|
||||
// Handle any other error codes.
|
||||
Log.d(this, "purchaseUpdateListener ERROR - message: " + billingResult.debugMessage)
|
||||
}
|
||||
}
|
||||
|
||||
private val billingClientStateListener: BillingClientStateListener = object : BillingClientStateListener {
|
||||
override fun onBillingSetupFinished(billingResult: BillingResult) {
|
||||
if (billingResult.responseCode == BillingClient.BillingResponseCode.OK) {
|
||||
Log.d(this, "onBillingSetupFinished OK")
|
||||
listener.onBillingConnectionReady()
|
||||
} else {
|
||||
listener.onBillingConnectionError("Error connecting to Play Store.")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBillingServiceDisconnected() {
|
||||
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.")
|
||||
}
|
||||
}
|
||||
|
||||
private val billingClient: BillingClient = BillingClient.newBuilder(context)
|
||||
.setListener(purchaseUpdateListener)
|
||||
.enablePendingPurchases()
|
||||
.build()
|
||||
|
||||
override fun startBillingConnection() {
|
||||
billingClient.startConnection(billingClientStateListener)
|
||||
}
|
||||
|
||||
override fun getSubscriptionState() =
|
||||
if (hasActiveSubscription()) syncActiveSubscription()
|
||||
else syncAvailableSubscription()
|
||||
|
||||
override fun purchaseSubscription(activity: Activity, skuDetails: SkuDetails) {
|
||||
Log.d(this, "launchBillingFlow for sku: ${skuDetails.sku}")
|
||||
val billingFlowParams = BillingFlowParams.newBuilder()
|
||||
.setSkuDetails(skuDetails)
|
||||
.build()
|
||||
billingClient.launchBillingFlow(activity, billingFlowParams)
|
||||
}
|
||||
|
||||
override fun syncActiveSubscription() {
|
||||
val hasNewsBlurSubscription = PrefsUtils.getIsPremium(context)
|
||||
val activePlayStoreSubscription = getActivePlayStoreSubscription()
|
||||
|
||||
if (hasNewsBlurSubscription || activePlayStoreSubscription != null) {
|
||||
val renewalString: String? = getRenewalMessage(activePlayStoreSubscription)
|
||||
listener.onActiveSubscription(renewalString)
|
||||
}
|
||||
|
||||
if (!hasNewsBlurSubscription && activePlayStoreSubscription != null) {
|
||||
saveSubscriptionReceipt(activePlayStoreSubscription)
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasActiveSubscription(): Boolean =
|
||||
PrefsUtils.getIsPremium(context) || getActivePlayStoreSubscription() != null
|
||||
|
||||
private fun getActivePlayStoreSubscription(): Purchase? {
|
||||
val result = billingClient.queryPurchases(BillingClient.SkuType.SUBS)
|
||||
return result.purchasesList?.let {
|
||||
it.find { purchase -> purchase.sku == AppConstants.PREMIUM_SKU }
|
||||
}
|
||||
}
|
||||
|
||||
private fun syncAvailableSubscription() {
|
||||
val params = SkuDetailsParams.newBuilder().apply {
|
||||
// add subscription SKUs from Play Store
|
||||
setSkusList(listOf(AppConstants.PREMIUM_SKU))
|
||||
setType(BillingClient.SkuType.SUBS)
|
||||
}.build()
|
||||
|
||||
billingClient.querySkuDetailsAsync(params) { _: BillingResult?, skuDetailsList: List<SkuDetails>? ->
|
||||
Log.d(this, "SkuDetailsResponse ${skuDetailsList.toString()}")
|
||||
skuDetailsList?.let {
|
||||
// Currently interested only in the premium yearly News Blur subscription.
|
||||
val premiumSubscription = it.find { skuDetails ->
|
||||
skuDetails.sku == AppConstants.PREMIUM_SKU
|
||||
}
|
||||
|
||||
premiumSubscription?.let { skuDetail ->
|
||||
Log.d(this, skuDetail.toString())
|
||||
listener.onAvailableSubscription(skuDetail)
|
||||
} ?: listener.onBillingConnectionError()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun handlePurchase(purchase: Purchase) {
|
||||
Log.d(this, "handlePurchase: ${purchase.orderId}")
|
||||
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED && purchase.isAcknowledged) {
|
||||
syncActiveSubscription()
|
||||
} else if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED && !purchase.isAcknowledged) {
|
||||
// need to acknowledge first time sub otherwise it will void
|
||||
Log.d(this, "acknowledge purchase: ${purchase.orderId}")
|
||||
AcknowledgePurchaseParams.newBuilder()
|
||||
.setPurchaseToken(purchase.purchaseToken)
|
||||
.build()
|
||||
.also {
|
||||
billingClient.acknowledgePurchase(it, acknowledgePurchaseListener)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
private fun getRenewalMessage(purchase: Purchase?): String? {
|
||||
val expirationTimeMs = PrefsUtils.getPremiumExpire(context)
|
||||
return when {
|
||||
// lifetime subscription
|
||||
expirationTimeMs == 0L -> {
|
||||
context.getString(R.string.premium_subscription_no_expiration)
|
||||
}
|
||||
expirationTimeMs > 0 -> {
|
||||
// date constructor expects ms
|
||||
val expirationDate = Date(expirationTimeMs * 1000)
|
||||
val dateFormat: DateFormat = SimpleDateFormat("EEE, MMMM d, yyyy", Locale.getDefault())
|
||||
dateFormat.timeZone = TimeZone.getDefault()
|
||||
if (purchase != null && !purchase.isAutoRenewing) {
|
||||
context.getString(R.string.premium_subscription_expiration, dateFormat.format(expirationDate))
|
||||
} else {
|
||||
context.getString(R.string.premium_subscription_renewal, dateFormat.format(expirationDate))
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue