Mike преди 7 години
родител
ревизия
ff893a0467
променени са 32 файла, в които са добавени 5486 реда и са изтрити 2 реда
  1. 3 2
      miaomiao/package.json
  2. 3 0
      miaomiao/plugins/android.json
  3. 275 0
      miaomiao/plugins/cordova-plugin-inapppurchase/README.md
  4. 37 0
      miaomiao/plugins/cordova-plugin-inapppurchase/gulpfile.babel.js
  5. 56 0
      miaomiao/plugins/cordova-plugin-inapppurchase/package.json
  6. 66 0
      miaomiao/plugins/cordova-plugin-inapppurchase/plugin.xml
  7. 570 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/android/Base64.java
  8. 32 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/android/Base64DecoderException.java
  9. 43 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/android/IabException.java
  10. 1007 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/android/IabHelper.java
  11. 45 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/android/IabResult.java
  12. 406 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/android/InAppBillingV3.java
  13. 91 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/android/Inventory.java
  14. 63 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/android/Purchase.java
  15. 123 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/android/Security.java
  16. 64 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/android/SkuDetails.java
  17. 144 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/android/billing/IInAppBillingService.aidl
  18. 20 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/ios/PaymentsPlugin.h
  19. 145 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/ios/PaymentsPlugin.m
  20. 308 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/ios/RMStore.h
  21. 791 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/ios/RMStore.m
  22. 131 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/js/index-android.js
  23. 108 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/js/index-ios.js
  24. 7 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/js/polyfills-android.js
  25. 43 0
      miaomiao/plugins/cordova-plugin-inapppurchase/src/js/utils.js
  26. 425 0
      miaomiao/plugins/cordova-plugin-inapppurchase/test/index-android.js
  27. 265 0
      miaomiao/plugins/cordova-plugin-inapppurchase/test/index-ios.js
  28. 40 0
      miaomiao/plugins/cordova-plugin-inapppurchase/test/utils.js
  29. 7 0
      miaomiao/plugins/cordova-plugin-inapppurchase/www/index-android.js
  30. 157 0
      miaomiao/plugins/cordova-plugin-inapppurchase/www/index-ios.js
  31. 8 0
      miaomiao/plugins/fetch.json
  32. 3 0
      miaomiao/plugins/ios.json

+ 3 - 2
miaomiao/package.json

xqd xqd
@@ -50,7 +50,8 @@
     "cordova-plugin-camera",
     "jpush-phonegap-plugin",
     "cordova-plugin-wechat",
-    "cordova-plugin-video-editor"
+    "cordova-plugin-video-editor",
+    "cordova-plugin-inapppurchase"
   ],
   "cordovaPlatforms": [
     "android",
@@ -87,4 +88,4 @@
     },
     "platforms": []
   }
-}
+}

+ 3 - 0
miaomiao/plugins/android.json

