diff --git a/build.gradle b/build.gradle index 2cf36f3..24d9116 100755 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { gradlePluginPortal() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.2' + classpath 'com.android.tools.build:gradle:4.2.2' classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' classpath 'com.vanniktech:gradle-maven-publish-plugin:0.13.0' } diff --git a/cashier-google-play-billing-debug/build.gradle b/cashier-google-play-billing-debug/build.gradle index 6c21b48..3139714 100644 --- a/cashier-google-play-billing-debug/build.gradle +++ b/cashier-google-play-billing-debug/build.gradle @@ -30,10 +30,7 @@ android { dependencies { api project(':cashier-google-play-billing') - compileOnly deps.autoValue compileOnly deps.supportAnnotations - annotationProcessor deps.autoValue - annotationProcessor deps.autoParcel testImplementation deps.robolectric testImplementation deps.junit diff --git a/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayBillingApi.java b/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayBillingApi.java index 90691f4..67b94d0 100644 --- a/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayBillingApi.java +++ b/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayBillingApi.java @@ -9,7 +9,9 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import com.android.billingclient.api.AcknowledgePurchaseResponseListener; import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.Purchase; import com.android.billingclient.api.SkuDetails; @@ -124,14 +126,14 @@ public void dispose() { @Override public int isBillingSupported(String itemType) { - return BillingClient.BillingResponse.OK; + return BillingClient.BillingResponseCode.OK; } @Override - public void launchBillingFlow(@NonNull Activity activity, @NonNull final String sku, final String itemType) { + public void launchBillingFlow(@NonNull Activity activity, @NonNull final String sku, final String itemType, @Nullable final String developerPayload, @Nullable final String accountId) { for (Product product : testProducts) { if (product.sku().equals(sku)) { - activity.startActivity(FakeGooglePlayCheckoutActivity.intent(activity, product, TEST_PRIVATE_KEY)); + activity.startActivity(FakeGooglePlayCheckoutActivity.intent(activity, product, TEST_PRIVATE_KEY, developerPayload, accountId)); // Put listener to pendingPurchases map and wait until either // notifyPurchaseSuccess or notifyPurchaseError is called from FakeGooglePlayCheckoutActivity @@ -144,13 +146,18 @@ public void onFakePurchaseSuccess(Purchase purchase) { } else { testInappPurchases.add(purchase); } - vendor.onPurchasesUpdated(BillingClient.BillingResponse.OK, Collections.singletonList(purchase)); + vendor.onPurchasesUpdated( + BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.OK) + .build(), Collections.singletonList(purchase)); } @Override public void onFakePurchaseError(int responseCode) { pendingPurchases.remove(sku); - vendor.onPurchasesUpdated(responseCode, null); + vendor.onPurchasesUpdated(BillingResult.newBuilder() + .setResponseCode(responseCode) + .build(), null); } }); return; @@ -200,7 +207,41 @@ public void run() { mainHandler.post(new Runnable() { @Override public void run() { - listener.onConsumeResponse(BillingClient.BillingResponse.OK, purchaseToken); + listener.onConsumeResponse(BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.OK) + .build(), purchaseToken); + } + }); + } + }.start(); + } + + @Override + public void acknowledgePurchase(@NonNull String purchaseToken, @NonNull AcknowledgePurchaseResponseListener listener) { + // Use new thread to simulate network operation + new Thread() { + public void run() { + // Wait 1 second to simulate network operation + try { sleep(1000L); } catch (InterruptedException e) {} + + for (Iterator it = testInappPurchases.iterator(); it.hasNext();) { + if (it.next().getPurchaseToken().equals(purchaseToken)) { + it.remove(); + } + } + for (Iterator it = testSubPurchases.iterator(); it.hasNext();) { + if (it.next().getPurchaseToken().equals(purchaseToken)) { + it.remove(); + } + } + + // Return result on main thread + mainHandler.post(new Runnable() { + @Override + public void run() { + listener.onAcknowledgePurchaseResponse(BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.OK) + .build()); } }); } @@ -229,7 +270,9 @@ public void run() { mainHandler.post(new Runnable() { @Override public void run() { - listener.onSkuDetailsResponse(BillingClient.BillingResponse.OK, details); + listener.onSkuDetailsResponse(BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.OK) + .build(), details); } }); } diff --git a/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayCheckoutActivity.java b/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayCheckoutActivity.java index 3032aeb..91c9944 100644 --- a/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayCheckoutActivity.java +++ b/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayCheckoutActivity.java @@ -22,12 +22,20 @@ public class FakeGooglePlayCheckoutActivity extends Activity { private static final String ARGUMENT_PRODUCT = "product"; + private static final String ARGUMENT_DEV_PAYLOAD = "developer_payload"; + private static final String ARGUMENT_ACCOUNT_ID = "account_id"; private Product product; - public static Intent intent(Context context, Product product, String privateKey64) { + private String developerPayload; + + private String accountId; + + public static Intent intent(Context context, Product product, String developerPayload, String accountId, String privateKey64) { Intent intent = new Intent(context, FakeGooglePlayCheckoutActivity.class); intent.putExtra(ARGUMENT_PRODUCT, product); + intent.putExtra(ARGUMENT_DEV_PAYLOAD, developerPayload); + intent.putExtra(ARGUMENT_ACCOUNT_ID, accountId); return intent; } @@ -38,6 +46,8 @@ protected void onCreate(Bundle savedInstanceState) { final Intent intent = getIntent(); product = intent.getParcelableExtra(ARGUMENT_PRODUCT); + developerPayload = intent.getStringExtra(ARGUMENT_DEV_PAYLOAD); + accountId = intent.getStringExtra(ARGUMENT_ACCOUNT_ID); final TextView productName = bind(R.id.product_name); final TextView productDescription = bind(R.id.product_description); @@ -65,6 +75,8 @@ public void onClick(View v) { purchaseJson.put("purchaseToken", product.sku() + "_" + System.currentTimeMillis()); purchaseJson.put("purchaseState", 0); purchaseJson.put("productId", product.sku()); + purchaseJson.put("developerPayload", developerPayload); + purchaseJson.put("obfuscatedAccountId", accountId); String json = purchaseJson.toString(); String signature = GooglePlayBillingSecurity.sign(FakeGooglePlayBillingApi.TEST_PRIVATE_KEY, json); Purchase purchase = new Purchase(json, signature); @@ -72,7 +84,7 @@ public void onClick(View v) { FakeGooglePlayBillingApi.notifyPurchaseSuccess(product.sku(), purchase); } catch (JSONException e) { - FakeGooglePlayBillingApi.notifyPurchaseError(product.sku(), BillingClient.BillingResponse.SERVICE_UNAVAILABLE); + FakeGooglePlayBillingApi.notifyPurchaseError(product.sku(), BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE); } finish(); } @@ -82,7 +94,7 @@ public void onClick(View v) { @Override public void onBackPressed() { super.onBackPressed(); - FakeGooglePlayBillingApi.notifyPurchaseError(product.sku(), BillingClient.BillingResponse.USER_CANCELED); + FakeGooglePlayBillingApi.notifyPurchaseError(product.sku(), BillingClient.BillingResponseCode.USER_CANCELED); } private SpannableString metadataField(String name, Object value) { diff --git a/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeSkuDetails.java b/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeSkuDetails.java index 182ed53..34c42db 100644 --- a/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeSkuDetails.java +++ b/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeSkuDetails.java @@ -15,7 +15,7 @@ public class FakeSkuDetails extends SkuDetails { private Product product; public FakeSkuDetails(Product product) throws JSONException { - super("{}"); + super("{productId:\""+product.sku()+"\", type:\""+(product.isSubscription() ? "subs" : "inapp")+"\"}"); this.product = product; } diff --git a/cashier-google-play-billing/build.gradle b/cashier-google-play-billing/build.gradle index e3aad73..d1d2ddf 100644 --- a/cashier-google-play-billing/build.gradle +++ b/cashier-google-play-billing/build.gradle @@ -32,9 +32,6 @@ dependencies { api deps.billingClient implementation deps.supportAnnotations - compileOnly deps.autoValue - annotationProcessor deps.autoValue - annotationProcessor deps.autoParcel testImplementation deps.robolectric testImplementation deps.junit diff --git a/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/AbstractGooglePlayBillingApi.java b/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/AbstractGooglePlayBillingApi.java index 498b233..e2386ec 100644 --- a/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/AbstractGooglePlayBillingApi.java +++ b/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/AbstractGooglePlayBillingApi.java @@ -22,6 +22,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.billingclient.api.AcknowledgePurchaseResponseListener; import com.android.billingclient.api.BillingClient.SkuType; import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.Purchase; @@ -63,7 +64,7 @@ public boolean initialize(@NonNull Context context, @NonNull GooglePlayBillingVe public abstract void getSkuDetails(@SkuType String itemType, @NonNull List skus, @NonNull SkuDetailsResponseListener listener); - public abstract void launchBillingFlow(@NonNull Activity activity, @NonNull String sku, @SkuType String itemType); + public abstract void launchBillingFlow(@NonNull Activity activity, @NonNull String sku, @SkuType String itemType, @Nullable String developerPayload, @Nullable String accountId); @Nullable public abstract List getPurchases(); @@ -73,6 +74,8 @@ public abstract void getSkuDetails(@SkuType String itemType, @NonNull List skus, } @Override - public void launchBillingFlow(@NonNull final Activity activity, @NonNull String sku, @SkuType String itemType) { + public void launchBillingFlow(@NonNull final Activity activity, @NonNull String sku, @SkuType String itemType, @Nullable String developerPayload, @Nullable String accountId) { throwIfUnavailable(); logSafely("Launching billing flow for " + sku + " with type " + itemType); @@ -158,20 +162,28 @@ public void launchBillingFlow(@NonNull final Activity activity, @NonNull String Collections.singletonList(sku), new SkuDetailsResponseListener() { @Override - public void onSkuDetailsResponse(int responseCode, List skuDetailsList) { + public void onSkuDetailsResponse(@NonNull BillingResult result, List skuDetailsList) { try { - if (responseCode == BillingResponse.OK && skuDetailsList.size() > 0) { - BillingFlowParams billingFlowParams = BillingFlowParams.newBuilder() - .setSkuDetails(skuDetailsList.get(0)) - .build(); + if (result.getResponseCode() == BillingClient.BillingResponseCode.OK && skuDetailsList.size() > 0) { + BillingFlowParams.Builder billingFlowParamsBuilder = BillingFlowParams.newBuilder() + .setSkuDetails(skuDetailsList.get(0)); + + if (accountId != null) { + billingFlowParamsBuilder.setObfuscatedAccountId(accountId); + } // This will call the {@link PurchasesUpdatedListener} specified in {@link #initialize} - billing.launchBillingFlow(activity, billingFlowParams); + billing.launchBillingFlow(activity, billingFlowParamsBuilder.build()); } else { - vendor.onPurchasesUpdated(BillingResponse.ERROR, null); + vendor.onPurchasesUpdated( + BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.ERROR) + .build(), null); } } catch (Exception e) { - vendor.onPurchasesUpdated(BillingResponse.ERROR, null); + vendor.onPurchasesUpdated(BillingResult.newBuilder() + .setResponseCode(BillingClient.BillingResponseCode.ERROR) + .build(), null); } } } @@ -188,18 +200,18 @@ public List getPurchases() { logSafely("Querying in-app purchases..."); Purchase.PurchasesResult inAppPurchasesResult = billing.queryPurchases(SkuType.INAPP); - if (inAppPurchasesResult.getResponseCode() == BillingResponse.OK) { + if (inAppPurchasesResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { List inAppPurchases = inAppPurchasesResult.getPurchasesList(); logSafely("In-app purchases: " + TextUtils.join(", ", inAppPurchases)); allPurchases.addAll(inAppPurchases); // Check if we support subscriptions and query those purchases as well boolean isSubscriptionSupported = - billing.isFeatureSupported(FeatureType.SUBSCRIPTIONS) == BillingResponse.OK; + billing.isFeatureSupported(FeatureType.SUBSCRIPTIONS).getResponseCode() == BillingClient.BillingResponseCode.OK; if (isSubscriptionSupported) { logSafely("Querying subscription purchases..."); Purchase.PurchasesResult subscriptionPurchasesResult = billing.queryPurchases(SkuType.SUBS); - if (subscriptionPurchasesResult.getResponseCode() == BillingResponse.OK) { + if (subscriptionPurchasesResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { List subscriptionPurchases = subscriptionPurchasesResult.getPurchasesList(); logSafely("Subscription purchases: " + TextUtils.join(", ", subscriptionPurchases)); allPurchases.addAll(subscriptionPurchases); @@ -224,7 +236,7 @@ public List getPurchases(String itemType) { throwIfUnavailable(); Purchase.PurchasesResult purchasesResult = billing.queryPurchases(itemType); - if (purchasesResult.getResponseCode() == BillingResponse.OK) { + if (purchasesResult.getResponseCode() == BillingClient.BillingResponseCode.OK) { List purchases = purchasesResult.getPurchasesList(); logSafely(itemType + " purchases: " + TextUtils.join(", ", purchases)); return purchases; @@ -238,14 +250,28 @@ public void consumePurchase(@NonNull String purchaseToken, @NonNull ConsumeRespo throwIfUnavailable(); logSafely("Consuming product with purchase token: " + purchaseToken); - billing.consumeAsync(purchaseToken, listener); + ConsumeParams params = ConsumeParams.newBuilder() + .setPurchaseToken(purchaseToken) + .build(); + billing.consumeAsync(params, listener); + } + + @Override + public void acknowledgePurchase(@NonNull String purchaseToken, @NonNull AcknowledgePurchaseResponseListener listener) { + throwIfUnavailable(); + + logSafely("Acknowledging subscription with purchase token: " + purchaseToken); + AcknowledgePurchaseParams params = AcknowledgePurchaseParams.newBuilder() + .setPurchaseToken(purchaseToken) + .build(); + billing.acknowledgePurchase(params, listener); } @Override - public void onBillingSetupFinished(@BillingResponse int billingResponseCode) { - logSafely("Service setup finished and connected. Response: " + billingResponseCode); + public void onBillingSetupFinished(BillingResult result) { + logSafely("Service setup finished and connected. Response: " + result.getResponseCode()); - if (billingResponseCode == BillingResponse.OK) { + if (result.getResponseCode() == BillingClient.BillingResponseCode.OK) { isServiceConnected = true; if (listener != null) { diff --git a/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingPurchase.java b/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingPurchase.java index fe6a80b..e797cba 100644 --- a/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingPurchase.java +++ b/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingPurchase.java @@ -1,11 +1,11 @@ package com.getkeepsafe.cashier.billing; +import android.os.Parcel; import android.os.Parcelable; import com.getkeepsafe.cashier.CashierPurchase; import com.getkeepsafe.cashier.Product; import com.getkeepsafe.cashier.Purchase; -import com.google.auto.value.AutoValue; import org.json.JSONException; import org.json.JSONObject; @@ -15,8 +15,41 @@ import static com.getkeepsafe.cashier.billing.GooglePlayBillingConstants.PurchaseConstants.PURCHASE_STATE_PURCHASED; import static com.getkeepsafe.cashier.billing.GooglePlayBillingConstants.PurchaseConstants.PURCHASE_STATE_REFUNDED; -@AutoValue -public abstract class GooglePlayBillingPurchase implements Parcelable, Purchase { +public class GooglePlayBillingPurchase implements Parcelable, Purchase { + + protected GooglePlayBillingPurchase(Parcel in) { + purchase = in.readParcelable(Purchase.class.getClassLoader()); + receipt = in.readString(); + token = in.readString(); + orderId = in.readString(); + purchaseState = in.readInt(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(purchase, flags); + dest.writeString(receipt); + dest.writeString(token); + dest.writeString(orderId); + dest.writeInt(purchaseState); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public GooglePlayBillingPurchase createFromParcel(Parcel in) { + return new GooglePlayBillingPurchase(in); + } + + @Override + public GooglePlayBillingPurchase[] newArray(int size) { + return new GooglePlayBillingPurchase[size]; + } + }; public static GooglePlayBillingPurchase create(Product product, com.android.billingclient.api.Purchase googlePlayPurchase) @@ -28,24 +61,51 @@ public static GooglePlayBillingPurchase create(Product product, googlePlayPurchase.getOrderId(), googlePlayPurchase.getPurchaseToken(), googlePlayPurchase.getOriginalJson(), - // NOTE: Developer payload is not supported with Google Play Billing - // https://issuetracker.google.com/issues/63381481 - ""); + googlePlayPurchase.getDeveloperPayload(), + googlePlayPurchase.getAccountIdentifiers().getObfuscatedAccountId()); - return new AutoValue_GooglePlayBillingPurchase(cashierPurchase, receipt, googlePlayPurchase.getPurchaseToken(), googlePlayPurchase.getOrderId(), purchaseState); + return new GooglePlayBillingPurchase( + cashierPurchase, + receipt, + googlePlayPurchase.getPurchaseToken(), + googlePlayPurchase.getOrderId(), + purchaseState + ); } - public abstract Purchase purchase(); + private final Purchase purchase; + private final String receipt; + private final String token; + private final String orderId; + private final int purchaseState; + + private GooglePlayBillingPurchase(Purchase purchase, String receipt, String token, String orderId, int purchaseState) { + this.purchase = purchase; + this.receipt = receipt; + this.token = token; + this.orderId = orderId; + this.purchaseState = purchaseState; + } + + public Purchase purchase() { + return purchase; + } /** * The original purchase data receipt from Google Play. This is useful for data signature * validation */ - public abstract String receipt(); + public String receipt() { + return receipt; + } - public abstract String token(); + public String token() { + return token; + } - public abstract String orderId(); + public String orderId() { + return orderId; + } /** * The purchase state of the order. @@ -56,7 +116,9 @@ public static GooglePlayBillingPurchase create(Product product, *
  • {@code 2} - Refunded
  • * */ - public abstract int purchaseState(); + public int purchaseState() { + return purchaseState; + } public Product product() { return purchase().product(); diff --git a/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingVendor.java b/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingVendor.java index aad7738..635f316 100644 --- a/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingVendor.java +++ b/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingVendor.java @@ -25,8 +25,10 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import com.android.billingclient.api.BillingClient.BillingResponse; +import com.android.billingclient.api.AcknowledgePurchaseResponseListener; +import com.android.billingclient.api.BillingClient; import com.android.billingclient.api.BillingClient.SkuType; +import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.ConsumeResponseListener; import com.android.billingclient.api.PurchasesUpdatedListener; import com.android.billingclient.api.SkuDetails; @@ -175,10 +177,10 @@ public synchronized void initialized(boolean success) { try { canPurchaseItems = - api.isBillingSupported(SkuType.INAPP) == BillingResponse.OK; + api.isBillingSupported(SkuType.INAPP) == BillingClient.BillingResponseCode.OK; canSubscribe = - api.isBillingSupported(SkuType.SUBS) == BillingResponse.OK; + api.isBillingSupported(SkuType.SUBS) == BillingClient.BillingResponseCode.OK; available = canPurchaseItems || canSubscribe; logSafely("Connected to service and it is " + (available ? "available" : "not available")); @@ -211,7 +213,7 @@ public void dispose(Context context) { } @Override - public synchronized void purchase(Activity activity, Product product, String developerPayload, PurchaseListener listener) { + public synchronized void purchase(Activity activity, Product product, String developerPayload, String accountId, PurchaseListener listener) { Preconditions.checkNotNull(activity, "Activity is null."); Preconditions.checkNotNull(product, "Product is null."); Preconditions.checkNotNull(listener, "Purchase listener is null."); @@ -231,7 +233,7 @@ public synchronized void purchase(Activity activity, Product product, String dev this.pendingProduct = product; logSafely("Launching Google Play Billing flow for " + product.sku()); try { - api.launchBillingFlow(activity, product.sku(), product.isSubscription() ? SkuType.SUBS : SkuType.INAPP); + api.launchBillingFlow(activity, product.sku(), product.isSubscription() ? SkuType.SUBS : SkuType.INAPP, developerPayload, accountId); } catch (Exception e) { clearPendingPurchase(); throw e; @@ -239,7 +241,7 @@ public synchronized void purchase(Activity activity, Product product, String dev } @Override - public void onPurchasesUpdated(@BillingResponse int responseCode, + public void onPurchasesUpdated(@NonNull BillingResult result, @Nullable List purchases) { if (purchaseListener == null) { pendingProduct = null; @@ -247,8 +249,9 @@ public void onPurchasesUpdated(@BillingResponse int responseCode, return; } + int responseCode = result.getResponseCode(); switch (responseCode) { - case BillingResponse.OK: + case BillingClient.BillingResponseCode.OK: if (purchases == null || purchases.isEmpty()) { purchaseListener.failure(pendingProduct, new Error(PURCHASE_FAILURE, responseCode)); clearPendingPurchase(); @@ -259,7 +262,7 @@ public void onPurchasesUpdated(@BillingResponse int responseCode, handlePurchase(purchase, responseCode); } return; - case BillingResponse.USER_CANCELED: + case BillingClient.BillingResponseCode.USER_CANCELED: logSafely("User canceled the purchase code: " + responseCode); purchaseListener.failure(pendingProduct, getPurchaseError(responseCode)); clearPendingPurchase(); @@ -286,11 +289,11 @@ private void handlePurchase(com.android.billingclient.api.Purchase purchase, int return; } - logSafely("Successful purchase of " + purchase.getSku() + "!"); + logSafely("Successful purchase of " + purchase.getSkus() + "!"); purchaseListener.success(cashierPurchase); clearPendingPurchase(); } catch (JSONException error) { - logSafely("Error in parsing purchase response: " + purchase.getSku()); + logSafely("Error in parsing purchase response: " + purchase.getSkus()); purchaseListener.failure(pendingProduct, new Error(PURCHASE_SUCCESS_RESULT_MALFORMED, responseCode)); clearPendingPurchase(); } @@ -308,8 +311,12 @@ public synchronized void consume(@NonNull final Context context, @NonNull final throwIfUninitialized(); final Product product = purchase.product(); - if (product.isSubscription()) { - throw new IllegalStateException("Cannot consume a subscription"); + + if (tokensToBeConsumed.contains(purchase.token())) { + // Purchase currently being consumed or already successfully consumed. + logSafely("Token was already scheduled to be consumed - skipping..."); + listener.failure(purchase, new Error(VendorConstants.CONSUME_UNAVAILABLE, -1)); + return; } if (tokensToBeConsumed.contains(purchase.token())) { @@ -319,23 +326,39 @@ public synchronized void consume(@NonNull final Context context, @NonNull final return; } - logSafely("Consuming " + product.sku()); - tokensToBeConsumed.add(purchase.token()); - - api.consumePurchase(purchase.token(), new ConsumeResponseListener() { - @Override - public void onConsumeResponse(int responseCode, String purchaseToken) { - if (responseCode == BillingResponse.OK) { - logSafely("Successfully consumed " + purchase.product().sku() + "!"); - listener.success(purchase); - } else { - // Failure in consuming token, remove from the list so retry is possible - logSafely("Error consuming " + purchase.product().sku() + " with code "+responseCode); - tokensToBeConsumed.remove(purchaseToken); - listener.failure(purchase, getConsumeError(responseCode)); + if (product.isSubscription()) { + api.acknowledgePurchase(purchase.token(), new AcknowledgePurchaseResponseListener() { + @Override + public void onAcknowledgePurchaseResponse(@NonNull BillingResult result) { + int responseCode = result.getResponseCode(); + if (responseCode == BillingClient.BillingResponseCode.OK) { + logSafely("Successfully acknowledged " + purchase.product().sku() + "!"); + listener.success(purchase); + } else { + // Failure in consuming token, remove from the list so retry is possible + logSafely("Error consuming " + purchase.product().sku() + " with code " + responseCode); + tokensToBeConsumed.remove(purchase.token()); + listener.failure(purchase, getConsumeError(responseCode)); + } } - } - }); + }); + } else { + api.consumePurchase(purchase.token(), new ConsumeResponseListener() { + @Override + public void onConsumeResponse(@NonNull BillingResult result, @NonNull String purchaseToken) { + int responseCode = result.getResponseCode(); + if (responseCode == BillingClient.BillingResponseCode.OK) { + logSafely("Successfully consumed " + purchase.product().sku() + "!"); + listener.success(purchase); + } else { + // Failure in consuming token, remove from the list so retry is possible + logSafely("Error consuming " + purchase.product().sku() + " with code " + responseCode); + tokensToBeConsumed.remove(purchaseToken); + listener.failure(purchase, getConsumeError(responseCode)); + } + } + }); + } } @Override @@ -357,8 +380,9 @@ public void getProductDetails(@NonNull Context context, @NonNull final String sk Collections.singletonList(sku), new SkuDetailsResponseListener() { @Override - public void onSkuDetailsResponse(int responseCode, List skuDetailsList) { - if (responseCode == BillingResponse.OK && skuDetailsList.size() == 1) { + public void onSkuDetailsResponse(BillingResult result, List skuDetailsList) { + int responseCode = result.getResponseCode(); + if (responseCode == BillingClient.BillingResponseCode.OK && skuDetailsList.size() == 1) { logSafely("Successfully got sku details for " + sku + "!"); listener.success( GooglePlayBillingProduct.create(skuDetailsList.get(0), isSubscription ? SkuType.SUBS : SkuType.INAPP) @@ -437,24 +461,24 @@ private void logAndDisable(String message) { private Error getPurchaseError(int responseCode) { final int code; switch (responseCode) { - case BillingResponse.FEATURE_NOT_SUPPORTED: - case BillingResponse.SERVICE_DISCONNECTED: - case BillingResponse.SERVICE_UNAVAILABLE: - case BillingResponse.BILLING_UNAVAILABLE: - case BillingResponse.ITEM_UNAVAILABLE: + case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED: + case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED: + case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE: + case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE: + case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE: code = PURCHASE_UNAVAILABLE; break; - case BillingResponse.USER_CANCELED: + case BillingClient.BillingResponseCode.USER_CANCELED: code = PURCHASE_CANCELED; break; - case BillingResponse.ITEM_ALREADY_OWNED: + case BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED: code = PURCHASE_ALREADY_OWNED; break; - case BillingResponse.ITEM_NOT_OWNED: + case BillingClient.BillingResponseCode.ITEM_NOT_OWNED: code = PURCHASE_NOT_OWNED; break; - case BillingResponse.DEVELOPER_ERROR: - case BillingResponse.ERROR: + case BillingClient.BillingResponseCode.DEVELOPER_ERROR: + case BillingClient.BillingResponseCode.ERROR: default: code = PURCHASE_FAILURE; break; @@ -466,20 +490,20 @@ private Error getPurchaseError(int responseCode) { private Error getConsumeError(int responseCode) { final int code; switch (responseCode) { - case BillingResponse.FEATURE_NOT_SUPPORTED: - case BillingResponse.SERVICE_DISCONNECTED: - case BillingResponse.BILLING_UNAVAILABLE: - case BillingResponse.ITEM_UNAVAILABLE: + case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED: + case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED: + case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE: + case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE: code = CONSUME_UNAVAILABLE; break; - case BillingResponse.USER_CANCELED: + case BillingClient.BillingResponseCode.USER_CANCELED: code = CONSUME_CANCELED; break; - case BillingResponse.ITEM_NOT_OWNED: + case BillingClient.BillingResponseCode.ITEM_NOT_OWNED: code = CONSUME_NOT_OWNED; break; - case BillingResponse.DEVELOPER_ERROR: - case BillingResponse.ERROR: + case BillingClient.BillingResponseCode.DEVELOPER_ERROR: + case BillingClient.BillingResponseCode.ERROR: default: code = CONSUME_FAILURE; break; @@ -491,17 +515,17 @@ private Error getConsumeError(int responseCode) { private Error getDetailsError(int responseCode) { final int code; switch (responseCode) { - case BillingResponse.FEATURE_NOT_SUPPORTED: - case BillingResponse.SERVICE_DISCONNECTED: - case BillingResponse.SERVICE_UNAVAILABLE: - case BillingResponse.BILLING_UNAVAILABLE: - case BillingResponse.ITEM_UNAVAILABLE: + case BillingClient.BillingResponseCode.FEATURE_NOT_SUPPORTED: + case BillingClient.BillingResponseCode.SERVICE_DISCONNECTED: + case BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE: + case BillingClient.BillingResponseCode.BILLING_UNAVAILABLE: + case BillingClient.BillingResponseCode.ITEM_UNAVAILABLE: code = PRODUCT_DETAILS_UNAVAILABLE; break; - case BillingResponse.USER_CANCELED: - case BillingResponse.ITEM_NOT_OWNED: - case BillingResponse.DEVELOPER_ERROR: - case BillingResponse.ERROR: + case BillingClient.BillingResponseCode.USER_CANCELED: + case BillingClient.BillingResponseCode.ITEM_NOT_OWNED: + case BillingClient.BillingResponseCode.DEVELOPER_ERROR: + case BillingClient.BillingResponseCode.ERROR: default: code = PRODUCT_DETAILS_QUERY_FAILURE; break; diff --git a/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/InventoryQuery.java b/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/InventoryQuery.java index 46d99f2..87bff8e 100644 --- a/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/InventoryQuery.java +++ b/cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/InventoryQuery.java @@ -4,6 +4,7 @@ import androidx.annotation.Nullable; import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.BillingResult; import com.android.billingclient.api.SkuDetails; import com.android.billingclient.api.SkuDetailsResponseListener; import com.getkeepsafe.cashier.Inventory; @@ -31,11 +32,11 @@ */ class InventoryQuery { - private Threading threading; + private final Threading threading; - private InventoryListener listener; + private final InventoryListener listener; - private AbstractGooglePlayBillingApi api; + private final AbstractGooglePlayBillingApi api; /** * Inapp product details returned from async getSkuDetails call @@ -55,9 +56,9 @@ class InventoryQuery { */ private List purchases = null; - private Collection inappSkus; + private final Collection inappSkus; - private Collection subSkus; + private final Collection subSkus; private int inappResponseCode = 0; @@ -103,7 +104,7 @@ public void run() { subsSkuDetails = null; Set inappSkusToQuery = new HashSet<>(); Set subSkusToQuery = new HashSet<>(); - boolean subscriptionsSupported = api.isBillingSupported(BillingClient.SkuType.SUBS) == BillingClient.BillingResponse.OK; + boolean subscriptionsSupported = api.isBillingSupported(BillingClient.SkuType.SUBS) == BillingClient.BillingResponseCode.OK; if (inappSkus != null) { inappSkusToQuery.addAll(inappSkus); @@ -129,20 +130,20 @@ public void run() { // Add all inapp purchases skus to skus to be queried list for (com.android.billingclient.api.Purchase inappPurchase : inappPurchases) { - inappSkusToQuery.add(inappPurchase.getSku()); + inappSkusToQuery.addAll(inappPurchase.getSkus()); } // Add all subscription purchases skus to skus to be queried list for (com.android.billingclient.api.Purchase subPurchase : subPurchases) { - subSkusToQuery.add(subPurchase.getSku()); + subSkusToQuery.addAll(subPurchase.getSkus()); } if (inappSkusToQuery.size() > 0) { // Perform async sku details query api.getSkuDetails(BillingClient.SkuType.INAPP, new ArrayList(inappSkusToQuery), new SkuDetailsResponseListener() { @Override - public void onSkuDetailsResponse(int responseCode, List skuDetailsList) { + public void onSkuDetailsResponse(BillingResult result, List skuDetailsList) { inappSkuDetails = skuDetailsList != null ? skuDetailsList : new ArrayList(); - inappResponseCode = responseCode; + inappResponseCode = result.getResponseCode(); // Check if other async operation finished notifyIfReady(); } @@ -155,9 +156,9 @@ public void onSkuDetailsResponse(int responseCode, List skuDetailsLi // Perform async sku details query api.getSkuDetails(BillingClient.SkuType.SUBS, new ArrayList(subSkusToQuery), new SkuDetailsResponseListener() { @Override - public void onSkuDetailsResponse(int responseCode, List skuDetailsList) { + public void onSkuDetailsResponse(BillingResult result, List skuDetailsList) { subsSkuDetails = skuDetailsList != null ? skuDetailsList : new ArrayList(); - subsResponseCode = responseCode; + subsResponseCode = result.getResponseCode(); // Check if other async operation finished notifyIfReady(); } @@ -182,7 +183,7 @@ private synchronized void notifyIfReady() { // and result may be delivered to listener if (purchases != null && inappSkuDetails != null && subsSkuDetails != null && !notified) { - if (inappResponseCode != BillingClient.BillingResponse.OK || subsResponseCode != BillingClient.BillingResponse.OK) { + if (inappResponseCode != BillingClient.BillingResponseCode.OK || subsResponseCode != BillingClient.BillingResponseCode.OK) { // Deliver result on main thread threading.runOnMainThread(new Runnable() { @Override @@ -216,22 +217,24 @@ public void run() { } for (com.android.billingclient.api.Purchase billingPurchase : purchases) { - SkuDetails skuDetails = details.get(billingPurchase.getSku()); - if (skuDetails != null) { - Product product = GooglePlayBillingProduct.create(skuDetails, skuDetails.getType()); - try { - Purchase purchase = GooglePlayBillingPurchase.create(product, billingPurchase); - inventory.addPurchase(purchase); - } catch (JSONException e) { - e.printStackTrace(); - // Deliver result on main thread - threading.runOnMainThread(new Runnable() { - @Override - public void run() { - listener.failure(new Vendor.Error(VendorConstants.INVENTORY_QUERY_MALFORMED_RESPONSE, -1)); - } - }); - return; + for (String sku : billingPurchase.getSkus()) { + SkuDetails skuDetails = details.get(sku); + if (skuDetails != null) { + Product product = GooglePlayBillingProduct.create(skuDetails, skuDetails.getType()); + try { + Purchase purchase = GooglePlayBillingPurchase.create(product, billingPurchase); + inventory.addPurchase(purchase); + } catch (JSONException e) { + e.printStackTrace(); + // Deliver result on main thread + threading.runOnMainThread(new Runnable() { + @Override + public void run() { + listener.failure(new Vendor.Error(VendorConstants.INVENTORY_QUERY_MALFORMED_RESPONSE, -1)); + } + }); + return; + } } } } diff --git a/cashier-iab-debug/build.gradle b/cashier-iab-debug/build.gradle index 1a85588..db733ee 100644 --- a/cashier-iab-debug/build.gradle +++ b/cashier-iab-debug/build.gradle @@ -29,10 +29,7 @@ android { dependencies { api project(':cashier-iab') - compileOnly deps.autoValue compileOnly deps.supportAnnotations - annotationProcessor deps.autoValue - annotationProcessor deps.autoParcel testImplementation deps.robolectric testImplementation deps.junit diff --git a/cashier-iab/build.gradle b/cashier-iab/build.gradle index 14e19f1..e59f2ea 100644 --- a/cashier-iab/build.gradle +++ b/cashier-iab/build.gradle @@ -28,11 +28,8 @@ android { dependencies { api project(':cashier') - - compileOnly deps.autoValue +\ compileOnly deps.supportAnnotations - annotationProcessor deps.autoValue - annotationProcessor deps.autoParcel testImplementation deps.robolectric testImplementation deps.junit diff --git a/cashier-iab/src/main/java/com/getkeepsafe/cashier/iab/InAppBillingPurchase.java b/cashier-iab/src/main/java/com/getkeepsafe/cashier/iab/InAppBillingPurchase.java index a9a17ce..8a5ae20 100644 --- a/cashier-iab/src/main/java/com/getkeepsafe/cashier/iab/InAppBillingPurchase.java +++ b/cashier-iab/src/main/java/com/getkeepsafe/cashier/iab/InAppBillingPurchase.java @@ -17,12 +17,12 @@ package com.getkeepsafe.cashier.iab; import android.content.Intent; +import android.os.Parcel; import android.os.Parcelable; import com.getkeepsafe.cashier.CashierPurchase; import com.getkeepsafe.cashier.Product; import com.getkeepsafe.cashier.Purchase; -import com.google.auto.value.AutoValue; import org.json.JSONException; import org.json.JSONObject; @@ -41,8 +41,7 @@ import static com.getkeepsafe.cashier.iab.InAppBillingConstants.RESPONSE_INAPP_PURCHASE_DATA; import static com.getkeepsafe.cashier.iab.InAppBillingConstants.RESPONSE_INAPP_SIGNATURE; -@AutoValue -public abstract class InAppBillingPurchase implements Parcelable, Purchase { +public class InAppBillingPurchase implements Parcelable, Purchase { public static final String GP_ORDER_ID_TEST = "TEST-ORDER-ID"; public static final String GP_KEY_PACKAGE_NAME = "gp-package-name"; @@ -52,29 +51,95 @@ public abstract class InAppBillingPurchase implements Parcelable, Purchase { public static final String GP_KEY_PURCHASE_STATE = "gp-purchase-state"; public static final String GP_KEY_PURCHASE_DATA = "gp-purchase-data"; - public abstract Purchase purchase(); + private final Purchase purchase; + private final String packageName; + private final String dataSignature; + private final boolean autoRenewing; + private final long purchaseTime; + private final int purchaseState; + private final String receipt; + + private InAppBillingPurchase(Purchase purchase, String packageName, String dataSignature, boolean autoRenewing, long purchaseTime, int purchaseState, String receipt) { + this.purchase = purchase; + this.packageName = packageName; + this.dataSignature = dataSignature; + this.autoRenewing = autoRenewing; + this.purchaseTime = purchaseTime; + this.purchaseState = purchaseState; + this.receipt = receipt; + } + + protected InAppBillingPurchase(Parcel in) { + purchase = in.readParcelable(Purchase.class.getClassLoader()); + packageName = in.readString(); + dataSignature = in.readString(); + autoRenewing = in.readByte() != 0; + purchaseTime = in.readLong(); + purchaseState = in.readInt(); + receipt = in.readString(); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeParcelable(purchase, flags); + dest.writeString(packageName); + dest.writeString(dataSignature); + dest.writeByte((byte) (autoRenewing ? 1 : 0)); + dest.writeLong(purchaseTime); + dest.writeInt(purchaseState); + dest.writeString(receipt); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public InAppBillingPurchase createFromParcel(Parcel in) { + return new InAppBillingPurchase(in); + } + + @Override + public InAppBillingPurchase[] newArray(int size) { + return new InAppBillingPurchase[size]; + } + }; + + public Purchase purchase() { + return purchase; + } /** * The application package from which the purchase originated */ - public abstract String packageName(); + public String packageName() { + return packageName; + } /** * String containing the signature of the purchase data that was signed with the private key * of the developer. */ - public abstract String dataSignature(); + public String dataSignature() { + return dataSignature; + } /** * Indicates whether a subscription renews automatically. {@code false} indicates a canceled * subscription. */ - public abstract boolean autoRenewing(); + public boolean autoRenewing() { + return autoRenewing; + } /** * The time the product was purchased, in milliseconds since the UNIX epoch */ - public abstract long purchaseTime(); + public long purchaseTime() { + return purchaseTime; + } /** * The purchase state of the order. @@ -85,13 +150,17 @@ public abstract class InAppBillingPurchase implements Parcelable, Purchase { *
  • {@code 2} - Refunded
  • * */ - public abstract int purchaseState(); + public int purchaseState() { + return purchaseState; + } /** * The original purchase data receipt from Google Play. This is useful for data signature * validation */ - public abstract String receipt(); + public String receipt() { + return receipt; + } public Product product() { return purchase().product(); @@ -171,7 +240,7 @@ public static InAppBillingPurchase create(Product product, String purchaseData, final int purchaseState = data.getInt(PURCHASE_STATE); final Purchase purchase = - CashierPurchase.create(product, orderId, purchaseToken, purchaseData, developerPayload); + CashierPurchase.create(product, orderId, purchaseToken, purchaseData, developerPayload, null); return create( purchase, @@ -187,7 +256,7 @@ public static InAppBillingPurchase create(Purchase purchase, String packageName, String dataSignature, boolean autoRenew, long purchaseTime, int purchaseState, String purchaseData) { - return new AutoValue_InAppBillingPurchase(purchase, + return new InAppBillingPurchase(purchase, packageName, dataSignature, autoRenew, diff --git a/cashier-iab/src/main/java/com/getkeepsafe/cashier/iab/InAppBillingV3Vendor.java b/cashier-iab/src/main/java/com/getkeepsafe/cashier/iab/InAppBillingV3Vendor.java index 6636efc..482576c 100644 --- a/cashier-iab/src/main/java/com/getkeepsafe/cashier/iab/InAppBillingV3Vendor.java +++ b/cashier-iab/src/main/java/com/getkeepsafe/cashier/iab/InAppBillingV3Vendor.java @@ -220,7 +220,7 @@ public boolean canPurchase(Product product) { } @Override - public void purchase(Activity activity, Product product, String developerPayload, + public void purchase(Activity activity, Product product, String developerPayload, String accountId, PurchaseListener listener) { if (activity == null || product == null || listener == null) { throw new IllegalArgumentException("Activity, product, or listener is null"); @@ -232,6 +232,10 @@ public void purchase(Activity activity, Product product, String developerPayload throw new IllegalArgumentException("Cannot purchase given product!" + product.toString()); } + if (accountId != null) { + throw new IllegalArgumentException("Account id is not supported!"); + } + log("Constructing buy intent..."); final String type = product.isSubscription() ? PRODUCT_TYPE_SUBSCRIPTION : PRODUCT_TYPE_ITEM; try { diff --git a/cashier/build.gradle b/cashier/build.gradle index 4c853d4..8b3cc72 100644 --- a/cashier/build.gradle +++ b/cashier/build.gradle @@ -29,10 +29,7 @@ android { } dependencies { - compileOnly deps.autoValue compileOnly deps.supportAnnotations - annotationProcessor deps.autoValue - annotationProcessor deps.autoParcel testImplementation deps.robolectric testImplementation deps.junit diff --git a/cashier/src/main/java/com/getkeepsafe/cashier/Cashier.java b/cashier/src/main/java/com/getkeepsafe/cashier/Cashier.java index 6e6cb42..94165a3 100644 --- a/cashier/src/main/java/com/getkeepsafe/cashier/Cashier.java +++ b/cashier/src/main/java/com/getkeepsafe/cashier/Cashier.java @@ -39,7 +39,7 @@ * There should only be one instance of this class for each Activity that hosts a billing flow */ public class Cashier { - private static HashMap vendorFactories = new HashMap<>(1); + private static final HashMap vendorFactories = new HashMap<>(1); static boolean sPurchaseInProgress = false; private final Context context; @@ -154,20 +154,34 @@ private Cashier(Context context, Vendor vendor) { * @param listener The {@link PurchaseListener} to handle the result */ public void purchase(Activity activity, Product product, PurchaseListener listener) { - purchase(activity, product, null, listener); + purchase(activity, product, null, null, listener); } - + + /** + * Initiates a purchase flow + * + * @param activity The activity that will host the purchase flow + * @param product The {@link Product} you wish to buy + * * @param developerPayload Your custom payload to pass along to the {@link Vendor} + * @param listener The {@link PurchaseListener} to handle the result + */ + public void purchase(Activity activity, Product product, String developerPayload, PurchaseListener listener) { + purchase(activity, product, developerPayload, null, listener); + } + /** * Initiates a purchase flow * * @param activity The activity that will host the purchase flow * @param product The {@link Product} you wish to buy * @param developerPayload Your custom payload to pass along to the {@link Vendor} + * @param accountId Custom account id for better purchase tracking * @param purchaseListener The {@link PurchaseListener} to handle the result */ public void purchase(final Activity activity, final Product product, @Nullable final String developerPayload, + @Nullable final String accountId, final PurchaseListener purchaseListener) { Preconditions.checkNotNull(product, "Product is null"); Preconditions.checkNotNull(purchaseListener, "PurchaseListener is null"); @@ -206,7 +220,7 @@ public void initialized() { final String payload = developerPayload == null ? "" : developerPayload; try { - vendor.purchase(activity, product, payload, purchaseListenerWrapper); + vendor.purchase(activity, product, payload, accountId, purchaseListenerWrapper); } catch (Exception e) { purchaseListenerWrapper.failure(product, new Vendor.Error(VendorConstants.PURCHASE_FAILURE, -1)); } diff --git a/cashier/src/main/java/com/getkeepsafe/cashier/CashierPurchase.java b/cashier/src/main/java/com/getkeepsafe/cashier/CashierPurchase.java index f2c31ed..f49d33a 100644 --- a/cashier/src/main/java/com/getkeepsafe/cashier/CashierPurchase.java +++ b/cashier/src/main/java/com/getkeepsafe/cashier/CashierPurchase.java @@ -16,27 +16,78 @@ package com.getkeepsafe.cashier; -import com.google.auto.value.AutoValue; +import android.os.Parcel; +import android.os.Parcelable; import org.json.JSONException; import org.json.JSONObject; -@AutoValue -public abstract class CashierPurchase implements Purchase { +public class CashierPurchase implements Purchase { public static final String KEY_ORDER_ID = "cashier-order-id"; public static final String KEY_TOKEN = "cashier-token"; public static final String KEY_RECEIPT = "cashier-receipt"; public static final String KEY_DEV_PAYLOAD = "cashier-developer-payload"; + public static final String KEY_ACCOUNT_ID = "cashier-account-id"; - public abstract Product product(); + private final Product product; + private final String orderId; + private final String token; + private final String receipt; + private final String developerPayload; + private final String accountId; - public abstract String orderId(); + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { - public abstract String token(); + @Override + public CashierPurchase createFromParcel(Parcel parcel) { + return new CashierPurchase( + parcel.readParcelable(Product.class.getClassLoader()), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString() + ); + } - public abstract String receipt(); + @Override + public CashierPurchase[] newArray(int size) { + return new CashierPurchase[size]; + } + }; - public abstract String developerPayload(); + private CashierPurchase(Product product, String orderId, String token, String receipt, String developerPayload, String accountId) { + this.product = product; + this.orderId = orderId; + this.token = token; + this.receipt = receipt; + this.developerPayload = developerPayload; + this.accountId = accountId; + } + + public Product product() { + return product; + } + + public String orderId() { + return orderId; + } + + public String token() { + return token; + } + + public String receipt() { + return receipt; + } + + public String developerPayload() { + return developerPayload; + } + + public String accountId() { + return accountId; + } public static CashierPurchase create(String json) throws JSONException { return create(new JSONObject(json)); @@ -47,15 +98,17 @@ public static CashierPurchase create(JSONObject json) throws JSONException { json.getString(KEY_ORDER_ID), json.getString(KEY_TOKEN), json.getString(KEY_RECEIPT), - json.getString(KEY_DEV_PAYLOAD)); + json.getString(KEY_DEV_PAYLOAD), + json.getString(KEY_ACCOUNT_ID)); } public static CashierPurchase create(Product product, String orderId, String token, String receipt, - String developerPayload) { - return new AutoValue_CashierPurchase(product, orderId, token, receipt, developerPayload); + String developerPayload, + String accountId) { + return new CashierPurchase(product, orderId, token, receipt, developerPayload, accountId); } public JSONObject toJson() throws JSONException { @@ -64,6 +117,22 @@ public JSONObject toJson() throws JSONException { object.put(KEY_TOKEN, token()); object.put(KEY_RECEIPT, receipt()); object.put(KEY_DEV_PAYLOAD, developerPayload()); + object.put(KEY_ACCOUNT_ID, accountId()); return object; } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeParcelable(product, 0); + parcel.writeString(orderId); + parcel.writeString(token); + parcel.writeString(receipt); + parcel.writeString(developerPayload); + parcel.writeString(accountId); + } } diff --git a/cashier/src/main/java/com/getkeepsafe/cashier/Product.java b/cashier/src/main/java/com/getkeepsafe/cashier/Product.java index b4d5d7e..d2a16b2 100644 --- a/cashier/src/main/java/com/getkeepsafe/cashier/Product.java +++ b/cashier/src/main/java/com/getkeepsafe/cashier/Product.java @@ -16,15 +16,13 @@ package com.getkeepsafe.cashier; +import android.os.Parcel; import android.os.Parcelable; -import com.google.auto.value.AutoValue; - import org.json.JSONException; import org.json.JSONObject; -@AutoValue -public abstract class Product implements Parcelable { +public class Product implements Parcelable { public static final String KEY_VENDOR_ID = "vendor-id"; public static final String KEY_SKU = "sku"; public static final String KEY_PRICE = "price"; @@ -34,21 +32,79 @@ public abstract class Product implements Parcelable { public static final String KEY_IS_SUB = "subscription"; public static final String KEY_MICRO_PRICE = "micros-price"; - public abstract String vendorId(); + private final String vendorId; + private final String sku; + private final String price; + private final String currency; + private final String name; + private final String description; + private final boolean isSubscription; + private final long microsPrice; + + public static final Parcelable.Creator CREATOR = new Parcelable.Creator() { + + @Override + public Product createFromParcel(Parcel parcel) { + return new Product( + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readInt() == 1, + parcel.readLong() + ); + } + + @Override + public Product[] newArray(int size) { + return new Product[size]; + } + }; + + private Product(String vendorId, String sku, String price, String currency, String name, String description, boolean isSubscription, long microsPrice) { + this.vendorId = vendorId; + this.sku = sku; + this.price = price; + this.currency = currency; + this.name = name; + this.description = description; + this.isSubscription = isSubscription; + this.microsPrice = microsPrice; + } - public abstract String sku(); + public String vendorId() { + return vendorId; + } - public abstract String price(); + public String sku() { + return sku; + } - public abstract String currency(); + public String price() { + return price; + } - public abstract String name(); + public String currency() { + return currency; + } - public abstract String description(); + public String name() { + return name; + } - public abstract boolean isSubscription(); + public String description() { + return description; + } - public abstract long microsPrice(); + public boolean isSubscription() { + return isSubscription; + } + + public long microsPrice() { + return microsPrice; + } public static Product create(String json) throws JSONException { return create(new JSONObject(json)); @@ -74,15 +130,16 @@ public static Product create(String vendorId, String description, boolean isSubscription, long microsPrice) { - return new AutoValue_Product( - vendorId, - sku, - price, - currency, - name, - description, - isSubscription, - microsPrice); + return new Product( + vendorId, + sku, + price, + currency, + name, + description, + isSubscription, + microsPrice) { + }; } public JSONObject toJson() throws JSONException { @@ -99,6 +156,23 @@ public JSONObject toJson() throws JSONException { } public String toJsonString() throws JSONException { - return toJson().toString(); + return toJson().toString(); + } + + @Override + public int describeContents() { + return 0; + } + + @Override + public void writeToParcel(Parcel parcel, int i) { + parcel.writeString(vendorId); + parcel.writeString(sku); + parcel.writeString(price); + parcel.writeString(currency); + parcel.writeString(name); + parcel.writeString(description); + parcel.writeInt(isSubscription ? 1 : 0); + parcel.writeLong(microsPrice); } } diff --git a/cashier/src/main/java/com/getkeepsafe/cashier/Vendor.java b/cashier/src/main/java/com/getkeepsafe/cashier/Vendor.java index 926a595..96a846a 100644 --- a/cashier/src/main/java/com/getkeepsafe/cashier/Vendor.java +++ b/cashier/src/main/java/com/getkeepsafe/cashier/Vendor.java @@ -61,6 +61,7 @@ public boolean equals(Object other) { void purchase(Activity activity, Product product, String developerPayload, + String accountId, PurchaseListener listener); void consume(Context context, diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7c17a40..6b4f35c 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip diff --git a/versions.gradle b/versions.gradle index f5fda00..a0148ea 100644 --- a/versions.gradle +++ b/versions.gradle @@ -1,14 +1,12 @@ ext { versions = [ - compileSdk : 28, + compileSdk : 30, minSdk : 9, - buildTools : '28.0.3', - versionName: '0.3.6', - autoValue : '1.3', - autoParcel : '0.2.5', - appCompat : '1.1.0', - support : '1.1.0', - billing : '1.2', + buildTools : '30.0.2', + versionName: '0.3.9', + appCompat : '1.3.0', + support : '28.0.0', + billing : '4.0.0', roboelectric: '3.3.2', junit : '4.12', mockito : '2.2.9', @@ -16,10 +14,8 @@ ext { ] deps = [ - autoValue : "com.google.auto.value:auto-value:${versions.autoValue}", - autoParcel : "com.ryanharter.auto.value:auto-value-parcel:${versions.autoParcel}", appCompat : "androidx.appcompat:appcompat:${versions.appCompat}", - supportAnnotations: "androidx.annotation:annotation:${versions.support}", + supportAnnotations: "com.android.support:support-annotations:${versions.support}", billingClient : "com.android.billingclient:billing:${versions.billing}", // Test dependencies