├── .github └── workflows │ └── connected-check.yml ├── .gitignore ├── .idea └── codeStyleSettings.xml ├── CHANGELOG.md ├── ISSUE_TEMPLATE.md ├── LICENSE ├── README.md ├── UPGRADING.md ├── build.gradle ├── checkstyle.xml ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── library ├── build.gradle ├── progress-proguard.txt └── src │ ├── androidTest │ ├── java │ │ └── com │ │ │ └── anjlab │ │ │ └── android │ │ │ └── iab │ │ │ └── v3 │ │ │ ├── BillingHistoryRecordTest.java │ │ │ ├── PurchaseInfoParcelableTest.java │ │ │ ├── SkuDetailsParcelableTest.java │ │ │ └── util │ │ │ └── ResourcesUtil.java │ └── resources │ │ ├── purchase_history_response.json │ │ ├── purchase_info.json │ │ ├── sku_in_app.json │ │ ├── sku_subscription.json │ │ ├── sku_subscription_introductory.json │ │ ├── sku_subscription_trial.json │ │ └── transaction_details.json │ └── main │ ├── AndroidManifest.xml │ └── java │ └── com │ └── anjlab │ └── android │ └── iab │ └── v3 │ ├── BillingBase.java │ ├── BillingCache.java │ ├── BillingHistoryRecord.java │ ├── BillingProcessor.java │ ├── Constants.java │ ├── PurchaseData.java │ ├── PurchaseInfo.java │ ├── PurchaseState.java │ ├── Security.java │ └── SkuDetails.java ├── sample ├── AndroidManifest.xml ├── build.gradle ├── libs │ └── anjlab-iabv3-current.jar ├── res │ ├── drawable-anydpi │ │ └── ic_launcher.png │ ├── layout │ │ └── activity_main.xml │ └── values │ │ ├── strings.xml │ │ └── styles.xml └── src │ └── com │ └── anjlab │ └── android │ └── iab │ └── v3 │ └── sample2 │ └── MainActivity.java └── settings.gradle /.github/workflows/connected-check.yml: -------------------------------------------------------------------------------- 1 | name: Connected Check 2 | 3 | on: 4 | push: 5 | branches: 6 | - '*' 7 | 8 | pull_request: 9 | branches: 10 | - master 11 | 12 | jobs: 13 | run-connected-checks: 14 | # put [skip ci] into the commit message if you don't want to run this workflow 15 | 16 | runs-on: macos-13 17 | 18 | timeout-minutes: 15 19 | 20 | steps: 21 | - name: Cancel previous runs 22 | uses: styfle/cancel-workflow-action@0.12.1 23 | with: 24 | access_token: ${{ github.token }} 25 | 26 | - uses: actions/checkout@v4 27 | 28 | - uses: actions/setup-java@v4 29 | with: 30 | distribution: adopt 31 | java-version: 17 32 | 33 | - name: Set up build properties 34 | run: | 35 | echo 'licenceKey=""' > local.properties 36 | echo 'android.useAndroidX=true' >> gradle.properties 37 | echo 'android.enableJetifier=true' >> gradle.properties 38 | 39 | - name: Run connectedCheck 40 | uses: reactivecircus/android-emulator-runner@v2 41 | with: 42 | api-level: 33 43 | target: google_apis 44 | arch: x86_64 45 | script: ./gradlew connectedCheck -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin/ 2 | gen/ 3 | .idea/ 4 | !.idea/codeStyleSettings.xml 5 | build 6 | local.properties 7 | gradle.properties 8 | .gradle 9 | *.iml 10 | .DS_Store 11 | .classpath 12 | .project 13 | .settings/ 14 | project.properties -------------------------------------------------------------------------------- /.idea/codeStyleSettings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 229 | 231 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 1.0.44 (8/7/2017) 2 | 3 | #### Features 4 | 5 | * [#295](https://github.com/anjlab/android-inapp-billing-v3/pull/295): Address a bug with a developer payload check for the promo codes - [@serggl](https://github.com/serggl). 6 | * [#293](https://github.com/anjlab/android-inapp-billing-v3/pull/293): Nullability and javadocs - [@AllanWang](https://github.com/AllanWang). 7 | * [#289](https://github.com/anjlab/android-inapp-billing-v3/pull/289): Add proguard rule - [@AllanWang](https://github.com/AllanWang). 8 | 9 | ## 1.0.43 (7/24/2017) 10 | 11 | #### Features 12 | 13 | * [#287](https://github.com/anjlab/android-inapp-billing-v3/pull/287): Support for getBuyIntentExtraParams() - [@ratm](https://github.com/ratm). 14 | 15 | ## 1.0.42 (7/7/2017) 16 | 17 | #### Bug Fixes 18 | 19 | * [#286](https://github.com/anjlab/android-inapp-billing-v3/pull/286): Removed Joda Time dependency introduced in 1.0.41 - [@moni890185](https://github.com/moni890185). 20 | 21 | ## 1.0.41 (7/2/2017) 22 | 23 | #### Features 24 | 25 | * [#281](https://github.com/anjlab/android-inapp-billing-v3/pull/281): Support for introductory price on subscriptions - [@landarskiy](https://github.com/landarskiy). 26 | 27 | ## 1.0.40 (6/3/2017) 28 | 29 | #### Features 30 | 31 | * [#273](https://github.com/anjlab/android-inapp-billing-v3/pull/273): Added ability to include developer payload in updateSubscription() methods - [@autonomousapps](https://github.com/autonomousapps). 32 | 33 | #### Refactor 34 | 35 | * [#271](https://github.com/anjlab/android-inapp-billing-v3/pull/271): Converted single-element arraylist into singleton list - [@autonomousapps](https://github.com/autonomousapps). 36 | 37 | ## 1.0.39 (4/3/2017) 38 | 39 | #### Features 40 | 41 | * [#252](https://github.com/anjlab/android-inapp-billing-v3/pull/252): Created new factory constructors that allow for late-init of play services - [@autonomousapps](https://github.com/autonomousapps). 42 | 43 | ## 1.0.38 (1/1/2017) 44 | 45 | #### Bug Fixes 46 | 47 | * [#224](https://github.com/anjlab/android-inapp-billing-v3/pull/224): Minor type for the function isOneTimePurchaseSupported() - [@omerfarukyilmaz](https://github.com/omerfarukyilmaz). 48 | 49 | ## 1.0.37 (12/24/2016) 50 | 51 | #### Features 52 | 53 | * [#223](https://github.com/anjlab/android-inapp-billing-v3/pull/223): additional service availability checker - [@MedetZhakupov](https://github.com/MedetZhakupov). 54 | 55 | #### Docs 56 | * [#220](https://github.com/anjlab/android-inapp-billing-v3/pull/220): document some promo codes usage nuances - [@serggl](https://github.com/serggl). 57 | 58 | ## 1.0.36 (11/22/2016) 59 | 60 | #### Code Cleanup 61 | 62 | * [deprecate PurchaseInfo.parseResponseData](https://github.com/anjlab/android-inapp-billing-v3/commit/d0d5492df200a3e7d324d7dacf8d364428554449) - [@serggl](https://github.com/serggl). 63 | 64 | ## 1.0.35 (11/22/2016) 65 | 66 | #### Bug Fixes 67 | 68 | * [#210](https://github.com/anjlab/android-inapp-billing-v3/issues/210): address null pointer issue in isIabServiceAvailable - [@serggl](https://github.com/serggl). -------------------------------------------------------------------------------- /ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Do you want to request a **feature** or report a **bug**? 2 | 3 | What is the current behavior? 4 | 5 | If the current behavior is a bug, **please provide the steps to reproduce** and if possible a minimal demo of the problem or a code snippet. 6 | Don't forget to mention: 7 | - which version of library you use 8 | - was it working in previous versions? 9 | - do you use fragments or not 10 | - how do you instanciate `BillingProcessor` (singleton or not) 11 | - which device/OS version do you use for testing (or its an emulator) 12 | - have you uploaded it to Google Play or not (if yes, when which channel: Prod/Beta/Alpha) 13 | - do you test with real products, or with a testing onces (e.g. `android.test.purchased`) 14 | 15 | The more details you provide - the faster you'll get an answer from community 16 | 17 | If you have a **general question**, please flag it in the title: "Question: how do I ..." -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2014 AnjLab 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Android In-App Billing v3 Library [![Build Status](https://github.com/anjlab/android-inapp-billing-v3/actions/workflows/connected-check.yml/badge.svg)](https://github.com/anjlab/android-inapp-billing-v3/actions/workflows/connected-check.yml) [![Maven Central](https://maven-badges.herokuapp.com/maven-central/com.anjlab.android.iab.v3/library/badge.svg)](https://maven-badges.herokuapp.com/maven-central/com.anjlab.android.iab.v3/library) 2 | This is a simple, straight-forward implementation of the Android v4 In-app billing API. 3 | 4 | It supports: In-App Product Purchases (both non-consumable and consumable) and Subscriptions. 5 | 6 | ## Maintainers Wanted 7 | 8 | This project is looking for maintainers. 9 | 10 | For now only pull requests of external contributors are being reviewed, accepted and welcomed. No more bug fixes or new features will be implemented by the Anjlab team. 11 | 12 | If you are interesting in giving this project some :heart:, please chime in! 13 | 14 | ## v4 API Upgrade Notice 15 | 16 | Originally this was Google's v2 Billing API implementation, for those who interested all source code kept safe [here](https://github.com/anjlab/android-inapp-billing-v3/tree/v2_billing_1_1_0). 17 | 18 | If you got your app using this library previously, here is the [Migration Guide](https://github.com/anjlab/android-inapp-billing-v3/blob/master/UPGRADING.md). 19 | 20 | ## Getting Started 21 | 22 | * You project should build against Android 4.0 SDK at least. 23 | 24 | * Add this *Android In-App Billing v3 Library* to your project: 25 | - If you guys are using Eclipse, download latest jar version from the [releases](https://github.com/anjlab/android-inapp-billing-v3/releases) section of this repository and add it as a dependency 26 | - If you guys are using Android Studio and Gradle, add this to you build.gradle file: 27 | ```groovy 28 | repositories { 29 | mavenCentral() 30 | } 31 | dependencies { 32 | implementation 'com.anjlab.android.iab.v3:library:2.0.3' 33 | } 34 | ``` 35 | 36 | * Create instance of BillingProcessor class and implement callback in your Activity source code. Constructor will take 3 parameters: 37 | - **Context** 38 | - **Your License Key from Google Developer console.** This will be used to verify purchase signatures. You can pass NULL if you would like to skip this check (*You can find your key in Google Play Console -> Your App Name -> Services & APIs*) 39 | - **IBillingHandler Interface implementation to handle purchase results and errors** (see below) 40 | ```java 41 | public class SomeActivity extends Activity implements BillingProcessor.IBillingHandler { 42 | BillingProcessor bp; 43 | 44 | @Override 45 | protected void onCreate(Bundle savedInstanceState) { 46 | super.onCreate(savedInstanceState); 47 | setContentView(R.layout.activity_main); 48 | 49 | bp = new BillingProcessor(this, "YOUR LICENSE KEY FROM GOOGLE PLAY CONSOLE HERE", this); 50 | bp.initialize(); 51 | // or bp = BillingProcessor.newBillingProcessor(this, "YOUR LICENSE KEY FROM GOOGLE PLAY CONSOLE HERE", this); 52 | // See below on why this is a useful alternative 53 | } 54 | 55 | // IBillingHandler implementation 56 | 57 | @Override 58 | public void onBillingInitialized() { 59 | /* 60 | * Called when BillingProcessor was initialized and it's ready to purchase 61 | */ 62 | } 63 | 64 | @Override 65 | public void onProductPurchased(String productId, PurchaseInfo purchaseInfo) { 66 | /* 67 | * Called when requested PRODUCT ID was successfully purchased 68 | */ 69 | } 70 | 71 | @Override 72 | public void onBillingError(int errorCode, Throwable error) { 73 | /* 74 | * Called when some error occurred. See Constants class for more details 75 | * 76 | * Note - this includes handling the case where the user canceled the buy dialog: 77 | * errorCode = Constants.BILLING_RESPONSE_RESULT_USER_CANCELED 78 | */ 79 | } 80 | 81 | @Override 82 | public void onPurchaseHistoryRestored() { 83 | /* 84 | * Called when purchase history was restored and the list of all owned PRODUCT ID's 85 | * was loaded from Google Play 86 | */ 87 | } 88 | } 89 | ``` 90 | 91 | * Call `purchase` method for a BillingProcessor instance to initiate purchase or `subscribe` to initiate a subscription: 92 | 93 | ```java 94 | bp.purchase(YOUR_ACTIVITY, "YOUR PRODUCT ID FROM GOOGLE PLAY CONSOLE HERE"); 95 | bp.subscribe(YOUR_ACTIVITY, "YOUR SUBSCRIPTION ID FROM GOOGLE PLAY CONSOLE HERE"); 96 | ``` 97 | 98 | 99 | * **That's it! A super small and fast in-app library ever!** 100 | 101 | * **And don't forget** 102 | to release your BillingProcessor instance! 103 | ```java 104 | @Override 105 | public void onDestroy() { 106 | if (bp != null) { 107 | bp.release(); 108 | } 109 | super.onDestroy(); 110 | } 111 | ``` 112 | 113 | ### Instantiating a `BillingProcessor` with late initialization 114 | The basic `new BillingProcessor(...)` actually binds to Play Services inside the constructor. This can, very rarely, lead to a race condition where Play Services are bound and `onBillingInitialized()` is called before the constructor finishes, and can lead to NPEs. To avoid this, we have the following: 115 | ```java 116 | bp = BillingProcessor.newBillingProcessor(this, "YOUR LICENSE KEY FROM GOOGLE PLAY CONSOLE HERE", this); // doesn't bind 117 | bp.initialize(); // binds 118 | ``` 119 | 120 | ## Testing In-app Billing 121 | 122 | Here is a [complete guide](https://developer.android.com/google/play/billing/billing_testing.html). 123 | Make sure you read it before you start testing 124 | 125 | ## Check Play Market services availability 126 | 127 | Before any usage it's good practice to check in-app billing services availability. 128 | In some older devices or chinese ones it may happen that Play Market is unavailable or is deprecated 129 | and doesn't support in-app billing. 130 | 131 | Simply call static method `BillingProcessor.isIabServiceAvailable(context)`: 132 | ```java 133 | boolean isAvailable = BillingProcessor.isIabServiceAvailable(this); 134 | if(!isAvailable) { 135 | // continue 136 | } 137 | ``` 138 | Please notice that calling `BillingProcessor.isIabServiceAvailable()` (only checks Play Market app installed or not) is not enough because there might be a case when it returns true but still payment won't succeed. 139 | Therefore, it's better to call `bp.isConnected()` after initializing `BillingProcessor`: 140 | ```java 141 | boolean isConnected = billingProcessor.isConnected(); 142 | if(isConnected) { 143 | // launch payment flow 144 | } 145 | ``` 146 | or call `isSubscriptionUpdateSupported()` for checking update subscription use case: 147 | ```java 148 | boolean isSubsUpdateSupported = billingProcessor.isSubscriptionUpdateSupported(); 149 | if(isSubsUpdateSupported) { 150 | // launch payment flow 151 | } 152 | ``` 153 | 154 | ## Consume Purchased Products 155 | 156 | You can always consume made purchase and allow to buy same product multiple times. To do this you need: 157 | ```java 158 | bp.consumePurchaseAsync("YOUR PRODUCT ID FROM GOOGLE PLAY CONSOLE HERE", new IPurchasesResponseListener()); 159 | ``` 160 | 161 | ## Restore Purchases & Subscriptions 162 | 163 | ```java 164 | bp.loadOwnedPurchasesFromGoogleAsync(new IPurchasesResponseListener()); 165 | ``` 166 | 167 | ## Getting Listing Details of Your Products 168 | 169 | To query listing price and a description of your product / subscription listed in Google Play use these methods: 170 | 171 | ```java 172 | bp.getPurchaseListingDetailsAsync("YOUR PRODUCT ID FROM GOOGLE PLAY CONSOLE HERE", new ISkuDetailsResponseListener()); 173 | bp.getSubscriptionListingDetailsAsync("YOUR SUBSCRIPTION ID FROM GOOGLE PLAY CONSOLE HERE", new ISkuDetailsResponseListener()); 174 | ``` 175 | 176 | As a result you will get a callback call including `List` data with one SkuDetails object with the following info included: 177 | 178 | ```java 179 | public final String productId; 180 | public final String title; 181 | public final String description; 182 | public final boolean isSubscription; 183 | public final String currency; 184 | public final Double priceValue; 185 | public final String priceText; 186 | ``` 187 | 188 | To get info for multiple products / subscriptions on one query, just pass a list of product ids: 189 | 190 | ```java 191 | bp.getPurchaseListingDetailsAsync(arrayListOfProductIds, new ISkuDetailsResponseListener()); 192 | bp.getSubscriptionListingDetailsAsync(arrayListOfProductIds, new ISkuDetailsResponseListener()); 193 | ``` 194 | 195 | where arrayListOfProductIds is a `ArrayList` containing either IDs for products or subscriptions. 196 | 197 | 198 | ## Getting Purchase Info Details 199 | `PurchaseInfo` object is passed to `onProductPurchased` method of a handler class. 200 | However, you can always retrieve it later calling these methods: 201 | 202 | ```java 203 | bp.getPurchaseInfo("YOUR PRODUCT ID FROM GOOGLE PLAY CONSOLE HERE"); 204 | bp.getSubscriptionPurchaseInfo("YOUR SUBSCRIPTION ID FROM GOOGLE PLAY CONSOLE HERE"); 205 | ``` 206 | 207 | As a result you will get a `PurchaseInfo` object with the following info included: 208 | 209 | ```java 210 | public final String responseData; 211 | public final String signature; 212 | 213 | // PurchaseData contains orderId, productId, purchaseTime, purchaseToken, purchaseState and autoRenewing fields 214 | public final PurchaseData purchaseData; 215 | ``` 216 | 217 | ## Handle Canceled Subscriptions 218 | 219 | Call `bp.getSubscriptionPurchaseInfo(...)` and check the `purchaseData.autoRenewing` flag. 220 | It will be set to `False` once subscription gets cancelled. 221 | Also notice, that you will need to call periodically `bp.loadOwnedPurchasesFromGoogleAsync()` method in order to update subscription information 222 | 223 | ## Promo Codes Support 224 | 225 | You can use promo codes along with this library. Promo codes can be entered in the purchase dialog or in the Google Play app. The URL https://play.google.com/redeem?code=YOUR_PROMO_CODE will launch the Google Play app with the promo code already entered. This could come in handy if you want to give users the option to enter a promo code within your app. 226 | 227 | ## Protection Against Fake "Markets" 228 | 229 | There are number of attacks which exploits some vulnerabilities of Google's Play Market. 230 | Among them is so-called *Freedom attack*: *Freedom* is special Android application, which 231 | intercepts application calls to Play Market services and substitutes them with fake ones. So in the 232 | end attacked application *thinks* that it receives valid responses from Play Market. 233 | 234 | In order to protect from this kind of attack you should specify your `merchantId`, which 235 | can be found in your [Payments Merchant Account](https://payments.google.com/merchant). 236 | Selecting *Settings->Public Profile* you will find your unique `merchantId` 237 | 238 | **WARNING:** keep your `merchantId` in safe place! 239 | 240 | Then using `merchantId` just call constructor: 241 | 242 | public BillingProcessor(Context context, String licenseKey, String merchantId, IBillingHandler handler); 243 | 244 | Later one can easily check transaction validity using method: 245 | 246 | public boolean isValidPurchaseInfo(PurchaseInfo purchaseInfo); 247 | 248 | P.S. This kind of protection works only for transactions dated between 5th December 2012 and 249 | 21st July 2015. Before December 2012 `orderId` wasn't contain `merchantId` and in the end of July this 250 | year Google suddenly changed `orderId` format. 251 | 252 | ## Proguard 253 | 254 | The necessary proguard rules are already added in the library. No further configurations are needed. 255 | 256 | The contents in the consumer proguard file contains: 257 | 258 | ``` 259 | -keep class com.android.vending.billing.** 260 | ``` 261 | 262 | ## License 263 | 264 | Copyright 2021 AnjLab 265 | 266 | Licensed under the Apache License, Version 2.0 (the "License"); 267 | you may not use this file except in compliance with the License. 268 | You may obtain a copy of the License at 269 | 270 | http://www.apache.org/licenses/LICENSE-2.0 271 | 272 | Unless required by applicable law or agreed to in writing, software 273 | distributed under the License is distributed on an "AS IS" BASIS, 274 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 275 | See the License for the specific language governing permissions and 276 | limitations under the License. 277 | 278 | ## Contributing 279 | 280 | 1. Fork it 281 | 2. Create your feature branch (`git checkout -b my-new-feature`) 282 | 3. Commit your changes (`git commit -am 'Add some feature'`) 283 | 4. Push to the branch (`git push origin my-new-feature`) 284 | 5. **Create New Pull Request** 285 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | ## Upgrading Android In-App Billing v3 Library 2 | 3 | ### Upgrading from 2.0.x to 2.1.0 4 | 5 | This release updates `com.android.billingclient:billing` library from version 4 to version 6. 6 | While your apps will probably continue to work, you still need to follow steps from official 7 | upgrade guide https://developer.android.com/google/play/billing/migrate-gpblv6 to make sure you're 8 | ready for future library updates from Google 9 | 10 | ### Upgrading from 1.x to 2.0.0 11 | 12 | Starting from Nov 1, 2021 Google will stop supporting v2 billing client library used as a dependency here. 13 | This library was upgraded accordingly (thanks to @Equin and @showdpro), but this led to some major braking changes: 14 | 15 | 1. These methods were renamed: 16 | - `getSubscriptionTransactionDetails` renamed to `getSubscriptionPurchaseInfo` 17 | - `isValidTransactionDetails` renamed to `isValidPurchaseInfo` 18 | 1. Consume/purchase related methods to not accept `developerPayload` argument anymore (dropped support by Google). If you used it - you'll need to find the workaroud 19 | 1. Some synchronous methods were dropped in favour of their asynchronous versions (which have success/failure callback as their last argument): 20 | - use `consumePurchaseAsync` instead of `consumePurchase` 21 | - use `loadOwnedPurchasesFromGoogleAsync` instead of `loadOwnedPurchasesFromGoogle` 22 | - use `getPurchaseListingDetailsAsync` instead of `getPurchaseListingDetails` 23 | - use `getSubscriptionListingDetailsAsync` instead of `getSubscriptionListingDetails` 24 | 1. Deprecated `TransactionDetails` class has been removed. Please use `PurchaseInfo` instead, here is the property mapping: 25 | - TransactionDetails (productId) -> PurchaseInfo (purchaseData.productId) 26 | - TransactionDetails (orderId) -> PurchaseInfo (purchaseInfo.purchaseData.orderId) 27 | - TransactionDetails (purchaseToken) -> PurchaseInfo (purchaseInfo.purchaseData.purchaseToken) 28 | - TransactionDetails (purchaseTime) -> PurchaseInfo (purchaseInfo.purchaseData.purchaseTime) 29 | 1. `handleActivityResult` method was removed, you don't need to override your app's `onActivityResult` anymore 30 | 1. Some billing flow related constants were removed from `Constants` class (`BILLING_RESPONSE_*` constants). if your app relies on those - use `BillingClient.BillingResponseCode.*` constants instead 31 | 32 | ### Upgrading to >= 1.0.44 33 | 34 | The workaround below for the promo codes should no longer be valid. Promo codes should work just fine right out of the box 35 | 36 | ### Upgrading to >= 1.0.37 37 | 38 | If you were supporting promo codes and faced troubled described in #156, 39 | you will need to change your workaround code: 40 | 41 | ```java 42 | errorCode == Constants.BILLING_ERROR_OTHER_ERROR && _billingProcessor.loadOwnedPurchasesFromGoogle() && _billingProcessor.isPurchased(SKU) 43 | ``` 44 | 45 | `errorCode` needs to be changed to `Constants.BILLING_ERROR_INVALID_DEVELOPER_PAYLOAD` 46 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | buildscript { 3 | repositories { 4 | mavenCentral() 5 | jcenter() 6 | maven { 7 | url 'https://maven.google.com/' 8 | name 'Google' 9 | } 10 | maven { 11 | url "https://plugins.gradle.org/m2/" 12 | } 13 | google() 14 | } 15 | dependencies { 16 | classpath 'com.android.tools.build:gradle:7.4.2' 17 | classpath "gradle.plugin.com.hierynomus.gradle.plugins:license-gradle-plugin:0.16.1" 18 | classpath "com.google.android.libraries.mapsplatform.secrets-gradle-plugin:secrets-gradle-plugin:1.2.0" 19 | } 20 | } 21 | 22 | allprojects { 23 | apply plugin: "com.github.hierynomus.license" 24 | 25 | repositories { 26 | mavenCentral() 27 | jcenter() 28 | maven { 29 | url 'https://maven.google.com/' 30 | name 'Google' 31 | } 32 | } 33 | 34 | license { 35 | header rootProject.file('LICENSE') 36 | strictCheck true 37 | include "**/*.java" 38 | exclude "**/*Test.java" 39 | exclude "com/anjlab/android/iab/v3/util/ResourcesUtil.java" 40 | exclude "com/anjlab/android/iab/v3/Security.java" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /checkstyle.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjlab/android-inapp-billing-v3/a13bb215b1176eac9d1d78322262de2bd237303e/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Tue Sep 14 14:17:11 EEST 2021 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip 7 | android.enableJetifier=true 8 | android.useAndroidX=true 9 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /library/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | apply plugin: 'maven-publish' 3 | apply plugin: 'signing' 4 | apply plugin: 'checkstyle' 5 | 6 | android { 7 | 8 | defaultConfig { 9 | compileSdk = 34 10 | buildToolsVersion = '30.0.3' 11 | 12 | minSdkVersion 21 13 | targetSdkVersion 34 14 | consumerProguardFiles 'progress-proguard.txt' 15 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 16 | } 17 | 18 | testOptions.unitTests { 19 | // Don't throw runtime exceptions for android calls that are not mocked 20 | returnDefaultValues = true 21 | 22 | // Always show the result of every unit test, even if it passes. 23 | all { 24 | testLogging { 25 | events 'passed', 'skipped', 'failed', 'standardOut', 'standardError' 26 | } 27 | } 28 | } 29 | } 30 | 31 | dependencies { 32 | androidTestImplementation 'androidx.test.ext:junit:1.1.3' 33 | androidTestImplementation 'androidx.test:rules:1.4.0' 34 | androidTestImplementation 'androidx.test:runner:1.4.0' 35 | implementation 'androidx.annotation:annotation:1.3.0' 36 | implementation 'com.android.billingclient:billing:7.0.0' 37 | } 38 | 39 | task androidJavadocs(type: Javadoc) { 40 | source = android.sourceSets.main.java.srcDirs 41 | classpath += project.files(android.getBootClasspath().join(File.pathSeparator)) 42 | android.libraryVariants.all { variant -> 43 | if (variant.name == 'release') { 44 | owner.classpath += variant.javaCompileProvider.get().classpath 45 | } 46 | } 47 | exclude '**/R.html', '**/R.*.html', '**/index.html' 48 | } 49 | 50 | task androidJavadocsJar(type: Jar, dependsOn: androidJavadocs) { 51 | archiveClassifier.set('javadoc') 52 | from androidJavadocs.destinationDir 53 | } 54 | 55 | task androidSourcesJar(type: Jar) { 56 | archiveClassifier.set('sources') 57 | from android.sourceSets.main.java.srcDirs 58 | } 59 | 60 | afterEvaluate { 61 | publishing { 62 | publications { 63 | maven(MavenPublication) { 64 | from components.release 65 | 66 | artifact androidJavadocsJar 67 | artifact androidSourcesJar 68 | 69 | groupId = 'com.anjlab.android.iab.v3' 70 | artifactId = 'library' 71 | version = '2.2.0' 72 | 73 | pom { 74 | name = 'Android In-App Billing v3 Library' 75 | description = 'A lightweight implementation of Android In-app Billing Version 3' 76 | url = 'https://github.com/anjlab/android-inapp-billing-v3' 77 | packaging = 'aar' 78 | licenses { 79 | license { 80 | name = 'The Apache Software License, Version 2.0' 81 | url = 'http://www.apache.org/licenses/LICENSE-2.0.txt' 82 | distribution = 'repo' 83 | } 84 | } 85 | developers { 86 | developer { 87 | id = 'serggl' 88 | name = 'Sergey Glukhov' 89 | email = 'sergey.glukhov@gmail.com' 90 | } 91 | } 92 | scm { 93 | url = 'scm:git@github.com:anjlab/android-inapp-billing-v3.git' 94 | connection = 'scm:git@github.com:anjlab/android-inapp-billing-v3.git' 95 | developerConnection = 'scm:git@github.com:anjlab/android-inapp-billing-v3.git' 96 | } 97 | } 98 | } 99 | } 100 | if (project.hasProperty('sonatypeRepo')) { 101 | repositories { 102 | maven { 103 | url sonatypeRepo 104 | credentials { 105 | username sonatypeUsername 106 | password sonatypePassword 107 | } 108 | } 109 | } 110 | } 111 | } 112 | 113 | signing { 114 | required { gradle.taskGraph.hasTask("publish") } 115 | sign publishing.publications.maven 116 | } 117 | } 118 | 119 | task checkstyle(type: Checkstyle) { 120 | configFile file("${project.rootDir}/checkstyle.xml") 121 | 122 | source 'src/main/java' 123 | include '**/*.java' 124 | exclude '**/gen/**' 125 | 126 | classpath = project.files(android.getBootClasspath()) 127 | } 128 | 129 | check.dependsOn('checkstyle') 130 | connectedCheck.dependsOn('checkstyle') 131 | 132 | android.libraryVariants.all { variant -> 133 | def name = variant.buildType.name 134 | if (name.equals('com.android.builder.BuilderConstants.DEBUG')) { 135 | return; // Skip debug builds. 136 | } 137 | def task = project.tasks.create "jar${name.capitalize()}", Jar 138 | task.dependsOn variant.javaCompileProvider 139 | task.from variant.javaCompileProvider.get().destinationDir 140 | task.baseName 'anjlab-iabv3' 141 | task.doLast { 142 | println "Copying jar to sample project..." 143 | copy { 144 | from task.archivePath 145 | into '../sample/libs' 146 | rename { String fileName -> 'anjlab-iabv3-current.jar' } 147 | } 148 | } 149 | artifacts.add('archives', task); 150 | } 151 | -------------------------------------------------------------------------------- /library/progress-proguard.txt: -------------------------------------------------------------------------------- 1 | -keep class com.android.vending.billing.** 2 | -keep class com.android.billingclient.api.** -------------------------------------------------------------------------------- /library/src/androidTest/java/com/anjlab/android/iab/v3/BillingHistoryRecordTest.java: -------------------------------------------------------------------------------- 1 | package com.anjlab.android.iab.v3; 2 | 3 | import android.os.Parcel; 4 | 5 | import com.anjlab.android.iab.v3.util.ResourcesUtil; 6 | 7 | import org.json.JSONException; 8 | import org.junit.Before; 9 | import org.junit.Test; 10 | import static junit.framework.Assert.assertEquals; 11 | 12 | public class BillingHistoryRecordTest 13 | { 14 | 15 | private String historyResponseJson; 16 | 17 | @Before 18 | public void setup() 19 | { 20 | historyResponseJson = ResourcesUtil.loadFile("purchase_history_response.json"); 21 | } 22 | 23 | @Test 24 | public void testCreatesFromJsonCorrectly() throws JSONException 25 | { 26 | BillingHistoryRecord record = new BillingHistoryRecord(historyResponseJson, "signature"); 27 | 28 | assertEquals("sample-product-id", record.productId); 29 | assertEquals("sample-purchase-token", record.purchaseToken); 30 | assertEquals(1563441231403L, record.purchaseTime); 31 | assertEquals("sample-developer-payload", record.developerPayload); 32 | assertEquals("signature", record.signature); 33 | } 34 | 35 | @Test 36 | public void testParcelizesCorrectly() throws JSONException 37 | { 38 | BillingHistoryRecord record = new BillingHistoryRecord(historyResponseJson, "signature"); 39 | 40 | Parcel parcel = Parcel.obtain(); 41 | record.writeToParcel(parcel, 0); 42 | parcel.setDataPosition(0); 43 | 44 | BillingHistoryRecord restoredRecord = BillingHistoryRecord.CREATOR.createFromParcel(parcel); 45 | assertEquals("sample-product-id", restoredRecord.productId); 46 | assertEquals("sample-purchase-token", restoredRecord.purchaseToken); 47 | assertEquals(1563441231403L, restoredRecord.purchaseTime); 48 | assertEquals("sample-developer-payload", restoredRecord.developerPayload); 49 | assertEquals("signature", restoredRecord.signature); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/anjlab/android/iab/v3/PurchaseInfoParcelableTest.java: -------------------------------------------------------------------------------- 1 | package com.anjlab.android.iab.v3; 2 | 3 | import android.os.Parcel; 4 | 5 | import com.anjlab.android.iab.v3.util.ResourcesUtil; 6 | 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | 10 | import static junit.framework.Assert.assertEquals; 11 | 12 | public class PurchaseInfoParcelableTest 13 | { 14 | 15 | private PurchaseInfo purchaseInfo; 16 | 17 | @Before 18 | public void init() 19 | { 20 | purchaseInfo = new PurchaseInfo(ResourcesUtil.loadFile("purchase_info.json"), "signature"); 21 | } 22 | 23 | @Test 24 | public void testParcelable() throws Exception 25 | { 26 | Parcel parcel = Parcel.obtain(); 27 | purchaseInfo.writeToParcel(parcel, 0); 28 | parcel.setDataPosition(0); 29 | 30 | PurchaseInfo newInfo = PurchaseInfo.CREATOR.createFromParcel(parcel); 31 | 32 | assertEquals(purchaseInfo.responseData, newInfo.responseData); 33 | assertEquals(purchaseInfo.signature, newInfo.signature); 34 | } 35 | 36 | @Test 37 | public void testResponseDataParcelable() throws Exception 38 | { 39 | PurchaseData responseData = purchaseInfo.parseResponseDataImpl(); 40 | 41 | Parcel parcel = Parcel.obtain(); 42 | responseData.writeToParcel(parcel, 0); 43 | parcel.setDataPosition(0); 44 | 45 | PurchaseData newData = PurchaseData.CREATOR.createFromParcel(parcel); 46 | 47 | assertEquals(responseData.autoRenewing, newData.autoRenewing); 48 | assertEquals(responseData.purchaseToken, newData.purchaseToken); 49 | assertEquals(responseData.developerPayload, newData.developerPayload); 50 | assertEquals(responseData.purchaseState, newData.purchaseState); 51 | assertEquals(responseData.purchaseTime, newData.purchaseTime); 52 | assertEquals(responseData.productId, newData.productId); 53 | assertEquals(responseData.packageName, newData.packageName); 54 | assertEquals(responseData.orderId, newData.orderId); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/anjlab/android/iab/v3/SkuDetailsParcelableTest.java: -------------------------------------------------------------------------------- 1 | package com.anjlab.android.iab.v3; 2 | 3 | import android.os.Parcel; 4 | 5 | import com.anjlab.android.iab.v3.util.ResourcesUtil; 6 | 7 | import org.json.JSONObject; 8 | import org.junit.Test; 9 | 10 | import static junit.framework.Assert.assertEquals; 11 | 12 | public class SkuDetailsParcelableTest 13 | { 14 | @Test 15 | public void testParcelableInApp() throws Exception 16 | { 17 | testParcelable(loadSkuDetails("sku_in_app.json"), false, false); 18 | } 19 | 20 | @Test 21 | public void testParcelableSubscription() throws Exception 22 | { 23 | testParcelable(loadSkuDetails("sku_subscription.json"), false, false); 24 | } 25 | 26 | @Test 27 | public void testParcelableSubscriptionIntroductory() throws Exception 28 | { 29 | testParcelable(loadSkuDetails("sku_subscription_introductory.json"), true, false); 30 | } 31 | 32 | @Test 33 | public void testParcelableSubscriptionTrial() throws Exception 34 | { 35 | testParcelable(loadSkuDetails("sku_subscription_trial.json"), false, true); 36 | } 37 | 38 | private SkuDetails loadSkuDetails(String jsonFilePath) throws Exception 39 | { 40 | JSONObject details = new JSONObject(ResourcesUtil.loadFile(jsonFilePath)); 41 | return new SkuDetails(details); 42 | } 43 | 44 | private void testParcelable(SkuDetails skuDetails, boolean isIntroPrice, boolean isTrial) throws Exception 45 | { 46 | Parcel parcel = Parcel.obtain(); 47 | 48 | skuDetails.writeToParcel(parcel, 0); 49 | 50 | parcel.setDataPosition(0); 51 | 52 | SkuDetails result = SkuDetails.CREATOR.createFromParcel(parcel); 53 | 54 | assertEquals(skuDetails.productId, result.productId); 55 | assertEquals(skuDetails.priceLong, result.priceLong); 56 | assertEquals(skuDetails.priceText, result.priceText); 57 | assertEquals(skuDetails.priceValue, result.priceValue); 58 | assertEquals(skuDetails.description, result.description); 59 | assertEquals(skuDetails.isSubscription, result.isSubscription); 60 | assertEquals(skuDetails.currency, result.currency); 61 | assertEquals(skuDetails.title, result.title); 62 | assertEquals(skuDetails.subscriptionPeriod, result.subscriptionPeriod); 63 | assertEquals(skuDetails.subscriptionFreeTrialPeriod, result.subscriptionFreeTrialPeriod); 64 | assertEquals(skuDetails.haveTrialPeriod, result.haveTrialPeriod); 65 | assertEquals(skuDetails.introductoryPriceValue, result.introductoryPriceValue); 66 | assertEquals(skuDetails.introductoryPricePeriod, result.introductoryPricePeriod); 67 | assertEquals(skuDetails.introductoryPriceCycles, result.introductoryPriceCycles); 68 | assertEquals(skuDetails.introductoryPriceLong, result.introductoryPriceLong); 69 | assertEquals(skuDetails.haveIntroductoryPeriod, result.haveIntroductoryPeriod); 70 | assertEquals(skuDetails.introductoryPriceText, result.introductoryPriceText); 71 | assertEquals(skuDetails.responseData, result.responseData); 72 | 73 | assertEquals(skuDetails.haveIntroductoryPeriod, isIntroPrice); 74 | assertEquals(skuDetails.haveTrialPeriod, isTrial); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /library/src/androidTest/java/com/anjlab/android/iab/v3/util/ResourcesUtil.java: -------------------------------------------------------------------------------- 1 | package com.anjlab.android.iab.v3.util; 2 | 3 | import java.util.Scanner; 4 | 5 | public class ResourcesUtil 6 | { 7 | public static String loadFile(String path) 8 | { 9 | String result = ""; 10 | Scanner sc = null; 11 | try 12 | { 13 | sc = new Scanner(ResourcesUtil.class.getClassLoader().getResourceAsStream(path)); 14 | while (sc.hasNextLine()) 15 | { 16 | result += sc.nextLine(); 17 | } 18 | sc.close(); 19 | } 20 | finally 21 | { 22 | if (sc != null) 23 | { 24 | sc.close(); 25 | } 26 | } 27 | return result; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /library/src/androidTest/resources/purchase_history_response.json: -------------------------------------------------------------------------------- 1 | { 2 | "productId":"sample-product-id", 3 | "purchaseToken":"sample-purchase-token", 4 | "purchaseTime":1563441231403, 5 | "developerPayload":"sample-developer-payload" 6 | } -------------------------------------------------------------------------------- /library/src/androidTest/resources/purchase_info.json: -------------------------------------------------------------------------------- 1 | { 2 | "orderId": "GPA.1234-5678-9012-34567", 3 | "packageName": "com.example.app", 4 | "productId": "exampleSku", 5 | "purchaseTime": 1345678900000, 6 | "purchaseState": 0, 7 | "developerPayload": "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ", 8 | "purchaseToken": "opaque-token-up-to-1000-characters" 9 | } -------------------------------------------------------------------------------- /library/src/androidTest/resources/sku_in_app.json: -------------------------------------------------------------------------------- 1 | { 2 | "productId": "test-id", 3 | "type": "inapp", 4 | "price": "€7.99", 5 | "price_amount_micros": "7990000", 6 | "price_currency_code": "GBP", 7 | "title": "Test Product", 8 | "description": "A great product for testing." 9 | } -------------------------------------------------------------------------------- /library/src/androidTest/resources/sku_subscription.json: -------------------------------------------------------------------------------- 1 | { 2 | "productId": "test-id", 3 | "type": "subs", 4 | "price": "€7.99", 5 | "price_amount_micros": "7990000", 6 | "price_currency_code": "GBP", 7 | "title": "Test Subscription", 8 | "description": "A great subscription for testing.", 9 | "subscriptionPeriod": "P1M" 10 | } -------------------------------------------------------------------------------- /library/src/androidTest/resources/sku_subscription_introductory.json: -------------------------------------------------------------------------------- 1 | { 2 | "productId": "test-id", 3 | "type": "subs", 4 | "price": "€7.99", 5 | "price_amount_micros": "7990000", 6 | "price_currency_code": "GBP", 7 | "title": "Test Subscription With Introductory", 8 | "description": "A great subscription with introductory for testing.", 9 | "subscriptionPeriod": "P1M", 10 | "introductoryPrice": "€0.99", 11 | "introductoryPriceAmountMicros": "990000", 12 | "introductoryPricePeriod": "P1W", 13 | "introductoryPriceCycles": 3 14 | } -------------------------------------------------------------------------------- /library/src/androidTest/resources/sku_subscription_trial.json: -------------------------------------------------------------------------------- 1 | { 2 | "productId": "test-id", 3 | "type": "subs", 4 | "price": "€7.99", 5 | "price_amount_micros": "7990000", 6 | "price_currency_code": "GBP", 7 | "title": "Test Subscription With Trial", 8 | "description": "A great subscription with trial for testing.", 9 | "subscriptionPeriod": "P1Y", 10 | "freeTrialPeriod": "P1W" 11 | } -------------------------------------------------------------------------------- /library/src/androidTest/resources/transaction_details.json: -------------------------------------------------------------------------------- 1 | { 2 | "orderId": "GPA.1234-5678-9012-34567", 3 | "packageName": "com.example.app", 4 | "productId": "exampleSku", 5 | "purchaseTime": 1345678900000, 6 | "purchaseState": 0, 7 | "developerPayload": "bGoa+V7g/yqDXvKRqq+JTFn4uQZbPiQJo4pf9RzJ", 8 | "purchaseToken": "opaque-token-up-to-1000-characters" 9 | } -------------------------------------------------------------------------------- /library/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /library/src/main/java/com/anjlab/android/iab/v3/BillingBase.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 AnjLab 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.anjlab.android.iab.v3; 17 | 18 | import android.content.Context; 19 | import android.content.SharedPreferences; 20 | import android.preference.PreferenceManager; 21 | 22 | class BillingBase 23 | { 24 | private Context context; 25 | 26 | BillingBase(Context context) 27 | { 28 | this.context = context; 29 | } 30 | 31 | Context getContext() 32 | { 33 | return context; 34 | } 35 | 36 | String getPreferencesBaseKey() 37 | { 38 | return getContext().getPackageName() + "_preferences"; 39 | } 40 | 41 | private SharedPreferences getPreferences() 42 | { 43 | return PreferenceManager.getDefaultSharedPreferences(getContext()); 44 | } 45 | 46 | boolean saveString(String key, String value) 47 | { 48 | SharedPreferences sp = getPreferences(); 49 | if (sp != null) 50 | { 51 | SharedPreferences.Editor spe = sp.edit(); 52 | spe.putString(key, value); 53 | spe.commit(); 54 | return true; 55 | } 56 | return false; 57 | } 58 | 59 | String loadString(String key, String defValue) 60 | { 61 | SharedPreferences sp = getPreferences(); 62 | if (sp != null) 63 | { 64 | return sp.getString(key, defValue); 65 | } 66 | return defValue; 67 | } 68 | 69 | boolean saveBoolean(String key, Boolean value) 70 | { 71 | SharedPreferences sp = getPreferences(); 72 | if (sp != null) 73 | { 74 | SharedPreferences.Editor spe = sp.edit(); 75 | spe.putBoolean(key, value); 76 | spe.commit(); 77 | return true; 78 | } 79 | return false; 80 | } 81 | 82 | boolean loadBoolean(String key, boolean defValue) 83 | { 84 | SharedPreferences sp = getPreferences(); 85 | if (sp != null) 86 | { 87 | return sp.getBoolean(key, defValue); 88 | } 89 | return defValue; 90 | } 91 | } 92 | 93 | -------------------------------------------------------------------------------- /library/src/main/java/com/anjlab/android/iab/v3/BillingCache.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 AnjLab 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.anjlab.android.iab.v3; 17 | 18 | import android.content.Context; 19 | import android.text.TextUtils; 20 | 21 | import java.util.ArrayList; 22 | import java.util.Date; 23 | import java.util.HashMap; 24 | import java.util.List; 25 | import java.util.regex.Pattern; 26 | 27 | class BillingCache extends BillingBase 28 | { 29 | private static final String ENTRY_DELIMITER = "#####"; 30 | private static final String LINE_DELIMITER = ">>>>>"; 31 | private static final String VERSION_KEY = ".version"; 32 | 33 | private HashMap data; 34 | private String cacheKey; 35 | private String version; 36 | 37 | BillingCache(Context context, String key) 38 | { 39 | super(context); 40 | data = new HashMap<>(); 41 | cacheKey = key; 42 | load(); 43 | } 44 | 45 | private String getPreferencesCacheKey() 46 | { 47 | return getPreferencesBaseKey() + cacheKey; 48 | } 49 | 50 | private String getPreferencesVersionKey() 51 | { 52 | return getPreferencesCacheKey() + VERSION_KEY; 53 | } 54 | 55 | private void load() 56 | { 57 | String[] entries = loadString(getPreferencesCacheKey(), "").split(Pattern.quote(ENTRY_DELIMITER)); 58 | for (String entry : entries) 59 | { 60 | if (!TextUtils.isEmpty(entry)) 61 | { 62 | String[] parts = entry.split(Pattern.quote(LINE_DELIMITER)); 63 | if (parts.length > 2) 64 | { 65 | data.put(parts[0], new PurchaseInfo(parts[1], parts[2])); 66 | } 67 | else if (parts.length > 1) 68 | { 69 | data.put(parts[0], new PurchaseInfo(parts[1], null)); 70 | } 71 | } 72 | } 73 | version = getCurrentVersion(); 74 | } 75 | 76 | private void flush() 77 | { 78 | ArrayList output = new ArrayList<>(); 79 | for (String productId : data.keySet()) 80 | { 81 | PurchaseInfo info = data.get(productId); 82 | output.add(productId + LINE_DELIMITER + info.responseData + LINE_DELIMITER + 83 | info.signature); 84 | } 85 | saveString(getPreferencesCacheKey(), TextUtils.join(ENTRY_DELIMITER, output)); 86 | version = Long.toString(new Date().getTime()); 87 | saveString(getPreferencesVersionKey(), version); 88 | } 89 | 90 | boolean includesProduct(String productId) 91 | { 92 | reloadDataIfNeeded(); 93 | return data.containsKey(productId); 94 | } 95 | 96 | PurchaseInfo getDetails(String productId) 97 | { 98 | reloadDataIfNeeded(); 99 | return data.containsKey(productId) ? data.get(productId) : null; 100 | } 101 | 102 | void put(String productId, String details, String signature) 103 | { 104 | reloadDataIfNeeded(); 105 | if (!data.containsKey(productId)) 106 | { 107 | data.put(productId, new PurchaseInfo(details, signature)); 108 | flush(); 109 | } 110 | } 111 | 112 | void remove(String productId) 113 | { 114 | reloadDataIfNeeded(); 115 | if (data.containsKey(productId)) 116 | { 117 | data.remove(productId); 118 | flush(); 119 | } 120 | } 121 | 122 | void clear() 123 | { 124 | reloadDataIfNeeded(); 125 | data.clear(); 126 | flush(); 127 | } 128 | 129 | private String getCurrentVersion() 130 | { 131 | return loadString(getPreferencesVersionKey(), "0"); 132 | } 133 | 134 | private void reloadDataIfNeeded() 135 | { 136 | if (!version.equalsIgnoreCase(getCurrentVersion())) 137 | { 138 | data.clear(); 139 | load(); 140 | } 141 | } 142 | 143 | List getContents() 144 | { 145 | return new ArrayList<>(data.keySet()); 146 | } 147 | 148 | @Override 149 | public String toString() 150 | { 151 | return TextUtils.join(", ", data.keySet()); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /library/src/main/java/com/anjlab/android/iab/v3/BillingHistoryRecord.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 AnjLab 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.anjlab.android.iab.v3; 17 | 18 | import org.json.JSONException; 19 | import org.json.JSONObject; 20 | 21 | import android.os.Parcel; 22 | import android.os.Parcelable; 23 | 24 | public class BillingHistoryRecord implements Parcelable 25 | { 26 | 27 | public final String productId; 28 | public final String purchaseToken; 29 | public final long purchaseTime; 30 | public final String developerPayload; 31 | public final String signature; 32 | 33 | public BillingHistoryRecord(String dataAsJson, String signature) throws JSONException 34 | { 35 | this(new JSONObject(dataAsJson), signature); 36 | } 37 | 38 | public BillingHistoryRecord(JSONObject json, String signature) throws JSONException 39 | { 40 | productId = json.getString("productId"); 41 | purchaseToken = json.getString("purchaseToken"); 42 | purchaseTime = json.getLong("purchaseTime"); 43 | developerPayload = json.getString("developerPayload"); 44 | this.signature = signature; 45 | } 46 | 47 | public BillingHistoryRecord(String productId, String purchaseToken, long purchaseTime, 48 | String developerPayload, String signature) 49 | { 50 | this.productId = productId; 51 | this.purchaseToken = purchaseToken; 52 | this.purchaseTime = purchaseTime; 53 | this.developerPayload = developerPayload; 54 | this.signature = signature; 55 | } 56 | 57 | protected BillingHistoryRecord(Parcel in) 58 | { 59 | productId = in.readString(); 60 | purchaseToken = in.readString(); 61 | purchaseTime = in.readLong(); 62 | developerPayload = in.readString(); 63 | signature = in.readString(); 64 | } 65 | 66 | @Override 67 | public void writeToParcel(Parcel dest, int flags) 68 | { 69 | dest.writeString(productId); 70 | dest.writeString(purchaseToken); 71 | dest.writeLong(purchaseTime); 72 | dest.writeString(developerPayload); 73 | dest.writeString(signature); 74 | } 75 | 76 | @Override 77 | public int describeContents() 78 | { 79 | return 0; 80 | } 81 | 82 | public static final Creator CREATOR = new Creator() 83 | { 84 | @Override 85 | public BillingHistoryRecord createFromParcel(Parcel in) 86 | { 87 | return new BillingHistoryRecord(in); 88 | } 89 | 90 | @Override 91 | public BillingHistoryRecord[] newArray(int size) 92 | { 93 | return new BillingHistoryRecord[size]; 94 | } 95 | }; 96 | 97 | @Override 98 | public String toString() 99 | { 100 | return "BillingHistoryRecord{" + 101 | "productId='" + productId + '\'' + 102 | ", purchaseToken='" + purchaseToken + '\'' + 103 | ", purchaseTime=" + purchaseTime + 104 | ", developerPayload='" + developerPayload + '\'' + 105 | ", signature='" + signature + '\'' + 106 | '}'; 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /library/src/main/java/com/anjlab/android/iab/v3/BillingProcessor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 AnjLab 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.anjlab.android.iab.v3; 17 | 18 | import com.android.billingclient.api.AcknowledgePurchaseParams; 19 | import com.android.billingclient.api.AcknowledgePurchaseResponseListener; 20 | import com.android.billingclient.api.BillingClient; 21 | import com.android.billingclient.api.BillingClientStateListener; 22 | import com.android.billingclient.api.BillingFlowParams; 23 | import com.android.billingclient.api.BillingResult; 24 | import com.android.billingclient.api.ConsumeParams; 25 | import com.android.billingclient.api.ConsumeResponseListener; 26 | import com.android.billingclient.api.Purchase; 27 | import com.android.billingclient.api.PurchasesResponseListener; 28 | import com.android.billingclient.api.PurchasesUpdatedListener; 29 | import com.android.billingclient.api.SkuDetailsParams; 30 | 31 | import org.json.JSONException; 32 | import org.json.JSONObject; 33 | 34 | import java.util.ArrayList; 35 | import java.util.Calendar; 36 | import java.util.Date; 37 | import java.util.List; 38 | import java.util.Locale; 39 | import java.util.UUID; 40 | 41 | import android.app.Activity; 42 | import android.content.Context; 43 | import android.content.Intent; 44 | import android.content.pm.PackageManager; 45 | import android.content.pm.ResolveInfo; 46 | import android.os.AsyncTask; 47 | import android.os.Handler; 48 | import android.os.Looper; 49 | import android.text.TextUtils; 50 | import android.util.Log; 51 | import androidx.annotation.NonNull; 52 | import androidx.annotation.Nullable; 53 | 54 | public class BillingProcessor extends BillingBase 55 | { 56 | 57 | /** 58 | * Callback methods where billing events are reported. 59 | * Apps must implement one of these to construct a BillingProcessor. 60 | */ 61 | public interface IBillingHandler 62 | { 63 | void onProductPurchased(@NonNull String productId, @Nullable PurchaseInfo details); 64 | 65 | void onPurchaseHistoryRestored(); 66 | 67 | void onBillingError(int errorCode, @Nullable Throwable error); 68 | 69 | void onBillingInitialized(); 70 | } 71 | 72 | /** 73 | * Callback methods for notifying about success or failure attempt to fetch purchases from the server. 74 | */ 75 | public interface IPurchasesResponseListener 76 | { 77 | void onPurchasesSuccess(); 78 | 79 | void onPurchasesError(); 80 | } 81 | 82 | /** 83 | * Callback methods where result of SkuDetails fetch returned or error message on failure. 84 | */ 85 | public interface ISkuDetailsResponseListener 86 | { 87 | void onSkuDetailsResponse(@Nullable List products); 88 | 89 | void onSkuDetailsError(String error); 90 | } 91 | 92 | private static final Date DATE_MERCHANT_LIMIT_1; //5th December 2012 93 | private static final Date DATE_MERCHANT_LIMIT_2; //21st July 2015 94 | 95 | static 96 | { 97 | Calendar calendar = Calendar.getInstance(); 98 | calendar.set(2012, Calendar.DECEMBER, 5); 99 | DATE_MERCHANT_LIMIT_1 = calendar.getTime(); 100 | calendar.set(2015, Calendar.JULY, 21); 101 | DATE_MERCHANT_LIMIT_2 = calendar.getTime(); 102 | } 103 | 104 | private static final String LOG_TAG = "iabv3"; 105 | private static final String SETTINGS_VERSION = ".v2_6"; 106 | private static final String RESTORE_KEY = ".products.restored" + SETTINGS_VERSION; 107 | private static final String MANAGED_PRODUCTS_CACHE_KEY = ".products.cache" + SETTINGS_VERSION; 108 | private static final String SUBSCRIPTIONS_CACHE_KEY = ".subscriptions.cache" + SETTINGS_VERSION; 109 | private static final String PURCHASE_PAYLOAD_CACHE_KEY = ".purchase.last" + SETTINGS_VERSION; 110 | 111 | private static final long RECONNECT_TIMER_START_MILLISECONDS = 1000L; 112 | private static final long RECONNECT_TIMER_MAX_TIME_MILLISECONDS = 1000L * 60L * 15L; 113 | 114 | private long reconnectMilliseconds = RECONNECT_TIMER_START_MILLISECONDS; 115 | 116 | private BillingClient billingService; 117 | private String signatureBase64; 118 | private BillingCache cachedProducts; 119 | private BillingCache cachedSubscriptions; 120 | private IBillingHandler eventHandler; 121 | private String developerMerchantId; 122 | private boolean isSubsUpdateSupported; 123 | private boolean isHistoryTaskExecuted = false; 124 | 125 | private Handler handler = new Handler(Looper.getMainLooper()); 126 | 127 | private class HistoryInitializationTask extends AsyncTask 128 | { 129 | @Override 130 | protected Boolean doInBackground(Void... nothing) 131 | { 132 | if (!isPurchaseHistoryRestored()) 133 | { 134 | loadOwnedPurchasesFromGoogleAsync(null); 135 | return true; 136 | } 137 | return false; 138 | } 139 | 140 | @Override 141 | protected void onPostExecute(Boolean restored) 142 | { 143 | 144 | isHistoryTaskExecuted = true; 145 | 146 | if (restored) 147 | { 148 | setPurchaseHistoryRestored(); 149 | if (eventHandler != null) 150 | { 151 | eventHandler.onPurchaseHistoryRestored(); 152 | } 153 | } 154 | if (eventHandler != null) 155 | { 156 | eventHandler.onBillingInitialized(); 157 | } 158 | } 159 | } 160 | 161 | /** 162 | * Returns a new {@link BillingProcessor}, without immediately binding to Play Services. If you use 163 | * this factory, then you must call {@link #initialize()} afterwards. 164 | * @param context Context object 165 | * @param licenseKey Licence key from Play Console 166 | * @param handler callback instance 167 | * @return BillingProcessor instance 168 | */ 169 | public static BillingProcessor newBillingProcessor(Context context, String licenseKey, IBillingHandler handler) 170 | { 171 | return newBillingProcessor(context, licenseKey, null, handler); 172 | } 173 | 174 | /** 175 | * Returns a new {@link BillingProcessor}, without immediately binding to Play Services. If you use 176 | * this factory, then you must call {@link #initialize()} afterwards. 177 | * @param context Context object 178 | * @param licenseKey Licence key from Play Console 179 | * @param merchantId Google merchant ID 180 | * @param handler callback instance 181 | * @return BillingProcessor instance 182 | */ 183 | public static BillingProcessor newBillingProcessor(Context context, String licenseKey, String merchantId, 184 | IBillingHandler handler) 185 | { 186 | return new BillingProcessor(context, licenseKey, merchantId, handler, false); 187 | } 188 | 189 | public BillingProcessor(Context context, String licenseKey, IBillingHandler handler) 190 | { 191 | this(context, licenseKey, null, handler); 192 | } 193 | 194 | public BillingProcessor(Context context, String licenseKey, String merchantId, 195 | IBillingHandler handler) 196 | { 197 | this(context, licenseKey, merchantId, handler, true); 198 | } 199 | 200 | private BillingProcessor(Context context, String licenseKey, String merchantId, IBillingHandler handler, 201 | boolean bindImmediately) 202 | { 203 | super(context.getApplicationContext()); 204 | signatureBase64 = licenseKey; 205 | eventHandler = handler; 206 | cachedProducts = new BillingCache(getContext(), MANAGED_PRODUCTS_CACHE_KEY); 207 | cachedSubscriptions = new BillingCache(getContext(), SUBSCRIPTIONS_CACHE_KEY); 208 | developerMerchantId = merchantId; 209 | init(context); 210 | if (bindImmediately) 211 | { 212 | initialize(); 213 | } 214 | } 215 | 216 | private static Intent getBindServiceIntent() 217 | { 218 | Intent intent = new Intent("com.android.vending.billing.InAppBillingService.BIND"); 219 | intent.setPackage("com.android.vending"); 220 | return intent; 221 | } 222 | 223 | public static boolean isIabServiceAvailable(Context context) 224 | { 225 | final PackageManager packageManager = context.getPackageManager(); 226 | List list = packageManager.queryIntentServices(getBindServiceIntent(), 0); 227 | return list != null && list.size() > 0; 228 | } 229 | 230 | private void init(Context context) 231 | { 232 | PurchasesUpdatedListener listener = new PurchasesUpdatedListener() 233 | { 234 | @Override 235 | public void onPurchasesUpdated(@NonNull BillingResult billingResult, 236 | @Nullable List purchases) 237 | { 238 | int responseCode = billingResult.getResponseCode(); 239 | 240 | if (responseCode == BillingClient.BillingResponseCode.OK) 241 | { 242 | if (purchases != null) 243 | { 244 | for (final Purchase purchase : purchases) 245 | { 246 | handlePurchase(purchase); 247 | } 248 | } 249 | } 250 | else if (responseCode == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) 251 | { 252 | String purchasePayload = getPurchasePayload(); 253 | if (TextUtils.isEmpty(purchasePayload)) 254 | { 255 | loadOwnedPurchasesFromGoogleAsync(null); 256 | } 257 | else 258 | { 259 | handleItemAlreadyOwned(purchasePayload.split(":")[1]); 260 | savePurchasePayload(null); 261 | } 262 | 263 | reportBillingError(responseCode, new Throwable(billingResult.getDebugMessage())); 264 | 265 | } 266 | else if (responseCode == BillingClient.BillingResponseCode.USER_CANCELED 267 | || responseCode == BillingClient.BillingResponseCode.SERVICE_UNAVAILABLE 268 | || responseCode == BillingClient.BillingResponseCode.BILLING_UNAVAILABLE 269 | || responseCode == BillingClient.BillingResponseCode.ITEM_UNAVAILABLE 270 | || responseCode == BillingClient.BillingResponseCode.DEVELOPER_ERROR 271 | || responseCode == BillingClient.BillingResponseCode.ERROR 272 | || responseCode == BillingClient.BillingResponseCode.ITEM_NOT_OWNED) 273 | { 274 | reportBillingError(responseCode, new Throwable(billingResult.getDebugMessage())); 275 | } 276 | } 277 | }; 278 | 279 | billingService = BillingClient.newBuilder(context) 280 | .enablePendingPurchases() 281 | .setListener(listener) 282 | .build(); 283 | } 284 | 285 | /** 286 | * Establishing Connection to Google Play 287 | * you should call this method if used {@link #newBillingProcessor} method or called constructor 288 | * with bindImmediately = false 289 | */ 290 | public void initialize() 291 | { 292 | if (billingService != null && !billingService.isReady()) 293 | { 294 | billingService.startConnection(new BillingClientStateListener() 295 | { 296 | @Override 297 | public void onBillingSetupFinished(@NonNull BillingResult billingResult) 298 | { 299 | if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) 300 | { 301 | reconnectMilliseconds = RECONNECT_TIMER_START_MILLISECONDS; 302 | // The BillingClient is ready. You can query purchases here. 303 | Log.d("GooglePlayConnection; ", "IsConnected"); 304 | 305 | //Initialize history of purchases if any exist. 306 | if (!isHistoryTaskExecuted) 307 | { 308 | new HistoryInitializationTask().execute(); 309 | } 310 | } 311 | else 312 | { 313 | retryBillingClientConnection(); 314 | reportBillingError( 315 | billingResult.getResponseCode(), 316 | new Throwable(billingResult.getDebugMessage())); 317 | } 318 | } 319 | 320 | @Override 321 | public void onBillingServiceDisconnected() 322 | { 323 | Log.d("ServiceDisconnected; ", "BillingServiceDisconnected, trying new Connection"); 324 | 325 | //retrying connection to GooglePlay 326 | if (!isConnected()) 327 | { 328 | retryBillingClientConnection(); 329 | } 330 | } 331 | }); 332 | } 333 | 334 | } 335 | 336 | /** 337 | * Retries the billing client connection with exponential backoff 338 | * Max out at the time specified by RECONNECT_TIMER_MAX_TIME_MILLISECONDS (15 minutes) 339 | */ 340 | private void retryBillingClientConnection() 341 | { 342 | handler.postDelayed(new Runnable() 343 | { 344 | @Override 345 | public void run() 346 | { 347 | initialize(); 348 | } 349 | }, reconnectMilliseconds); 350 | 351 | reconnectMilliseconds = 352 | Math.min(reconnectMilliseconds * 2, RECONNECT_TIMER_MAX_TIME_MILLISECONDS); 353 | } 354 | 355 | /** 356 | * Check for billingClient is initialized and connected, if true then its ready for use. 357 | * @return true or false 358 | * */ 359 | public boolean isConnected() 360 | { 361 | return isInitialized() && billingService.isReady(); 362 | } 363 | 364 | /** 365 | * This method should be called when you are done with BillingProcessor. 366 | * BillingClient object holds a binding to the in-app billing service and the manager to handle 367 | * broadcast events, which will leak unless you dispose it correctly. 368 | **/ 369 | public void release() 370 | { 371 | if (isConnected()) 372 | { 373 | Log.d(LOG_TAG, "BillingClient can only be used once -- closing connection"); 374 | billingService.endConnection(); 375 | } 376 | } 377 | 378 | public boolean isInitialized() 379 | { 380 | return billingService != null; 381 | } 382 | 383 | public boolean isPurchased(String productId) 384 | { 385 | return cachedProducts.includesProduct(productId); 386 | } 387 | 388 | public boolean isSubscribed(String productId) 389 | { 390 | return cachedSubscriptions.includesProduct(productId); 391 | } 392 | 393 | public List listOwnedProducts() 394 | { 395 | return cachedProducts.getContents(); 396 | } 397 | 398 | public List listOwnedSubscriptions() 399 | { 400 | return cachedSubscriptions.getContents(); 401 | } 402 | 403 | private void loadPurchasesByTypeAsync(String type, final BillingCache cacheStorage, 404 | final IPurchasesResponseListener listener) 405 | { 406 | if (!isConnected()) 407 | { 408 | reportPurchasesError(listener); 409 | retryBillingClientConnection(); 410 | return; 411 | } 412 | 413 | billingService.queryPurchasesAsync(type, new PurchasesResponseListener() 414 | { 415 | @Override 416 | public void onQueryPurchasesResponse(@NonNull BillingResult billingResult, 417 | @NonNull List list) 418 | { 419 | if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) 420 | { 421 | cacheStorage.clear(); 422 | for (Purchase purchaseItem : list) 423 | { 424 | String jsonData = purchaseItem.getOriginalJson(); 425 | if (!TextUtils.isEmpty(jsonData)) 426 | { 427 | try 428 | { 429 | /* 430 | This is a replacement for the bundling in the old version 431 | here we query all users' purchases and save it locally 432 | However, it is also recommended to save and verify all purchases 433 | on own server 434 | */ 435 | JSONObject purchase = new JSONObject(jsonData); 436 | cacheStorage.put( 437 | purchase.getString(Constants.RESPONSE_PRODUCT_ID), 438 | jsonData, 439 | purchaseItem.getSignature()); 440 | } 441 | catch (Exception e) 442 | { 443 | reportBillingError( 444 | Constants.BILLING_ERROR_FAILED_LOAD_PURCHASES, e); 445 | Log.e(LOG_TAG, "Error in loadPurchasesByType", e); 446 | reportPurchasesError(listener); 447 | } 448 | } 449 | } 450 | 451 | reportPurchasesSuccess(listener); 452 | } 453 | else 454 | { 455 | reportPurchasesError(listener); 456 | } 457 | } 458 | }); 459 | } 460 | 461 | /** 462 | * Attempt to fetch purchases from the server and update our cache if successful 463 | * 464 | * @param listener invokes method onPurchasesError if all retrievals are failure, 465 | * onPurchasesSuccess if even one retrieval succeeded 466 | */ 467 | public void loadOwnedPurchasesFromGoogleAsync(final IPurchasesResponseListener listener) 468 | { 469 | final IPurchasesResponseListener successListener = new IPurchasesResponseListener() 470 | { 471 | @Override 472 | public void onPurchasesSuccess() 473 | { 474 | reportPurchasesSuccess(listener); 475 | } 476 | 477 | @Override 478 | public void onPurchasesError() 479 | { 480 | reportPurchasesError(listener); 481 | } 482 | }; 483 | 484 | final IPurchasesResponseListener errorListener = new IPurchasesResponseListener() 485 | { 486 | @Override 487 | public void onPurchasesSuccess() 488 | { 489 | reportPurchasesError(listener); 490 | } 491 | 492 | @Override 493 | public void onPurchasesError() 494 | { 495 | reportPurchasesError(listener); 496 | } 497 | }; 498 | 499 | loadPurchasesByTypeAsync( 500 | Constants.PRODUCT_TYPE_MANAGED, 501 | cachedProducts, 502 | new IPurchasesResponseListener() 503 | { 504 | @Override 505 | public void onPurchasesSuccess() 506 | { 507 | loadPurchasesByTypeAsync( 508 | Constants.PRODUCT_TYPE_SUBSCRIPTION, 509 | cachedSubscriptions, 510 | successListener); 511 | } 512 | 513 | @Override 514 | public void onPurchasesError() 515 | { 516 | loadPurchasesByTypeAsync( 517 | Constants.PRODUCT_TYPE_SUBSCRIPTION, 518 | cachedSubscriptions, 519 | errorListener); 520 | } 521 | }); 522 | } 523 | 524 | /*** 525 | * Purchase a product 526 | * 527 | * @param activity the activity calling this method 528 | * @param productId the product id to purchase 529 | * @return {@code false} if the billing system is not initialized, {@code productId} is empty 530 | * or if an exception occurs. Will return {@code true} otherwise. 531 | */ 532 | public boolean purchase(Activity activity, String productId) 533 | { 534 | return purchase(activity, null, productId, Constants.PRODUCT_TYPE_MANAGED); 535 | } 536 | 537 | /*** 538 | * Subscribe for a product 539 | * 540 | * @param activity the activity calling this method 541 | * @param productId the product id to subscribe 542 | * @return {@code false} if the billing system is not initialized, {@code productId} is empty 543 | * or if an exception occurs. Will return {@code true} otherwise. 544 | */ 545 | public boolean subscribe(Activity activity, String productId) 546 | { 547 | return purchase(activity, null, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION); 548 | } 549 | 550 | /** 551 | * @deprecated always returns true. 552 | * @return true 553 | */ 554 | @Deprecated 555 | public boolean isOneTimePurchaseSupported() 556 | { 557 | return true; 558 | } 559 | 560 | public boolean isSubscriptionUpdateSupported() 561 | { 562 | // Avoid calling the service again if this value is true 563 | if (isSubsUpdateSupported) 564 | { 565 | return true; 566 | } 567 | 568 | if (!isConnected()) 569 | { 570 | return false; 571 | } 572 | 573 | BillingResult result = 574 | billingService.isFeatureSupported(BillingClient.FeatureType.SUBSCRIPTIONS_UPDATE); 575 | isSubsUpdateSupported = result.getResponseCode() == BillingClient.BillingResponseCode.OK; 576 | 577 | return isSubsUpdateSupported; 578 | } 579 | 580 | /** 581 | * Change subscription i.e. upgrade or downgrade 582 | * 583 | * @param activity the activity calling this method 584 | * @param oldProductId passing null or empty string will act the same as {@link #subscribe(Activity, String)} 585 | * @param productId the new subscription id 586 | * @return {@code false} if {@code oldProductId} is not {@code null} AND change subscription 587 | * is not supported. 588 | */ 589 | public boolean updateSubscription(Activity activity, String oldProductId, String productId) 590 | { 591 | if (oldProductId != null && !isSubscriptionUpdateSupported()) 592 | { 593 | return false; 594 | } 595 | return purchase(activity, oldProductId, productId, Constants.PRODUCT_TYPE_SUBSCRIPTION); 596 | } 597 | 598 | private boolean purchase(Activity activity, String productId, String purchaseType) 599 | { 600 | return purchase(activity, null, productId, purchaseType); 601 | } 602 | 603 | private boolean purchase(final Activity activity, final String oldProductId, final String productId, 604 | String purchaseType) 605 | { 606 | if (!isConnected() || TextUtils.isEmpty(productId) || TextUtils.isEmpty(purchaseType)) 607 | { 608 | if (!isConnected()) 609 | { 610 | retryBillingClientConnection(); 611 | } 612 | 613 | return false; 614 | } 615 | 616 | if (TextUtils.isEmpty(productId)) 617 | { 618 | reportBillingError(Constants.BILLING_ERROR_PRODUCT_ID_NOT_SPECIFIED, null); 619 | return false; 620 | } 621 | 622 | try 623 | { 624 | String purchasePayload = purchaseType + ":" + productId; 625 | if (!purchaseType.equals(Constants.PRODUCT_TYPE_SUBSCRIPTION)) 626 | { 627 | purchasePayload += ":" + UUID.randomUUID().toString(); 628 | } 629 | savePurchasePayload(purchasePayload); 630 | 631 | List skuList = new ArrayList<>(); 632 | skuList.add(productId); 633 | SkuDetailsParams params = SkuDetailsParams.newBuilder() 634 | .setSkusList(skuList) 635 | .setType(purchaseType) 636 | .build(); 637 | 638 | billingService.querySkuDetailsAsync( 639 | params, 640 | new com.android.billingclient.api.SkuDetailsResponseListener() 641 | { 642 | @Override 643 | public void onSkuDetailsResponse( 644 | @NonNull BillingResult billingResult, 645 | @Nullable List skuList) 646 | { 647 | 648 | if (skuList != null && !skuList.isEmpty()) 649 | { 650 | startPurchaseFlow(activity, skuList.get(0), oldProductId); 651 | } 652 | else 653 | { 654 | // This will occur if product id does not match with the product type 655 | Log.d("onSkuResponse: ", "product id mismatch with Product type"); 656 | reportBillingError( 657 | Constants.BILLING_ERROR_FAILED_TO_INITIALIZE_PURCHASE, 658 | null); 659 | } 660 | } 661 | }); 662 | 663 | return true; 664 | } 665 | catch (Exception e) 666 | { 667 | Log.e(LOG_TAG, "Error in purchase", e); 668 | reportBillingError(Constants.BILLING_ERROR_OTHER_ERROR, e); 669 | } 670 | return false; 671 | } 672 | 673 | private void startPurchaseFlow(final Activity activity, 674 | final com.android.billingclient.api.SkuDetails skuDetails, 675 | final String oldProductId) 676 | { 677 | final String productId = skuDetails.getSku(); 678 | 679 | handler.post(new Runnable() 680 | { 681 | @Override 682 | public void run() 683 | { 684 | BillingFlowParams.Builder billingFlowParamsBuilder = BillingFlowParams.newBuilder(); 685 | billingFlowParamsBuilder.setSkuDetails(skuDetails); 686 | 687 | if (!TextUtils.isEmpty(oldProductId)) 688 | { 689 | PurchaseInfo oldProductDetails = getSubscriptionPurchaseInfo(oldProductId); 690 | 691 | if (oldProductDetails != null) 692 | { 693 | String oldToken = oldProductDetails.purchaseData.purchaseToken; 694 | billingFlowParamsBuilder.setSubscriptionUpdateParams( 695 | BillingFlowParams.SubscriptionUpdateParams 696 | .newBuilder() 697 | .setOldPurchaseToken(oldToken) 698 | .build()); 699 | } 700 | } 701 | 702 | BillingFlowParams params = billingFlowParamsBuilder.build(); 703 | 704 | int responseCode = billingService.launchBillingFlow(activity, params).getResponseCode(); 705 | 706 | if (responseCode == BillingClient.BillingResponseCode.ITEM_ALREADY_OWNED) 707 | { 708 | handleItemAlreadyOwned(productId); 709 | } 710 | } 711 | }); 712 | } 713 | 714 | private void handleItemAlreadyOwned(final String productId) 715 | { 716 | if (!isPurchased(productId) && !isSubscribed(productId)) 717 | { 718 | loadOwnedPurchasesFromGoogleAsync(new IPurchasesResponseListener() 719 | { 720 | @Override 721 | public void onPurchasesSuccess() 722 | { 723 | handleOwnedPurchaseTransaction(productId); 724 | } 725 | 726 | @Override 727 | public void onPurchasesError() 728 | { 729 | handleOwnedPurchaseTransaction(productId); 730 | } 731 | }); 732 | } 733 | else 734 | { 735 | handleOwnedPurchaseTransaction(productId); 736 | } 737 | } 738 | 739 | private void handleOwnedPurchaseTransaction(String productId) 740 | { 741 | PurchaseInfo details = getPurchaseInfo(productId); 742 | if (!checkMerchant(details)) 743 | { 744 | Log.i(LOG_TAG, "Invalid or tampered merchant id!"); 745 | reportBillingError(Constants.BILLING_ERROR_INVALID_MERCHANT_ID, null); 746 | } 747 | 748 | if (eventHandler != null) 749 | { 750 | if (details == null) 751 | { 752 | details = getSubscriptionPurchaseInfo(productId); 753 | } 754 | 755 | reportProductPurchased(productId, details); 756 | } 757 | } 758 | 759 | /** 760 | * Checks merchant's id validity. If purchase was generated by Freedom alike program it doesn't know 761 | * real merchant id, unless publisher GoogleId was hacked 762 | * If merchantId was not supplied function checks nothing 763 | * 764 | * @param details PurchaseInfo 765 | * @return boolean 766 | */ 767 | private boolean checkMerchant(PurchaseInfo details) 768 | { 769 | if (developerMerchantId == null) //omit merchant id checking 770 | { 771 | return true; 772 | } 773 | if (details.purchaseData.purchaseTime.before(DATE_MERCHANT_LIMIT_1)) //newest format applied 774 | { 775 | return true; 776 | } 777 | if (details.purchaseData.purchaseTime.after(DATE_MERCHANT_LIMIT_2)) //newest format applied 778 | { 779 | return true; 780 | } 781 | if (details.purchaseData.orderId == null || 782 | details.purchaseData.orderId.trim().length() == 0) 783 | { 784 | return false; 785 | } 786 | int index = details.purchaseData.orderId.indexOf('.'); 787 | if (index <= 0) 788 | { 789 | return false; //protect on missing merchant id 790 | } 791 | //extract merchant id 792 | String merchantId = details.purchaseData.orderId.substring(0, index); 793 | return merchantId.compareTo(developerMerchantId) == 0; 794 | } 795 | 796 | @Nullable 797 | private PurchaseInfo getPurchaseInfo(String productId, BillingCache cache) 798 | { 799 | PurchaseInfo details = cache.getDetails(productId); 800 | if (details != null && !TextUtils.isEmpty(details.responseData)) 801 | { 802 | return details; 803 | } 804 | return null; 805 | } 806 | 807 | public void consumePurchaseAsync(final String productId, final IPurchasesResponseListener listener) 808 | { 809 | if (!isConnected()) 810 | { 811 | reportPurchasesError(listener); 812 | } 813 | 814 | try 815 | { 816 | PurchaseInfo purchaseInfo = getPurchaseInfo(productId, cachedProducts); 817 | if (purchaseInfo != null && !TextUtils.isEmpty(purchaseInfo.purchaseData.purchaseToken)) 818 | { 819 | ConsumeParams consumeParams = 820 | ConsumeParams.newBuilder() 821 | .setPurchaseToken(purchaseInfo.purchaseData.purchaseToken) 822 | .build(); 823 | 824 | billingService.consumeAsync(consumeParams, new ConsumeResponseListener() 825 | { 826 | @Override 827 | public void onConsumeResponse(@NonNull BillingResult billingResult, 828 | @NonNull String purchaseToken) 829 | { 830 | if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) 831 | { 832 | cachedProducts.remove(productId); 833 | Log.d(LOG_TAG, "Successfully consumed " + productId + " purchase."); 834 | 835 | reportPurchasesSuccess(listener); 836 | } 837 | else 838 | { 839 | Log.d(LOG_TAG, "Failure consume " + productId + " purchase."); 840 | reportBillingError(Constants.BILLING_ERROR_CONSUME_FAILED, 841 | new Exception(billingResult.getDebugMessage())); 842 | reportPurchasesError(listener); 843 | } 844 | } 845 | }); 846 | } 847 | } 848 | catch (Exception e) 849 | { 850 | Log.e(LOG_TAG, "Error in consumePurchase", e); 851 | reportBillingError(Constants.BILLING_ERROR_CONSUME_FAILED, e); 852 | reportPurchasesError(listener); 853 | } 854 | } 855 | 856 | private void getSkuDetailsAsync(final String productId, String purchaseType, 857 | final ISkuDetailsResponseListener listener) 858 | { 859 | ArrayList productIdList = new ArrayList<>(); 860 | productIdList.add(productId); 861 | getSkuDetailsAsync(productIdList, purchaseType, new ISkuDetailsResponseListener() 862 | { 863 | @Override 864 | public void onSkuDetailsResponse(@Nullable List products) 865 | { 866 | if (products != null) 867 | { 868 | if (listener != null) 869 | { 870 | reportSkuDetailsResponseCaller(products, listener); 871 | } 872 | } 873 | } 874 | 875 | @Override 876 | public void onSkuDetailsError(String string) 877 | { 878 | reportSkuDetailsErrorCaller(string, listener); 879 | } 880 | }); 881 | } 882 | 883 | private void getSkuDetailsAsync(final ArrayList productIdList, String purchaseType, 884 | final ISkuDetailsResponseListener listener) 885 | { 886 | if (billingService == null || !billingService.isReady()) 887 | { 888 | reportSkuDetailsErrorCaller("Failed to call getSkuDetails. Service may not be connected", listener); 889 | return; 890 | } 891 | if (productIdList == null || productIdList.isEmpty()) 892 | { 893 | reportSkuDetailsErrorCaller("Empty products list", listener); 894 | return; 895 | } 896 | 897 | try 898 | { 899 | SkuDetailsParams skuDetailsParams = SkuDetailsParams.newBuilder() 900 | .setSkusList(productIdList) 901 | .setType(purchaseType) 902 | .build(); 903 | final ArrayList productDetails = new ArrayList<>(); 904 | 905 | billingService.querySkuDetailsAsync( 906 | skuDetailsParams, 907 | new com.android.billingclient.api.SkuDetailsResponseListener() 908 | { 909 | @Override 910 | public void onSkuDetailsResponse( 911 | @NonNull BillingResult billingResult, 912 | @Nullable List detailsList) 913 | { 914 | int response = billingResult.getResponseCode(); 915 | if (response == BillingClient.BillingResponseCode.OK) 916 | { 917 | if (detailsList != null && detailsList.size() > 0) 918 | { 919 | for (com.android.billingclient.api.SkuDetails skuDetails : detailsList) 920 | { 921 | try 922 | { 923 | JSONObject object = new JSONObject(skuDetails.getOriginalJson()); 924 | productDetails.add(new SkuDetails(object)); 925 | } 926 | catch (JSONException jsonException) 927 | { 928 | jsonException.printStackTrace(); 929 | } 930 | } 931 | } 932 | 933 | reportSkuDetailsResponseCaller(productDetails, listener); 934 | } 935 | else 936 | { 937 | reportBillingError(response, null); 938 | String errorMessage = String.format(Locale.US, 939 | "Failed to retrieve info for %d products, %d", 940 | productIdList.size(), response); 941 | Log.e(LOG_TAG, errorMessage); 942 | 943 | reportSkuDetailsErrorCaller(errorMessage, listener); 944 | } 945 | } 946 | }); 947 | } 948 | catch (Exception e) 949 | { 950 | Log.e(LOG_TAG, "Failed to call getSkuDetails", e); 951 | reportBillingError(Constants.BILLING_ERROR_SKUDETAILS_FAILED, e); 952 | 953 | reportSkuDetailsErrorCaller(e.getLocalizedMessage(), listener); 954 | } 955 | } 956 | 957 | public void getPurchaseListingDetailsAsync(String productId, final ISkuDetailsResponseListener listener) 958 | { 959 | getSkuDetailsAsync(productId, Constants.PRODUCT_TYPE_MANAGED, listener); 960 | } 961 | 962 | public void getPurchaseListingDetailsAsync(ArrayList productIdList, 963 | final ISkuDetailsResponseListener listener) 964 | { 965 | getSkuDetailsAsync(productIdList, Constants.PRODUCT_TYPE_MANAGED, listener); 966 | } 967 | 968 | public void getSubscriptionListingDetailsAsync(String productId, ISkuDetailsResponseListener listener) 969 | { 970 | getSkuDetailsAsync(productId, Constants.PRODUCT_TYPE_SUBSCRIPTION, listener); 971 | } 972 | 973 | public void getSubscriptionsListingDetailsAsync(ArrayList productIds, ISkuDetailsResponseListener listener) 974 | { 975 | getSkuDetailsAsync(productIds, Constants.PRODUCT_TYPE_SUBSCRIPTION, listener); 976 | } 977 | 978 | @Nullable 979 | public PurchaseInfo getPurchaseInfo(String productId) 980 | { 981 | return getPurchaseInfo(productId, cachedProducts); 982 | } 983 | 984 | @Nullable 985 | public PurchaseInfo getSubscriptionPurchaseInfo(String productId) 986 | { 987 | return getPurchaseInfo(productId, cachedSubscriptions); 988 | } 989 | 990 | private String detectPurchaseTypeFromPurchaseResponseData(JSONObject purchase) 991 | { 992 | String purchasePayload = getPurchasePayload(); 993 | // regular flow, based on developer payload 994 | if (!TextUtils.isEmpty(purchasePayload) && purchasePayload.startsWith(Constants.PRODUCT_TYPE_SUBSCRIPTION)) 995 | { 996 | return Constants.PRODUCT_TYPE_SUBSCRIPTION; 997 | } 998 | // backup check for the promo codes (no payload available) 999 | if (purchase != null && purchase.has(Constants.RESPONSE_AUTO_RENEWING)) 1000 | { 1001 | return Constants.PRODUCT_TYPE_SUBSCRIPTION; 1002 | } 1003 | return Constants.PRODUCT_TYPE_MANAGED; 1004 | } 1005 | 1006 | private void verifyAndCachePurchase(Purchase purchase) 1007 | { 1008 | String purchaseData = purchase.getOriginalJson(); 1009 | String dataSignature = purchase.getSignature(); 1010 | try 1011 | { 1012 | JSONObject purchaseJsonObject = new JSONObject(purchaseData); 1013 | String productId = purchaseJsonObject.getString(Constants.RESPONSE_PRODUCT_ID); 1014 | if (verifyPurchaseSignature(productId, purchaseData, dataSignature)) 1015 | { 1016 | String purchaseType = 1017 | detectPurchaseTypeFromPurchaseResponseData(purchaseJsonObject); 1018 | BillingCache cache = purchaseType.equals(Constants.PRODUCT_TYPE_SUBSCRIPTION) 1019 | ? cachedSubscriptions : cachedProducts; 1020 | cache.put(productId, purchaseData, dataSignature); 1021 | if (eventHandler != null) 1022 | { 1023 | PurchaseInfo purchaseInfo = new PurchaseInfo(purchaseData, 1024 | dataSignature, 1025 | getPurchasePayload()); 1026 | reportProductPurchased(productId, purchaseInfo); 1027 | } 1028 | } 1029 | else 1030 | { 1031 | Log.e(LOG_TAG, "Public key signature doesn't match!"); 1032 | reportBillingError(Constants.BILLING_ERROR_INVALID_SIGNATURE, null); 1033 | } 1034 | } 1035 | catch (Exception e) 1036 | { 1037 | Log.e(LOG_TAG, "Error in verifyAndCachePurchase", e); 1038 | reportBillingError(Constants.BILLING_ERROR_OTHER_ERROR, e); 1039 | } 1040 | savePurchasePayload(null); 1041 | } 1042 | 1043 | private boolean verifyPurchaseSignature(String productId, String purchaseData, String dataSignature) 1044 | { 1045 | try 1046 | { 1047 | /* 1048 | * Skip the signature check if the provided License Key is NULL and return true in order to 1049 | * continue the purchase flow 1050 | */ 1051 | return TextUtils.isEmpty(signatureBase64) || 1052 | Security.verifyPurchase(productId, signatureBase64, purchaseData, dataSignature); 1053 | } 1054 | catch (Exception e) 1055 | { 1056 | return false; 1057 | } 1058 | } 1059 | 1060 | public boolean isValidPurchaseInfo(PurchaseInfo purchaseInfo) 1061 | { 1062 | return verifyPurchaseSignature(purchaseInfo.purchaseData.productId, 1063 | purchaseInfo.responseData, 1064 | purchaseInfo.signature) && 1065 | checkMerchant(purchaseInfo); 1066 | } 1067 | 1068 | private boolean isPurchaseHistoryRestored() 1069 | { 1070 | return loadBoolean(getPreferencesBaseKey() + RESTORE_KEY, false); 1071 | } 1072 | 1073 | private void setPurchaseHistoryRestored() 1074 | { 1075 | saveBoolean(getPreferencesBaseKey() + RESTORE_KEY, true); 1076 | } 1077 | 1078 | private void savePurchasePayload(String value) 1079 | { 1080 | saveString(getPreferencesBaseKey() + PURCHASE_PAYLOAD_CACHE_KEY, value); 1081 | } 1082 | 1083 | private String getPurchasePayload() 1084 | { 1085 | return loadString(getPreferencesBaseKey() + PURCHASE_PAYLOAD_CACHE_KEY, null); 1086 | } 1087 | 1088 | private void reportBillingError(int errorCode, Throwable error) 1089 | { 1090 | if (eventHandler != null && handler != null) 1091 | { 1092 | handler.post(() -> eventHandler.onBillingError(errorCode, error)); 1093 | } 1094 | } 1095 | 1096 | private void reportPurchasesSuccess(final IPurchasesResponseListener listener) 1097 | { 1098 | if (listener != null && handler != null) 1099 | { 1100 | handler.post(() -> listener.onPurchasesSuccess()); 1101 | } 1102 | } 1103 | 1104 | private void reportPurchasesError(final IPurchasesResponseListener listener) 1105 | { 1106 | if (listener != null && handler != null) 1107 | { 1108 | handler.post(() -> listener.onPurchasesError()); 1109 | } 1110 | } 1111 | 1112 | private void reportSkuDetailsErrorCaller(final String error, final ISkuDetailsResponseListener listener) 1113 | { 1114 | if (listener != null && handler != null) 1115 | { 1116 | handler.post(() -> listener.onSkuDetailsError(error)); 1117 | } 1118 | } 1119 | 1120 | private void reportSkuDetailsResponseCaller(@Nullable final List products, 1121 | final ISkuDetailsResponseListener listener) 1122 | { 1123 | if (listener != null && handler != null) 1124 | { 1125 | handler.post(() -> listener.onSkuDetailsResponse(products)); 1126 | } 1127 | } 1128 | 1129 | private void reportProductPurchased(@NonNull String productId, @Nullable PurchaseInfo details) 1130 | { 1131 | if (eventHandler != null && handler != null) 1132 | { 1133 | handler.post(() -> eventHandler.onProductPurchased(productId, details)); 1134 | } 1135 | } 1136 | 1137 | private void handlePurchase(final Purchase purchase) 1138 | { 1139 | // Verify the purchase. 1140 | // Ensure entitlement was not already granted for this purchaseToken. 1141 | // Grant entitlement to the user. 1142 | 1143 | //Acknowledging purchase 1144 | if (purchase.getPurchaseState() == Purchase.PurchaseState.PURCHASED) 1145 | { 1146 | if (purchase.isAcknowledged()) 1147 | { 1148 | verifyAndCachePurchase(purchase); 1149 | } 1150 | else 1151 | { 1152 | AcknowledgePurchaseParams acknowledgePurchaseParams = 1153 | AcknowledgePurchaseParams.newBuilder() 1154 | .setPurchaseToken(purchase.getPurchaseToken()) 1155 | .build(); 1156 | 1157 | billingService.acknowledgePurchase( 1158 | acknowledgePurchaseParams, 1159 | new AcknowledgePurchaseResponseListener() 1160 | { 1161 | @Override 1162 | public void onAcknowledgePurchaseResponse(@NonNull BillingResult billingResult) 1163 | { 1164 | if (billingResult.getResponseCode() == BillingClient.BillingResponseCode.OK) 1165 | { 1166 | verifyAndCachePurchase(purchase); 1167 | } 1168 | else 1169 | { 1170 | reportBillingError(Constants.BILLING_ERROR_FAILED_TO_ACKNOWLEDGE_PURCHASE, null); 1171 | } 1172 | } 1173 | }); 1174 | } 1175 | } 1176 | } 1177 | } 1178 | -------------------------------------------------------------------------------- /library/src/main/java/com/anjlab/android/iab/v3/Constants.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 AnjLab 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.anjlab.android.iab.v3; 17 | 18 | public class Constants 19 | { 20 | public static final String PRODUCT_TYPE_MANAGED = "inapp"; 21 | public static final String PRODUCT_TYPE_SUBSCRIPTION = "subs"; 22 | 23 | public static final String RESPONSE_ORDER_ID = "orderId"; 24 | public static final String RESPONSE_PRODUCT_ID = "productId"; 25 | public static final String RESPONSE_PACKAGE_NAME = "packageName"; 26 | public static final String RESPONSE_PURCHASE_TIME = "purchaseTime"; 27 | public static final String RESPONSE_PURCHASE_STATE = "purchaseState"; 28 | public static final String RESPONSE_PURCHASE_TOKEN = "purchaseToken"; 29 | public static final String RESPONSE_TYPE = "type"; 30 | public static final String RESPONSE_TITLE = "title"; 31 | public static final String RESPONSE_DESCRIPTION = "description"; 32 | public static final String RESPONSE_PRICE = "price"; 33 | public static final String RESPONSE_PRICE_CURRENCY = "price_currency_code"; 34 | public static final String RESPONSE_PRICE_MICROS = "price_amount_micros"; 35 | public static final String RESPONSE_SUBSCRIPTION_PERIOD = "subscriptionPeriod"; 36 | public static final String RESPONSE_AUTO_RENEWING = "autoRenewing"; 37 | public static final String RESPONSE_FREE_TRIAL_PERIOD = "freeTrialPeriod"; 38 | public static final String RESPONSE_INTRODUCTORY_PRICE = "introductoryPrice"; 39 | public static final String RESPONSE_INTRODUCTORY_PRICE_MICROS = "introductoryPriceAmountMicros"; 40 | public static final String RESPONSE_INTRODUCTORY_PRICE_PERIOD = "introductoryPricePeriod"; 41 | public static final String RESPONSE_INTRODUCTORY_PRICE_CYCLES = "introductoryPriceCycles"; 42 | 43 | public static final int BILLING_ERROR_FAILED_LOAD_PURCHASES = 100; 44 | public static final int BILLING_ERROR_FAILED_TO_INITIALIZE_PURCHASE = 101; 45 | public static final int BILLING_ERROR_INVALID_SIGNATURE = 102; 46 | public static final int BILLING_ERROR_INVALID_MERCHANT_ID = 104; 47 | public static final int BILLING_ERROR_FAILED_TO_ACKNOWLEDGE_PURCHASE = 115; 48 | 49 | @Deprecated 50 | public static final int BILLING_ERROR_LOST_CONTEXT = 103; 51 | public static final int BILLING_ERROR_PRODUCT_ID_NOT_SPECIFIED = 106; 52 | public static final int BILLING_ERROR_OTHER_ERROR = 110; 53 | public static final int BILLING_ERROR_CONSUME_FAILED = 111; 54 | public static final int BILLING_ERROR_SKUDETAILS_FAILED = 112; 55 | } 56 | -------------------------------------------------------------------------------- /library/src/main/java/com/anjlab/android/iab/v3/PurchaseData.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 AnjLab 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.anjlab.android.iab.v3; 17 | 18 | import java.util.Date; 19 | 20 | import android.os.Parcel; 21 | import android.os.Parcelable; 22 | 23 | public class PurchaseData implements Parcelable 24 | { 25 | public String orderId; 26 | public String packageName; 27 | public String productId; 28 | public Date purchaseTime; 29 | public PurchaseState purchaseState; 30 | /** 31 | * @deprecated Google does not support developer payloads anymore. 32 | */ 33 | @Deprecated 34 | public String developerPayload; 35 | public String purchaseToken; 36 | public boolean autoRenewing; 37 | 38 | @Override 39 | public int describeContents() 40 | { 41 | return 0; 42 | } 43 | 44 | @Override 45 | public void writeToParcel(Parcel dest, int flags) 46 | { 47 | dest.writeString(this.orderId); 48 | dest.writeString(this.packageName); 49 | dest.writeString(this.productId); 50 | dest.writeLong(purchaseTime != null ? purchaseTime.getTime() : -1); 51 | dest.writeInt(this.purchaseState == null ? -1 : this.purchaseState.ordinal()); 52 | dest.writeString(this.developerPayload); 53 | dest.writeString(this.purchaseToken); 54 | dest.writeByte(autoRenewing ? (byte) 1 : (byte) 0); 55 | } 56 | 57 | public PurchaseData() 58 | { 59 | } 60 | 61 | protected PurchaseData(Parcel in) 62 | { 63 | this.orderId = in.readString(); 64 | this.packageName = in.readString(); 65 | this.productId = in.readString(); 66 | long tmpPurchaseTime = in.readLong(); 67 | this.purchaseTime = tmpPurchaseTime == -1 ? null : new Date(tmpPurchaseTime); 68 | int tmpPurchaseState = in.readInt(); 69 | this.purchaseState = 70 | tmpPurchaseState == -1 ? null : PurchaseState.values()[tmpPurchaseState]; 71 | this.developerPayload = in.readString(); 72 | this.purchaseToken = in.readString(); 73 | this.autoRenewing = in.readByte() != 0; 74 | } 75 | 76 | public static final Parcelable.Creator CREATOR = 77 | new Parcelable.Creator() 78 | { 79 | public PurchaseData createFromParcel(Parcel source) 80 | { 81 | return new PurchaseData(source); 82 | } 83 | 84 | public PurchaseData[] newArray(int size) 85 | { 86 | return new PurchaseData[size]; 87 | } 88 | }; 89 | } -------------------------------------------------------------------------------- /library/src/main/java/com/anjlab/android/iab/v3/PurchaseInfo.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 AnjLab 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.anjlab.android.iab.v3; 17 | 18 | import org.json.JSONException; 19 | import org.json.JSONObject; 20 | 21 | import java.util.Date; 22 | 23 | import android.os.Parcel; 24 | import android.os.Parcelable; 25 | import android.util.Log; 26 | 27 | /** 28 | * With this PurchaseInfo a developer is able verify 29 | * a purchase from the google play store on his own 30 | * server. An example implementation of how to verify 31 | * a purchase you can find here: 32 | *
 33 |  * See  here 
 34 |  * 
