├── .gitignore ├── LICENSE ├── README.md ├── android ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src │ └── main │ ├── AndroidManifest.xml │ ├── aidl │ └── com │ │ └── android │ │ └── vending │ │ └── billing │ │ └── IInAppBillingService.aidl │ └── java │ └── com │ ├── customBilling │ └── reactnative │ │ ├── RNCustomBillingModule.java │ │ └── RNCustomBillingPackage.java │ └── util │ ├── Base64.java │ ├── Base64DecoderException.java │ ├── IabException.java │ ├── IabHelper.java │ ├── IabResult.java │ ├── Inventory.java │ ├── Purchase.java │ ├── Security.java │ └── SkuDetails.java ├── index.js ├── package.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # --------------------------------------- File system 2 | *.swp 3 | 4 | # --------------------------------------- OSX 5 | # 6 | .DS_Store 7 | 8 | # --------------------------------------- Android/IntelliJ 9 | # 10 | .project 11 | org.eclipse.* 12 | .classpath 13 | .idea 14 | .gradle 15 | local.properties 16 | *.iml 17 | 18 | # --------------------------------------- node.js 19 | # 20 | node_modules/ 21 | npm-debug.log 22 | yarn-error.log 23 | *.swp 24 | 25 | # --------------------------------------- react native 26 | # 27 | react-native-packager-cache-*/ 28 | haste-map-react-native-packager-* 29 | metro-bundler-symbolicate* 30 | 31 | # --------------------------------------- BUCK 32 | buck-out/ 33 | \.buckd/ 34 | android/app/libs 35 | *.keystore 36 | 37 | # --------------------------------------- fastlane 38 | # 39 | # It is recommended to not store the screenshots in the git repo. Instead, use fastlane to re-generate the 40 | # screenshots whenever they are needed. 41 | # For more information about the recommended setup visit: 42 | # https://github.com/fastlane/fastlane/blob/master/fastlane/docs/Gitignore.md 43 | 44 | fastlane/report.xml 45 | fastlane/Preview.html 46 | fastlane/screenshots 47 | 48 | # Bundle artifact 49 | *.jsbundle 50 | # --------------------------------------- Test 51 | # 52 | coverage/* 53 | jest/* 54 | .git 55 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 HessiPard 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | InApp Billing for CafeBazaar/Myket/IranApps/Google (Android) 2 | ============= 3 | **React Native Custom Billing** is built to provide an easy interface to InApp Billing for **CafeBazaar/Myket/IranApps/Google (Android)**, 4 | 5 | 6 | ## Getting started 7 | 8 | `$ npm install react-native-custom-billing --save` 9 | 10 | ### Mostly automatic installation (for React Native <= 0.59 only, React Native >= 0.60 skip this as auto-linking should work) 11 | 12 | `$ react-native link react-native-custom-billing` 13 | 14 | ### Manual installation (Skip step 1,2,3 for React Native >= 0.60) 15 | 16 | 17 | #### Android 18 | 19 | 1. Open up `android/app/src/main/java/[...]/MainApplication.java` 20 | - Add `import com.customBilling.reactnative.RNCustomBillingPackage;` to the imports at the top of the file 21 | - Add `new RNCustomBillingPackage()` to the list returned by the `getPackages()` method 22 | 2. Append the following lines to `android/settings.gradle`: 23 | ``` 24 | include ':react-native-custom-billing' 25 | project(':react-native-custom-billing').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-custom-billing/android') 26 | ``` 27 | 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: 28 | ``` 29 | compile project(':react-native-custom-billing') 30 | ``` 31 | 4. Add your Vendor Name (CafeBazaar/Myket/IranApps/Google) as a line to your `android/app/src/main/res/values/strings.xml` with the name `RNCB_VENDOR_NAME`. For example: 32 | ```xml 33 | bazaar 34 | or 35 | myket 36 | or 37 | google 38 | or 39 | iranapps 40 | ``` 41 | 5. Add your Vendor Public key as a line to your `android/app/src/main/res/values/strings.xml` with the name `RNCB_VENDOR_PUBLIC_KEY`. For example: 42 | ```xml 43 | YOUR_CAFE_BAZAAR_PUBLIC_KEY 44 | or 45 | YOUR_MYKET_PUBLIC_KEY 46 | or 47 | YOUR_IRAN_APPS_PUBLIC_KEY 48 | or 49 | YOUR_GOOGLE_PUBLIC_KEY 50 | ``` 51 | 6. Add the billing permission as follows to AndroidManifest.xml file based on your Vendor (CafeBazaar/Myket/Google): 52 | ```xml 53 | Cafe Bazaar: 54 | or 55 | Myket: 56 | or 57 | Iran Apps: 58 | or 59 | Google: 60 | ``` 61 | 62 | ## Javascript API 63 | Most of methods returns a `Promise`. 64 | 65 | ### open() 66 | 67 | **Important:** Opens the service channel to RNCustomBilling. Must be called (once!) before any other billing methods can be called. 68 | 69 | ```javascript 70 | import RNCustomBilling from 'react-native-custom-billing' 71 | 72 | ... 73 | 74 | RNCustomBilling.open() 75 | .then(() => RNCustomBilling.purchase('YOUR_SKU','DEVELOPER_PAYLOAD',RC_REQUEST)) 76 | .catch(err => console.log('Vendor err:', err)) 77 | ``` 78 | 79 | ### close() 80 | **Important:** Must be called to close the service channel to RNCustomBilling, when you are done doing billing related work. Failure to close the service channel may degrade the performance of your app. 81 | ```javascript 82 | RNCustomBilling.open() 83 | .then(() => RNCustomBilling.purchase('YOUR_SKU','DEVELOPER_PAYLOAD',RC_REQUEST)) 84 | .then((details) => { 85 | console.log(details) 86 | return RNCustomBilling.close() 87 | }) 88 | .catch(err => console.log('Vendor err:', err)) 89 | ``` 90 | 91 | ### purchase('YOUR_SKU','DEVELOPER_PAYLOAD',RC_REQUEST) 92 | ##### Parameter(s) 93 | * **productSKU (required):** String 94 | * **developerPayload:** String 95 | * **rcRequest:** Integer 96 | 97 | ##### Returns: 98 | * **Details:** JSONObject: 99 | * **mDeveloperPayload:** 100 | * **mItemType:** 101 | * **mOrderId:** 102 | * **mOriginalJson:** 103 | * **mPackageName:** 104 | * **mPurchaseState:** 105 | * **mPurchaseTime:** 106 | * **mSignature:** 107 | * **mSku:** 108 | * **mToken:** 109 | 110 | ```javascript 111 | RNCustomBilling.purchase('YOUR_SKU','DEVELOPER_PAYLOAD',RC_REQUEST) 112 | .then((details) => { 113 | console.log(details) 114 | }) 115 | .catch(err => console.log('RNCustomBilling err:', err)) 116 | ``` 117 | 118 | ### consume('YOUR_SKU') 119 | ##### Parameter(s) 120 | * **productSKU (required):** String 121 | 122 | ##### Returns: 123 | * **Details:** JSONObject: 124 | * **mDeveloperPayload:** 125 | * **mItemType:** 126 | * **mOrderId:** 127 | * **mOriginalJson:** 128 | * **mPackageName:** 129 | * **mPurchaseState:** 130 | * **mPurchaseTime:** 131 | * **mSignature:** 132 | * **mSku:** 133 | * **mToken:** 134 | 135 | ```javascript 136 | RNCustomBilling.consume('YOUR_SKU') 137 | .then(...) 138 | .catch(err => console.log('Vendor err:', err)) 139 | ``` 140 | 141 | ### loadOwnedItems() 142 | 143 | ##### Returns: 144 | * **items:** JSONArray: 145 | 146 | 147 | ```javascript 148 | RNCustomBilling.loadOwnedItems() 149 | .then((details) => { 150 | console.log(details) 151 | }) 152 | .catch(err => console.log('Vendor err:', err)) 153 | ``` 154 | 155 | ### loadInventory([item1_SKU,item2_SKU,...]) 156 | ##### Parameter(s) 157 | * **productSKUs (required):** Array 158 | 159 | ##### Returns: 160 | * **mPurchaseMap:** JSONObject 161 | * **mSkuMap:** JSONObject 162 | 163 | ```javascript 164 | RNCustomBilling.loadInventory([]) 165 | .then(...) 166 | .catch(err => console.log('Vendor err:', err)) 167 | ``` 168 | 169 | ## Use event listener 170 | Below function dispatch **Event** instead of **Promise** and return value is same. 171 | 172 | ### purchaseWithEvent('YOUR_SKU','DEVELOPER_PAYLOAD',RC_REQUEST) 173 | ### consumeWithEvent('YOUR_SKU') 174 | ### loadOwnedItemsWithEvent() 175 | 176 | 177 | ```javascript 178 | import {DeviceEventEmitter} from 'react-native'; 179 | ... 180 | componentDidMount(){ 181 | DeviceEventEmitter.addListener('Vendor', function(e: Event) { 182 | // handle event. 183 | console.log(e); 184 | }); 185 | } 186 | ``` 187 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | dependencies { 7 | classpath 'com.android.tools.build:gradle:3.6.4' 8 | } 9 | } 10 | 11 | apply plugin: "com.android.library" 12 | 13 | android { 14 | compileSdkVersion 34 15 | buildToolsVersion "34.0.0" 16 | 17 | defaultConfig { 18 | minSdkVersion 16 19 | targetSdkVersion 28 20 | versionCode 2 21 | versionName "2.1.0" 22 | } 23 | buildTypes { 24 | release { 25 | minifyEnabled false 26 | proguardFiles getDefaultProguardFile("proguard-android.txt"), "proguard-rules.pro" 27 | } 28 | } 29 | } 30 | 31 | allprojects { 32 | repositories { 33 | google() 34 | jcenter() 35 | maven { 36 | // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm 37 | url "$rootDir/../node_modules/react-native/android" 38 | } 39 | } 40 | } 41 | 42 | dependencies { 43 | implementation 'org.jetbrains:annotations:+@jar' 44 | implementation 'com.google.code.gson:gson:2.2.4' 45 | implementation "com.facebook.react:react-native:+" // From node_modules 46 | } 47 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true 19 | 20 | android.useDeprecatedNdk=true 21 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BazamBazi/react-native-custom-billing/6909c24076adc051b97805545d4dc83db73685c3/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip 6 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /android/src/main/aidl/com/android/vending/billing/IInAppBillingService.aidl: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2012 The Android Open Source Project 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.android.vending.billing; 18 | 19 | import android.os.Bundle; 20 | 21 | /** 22 | * InAppBillingService is the service that provides in-app billing version 3 and beyond. 23 | * This service provides the following features: 24 | * 1. Provides a new API to get details of in-app items published for the app including 25 | * price, type, title and description. 26 | * 2. The purchase flow is synchronous and purchase information is available immediately 27 | * after it completes. 28 | * 3. Purchase information of in-app purchases is maintained within the Google Play system 29 | * till the purchase is consumed. 30 | * 4. An API to consume a purchase of an inapp item. All purchases of one-time 31 | * in-app items are consumable and thereafter can be purchased again. 32 | * 5. An API to get current purchases of the user immediately. This will not contain any 33 | * consumed purchases. 34 | * 35 | * All calls will give a response code with the following possible values 36 | * RESULT_OK = 0 - success 37 | * RESULT_USER_CANCELED = 1 - user pressed back or canceled a dialog 38 | * RESULT_BILLING_UNAVAILABLE = 3 - this billing API version is not supported for the type requested 39 | * RESULT_ITEM_UNAVAILABLE = 4 - requested SKU is not available for purchase 40 | * RESULT_DEVELOPER_ERROR = 5 - invalid arguments provided to the API 41 | * RESULT_ERROR = 6 - Fatal error during the API action 42 | * RESULT_ITEM_ALREADY_OWNED = 7 - Failure to purchase since item is already owned 43 | * RESULT_ITEM_NOT_OWNED = 8 - Failure to consume since item is not owned 44 | */ 45 | interface IInAppBillingService { 46 | /** 47 | * Checks support for the requested billing API version, package and in-app type. 48 | * Minimum API version supported by this interface is 3. 49 | * @param apiVersion the billing version which the app is using 50 | * @param packageName the package name of the calling app 51 | * @param type type of the in-app item being purchased "inapp" for one-time purchases 52 | * and "subs" for subscription. 53 | * @return RESULT_OK(0) on success, corresponding result code on failures 54 | */ 55 | int isBillingSupported(int apiVersion, String packageName, String type); 56 | 57 | /** 58 | * Provides details of a list of SKUs 59 | * Given a list of SKUs of a valid type in the skusBundle, this returns a bundle 60 | * with a list JSON strings containing the productId, price, title and description. 61 | * This API can be called with a maximum of 20 SKUs. 62 | * @param apiVersion billing API version that the Third-party is using 63 | * @param packageName the package name of the calling app 64 | * @param skusBundle bundle containing a StringArrayList of SKUs with key "ITEM_ID_LIST" 65 | * @return Bundle containing the following key-value pairs 66 | * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on 67 | * failure as listed above. 68 | * "DETAILS_LIST" with a StringArrayList containing purchase information 69 | * in JSON format similar to: 70 | * '{ "productId" : "exampleSku", "type" : "inapp", "price" : "$5.00", 71 | * "title : "Example Title", "description" : "This is an example description" }' 72 | */ 73 | Bundle getSkuDetails(int apiVersion, String packageName, String type, in Bundle skusBundle); 74 | 75 | /** 76 | * Returns a pending intent to launch the purchase flow for an in-app item by providing a SKU, 77 | * the type, a unique purchase token and an optional developer payload. 78 | * @param apiVersion billing API version that the app is using 79 | * @param packageName package name of the calling app 80 | * @param sku the SKU of the in-app item as published in the developer console 81 | * @param type the type of the in-app item ("inapp" for one-time purchases 82 | * and "subs" for subscription). 83 | * @param developerPayload optional argument to be sent back with the purchase information 84 | * @return Bundle containing the following key-value pairs 85 | * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on 86 | * failure as listed above. 87 | * "BUY_INTENT" - PendingIntent to start the purchase flow 88 | * 89 | * The Pending intent should be launched with startIntentSenderForResult. When purchase flow 90 | * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. 91 | * If the purchase is successful, the result data will contain the following key-value pairs 92 | * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on 93 | * failure as listed above. 94 | * "INAPP_PURCHASE_DATA" - String in JSON format similar to 95 | * '{"orderId":"12999763169054705758.1371079406387615", 96 | * "packageName":"com.example.app", 97 | * "productId":"exampleSku", 98 | * "purchaseTime":1345678900000, 99 | * "purchaseToken" : "122333444455555", 100 | * "developerPayload":"example developer payload" }' 101 | * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that 102 | * was signed with the private key of the developer 103 | * TODO: change this to app-specific keys. 104 | */ 105 | Bundle getBuyIntent(int apiVersion, String packageName, String sku, String type, 106 | String developerPayload); 107 | 108 | /** 109 | * Returns the current SKUs owned by the user of the type and package name specified along with 110 | * purchase information and a signature of the data to be validated. 111 | * This will return all SKUs that have been purchased in V3 and managed items purchased using 112 | * V1 and V2 that have not been consumed. 113 | * @param apiVersion billing API version that the app is using 114 | * @param packageName package name of the calling app 115 | * @param type the type of the in-app items being requested 116 | * ("inapp" for one-time purchases and "subs" for subscription). 117 | * @param continuationToken to be set as null for the first call, if the number of owned 118 | * skus are too many, a continuationToken is returned in the response bundle. 119 | * This method can be called again with the continuation token to get the next set of 120 | * owned skus. 121 | * @return Bundle containing the following key-value pairs 122 | * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on 123 | * failure as listed above. 124 | * "INAPP_PURCHASE_ITEM_LIST" - StringArrayList containing the list of SKUs 125 | * "INAPP_PURCHASE_DATA_LIST" - StringArrayList containing the purchase information 126 | * "INAPP_DATA_SIGNATURE_LIST"- StringArrayList containing the signatures 127 | * of the purchase information 128 | * "INAPP_CONTINUATION_TOKEN" - String containing a continuation token for the 129 | * next set of in-app purchases. Only set if the 130 | * user has more owned skus than the current list. 131 | */ 132 | Bundle getPurchases(int apiVersion, String packageName, String type, String continuationToken); 133 | 134 | /** 135 | * Consume the last purchase of the given SKU. This will result in this item being removed 136 | * from all subsequent responses to getPurchases() and allow re-purchase of this item. 137 | * @param apiVersion billing API version that the app is using 138 | * @param packageName package name of the calling app 139 | * @param purchaseToken token in the purchase information JSON that identifies the purchase 140 | * to be consumed 141 | * @return 0 if consumption succeeded. Appropriate error values for failures. 142 | */ 143 | int consumePurchase(int apiVersion, String packageName, String purchaseToken); 144 | 145 | /** 146 | * Returns an intent to launch the purchase flow for an in-app item by providing a SKU, 147 | * the type, a unique purchase token and an optional developer payload. 148 | * @param apiVersion billing API version that the app is using 149 | * @param packageName package name of the calling app 150 | * @param sku the SKU of the in-app item as published in the developer console 151 | * @param type the type of the in-app item ("inapp" for one-time purchases 152 | * and "subs" for subscription). 153 | * @param developerPayload optional argument to be sent back with the purchase information 154 | * @return Bundle containing the following key-value pairs 155 | * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on 156 | * failure as listed above. 157 | * "BUY_INTENT" - Intent to start the purchase flow 158 | * 159 | * The intent should be launched with startActivityForResult. When purchase flow 160 | * has completed, the onActivityResult() will give a resultCode of OK or CANCELED. 161 | * If the purchase is successful, the result data will contain the following key-value pairs 162 | * "RESPONSE_CODE" with int value, RESULT_OK(0) if success, other response codes on 163 | * failure as listed above. 164 | * "INAPP_PURCHASE_DATA" - String in JSON format similar to 165 | * '{"orderId":"12999763169054705758.1371079406387615", 166 | * "packageName":"com.example.app", 167 | * "productId":"exampleSku", 168 | * "purchaseTime":1345678900000, 169 | * "purchaseToken" : "122333444455555", 170 | * "developerPayload":"example developer payload" }' 171 | * "INAPP_DATA_SIGNATURE" - String containing the signature of the purchase data that 172 | * was signed with the private key of the developer 173 | * TODO: change this to app-specific keys. 174 | */ 175 | Bundle getBuyIntentV2(int apiVersion, String packageName, String sku, String type, 176 | String developerPayload); 177 | 178 | /** 179 | * Returns the config of purchase. 180 | * 181 | * @return Bundle containing the following key-value pair 182 | * "INTENT_V2_SUPPORT" with boolean value 183 | */ 184 | Bundle getPurchaseConfig(int apiVersion); 185 | } 186 | -------------------------------------------------------------------------------- /android/src/main/java/com/customBilling/reactnative/RNCustomBillingModule.java: -------------------------------------------------------------------------------- 1 | 2 | package com.customBilling.reactnative; 3 | 4 | import android.app.Activity; 5 | import android.util.Log; 6 | import android.content.Intent; 7 | 8 | import java.util.ArrayList; 9 | import java.util.HashMap; 10 | import java.util.List; 11 | import java.util.Map; 12 | import java.lang.Object; 13 | 14 | import com.facebook.react.bridge.NativeModule; 15 | import com.facebook.react.bridge.ReactApplicationContext; 16 | import com.facebook.react.bridge.ReactContext; 17 | import com.facebook.react.bridge.ReactContextBaseJavaModule; 18 | import com.facebook.react.bridge.ReactMethod; 19 | import com.facebook.react.bridge.Promise; 20 | import com.facebook.react.bridge.Arguments; 21 | import com.facebook.react.bridge.ReadableArray; 22 | import com.facebook.react.bridge.WritableArray; 23 | import com.facebook.react.bridge.WritableMap; 24 | import com.facebook.react.bridge.ReadableMap; 25 | import com.facebook.react.bridge.WritableNativeArray; 26 | import com.facebook.react.bridge.JavaScriptModule; 27 | import com.facebook.react.modules.core.DeviceEventManagerModule; 28 | import com.facebook.react.bridge.ActivityEventListener; 29 | 30 | import com.util.IabHelper; 31 | import com.util.IabResult; 32 | import com.util.Inventory; 33 | import com.util.Purchase; 34 | 35 | import org.json.JSONArray; 36 | import org.json.JSONException; 37 | import org.json.JSONObject; 38 | import com.google.gson.Gson; 39 | import org.jetbrains.annotations.Nullable; 40 | 41 | public class RNCustomBillingModule extends ReactContextBaseJavaModule implements ActivityEventListener { 42 | 43 | private final ReactApplicationContext _reactContext; 44 | private String LICENSE_KEY = null; 45 | private String VENDOR_INTENT = null; 46 | private String VENDOR_PACKAGE = null; 47 | private IabHelper mHelper; 48 | private final Gson gson = new Gson(); 49 | private static final String E_SETUP_ERROR = "E_SETUP_ERROR"; 50 | private static final String E_SETUP_DISCONNECT = "E_SETUP_DISCONNECT"; 51 | private static final String E_LOAD_ITEMS_FAILURE = "E_LOAD_ITEMS_FAILURE"; 52 | private static final String E_LAYOUT_ERROR = "E_LAYOUT_ERROR"; 53 | private static final String E_PURCHASE_DISCONNECT = "E_PURCHASE_DISCONNECT"; 54 | private static final String E_PURCHASE_FAILURE = "E_PURCHASE_FAILURE"; 55 | private static final String E_PURCHASE_PAYLOAD_VERIFY = "E_PURCHASE_PAYLOAD_VERIFY"; 56 | private static final String E_PURCHASE_ERROR = "E_PURCHASE_ERROR"; 57 | private static final String E_CONSUME_FAILURE = "E_CONSUME_FAILURE"; 58 | private static final String E_CONSUME_ERROR = "E_CONSUME_ERROR"; 59 | private static final String E_CONSUME_INITIAL = "E_CONSUME_INITIAL"; 60 | private final String TAG; 61 | private Inventory userInvo; 62 | 63 | public RNCustomBillingModule(ReactApplicationContext reactContext) { 64 | super(reactContext); 65 | _reactContext = reactContext; 66 | 67 | int vendorResourceId = _reactContext 68 | .getResources() 69 | .getIdentifier("RNCB_VENDOR_NAME", "string", _reactContext.getPackageName()); 70 | TAG = _reactContext.getString(vendorResourceId); 71 | 72 | int keyResourceId = _reactContext 73 | .getResources() 74 | .getIdentifier("RNCB_VENDOR_PUBLIC_KEY", "string", _reactContext.getPackageName()); 75 | this.LICENSE_KEY = _reactContext.getString(keyResourceId); 76 | 77 | switch (TAG) { 78 | case "google": 79 | VENDOR_INTENT = "com.android.vending.billing.InAppBillingService.BIND"; 80 | VENDOR_PACKAGE = "com.android.vending"; 81 | break; 82 | case "bazaar": 83 | VENDOR_INTENT = "ir.cafebazaar.pardakht.InAppBillingService.BIND"; 84 | VENDOR_PACKAGE = "com.farsitel.bazaar"; 85 | break; 86 | case "myket": 87 | VENDOR_INTENT = "ir.mservices.market.InAppBillingService.BIND"; 88 | VENDOR_PACKAGE = "ir.mservices.market"; 89 | break; 90 | case "iranapps": 91 | VENDOR_INTENT = "ir.tgbs.iranapps.billing.InAppBillingService.BIND"; 92 | VENDOR_PACKAGE = "ir.tgbs.android.iranapp"; 93 | break; 94 | default: 95 | break; 96 | } 97 | 98 | reactContext.addActivityEventListener(this); 99 | } 100 | 101 | @Override 102 | public String getName() { 103 | return "RNCustomBilling"; 104 | } 105 | 106 | @ReactMethod 107 | public void open(final Promise promise) { 108 | if (VENDOR_INTENT == null) { 109 | // Oh noes, there was a problem. 110 | promise.reject(E_SETUP_ERROR, "No Vendor Defined !"); 111 | return; 112 | } 113 | mHelper = new IabHelper(_reactContext, LICENSE_KEY); 114 | 115 | // enable debug logging (for a production application, you should set this to 116 | // false). 117 | mHelper.enableDebugLogging(false); 118 | 119 | try { 120 | // Start setup. This is asynchronous and the specified listener 121 | // will be called once setup completes. 122 | mHelper.startSetup(VENDOR_INTENT, VENDOR_PACKAGE, new IabHelper.OnIabSetupFinishedListener() { 123 | public void onIabSetupFinished(IabResult result) { 124 | 125 | if (result.isSuccess()) { 126 | promise.resolve(gson.toJson(result)); 127 | } else { 128 | // Have we been disposed of in the meantime? If so, quit. 129 | if (mHelper == null) { 130 | promise.reject(E_SETUP_DISCONNECT, "there is no connection to " + TAG); 131 | } else { 132 | // Oh noes, there was a problem. 133 | promise.reject(E_SETUP_ERROR, "There is a problem in setting up" + TAG); 134 | } 135 | } 136 | } 137 | }); 138 | 139 | } catch (Exception excep) { 140 | promise.reject(E_SETUP_DISCONNECT, "there is no connection to " + TAG); 141 | Log.e(TAG + "OpenError", "Could not open " + TAG, excep); 142 | } 143 | } 144 | 145 | @ReactMethod 146 | public void loadInventory(ReadableArray skuList, final Promise promise) { 147 | ArrayList skus = new ArrayList<>(); 148 | for (int i = 0; i < skuList.size(); i++) { 149 | skus.add(skuList.getString(i)); 150 | } 151 | mHelper.queryInventoryAsync(true, skus, new IabHelper.QueryInventoryFinishedListener() { 152 | public void onQueryInventoryFinished(IabResult result, Inventory inventory) { 153 | // Have we been disposed of in the meantime? If so, quit. 154 | if (mHelper == null) { 155 | promise.reject(E_SETUP_DISCONNECT, "there is no connection to " + TAG); 156 | } 157 | // Is it a failure? 158 | else if (result.isFailure()) { 159 | promise.reject(E_LAYOUT_ERROR, "Failed to query inventory: " + result.getMessage()); 160 | } else { 161 | promise.resolve(gson.toJson(inventory)); 162 | } 163 | } 164 | }); 165 | } 166 | 167 | @ReactMethod 168 | public void loadOwnedItems(final Promise promise) { 169 | mHelper.queryInventoryAsync(new IabHelper.QueryInventoryFinishedListener() { 170 | public void onQueryInventoryFinished(IabResult result, Inventory inventory) { 171 | // Have we been disposed of in the meantime? If so, quit. 172 | WritableMap params = Arguments.createMap(); 173 | if (mHelper == null) { 174 | promise.reject(E_SETUP_DISCONNECT, "there is no connection to " + TAG); 175 | } 176 | // Is it a failure? 177 | else if (result.isFailure()) { 178 | promise.reject(E_LOAD_ITEMS_FAILURE, result.getMessage()); 179 | } else { 180 | userInvo = inventory; 181 | promise.resolve(gson.toJson(inventory.getAllOwnedSkus())); 182 | } 183 | } 184 | }); 185 | } 186 | 187 | public void loadOwnedItemsWithEvent() { 188 | mHelper.queryInventoryAsync(new IabHelper.QueryInventoryFinishedListener() { 189 | public void onQueryInventoryFinished(IabResult result, Inventory inventory) { 190 | // Have we been disposed of in the meantime? If so, quit. 191 | WritableMap params = Arguments.createMap(); 192 | if (mHelper == null) { 193 | params.putString("LoadOwnedItem", "Disposed!"); 194 | sendEvent(TAG, params); 195 | } 196 | // Is it a failure? 197 | if (result.isFailure()) { 198 | params.putString("LoadOwnedItem", result.getMessage()); 199 | sendEvent(TAG, params); 200 | } 201 | userInvo = inventory; 202 | params.putString("LoadOwnedItem", gson.toJson(inventory.getAllOwnedSkus())); 203 | sendEvent(TAG, params); 204 | } 205 | }); 206 | } 207 | 208 | @ReactMethod 209 | public void purchaseWithEvent(String sku, String payload, int rcRequest) { 210 | try { 211 | mHelper.launchPurchaseFlow(getCurrentActivity(), sku, rcRequest, 212 | new IabHelper.OnIabPurchaseFinishedListener() { 213 | public void onIabPurchaseFinished(IabResult result, Purchase purchase) { 214 | // if we were disposed of in the meantime, quit. 215 | WritableMap params = Arguments.createMap(); 216 | 217 | if (mHelper == null) { 218 | params.putString("PurchaseResult", "Connection Error!"); 219 | sendEvent(TAG, params); 220 | } else { 221 | if (result.isFailure()) { 222 | params.putString("Error", result.getMessage()); 223 | sendEvent(TAG, params); 224 | } else { 225 | if (!verifyDeveloperPayload(purchase)) { 226 | params.putString("Error", "could not verify developer payload"); 227 | sendEvent(TAG, params); 228 | } else { 229 | params.putString("Details", gson.toJson(purchase)); 230 | sendEvent(TAG, params); 231 | } 232 | } 233 | } 234 | } 235 | }, payload); 236 | } catch (Exception ex) { 237 | WritableMap params = Arguments.createMap(); 238 | params.putString("Error", ex.getMessage()); 239 | sendEvent(TAG, params); 240 | } 241 | } 242 | 243 | @ReactMethod 244 | public void purchase(String sku, String payload, int rcRequest, final Promise promise) { 245 | try { 246 | mHelper.launchPurchaseFlow(getCurrentActivity(), sku, rcRequest, 247 | new IabHelper.OnIabPurchaseFinishedListener() { 248 | public void onIabPurchaseFinished(IabResult result, Purchase purchase) { 249 | // if we were disposed of in the meantime, quit. 250 | WritableMap params = Arguments.createMap(); 251 | 252 | if (mHelper == null) { 253 | promise.reject(E_PURCHASE_DISCONNECT, "Connection Error!"); 254 | } else { 255 | if (result.isFailure()) { 256 | promise.reject(E_PURCHASE_FAILURE, result.getMessage()); 257 | } else { 258 | if (!verifyDeveloperPayload(purchase)) { 259 | promise.reject(E_PURCHASE_PAYLOAD_VERIFY, "could not verify developer payload"); 260 | } else { 261 | promise.resolve(gson.toJson(purchase)); 262 | } 263 | } 264 | } 265 | } 266 | }, payload); 267 | } catch (Exception ex) { 268 | promise.reject(E_PURCHASE_ERROR, ex.getMessage()); 269 | } 270 | } 271 | 272 | /** Verifies the developer payload of a purchase. */ 273 | boolean verifyDeveloperPayload(Purchase p) { 274 | String payload = p.getDeveloperPayload(); 275 | 276 | /* 277 | * TODO: verify that the developer payload of the purchase is correct. It will 278 | * be the same one that you sent when initiating the purchase. 279 | * 280 | * WARNING: Locally generating a random string when starting a purchase and 281 | * verifying it here might seem like a good approach, but this will fail in the 282 | * case where the user purchases an item on one device and then uses your app on 283 | * a different device, because on the other device you will not have access to 284 | * the random string you originally generated. 285 | * 286 | * So a good developer payload has these characteristics: 287 | * 288 | * 1. If two different users purchase an item, the payload is different between 289 | * them, so that one user's purchase can't be replayed to another user. 290 | * 291 | * 2. The payload must be such that you can verify it even when the app wasn't 292 | * the one who initiated the purchase flow (so that items purchased by the user 293 | * on one device work on other devices owned by the user). 294 | * 295 | * Using your own server to store and verify developer payloads across app 296 | * installations is recommended. 297 | */ 298 | 299 | return true; 300 | } 301 | 302 | private void sendEvent(String eventName, @Nullable WritableMap params) { 303 | _reactContext.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter.class).emit(eventName, params); 304 | } 305 | 306 | @ReactMethod 307 | public void consume(String sku, final Promise promise) { 308 | if (userInvo != null) { 309 | if (userInvo.hasPurchase(sku)) { 310 | mHelper.consumeAsync(userInvo.getPurchase(sku), new IabHelper.OnConsumeFinishedListener() { 311 | public void onConsumeFinished(Purchase purchase, IabResult result) { 312 | WritableMap params = Arguments.createMap(); 313 | if (result.isSuccess()) { 314 | // provision the in-app purchase to the user 315 | promise.resolve(gson.toJson(purchase)); 316 | } else { 317 | // handle error 318 | promise.reject(E_CONSUME_FAILURE, result.getMessage()); 319 | } 320 | } 321 | }); 322 | } else { 323 | promise.reject(E_CONSUME_ERROR, "user did not purchase item"); 324 | } 325 | } else { 326 | promise.reject(E_CONSUME_INITIAL, "inventory not loaded!"); 327 | } 328 | } 329 | 330 | @ReactMethod 331 | public void consumeWithEvent(String sku) { 332 | if (userInvo != null) { 333 | if (userInvo.hasPurchase(sku)) { 334 | mHelper.consumeAsync(userInvo.getPurchase(sku), new IabHelper.OnConsumeFinishedListener() { 335 | public void onConsumeFinished(Purchase purchase, IabResult result) { 336 | WritableMap params = Arguments.createMap(); 337 | if (result.isSuccess()) { 338 | // provision the in-app purchase to the user 339 | // (for example, credit 50 gold coins to player's character) 340 | params.putString("SuccessfulConsume", gson.toJson(purchase)); 341 | sendEvent(TAG, params); 342 | } else { 343 | // handle error 344 | params.putString("Error", result.getMessage()); 345 | sendEvent(TAG, params); 346 | } 347 | } 348 | }); 349 | } else { 350 | WritableMap params = Arguments.createMap(); 351 | params.putString("Error", "user did not purchase item"); 352 | sendEvent(TAG, params); 353 | } 354 | } else { 355 | WritableMap params = Arguments.createMap(); 356 | params.putString("Error", "inventory not loaded!"); 357 | sendEvent(TAG, params); 358 | } 359 | } 360 | 361 | @Deprecated 362 | public void onActivityResult(final int requestCode, final int resultCode, final Intent intent) { 363 | if (mHelper == null) 364 | return; 365 | 366 | // Pass on the activity result to the helper for handling 367 | if (!mHelper.handleActivityResult(requestCode, resultCode, intent)) { 368 | // not handled, so handle it ourselves (here's where you'd 369 | // perform any handling of activity results not related to in-app 370 | // billing... 371 | WritableMap params = Arguments.createMap(); 372 | params.putString("Warning", "you need to use your activity to perfom billing"); 373 | sendEvent(TAG, params); 374 | } 375 | } 376 | 377 | public void onActivityResult(final Activity activity, final int requestCode, final int resultCode, 378 | final Intent intent) { 379 | if (mHelper == null) 380 | return; 381 | 382 | // Pass on the activity result to the helper for handling 383 | if (!mHelper.handleActivityResult(requestCode, resultCode, intent)) { 384 | // not handled, so handle it ourselves (here's where you'd 385 | // perform any handling of activity results not related to in-app 386 | // billing... 387 | WritableMap params = Arguments.createMap(); 388 | params.putString("Warning", "you need to use your activity to perfom billing"); 389 | sendEvent(TAG, params); 390 | } 391 | } 392 | 393 | @Override 394 | public void onNewIntent(Intent intent) { 395 | 396 | } 397 | 398 | @ReactMethod 399 | public void close(final Promise promise) { 400 | // very important: 401 | if (mHelper != null) { 402 | mHelper.dispose(); 403 | mHelper = null; 404 | } 405 | promise.resolve(true); 406 | } 407 | } -------------------------------------------------------------------------------- /android/src/main/java/com/customBilling/reactnative/RNCustomBillingPackage.java: -------------------------------------------------------------------------------- 1 | 2 | package com.customBilling.reactnative; 3 | 4 | import java.util.ArrayList; 5 | import java.util.Collections; 6 | import java.util.List; 7 | 8 | import com.facebook.react.ReactPackage; 9 | import com.facebook.react.bridge.NativeModule; 10 | import com.facebook.react.bridge.ReactApplicationContext; 11 | import com.facebook.react.uimanager.ViewManager; 12 | import com.facebook.react.bridge.JavaScriptModule; 13 | 14 | public class RNCustomBillingPackage implements ReactPackage { 15 | @Override 16 | public List createNativeModules(ReactApplicationContext reactContext) { 17 | List modules = new ArrayList<>(); 18 | 19 | modules.add(new RNCustomBillingModule(reactContext)); 20 | 21 | return modules; 22 | } 23 | 24 | // Deprecated from RN 0.47 25 | public List> createJSModules() { 26 | return Collections.emptyList(); 27 | } 28 | 29 | @Override 30 | public List createViewManagers(ReactApplicationContext reactContext) { 31 | return Collections.emptyList(); 32 | } 33 | } -------------------------------------------------------------------------------- /android/src/main/java/com/util/Base64.java: -------------------------------------------------------------------------------- 1 | // Portions copyright 2002, Google, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.util; 16 | 17 | // This code was converted from code at http://iharder.sourceforge.net/base64/ 18 | // Lots of extraneous features were removed. 19 | /* The original code said: 20 | *

