Comments (1)
I have removed all stream and used kotlin equivalents. below is BillingConnector.kt class
class BillingConnector(context: Context, base64Key: String) {
private var reconnectMilliseconds = RECONNECT_TIMER_START_MILLISECONDS
private val base64Key: String
private var billingClient: BillingClient? = null
private var billingEventListener: BillingEventListener? = null
private var consumableIds: List<String>? = null
private var nonConsumableIds: List<String>? = null
private var subscriptionIds: List<String>? = null
private val allProductList: MutableList<QueryProductDetailsParams.Product> = ArrayList()
private val fetchedProductInfoList: MutableList<ProductInfo> = ArrayList()
private val purchasedProductsList: MutableList<PurchaseInfo> = ArrayList()
private var shouldAutoAcknowledge = false
private var shouldAutoConsume = false
private var shouldEnableLogging = false
private var isConnected = false
private var fetchedPurchasedProducts = false
/**
* BillingConnector public constructor
*
* @param context - is the application context
* @param base64Key - is the public developer key from Play Console
*/
init {
init(context)
this.base64Key = base64Key
}
/**
* To initialize BillingConnector
*/
private fun init(context: Context) {
billingClient = BillingClient.newBuilder(context)
.enablePendingPurchases()
.setListener { billingResult: BillingResult, purchases: List<Purchase>? ->
when (billingResult.responseCode) {
BillingResponseCode.OK -> if (purchases != null) {
processPurchases(ProductType.COMBINED, purchases, false)
}
BillingResponseCode.USER_CANCELED -> {
Log("User pressed back or canceled a dialog." + " Response code: " + billingResult.responseCode)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector,
BillingResponse(ErrorType.USER_CANCELED, billingResult)
)
}
}
BillingResponseCode.SERVICE_UNAVAILABLE -> {
Log("Network connection is down." + " Response code: " + billingResult.responseCode)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector,
BillingResponse(ErrorType.SERVICE_UNAVAILABLE, billingResult)
)
}
}
BillingResponseCode.BILLING_UNAVAILABLE -> {
Log("Billing API version is not supported for the type requested." + " Response code: " + billingResult.responseCode)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector,
BillingResponse(ErrorType.BILLING_UNAVAILABLE, billingResult)
)
}
}
BillingResponseCode.ITEM_UNAVAILABLE -> {
Log("Requested product is not available for purchase." + " Response code: " + billingResult.responseCode)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector,
BillingResponse(ErrorType.ITEM_UNAVAILABLE, billingResult)
)
}
}
BillingResponseCode.DEVELOPER_ERROR -> {
Log("Invalid arguments provided to the API." + " Response code: " + billingResult.responseCode)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector,
BillingResponse(ErrorType.DEVELOPER_ERROR, billingResult)
)
}
}
BillingResponseCode.ERROR -> {
Log("Fatal error during the API action." + " Response code: " + billingResult.responseCode)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector,
BillingResponse(ErrorType.ERROR, billingResult)
)
}
}
BillingResponseCode.ITEM_ALREADY_OWNED -> {
Log("Failure to purchase since item is already owned." + " Response code: " + billingResult.responseCode)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector,
BillingResponse(ErrorType.ITEM_ALREADY_OWNED, billingResult)
)
}
}
BillingResponseCode.ITEM_NOT_OWNED -> {
Log("Failure to consume since item is not owned." + " Response code: " + billingResult.responseCode)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector,
BillingResponse(ErrorType.ITEM_NOT_OWNED, billingResult)
)
}
}
BillingResponseCode.SERVICE_DISCONNECTED, BillingResponseCode.SERVICE_TIMEOUT -> Log(
"Initialization error: service disconnected/timeout. Trying to reconnect..."
)
else -> Log(
"Initialization error: " + BillingResponse(
ErrorType.BILLING_ERROR,
billingResult
)
)
}
}
.build()
}
/**
* To attach an event listener to establish a bridge with the caller
*/
fun setBillingEventListener(billingEventListener: BillingEventListener?) {
this.billingEventListener = billingEventListener
}
/**
* To set consumable products ids
*/
fun setConsumableIds(consumableIds: List<String>?): BillingConnector {
this.consumableIds = consumableIds
return this
}
/**
* To set non-consumable products ids
*/
fun setNonConsumableIds(nonConsumableIds: List<String>?): BillingConnector {
this.nonConsumableIds = nonConsumableIds
return this
}
/**
* To set subscription products ids
*/
fun setSubscriptionIds(subscriptionIds: List<String>?): BillingConnector {
this.subscriptionIds = subscriptionIds
return this
}
/**
* To auto acknowledge the purchase
*/
fun autoAcknowledge(): BillingConnector {
shouldAutoAcknowledge = true
return this
}
/**
* To auto consume the purchase
*/
fun autoConsume(): BillingConnector {
shouldAutoConsume = true
return this
}
/**
* To enable logging for debugging
*/
fun enableLogging(): BillingConnector {
shouldEnableLogging = true
return this
}
/**
* Returns the state of the billing client
*/
val isReady: Boolean
get() {
if (!isConnected) {
Log("Billing client is not ready because no connection is established yet")
}
if (!billingClient!!.isReady) {
Log("Billing client is not ready yet")
}
return isConnected && billingClient!!.isReady && !fetchedProductInfoList.isEmpty()
}
/**
* Returns a boolean state of the product
*
* @param productId - is the product id that has to be checked
*/
private fun checkProductBeforeInteraction(productId: String?): Boolean {
if (!isReady) {
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector, BillingResponse(
ErrorType.CLIENT_NOT_READY,
"Client is not ready yet", defaultResponseCode
)
)
}
} else if (productId != null && fetchedProductInfoList.none { it: ProductInfo -> it.product == productId }) {
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector, BillingResponse(
ErrorType.PRODUCT_NOT_EXIST,
"The product id: $productId doesn't seem to exist on Play Console",
defaultResponseCode
)
)
}
} else return isReady
return false
}
/**
* To connect the billing client with Play Console
*/
fun connect(): BillingConnector {
val productInAppList: MutableList<QueryProductDetailsParams.Product> = ArrayList()
val productSubsList: MutableList<QueryProductDetailsParams.Product> = ArrayList()
//set empty list to null so we only have to deal with lists that are null or not empty
if (consumableIds == null || consumableIds!!.isEmpty()) {
consumableIds = null
} else {
for (id in consumableIds!!) {
productInAppList.add(
QueryProductDetailsParams.Product.newBuilder().setProductId(id).setProductType(
BillingClient.ProductType.INAPP
).build()
)
}
}
if (nonConsumableIds == null || nonConsumableIds!!.isEmpty()) {
nonConsumableIds = null
} else {
for (id in nonConsumableIds!!) {
productInAppList.add(
QueryProductDetailsParams.Product.newBuilder().setProductId(id).setProductType(
BillingClient.ProductType.INAPP
).build()
)
}
}
if (subscriptionIds == null || subscriptionIds!!.isEmpty()) {
subscriptionIds = null
} else {
for (id in subscriptionIds!!) {
productSubsList.add(
QueryProductDetailsParams.Product.newBuilder().setProductId(id).setProductType(
BillingClient.ProductType.SUBS
).build()
)
}
}
allProductList.addAll(productInAppList)
allProductList.addAll(productSubsList)
//check if any list is provided
require(allProductList.isNotEmpty()) { "At least one list of consumables, non-consumables or subscriptions is needed" }
//check for duplicates product ids
val allIdsSize = allProductList.size
val allIdsSizeDistinct = allProductList.distinct().count()
require(allIdsSize == allIdsSizeDistinct) { "The product id must appear only once in a list. Also, it must not be in different lists" }
Log("Billing service: connecting...")
if (!billingClient!!.isReady) {
billingClient!!.startConnection(object : BillingClientStateListener {
override fun onBillingServiceDisconnected() {
isConnected = false
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector, BillingResponse(
ErrorType.CLIENT_DISCONNECTED,
"Billing service: disconnected", defaultResponseCode
)
)
}
Log("Billing service: Trying to reconnect...")
retryBillingClientConnection()
}
override fun onBillingSetupFinished(billingResult: BillingResult) {
isConnected = false
when (billingResult.responseCode) {
BillingResponseCode.OK -> {
isConnected = true
Log("Billing service: connected")
//query consumable and non-consumable product details
if (!productInAppList.isEmpty()) {
queryProductDetails(
BillingClient.ProductType.INAPP,
productInAppList
)
}
//query subscription product details
if (subscriptionIds != null) {
queryProductDetails(BillingClient.ProductType.SUBS, productSubsList)
}
}
BillingResponseCode.BILLING_UNAVAILABLE -> {
Log("Billing service: unavailable")
retryBillingClientConnection()
}
else -> {
Log("Billing service: error")
retryBillingClientConnection()
}
}
}
})
}
return this
}
/**
* Retries the billing client connection with exponential backoff
* Max out at the time specified by RECONNECT_TIMER_MAX_TIME_MILLISECONDS (15 minutes)
*/
private fun retryBillingClientConnection() {
findUiHandler().postDelayed({ connect() }, reconnectMilliseconds)
reconnectMilliseconds =
Math.min(reconnectMilliseconds * 2, RECONNECT_TIMER_MAX_TIME_MILLISECONDS)
}
/**
* Fires a query in Play Console to show products available to purchase
*/
private fun queryProductDetails(
productType: String,
productList: List<QueryProductDetailsParams.Product>,
) {
val productDetailsParams =
QueryProductDetailsParams.newBuilder().setProductList(productList).build()
billingClient!!.queryProductDetailsAsync(productDetailsParams) { billingResult: BillingResult, productDetailsList: List<ProductDetails> ->
if (billingResult.responseCode == BillingResponseCode.OK) {
if (productDetailsList.isEmpty()) {
Log("Query Product Details: data not found. Make sure product ids are configured on Play Console")
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector, BillingResponse(
ErrorType.BILLING_ERROR,
"No product found", defaultResponseCode
)
)
}
} else {
Log("Query Product Details: data found")
val fetchedProductInfo = productDetailsList.map { generateProductInfo(it) }.toMutableList()
fetchedProductInfoList.addAll(fetchedProductInfo)
when (productType) {
BillingClient.ProductType.INAPP, BillingClient.ProductType.SUBS -> findUiHandler().post {
billingEventListener!!.onProductsFetched(
fetchedProductInfo
)
}
else -> throw IllegalStateException("Product type is not implemented")
}
val fetchedProductIds = fetchedProductInfo.map { it.product }
val productListIds = productList.map { it.zza() } //according to the documentation "zza" is the product id
val isFetched = fetchedProductIds.any { o: String -> productListIds.contains(o) }
if (isFetched) {
fetchPurchasedProducts()
}
}
} else {
Log("Query Product Details: failed")
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector, BillingResponse(
ErrorType.BILLING_ERROR, billingResult
)
)
}
}
}
}
/**
* Returns a new ProductInfo object containing the product type and product details
*
* @param productDetails - is the object provided by the billing client API
*/
private fun generateProductInfo(productDetails: ProductDetails): ProductInfo {
val skuProductType: SkuProductType = when (productDetails.productType) {
BillingClient.ProductType.INAPP -> {
val consumable = isProductIdConsumable(productDetails.productId)
if (consumable) {
SkuProductType.CONSUMABLE
} else {
SkuProductType.NON_CONSUMABLE
}
}
BillingClient.ProductType.SUBS -> SkuProductType.SUBSCRIPTION
else -> throw IllegalStateException("Product type is not implemented correctly")
}
return ProductInfo(skuProductType, productDetails)
}
private fun isProductIdConsumable(productId: String): Boolean {
return if (consumableIds == null) {
false
} else consumableIds!!.contains(productId)
}
/**
* Returns purchases details for currently owned items without a network request
*/
private fun fetchPurchasedProducts() {
if (billingClient!!.isReady) {
billingClient!!.queryPurchasesAsync(
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.INAPP)
.build()
) { billingResult: BillingResult, purchases: List<Purchase> ->
if (billingResult.responseCode == BillingResponseCode.OK) {
if (purchases.isEmpty()) {
Log("Query IN-APP Purchases: the list is empty")
} else {
Log("Query IN-APP Purchases: data found and progress")
}
processPurchases(ProductType.INAPP, purchases, true)
} else {
Log("Query IN-APP Purchases: failed")
}
}
//query subscription purchases for supported devices
if (isSubscriptionSupported === SupportState.SUPPORTED) {
billingClient!!.queryPurchasesAsync(
QueryPurchasesParams.newBuilder().setProductType(BillingClient.ProductType.SUBS)
.build()
) { billingResult: BillingResult, purchases: List<Purchase> ->
if (billingResult.responseCode == BillingResponseCode.OK) {
if (purchases.isEmpty()) {
Log("Query SUBS Purchases: the list is empty")
} else {
Log("Query SUBS Purchases: data found and progress")
}
processPurchases(ProductType.SUBS, purchases, true)
} else {
Log("Query SUBS Purchases: failed")
}
}
}
} else {
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector, BillingResponse(
ErrorType.FETCH_PURCHASED_PRODUCTS_ERROR,
"Billing client is not ready yet", defaultResponseCode
)
)
}
}
}
/**
* Before using subscriptions, device-support must be checked
* Not all devices support subscriptions
*/
val isSubscriptionSupported: SupportState
get() {
val response = billingClient!!.isFeatureSupported(FeatureType.SUBSCRIPTIONS)
return when (response.responseCode) {
BillingResponseCode.OK -> {
Log("Subscriptions support check: success")
SupportState.SUPPORTED
}
BillingResponseCode.SERVICE_DISCONNECTED -> {
Log("Subscriptions support check: disconnected. Trying to reconnect...")
SupportState.DISCONNECTED
}
else -> {
Log("Subscriptions support check: error -> " + response.responseCode + " " + response.debugMessage)
SupportState.NOT_SUPPORTED
}
}
}
/**
* Checks purchases signature for more security
*/
private fun processPurchases(
productType: ProductType,
allPurchases: List<Purchase>,
purchasedProductsFetched: Boolean,
) {
val signatureValidPurchases: MutableList<PurchaseInfo> = ArrayList()
//create a list with signature valid purchases
val validPurchases = allPurchases.filter { purchase: Purchase -> isPurchaseSignatureValid(purchase) }
for (purchase in validPurchases) {
//query all products as a list
val purchasesProducts = purchase.products
//loop through all products and progress for each product individually
for (i in purchasesProducts.indices) {
val purchaseProduct = purchasesProducts[i]
val productInfo =
fetchedProductInfoList.firstOrNull { it: ProductInfo -> it.product == purchaseProduct }
productInfo?.let {
val productDetails = it.productDetails
val purchaseInfo = PurchaseInfo(generateProductInfo(productDetails), purchase)
signatureValidPurchases.add(purchaseInfo)
}
}
}
if (purchasedProductsFetched) {
fetchedPurchasedProducts = true
findUiHandler().post {
billingEventListener!!.onPurchasedProductsFetched(
productType,
signatureValidPurchases
)
}
} else {
findUiHandler().post {
billingEventListener!!.onProductsPurchased(
signatureValidPurchases
)
}
}
purchasedProductsList.addAll(signatureValidPurchases)
for (purchaseInfo in signatureValidPurchases) {
if (shouldAutoConsume) {
consumePurchase(purchaseInfo)
}
if (shouldAutoAcknowledge) {
val isProductConsumable = purchaseInfo.skuProductType === SkuProductType.CONSUMABLE
if (!isProductConsumable) {
acknowledgePurchase(purchaseInfo)
}
}
}
}
/**
* Consume consumable products so that the user can buy the item again
*
*
* Consumable products might be bought/consumed by users multiple times (for eg. diamonds, coins etc)
* They have to be consumed within 3 days otherwise Google will refund the products
*/
fun consumePurchase(purchaseInfo: PurchaseInfo) {
if (checkProductBeforeInteraction(purchaseInfo.product)) {
if (purchaseInfo.skuProductType === SkuProductType.CONSUMABLE) {
if (purchaseInfo.purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
val consumeParams = ConsumeParams.newBuilder()
.setPurchaseToken(purchaseInfo.purchase.purchaseToken).build()
billingClient!!.consumeAsync(consumeParams) { billingResult: BillingResult, purchaseToken: String? ->
if (billingResult.responseCode == BillingResponseCode.OK) {
purchasedProductsList.remove(purchaseInfo)
findUiHandler().post {
billingEventListener!!.onPurchaseConsumed(
purchaseInfo
)
}
} else {
Log("Handling consumables: error during consumption attempt: " + billingResult.debugMessage)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector,
BillingResponse(ErrorType.CONSUME_ERROR, billingResult)
)
}
}
}
} else if (purchaseInfo.purchase.purchaseState == Purchase.PurchaseState.PENDING) {
Log(
"Handling consumables: purchase can not be consumed because the state is PENDING. " +
"A purchase can be consumed only when the state is PURCHASED"
)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector, BillingResponse(
ErrorType.CONSUME_WARNING,
"Warning: purchase can not be consumed because the state is PENDING. Please consume the purchase later",
defaultResponseCode
)
)
}
}
}
}
}
/**
* Acknowledge non-consumable products & subscriptions
*
*
* This will avoid refunding for these products to users by Google
*/
fun acknowledgePurchase(purchaseInfo: PurchaseInfo) {
if (checkProductBeforeInteraction(purchaseInfo.product)) {
when (purchaseInfo.skuProductType) {
SkuProductType.NON_CONSUMABLE, SkuProductType.SUBSCRIPTION -> if (purchaseInfo.purchase.purchaseState == Purchase.PurchaseState.PURCHASED) {
if (!purchaseInfo.purchase.isAcknowledged) {
val acknowledgePurchaseParams = AcknowledgePurchaseParams.newBuilder()
.setPurchaseToken(purchaseInfo.purchase.purchaseToken).build()
billingClient!!.acknowledgePurchase(acknowledgePurchaseParams) { billingResult: BillingResult ->
if (billingResult.responseCode == BillingResponseCode.OK) {
findUiHandler().post {
billingEventListener!!.onPurchaseAcknowledged(
purchaseInfo
)
}
} else {
Log("Handling acknowledges: error during acknowledgment attempt: " + billingResult.debugMessage)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector,
BillingResponse(ErrorType.ACKNOWLEDGE_ERROR, billingResult)
)
}
}
}
}
} else if (purchaseInfo.purchase.purchaseState == Purchase.PurchaseState.PENDING) {
Log(
"Handling acknowledges: purchase can not be acknowledged because the state is PENDING. " +
"A purchase can be acknowledged only when the state is PURCHASED"
)
findUiHandler().post {
billingEventListener!!.onBillingError(
this@BillingConnector, BillingResponse(
ErrorType.ACKNOWLEDGE_WARNING,
"Warning: purchase can not be acknowledged because the state is PENDING. Please acknowledge the purchase later",
defaultResponseCode
)
)
}
}
else -> {}
}
}
}
/**
* Called to purchase a non-consumable/consumable product
*/
fun purchase(activity: Activity, productId: String) {
purchase(activity, productId, 0)
}
/**
* Called to purchase a non-consumable/consumable product
*
*
* The offer index represents the different offers in the subscription.
*/
private fun purchase(activity: Activity, productId: String, selectedOfferIndex: Int) {
if (checkProductBeforeInteraction(productId)) {
val productInfo = fetchedProductInfoList.firstOrNull { it.product == productId }
productInfo?.let {
val productDetails = it.productDetails
val productDetailsParamsList: ImmutableList<ProductDetailsParams> =
if (productDetails.productType == BillingClient.ProductType.SUBS && productDetails.subscriptionOfferDetails != null) {
//the offer index represents the different offers in the subscription
//offer index is only available for subscriptions starting with Google Billing v5+
ImmutableList.of(
ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.setOfferToken(productDetails.subscriptionOfferDetails!![selectedOfferIndex].offerToken)
.build()
)
} else {
ImmutableList.of(
ProductDetailsParams.newBuilder()
.setProductDetails(productDetails)
.build()
)
}
val billingFlowParams = BillingFlowParams.newBuilder()
.setProductDetailsParamsList(productDetailsParamsList)
.build()
billingClient!!.launchBillingFlow(activity, billingFlowParams)
}
if (productInfo == null) {
Log("Billing client can not launch billing flow because product details are missing")
}
}
}
/**
* Called to purchase a subscription with offers
*
*
* To avoid confusion while trying to purchase a subscription
* Does the same thing as purchase() method
*
*
* For subscription with only one base package, use subscribe(activity, productId) method or selectedOfferIndex = 0
*/
fun subscribe(activity: Activity, productId: String, selectedOfferIndex: Int) {
purchase(activity, productId, selectedOfferIndex)
}
/**
* Called to purchase a simple subscription
*
*
* To avoid confusion while trying to purchase a subscription
* Does the same thing as purchase() method
*
*
* For subscription with multiple offers, use subscribe(activity, productId, selectedOfferIndex) method
*/
fun subscribe(activity: Activity, productId: String) {
purchase(activity, productId)
}
/**
* Called to cancel a subscription
*/
fun unsubscribe(activity: Activity, productId: String) {
try {
val subscriptionUrl =
"http://play.google.com/store/account/subscriptions?package=" + activity.packageName + "&sku=" + productId
val intent = Intent()
intent.action = Intent.ACTION_VIEW
intent.data = Uri.parse(subscriptionUrl)
activity.startActivity(intent)
activity.finish()
} catch (e: Exception) {
Log("Handling subscription cancellation: error while trying to unsubscribe")
e.printStackTrace()
}
}
/**
* Checks purchase state synchronously
*/
fun isPurchased(productInfo: ProductInfo): PurchasedResult {
return checkPurchased(productInfo.product)
}
private fun checkPurchased(productId: String): PurchasedResult {
return if (!isReady) {
PurchasedResult.CLIENT_NOT_READY
} else if (!fetchedPurchasedProducts) {
PurchasedResult.PURCHASED_PRODUCTS_NOT_FETCHED_YET
} else {
for (purchaseInfo in purchasedProductsList) {
if (purchaseInfo.product == productId) {
return PurchasedResult.YES
}
}
PurchasedResult.NO
}
}
/**
* Checks purchase signature validity
*/
private fun isPurchaseSignatureValid(purchase: Purchase): Boolean {
return verifyPurchase(base64Key, purchase.originalJson, purchase.signature)
}
/**
* Returns the main thread for operations that need to be executed on the UI thread
*
*
* BillingEventListener runs on it
*/
private fun findUiHandler(): Handler {
return Handler(Looper.getMainLooper())
}
/**
* To print a log while debugging BillingConnector
*/
private fun Log(debugMessage: String) {
if (shouldEnableLogging) {
android.util.Log.d(TAG, debugMessage)
}
}
/**
* Called to release the BillingClient instance
*
*
* To avoid leaks this method should be called when BillingConnector is no longer needed
*/
fun release() {
if (billingClient != null && billingClient!!.isReady) {
Log("BillingConnector instance release: ending connection...")
billingClient!!.endConnection()
}
}
companion object {
private const val TAG = "BillingConnector"
private const val defaultResponseCode = 99
private const val RECONNECT_TIMER_START_MILLISECONDS = 1000L
private const val RECONNECT_TIMER_MAX_TIME_MILLISECONDS = 1000L * 60L * 15L
}
}
from google-inapp-billing.
Related Issues (20)
- onPurchasedProductsFetched is always called twice HOT 4
- How to purchase multi-quantity in single transaction HOT 1
- Billing API version is not supported for the type requested HOT 3
- java.lang.NoClassDefFoundError V1.1.1 HOT 5
- billingConnector.isPurchased(productInfo) == PurchasedResult.YES does not work HOT 8
- What the hell? HOT 4
- Having problem fetching product price at onProductsFetched HOT 5
- Need example to get Subscriptions price HOT 1
- Blank Google Billpay popup HOT 1
- Android Billing Library version 5 or newer and targeting Android 14 or higher, you must update to [PBL 5.2.1] By November 1, 2023 HOT 4
- Latest Release (1.1.3) not building on JitPack.io HOT 1
- I don't know what to do with that HOT 1
- How to suscribe to a product->offer->Phase ? HOT 2
- onProductsPurchased is calling twice
- No indication that a purchase has been made HOT 1
- Got BillingResponse: Error type: DEVELOPER_ERROR Response code: 5 Message: Expired Product details. HOT 1
- How to detect refund? HOT 3
- Purchases not being acknowledged sometimes HOT 5
- So many issues with this library HOT 3
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from google-inapp-billing.