From 0a24384e74dbc79d8c3c6560cbf48f855753cae2 Mon Sep 17 00:00:00 2001 From: tomasz-keepsafe <40766236+tomasz-keepsafe@users.noreply.github.com> Date: Thu, 29 Aug 2019 21:32:41 +0200 Subject: [PATCH] Google Play Billing implementation (#32) Google Play Billing implementation --- .travis.yml | 4 +- CHANGELOG.md | 11 + README.md | 76 ++- build.gradle | 2 +- .../.gitignore | 0 .../build.gradle | 41 ++ .../src/main/AndroidManifest.xml | 13 + .../debug/FakeGooglePlayBillingApi.java | 242 +++++++++ .../debug/FakeGooglePlayCheckoutActivity.java | 98 ++++ .../cashier/billing/debug/FakeSkuDetails.java | 56 ++ .../res/layout/activity_fake_checkout.xml | 135 +++++ .../src/main/res/values/colors.xml | 4 + .../src/main/res/values/style.xml | 9 + .../billing/debug/ExampleUnitTest.java | 17 + cashier-google-play-billing/.gitignore | 1 + cashier-google-play-billing/build.gradle | 42 ++ .../proguard-rules.pro | 2 + .../src/main/AndroidManifest.xml | 6 + .../billing/AbstractGooglePlayBillingApi.java | 80 +++ .../cashier/billing/GooglePlayBillingApi.java | 276 ++++++++++ .../billing/GooglePlayBillingConstants.java | 33 ++ .../billing/GooglePlayBillingProduct.java | 21 + .../billing/GooglePlayBillingPurchase.java | 85 +++ .../billing/GooglePlayBillingSecurity.java | 109 ++++ .../billing/GooglePlayBillingVendor.java | 512 ++++++++++++++++++ .../cashier/billing/InventoryQuery.java | 249 +++++++++ .../cashier/billing/Threading.java | 23 + .../GooglePlayBillingPurchaseTest.java | 65 +++ .../GooglePlayBillingSecurityTest.java | 73 +++ .../billing/GooglePlayBillingVendorTest.java | 439 +++++++++++++++ .../cashier/billing/InventoryQueryTest.java | 190 +++++++ .../getkeepsafe/cashier/billing/TestData.java | 118 ++++ .../cashier/billing/TestHelper.java | 115 ++++ .../cashier/billing/TestPurchase.java | 45 ++ .../cashier/billing/TestSkuDetails.java | 56 ++ .../cashier/iab/InAppBillingV3Vendor.java | 5 + cashier-sample-google-play-billing/.gitignore | 1 + .../build.gradle | 29 + .../proguard-rules.pro | 21 + .../src/main/AndroidManifest.xml | 21 + .../sample/googleplaybilling/Item.java | 29 + .../googleplaybilling/ItemsAdapter.java | 129 +++++ .../googleplaybilling/MainActivity.java | 321 +++++++++++ .../main/res/drawable-mdpi/ic_launcher.png | Bin .../main/res/drawable-xhdpi/ic_launcher.png | Bin .../main/res/drawable-xxhdpi/ic_launcher.png | Bin .../main/res/drawable-xxxhdpi/ic_launcher.png | Bin .../src/main/res/layout/activity_main.xml | 58 ++ .../src/main/res/layout/view_item.xml | 47 ++ .../src/main/res/values/colors.xml | 0 .../src/main/res/values/strings.xml | 10 + .../src/main/res/values/styles.xml | 0 cashier-sample-iab/.gitignore | 1 + .../build.gradle | 2 +- .../proguard-rules.pro | 0 .../src/main/AndroidManifest.xml | 0 .../cashier/sample/MainActivity.java | 1 - .../main/res/drawable-mdpi/ic_launcher.png | Bin 0 -> 2302 bytes .../main/res/drawable-xhdpi/ic_launcher.png | Bin 0 -> 6182 bytes .../main/res/drawable-xxhdpi/ic_launcher.png | Bin 0 -> 11971 bytes .../main/res/drawable-xxxhdpi/ic_launcher.png | Bin 0 -> 18493 bytes .../src/main/res/layout/activity_main.xml | 0 .../src/main/res/values/colors.xml | 6 + .../src/main/res/values/strings.xml | 0 .../src/main/res/values/styles.xml | 11 + .../getkeepsafe/cashier/Preconditions.java | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- settings.gradle | 2 +- versions.gradle | 37 +- 69 files changed, 3946 insertions(+), 37 deletions(-) create mode 100644 CHANGELOG.md rename {cashier-sample => cashier-google-play-billing-debug}/.gitignore (100%) create mode 100644 cashier-google-play-billing-debug/build.gradle create mode 100644 cashier-google-play-billing-debug/src/main/AndroidManifest.xml create mode 100644 cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayBillingApi.java create mode 100644 cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayCheckoutActivity.java create mode 100644 cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeSkuDetails.java create mode 100644 cashier-google-play-billing-debug/src/main/res/layout/activity_fake_checkout.xml create mode 100644 cashier-google-play-billing-debug/src/main/res/values/colors.xml create mode 100644 cashier-google-play-billing-debug/src/main/res/values/style.xml create mode 100644 cashier-google-play-billing-debug/src/test/java/com/getkeepsafe/cashier/billing/debug/ExampleUnitTest.java create mode 100644 cashier-google-play-billing/.gitignore create mode 100644 cashier-google-play-billing/build.gradle create mode 100644 cashier-google-play-billing/proguard-rules.pro create mode 100644 cashier-google-play-billing/src/main/AndroidManifest.xml create mode 100644 cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/AbstractGooglePlayBillingApi.java create mode 100644 cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingApi.java create mode 100644 cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingConstants.java create mode 100644 cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingProduct.java create mode 100644 cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingPurchase.java create mode 100644 cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingSecurity.java create mode 100644 cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/GooglePlayBillingVendor.java create mode 100644 cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/InventoryQuery.java create mode 100644 cashier-google-play-billing/src/main/java/com/getkeepsafe/cashier/billing/Threading.java create mode 100644 cashier-google-play-billing/src/test/java/com/getkeepsafe/cashier/billing/GooglePlayBillingPurchaseTest.java create mode 100644 cashier-google-play-billing/src/test/java/com/getkeepsafe/cashier/billing/GooglePlayBillingSecurityTest.java create mode 100644 cashier-google-play-billing/src/test/java/com/getkeepsafe/cashier/billing/GooglePlayBillingVendorTest.java create mode 100644 cashier-google-play-billing/src/test/java/com/getkeepsafe/cashier/billing/InventoryQueryTest.java create mode 100644 cashier-google-play-billing/src/test/java/com/getkeepsafe/cashier/billing/TestData.java create mode 100644 cashier-google-play-billing/src/test/java/com/getkeepsafe/cashier/billing/TestHelper.java create mode 100644 cashier-google-play-billing/src/test/java/com/getkeepsafe/cashier/billing/TestPurchase.java create mode 100644 cashier-google-play-billing/src/test/java/com/getkeepsafe/cashier/billing/TestSkuDetails.java create mode 100644 cashier-sample-google-play-billing/.gitignore create mode 100644 cashier-sample-google-play-billing/build.gradle create mode 100644 cashier-sample-google-play-billing/proguard-rules.pro create mode 100644 cashier-sample-google-play-billing/src/main/AndroidManifest.xml create mode 100644 cashier-sample-google-play-billing/src/main/java/com/getkeepsafe/cashier/sample/googleplaybilling/Item.java create mode 100644 cashier-sample-google-play-billing/src/main/java/com/getkeepsafe/cashier/sample/googleplaybilling/ItemsAdapter.java create mode 100644 cashier-sample-google-play-billing/src/main/java/com/getkeepsafe/cashier/sample/googleplaybilling/MainActivity.java rename {cashier-sample => cashier-sample-google-play-billing}/src/main/res/drawable-mdpi/ic_launcher.png (100%) rename {cashier-sample => cashier-sample-google-play-billing}/src/main/res/drawable-xhdpi/ic_launcher.png (100%) rename {cashier-sample => cashier-sample-google-play-billing}/src/main/res/drawable-xxhdpi/ic_launcher.png (100%) rename {cashier-sample => cashier-sample-google-play-billing}/src/main/res/drawable-xxxhdpi/ic_launcher.png (100%) create mode 100644 cashier-sample-google-play-billing/src/main/res/layout/activity_main.xml create mode 100644 cashier-sample-google-play-billing/src/main/res/layout/view_item.xml rename {cashier-sample => cashier-sample-google-play-billing}/src/main/res/values/colors.xml (100%) create mode 100644 cashier-sample-google-play-billing/src/main/res/values/strings.xml rename {cashier-sample => cashier-sample-google-play-billing}/src/main/res/values/styles.xml (100%) create mode 100644 cashier-sample-iab/.gitignore rename {cashier-sample => cashier-sample-iab}/build.gradle (96%) rename {cashier-sample => cashier-sample-iab}/proguard-rules.pro (100%) rename {cashier-sample => cashier-sample-iab}/src/main/AndroidManifest.xml (100%) rename {cashier-sample => cashier-sample-iab}/src/main/java/com/getkeepsafe/cashier/sample/MainActivity.java (99%) create mode 100644 cashier-sample-iab/src/main/res/drawable-mdpi/ic_launcher.png create mode 100644 cashier-sample-iab/src/main/res/drawable-xhdpi/ic_launcher.png create mode 100644 cashier-sample-iab/src/main/res/drawable-xxhdpi/ic_launcher.png create mode 100644 cashier-sample-iab/src/main/res/drawable-xxxhdpi/ic_launcher.png rename {cashier-sample => cashier-sample-iab}/src/main/res/layout/activity_main.xml (100%) create mode 100644 cashier-sample-iab/src/main/res/values/colors.xml rename {cashier-sample => cashier-sample-iab}/src/main/res/values/strings.xml (100%) create mode 100644 cashier-sample-iab/src/main/res/values/styles.xml diff --git a/.travis.yml b/.travis.yml index 07e2a12..a32b67b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,8 @@ android: components: - platform-tools - tools - - build-tools-27.0.3 - - android-27 + - build-tools-28.0.3 + - android-28 - extra-google-m2repository - extra-android-m2repository diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..a2548fc --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,11 @@ +# Changelog + +## 0.3.5 + - Fixed inventory query uncaught exception + +## 0.3.0 + - Google Play Billing lib implementation - GooglePlayBillingVendor + - Google Play Billing implementation unit tests + - Fake purchase flow for GooglePlayBillingVendor + - New sample app using GooglePlayBillingVendor + - Added changelog \ No newline at end of file diff --git a/README.md b/README.md index 4a41081..8e2bd77 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@

Cashier
- Cashier + Cashier

[![Build Status](https://travis-ci.org/KeepSafe/Cashier.svg?branch=master)](https://travis-ci.org/KeepSafe/Cashier) @@ -19,28 +19,34 @@ Cashier also aims to bridge the gap between development testing and production t ### Features - - Google Play's In-App-Billing (IAB) + - Google Play's In-App-Billing (IAB) - Deprecated - Purchasing for products and subscriptions, consuming for consumable products - Fake checkout, facilitating faster development - Local receipt verification - Inventory querying + - Google Play Billing + - Purchasing for products and subscriptions, consuming for consumable products + - Fake checkout, facilitating faster development + - Local receipt verification + - Inventory querying + - For now, developer payload is not supported (will be added in GPB v2) + ## Installation Cashier is distributed using [jcenter](https://bintray.com/keepsafesoftware/Android/Cashier/view). ```groovy -repositories { +repositories { jcenter() } - + dependencies { compile 'com.getkeepsafe.cashier:cashier:0.x.x' // Core library, required - - // Google Play - compile 'com.getkeepsafe.cashier:cashier-iab:0.x.x' - debugCompile 'com.getkeepsafe.cashier:cashier-iab-debug:0.x.x' // For fake checkout and testing - releaseCompile 'com.getkeepsafe.cashier:cashier-iab-debug-no-op:0.x.x' + + // Google Play Billing + compile 'com.getkeepsafe.cashier:cashier-google-play-billing:0.x.x' + debugCompile 'com.getkeepsafe.cashier:cashier-google-play-billing-debug:0.x.x' // For fake checkout and testing } ``` @@ -50,7 +56,7 @@ General usage is as follows: ```java // First choose a vendor -final Vendor vendor = new InAppBillingV3Vendor(); +final Vendor vendor = new GooglePlayBillingVendor(); // Get a product to buy final Product product = Product.create( @@ -82,9 +88,57 @@ final Cashier cashier = Cashier.forVendor(activity, vendor); cashier.purchase(activity, product, "my custom dev payload", listener); ``` +To test app in debug mode with fake purchase flow: +```java +// Create vendor with fake API implementation +vendor = new GooglePlayBillingVendor( + new FakeGooglePlayBillingApi(MainActivity.this, + FakeGooglePlayBillingApi.TEST_PUBLIC_KEY)); + +// Add products definitions +final Product product = Product.create( + vendor.id(), // The vendor that produces this product + "my.sku", // The SKU of the product + "$0.99", // The display price of the product + "USD", // The currency of the display price + "My Awesome Product", // The product's title + "Provides awesomeness!", // The product's description + false, // Whether the product is a subscription or not (consumable) + 990_000L); // The product price in micros + +FakeGooglePlayBillingApi.addTestProduct(product) +``` + +```FakeGooglePlayBillingApi``` uses predefined private key to sign purchase receipt. +If you want to verify purchase signature in your code, use corresponding public key defined in +```FakeGooglePlayBillingApi.TEST_PUBLIC_KEY```. + +## Migrating from In App Billing to Google Play Billing + +All you need to do is change vendor implementation from depracated `InAppBillingV3Vendor` to `GooglePlayBillingVendor`. +Since both implementations are just different ways to connect to Google Play Store, all your products and purchase +flows remain the same. + +1. In your dependencies replace +```compile 'com.getkeepsafe.cashier:cashier-iab:0.x.x' + debugCompile 'com.getkeepsafe.cashier:cashier-iab-debug:0.x.x' // For fake checkout and testing + releaseCompile 'com.getkeepsafe.cashier:cashier-iab-debug-no-op:0.x.x'``` + +with +``` +compile 'com.getkeepsafe.cashier:cashier-google-play-billing:0.x.x' +debugCompile 'com.getkeepsafe.cashier:cashier-google-play-billing-debug:0.x.x' // For fake checkout and testing +``` + +2. Replace `InAppBillingV3Vendor` with `GooglePlayBillingVendor`. To test the app in debug mode use `FakeGooglePlayBillingApi` in place of `FakeAppBillingV3Api`. +Definition of products remains the same, but now you need to add them by calling +```FakeGooglePlayBillingApi.addTestProduct(product)``` + +3. That's it! Now your app will use new Google Play Billing API!! + ## Sample App -For a buildable / workable sample app, please see the `cashier-sample` project under `cashier-sample/`. +For a buildable / workable sample app, please see the `cashier-sample-google-play-billing` project. ## Acknowledgements diff --git a/build.gradle b/build.gradle index b6b7e79..d99d4fd 100755 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.1.3' + classpath 'com.android.tools.build:gradle:3.3.1' classpath 'com.github.dcendents:android-maven-gradle-plugin:1.5' } } diff --git a/cashier-sample/.gitignore b/cashier-google-play-billing-debug/.gitignore similarity index 100% rename from cashier-sample/.gitignore rename to cashier-google-play-billing-debug/.gitignore diff --git a/cashier-google-play-billing-debug/build.gradle b/cashier-google-play-billing-debug/build.gradle new file mode 100644 index 0000000..073c9ea --- /dev/null +++ b/cashier-google-play-billing-debug/build.gradle @@ -0,0 +1,41 @@ +apply plugin: 'com.android.library' +apply plugin: 'com.github.dcendents.android-maven' + +android { + compileSdkVersion versions.compileSdk + buildToolsVersion versions.buildTools + + defaultConfig { + minSdkVersion versions.minSdk + } + + buildTypes { + release { + minifyEnabled false + } + } + testOptions { + unitTests.all { + testLogging { + exceptionFormat 'full' + showStackTraces true + showCauses true + events "passed", "skipped", "failed", "standardError" + } + } + } +} + +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 + testImplementation deps.mockito + testImplementation deps.truth +} diff --git a/cashier-google-play-billing-debug/src/main/AndroidManifest.xml b/cashier-google-play-billing-debug/src/main/AndroidManifest.xml new file mode 100644 index 0000000..b6ee83b --- /dev/null +++ b/cashier-google-play-billing-debug/src/main/AndroidManifest.xml @@ -0,0 +1,13 @@ + + + + + + + + + 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 new file mode 100644 index 0000000..798bafc --- /dev/null +++ b/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayBillingApi.java @@ -0,0 +1,242 @@ +package com.getkeepsafe.cashier.billing.debug; + +import android.app.Activity; +import android.content.Context; +import android.os.Handler; +import android.os.Looper; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.annotation.VisibleForTesting; + +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.ConsumeResponseListener; +import com.android.billingclient.api.Purchase; +import com.android.billingclient.api.SkuDetails; +import com.android.billingclient.api.SkuDetailsResponseListener; +import com.getkeepsafe.cashier.Product; +import com.getkeepsafe.cashier.billing.AbstractGooglePlayBillingApi; +import com.getkeepsafe.cashier.billing.GooglePlayBillingVendor; +import com.getkeepsafe.cashier.logging.Logger; + +import org.json.JSONException; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.Set; + +public class FakeGooglePlayBillingApi extends AbstractGooglePlayBillingApi { + + @VisibleForTesting + static final String TEST_PRIVATE_KEY = + "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBALXolIcA1LIcYDnO\n" + + "2nfalbkOD2UAQ3KfqsdEGLddG2rW8Cyl2LIyiWVvQ6bp2q5qBoYCds9lBQT21uo1\n" + + "VHTcv4mnaLfdBjMlzecrK8y1FzRLKFXyoMqiau8wunFeqFsdzHQ774PbYyNgMGdr\n" + + "zUDXqIdQONL8Eq/0pgddk07uNxwbAgMBAAECgYAJInvK57zGkOw4Gu4XlK9uEomt\n" + + "Xb0FVYVC6mV/V7qXu+FlrJJcKHOD13mDOT0VAxf+xMLomT8OR8L1EeaC087+aeza\n" + + "twYUVx4d+J0cQ8xo3ILwY5Bg4/Y4R0gIbdKupHbhPKaLSAiMxilNKqNfY8upT2X/\n" + + "S4OFDDbm7aK8SlGPEQJBAN+YlMb4PS54aBpWgeAP8fzgtOL0Q157bmoQyCokiWv3\n" + + "OGa89LraifCtlsqmmAxyFbPzO2cFHYvzzEeU86XZVFkCQQDQRWQ0QJKJsfqxEeYG\n" + + "rq9e3TkY8uQeHz8BmgxRcYC0v43bl9ggAJAzh9h9o0X9da1YzkoQ0/cWUp5NK95F\n" + + "93WTAkEAxqm1/rcO/RwEOuqDyIXCVxF8Bm5K8UawCtNQVYlTBDeKyFW5B9AmYU6K\n" + + "vRGZ5Oz0dYd2TwlPgEqkRTGF7eSUOQJAfyK85oC8cz2oMMsiRdYAy8Hzht1Oj2y3\n" + + "g3zMJDNLRArix7fLgM2XOT2l1BwFL5HUPa+/2sHpxUCtzaIHz2Id7QJATyF+fzUR\n" + + "eVw04ogIsOIdG0ECrN5/3g9pQnAjxcReQ/4KVCpIE8lQFYjAzQYUkK9VOjX9LYp9\n" + + "DGEnpooCco1ZjA=="; + + /** + * {@link com.getkeepsafe.cashier.billing.debug.FakeGooglePlayBillingApi} is using predefined + * private key to sign purchase receipt. Use this matching public key if you want to verify + * signature in your code. + */ + public static final String TEST_PUBLIC_KEY = + "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC16JSHANSyHGA5ztp32pW5Dg9l\n" + + "AENyn6rHRBi3XRtq1vAspdiyMollb0Om6dquagaGAnbPZQUE9tbqNVR03L+Jp2i3\n" + + "3QYzJc3nKyvMtRc0SyhV8qDKomrvMLpxXqhbHcx0O++D22MjYDBna81A16iHUDjS\n" + + "/BKv9KYHXZNO7jccGwIDAQAB"; + + private static final Set testProducts = new HashSet<>(); + private static final Set testInappPurchases = new HashSet<>(); + private static final Set testSubPurchases = new HashSet<>(); + + private static final Map pendingPurchases = new HashMap<>(); + + private GooglePlayBillingVendor vendor; + + private Handler mainHandler = new Handler(Looper.getMainLooper()); + + public FakeGooglePlayBillingApi(Context context) { + this(context, TEST_PRIVATE_KEY); + } + + public FakeGooglePlayBillingApi(Context context, String privateKey64) { + } + + public static void addTestProduct(Product product) { + testProducts.add(product); + } + + /** + * Notifies pending purchase listeners of successful transaction + * @param sku Sku of purchased product + * @param purchase Purchase object representing successful transaction + */ + static void notifyPurchaseSuccess(String sku, Purchase purchase) { + FakePurchaseListener listener = pendingPurchases.get(sku); + if (listener != null) { + listener.onFakePurchaseSuccess(purchase); + } + } + + /** + * Notifies pending purchase listeners of transation error + * @param sku Sku of purchased product + * @param responseCode Error code + */ + static void notifyPurchaseError(String sku, int responseCode) { + FakePurchaseListener listener = pendingPurchases.get(sku); + if (listener != null) { + listener.onFakePurchaseError(responseCode); + } + } + + @Override + public boolean initialize(@NonNull Context context, @NonNull GooglePlayBillingVendor vendor, LifecycleListener listener, Logger logger) { + super.initialize(context, vendor, listener, logger); + this.vendor = vendor; + listener.initialized(true); + return true; + } + + @Override + public boolean available() { + return true; + } + + @Override + public void dispose() { + } + + @Override + public int isBillingSupported(String itemType) { + return BillingClient.BillingResponse.OK; + } + + @Override + public void launchBillingFlow(@NonNull Activity activity, @NonNull final String sku, final String itemType) { + for (Product product : testProducts) { + if (product.sku().equals(sku)) { + activity.startActivity(FakeGooglePlayCheckoutActivity.intent(activity, product, TEST_PRIVATE_KEY)); + + // Put listener to pendingPurchases map and wait until either + // notifyPurchaseSuccess or notifyPurchaseError is called from FakeGooglePlayCheckoutActivity + pendingPurchases.put(sku, new FakePurchaseListener() { + @Override + public void onFakePurchaseSuccess(Purchase purchase) { + pendingPurchases.remove(sku); + if (itemType.equals(BillingClient.SkuType.SUBS)) { + testSubPurchases.add(purchase); + } else { + testInappPurchases.add(purchase); + } + vendor.onPurchasesUpdated(BillingClient.BillingResponse.OK, Collections.singletonList(purchase)); + } + + @Override + public void onFakePurchaseError(int responseCode) { + pendingPurchases.remove(sku); + vendor.onPurchasesUpdated(responseCode, null); + } + }); + return; + } + } + } + + @Nullable + @Override + public List getPurchases() { + ArrayList purchases = new ArrayList<>(); + purchases.addAll(testInappPurchases); + purchases.addAll(testSubPurchases); + return purchases; + } + + @Nullable + @Override + public List getPurchases(String itemType) { + if (itemType.equals(BillingClient.SkuType.SUBS)) { + return new ArrayList<>(testSubPurchases); + } else { + return new ArrayList<>(testInappPurchases); + } + } + + @Override + public void consumePurchase(final @NonNull String purchaseToken, final @NonNull ConsumeResponseListener 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.onConsumeResponse(BillingClient.BillingResponse.OK, purchaseToken); + } + }); + } + }.start(); + } + + @Override + public void getSkuDetails(final String itemType, final @NonNull List skus, final @NonNull SkuDetailsResponseListener 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) {} + + final List details = new ArrayList<>(); + for (Product product : testProducts) { + if (skus.contains(product.sku())) { + try { + details.add(new FakeSkuDetails(product)); + } catch (JSONException e) { + } + } + } + + // Return result on main thread + mainHandler.post(new Runnable() { + @Override + public void run() { + listener.onSkuDetailsResponse(BillingClient.BillingResponse.OK, details); + } + }); + } + }.start(); + } + + public static interface FakePurchaseListener { + void onFakePurchaseSuccess(Purchase purchase); + void onFakePurchaseError(int responseCode); + } +} 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 new file mode 100644 index 0000000..3032aeb --- /dev/null +++ b/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeGooglePlayCheckoutActivity.java @@ -0,0 +1,98 @@ +package com.getkeepsafe.cashier.billing.debug; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.graphics.Typeface; +import android.os.Bundle; +import android.text.SpannableString; +import android.text.style.StyleSpan; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; + +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.Purchase; +import com.getkeepsafe.cashier.Product; +import com.getkeepsafe.cashier.billing.GooglePlayBillingSecurity; + +import org.json.JSONException; +import org.json.JSONObject; + +public class FakeGooglePlayCheckoutActivity extends Activity { + + private static final String ARGUMENT_PRODUCT = "product"; + + private Product product; + + public static Intent intent(Context context, Product product, String privateKey64) { + Intent intent = new Intent(context, FakeGooglePlayCheckoutActivity.class); + intent.putExtra(ARGUMENT_PRODUCT, product); + return intent; + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_fake_checkout); + + final Intent intent = getIntent(); + product = intent.getParcelableExtra(ARGUMENT_PRODUCT); + + final TextView productName = bind(R.id.product_name); + final TextView productDescription = bind(R.id.product_description); + final TextView productPrice = bind(R.id.product_price); + final TextView productMetadata = bind(R.id.product_metadata); + final Button buyButton = bind(R.id.buy); + + productName.setText(product.name()); + productDescription.setText(product.description()); + productPrice.setText(product.price()); + + productMetadata.setText(String.valueOf( + metadataField("Vendor", product.vendorId())) + + metadataField("SKU", product.sku()) + + metadataField("Subscription", product.isSubscription()) + + metadataField("Micro-price", product.microsPrice()) + + metadataField("Currency", product.currency())); + + buyButton.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + try { + JSONObject purchaseJson = new JSONObject(); + purchaseJson.put("orderId", String.valueOf(System.currentTimeMillis())); + purchaseJson.put("purchaseToken", product.sku() + "_" + System.currentTimeMillis()); + purchaseJson.put("purchaseState", 0); + purchaseJson.put("productId", product.sku()); + String json = purchaseJson.toString(); + String signature = GooglePlayBillingSecurity.sign(FakeGooglePlayBillingApi.TEST_PRIVATE_KEY, json); + Purchase purchase = new Purchase(json, signature); + + FakeGooglePlayBillingApi.notifyPurchaseSuccess(product.sku(), purchase); + + } catch (JSONException e) { + FakeGooglePlayBillingApi.notifyPurchaseError(product.sku(), BillingClient.BillingResponse.SERVICE_UNAVAILABLE); + } + finish(); + } + }); + } + + @Override + public void onBackPressed() { + super.onBackPressed(); + FakeGooglePlayBillingApi.notifyPurchaseError(product.sku(), BillingClient.BillingResponse.USER_CANCELED); + } + + private SpannableString metadataField(String name, Object value) { + final SpannableString string = new SpannableString(name + ": " + value.toString() + "\n"); + string.setSpan(new StyleSpan(Typeface.BOLD), 0, name.length() + 1, 0); + return string; + } + + @SuppressWarnings("unchecked") + private T bind(int id) { + return (T) findViewById(id); + } +} 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 new file mode 100644 index 0000000..182ed53 --- /dev/null +++ b/cashier-google-play-billing-debug/src/main/java/com/getkeepsafe/cashier/billing/debug/FakeSkuDetails.java @@ -0,0 +1,56 @@ +package com.getkeepsafe.cashier.billing.debug; + +import com.android.billingclient.api.BillingClient; +import com.android.billingclient.api.SkuDetails; +import com.getkeepsafe.cashier.Product; + +import org.json.JSONException; + +/** + * Fake sku details query result. SkuDetails requires json to be constructed, + * this class overrides mostly used fields to read values from Product directly. + */ +public class FakeSkuDetails extends SkuDetails { + + private Product product; + + public FakeSkuDetails(Product product) throws JSONException { + super("{}"); + this.product = product; + } + + @Override + public String getTitle() { + return product.name(); + } + + @Override + public String getDescription() { + return product.description(); + } + + @Override + public String getSku() { + return product.sku(); + } + + @Override + public String getType() { + return product.isSubscription() ? BillingClient.SkuType.SUBS : BillingClient.SkuType.INAPP; + } + + @Override + public String getPrice() { + return product.price(); + } + + @Override + public long getPriceAmountMicros() { + return product.microsPrice(); + } + + @Override + public String getPriceCurrencyCode() { + return product.currency(); + } +} diff --git a/cashier-google-play-billing-debug/src/main/res/layout/activity_fake_checkout.xml b/cashier-google-play-billing-debug/src/main/res/layout/activity_fake_checkout.xml new file mode 100644 index 0000000..f23ac78 --- /dev/null +++ b/cashier-google-play-billing-debug/src/main/res/layout/activity_fake_checkout.xml @@ -0,0 +1,135 @@ + + + + + + + + + + + + + + + + + + + +