21 | * I am placing this code in the Public Domain. Do with it as you will. 22 | * This software comes with no guarantees or warranties but with 23 | * plenty of well-wishing instead! 24 | * Please visit 25 | * http://iharder.net/xmlizable 26 | * periodically to check for updates or to contribute improvements. 27 | *

28 | * 29 | * @author Robert Harder 30 | * @author rharder@usa.net 31 | * @version 1.3 32 | */ 33 | 34 | /** 35 | * Base64 converter class. This code is not a complete MIME encoder; 36 | * it simply converts binary data to base64 data and back. 37 | * 38 | *

Note {@link CharBase64} is a GWT-compatible implementation of this 39 | * class. 40 | */ 41 | public class Base64 { 42 | /** Specify encoding (value is {@code true}). */ 43 | public final static boolean ENCODE = true; 44 | 45 | /** Specify decoding (value is {@code false}). */ 46 | public final static boolean DECODE = false; 47 | 48 | /** The equals sign (=) as a byte. */ 49 | private final static byte EQUALS_SIGN = (byte) '='; 50 | 51 | /** The new line character (\n) as a byte. */ 52 | private final static byte NEW_LINE = (byte) '\n'; 53 | 54 | /** 55 | * The 64 valid Base64 values. 56 | */ 57 | private final static byte[] ALPHABET = 58 | {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', 59 | (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', 60 | (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', 61 | (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', 62 | (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', 63 | (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', 64 | (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', 65 | (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', 66 | (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', 67 | (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', 68 | (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', 69 | (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', 70 | (byte) '9', (byte) '+', (byte) '/'}; 71 | 72 | /** 73 | * The 64 valid web safe Base64 values. 74 | */ 75 | private final static byte[] WEBSAFE_ALPHABET = 76 | {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', 77 | (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', 78 | (byte) 'L', (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', 79 | (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', 80 | (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', 81 | (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', 82 | (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', 83 | (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o', 84 | (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', 85 | (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', 86 | (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', 87 | (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', 88 | (byte) '9', (byte) '-', (byte) '_'}; 89 | 90 | /** 91 | * Translates a Base64 value to either its 6-bit reconstruction value 92 | * or a negative number indicating some other meaning. 93 | **/ 94 | private final static byte[] DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 95 | -5, -5, // Whitespace: Tab and Linefeed 96 | -9, -9, // Decimal 11 - 12 97 | -5, // Whitespace: Carriage Return 98 | -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 99 | -9, -9, -9, -9, -9, // Decimal 27 - 31 100 | -5, // Whitespace: Space 101 | -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 102 | 62, // Plus sign at decimal 43 103 | -9, -9, -9, // Decimal 44 - 46 104 | 63, // Slash at decimal 47 105 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine 106 | -9, -9, -9, // Decimal 58 - 60 107 | -1, // Equals sign at decimal 61 108 | -9, -9, -9, // Decimal 62 - 64 109 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' 110 | 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' 111 | -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 112 | 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' 113 | 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' 114 | -9, -9, -9, -9, -9 // Decimal 123 - 127 115 | /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 116 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 117 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 118 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 119 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 120 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 121 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 122 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 123 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 124 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ 125 | }; 126 | 127 | /** The web safe decodabet */ 128 | private final static byte[] WEBSAFE_DECODABET = 129 | {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 130 | -5, -5, // Whitespace: Tab and Linefeed 131 | -9, -9, // Decimal 11 - 12 132 | -5, // Whitespace: Carriage Return 133 | -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 134 | -9, -9, -9, -9, -9, // Decimal 27 - 31 135 | -5, // Whitespace: Space 136 | -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 44 137 | 62, // Dash '-' sign at decimal 45 138 | -9, -9, // Decimal 46-47 139 | 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine 140 | -9, -9, -9, // Decimal 58 - 60 141 | -1, // Equals sign at decimal 61 142 | -9, -9, -9, // Decimal 62 - 64 143 | 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' 144 | 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' 145 | -9, -9, -9, -9, // Decimal 91-94 146 | 63, // Underscore '_' at decimal 95 147 | -9, // Decimal 96 148 | 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' 149 | 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' 150 | -9, -9, -9, -9, -9 // Decimal 123 - 127 151 | /* ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 139 152 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 153 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 154 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 155 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 156 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 157 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 158 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 159 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 160 | -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ 161 | }; 162 | 163 | // Indicates white space in encoding 164 | private final static byte WHITE_SPACE_ENC = -5; 165 | // Indicates equals sign in encoding 166 | private final static byte EQUALS_SIGN_ENC = -1; 167 | 168 | /** Defeats instantiation. */ 169 | private Base64() { 170 | } 171 | 172 | /* ******** E N C O D I N G M E T H O D S ******** */ 173 | 174 | /** 175 | * Encodes up to three bytes of the array source 176 | * and writes the resulting four Base64 bytes to destination. 177 | * The source and destination arrays can be manipulated 178 | * anywhere along their length by specifying 179 | * srcOffset and destOffset. 180 | * This method does not check to make sure your arrays 181 | * are large enough to accommodate srcOffset + 3 for 182 | * the source array or destOffset + 4 for 183 | * the destination array. 184 | * The actual number of significant bytes in your array is 185 | * given by numSigBytes. 186 | * 187 | * @param source the array to convert 188 | * @param srcOffset the index where conversion begins 189 | * @param numSigBytes the number of significant bytes in your array 190 | * @param destination the array to hold the conversion 191 | * @param destOffset the index where output will be put 192 | * @param alphabet is the encoding alphabet 193 | * @return the destination array 194 | * @since 1.3 195 | */ 196 | private static byte[] encode3to4(byte[] source, int srcOffset, 197 | int numSigBytes, byte[] destination, int destOffset, byte[] alphabet) { 198 | // 1 2 3 199 | // 01234567890123456789012345678901 Bit position 200 | // --------000000001111111122222222 Array position from threeBytes 201 | // --------| || || || | Six bit groups to index alphabet 202 | // >>18 >>12 >> 6 >> 0 Right shift necessary 203 | // 0x3f 0x3f 0x3f Additional AND 204 | 205 | // Create buffer with zero-padding if there are only one or two 206 | // significant bytes passed in the array. 207 | // We have to shift left 24 in order to flush out the 1's that appear 208 | // when Java treats a value as negative that is cast from a byte to an int. 209 | int inBuff = 210 | (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) 211 | | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) 212 | | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); 213 | 214 | switch (numSigBytes) { 215 | case 3: 216 | destination[destOffset] = alphabet[(inBuff >>> 18)]; 217 | destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; 218 | destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; 219 | destination[destOffset + 3] = alphabet[(inBuff) & 0x3f]; 220 | return destination; 221 | case 2: 222 | destination[destOffset] = alphabet[(inBuff >>> 18)]; 223 | destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; 224 | destination[destOffset + 2] = alphabet[(inBuff >>> 6) & 0x3f]; 225 | destination[destOffset + 3] = EQUALS_SIGN; 226 | return destination; 227 | case 1: 228 | destination[destOffset] = alphabet[(inBuff >>> 18)]; 229 | destination[destOffset + 1] = alphabet[(inBuff >>> 12) & 0x3f]; 230 | destination[destOffset + 2] = EQUALS_SIGN; 231 | destination[destOffset + 3] = EQUALS_SIGN; 232 | return destination; 233 | default: 234 | return destination; 235 | } // end switch 236 | } // end encode3to4 237 | 238 | /** 239 | * Encodes a byte array into Base64 notation. 240 | * Equivalent to calling 241 | * {@code encodeBytes(source, 0, source.length)} 242 | * 243 | * @param source The data to convert 244 | * @since 1.4 245 | */ 246 | public static String encode(byte[] source) { 247 | return encode(source, 0, source.length, ALPHABET, true); 248 | } 249 | 250 | /** 251 | * Encodes a byte array into web safe Base64 notation. 252 | * 253 | * @param source The data to convert 254 | * @param doPadding is {@code true} to pad result with '=' chars 255 | * if it does not fall on 3 byte boundaries 256 | */ 257 | public static String encodeWebSafe(byte[] source, boolean doPadding) { 258 | return encode(source, 0, source.length, WEBSAFE_ALPHABET, doPadding); 259 | } 260 | 261 | /** 262 | * Encodes a byte array into Base64 notation. 263 | * 264 | * @param source the data to convert 265 | * @param off offset in array where conversion should begin 266 | * @param len length of data to convert 267 | * @param alphabet the encoding alphabet 268 | * @param doPadding is {@code true} to pad result with '=' chars 269 | * if it does not fall on 3 byte boundaries 270 | * @since 1.4 271 | */ 272 | public static String encode(byte[] source, int off, int len, byte[] alphabet, 273 | boolean doPadding) { 274 | byte[] outBuff = encode(source, off, len, alphabet, Integer.MAX_VALUE); 275 | int outLen = outBuff.length; 276 | 277 | // If doPadding is false, set length to truncate '=' 278 | // padding characters 279 | while (doPadding == false && outLen > 0) { 280 | if (outBuff[outLen - 1] != '=') { 281 | break; 282 | } 283 | outLen -= 1; 284 | } 285 | 286 | return new String(outBuff, 0, outLen); 287 | } 288 | 289 | /** 290 | * Encodes a byte array into Base64 notation. 291 | * 292 | * @param source the data to convert 293 | * @param off offset in array where conversion should begin 294 | * @param len length of data to convert 295 | * @param alphabet is the encoding alphabet 296 | * @param maxLineLength maximum length of one line. 297 | * @return the BASE64-encoded byte array 298 | */ 299 | public static byte[] encode(byte[] source, int off, int len, byte[] alphabet, 300 | int maxLineLength) { 301 | int lenDiv3 = (len + 2) / 3; // ceil(len / 3) 302 | int len43 = lenDiv3 * 4; 303 | byte[] outBuff = new byte[len43 // Main 4:3 304 | + (len43 / maxLineLength)]; // New lines 305 | 306 | int d = 0; 307 | int e = 0; 308 | int len2 = len - 2; 309 | int lineLength = 0; 310 | for (; d < len2; d += 3, e += 4) { 311 | 312 | // The following block of code is the same as 313 | // encode3to4( source, d + off, 3, outBuff, e, alphabet ); 314 | // but inlined for faster encoding (~20% improvement) 315 | int inBuff = 316 | ((source[d + off] << 24) >>> 8) 317 | | ((source[d + 1 + off] << 24) >>> 16) 318 | | ((source[d + 2 + off] << 24) >>> 24); 319 | outBuff[e] = alphabet[(inBuff >>> 18)]; 320 | outBuff[e + 1] = alphabet[(inBuff >>> 12) & 0x3f]; 321 | outBuff[e + 2] = alphabet[(inBuff >>> 6) & 0x3f]; 322 | outBuff[e + 3] = alphabet[(inBuff) & 0x3f]; 323 | 324 | lineLength += 4; 325 | if (lineLength == maxLineLength) { 326 | outBuff[e + 4] = NEW_LINE; 327 | e++; 328 | lineLength = 0; 329 | } // end if: end of line 330 | } // end for: each piece of array 331 | 332 | if (d < len) { 333 | encode3to4(source, d + off, len - d, outBuff, e, alphabet); 334 | 335 | lineLength += 4; 336 | if (lineLength == maxLineLength) { 337 | // Add a last newline 338 | outBuff[e + 4] = NEW_LINE; 339 | e++; 340 | } 341 | e += 4; 342 | } 343 | 344 | assert (e == outBuff.length); 345 | return outBuff; 346 | } 347 | 348 | 349 | /* ******** D E C O D I N G M E T H O D S ******** */ 350 | 351 | 352 | /** 353 | * Decodes four bytes from array source 354 | * and writes the resulting bytes (up to three of them) 355 | * to destination. 356 | * The source and destination arrays can be manipulated 357 | * anywhere along their length by specifying 358 | * srcOffset and destOffset. 359 | * This method does not check to make sure your arrays 360 | * are large enough to accommodate srcOffset + 4 for 361 | * the source array or destOffset + 3 for 362 | * the destination array. 363 | * This method returns the actual number of bytes that 364 | * were converted from the Base64 encoding. 365 | * 366 | * 367 | * @param source the array to convert 368 | * @param srcOffset the index where conversion begins 369 | * @param destination the array to hold the conversion 370 | * @param destOffset the index where output will be put 371 | * @param decodabet the decodabet for decoding Base64 content 372 | * @return the number of decoded bytes converted 373 | * @since 1.3 374 | */ 375 | private static int decode4to3(byte[] source, int srcOffset, 376 | byte[] destination, int destOffset, byte[] decodabet) { 377 | // Example: Dk== 378 | if (source[srcOffset + 2] == EQUALS_SIGN) { 379 | int outBuff = 380 | ((decodabet[source[srcOffset]] << 24) >>> 6) 381 | | ((decodabet[source[srcOffset + 1]] << 24) >>> 12); 382 | 383 | destination[destOffset] = (byte) (outBuff >>> 16); 384 | return 1; 385 | } else if (source[srcOffset + 3] == EQUALS_SIGN) { 386 | // Example: DkL= 387 | int outBuff = 388 | ((decodabet[source[srcOffset]] << 24) >>> 6) 389 | | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) 390 | | ((decodabet[source[srcOffset + 2]] << 24) >>> 18); 391 | 392 | destination[destOffset] = (byte) (outBuff >>> 16); 393 | destination[destOffset + 1] = (byte) (outBuff >>> 8); 394 | return 2; 395 | } else { 396 | // Example: DkLE 397 | int outBuff = 398 | ((decodabet[source[srcOffset]] << 24) >>> 6) 399 | | ((decodabet[source[srcOffset + 1]] << 24) >>> 12) 400 | | ((decodabet[source[srcOffset + 2]] << 24) >>> 18) 401 | | ((decodabet[source[srcOffset + 3]] << 24) >>> 24); 402 | 403 | destination[destOffset] = (byte) (outBuff >> 16); 404 | destination[destOffset + 1] = (byte) (outBuff >> 8); 405 | destination[destOffset + 2] = (byte) (outBuff); 406 | return 3; 407 | } 408 | } // end decodeToBytes 409 | 410 | 411 | /** 412 | * Decodes data from Base64 notation. 413 | * 414 | * @param s the string to decode (decoded in default encoding) 415 | * @return the decoded data 416 | * @since 1.4 417 | */ 418 | public static byte[] decode(String s) throws Base64DecoderException { 419 | byte[] bytes = s.getBytes(); 420 | return decode(bytes, 0, bytes.length); 421 | } 422 | 423 | /** 424 | * Decodes data from web safe Base64 notation. 425 | * Web safe encoding uses '-' instead of '+', '_' instead of '/' 426 | * 427 | * @param s the string to decode (decoded in default encoding) 428 | * @return the decoded data 429 | */ 430 | public static byte[] decodeWebSafe(String s) throws Base64DecoderException { 431 | byte[] bytes = s.getBytes(); 432 | return decodeWebSafe(bytes, 0, bytes.length); 433 | } 434 | 435 | /** 436 | * Decodes Base64 content in byte array format and returns 437 | * the decoded byte array. 438 | * 439 | * @param source The Base64 encoded data 440 | * @return decoded data 441 | * @since 1.3 442 | * @throws Base64DecoderException 443 | */ 444 | public static byte[] decode(byte[] source) throws Base64DecoderException { 445 | return decode(source, 0, source.length); 446 | } 447 | 448 | /** 449 | * Decodes web safe Base64 content in byte array format and returns 450 | * the decoded data. 451 | * Web safe encoding uses '-' instead of '+', '_' instead of '/' 452 | * 453 | * @param source the string to decode (decoded in default encoding) 454 | * @return the decoded data 455 | */ 456 | public static byte[] decodeWebSafe(byte[] source) 457 | throws Base64DecoderException { 458 | return decodeWebSafe(source, 0, source.length); 459 | } 460 | 461 | /** 462 | * Decodes Base64 content in byte array format and returns 463 | * the decoded byte array. 464 | * 465 | * @param source the Base64 encoded data 466 | * @param off the offset of where to begin decoding 467 | * @param len the length of characters to decode 468 | * @return decoded data 469 | * @since 1.3 470 | * @throws Base64DecoderException 471 | */ 472 | public static byte[] decode(byte[] source, int off, int len) 473 | throws Base64DecoderException { 474 | return decode(source, off, len, DECODABET); 475 | } 476 | 477 | /** 478 | * Decodes web safe Base64 content in byte array format and returns 479 | * the decoded byte array. 480 | * Web safe encoding uses '-' instead of '+', '_' instead of '/' 481 | * 482 | * @param source the Base64 encoded data 483 | * @param off the offset of where to begin decoding 484 | * @param len the length of characters to decode 485 | * @return decoded data 486 | */ 487 | public static byte[] decodeWebSafe(byte[] source, int off, int len) 488 | throws Base64DecoderException { 489 | return decode(source, off, len, WEBSAFE_DECODABET); 490 | } 491 | 492 | /** 493 | * Decodes Base64 content using the supplied decodabet and returns 494 | * the decoded byte array. 495 | * 496 | * @param source the Base64 encoded data 497 | * @param off the offset of where to begin decoding 498 | * @param len the length of characters to decode 499 | * @param decodabet the decodabet for decoding Base64 content 500 | * @return decoded data 501 | */ 502 | public static byte[] decode(byte[] source, int off, int len, byte[] decodabet) 503 | throws Base64DecoderException { 504 | int len34 = len * 3 / 4; 505 | byte[] outBuff = new byte[2 + len34]; // Upper limit on size of output 506 | int outBuffPosn = 0; 507 | 508 | byte[] b4 = new byte[4]; 509 | int b4Posn = 0; 510 | int i = 0; 511 | byte sbiCrop = 0; 512 | byte sbiDecode = 0; 513 | for (i = 0; i < len; i++) { 514 | sbiCrop = (byte) (source[i + off] & 0x7f); // Only the low seven bits 515 | sbiDecode = decodabet[sbiCrop]; 516 | 517 | if (sbiDecode >= WHITE_SPACE_ENC) { // White space Equals sign or better 518 | if (sbiDecode >= EQUALS_SIGN_ENC) { 519 | // An equals sign (for padding) must not occur at position 0 or 1 520 | // and must be the last byte[s] in the encoded value 521 | if (sbiCrop == EQUALS_SIGN) { 522 | int bytesLeft = len - i; 523 | byte lastByte = (byte) (source[len - 1 + off] & 0x7f); 524 | if (b4Posn == 0 || b4Posn == 1) { 525 | throw new Base64DecoderException( 526 | "invalid padding byte '=' at byte offset " + i); 527 | } else if ((b4Posn == 3 && bytesLeft > 2) 528 | || (b4Posn == 4 && bytesLeft > 1)) { 529 | throw new Base64DecoderException( 530 | "padding byte '=' falsely signals end of encoded value " 531 | + "at offset " + i); 532 | } else if (lastByte != EQUALS_SIGN && lastByte != NEW_LINE) { 533 | throw new Base64DecoderException( 534 | "encoded value has invalid trailing byte"); 535 | } 536 | break; 537 | } 538 | 539 | b4[b4Posn++] = sbiCrop; 540 | if (b4Posn == 4) { 541 | outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); 542 | b4Posn = 0; 543 | } 544 | } 545 | } else { 546 | throw new Base64DecoderException("Bad Base64 input character at " + i 547 | + ": " + source[i + off] + "(decimal)"); 548 | } 549 | } 550 | 551 | // Because web safe encoding allows non padding base64 encodes, we 552 | // need to pad the rest of the b4 buffer with equal signs when 553 | // b4Posn != 0. There can be at most 2 equal signs at the end of 554 | // four characters, so the b4 buffer must have two or three 555 | // characters. This also catches the case where the input is 556 | // padded with EQUALS_SIGN 557 | if (b4Posn != 0) { 558 | if (b4Posn == 1) { 559 | throw new Base64DecoderException("single trailing character at offset " 560 | + (len - 1)); 561 | } 562 | b4[b4Posn++] = EQUALS_SIGN; 563 | outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, decodabet); 564 | } 565 | 566 | byte[] out = new byte[outBuffPosn]; 567 | System.arraycopy(outBuff, 0, out, 0, outBuffPosn); 568 | return out; 569 | } 570 | } 571 | -------------------------------------------------------------------------------- /android/src/main/java/com/util/Base64DecoderException.java: -------------------------------------------------------------------------------- 1 | // Copyright 2002, Google, Inc. 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | package com.util; 16 | 17 | /** 18 | * Exception thrown when encountering an invalid Base64 input character. 19 | * 20 | * @author nelson 21 | */ 22 | public class Base64DecoderException extends Exception { 23 | public Base64DecoderException() { 24 | super(); 25 | } 26 | 27 | public Base64DecoderException(String s) { 28 | super(s); 29 | } 30 | 31 | private static final long serialVersionUID = 1L; 32 | } 33 | -------------------------------------------------------------------------------- /android/src/main/java/com/util/IabException.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Google Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package com.util; 17 | 18 | /** 19 | * Exception thrown when something went wrong with in-app billing. 20 | * An IabException has an associated IabResult (an error). 21 | * To get the IAB result that caused this exception to be thrown, 22 | * call {@link #getResult()}. 23 | */ 24 | public class IabException extends Exception { 25 | IabResult mResult; 26 | 27 | public IabException(IabResult r) { 28 | this(r, null); 29 | } 30 | public IabException(int response, String message) { 31 | this(new IabResult(response, message)); 32 | } 33 | public IabException(IabResult r, Exception cause) { 34 | super(r.getMessage(), cause); 35 | mResult = r; 36 | } 37 | public IabException(int response, String message, Exception cause) { 38 | this(new IabResult(response, message), cause); 39 | } 40 | 41 | /** Returns the IAB result (error) that this exception signals. */ 42 | public IabResult getResult() { return mResult; } 43 | } 44 | -------------------------------------------------------------------------------- /android/src/main/java/com/util/IabHelper.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Google Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package com.util; 17 | 18 | import android.app.Activity; 19 | import android.app.PendingIntent; 20 | import android.content.ComponentName; 21 | import android.content.Context; 22 | import android.content.Intent; 23 | import android.content.IntentSender; 24 | import android.content.ServiceConnection; 25 | import android.content.pm.PackageManager; 26 | import android.content.pm.ResolveInfo; 27 | import android.os.Bundle; 28 | import android.os.Handler; 29 | import android.os.IBinder; 30 | import android.os.RemoteException; 31 | import android.text.TextUtils; 32 | import android.util.Log; 33 | 34 | import com.android.vending.billing.IInAppBillingService; 35 | 36 | import org.json.JSONException; 37 | 38 | import java.util.ArrayList; 39 | import java.util.List; 40 | 41 | 42 | /** 43 | * Provides convenience methods for in-app billing. You can create one instance of this 44 | * class for your application and use it to process in-app billing operations. 45 | * It provides synchronous (blocking) and asynchronous (non-blocking) methods for 46 | * many common in-app billing operations, as well as automatic signature 47 | * verification. 48 | * 49 | * After instantiating, you must perform setup in order to start using the object. 50 | * To perform setup, call the {@link #startSetup} method and provide a listener; 51 | * that listener will be notified when setup is complete, after which (and not before) 52 | * you may call other methods. 53 | * 54 | * After setup is complete, you will typically want to request an inventory of owned 55 | * items and subscriptions. See {@link #queryInventory}, {@link #queryInventoryAsync} 56 | * and related methods. 57 | * 58 | * When you are done with this object, don't forget to call {@link #dispose} 59 | * to ensure proper cleanup. This object holds a binding to the in-app billing 60 | * service, which will leak unless you dispose of it correctly. If you created 61 | * the object on an Activity's onCreate method, then the recommended 62 | * place to dispose of it is the Activity's onDestroy method. 63 | * 64 | * A note about threading: When using this object from a background thread, you may 65 | * call the blocking versions of methods; when using from a UI thread, call 66 | * only the asynchronous versions and handle the results via callbacks. 67 | * Also, notice that you can only call one asynchronous operation at a time; 68 | * attempting to start a second asynchronous operation while the first one 69 | * has not yet completed will result in an exception being thrown. 70 | * 71 | * @author Bruno Oliveira (Google) 72 | * 73 | */ 74 | public class IabHelper { 75 | // Is debug logging enabled? 76 | boolean mDebugLog = false; 77 | String mDebugTag = "IabHelper"; 78 | 79 | // Is setup done? 80 | boolean mSetupDone = false; 81 | 82 | // Has this object been disposed of? (If so, we should ignore callbacks, etc) 83 | boolean mDisposed = false; 84 | 85 | // Are subscriptions supported? 86 | boolean mSubscriptionsSupported = false; 87 | 88 | // Is an asynchronous operation in progress? 89 | // (only one at a time can be in progress) 90 | boolean mAsyncInProgress = false; 91 | 92 | // (for logging/debugging) 93 | // if mAsyncInProgress == true, what asynchronous operation is in progress? 94 | String mAsyncOperation = ""; 95 | 96 | // Context we were passed during initialization 97 | Context mContext; 98 | 99 | // Connection to the service 100 | IInAppBillingService mService; 101 | ServiceConnection mServiceConn; 102 | 103 | // The request code used to launch purchase flow 104 | int mRequestCode; 105 | 106 | // The item type of the current purchase flow 107 | String mPurchasingItemType; 108 | 109 | // Public key for verifying signature, in base64 encoding 110 | String mSignatureBase64 = null; 111 | 112 | // Billing response codes 113 | public static final int BILLING_RESPONSE_RESULT_OK = 0; 114 | public static final int BILLING_RESPONSE_RESULT_USER_CANCELED = 1; 115 | public static final int BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE = 3; 116 | public static final int BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE = 4; 117 | public static final int BILLING_RESPONSE_RESULT_DEVELOPER_ERROR = 5; 118 | public static final int BILLING_RESPONSE_RESULT_ERROR = 6; 119 | public static final int BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED = 7; 120 | public static final int BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED = 8; 121 | 122 | // IAB Helper error codes 123 | public static final int IABHELPER_ERROR_BASE = -1000; 124 | public static final int IABHELPER_REMOTE_EXCEPTION = -1001; 125 | public static final int IABHELPER_BAD_RESPONSE = -1002; 126 | public static final int IABHELPER_VERIFICATION_FAILED = -1003; 127 | public static final int IABHELPER_SEND_INTENT_FAILED = -1004; 128 | public static final int IABHELPER_USER_CANCELLED = -1005; 129 | public static final int IABHELPER_UNKNOWN_PURCHASE_RESPONSE = -1006; 130 | public static final int IABHELPER_MISSING_TOKEN = -1007; 131 | public static final int IABHELPER_UNKNOWN_ERROR = -1008; 132 | public static final int IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE = -1009; 133 | public static final int IABHELPER_INVALID_CONSUMPTION = -1010; 134 | 135 | // Keys for the responses from InAppBillingService 136 | public static final String RESPONSE_CODE = "RESPONSE_CODE"; 137 | public static final String RESPONSE_GET_SKU_DETAILS_LIST = "DETAILS_LIST"; 138 | public static final String RESPONSE_BUY_INTENT = "BUY_INTENT"; 139 | public static final String RESPONSE_INAPP_PURCHASE_DATA = "INAPP_PURCHASE_DATA"; 140 | public static final String RESPONSE_INAPP_SIGNATURE = "INAPP_DATA_SIGNATURE"; 141 | public static final String RESPONSE_INAPP_ITEM_LIST = "INAPP_PURCHASE_ITEM_LIST"; 142 | public static final String RESPONSE_INAPP_PURCHASE_DATA_LIST = "INAPP_PURCHASE_DATA_LIST"; 143 | public static final String RESPONSE_INAPP_SIGNATURE_LIST = "INAPP_DATA_SIGNATURE_LIST"; 144 | public static final String INAPP_CONTINUATION_TOKEN = "INAPP_CONTINUATION_TOKEN"; 145 | 146 | // Keys for the response from getPurchaseConfig 147 | private static final String INTENT_V2_SUPPORT = "INTENT_V2_SUPPORT"; 148 | 149 | // Item types 150 | public static final String ITEM_TYPE_INAPP = "inapp"; 151 | public static final String ITEM_TYPE_SUBS = "subs"; 152 | 153 | // some fields on the getSkuDetails response bundle 154 | public static final String GET_SKU_DETAILS_ITEM_LIST = "ITEM_ID_LIST"; 155 | public static final String GET_SKU_DETAILS_ITEM_TYPE_LIST = "ITEM_TYPE_LIST"; 156 | 157 | /** 158 | * Creates an instance. After creation, it will not yet be ready to use. You must perform 159 | * setup by calling {@link #startSetup} and wait for setup to complete. This constructor does not 160 | * block and is safe to call from a UI thread. 161 | * 162 | * @param ctx Your application or Activity context. Needed to bind to the in-app billing service. 163 | * @param base64PublicKey Your application's public key, encoded in base64. 164 | * This is used for verification of purchase signatures. You can find your app's base64-encoded 165 | * public key in your application's page on Google Play Developer Console. Note that this 166 | * is NOT your "developer public key". 167 | */ 168 | public IabHelper(Context ctx, String base64PublicKey) { 169 | mContext = ctx.getApplicationContext(); 170 | mSignatureBase64 = base64PublicKey; 171 | logDebug("IAB helper created."); 172 | } 173 | 174 | /** 175 | * Enables or disable debug logging through LogCat. 176 | */ 177 | public void enableDebugLogging(boolean enable, String tag) { 178 | checkNotDisposed(); 179 | mDebugLog = enable; 180 | mDebugTag = tag; 181 | } 182 | 183 | public void enableDebugLogging(boolean enable) { 184 | checkNotDisposed(); 185 | mDebugLog = enable; 186 | } 187 | 188 | /** 189 | * Callback for setup process. This listener's {@link #onIabSetupFinished} method is called 190 | * when the setup process is complete. 191 | */ 192 | public interface OnIabSetupFinishedListener { 193 | /** 194 | * Called to notify that setup is complete. 195 | * 196 | * @param result The result of the setup process. 197 | */ 198 | public void onIabSetupFinished(IabResult result); 199 | } 200 | 201 | /** 202 | * Starts the setup process. This will start up the setup process asynchronously. 203 | * You will be notified through the listener when the setup process is complete. 204 | * This method is safe to call from a UI thread. 205 | * 206 | * @param listener The listener to notify when the setup process is complete. 207 | */ 208 | public void startSetup(String vendorIntent, String vendorPackage, final OnIabSetupFinishedListener listener) { 209 | // If already set up, can't do it again. 210 | checkNotDisposed(); 211 | if (mSetupDone) throw new IllegalStateException("IAB helper is already set up."); 212 | 213 | // Connection to IAB service 214 | logDebug("Starting in-app billing setup."); 215 | mServiceConn = new ServiceConnection() { 216 | @Override 217 | public void onServiceDisconnected(ComponentName name) { 218 | logDebug("Billing service disconnected."); 219 | mService = null; 220 | } 221 | 222 | @Override 223 | public void onServiceConnected(ComponentName name, IBinder service) { 224 | if (mDisposed) return; 225 | logDebug("Billing service connected."); 226 | mService = IInAppBillingService.Stub.asInterface(service); 227 | String packageName = mContext.getPackageName(); 228 | try { 229 | logDebug("Checking for in-app billing 3 support."); 230 | 231 | // check for in-app billing v3 support 232 | int response = mService.isBillingSupported(3, packageName, ITEM_TYPE_INAPP); 233 | if (response != BILLING_RESPONSE_RESULT_OK) { 234 | if (listener != null) listener.onIabSetupFinished(new IabResult(response, 235 | "Error checking for billing v3 support.")); 236 | 237 | // if in-app purchases aren't supported, neither are subscriptions. 238 | mSubscriptionsSupported = false; 239 | return; 240 | } 241 | logDebug("In-app billing version 3 supported for " + packageName); 242 | 243 | // check for v3 subscriptions support 244 | response = mService.isBillingSupported(3, packageName, ITEM_TYPE_SUBS); 245 | if (response == BILLING_RESPONSE_RESULT_OK) { 246 | logDebug("Subscriptions AVAILABLE."); 247 | mSubscriptionsSupported = true; 248 | } 249 | else { 250 | logDebug("Subscriptions NOT AVAILABLE. Response: " + response); 251 | } 252 | 253 | mSetupDone = true; 254 | } 255 | catch (RemoteException e) { 256 | if (listener != null) { 257 | listener.onIabSetupFinished(new IabResult(IABHELPER_REMOTE_EXCEPTION, 258 | "RemoteException while setting up in-app billing.")); 259 | } 260 | e.printStackTrace(); 261 | return; 262 | } 263 | 264 | if (listener != null) { 265 | listener.onIabSetupFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Setup successful.")); 266 | } 267 | } 268 | }; 269 | 270 | Intent serviceIntent = new Intent(vendorIntent); 271 | serviceIntent.setPackage(vendorPackage); 272 | 273 | PackageManager pm=mContext.getPackageManager(); 274 | List intentServices = pm.queryIntentServices(serviceIntent, 0); 275 | if (intentServices != null && !intentServices.isEmpty()) { 276 | // service available to handle that Intent 277 | mContext.bindService(serviceIntent, mServiceConn, Context.BIND_AUTO_CREATE); 278 | } 279 | else { 280 | // no service available to handle that Intent 281 | if (listener != null) { 282 | listener.onIabSetupFinished( 283 | new IabResult(BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, 284 | "Billing service unavailable on device.")); 285 | } 286 | } 287 | } 288 | 289 | /** 290 | * Dispose of object, releasing resources. It's very important to call this 291 | * method when you are done with this object. It will release any resources 292 | * used by it such as service connections. Naturally, once the object is 293 | * disposed of, it can't be used again. 294 | */ 295 | public void dispose() { 296 | logDebug("Disposing."); 297 | mSetupDone = false; 298 | if (mServiceConn != null) { 299 | logDebug("Unbinding from service."); 300 | if (mContext != null && mService != null) { 301 | mContext.unbindService(mServiceConn); 302 | } 303 | } 304 | mDisposed = true; 305 | mContext = null; 306 | mServiceConn = null; 307 | mService = null; 308 | mPurchaseListener = null; 309 | } 310 | 311 | private void checkNotDisposed() { 312 | if (mDisposed) throw new IllegalStateException("IabHelper was disposed of, so it cannot be used."); 313 | } 314 | 315 | /** Returns whether subscriptions are supported. */ 316 | public boolean subscriptionsSupported() { 317 | checkNotDisposed(); 318 | return mSubscriptionsSupported; 319 | } 320 | 321 | 322 | /** 323 | * Callback that notifies when a purchase is finished. 324 | */ 325 | public interface OnIabPurchaseFinishedListener { 326 | /** 327 | * Called to notify that an in-app purchase finished. If the purchase was successful, 328 | * then the sku parameter specifies which item was purchased. If the purchase failed, 329 | * the sku and extraData parameters may or may not be null, depending on how far the purchase 330 | * process went. 331 | * 332 | * @param result The result of the purchase. 333 | * @param info The purchase information (null if purchase failed) 334 | */ 335 | public void onIabPurchaseFinished(IabResult result, Purchase info); 336 | } 337 | 338 | // The listener registered on launchPurchaseFlow, which we have to call back when 339 | // the purchase finishes 340 | OnIabPurchaseFinishedListener mPurchaseListener; 341 | 342 | public void launchPurchaseFlow(Activity act, String sku, int requestCode, OnIabPurchaseFinishedListener listener) { 343 | launchPurchaseFlow(act, sku, requestCode, listener, ""); 344 | } 345 | 346 | public void launchPurchaseFlow(Activity act, String sku, int requestCode, 347 | OnIabPurchaseFinishedListener listener, String extraData) { 348 | launchPurchaseFlow(act, sku, ITEM_TYPE_INAPP, requestCode, listener, extraData); 349 | } 350 | 351 | public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, 352 | OnIabPurchaseFinishedListener listener) { 353 | launchSubscriptionPurchaseFlow(act, sku, requestCode, listener, ""); 354 | } 355 | 356 | public void launchSubscriptionPurchaseFlow(Activity act, String sku, int requestCode, 357 | OnIabPurchaseFinishedListener listener, String extraData) { 358 | launchPurchaseFlow(act, sku, ITEM_TYPE_SUBS, requestCode, listener, extraData); 359 | } 360 | 361 | /** 362 | * Initiate the UI flow for an in-app purchase. Call this method to initiate an in-app purchase, 363 | * which will involve bringing up the Google Play screen. The calling activity will be paused while 364 | * the user interacts with Google Play, and the result will be delivered via the activity's 365 | * {@link android.app.Activity#onActivityResult} method, at which point you must call 366 | * this object's {@link #handleActivityResult} method to continue the purchase flow. This method 367 | * MUST be called from the UI thread of the Activity. 368 | * 369 | * @param act The calling activity. 370 | * @param sku The sku of the item to purchase. 371 | * @param itemType indicates if it's a product or a subscription (ITEM_TYPE_INAPP or ITEM_TYPE_SUBS) 372 | * @param requestCode A request code (to differentiate from other responses -- 373 | * as in {@link android.app.Activity#startActivityForResult}). 374 | * @param listener The listener to notify when the purchase process finishes 375 | * @param extraData Extra data (developer payload), which will be returned with the purchase data 376 | * when the purchase completes. This extra data will be permanently bound to that purchase 377 | * and will always be returned when the purchase is queried. 378 | */ 379 | public void launchPurchaseFlow(Activity act, String sku, String itemType, int requestCode, 380 | OnIabPurchaseFinishedListener listener, String extraData) { 381 | checkNotDisposed(); 382 | checkSetupDone("launchPurchaseFlow"); 383 | flagStartAsync("launchPurchaseFlow"); 384 | IabResult result; 385 | 386 | if (itemType.equals(ITEM_TYPE_SUBS) && !mSubscriptionsSupported) { 387 | IabResult r = new IabResult(IABHELPER_SUBSCRIPTIONS_NOT_AVAILABLE, 388 | "Subscriptions are not available."); 389 | flagEndAsync(); 390 | if (listener != null) listener.onIabPurchaseFinished(r, null); 391 | return; 392 | } 393 | 394 | try { 395 | logDebug("Constructing buy intent for " + sku + ", item type: " + itemType); 396 | 397 | int apiVersion = 3; 398 | String packageName = mContext.getPackageName(); 399 | 400 | Bundle configBundle = mService.getPurchaseConfig(apiVersion); 401 | if (configBundle.getBoolean(INTENT_V2_SUPPORT)) { 402 | logDebug("launchBuyIntentV2 for " + sku + ", item type: " + itemType); 403 | launchBuyIntentV2(act, sku, itemType, requestCode, listener, extraData); 404 | } else { 405 | logDebug("launchBuyIntent for " + sku + ", item type: " + itemType); 406 | launchBuyIntent(act, sku, itemType, requestCode, listener, extraData); 407 | } 408 | } catch (IntentSender.SendIntentException e) { 409 | logError("SendIntentException while launching purchase flow for sku " + sku); 410 | e.printStackTrace(); 411 | flagEndAsync(); 412 | 413 | result = new IabResult(IABHELPER_SEND_INTENT_FAILED, "Failed to send intent."); 414 | if (listener != null) listener.onIabPurchaseFinished(result, null); 415 | } catch (RemoteException e) { 416 | logError("RemoteException while launching purchase flow for sku " + sku); 417 | e.printStackTrace(); 418 | flagEndAsync(); 419 | 420 | result = new IabResult(IABHELPER_REMOTE_EXCEPTION, "Remote exception while starting purchase flow"); 421 | if (listener != null) listener.onIabPurchaseFinished(result, null); 422 | } 423 | } 424 | 425 | private void launchBuyIntentV2( 426 | Activity act, 427 | String sku, 428 | String itemType, 429 | int requestCode, 430 | OnIabPurchaseFinishedListener listener, 431 | String extraData 432 | ) throws RemoteException { 433 | int apiVersion = 3; 434 | String packageName = mContext.getPackageName(); 435 | 436 | Bundle buyIntentBundle = mService.getBuyIntentV2(apiVersion, packageName, sku, itemType, extraData); 437 | int response = getResponseCodeFromBundle(buyIntentBundle); 438 | if (response != BILLING_RESPONSE_RESULT_OK) { 439 | logError("Unable to buy item, Error response: " + getResponseDesc(response)); 440 | flagEndAsync(); 441 | IabResult result = new IabResult(response, "Unable to buy item"); 442 | if (listener != null) listener.onIabPurchaseFinished(result, null); 443 | return; 444 | } 445 | 446 | Intent intent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); 447 | logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode); 448 | mRequestCode = requestCode; 449 | mPurchaseListener = listener; 450 | mPurchasingItemType = itemType; 451 | act.startActivityForResult(intent, requestCode); 452 | } 453 | 454 | private void launchBuyIntent( 455 | Activity act, 456 | String sku, 457 | String itemType, 458 | int requestCode, 459 | OnIabPurchaseFinishedListener listener, 460 | String extraData 461 | ) throws RemoteException, IntentSender.SendIntentException { 462 | 463 | int apiVersion = 3; 464 | String packageName = mContext.getPackageName(); 465 | 466 | Bundle buyIntentBundle = mService.getBuyIntent(apiVersion, packageName, sku, itemType, extraData); 467 | int response = getResponseCodeFromBundle(buyIntentBundle); 468 | if (response != BILLING_RESPONSE_RESULT_OK) { 469 | logError("Unable to buy item, Error response: " + getResponseDesc(response)); 470 | flagEndAsync(); 471 | IabResult result = new IabResult(response, "Unable to buy item"); 472 | if (listener != null) listener.onIabPurchaseFinished(result, null); 473 | return; 474 | } 475 | 476 | 477 | PendingIntent pendingIntent = buyIntentBundle.getParcelable(RESPONSE_BUY_INTENT); 478 | logDebug("Launching buy intent for " + sku + ". Request code: " + requestCode); 479 | mRequestCode = requestCode; 480 | mPurchaseListener = listener; 481 | mPurchasingItemType = itemType; 482 | act.startIntentSenderForResult(pendingIntent.getIntentSender(), 483 | requestCode, new Intent(), 484 | Integer.valueOf(0), Integer.valueOf(0), 485 | Integer.valueOf(0)); 486 | } 487 | 488 | /** 489 | * Handles an activity result that's part of the purchase flow in in-app billing. If you 490 | * are calling {@link #launchPurchaseFlow}, then you must call this method from your 491 | * Activity's {@link android.app.Activity@onActivityResult} method. This method 492 | * MUST be called from the UI thread of the Activity. 493 | * 494 | * @param requestCode The requestCode as you received it. 495 | * @param resultCode The resultCode as you received it. 496 | * @param data The data (Intent) as you received it. 497 | * @return Returns true if the result was related to a purchase flow and was handled; 498 | * false if the result was not related to a purchase, in which case you should 499 | * handle it normally. 500 | */ 501 | public boolean handleActivityResult(int requestCode, int resultCode, Intent data) { 502 | IabResult result; 503 | if (requestCode != mRequestCode) return false; 504 | 505 | checkNotDisposed(); 506 | checkSetupDone("handleActivityResult"); 507 | 508 | // end of async purchase operation that started on launchPurchaseFlow 509 | flagEndAsync(); 510 | 511 | if (data == null) { 512 | logError("Null data in IAB activity result."); 513 | result = new IabResult(IABHELPER_BAD_RESPONSE, "Null data in IAB result"); 514 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); 515 | return true; 516 | } 517 | 518 | int responseCode = getResponseCodeFromIntent(data); 519 | String purchaseData = data.getStringExtra(RESPONSE_INAPP_PURCHASE_DATA); 520 | String dataSignature = data.getStringExtra(RESPONSE_INAPP_SIGNATURE); 521 | 522 | if (resultCode == Activity.RESULT_OK && responseCode == BILLING_RESPONSE_RESULT_OK) { 523 | logDebug("Successful resultcode from purchase activity."); 524 | logDebug("Purchase data: " + purchaseData); 525 | logDebug("Data signature: " + dataSignature); 526 | logDebug("Extras: " + data.getExtras()); 527 | logDebug("Expected item type: " + mPurchasingItemType); 528 | 529 | if (purchaseData == null || dataSignature == null) { 530 | logError("BUG: either purchaseData or dataSignature is null."); 531 | logDebug("Extras: " + data.getExtras().toString()); 532 | result = new IabResult(IABHELPER_UNKNOWN_ERROR, "IAB returned null purchaseData or dataSignature"); 533 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); 534 | return true; 535 | } 536 | 537 | Purchase purchase = null; 538 | try { 539 | purchase = new Purchase(mPurchasingItemType, purchaseData, dataSignature); 540 | String sku = purchase.getSku(); 541 | 542 | // Verify signature 543 | if (!Security.verifyPurchase(mSignatureBase64, purchaseData, dataSignature)) { 544 | logError("Purchase signature verification FAILED for sku " + sku); 545 | result = new IabResult(IABHELPER_VERIFICATION_FAILED, "Signature verification failed for sku " + sku); 546 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, purchase); 547 | return true; 548 | } 549 | logDebug("Purchase signature successfully verified."); 550 | } 551 | catch (JSONException e) { 552 | logError("Failed to parse purchase data."); 553 | e.printStackTrace(); 554 | result = new IabResult(IABHELPER_BAD_RESPONSE, "Failed to parse purchase data."); 555 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); 556 | return true; 557 | } 558 | 559 | if (mPurchaseListener != null) { 560 | mPurchaseListener.onIabPurchaseFinished(new IabResult(BILLING_RESPONSE_RESULT_OK, "Success"), purchase); 561 | } 562 | } 563 | else if (resultCode == Activity.RESULT_OK) { 564 | // result code was OK, but in-app billing response was not OK. 565 | logDebug("Result code was OK but in-app billing response was not OK: " + getResponseDesc(responseCode)); 566 | if (mPurchaseListener != null) { 567 | result = new IabResult(responseCode, "Problem purchashing item."); 568 | mPurchaseListener.onIabPurchaseFinished(result, null); 569 | } 570 | } 571 | else if (resultCode == Activity.RESULT_CANCELED) { 572 | logDebug("Purchase canceled - Response: " + getResponseDesc(responseCode)); 573 | result = new IabResult(IABHELPER_USER_CANCELLED, "User canceled."); 574 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); 575 | } 576 | else { 577 | logError("Purchase failed. Result code: " + Integer.toString(resultCode) 578 | + ". Response: " + getResponseDesc(responseCode)); 579 | result = new IabResult(IABHELPER_UNKNOWN_PURCHASE_RESPONSE, "Unknown purchase response."); 580 | if (mPurchaseListener != null) mPurchaseListener.onIabPurchaseFinished(result, null); 581 | } 582 | return true; 583 | } 584 | 585 | public Inventory queryInventory(boolean querySkuDetails, List moreSkus) throws IabException { 586 | return queryInventory(querySkuDetails, moreSkus, null); 587 | } 588 | 589 | /** 590 | * Queries the inventory. This will query all owned items from the server, as well as 591 | * information on additional skus, if specified. This method may block or take long to execute. 592 | * Do not call from a UI thread. 593 | * 594 | * @param querySkuDetails if true, SKU details (price, description, etc) will be queried as well 595 | * as purchase information. 596 | * @param moreItemSkus additional PRODUCT skus to query information on, regardless of ownership. 597 | * Ignored if null or if querySkuDetails is false. 598 | * @param moreSubsSkus additional SUBSCRIPTIONS skus to query information on, regardless of ownership. 599 | * Ignored if null or if querySkuDetails is false. 600 | * @throws IabException if a problem occurs while refreshing the inventory. 601 | */ 602 | public Inventory queryInventory(boolean querySkuDetails, List moreItemSkus, 603 | List moreSubsSkus) throws IabException { 604 | checkNotDisposed(); 605 | checkSetupDone("queryInventory"); 606 | try { 607 | Inventory inv = new Inventory(); 608 | int r = queryPurchases(inv, ITEM_TYPE_INAPP); 609 | if (r != BILLING_RESPONSE_RESULT_OK) { 610 | throw new IabException(r, "Error refreshing inventory (querying owned items)."); 611 | } 612 | 613 | if (querySkuDetails) { 614 | r = querySkuDetails(ITEM_TYPE_INAPP, inv, moreItemSkus); 615 | if (r != BILLING_RESPONSE_RESULT_OK) { 616 | throw new IabException(r, "Error refreshing inventory (querying prices of items)."); 617 | } 618 | } 619 | 620 | // if subscriptions are supported, then also query for subscriptions 621 | if (mSubscriptionsSupported) { 622 | r = queryPurchases(inv, ITEM_TYPE_SUBS); 623 | if (r != BILLING_RESPONSE_RESULT_OK) { 624 | throw new IabException(r, "Error refreshing inventory (querying owned subscriptions)."); 625 | } 626 | 627 | if (querySkuDetails) { 628 | r = querySkuDetails(ITEM_TYPE_SUBS, inv, moreItemSkus); 629 | if (r != BILLING_RESPONSE_RESULT_OK) { 630 | throw new IabException(r, "Error refreshing inventory (querying prices of subscriptions)."); 631 | } 632 | } 633 | } 634 | 635 | return inv; 636 | } 637 | catch (RemoteException e) { 638 | throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while refreshing inventory.", e); 639 | } 640 | catch (JSONException e) { 641 | throw new IabException(IABHELPER_BAD_RESPONSE, "Error parsing JSON response while refreshing inventory.", e); 642 | } 643 | } 644 | 645 | /** 646 | * Listener that notifies when an inventory query operation completes. 647 | */ 648 | public interface QueryInventoryFinishedListener { 649 | /** 650 | * Called to notify that an inventory query operation completed. 651 | * 652 | * @param result The result of the operation. 653 | * @param inv The inventory. 654 | */ 655 | public void onQueryInventoryFinished(IabResult result, Inventory inv); 656 | } 657 | 658 | 659 | /** 660 | * Asynchronous wrapper for inventory query. This will perform an inventory 661 | * query as described in {@link #queryInventory}, but will do so asynchronously 662 | * and call back the specified listener upon completion. This method is safe to 663 | * call from a UI thread. 664 | * 665 | * @param querySkuDetails as in {@link #queryInventory} 666 | * @param moreSkus as in {@link #queryInventory} 667 | * @param listener The listener to notify when the refresh operation completes. 668 | */ 669 | public void queryInventoryAsync(final boolean querySkuDetails, 670 | final List moreSkus, 671 | final QueryInventoryFinishedListener listener) { 672 | final Handler handler = new Handler(); 673 | checkNotDisposed(); 674 | checkSetupDone("queryInventory"); 675 | flagStartAsync("refresh inventory"); 676 | (new Thread(new Runnable() { 677 | public void run() { 678 | IabResult result = new IabResult(BILLING_RESPONSE_RESULT_OK, "Inventory refresh successful."); 679 | Inventory inv = null; 680 | try { 681 | inv = queryInventory(querySkuDetails, moreSkus); 682 | } 683 | catch (IabException ex) { 684 | result = ex.getResult(); 685 | } 686 | 687 | flagEndAsync(); 688 | 689 | final IabResult result_f = result; 690 | final Inventory inv_f = inv; 691 | if (!mDisposed && listener != null) { 692 | handler.post(new Runnable() { 693 | public void run() { 694 | listener.onQueryInventoryFinished(result_f, inv_f); 695 | } 696 | }); 697 | } 698 | } 699 | })).start(); 700 | } 701 | 702 | public void queryInventoryAsync(QueryInventoryFinishedListener listener) { 703 | queryInventoryAsync(true, null, listener); 704 | } 705 | 706 | public void queryInventoryAsync(boolean querySkuDetails, QueryInventoryFinishedListener listener) { 707 | queryInventoryAsync(querySkuDetails, null, listener); 708 | } 709 | 710 | 711 | /** 712 | * Consumes a given in-app product. Consuming can only be done on an item 713 | * that's owned, and as a result of consumption, the user will no longer own it. 714 | * This method may block or take long to return. Do not call from the UI thread. 715 | * For that, see {@link #consumeAsync}. 716 | * 717 | * @param itemInfo The PurchaseInfo that represents the item to consume. 718 | * @throws IabException if there is a problem during consumption. 719 | */ 720 | void consume(Purchase itemInfo) throws IabException { 721 | checkNotDisposed(); 722 | checkSetupDone("consume"); 723 | 724 | if (!itemInfo.mItemType.equals(ITEM_TYPE_INAPP)) { 725 | throw new IabException(IABHELPER_INVALID_CONSUMPTION, 726 | "Items of type '" + itemInfo.mItemType + "' can't be consumed."); 727 | } 728 | 729 | try { 730 | String token = itemInfo.getToken(); 731 | String sku = itemInfo.getSku(); 732 | if (token == null || token.equals("")) { 733 | logError("Can't consume "+ sku + ". No token."); 734 | throw new IabException(IABHELPER_MISSING_TOKEN, "PurchaseInfo is missing token for sku: " 735 | + sku + " " + itemInfo); 736 | } 737 | 738 | logDebug("Consuming sku: " + sku + ", token: " + token); 739 | int response = mService.consumePurchase(3, mContext.getPackageName(), token); 740 | if (response == BILLING_RESPONSE_RESULT_OK) { 741 | logDebug("Successfully consumed sku: " + sku); 742 | } 743 | else { 744 | logDebug("Error consuming consuming sku " + sku + ". " + getResponseDesc(response)); 745 | throw new IabException(response, "Error consuming sku " + sku); 746 | } 747 | } 748 | catch (RemoteException e) { 749 | throw new IabException(IABHELPER_REMOTE_EXCEPTION, "Remote exception while consuming. PurchaseInfo: " + itemInfo, e); 750 | } 751 | } 752 | 753 | /** 754 | * Callback that notifies when a consumption operation finishes. 755 | */ 756 | public interface OnConsumeFinishedListener { 757 | /** 758 | * Called to notify that a consumption has finished. 759 | * 760 | * @param purchase The purchase that was (or was to be) consumed. 761 | * @param result The result of the consumption operation. 762 | */ 763 | public void onConsumeFinished(Purchase purchase, IabResult result); 764 | } 765 | 766 | /** 767 | * Callback that notifies when a multi-item consumption operation finishes. 768 | */ 769 | public interface OnConsumeMultiFinishedListener { 770 | /** 771 | * Called to notify that a consumption of multiple items has finished. 772 | * 773 | * @param purchases The purchases that were (or were to be) consumed. 774 | * @param results The results of each consumption operation, corresponding to each 775 | * sku. 776 | */ 777 | public void onConsumeMultiFinished(List purchases, List results); 778 | } 779 | 780 | /** 781 | * Asynchronous wrapper to item consumption. Works like {@link #consume}, but 782 | * performs the consumption in the background and notifies completion through 783 | * the provided listener. This method is safe to call from a UI thread. 784 | * 785 | * @param purchase The purchase to be consumed. 786 | * @param listener The listener to notify when the consumption operation finishes. 787 | */ 788 | public void consumeAsync(Purchase purchase, OnConsumeFinishedListener listener) { 789 | checkNotDisposed(); 790 | checkSetupDone("consume"); 791 | List purchases = new ArrayList(); 792 | purchases.add(purchase); 793 | consumeAsyncInternal(purchases, listener, null); 794 | } 795 | 796 | /** 797 | * Same as {@link #consumeAsync(Purchase, OnConsumeFinishedListener)}, but for multiple items at once. 798 | * 799 | * @param purchases The list of PurchaseInfo objects representing the purchases to consume. 800 | * @param listener The listener to notify when the consumption operation finishes. 801 | */ 802 | public void consumeAsync(List purchases, OnConsumeMultiFinishedListener listener) { 803 | checkNotDisposed(); 804 | checkSetupDone("consume"); 805 | consumeAsyncInternal(purchases, null, listener); 806 | } 807 | 808 | /** 809 | * Returns a human-readable description for the given response code. 810 | * 811 | * @param code The response code 812 | * @return A human-readable string explaining the result code. 813 | * It also includes the result code numerically. 814 | */ 815 | public static String getResponseDesc(int code) { 816 | String[] iab_msgs = ("0:OK/1:User Canceled/2:Unknown/" + 817 | "3:Billing Unavailable/4:Item unavailable/" + 818 | "5:Developer Error/6:Error/7:Item Already Owned/" + 819 | "8:Item not owned").split("/"); 820 | String[] iabhelper_msgs = ("0:OK/-1001:Remote exception during initialization/" + 821 | "-1002:Bad response received/" + 822 | "-1003:Purchase signature verification failed/" + 823 | "-1004:Send intent failed/" + 824 | "-1005:User cancelled/" + 825 | "-1006:Unknown purchase response/" + 826 | "-1007:Missing token/" + 827 | "-1008:Unknown error/" + 828 | "-1009:Subscriptions not available/" + 829 | "-1010:Invalid consumption attempt").split("/"); 830 | 831 | if (code <= IABHELPER_ERROR_BASE) { 832 | int index = IABHELPER_ERROR_BASE - code; 833 | if (index >= 0 && index < iabhelper_msgs.length) return iabhelper_msgs[index]; 834 | else return String.valueOf(code) + ":Unknown IAB Helper Error"; 835 | } 836 | else if (code < 0 || code >= iab_msgs.length) 837 | return String.valueOf(code) + ":Unknown"; 838 | else 839 | return iab_msgs[code]; 840 | } 841 | 842 | 843 | // Checks that setup was done; if not, throws an exception. 844 | void checkSetupDone(String operation) { 845 | if (!mSetupDone) { 846 | logError("Illegal state for operation (" + operation + "): IAB helper is not set up."); 847 | throw new IllegalStateException("IAB helper is not set up. Can't perform operation: " + operation); 848 | } 849 | } 850 | 851 | // Workaround to bug where sometimes response codes come as Long instead of Integer 852 | int getResponseCodeFromBundle(Bundle b) { 853 | Object o = b.get(RESPONSE_CODE); 854 | if (o == null) { 855 | logDebug("Bundle with null response code, assuming OK (known issue)"); 856 | return BILLING_RESPONSE_RESULT_OK; 857 | } 858 | else if (o instanceof Integer) return ((Integer)o).intValue(); 859 | else if (o instanceof Long) return (int)((Long)o).longValue(); 860 | else { 861 | logError("Unexpected type for bundle response code."); 862 | logError(o.getClass().getName()); 863 | throw new RuntimeException("Unexpected type for bundle response code: " + o.getClass().getName()); 864 | } 865 | } 866 | 867 | // Workaround to bug where sometimes response codes come as Long instead of Integer 868 | int getResponseCodeFromIntent(Intent i) { 869 | Object o = i.getExtras().get(RESPONSE_CODE); 870 | if (o == null) { 871 | logError("Intent with no response code, assuming OK (known issue)"); 872 | return BILLING_RESPONSE_RESULT_OK; 873 | } 874 | else if (o instanceof Integer) return ((Integer)o).intValue(); 875 | else if (o instanceof Long) return (int)((Long)o).longValue(); 876 | else { 877 | logError("Unexpected type for intent response code."); 878 | logError(o.getClass().getName()); 879 | throw new RuntimeException("Unexpected type for intent response code: " + o.getClass().getName()); 880 | } 881 | } 882 | 883 | void flagStartAsync(String operation) { 884 | if (mAsyncInProgress) throw new IllegalStateException("Can't start async operation (" + 885 | operation + ") because another async operation(" + mAsyncOperation + ") is in progress."); 886 | mAsyncOperation = operation; 887 | mAsyncInProgress = true; 888 | logDebug("Starting async operation: " + operation); 889 | } 890 | 891 | void flagEndAsync() { 892 | logDebug("Ending async operation: " + mAsyncOperation); 893 | mAsyncOperation = ""; 894 | mAsyncInProgress = false; 895 | } 896 | 897 | 898 | int queryPurchases(Inventory inv, String itemType) throws JSONException, RemoteException { 899 | // Query purchases 900 | logDebug("Querying owned items, item type: " + itemType); 901 | logDebug("Package name: " + mContext.getPackageName()); 902 | boolean verificationFailed = false; 903 | String continueToken = null; 904 | 905 | do { 906 | logDebug("Calling getPurchases with continuation token: " + continueToken); 907 | Bundle ownedItems = mService.getPurchases(3, mContext.getPackageName(), 908 | itemType, continueToken); 909 | 910 | int response = getResponseCodeFromBundle(ownedItems); 911 | logDebug("Owned items response: " + String.valueOf(response)); 912 | if (response != BILLING_RESPONSE_RESULT_OK) { 913 | logDebug("getPurchases() failed: " + getResponseDesc(response)); 914 | return response; 915 | } 916 | if (!ownedItems.containsKey(RESPONSE_INAPP_ITEM_LIST) 917 | || !ownedItems.containsKey(RESPONSE_INAPP_PURCHASE_DATA_LIST) 918 | || !ownedItems.containsKey(RESPONSE_INAPP_SIGNATURE_LIST)) { 919 | logError("Bundle returned from getPurchases() doesn't contain required fields."); 920 | return IABHELPER_BAD_RESPONSE; 921 | } 922 | 923 | ArrayList ownedSkus = ownedItems.getStringArrayList( 924 | RESPONSE_INAPP_ITEM_LIST); 925 | ArrayList purchaseDataList = ownedItems.getStringArrayList( 926 | RESPONSE_INAPP_PURCHASE_DATA_LIST); 927 | ArrayList signatureList = ownedItems.getStringArrayList( 928 | RESPONSE_INAPP_SIGNATURE_LIST); 929 | 930 | for (int i = 0; i < purchaseDataList.size(); ++i) { 931 | String purchaseData = purchaseDataList.get(i); 932 | String signature = signatureList.get(i); 933 | String sku = ownedSkus.get(i); 934 | if (Security.verifyPurchase(mSignatureBase64, purchaseData, signature)) { 935 | logDebug("Sku is owned: " + sku); 936 | Purchase purchase = new Purchase(itemType, purchaseData, signature); 937 | 938 | if (TextUtils.isEmpty(purchase.getToken())) { 939 | logWarn("BUG: empty/null token!"); 940 | logDebug("Purchase data: " + purchaseData); 941 | } 942 | 943 | // Record ownership and token 944 | inv.addPurchase(purchase); 945 | } 946 | else { 947 | logWarn("Purchase signature verification **FAILED**. Not adding item."); 948 | logDebug(" Purchase data: " + purchaseData); 949 | logDebug(" Signature: " + signature); 950 | verificationFailed = true; 951 | } 952 | } 953 | 954 | continueToken = ownedItems.getString(INAPP_CONTINUATION_TOKEN); 955 | logDebug("Continuation token: " + continueToken); 956 | } while (!TextUtils.isEmpty(continueToken)); 957 | 958 | return verificationFailed ? IABHELPER_VERIFICATION_FAILED : BILLING_RESPONSE_RESULT_OK; 959 | } 960 | 961 | int querySkuDetails(String itemType, Inventory inv, List moreSkus) 962 | throws RemoteException, JSONException { 963 | logDebug("Querying SKU details."); 964 | ArrayList skuList = new ArrayList(); 965 | skuList.addAll(inv.getAllOwnedSkus(itemType)); 966 | if (moreSkus != null) { 967 | for (String sku : moreSkus) { 968 | if (!skuList.contains(sku)) { 969 | skuList.add(sku); 970 | } 971 | } 972 | } 973 | 974 | if (skuList.size() == 0) { 975 | logDebug("queryPrices: nothing to do because there are no SKUs."); 976 | return BILLING_RESPONSE_RESULT_OK; 977 | } 978 | 979 | Bundle querySkus = new Bundle(); 980 | querySkus.putStringArrayList(GET_SKU_DETAILS_ITEM_LIST, skuList); 981 | Bundle skuDetails = mService.getSkuDetails(3, mContext.getPackageName(), 982 | itemType, querySkus); 983 | 984 | if (!skuDetails.containsKey(RESPONSE_GET_SKU_DETAILS_LIST)) { 985 | int response = getResponseCodeFromBundle(skuDetails); 986 | if (response != BILLING_RESPONSE_RESULT_OK) { 987 | logDebug("getSkuDetails() failed: " + getResponseDesc(response)); 988 | return response; 989 | } 990 | else { 991 | logError("getSkuDetails() returned a bundle with neither an error nor a detail list."); 992 | return IABHELPER_BAD_RESPONSE; 993 | } 994 | } 995 | 996 | ArrayList responseList = skuDetails.getStringArrayList( 997 | RESPONSE_GET_SKU_DETAILS_LIST); 998 | 999 | for (String thisResponse : responseList) { 1000 | SkuDetails d = new SkuDetails(itemType, thisResponse); 1001 | logDebug("Got sku details: " + d); 1002 | inv.addSkuDetails(d); 1003 | } 1004 | return BILLING_RESPONSE_RESULT_OK; 1005 | } 1006 | 1007 | 1008 | void consumeAsyncInternal(final List purchases, 1009 | final OnConsumeFinishedListener singleListener, 1010 | final OnConsumeMultiFinishedListener multiListener) { 1011 | final Handler handler = new Handler(); 1012 | flagStartAsync("consume"); 1013 | (new Thread(new Runnable() { 1014 | public void run() { 1015 | final List results = new ArrayList(); 1016 | for (Purchase purchase : purchases) { 1017 | try { 1018 | consume(purchase); 1019 | results.add(new IabResult(BILLING_RESPONSE_RESULT_OK, "Successful consume of sku " + purchase.getSku())); 1020 | } 1021 | catch (IabException ex) { 1022 | results.add(ex.getResult()); 1023 | } 1024 | } 1025 | 1026 | flagEndAsync(); 1027 | if (!mDisposed && singleListener != null) { 1028 | handler.post(new Runnable() { 1029 | public void run() { 1030 | singleListener.onConsumeFinished(purchases.get(0), results.get(0)); 1031 | } 1032 | }); 1033 | } 1034 | if (!mDisposed && multiListener != null) { 1035 | handler.post(new Runnable() { 1036 | public void run() { 1037 | multiListener.onConsumeMultiFinished(purchases, results); 1038 | } 1039 | }); 1040 | } 1041 | } 1042 | })).start(); 1043 | } 1044 | 1045 | void logDebug(String msg) { 1046 | if (mDebugLog) Log.d(mDebugTag, msg); 1047 | } 1048 | 1049 | void logError(String msg) { 1050 | Log.e(mDebugTag, "In-app billing error: " + msg); 1051 | } 1052 | 1053 | void logWarn(String msg) { 1054 | Log.w(mDebugTag, "In-app billing warning: " + msg); 1055 | } 1056 | } 1057 | -------------------------------------------------------------------------------- /android/src/main/java/com/util/IabResult.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Google Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package com.util; 17 | 18 | /** 19 | * Represents the result of an in-app billing operation. 20 | * A result is composed of a response code (an integer) and possibly a 21 | * message (String). You can get those by calling 22 | * {@link #getResponse} and {@link #getMessage()}, respectively. You 23 | * can also inquire whether a result is a success or a failure by 24 | * calling {@link #isSuccess()} and {@link #isFailure()}. 25 | */ 26 | public class IabResult { 27 | int mResponse; 28 | String mMessage; 29 | 30 | public IabResult(int response, String message) { 31 | mResponse = response; 32 | if (message == null || message.trim().length() == 0) { 33 | mMessage = IabHelper.getResponseDesc(response); 34 | } 35 | else { 36 | mMessage = message + " (response: " + IabHelper.getResponseDesc(response) + ")"; 37 | } 38 | } 39 | public int getResponse() { return mResponse; } 40 | public String getMessage() { return mMessage; } 41 | public boolean isSuccess() { return mResponse == IabHelper.BILLING_RESPONSE_RESULT_OK; } 42 | public boolean isFailure() { return !isSuccess(); } 43 | public String toString() { return "IabResult: " + getMessage(); } 44 | } 45 | -------------------------------------------------------------------------------- /android/src/main/java/com/util/Inventory.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Google Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package com.util; 17 | 18 | import java.util.ArrayList; 19 | import java.util.HashMap; 20 | import java.util.List; 21 | import java.util.Map; 22 | import java.lang.String; 23 | 24 | /** 25 | * Represents a block of information about in-app items. 26 | * An Inventory is returned by such methods as {@link IabHelper#queryInventory}. 27 | */ 28 | public class Inventory { 29 | Map mSkuMap = new HashMap(); 30 | Map mPurchaseMap = new HashMap(); 31 | 32 | Inventory() { } 33 | 34 | /** Returns the listing details for an in-app product. */ 35 | public SkuDetails getSkuDetails(String sku) { 36 | return mSkuMap.get(sku); 37 | } 38 | 39 | /** Returns purchase information for a given product, or null if there is no purchase. */ 40 | public Purchase getPurchase(String sku) { 41 | return mPurchaseMap.get(sku); 42 | } 43 | 44 | /** Returns whether or not there exists a purchase of the given product. */ 45 | public boolean hasPurchase(String sku) { 46 | return mPurchaseMap.containsKey(sku); 47 | } 48 | 49 | /** Return whether or not details about the given product are available. */ 50 | public boolean hasDetails(String sku) { 51 | return mSkuMap.containsKey(sku); 52 | } 53 | 54 | /** 55 | * Erase a purchase (locally) from the inventory, given its product ID. This just 56 | * modifies the Inventory object locally and has no effect on the server! This is 57 | * useful when you have an existing Inventory object which you know to be up to date, 58 | * and you have just consumed an item successfully, which means that erasing its 59 | * purchase data from the Inventory you already have is quicker than querying for 60 | * a new Inventory. 61 | */ 62 | public void erasePurchase(String sku) { 63 | if (mPurchaseMap.containsKey(sku)) mPurchaseMap.remove(sku); 64 | } 65 | 66 | /** Returns a list of all owned product IDs. */ 67 | public List getAllOwnedSkus() { 68 | return new ArrayList(mPurchaseMap.keySet()); 69 | } 70 | 71 | /** Returns a list of all owned product IDs of a given type */ 72 | List getAllOwnedSkus(String itemType) { 73 | List result = new ArrayList(); 74 | for (Purchase p : mPurchaseMap.values()) { 75 | if (p.getItemType().equals(itemType)) result.add(p.getSku()); 76 | } 77 | return result; 78 | } 79 | 80 | /** Returns a list of all purchases. */ 81 | public List getAllPurchases() { 82 | return new ArrayList(mPurchaseMap.values()); 83 | } 84 | 85 | void addSkuDetails(SkuDetails d) { 86 | mSkuMap.put(d.getSku(), d); 87 | } 88 | 89 | void addPurchase(Purchase p) { 90 | mPurchaseMap.put(p.getSku(), p); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /android/src/main/java/com/util/Purchase.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Google Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package com.util; 17 | 18 | import org.json.JSONException; 19 | import org.json.JSONObject; 20 | 21 | /** 22 | * Represents an in-app billing purchase. 23 | */ 24 | public class Purchase { 25 | String mItemType; // ITEM_TYPE_INAPP or ITEM_TYPE_SUBS 26 | String mOrderId; 27 | String mPackageName; 28 | String mSku; 29 | long mPurchaseTime; 30 | int mPurchaseState; 31 | String mDeveloperPayload; 32 | String mToken; 33 | String mOriginalJson; 34 | String mSignature; 35 | 36 | public Purchase(String itemType, String jsonPurchaseInfo, String signature) throws JSONException { 37 | mItemType = itemType; 38 | mOriginalJson = jsonPurchaseInfo; 39 | JSONObject o = new JSONObject(mOriginalJson); 40 | mOrderId = o.optString("orderId"); 41 | mPackageName = o.optString("packageName"); 42 | mSku = o.optString("productId"); 43 | mPurchaseTime = o.optLong("purchaseTime"); 44 | mPurchaseState = o.optInt("purchaseState"); 45 | mDeveloperPayload = o.optString("developerPayload"); 46 | mToken = o.optString("token", o.optString("purchaseToken")); 47 | mSignature = signature; 48 | } 49 | 50 | public String getItemType() { return mItemType; } 51 | public String getOrderId() { return mOrderId; } 52 | public String getPackageName() { return mPackageName; } 53 | public String getSku() { return mSku; } 54 | public long getPurchaseTime() { return mPurchaseTime; } 55 | public int getPurchaseState() { return mPurchaseState; } 56 | public String getDeveloperPayload() { return mDeveloperPayload; } 57 | public String getToken() { return mToken; } 58 | public String getOriginalJson() { return mOriginalJson; } 59 | public String getSignature() { return mSignature; } 60 | 61 | @Override 62 | public String toString() { return "PurchaseInfo(type:" + mItemType + "):" + mOriginalJson; } 63 | } 64 | -------------------------------------------------------------------------------- /android/src/main/java/com/util/Security.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Google Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package com.util; 17 | 18 | import android.text.TextUtils; 19 | import android.util.Log; 20 | 21 | import org.json.JSONException; 22 | import org.json.JSONObject; 23 | 24 | 25 | import java.security.InvalidKeyException; 26 | import java.security.KeyFactory; 27 | import java.security.NoSuchAlgorithmException; 28 | import java.security.PublicKey; 29 | import java.security.Signature; 30 | import java.security.SignatureException; 31 | import java.security.spec.InvalidKeySpecException; 32 | import java.security.spec.X509EncodedKeySpec; 33 | 34 | /** 35 | * Security-related methods. For a secure implementation, all of this code 36 | * should be implemented on a server that communicates with the 37 | * application on the device. For the sake of simplicity and clarity of this 38 | * example, this code is included here and is executed on the device. If you 39 | * must verify the purchases on the phone, you should obfuscate this code to 40 | * make it harder for an attacker to replace the code with stubs that treat all 41 | * purchases as verified. 42 | */ 43 | public class Security { 44 | private static final String TAG = "IABUtil/Security"; 45 | 46 | private static final String KEY_FACTORY_ALGORITHM = "RSA"; 47 | private static final String SIGNATURE_ALGORITHM = "SHA1withRSA"; 48 | 49 | /** 50 | * Verifies that the data was signed with the given signature, and returns 51 | * the verified purchase. The data is in JSON format and signed 52 | * with a private key. The data also contains the {@link PurchaseState} 53 | * and product ID of the purchase. 54 | * @param base64PublicKey the base64-encoded public key to use for verifying. 55 | * @param signedData the signed JSON string (signed, not encrypted) 56 | * @param signature the signature for the data, signed with the private key 57 | */ 58 | public static boolean verifyPurchase(String base64PublicKey, String signedData, String signature) { 59 | if (TextUtils.isEmpty(signedData) || TextUtils.isEmpty(base64PublicKey) || 60 | TextUtils.isEmpty(signature)) { 61 | Log.e(TAG, "Purchase verification failed: missing data."); 62 | return false; 63 | } 64 | 65 | PublicKey key = Security.generatePublicKey(base64PublicKey); 66 | return Security.verify(key, signedData, signature); 67 | } 68 | 69 | /** 70 | * Generates a PublicKey instance from a string containing the 71 | * Base64-encoded public key. 72 | * 73 | * @param encodedPublicKey Base64-encoded public key 74 | * @throws IllegalArgumentException if encodedPublicKey is invalid 75 | */ 76 | public static PublicKey generatePublicKey(String encodedPublicKey) { 77 | try { 78 | byte[] decodedKey = Base64.decode(encodedPublicKey); 79 | KeyFactory keyFactory = KeyFactory.getInstance(KEY_FACTORY_ALGORITHM); 80 | return keyFactory.generatePublic(new X509EncodedKeySpec(decodedKey)); 81 | } catch (NoSuchAlgorithmException e) { 82 | throw new RuntimeException(e); 83 | } catch (InvalidKeySpecException e) { 84 | Log.e(TAG, "Invalid key specification."); 85 | throw new IllegalArgumentException(e); 86 | } catch (Base64DecoderException e) { 87 | Log.e(TAG, "Base64 decoding failed."); 88 | throw new IllegalArgumentException(e); 89 | } 90 | } 91 | 92 | /** 93 | * Verifies that the signature from the server matches the computed 94 | * signature on the data. Returns true if the data is correctly signed. 95 | * 96 | * @param publicKey public key associated with the developer account 97 | * @param signedData signed data from server 98 | * @param signature server signature 99 | * @return true if the data and signature match 100 | */ 101 | public static boolean verify(PublicKey publicKey, String signedData, String signature) { 102 | Signature sig; 103 | try { 104 | sig = Signature.getInstance(SIGNATURE_ALGORITHM); 105 | sig.initVerify(publicKey); 106 | sig.update(signedData.getBytes()); 107 | if (!sig.verify(Base64.decode(signature))) { 108 | Log.e(TAG, "Signature verification failed."); 109 | return false; 110 | } 111 | return true; 112 | } catch (NoSuchAlgorithmException e) { 113 | Log.e(TAG, "NoSuchAlgorithmException."); 114 | } catch (InvalidKeyException e) { 115 | Log.e(TAG, "Invalid key specification."); 116 | } catch (SignatureException e) { 117 | Log.e(TAG, "Signature exception."); 118 | } catch (Base64DecoderException e) { 119 | Log.e(TAG, "Base64 decoding failed."); 120 | } 121 | return false; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /android/src/main/java/com/util/SkuDetails.java: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 Google Inc. 2 | * 3 | * Licensed under the Apache License, Version 2.0 (the "License"); 4 | * you may not use this file except in compliance with the License. 5 | * You may obtain a copy of the License at 6 | * 7 | * http://www.apache.org/licenses/LICENSE-2.0 8 | * 9 | * Unless required by applicable law or agreed to in writing, software 10 | * distributed under the License is distributed on an "AS IS" BASIS, 11 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | * See the License for the specific language governing permissions and 13 | * limitations under the License. 14 | */ 15 | 16 | package com.util; 17 | 18 | import org.json.JSONException; 19 | import org.json.JSONObject; 20 | 21 | /** 22 | * Represents an in-app product's listing details. 23 | */ 24 | public class SkuDetails { 25 | String mItemType; 26 | String mSku; 27 | String mType; 28 | String mPrice; 29 | String mTitle; 30 | String mDescription; 31 | String mJson; 32 | 33 | public SkuDetails(String jsonSkuDetails) throws JSONException { 34 | this(IabHelper.ITEM_TYPE_INAPP, jsonSkuDetails); 35 | } 36 | 37 | public SkuDetails(String itemType, String jsonSkuDetails) throws JSONException { 38 | mItemType = itemType; 39 | mJson = jsonSkuDetails; 40 | JSONObject o = new JSONObject(mJson); 41 | mSku = o.optString("productId"); 42 | mType = o.optString("type"); 43 | mPrice = o.optString("price"); 44 | mTitle = o.optString("title"); 45 | mDescription = o.optString("description"); 46 | } 47 | 48 | public String getSku() { return mSku; } 49 | public String getType() { return mType; } 50 | public String getPrice() { return mPrice; } 51 | public String getTitle() { return mTitle; } 52 | public String getDescription() { return mDescription; } 53 | 54 | @Override 55 | public String toString() { 56 | return "SkuDetails:" + mJson; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | import { NativeModules } from 'react-native'; 3 | 4 | const { RNCustomBilling } = NativeModules; 5 | 6 | export default RNCustomBilling; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-native-custom-billing", 3 | "version": "2.2.0", 4 | "description": "Customizable InAppBilling feature for react native application", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "react-native", 11 | "billing", 12 | "iap", 13 | "cafe", 14 | "bazaar", 15 | "myket", 16 | "persian", 17 | "market" 18 | ], 19 | "author": { 20 | "name": "Hesam Ebrahimy", 21 | "email": "hesam.ebrahimy@gmail.com" 22 | }, 23 | "license": "", 24 | "peerDependencies": { 25 | "react-native": "^0.41.2" 26 | } 27 | } 28 | --------------------------------------------------------------------------------