+
+ Debugging & Logging
+
+
+ Enable verbose logging to see internal operations, warnings, and debug
+ information. This is especially useful during development to diagnose
+ issues and understand library behavior.
+
+
+
+ Enable Logging
+
+
+ Logging is disabled by default in production. Enable
+ it only during development to see detailed logs.
+
+
+
+ {{
+ ios: (
+ {`// Enable logging for debug builds only
+#if DEBUG
+OpenIapLog.enable(true)
+#endif
+
+// Or enable unconditionally
+OpenIapLog.enable(true)
+
+// Disable logging
+OpenIapLog.enable(false)`}
+ ),
+ android: (
+ {`// Enable logging for debug builds only
+if (BuildConfig.DEBUG) {
+ OpenIapLog.enable(true)
+}
+
+// Or enable unconditionally
+OpenIapLog.enable(true)
+
+// Disable logging
+OpenIapLog.enable(false)`}
+ ),
+ }}
+
+
+
+ Common Warnings
+
+
+ When logging is enabled, you may see warnings about specific
+ scenarios:
+
+
+ Multiple Subscription Offers
+
+
+ Warning:{' '}
+
+ Multiple offers (3) found for premium_subscription, using first
+ basePlanId (may be inaccurate)
+
+
+
+
+ This warning appears when a subscription product has multiple offers
+ (e.g., monthly, annual, promotional). Due to Google Play Billing
+ Library limitations, the Purchase object doesn't expose
+ which specific offer was purchased. The library uses the first offer's{' '}
+ basePlanId as a best-effort approach.
+
+
+
+ Impact: The currentPlanId field in{' '}
+ PurchaseAndroid and basePlanIdAndroid in{' '}
+ ActiveSubscription may be inaccurate if users purchase
+ different offers.
+
+
+
+ Solutions:
+
+
+ -
+ Backend Validation (Recommended): Use Google Play
+ Developer API's{' '}
+
purchases.subscriptionsv2:get endpoint with the{' '}
+ purchaseToken to get accurate{' '}
+ basePlanId and offerId
+
+ -
+ Single Offer: Design your subscription products
+ with a single offer per product (most common approach)
+
+ -
+ Offer Tags: Use offer tags in Google Play Console
+ to help identify offers, though this doesn't solve the client-side
+ tracking issue
+
+
+
+ {`// Example: Backend validation to get accurate basePlanId
+// GET https://androidpublisher.googleapis.com/androidpublisher/v3/
+// applications/{packageName}/purchases/subscriptionsv2/tokens/{token}
+//
+// Response includes:
+// {
+// "lineItems": [{
+// "offerDetails": {
+// "basePlanId": "premium-annual", // Accurate!
+// "offerId": "intro-offer"
+// }
+// }]
+// }`}
+
+
+
+ Note: This limitation only affects products with
+ multiple offers. If your subscription has a single offer (the most
+ common case), the basePlanId will always be accurate.
+
+
+
);
}
diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt
index c64b7533..ef178ca7 100644
--- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt
+++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/OpenIapModule.kt
@@ -713,9 +713,21 @@ class OpenIapModule(
BillingClient.ProductType.INAPP
}
}
- OpenIapLog.d("Mapping purchase products=${purchase.products} to type=$type (cached=${cachedProduct != null})", TAG)
- val converted = purchase.toPurchase()
+ // Extract basePlanId from ProductDetails for subscriptions
+ val basePlanId = if (type == BillingClient.ProductType.SUBS) {
+ val offers = cachedProduct?.subscriptionOfferDetails.orEmpty()
+ if (offers.size > 1) {
+ OpenIapLog.w("Multiple offers (${offers.size}) found for ${firstProductId}, using first basePlanId (may be inaccurate)", TAG)
+ }
+ offers.firstOrNull()?.basePlanId
+ } else {
+ null
+ }
+
+ OpenIapLog.d("Mapping purchase products=${purchase.products} to type=$type basePlanId=$basePlanId (cached=${cachedProduct != null})", TAG)
+
+ val converted = purchase.toPurchase(basePlanId)
OpenIapLog.d("Converted purchase: productId=${converted.productId}, acknowledged=${purchase.isAcknowledged()}", TAG)
converted
}
diff --git a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/utils/BillingConverters.kt b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/utils/BillingConverters.kt
index be9cfb94..f825d110 100644
--- a/packages/google/openiap/src/horizon/java/dev/hyo/openiap/utils/BillingConverters.kt
+++ b/packages/google/openiap/src/horizon/java/dev/hyo/openiap/utils/BillingConverters.kt
@@ -95,13 +95,14 @@ internal object HorizonBillingConverters {
)
}
- fun HorizonPurchase.toPurchase(): PurchaseAndroid {
+ fun HorizonPurchase.toPurchase(basePlanId: String? = null): PurchaseAndroid {
val token = purchaseToken
val productsList = products ?: emptyList()
val state = PurchaseState.fromHorizonState(getPurchaseState())
return PurchaseAndroid(
autoRenewingAndroid = isAutoRenewing(),
+ currentPlanId = basePlanId,
dataAndroid = originalJson,
developerPayloadAndroid = developerPayload,
id = orderId ?: token,
@@ -124,20 +125,26 @@ internal object HorizonBillingConverters {
fun HorizonPurchase.toActiveSubscription(): ActiveSubscription = ActiveSubscription(
autoRenewingAndroid = isAutoRenewing(),
+ basePlanIdAndroid = null,
+ currentPlanId = null,
isActive = true,
productId = products?.firstOrNull().orEmpty(),
purchaseToken = purchaseToken,
+ purchaseTokenAndroid = purchaseToken,
transactionDate = (purchaseTime ?: 0L).toDouble(),
transactionId = orderId ?: purchaseToken
)
fun PurchaseAndroid.toActiveSubscription(): ActiveSubscription = ActiveSubscription(
autoRenewingAndroid = autoRenewingAndroid,
+ basePlanIdAndroid = currentPlanId,
+ currentPlanId = currentPlanId,
isActive = true,
productId = productId,
- purchaseToken = purchaseToken.orEmpty(),
+ purchaseToken = purchaseToken,
+ purchaseTokenAndroid = purchaseToken,
transactionDate = transactionDate,
- transactionId = transactionId.orEmpty()
+ transactionId = id
)
}
diff --git a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
index e3a3406f..9860ea6c 100644
--- a/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
+++ b/packages/google/openiap/src/play/java/dev/hyo/openiap/OpenIapModule.kt
@@ -4,7 +4,6 @@ import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
-import android.util.Log
import com.android.billingclient.api.AcknowledgePurchaseParams
import com.android.billingclient.api.BillingClient
import com.android.billingclient.api.BillingClientStateListener
@@ -213,7 +212,23 @@ class OpenIapModule(
} else {
androidPurchases.filter { it.productId in ids }
}
- filtered.map { it.toActiveSubscription() }
+
+ // Enrich purchases with basePlanId from ProductDetails cache
+ filtered.map { purchase ->
+ val productDetails = productManager.get(purchase.productId)
+ val offers = productDetails?.subscriptionOfferDetails.orEmpty()
+ if (offers.size > 1) {
+ OpenIapLog.w("Multiple offers (${offers.size}) found for ${purchase.productId}, using first basePlanId (may be inaccurate)", TAG)
+ }
+ val basePlanId = offers.firstOrNull()?.basePlanId
+
+ // If basePlanId is available and not already set, update the purchase
+ if (basePlanId != null && purchase.currentPlanId == null) {
+ purchase.copy(currentPlanId = basePlanId).toActiveSubscription()
+ } else {
+ purchase.toActiveSubscription()
+ }
+ }
}
}
@@ -841,11 +856,11 @@ class OpenIapModule(
}
override fun onPurchasesUpdated(billingResult: BillingResult, purchases: List