@@ -150,33 +150,44 @@ class OpenIapModule(private val context: Context) : OpenIapProtocol, PurchasesUp
150150 return @suspendCancellableCoroutine
151151 }
152152
153- // Prefer cached details if available
154- val cachedDetails = request.skus
155- .mapNotNull { sku -> productManager.get(sku) }
156- .firstOrNull { it.productType == desiredType }
157-
158- val useDetails: (ProductDetails ) -> Unit = details@{ productDetails ->
159- Log .d(TAG , " Using ProductDetails: sku=${productDetails.productId} , title=${productDetails.title} , type=$type " )
160- val pdParamsBuilder = BillingFlowParams .ProductDetailsParams .newBuilder()
161- .setProductDetails(productDetails)
162- if (type == ProductRequest .ProductRequestType .SUBS ) {
163- val offerToken = productDetails.subscriptionOfferDetails
164- ?.firstOrNull()
165- ?.offerToken
166- if (offerToken.isNullOrEmpty()) {
167- Log .w(TAG , " No subscription offer available for ${request.skus} " )
168- val err = OpenIapError .SkuOfferMismatch
169- purchaseErrorListeners.forEach { runCatching { it.onPurchaseError(err) } }
170- currentPurchaseCallback?.invoke(Result .success(emptyList()))
171- return @details
153+ // Validate SKU list
154+ if (request.skus.isEmpty()) {
155+ val err = OpenIapError .EmptySkuList
156+ purchaseErrorListeners.forEach { runCatching { it.onPurchaseError(err) } }
157+ currentPurchaseCallback?.invoke(Result .success(emptyList()))
158+ return @suspendCancellableCoroutine
159+ }
160+
161+ // Resolve details from cache first
162+ val detailsBySku = mutableMapOf<String , ProductDetails >()
163+ request.skus.forEach { sku ->
164+ val d = productManager.get(sku)
165+ if (d != null && d.productType == desiredType) detailsBySku[sku] = d
166+ }
167+ val missingSkus = request.skus.filter { ! detailsBySku.containsKey(it) }
168+
169+ fun launchBillingWith (detailsList : List <ProductDetails >) {
170+ // Build params for all requested SKUs in order
171+ val paramsList = mutableListOf<BillingFlowParams .ProductDetailsParams >()
172+ for (pd in detailsList) {
173+ val builder = BillingFlowParams .ProductDetailsParams .newBuilder()
174+ .setProductDetails(pd)
175+ 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} " )
179+ val err = OpenIapError .SkuOfferMismatch
180+ purchaseErrorListeners.forEach { runCatching { it.onPurchaseError(err) } }
181+ currentPurchaseCallback?.invoke(Result .success(emptyList()))
182+ return
183+ }
184+ builder.setOfferToken(offerToken)
172185 }
173- Log .d(TAG , " Using offerToken=$offerToken for SUBS purchase" )
174- pdParamsBuilder.setOfferToken(offerToken)
186+ paramsList.add(builder.build())
175187 }
176- val productDetailsParamsList = listOf (pdParamsBuilder.build())
177188
178189 val billingFlowParams = BillingFlowParams .newBuilder()
179- .setProductDetailsParamsList(productDetailsParamsList )
190+ .setProductDetailsParamsList(paramsList )
180191 .setIsOfferPersonalized(request.isOfferPersonalized == true )
181192 .apply {
182193 request.obfuscatedAccountIdAndroid?.let { setObfuscatedAccountId(it) }
@@ -193,11 +204,19 @@ class OpenIapModule(private val context: Context) : OpenIapProtocol, PurchasesUp
193204 }
194205 }
195206
196- if (cachedDetails != null ) {
197- useDetails(cachedDetails)
207+ if (missingSkus.isEmpty()) {
208+ val ordered = request.skus.mapNotNull { detailsBySku[it] }
209+ if (ordered.size != request.skus.size) {
210+ val missing = request.skus.firstOrNull { ! detailsBySku.containsKey(it) }
211+ val err = OpenIapError .SkuNotFound (missing ? : " " )
212+ purchaseErrorListeners.forEach { runCatching { it.onPurchaseError(err) } }
213+ currentPurchaseCallback?.invoke(Result .success(emptyList()))
214+ return @suspendCancellableCoroutine
215+ }
216+ launchBillingWith(ordered)
198217 } else {
199- // Query product details , update cache, then launch flow
200- val productList = request.skus .map { sku ->
218+ // Query missing , update cache and then launch
219+ val productList = missingSkus .map { sku ->
201220 QueryProductDetailsParams .Product .newBuilder()
202221 .setProductId(sku)
203222 .setProductType(desiredType)
@@ -212,11 +231,18 @@ class OpenIapModule(private val context: Context) : OpenIapProtocol, PurchasesUp
212231 if (billingResult.responseCode == BillingClient .BillingResponseCode .OK &&
213232 productDetailsList != null && productDetailsList.isNotEmpty()
214233 ) {
215- // Update cache
216234 productManager.putAll(productDetailsList)
217- val productDetails = productDetailsList.first()
218- useDetails(productDetails)
219- // Do not complete here; wait for onPurchasesUpdated to resolve the result
235+ productDetailsList.forEach { detailsBySku[it.productId] = it }
236+ val ordered = request.skus.mapNotNull { detailsBySku[it] }
237+ if (ordered.size != request.skus.size) {
238+ val missing = request.skus.firstOrNull { ! detailsBySku.containsKey(it) }
239+ val err = OpenIapError .SkuNotFound (missing ? : " " )
240+ purchaseErrorListeners.forEach { runCatching { it.onPurchaseError(err) } }
241+ currentPurchaseCallback?.invoke(Result .success(emptyList()))
242+ return @queryProductDetailsAsync
243+ }
244+ launchBillingWith(ordered)
245+ // Do not complete here; wait for onPurchasesUpdated
220246 } else {
221247 Log .w(TAG , " queryProductDetails failed: code=${billingResult.responseCode} msg=${billingResult.debugMessage} " )
222248 val err = OpenIapError .QueryProduct (billingResult.debugMessage ? : " Product not found: ${request.skus} " )
0 commit comments