xqd
@@ -40,6 +40,9 @@
         },
         "cordova-plugin-video-editor": {
             "PACKAGE_NAME": "com.miaomiao.app"
+        },
+        "cordova-plugin-inapppurchase": {
+            "PACKAGE_NAME": "com.miaomiao.app"
         }
     },
     "dependent_plugins": {

+ 275 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/README.md

xqd
@@ -0,0 +1,275 @@
+# cordova-plugin-inapppurchase 📱💰
+
+[![Build Status](https://travis-ci.org/AlexDisler/cordova-plugin-inapppurchase.svg?branch=master)](https://travis-ci.org/AlexDisler/cordova-plugin-inapppurchase)
+[![Coverage Status](https://coveralls.io/repos/github/AlexDisler/cordova-plugin-inapppurchase/badge.svg?branch=master&a=1)](https://coveralls.io/github/AlexDisler/cordova-plugin-inapppurchase?branch=master)
+
+A lightweight Cordova plugin for in app purchases on iOS/Android. See [demo app](https://github.com/AlexDisler/cordova-inapppurchases-app) and [blog post](https://alexdisler.com/2016/02/29/in-app-purchases-ionic-cordova/).
+
+## Looking for maintainers
+
+If you would like to maintain this project, get in touch.
+
+## Features
+
+- Simple, promise-based API
+- Support for consumable/non-consumable products and paid/free subscriptions
+- Support for restoring purchases
+- Uses well tested native libraries internally - [RMStore](https://github.com/robotmedia/RMStore) for iOS and an adjusted  [com.google.payments](https://github.com/MobileChromeApps/cordova-plugin-google-payments/tree/master/src/android) for Android
+
+## Install
+
+    $ cordova plugin add cordova-plugin-inapppurchase
+
+## Configuration
+
+### iOS
+
+No configuration is necessary.
+
+### Android
+
+You must create a ```manifest.json``` in your project's ```www``` folder with your Android Billing Key:
+
+    { "play_store_key": "<Base64-encoded public key from the Google Play Store>" }
+
+You can get this key from the Google Play Store (under "Services & APIs") after uploading your app.
+
+## Setting up and testing purchases
+
+- [Configuring Cordova in app purchases on iOS and Android](https://alexdisler.com/2016/04/30/configuring-in-app-purchases-cordova-ionic-ios-android/)
+- [Testing in app purchases](https://alexdisler.com/2016/04/04/testing-cordova-in-app-purchases-on-ios-android/)
+- [Receipt validation (with nodejs)](https://alexdisler.com/2016/03/20/validating-cordova-in-app-purchases-on-ios-and-android-using-nodejs/)
+- [Tips for signing and running Cordova apps on Android to test in app purchases locally](https://alexdisler.com/2016/04/01/tips-for-signing-installing-cordova-apps-on-android/)
+
+## API
+
+All functions return a Promise.
+
+### Get Products
+
+#### inAppPurchase.getProducts(productIds)
+
+- ___productIds___ - an array of product ids
+
+Retrieves a list of full product data from Apple/Google. **This function must be called before making purchases.**
+
+If successful, the promise resolves to an array of objects. Each object has the following attributes:
+
+- ```productId``` - SKU / product bundle id (such as 'com.yourapp.prod1')
+- ```title``` - short localized title
+- ```description``` - long localized description
+- ```currency``` - currency of the price (such as 'EUR', 'USD')
+- ```price``` - localized price
+- ```priceAsDecimal``` - price as a decimal
+
+___Example:___
+
+```js
+inAppPurchase
+  .getProducts(['com.yourapp.prod1', 'com.yourapp.prod2', ...])
+  .then(function (products) {
+    console.log(products);
+    /*
+       [{ productId: 'com.yourapp.prod1', 'title': '...', description: '...', currency: '...', price: '...', priceAsDecimal: '...' }, ...]
+    */
+  })
+  .catch(function (err) {
+    console.log(err);
+  });
+```
+
+### Buy
+
+#### inAppPurchase.buy(productId)
+
+- ___productId___ - a string of the productId
+
+If successful, the promise resolves to an object with the following attributes that you will need for the receipt validation:
+
+- ```transactionId``` - The transaction/order id
+- ```receipt``` - On ***iOS*** it will be the base64 string of the receipt, on ***Android*** it will be a string of a json with all the transaction details required for validation such as ```{"orderId":"...","packageName:"...","productId":"...","purchaseTime":"...", "purchaseState":"...","purchaseToken":"..."}```
+- ```signature``` - On Android it can be used to [consume](https://github.com/AlexDisler/cordova-plugin-inapppurchase#inapppurchaseconsumeproducttype-receipt-signature) a purchase. On iOS it will be an empty string.
+- ```productType``` - On Android it can be used to [consume](https://github.com/AlexDisler/cordova-plugin-inapppurchase#inapppurchaseconsumeproducttype-receipt-signature) a purchase. On iOS it will be an empty string.
+
+***Receipt validation:*** - To [validate your receipt](https://alexdisler.com/2016/03/20/validating-cordova-in-app-purchases-on-ios-and-android-using-nodejs/), you will need the ```receipt``` and ```signature``` on Android and the ```receipt``` and ```transactionId``` on iOS.
+
+___Example:___
+
+```js
+inAppPurchase
+  .buy('com.yourapp.prod1')
+  .then(function (data) {
+    console.log(data);
+    /*
+      {
+        transactionId: ...
+        receipt: ...
+        signature: ...
+      }
+    */
+  })
+  .catch(function (err) {
+    console.log(err);
+  });
+```
+
+### Subscribe
+
+#### inAppPurchase.subscribe(productId)
+
+- ___productId___ - a string of the productId
+
+This function behaves the same as [buy()](https://github.com/AlexDisler/cordova-plugin-inapppurchase#buy) but with subscriptions.
+
+### Consume
+
+#### inAppPurchase.consume(productType, receipt, signature)
+
+- ___productType___ - string
+- ___receipt___ - string (containing a json)
+- ___signature___ - string
+
+All 3 parameters are returned by the [buy()](https://github.com/AlexDisler/cordova-plugin-inapppurchase#buy) or [restorePurchases()](https://github.com/AlexDisler/cordova-plugin-inapppurchase#inapppurchaserestorepurchases) functions.
+
+Call this function after purchasing a "consumable" product to mark it as consumed.
+
+___NOTE: This function is only relevant to Android purchases.___
+
+On ***Android***, you must consume products that you want to let the user purchase multiple times. If you will not consume the product after a purchase, the next time you will attempt to purchase it you will get the error message:
+```Unable to buy item / Item already owned```.
+
+On ***iOS*** there is no need to "consume" a product. However, in order to make your code cross platform, it is recommended to call it for iOS consumable purchases as well.
+
+___Example:___
+
+```js
+// first buy the product...
+inAppPurchase
+  .buy('com.yourapp.consumable_prod1')
+  .then(function (data) {
+    // ...then mark it as consumed:
+    return inAppPurchase.consume(data.productType, data.receipt, data.signature);
+  })
+  .then(function () {
+    console.log('product was successfully consumed!');
+  })
+  .catch(function (err) {
+    console.log(err);
+  });
+```
+
+### Restore Purchases
+
+#### inAppPurchase.restorePurchases()
+
+If successful, the promise resolves to an array of objects with the following attributes:
+
+- ```productId```
+- ```state``` - the state of the product. On ***Android*** the statuses are: ```0 - ACTIVE, 1 - CANCELLED,  2 - REFUNDED```
+- ```transactionId```
+- ```date``` - timestamp of the purchase
+- ```productType``` - On Android it can be used to [consume](https://github.com/AlexDisler/cordova-plugin-inapppurchase#inapppurchaseconsumeproducttype-receipt-signature) a purchase. On iOS it will be an empty string.
+- ```receipt``` - On Android it can be used to [consume](https://github.com/AlexDisler/cordova-plugin-inapppurchase#inapppurchaseconsumeproducttype-receipt-signature) a purchase. On iOS it will be an empty string.
+- ```signature``` - On Android it can be used to [consume](https://github.com/AlexDisler/cordova-plugin-inapppurchase#inapppurchaseconsumeproducttype-receipt-signature) a purchase. On iOS it will be an empty string.
+
+___Example:___
+
+```js
+inAppPurchase
+  .restorePurchases()
+  .then(function (data) {
+    console.log(data);
+    /*
+      [{
+        transactionId: ...
+        productId: ...
+        state: ...
+        date: ...
+      }]
+    */
+  })
+  .catch(function (err) {
+    console.log(err);
+  });
+```
+
+See [Differences Between Product Types](https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/Products.html)
+
+### Get Receipt
+
+#### inAppPurchase.getReceipt()
+
+On ***iOS***, you can get the receipt at any moment by calling the getReceipt() function. Note that on iOS the receipt can contain multiple transactions. If successful, the promise returned by this function will resolve to a string with the receipt.
+
+On ***Android*** this function will always return an empty string since it's not needed for Android purchases.
+
+___Example:___
+
+```js
+inAppPurchase
+  .getReceipt()
+  .then(function (receipt) {
+    console.log(receipt);
+  })
+  .catch(function (err) {
+    console.log(err);
+  });
+```
+
+## Developing
+
+### Build:
+
+    $ npm install
+    $ gulp watch
+
+### Run tests:
+
+    $ npm test
+
+Or, if you would like to watch and re-run tests:
+
+    $ npm run watch
+
+Coverage report:
+
+    $ nyc npm test
+
+## Debugging
+
+- Are you testing on a real device? In App purchases are not supported in emulators/simulators.
+- Have you enabled In-App Purchases for your App ID?
+- Have you checked Cleared for Sale for your product?
+- Does your project’s .plist Bundle ID match your App ID?
+- Have you generated and installed a new provisioning profile for the new App ID?
+- Have you configured your project to code sign using this new provisioning profile?
+- Have you waited several hours since adding your product to iTunes Connect?
+- Have you tried deleting the app from your device and reinstalling?
+- Have you accepted contracts for IAPs in iTunes connect?
+- Is your device jailbroken? If so, you need to revert the jailbreak for IAP to work.
+
+## Thanks / Credits
+
+- [Adar Porat](https://github.com/aporat)
+- [Robot Media](https://github.com/robotmedia)
+- [MobileChromeApps](https://github.com/MobileChromeApps)
+
+## More
+
+- [cordova-icon](https://github.com/AlexDisler/cordova-icon) - automatic icon resizing for cordova
+- [ng-special-offer](https://github.com/AlexDisler/ng-special-offer) - prompt users to rate your cordova app in the app store
+- [ionic-lock-screen](https://github.com/AlexDisler/ionic-lock-screen) - passcode lock screen for ionic (with touch id support for iOS)
+- [ionic-zoom-view](https://github.com/AlexDisler/ionic-zoom-view) - an easy way to add a zoom view to images using an ionic modal
+- [ng-persist](https://github.com/AlexDisler/ng-persist) - store data on mobile devices (using cordova) that persists even if the user reinstalls the app
+
+## License
+
+The MIT License
+
+Copyright (c) 2016, Alex Disler
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 37 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/gulpfile.babel.js

xqd
@@ -0,0 +1,37 @@
+import concat  from 'gulp-concat';
+import gulp    from 'gulp';
+import plumber from 'gulp-plumber';
+import babel   from 'gulp-babel';
+import addsrc  from 'gulp-add-src';
+
+const src = './src/js/';
+const dist = './www';
+const indexAndroid = 'index-android.js';
+const polyfillsAndroid = 'polyfills-android.js';
+const indexIos = 'index-ios.js';
+const utils = 'utils.js';
+
+const build = () => {
+  gulp
+    .src([ src + utils, src + indexIos ])
+    .pipe(plumber())
+    .pipe(babel())
+    .pipe(concat(indexIos))
+    .pipe(gulp.dest(dist));
+  gulp
+    .src([ src + utils, src + indexAndroid ])
+    .pipe(plumber())
+    .pipe(babel())
+    .pipe(addsrc.prepend(src + polyfillsAndroid))
+    .pipe(concat(indexAndroid))
+    .pipe(gulp.dest(dist));
+};
+
+gulp.task('build', build);
+
+gulp.task('watch', () => {
+  gulp.run(['build']);
+  gulp.watch(src + '*.js', ['build']);
+});
+
+gulp.task('default', ['build']);

+ 56 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/package.json

xqd
@@ -0,0 +1,56 @@
+{
+  "name": "cordova-plugin-inapppurchase",
+  "version": "1.2.0",
+  "description": "A lightweight cordova plugin for in app purchases on iOS and Android with a simple promise based API.",
+  "main": "www/index-ios.js",
+  "scripts": {
+    "test": "mocha --compilers js:babel-core/register --require babel-polyfill test/*",
+    "watch": "mocha --watch --compilers js:babel-core/register --require babel-polyfill test/*",
+    "coverage": "nyc npm test && nyc report --reporter=text-lcov | coveralls"
+  },
+  "repository": {
+    "type": "git",
+    "url": "git://github.com/AlexDisler/cordova-plugin-inapppurchase.git"
+  },
+  "keywords": [
+    "cordova",
+    "ecosystem:cordova",
+    "purchase",
+    "purchases",
+    "cordova-android",
+    "cordova-ios",
+    "payments",
+    "in",
+    "app",
+    "promise",
+    "promises",
+    "android",
+    "ios",
+    "itunes",
+    "google",
+    "play",
+    "ionic"
+  ],
+  "author": {
+    "name": "Alex Disler",
+    "email": "alexdisler@gmail.com",
+    "url": "alexdisler.com"
+  },
+  "license": "MIT",
+  "homepage": "https://github.com/AlexDisler/cordova-plugin-inapppurchase",
+  "devDependencies": {
+    "babel-core": "6.5.2",
+    "babel-plugin-syntax-async-functions": "6.5.0",
+    "babel-plugin-transform-regenerator": "6.5.2",
+    "babel-polyfill": "6.5.0",
+    "babel-preset-es2015": "6.5.0",
+    "gulp": "3.9.1",
+    "gulp-add-src": "^0.2.0",
+    "gulp-babel": "6.1.2",
+    "gulp-concat": "2.6.0",
+    "gulp-plumber": "1.1.0",
+    "gulp-util": "^3.0.7",
+    "mocha": "^2.4.5",
+    "vinyl-source-stream": "^1.1.0"
+  }
+}

+ 66 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/plugin.xml

xqd
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<plugin xmlns="http://apache.org/cordova/ns/plugins/1.0"
+  xmlns:android="http://schemas.android.com/apk/res/android"
+  id="cordova-plugin-inapppurchase"
+  version="1.1.0">
+
+  <name>In App Purchase</name>
+  <description>A lightweight cordova plugin for in app purchases on iOS and Android with a simple promise based API.</description>
+  <engines>
+    <engine name="cordova" version=">=5.0.0" />
+  </engines>
+  <repo>https://github.com/AlexDisler/cordova-plugin-inapppurchase.git</repo>
+  <issue>https://github.com/AlexDisler/cordova-plugin-inapppurchase/issues</issue>
+
+  <license>MIT</license>
+  <keywords>cordova,phonegap,ionic,purchase,storekit,ios,android,play,appstore</keywords>
+
+  <platform name="ios">
+    <js-module src="www/index-ios.js" name="PaymentsPlugin">
+      <clobbers target="inAppPurchase" />
+    </js-module>
+
+    <config-file target="config.xml" parent="/*">
+      <feature name="PaymentsPlugin">
+        <param name="ios-package" value="PaymentsPlugin"/>
+      </feature>
+    </config-file>
+
+    <header-file src="src/ios/RMStore.h" />
+    <source-file src="src/ios/RMStore.m" />
+    <header-file src="src/ios/PaymentsPlugin.h" />
+    <source-file src="src/ios/PaymentsPlugin.m" />
+
+    <framework src="StoreKit.framework" />
+  </platform>
+
+  <platform name="android">
+    <js-module src="www/index-android.js" name="InAppBillingV3">
+      <merges target="inAppPurchase" />
+    </js-module>
+
+    <config-file target="AndroidManifest.xml" parent="/manifest">
+      <uses-permission android:name="com.android.vending.BILLING" />
+    </config-file>
+
+    <config-file target="res/xml/config.xml" parent="/*">
+      <feature name="InAppBillingV3">
+        <param name="android-package" value="com.alexdisler.inapppurchases.InAppBillingV3"/>
+      </feature>
+    </config-file>
+
+    <source-file src="src/android/InAppBillingV3.java" target-dir="src/com/alexdisler/inapppurchases" />
+    <source-file src="src/android/Base64.java" target-dir="src/com/alexdisler/inapppurchases" />
+    <source-file src="src/android/Base64DecoderException.java" target-dir="src/com/alexdisler/inapppurchases" />
+    <source-file src="src/android/IabException.java" target-dir="src/com/alexdisler/inapppurchases" />
+    <source-file src="src/android/IabHelper.java" target-dir="src/com/alexdisler/inapppurchases" />
+    <source-file src="src/android/IabResult.java" target-dir="src/com/alexdisler/inapppurchases" />
+    <source-file src="src/android/Inventory.java" target-dir="src/com/alexdisler/inapppurchases" />
+    <source-file src="src/android/Purchase.java" target-dir="src/com/alexdisler/inapppurchases" />
+    <source-file src="src/android/Security.java" target-dir="src/com/alexdisler/inapppurchases" />
+    <source-file src="src/android/SkuDetails.java" target-dir="src/com/alexdisler/inapppurchases" />
+    <source-file src="src/android/billing/IInAppBillingService.aidl" target-dir="src/com/android/vending/billing" />
+  </platform>
+
+</plugin>

+ 570 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/android/Base64.java

xqd
@@ -0,0 +1,570 @@
+// Portions copyright 2002, Google, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.alexdisler.inapppurchases;
+
+// This code was converted from code at http://iharder.sourceforge.net/base64/
+// Lots of extraneous features were removed.
+/* The original code said:
+ * <p>
+ * I am placing this code in the Public Domain. Do with it as you will.
+ * This software comes with no guarantees or warranties but with
+ * plenty of well-wishing instead!
+ * Please visit
+ * <a href="http://iharder.net/xmlizable">http://iharder.net/xmlizable</a>
+ * periodically to check for updates or to contribute improvements.
+ * </p>
+ *
+ * @author Robert Harder
+ * @author rharder@usa.net
+ * @version 1.3
+ */
+
+/**
+ * Base64 converter class. This code is not a complete MIME encoder;
+ * it simply converts binary data to base64 data and back.
+ *
+ * <p>Note {@link CharBase64} is a GWT-compatible implementation of this
+ * class.
+ */
+public class Base64 {
+    /** Specify encoding (value is {@code true}). */
+    public final static boolean ENCODE = true;
+
+    /** Specify decoding (value is {@code false}). */
+    public final static boolean DECODE = false;
+
+    /** The equals sign (=) as a byte. */
+    private final static byte EQUALS_SIGN = (byte) '=';
+
+    /** The new line character (\n) as a byte. */
+    private final static byte NEW_LINE = (byte) '\n';
+
+    /**
+     * The 64 valid Base64 values.
+     */
+    private final static byte[] ALPHABET =
+        {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+        (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+        (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+        (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+        (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+        (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+        (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+        (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+        (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+        (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+        (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+        (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+        (byte) '9', (byte) '+', (byte) '/'};
+
+    /**
+     * The 64 valid web safe Base64 values.
+     */
+    private final static byte[] WEBSAFE_ALPHABET =
+        {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F',
+        (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K',
+        (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P',
+        (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
+        (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z',
+        (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e',
+        (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j',
+        (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
+        (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't',
+        (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y',
+        (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
+        (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8',
+        (byte) '9', (byte) '-', (byte) '_'};
+
+    /**
+     * Translates a Base64 value to either its 6-bit reconstruction value
+     * or a negative number indicating some other meaning.
+     **/
+    private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8
+        -5, -5, // Whitespace: Tab and Linefeed
+        -9, -9, // Decimal 11 - 12
+        -5, // Whitespace: Carriage Return
+        -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+        -9, -9, -9, -9, -9, // Decimal 27 - 31
+        -5, // Whitespace: Space
+        -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
+        62, // Plus sign at decimal 43
+        -9, -9, -9, // Decimal 44 - 46
+        63, // Slash at decimal 47
+        52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+        -9, -9, -9, // Decimal 58 - 60
+        -1, // Equals sign at decimal 61
+        -9, -9, -9, // Decimal 62 - 64
+        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+        14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+        -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
+        26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+        39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+        -9, -9, -9, -9, -9 // Decimal 123 - 127
+        /*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */
+    };
+
+    /** The web safe decodabet */
+    private final static byte[] WEBSAFE_DECODABET =
+        {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal  0 -  8
+        -5, -5, // Whitespace: Tab and Linefeed
+        -9, -9, // Decimal 11 - 12
+        -5, // Whitespace: Carriage Return
+        -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
+        -9, -9, -9, -9, -9, // Decimal 27 - 31
+        -5, // Whitespace: Space
+        -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44
+        62, // Dash '-' sign at decimal 45
+        -9, -9, // Decimal 46-47
+        52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
+        -9, -9, -9, // Decimal 58 - 60
+        -1, // Equals sign at decimal 61
+        -9, -9, -9, // Decimal 62 - 64
+        0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
+        14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
+        -9, -9, -9, -9, // Decimal 91-94
+        63, // Underscore '_' at decimal 95
+        -9, // Decimal 96
+        26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
+        39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
+        -9, -9, -9, -9, -9 // Decimal 123 - 127
+        /*  ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 128 - 139
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 140 - 152
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 153 - 165
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 166 - 178
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 179 - 191
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 192 - 204
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 205 - 217
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 218 - 230
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,     // Decimal 231 - 243
+        -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9         // Decimal 244 - 255 */
+        };
+
+    // Indicates white space in encoding
+    private final static byte WHITE_SPACE_ENC = -5;
+    // Indicates equals sign in encoding
+    private final static byte EQUALS_SIGN_ENC = -1;
+
+    /** Defeats instantiation. */
+    private Base64() {
+    }
+
+    /* ********  E N C O D I N G   M E T H O D S  ******** */
+
+    /**
+     * Encodes up to three bytes of the array <var>source</var>
+     * and writes the resulting four Base64 bytes to <var>destination</var>.
+     * The source and destination arrays can be manipulated
+     * anywhere along their length by specifying
+     * <var>srcOffset</var> and <var>destOffset</var>.
+     * This method does not check to make sure your arrays
+     * are large enough to accommodate <var>srcOffset</var> + 3 for
+     * the <var>source</var> array or <var>destOffset</var> + 4 for
+     * the <var>destination</var> array.
+     * The actual number of significant bytes in your array is
+     * given by <var>numSigBytes</var>.
+     *
+     * @param source the array to convert
+     * @param srcOffset the index where conversion begins
+     * @param numSigBytes the number of significant bytes in your array
+     * @param destination the array to hold the conversion
+     * @param destOffset the index where output will be put
+     * @param alphabet is the encoding alphabet
+     * @return the <var>destination</var> array
+     * @since 1.3
+     */
+    private static byte[] encode3to4(byte[] source, int srcOffset,
+            int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) {
+        //           1         2         3
+        // 01234567890123456789012345678901 Bit position
+        // --------000000001111111122222222 Array position from threeBytes
+        // --------|    ||    ||    ||    | Six bit groups to index alphabet
+        //          >>18  >>12  >> 6  >> 0  Right shift necessary
+        //                0x3f  0x3f  0x3f  Additional AND
+
+        // Create buffer with zero-padding if there are only one or two
+        // significant bytes passed in the array.
+        // We have to shift left 24 in order to flush out the 1's that appear
+        // when Java treats a value as negative that is cast from a byte to an int.
+        int inBuff =
+                (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0)
+                | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0)
+                | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0);
+
+        switch (numSigBytes) {
+            case 3:
+                destination[destOffset] = alphabet[(inBuff >>> 18)];
+                destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+                destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+                destination[destOffset + 3] = alphabet[(inBuff) & 0x3f];
+                return destination;
+            case 2:
+                destination[destOffset] = alphabet[(inBuff >>> 18)];
+                destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+                destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+                destination[destOffset + 3] = EQUALS_SIGN;
+                return destination;
+            case 1:
+                destination[destOffset] = alphabet[(inBuff >>> 18)];
+                destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+                destination[destOffset + 2] = EQUALS_SIGN;
+                destination[destOffset + 3] = EQUALS_SIGN;
+                return destination;
+            default:
+                return destination;
+        } // end switch
+    } // end encode3to4
+
+    /**
+     * Encodes a byte array into Base64 notation.
+     * Equivalent to calling
+     * {@code encodeBytes(source, 0, source.length)}
+     *
+     * @param source The data to convert
+     * @since 1.4
+     */
+    public static String encode(byte[] source) {
+        return encode(source, 0, source.length, ALPHABET, true);
+    }
+
+    /**
+     * Encodes a byte array into web safe Base64 notation.
+     *
+     * @param source The data to convert
+     * @param doPadding is {@code true} to pad result with '=' chars
+     *        if it does not fall on 3 byte boundaries
+     */
+    public static String encodeWebSafe(byte[] source, boolean doPadding) {
+        return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding);
+    }
+
+    /**
+     * Encodes a byte array into Base64 notation.
+     *
+     * @param source the data to convert
+     * @param off offset in array where conversion should begin
+     * @param len length of data to convert
+     * @param alphabet the encoding alphabet
+     * @param doPadding is {@code true} to pad result with '=' chars
+     * if it does not fall on 3 byte boundaries
+     * @since 1.4
+     */
+    public static String encode(byte[] source, int off, int len, byte[] alphabet,
+            boolean doPadding) {
+        byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE);
+        int outLen = outBuff.length;
+
+        // If doPadding is false, set length to truncate '='
+        // padding characters
+        while (doPadding == false && outLen > 0) {
+            if (outBuff[outLen - 1] != '=') {
+                break;
+            }
+            outLen -= 1;
+        }
+
+        return new String(outBuff, 0, outLen);
+    }
+
+    /**
+     * Encodes a byte array into Base64 notation.
+     *
+     * @param source the data to convert
+     * @param off offset in array where conversion should begin
+     * @param len length of data to convert
+     * @param alphabet is the encoding alphabet
+     * @param maxLineLength maximum length of one line.
+     * @return the BASE64-encoded byte array
+     */
+    public static byte[] encode(byte[] source, int off, int len, byte[] alphabet,
+            int maxLineLength) {
+        int lenDiv3 = (len + 2) / 3; // ceil(len / 3)
+        int len43 = lenDiv3 * 4;
+        byte[] outBuff = new byte[len43 // Main 4:3
+                                  + (len43 / maxLineLength)]; // New lines
+
+        int d = 0;
+        int e = 0;
+        int len2 = len - 2;
+        int lineLength = 0;
+        for (; d < len2; d += 3, e += 4) {
+
+            // The following block of code is the same as
+            // encode3to4( source, d + off, 3, outBuff, e, alphabet );
+            // but inlined for faster encoding (~20% improvement)
+            int inBuff =
+                    ((source[d + off] << 24) >>> 8)
+                    | ((source[d + 1 + off] << 24) >>> 16)
+                    | ((source[d + 2 + off] << 24) >>> 24);
+            outBuff[e] = alphabet[(inBuff >>> 18)];
+            outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f];
+            outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f];
+            outBuff[e + 3] = alphabet[(inBuff) & 0x3f];
+
+            lineLength += 4;
+            if (lineLength == maxLineLength) {
+                outBuff[e + 4] = NEW_LINE;
+                e++;
+                lineLength = 0;
+            } // end if: end of line
+        } // end for: each piece of array
+
+        if (d < len) {
+            encode3to4(source, d + off, len - d, outBuff, e, alphabet);
+
+            lineLength += 4;
+            if (lineLength == maxLineLength) {
+                // Add a last newline
+                outBuff[e + 4] = NEW_LINE;
+                e++;
+            }
+            e += 4;
+        }
+
+        assert (e == outBuff.length);
+        return outBuff;
+    }
+
+
+    /* ********  D E C O D I N G   M E T H O D S  ******** */
+
+
+    /**
+     * Decodes four bytes from array <var>source</var>
+     * and writes the resulting bytes (up to three of them)
+     * to <var>destination</var>.
+     * The source and destination arrays can be manipulated
+     * anywhere along their length by specifying
+     * <var>srcOffset</var> and <var>destOffset</var>.
+     * This method does not check to make sure your arrays
+     * are large enough to accommodate <var>srcOffset</var> + 4 for
+     * the <var>source</var> array or <var>destOffset</var> + 3 for
+     * the <var>destination</var> array.
+     * This method returns the actual number of bytes that
+     * were converted from the Base64 encoding.
+     *
+     *
+     * @param source the array to convert
+     * @param srcOffset the index where conversion begins
+     * @param destination the array to hold the conversion
+     * @param destOffset the index where output will be put
+     * @param decodabet the decodabet for decoding Base64 content
+     * @return the number of decoded bytes converted
+     * @since 1.3
+     */
+    private static int decode4to3(byte[] source, int srcOffset,
+            byte[] destination, int destOffset, byte[] decodabet) {
+        // Example: Dk==
+        if (source[srcOffset + 2] == EQUALS_SIGN) {
+            int outBuff =
+                    ((decodabet[source[srcOffset]] << 24) >>> 6)
+                    | ((decodabet[source[srcOffset + 1]] << 24) >>> 12);
+
+            destination[destOffset] = (byte) (outBuff >>> 16);
+            return 1;
+        } else if (source[srcOffset + 3] == EQUALS_SIGN) {
+            // Example: DkL=
+            int outBuff =
+                    ((decodabet[source[srcOffset]] << 24) >>> 6)
+                    | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+                    | ((decodabet[source[srcOffset + 2]] << 24) >>> 18);
+
+            destination[destOffset] = (byte) (outBuff >>> 16);
+            destination[destOffset + 1] = (byte) (outBuff >>> 8);
+            return 2;
+        } else {
+            // Example: DkLE
+            int outBuff =
+                    ((decodabet[source[srcOffset]] << 24) >>> 6)
+                    | ((decodabet[source[srcOffset + 1]] << 24) >>> 12)
+                    | ((decodabet[source[srcOffset + 2]] << 24) >>> 18)
+                    | ((decodabet[source[srcOffset + 3]] << 24) >>> 24);
+
+            destination[destOffset] = (byte) (outBuff >> 16);
+            destination[destOffset + 1] = (byte) (outBuff >> 8);
+            destination[destOffset + 2] = (byte) (outBuff);
+            return 3;
+        }
+    } // end decodeToBytes
+
+
+    /**
+     * Decodes data from Base64 notation.
+     *
+     * @param s the string to decode (decoded in default encoding)
+     * @return the decoded data
+     * @since 1.4
+     */
+    public static byte[] decode(String s) throws Base64DecoderException {
+        byte[] bytes = s.getBytes();
+        return decode(bytes, 0, bytes.length);
+    }
+
+    /**
+     * Decodes data from web safe Base64 notation.
+     * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+     *
+     * @param s the string to decode (decoded in default encoding)
+     * @return the decoded data
+     */
+    public static byte[] decodeWebSafe(String s) throws Base64DecoderException {
+        byte[] bytes = s.getBytes();
+        return decodeWebSafe(bytes, 0, bytes.length);
+    }
+
+    /**
+     * Decodes Base64 content in byte array format and returns
+     * the decoded byte array.
+     *
+     * @param source The Base64 encoded data
+     * @return decoded data
+     * @since 1.3
+     * @throws Base64DecoderException
+     */
+    public static byte[] decode(byte[] source) throws Base64DecoderException {
+        return decode(source, 0, source.length);
+    }
+
+    /**
+     * Decodes web safe Base64 content in byte array format and returns
+     * the decoded data.
+     * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+     *
+     * @param source the string to decode (decoded in default encoding)
+     * @return the decoded data
+     */
+    public static byte[] decodeWebSafe(byte[] source)
+            throws Base64DecoderException {
+        return decodeWebSafe(source, 0, source.length);
+    }
+
+    /**
+     * Decodes Base64 content in byte array format and returns
+     * the decoded byte array.
+     *
+     * @param source the Base64 encoded data
+     * @param off    the offset of where to begin decoding
+     * @param len    the length of characters to decode
+     * @return decoded data
+     * @since 1.3
+     * @throws Base64DecoderException
+     */
+    public static byte[] decode(byte[] source, int off, int len)
+            throws Base64DecoderException {
+        return decode(source, off, len, DECODABET);
+    }
+
+    /**
+     * Decodes web safe Base64 content in byte array format and returns
+     * the decoded byte array.
+     * Web safe encoding uses '-' instead of '+', '_' instead of '/'
+     *
+     * @param source the Base64 encoded data
+     * @param off    the offset of where to begin decoding
+     * @param len    the length of characters to decode
+     * @return decoded data
+     */
+    public static byte[] decodeWebSafe(byte[] source, int off, int len)
+            throws Base64DecoderException {
+        return decode(source, off, len, WEBSAFE_DECODABET);
+    }
+
+    /**
+     * Decodes Base64 content using the supplied decodabet and returns
+     * the decoded byte array.
+     *
+     * @param source the Base64 encoded data
+     * @param off the offset of where to begin decoding
+     * @param len the length of characters to decode
+     * @param decodabet the decodabet for decoding Base64 content
+     * @return decoded data
+     */
+    public static byte[] decode(byte[] source, int off, int len, byte[] decodabet)
+            throws Base64DecoderException {
+        int len34 = len * 3 / 4;
+        byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output
+        int outBuffPosn = 0;
+
+        byte[] b4 = new byte[4];
+        int b4Posn = 0;
+        int i = 0;
+        byte sbiCrop = 0;
+        byte sbiDecode = 0;
+        for (i = 0; i < len; i++) {
+            sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits
+            sbiDecode = decodabet[sbiCrop];
+
+            if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better
+                if (sbiDecode >= EQUALS_SIGN_ENC) {
+                    // An equals sign (for padding) must not occur at position 0 or 1
+                    // and must be the last byte[s] in the encoded value
+                    if (sbiCrop == EQUALS_SIGN) {
+                        int bytesLeft = len - i;
+                        byte lastByte = (byte) (source[len - 1 + off] & 0x7f);
+                        if (b4Posn == 0 || b4Posn == 1) {
+                            throw new Base64DecoderException(
+                                    "invalid padding byte '=' at byte offset " + i);
+                        } else if ((b4Posn == 3 && bytesLeft > 2)
+                                || (b4Posn == 4 && bytesLeft > 1)) {
+                            throw new Base64DecoderException(
+                                    "padding byte '=' falsely signals end of encoded value "
+                                            + "at offset " + i);
+                        } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) {
+                            throw new Base64DecoderException(
+                                    "encoded value has invalid trailing byte");
+                        }
+                        break;
+                    }
+
+                    b4[b4Posn++] = sbiCrop;
+                    if (b4Posn == 4) {
+                        outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+                        b4Posn = 0;
+                    }
+                }
+            } else {
+                throw new Base64DecoderException("Bad Base64 input character at " + i
+                        + ": " + source[i + off] + "(decimal)");
+            }
+        }
+
+        // Because web safe encoding allows non padding base64 encodes, we
+        // need to pad the rest of the b4 buffer with equal signs when
+        // b4Posn != 0.  There can be at most 2 equal signs at the end of
+        // four characters, so the b4 buffer must have two or three
+        // characters.  This also catches the case where the input is
+        // padded with EQUALS_SIGN
+        if (b4Posn != 0) {
+            if (b4Posn == 1) {
+                throw new Base64DecoderException("single trailing character at offset "
+                        + (len - 1));
+            }
+            b4[b4Posn++] = EQUALS_SIGN;
+            outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet);
+        }
+
+        byte[] out = new byte[outBuffPosn];
+        System.arraycopy(outBuff, 0, out, 0, outBuffPosn);
+        return out;
+    }
+}

+ 32 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/android/Base64DecoderException.java

xqd
@@ -0,0 +1,32 @@
+// Copyright 2002, Google, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.alexdisler.inapppurchases;
+
+/**
+ * Exception thrown when encountering an invalid Base64 input character.
+ *
+ * @author nelson
+ */
+public class Base64DecoderException extends Exception {
+    public Base64DecoderException() {
+        super();
+    }
+
+    public Base64DecoderException(String s) {
+        super(s);
+    }
+
+    private static final long serialVersionUID = 1L;
+}

+ 43 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/android/IabException.java

xqd
@@ -0,0 +1,43 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.alexdisler.inapppurchases;
+
+/**
+ * Exception thrown when something went wrong with in-app billing.
+ * An IabException has an associated IabResult (an error).
+ * To get the IAB result that caused this exception to be thrown,
+ * call {@link #getResult()}.
+ */
+public class IabException extends Exception {
+    IabResult mResult;
+
+    public IabException(IabResult r) {
+        this(r, null);
+    }
+    public IabException(int response, String message) {
+        this(new IabResult(response, message));
+    }
+    public IabException(IabResult r, Exception cause) {
+        super(r.getMessage(), cause);
+        mResult = r;
+    }
+    public IabException(int response, String message, Exception cause) {
+        this(new IabResult(response, message), cause);
+    }
+
+    /** Returns the IAB result (error) that this exception signals. */
+    public IabResult getResult() { return mResult; }
+}

+ 1007 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/android/IabHelper.java

xqd
@@ -0,0 +1,1007 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.alexdisler.inapppurchases;
+
+import android.app.Activity;
+import android.app.PendingIntent;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentSender.SendIntentException;
+import android.content.ServiceConnection;
+import android.content.pm.ApplicationInfo;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.RemoteException;
+import android.text.TextUtils;
+import android.util.Log;
+
+import com.android.vending.billing.IInAppBillingService;
+
+import org.json.JSONException;
+
+import java.util.ArrayList;
+import java.util.List;
+
+
+/**
+ * Provides convenience methods for in-app billing. You can create one instance of this
+ * class for your application and use it to process in-app billing operations.
+ * It provides synchronous (blocking) and asynchronous (non-blocking) methods for
+ * many common in-app billing operations, as well as automatic signature
+ * verification.
+ *
+ * After instantiating, you must perform setup in order to start using the object.
+ * To perform setup, call the {@link #startSetup} method and provide a listener;
+ * that listener will be notified when setup is complete, after which (and not before)
+ * you may call other methods.
+ *
+ * After setup is complete, you will typically want to request an inventory of owned
+ * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync}
+ * and related methods.
+ *
+ * When you are done with this object, don't forget to call {@link #dispose}
+ * to ensure proper cleanup. This object holds a binding to the in-app billing
+ * service, which will leak unless you dispose of it correctly. If you created
+ * the object on an Activity's onCreate method, then the recommended
+ * place to dispose of it is the Activity's onDestroy method.
+ *
+ * A note about threading: When using this object from a background thread, you may
+ * call the blocking versions of methods; when using from a UI thread, call
+ * only the asynchronous versions and handle the results via callbacks.
+ * Also, notice that you can only call one asynchronous operation at a time;
+ * attempting to start a second asynchronous operation while the first one
+ * has not yet completed will result in an exception being thrown.
+ *
+ * @author Bruno Oliveira (Google)
+ *
+ */
+public class IabHelper {
+    // Is debug logging enabled?
+    boolean mDebugLog = false;
+    String mDebugTag = "IabHelper";
+
+    // Can we skip the online purchase verification?
+    // (Only allowed if the app is debuggable)
+	private boolean mSkipPurchaseVerification = false;
+
+    // Is setup done?
+    boolean mSetupDone = false;
+
+    // Has this object been disposed of? (If so, we should ignore callbacks, etc)
+    boolean mDisposed = false;
+
+    // Are subscriptions supported?
+    boolean mSubscriptionsSupported = false;
+
+    // Is an asynchronous operation in progress?
+    // (only one at a time can be in progress)
+    boolean mAsyncInProgress = false;
+
+    // (for logging/debugging)
+    // if mAsyncInProgress == true, what asynchronous operation is in progress?
+    String mAsyncOperation = "";
+
+    // Context we were passed during initialization
+    Context mContext;
+
+    // Connection to the service
+    IInAppBillingService mService;
+    ServiceConnection mServiceConn;
+
+    // The request code used to launch purchase flow
+    int mRequestCode;
+
+    // The item type of the current purchase flow
+    String mPurchasingItemType;
+
+    // Public key for verifying signature, in base64 encoding
+    String mSignatureBase64 = null;
+
+    // Billing response codes
+    public static final int BILLING_RESPONSE_RESULT_OK = 0;
+    public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1;
+    public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3;
+    public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4;
+    public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5;
+    public static final int BILLING_RESPONSE_RESULT_ERROR = 6;
+    public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7;
+    public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8;
+
+    // IAB Helper error codes
+    public static final int IABHELPER_ERROR_BASE = -1000;
+    public static final int IABHELPER_REMOTE_EXCEPTION = -1001;
+    public static final int IABHELPER_BAD_RESPONSE = -1002;
+    public static final int IABHELPER_VERIFICATION_FAILED = -1003;
+    public static final int IABHELPER_SEND_INTENT_FAILED = -1004;
+    public static final int IABHELPER_USER_CANCELLED = -1005;
+    public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006;
+    public static final int IABHELPER_MISSING_TOKEN = -1007;
+    public static final int IABHELPER_UNKNOWN_ERROR = -1008;
+    public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009;
+    public static final int IABHELPER_INVALID_CONSUMPTION = -1010;
+
+    // Keys for the responses from InAppBillingService
+    public static final String RESPONSE_CODE = "RESPONSE_CODE";
+    public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST";
+    public static final String RESPONSE_BUY_INTENT = "BUY_INTENT";
+    public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA";
+    public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE";
+    public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST";
+    public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST";
+    public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST";
+    public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN";
+
+    // Item types
+    public static final String ITEM_TYPE_INAPP = "inapp";
+    public static final String ITEM_TYPE_SUBS = "subs";
+
+    // some fields on the getSkuDetails response bundle
+    public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST";
+    public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST";
+
+    /**
+     * Creates an instance. After creation, it will not yet be ready to use. You must perform
+     * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not
+     * block and is safe to call from a UI thread.
+     *
+     * @param ctx Your application or Activity context. Needed to bind to the in-app billing service.
+     * @param base64PublicKey Your application's public key, encoded in base64.
+     *     This is used for verification of purchase signatures. You can find your app's base64-encoded
+     *     public key in your application's page on Google Play Developer Console. Note that this
+     *     is NOT your "developer public key".
+     */
+    public IabHelper(Context ctx, String base64PublicKey) {
+        mContext = ctx.getApplicationContext();
+        mSignatureBase64 = base64PublicKey;
+        logDebug("IAB helper created.");
+    }
+
+    /**
+     * Enables or disable debug logging through LogCat.
+     */
+    public void enableDebugLogging(boolean enable, String tag) {
+        checkNotDisposed();
+        mDebugLog = enable;
+        mDebugTag = tag;
+    }
+
+    public void enableDebugLogging(boolean enable) {
+        checkNotDisposed();
+        mDebugLog = enable;
+    }
+
+    public void setSkipPurchaseVerification(boolean shouldSkipPurchaseVerification) {
+        mSkipPurchaseVerification = shouldSkipPurchaseVerification;
+    }
+
+    /**
+     * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called
+     * when the setup process is complete.
+     */
+    public interface OnIabSetupFinishedListener {
+        /**
+         * Called to notify that setup is complete.
+         *
+         * @param result The result of the setup process.
+         */
+        public void onIabSetupFinished(IabResult result);
+    }
+
+    /**
+     * Starts the setup process. This will start up the setup process asynchronously.
+     * You will be notified through the listener when the setup process is complete.
+     * This method is safe to call from a UI thread.
+     *
+     * @param listener The listener to notify when the setup process is complete.
+     */
+    public void startSetup(final OnIabSetupFinishedListener listener) {
+        // If already set up, can't do it again.
+        checkNotDisposed();
+        if (mSetupDone) throw new IllegalStateException("IAB helper is already set up.");
+
+        // Connection to IAB service
+        logDebug("Starting in-app billing setup.");
+        mServiceConn = new ServiceConnection() {
+            @Override
+            public void onServiceDisconnected(ComponentName name) {
+                logDebug("Billing service disconnected.");
+                mService = null;
+            }
+
+            @Override
+            public void onServiceConnected(ComponentName name, IBinder service) {
+                if (mDisposed) return;
+                logDebug("Billing service connected.");
+                mService = IInAppBillingService.Stub.asInterface(service);
+                String packageName = mContext.getPackageName();
+                try {
+                    logDebug("Checking for in-app billing 3 support.");
+
+                    // check for in-app billing v3 support
+                    int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP);
+                    if (response != BILLING_RESPONSE_RESULT_OK) {
+                        if (listener != null) listener.onIabSetupFinished(new IabResult(response,
+                                "Error checking for billing v3 support."));
+
+                        // if in-app purchases aren't supported, neither are subscriptions.
+                        mSubscriptionsSupported = false;
+                        return;
+                    }
+                    logDebug("In-app billing version 3 supported for " + packageName);
+
+                    // check for v3 subscriptions support
+                    response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS);
+                    if (response == BILLING_RESPONSE_RESULT_OK) {
+                        logDebug("Subscriptions AVAILABLE.");
+                        mSubscriptionsSupported = true;
+                    }
+                    else {
+                        logDebug("Subscriptions NOT AVAILABLE. Response: " + response);
+                    }
+
+                    mSetupDone = true;
+                }
+                catch (RemoteException e) {
+                    if (listener != null) {
+                        listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION,
+                                                    "RemoteException while setting up in-app billing."));
+                    }
+                    e.printStackTrace();
+                    return;
+                }
+
+                if (listener != null) {
+                    listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful."));
+                }
+            }
+        };
+
+        Intent serviceIntent = new Intent("com.android.vending.billing.InAppBillingService.BIND");
+        serviceIntent.setPackage("com.android.vending");
+        if (!mContext.getPackageManager().queryIntentServices(serviceIntent, 0).isEmpty()) {
+            // service available to handle that Intent
+            mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE);
+        }
+        else {
+            // no service available to handle that Intent
+            if (listener != null) {
+                listener.onIabSetupFinished(
+                        new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE,
+                        "Billing service unavailable on device."));
+            }
+        }
+    }
+
+    /**
+     * Dispose of object, releasing resources. It's very important to call this
+     * method when you are done with this object. It will release any resources
+     * used by it such as service connections. Naturally, once the object is
+     * disposed of, it can't be used again.
+     */
+    public void dispose() {
+        logDebug("Disposing.");
+        mSetupDone = false;
+        if (mServiceConn != null) {
+            logDebug("Unbinding from service.");
+            if (mContext != null && mService != null) mContext.unbindService(mServiceConn);
+        }
+        mDisposed = true;
+        mContext = null;
+        mServiceConn = null;
+        mService = null;
+        mPurchaseListener = null;
+    }
+
+    private void checkNotDisposed() {
+        if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used.");
+    }
+
+    /** Returns whether subscriptions are supported. */
+    public boolean subscriptionsSupported() {
+        checkNotDisposed();
+        return mSubscriptionsSupported;
+    }
+
+
+    /**
+     * Callback that notifies when a purchase is finished.
+     */
+    public interface OnIabPurchaseFinishedListener {
+        /**
+         * Called to notify that an in-app purchase finished. If the purchase was successful,
+         * then the sku parameter specifies which item was purchased. If the purchase failed,
+         * the sku and extraData parameters may or may not be null, depending on how far the purchase
+         * process went.
+         *
+         * @param result The result of the purchase.
+         * @param info The purchase information (null if purchase failed)
+         */
+        public void onIabPurchaseFinished(IabResult result, Purchase info);
+    }
+
+    // The listener registered on launchPurchaseFlow, which we have to call back when
+    // the purchase finishes
+    OnIabPurchaseFinishedListener mPurchaseListener;
+
+    public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) {
+        launchPurchaseFlow(act, sku, requestCode, listener, "");
+    }
+
+    public void launchPurchaseFlow(Activity act, String sku, int requestCode,
+            OnIabPurchaseFinishedListener listener, String extraData) {
+        launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData);
+    }
+
+    public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+            OnIabPurchaseFinishedListener listener) {
+        launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, "");
+    }
+
+    public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode,
+            OnIabPurchaseFinishedListener listener, String extraData) {
+        launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData);
+    }
+
+    /**
+     * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase,
+     * which will involve bringing up the Google Play screen. The calling activity will be paused while
+     * the user interacts with Google Play, and the result will be delivered via the activity's
+     * {@link android.app.Activity#onActivityResult} method, at which point you must call
+     * this object's {@link #handleActivityResult} method to continue the purchase flow. This method
+     * MUST be called from the UI thread of the Activity.
+     *
+     * @param act The calling activity.
+     * @param sku The sku of the item to purchase.
+     * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS)
+     * @param requestCode A request code (to differentiate from other responses --
+     *     as in {@link android.app.Activity#startActivityForResult}).
+     * @param listener The listener to notify when the purchase process finishes
+     * @param extraData Extra data (developer payload), which will be returned with the purchase data
+     *     when the purchase completes. This extra data will be permanently bound to that purchase
+     *     and will always be returned when the purchase is queried.
+     */
+    public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode,
+                        OnIabPurchaseFinishedListener listener, String extraData) {
+        checkNotDisposed();
+        checkSetupDone("launchPurchaseFlow");
+        flagStartAsync("launchPurchaseFlow");
+        IabResult result;
+
+        if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) {
+            IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE,
+                    "Subscriptions are not available.");
+            flagEndAsync();
+            if (listener != null) listener.onIabPurchaseFinished(r, null);
+            return;
+        }
+
+        try {
+            logDebug("Constructing buy intent for " + sku + ", item type: " + itemType);
+            Bundle buyIntentBundle = mService.getBuyIntent(3, mContext.getPackageName(), sku, itemType, extraData);
+            int response = getResponseCodeFromBundle(buyIntentBundle);
+            if (response != BILLING_RESPONSE_RESULT_OK) {
+                logError("Unable to buy item, Error response: " + getResponseDesc(response));
+                flagEndAsync();
+                result = new IabResult(response, "Unable to buy item");
+                if (listener != null) listener.onIabPurchaseFinished(result, null);
+                return;
+            }
+
+            PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT);
+            logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode);
+            mRequestCode = requestCode;
+            mPurchaseListener = listener;
+            mPurchasingItemType = itemType;
+            act.startIntentSenderForResult(pendingIntent.getIntentSender(),
+                                           requestCode, new Intent(),
+                                           Integer.valueOf(0), Integer.valueOf(0),
+                                           Integer.valueOf(0));
+        }
+        catch (SendIntentException e) {
+            logError("SendIntentException while launching purchase flow for sku " + sku);
+            e.printStackTrace();
+            flagEndAsync();
+
+            result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent.");
+            if (listener != null) listener.onIabPurchaseFinished(result, null);
+        }
+        catch (RemoteException e) {
+            logError("RemoteException while launching purchase flow for sku " + sku);
+            e.printStackTrace();
+            flagEndAsync();
+
+            result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow");
+            if (listener != null) listener.onIabPurchaseFinished(result, null);
+        }
+    }
+
+    /**
+     * Handles an activity result that's part of the purchase flow in in-app billing. If you
+     * are calling {@link #launchPurchaseFlow}, then you must call this method from your
+     * Activity's {@link android.app.Activity@onActivityResult} method. This method
+     * MUST be called from the UI thread of the Activity.
+     *
+     * @param requestCode The requestCode as you received it.
+     * @param resultCode The resultCode as you received it.
+     * @param data The data (Intent) as you received it.
+     * @return Returns true if the result was related to a purchase flow and was handled;
+     *     false if the result was not related to a purchase, in which case you should
+     *     handle it normally.
+     */
+    public boolean handleActivityResult(int requestCode, int resultCode, Intent data) {
+        IabResult result;
+        if (requestCode != mRequestCode) return false;
+
+        checkNotDisposed();
+        checkSetupDone("handleActivityResult");
+
+        // end of async purchase operation that started on launchPurchaseFlow
+        flagEndAsync();
+
+        if (data == null) {
+            logError("Null data in IAB activity result.");
+            result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result");
+            if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+            return true;
+        }
+
+        int responseCode = getResponseCodeFromIntent(data);
+        String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA);
+        String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE);
+
+        if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) {
+            logDebug("Successful resultcode from purchase activity.");
+            logDebug("Purchase data: " + purchaseData);
+            logDebug("Data signature: " + dataSignature);
+            logDebug("Extras: " + data.getExtras());
+            logDebug("Expected item type: " + mPurchasingItemType);
+
+            if (purchaseData == null || dataSignature == null) {
+                logError("BUG: either purchaseData or dataSignature is null.");
+                logDebug("Extras: " + data.getExtras().toString());
+                result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature");
+                if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+                return true;
+            }
+
+            Purchase purchase = null;
+            try {
+                purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature);
+                String sku = purchase.getSku();
+                // Only allow purchase verification to be skipped if we are debuggable
+                boolean skipPurchaseVerification = (this.mSkipPurchaseVerification  &&
+                            ((mContext.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0));
+                // Verify signature
+                if (!skipPurchaseVerification) {
+                    if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) {
+                        logError("Purchase signature verification FAILED for sku " + sku);
+                        result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku);
+                        if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase);
+                        return true;
+                    }
+                    logDebug("Purchase signature successfully verified.");
+                }
+            }
+            catch (JSONException e) {
+                logError("Failed to parse purchase data.");
+                e.printStackTrace();
+                result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data.");
+                if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+                return true;
+            }
+
+            if (mPurchaseListener != null) {
+                mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase);
+            }
+        }
+        else if (resultCode == Activity.RESULT_OK) {
+            // result code was OK, but in-app billing response was not OK.
+            logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode));
+            if (mPurchaseListener != null) {
+                result = new IabResult(responseCode, "Problem purchashing item.");
+                mPurchaseListener.onIabPurchaseFinished(result, null);
+            }
+        }
+        else if (resultCode == Activity.RESULT_CANCELED) {
+            logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode));
+            result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled.");
+            if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+        }
+        else {
+            logError("Purchase failed. Result code: " + Integer.toString(resultCode)
+                    + ". Response: " + getResponseDesc(responseCode));
+            result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response.");
+            if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null);
+        }
+        return true;
+    }
+
+    public Inventory queryInventory(boolean querySkuDetails, List<String> moreSkus) throws IabException {
+        return queryInventory(querySkuDetails, moreSkus, null);
+    }
+
+    /**
+     * Queries the inventory. This will query all owned items from the server, as well as
+     * information on additional skus, if specified. This method may block or take long to execute.
+     * Do not call from a UI thread. For that, use the non-blocking version {@link #refreshInventoryAsync}.
+     *
+     * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well
+     *     as purchase information.
+     * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership.
+     *     Ignored if null or if querySkuDetails is false.
+     * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership.
+     *     Ignored if null or if querySkuDetails is false.
+     * @throws IabException if a problem occurs while refreshing the inventory.
+     */
+    public Inventory queryInventory(boolean querySkuDetails, List<String> moreItemSkus,
+                                        List<String> moreSubsSkus) throws IabException {
+        checkNotDisposed();
+        checkSetupDone("queryInventory");
+        try {
+            Inventory inv = new Inventory();
+            int r = queryPurchases(inv, ITEM_TYPE_INAPP);
+            if (r != BILLING_RESPONSE_RESULT_OK) {
+                throw new IabException(r, "Error refreshing inventory (querying owned items).");
+            }
+
+            if (querySkuDetails) {
+                r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus);
+                if (r != BILLING_RESPONSE_RESULT_OK) {
+                    throw new IabException(r, "Error refreshing inventory (querying prices of items).");
+                }
+            }
+
+            // if subscriptions are supported, then also query for subscriptions
+            if (mSubscriptionsSupported) {
+                r = queryPurchases(inv, ITEM_TYPE_SUBS);
+                if (r != BILLING_RESPONSE_RESULT_OK) {
+                    throw new IabException(r, "Error refreshing inventory (querying owned subscriptions).");
+                }
+
+                if (querySkuDetails) {
+                    r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus);
+                    if (r != BILLING_RESPONSE_RESULT_OK) {
+                        throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions).");
+                    }
+                }
+            }
+
+            return inv;
+        }
+        catch (RemoteException e) {
+            throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e);
+        }
+        catch (JSONException e) {
+            throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e);
+        }
+    }
+
+    /**
+     * Listener that notifies when an inventory query operation completes.
+     */
+    public interface QueryInventoryFinishedListener {
+        /**
+         * Called to notify that an inventory query operation completed.
+         *
+         * @param result The result of the operation.
+         * @param inv The inventory.
+         */
+        public void onQueryInventoryFinished(IabResult result, Inventory inv);
+    }
+
+
+    /**
+     * Asynchronous wrapper for inventory query. This will perform an inventory
+     * query as described in {@link #queryInventory}, but will do so asynchronously
+     * and call back the specified listener upon completion. This method is safe to
+     * call from a UI thread.
+     *
+     * @param querySkuDetails as in {@link #queryInventory}
+     * @param moreSkus as in {@link #queryInventory}
+     * @param listener The listener to notify when the refresh operation completes.
+     */
+    public void queryInventoryAsync(final boolean querySkuDetails,
+                               final List<String> moreSkus,
+                               final QueryInventoryFinishedListener listener) {
+        final Handler handler = new Handler();
+        checkNotDisposed();
+        checkSetupDone("queryInventory");
+        flagStartAsync("refresh inventory");
+        (new Thread(new Runnable() {
+            public void run() {
+                IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful.");
+                Inventory inv = null;
+                try {
+                    inv = queryInventory(querySkuDetails, moreSkus);
+                }
+                catch (IabException ex) {
+                    result = ex.getResult();
+                }
+
+                flagEndAsync();
+
+                final IabResult result_f = result;
+                final Inventory inv_f = inv;
+                if (!mDisposed && listener != null) {
+                    handler.post(new Runnable() {
+                        public void run() {
+                            listener.onQueryInventoryFinished(result_f, inv_f);
+                        }
+                    });
+                }
+            }
+        })).start();
+    }
+
+    public void queryInventoryAsync(QueryInventoryFinishedListener listener) {
+        queryInventoryAsync(true, null, listener);
+    }
+
+    public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) {
+        queryInventoryAsync(querySkuDetails, null, listener);
+    }
+
+
+    /**
+     * Consumes a given in-app product. Consuming can only be done on an item
+     * that's owned, and as a result of consumption, the user will no longer own it.
+     * This method may block or take long to return. Do not call from the UI thread.
+     * For that, see {@link #consumeAsync}.
+     *
+     * @param itemInfo The PurchaseInfo that represents the item to consume.
+     * @throws IabException if there is a problem during consumption.
+     */
+    void consume(Purchase itemInfo) throws IabException {
+        checkNotDisposed();
+        checkSetupDone("consume");
+
+        if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) {
+            throw new IabException(IABHELPER_INVALID_CONSUMPTION,
+                    "Items of type '" + itemInfo.mItemType + "' can't be consumed.");
+        }
+
+        try {
+            String token = itemInfo.getToken();
+            String sku = itemInfo.getSku();
+            if (token == null || token.equals("")) {
+               logError("Can't consume "+ sku + ". No token.");
+               throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: "
+                   + sku + " " + itemInfo);
+            }
+
+            logDebug("Consuming sku: " + sku + ", token: " + token);
+            int response = mService.consumePurchase(3, mContext.getPackageName(), token);
+            if (response == BILLING_RESPONSE_RESULT_OK) {
+               logDebug("Successfully consumed sku: " + sku);
+            }
+            else {
+               logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response));
+               throw new IabException(response, "Error consuming sku " + sku);
+            }
+        }
+        catch (RemoteException e) {
+            throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e);
+        }
+    }
+
+    /**
+     * Callback that notifies when a consumption operation finishes.
+     */
+    public interface OnConsumeFinishedListener {
+        /**
+         * Called to notify that a consumption has finished.
+         *
+         * @param purchase The purchase that was (or was to be) consumed.
+         * @param result The result of the consumption operation.
+         */
+        public void onConsumeFinished(Purchase purchase, IabResult result);
+    }
+
+    /**
+     * Callback that notifies when a multi-item consumption operation finishes.
+     */
+    public interface OnConsumeMultiFinishedListener {
+        /**
+         * Called to notify that a consumption of multiple items has finished.
+         *
+         * @param purchases The purchases that were (or were to be) consumed.
+         * @param results The results of each consumption operation, corresponding to each
+         *     sku.
+         */
+        public void onConsumeMultiFinished(List<Purchase> purchases, List<IabResult> results);
+    }
+
+    /**
+     * Asynchronous wrapper to item consumption. Works like {@link #consume}, but
+     * performs the consumption in the background and notifies completion through
+     * the provided listener. This method is safe to call from a UI thread.
+     *
+     * @param purchase The purchase to be consumed.
+     * @param listener The listener to notify when the consumption operation finishes.
+     */
+    public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) {
+        checkNotDisposed();
+        checkSetupDone("consume");
+        List<Purchase> purchases = new ArrayList<Purchase>();
+        purchases.add(purchase);
+        consumeAsyncInternal(purchases, listener, null);
+    }
+
+    /**
+     * Same as {@link consumeAsync}, but for multiple items at once.
+     * @param purchases The list of PurchaseInfo objects representing the purchases to consume.
+     * @param listener The listener to notify when the consumption operation finishes.
+     */
+    public void consumeAsync(List<Purchase> purchases, OnConsumeMultiFinishedListener listener) {
+        checkNotDisposed();
+        checkSetupDone("consume");
+        consumeAsyncInternal(purchases, null, listener);
+    }
+
+    /**
+     * Returns a human-readable description for the given response code.
+     *
+     * @param code The response code
+     * @return A human-readable string explaining the result code.
+     *     It also includes the result code numerically.
+     */
+    public static String getResponseDesc(int code) {
+        String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" +
+                "3:Billing Unavailable/4:Item unavailable/" +
+                "5:Developer Error/6:Error/7:Item Already Owned/" +
+                "8:Item not owned").split("/");
+        String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" +
+                                   "-1002:Bad response received/" +
+                                   "-1003:Purchase signature verification failed/" +
+                                   "-1004:Send intent failed/" +
+                                   "-1005:User cancelled/" +
+                                   "-1006:Unknown purchase response/" +
+                                   "-1007:Missing token/" +
+                                   "-1008:Unknown error/" +
+                                   "-1009:Subscriptions not available/" +
+                                   "-1010:Invalid consumption attempt").split("/");
+
+        if (code <= IABHELPER_ERROR_BASE) {
+            int index = IABHELPER_ERROR_BASE - code;
+            if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index];
+            else return String.valueOf(code) + ":Unknown IAB Helper Error";
+        }
+        else if (code < 0 || code >= iab_msgs.length)
+            return String.valueOf(code) + ":Unknown";
+        else
+            return iab_msgs[code];
+    }
+
+
+    // Checks that setup was done; if not, throws an exception.
+    void checkSetupDone(String operation) {
+        if (!mSetupDone) {
+            logError("Illegal state for operation (" + operation + "): IAB helper is not set up.");
+            throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation);
+        }
+    }
+
+    // Workaround to bug where sometimes response codes come as Long instead of Integer
+    int getResponseCodeFromBundle(Bundle b) {
+        Object o = b.get(RESPONSE_CODE);
+        if (o == null) {
+            logDebug("Bundle with null response code, assuming OK (known issue)");
+            return BILLING_RESPONSE_RESULT_OK;
+        }
+        else if (o instanceof Integer) return ((Integer)o).intValue();
+        else if (o instanceof Long) return (int)((Long)o).longValue();
+        else {
+            logError("Unexpected type for bundle response code.");
+            logError(o.getClass().getName());
+            throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName());
+        }
+    }
+
+    // Workaround to bug where sometimes response codes come as Long instead of Integer
+    int getResponseCodeFromIntent(Intent i) {
+        Object o = i.getExtras().get(RESPONSE_CODE);
+        if (o == null) {
+            logError("Intent with no response code, assuming OK (known issue)");
+            return BILLING_RESPONSE_RESULT_OK;
+        }
+        else if (o instanceof Integer) return ((Integer)o).intValue();
+        else if (o instanceof Long) return (int)((Long)o).longValue();
+        else {
+            logError("Unexpected type for intent response code.");
+            logError(o.getClass().getName());
+            throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName());
+        }
+    }
+
+    void flagStartAsync(String operation) {
+        if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" +
+                operation + ") because another async operation(" + mAsyncOperation + ") is in progress.");
+        mAsyncOperation = operation;
+        mAsyncInProgress = true;
+        logDebug("Starting async operation: " + operation);
+    }
+
+    void flagEndAsync() {
+        logDebug("Ending async operation: " + mAsyncOperation);
+        mAsyncOperation = "";
+        mAsyncInProgress = false;
+    }
+
+
+    int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException {
+        // Query purchases
+        logDebug("Querying owned items, item type: " + itemType);
+        logDebug("Package name: " + mContext.getPackageName());
+        boolean verificationFailed = false;
+        // Only allow purchase verification to be skipped if we are debuggable
+        boolean skipPurchaseVerification = (this.mSkipPurchaseVerification  &&
+                    ((mContext.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0));
+        String continueToken = null;
+
+        do {
+            logDebug("Calling getPurchases with continuation token: " + continueToken);
+            Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(),
+                    itemType, continueToken);
+
+            int response = getResponseCodeFromBundle(ownedItems);
+            logDebug("Owned items response: " + String.valueOf(response));
+            if (response != BILLING_RESPONSE_RESULT_OK) {
+                logDebug("getPurchases() failed: " + getResponseDesc(response));
+                return response;
+            }
+            if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST)
+                    || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST)
+                    || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) {
+                logError("Bundle returned from getPurchases() doesn't contain required fields.");
+                return IABHELPER_BAD_RESPONSE;
+            }
+
+            ArrayList<String> ownedSkus = ownedItems.getStringArrayList(
+                        RESPONSE_INAPP_ITEM_LIST);
+            ArrayList<String> purchaseDataList = ownedItems.getStringArrayList(
+                        RESPONSE_INAPP_PURCHASE_DATA_LIST);
+            ArrayList<String> signatureList = ownedItems.getStringArrayList(
+                        RESPONSE_INAPP_SIGNATURE_LIST);
+
+            for (int i = 0; i < purchaseDataList.size(); ++i) {
+                String purchaseData = purchaseDataList.get(i);
+                String signature = signatureList.get(i);
+                String sku = ownedSkus.get(i);
+                if (skipPurchaseVerification || Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) {
+                    logDebug("Sku is owned: " + sku);
+                    Purchase purchase = new Purchase(itemType, purchaseData, signature);
+
+                    if (TextUtils.isEmpty(purchase.getToken())) {
+                        logWarn("BUG: empty/null token!");
+                        logDebug("Purchase data: " + purchaseData);
+                    }
+
+                    // Record ownership and token
+                    inv.addPurchase(purchase);
+                }
+                else {
+                    logWarn("Purchase signature verification **FAILED**. Not adding item.");
+                    logDebug("   Purchase data: " + purchaseData);
+                    logDebug("   Signature: " + signature);
+                    verificationFailed = true;
+                }
+            }
+
+            continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN);
+            logDebug("Continuation token: " + continueToken);
+        } while (!TextUtils.isEmpty(continueToken));
+
+        return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK;
+    }
+
+    int querySkuDetails(String itemType, Inventory inv, List<String> moreSkus)
+                                throws RemoteException, JSONException {
+        logDebug("Querying SKU details.");
+        ArrayList<String> skuList = new ArrayList<String>();
+        skuList.addAll(inv.getAllOwnedSkus(itemType));
+        if (moreSkus != null) {
+            for (String sku : moreSkus) {
+                if (!skuList.contains(sku)) {
+                    skuList.add(sku);
+                }
+            }
+        }
+
+        if (skuList.size() == 0) {
+            logDebug("queryPrices: nothing to do because there are no SKUs.");
+            return BILLING_RESPONSE_RESULT_OK;
+        }
+
+        Bundle querySkus = new Bundle();
+        querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList);
+        Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(),
+                itemType, querySkus);
+
+        if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) {
+            int response = getResponseCodeFromBundle(skuDetails);
+            if (response != BILLING_RESPONSE_RESULT_OK) {
+                logDebug("getSkuDetails() failed: " + getResponseDesc(response));
+                return response;
+            }
+            else {
+                logError("getSkuDetails() returned a bundle with neither an error nor a detail list.");
+                return IABHELPER_BAD_RESPONSE;
+            }
+        }
+
+        ArrayList<String> responseList = skuDetails.getStringArrayList(
+                RESPONSE_GET_SKU_DETAILS_LIST);
+
+        for (String thisResponse : responseList) {
+            SkuDetails d = new SkuDetails(itemType, thisResponse);
+            logDebug("Got sku details: " + d);
+            inv.addSkuDetails(d);
+        }
+        return BILLING_RESPONSE_RESULT_OK;
+    }
+
+
+    void consumeAsyncInternal(final List<Purchase> purchases,
+                              final OnConsumeFinishedListener singleListener,
+                              final OnConsumeMultiFinishedListener multiListener) {
+        final Handler handler = new Handler();
+        flagStartAsync("consume");
+        (new Thread(new Runnable() {
+            public void run() {
+                final List<IabResult> results = new ArrayList<IabResult>();
+                for (Purchase purchase : purchases) {
+                    try {
+                        consume(purchase);
+                        results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku()));
+                    }
+                    catch (IabException ex) {
+                        results.add(ex.getResult());
+                    }
+                }
+
+                flagEndAsync();
+                if (!mDisposed && singleListener != null) {
+                    handler.post(new Runnable() {
+                        public void run() {
+                            singleListener.onConsumeFinished(purchases.get(0), results.get(0));
+                        }
+                    });
+                }
+                if (!mDisposed && multiListener != null) {
+                    handler.post(new Runnable() {
+                        public void run() {
+                            multiListener.onConsumeMultiFinished(purchases, results);
+                        }
+                    });
+                }
+            }
+        })).start();
+    }
+
+    void logDebug(String msg) {
+        if (mDebugLog) Log.d(mDebugTag, msg);
+    }
+
+    void logError(String msg) {
+        Log.e(mDebugTag, "In-app billing error: " + msg);
+    }
+
+    void logWarn(String msg) {
+        Log.w(mDebugTag, "In-app billing warning: " + msg);
+    }
+}

