From 249ee38d68d77a551d85a3d7efefce4cc0870376 Mon Sep 17 00:00:00 2001 From: Zheng-Xiang Date: Fri, 3 Jan 2025 16:32:28 +0800 Subject: [PATCH 1/4] Support multiple offers in one product --- .../limurse/iapsample/JavaSampleActivity.java | 4 +- .../limurse/iapsample/KotlinSampleActivity.kt | 4 +- .../java/com/limurse/iap/BillingService.kt | 79 +++++++++++++------ .../com/limurse/iap/BillingServiceListener.kt | 2 +- .../main/java/com/limurse/iap/DataWrappers.kt | 23 ++++-- .../java/com/limurse/iap/IBillingService.kt | 6 +- .../main/java/com/limurse/iap/IapConnector.kt | 5 +- .../limurse/iap/PurchaseServiceListener.kt | 2 +- 8 files changed, 82 insertions(+), 43 deletions(-) diff --git a/app/src/main/java/com/limurse/iapsample/JavaSampleActivity.java b/app/src/main/java/com/limurse/iapsample/JavaSampleActivity.java index b747e0e..7373f0c 100644 --- a/app/src/main/java/com/limurse/iapsample/JavaSampleActivity.java +++ b/app/src/main/java/com/limurse/iapsample/JavaSampleActivity.java @@ -102,11 +102,11 @@ public void onPurchaseFailed(@Nullable DataWrappers.PurchaseInfo purchaseInfo, @ ); binding.btnMonthly.setOnClickListener(it -> - iapConnector.subscribe(this, "subscription", null, null) + iapConnector.subscribe(this, "subscription", "", null, null) ); binding.btnYearly.setOnClickListener(it -> - iapConnector.subscribe(this, "yearly", null, null) + iapConnector.subscribe(this, "yearly", "", null, null) ); binding.btnQuite.setOnClickListener(it -> diff --git a/app/src/main/java/com/limurse/iapsample/KotlinSampleActivity.kt b/app/src/main/java/com/limurse/iapsample/KotlinSampleActivity.kt index 77d16c5..b42836b 100644 --- a/app/src/main/java/com/limurse/iapsample/KotlinSampleActivity.kt +++ b/app/src/main/java/com/limurse/iapsample/KotlinSampleActivity.kt @@ -51,7 +51,7 @@ class KotlinSampleActivity : AppCompatActivity() { }) iapConnector.addPurchaseListener(object : PurchaseServiceListener { - override fun onPricesUpdated(iapKeyPrices: Map>) { + override fun onPricesUpdated(iapKeyPrices: Map) { // list of available products will be received here, so you can update UI with prices if needed } @@ -102,7 +102,7 @@ class KotlinSampleActivity : AppCompatActivity() { } } - override fun onPricesUpdated(iapKeyPrices: Map>) { + override fun onPricesUpdated(iapKeyPrices: Map) { // list of available products will be received here, so you can update UI with prices if needed } diff --git a/iap/src/main/java/com/limurse/iap/BillingService.kt b/iap/src/main/java/com/limurse/iap/BillingService.kt index bf64d3c..1a5d89d 100644 --- a/iap/src/main/java/com/limurse/iap/BillingService.kt +++ b/iap/src/main/java/com/limurse/iap/BillingService.kt @@ -82,19 +82,19 @@ class BillingService( return } - launchBillingFlow(activity, sku, BillingClient.ProductType.INAPP, obfuscatedAccountId, obfuscatedProfileId) + launchBillingFlow(activity, sku, BillingClient.ProductType.INAPP, "", obfuscatedAccountId, obfuscatedProfileId) } - override fun subscribe(activity: Activity, sku: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) { + override fun subscribe(activity: Activity, sku: String, offerId: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) { if (!sku.isProductReady()) { log("buy. Google billing service is not ready yet. (SKU is not ready yet -2)") return } - launchBillingFlow(activity, sku, BillingClient.ProductType.SUBS, obfuscatedAccountId, obfuscatedProfileId) + launchBillingFlow(activity, sku, BillingClient.ProductType.SUBS, offerId, obfuscatedAccountId, obfuscatedProfileId) } - private fun launchBillingFlow(activity: Activity, sku: String, type: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) { + private fun launchBillingFlow(activity: Activity, sku: String, type: String, offerId: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) { sku.toProductDetails(type) { productDetails -> if (productDetails != null) { @@ -102,8 +102,16 @@ class BillingService( val builder = BillingFlowParams.ProductDetailsParams.newBuilder() .setProductDetails(productDetails) - if(type == BillingClient.ProductType.SUBS){ - productDetails.subscriptionOfferDetails?.getOrNull(0)?.let { + if(type == BillingClient.ProductType.SUBS) { + var index = 0 + if (offerId.isNotBlank()) { + productDetails.subscriptionOfferDetails?.indexOfFirst { it.offerId == offerId }?.also { + if (it >= 0) { + index = it + } + } + } + productDetails.subscriptionOfferDetails?.getOrNull(index)?.also { builder.setOfferToken(it.offerToken) } } @@ -306,30 +314,49 @@ class BillingService( entry.value?.let { when(it.productType){ BillingClient.ProductType.SUBS->{ - entry.key to (it.subscriptionOfferDetails?.getOrNull(0)?.pricingPhases?.pricingPhaseList?.map { pricingPhase -> - DataWrappers.ProductDetails( - title = it.title, - description = it.description, - priceCurrencyCode = pricingPhase.priceCurrencyCode, - price = pricingPhase.formattedPrice, - priceAmount = pricingPhase.priceAmountMicros.div(1000000.0), - billingCycleCount = pricingPhase.billingCycleCount, - billingPeriod = pricingPhase.billingPeriod, - recurrenceMode = pricingPhase.recurrenceMode - ) - } ?: listOf()) + entry.key to DataWrappers.ProductDetails( + title = it.title, + description = it.description, + offers = it.subscriptionOfferDetails?.map { offerDetails -> + DataWrappers.Offer( + id = offerDetails.offerId, + token = offerDetails.offerToken, + tags = offerDetails.offerTags, + pricingPhase = offerDetails.pricingPhases.pricingPhaseList.map { pricingPhase -> + DataWrappers.PricingPhase( + priceCurrencyCode = pricingPhase.priceCurrencyCode, + price = pricingPhase.formattedPrice, + priceAmount = pricingPhase.priceAmountMicros.div(1000000.0), + billingCycleCount = pricingPhase.billingCycleCount, + billingPeriod = pricingPhase.billingPeriod, + recurrenceMode = pricingPhase.recurrenceMode + ) + } + ) + } + ) } else->{ - entry.key to listOf(DataWrappers.ProductDetails( + entry.key to DataWrappers.ProductDetails( title = it.title, description = it.description, - priceCurrencyCode = it.oneTimePurchaseOfferDetails?.priceCurrencyCode, - price = it.oneTimePurchaseOfferDetails?.formattedPrice, - priceAmount = it.oneTimePurchaseOfferDetails?.priceAmountMicros?.div(1000000.0), - billingCycleCount = null, - billingPeriod = null, - recurrenceMode = ProductDetails.RecurrenceMode.NON_RECURRING - )) + offers = it.oneTimePurchaseOfferDetails?.let { offerDetails -> + listOf(DataWrappers.Offer( + id = null, + token = null, + tags = null, + pricingPhase = listOf( + DataWrappers.PricingPhase( + priceCurrencyCode = offerDetails.priceCurrencyCode, + price = offerDetails.formattedPrice, + priceAmount = offerDetails.priceAmountMicros.div(1000000.0), + billingCycleCount = null, + billingPeriod = null, + recurrenceMode = ProductDetails.RecurrenceMode.NON_RECURRING + )) + )) + } ?: listOf() + ) } } } diff --git a/iap/src/main/java/com/limurse/iap/BillingServiceListener.kt b/iap/src/main/java/com/limurse/iap/BillingServiceListener.kt index 2b53399..3836ece 100644 --- a/iap/src/main/java/com/limurse/iap/BillingServiceListener.kt +++ b/iap/src/main/java/com/limurse/iap/BillingServiceListener.kt @@ -6,7 +6,7 @@ interface BillingServiceListener { * * @param iapKeyPrices - a map with available products */ - fun onPricesUpdated(iapKeyPrices: Map>) + fun onPricesUpdated(iapKeyPrices: Map) /** * Callback will be triggered when a purchase was failed. diff --git a/iap/src/main/java/com/limurse/iap/DataWrappers.kt b/iap/src/main/java/com/limurse/iap/DataWrappers.kt index 38b2b9e..a2a8f20 100644 --- a/iap/src/main/java/com/limurse/iap/DataWrappers.kt +++ b/iap/src/main/java/com/limurse/iap/DataWrappers.kt @@ -7,12 +7,7 @@ class DataWrappers { data class ProductDetails( val title: String?, val description: String?, - val price: String?, - val priceAmount: Double?, - val priceCurrencyCode: String?, - val billingCycleCount: Int?, - val billingPeriod: String?, - val recurrenceMode: Int? + val offers: List? ) data class PurchaseInfo( @@ -29,4 +24,20 @@ class DataWrappers { val sku: String, val accountIdentifiers: AccountIdentifiers? ) + + data class Offer( + val id: String?, + val token: String?, + val tags: List?, + val pricingPhase: List + ) + + data class PricingPhase( + val price: String?, + val priceAmount: Double?, + val priceCurrencyCode: String?, + val billingCycleCount: Int?, + val billingPeriod: String?, + val recurrenceMode: Int? + ) } \ No newline at end of file diff --git a/iap/src/main/java/com/limurse/iap/IBillingService.kt b/iap/src/main/java/com/limurse/iap/IBillingService.kt index 4411b10..e4ca378 100644 --- a/iap/src/main/java/com/limurse/iap/IBillingService.kt +++ b/iap/src/main/java/com/limurse/iap/IBillingService.kt @@ -83,13 +83,13 @@ abstract class IBillingService { } } - fun updatePrices(iapKeyPrices: Map>) { + fun updatePrices(iapKeyPrices: Map) { findUiHandler().post { updatePricesInternal(iapKeyPrices) } } - private fun updatePricesInternal(iapKeyPrices: Map>) { + private fun updatePricesInternal(iapKeyPrices: Map) { for (billingServiceListener in purchaseServiceListeners) { billingServiceListener.onPricesUpdated(iapKeyPrices) } @@ -121,7 +121,7 @@ abstract class IBillingService { abstract fun init(key: String?) abstract fun buy(activity: Activity, sku: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) - abstract fun subscribe(activity: Activity, sku: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) + abstract fun subscribe(activity: Activity, sku: String, offerId: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) abstract fun unsubscribe(activity: Activity, sku: String) abstract fun enableDebugLogging(enable: Boolean) diff --git a/iap/src/main/java/com/limurse/iap/IapConnector.kt b/iap/src/main/java/com/limurse/iap/IapConnector.kt index eb68c51..7ba8718 100644 --- a/iap/src/main/java/com/limurse/iap/IapConnector.kt +++ b/iap/src/main/java/com/limurse/iap/IapConnector.kt @@ -61,8 +61,9 @@ class IapConnector @JvmOverloads constructor( getBillingService().buy(activity, sku, obfuscatedAccountId, obfuscatedProfileId) } - fun subscribe(activity: Activity, sku: String, obfuscatedAccountId: String? = null, obfuscatedProfileId: String? = null) { - getBillingService().subscribe(activity, sku, obfuscatedAccountId, obfuscatedProfileId) + fun subscribe(activity: Activity, sku: String, offerId: String = "", + obfuscatedAccountId: String? = null, obfuscatedProfileId: String? = null) { + getBillingService().subscribe(activity, sku, offerId, obfuscatedAccountId, obfuscatedProfileId) } fun unsubscribe(activity: Activity, sku: String) { diff --git a/iap/src/main/java/com/limurse/iap/PurchaseServiceListener.kt b/iap/src/main/java/com/limurse/iap/PurchaseServiceListener.kt index 8d36931..36867c2 100644 --- a/iap/src/main/java/com/limurse/iap/PurchaseServiceListener.kt +++ b/iap/src/main/java/com/limurse/iap/PurchaseServiceListener.kt @@ -6,7 +6,7 @@ interface PurchaseServiceListener : BillingServiceListener { * * @param iapKeyPrices - a map with available products */ - override fun onPricesUpdated(iapKeyPrices: Map>) + override fun onPricesUpdated(iapKeyPrices: Map) /** * Callback will be triggered when a product purchased successfully From e2e99363807876e5f80fe25bb374f84690853c80 Mon Sep 17 00:00:00 2001 From: Zheng-Xiang Date: Fri, 3 Jan 2025 16:53:53 +0800 Subject: [PATCH 2/4] Correct naming --- iap/src/main/java/com/limurse/iap/BillingService.kt | 4 ++-- iap/src/main/java/com/limurse/iap/DataWrappers.kt | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iap/src/main/java/com/limurse/iap/BillingService.kt b/iap/src/main/java/com/limurse/iap/BillingService.kt index 1a5d89d..8ab3103 100644 --- a/iap/src/main/java/com/limurse/iap/BillingService.kt +++ b/iap/src/main/java/com/limurse/iap/BillingService.kt @@ -322,7 +322,7 @@ class BillingService( id = offerDetails.offerId, token = offerDetails.offerToken, tags = offerDetails.offerTags, - pricingPhase = offerDetails.pricingPhases.pricingPhaseList.map { pricingPhase -> + pricingPhases = offerDetails.pricingPhases.pricingPhaseList.map { pricingPhase -> DataWrappers.PricingPhase( priceCurrencyCode = pricingPhase.priceCurrencyCode, price = pricingPhase.formattedPrice, @@ -345,7 +345,7 @@ class BillingService( id = null, token = null, tags = null, - pricingPhase = listOf( + pricingPhases = listOf( DataWrappers.PricingPhase( priceCurrencyCode = offerDetails.priceCurrencyCode, price = offerDetails.formattedPrice, diff --git a/iap/src/main/java/com/limurse/iap/DataWrappers.kt b/iap/src/main/java/com/limurse/iap/DataWrappers.kt index a2a8f20..42da225 100644 --- a/iap/src/main/java/com/limurse/iap/DataWrappers.kt +++ b/iap/src/main/java/com/limurse/iap/DataWrappers.kt @@ -29,7 +29,7 @@ class DataWrappers { val id: String?, val token: String?, val tags: List?, - val pricingPhase: List + val pricingPhases: List ) data class PricingPhase( From 4f766365785910b360b69ffd8602502c2c90fcb1 Mon Sep 17 00:00:00 2001 From: Zheng-Xiang Date: Mon, 6 Jan 2025 15:32:34 +0800 Subject: [PATCH 3/4] Declare offerId optional to align ProductDetails.offerId --- .../com/limurse/iapsample/JavaSampleActivity.java | 4 ++-- iap/src/main/java/com/limurse/iap/BillingService.kt | 12 +++++------- iap/src/main/java/com/limurse/iap/IBillingService.kt | 2 +- iap/src/main/java/com/limurse/iap/IapConnector.kt | 2 +- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/app/src/main/java/com/limurse/iapsample/JavaSampleActivity.java b/app/src/main/java/com/limurse/iapsample/JavaSampleActivity.java index 7373f0c..88d892e 100644 --- a/app/src/main/java/com/limurse/iapsample/JavaSampleActivity.java +++ b/app/src/main/java/com/limurse/iapsample/JavaSampleActivity.java @@ -102,11 +102,11 @@ public void onPurchaseFailed(@Nullable DataWrappers.PurchaseInfo purchaseInfo, @ ); binding.btnMonthly.setOnClickListener(it -> - iapConnector.subscribe(this, "subscription", "", null, null) + iapConnector.subscribe(this, "subscription", null, null, null) ); binding.btnYearly.setOnClickListener(it -> - iapConnector.subscribe(this, "yearly", "", null, null) + iapConnector.subscribe(this, "yearly", null, null, null) ); binding.btnQuite.setOnClickListener(it -> diff --git a/iap/src/main/java/com/limurse/iap/BillingService.kt b/iap/src/main/java/com/limurse/iap/BillingService.kt index 8ab3103..7e83015 100644 --- a/iap/src/main/java/com/limurse/iap/BillingService.kt +++ b/iap/src/main/java/com/limurse/iap/BillingService.kt @@ -85,7 +85,7 @@ class BillingService( launchBillingFlow(activity, sku, BillingClient.ProductType.INAPP, "", obfuscatedAccountId, obfuscatedProfileId) } - override fun subscribe(activity: Activity, sku: String, offerId: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) { + override fun subscribe(activity: Activity, sku: String, offerId: String?, obfuscatedAccountId: String?, obfuscatedProfileId: String?) { if (!sku.isProductReady()) { log("buy. Google billing service is not ready yet. (SKU is not ready yet -2)") return @@ -94,7 +94,7 @@ class BillingService( launchBillingFlow(activity, sku, BillingClient.ProductType.SUBS, offerId, obfuscatedAccountId, obfuscatedProfileId) } - private fun launchBillingFlow(activity: Activity, sku: String, type: String, offerId: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) { + private fun launchBillingFlow(activity: Activity, sku: String, type: String, offerId: String?, obfuscatedAccountId: String?, obfuscatedProfileId: String?) { sku.toProductDetails(type) { productDetails -> if (productDetails != null) { @@ -104,11 +104,9 @@ class BillingService( if(type == BillingClient.ProductType.SUBS) { var index = 0 - if (offerId.isNotBlank()) { - productDetails.subscriptionOfferDetails?.indexOfFirst { it.offerId == offerId }?.also { - if (it >= 0) { - index = it - } + productDetails.subscriptionOfferDetails?.indexOfFirst { it.offerId == offerId }?.also { + if (it >= 0) { + index = it } } productDetails.subscriptionOfferDetails?.getOrNull(index)?.also { diff --git a/iap/src/main/java/com/limurse/iap/IBillingService.kt b/iap/src/main/java/com/limurse/iap/IBillingService.kt index e4ca378..6476320 100644 --- a/iap/src/main/java/com/limurse/iap/IBillingService.kt +++ b/iap/src/main/java/com/limurse/iap/IBillingService.kt @@ -121,7 +121,7 @@ abstract class IBillingService { abstract fun init(key: String?) abstract fun buy(activity: Activity, sku: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) - abstract fun subscribe(activity: Activity, sku: String, offerId: String, obfuscatedAccountId: String?, obfuscatedProfileId: String?) + abstract fun subscribe(activity: Activity, sku: String, offerId: String?, obfuscatedAccountId: String?, obfuscatedProfileId: String?) abstract fun unsubscribe(activity: Activity, sku: String) abstract fun enableDebugLogging(enable: Boolean) diff --git a/iap/src/main/java/com/limurse/iap/IapConnector.kt b/iap/src/main/java/com/limurse/iap/IapConnector.kt index 7ba8718..057af5c 100644 --- a/iap/src/main/java/com/limurse/iap/IapConnector.kt +++ b/iap/src/main/java/com/limurse/iap/IapConnector.kt @@ -61,7 +61,7 @@ class IapConnector @JvmOverloads constructor( getBillingService().buy(activity, sku, obfuscatedAccountId, obfuscatedProfileId) } - fun subscribe(activity: Activity, sku: String, offerId: String = "", + fun subscribe(activity: Activity, sku: String, offerId: String? = null, obfuscatedAccountId: String? = null, obfuscatedProfileId: String? = null) { getBillingService().subscribe(activity, sku, offerId, obfuscatedAccountId, obfuscatedProfileId) } From dbf5a2aff28a5e59e83213b8b68b539cc9ba8e43 Mon Sep 17 00:00:00 2001 From: Zheng-Xiang Date: Mon, 6 Jan 2025 15:35:27 +0800 Subject: [PATCH 4/4] Null offer for buy one-time product --- iap/src/main/java/com/limurse/iap/BillingService.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iap/src/main/java/com/limurse/iap/BillingService.kt b/iap/src/main/java/com/limurse/iap/BillingService.kt index 7e83015..40c8e68 100644 --- a/iap/src/main/java/com/limurse/iap/BillingService.kt +++ b/iap/src/main/java/com/limurse/iap/BillingService.kt @@ -82,7 +82,7 @@ class BillingService( return } - launchBillingFlow(activity, sku, BillingClient.ProductType.INAPP, "", obfuscatedAccountId, obfuscatedProfileId) + launchBillingFlow(activity, sku, BillingClient.ProductType.INAPP, null, obfuscatedAccountId, obfuscatedProfileId) } override fun subscribe(activity: Activity, sku: String, offerId: String?, obfuscatedAccountId: String?, obfuscatedProfileId: String?) {