@@ -236,6 +236,7 @@ class OpenIapModule(private val context: Context) : PurchasesUpdatedListener {
236236 if (androidArgs.type == ProductQueryType .Subs ) {
237237 androidArgs.subscriptionOffers.orEmpty().forEach { offer ->
238238 if (offer.offerToken.isNotEmpty()) {
239+ OpenIapLog .d(" Adding offer token for SKU ${offer.sku} : ${offer.offerToken} " , TAG )
239240 val queue = requestedOffersBySku.getOrPut(offer.sku) { mutableListOf () }
240241 queue.add(offer.offerToken)
241242 }
@@ -247,14 +248,22 @@ class OpenIapModule(private val context: Context) : PurchasesUpdatedListener {
247248 .setProductDetails(productDetails)
248249
249250 if (androidArgs.type == ProductQueryType .Subs ) {
251+ val availableOffers = productDetails.subscriptionOfferDetails?.map {
252+ " ${it.basePlanId} :${it.offerToken} "
253+ } ? : emptyList()
254+ OpenIapLog .d(" Available offers for ${productDetails.productId} : $availableOffers " , TAG )
255+
250256 val availableTokens = productDetails.subscriptionOfferDetails?.map { it.offerToken } ? : emptyList()
251257 val fromQueue = requestedOffersBySku[productDetails.productId]?.let { queue ->
252258 if (queue.isNotEmpty()) queue.removeAt(0 ) else null
253259 }
254260 val fromIndex = androidArgs.subscriptionOffers?.getOrNull(index)?.takeIf { it.sku == productDetails.productId }?.offerToken
255261 val resolved = fromQueue ? : fromIndex ? : productDetails.subscriptionOfferDetails?.firstOrNull()?.offerToken
256262
263+ OpenIapLog .d(" Resolved offer token for ${productDetails.productId} : $resolved " , TAG )
264+
257265 if (resolved.isNullOrEmpty() || (availableTokens.isNotEmpty() && ! availableTokens.contains(resolved))) {
266+ OpenIapLog .w(" Invalid offer token: $resolved not in $availableTokens " , TAG )
258267 val err = OpenIapError .SkuOfferMismatch
259268 purchaseErrorListeners.forEach { listener -> runCatching { listener.onPurchaseError(err) } }
260269 currentPurchaseCallback?.invoke(Result .success(emptyList()))
@@ -272,18 +281,49 @@ class OpenIapModule(private val context: Context) : PurchasesUpdatedListener {
272281 .setIsOfferPersonalized(androidArgs.isOfferPersonalized == true )
273282
274283 androidArgs.obfuscatedAccountId?.let { flowBuilder.setObfuscatedAccountId(it) }
275- androidArgs.obfuscatedProfileId?.let { flowBuilder.setObfuscatedProfileId(it) }
276284
285+ // For subscription upgrades/downgrades, purchaseToken and obfuscatedProfileId are mutually exclusive
277286 if (androidArgs.type == ProductQueryType .Subs && ! androidArgs.purchaseTokenAndroid.isNullOrBlank()) {
287+ // This is a subscription upgrade/downgrade - do not set obfuscatedProfileId
288+ OpenIapLog .d(" === Subscription Upgrade Flow ===" , TAG )
289+ OpenIapLog .d(" - Old Token: ${androidArgs.purchaseTokenAndroid.take(10 )} ..." , TAG )
290+ OpenIapLog .d(" - Target SKUs: ${androidArgs.skus} " , TAG )
291+ OpenIapLog .d(" - Replacement mode: ${androidArgs.replacementModeAndroid} " , TAG )
292+ OpenIapLog .d(" - Product Details Count: ${paramsList.size} " , TAG )
293+ paramsList.forEachIndexed { index, params ->
294+ OpenIapLog .d(" - Product[$index ]: SKU=${details[index].productId} , offerToken=..." , TAG )
295+ }
296+
278297 val updateParamsBuilder = BillingFlowParams .SubscriptionUpdateParams .newBuilder()
279298 .setOldPurchaseToken(androidArgs.purchaseTokenAndroid)
280- androidArgs.replacementModeAndroid?.let { updateParamsBuilder.setSubscriptionReplacementMode(it) }
281- flowBuilder.setSubscriptionUpdateParams(updateParamsBuilder.build())
299+
300+ // Set replacement mode - this is critical for upgrades
301+ val replacementMode = androidArgs.replacementModeAndroid ? : 5 // Default to CHARGE_FULL_PRICE
302+ updateParamsBuilder.setSubscriptionReplacementMode(replacementMode)
303+ OpenIapLog .d(" - Final replacement mode: $replacementMode " , TAG )
304+
305+ val updateParams = updateParamsBuilder.build()
306+ flowBuilder.setSubscriptionUpdateParams(updateParams)
307+ OpenIapLog .d(" === Subscription Update Params Set ===" , TAG )
308+ } else {
309+ // Only set obfuscatedProfileId for new purchases, not upgrades
310+ androidArgs.obfuscatedProfileId?.let {
311+ OpenIapLog .d(" Setting obfuscatedProfileId for new purchase" , TAG )
312+ flowBuilder.setObfuscatedProfileId(it)
313+ }
282314 }
283315
284316 val result = client.launchBillingFlow(activity, flowBuilder.build())
317+ OpenIapLog .d(" launchBillingFlow result: ${result.responseCode} - ${result.debugMessage} " , TAG )
285318 if (result.responseCode != BillingClient .BillingResponseCode .OK ) {
286- val err = OpenIapError .PurchaseFailed
319+ val err = when (result.responseCode) {
320+ BillingClient .BillingResponseCode .DEVELOPER_ERROR -> {
321+ OpenIapLog .w(" DEVELOPER_ERROR: Invalid arguments. Check if subscriptions are in the same group." , TAG )
322+ OpenIapError .PurchaseFailed
323+ }
324+ BillingClient .BillingResponseCode .USER_CANCELED -> OpenIapError .UserCancelled
325+ else -> OpenIapError .PurchaseFailed
326+ }
287327 purchaseErrorListeners.forEach { listener -> runCatching { listener.onPurchaseError(err) } }
288328 currentPurchaseCallback?.invoke(Result .success(emptyList()))
289329 }
0 commit comments