diff --git a/android/src/main/java/com/dooboolab/flutterinapppurchase/AmazonInappPurchasePlugin.java b/android/src/main/java/com/dooboolab/flutterinapppurchase/AmazonInappPurchasePlugin.java index 64cbae7f..c564c0ee 100644 --- a/android/src/main/java/com/dooboolab/flutterinapppurchase/AmazonInappPurchasePlugin.java +++ b/android/src/main/java/com/dooboolab/flutterinapppurchase/AmazonInappPurchasePlugin.java @@ -4,32 +4,28 @@ import android.content.Context; import android.util.Log; +import androidx.annotation.NonNull; + import com.amazon.device.iap.PurchasingListener; import com.amazon.device.iap.PurchasingService; import com.amazon.device.iap.model.FulfillmentResult; import com.amazon.device.iap.model.Product; import com.amazon.device.iap.model.ProductDataResponse; -import com.amazon.device.iap.model.ProductType; import com.amazon.device.iap.model.PurchaseResponse; import com.amazon.device.iap.model.PurchaseUpdatesResponse; import com.amazon.device.iap.model.Receipt; import com.amazon.device.iap.model.RequestId; import com.amazon.device.iap.model.UserDataResponse; -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - import java.text.NumberFormat; import java.text.ParseException; import java.util.ArrayList; -import java.util.Date; +import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import io.flutter.plugin.common.BinaryMessenger; import io.flutter.plugin.common.MethodCall; import io.flutter.plugin.common.MethodChannel; import io.flutter.plugin.common.MethodChannel.MethodCallHandler; @@ -57,14 +53,14 @@ public void setChannel(MethodChannel channel) { } @Override - public void onMethodCall(final MethodCall call, final Result result) { + public void onMethodCall(final @NonNull MethodCall call, final @NonNull Result result) { this.result=result; try { PurchasingService.registerListener(context, purchasesUpdatedListener); - } catch (Exception e) { result.error(call.method, "Call endConnection method if you want to start over.", e.getMessage()); } + if (call.method.equals("getPlatformVersion")) { try { result.success("Android " + android.os.Build.VERSION.RELEASE); @@ -78,7 +74,7 @@ public void onMethodCall(final MethodCall call, final Result result) { result.success("Billing client has ended."); } else if (call.method.equals("consumeAllItems")) { // consumable is a separate type in amazon - result.success("no-ops in amazon"); + result.error("AMAZON_NOT_USED","no-ops in amazon",null); } else if (call.method.equals("getItemsByType")) { Log.d(TAG, "getItemsByType"); String type = call.argument("type"); @@ -91,21 +87,27 @@ public void onMethodCall(final MethodCall call, final Result result) { } PurchasingService.getProductData(productSkus); + final ArrayList> list = new ArrayList<>(); + result.success(list); } else if (call.method.equals("getAvailableItemsByType")) { String type = call.argument("type"); Log.d(TAG, "gaibt="+type); + + final ArrayList> list = new ArrayList<>(); // NOTE: getPurchaseUpdates doesnt return Consumables which are FULFILLED if(type.equals("inapp")) { PurchasingService.getPurchaseUpdates(true); + result.success(list); } else if(type.equals("subs")) { // Subscriptions are retrieved during inapp, so we just return empty list - result.success("[]"); + result.success(list); } else { result.notImplemented(); } } else if (call.method.equals("getPurchaseHistoryByType")) { + final ArrayList> list = new ArrayList<>(); // No equivalent - result.success("[]"); + result.success(list); } else if (call.method.equals("buyItemByType")) { final String type = call.argument("type"); final String obfuscatedAccountId = call.argument("obfuscatedAccountId"); @@ -113,12 +115,15 @@ public void onMethodCall(final MethodCall call, final Result result) { final String sku = call.argument("sku"); final String oldSku = call.argument("oldSku"); final int prorationMode = call.argument("prorationMode"); + Log.d(TAG, "type="+type+"||sku="+sku+"||oldsku="+oldSku); final RequestId requestId = PurchasingService.purchase(sku); Log.d(TAG, "resid="+requestId.toString()); + + result.success(null); } else if (call.method.equals("consumeProduct")) { // consumable is a separate type in amazon - result.success("no-ops in amazon"); + result.error("AMAZON_NOT_USED","no-ops in amazon",null); } else { result.notImplemented(); } @@ -147,49 +152,27 @@ public void onProductDataResponse(ProductDataResponse response) { final Set unavailableSkus = response.getUnavailableSkus(); Log.d(TAG, "onProductDataResponse: " + unavailableSkus.size() + " unavailable skus"); Log.d(TAG, "unavailableSkus="+unavailableSkus.toString()); - JSONArray items = new JSONArray(); - try { - for (Map.Entry skuDetails : productData.entrySet()) { - Product product=skuDetails.getValue(); - NumberFormat format = NumberFormat.getCurrencyInstance(); - - Number number; - try { - number = format.parse(product.getPrice()); - } catch (ParseException e) { - result.error(TAG, "Price Parsing error", e.getMessage()); - return; - } - JSONObject item = new JSONObject(); - item.put("productId", product.getSku()); - item.put("price", number.toString()); - item.put("currency", null); - ProductType productType = product.getProductType(); - switch (productType) { - case ENTITLED: - case CONSUMABLE: - item.put("type", "inapp"); - break; - case SUBSCRIPTION: - item.put("type", "subs"); - break; - } - item.put("localizedPrice", product.getPrice()); - item.put("title", product.getTitle()); - item.put("description", product.getDescription()); - item.put("introductoryPrice", ""); - item.put("subscriptionPeriodAndroid", ""); - item.put("freeTrialPeriodAndroid", ""); - item.put("introductoryPriceCyclesAndroid", 0); - item.put("introductoryPricePeriodAndroid", ""); - Log.d(TAG, "opdr Putting "+item.toString()); - items.put(item); + ArrayList> items = new ArrayList<>(); + + for (Map.Entry skuDetails : productData.entrySet()) { + Product product=skuDetails.getValue(); + NumberFormat format = NumberFormat.getCurrencyInstance(); + + Number number; + try { + number = format.parse(product.getPrice()); + } catch (ParseException e) { + result.error(TAG, "Price Parsing error", e.getMessage()); + return; } - //System.err.println("Sending "+items.toString()); - result.success(items.toString()); - } catch (JSONException e) { - result.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.getMessage()); + + final HashMap item = FlutterEntitiesBuilder.buildSkuDetailsMap(product); + Log.d(TAG, "opdr Putting "+item.toString()); + items.add(item); } + //System.err.println("Sending "+items.toString()); + result.success(items); + break; case FAILED: result.error(TAG,"FAILED",null); @@ -209,19 +192,12 @@ public void onPurchaseResponse(PurchaseResponse response) { case SUCCESSFUL: Receipt receipt = response.getReceipt(); PurchasingService.notifyFulfillment(receipt.getReceiptId(), FulfillmentResult.FULFILLED); - Date date = receipt.getPurchaseDate(); - Long transactionDate=date.getTime(); - try { - JSONObject item = getPurchaseData(receipt.getSku(), - receipt.getReceiptId(), - receipt.getReceiptId(), - transactionDate.doubleValue()); - Log.d(TAG, "opr Putting "+item.toString()); - result.success(item.toString()); - channel.invokeMethod("purchase-updated", item.toString()); - } catch (JSONException e) { - result.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.getMessage()); - } + + final HashMap item = FlutterEntitiesBuilder.buildPurchaseMap(receipt); + Log.d(TAG, "opr Putting "+item.toString()); + result.success(item); + channel.invokeMethod("purchase-updated", item); + break; case FAILED: result.error(TAG, "buyItemByType", "billingResponse is not ok: " + status); @@ -237,24 +213,16 @@ public void onPurchaseUpdatesResponse(PurchaseUpdatesResponse response) { switch(status) { case SUCCESSFUL: - JSONArray items = new JSONArray(); - try { - List receipts = response.getReceipts(); - for(Receipt receipt : receipts) { - Date date = receipt.getPurchaseDate(); - Long transactionDate=date.getTime(); - JSONObject item = getPurchaseData(receipt.getSku(), - receipt.getReceiptId(), - receipt.getReceiptId(), - transactionDate.doubleValue()); - - Log.d(TAG, "opudr Putting "+item.toString()); - items.put(item); - } - result.success(items.toString()); - } catch (JSONException e) { - result.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.getMessage()); + ArrayList> items = new ArrayList<>(); + + List receipts = response.getReceipts(); + for(Receipt receipt : receipts) { + final HashMap item = FlutterEntitiesBuilder.buildPurchaseMap(receipt); + Log.d(TAG, "opudr Putting "+item.toString()); + items.add(item); } + result.success(items); + break; case FAILED: result.error(TAG,"FAILED",null); @@ -267,16 +235,4 @@ public void onPurchaseUpdatesResponse(PurchaseUpdatesResponse response) { } }; - JSONObject getPurchaseData(String productId, String transactionId, String transactionReceipt, - Double transactionDate) throws JSONException { - JSONObject item = new JSONObject(); - item.put("productId", productId); - item.put("transactionId", transactionId); - item.put("transactionReceipt", transactionReceipt); - item.put("transactionDate", Double.toString(transactionDate)); - item.put("dataAndroid",null); - item.put("signatureAndroid",null); - item.put("purchaseToken",null); - return item; - } } diff --git a/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java b/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java index 57f37f4f..eb8258fb 100644 --- a/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java +++ b/android/src/main/java/com/dooboolab/flutterinapppurchase/AndroidInappPurchasePlugin.java @@ -9,6 +9,7 @@ import android.os.Bundle; import android.util.Log; +import com.android.billingclient.api.AccountIdentifiers; import com.android.billingclient.api.AcknowledgePurchaseParams; import com.android.billingclient.api.AcknowledgePurchaseResponseListener; import com.android.billingclient.api.BillingClient; @@ -241,20 +242,15 @@ public void onSkuDetailsResponse(@NonNull BillingResult billingResult, List> items = new ArrayList<>(); - for (SkuDetails skuDetails : skuDetailsList) { - items.add(buildSkuDetailsMap(skuDetails)); - } - result.success(items); - } catch (FlutterException fe) { - result.error(call.method, fe.getMessage(), fe.getLocalizedMessage()); + + ArrayList> items = new ArrayList<>(); + for (SkuDetails skuDetails : skuDetailsList) { + items.add(FlutterEntitiesBuilder.buildSkuDetailsMap(skuDetails)); } + result.success(items); } }); } @@ -270,23 +266,18 @@ else if (call.method.equals("getAvailableItemsByType")) { } final String type = call.argument("type"); - final Purchase.PurchasesResult purchasesResult = billingClient.queryPurchases(type.equals("subs") ? BillingClient.SkuType.SUBS : BillingClient.SkuType.INAPP); final List purchases = purchasesResult.getPurchasesList(); - try { - if (purchases != null) { - ArrayList> items = new ArrayList<>(); + ArrayList> items = new ArrayList<>(); - for (Purchase purchase : purchases) { - items.add(buildPurchaseMap(purchase)); - } - - result.success(items); + if (purchases != null) { + for (Purchase purchase : purchases) { + items.add(FlutterEntitiesBuilder.buildPurchaseMap(purchase)); } - } catch (FlutterException fe) { - result.error(call.method, fe.getMessage(), fe.getLocalizedMessage()); } + + result.success(items); } /* @@ -307,7 +298,7 @@ public void onPurchaseHistoryResponse(@NonNull BillingResult billingResult, List ArrayList> items = new ArrayList<>(); for (PurchaseHistoryRecord record : purchaseHistoryRecordList) { - items.add(buildPurchaseHistoryRecordMap(record)); + items.add(FlutterEntitiesBuilder.buildPurchaseHistoryRecordMap(record)); } result.success(items); @@ -330,7 +321,6 @@ else if (call.method.equals("buyItemByType")) { final String type = call.argument("type"); final int prorationMode = call.argument("prorationMode"); - final String obfuscatedAccountId = call.argument("obfuscatedAccountId"); final String obfuscatedProfileId = call.argument("obfuscatedProfileId"); final String oldSku = call.argument("oldSku"); @@ -372,6 +362,9 @@ else if (call.method.equals("buyItemByType")) { if (activity != null) { billingClient.launchBillingFlow(activity, flowParams); } + + // Releases async invokeMethod on Flutter side + result.success(null); } /* @@ -397,7 +390,7 @@ public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); result.error(call.method, errorData[0], errorData[1]); } else { - final HashMap resultMap = buildBillingResultMap(billingResult); + final HashMap resultMap = FlutterEntitiesBuilder.buildBillingResultMap(billingResult); result.success(resultMap); } } @@ -426,7 +419,7 @@ public void onConsumeResponse(@NonNull BillingResult billingResult,@NonNull Stri String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); result.error(call.method, errorData[0], errorData[1]); } else{ - final HashMap resultMap = buildBillingResultMap(billingResult); + final HashMap resultMap = FlutterEntitiesBuilder.buildBillingResultMap(billingResult); result.success(resultMap); } } @@ -442,20 +435,20 @@ public void onConsumeResponse(@NonNull BillingResult billingResult,@NonNull Stri public void onPurchasesUpdated(BillingResult billingResult, @Nullable List purchases) { if (billingResult.getResponseCode() != BillingClient.BillingResponseCode.OK) { - final HashMap resultMap = buildBillingResultMap(billingResult); + final HashMap resultMap = FlutterEntitiesBuilder.buildBillingResultMap(billingResult); channel.invokeMethod("purchase-error", resultMap); return; } if (purchases == null){ String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); - final HashMap resultMap = buildBillingResultMap(billingResult,errorData[0],"purchases returns null"); + final HashMap resultMap = FlutterEntitiesBuilder.buildBillingResultMap(billingResult,errorData[0],"purchases returns null"); channel.invokeMethod("purchase-error", resultMap); return; } for (Purchase purchase : purchases) { - channel.invokeMethod("purchase-updated", buildPurchaseMap(purchase)); + channel.invokeMethod("purchase-updated", FlutterEntitiesBuilder.buildPurchaseMap(purchase)); } } }; @@ -471,73 +464,5 @@ private void endBillingClientConnection() { } } - private HashMap buildPurchaseMap(Purchase purchase){ - HashMap map = new HashMap<>(); - // part of PurchaseHistory object - map.put("productId", purchase.getSku()); - map.put("signatureAndroid", purchase.getSignature()); - map.put("purchaseToken", purchase.getPurchaseToken()); - map.put("transactionDate", purchase.getPurchaseTime()); - map.put("transactionReceipt", purchase.getOriginalJson()); - - // additional fields for purchase - map.put("orderId", purchase.getOrderId()); - map.put("transactionId", purchase.getOrderId()); - map.put("autoRenewingAndroid", purchase.isAutoRenewing()); - map.put("isAcknowledgedAndroid", purchase.isAcknowledged()); - map.put("purchaseStateAndroid", purchase.getPurchaseState()); - - return map; - } - - private HashMap buildPurchaseHistoryRecordMap(PurchaseHistoryRecord record){ - HashMap map = new HashMap<>(); - - map.put("productId", record.getSku()); - map.put("signatureAndroid", record.getSignature()); - map.put("purchaseToken", record.getPurchaseToken()); - map.put("transactionDate", record.getPurchaseTime()); - map.put("transactionReceipt", record.getOriginalJson()); - - return map; - } - - private HashMap buildSkuDetailsMap(SkuDetails skuDetails){ - HashMap map = new HashMap<>(); - - map.put("productId", skuDetails.getSku()); - map.put("price", String.valueOf(skuDetails.getPriceAmountMicros() / 1000000f)); - map.put("currency", skuDetails.getPriceCurrencyCode()); - map.put("type", skuDetails.getType()); - map.put("localizedPrice", skuDetails.getPrice()); - map.put("title", skuDetails.getTitle()); - map.put("description", skuDetails.getDescription()); - map.put("introductoryPrice", skuDetails.getIntroductoryPrice()); - map.put("subscriptionPeriodAndroid", skuDetails.getSubscriptionPeriod()); - map.put("freeTrialPeriodAndroid", skuDetails.getFreeTrialPeriod()); - map.put("introductoryPriceCyclesAndroid", skuDetails.getIntroductoryPriceCycles()); - map.put("introductoryPricePeriodAndroid", skuDetails.getIntroductoryPricePeriod()); - map.put("iconUrl", skuDetails.getIconUrl()); - map.put("originalJson", skuDetails.getOriginalJson()); - map.put("originalPrice", skuDetails.getOriginalPriceAmountMicros() / 1000000f); - - return map; - } - - private HashMap buildBillingResultMap(BillingResult billingResult){ - String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); - return buildBillingResultMap(billingResult, errorData[0], errorData[1]); - } - - private HashMap buildBillingResultMap(BillingResult billingResult, String errorCode, String message){ - HashMap map = new HashMap<>(); - - map.put("responseCode", billingResult.getResponseCode()); - map.put("debugMessage", billingResult.getDebugMessage()); - map.put("message", message); - map.put("code", errorCode); - - return map; - } } diff --git a/android/src/main/java/com/dooboolab/flutterinapppurchase/FlutterEntitiesBuilder.java b/android/src/main/java/com/dooboolab/flutterinapppurchase/FlutterEntitiesBuilder.java new file mode 100644 index 00000000..4744a0f1 --- /dev/null +++ b/android/src/main/java/com/dooboolab/flutterinapppurchase/FlutterEntitiesBuilder.java @@ -0,0 +1,135 @@ +package com.dooboolab.flutterinapppurchase; + +import com.amazon.device.iap.model.Product; +import com.amazon.device.iap.model.ProductType; +import com.amazon.device.iap.model.Receipt; +import com.android.billingclient.api.AccountIdentifiers; +import com.android.billingclient.api.BillingResult; +import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.PurchaseHistoryRecord; +import com.android.billingclient.api.SkuDetails; + +import java.util.HashMap; + +class FlutterEntitiesBuilder { + static public HashMap buildPurchaseMap(Purchase purchase){ + HashMap map = new HashMap<>(); + + // part of PurchaseHistory object + map.put("productId", purchase.getSku()); + map.put("signatureAndroid", purchase.getSignature()); + map.put("purchaseToken", purchase.getPurchaseToken()); + map.put("transactionDate", purchase.getPurchaseTime()); + map.put("transactionReceipt", purchase.getOriginalJson()); + + // additional fields for purchase + map.put("orderId", purchase.getOrderId()); + map.put("transactionId", purchase.getOrderId()); + map.put("autoRenewingAndroid", purchase.isAutoRenewing()); + map.put("isAcknowledgedAndroid", purchase.isAcknowledged()); + map.put("purchaseStateAndroid", purchase.getPurchaseState()); + + final AccountIdentifiers identifiers = purchase.getAccountIdentifiers(); + if(identifiers!=null){ + map.put("obfuscatedAccountId", identifiers.getObfuscatedAccountId()); + map.put("obfuscatedProfileId", identifiers.getObfuscatedProfileId()); + } + + + return map; + } + + // Amazon + static public HashMap buildPurchaseMap(Receipt receipt){ + HashMap map = new HashMap<>(); + + // part of PurchaseHistory object + map.put("productId", receipt.getSku()); + map.put("transactionDate", receipt.getPurchaseDate().getTime()); + map.put("transactionReceipt", receipt.getReceiptId()); + map.put("transactionId", receipt.getReceiptId()); + + + return map; + } + + static public HashMap buildPurchaseHistoryRecordMap(PurchaseHistoryRecord record){ + HashMap map = new HashMap<>(); + + map.put("productId", record.getSku()); + map.put("signatureAndroid", record.getSignature()); + map.put("purchaseToken", record.getPurchaseToken()); + map.put("transactionDate", record.getPurchaseTime()); + map.put("transactionReceipt", record.getOriginalJson()); + + return map; + } + + static public HashMap buildSkuDetailsMap(SkuDetails skuDetails){ + HashMap map = new HashMap<>(); + + map.put("productId", skuDetails.getSku()); + map.put("price", String.valueOf(skuDetails.getPriceAmountMicros() / 1000000f)); + map.put("currency", skuDetails.getPriceCurrencyCode()); + map.put("type", skuDetails.getType()); + map.put("localizedPrice", skuDetails.getPrice()); + map.put("title", skuDetails.getTitle()); + map.put("description", skuDetails.getDescription()); + map.put("introductoryPrice", skuDetails.getIntroductoryPrice()); + map.put("subscriptionPeriodAndroid", skuDetails.getSubscriptionPeriod()); + map.put("freeTrialPeriodAndroid", skuDetails.getFreeTrialPeriod()); + map.put("introductoryPriceCyclesAndroid", skuDetails.getIntroductoryPriceCycles()); + map.put("introductoryPricePeriodAndroid", skuDetails.getIntroductoryPricePeriod()); + map.put("iconUrl", skuDetails.getIconUrl()); + map.put("originalJson", skuDetails.getOriginalJson()); + map.put("originalPrice", skuDetails.getOriginalPriceAmountMicros() / 1000000f); + + return map; + } + + // Amazon + static public HashMap buildSkuDetailsMap(Product amazonProduct){ + HashMap map = new HashMap<>(); + + map.put("productId", amazonProduct.getSku()); + map.put("price", amazonProduct.toString()); + map.put("currency", null); + map.put("localizedPrice", amazonProduct.getPrice()); + map.put("title", amazonProduct.getTitle()); + map.put("description", amazonProduct.getDescription()); + map.put("introductoryPrice", ""); + map.put("subscriptionPeriodAndroid", ""); + map.put("freeTrialPeriodAndroid", ""); + map.put("introductoryPriceCyclesAndroid", 0); + map.put("introductoryPricePeriodAndroid", ""); + + ProductType productType = amazonProduct.getProductType(); + switch (productType) { + case ENTITLED: + case CONSUMABLE: + map.put("type", "inapp"); + break; + case SUBSCRIPTION: + map.put("type", "subs"); + break; + } + + return map; + } + + static public HashMap buildBillingResultMap(BillingResult billingResult){ + String[] errorData = DoobooUtils.getInstance().getBillingResponseData(billingResult.getResponseCode()); + return buildBillingResultMap(billingResult, errorData[0], errorData[1]); + } + + static public HashMap buildBillingResultMap(BillingResult billingResult, String errorCode, String message){ + HashMap map = new HashMap<>(); + + map.put("responseCode", billingResult.getResponseCode()); + map.put("debugMessage", billingResult.getDebugMessage()); + map.put("message", message); + map.put("code", errorCode); + + return map; + } +}