Skip to content

Instantly share code, notes, and snippets.

@risalfajar
Created August 31, 2020 12:55
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save risalfajar/8609a2b027b09e628a31f42e877ea88e to your computer and use it in GitHub Desktop.
Save risalfajar/8609a2b027b09e628a31f42e877ea88e to your computer and use it in GitHub Desktop.
Billing Repository Full
class BillingRepository private constructor(private val application: Application) :
PurchasesUpdatedListener, BillingClientStateListener {
private lateinit var playStoreBillingClient: BillingClient
val coinsSkuDetailsListLiveData: MutableLiveData<List<SkuDetails>> = MutableLiveData()
val subsSkuDetailsListLiveData: MutableLiveData<List<SkuDetails>> = MutableLiveData()
companion object {
@Volatile
private var INSTANCE: BillingRepository? = null
fun getInstance(application: Application): BillingRepository =
INSTANCE ?: synchronized(this) {
INSTANCE ?: BillingRepository(application).also {
INSTANCE = it
}
}
}
fun startDataSourceConnections() {
Timber.d("startDataSourceConnections")
instantiateAndConnectToPlayBillingService()
}
fun endDataSourceConnections() {
playStoreBillingClient.endConnection()
}
private fun instantiateAndConnectToPlayBillingService() {
playStoreBillingClient = BillingClient.newBuilder(application.applicationContext)
.setListener(this)
.enablePendingPurchases()
.build()
connectToPlayBillingService()
}
private fun connectToPlayBillingService() {
Timber.d("connectToPlayBillingService")
if (!playStoreBillingClient.isReady) {
playStoreBillingClient.startConnection(this)
}
}
override fun onBillingSetupFinished(billingResult: BillingResult) {
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
Timber.d("onBillingSetupFinished successfully")
querySkuDetailsAsync(BillingClient.SkuType.INAPP, SkuKeys.INAPP_SKUS)
querySkuDetailsAsync(BillingClient.SkuType.SUBS, SkuKeys.SUBS_SKUS)
queryPurchasesAsync()
}
BillingClient.BillingResponseCode.BILLING_UNAVAILABLE -> Timber.d(billingResult.debugMessage)
else -> Timber.d(billingResult.debugMessage)
}
}
override fun onBillingServiceDisconnected() {
connectToPlayBillingService()
}
private fun queryPurchasesAsync() {
Timber.d("queryPurchasesAsync called")
val purchasesResult = HashSet<Purchase>()
var result = playStoreBillingClient.queryPurchases(BillingClient.SkuType.INAPP)
Timber.d("queryPurchasesAsync INAPP results: ${result?.purchasesList?.size}")
result?.purchasesList?.apply { purchasesResult.addAll(this) }
if (isSubScriptionSupported()) {
result = playStoreBillingClient.queryPurchases(BillingClient.SkuType.SUBS)
result?.purchasesList?.apply { purchasesResult.addAll(this) }
Timber.d("queryPurchasesAsync SUBS results: ${result?.purchasesList?.size}")
}
processPurchases(purchasesResult)
}
private fun processPurchases(purchasesResult: Set<Purchase>) {
Timber.d("processPurchases called")
val validPurchases = HashSet<Purchase>(purchasesResult.size)
Timber.d("processPurchases newBatch content $purchasesResult")
purchasesResult.forEach { purchase ->
if (purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
// TODO: You should verify valid signature before marking the purchase as valid
validPurchases.add(purchase)
} else if (purchase.purchaseState == Purchase.PurchaseState.PENDING) {
// TODO: Handle pending purchase
}
}
val (consumables, subscriptions) = validPurchases.partition {
SkuKeys.CONSUMABLE_SKUS.contains(it.sku)
}
Timber.d("processPurchases consumables content: $consumables")
Timber.d("processPurchases subscription content: $subscriptions")
handleConsumablePurchasesAsync(consumables)
handleSubscriptionPurchasesAsync(subscriptions)
}
private fun handleConsumablePurchasesAsync(consumables: List<Purchase>) {
Timber.d("handleConsumablePurchasesAsync called")
consumables.forEach { purchase ->
Timber.d("handleConsumablePurchasesAsync foreach it is $purchase")
val params = ConsumeParams
.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
playStoreBillingClient.consumeAsync(params) { billingResult, purchaseToken ->
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
purchaseToken.apply { disburseConsumableEntitlements(purchase) }
}
else -> Timber.w(billingResult.debugMessage)
}
}
}
}
private fun disburseConsumableEntitlements(purchase: Purchase) {
when (purchase.sku) {
SkuKeys.COINS_5K -> addCoin(5000)
SkuKeys.COINS_10K -> addCoin(10000)
SkuKeys.COINS_100K -> addCoin(100000)
SkuKeys.COINS_500K -> addCoin(500000)
SkuKeys.COINS_1M -> addCoin(1000000)
SkuKeys.COINS_2M -> addCoin(2000000)
}
//NOTE: you can add more logic here to handle failed entitlement
}
private fun handleSubscriptionPurchasesAsync(subscriptions: List<Purchase>) {
Timber.d("acknowledgeNonConsumablePurchasesAsync called")
if(subscriptions.isNullOrEmpty()){
setUserAsVip(false)
}else{
subscriptions.forEach { purchase ->
if(!purchase.isAcknowledged){
acknowledgeSubscription(purchase)
}else{
setUserAsVip(true)
}
}
}
}
private fun acknowledgeSubscription(purchase: Purchase){
val params = AcknowledgePurchaseParams
.newBuilder()
.setPurchaseToken(purchase.purchaseToken)
.build()
playStoreBillingClient.acknowledgePurchase(params) { billingResult ->
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> setUserAsVip(true)
else -> Timber.d("acknowledgeNonConsumablePurchasesAsync response is ${billingResult.debugMessage}")
}
}
}
private fun setUserAsVip(vip : Boolean){
FirestoreRepository.setUserVip(vip)
}
private fun querySkuDetailsAsync(
@BillingClient.SkuType skuType: String,
skuList: List<String>
) {
Timber.d("querySkuDetailsAsync for $skuType")
val params = SkuDetailsParams
.newBuilder()
.setSkusList(skuList)
.setType(skuType)
.build()
playStoreBillingClient.querySkuDetailsAsync(params) { billingResult, skuDetailsList ->
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> {
Timber.d("querySkuDetailsAsync result: $skuDetailsList")
if (skuDetailsList.orEmpty().isNotEmpty()) {
if (SkuKeys.INAPP_SKUS.contains(skuList[0]))
coinsSkuDetailsListLiveData.value = skuDetailsList.apply {
this?.sortByDescending { it.priceAmountMicros }
}
else
subsSkuDetailsListLiveData.value = skuDetailsList.apply {
this?.sortByDescending { it.priceAmountMicros }
}
}
}
}
}
}
private fun addCoin(amount: Long) =
FirestoreRepository.incrementCoins(FirebaseAuthRepository.currentUserId, amount)
override fun onPurchasesUpdated(
billingResult: BillingResult,
purchases: MutableList<Purchase>?
) {
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> purchases?.apply { processPurchases(this.toSet()) }
BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED -> {
//call queryPurchasesAsync to verify and process all such items
Timber.d("ITEM_ALREADY_OWNED: ${billingResult.debugMessage}")
queryPurchasesAsync()
}
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> connectToPlayBillingService()
else -> Timber.i(billingResult.debugMessage)
}
}
private fun isSubScriptionSupported(): Boolean {
val billingResult =
playStoreBillingClient.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS)
var result = false
when (billingResult.responseCode) {
BillingClient.BillingResponseCode.OK -> result = true
BillingClient.BillingResponseCode.SERVICE_DISCONNECTED -> connectToPlayBillingService()
else -> Timber.w("isSubScriptionSupported error: ${billingResult.debugMessage}")
}
return result
}
fun launchBillingFlow(activity: Activity, skuDetails: SkuDetails) {
val purchaseParams = BillingFlowParams
.newBuilder()
.setSkuDetails(skuDetails)
.build()
playStoreBillingClient.launchBillingFlow(activity, purchaseParams)
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment