Skip to content

Commit

Permalink
Fix billing library
Browse files Browse the repository at this point in the history
 - Also start using Google-IAP, a simplified wrapper around IAPs
  • Loading branch information
mattttvaughn committed Mar 27, 2022
1 parent 1047016 commit 95ed18c
Show file tree
Hide file tree
Showing 11 changed files with 39 additions and 155 deletions.
8 changes: 3 additions & 5 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ apply plugin: "kotlin-parcelize"
apply plugin: "com.google.android.gms.oss-licenses-plugin"

android {
compileSdkVersion 30
buildToolsVersion "29.0.3"
compileSdkVersion 31
defaultConfig {
applicationId "io.github.mattpvaughn.chronicle"
minSdkVersion 21
targetSdkVersion 30
targetSdkVersion 31
versionCode 23
versionName "0.52.0"
testInstrumentationRunner "io.github.mattpvaughn.chronicle.application.ChronicleTestRunner"
Expand Down Expand Up @@ -92,8 +91,7 @@ dependencies {
implementation "androidx.lifecycle:lifecycle-process:$lifecycleVersion"

// Google Play Billing
implementation "com.android.billingclient:billing:$billingVersion"
implementation "com.android.billingclient:billing-ktx:$billingVersion"
implementation "com.github.akshaaatt:Google-IAP:$iapWrapperVersion"

// Retrofit
implementation "com.squareup.retrofit2:retrofit:$retrofitVersion"
Expand Down
6 changes: 4 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,8 @@
tools:ignore="GoogleAppIndexingWarning">

<activity
android:exported="true"
android:name="io.github.mattpvaughn.chronicle.application.MainActivity"
android:label="@string/app_name"
android:launchMode="singleInstance">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand Down Expand Up @@ -52,7 +52,9 @@
android:foregroundServiceType="dataSync"
tools:node="merge" />

<receiver android:name="androidx.media.session.MediaButtonReceiver">
<receiver
android:exported="true"
android:name="androidx.media.session.MediaButtonReceiver">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
</intent-filter>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,6 @@ import android.net.Network
import android.os.Build
import android.os.StrictMode
import android.os.StrictMode.VmPolicy
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClient.BillingResponseCode.OK
import com.android.billingclient.api.BillingClientStateListener
import com.android.billingclient.api.BillingResult
import com.bumptech.glide.Glide
import com.facebook.drawee.backends.pipeline.Fresco
import com.facebook.imagepipeline.core.ImagePipelineConfig
Expand Down Expand Up @@ -62,9 +58,6 @@ open class ChronicleApplication : Application() {
@Inject
lateinit var billingManager: ChronicleBillingManager

@Inject
lateinit var billingClient: BillingClient

@Inject
lateinit var unhandledExceptionHandler: CoroutineExceptionHandler

Expand Down Expand Up @@ -105,7 +98,6 @@ open class ChronicleApplication : Application() {

appComponent.inject(this)
setupNetwork(plexPrefs)
setupBilling()
updateDownloadedFileState()
super.onCreate()
Fresco.initialize(this, frescoConfig)
Expand All @@ -128,30 +120,6 @@ open class ChronicleApplication : Application() {
}
}

private var billingSetupAttempts = 0

private fun setupBilling() {
billingClient.startConnection(object : BillingClientStateListener {
override fun onBillingSetupFinished(billingResult: BillingResult) {
if (billingResult.responseCode == OK) {
Timber.i("Billing client setup successful: $billingClient")
billingManager.billingClient = billingClient
} else {
Timber.w("Billing client setup failed! ${billingResult.debugMessage}")
}
}

override fun onBillingServiceDisconnected() {
// Try to restart the connection on the next request to
// Google Play by calling the startConnection() method.
billingSetupAttempts++
if (billingSetupAttempts < 3) {
billingClient.startConnection(this)
}
}
})
}

open fun initializeComponent(): AppComponent {
// We pass the applicationContext that will be used as Context in the graph
return DaggerAppComponent.builder().appModule(AppModule(this)).build()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
package io.github.mattpvaughn.chronicle.application

import android.app.Activity
import com.android.billingclient.api.*
import com.android.billingclient.api.BillingClient.BillingResponseCode.OK
import com.android.billingclient.api.BillingClient.SkuType.INAPP
import com.android.billingclient.api.Purchase.PurchaseState.PURCHASED
import android.content.Context
import com.aemerse.iap.BuildConfig
import com.aemerse.iap.DataWrappers
import com.aemerse.iap.IapConnector
import com.aemerse.iap.PurchaseServiceListener
import io.github.mattpvaughn.chronicle.data.local.PrefsRepo
import io.github.mattpvaughn.chronicle.data.local.PrefsRepo.Companion.NO_PREMIUM_TOKEN
import kotlinx.coroutines.*
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton

Expand All @@ -20,107 +18,32 @@ import javax.inject.Singleton
*/
@Singleton
class ChronicleBillingManager @Inject constructor(
applicationContext: Context,
private val prefsRepo: PrefsRepo,
private val unhandledExceptionHandler: CoroutineExceptionHandler
) : PurchasesUpdatedListener {
) {

private lateinit var premiumUpgradeSku: SkuDetails
fun launchBillingFlow(activity: Activity) {
iapConnector.purchase(activity, PREMIUM_IAP_SKU)
}

var billingClient: BillingClient? = null
set(value) {
field = value
Timber.i("Billing client set: $billingClient")
GlobalScope.launch(unhandledExceptionHandler) {
querySkuDetails(requireNotNull(billingClient))
getPurchaseHistory()
private val iapConnector = IapConnector(
context = applicationContext,
nonConsumableKeys = listOf(PREMIUM_IAP_SKU),
enableLogging = BuildConfig.DEBUG
).apply {
addPurchaseListener(object : PurchaseServiceListener {
override fun onPricesUpdated(iapKeyPrices: Map<String, DataWrappers.SkuDetails>) {
// no-op
}
}

private fun getPurchaseHistory() {
if (billingClient == null) {
return
}
val purchase = requireNotNull(billingClient).queryPurchases(INAPP)
if (purchase.billingResult.responseCode == OK) {
if (purchase.purchasesList.isNullOrEmpty()) {
Timber.i("Retrieved purchase list but it was empty or null: ${purchase.purchasesList}")
return
}
val premiumSku =
purchase.purchasesList!!.find { record -> PREMIUM_IAP_SKU in record.skus }
if (premiumSku != null && premiumSku.purchaseState == PURCHASED) {
Timber.i("Found premium SKU in user's history: $premiumSku")
prefsRepo.premiumPurchaseToken = premiumSku.purchaseToken
} else {
prefsRepo.premiumPurchaseToken = NO_PREMIUM_TOKEN
override fun onProductPurchased(purchaseInfo: DataWrappers.PurchaseInfo) {
prefsRepo.premiumPurchaseToken = purchaseInfo.purchaseToken
}
} else {
Timber.i("getPurchaseHistory() failed: %s", purchase.billingResult.debugMessage)
}
}

private suspend fun querySkuDetails(billingClient: BillingClient) {
Timber.i("Querying sku details")
val params = SkuDetailsParams.newBuilder()
.setSkusList(IAP_SKU_LIST)
.setType(INAPP)
.build()

val skuDetailsResult = withContext(Dispatchers.IO) {
billingClient.querySkuDetails(params)
}

if (skuDetailsResult.billingResult.responseCode == OK) {
Timber.i("SKUs available: ${skuDetailsResult.skuDetailsList}")
val skuDetailsList = skuDetailsResult.skuDetailsList ?: emptyList()
for (skuDetails in skuDetailsList) {
val sku = skuDetails.sku
Timber.i("$PREMIUM_IAP_SKU vs. $sku")
if (sku == PREMIUM_IAP_SKU) {
premiumUpgradeSku = skuDetails
}
override fun onProductRestored(purchaseInfo: DataWrappers.PurchaseInfo) {
prefsRepo.premiumPurchaseToken = purchaseInfo.purchaseToken
}
} else {
Timber.i(
"Failed to load SKU details: ${skuDetailsResult.billingResult.debugMessage}"
)
}
})
}

fun launchBillingFlow(activity: Activity): BillingResult {
Timber.i("Premium upgrade sku initialized? ${::premiumUpgradeSku.isInitialized}")
return if (::premiumUpgradeSku.isInitialized) {
val flowParams = BillingFlowParams.newBuilder()
.setSkuDetails(premiumUpgradeSku)
.build()
billingClient?.launchBillingFlow(activity, flowParams) ?: BillingResult()
} else {
BillingResult()
}
}

override fun onPurchasesUpdated(
billingResult: BillingResult,
nullablePurchases: MutableList<Purchase>?
) {
Timber.i("Checking for purchases")
val purchases = nullablePurchases?.toList() ?: emptyList()
if (billingResult.responseCode == OK && purchases.isNotEmpty()) {
Timber.i("IAP Purchased!")
val purchase = purchases[0]
prefsRepo.premiumPurchaseToken = purchase.purchaseToken
if (!purchase.isAcknowledged) {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
GlobalScope.launch(unhandledExceptionHandler) {
billingClient?.acknowledgePurchase(acknowledgePurchaseParams.build())
}
}
// else {
// TODO: retry?
// }
} else {
Timber.e("Purchase failed (${billingResult.responseCode})")
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,7 @@ class MainActivityViewModel(
COLLAPSED -> _currentlyPlayingLayoutState.postValue(EXPANDED)
EXPANDED -> _currentlyPlayingLayoutState.postValue(COLLAPSED)
HIDDEN -> throw IllegalStateException("Cannot click on hidden sheet!")
else -> {}
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package io.github.mattpvaughn.chronicle.data.local

import android.content.SharedPreferences
import com.android.billingclient.api.Purchase
import io.github.mattpvaughn.chronicle.BuildConfig
import io.github.mattpvaughn.chronicle.application.Injector
import io.github.mattpvaughn.chronicle.data.local.PrefsRepo.Companion.KEY_ALLOW_AUTO
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ fun bindLoadingStatus(
LoadingStatus.ERROR -> recyclerView.visibility = View.GONE
LoadingStatus.DONE -> recyclerView.visibility = View.VISIBLE
LoadingStatus.LOADING -> recyclerView.visibility = View.GONE
else -> {}
}
}

Expand All @@ -30,6 +31,7 @@ fun bindLoadingStatus(errorView: TextView, loadingStatus: LoadingStatus?) {
LoadingStatus.ERROR -> errorView.visibility = View.VISIBLE
LoadingStatus.DONE -> errorView.visibility = View.GONE
LoadingStatus.LOADING -> errorView.visibility = View.GONE
else -> {}
}
}

Expand All @@ -42,6 +44,7 @@ fun bindLoadingStatus(
LoadingStatus.ERROR -> progressBar.visibility = View.GONE
LoadingStatus.DONE -> progressBar.visibility = View.GONE
LoadingStatus.LOADING -> progressBar.visibility = View.VISIBLE
else -> {}
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,6 @@ class SettingsFragment : Fragment() {
})

viewModel.upgradeToPremium.observeEvent(viewLifecycleOwner) {
Timber.i(chronicleBillingManager.billingClient.toString())
chronicleBillingManager.launchBillingFlow(requireActivity())
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import android.net.Uri
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import androidx.work.WorkManager
import com.android.billingclient.api.BillingClient
import com.facebook.imagepipeline.backends.okhttp3.BuildConfig
import com.facebook.imagepipeline.backends.okhttp3.OkHttpImagePipelineConfigFactory
import com.facebook.imagepipeline.cache.DefaultCacheKeyFactory
Expand All @@ -19,7 +18,6 @@ import com.tonyodev.fetch2.Fetch
import com.tonyodev.fetch2.FetchConfiguration
import dagger.Module
import dagger.Provides
import io.github.mattpvaughn.chronicle.application.ChronicleBillingManager
import io.github.mattpvaughn.chronicle.application.LOG_NETWORK_REQUESTS
import io.github.mattpvaughn.chronicle.data.local.*
import io.github.mattpvaughn.chronicle.data.sources.plex.*
Expand Down Expand Up @@ -187,14 +185,6 @@ class AppModule(private val app: Application) {
fun plexLoginService(@Named(OKHTTP_CLIENT_LOGIN) loginRetrofit: Retrofit): PlexLoginService =
loginRetrofit.create(PlexLoginService::class.java)

@Provides
@Singleton
fun billingClient(billingManager: ChronicleBillingManager): BillingClient {
return BillingClient.newBuilder(app.applicationContext)
.enablePendingPurchases()
.setListener(billingManager).build()
}

@Provides
@Singleton
fun exceptionHandler(): CoroutineExceptionHandler = CoroutineExceptionHandler { _, e ->
Expand Down
7 changes: 4 additions & 3 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

buildscript {
ext {
kotlinVersion = '1.4.32'
kotlinVersion = '1.6.0'
coroutinesVersion = "1.3.8"
archLifecycleVersion = "1.1.1"
gradleVersion = '7.0.2'
supportlibVersion = '1.2.0'
materialLibVersion = '1.3.0'
retrofitVersion = "2.9.0"
moshiKotlinVersion = '1.11.0'
moshiKotlinVersion = '1.13.0'
okhttpVersion = '4.9.1'
dataBindingCompilerVersion = gradleVersion // Always need to be the same.
roomVersion = "2.2.6"
roomVersion = "2.4.2"
frescoVersion = "2.4.0"
exoplayerVersion = "2.11.4"
workVersion = '2.5.0'
Expand All @@ -33,6 +33,7 @@ buildscript {
androidXCustomTabsVersion = "1.3.0"
swipeRefreshLayoutVersion = "1.1.0"
billingVersion = '4.0.0'
iapWrapperVersion = '1.2.2'
constraintLayoutVersion = '2.0.4'
kotlinResultVersion = '1.1.11'
timberVersion = "4.7.1"
Expand Down
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#Fri Jan 22 08:44:49 PST 2021
#Sun Mar 27 10:12:22 EDT 2022
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
Expand Down

0 comments on commit 95ed18c

Please sign in to comment.