diff --git a/node_modules/@capgo/native-purchases/android/src/main/java/ee/forgr/nativepurchases/NativePurchasesPlugin.java b/node_modules/@capgo/native-purchases/android/src/main/java/ee/forgr/nativepurchases/NativePurchasesPlugin.java index 49189fb..9452551 100644 --- a/node_modules/@capgo/native-purchases/android/src/main/java/ee/forgr/nativepurchases/NativePurchasesPlugin.java +++ b/node_modules/@capgo/native-purchases/android/src/main/java/ee/forgr/nativepurchases/NativePurchasesPlugin.java @@ -152,6 +152,33 @@ public class NativePurchasesPlugin extends Plugin { return purchaseDateFormat.format(new java.util.Date(timeMillis)); } + // BANA-X SmartCents patch (D-159): additive. Every subscription offer of the product query also carries Google's own + // fields from ProductDetails.SubscriptionOfferDetails: basePlanId, offerId (null for the base plan), offerTags and + // every PricingPhase exactly as Google returns it. No existing field changes. + private static void putGoogleOfferFields(JSObject product, ProductDetails.SubscriptionOfferDetails offerDetails) { + product.put("basePlanId", offerDetails.getBasePlanId()); + product.put("offerId", offerDetails.getOfferId() == null ? org.json.JSONObject.NULL : offerDetails.getOfferId()); + JSONArray offerTags = new JSONArray(); + for (String tag : offerDetails.getOfferTags()) { + offerTags.put(tag); + } + product.put("offerTags", offerTags); + JSONArray pricingPhases = new JSONArray(); + if (offerDetails.getPricingPhases() != null) { + for (ProductDetails.PricingPhase phase : offerDetails.getPricingPhases().getPricingPhaseList()) { + JSObject pricingPhase = new JSObject(); + pricingPhase.put("billingPeriod", phase.getBillingPeriod()); + pricingPhase.put("priceAmountMicros", phase.getPriceAmountMicros()); + pricingPhase.put("formattedPrice", phase.getFormattedPrice()); + pricingPhase.put("priceCurrencyCode", phase.getPriceCurrencyCode()); + pricingPhase.put("recurrenceMode", phase.getRecurrenceMode()); + pricingPhase.put("billingCycleCount", phase.getBillingCycleCount()); + pricingPhases.put(pricingPhase); + } + } + product.put("pricingPhases", pricingPhases); + } + private JSObject buildPurchaseTransactionObject(Purchase purchase, String productType) { AccountIdentifiers accountIdentifiers = purchase.getAccountIdentifiers(); String purchaseAccountId = accountIdentifiers != null ? accountIdentifiers.getObfuscatedAccountId() : null; @@ -812,24 +839,38 @@ public class NativePurchasesPlugin extends Plugin { BillingFlowParams.ProductDetailsParams.newBuilder().setProductDetails(productDetailsItem); if (productType.equals("subs")) { NativePurchasesLog.d(TAG, "Processing subscription product"); - // list the SubscriptionOfferDetails and find the one who match the planIdentifier if not found get the first one + // BANA-X SmartCents patch (D-142.3): the offer must match planIdentifier AND, when given, the caller's + // offerToken; no match is a rejection, never a fallback to another offer or base plan. ProductDetails.SubscriptionOfferDetails selectedOfferDetails = null; assert productDetailsItem.getSubscriptionOfferDetails() != null; NativePurchasesLog.d( TAG, "Available offer details count: " + productDetailsItem.getSubscriptionOfferDetails().size() ); + boolean offerTokenGiven = offerToken != null && !offerToken.isEmpty(); for (ProductDetails.SubscriptionOfferDetails offerDetails : productDetailsItem.getSubscriptionOfferDetails()) { NativePurchasesLog.d(TAG, "Checking offer: " + offerDetails.getBasePlanId()); - if (offerDetails.getBasePlanId().equals(planIdentifier)) { - selectedOfferDetails = offerDetails; - NativePurchasesLog.d(TAG, "Found matching plan: " + planIdentifier); - break; + if (!offerDetails.getBasePlanId().equals(planIdentifier)) { + continue; } + if (offerTokenGiven && !offerToken.equals(offerDetails.getOfferToken())) { + continue; + } + selectedOfferDetails = offerDetails; + NativePurchasesLog.d(TAG, "Found matching plan: " + planIdentifier); + break; } if (selectedOfferDetails == null) { - selectedOfferDetails = productDetailsItem.getSubscriptionOfferDetails().get(0); - NativePurchasesLog.d(TAG, "Using first available offer: " + selectedOfferDetails.getBasePlanId()); + String code = offerTokenGiven ? "OFFER_NOT_FOUND" : "PLAN_NOT_FOUND"; + NativePurchasesLog.d(TAG, "No subscription offer matches: " + code); + closeBillingClient(); + call.reject( + offerTokenGiven + ? "Offer token not found for plan: " + planIdentifier + : "Plan not found for product: " + productIdentifier, + code + ); + return; } productDetailsParams.setOfferToken(selectedOfferDetails.getOfferToken()); NativePurchasesLog.d( @@ -1074,6 +1115,7 @@ public class NativePurchasesPlugin extends Plugin { product.put("offerToken", selectedOfferDetails.getOfferToken()); product.put("offerId", selectedOfferDetails.getOfferId()); ProductPayloadMapper.applySubscriptionPricing(product, selectedOfferDetails, offerDetailsList); + putGoogleOfferFields(product, selectedOfferDetails); double price = product.optDouble("price", 0.0); NativePurchasesLog.d(TAG, "Plan identifier: " + productDetails.getProductId()); NativePurchasesLog.d(TAG, "Base plan ID: " + selectedOfferDetails.getBasePlanId()); @@ -1223,6 +1265,7 @@ public class NativePurchasesPlugin extends Plugin { product.put("isFamilyShareable", false); ProductPayloadMapper.applySubscriptionPricing(product, offerDetails, offerDetailsList); + putGoogleOfferFields(product, offerDetails); double price = product.optDouble("price", 0.0); NativePurchasesLog.d(TAG, "Plan identifier: " + productDetails.getProductId());