+ 45 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/android/IabResult.java

xqd
@@ -0,0 +1,45 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.alexdisler.inapppurchases;
+
+/**
+ * Represents the result of an in-app billing operation.
+ * A result is composed of a response code (an integer) and possibly a
+ * message (String). You can get those by calling
+ * {@link #getResponse} and {@link #getMessage()}, respectively. You
+ * can also inquire whether a result is a success or a failure by
+ * calling {@link #isSuccess()} and {@link #isFailure()}.
+ */
+public class IabResult {
+    int mResponse;
+    String mMessage;
+
+    public IabResult(int response, String message) {
+        mResponse = response;
+        if (message == null || message.trim().length() == 0) {
+            mMessage = IabHelper.getResponseDesc(response);
+        }
+        else {
+            mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")";
+        }
+    }
+    public int getResponse() { return mResponse; }
+    public String getMessage() { return mMessage; }
+    public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; }
+    public boolean isFailure() { return !isSuccess(); }
+    public String toString() { return "IabResult: " + getMessage(); }
+}
+

+ 406 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/android/InAppBillingV3.java

xqd
@@ -0,0 +1,406 @@
+// Copyright (c) 2013 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+/**
+ *
+ * Modifications: Alex Disler (alexdisler.com)
+ * github.com/alexdisler/cordova-plugin-inapppurchase
+ *
+ */
+
+package com.alexdisler.inapppurchases;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Scanner;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+import org.apache.cordova.CordovaInterface;
+import org.apache.cordova.CordovaPlugin;
+import org.apache.cordova.CallbackContext;
+import org.apache.cordova.CordovaWebView;
+
+import com.alexdisler.inapppurchases.IabHelper.OnConsumeFinishedListener;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.Intent;
+import android.util.Log;
+
+public class InAppBillingV3 extends CordovaPlugin {
+
+  protected static final String TAG = "google.payments";
+
+  public static final int OK = 0;
+  public static final int INVALID_ARGUMENTS = -1;
+  public static final int UNABLE_TO_INITIALIZE = -2;
+  public static final int BILLING_NOT_INITIALIZED = -3;
+  public static final int UNKNOWN_ERROR = -4;
+  public static final int USER_CANCELLED = -5;
+  public static final int BAD_RESPONSE_FROM_SERVER = -6;
+  public static final int VERIFICATION_FAILED = -7;
+  public static final int ITEM_UNAVAILABLE = -8;
+  public static final int ITEM_ALREADY_OWNED = -9;
+  public static final int ITEM_NOT_OWNED = -10;
+  public static final int CONSUME_FAILED = -11;
+
+  public static final int PURCHASE_PURCHASED = 0;
+  public static final int PURCHASE_CANCELLED = 1;
+  public static final int PURCHASE_REFUNDED = 2;
+
+  private IabHelper iabHelper = null;
+  boolean billingInitialized = false;
+  AtomicInteger orderSerial = new AtomicInteger(0);
+
+  private JSONObject manifestObject = null;
+
+  private JSONObject getManifestContents() {
+    if (manifestObject != null) return manifestObject;
+
+    Context context = this.cordova.getActivity();
+    InputStream is;
+    try {
+      is = context.getAssets().open("www/manifest.json");
+      Scanner s = new Scanner(is).useDelimiter("\\A");
+      String manifestString = s.hasNext() ? s.next() : "";
+      Log.d(TAG, "manifest:" + manifestString);
+      manifestObject = new JSONObject(manifestString);
+    } catch (IOException e) {
+      Log.d(TAG, "Unable to read manifest file:" + e.toString());
+      manifestObject = null;
+    } catch (JSONException e) {
+      Log.d(TAG, "Unable to parse manifest file:" + e.toString());
+      manifestObject = null;
+    }
+    return manifestObject;
+  }
+
+  protected String getBase64EncodedPublicKey() {
+    JSONObject manifestObject = getManifestContents();
+    if (manifestObject != null) {
+      return manifestObject.optString("play_store_key");
+    }
+    return null;
+  }
+
+  protected boolean shouldSkipPurchaseVerification() {
+    JSONObject manifestObject = getManifestContents();
+    if (manifestObject != null) {
+      return manifestObject.optBoolean("skip_purchase_verification");
+    }
+    return false;
+  }
+
+  protected boolean initializeBillingHelper() {
+    if (iabHelper != null) {
+      Log.d(TAG, "Billing already initialized");
+      return true;
+    }
+    Context context = this.cordova.getActivity();
+    String base64EncodedPublicKey = getBase64EncodedPublicKey();
+    boolean skipPurchaseVerification = shouldSkipPurchaseVerification();
+    if (base64EncodedPublicKey != null) {
+      iabHelper = new IabHelper(context, base64EncodedPublicKey);
+      iabHelper.setSkipPurchaseVerification(skipPurchaseVerification);
+      billingInitialized = false;
+      return true;
+    }
+    Log.d(TAG, "Unable to initialize billing");
+    return false;
+  }
+
+  @Override
+  public void initialize(CordovaInterface cordova, CordovaWebView webView) {
+    super.initialize(cordova, webView);
+    initializeBillingHelper();
+  }
+
+  protected JSONObject makeError(String message) {
+    return makeError(message, null, null, null);
+  }
+
+  protected JSONObject makeError(String message, Integer resultCode) {
+    return makeError(message, resultCode, null, null);
+  }
+
+  protected JSONObject makeError(String message, Integer resultCode, IabResult result) {
+    return makeError(message, resultCode, result.getMessage(), result.getResponse());
+  }
+
+  protected JSONObject makeError(String message, Integer resultCode, String text, Integer response) {
+    if (message != null) {
+      Log.d(TAG, "Error: " + message);
+    }
+    JSONObject error = new JSONObject();
+    try {
+      if (resultCode != null) {
+        error.put("code", (int)resultCode);
+      }
+      if (message != null) {
+        error.put("message", message);
+      }
+      if (text != null) {
+        error.put("text", text);
+      }
+      if (response != null) {
+        error.put("response", response);
+      }
+    } catch (JSONException e) {}
+    return error;
+  }
+
+  @Override
+  public boolean execute(String action, final JSONArray args, final CallbackContext callbackContext) {
+    Log.d(TAG, "executing on android");
+    if ("init".equals(action)) {
+      return init(args, callbackContext);
+    } else if ("buy".equals(action)) {
+      return buy(args, callbackContext);
+    } else if ("subscribe".equals(action)) {
+      return subscribe(args, callbackContext);
+    } else if ("consumePurchase".equals(action)) {
+      return consumePurchase(args, callbackContext);
+    } else if ("getSkuDetails".equals(action)) {
+      return getSkuDetails(args, callbackContext);
+    } else if ("restorePurchases".equals(action)) {
+      return restorePurchases(args, callbackContext);
+    }
+    return false;
+  }
+
+  protected boolean init(final JSONArray args, final CallbackContext callbackContext) {
+    if (billingInitialized == true) {
+      Log.d(TAG, "Billing already initialized");
+      callbackContext.success();
+    } else if (iabHelper == null) {
+      callbackContext.error(makeError("Billing cannot be initialized", UNABLE_TO_INITIALIZE));
+    } else {
+      iabHelper.startSetup(new IabHelper.OnIabSetupFinishedListener() {
+        public void onIabSetupFinished(IabResult result) {
+          if (!result.isSuccess()) {
+            callbackContext.error(makeError("Unable to initialize billing: " + result.toString(), UNABLE_TO_INITIALIZE, result));
+          } else {
+            Log.d(TAG, "Billing initialized");
+            billingInitialized = true;
+            callbackContext.success();
+          }
+        }
+      });
+    }
+    return true;
+  }
+
+  protected boolean runPayment(final JSONArray args, final CallbackContext callbackContext, boolean subscribe) {
+    final String sku;
+    try {
+      sku = args.getString(0);
+    } catch (JSONException e) {
+      callbackContext.error(makeError("Invalid SKU", INVALID_ARGUMENTS));
+      return false;
+    }
+    if (iabHelper == null || !billingInitialized) {
+      callbackContext.error(makeError("Billing is not initialized", BILLING_NOT_INITIALIZED));
+      return false;
+    }
+    final Activity cordovaActivity = this.cordova.getActivity();
+    int newOrder = orderSerial.getAndIncrement();
+    this.cordova.setActivityResultCallback(this);
+
+    IabHelper.OnIabPurchaseFinishedListener oipfl = new IabHelper.OnIabPurchaseFinishedListener() {
+      public void onIabPurchaseFinished(IabResult result, Purchase purchase) {
+        if (result.isFailure()) {
+          int response = result.getResponse();
+          if (response == IabHelper.IABHELPER_BAD_RESPONSE || response == IabHelper.IABHELPER_UNKNOWN_ERROR) {
+            callbackContext.error(makeError("Could not complete purchase", BAD_RESPONSE_FROM_SERVER, result));
+          } else if (response == IabHelper.IABHELPER_VERIFICATION_FAILED) {
+            callbackContext.error(makeError("Could not complete purchase", BAD_RESPONSE_FROM_SERVER, result));
+          } else if (response == IabHelper.IABHELPER_USER_CANCELLED) {
+            callbackContext.error(makeError("Purchase Cancelled", USER_CANCELLED, result));
+          } else if (response == IabHelper.BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED) {
+            callbackContext.error(makeError("Item already owned", ITEM_ALREADY_OWNED, result));
+          } else {
+            callbackContext.error(makeError("Error completing purchase: " + response, UNKNOWN_ERROR, result));
+          }
+        } else {
+          try {
+            JSONObject pluginResponse = new JSONObject();
+            pluginResponse.put("orderId", purchase.getOrderId());
+            pluginResponse.put("packageName", purchase.getPackageName());
+            pluginResponse.put("productId", purchase.getSku());
+            pluginResponse.put("purchaseTime", purchase.getPurchaseTime());
+            pluginResponse.put("purchaseState", purchase.getPurchaseState());
+            pluginResponse.put("purchaseToken", purchase.getToken());
+            pluginResponse.put("signature", purchase.getSignature());
+            pluginResponse.put("type", purchase.getItemType());
+            pluginResponse.put("receipt", purchase.getOriginalJson());
+            callbackContext.success(pluginResponse);
+          } catch (JSONException e) {
+            callbackContext.error("Purchase succeeded but success handler failed");
+          }
+        }
+      }
+    };
+    if(subscribe){
+      iabHelper.launchSubscriptionPurchaseFlow(cordovaActivity, sku, newOrder, oipfl, "");
+    } else {
+      iabHelper.launchPurchaseFlow(cordovaActivity, sku, newOrder, oipfl, "");
+    }
+    return true;
+  }
+
+  protected boolean subscribe(final JSONArray args, final CallbackContext callbackContext) {
+    return runPayment(args, callbackContext, true);
+  }
+
+  protected boolean buy(final JSONArray args, final CallbackContext callbackContext) {
+    return runPayment(args, callbackContext, false);
+  }
+
+  protected boolean consumePurchase(final JSONArray args, final CallbackContext callbackContext) {
+    final Purchase purchase;
+    try {
+      String type = args.getString(0);
+      String receipt = args.getString(1);
+      String signature = args.getString(2);
+      purchase = new Purchase(type, receipt, signature);
+    } catch (JSONException e) {
+      callbackContext.error(makeError("Unable to parse purchase token", INVALID_ARGUMENTS));
+      return false;
+    }
+    if (purchase == null) {
+      callbackContext.error(makeError("Unrecognized purchase token", INVALID_ARGUMENTS));
+      return false;
+    }
+    if (iabHelper == null || !billingInitialized) {
+      callbackContext.error(makeError("Billing is not initialized", BILLING_NOT_INITIALIZED));
+      return false;
+    }
+    iabHelper.consumeAsync(purchase, new OnConsumeFinishedListener() {
+      public void onConsumeFinished(Purchase purchase, IabResult result) {
+        if (result.isFailure()) {
+          int response = result.getResponse();
+          if (response == IabHelper.BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED) {
+            callbackContext.error(makeError("Error consuming purchase", ITEM_NOT_OWNED, result));
+          } else {
+            callbackContext.error(makeError("Error consuming purchase", CONSUME_FAILED, result));
+          }
+        } else {
+          try {
+            JSONObject pluginResponse = new JSONObject();
+            pluginResponse.put("transactionId", purchase.getOrderId());
+            pluginResponse.put("productId", purchase.getSku());
+            pluginResponse.put("token", purchase.getToken());
+            callbackContext.success(pluginResponse);
+          } catch (JSONException e) {
+            callbackContext.error("Consume succeeded but success handler failed");
+          }
+        }
+      }
+    });
+    return true;
+  }
+
+  protected boolean getSkuDetails(final JSONArray args, final CallbackContext callbackContext) {
+    final List<String> moreSkus = new ArrayList<String>();
+    try {
+      for (int i = 0; i < args.length(); i++) {
+        moreSkus.add(args.getString(i));
+        Log.d(TAG, "get sku:" + args.getString(i));
+      }
+    } catch (JSONException e) {
+      callbackContext.error(makeError("Invalid SKU", INVALID_ARGUMENTS));
+      return false;
+    }
+    if (iabHelper == null || !billingInitialized) {
+      callbackContext.error(makeError("Billing is not initialized", BILLING_NOT_INITIALIZED));
+      return false;
+    }
+    iabHelper.queryInventoryAsync(true, moreSkus, new IabHelper.QueryInventoryFinishedListener() {
+      public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
+        if (result.isFailure()) {
+          callbackContext.error("Error retrieving SKU details");
+          return;
+        }
+        JSONArray response = new JSONArray();
+        try {
+          for (String sku : moreSkus) {
+            SkuDetails skuDetails = inventory.getSkuDetails(sku);
+            if (skuDetails != null) {
+              JSONObject detailsJson = new JSONObject();
+              detailsJson.put("productId", skuDetails.getSku());
+              detailsJson.put("title", skuDetails.getTitle());
+              detailsJson.put("description", skuDetails.getDescription());
+              detailsJson.put("priceAsDecimal", skuDetails.getPriceAsDecimal());
+              detailsJson.put("price", skuDetails.getPrice());
+              detailsJson.put("type", skuDetails.getType());
+              detailsJson.put("currency", skuDetails.getPriceCurrency());
+              response.put(detailsJson);
+            }
+          }
+        } catch (JSONException e) {
+          callbackContext.error(e.getMessage());
+        }
+        callbackContext.success(response);
+      }
+    });
+    return true;
+  }
+
+  protected boolean restorePurchases(final JSONArray args, final CallbackContext callbackContext) {
+    if (iabHelper == null || !billingInitialized) {
+      callbackContext.error(makeError("Billing is not initialized", BILLING_NOT_INITIALIZED));
+    } else {
+      iabHelper.queryInventoryAsync(new IabHelper.QueryInventoryFinishedListener() {
+        public void onQueryInventoryFinished(IabResult result, Inventory inventory) {
+          if (result.isFailure()) {
+            callbackContext.error("Error retrieving purchase details");
+            return;
+          }
+          JSONArray response = new JSONArray();
+          try {
+            for (Purchase purchase : inventory.getAllPurchases()) {
+              if (purchase != null) {
+                JSONObject detailsJson = new JSONObject();
+                detailsJson.put("orderId", purchase.getOrderId());
+                detailsJson.put("packageName", purchase.getPackageName());
+                detailsJson.put("productId", purchase.getSku());
+                detailsJson.put("purchaseTime", purchase.getPurchaseTime());
+                detailsJson.put("purchaseState", purchase.getPurchaseState());
+                detailsJson.put("purchaseToken", purchase.getToken());
+                detailsJson.put("signature", purchase.getSignature());
+                detailsJson.put("type", purchase.getItemType());
+                detailsJson.put("receipt", purchase.getOriginalJson());
+                response.put(detailsJson);
+              }
+            }
+          } catch (JSONException e) {
+            callbackContext.error(e.getMessage());
+          }
+          callbackContext.success(response);
+        }
+      });
+    }
+    return true;
+  }
+
+  @Override
+  public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+    if (!iabHelper.handleActivityResult(requestCode, resultCode, intent)) {
+      super.onActivityResult(requestCode, resultCode, intent);
+    }
+  }
+
+
+  @Override
+  public void onDestroy() {
+    if (iabHelper != null) iabHelper.dispose();
+    iabHelper = null;
+    billingInitialized = false;
+  }
+}