35 | */ 36 | public class PurchaseInfo implements Parcelable 37 | { 38 | private static final String LOG_TAG = "iabv3.purchaseInfo"; 39 | 40 | public final String responseData; 41 | public final String signature; 42 | /** 43 | * @deprecated Google does not support developer payloads anymore. 44 | */ 45 | @Deprecated 46 | public final String developerPayload; 47 | public final PurchaseData purchaseData; 48 | 49 | public PurchaseInfo(String responseData, String signature) 50 | { 51 | this.responseData = responseData; 52 | this.signature = signature; 53 | this.developerPayload = ""; 54 | this.purchaseData = parseResponseDataImpl(); 55 | } 56 | 57 | public PurchaseInfo(String responseData, String signature, String developerPayload) 58 | { 59 | this.responseData = responseData; 60 | this.signature = signature; 61 | this.developerPayload = developerPayload; 62 | this.purchaseData = parseResponseDataImpl(); 63 | } 64 | 65 | PurchaseData parseResponseDataImpl() 66 | { 67 | try 68 | { 69 | JSONObject json = new JSONObject(responseData); 70 | PurchaseData data = new PurchaseData(); 71 | data.orderId = json.optString(Constants.RESPONSE_ORDER_ID); 72 | data.packageName = json.optString(Constants.RESPONSE_PACKAGE_NAME); 73 | data.productId = json.optString(Constants.RESPONSE_PRODUCT_ID); 74 | long purchaseTimeMillis = json.optLong(Constants.RESPONSE_PURCHASE_TIME, 0); 75 | data.purchaseTime = purchaseTimeMillis != 0 ? new Date(purchaseTimeMillis) : null; 76 | data.purchaseState = PurchaseState.values()[json.optInt(Constants.RESPONSE_PURCHASE_STATE, 1)]; 77 | data.developerPayload = developerPayload; 78 | data.purchaseToken = json.getString(Constants.RESPONSE_PURCHASE_TOKEN); 79 | data.autoRenewing = json.optBoolean(Constants.RESPONSE_AUTO_RENEWING); 80 | return data; 81 | } 82 | catch (JSONException e) 83 | { 84 | Log.e(LOG_TAG, "Failed to parse response data", e); 85 | return null; 86 | } 87 | } 88 | 89 | @Override 90 | public int describeContents() 91 | { 92 | return 0; 93 | } 94 | 95 | @Override 96 | public void writeToParcel(Parcel dest, int flags) 97 | { 98 | dest.writeString(this.responseData); 99 | dest.writeString(this.developerPayload); 100 | dest.writeString(this.signature); 101 | } 102 | 103 | protected PurchaseInfo(Parcel in) 104 | { 105 | this.responseData = in.readString(); 106 | this.developerPayload = in.readString(); 107 | this.signature = in.readString(); 108 | this.purchaseData = parseResponseDataImpl(); 109 | } 110 | 111 | public static final Parcelable.Creator CREATOR = 112 | new Parcelable.Creator() 113 | { 114 | public PurchaseInfo createFromParcel(Parcel source) 115 | { 116 | return new PurchaseInfo(source); 117 | } 118 | 119 | public PurchaseInfo[] newArray(int size) 120 | { 121 | return new PurchaseInfo[size]; 122 | } 123 | }; 124 | 125 | @Override 126 | public boolean equals(Object o) 127 | { 128 | if (this == o) 129 | { 130 | return true; 131 | } 132 | if (o == null || !(o instanceof PurchaseInfo)) 133 | { 134 | return false; 135 | } 136 | PurchaseInfo other = (PurchaseInfo) o; 137 | return responseData.equals(other.responseData) 138 | && signature.equals(other.signature) 139 | && developerPayload.equals(other.developerPayload) 140 | && purchaseData.purchaseToken.equals(other.purchaseData.purchaseToken) 141 | && purchaseData.purchaseTime.equals(other.purchaseData.purchaseTime); 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /library/src/main/java/com/anjlab/android/iab/v3/PurchaseState.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 AnjLab 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.anjlab.android.iab.v3; 17 | 18 | public enum PurchaseState 19 | { 20 | PurchasedSuccessfully, 21 | Canceled, 22 | Refunded, 23 | SubscriptionExpired 24 | } 25 | -------------------------------------------------------------------------------- /library/src/main/java/com/anjlab/android/iab/v3/Security.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Google Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package com.anjlab.android.iab.v3; 17 | 18 | import android.text.TextUtils; 19 | import android.util.Base64; 20 | import android.util.Log; 21 | 22 | import java.security.InvalidKeyException; 23 | import java.security.KeyFactory; 24 | import java.security.NoSuchAlgorithmException; 25 | import java.security.PublicKey; 26 | import java.security.Signature; 27 | import java.security.SignatureException; 28 | import java.security.spec.InvalidKeySpecException; 29 | import java.security.spec.X509EncodedKeySpec; 30 | 31 | /** 32 | * Security-related methods. For a secure implementation, all of this code 33 | * should be implemented on a server that communicates with the 34 | * application on the device. For the sake of simplicity and clarity of this 35 | * example, this code is included here and is executed on the device. If you 36 | * must verify the purchases on the phone, you should obfuscate this code to 37 | * make it harder for an attacker to replace the code with stubs that treat all 38 | * purchases as verified. 39 | */ 40 | class Security 41 | { 42 | private static final String TAG = "IABUtil/Security"; 43 | 44 | private static final String KEY_FACTORY_ALGORITHM = "RSA"; 45 | private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; 46 | 47 | /** 48 | * Verifies that the data was signed with the given signature, and returns 49 | * the verified purchase. The data is in JSON format and signed 50 | * with a private key. The data also contains the {@link PurchaseState} 51 | * and product ID of the purchase. 52 | * 53 | * @param productId the product Id used for debug validation. 54 | * @param base64PublicKey the base64-encoded public key to use for verifying. 55 | * @param signedData the signed JSON string (signed, not encrypted) 56 | * @param signature the signature for the data, signed with the private key 57 | */ 58 | public static boolean verifyPurchase(String productId, String base64PublicKey, 59 | String signedData, String signature) 60 | { 61 | if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) || 62 | TextUtils.isEmpty(signature)) 63 | { 64 | 65 | if ( 66 | productId.equals("android.test.purchased") || 67 | productId.equals("android.test.canceled") || 68 | productId.equals("android.test.refunded") || 69 | productId.equals("android.test.item_unavailable") 70 | ) 71 | { 72 | return true; 73 | } 74 | 75 | Log.e(TAG, "Purchase verification failed: missing data."); 76 | return false; 77 | } 78 | 79 | PublicKey key = Security.generatePublicKey(base64PublicKey); 80 | return Security.verify(key, signedData, signature); 81 | } 82 | 83 | /** 84 | * Generates a PublicKey instance from a string containing the 85 | * Base64-encoded public key. 86 | * 87 | * @param encodedPublicKey Base64-encoded public key 88 | * @throws IllegalArgumentException if encodedPublicKey is invalid 89 | */ 90 | public static PublicKey generatePublicKey(String encodedPublicKey) 91 | { 92 | try 93 | { 94 | byte[] decodedKey = Base64.decode(encodedPublicKey, Base64.DEFAULT); 95 | KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); 96 | return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); 97 | } 98 | catch (NoSuchAlgorithmException e) 99 | { 100 | throw new RuntimeException(e); 101 | } 102 | catch (InvalidKeySpecException e) 103 | { 104 | Log.e(TAG, "Invalid key specification."); 105 | throw new IllegalArgumentException(e); 106 | } 107 | catch (IllegalArgumentException e) 108 | { 109 | Log.e(TAG, "Base64 decoding failed."); 110 | throw e; 111 | } 112 | } 113 | 114 | /** 115 | * Verifies that the signature from the server matches the computed 116 | * signature on the data. Returns true if the data is correctly signed. 117 | * 118 | * @param publicKey public key associated with the developer account 119 | * @param signedData signed data from server 120 | * @param signature server signature 121 | * @return true if the data and signature match 122 | */ 123 | public static boolean verify(PublicKey publicKey, String signedData, String signature) 124 | { 125 | Signature sig; 126 | try 127 | { 128 | sig = Signature.getInstance(SIGNATURE_ALGORITHM); 129 | sig.initVerify(publicKey); 130 | sig.update(signedData.getBytes()); 131 | if (!sig.verify(Base64.decode(signature, Base64.DEFAULT))) 132 | { 133 | Log.e(TAG, "Signature verification failed."); 134 | return false; 135 | } 136 | return true; 137 | } 138 | catch (NoSuchAlgorithmException e) 139 | { 140 | Log.e(TAG, "NoSuchAlgorithmException."); 141 | } 142 | catch (InvalidKeyException e) 143 | { 144 | Log.e(TAG, "Invalid key specification."); 145 | } 146 | catch (SignatureException e) 147 | { 148 | Log.e(TAG, "Signature exception."); 149 | } 150 | catch (IllegalArgumentException e) 151 | { 152 | Log.e(TAG, "Base64 decoding failed."); 153 | } 154 | return false; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /library/src/main/java/com/anjlab/android/iab/v3/SkuDetails.java: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2014 AnjLab 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package com.anjlab.android.iab.v3; 17 | 18 | import android.os.Parcel; 19 | import android.os.Parcelable; 20 | import android.text.TextUtils; 21 | 22 | import org.json.JSONException; 23 | import org.json.JSONObject; 24 | 25 | import java.util.Locale; 26 | 27 | public class SkuDetails implements Parcelable 28 | { 29 | 30 | public final String productId; 31 | 32 | public final String title; 33 | 34 | public final String description; 35 | 36 | public final boolean isSubscription; 37 | 38 | public final String currency; 39 | 40 | public final Double priceValue; 41 | 42 | public final String subscriptionPeriod; 43 | 44 | public final String subscriptionFreeTrialPeriod; 45 | 46 | public final boolean haveTrialPeriod; 47 | 48 | public final double introductoryPriceValue; 49 | 50 | public final String introductoryPricePeriod; 51 | 52 | public final boolean haveIntroductoryPeriod; 53 | 54 | public final int introductoryPriceCycles; 55 | 56 | /** 57 | * Use this value to return the raw price from the product. 58 | * This allows math to be performed without needing to worry about errors 59 | * caused by floating point representations of the product's price. 60 | *

61 | * This is in micros from the Play Store. 62 | */ 63 | public final long priceLong; 64 | 65 | public final String priceText; 66 | 67 | public final long introductoryPriceLong; 68 | 69 | public final String introductoryPriceText; 70 | 71 | public final String responseData; 72 | 73 | public SkuDetails(JSONObject source) throws JSONException 74 | { 75 | String responseType = source.optString(Constants.RESPONSE_TYPE); 76 | if (responseType == null) 77 | { 78 | responseType = Constants.PRODUCT_TYPE_MANAGED; 79 | } 80 | productId = source.optString(Constants.RESPONSE_PRODUCT_ID); 81 | title = source.optString(Constants.RESPONSE_TITLE); 82 | description = source.optString(Constants.RESPONSE_DESCRIPTION); 83 | isSubscription = responseType.equalsIgnoreCase(Constants.PRODUCT_TYPE_SUBSCRIPTION); 84 | currency = source.optString(Constants.RESPONSE_PRICE_CURRENCY); 85 | priceLong = source.optLong(Constants.RESPONSE_PRICE_MICROS); 86 | priceValue = priceLong / 1000000d; 87 | priceText = source.optString(Constants.RESPONSE_PRICE); 88 | subscriptionPeriod = source.optString(Constants.RESPONSE_SUBSCRIPTION_PERIOD); 89 | subscriptionFreeTrialPeriod = source.optString(Constants.RESPONSE_FREE_TRIAL_PERIOD); 90 | haveTrialPeriod = !TextUtils.isEmpty(subscriptionFreeTrialPeriod); 91 | introductoryPriceLong = source.optLong(Constants.RESPONSE_INTRODUCTORY_PRICE_MICROS); 92 | introductoryPriceValue = introductoryPriceLong / 1000000d; 93 | introductoryPriceText = source.optString(Constants.RESPONSE_INTRODUCTORY_PRICE); 94 | introductoryPricePeriod = source.optString(Constants.RESPONSE_INTRODUCTORY_PRICE_PERIOD); 95 | haveIntroductoryPeriod = !TextUtils.isEmpty(introductoryPricePeriod); 96 | introductoryPriceCycles = source.optInt(Constants.RESPONSE_INTRODUCTORY_PRICE_CYCLES); 97 | responseData = source.toString(); 98 | } 99 | 100 | @Override 101 | public String toString() 102 | { 103 | return String.format(Locale.US, "%s: %s(%s) %f in %s (%s)", 104 | productId, 105 | title, 106 | description, 107 | priceValue, 108 | currency, 109 | priceText); 110 | } 111 | 112 | @Override 113 | public boolean equals(Object o) 114 | { 115 | if (this == o) 116 | { 117 | return true; 118 | } 119 | if (o == null || getClass() != o.getClass()) 120 | { 121 | return false; 122 | } 123 | 124 | SkuDetails that = (SkuDetails) o; 125 | 126 | if (isSubscription != that.isSubscription) 127 | { 128 | return false; 129 | } 130 | return !(productId != null ? !productId.equals(that.productId) : that.productId != null); 131 | } 132 | 133 | @Override 134 | public int hashCode() 135 | { 136 | int result = productId != null ? productId.hashCode() : 0; 137 | result = 31 * result + (isSubscription ? 1 : 0); 138 | return result; 139 | } 140 | 141 | @Override 142 | public int describeContents() 143 | { 144 | return 0; 145 | } 146 | 147 | @Override 148 | public void writeToParcel(Parcel dest, int flags) 149 | { 150 | dest.writeString(this.productId); 151 | dest.writeString(this.title); 152 | dest.writeString(this.description); 153 | dest.writeByte(isSubscription ? (byte) 1 : (byte) 0); 154 | dest.writeString(this.currency); 155 | dest.writeDouble(this.priceValue); 156 | dest.writeLong(this.priceLong); 157 | dest.writeString(this.priceText); 158 | dest.writeString(this.subscriptionPeriod); 159 | dest.writeString(this.subscriptionFreeTrialPeriod); 160 | dest.writeByte(this.haveTrialPeriod ? (byte) 1 : (byte) 0); 161 | dest.writeDouble(this.introductoryPriceValue); 162 | dest.writeLong(this.introductoryPriceLong); 163 | dest.writeString(this.introductoryPriceText); 164 | dest.writeString(this.introductoryPricePeriod); 165 | dest.writeByte(this.haveIntroductoryPeriod ? (byte) 1 : (byte) 0); 166 | dest.writeInt(this.introductoryPriceCycles); 167 | dest.writeString(this.responseData); 168 | } 169 | 170 | protected SkuDetails(Parcel in) 171 | { 172 | this.productId = in.readString(); 173 | this.title = in.readString(); 174 | this.description = in.readString(); 175 | this.isSubscription = in.readByte() != 0; 176 | this.currency = in.readString(); 177 | this.priceValue = in.readDouble(); 178 | this.priceLong = in.readLong(); 179 | this.priceText = in.readString(); 180 | this.subscriptionPeriod = in.readString(); 181 | this.subscriptionFreeTrialPeriod = in.readString(); 182 | this.haveTrialPeriod = in.readByte() != 0; 183 | this.introductoryPriceValue = in.readDouble(); 184 | this.introductoryPriceLong = in.readLong(); 185 | this.introductoryPriceText = in.readString(); 186 | this.introductoryPricePeriod = in.readString(); 187 | this.haveIntroductoryPeriod = in.readByte() != 0; 188 | this.introductoryPriceCycles = in.readInt(); 189 | this.responseData = in.readString(); 190 | } 191 | 192 | public static final Parcelable.Creator CREATOR = 193 | new Parcelable.Creator() 194 | { 195 | public SkuDetails createFromParcel(Parcel source) 196 | { 197 | return new SkuDetails(source); 198 | } 199 | 200 | public SkuDetails[] newArray(int size) 201 | { 202 | return new SkuDetails[size]; 203 | } 204 | }; 205 | } 206 | -------------------------------------------------------------------------------- /sample/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 10 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /sample/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.google.android.libraries.mapsplatform.secrets-gradle-plugin' 3 | } 4 | apply plugin: 'com.android.application' 5 | 6 | dependencies { 7 | implementation project(':library') 8 | // implementation fileTree(dir: 'libs', include: '*.jar') 9 | // implementation 'com.anjlab.android.iab.v3:library:2.1.0' 10 | implementation 'androidx.annotation:annotation:1.3.0' 11 | } 12 | 13 | android { 14 | namespace 'com.anjlab.android.iab.v3.sample2' 15 | 16 | defaultConfig { 17 | versionCode 6 18 | versionName '6.0' 19 | minSdkVersion 21 20 | targetSdkVersion 34 21 | compileSdk = 34 22 | buildToolsVersion = '30.0.3' 23 | } 24 | 25 | sourceSets.main { 26 | manifest.srcFile 'AndroidManifest.xml' 27 | java.srcDir 'src' 28 | resources.srcDir 'src' 29 | res.srcDir 'res' 30 | } 31 | 32 | signingConfigs { 33 | release 34 | } 35 | 36 | buildTypes { 37 | release { 38 | signingConfig signingConfigs.release 39 | } 40 | } 41 | 42 | if (project.hasProperty('keyStoreFile')) { 43 | android.signingConfigs.release.storeFile = file(keyStoreFile) 44 | } 45 | 46 | if (project.hasProperty('keyStorePassword')) { 47 | android.signingConfigs.release.storePassword = keyStorePassword 48 | } 49 | 50 | if (project.hasProperty('keyStoreKeyAlias')) { 51 | android.signingConfigs.release.keyAlias = keyStoreKeyAlias 52 | } 53 | 54 | if (project.hasProperty('keyStoreKeyPassword')) { 55 | android.signingConfigs.release.keyPassword = keyStoreKeyPassword 56 | } 57 | } -------------------------------------------------------------------------------- /sample/libs/anjlab-iabv3-current.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjlab/android-inapp-billing-v3/a13bb215b1176eac9d1d78322262de2bd237303e/sample/libs/anjlab-iabv3-current.jar -------------------------------------------------------------------------------- /sample/res/drawable-anydpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/anjlab/android-inapp-billing-v3/a13bb215b1176eac9d1d78322262de2bd237303e/sample/res/drawable-anydpi/ic_launcher.png -------------------------------------------------------------------------------- /sample/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | 13 | 20 | 21 |