@@ -48,11 +48,14 @@ import dev.hyo.openiap.SubscriptionPurchaseUpdatedHandler
4848import dev.hyo.openiap.SubscriptionSubscriptionBillingIssueHandler
4949import dev.hyo.openiap.VerifyPurchaseProps
5050import dev.hyo.openiap.helpers.AndroidPurchaseArgs
51+ import dev.hyo.openiap.helpers.SubscriptionBasePlanOffer
5152import dev.hyo.openiap.helpers.onPurchaseError
5253import dev.hyo.openiap.helpers.onPurchaseUpdated
5354import dev.hyo.openiap.helpers.onSubscriptionBillingIssue
55+ import dev.hyo.openiap.helpers.queryAlreadyOwnedPurchases
5456import dev.hyo.openiap.helpers.queryProductDetails
5557import dev.hyo.openiap.helpers.queryPurchases
58+ import dev.hyo.openiap.helpers.resolveBasePlanIdForOfferToken
5659import dev.hyo.openiap.helpers.resumeGuard
5760import dev.hyo.openiap.helpers.restorePurchases as restorePurchasesHelper
5861import dev.hyo.openiap.helpers.toAndroidPurchaseArgs
@@ -107,10 +110,10 @@ class OpenIapModule(
107110 private val gson = Gson ()
108111 private val fallbackActivity: Activity ? = if (context is Activity ) context else null
109112
110- private val purchaseUpdateListeners = mutableSetOf <OpenIapPurchaseUpdateListener >()
111- private val purchaseErrorListeners = mutableSetOf <OpenIapPurchaseErrorListener >()
112- private val userChoiceBillingListeners = mutableSetOf <OpenIapUserChoiceBillingListener >()
113- private val developerProvidedBillingListeners = mutableSetOf <OpenIapDeveloperProvidedBillingListener >()
113+ private val purchaseUpdateListeners = java.util.concurrent. CopyOnWriteArraySet <OpenIapPurchaseUpdateListener >()
114+ private val purchaseErrorListeners = java.util.concurrent. CopyOnWriteArraySet <OpenIapPurchaseErrorListener >()
115+ private val userChoiceBillingListeners = java.util.concurrent. CopyOnWriteArraySet <OpenIapUserChoiceBillingListener >()
116+ private val developerProvidedBillingListeners = java.util.concurrent. CopyOnWriteArraySet <OpenIapDeveloperProvidedBillingListener >()
114117 // Thread-safe: listeners can be added/removed on the main thread while
115118 // notifySuspendedSubscriptions iterates from Dispatchers.IO.
116119 private val subscriptionBillingIssueListeners =
@@ -901,6 +904,8 @@ class OpenIapModule(
901904 return @withContext emptyList()
902905 }
903906
907+ val desiredType = if (androidArgs.type == ProductQueryType .Subs ) BillingClient .ProductType .SUBS else BillingClient .ProductType .INAPP
908+
904909 suspendCancellableCoroutine<List <Purchase >> { continuation ->
905910 var callbackRef: ((Result <List <Purchase >>) -> Unit )? = null
906911 val resumer = continuation.resumeGuard {
@@ -922,8 +927,6 @@ class OpenIapModule(
922927 return @suspendCancellableCoroutine
923928 }
924929
925- val desiredType = if (androidArgs.type == ProductQueryType .Subs ) BillingClient .ProductType .SUBS else BillingClient .ProductType .INAPP
926-
927930 val detailsBySku = mutableMapOf<String , ProductDetails >()
928931 for (sku in androidArgs.skus) {
929932 productManager.get(sku)?.takeIf { it.productType == desiredType }?.let { detailsBySku[sku] = it }
@@ -1093,6 +1096,51 @@ class OpenIapModule(
10931096 val result = client.launchBillingFlow(activity, flowBuilder.build())
10941097 OpenIapLog .d(" launchBillingFlow result: ${result.responseCode} - ${result.debugMessage} " , TAG )
10951098 if (result.responseCode != BillingClient .BillingResponseCode .OK ) {
1099+ if (result.responseCode == BillingClient .BillingResponseCode .ITEM_ALREADY_OWNED ) {
1100+ val err = OpenIapError .fromBillingResponseCode(
1101+ result.responseCode,
1102+ result.debugMessage
1103+ )
1104+ OpenIapLog .d(" ITEM_ALREADY_OWNED received; querying owned purchases for ${androidArgs.skus} " , TAG )
1105+ val basePlanIdsBySku = if (desiredType == BillingClient .ProductType .SUBS ) {
1106+ details.associate { productDetails ->
1107+ val requestedOfferToken = androidArgs.subscriptionOffers
1108+ ?.find { it.sku == productDetails.productId }
1109+ ?.offerToken
1110+ val offers = productDetails.subscriptionOfferDetails
1111+ .orEmpty()
1112+ .map { offer ->
1113+ SubscriptionBasePlanOffer (
1114+ offerToken = offer.offerToken,
1115+ basePlanId = offer.basePlanId
1116+ )
1117+ }
1118+ productDetails.productId to resolveBasePlanIdForOfferToken(
1119+ offers,
1120+ requestedOfferToken
1121+ )
1122+ }
1123+ } else {
1124+ emptyMap()
1125+ }
1126+ queryAlreadyOwnedPurchases(client, desiredType, androidArgs.skus, basePlanIdsBySku) { recovered ->
1127+ if (recovered.isNotEmpty()) {
1128+ OpenIapLog .d(" Recovered ${recovered.size} already-owned purchase(s)" , TAG )
1129+ notifySuspendedSubscriptions(recovered)
1130+ for (purchase in recovered) {
1131+ for (listener in purchaseUpdateListeners) {
1132+ runCatching { listener.onPurchaseUpdated(purchase) }
1133+ }
1134+ }
1135+ consumePurchaseCallback(Result .success(recovered))
1136+ } else {
1137+ OpenIapLog .w(" ITEM_ALREADY_OWNED recovery found no matching owned purchases" , TAG )
1138+ for (listener in purchaseErrorListeners) { runCatching { listener.onPurchaseError(err) } }
1139+ consumePurchaseCallback(Result .success(emptyList()))
1140+ }
1141+ }
1142+ return
1143+ }
10961144 val err = when (result.responseCode) {
10971145 BillingClient .BillingResponseCode .DEVELOPER_ERROR -> {
10981146 OpenIapLog .w(" DEVELOPER_ERROR: Invalid arguments. Check if subscriptions are in the same group." , TAG )
0 commit comments