+ 91 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/android/Inventory.java

xqd
@@ -0,0 +1,91 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.alexdisler.inapppurchases;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Represents a block of information about in-app items.
+ * An Inventory is returned by such methods as {@link IabHelper#queryInventory}.
+ */
+public class Inventory {
+    Map<String,SkuDetails> mSkuMap = new HashMap<String,SkuDetails>();
+    Map<String,Purchase> mPurchaseMap = new HashMap<String,Purchase>();
+
+    Inventory() { }
+
+    /** Returns the listing details for an in-app product. */
+    public SkuDetails getSkuDetails(String sku) {
+        return mSkuMap.get(sku);
+    }
+
+    /** Returns purchase information for a given product, or null if there is no purchase. */
+    public Purchase getPurchase(String sku) {
+        return mPurchaseMap.get(sku);
+    }
+
+    /** Returns whether or not there exists a purchase of the given product. */
+    public boolean hasPurchase(String sku) {
+        return mPurchaseMap.containsKey(sku);
+    }
+
+    /** Return whether or not details about the given product are available. */
+    public boolean hasDetails(String sku) {
+        return mSkuMap.containsKey(sku);
+    }
+
+    /**
+     * Erase a purchase (locally) from the inventory, given its product ID. This just
+     * modifies the Inventory object locally and has no effect on the server! This is
+     * useful when you have an existing Inventory object which you know to be up to date,
+     * and you have just consumed an item successfully, which means that erasing its
+     * purchase data from the Inventory you already have is quicker than querying for
+     * a new Inventory.
+     */
+    public void erasePurchase(String sku) {
+        if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku);
+    }
+
+    /** Returns a list of all owned product IDs. */
+    List<String> getAllOwnedSkus() {
+        return new ArrayList<String>(mPurchaseMap.keySet());
+    }
+
+    /** Returns a list of all owned product IDs of a given type */
+    List<String> getAllOwnedSkus(String itemType) {
+        List<String> result = new ArrayList<String>();
+        for (Purchase p : mPurchaseMap.values()) {
+            if (p.getItemType().equals(itemType)) result.add(p.getSku());
+        }
+        return result;
+    }
+
+    /** Returns a list of all purchases. */
+    List<Purchase> getAllPurchases() {
+        return new ArrayList<Purchase>(mPurchaseMap.values());
+    }
+
+    void addSkuDetails(SkuDetails d) {
+        mSkuMap.put(d.getSku(), d);
+    }
+
+    void addPurchase(Purchase p) {
+        mPurchaseMap.put(p.getSku(), p);
+    }
+}

+ 63 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/android/Purchase.java

xqd
@@ -0,0 +1,63 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.alexdisler.inapppurchases;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app billing purchase.
+ */
+public class Purchase {
+    String mItemType;  // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS
+    String mOrderId;
+    String mPackageName;
+    String mSku;
+    long mPurchaseTime;
+    int mPurchaseState;
+    String mDeveloperPayload;
+    String mToken;
+    String mOriginalJson;
+    String mSignature;
+
+    public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException {
+        mItemType = itemType;
+        mOriginalJson = jsonPurchaseInfo;
+        JSONObject o = new JSONObject(mOriginalJson);
+        mOrderId = o.optString("orderId");
+        mPackageName = o.optString("packageName");
+        mSku = o.optString("productId");
+        mPurchaseTime = o.optLong("purchaseTime");
+        mPurchaseState = o.optInt("purchaseState");
+        mDeveloperPayload = o.optString("developerPayload");
+        mToken = o.optString("token", o.optString("purchaseToken"));
+        mSignature = signature;
+    }
+
+    public String getItemType() { return mItemType; }
+    public String getOrderId() { return mOrderId; }
+    public String getPackageName() { return mPackageName; }
+    public String getSku() { return mSku; }
+    public long getPurchaseTime() { return mPurchaseTime; }
+    public int getPurchaseState() { return mPurchaseState; }
+    public String getDeveloperPayload() { return mDeveloperPayload; }
+    public String getToken() { return mToken; }
+    public String getOriginalJson() { return mOriginalJson; }
+    public String getSignature() { return mSignature; }
+
+    @Override
+    public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; }
+}

+ 123 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/android/Security.java

xqd
@@ -0,0 +1,123 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.alexdisler.inapppurchases;
+
+import android.text.TextUtils;
+import android.util.Log;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+
+import java.security.InvalidKeyException;
+import java.security.KeyFactory;
+import java.security.NoSuchAlgorithmException;
+import java.security.PublicKey;
+import java.security.Signature;
+import java.security.SignatureException;
+import java.security.spec.InvalidKeySpecException;
+import java.security.spec.X509EncodedKeySpec;
+
+/**
+ * Security-related methods. For a secure implementation, all of this code
+ * should be implemented on a server that communicates with the
+ * application on the device. For the sake of simplicity and clarity of this
+ * example, this code is included here and is executed on the device. If you
+ * must verify the purchases on the phone, you should obfuscate this code to
+ * make it harder for an attacker to replace the code with stubs that treat all
+ * purchases as verified.
+ */
+public class Security {
+    private static final String TAG = "IABUtil/Security";
+
+    private static final String KEY_FACTORY_ALGORITHM = "RSA";
+    private static final String SIGNATURE_ALGORITHM = "SHA1withRSA";
+
+    /**
+     * Verifies that the data was signed with the given signature, and returns
+     * the verified purchase. The data is in JSON format and signed
+     * with a private key. The data also contains the {@link PurchaseState}
+     * and product ID of the purchase.
+     * @param base64PublicKey the base64-encoded public key to use for verifying.
+     * @param signedData the signed JSON string (signed, not encrypted)
+     * @param signature the signature for the data, signed with the private key
+     */
+    public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) {
+        if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) ||
+                TextUtils.isEmpty(signature)) {
+            Log.e(TAG, "Purchase verification failed: missing data.");
+            return false;
+        }
+
+        PublicKey key = Security.generatePublicKey(base64PublicKey);
+        return Security.verify(key, signedData, signature);
+    }
+
+    /**
+     * Generates a PublicKey instance from a string containing the
+     * Base64-encoded public key.
+     *
+     * @param encodedPublicKey Base64-encoded public key
+     * @throws IllegalArgumentException if encodedPublicKey is invalid
+     */
+    public static PublicKey generatePublicKey(String encodedPublicKey) {
+        try {
+            byte[] decodedKey = Base64.decode(encodedPublicKey);
+            KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM);
+            return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey));
+        } catch (NoSuchAlgorithmException e) {
+            throw new RuntimeException(e);
+        } catch (InvalidKeySpecException e) {
+            Log.e(TAG, "Invalid key specification.");
+            throw new IllegalArgumentException(e);
+        } catch (Base64DecoderException e) {
+            Log.e(TAG, "Base64 decoding failed.");
+            throw new IllegalArgumentException(e);
+        }
+    }
+
+    /**
+     * Verifies that the signature from the server matches the computed
+     * signature on the data.  Returns true if the data is correctly signed.
+     *
+     * @param publicKey public key associated with the developer account
+     * @param signedData signed data from server
+     * @param signature server signature
+     * @return true if the data and signature match
+     */
+    public static boolean verify(PublicKey publicKey, String signedData, String signature) {
+        Signature sig;
+        try {
+            sig = Signature.getInstance(SIGNATURE_ALGORITHM);
+            sig.initVerify(publicKey);
+            sig.update(signedData.getBytes());
+            if (!sig.verify(Base64.decode(signature))) {
+                Log.e(TAG, "Signature verification failed.");
+                return false;
+            }
+            return true;
+        } catch (NoSuchAlgorithmException e) {
+            Log.e(TAG, "NoSuchAlgorithmException.");
+        } catch (InvalidKeyException e) {
+            Log.e(TAG, "Invalid key specification.");
+        } catch (SignatureException e) {
+            Log.e(TAG, "Signature exception.");
+        } catch (Base64DecoderException e) {
+            Log.e(TAG, "Base64 decoding failed.");
+        }
+        return false;
+    }
+}

