Skip to content
This repository was archived by the owner on Oct 17, 2025. It is now read-only.

Commit 9a9733e

Browse files
authored
Fix Android offer token handling (#8)
## Summary Honor provided subscription offer tokens in the Android module. Log warnings through OpenIapLog when mismatches occur. Fixes hyochan/expo-iap#207 ## Testing Not tested (not requested).
1 parent 1470076 commit 9a9733e

File tree

2 files changed

+38
-10
lines changed

2 files changed

+38
-10
lines changed

openiap/src/main/java/dev/hyo/openiap/OpenIapModule.kt

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -169,19 +169,46 @@ class OpenIapModule(private val context: Context) : OpenIapProtocol, PurchasesUp
169169
fun launchBillingWith(detailsList: List<ProductDetails>) {
170170
// Build params for all requested SKUs in order
171171
val paramsList = mutableListOf<BillingFlowParams.ProductDetailsParams>()
172-
for (pd in detailsList) {
172+
val requestedOffersBySku = mutableMapOf<String, MutableList<String>>()
173+
if (type == ProductRequest.ProductRequestType.Subs) {
174+
request.subscriptionOffers.forEach { offer ->
175+
if (offer.offerToken.isNotEmpty()) {
176+
val queue = requestedOffersBySku.getOrPut(offer.sku) { mutableListOf() }
177+
queue.add(offer.offerToken)
178+
}
179+
}
180+
}
181+
182+
for ((index, pd) in detailsList.withIndex()) {
173183
val builder = BillingFlowParams.ProductDetailsParams.newBuilder()
174184
.setProductDetails(pd)
175185
if (type == ProductRequest.ProductRequestType.Subs) {
176-
val offerToken = pd.subscriptionOfferDetails?.firstOrNull()?.offerToken
177-
if (offerToken.isNullOrEmpty()) {
178-
Log.w(TAG, "No subscription offer available for ${pd.productId}")
186+
val availableOfferTokens = pd.subscriptionOfferDetails?.map { it.offerToken } ?: emptyList()
187+
val requestedTokenFromQueue = requestedOffersBySku[pd.productId]?.let { queue ->
188+
if (queue.isNotEmpty()) queue.removeAt(0) else null
189+
}
190+
val requestedTokenFromIndex = request.subscriptionOffers.getOrNull(index)?.takeIf { it.sku == pd.productId }?.offerToken
191+
val resolvedOfferToken = requestedTokenFromQueue
192+
?: requestedTokenFromIndex
193+
?: pd.subscriptionOfferDetails?.firstOrNull()?.offerToken
194+
195+
if (resolvedOfferToken.isNullOrEmpty()) {
196+
OpenIapLog.w("No subscription offer available for ${pd.productId}", TAG)
179197
val err = OpenIapError.SkuOfferMismatch
180198
purchaseErrorListeners.forEach { runCatching { it.onPurchaseError(err) } }
181199
currentPurchaseCallback?.invoke(Result.success(emptyList()))
182200
return
183201
}
184-
builder.setOfferToken(offerToken)
202+
203+
if (availableOfferTokens.isNotEmpty() && !availableOfferTokens.contains(resolvedOfferToken)) {
204+
OpenIapLog.w("Requested offerToken=$resolvedOfferToken not found for ${pd.productId}", TAG)
205+
val err = OpenIapError.SkuOfferMismatch
206+
purchaseErrorListeners.forEach { runCatching { it.onPurchaseError(err) } }
207+
currentPurchaseCallback?.invoke(Result.success(emptyList()))
208+
return
209+
}
210+
211+
builder.setOfferToken(resolvedOfferToken)
185212
}
186213
paramsList.add(builder.build())
187214
}
@@ -244,7 +271,7 @@ class OpenIapModule(private val context: Context) : OpenIapProtocol, PurchasesUp
244271
launchBillingWith(ordered)
245272
// Do not complete here; wait for onPurchasesUpdated
246273
} else {
247-
Log.w(TAG, "queryProductDetails failed: code=${billingResult.responseCode} msg=${billingResult.debugMessage}")
274+
OpenIapLog.w("queryProductDetails failed: code=${billingResult.responseCode} msg=${billingResult.debugMessage}", TAG)
248275
val err = OpenIapError.QueryProduct()
249276
purchaseErrorListeners.forEach { runCatching { it.onPurchaseError(err) } }
250277
currentPurchaseCallback?.invoke(Result.success(emptyList()))
@@ -397,7 +424,7 @@ class OpenIapModule(private val context: Context) : OpenIapProtocol, PurchasesUp
397424
billingResult.responseCode,
398425
billingResult.debugMessage
399426
)
400-
Log.w(TAG, "Purchase failed: code=${billingResult.responseCode} msg=${error.message}")
427+
OpenIapLog.w("Purchase failed: code=${billingResult.responseCode} msg=${error.message}", TAG)
401428
// Surface framework-specific error upstream (maintains type for UserCancelled, etc.)
402429
purchaseErrorListeners.forEach { listener ->
403430
runCatching { listener.onPurchaseError(error) }
@@ -569,9 +596,9 @@ class OpenIapModule(private val context: Context) : OpenIapProtocol, PurchasesUp
569596
object : BillingClientStateListener {
570597
override fun onBillingSetupFinished(billingResult: BillingResult) {
571598
if (billingResult.responseCode != BillingClient.BillingResponseCode.OK) {
572-
Log.w(
573-
TAG,
599+
OpenIapLog.w(
574600
"Billing setup finished with error: ${billingResult.debugMessage}",
601+
TAG,
575602
)
576603
onFailure(IllegalStateException(billingResult.debugMessage ?: "Billing setup failed"))
577604
return

openiap/src/main/java/dev/hyo/openiap/models/OpenIapRequestTypes.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ data class RequestPurchaseParams(
88
val skus: List<String>,
99
val obfuscatedAccountIdAndroid: String? = null,
1010
val obfuscatedProfileIdAndroid: String? = null,
11-
val isOfferPersonalized: Boolean? = null
11+
val isOfferPersonalized: Boolean? = null,
12+
val subscriptionOffers: List<RequestSubscriptionAndroidProps.SubscriptionOffer> = emptyList()
1213
)
1314

1415
/**

0 commit comments

Comments
 (0)