+ 64 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/android/SkuDetails.java

xqd
@@ -0,0 +1,64 @@
+/* Copyright (c) 2012 Google Inc.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.alexdisler.inapppurchases;
+
+import org.json.JSONException;
+import org.json.JSONObject;
+
+/**
+ * Represents an in-app product's listing details.
+ */
+public class SkuDetails {
+    String mItemType;
+    String mSku;
+    String mType;
+    Double mPriceAsDecimal;
+    String mPrice;
+    String mPriceCurrency;
+    String mTitle;
+    String mDescription;
+    String mJson;
+
+    public SkuDetails(String jsonSkuDetails) throws JSONException {
+        this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails);
+    }
+
+    public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException {
+        mItemType = itemType;
+        mJson = jsonSkuDetails;
+        JSONObject o = new JSONObject(mJson);
+        mSku = o.optString("productId");
+        mType = o.optString("type");
+        mPrice = o.optString("price");
+        mPriceCurrency = o.optString("price_currency_code");
+        mPriceAsDecimal = Double.parseDouble(o.optString("price_amount_micros"))/Double.valueOf(1000000);
+        mTitle = o.optString("title");
+        mDescription = o.optString("description");
+    }
+
+    public String getSku() { return mSku; }
+    public String getType() { return mType; }
+    public String getPrice() { return mPrice; }
+    public String getPriceCurrency() { return mPriceCurrency; }
+    public Double getPriceAsDecimal() { return mPriceAsDecimal; }
+    public String getTitle() { return mTitle; }
+    public String getDescription() { return mDescription; }
+
+    @Override
+    public String toString() {
+        return "SkuDetails:" + mJson;
+    }
+}

+ 144 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/android/billing/IInAppBillingService.aidl

xqd
@@ -0,0 +1,144 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.vending.billing;
+
+import android.os.Bundle;
+
+/**
+ * InAppBillingService is the service that provides in-app billing version 3 and beyond.
+ * This service provides the following features:
+ * 1. Provides a new API to get details of in-app items published for the app including
+ *    price, type, title and description.
+ * 2. The purchase flow is synchronous and purchase information is available immediately
+ *    after it completes.
+ * 3. Purchase information of in-app purchases is maintained within the Google Play system
+ *    till the purchase is consumed.
+ * 4. An API to consume a purchase of an inapp item. All purchases of one-time
+ *    in-app items are consumable and thereafter can be purchased again.
+ * 5. An API to get current purchases of the user immediately. This will not contain any
+ *    consumed purchases.
+ *
+ * All calls will give a response code with the following possible values
+ * RESULT_OK = 0 - success
+ * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog
+ * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested
+ * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase
+ * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API
+ * RESULT_ERROR = 6 - Fatal error during the API action
+ * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned
+ * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned
+ */
+interface IInAppBillingService {
+    /**
+     * Checks support for the requested billing API version, package and in-app type.
+     * Minimum API version supported by this interface is 3.
+     * @param apiVersion the billing version which the app is using
+     * @param packageName the package name of the calling app
+     * @param type type of the in-app item being purchased "inapp" for one-time purchases
+     *        and "subs" for subscription.
+     * @return RESULT_OK(0) on success, corresponding result code on failures
+     */
+    int isBillingSupported(int apiVersion, String packageName, String type);
+
+    /**
+     * Provides details of a list of SKUs
+     * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle
+     * with a list JSON strings containing the productId, price, title and description.
+     * This API can be called with a maximum of 20 SKUs.
+     * @param apiVersion billing API version that the Third-party is using
+     * @param packageName the package name of the calling app
+     * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST"
+     * @return Bundle containing the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+     *              failure as listed above.
+     *         "DETAILS_LIST" with a StringArrayList containing purchase information
+     *              in JSON format similar to:
+     *              '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00",
+     *                 "title : "Example Title", "description" : "This is an example description" }'
+     */
+    Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle);
+
+    /**
+     * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU,
+     * the type, a unique purchase token and an optional developer payload.
+     * @param apiVersion billing API version that the app is using
+     * @param packageName package name of the calling app
+     * @param sku the SKU of the in-app item as published in the developer console
+     * @param type the type of the in-app item ("inapp" for one-time purchases
+     *        and "subs" for subscription).
+     * @param developerPayload optional argument to be sent back with the purchase information
+     * @return Bundle containing the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+     *              failure as listed above.
+     *         "BUY_INTENT" - PendingIntent to start the purchase flow
+     *
+     * The Pending intent should be launched with startIntentSenderForResult. When purchase flow
+     * has completed, the onActivityResult() will give a resultCode of OK or CANCELED.
+     * If the purchase is successful, the result data will contain the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+     *              failure as listed above.
+     *         "INAPP_PURCHASE_DATA" - String in JSON format similar to
+     *              '{"orderId":"12999763169054705758.1371079406387615",
+     *                "packageName":"com.example.app",
+     *                "productId":"exampleSku",
+     *                "purchaseTime":1345678900000,
+     *                "purchaseToken" : "122333444455555",
+     *                "developerPayload":"example developer payload" }'
+     *         "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that
+     *                                  was signed with the private key of the developer
+     *                                  TODO: change this to app-specific keys.
+     */
+    Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type,
+        String developerPayload);
+
+    /**
+     * Returns the current SKUs owned by the user of the type and package name specified along with
+     * purchase information and a signature of the data to be validated.
+     * This will return all SKUs that have been purchased in V3 and managed items purchased using
+     * V1 and V2 that have not been consumed.
+     * @param apiVersion billing API version that the app is using
+     * @param packageName package name of the calling app
+     * @param type the type of the in-app items being requested
+     *        ("inapp" for one-time purchases and "subs" for subscription).
+     * @param continuationToken to be set as null for the first call, if the number of owned
+     *        skus are too many, a continuationToken is returned in the response bundle.
+     *        This method can be called again with the continuation token to get the next set of
+     *        owned skus.
+     * @return Bundle containing the following key-value pairs
+     *         "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on
+     *              failure as listed above.
+     *         "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs
+     *         "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information
+     *         "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures
+     *                                      of the purchase information
+     *         "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the
+     *                                      next set of in-app purchases. Only set if the
+     *                                      user has more owned skus than the current list.
+     */
+    Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken);
+
+    /**
+     * Consume the last purchase of the given SKU. This will result in this item being removed
+     * from all subsequent responses to getPurchases() and allow re-purchase of this item.
+     * @param apiVersion billing API version that the app is using
+     * @param packageName package name of the calling app
+     * @param purchaseToken token in the purchase information JSON that identifies the purchase
+     *        to be consumed
+     * @return 0 if consumption succeeded. Appropriate error values for failures.
+     */
+    int consumePurchase(int apiVersion, String packageName, String purchaseToken);
+}

+ 20 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/ios/PaymentsPlugin.h

xqd
@@ -0,0 +1,20 @@
+/*!
+ *
+ * Author: Alex Disler (alexdisler.com)
+ * github.com/alexdisler/cordova-plugin-inapppurchase
+ *
+ * Licensed under the MIT license. Please see README for more information.
+ *
+ */
+
+#import <Foundation/Foundation.h>
+#import <Cordova/CDVPlugin.h>
+
+@interface PaymentsPlugin : CDVPlugin
+
+- (void)getProducts:(CDVInvokedUrlCommand *)command;
+- (void)buy:(CDVInvokedUrlCommand *)command;
+- (void)restorePurchases:(CDVInvokedUrlCommand *)command;
+- (void)getReceipt:(CDVInvokedUrlCommand *)command;
+
+@end

+ 145 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/ios/PaymentsPlugin.m

xqd
@@ -0,0 +1,145 @@
+/*!
+ *
+ * Author: Alex Disler (alexdisler.com)
+ * github.com/alexdisler/cordova-plugin-inapppurchase
+ *
+ * Licensed under the MIT license. Please see README for more information.
+ *
+ */
+
+#import "PaymentsPlugin.h"
+#import "RMStore.h"
+
+#define NILABLE(obj) ((obj) != nil ? (NSObject *)(obj) : (NSObject *)[NSNull null])
+
+@implementation PaymentsPlugin
+
+- (void)pluginInitialize {
+}
+
+- (void)getProducts:(CDVInvokedUrlCommand *)command {
+  id productIds = [command.arguments objectAtIndex:0];
+
+  if (![productIds isKindOfClass:[NSArray class]]) {
+    CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"ProductIds must be an array"];
+    [pluginResult setKeepCallbackAsBool:YES];
+    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+    return;
+  }
+
+  NSSet *products = [NSSet setWithArray:productIds];
+  [[RMStore defaultStore] requestProducts:products success:^(NSArray *products, NSArray *invalidProductIdentifiers) {
+
+    NSMutableDictionary *result = [NSMutableDictionary dictionary];
+    NSMutableArray *validProducts = [NSMutableArray array];
+    for (SKProduct *product in products) {
+        NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
+        [numberFormatter setFormatterBehavior:NSNumberFormatterBehavior10_4];
+        [numberFormatter setNumberStyle:NSNumberFormatterCurrencyStyle];
+        [numberFormatter setLocale:product.priceLocale];
+        NSString *currencyCode = [numberFormatter currencyCode];
+        
+        [validProducts addObject:@{
+                                 @"productId": NILABLE(product.productIdentifier),
+                                 @"title": NILABLE(product.localizedTitle),
+                                 @"description": NILABLE(product.localizedDescription),
+                                 @"priceAsDecimal": NILABLE(product.price),
+                                 @"price": NILABLE([RMStore localizedPriceOfProduct:product]),
+                                 @"currency": NILABLE(currencyCode)
+                                 }];
+    }
+    [result setObject:validProducts forKey:@"products"];
+    [result setObject:invalidProductIdentifiers forKey:@"invalidProductsIds"];
+    CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result];
+    [pluginResult setKeepCallbackAsBool:YES];
+    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+  } failure:^(NSError *error) {
+    CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:@{
+                                                                                                                   @"errorCode": NILABLE([NSNumber numberWithInteger:error.code]),
+                                                                                                                   @"errorMessage": NILABLE(error.localizedDescription)
+                                                                                                                   }];
+    [pluginResult setKeepCallbackAsBool:YES];
+    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+  }];
+}
+
+- (void)buy:(CDVInvokedUrlCommand *)command {
+  id productId = [command.arguments objectAtIndex:0];
+  if (![productId isKindOfClass:[NSString class]]) {
+    CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsString:@"ProductId must be a string"];
+    [pluginResult setKeepCallbackAsBool:YES];
+    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+    return;
+  }
+  [[RMStore defaultStore] addPayment:productId success:^(SKPaymentTransaction *transaction) {
+    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
+    NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
+    NSString *encReceipt = [receiptData base64EncodedStringWithOptions:0];
+    CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:@{
+                                                                                                                   @"transactionId": NILABLE(transaction.transactionIdentifier),
+                                                                                                                   @"receipt": NILABLE(encReceipt)
+                                                                                                                   }];
+    [pluginResult setKeepCallbackAsBool:YES];
+    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+
+  } failure:^(SKPaymentTransaction *transaction, NSError *error) {
+    CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:@{
+                                                                                                                   @"errorCode": NILABLE([NSNumber numberWithInteger:error.code]),
+                                                                                                                   @"errorMessage": NILABLE(error.localizedDescription)
+                                                                                                                   }];
+    [pluginResult setKeepCallbackAsBool:YES];
+    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+  }];
+
+}
+
+- (void)restorePurchases:(CDVInvokedUrlCommand *)command {
+  [[RMStore defaultStore] restoreTransactionsOnSuccess:^(NSArray *transactions){
+    NSMutableArray *validTransactions = [NSMutableArray array];
+    NSMutableDictionary *result = [NSMutableDictionary dictionary];
+    NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
+    formatter.locale = [NSLocale localeWithLocaleIdentifier:@"en_US_POSIX"];
+    formatter.timeZone = [NSTimeZone timeZoneForSecondsFromGMT:0];
+    formatter.dateFormat = @"yyyy-MM-dd'T'HH:mm:ss'Z'";
+    for (SKPaymentTransaction *transaction in transactions) {
+      NSString *transactionDateString = [formatter stringFromDate:transaction.transactionDate];
+      [validTransactions addObject:@{
+                                 @"productId": NILABLE(transaction.payment.productIdentifier),
+                                 @"date": NILABLE(transactionDateString),
+                                 @"transactionId": NILABLE(transaction.transactionIdentifier),
+                                 @"transactionState": NILABLE([NSNumber numberWithInteger:transaction.transactionState])
+                                 }];
+    }
+    [result setObject:validTransactions forKey:@"transactions"];
+    CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:result];
+    [pluginResult setKeepCallbackAsBool:YES];
+    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+  } failure:^(NSError *error) {
+    CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:@{
+                                                                                                                   @"errorCode": NILABLE([NSNumber numberWithInteger:error.code]),
+                                                                                                                   @"errorMessage": NILABLE(error.localizedDescription)
+                                                                                                                   }];
+    [pluginResult setKeepCallbackAsBool:YES];
+    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+  }];
+}
+
+- (void)getReceipt:(CDVInvokedUrlCommand *)command {
+  [[RMStore defaultStore] refreshReceiptOnSuccess:^{
+    NSURL *receiptURL = [[NSBundle mainBundle] appStoreReceiptURL];
+    NSData *receiptData = [NSData dataWithContentsOfURL:receiptURL];
+    NSString *encReceipt = [receiptData base64EncodedStringWithOptions:0];
+    CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_OK messageAsDictionary:@{@"receipt": NILABLE(encReceipt) }];
+    [pluginResult setKeepCallbackAsBool:YES];
+    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+  } failure:^(NSError *error) {
+    CDVPluginResult *pluginResult = [CDVPluginResult resultWithStatus:CDVCommandStatus_ERROR messageAsDictionary:@{
+                                                                                                                   @"errorCode": NILABLE([NSNumber numberWithInteger:error.code]),
+                                                                                                                   @"errorMessage": NILABLE(error.localizedDescription)
+                                                                                                                   }];
+    [pluginResult setKeepCallbackAsBool:YES];
+    [self.commandDelegate sendPluginResult:pluginResult callbackId:command.callbackId];
+  }];
+}
+
+@end

+ 308 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/ios/RMStore.h

xqd
@@ -0,0 +1,308 @@
+//
+//  RMStore.h
+//  RMStore
+//
+//  Created by Hermes Pique on 12/6/09.
+//  Copyright (c) 2013 Robot Media SL (http://www.robotmedia.net)
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+//
+
+#import <Foundation/Foundation.h>
+#import <StoreKit/StoreKit.h>
+
+@protocol RMStoreContentDownloader;
+@protocol RMStoreReceiptVerifier;
+@protocol RMStoreTransactionPersistor;
+@protocol RMStoreObserver;
+
+extern NSString *const RMStoreErrorDomain;
+extern NSInteger const RMStoreErrorCodeDownloadCanceled;
+extern NSInteger const RMStoreErrorCodeUnknownProductIdentifier;
+extern NSInteger const RMStoreErrorCodeUnableToCompleteVerification;
+
+/** A StoreKit wrapper that adds blocks and notifications, plus optional receipt verification and purchase management.
+ */
+@interface RMStore : NSObject<SKPaymentTransactionObserver>
+
+///---------------------------------------------
+/// @name Getting the Store
+///---------------------------------------------
+
+/** Returns the singleton store instance.
+ */
++ (RMStore*)defaultStore;
+
+#pragma mark StoreKit Wrapper
+///---------------------------------------------
+/// @name Calling StoreKit
+///---------------------------------------------
+
+/** Returns whether the user is allowed to make payments.
+ */
++ (BOOL)canMakePayments;
+
+/** Request payment of the product with the given product identifier.
+ @param productIdentifier The identifier of the product whose payment will be requested.
+ */
+- (void)addPayment:(NSString*)productIdentifier;
+
+/** Request payment of the product with the given product identifier. `successBlock` will be called if the payment is successful, `failureBlock` if it isn't.
+ @param productIdentifier The identifier of the product whose payment will be requested.
+ @param successBlock The block to be called if the payment is sucessful. Can be `nil`.
+ @param failureBlock The block to be called if the payment fails or there isn't any product with the given identifier. Can be `nil`.
+ */
+- (void)addPayment:(NSString*)productIdentifier
+           success:(void (^)(SKPaymentTransaction *transaction))successBlock
+           failure:(void (^)(SKPaymentTransaction *transaction, NSError *error))failureBlock;
+
+/** Request payment of the product with the given product identifier. `successBlock` will be called if the payment is successful, `failureBlock` if it isn't.
+ @param productIdentifier The identifier of the product whose payment will be requested.
+ @param userIdentifier An opaque identifier of the user’s account, if applicable. Can be `nil`.
+ @param successBlock The block to be called if the payment is sucessful. Can be `nil`.
+ @param failureBlock The block to be called if the payment fails or there isn't any product with the given identifier. Can be `nil`.
+ @see [SKPayment applicationUsername]
+ */
+- (void)addPayment:(NSString*)productIdentifier
+              user:(NSString*)userIdentifier
+           success:(void (^)(SKPaymentTransaction *transaction))successBlock
+           failure:(void (^)(SKPaymentTransaction *transaction, NSError *error))failureBlock __attribute__((availability(ios,introduced=7.0)));
+
+/** Request localized information about a set of products from the Apple App Store.
+ @param identifiers The set of product identifiers for the products you wish to retrieve information of.
+ */
+- (void)requestProducts:(NSSet*)identifiers;
+
+/** Request localized information about a set of products from the Apple App Store. `successBlock` will be called if the products request is successful, `failureBlock` if it isn't.
+ @param identifiers The set of product identifiers for the products you wish to retrieve information of.
+ @param successBlock The block to be called if the products request is sucessful. Can be `nil`. It takes two parameters: `products`, an array of SKProducts, one product for each valid product identifier provided in the original request, and `invalidProductIdentifiers`, an array of product identifiers that were not recognized by the App Store.
+ @param failureBlock The block to be called if the products request fails. Can be `nil`.
+ */
+- (void)requestProducts:(NSSet*)identifiers
+                success:(void (^)(NSArray *products, NSArray *invalidProductIdentifiers))successBlock
+                failure:(void (^)(NSError *error))failureBlock;
+
+/** Request to restore previously completed purchases.
+ */
+- (void)restoreTransactions;
+
+/** Request to restore previously completed purchases. `successBlock` will be called if the restore transactions request is successful, `failureBlock` if it isn't.
+ @param successBlock The block to be called if the restore transactions request is sucessful. Can be `nil`.
+ @param failureBlock The block to be called if the restore transactions request fails. Can be `nil`.
+ */
+- (void)restoreTransactionsOnSuccess:(void (^)(NSArray *transactions))successBlock
+                             failure:(void (^)(NSError *error))failureBlock;
+
+
+/** Request to restore previously completed purchases of a certain user. `successBlock` will be called if the restore transactions request is successful, `failureBlock` if it isn't.
+ @param userIdentifier An opaque identifier of the user’s account.
+ @param successBlock The block to be called if the restore transactions request is sucessful. Can be `nil`.
+ @param failureBlock The block to be called if the restore transactions request fails. Can be `nil`.
+ */
+- (void)restoreTransactionsOfUser:(NSString*)userIdentifier
+                        onSuccess:(void (^)(NSArray *transactions))successBlock
+                          failure:(void (^)(NSError *error))failureBlock __attribute__((availability(ios,introduced=7.0)));
+
+#pragma mark Receipt
+///---------------------------------------------
+/// @name Getting the receipt
+///---------------------------------------------
+
+/** Returns the url of the bundle’s App Store receipt, or nil if the receipt is missing.
+ If this method returns `nil` you should refresh the receipt by calling `refreshReceipt`.
+ @see refreshReceipt
+ */
++ (NSURL*)receiptURL __attribute__((availability(ios,introduced=7.0)));
+
+/** Request to refresh the App Store receipt in case the receipt is invalid or missing.
+ */
+- (void)refreshReceipt __attribute__((availability(ios,introduced=7.0)));
+
+/** Request to refresh the App Store receipt in case the receipt is invalid or missing. `successBlock` will be called if the refresh receipt request is successful, `failureBlock` if it isn't.
+ @param successBlock The block to be called if the refresh receipt request is sucessful. Can be `nil`.
+ @param failureBlock The block to be called if the refresh receipt request fails. Can be `nil`.
+ */
+- (void)refreshReceiptOnSuccess:(void (^)())successBlock
+                        failure:(void (^)(NSError *error))failureBlock __attribute__((availability(ios,introduced=7.0)));
+
+///---------------------------------------------
+/// @name Setting Delegates
+///---------------------------------------------
+
+/**
+ The content downloader. Required to download product content from your own server.
+ @discussion Hosted content from Apple’s server (SKDownload) is handled automatically. You don't need to provide a content downloader for it.
+ */
+@property (nonatomic, weak) id<RMStoreContentDownloader> contentDownloader;
+
+/** The receipt verifier. You can provide your own or use one of the reference implementations provided by the library.
+ @see RMStoreAppReceiptVerifier
+ @see RMStoreTransactionReceiptVerifier
+ */
+@property (nonatomic, weak) id<RMStoreReceiptVerifier> receiptVerifier;
+
+/**
+ The transaction persistor. It is recommended to provide your own obfuscator if piracy is a concern. The store will use weak obfuscation via `NSKeyedArchiver` by default.
+ @see RMStoreKeychainPersistence
+ @see RMStoreUserDefaultsPersistence
+ */
+@property (nonatomic, weak) id<RMStoreTransactionPersistor> transactionPersistor;
+
+
+#pragma mark Product management
+///---------------------------------------------
+/// @name Managing Products
+///---------------------------------------------
+
+- (SKProduct*)productForIdentifier:(NSString*)productIdentifier;
+
++ (NSString*)localizedPriceOfProduct:(SKProduct*)product;
+
+#pragma mark Notifications
+///---------------------------------------------
+/// @name Managing Observers
+///---------------------------------------------
+
+/** Adds an observer to the store.
+ Unlike `SKPaymentQueue`, it is not necessary to set an observer.
+ @param observer The observer to add.
+ */
+- (void)addStoreObserver:(id<RMStoreObserver>)observer;
+
+/** Removes an observer from the store.
+ @param observer The observer to remove.
+ */
+- (void)removeStoreObserver:(id<RMStoreObserver>)observer;
+
+@end
+
+@protocol RMStoreContentDownloader <NSObject>
+
+/**
+ Downloads the self-hosted content associated to the given transaction and calls the given success or failure block accordingly. Can also call the given progress block to notify progress.
+ @param transaction The transaction whose associated content will be downloaded.
+ @param successBlock Called if the download was successful. Must be called in the main queue.
+ @param progressBlock Called to notify progress. Provides a number between 0.0 and 1.0, inclusive, where 0.0 means no data has been downloaded and 1.0 means all the data has been downloaded. Must be called in the main queue.
+ @param failureBlock Called if the download failed. Must be called in the main queue.
+ @discussion Hosted content from Apple’s server (@c SKDownload) is handled automatically by RMStore.
+ */
+- (void)downloadContentForTransaction:(SKPaymentTransaction*)transaction
+                              success:(void (^)())successBlock
+                             progress:(void (^)(float progress))progressBlock
+                              failure:(void (^)(NSError *error))failureBlock;
+
+@end
+
+@protocol RMStoreTransactionPersistor<NSObject>
+
+- (void)persistTransaction:(SKPaymentTransaction*)transaction;
+
+@end
+
+@protocol RMStoreReceiptVerifier <NSObject>
+
+/** Verifies the given transaction and calls the given success or failure block accordingly.
+ @param transaction The transaction to be verified.
+ @param successBlock Called if the transaction passed verification. Must be called in the main queu.
+ @param failureBlock Called if the transaction failed verification. If verification could not be completed (e.g., due to connection issues), then error must be of code RMStoreErrorCodeUnableToCompleteVerification to prevent RMStore to finish the transaction. Must be called in the main queu.
+ */
+- (void)verifyTransaction:(SKPaymentTransaction*)transaction
+                  success:(void (^)())successBlock
+                  failure:(void (^)(NSError *error))failureBlock;
+
+@end
+
+@protocol RMStoreObserver<NSObject>
+@optional
+
+/**
+ Tells the observer that a download has been canceled.
+ @discussion Only for Apple-hosted downloads.
+ */
+- (void)storeDownloadCanceled:(NSNotification*)notification __attribute__((availability(ios,introduced=6.0)));
+
+/**
+ Tells the observer that a download has failed. Use @c storeError to get the cause.
+ */
+- (void)storeDownloadFailed:(NSNotification*)notification __attribute__((availability(ios,introduced=6.0)));
+
+/**
+ Tells the observer that a download has finished.
+ */
+- (void)storeDownloadFinished:(NSNotification*)notification __attribute__((availability(ios,introduced=6.0)));
+
+/**
+ Tells the observer that a download has been paused.
+ @discussion Only for Apple-hosted downloads.
+ */
+- (void)storeDownloadPaused:(NSNotification*)notification __attribute__((availability(ios,introduced=6.0)));
+
+/**
+ Tells the observer that a download has been updated. Use @c downloadProgress to get the progress.
+ */
+- (void)storeDownloadUpdated:(NSNotification*)notification __attribute__((availability(ios,introduced=6.0)));
+
+- (void)storePaymentTransactionDeferred:(NSNotification*)notification __attribute__((availability(ios,introduced=8.0)));
+- (void)storePaymentTransactionFailed:(NSNotification*)notification;
+- (void)storePaymentTransactionFinished:(NSNotification*)notification;
+- (void)storeProductsRequestFailed:(NSNotification*)notification;
+- (void)storeProductsRequestFinished:(NSNotification*)notification;
+- (void)storeRefreshReceiptFailed:(NSNotification*)notification __attribute__((availability(ios,introduced=7.0)));
+- (void)storeRefreshReceiptFinished:(NSNotification*)notification __attribute__((availability(ios,introduced=7.0)));
+- (void)storeRestoreTransactionsFailed:(NSNotification*)notification;
+- (void)storeRestoreTransactionsFinished:(NSNotification*)notification;
+
+@end
+
+/**
+ Category on NSNotification to recover store data from userInfo without requiring to know the keys.
+ */
+@interface NSNotification(RMStore)
+
+/**
+ A value that indicates how much of the file has been downloaded.
+ The value of this property is a floating point number between 0.0 and 1.0, inclusive, where 0.0 means no data has been downloaded and 1.0 means all the data has been downloaded. Typically, your app uses the value of this property to update a user interface element, such as a progress bar, that displays how much of the file has been downloaded.
+ @discussion Corresponds to [SKDownload progress].
+ @discussion Used in @c storeDownloadUpdated:.
+ */
+@property (nonatomic, readonly) float rm_downloadProgress;
+
+/** Array of product identifiers that were not recognized by the App Store. Used in @c storeProductsRequestFinished:.
+ */
+@property (nonatomic, readonly) NSArray *rm_invalidProductIdentifiers;
+
+/** Used in @c storeDownload*:, @c storePaymentTransactionFinished: and @c storePaymentTransactionFailed:.
+ */
+@property (nonatomic, readonly) NSString *rm_productIdentifier;
+
+/** Array of SKProducts, one product for each valid product identifier provided in the corresponding request. Used in @c storeProductsRequestFinished:.
+ */
+@property (nonatomic, readonly) NSArray *rm_products;
+
+/** Used in @c storeDownload*:.
+ */
+@property (nonatomic, readonly) SKDownload *rm_storeDownload __attribute__((availability(ios,introduced=6.0)));
+
+/** Used in @c storeDownloadFailed:, @c storePaymentTransactionFailed:, @c storeProductsRequestFailed:, @c storeRefreshReceiptFailed: and @c storeRestoreTransactionsFailed:.
+ */
+@property (nonatomic, readonly) NSError *rm_storeError;
+
+/** Used in @c storeDownload*:, @c storePaymentTransactionFinished: and in @c storePaymentTransactionFailed:.
+ */
+@property (nonatomic, readonly) SKPaymentTransaction *rm_transaction;
+
+/** Used in @c storeRestoreTransactionsFinished:.
+ */
+@property (nonatomic, readonly) NSArray *rm_transactions;
+
+@end

+ 791 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/ios/RMStore.m

xqd
@@ -0,0 +1,791 @@
+//
+//  RMStore.h
+//  RMStore
+//
+//  Created by Hermes Pique on 12/6/09.
+//  Copyright (c) 2013 Robot Media SL (http://www.robotmedia.net)
+//
+//  Licensed under the Apache License, Version 2.0 (the "License");
+//  you may not use this file except in compliance with the License.
+//  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+//  Unless required by applicable law or agreed to in writing, software
+//  distributed under the License is distributed on an "AS IS" BASIS,
+//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+//  See the License for the specific language governing permissions and
+//  limitations under the License.
+//
+
+#import "RMStore.h"
+
+NSString *const RMStoreErrorDomain = @"net.robotmedia.store";
+NSInteger const RMStoreErrorCodeDownloadCanceled = 300;
+NSInteger const RMStoreErrorCodeUnknownProductIdentifier = 100;
+NSInteger const RMStoreErrorCodeUnableToCompleteVerification = 200;
+
+NSString* const RMSKDownloadCanceled = @"RMSKDownloadCanceled";
+NSString* const RMSKDownloadFailed = @"RMSKDownloadFailed";
+NSString* const RMSKDownloadFinished = @"RMSKDownloadFinished";
+NSString* const RMSKDownloadPaused = @"RMSKDownloadPaused";
+NSString* const RMSKDownloadUpdated = @"RMSKDownloadUpdated";
+NSString* const RMSKPaymentTransactionDeferred = @"RMSKPaymentTransactionDeferred";
+NSString* const RMSKPaymentTransactionFailed = @"RMSKPaymentTransactionFailed";
+NSString* const RMSKPaymentTransactionFinished = @"RMSKPaymentTransactionFinished";
+NSString* const RMSKProductsRequestFailed = @"RMSKProductsRequestFailed";
+NSString* const RMSKProductsRequestFinished = @"RMSKProductsRequestFinished";
+NSString* const RMSKRefreshReceiptFailed = @"RMSKRefreshReceiptFailed";
+NSString* const RMSKRefreshReceiptFinished = @"RMSKRefreshReceiptFinished";
+NSString* const RMSKRestoreTransactionsFailed = @"RMSKRestoreTransactionsFailed";
+NSString* const RMSKRestoreTransactionsFinished = @"RMSKRestoreTransactionsFinished";
+
+NSString* const RMStoreNotificationInvalidProductIdentifiers = @"invalidProductIdentifiers";
+NSString* const RMStoreNotificationDownloadProgress = @"downloadProgress";
+NSString* const RMStoreNotificationProductIdentifier = @"productIdentifier";
+NSString* const RMStoreNotificationProducts = @"products";
+NSString* const RMStoreNotificationStoreDownload = @"storeDownload";
+NSString* const RMStoreNotificationStoreError = @"storeError";
+NSString* const RMStoreNotificationStoreReceipt = @"storeReceipt";
+NSString* const RMStoreNotificationTransaction = @"transaction";
+NSString* const RMStoreNotificationTransactions = @"transactions";
+
+#if DEBUG
+#define RMStoreLog(...) NSLog(@"RMStore: %@", [NSString stringWithFormat:__VA_ARGS__]);
+#else
+#define RMStoreLog(...)
+#endif
+
+typedef void (^RMSKPaymentTransactionFailureBlock)(SKPaymentTransaction *transaction, NSError *error);
+typedef void (^RMSKPaymentTransactionSuccessBlock)(SKPaymentTransaction *transaction);
+typedef void (^RMSKProductsRequestFailureBlock)(NSError *error);
+typedef void (^RMSKProductsRequestSuccessBlock)(NSArray *products, NSArray *invalidIdentifiers);
+typedef void (^RMStoreFailureBlock)(NSError *error);
+typedef void (^RMStoreSuccessBlock)();
+
+@implementation NSNotification(RMStore)
+
+- (float)rm_downloadProgress
+{
+    return [self.userInfo[RMStoreNotificationDownloadProgress] floatValue];
+}
+
+- (NSArray*)rm_invalidProductIdentifiers
+{
+    return (self.userInfo)[RMStoreNotificationInvalidProductIdentifiers];
+}
+
+- (NSString*)rm_productIdentifier
+{
+    return (self.userInfo)[RMStoreNotificationProductIdentifier];
+}
+
+- (NSArray*)rm_products
+{
+    return (self.userInfo)[RMStoreNotificationProducts];
+}
+
+- (SKDownload*)rm_storeDownload
+{
+    return (self.userInfo)[RMStoreNotificationStoreDownload];
+}
+
+- (NSError*)rm_storeError
+{
+    return (self.userInfo)[RMStoreNotificationStoreError];
+}
+
+- (SKPaymentTransaction*)rm_transaction
+{
+    return (self.userInfo)[RMStoreNotificationTransaction];
+}
+
+- (NSArray*)rm_transactions {
+    return (self.userInfo)[RMStoreNotificationTransactions];
+}
+
+@end
+
+@interface RMProductsRequestDelegate : NSObject<SKProductsRequestDelegate>
+
+@property (nonatomic, strong) RMSKProductsRequestSuccessBlock successBlock;
+@property (nonatomic, strong) RMSKProductsRequestFailureBlock failureBlock;
+@property (nonatomic, weak) RMStore *store;
+
+@end
+
+@interface RMAddPaymentParameters : NSObject
+
+@property (nonatomic, strong) RMSKPaymentTransactionSuccessBlock successBlock;
+@property (nonatomic, strong) RMSKPaymentTransactionFailureBlock failureBlock;
+
+@end
+
+@implementation RMAddPaymentParameters
+
+@end
+
+@interface RMStore() <SKRequestDelegate>
+
+@end
+
+@implementation RMStore {
+    NSMutableDictionary *_addPaymentParameters; // HACK: We use a dictionary of product identifiers because the returned SKPayment is different from the one we add to the queue. Bad Apple.
+    NSMutableDictionary *_products;
+    NSMutableSet *_productsRequestDelegates;
+    
+    NSMutableArray *_restoredTransactions;
+    
+    NSInteger _pendingRestoredTransactionsCount;
+    BOOL _restoredCompletedTransactionsFinished;
+    
+    SKReceiptRefreshRequest *_refreshReceiptRequest;
+    void (^_refreshReceiptFailureBlock)(NSError* error);
+    void (^_refreshReceiptSuccessBlock)();
+    
+    void (^_restoreTransactionsFailureBlock)(NSError* error);
+    void (^_restoreTransactionsSuccessBlock)(NSArray* transactions);
+}
+
+- (instancetype) init
+{
+    if (self = [super init])
+    {
+        _addPaymentParameters = [NSMutableDictionary dictionary];
+        _products = [NSMutableDictionary dictionary];
+        _productsRequestDelegates = [NSMutableSet set];
+        _restoredTransactions = [NSMutableArray array];
+        [[SKPaymentQueue defaultQueue] addTransactionObserver:self];
+    }
+    return self;
+}
+
+- (void)dealloc
+{
+    [[SKPaymentQueue defaultQueue] removeTransactionObserver:self];
+}
+
++ (RMStore *)defaultStore
+{
+    static RMStore *sharedInstance = nil;
+    static dispatch_once_t onceToken;
+    dispatch_once(&onceToken, ^{
+        sharedInstance = [[[self class] alloc] init];
+    });
+    return sharedInstance;
+}
+
+#pragma mark StoreKit wrapper
+
++ (BOOL)canMakePayments
+{
+    return [SKPaymentQueue canMakePayments];
+}
+
+- (void)addPayment:(NSString*)productIdentifier
+{
+    [self addPayment:productIdentifier success:nil failure:nil];
+}
+
+- (void)addPayment:(NSString*)productIdentifier
+           success:(void (^)(SKPaymentTransaction *transaction))successBlock
+           failure:(void (^)(SKPaymentTransaction *transaction, NSError *error))failureBlock
+{
+    [self addPayment:productIdentifier user:nil success:successBlock failure:failureBlock];
+}
+
+- (void)addPayment:(NSString*)productIdentifier
+              user:(NSString*)userIdentifier
+           success:(void (^)(SKPaymentTransaction *transaction))successBlock
+           failure:(void (^)(SKPaymentTransaction *transaction, NSError *error))failureBlock
+{
+    SKProduct *product = [self productForIdentifier:productIdentifier];
+    if (product == nil)
+    {
+        RMStoreLog(@"unknown product id %@", productIdentifier)
+        if (failureBlock != nil)
+        {
+            NSError *error = [NSError errorWithDomain:RMStoreErrorDomain code:RMStoreErrorCodeUnknownProductIdentifier userInfo:@{NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"Unknown product identifier", @"RMStore", @"Error description")}];
+            failureBlock(nil, error);
+        }
+        return;
+    }
+    SKMutablePayment *payment = [SKMutablePayment paymentWithProduct:product];
+    if ([payment respondsToSelector:@selector(setApplicationUsername:)])
+    {
+        payment.applicationUsername = userIdentifier;
+    }
+    
+    RMAddPaymentParameters *parameters = [[RMAddPaymentParameters alloc] init];
+    parameters.successBlock = successBlock;
+    parameters.failureBlock = failureBlock;
+    _addPaymentParameters[productIdentifier] = parameters;
+    
+    [[SKPaymentQueue defaultQueue] addPayment:payment];
+}
+
+- (void)requestProducts:(NSSet*)identifiers
+{
+    [self requestProducts:identifiers success:nil failure:nil];
+}
+
+- (void)requestProducts:(NSSet*)identifiers
+                success:(RMSKProductsRequestSuccessBlock)successBlock
+                failure:(RMSKProductsRequestFailureBlock)failureBlock
+{
+    RMProductsRequestDelegate *delegate = [[RMProductsRequestDelegate alloc] init];
+    delegate.store = self;
+    delegate.successBlock = successBlock;
+    delegate.failureBlock = failureBlock;
+    [_productsRequestDelegates addObject:delegate];
+ 
+    SKProductsRequest *productsRequest = [[SKProductsRequest alloc] initWithProductIdentifiers:identifiers];
+	productsRequest.delegate = delegate;
+    
+    [productsRequest start];
+}
+
+- (void)restoreTransactions
+{
+    [self restoreTransactionsOnSuccess:nil failure:nil];
+}
+
+- (void)restoreTransactionsOnSuccess:(void (^)(NSArray *transactions))successBlock
+                             failure:(void (^)(NSError *error))failureBlock
+{
+    _restoredCompletedTransactionsFinished = NO;
+    _pendingRestoredTransactionsCount = 0;
+    _restoredTransactions = [NSMutableArray array];
+    _restoreTransactionsSuccessBlock = successBlock;
+    _restoreTransactionsFailureBlock = failureBlock;
+    [[SKPaymentQueue defaultQueue] restoreCompletedTransactions];
+}
+
+- (void)restoreTransactionsOfUser:(NSString*)userIdentifier
+                        onSuccess:(void (^)(NSArray *transactions))successBlock
+                          failure:(void (^)(NSError *error))failureBlock
+{
+    NSAssert([[SKPaymentQueue defaultQueue] respondsToSelector:@selector(restoreCompletedTransactionsWithApplicationUsername:)], @"restoreCompletedTransactionsWithApplicationUsername: not supported in this iOS version. Use restoreTransactionsOnSuccess:failure: instead.");
+    _restoredCompletedTransactionsFinished = NO;
+    _pendingRestoredTransactionsCount = 0;
+    _restoreTransactionsSuccessBlock = successBlock;
+    _restoreTransactionsFailureBlock = failureBlock;
+    [[SKPaymentQueue defaultQueue] restoreCompletedTransactionsWithApplicationUsername:userIdentifier];
+}
+
+#pragma mark Receipt
+
++ (NSURL*)receiptURL
+{
+    // The general best practice of weak linking using the respondsToSelector: method cannot be used here. Prior to iOS 7, the method was implemented as private API, but that implementation called the doesNotRecognizeSelector: method.
+    NSAssert(floor(NSFoundationVersionNumber) > NSFoundationVersionNumber_iOS_6_1, @"appStoreReceiptURL not supported in this iOS version.");
+    NSURL *url = [NSBundle mainBundle].appStoreReceiptURL;
+    return url;
+}
+
+- (void)refreshReceipt
+{
+    [self refreshReceiptOnSuccess:nil failure:nil];
+}
+
+- (void)refreshReceiptOnSuccess:(RMStoreSuccessBlock)successBlock
+                        failure:(RMStoreFailureBlock)failureBlock
+{
+    _refreshReceiptFailureBlock = failureBlock;
+    _refreshReceiptSuccessBlock = successBlock;
+    _refreshReceiptRequest = [[SKReceiptRefreshRequest alloc] initWithReceiptProperties:@{}];
+    _refreshReceiptRequest.delegate = self;
+    [_refreshReceiptRequest start];
+}
+
+#pragma mark Product management
+
+- (SKProduct*)productForIdentifier:(NSString*)productIdentifier
+{
+    return _products[productIdentifier];
+}
+
++ (NSString*)localizedPriceOfProduct:(SKProduct*)product
+{
+	NSNumberFormatter *numberFormatter = [[NSNumberFormatter alloc] init];
+	numberFormatter.numberStyle = NSNumberFormatterCurrencyStyle;
+	numberFormatter.locale = product.priceLocale;
+	NSString *formattedString = [numberFormatter stringFromNumber:product.price];
+	return formattedString;
+}
+
+#pragma mark Observers
+
+- (void)addStoreObserver:(id<RMStoreObserver>)observer
+{
+    [self addStoreObserver:observer selector:@selector(storeDownloadCanceled:) notificationName:RMSKDownloadCanceled];
+    [self addStoreObserver:observer selector:@selector(storeDownloadFailed:) notificationName:RMSKDownloadFailed];
+    [self addStoreObserver:observer selector:@selector(storeDownloadFinished:) notificationName:RMSKDownloadFinished];
+    [self addStoreObserver:observer selector:@selector(storeDownloadPaused:) notificationName:RMSKDownloadPaused];
+    [self addStoreObserver:observer selector:@selector(storeDownloadUpdated:) notificationName:RMSKDownloadUpdated];
+    [self addStoreObserver:observer selector:@selector(storeProductsRequestFailed:) notificationName:RMSKProductsRequestFailed];
+    [self addStoreObserver:observer selector:@selector(storeProductsRequestFinished:) notificationName:RMSKProductsRequestFinished];
+    [self addStoreObserver:observer selector:@selector(storePaymentTransactionDeferred:) notificationName:RMSKPaymentTransactionDeferred];
+    [self addStoreObserver:observer selector:@selector(storePaymentTransactionFailed:) notificationName:RMSKPaymentTransactionFailed];
+    [self addStoreObserver:observer selector:@selector(storePaymentTransactionFinished:) notificationName:RMSKPaymentTransactionFinished];
+    [self addStoreObserver:observer selector:@selector(storeRefreshReceiptFailed:) notificationName:RMSKRefreshReceiptFailed];
+    [self addStoreObserver:observer selector:@selector(storeRefreshReceiptFinished:) notificationName:RMSKRefreshReceiptFinished];
+    [self addStoreObserver:observer selector:@selector(storeRestoreTransactionsFailed:) notificationName:RMSKRestoreTransactionsFailed];
+    [self addStoreObserver:observer selector:@selector(storeRestoreTransactionsFinished:) notificationName:RMSKRestoreTransactionsFinished];
+}
+
+- (void)removeStoreObserver:(id<RMStoreObserver>)observer
+{
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKDownloadCanceled object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKDownloadFailed object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKDownloadFinished object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKDownloadPaused object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKDownloadUpdated object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKProductsRequestFailed object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKProductsRequestFinished object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKPaymentTransactionDeferred object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKPaymentTransactionFailed object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKPaymentTransactionFinished object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKRefreshReceiptFailed object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKRefreshReceiptFinished object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKRestoreTransactionsFailed object:self];
+    [[NSNotificationCenter defaultCenter] removeObserver:observer name:RMSKRestoreTransactionsFinished object:self];
+}
+
+// Private
+
+- (void)addStoreObserver:(id<RMStoreObserver>)observer selector:(SEL)aSelector notificationName:(NSString*)notificationName
+{
+    if ([observer respondsToSelector:aSelector])
+    {
+        [[NSNotificationCenter defaultCenter] addObserver:observer selector:aSelector name:notificationName object:self];
+    }
+}
+
+#pragma mark SKPaymentTransactionObserver
+
+- (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions
+{
+    for (SKPaymentTransaction *transaction in transactions)
+    {
+        switch (transaction.transactionState)
+        {
+            case SKPaymentTransactionStatePurchased:
+                [self didPurchaseTransaction:transaction queue:queue];
+                break;
+            case SKPaymentTransactionStateFailed:
+                [self didFailTransaction:transaction queue:queue error:transaction.error];
+                break;
+            case SKPaymentTransactionStateRestored:
+                [self didRestoreTransaction:transaction queue:queue];
+                break;
+            case SKPaymentTransactionStateDeferred:
+                [self didDeferTransaction:transaction];
+                break;
+            default:
+                break;
+        }
+    }
+}
+
+- (void)paymentQueueRestoreCompletedTransactionsFinished:(SKPaymentQueue *)queue
+{
+    RMStoreLog(@"restore transactions finished");
+    _restoredCompletedTransactionsFinished = YES;
+    
+    [self notifyRestoreTransactionFinishedIfApplicableAfterTransaction:nil];
+}
+
+- (void)paymentQueue:(SKPaymentQueue *)queue restoreCompletedTransactionsFailedWithError:(NSError *)error
+{
+    RMStoreLog(@"restored transactions failed with error %@", error.debugDescription);
+    if (_restoreTransactionsFailureBlock != nil)
+    {
+        _restoreTransactionsFailureBlock(error);
+        _restoreTransactionsFailureBlock = nil;
+    }
+    NSDictionary *userInfo = nil;
+    if (error)
+    { // error might be nil (e.g., on airplane mode)
+        userInfo = @{RMStoreNotificationStoreError: error};
+    }
+    [[NSNotificationCenter defaultCenter] postNotificationName:RMSKRestoreTransactionsFailed object:self userInfo:userInfo];
+}
+
+- (void)paymentQueue:(SKPaymentQueue *)queue updatedDownloads:(NSArray *)downloads
+{
+    for (SKDownload *download in downloads)
+    {
+        switch (download.downloadState)
+        {
+            case SKDownloadStateActive:
+                [self didUpdateDownload:download queue:queue];
+                break;
+            case SKDownloadStateCancelled:
+                [self didCancelDownload:download queue:queue];
+                break;
+            case SKDownloadStateFailed:
+                [self didFailDownload:download queue:queue];
+                break;
+            case SKDownloadStateFinished:
+                [self didFinishDownload:download queue:queue];
+                break;
+            case SKDownloadStatePaused:
+                [self didPauseDownload:download queue:queue];
+                break;
+            case SKDownloadStateWaiting:
+                // Do nothing
+                break;
+        }
+    }
+}
+
+#pragma mark Download State
+
+- (void)didCancelDownload:(SKDownload*)download queue:(SKPaymentQueue*)queue
+{
+    SKPaymentTransaction *transaction = download.transaction;
+    RMStoreLog(@"download %@ for product %@ canceled", download.contentIdentifier, download.transaction.payment.productIdentifier);
+
+    [self postNotificationWithName:RMSKDownloadCanceled download:download userInfoExtras:nil];
+
+    NSError *error = [NSError errorWithDomain:RMStoreErrorDomain code:RMStoreErrorCodeDownloadCanceled userInfo:@{NSLocalizedDescriptionKey: NSLocalizedStringFromTable(@"Download canceled", @"RMStore", @"Error description")}];
+
+    const BOOL hasPendingDownloads = [self.class hasPendingDownloadsInTransaction:transaction];
+    if (!hasPendingDownloads)
+    {
+        [self didFailTransaction:transaction queue:queue error:error];
+    }
+}
+
+- (void)didFailDownload:(SKDownload*)download queue:(SKPaymentQueue*)queue
+{
+    NSError *error = download.error;
+    SKPaymentTransaction *transaction = download.transaction;
+    RMStoreLog(@"download %@ for product %@ failed with error %@", download.contentIdentifier, transaction.payment.productIdentifier, error.debugDescription);
+
+    NSDictionary *extras = error ? @{RMStoreNotificationStoreError : error} : nil;
+    [self postNotificationWithName:RMSKDownloadFailed download:download userInfoExtras:extras];
+
+    const BOOL hasPendingDownloads = [self.class hasPendingDownloadsInTransaction:transaction];
+    if (!hasPendingDownloads)
+    {
+        [self didFailTransaction:transaction queue:queue error:error];
+    }
+}
+
+- (void)didFinishDownload:(SKDownload*)download queue:(SKPaymentQueue*)queue
+{
+    SKPaymentTransaction *transaction = download.transaction;
+    RMStoreLog(@"download %@ for product %@ finished", download.contentIdentifier, transaction.payment.productIdentifier);
+    
+    [self postNotificationWithName:RMSKDownloadFinished download:download userInfoExtras:nil];
+
+    const BOOL hasPendingDownloads = [self.class hasPendingDownloadsInTransaction:transaction];
+    if (!hasPendingDownloads)
+    {
+        [self finishTransaction:download.transaction queue:queue];
+    }
+}
+
+- (void)didPauseDownload:(SKDownload*)download queue:(SKPaymentQueue*)queue
+{
+    RMStoreLog(@"download %@ for product %@ paused", download.contentIdentifier, download.transaction.payment.productIdentifier);
+    [self postNotificationWithName:RMSKDownloadPaused download:download userInfoExtras:nil];
+}
+
+- (void)didUpdateDownload:(SKDownload*)download queue:(SKPaymentQueue*)queue
+{
+    RMStoreLog(@"download %@ for product %@ updated", download.contentIdentifier, download.transaction.payment.productIdentifier);
+    NSDictionary *extras = @{RMStoreNotificationDownloadProgress : @(download.progress)};
+    [self postNotificationWithName:RMSKDownloadUpdated download:download userInfoExtras:extras];
+}
+
++ (BOOL)hasPendingDownloadsInTransaction:(SKPaymentTransaction*)transaction
+{
+    for (SKDownload *download in transaction.downloads)
+    {
+        switch (download.downloadState)
+        {
+            case SKDownloadStateActive:
+            case SKDownloadStatePaused:
+            case SKDownloadStateWaiting:
+                return YES;
+            case SKDownloadStateCancelled:
+            case SKDownloadStateFailed:
+            case SKDownloadStateFinished:
+                continue;
+        }
+    }
+    return NO;
+}
+
+#pragma mark Transaction State
+
+- (void)didPurchaseTransaction:(SKPaymentTransaction *)transaction queue:(SKPaymentQueue*)queue
+{
+    RMStoreLog(@"transaction purchased with product %@", transaction.payment.productIdentifier);
+    
+    if (self.receiptVerifier != nil)
+    {
+        [self.receiptVerifier verifyTransaction:transaction success:^{
+            [self didVerifyTransaction:transaction queue:queue];
+        } failure:^(NSError *error) {
+            [self didFailTransaction:transaction queue:queue error:error];
+        }];
+    }
+    else
+    {
+        RMStoreLog(@"WARNING: no receipt verification");
+        [self didVerifyTransaction:transaction queue:queue];
+    }
+}
+
+- (void)didFailTransaction:(SKPaymentTransaction *)transaction queue:(SKPaymentQueue*)queue error:(NSError*)error
+{
+    SKPayment *payment = transaction.payment;
+	NSString* productIdentifier = payment.productIdentifier;
+    RMStoreLog(@"transaction failed with product %@ and error %@", productIdentifier, error.debugDescription);
+    
+    if (error.code != RMStoreErrorCodeUnableToCompleteVerification)
+    { // If we were unable to complete the verification we want StoreKit to keep reminding us of the transaction
+        [queue finishTransaction:transaction];
+    }
+    
+    RMAddPaymentParameters *parameters = [self popAddPaymentParametersForIdentifier:productIdentifier];
+    if (parameters.failureBlock != nil)
+    {
+        parameters.failureBlock(transaction, error);
+    }
+    
+    NSDictionary *extras = error ? @{RMStoreNotificationStoreError : error} : nil;
+    [self postNotificationWithName:RMSKPaymentTransactionFailed transaction:transaction userInfoExtras:extras];
+    
+    if (transaction.transactionState == SKPaymentTransactionStateRestored)
+    {
+        [self notifyRestoreTransactionFinishedIfApplicableAfterTransaction:transaction];
+    }
+}
+
+- (void)didRestoreTransaction:(SKPaymentTransaction *)transaction queue:(SKPaymentQueue*)queue
+{
+    RMStoreLog(@"transaction restored with product %@", transaction.originalTransaction.payment.productIdentifier);
+    
+    _pendingRestoredTransactionsCount++;
+    if (self.receiptVerifier != nil)
+    {
+        [self.receiptVerifier verifyTransaction:transaction success:^{
+            [self didVerifyTransaction:transaction queue:queue];
+        } failure:^(NSError *error) {
+            [self didFailTransaction:transaction queue:queue error:error];
+        }];
+    }
+    else
+    {
+        RMStoreLog(@"WARNING: no receipt verification");
+        [self didVerifyTransaction:transaction queue:queue];
+    }
+}
+
+- (void)didDeferTransaction:(SKPaymentTransaction *)transaction
+{
+    [self postNotificationWithName:RMSKPaymentTransactionDeferred transaction:transaction userInfoExtras:nil];
+}
+
+- (void)didVerifyTransaction:(SKPaymentTransaction *)transaction queue:(SKPaymentQueue*)queue
+{
+    if (self.contentDownloader != nil)
+    {
+        [self.contentDownloader downloadContentForTransaction:transaction success:^{
+            [self postNotificationWithName:RMSKDownloadFinished transaction:transaction userInfoExtras:nil];
+            [self didDownloadSelfHostedContentForTransaction:transaction queue:queue];
+        } progress:^(float progress) {
+            NSDictionary *extras = @{RMStoreNotificationDownloadProgress : @(progress)};
+            [self postNotificationWithName:RMSKDownloadUpdated transaction:transaction userInfoExtras:extras];
+        } failure:^(NSError *error) {
+            NSDictionary *extras = error ? @{RMStoreNotificationStoreError : error} : nil;
+            [self postNotificationWithName:RMSKDownloadFailed transaction:transaction userInfoExtras:extras];
+            [self didFailTransaction:transaction queue:queue error:error];
+        }];
+    }
+    else
+    {
+        [self didDownloadSelfHostedContentForTransaction:transaction queue:queue];
+    }
+}
+
+- (void)didDownloadSelfHostedContentForTransaction:(SKPaymentTransaction *)transaction queue:(SKPaymentQueue*)queue
+{
+    NSArray *downloads = [transaction respondsToSelector:@selector(downloads)] ? transaction.downloads : @[];
+    if (downloads.count > 0)
+    {
+        RMStoreLog(@"starting downloads for product %@ started", transaction.payment.productIdentifier);
+        [queue startDownloads:downloads];
+    }
+    else
+    {
+        [self finishTransaction:transaction queue:queue];
+    }
+}
+
+- (void)finishTransaction:(SKPaymentTransaction *)transaction queue:(SKPaymentQueue*)queue
+{
+    SKPayment *payment = transaction.payment;
+	NSString* productIdentifier = payment.productIdentifier;
+    [queue finishTransaction:transaction];
+    [self.transactionPersistor persistTransaction:transaction];
+    
+    RMAddPaymentParameters *wrapper = [self popAddPaymentParametersForIdentifier:productIdentifier];
+    if (wrapper.successBlock != nil)
+    {
+        wrapper.successBlock(transaction);
+    }
+    
+    [self postNotificationWithName:RMSKPaymentTransactionFinished transaction:transaction userInfoExtras:nil];
+    
+    if (transaction.transactionState == SKPaymentTransactionStateRestored)
+    {
+        [self notifyRestoreTransactionFinishedIfApplicableAfterTransaction:transaction];
+    }
+}
+
+- (void)notifyRestoreTransactionFinishedIfApplicableAfterTransaction:(SKPaymentTransaction*)transaction
+{
+    if (transaction != nil)
+    {
+        [_restoredTransactions addObject:transaction];
+        _pendingRestoredTransactionsCount--;
+    }
+    if (_restoredCompletedTransactionsFinished && _pendingRestoredTransactionsCount == 0)
+    { // Wait until all restored transations have been verified
+        NSArray *restoredTransactions = [_restoredTransactions copy];
+        if (_restoreTransactionsSuccessBlock != nil)
+        {
+            _restoreTransactionsSuccessBlock(restoredTransactions);
+            _restoreTransactionsSuccessBlock = nil;
+        }
+        NSDictionary *userInfo = @{ RMStoreNotificationTransactions : restoredTransactions };
+        [[NSNotificationCenter defaultCenter] postNotificationName:RMSKRestoreTransactionsFinished object:self userInfo:userInfo];
+    }
+}
+
+- (RMAddPaymentParameters*)popAddPaymentParametersForIdentifier:(NSString*)identifier
+{
+    RMAddPaymentParameters *parameters = _addPaymentParameters[identifier];
+    [_addPaymentParameters removeObjectForKey:identifier];
+    return parameters;
+}
+
+#pragma mark SKRequestDelegate
+
+- (void)requestDidFinish:(SKRequest *)request
+{
+    RMStoreLog(@"refresh receipt finished");
+    _refreshReceiptRequest = nil;
+    if (_refreshReceiptSuccessBlock)
+    {
+        _refreshReceiptSuccessBlock();
+        _refreshReceiptSuccessBlock = nil;
+    }
+    [[NSNotificationCenter defaultCenter] postNotificationName:RMSKRefreshReceiptFinished object:self];
+}
+
+- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
+{
+    RMStoreLog(@"refresh receipt failed with error %@", error.debugDescription);
+    _refreshReceiptRequest = nil;
+    if (_refreshReceiptFailureBlock)
+    {
+        _refreshReceiptFailureBlock(error);
+        _refreshReceiptFailureBlock = nil;
+    }
+    NSDictionary *userInfo = nil;
+    if (error)
+    { // error might be nil (e.g., on airplane mode)
+        userInfo = @{RMStoreNotificationStoreError: error};
+    }
+    [[NSNotificationCenter defaultCenter] postNotificationName:RMSKRefreshReceiptFailed object:self userInfo:userInfo];
+}
+
+#pragma mark Private
+
+- (void)addProduct:(SKProduct*)product
+{
+    _products[product.productIdentifier] = product;    
+}
+
+- (void)postNotificationWithName:(NSString*)notificationName download:(SKDownload*)download userInfoExtras:(NSDictionary*)extras
+{
+    NSMutableDictionary *mutableExtras = extras ? [NSMutableDictionary dictionaryWithDictionary:extras] : [NSMutableDictionary dictionary];
+    mutableExtras[RMStoreNotificationStoreDownload] = download;
+    [self postNotificationWithName:notificationName transaction:download.transaction userInfoExtras:mutableExtras];
+}
+
+- (void)postNotificationWithName:(NSString*)notificationName transaction:(SKPaymentTransaction*)transaction userInfoExtras:(NSDictionary*)extras
+{
+    NSString *productIdentifier = transaction.payment.productIdentifier;
+    NSMutableDictionary *userInfo = [NSMutableDictionary dictionary];
+    userInfo[RMStoreNotificationTransaction] = transaction;
+    userInfo[RMStoreNotificationProductIdentifier] = productIdentifier;
+    if (extras)
+    {
+        [userInfo addEntriesFromDictionary:extras];
+    }
+    [[NSNotificationCenter defaultCenter] postNotificationName:notificationName object:self userInfo:userInfo];
+}
+
+- (void)removeProductsRequestDelegate:(RMProductsRequestDelegate*)delegate
+{
+    [_productsRequestDelegates removeObject:delegate];
+}
+
+@end
+
+@implementation RMProductsRequestDelegate
+
+- (void)productsRequest:(SKProductsRequest *)request didReceiveResponse:(SKProductsResponse *)response
+{
+    RMStoreLog(@"products request received response");
+    NSArray *products = [NSArray arrayWithArray:response.products];
+    NSArray *invalidProductIdentifiers = [NSArray arrayWithArray:response.invalidProductIdentifiers];
+    
+    for (SKProduct *product in products)
+    {
+        RMStoreLog(@"received product with id %@", product.productIdentifier);
+        [self.store addProduct:product];
+    }
+    
+    [invalidProductIdentifiers enumerateObjectsUsingBlock:^(NSString *invalid, NSUInteger idx, BOOL *stop) {
+        RMStoreLog(@"invalid product with id %@", invalid);
+    }];
+    
+    if (self.successBlock)
+    {
+        self.successBlock(products, invalidProductIdentifiers);
+    }
+    NSDictionary *userInfo = @{RMStoreNotificationProducts: products, RMStoreNotificationInvalidProductIdentifiers: invalidProductIdentifiers};
+    [[NSNotificationCenter defaultCenter] postNotificationName:RMSKProductsRequestFinished object:self.store userInfo:userInfo];
+}
+
+- (void)requestDidFinish:(SKRequest *)request
+{
+    [self.store removeProductsRequestDelegate:self];
+}
+
+- (void)request:(SKRequest *)request didFailWithError:(NSError *)error
+{
+    RMStoreLog(@"products request failed with error %@", error.debugDescription);
+    if (self.failureBlock)
+    {
+        self.failureBlock(error);
+    }
+    NSDictionary *userInfo = nil;
+    if (error)
+    { // error might be nil (e.g., on airplane mode)
+        userInfo = @{RMStoreNotificationStoreError: error};
+    }
+    [[NSNotificationCenter defaultCenter] postNotificationName:RMSKProductsRequestFailed object:self.store userInfo:userInfo];
+    [self.store removeProductsRequestDelegate:self];
+}
+
+@end

+ 131 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/js/index-android.js

xqd
@@ -0,0 +1,131 @@
+/*!
+ *
+ * Author: Alex Disler (alexdisler.com)
+ * github.com/alexdisler/cordova-plugin-inapppurchase
+ *
+ * Licensed under the MIT license. Please see README for more information.
+ *
+ */
+
+const inAppPurchase = { utils };
+
+const createIapError = (reject) => {
+  return (err = {}) => {
+    err.errorCode = err.code;
+    return reject(err);
+  };
+};
+
+const nativeCall = (name, args = []) => {
+  return new Promise((resolve, reject) => {
+    window.cordova.exec((res) => {
+      resolve(res);
+    }, createIapError(reject), 'InAppBillingV3', name, args);
+  });
+};
+
+const chunkedGetSkuDetails = (productIds) => {
+  // We need to chunk the getSkuDetails call cause it is only allowed to provide a maximum of 20 items per call
+  return utils.chunk(productIds, 19).reduce((promise, productIds) => {
+    return promise.then((result) => {
+      return nativeCall('getSkuDetails', productIds).then((items) => result.concat(items));
+    });
+  }, Promise.resolve([]));
+};
+
+inAppPurchase.getProducts = (productIds) => {
+  return new Promise((resolve, reject) => {
+    if(!inAppPurchase.utils.validArrayOfStrings(productIds)) {
+      reject(new Error(inAppPurchase.utils.errors[101]));
+    } else {
+      nativeCall('init', []).then(() => {
+        return chunkedGetSkuDetails(productIds);
+      })
+      .then((items) => {
+        const arr = items.map((val) => {
+          return {
+            productId   : val.productId,
+            title       : val.title,
+            description : val.description,
+            price       : val.price,
+            currency    : val.currency,
+            priceAsDecimal : val.priceAsDecimal,
+          };
+        });
+        resolve(arr);
+      }).catch(reject);
+    }
+  });
+};
+
+const executePaymentOfType = (type, productId) => {
+  return new Promise((resolve, reject) => {
+    if (!inAppPurchase.utils.validString(productId)) {
+      reject(new Error(inAppPurchase.utils.errors[102]));
+    } else {
+      nativeCall(type, [productId]).then((res) => {
+        resolve({
+          signature: res.signature,
+          productId: res.productId,
+          transactionId: res.purchaseToken,
+          type : res.type,
+          productType : res.type,
+          receipt : res.receipt,
+        });
+      }).catch(reject);
+    }
+  });
+};
+
+inAppPurchase.buy = (productId) => {
+  return executePaymentOfType('buy', productId);
+};
+
+inAppPurchase.subscribe = (productId) => {
+  return executePaymentOfType('subscribe', productId);
+};
+
+inAppPurchase.consume = (type, receipt, signature) => {
+  return new Promise((resolve, reject) => {
+    if(!inAppPurchase.utils.validString(type)) {
+      reject(new Error(inAppPurchase.utils.errors[103]));
+    } else if (!inAppPurchase.utils.validString(receipt)) {
+      reject(new Error(inAppPurchase.utils.errors[104]));
+    } else if (!inAppPurchase.utils.validString(signature)) {
+      reject(new Error(inAppPurchase.utils.errors[105]));
+    } else {
+      nativeCall('consumePurchase', [type, receipt, signature]).then(resolve).catch(reject);
+    }
+  });
+};
+
+inAppPurchase.restorePurchases = () => {
+  return nativeCall('init', [])
+    .then(() => {
+      return nativeCall('restorePurchases', []);
+    })
+    .then((purchases) => {
+      let arr = [];
+      if (purchases) {
+        arr = purchases.map((val) => {
+          return {
+            productId: val.productId,
+            state : val.state,
+            transactionId: val.orderId,
+            date : val.date,
+            type : val.type,
+            productType : val.type,
+            signature: val.signature,
+            receipt : val.receipt,
+          };
+        });
+      }
+      return Promise.resolve(arr);
+    });
+};
+
+inAppPurchase.getReceipt = () => {
+  return Promise.resolve('');
+};
+
+module.exports = inAppPurchase;

+ 108 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/js/index-ios.js

xqd
@@ -0,0 +1,108 @@
+/*!
+ *
+ * Author: Alex Disler (alexdisler.com)
+ * github.com/alexdisler/cordova-plugin-inapppurchase
+ *
+ * Licensed under the MIT license. Please see README for more information.
+ *
+ */
+
+const inAppPurchase = { utils };
+
+const nativeCall = (name, args = []) => {
+  return new Promise((resolve, reject) => {
+    window.cordova.exec((res) => {
+      resolve(res);
+    }, (err) => {
+      reject(err);
+    }, 'PaymentsPlugin', name, args);
+  });
+};
+
+inAppPurchase.getProducts = (productIds) => {
+  return new Promise((resolve, reject) => {
+    if(!inAppPurchase.utils.validArrayOfStrings(productIds)) {
+      reject(new Error(inAppPurchase.utils.errors[101]));
+    } else {
+      return nativeCall('getProducts', [productIds]).then((res) => {
+        if (!res || !res.products) {
+          resolve([]);
+        } else {
+          const arr = res.products.map((val) => {
+            return {
+              productId   : val.productId,
+              title       : val.title,
+              description : val.description,
+              priceAsDecimal : val.priceAsDecimal,
+              price       : val.price,
+              currency    : val.currency,
+            };
+          });
+          resolve(arr);
+        }
+      }).catch(reject);
+    }
+  });
+};
+
+inAppPurchase.buy = (productId) => {
+  return new Promise((resolve, reject) => {
+    if(!inAppPurchase.utils.validString(productId)) {
+      reject(new Error(inAppPurchase.utils.errors[102]));
+    } else {
+      nativeCall('buy', [productId]).then((res) => {
+        resolve({
+          transactionId : res.transactionId,
+          receipt       : res.receipt,
+        });
+      }).catch(reject);
+    }
+  });
+};
+
+/**
+ * This function exists so that the iOS plugin API will be compatible with that of Android -
+ * where this function is required.
+ * See README for more details.
+ */
+inAppPurchase.subscribe = (productId) => {
+  return inAppPurchase.buy(productId);
+};
+
+/**
+ * This function exists so that the iOS plugin API will be compatible with that of Android -
+ * where this function is required.
+ * See README for more details.
+ */
+inAppPurchase.consume = () => {
+  return Promise.resolve();
+};
+
+inAppPurchase.getReceipt = () => {
+  return nativeCall('getReceipt').then((res) => {
+    let receipt = '';
+    if (res && res.receipt) {
+      receipt = res.receipt;
+    }
+    return receipt;
+  });
+};
+
+inAppPurchase.restorePurchases = () => {
+  return nativeCall('restorePurchases').then((res) => {
+    let arr = [];
+    if (res && res.transactions) {
+      arr = res.transactions.map((val) => {
+        return {
+          productId     : val.productId,
+          date          : val.date,
+          transactionId : val.transactionId,
+          state         : val.transactionState,
+        };
+      });
+    }
+    return arr;
+  });
+};
+
+module.exports = inAppPurchase;

Файловите разлики са ограничени, защото са твърде много
+ 7 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/js/polyfills-android.js


+ 43 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/src/js/utils.js

xqd
@@ -0,0 +1,43 @@
+/*!
+ *
+ * Author: Alex Disler (alexdisler.com)
+ * github.com/alexdisler/cordova-plugin-inapppurchase
+ *
+ * Licensed under the MIT license. Please see README for more information.
+ *
+ */
+
+const utils = {};
+
+utils.errors = {
+  101: 'invalid argument - productIds must be an array of strings',
+  102: 'invalid argument - productId must be a string',
+  103: 'invalid argument - product type must be a string',
+  104: 'invalid argument - receipt must be a string of a json',
+  105: 'invalid argument - signature must be a string',
+};
+
+utils.validArrayOfStrings = (val) => {
+  return (val && Array.isArray(val) && val.length > 0 && !val.find(i => !i.length || typeof i !== 'string'));
+};
+
+utils.validString = (val) => {
+  return (val && val.length && typeof val === 'string');
+};
+
+utils.chunk = (array, size) => {
+  if (!Array.isArray(array)) {
+    throw new Error('Invalid array');
+  }
+
+  if (typeof size !== 'number' || size < 1) {
+    throw new Error('Invalid size');
+  }
+
+  const times = Math.ceil(array.length / size);
+  return Array
+    .apply(null, Array(times))
+    .reduce((result, val, i) => {
+      return result.concat([array.slice(i * size, (i + 1) * size)]);
+    }, []);
+};

+ 425 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/test/index-android.js

xqd
@@ -0,0 +1,425 @@
+import inAppPurchase from '../www/index-android';
+import assert from 'assert';
+
+describe('Android purchases', () => {
+
+  const execError = code => (success, err, pluginName, name, args) => err({ code });
+
+  before(() => {
+    GLOBAL.window = {};
+    GLOBAL.window.cordova = {};
+  });
+
+  describe('#getProducts()', () => {
+
+    it('should initialize the Android plugin', async (done) => {
+      try {
+        const productIds = ['com.test.prod1', 'com.test.prod2'];
+        let initCalled = false;
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name, args) => {
+          if (name === 'init') {
+            assert(typeof success === 'function', 'should define a success callback');
+            assert(typeof err === 'function', 'should define an error callback');
+            assert(pluginName === 'InAppBillingV3', 'invalid Android plugin name');
+            assert(args.length === 0, 'args should be empty');
+            initCalled = true;
+            success();
+          } else if (name === 'getSkuDetails') {
+            success([]);
+          }
+        };
+        await inAppPurchase.getProducts(productIds);
+        assert(initCalled, 'init() should be called');
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should call the Android getSkuDetails() function with the correct args', async (done) => {
+      try {
+        const productIds = ['com.test.prod1', 'com.test.prod2'];
+        let getSkuDetailsCalled = false;
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name, args) => {
+          if (name === 'getSkuDetails') {
+            assert(typeof success === 'function', 'should define a success callback');
+            assert(typeof err === 'function', 'should define an error callback');
+            assert(pluginName === 'InAppBillingV3', 'invalid Android plugin name');
+            assert.deepEqual(args, productIds, 'should get productIds as args');
+            getSkuDetailsCalled = true;
+            success([]);
+          } else if (name === 'init') {
+            success();
+          }
+        };
+        await inAppPurchase.getProducts(productIds);
+        assert(getSkuDetailsCalled, 'getSkuDetails() should be called');
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should chunk the getSkuDetails call when more than 19 product ids are given', async (done) => {
+      try {
+        const productIds = [
+          '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21'
+        ];
+        const calls = [];
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name, args) => {
+          if (name === 'getSkuDetails') {
+            calls.push(args);
+            success([]);
+          } else if (name === 'init') {
+            success();
+          }
+        };
+        await inAppPurchase.getProducts(productIds);
+        assert.deepEqual(calls, [
+          ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19'],
+          ['20', '21']
+        ],'getSkuDetails() should be called chunked');
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should return an array of objects', async (done) => {
+      try {
+        const products = [
+          { productId: 'com.test.prod1', title: 'prod1 title', description: 'prod1 description', price: '$0.99', currency: 'USD', priceAsDecimal: 0.99 },
+          { productId: 'com.test.prod2', title: 'prod2 title', description: 'prod2 description', price: '$1.99', currency: 'USD', priceAsDecimal: 1.99 }
+        ];
+        const productIds = products.map(i => i.productId );
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name) => {
+          if (name === 'getSkuDetails') {
+            success(products);
+          } else if (name === 'init') {
+            success();
+          }
+        };
+        const resProducts = await inAppPurchase.getProducts(productIds);
+        assert.deepEqual(resProducts, products);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should throw an error on an invalid argument', async (done) => {
+      let err1 = false;
+      let err2 = false;
+      let err3 = false;
+      try {
+        await inAppPurchase.getProducts();
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[101]) {
+          err1 = true;
+        } else {
+          done(err);
+        }
+      }
+      try {
+        await inAppPurchase.getProducts([1]);
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[101]) {
+          err2 = true;
+        } else {
+          done(err);
+        }
+      }
+      try {
+        await inAppPurchase.getProducts('test');
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[101]) {
+          err3 = true;
+        } else {
+          done(err);
+        }
+      }
+      assert(err1, 'should throw an error for no arguments');
+      assert(err2, 'should throw an error for an array of int');
+      assert(err3, 'should throw an error for a string');
+      done();
+    });
+
+    it('should return an errorCode property when there is an error', async (done) => {
+      try {
+        GLOBAL.window.cordova.exec = execError(-1)
+        await inAppPurchase.getProducts(['com.test.prod1']);
+        done(new Error('Call to #getProducts() suceeded but was expected to fail.'));
+      } catch (err) {
+        assert(err.errorCode === -1, 'should create an errorCode property');
+        done();
+      }
+    });
+
+  });
+
+  describe('#buy()', () => {
+
+    it('should call the Android buy() function with the correct args ', async (done) => {
+      try {
+        const productId = 'com.test.prod1';
+        const orderId = '_some_order_id_';
+        const purchaseToken = '_some_purchase_token_';
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name, args) => {
+          assert(typeof success === 'function', 'should define a success callback');
+          assert(typeof err === 'function', 'should define an error callback');
+          assert(pluginName === 'InAppBillingV3', 'invalid Android plugin name');
+          assert(name === 'buy', 'invalid function name');
+          assert(args[0] === productId, 'should get productId as args');
+          success({ orderId, purchaseToken });
+        };
+        await inAppPurchase.buy(productId);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should throw an error on an invalid argument', async (done) => {
+      let err1 = false;
+      let err2 = false;
+      let err3 = false;
+      try {
+        await inAppPurchase.buy();
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[102]) {
+          err1 = true;
+        } else {
+          done(err);
+        }
+      }
+      try {
+        await inAppPurchase.buy([1]);
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[102]) {
+          err2 = true;
+        } else {
+          done(err);
+        }
+      }
+      try {
+        await inAppPurchase.buy(1);
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[102]) {
+          err3 = true;
+        } else {
+          done(err);
+        }
+      }
+      assert(err1, 'should throw an error for no arguments');
+      assert(err2, 'should throw an error for an array of int');
+      assert(err3, 'should throw an error for an int');
+      done();
+    });
+
+    it('should return an object with the correct attributes', async (done) => {
+      try {
+        const productId = 'com.test.prod1';
+        const packageName = 'com.test';
+        const orderId = '_some_order_id_';
+        const purchaseToken = '_some_purchase_token_';
+        const signature = '_some_signature_';
+        const receipt = '_some_receipt_';
+        const purchaseTime = Date.now();
+        const purchaseState = 0;
+        GLOBAL.window.cordova.exec = (success) => {
+          success({ productId, orderId, purchaseToken, signature, packageName, purchaseTime, purchaseState, receipt });
+        };
+        const res = await inAppPurchase.buy(productId);
+        assert(res.signature === signature);
+        assert(res.productId === productId);
+        assert(res.transactionId === purchaseToken);
+        assert(res.receipt === receipt);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should return an errorCode property when there is an error', async (done) => {
+      try {
+        GLOBAL.window.cordova.exec = execError(-1);
+        await inAppPurchase.buy('com.test.prod1');
+        done(new Error('Call to #buy() suceeded but was expected to fail.'));
+      } catch (err) {
+        assert(err.errorCode === -1, 'should create an errorCode property');
+        done();
+      }
+    });
+
+  });
+
+  describe('#subscribe()', () => {
+
+    it('should call the Android subscribe() function with the correct args ', async (done) => {
+      try {
+        const productId = 'com.test.prod1';
+        const orderId = '_some_order_id_';
+        const purchaseToken = '_some_purchase_token_';
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name, args) => {
+          assert(typeof success === 'function', 'should define a success callback');
+          assert(typeof err === 'function', 'should define an error callback');
+          assert(pluginName === 'InAppBillingV3', 'invalid Android plugin name');
+          assert(name === 'subscribe', 'invalid function name');
+          assert(args[0] === productId, 'should get productId as args');
+          success({ orderId, purchaseToken });
+        };
+        await inAppPurchase.subscribe(productId);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should return an errorCode property when there is an error', async (done) => {
+      try {
+        GLOBAL.window.cordova.exec = execError(-1);
+        await inAppPurchase.subscribe('com.test.prod1');
+        done(new Error('Call to #subscribe() suceeded but was expected to fail.'));
+      } catch (err) {
+        assert(err.errorCode === -1, 'should create an errorCode property');
+        done();
+      }
+    });
+
+  });
+
+  describe('#consume()', () => {
+
+    it('should call the Android consume() function with the correct args ', async (done) => {
+      try {
+        const receipt = '_some_receipt_';
+        const signature = '_some_signature_';
+        const type = 'inapp';
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name, args) => {
+          assert(typeof success === 'function', 'should define a success callback');
+          assert(typeof err === 'function', 'should define an error callback');
+          assert(pluginName === 'InAppBillingV3', 'invalid Android plugin name');
+          assert(name === 'consumePurchase', 'invalid function name');
+          assert(args[0] === type, 'should get type as args 1');
+          assert(args[1] === receipt, 'should get receipt as args 2');
+          assert(args[2] === signature, 'should get signature as arg 3');
+          success({});
+        };
+        await inAppPurchase.consume(type, receipt, signature);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should return an errorCode property when there is an error', async(done) => {
+      try {
+        const receipt = '_some_receipt_';
+        const signature = '_some_signature_';
+        const type = 'inapp';
+        GLOBAL.window.cordova.exec = execError(-1);
+        await inAppPurchase.consume(type, receipt, signature);
+        done(new Error('Call to #consume() suceeded but was expected to fail.'));
+      } catch (err) {
+        assert(err.errorCode === -1, 'should create an errorCode property');
+        done();
+      }
+    });
+
+  });
+
+  describe('#restorePurchases()', () => {
+
+    it('should initialize the Android plugin', async (done) => {
+      try {
+        let initCalled = false;
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name, args) => {
+          if (name === 'init') {
+            assert(typeof success === 'function', 'should define a success callback');
+            assert(typeof err === 'function', 'should define an error callback');
+            assert(pluginName === 'InAppBillingV3', 'invalid Android plugin name');
+            assert(args.length === 0, 'args should be empty');
+            initCalled = true;
+            success();
+          } else if (name === 'restorePurchases') {
+            success([]);
+          }
+        };
+        await inAppPurchase.restorePurchases();
+        assert(initCalled, 'init() should be called');
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should call the Android restorePurchases() function with the correct args ', async (done) => {
+      try {
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name) => {
+          if (name === 'restorePurchases') {
+            assert(typeof success === 'function', 'should define a success callback');
+            assert(typeof err === 'function', 'should define an error callback');
+            assert(pluginName === 'InAppBillingV3', 'invalid Android plugin name');
+            success([{}]);
+          } else if (name === 'init') {
+            success();
+          }
+        };
+        await inAppPurchase.restorePurchases();
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should return an array of objects with the correct attributes', async (done) => {
+      const productId = 'com.test.prod1';
+      const state = 0;
+      const date = new Date();
+      const type = 'inapp';
+      try {
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name) => {
+          if (name === 'restorePurchases') {
+            success([{ productId, state, date, type }]);
+          } else if (name === 'init') {
+            success();
+          }
+        };
+        const res = await inAppPurchase.restorePurchases();
+        assert(res[0].productId === productId);
+        assert(res[0].state === state);
+        assert(res[0].date === date);
+        assert(res[0].type === type);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should return an errorCode property when there is an error', async(done) => {
+      try {
+        GLOBAL.window.cordova.exec = execError(-1);
+        await inAppPurchase.restorePurchases();
+        done(new Error('Call to #restorePurchases() suceeded but was expected to fail.'));
+      } catch (err) {
+        assert(err.errorCode === -1, 'should create an errorCode property');
+        done();
+      }
+    });
+
+  });
+
+  describe('#getReceipt()', () => {
+
+    it('should always successfully resolve without doing anything', async (done) => {
+      try {
+        await inAppPurchase.getReceipt();
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+  });
+
+});

+ 265 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/test/index-ios.js

xqd
@@ -0,0 +1,265 @@
+import inAppPurchase from '../www/index-ios';
+import assert from 'assert';
+
+describe('iOS purchases', () => {
+
+  before(() => {
+    GLOBAL.window = {};
+    GLOBAL.window.cordova = {};
+  });
+
+  describe('#getProducts()', () => {
+
+    it('should call the iOS requestProducts() function with the correct args', async (done) => {
+      try {
+        const productIds = ['com.test.prod1', 'com.test.prod2'];
+        let requestProductsCalled = false;
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name, args) => {
+          assert(typeof success === 'function', 'should define a success callback');
+          assert(typeof err === 'function', 'should define an error callback');
+          assert(pluginName === 'PaymentsPlugin', 'invalid iOS plugin name');
+          assert(args[0] === productIds, 'should get productIds as args');
+          requestProductsCalled = true;
+          success([]);
+        };
+        await inAppPurchase.getProducts(productIds);
+        assert(requestProductsCalled, 'requestProducts() should be called');
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should return an array of objects', async (done) => {
+      try {
+        const products = [
+          { productId: 'com.test.prod1', title: 'prod1 title', description: 'prod1 description', price: '$0.99', currency: 'USD', priceAsDecimal: 0.99 },
+          { productId: 'com.test.prod2', title: 'prod2 title', description: 'prod2 description', price: '$1.99', currency: 'USD', priceAsDecimal: 1.99 }
+        ];
+        const productIds = products.map(i => i.productId );
+        GLOBAL.window.cordova.exec = (success) => {
+          success({ products });
+        };
+        const resProducts = await inAppPurchase.getProducts(productIds);
+        assert.deepEqual(resProducts, products);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should throw an error on an invalid argument', async (done) => {
+      let err1 = false;
+      let err2 = false;
+      let err3 = false;
+      try {
+        await inAppPurchase.getProducts();
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[101]) {
+          err1 = true;
+        } else {
+          done(err);
+        }
+      }
+      try {
+        await inAppPurchase.getProducts([1]);
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[101]) {
+          err2 = true;
+        } else {
+          done(err);
+        }
+      }
+      try {
+        await inAppPurchase.getProducts('test');
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[101]) {
+          err3 = true;
+        } else {
+          done(err);
+        }
+      }
+      assert(err1, 'should throw an error for no arguments');
+      assert(err2, 'should throw an error for an array of int');
+      assert(err3, 'should throw an error for a string');
+      done();
+    });
+
+  });
+
+  describe('#buy()', () => {
+
+    it('should call the iOS buy() function with the correct args ', async (done) => {
+      try {
+        const productId = 'com.test.prod1';
+        const transactionId = '111111111';
+        const receipt = '222222222';
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name, args) => {
+          assert(typeof success === 'function', 'should define a success callback');
+          assert(typeof err === 'function', 'should define an error callback');
+          assert(pluginName === 'PaymentsPlugin', 'invalid iOS plugin name');
+          assert(name === 'buy', 'invalid function name');
+          assert(args[0] === productId, 'should get productId as args');
+          success({ productId, transactionId, receipt });
+        };
+        await inAppPurchase.buy(productId);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should throw an error on an invalid argument', async (done) => {
+      let err1 = false;
+      let err2 = false;
+      let err3 = false;
+      try {
+        await inAppPurchase.buy();
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[102]) {
+          err1 = true;
+        } else {
+          done(err);
+        }
+      }
+      try {
+        await inAppPurchase.buy([1]);
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[102]) {
+          err2 = true;
+        } else {
+          done(err);
+        }
+      }
+      try {
+        await inAppPurchase.buy(1);
+      } catch (err) {
+        if (err.message === inAppPurchase.utils.errors[102]) {
+          err3 = true;
+        } else {
+          done(err);
+        }
+      }
+      assert(err1, 'should throw an error for no arguments');
+      assert(err2, 'should throw an error for an array of int');
+      assert(err3, 'should throw an error for an int');
+      done();
+    });
+
+    it('should return an object with the correct attributes', async (done) => {
+      try {
+        const productId = 'com.test.prod1';
+        const transactionId = '111111111';
+        const receipt = '222222222';
+        GLOBAL.window.cordova.exec = (success) => {
+          success({ transactionId, receipt });
+        };
+        const res = await inAppPurchase.buy(productId);
+        assert(res.transactionId === transactionId);
+        assert(res.receipt === receipt);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+  });
+
+  describe('#subscribe()', () => {
+
+    it('should call the iOS buy() function with the correct args ', async (done) => {
+      try {
+        const productId = 'com.test.prod1';
+        const transactionId = '111111111';
+        const receipt = '222222222';
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name, args) => {
+          assert(typeof success === 'function', 'should define a success callback');
+          assert(typeof err === 'function', 'should define an error callback');
+          assert(pluginName === 'PaymentsPlugin', 'invalid iOS plugin name');
+          assert(name === 'buy', 'invalid function name');
+          assert(args[0] === productId, 'should get productId as args');
+          success({ productId, transactionId, receipt });
+        };
+        await inAppPurchase.subscribe(productId);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+  });
+
+  describe('#consume()', () => {
+
+    it('should always successfully resolve without doing anything', async (done) => {
+      try {
+        await inAppPurchase.consume();
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+  });
+
+  describe('#restorePurchases()', () => {
+
+    it('should call the iOS restoreTransactions() function with the correct args ', async (done) => {
+      try {
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name) => {
+          assert(typeof success === 'function', 'should define a success callback');
+          assert(typeof err === 'function', 'should define an error callback');
+          assert(pluginName === 'PaymentsPlugin', 'invalid iOS plugin name');
+          assert(name === 'restorePurchases', 'invalid function name');
+          success([{}]);
+        };
+        await inAppPurchase.restorePurchases();
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+    it('should return an array of objects with the correct attributes', async (done) => {
+      const transactionId = '111111111';
+      const productId = '111111111';
+      const transactionState = 'REFUNDED';
+      const date = new Date();
+      try {
+        GLOBAL.window.cordova.exec = (success) => {
+          success({ transactions : [{ transactionId, productId, transactionState, date }]});
+        };
+        const res = await inAppPurchase.restorePurchases();
+        assert(res[0].transactionId === transactionId);
+        assert(res[0].productId === productId);
+        assert(res[0].state === transactionState);
+        assert(res[0].date === date);
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+  });
+
+  describe('#getReceipt()', () => {
+
+    it('should call the iOS getReceipt() function', async (done) => {
+      try {
+        GLOBAL.window.cordova.exec = (success, err, pluginName, name) => {
+          assert(typeof success === 'function', 'should define a success callback');
+          assert(typeof err === 'function', 'should define an error callback');
+          assert(pluginName === 'PaymentsPlugin', 'invalid iOS plugin name');
+          assert(name === 'getReceipt', 'invalid function name');
+          success('');
+        };
+        await inAppPurchase.getReceipt();
+        done();
+      } catch (err) {
+        done(err);
+      }
+    });
+
+  });
+
+});

+ 40 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/test/utils.js

xqd
@@ -0,0 +1,40 @@
+import { utils } from '../www/index-ios';
+import assert from 'assert';
+
+describe('utils', () => {
+  describe('#chunk', () => {
+    it('should chunk a given array', () => {
+      const chunks = utils.chunk(['1', '2', '3', '4', '5'], 2);
+      assert.deepEqual(chunks, [['1', '2'], ['3', '4'], ['5']]);
+    });
+
+    it('should return on chunk when the size is bigger than the array size', () => {
+      const chunks = utils.chunk(['1', '2', '3', '4', '5'], 42);
+      assert.deepEqual(chunks, [['1', '2', '3', '4', '5']]);
+    });
+
+    it('should throw an error on size smaller 1', () => {
+      assert.throws(() => {
+        utils.chunk(['1', '2', '3', '4', '5'], 0);
+      }, (err) => {
+        return err.message === 'Invalid size';
+      }, 'unexpected error');
+    });
+
+    it('should throw an error on non numeric size', () => {
+      assert.throws(() => {
+        utils.chunk(['1', '2', '3', '4', '5'], 'not a number');
+      }, (err) => {
+        return err.message === 'Invalid size';
+      }, 'unexpected error');
+    });
+
+    it('should throw an error when on a non array type', () => {
+      assert.throws(() => {
+        utils.chunk(null, 2);
+      }, (err) => {
+        return err.message === 'Invalid array';
+      }, 'unexpected error');
+    });
+  });
+});

Файловите разлики са ограничени, защото са твърде много
+ 7 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/www/index-android.js


+ 157 - 0
miaomiao/plugins/cordova-plugin-inapppurchase/www/index-ios.js

xqd
@@ -0,0 +1,157 @@
+'use strict';
+
+/*!
+ *
+ * Author: Alex Disler (alexdisler.com)
+ * github.com/alexdisler/cordova-plugin-inapppurchase
+ *
+ * Licensed under the MIT license. Please see README for more information.
+ *
+ */
+
+var utils = {};
+
+utils.errors = {
+  101: 'invalid argument - productIds must be an array of strings',
+  102: 'invalid argument - productId must be a string',
+  103: 'invalid argument - product type must be a string',
+  104: 'invalid argument - receipt must be a string of a json',
+  105: 'invalid argument - signature must be a string'
+};
+
+utils.validArrayOfStrings = function (val) {
+  return val && Array.isArray(val) && val.length > 0 && !val.find(function (i) {
+    return !i.length || typeof i !== 'string';
+  });
+};
+
+utils.validString = function (val) {
+  return val && val.length && typeof val === 'string';
+};
+
+utils.chunk = function (array, size) {
+  if (!Array.isArray(array)) {
+    throw new Error('Invalid array');
+  }
+
+  if (typeof size !== 'number' || size < 1) {
+    throw new Error('Invalid size');
+  }
+
+  var times = Math.ceil(array.length / size);
+  return Array.apply(null, Array(times)).reduce(function (result, val, i) {
+    return result.concat([array.slice(i * size, (i + 1) * size)]);
+  }, []);
+};
+'use strict';
+
+/*!
+ *
+ * Author: Alex Disler (alexdisler.com)
+ * github.com/alexdisler/cordova-plugin-inapppurchase
+ *
+ * Licensed under the MIT license. Please see README for more information.
+ *
+ */
+
+var inAppPurchase = { utils: utils };
+
+var nativeCall = function nativeCall(name) {
+  var args = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : [];
+
+  return new Promise(function (resolve, reject) {
+    window.cordova.exec(function (res) {
+      resolve(res);
+    }, function (err) {
+      reject(err);
+    }, 'PaymentsPlugin', name, args);
+  });
+};
+
+inAppPurchase.getProducts = function (productIds) {
+  return new Promise(function (resolve, reject) {
+    if (!inAppPurchase.utils.validArrayOfStrings(productIds)) {
+      reject(new Error(inAppPurchase.utils.errors[101]));
+    } else {
+      return nativeCall('getProducts', [productIds]).then(function (res) {
+        if (!res || !res.products) {
+          resolve([]);
+        } else {
+          var arr = res.products.map(function (val) {
+            return {
+              productId: val.productId,
+              title: val.title,
+              description: val.description,
+              price: val.price,
+              currency: val.currency,
+              priceAsDecimal: val.priceAsDecimal,
+            };
+          });
+          resolve(arr);
+        }
+      }).catch(reject);
+    }
+  });
+};
+
+inAppPurchase.buy = function (productId) {
+  return new Promise(function (resolve, reject) {
+    if (!inAppPurchase.utils.validString(productId)) {
+      reject(new Error(inAppPurchase.utils.errors[102]));
+    } else {
+      nativeCall('buy', [productId]).then(function (res) {
+        resolve({
+          transactionId: res.transactionId,
+          receipt: res.receipt
+        });
+      }).catch(reject);
+    }
+  });
+};
+
+/**
+ * This function exists so that the iOS plugin API will be compatible with that of Android -
+ * where this function is required.
+ * See README for more details.
+ */
+inAppPurchase.subscribe = function (productId) {
+  return inAppPurchase.buy(productId);
+};
+
+/**
+ * This function exists so that the iOS plugin API will be compatible with that of Android -
+ * where this function is required.
+ * See README for more details.
+ */
+inAppPurchase.consume = function () {
+  return Promise.resolve();
+};
+
+inAppPurchase.getReceipt = function () {
+  return nativeCall('getReceipt').then(function (res) {
+    var receipt = '';
+    if (res && res.receipt) {
+      receipt = res.receipt;
+    }
+    return receipt;
+  });
+};
+
+inAppPurchase.restorePurchases = function () {
+  return nativeCall('restorePurchases').then(function (res) {
+    var arr = [];
+    if (res && res.transactions) {
+      arr = res.transactions.map(function (val) {
+        return {
+          productId: val.productId,
+          date: val.date,
+          transactionId: val.transactionId,
+          state: val.transactionState
+        };
+      });
+    }
+    return arr;
+  });
+};
+
+module.exports = inAppPurchase;

+ 8 - 0
miaomiao/plugins/fetch.json

xqd
@@ -158,5 +158,13 @@
         },
         "is_top_level": true,
         "variables": {}
+    },
+    "cordova-plugin-inapppurchase": {
+        "source": {
+            "type": "registry",
+            "id": "cordova-plugin-inapppurchase"
+        },
+        "is_top_level": true,
+        "variables": {}
     }
 }

+ 3 - 0
miaomiao/plugins/ios.json

xqd
@@ -41,6 +41,9 @@
         },
         "cordova-plugin-video-editor": {
             "PACKAGE_NAME": "com.miaomiao.app"
+        },
+        "cordova-plugin-inapppurchase": {
+            "PACKAGE_NAME": "com.miaomiao.app"
         }
     },
     "dependent_plugins": {

Някои файлове не бяха показани, защото твърде много файлове са промени