├── .github └── workflows │ ├── ci.yml │ └── stale.yml ├── .gitignore ├── CHANGELOG.md ├── KINDLE.md ├── LICENSE ├── README.md ├── analysis_options.yaml ├── ancillary └── amazon.sdktester.json ├── android ├── .gitignore ├── .project ├── .settings │ └── org.eclipse.buildship.core.prefs ├── build.gradle ├── gradle.properties ├── gradle │ └── wrapper │ │ ├── gradle-wrapper.jar │ │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jars │ └── in-app-purchasing-2.0.76.jar ├── settings.gradle └── src │ └── main │ ├── AndroidManifest.xml │ └── kotlin │ └── com │ └── dooboolab │ └── flutterinapppurchase │ ├── AmazonInappPurchasePlugin.kt │ ├── AndroidInappPurchasePlugin.kt │ ├── BillingError.kt │ ├── FlutterInappPurchasePlugin.kt │ └── MethodResultWrapper.kt ├── example ├── .gitignore ├── .metadata ├── README.md ├── android │ ├── .gitignore │ ├── .project │ ├── .settings │ │ └── org.eclipse.buildship.core.prefs │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── java │ │ │ └── com │ │ │ │ └── dooboolab │ │ │ │ ├── iapexample │ │ │ │ └── MainActivity.java │ │ │ │ └── test │ │ │ │ └── MainActivity.java │ │ │ └── res │ │ │ ├── drawable │ │ │ └── launch_background.xml │ │ │ ├── mipmap-hdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-mdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxhdpi │ │ │ └── ic_launcher.png │ │ │ ├── mipmap-xxxhdpi │ │ │ └── ic_launcher.png │ │ │ └── values │ │ │ └── styles.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ ├── gradle-wrapper.jar │ │ │ └── gradle-wrapper.properties │ ├── gradlew │ ├── gradlew.bat │ └── settings.gradle ├── flutter_inapp_purchase_example.iml ├── flutter_inapp_purchase_example_android.iml ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── .last_build_id │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ ├── Flutter.podspec │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ └── Runner │ │ ├── AppDelegate.h │ │ ├── AppDelegate.m │ │ ├── Assets.xcassets │ │ ├── AppIcon.appiconset │ │ │ ├── Contents.json │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ ├── Icon-App-20x20@1x.png │ │ │ ├── Icon-App-20x20@2x.png │ │ │ ├── Icon-App-20x20@3x.png │ │ │ ├── Icon-App-29x29@1x.png │ │ │ ├── Icon-App-29x29@2x.png │ │ │ ├── Icon-App-29x29@3x.png │ │ │ ├── Icon-App-40x40@1x.png │ │ │ ├── Icon-App-40x40@2x.png │ │ │ ├── Icon-App-40x40@3x.png │ │ │ ├── Icon-App-60x60@2x.png │ │ │ ├── Icon-App-60x60@3x.png │ │ │ ├── Icon-App-76x76@1x.png │ │ │ ├── Icon-App-76x76@2x.png │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ └── LaunchImage.imageset │ │ │ ├── Contents.json │ │ │ ├── LaunchImage.png │ │ │ ├── LaunchImage@2x.png │ │ │ ├── LaunchImage@3x.png │ │ │ └── README.md │ │ ├── Base.lproj │ │ ├── LaunchScreen.storyboard │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── main.m ├── lib │ └── main.dart ├── pubspec.yaml └── test │ └── widget_test.dart ├── flutter_inapp_purchase_android.iml ├── ios ├── .gitignore ├── Assets │ └── .gitkeep ├── Classes │ ├── FlutterInappPurchasePlugin.h │ ├── FlutterInappPurchasePlugin.m │ ├── IAPPromotionObserver.h │ └── IAPPromotionObserver.m └── flutter_inapp_purchase.podspec ├── issue_template.md ├── lib ├── Store.dart ├── flutter_inapp_purchase.dart ├── modules.dart └── utils.dart ├── pubspec.yaml └── test ├── flutter_inapp_purchase_test.dart └── utils_test.dart /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: Flutter CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v4 14 | - uses: actions/setup-java@v3 15 | with: 16 | distribution: 'adopt' 17 | java-version: "14.x" 18 | 19 | - uses: subosito/flutter-action@v2 20 | with: 21 | channel: "stable" 22 | flutter-version: "3.x" 23 | 24 | - run: flutter pub get 25 | 26 | - run: dart format --set-exit-if-changed . 27 | 28 | # - run: flutter analyze . 29 | 30 | - run: flutter test --coverage 31 | 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v3 34 | with: 35 | token: ${{ secrets.CODECOV_TOKEN }} 36 | file: coverage/lcov.info 37 | 38 | # - run: flutter build apk 39 | 40 | # Upload generated apk to the artifacts. 41 | # - uses: actions/upload-artifact@v1 42 | # with: 43 | # name: release-apk 44 | # path: build/app/outputs/apk/release/app-release.apk 45 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: Mark stale issues and pull requests 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | jobs: 8 | stale: 9 | 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/stale@v8 14 | with: 15 | repo-token: ${{ secrets.GITHUB_TOKEN }} 16 | days-before-stale: 90 17 | days-before-close: 7 18 | stale-issue-message: 'This issue is stale because it has been open 90 days with no activity. Leave a comment or this will be closed in 7 days.' 19 | stale-pr-message: 'This PR is stale because it has been open 90 days with no activity. Leave a comment or this will be closed in 7 days' 20 | stale-issue-label: 'no-issue-activity' 21 | stale-pr-label: 'no-pr-activity' 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | pubspec.lock 7 | 8 | build/ 9 | 10 | # dooboolab 11 | .idea/ 12 | flutter_inapp.iml 13 | flutter_inapp_android.iml 14 | flutter_export_environment* 15 | launch.json 16 | .flutter-plugins-* 17 | 18 | coverage -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 5.6.2 2 | 3 | - fix: removed references to deprecated v1 Android embedding by @moodstubos in https://github.com/hyochan/flutter_inapp_purchase/pull/497 4 | 5 | ## 5.6.1 6 | 7 | - Erroneous duplicate item by @deakjahn in https://github.com/hyochan/flutter_inapp_purchase/pull/441 8 | - Fixed consumable products reading on Android by @33-Elephants in https://github.com/hyochan/flutter_inapp_purchase/pull/439 9 | - fix: Support AGP8 namespace by @dev-yakuza in https://github.com/hyochan/flutter_inapp_purchase/pull/467 10 | 11 | ## 5.6.0 12 | 13 | - refactor: android init connection 14 | ``` 15 | Used Kotlin apply for cleaner initialization of billingClient. 16 | Introduced context ?: return for null-safety with context. 17 | Merged repetitive code into the updateConnectionStatus method to avoid duplication. 18 | Improved the handling of the alreadyFinished flag to ensure it is only set once and at the appropriate time. 19 | Streamlined the error and success handling for clarity. 20 | ``` 21 | - Migrate android billingClient to 6.0.1 22 | - https://developer.android.com/google/play/billing/release-notes#6-0-1 23 | 24 | ## 5.5.0 25 | 26 | - Erroneous duplicate item (#441) - Remove extra `introductoryPricePaymentModeIOS` 27 | - Fixed consumable products reading on Android (#439) 28 | - chore(deps): migrate internal packages to recent 29 | ``` 30 | http: ^1.1.0 31 | meta: ^1.10.0 32 | platform: ^3.1.3 33 | ``` 34 | - chore: migrate example project to recent flutter version, 3.16.0-0.3.pre 35 | 36 | ## 5.4.2 37 | 38 | ## What's Changed 39 | 40 | - Update actions/stale action to v8 by @renovate in https://github.com/hyochan/flutter_inapp_purchase/pull/414 41 | - Fix - wrong casting by @BrunoFSimon in https://github.com/hyochan/flutter_inapp_purchase/pull/427 42 | - Fixed consumable product purchase on Android by @33-Elephants in https://github.com/hyochan/flutter_inapp_purchase/pull/420 43 | 44 | ## New Contributors 45 | 46 | - @BrunoFSimon made their first contribution in https://github.com/hyochan/flutter_inapp_purchase/pull/427 47 | - @33-Elephants made their first contribution in https://github.com/hyochan/flutter_inapp_purchase/pull/420 48 | 49 | **Full Changelog**: https://github.com/hyochan/flutter_inapp_purchase/compare/5.4.1...5.4.2 50 | 51 | ## 5.4.1 52 | 53 | - Fixed concurrency issue on iOS. by @OctavianLfrd in https://github.com/dooboolab/flutter_inapp_purchase/pull/413 54 | 55 | ## 5.4.0 56 | 57 | - Fixed wrong casting in checkSubscribed method by @kleeb in https://github.com/dooboolab/flutter_inapp_purchase/pull/368 58 | - Upgrade to billing 5.1 (reverse compatible) by @SamBergeron in https://github.com/dooboolab/flutter_inapp_purchase/pull/392 59 | 60 | ## 5.3.0 61 | 62 | ## What's Changed 63 | 64 | - Refactor java to kotlin, add showInAppMessageAndroid by @offline-first in https://github.com/dooboolab/flutter_inapp_purchase/pull/365 65 | 66 | ## New Contributors 67 | 68 | - @offline-first made their first contribution in https://github.com/dooboolab/flutter_inapp_purchase/pull/365 69 | 70 | **Full Changelog**: https://github.com/dooboolab/flutter_inapp_purchase/compare/5.2.0...5.3.0 71 | 72 | ## 5.2.0 73 | 74 | Bugfix #356 75 | 76 | ## 5.1.1 77 | 78 | Run on UiThread and few others (#328) 79 | 80 | - Related #272 81 | 82 | - The main difference is a new MethodResultWrapper class that wraps both the result and the channel. onMethodCall() now immediately saves this wrapped result-channel to a field and only uses that later to set both the result and to send back info on the channel. I did this in both Google and Amazon but I can't test the Amazon one. 83 | 84 | - Included the plugin registration differences. 85 | 86 | - Midified suggested in one of the issues that initConnection, endConnection and consumeAllItems shouldn't be accessors. This is very much so, property accessors are not supposed to do work and have side effects, just return a value. Now three new functions are suggested and marked the old ones deprecated. 87 | 88 | Fourth, EnumUtil.getValueString() is not really necessary, we have describeEnum() in the Flutter engine just for this purpose. 89 | 90 | ## 5.1.0 91 | 92 | Upgrade android billing client to `4.0.0` (#326) 93 | 94 | Remove `orderId` in `Purchase` 95 | 96 | - This is duplicate of `transactionId`. 97 | 98 | Support for Amazon devices with Google Play sideloaded (#313) 99 | 100 | ## 5.0.4 101 | 102 | - Add iOS promo codes (#325) 103 | - Use http client in validateReceiptIos (#322) 104 | - Amazon `getPrice` directly withoiut formatting (#316) 105 | 106 | ## 5.0.3 107 | 108 | - Fix plugin exception for `requestProductWithQuantityIOS` [#306](https://github.com/dooboolab/flutter_inapp_purchase/pull/306) 109 | 110 | ## 5.0.2 111 | 112 | - Replaced obfuscatedAccountIdAndroid with obfuscatedAccountId in request purchase method [#299](https://github.com/dooboolab/flutter_inapp_purchase/pull/299) 113 | 114 | ## 5.0.1 115 | 116 | - Add AndroidProrationMode values [#273](https://github.com/dooboolab/flutter_inapp_purchase/pull/273) 117 | 118 | ## 5.0.0 119 | 120 | - Support null safety [#275](https://github.com/dooboolab/flutter_inapp_purchase/pull/275) 121 | 122 | ## 4.0.2 123 | 124 | - The dart side requires "introductoryPriceCyclesAndroid" to be a int [#268](https://github.com/dooboolab/flutter_inapp_purchase/pull/268) 125 | 126 | ## 4.0.1 127 | 128 | - `platform` dep version `>=2.0.0 <4.0.0` 129 | 130 | ## 4.0.0 131 | 132 | - Support flutter v2 [#265](https://github.com/dooboolab/flutter_inapp_purchase/pull/265) 133 | 134 | ## 3.0.1 135 | 136 | - Migrate to flutter embedding v2 [#240](https://github.com/dooboolab/flutter_inapp_purchase/pull/240) 137 | - Expose android purchase state as enum [#243](https://github.com/dooboolab/flutter_inapp_purchase/pull/243) 138 | 139 | ## 3.0.0 140 | 141 | - Upgrade android billing client to `2.1.0` from `3.0.0`. 142 | - Removed `deveoperId` and `accountId` when requesting `purchase` or `subscription` in `android`. 143 | - Added `obfuscatedAccountIdAndroid` and `obfuscatedProfileIdAndroid` when requesting `purchase` or `subscription` in `android`. 144 | - Removed `developerPayload` in `android`. 145 | - Added `purchaseTokenAndroid` as an optional parameter to `requestPurchase` and `requestSubscription`. 146 | 147 | ## 2.3.1 148 | 149 | Republishing since sourcode seems not merged correctly. 150 | 151 | ## 2.3.0 152 | 153 | - Bugfix IAPItem deserialization [#212](https://github.com/dooboolab/flutter_inapp_purchase/pull/212) 154 | - Add introductoryPriceNumberIOS [#214](https://github.com/dooboolab/flutter_inapp_purchase/pull/214) 155 | - Fix iOS promotional offers [#220](https://github.com/dooboolab/flutter_inapp_purchase/pull/220) 156 | 157 | ## 2.2.0 158 | 159 | - Implement `endConnection` method to declaratively finish observer in iOS. 160 | - Remove `addTransactionObserver` in IAPPromotionObserver.m for dup observer problems. 161 | - Automatically startPromotionObserver in `initConnection` for iOS. 162 | 163 | ## 2.1.5 164 | 165 | - Fix ios failed purchase handling problem in 11.4+ [#176](https://github.com/dooboolab/flutter_inapp_purchase/pull/176) 166 | 167 | ## 2.1.4 168 | 169 | - Fix dart side expression warning [#169](https://github.com/dooboolab/flutter_inapp_purchase/pull/169). 170 | 171 | ## 2.1.3 172 | 173 | - Fix wrong introductory price number of periods [#164](https://github.com/dooboolab/flutter_inapp_purchase/pull/164). 174 | 175 | ## 2.1.2 176 | 177 | - Trigger purchaseUpdated callback when iap purchased [#165](https://github.com/dooboolab/flutter_inapp_purchase/pull/165). 178 | 179 | ## 2.1.1 180 | 181 | - Renamed `finishTransactionIOS` argument `purchaseToken` to `transactionId`. 182 | 183 | ## 2.1.0 184 | 185 | - `finishTransaction` parameter changes to `purchasedItem` from `purchaseToken`. 186 | - Update android billing client to `2.1.0` from `2.0.3`. 187 | 188 | ## 2.0.5 189 | 190 | - [bugfix] Fix double call of result reply on connection init [#126](https://github.com/dooboolab/flutter_inapp_purchase/pull/126) 191 | 192 | ## 2.0.4 193 | 194 | - [bugfix] Fix plugin throws exceptions with flutter v1.10.7 beta [#117](https://github.com/dooboolab/flutter_inapp_purchase/pull/117) 195 | 196 | ## 2.0.3 197 | 198 | - [bugfix] Decode response code for connection updates stream [#114](https://github.com/dooboolab/flutter_inapp_purchase/pull/114) 199 | - [bugfix] Fix typo in `consumePurchase` [#115](https://github.com/dooboolab/flutter_inapp_purchase/pull/115) 200 | 201 | ## 2.0.2 202 | 203 | - use ConnectionResult as type for connection stream, fix controller creation [#112](https://github.com/dooboolab/flutter_inapp_purchase/pull/112) 204 | 205 | ## 2.0.0+16 206 | 207 | - Resolve [#106](https://github.com/dooboolab/flutter_inapp_purchase/issues/106) by not sending `result.error` to the listener. Created use `_conectionSubscription`. 208 | 209 | ## 2.0.0+15 210 | 211 | - Fixed minor typo when generating string with `toString`. Resolve [#110](https://github.com/dooboolab/flutter_inapp_purchase/issues/110). 212 | 213 | ## 2.0.0+14 214 | 215 | - Pass android exception to flutter side. 216 | 217 | ## 2.0.0+13 218 | 219 | - Android receipt validation api upgrade to `v3`. 220 | 221 | ## 2.0.0+12 222 | 223 | - Resolve [#102](https://github.com/dooboolab/flutter_inapp_purchase/issues/102). Fluter seems to only sends strings between platforms. 224 | 225 | ## 2.0.0+9 226 | 227 | - Resolve [#101](https://github.com/dooboolab/flutter_inapp_purchase/issues/101). 228 | 229 | ## 2.0.0+8 230 | 231 | - Resolve [#100](https://github.com/dooboolab/flutter_inapp_purchase/issues/100). 232 | 233 | ## 2.0.0+7 234 | 235 | - Resolve [#99](https://github.com/dooboolab/flutter_inapp_purchase/issues/99). 236 | 237 | ## 2.0.0+6 238 | 239 | - Send `purchase-error` with purchases returns null. 240 | 241 | ## 2.0.0+5 242 | 243 | - Renamed invoked parameters non-platform specific. 244 | 245 | ## 2.0.0+4 246 | 247 | - Add `deveoperId` and `accountId` when requesting `purchase` or `subscription` in `android`. Find out more in `requestPurchase` and `requestSubscription`. 248 | 249 | ## 2.0.0+3 250 | 251 | - Correctly mock invoke method and return results [#94](https://github.com/dooboolab/flutter_inapp_purchase/pull/96) 252 | 253 | ## 2.0.0+2 254 | 255 | - Seperate long `example` code to `example` readme. 256 | 257 | ## 2.0.0+1 258 | 259 | - Properly set return type `PurchaseResult` of when finishing transaction. 260 | 261 | ## 2.0.0 :tada: 262 | 263 | - Removed deprecated note in the `readme`. 264 | - Make the previous tests work in `travis`. 265 | - Documentation on `readme` for breaking features. 266 | - Abstracts `finishTransaction`. 267 | - `acknowledgePurchaseAndroid`, `consumePurchaseAndroid`, `finishTransactionIOS`. 268 | 269 | [Android] 270 | 271 | - Completely remove prepare. 272 | - Upgrade billingclient to 2.0.3 which is currently recent in Sep 15 2019. 273 | - Remove [IInAppBillingService] binding since billingClient has its own functionalities. 274 | - Add [DoobooUtils] and add `getBillingResponseData` that visualizes erorr codes better. 275 | - `buyProduct` no more return asyn result. It rather relies on the `purchaseUpdatedListener`. 276 | - Add feature method `acknowledgePurchaseAndroid` 277 | - Implement `acknowledgePurchaseAndroid`. 278 | - Renamed `consumePurchase` to `consumePurchaseAndroid` in dart side. 279 | - Update test codes. 280 | - Renamed methods 281 | - `buyProduct` to `requestPurchase`. 282 | - `buySubscription` to `requestSubscription`. 283 | 284 | [iOS] 285 | 286 | - Implment features in new releases. 287 | - enforce to `finishTransaction` after purchases. 288 | - Work with `purchaseUpdated` and `purchaseError` listener as in android. 289 | - Feature set from `react-native-iap v3`. 290 | - Should call finish transaction in every purchase request. 291 | - Add `IAPPromotionObserver` cocoa touch file 292 | - Convert dic to json string before invoking purchase-updated 293 | - Add `getPromotedProductIOS` and `requestPromotedProductIOS` methods 294 | - Implement clearTransaction for ios 295 | - Include `purchasePromoted` stream that listens to `iap-promoted-product`. 296 | 297 | ## 1.0.0 298 | 299 | - Add `DEPRECATION` note. Please use [in_app_purchase](https://pub.dev/packages/in_app_purchase). 300 | 301 | ## 0.9.+ 302 | 303 | - Breaking change. Migrate from the deprecated original Android Support Library to AndroidX. This shouldn't result in any functional changes, but it requires any Android apps using this plugin to also migrate to Android X if they're using the original support library. [Android's Migrating to Android X Guide](https://developer.android.com/jetpack/androidx/migrate). 304 | 305 | * Improved getPurchaseHistory's speed 44% faster [#68](https://github.com/dooboolab/flutter_inapp_purchase/pull/68). 306 | 307 | ## 0.8.+ 308 | 309 | - Fixed receipt validation param for `android`. 310 | - Updated `http` package. 311 | - Implemented new method `getAppStoreInitiatedProducts`. 312 | - Handling of iOS method `paymentQueue:shouldAddStorePayment:forProduct:` 313 | - Has no effect on Android. 314 | - Fixed issue with method `buyProductWithoutFinishTransaction` for iOS, was not getting the productId. 315 | - Fixed issue with `toString` method of class `IAPItem`, was printing incorrect values. 316 | - Fixes for #44. Unsafe getting `originalJson` when restoring item and `Android`. 317 | - Use dictionaryWithObjectsAndKeys in NSDictionary to fetch product values. This will prevent from NSInvalidArgumentException in ios which rarely occurs. 318 | - Fixed wrong npe in `android` when `getAvailablePurchases`. 319 | 320 | * Only parse `orderId` when exists in `Android` to prevent crashing. 321 | * Add additional success purchase listener in `iOS`. Related [#54](https://github.com/dooboolab/flutter_inapp_purchase/issues/54) 322 | 323 | ## 0.7.1 324 | 325 | - Implemented receiptValidation for both android and ios. 326 | - In Android, you need own backend to get your `accessToken`. 327 | 328 | ## 0.7.0 329 | 330 | - Addition of Amazon In-App Purchases. 331 | 332 | ## 0.6.9 333 | 334 | - Prevent nil element exception when getting products. 335 | 336 | ## 0.6.8 337 | 338 | - Prevent nil exception in ios when fetching products. 339 | 340 | ## 0.6.7 341 | 342 | - Fix broken images on pub. 343 | 344 | ## 0.6.6 345 | 346 | - Added missing introductory fields in ios. 347 | 348 | ## 0.6.5 349 | 350 | - convert dynamic objects to PurchasedItems. 351 | - Fix return type for getAvailablePurchases(). 352 | - Fix ios null value if optional operator. 353 | 354 | ## 0.6.3 355 | 356 | - Update readme. 357 | 358 | ## 0.6.2 359 | 360 | - Fixed failing when there is no introductory price in ios. 361 | 362 | ## 0.6.1 363 | 364 | - Fixed `checkSubscribed` that can interrupt billing lifecycle. 365 | 366 | ## 0.6.0 367 | 368 | - Major code refactoring by lukepighetti. Unify PlatformException, cleanup new, DateTime instead of string. 369 | 370 | ## 0.5.9 371 | 372 | - Fix getSubscription json encoding problem in `ios`. 373 | 374 | ## 0.5.8 375 | 376 | - Avoid crashing on android caused by IllegalStateException. 377 | 378 | ## 0.5.7 379 | 380 | - Avoid possible memory leak in android by deleting static declaration of activity and context. 381 | 382 | ## 0.5.6 383 | 384 | - Few types fixed. 385 | 386 | ## 0.5.4 387 | 388 | - Fixed error parsing IAPItem. 389 | 390 | ## 0.5.3 391 | 392 | - Fixed error parsing purchaseHistory. 393 | 394 | ## 0.5.2 395 | 396 | - Fix crashing on error. 397 | 398 | ## 0.5.1 399 | 400 | - Give better error message on ios. 401 | 402 | ## 0.5.0 403 | 404 | - Code migration. 405 | - Support subscription period. 406 | - There was parameter renaming in `0.5.0` to identify different parameters sent from the device. Please check the readme. 407 | 408 | ## 0.4.3 409 | 410 | - Fixed subscription return types. 411 | 412 | ## 0.4.0 413 | 414 | - Well formatted code. 415 | 416 | ## 0.3.3 417 | 418 | - Code formatted 419 | - Updated missing data types 420 | 421 | ## 0.3.1 422 | 423 | - Upgraded readme for ease of usage. 424 | - Enabled strong mode. 425 | 426 | ## 0.3.0 427 | 428 | - Moved dynamic return type away and instead give `PurchasedItem`. 429 | 430 | ## 0.2.3 431 | 432 | - Quickly fixed purchase bug out there in [issue](https://github.com/dooboolab/flutter_inapp_purchase/issues/2). Need much more improvement currently. 433 | 434 | ## 0.2.2 435 | 436 | - Migrated packages from FlutterInApp to FlutterInAppPurchase because pub won't get it. 437 | 438 | ## 0.1.0 439 | 440 | - Initial release of beta 441 | - Moved code from [react-native-iap](https://github.com/dooboolab/react-native-iap) 442 | -------------------------------------------------------------------------------- /KINDLE.md: -------------------------------------------------------------------------------- 1 | # Amazon Kindle Fire / Fire TV In-App Purchases Guide 2 | 3 | The plugin will automatically detect Amazon devices during runtime. 4 | 5 | ## Testing In-App Purchases 6 | 7 | To test your purchases, you do not need to create an Amazon developer account. 8 | 9 | Install the Amazon App Tester (AAT) : 10 | [Amazon App Tester](https://www.amazon.com/Amazon-App-Tester/dp/B00BN3YZM2) 11 | 12 | You need to create an amazon.sdktester.json file. 13 | 14 | Example : [amazon.sdktester.json](https://github.com/dooboolab/flutter_inapp_purchase/blob/main/ancillary/amazon.sdktester.json) 15 | Edit this to add your own Product Ids. 16 | 17 | Put this file into the kindle sdcard with : 18 | 19 | adb push amazon.sdktester.json /sdcard/ 20 | 21 | You can verify if the file is valid in the AAT and view the purchases. 22 | 23 | Add android.permission.INTERNET and com.amazon.device.iap.ResponseReceiver to your AndroidManifest like in the example https://github.com/dooboolab/flutter_inapp_purchase/blob/main/example/android/app/src/main/AndroidManifest.xml. 24 | 25 | Now, when you make a purchase the AAT will intercept, show the purchases screen and allow you to make a purchase. Your app will think a real purchase has been made and you can test the full purchase flow. 26 | 27 | ## Testing Live Purchases 28 | Add your apk into the "Live App Testing" tab. Add your IAP into the "In-App Items" tab. You must fill in your bank details first and submit your IAP so that the status is "live". 29 | 30 | Also with Gradle 3.4.0 or higher you need to take care of the obfuscating. To do so, create a Proguard file named "proguard-rules.pro" in the android/app folder. Into that file put the following content: 31 | 32 | -dontwarn com.amazon.**
33 | -keep class com.amazon.** {*;}
34 | -keepattributes *Annotation*
35 | 36 | Then edit your build.gradle in app level like: 37 | 38 | build.gradle: 39 | 40 | buildTypes { 41 | release { 42 | shrinkResources false 43 | signingConfig signingConfigs.release 44 | minifyEnabled false 45 | useProguard true 46 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 47 | } 48 | } 49 | 50 | 51 | If you need more help, check out https://stackoverflow.com/questions/62833628/how-to-disable-code-obfuscation-in-flutter-release-apk and the amazon part https://developer.amazon.com/de/docs/in-app-purchasing/iap-obfuscate-the-code.html. 52 | 53 | Now your testers will be sent a link to the test version of your app. They can make purchases at no cost to test your app. 54 | 55 | ## Submitting to the Amazon store 56 | Amazon developer accounts are free. I found the Amazon store the easiest to submit to (compared with Googles play & Apple store). Required screenshots are the same size as a Nexus 7 so that is what I used. 57 | 58 | I found the staff who checked my app very helpful (such as providing logcat output on request for example). Text is the same as other stores, except there is an additional up-to 10 bullet point summary of your app you can add. 59 | 60 | When you submit your app to the store there will be a warning that google billing is detected in your code. When you submit your app for approval you can mention in the testing instructions for the Amazon reviewer that you are using a cross-platform tool and the google IAP code is not used. I dont know if this is necessary but my app was approved anyway. 61 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 dooboolab 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 | # flutter_inapp_purchase 2 | 3 | [![Pub Version](https://img.shields.io/pub/v/flutter_inapp_purchase.svg?style=flat-square)](https://pub.dartlang.org/packages/flutter_inapp_purchase) 4 | [![Flutter CI](https://github.com/dooboolab/flutter_inapp_purchase/actions/workflows/ci.yml/badge.svg)](https://github.com/dooboolab/flutter_inapp_purchase/actions/workflows/ci.yml) 5 | [![Coverage Status](https://codecov.io/gh/dooboolab/flutter_inapp_purchase/branch/main/graph/badge.svg?token=WXBlKvRB2G)](https://codecov.io/gh/dooboolab/flutter_inapp_purchase) 6 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) 7 | 8 | ## Flutter V2 9 | 10 | This packages is compatible with flutter v2 from `4.0.0`. For those who use older version please use `< 4.0.0`. 11 | 12 | ## Sun Rise :sunrise: 13 | 14 | Since many one of you wanted me to keep working on this plugin in [#93](https://github.com/dooboolab/flutter_inapp_purchase/issues/93), I've decided to keep working on current project. I hope many one of you can help me maintain this. Thank you for all your supports in advance :tada:. 15 | 16 | ~~## Deprecated 17 | I've been maintaining this plugin since there wasn't an official plugin out when I implemented it. I saw in `flutter` github [issue #9591](https://github.com/flutter/flutter/issues/9591) that many people have been waiting for this plugin for more than a year before I've thought of building one. However, there has been an official `Google` plugin rised today which is [in_app_purchase](https://pub.dev/packages/in_app_purchase). Please try to use an official one because you might want to get much prompt support from giant `Google`. 18 | Also, thanks for all your supports that made me stubborn to work hard on this plugin. I've had great experience with all of you and hope we can meet someday with other projects. 19 | I'll leave this project as live for those who need time. I'll also try to merge the new `PR`'s and publish to `pub` if there's any further work given to this repo.~~ 20 | 21 | ## What this plugin do 22 | 23 | This is an `In App Purchase` plugin for flutter. This project has been `forked` from [react-native-iap](https://github.com/dooboolab/react-native-iap). We are trying to share same experience of `in-app-purchase` in `flutter` as in `react-native`. 24 | We will keep working on it as time goes by just like we did in `react-native-iap`. 25 | 26 | `PR` is always welcomed. 27 | 28 | ## Breaking Changes 29 | 30 | - Sunrise in `2.0.0` for highly requests from customers on discomfort in what's called an `official` plugin [in_app_purchase](https://pub.dev/packages/in_app_purchase). 31 | - Migrated to Android X in `0.9.0`. Please check the [Migration Guide](#migration-guide). 32 | - There was parameter renaming in `0.5.0` to identify different parameters sent from the device. Please check the readme. 33 | 34 | ## Migration Guide 35 | 36 | To migrate to `0.9.0` you must migrate your Android app to Android X by following the [Migrating to AndroidX Guide](https://developer.android.com/jetpack/androidx/migrate). 37 | 38 | ## Getting Started 39 | 40 | Follow the [Medium Blog](https://medium.com/@dooboolab/flutter-in-app-purchase-7a3fb9345e2a) for the configuration. 41 | 42 | Follow the [Medium Blog](https://medium.com/bosc-tech-labs-private-limited/how-to-implement-subscriptions-in-app-purchase-in-flutter-7ce8906e608a) to add **subscriptions** in app purchase. 43 | 44 | For help getting started with Flutter, view our online 45 | [documentation](https://flutter.io/). 46 | 47 | For help on editing plugin code, view the [documentation](https://flutter.io/developing-packages/#edit-plugin-package). 48 | 49 | ## Methods 50 | 51 | | Func | Param | Return | Description | 52 | | :--------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :-------------------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 53 | | initConnection | | `String` | Prepare IAP module. Must be called on Android before any other purchase flow methods. In ios, it will simply call `canMakePayments` method and return value. | 54 | | getProducts | `List` Product IDs/skus | `List` | Get a list of products (consumable and non-consumable items, but not subscriptions). Note: On iOS versions earlier than 11.2 this method _will_ return subscriptions if they are included in your list of SKUs. This is because we cannot differentiate between IAP products and subscriptions prior to 11.2. | 55 | | getSubscriptions | `List` Subscription IDs/skus | `List` | Get a list of subscriptions. Note: On iOS this method has the same output as `getProducts`. Because iOS does not differentiate between IAP products and subscriptions. | 56 | | getPurchaseHistory | | `List` | Gets an invetory of purchases made by the user regardless of consumption status (where possible) | 57 | | getAvailablePurchases | | `List` | (aka restore purchase) Get all purchases made by the user (either non-consumable, or haven't been consumed yet) | 58 | | getAppStoreInitiatedProducts | | `List` | If the user has initiated a purchase directly on the App Store, the products that the user is attempting to purchase will be returned here. (iOS only) Note: On iOS versions earlier than 11.0 this method will always return an empty list, as the functionality was introduced in v11.0. [See Apple Docs for more info](https://developer.apple.com/documentation/storekit/skpaymenttransactionobserver/2877502-paymentqueue) Always returns an empty list on Android. | 59 | | requestSubscription | `String` sku, `String` oldSkuAndroid?, `int` prorationModeAndroid?, `String` obfuscatedAccountIdAndroid?, `String` obfuscatedProfileIdAndroid?, `String` purchaseTokenAndroid? | Null | Create (request) a subscription to a sku. For upgrading/downgrading subscription on Android pass second parameter with current subscription ID, on iOS this is handled automatically by store. `purchaseUpdatedListener` will receive the result. | 60 | | requestPurchase | `String` sku, `String` obfuscatedAccountIdAndroid?, `String` obfuscatedProfileIdAndroid?, `String` purchaseToken? | Null | Request a purchase. `purchaseUpdatedListener` will receive the result. | 61 | | finishTransactionIOS | `String` purchaseTokenAndroid | `PurchaseResult` | Send finishTransaction call to Apple IAP server. Call this function after receipt validation process | 62 | | acknowledgePurchaseAndroid | `String` purchaseToken | `PurchaseResult` | Acknowledge a product (on Android) for `non-consumable` and `subscription` purchase. No-op on iOS. | 63 | | consumePurchaseAndroid | `String` purchaseToken | `PurchaseResult` | Consume a product (on Android) for `consumable` purchase. No-op on iOS. | 64 | | finishTransaction | `String` purchaseToken, `bool` isConsumable? } | `PurchaseResult` | Send finishTransaction call that abstracts all `acknowledgePurchaseAndroid`, `finishTransactionIOS`, `consumePurchaseAndroid` methods. | 65 | | endConnection | | `String` | End billing connection. | 66 | | consumeAllItems | | `String` | Manually consume all items in android. Do NOT call if you have any non-consumables (one time purchase items). No-op on iOS. | 67 | | validateReceiptIos | `Map` receiptBody, `bool` isTest | `http.Response` | Validate receipt for ios. | 68 | | validateReceiptAndroid | `String` packageName, `String` productId, `String` productToken, `String` accessToken, `bool` isSubscription | `http.Response` | Validate receipt for android. | 69 | | showPromoCodesIOS | | | Show redeem codes in iOS. | 70 | | showInAppMessageAndroid | | | Google Play will show users messaging during grace period and account hold once per day and provide them an opportunity to fix their payment without leaving the app | 71 | 72 | ## Purchase flow in `flutter_inapp_purchase@2.0.0+ 73 | 74 | ![purchase-flow-sequence](https://react-native-iap.dooboolab.com/assets/images/react-native-iapv3-8467b005f57bac1f11896c06e15577aa.svg) 75 | 76 | > When you've successfully received result from `purchaseUpdated` listener, you'll have to `verify` the purchase either by `acknowledgePurchaseAndroid`, `consumePurchaseAndroid`, `finishTransactionIOS` depending on the purchase types or platforms. You'll have to use `consumePurchaseAndroid` for `consumable` products and `android` and `acknowledgePurchaseAndroid` for `non-consumable` products either `subscription`. For `ios`, there is no differences in `verifying` purchases. You can just call `finishTransaction`. If you do not verify the purchase, it will be refunded within 3 days to users. We recommend you to `verifyReceipt` first before actually finishing transaction. Lastly, if you want to abstract three different methods into one, consider using `finishTransaction` method. 77 | 78 | ## Data Types 79 | 80 | - IAPItem 81 | 82 | ```dart 83 | final String productId; 84 | final String price; 85 | final String currency; 86 | final String localizedPrice; 87 | final String title; 88 | final String description; 89 | final String introductoryPrice; 90 | 91 | /// ios only 92 | final String subscriptionPeriodNumberIOS; 93 | final String subscriptionPeriodUnitIOS; 94 | final String introductoryPricePaymentModeIOS; 95 | final String introductoryPriceNumberOfPeriodsIOS; 96 | final String introductoryPriceSubscriptionPeriodIOS; 97 | 98 | /// android only 99 | final String subscriptionPeriodAndroid; 100 | final String introductoryPriceCyclesAndroid; 101 | final String introductoryPricePeriodAndroid; 102 | final String freeTrialPeriodAndroid; 103 | final String signatureAndroid; 104 | 105 | final String iconUrl; 106 | final String originalJson; 107 | final String originalPrice; 108 | ``` 109 | 110 | - PurchasedItem 111 | 112 | ```dart 113 | final String productId; 114 | final String transactionId; 115 | final DateTime transactionDate; 116 | final String transactionReceipt; 117 | final String purchaseToken; 118 | 119 | // Android only 120 | final String dataAndroid; 121 | final String signatureAndroid; 122 | final bool autoRenewingAndroid; 123 | final bool isAcknowledgedAndroid; 124 | final int purchaseStateAndroid; 125 | 126 | // iOS only 127 | final DateTime originalTransactionDateIOS; 128 | final String originalTransactionIdentifierIOS; 129 | ``` 130 | 131 | ## Install 132 | 133 | Add `flutter_inapp_purchase` as a dependency in pubspec.yaml 134 | 135 | For help on adding as a dependency, view the [documentation](https://flutter.io/using-packages/). 136 | 137 | ## Configuring in app purchase 138 | 139 | - Please refer to [Blog](https://medium.com/@dooboolab/react-native-in-app-purchase-121622d26b67). 140 | - [Amazon Kindle Fire](KINDLE.md) 141 | 142 | ## Usage Guide 143 | 144 | #### Android `connect` and `endConnection` 145 | 146 | - You should start the billing service in android to use its funtionalities. We recommend you to use `initConnection` getter method in `initState()`. Note that this step is necessary in `ios` also from `flutter_inapp_purchase@2.0.0+` which will also register the `purchaseUpdated` and `purchaseError` `Stream`. 147 | 148 | ```dart 149 | /// start connection for android 150 | @override 151 | void initState() { 152 | super.initState(); 153 | asyncInitState(); // async is not allowed on initState() directly 154 | } 155 | 156 | void asyncInitState() async { 157 | await FlutterInappPurchase.instance.initConnection; 158 | } 159 | ``` 160 | 161 | - You should end the billing service in android when you are done with it. Otherwise it will be keep running in background. We recommend to use this feature in `dispose()`. 162 | 163 | - Additionally, we've added `connectionUpdated` stream just in case if you'd like to monitor the connection more thoroughly form `2.0.1`. 164 | 165 | ``` 166 | _conectionSubscription = FlutterInappPurchase.connectionUpdated.listen((connected) { 167 | print('connected: $connected'); 168 | }); 169 | ``` 170 | 171 | > You can see how you can use this in detail in `example` project. 172 | 173 | ```dart 174 | /// start connection for android 175 | @override 176 | void dispose() async{ 177 | super.dispose(); 178 | await FlutterInappPurchase.instance.endConnection; 179 | } 180 | ``` 181 | 182 | #### Get IAP items 183 | 184 | ```dart 185 | void getItems () async { 186 | List items = await FlutterInappPurchase.instance.getProducts(_productLists); 187 | for (var item in items) { 188 | print('${item.toString()}'); 189 | this._items.add(item); 190 | } 191 | } 192 | ``` 193 | 194 | #### Purchase Item 195 | 196 | ```dart 197 | void purchase() { 198 | FlutterInappPurchase.instance.requestPurchase(item.productId); 199 | } 200 | ``` 201 | 202 | #### Register listeners to receive purchase 203 | 204 | ```dart 205 | StreamSubscription _purchaseUpdatedSubscription = FlutterInappPurchase.purchaseUpdated.listen((productItem) { 206 | print('purchase-updated: $productItem'); 207 | }); 208 | 209 | StreamSubscription _purchaseErrorSubscription = FlutterInappPurchase.purchaseError.listen((purchaseError) { 210 | print('purchase-error: $purchaseError'); 211 | }); 212 | ``` 213 | 214 | #### Remove listeners when ending connection 215 | 216 | ```dart 217 | _purchaseUpdatedSubscription.cancel(); 218 | _purchaseUpdatedSubscription = null; 219 | _purchaseErrorSubscription.cancel(); 220 | _purchaseErrorSubscription = null; 221 | ``` 222 | 223 | #### Receipt validation 224 | 225 | From `0.7.1`, we support receipt validation. For Android, you need separate json file from the service account to get the `access_token` from `google-apis`, therefore it is impossible to implement serverless. You should have your own backend and get `access_token`. With `access_token` you can simply call `validateReceiptAndroid` method we implemented. Further reading is [here](https://stackoverflow.com/questions/35127086/android-inapp-purchase-receipt-validation-google-play?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa). 226 | Currently, serverless receipt validation is possible using `validateReceiptIos` method. The first parameter, you should pass `transactionReceipt` which returns after `requestPurchase`. The second parameter, you should pass whether this is `test` environment. If `true`, it will request to `sandbox` and `false` it will request to `production`. 227 | 228 | ```dart 229 | validateReceipt() async { 230 | var receiptBody = { 231 | 'receipt-data': purchased.transactionReceipt, 232 | 'password': '******' 233 | }; 234 | const result = await validateReceiptIos(receiptBody, false); 235 | console.log(result); 236 | } 237 | ``` 238 | 239 | For further information, please refer to [guide](https://developer.apple.com/library/content/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html). 240 | 241 | #### App Store initiated purchases 242 | 243 | When the user starts an in-app purchase in the App Store, the transaction continues in your app, the product will then be added to a list that you can access through the method `getAppStoreInitiatedProducts`. This means you can decide how and when to continue the transaction. 244 | To continue the transaction simple use the standard purchase flow from this plugin. 245 | 246 | ```dart 247 | void checkForAppStoreInitiatedProducts() async { 248 | List appStoreProducts = await FlutterInappPurchase.getAppStoreInitiatedProducts(); // Get list of products 249 | if (appStoreProducts.length > 0) { 250 | _requestPurchase(appStoreProducts.last); // Buy last product in the list 251 | } 252 | } 253 | ``` 254 | 255 | ## Example 256 | 257 | Direct to [example readme](example/README.md) which is just a `cp` from example project. You can test this in real example project. 258 | 259 | ## ProGuard 260 | 261 | If you have enabled proguard you will need to add the following rules to your `proguard-rules.pro` 262 | 263 | ``` 264 | #In app Purchase 265 | -keep class com.amazon.** {*;} 266 | -keep class com.dooboolab.** { *; } 267 | -keep class com.android.vending.billing.** 268 | -dontwarn com.amazon.** 269 | -keepattributes *Annotation* 270 | ``` 271 | 272 | ## Q & A 273 | 274 | #### Can I buy product right away skipping fetching products if I already know productId? 275 | 276 | - You can in `Android` but not in `ios`. In `ios` you should always `fetchProducts` first. You can see more info [here](https://medium.com/ios-development-tips-and-tricks/working-with-ios-in-app-purchases-e4b55491479b). 277 | 278 | #### How do I validate receipt in ios? 279 | 280 | - Official doc is [here](https://developer.apple.com/library/archive/releasenotes/General/ValidateAppStoreReceipt/Chapters/ValidateRemotely.html). 281 | 282 | #### How do I validate receipt in android? 283 | 284 | - Offical doc is [here](https://developer.android.com/google/play/billing/billing_library_overview). 285 | - I've developed this feature for other developers to contribute easily who are aware of these things. The doc says you can also get the `accessToken` via play console without any of your backend server. You can get this by following process. 286 | - Select your app > Services & APIs > "YOUR LICENSE KEY FOR THIS APPLICATION Base64-encoded RSA public key to include in your binary". [reference](https://stackoverflow.com/questions/27132443/how-to-find-my-google-play-services-android-base64-public-key). 287 | 288 | #### Invalid productId in ios. 289 | 290 | - Please try below and make sure you've done belows. 291 | - Steps 292 | 1. Completed an effective "Agreements, Tax, and Banking." 293 | 2. Setup sandbox testing account in "Users and Roles." 294 | 3. Signed into iOS device with sandbox account. 295 | 4. Set up three In-App Purchases with the following status: 296 | i. Ready to Submit 297 | ii. Missing Metadata 298 | iii. Waiting for Review 299 | 5. Enable "In-App Purchase" in Xcode "Capabilities" and in Apple Developer -> "App ID" setting. Delete app / Restart device / Quit "store" related processes in Activity Monitor / Xcode Development Provisioning Profile -> Clean -> Build. 300 | 301 | ## Help Maintenance 302 | 303 | I've been maintaining quite many repos these days and burning out slowly. If you could help me cheer up, buying me a cup of coffee will make my life really happy and get much energy out of it. 304 | 305 | [![Paypal](https://www.paypalobjects.com/webstatic/mktg/Logo/pp-logo-100px.png)](https://paypal.me/dooboolab) 306 | Buy Me A Coffee 307 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | analyzer: 2 | strong-mode: 3 | implicit-casts: true 4 | implicit-dynamic: true -------------------------------------------------------------------------------- /ancillary/amazon.sdktester.json: -------------------------------------------------------------------------------- 1 | { 2 | "android.test.purchased" : { 3 | "itemType" : "ENTITLED", 4 | "price" : "4.99", 5 | "title": "Test Single Purchase", 6 | "description": "One purchase only", 7 | "smallIconUrl": "./assets/sample.theme.red.jpg" 8 | }, 9 | "android.test.canceled" : { 10 | "itemType" : "ENTITLED", 11 | "price" : "0.99", 12 | "title": "Test Single Purchase", 13 | "description": "One purchase only", 14 | "smallIconUrl": "./assets/sample.theme.green.jpg" 15 | }, 16 | "point_1000" : { 17 | "itemType" : "SUBSCRIPTION", 18 | "price" : "0.99", 19 | "title": "1000 Points", 20 | "description": "1000 Points to spend", 21 | "smallIconUrl": "./assets/sample.theme.blue.jpg" 22 | }, 23 | "5000_point" : { 24 | "itemType": "CONSUMABLE", 25 | "price": "10.00", 26 | "title": "Orange", 27 | "description": "An orange", 28 | "smallIconUrl": "http://www.amazon.com/orange.jpg" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | -------------------------------------------------------------------------------- /android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android_ 4 | Project android_ created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | 19 | 1636786187292 20 | 21 | 30 22 | 23 | org.eclipse.core.resources.regexFilterMatcher 24 | node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(VERSION(7.0-rc-1)) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'com.dooboolab.flutterinapppurchase' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.8.10' 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:7.4.2' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | 27 | android { 28 | if (project.android.hasProperty('namespace')) { 29 | namespace 'com.dooboolab.flutterinapppurchase' 30 | } 31 | compileSdkVersion 33 32 | 33 | defaultConfig { 34 | minSdkVersion 21 35 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 36 | } 37 | sourceSets { 38 | main.java.srcDirs += 'src/main/kotlin' 39 | } 40 | lintOptions { 41 | disable 'InvalidPackage' 42 | } 43 | compileOptions { 44 | sourceCompatibility JavaVersion.VERSION_1_8 45 | targetCompatibility JavaVersion.VERSION_1_8 46 | } 47 | 48 | kotlinOptions { 49 | jvmTarget = '1.8' 50 | } 51 | } 52 | 53 | dependencies { 54 | def billing_version = "6.0.1" 55 | 56 | implementation "com.android.billingclient:billing-ktx:$billing_version" 57 | implementation files('jars/in-app-purchasing-2.0.76.jar') 58 | implementation 'androidx.annotation:annotation:1.6.0' 59 | } 60 | 61 | -------------------------------------------------------------------------------- /android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-bin.zip 4 | networkTimeout=10000 5 | zipStoreBase=GRADLE_USER_HOME 6 | zipStorePath=wrapper/dists 7 | -------------------------------------------------------------------------------- /android/gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | # This is normally unused 84 | # shellcheck disable=SC2034 85 | APP_BASE_NAME=${0##*/} 86 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. 147 | # shellcheck disable=SC3045 148 | MAX_FD=$( ulimit -H -n ) || 149 | warn "Could not query maximum file descriptor limit" 150 | esac 151 | case $MAX_FD in #( 152 | '' | soft) :;; #( 153 | *) 154 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. 155 | # shellcheck disable=SC3045 156 | ulimit -n "$MAX_FD" || 157 | warn "Could not set maximum file descriptor limit to $MAX_FD" 158 | esac 159 | fi 160 | 161 | # Collect all arguments for the java command, stacking in reverse order: 162 | # * args from the command line 163 | # * the main class name 164 | # * -classpath 165 | # * -D...appname settings 166 | # * --module-path (only if needed) 167 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 168 | 169 | # For Cygwin or MSYS, switch paths to Windows format before running java 170 | if "$cygwin" || "$msys" ; then 171 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 172 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 173 | 174 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 175 | 176 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 177 | for arg do 178 | if 179 | case $arg in #( 180 | -*) false ;; # don't mess with options #( 181 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 182 | [ -e "$t" ] ;; #( 183 | *) false ;; 184 | esac 185 | then 186 | arg=$( cygpath --path --ignore --mixed "$arg" ) 187 | fi 188 | # Roll the args list around exactly as many times as the number of 189 | # args, so each arg winds up back in the position where it started, but 190 | # possibly modified. 191 | # 192 | # NB: a `for` loop captures its iteration list before it begins, so 193 | # changing the positional parameters here affects neither the number of 194 | # iterations, nor the values presented in `arg`. 195 | shift # remove old arg 196 | set -- "$@" "$arg" # push replacement arg 197 | done 198 | fi 199 | 200 | # Collect all arguments for the java command; 201 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 202 | # shell script including quotes and variable substitutions, so put them in 203 | # double quotes to make sure that they get re-expanded; and 204 | # * put everything else in single quotes, so that it's not re-expanded. 205 | 206 | set -- \ 207 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 208 | -classpath "$CLASSPATH" \ 209 | org.gradle.wrapper.GradleWrapperMain \ 210 | "$@" 211 | 212 | # Stop when "xargs" is not available. 213 | if ! command -v xargs >/dev/null 2>&1 214 | then 215 | die "xargs is not available" 216 | fi 217 | 218 | # Use "xargs" to parse quoted args. 219 | # 220 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 221 | # 222 | # In Bash we could simply go: 223 | # 224 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 225 | # set -- "${ARGS[@]}" "$@" 226 | # 227 | # but POSIX shell has neither arrays nor command substitution, so instead we 228 | # post-process each arg (as a line of input to sed) to backslash-escape any 229 | # character that might be a shell metacharacter, then use eval to reverse 230 | # that process (while maintaining the separation between arguments), and wrap 231 | # the whole thing up as a single "set" statement. 232 | # 233 | # This will of course break if any of these variables contains a newline or 234 | # an unmatched quote. 235 | # 236 | 237 | eval "set -- $( 238 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 239 | xargs -n1 | 240 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 241 | tr '\n' ' ' 242 | )" '"$@"' 243 | 244 | exec "$JAVACMD" "$@" 245 | -------------------------------------------------------------------------------- /android/gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | @rem This is normally unused 30 | set APP_BASE_NAME=%~n0 31 | set APP_HOME=%DIRNAME% 32 | 33 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 34 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 35 | 36 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 37 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 38 | 39 | @rem Find java.exe 40 | if defined JAVA_HOME goto findJavaFromJavaHome 41 | 42 | set JAVA_EXE=java.exe 43 | %JAVA_EXE% -version >NUL 2>&1 44 | if %ERRORLEVEL% equ 0 goto execute 45 | 46 | echo. 47 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 48 | echo. 49 | echo Please set the JAVA_HOME variable in your environment to match the 50 | echo location of your Java installation. 51 | 52 | goto fail 53 | 54 | :findJavaFromJavaHome 55 | set JAVA_HOME=%JAVA_HOME:"=% 56 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 57 | 58 | if exist "%JAVA_EXE%" goto execute 59 | 60 | echo. 61 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 62 | echo. 63 | echo Please set the JAVA_HOME variable in your environment to match the 64 | echo location of your Java installation. 65 | 66 | goto fail 67 | 68 | :execute 69 | @rem Setup the command line 70 | 71 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 72 | 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 %* 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if %ERRORLEVEL% equ 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 | set EXIT_CODE=%ERRORLEVEL% 85 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 86 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 87 | exit /b %EXIT_CODE% 88 | 89 | :mainEnd 90 | if "%OS%"=="Windows_NT" endlocal 91 | 92 | :omega 93 | -------------------------------------------------------------------------------- /android/jars/in-app-purchasing-2.0.76.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/android/jars/in-app-purchasing-2.0.76.jar -------------------------------------------------------------------------------- /android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'flutter_inapp_purchase' 2 | -------------------------------------------------------------------------------- /android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/dooboolab/flutterinapppurchase/AmazonInappPurchasePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.dooboolab.flutterinapppurchase 2 | 3 | import android.app.Activity 4 | import android.content.Context 5 | import android.util.Log 6 | import com.amazon.device.iap.PurchasingListener 7 | import com.amazon.device.iap.PurchasingService 8 | import com.amazon.device.iap.model.* 9 | import io.flutter.plugin.common.MethodCall 10 | import io.flutter.plugin.common.MethodChannel 11 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler 12 | import org.json.JSONArray 13 | import org.json.JSONException 14 | import org.json.JSONObject 15 | 16 | /** AmazonInappPurchasePlugin */ 17 | class AmazonInappPurchasePlugin : MethodCallHandler { 18 | private val TAG = "InappPurchasePlugin" 19 | private var safeResult: MethodResultWrapper? = null 20 | private var channel: MethodChannel? = null 21 | private var context: Context? = null 22 | private var activity: Activity? = null 23 | fun setContext(context: Context?) { 24 | this.context = context 25 | } 26 | 27 | fun setActivity(activity: Activity?) { 28 | this.activity = activity 29 | } 30 | 31 | fun setChannel(channel: MethodChannel?) { 32 | this.channel = channel 33 | } 34 | 35 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 36 | if(call.method == "getStore"){ 37 | result.success(FlutterInappPurchasePlugin.getStore()) 38 | return 39 | } 40 | 41 | safeResult = MethodResultWrapper(result, channel!!) 42 | 43 | try { 44 | PurchasingService.registerListener(context, purchasesUpdatedListener) 45 | } catch (e: Exception) { 46 | safeResult!!.error( 47 | call.method, 48 | "Call endConnection method if you want to start over.", 49 | e.message 50 | ) 51 | } 52 | when (call.method) { 53 | "initConnection" -> { 54 | PurchasingService.getUserData() 55 | safeResult!!.success("Billing client ready") 56 | } 57 | "endConnection" -> { 58 | safeResult!!.success("Billing client has ended.") 59 | } 60 | "isReady" -> { 61 | safeResult!!.success(true) 62 | } 63 | "showInAppMessages" -> { 64 | safeResult!!.success("in app messages not supported for amazon") 65 | } 66 | "consumeAllItems" -> { 67 | // consumable is a separate type in amazon 68 | safeResult!!.success("no-ops in amazon") 69 | } 70 | "getProducts", 71 | "getSubscriptions" -> { 72 | Log.d(TAG, call.method) 73 | val skus = call.argument>("skus")!! 74 | val productSkus: MutableSet = HashSet() 75 | for (i in skus.indices) { 76 | Log.d(TAG, "Adding " + skus[i]) 77 | productSkus.add(skus[i]) 78 | } 79 | PurchasingService.getProductData(productSkus) 80 | } 81 | "getAvailableItemsByType" -> { 82 | val type = call.argument("type") 83 | Log.d(TAG, "gaibt=$type") 84 | // NOTE: getPurchaseUpdates doesnt return Consumables which are FULFILLED 85 | if (type == "inapp") { 86 | PurchasingService.getPurchaseUpdates(true) 87 | } else if (type == "subs") { 88 | // Subscriptions are retrieved during inapp, so we just return empty list 89 | safeResult!!.success("[]") 90 | } else { 91 | safeResult!!.notImplemented() 92 | } 93 | } 94 | "getPurchaseHistoryByType" -> { 95 | // No equivalent 96 | safeResult!!.success("[]") 97 | } 98 | "buyItemByType" -> { 99 | val type = call.argument("type") 100 | //val obfuscatedAccountId = call.argument("obfuscatedAccountId") 101 | //val obfuscatedProfileId = call.argument("obfuscatedProfileId") 102 | val sku = call.argument("sku") 103 | val oldSku = call.argument("oldSku") 104 | //val prorationMode = call.argument("prorationMode")!! 105 | Log.d(TAG, "type=$type||sku=$sku||oldsku=$oldSku") 106 | val requestId = PurchasingService.purchase(sku) 107 | Log.d(TAG, "resid=$requestId") 108 | } 109 | "consumeProduct" -> { 110 | // consumable is a separate type in amazon 111 | safeResult!!.success("no-ops in amazon") 112 | } 113 | else -> { 114 | safeResult!!.notImplemented() 115 | } 116 | } 117 | } 118 | 119 | private val purchasesUpdatedListener: PurchasingListener = object : PurchasingListener { 120 | override fun onUserDataResponse(userDataResponse: UserDataResponse) { 121 | Log.d(TAG, "oudr=$userDataResponse") 122 | } 123 | 124 | // getItemsByType 125 | override fun onProductDataResponse(response: ProductDataResponse) { 126 | Log.d(TAG, "opdr=$response") 127 | val status = response.requestStatus 128 | Log.d(TAG, "onProductDataResponse: RequestStatus ($status)") 129 | when (status) { 130 | ProductDataResponse.RequestStatus.SUCCESSFUL -> { 131 | Log.d( 132 | TAG, 133 | "onProductDataResponse: successful. The item data map in this response includes the valid SKUs" 134 | ) 135 | val productData = response.productData 136 | //Log.d(TAG, "productData="+productData.toString()); 137 | val unavailableSkus = response.unavailableSkus 138 | Log.d( 139 | TAG, 140 | "onProductDataResponse: " + unavailableSkus.size + " unavailable skus" 141 | ) 142 | Log.d(TAG, "unavailableSkus=$unavailableSkus") 143 | val items = JSONArray() 144 | try { 145 | for ((_, product) in productData) { 146 | //val format = NumberFormat.getCurrencyInstance() 147 | val item = JSONObject() 148 | item.put("productId", product.sku) 149 | item.put("price", product.price) 150 | item.put("currency", null) 151 | when (product.productType) { 152 | ProductType.ENTITLED, ProductType.CONSUMABLE -> item.put( 153 | "type", 154 | "inapp" 155 | ) 156 | ProductType.SUBSCRIPTION -> item.put("type", "subs") 157 | } 158 | item.put("localizedPrice", product.price) 159 | item.put("title", product.title) 160 | item.put("description", product.description) 161 | item.put("introductoryPrice", "") 162 | item.put("subscriptionPeriodAndroid", "") 163 | item.put("freeTrialPeriodAndroid", "") 164 | item.put("introductoryPriceCyclesAndroid", 0) 165 | item.put("introductoryPricePeriodAndroid", "") 166 | Log.d(TAG, "opdr Putting $item") 167 | items.put(item) 168 | } 169 | //System.err.println("Sending "+items.toString()); 170 | safeResult!!.success(items.toString()) 171 | } catch (e: JSONException) { 172 | safeResult!!.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.message) 173 | } 174 | } 175 | ProductDataResponse.RequestStatus.FAILED -> { 176 | safeResult!!.error(TAG, "FAILED", null) 177 | Log.d(TAG, "onProductDataResponse: failed, should retry request") 178 | safeResult!!.error(TAG, "NOT_SUPPORTED", null) 179 | } 180 | ProductDataResponse.RequestStatus.NOT_SUPPORTED -> { 181 | Log.d(TAG, "onProductDataResponse: failed, should retry request") 182 | safeResult!!.error(TAG, "NOT_SUPPORTED", null) 183 | } 184 | } 185 | } 186 | 187 | // buyItemByType 188 | override fun onPurchaseResponse(response: PurchaseResponse) { 189 | Log.d(TAG, "opr=$response") 190 | when (val status = response.requestStatus) { 191 | PurchaseResponse.RequestStatus.SUCCESSFUL -> { 192 | val receipt = response.receipt 193 | PurchasingService.notifyFulfillment( 194 | receipt.receiptId, 195 | FulfillmentResult.FULFILLED 196 | ) 197 | val date = receipt.purchaseDate 198 | val transactionDate = date.time 199 | try { 200 | val item = getPurchaseData( 201 | receipt.sku, 202 | receipt.receiptId, 203 | receipt.receiptId, 204 | transactionDate.toDouble() 205 | ) 206 | Log.d(TAG, "opr Putting $item") 207 | safeResult!!.success(item.toString()) 208 | safeResult!!.invokeMethod("purchase-updated", item.toString()) 209 | } catch (e: JSONException) { 210 | safeResult!!.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.message) 211 | } 212 | } 213 | PurchaseResponse.RequestStatus.FAILED -> safeResult!!.error( 214 | TAG, 215 | "buyItemByType", 216 | "billingResponse is not ok: $status" 217 | ) 218 | else -> {} 219 | } 220 | } 221 | 222 | // getAvailableItemsByType 223 | override fun onPurchaseUpdatesResponse(response: PurchaseUpdatesResponse) { 224 | Log.d(TAG, "opudr=$response") 225 | when (response.requestStatus) { 226 | PurchaseUpdatesResponse.RequestStatus.SUCCESSFUL -> { 227 | val items = JSONArray() 228 | try { 229 | val receipts = response.receipts 230 | for (receipt in receipts) { 231 | val date = receipt.purchaseDate 232 | val transactionDate = date.time 233 | val item = getPurchaseData( 234 | receipt.sku, 235 | receipt.receiptId, 236 | receipt.receiptId, 237 | transactionDate.toDouble() 238 | ) 239 | Log.d(TAG, "opudr Putting $item") 240 | items.put(item) 241 | } 242 | safeResult!!.success(items.toString()) 243 | } catch (e: JSONException) { 244 | safeResult!!.error(TAG, "E_BILLING_RESPONSE_JSON_PARSE_ERROR", e.message) 245 | } 246 | } 247 | PurchaseUpdatesResponse.RequestStatus.FAILED -> safeResult!!.error( 248 | TAG, 249 | "FAILED", 250 | null 251 | ) 252 | PurchaseUpdatesResponse.RequestStatus.NOT_SUPPORTED -> { 253 | Log.d(TAG, "onPurchaseUpdatesResponse: failed, should retry request") 254 | safeResult!!.error(TAG, "NOT_SUPPORTED", null) 255 | } 256 | } 257 | } 258 | } 259 | 260 | @Throws(JSONException::class) 261 | fun getPurchaseData( 262 | productId: String?, transactionId: String?, transactionReceipt: String?, 263 | transactionDate: Double? 264 | ): JSONObject { 265 | val item = JSONObject() 266 | item.put("productId", productId) 267 | item.put("transactionId", transactionId) 268 | item.put("transactionReceipt", transactionReceipt) 269 | item.put("transactionDate", (transactionDate!!).toString()) 270 | item.put("dataAndroid", null) 271 | item.put("signatureAndroid", null) 272 | item.put("purchaseToken", null) 273 | return item 274 | } 275 | } -------------------------------------------------------------------------------- /android/src/main/kotlin/com/dooboolab/flutterinapppurchase/BillingError.kt: -------------------------------------------------------------------------------- 1 | package com.dooboolab.flutterinapppurchase 2 | 3 | import android.util.Log 4 | import com.android.billingclient.api.BillingClient.BillingResponseCode 5 | 6 | class BillingError { 7 | companion object { 8 | private const val TAG = "DoobooUtils" 9 | private const val E_UNKNOWN = "E_UNKNOWN" 10 | const val E_NOT_PREPARED = "E_NOT_PREPARED" 11 | private const val E_NOT_ENDED = "E_NOT_ENDED" 12 | private const val E_USER_CANCELLED = "E_USER_CANCELLED" 13 | private const val E_ITEM_UNAVAILABLE = "E_ITEM_UNAVAILABLE" 14 | private const val E_NETWORK_ERROR = "E_NETWORK_ERROR" 15 | private const val E_SERVICE_ERROR = "E_SERVICE_ERROR" 16 | private const val E_ALREADY_OWNED = "E_ALREADY_OWNED" 17 | private const val E_REMOTE_ERROR = "E_REMOTE_ERROR" 18 | private const val E_USER_ERROR = "E_USER_ERROR" 19 | private const val E_DEVELOPER_ERROR = "E_DEVELOPER_ERROR" 20 | const val E_BILLING_RESPONSE_JSON_PARSE_ERROR = "E_BILLING_RESPONSE_JSON_PARSE_ERROR" 21 | 22 | fun getErrorFromResponseData(responseCode: Int): ErrorData { 23 | Log.e(TAG, "Error Code : $responseCode") 24 | return when (responseCode) { 25 | BillingResponseCode.FEATURE_NOT_SUPPORTED -> 26 | ErrorData(E_SERVICE_ERROR,"This feature is not available on your device.") 27 | BillingResponseCode.SERVICE_DISCONNECTED -> 28 | ErrorData(E_NETWORK_ERROR, "The service is disconnected (check your internet connection.)") 29 | BillingResponseCode.OK -> ErrorData("OK","") 30 | BillingResponseCode.USER_CANCELED -> 31 | ErrorData(E_USER_CANCELLED, "Payment is Cancelled.") 32 | BillingResponseCode.SERVICE_UNAVAILABLE -> 33 | ErrorData(E_SERVICE_ERROR, "The service is unreachable. This may be your internet connection, or the Play Store may be down.") 34 | BillingResponseCode.BILLING_UNAVAILABLE -> 35 | ErrorData(E_SERVICE_ERROR, "Billing is unavailable. This may be a problem with your device, or the Play Store may be down.") 36 | BillingResponseCode.ITEM_UNAVAILABLE -> 37 | ErrorData( E_ITEM_UNAVAILABLE, "That item is unavailable.") 38 | BillingResponseCode.DEVELOPER_ERROR -> 39 | ErrorData(E_DEVELOPER_ERROR, "Google is indicating that we have some issue connecting to payment.") 40 | BillingResponseCode.ERROR -> 41 | ErrorData(E_UNKNOWN,"An unknown or unexpected error has occured. Please try again later.") 42 | BillingResponseCode.ITEM_ALREADY_OWNED -> 43 | ErrorData(E_ALREADY_OWNED, "You already own this item.") 44 | else -> ErrorData(E_UNKNOWN,"Purchase failed with code: $responseCode") 45 | } 46 | } 47 | } 48 | } 49 | 50 | data class ErrorData(val code: String, val message: String) -------------------------------------------------------------------------------- /android/src/main/kotlin/com/dooboolab/flutterinapppurchase/FlutterInappPurchasePlugin.kt: -------------------------------------------------------------------------------- 1 | package com.dooboolab.flutterinapppurchase 2 | 3 | import io.flutter.embedding.engine.plugins.FlutterPlugin 4 | import io.flutter.embedding.engine.plugins.activity.ActivityAware 5 | import android.content.Context 6 | import io.flutter.plugin.common.MethodChannel 7 | import io.flutter.embedding.engine.plugins.FlutterPlugin.FlutterPluginBinding 8 | import io.flutter.plugin.common.BinaryMessenger 9 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 10 | import android.content.pm.PackageManager.NameNotFoundException 11 | 12 | /** FlutterInappPurchasePlugin */ 13 | class FlutterInappPurchasePlugin : FlutterPlugin, ActivityAware { 14 | private var androidInappPurchasePlugin: AndroidInappPurchasePlugin? = null 15 | private var amazonInappPurchasePlugin: AmazonInappPurchasePlugin? = null 16 | private var channel: MethodChannel? = null 17 | override fun onAttachedToEngine(binding: FlutterPluginBinding) { 18 | onAttached(binding.applicationContext, binding.binaryMessenger) 19 | } 20 | 21 | private fun onAttached(context: Context, messenger: BinaryMessenger) { 22 | isAndroid = isPackageInstalled(context, "com.android.vending") 23 | isAmazon = isPackageInstalled(context, "com.amazon.venezia") 24 | 25 | // In the case of an amazon device which has been side loaded with the Google Play store, 26 | // we should use the store the app was installed from. 27 | if (isAmazon && isAndroid) { 28 | if (isAppInstalledFrom(context, "amazon")) { 29 | isAndroid = false 30 | } else { 31 | isAmazon = false 32 | } 33 | } 34 | channel = MethodChannel(messenger, "flutter_inapp") 35 | if (isAndroid) { 36 | androidInappPurchasePlugin = AndroidInappPurchasePlugin() 37 | androidInappPurchasePlugin!!.setContext(context) 38 | androidInappPurchasePlugin!!.setChannel(channel) 39 | channel!!.setMethodCallHandler(androidInappPurchasePlugin) 40 | } else if (isAmazon) { 41 | amazonInappPurchasePlugin = AmazonInappPurchasePlugin() 42 | amazonInappPurchasePlugin!!.setContext(context) 43 | amazonInappPurchasePlugin!!.setChannel(channel) 44 | channel!!.setMethodCallHandler(amazonInappPurchasePlugin) 45 | } 46 | } 47 | 48 | override fun onDetachedFromEngine(binding: FlutterPluginBinding) { 49 | channel!!.setMethodCallHandler(null) 50 | channel = null 51 | if (isAndroid) { 52 | androidInappPurchasePlugin!!.setChannel(null) 53 | } else if (isAmazon) { 54 | amazonInappPurchasePlugin!!.setChannel(null) 55 | } 56 | } 57 | 58 | override fun onAttachedToActivity(binding: ActivityPluginBinding) { 59 | if (isAndroid) { 60 | androidInappPurchasePlugin!!.setActivity(binding.activity) 61 | } else if (isAmazon) { 62 | amazonInappPurchasePlugin!!.setActivity(binding.activity) 63 | } 64 | } 65 | 66 | override fun onDetachedFromActivity() { 67 | if (isAndroid) { 68 | androidInappPurchasePlugin!!.setActivity(null) 69 | androidInappPurchasePlugin!!.onDetachedFromActivity() 70 | } else if (isAmazon) { 71 | amazonInappPurchasePlugin!!.setActivity(null) 72 | } 73 | } 74 | 75 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { 76 | onAttachedToActivity(binding) 77 | } 78 | 79 | override fun onDetachedFromActivityForConfigChanges() { 80 | onDetachedFromActivity() 81 | } 82 | 83 | private fun setAndroidInappPurchasePlugin(androidInappPurchasePlugin: AndroidInappPurchasePlugin) { 84 | this.androidInappPurchasePlugin = androidInappPurchasePlugin 85 | } 86 | 87 | private fun setAmazonInappPurchasePlugin(amazonInappPurchasePlugin: AmazonInappPurchasePlugin) { 88 | this.amazonInappPurchasePlugin = amazonInappPurchasePlugin 89 | } 90 | 91 | companion object { 92 | private var isAndroid = false 93 | private var isAmazon = false 94 | 95 | fun getStore(): String { 96 | return if (!isAndroid && !isAmazon) "none" else if (isAndroid) "play_store" else "amazon" 97 | } 98 | 99 | private fun isPackageInstalled(ctx: Context, packageName: String): Boolean { 100 | return try { 101 | ctx.packageManager.getPackageInfo(packageName, 0) 102 | true 103 | } catch (e: NameNotFoundException) { 104 | false 105 | } 106 | } 107 | 108 | fun isAppInstalledFrom(ctx: Context, installer: String?): Boolean { 109 | val installerPackageName = ctx.packageManager.getInstallerPackageName(ctx.packageName) 110 | return installer != null && installerPackageName != null && installerPackageName.contains( 111 | installer 112 | ) 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/dooboolab/flutterinapppurchase/MethodResultWrapper.kt: -------------------------------------------------------------------------------- 1 | package com.dooboolab.flutterinapppurchase 2 | 3 | import android.os.Handler 4 | import io.flutter.plugin.common.MethodChannel 5 | import android.os.Looper 6 | 7 | // MethodChannel.Result wrapper that responds on the platform thread. 8 | class MethodResultWrapper internal constructor( 9 | private val safeResult: MethodChannel.Result, 10 | private val safeChannel: MethodChannel 11 | ) : MethodChannel.Result { 12 | private val handler: Handler = Handler(Looper.getMainLooper()) 13 | private var exhausted: Boolean = false 14 | override fun success(result: Any?) { 15 | if (!exhausted) { 16 | exhausted = true 17 | 18 | handler.post { safeResult.success(result) } 19 | } 20 | } 21 | 22 | override fun error(errorCode: String, errorMessage: String?, errorDetails: Any?) { 23 | if (!exhausted) { 24 | exhausted = true 25 | 26 | handler.post { safeResult.error(errorCode, errorMessage, errorDetails) } 27 | } 28 | } 29 | 30 | override fun notImplemented() { 31 | if (!exhausted) { 32 | exhausted = true 33 | 34 | handler.post { safeResult.notImplemented() } 35 | } 36 | } 37 | 38 | fun invokeMethod(method: String?, arguments: Any?) { 39 | handler.post { safeChannel.invokeMethod(method!!, arguments, null) } 40 | } 41 | 42 | } -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .dart_tool/ 3 | 4 | .packages 5 | .pub/ 6 | 7 | build/ 8 | 9 | .flutter-plugins 10 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: 66091f969653fd3535b265ddcd87436901858a1d 8 | channel: dev 9 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # flutter_inapp_purchase_example 2 | 3 | Demonstrates how to use the flutter_inapp_purchase plugin. 4 | 5 | ## Getting Started 6 | 7 | For help getting started with Flutter, view our online 8 | [documentation](https://flutter.io/). 9 | 10 | ## Example 11 | Below code is just a `cp` from example project. You can test this in real example project. 12 | ```dart 13 | class InApp extends StatefulWidget { 14 | @override 15 | _InAppState createState() => new _InAppState(); 16 | } 17 | 18 | class _InAppState extends State { 19 | StreamSubscription _purchaseUpdatedSubscription; 20 | StreamSubscription _purchaseErrorSubscription; 21 | final List _productLists = Platform.isAndroid 22 | ? [ 23 | 'android.test.purchased', 24 | 'point_1000', 25 | '5000_point', 26 | 'android.test.canceled', 27 | ] 28 | : ['com.cooni.point1000', 'com.cooni.point5000']; 29 | 30 | String _platformVersion = 'Unknown'; 31 | List _items = []; 32 | List _purchases = []; 33 | 34 | @override 35 | void initState() { 36 | super.initState(); 37 | initPlatformState(); 38 | } 39 | 40 | // Platform messages are asynchronous, so we initialize in an async method. 41 | Future initPlatformState() async { 42 | String platformVersion; 43 | // Platform messages may fail, so we use a try/catch PlatformException. 44 | try { 45 | platformVersion = await FlutterInappPurchase.instance.platformVersion; 46 | } on PlatformException { 47 | platformVersion = 'Failed to get platform version.'; 48 | } 49 | 50 | // prepare 51 | var result = await FlutterInappPurchase.instance.initConnection; 52 | print('result: $result'); 53 | 54 | // If the widget was removed from the tree while the asynchronous platform 55 | // message was in flight, we want to discard the reply rather than calling 56 | // setState to update our non-existent appearance. 57 | if (!mounted) return; 58 | 59 | setState(() { 60 | _platformVersion = platformVersion; 61 | }); 62 | 63 | // refresh items for android 64 | try { 65 | String msg = await FlutterInappPurchase.instance.consumeAllItems; 66 | print('consumeAllItems: $msg'); 67 | } catch (err) { 68 | print('consumeAllItems error: $err'); 69 | } 70 | 71 | _purchaseUpdatedSubscription = FlutterInappPurchase.purchaseUpdated.listen((productItem) { 72 | print('purchase-updated: $productItem'); 73 | }); 74 | 75 | _purchaseErrorSubscription = FlutterInappPurchase.purchaseError.listen((purchaseError) { 76 | print('purchase-error: $purchaseError'); 77 | }); 78 | } 79 | 80 | void _requestPurchase(IAPItem item) { 81 | FlutterInappPurchase.instance.requestPurchase(item.productId); 82 | } 83 | 84 | Future _getProduct() async { 85 | List items = await FlutterInappPurchase.instance.getProducts(_productLists); 86 | for (var item in items) { 87 | print('${item.toString()}'); 88 | this._items.add(item); 89 | } 90 | 91 | setState(() { 92 | this._items = items; 93 | this._purchases = []; 94 | }); 95 | } 96 | 97 | Future _getPurchases() async { 98 | List items = 99 | await FlutterInappPurchase.instance.getAvailablePurchases(); 100 | for (var item in items) { 101 | print('${item.toString()}'); 102 | this._purchases.add(item); 103 | } 104 | 105 | setState(() { 106 | this._items = []; 107 | this._purchases = items; 108 | }); 109 | } 110 | 111 | Future _getPurchaseHistory() async { 112 | List items = await FlutterInappPurchase.instance.getPurchaseHistory(); 113 | for (var item in items) { 114 | print('${item.toString()}'); 115 | this._purchases.add(item); 116 | } 117 | 118 | setState(() { 119 | this._items = []; 120 | this._purchases = items; 121 | }); 122 | } 123 | 124 | List _renderInApps() { 125 | List widgets = this 126 | ._items 127 | .map((item) => Container( 128 | margin: EdgeInsets.symmetric(vertical: 10.0), 129 | child: Container( 130 | child: Column( 131 | children: [ 132 | Container( 133 | margin: EdgeInsets.only(bottom: 5.0), 134 | child: Text( 135 | item.toString(), 136 | style: TextStyle( 137 | fontSize: 18.0, 138 | color: Colors.black, 139 | ), 140 | ), 141 | ), 142 | FlatButton( 143 | color: Colors.orange, 144 | onPressed: () { 145 | print("---------- Buy Item Button Pressed"); 146 | this._requestPurchase(item); 147 | }, 148 | child: Row( 149 | children: [ 150 | Expanded( 151 | child: Container( 152 | height: 48.0, 153 | alignment: Alignment(-1.0, 0.0), 154 | child: Text('Buy Item'), 155 | ), 156 | ), 157 | ], 158 | ), 159 | ), 160 | ], 161 | ), 162 | ), 163 | )) 164 | .toList(); 165 | return widgets; 166 | } 167 | 168 | List _renderPurchases() { 169 | List widgets = this 170 | ._purchases 171 | .map((item) => Container( 172 | margin: EdgeInsets.symmetric(vertical: 10.0), 173 | child: Container( 174 | child: Column( 175 | children: [ 176 | Container( 177 | margin: EdgeInsets.only(bottom: 5.0), 178 | child: Text( 179 | item.toString(), 180 | style: TextStyle( 181 | fontSize: 18.0, 182 | color: Colors.black, 183 | ), 184 | ), 185 | ) 186 | ], 187 | ), 188 | ), 189 | )) 190 | .toList(); 191 | return widgets; 192 | } 193 | 194 | @override 195 | Widget build(BuildContext context) { 196 | double screenWidth = MediaQuery.of(context).size.width-20; 197 | double buttonWidth=(screenWidth/3)-20; 198 | 199 | return Container( 200 | padding: EdgeInsets.all(10.0), 201 | child: ListView( 202 | children: [ 203 | Column( 204 | crossAxisAlignment: CrossAxisAlignment.start, 205 | mainAxisAlignment: MainAxisAlignment.start, 206 | children: [ 207 | Container( 208 | child: Text( 209 | 'Running on: $_platformVersion\n', 210 | style: TextStyle(fontSize: 18.0), 211 | ), 212 | ), 213 | Column( 214 | children: [ 215 | Row( 216 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 217 | children: [ 218 | Container( 219 | width: buttonWidth, 220 | height: 60.0, 221 | margin: EdgeInsets.all(7.0), 222 | child: FlatButton( 223 | color: Colors.amber, 224 | padding: EdgeInsets.all(0.0), 225 | onPressed: () async { 226 | print("---------- Connect Billing Button Pressed"); 227 | await FlutterInappPurchase.instance.initConnection; 228 | }, 229 | child: Container( 230 | padding: EdgeInsets.symmetric(horizontal: 20.0), 231 | alignment: Alignment(0.0, 0.0), 232 | child: Text( 233 | 'Connect Billing', 234 | style: TextStyle( 235 | fontSize: 16.0, 236 | ), 237 | ), 238 | ), 239 | ), 240 | ), 241 | Container( 242 | width: buttonWidth, 243 | height: 60.0, 244 | margin: EdgeInsets.all(7.0), 245 | child: FlatButton( 246 | color: Colors.amber, 247 | padding: EdgeInsets.all(0.0), 248 | onPressed: () async { 249 | print("---------- End Connection Button Pressed"); 250 | await FlutterInappPurchase.instance.endConnection; 251 | _purchaseUpdatedSubscription.cancel(); 252 | _purchaseUpdatedSubscription = null; 253 | _purchaseErrorSubscription.cancel(); 254 | _purchaseErrorSubscription = null; 255 | setState(() { 256 | this._items = []; 257 | this._purchases = []; 258 | }); 259 | }, 260 | child: Container( 261 | padding: EdgeInsets.symmetric(horizontal: 20.0), 262 | alignment: Alignment(0.0, 0.0), 263 | child: Text( 264 | 'End Connection', 265 | style: TextStyle( 266 | fontSize: 16.0, 267 | ), 268 | ), 269 | ), 270 | ), 271 | ), 272 | ], 273 | ), 274 | Row( 275 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 276 | children: [ 277 | Container( 278 | width: buttonWidth, 279 | height: 60.0, 280 | margin: EdgeInsets.all(7.0), 281 | child: FlatButton( 282 | color: Colors.green, 283 | padding: EdgeInsets.all(0.0), 284 | onPressed: () { 285 | print("---------- Get Items Button Pressed"); 286 | this._getProduct(); 287 | }, 288 | child: Container( 289 | padding: EdgeInsets.symmetric(horizontal: 20.0), 290 | alignment: Alignment(0.0, 0.0), 291 | child: Text( 292 | 'Get Items', 293 | style: TextStyle( 294 | fontSize: 16.0, 295 | ), 296 | ), 297 | ), 298 | )), 299 | Container( 300 | width: buttonWidth, 301 | height: 60.0, 302 | margin: EdgeInsets.all(7.0), 303 | child: FlatButton( 304 | color: Colors.green, 305 | padding: EdgeInsets.all(0.0), 306 | onPressed: () { 307 | print( 308 | "---------- Get Purchases Button Pressed"); 309 | this._getPurchases(); 310 | }, 311 | child: Container( 312 | padding: EdgeInsets.symmetric(horizontal: 20.0), 313 | alignment: Alignment(0.0, 0.0), 314 | child: Text( 315 | 'Get Purchases', 316 | style: TextStyle( 317 | fontSize: 16.0, 318 | ), 319 | ), 320 | ), 321 | )), 322 | Container( 323 | width: buttonWidth, 324 | height: 60.0, 325 | margin: EdgeInsets.all(7.0), 326 | child: FlatButton( 327 | color: Colors.green, 328 | padding: EdgeInsets.all(0.0), 329 | onPressed: () { 330 | print( 331 | "---------- Get Purchase History Button Pressed"); 332 | this._getPurchaseHistory(); 333 | }, 334 | child: Container( 335 | padding: EdgeInsets.symmetric(horizontal: 20.0), 336 | alignment: Alignment(0.0, 0.0), 337 | child: Text( 338 | 'Get Purchase History', 339 | style: TextStyle( 340 | fontSize: 16.0, 341 | ), 342 | ), 343 | ), 344 | )), 345 | ]), 346 | ], 347 | ), 348 | Column( 349 | children: this._renderInApps(), 350 | ), 351 | Column( 352 | children: this._renderPurchases(), 353 | ), 354 | ], 355 | ), 356 | ], 357 | ), 358 | ); 359 | } 360 | } 361 | ``` 362 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | *.class 3 | .gradle 4 | /local.properties 5 | /.idea/workspace.xml 6 | /.idea/libraries 7 | .DS_Store 8 | /build 9 | /captures 10 | GeneratedPluginRegistrant.java 11 | -------------------------------------------------------------------------------- /example/android/.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | android 4 | Project android created by Buildship. 5 | 6 | 7 | 8 | 9 | org.eclipse.buildship.core.gradleprojectbuilder 10 | 11 | 12 | 13 | 14 | 15 | org.eclipse.buildship.core.gradleprojectnature 16 | 17 | 18 | 19 | 1636786187287 20 | 21 | 30 22 | 23 | org.eclipse.core.resources.regexFilterMatcher 24 | node_modules|.git|__CREATED_BY_JAVA_LANGUAGE_SERVER__ 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /example/android/.settings/org.eclipse.buildship.core.prefs: -------------------------------------------------------------------------------- 1 | arguments= 2 | auto.sync=false 3 | build.scans.enabled=false 4 | connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER) 5 | connection.project.dir= 6 | eclipse.preferences.version=1 7 | gradle.user.home= 8 | java.home=/Library/Java/JavaVirtualMachines/zulu-11.jdk/Contents/Home 9 | jvm.arguments= 10 | offline.mode=false 11 | override.workspace.settings=true 12 | show.console.view=true 13 | show.executions.view=true 14 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | throw new GradleException("versionCode not found. Define flutter.versionCode in the local.properties file.") 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | throw new GradleException("versionName not found. Define flutter.versionName in the local.properties file.") 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 26 | 27 | android { 28 | compileSdkVersion 33 29 | 30 | lintOptions { 31 | disable 'InvalidPackage' 32 | } 33 | 34 | defaultConfig { 35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 36 | applicationId "com.dooboolab.flutterinapppurchaseexample" 37 | minSdkVersion 21 38 | targetSdkVersion 33 39 | versionCode flutterVersionCode.toInteger() 40 | versionName flutterVersionName 41 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" 42 | } 43 | 44 | buildTypes { 45 | release { 46 | // TODO: Add your own signing config for the release build. 47 | // Signing with the debug keys for now, so `flutter run --release` works. 48 | signingConfig signingConfigs.debug 49 | } 50 | } 51 | } 52 | 53 | flutter { 54 | source '../..' 55 | } 56 | 57 | dependencies { 58 | testImplementation 'junit:junit:4.13.2' 59 | androidTestImplementation 'androidx.test:runner:1.5.2' 60 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' 61 | } 62 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 8 | 9 | 10 | 15 | 18 | 26 | 27 | 28 | 32 | 35 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 54 | 55 | 57 | 58 | 59 | 60 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/dooboolab/iapexample/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.dooboolab.iapexample; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.embedding.android.FlutterActivity; 5 | 6 | public class MainActivity extends FlutterActivity { 7 | @Override 8 | protected void onCreate(Bundle savedInstanceState) { 9 | super.onCreate(savedInstanceState); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/android/app/src/main/java/com/dooboolab/test/MainActivity.java: -------------------------------------------------------------------------------- 1 | package com.dooboolab.test; 2 | 3 | import android.os.Bundle; 4 | import io.flutter.embedding.android.FlutterActivity; 5 | 6 | public class MainActivity extends FlutterActivity { 7 | @Override 8 | protected void onCreate(Bundle savedInstanceState) { 9 | super.onCreate(savedInstanceState); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 8 | 11 | 12 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | repositories { 3 | google() 4 | jcenter() 5 | } 6 | 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:7.4.2' 9 | } 10 | } 11 | 12 | allprojects { 13 | repositories { 14 | google() 15 | jcenter() 16 | } 17 | } 18 | 19 | rootProject.buildDir = '../build' 20 | subprojects { 21 | project.buildDir = "${rootProject.buildDir}/${project.name}" 22 | } 23 | subprojects { 24 | project.evaluationDependsOn(':app') 25 | } 26 | 27 | tasks.register("clean", Delete) { 28 | delete rootProject.buildDir 29 | } 30 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | android.enableJetifier=true 2 | android.useAndroidX=true 3 | org.gradle.jvmargs=-Xmx1536M 4 | android.enableR8=true 5 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/android/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.0.2-all.zip 7 | -------------------------------------------------------------------------------- /example/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 | # Attempt to set APP_HOME 46 | # Resolve links: $0 may be a link 47 | PRG="$0" 48 | # Need this for relative symlinks. 49 | while [ -h "$PRG" ] ; do 50 | ls=`ls -ld "$PRG"` 51 | link=`expr "$ls" : '.*-> \(.*\)$'` 52 | if expr "$link" : '/.*' > /dev/null; then 53 | PRG="$link" 54 | else 55 | PRG=`dirname "$PRG"`"/$link" 56 | fi 57 | done 58 | SAVED="`pwd`" 59 | cd "`dirname \"$PRG\"`/" >/dev/null 60 | APP_HOME="`pwd -P`" 61 | cd "$SAVED" >/dev/null 62 | 63 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 64 | 65 | # Determine the Java command to use to start the JVM. 66 | if [ -n "$JAVA_HOME" ] ; then 67 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 68 | # IBM's JDK on AIX uses strange locations for the executables 69 | JAVACMD="$JAVA_HOME/jre/sh/java" 70 | else 71 | JAVACMD="$JAVA_HOME/bin/java" 72 | fi 73 | if [ ! -x "$JAVACMD" ] ; then 74 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 75 | 76 | Please set the JAVA_HOME variable in your environment to match the 77 | location of your Java installation." 78 | fi 79 | else 80 | JAVACMD="java" 81 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 82 | 83 | Please set the JAVA_HOME variable in your environment to match the 84 | location of your Java installation." 85 | fi 86 | 87 | # Increase the maximum file descriptors if we can. 88 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 89 | MAX_FD_LIMIT=`ulimit -H -n` 90 | if [ $? -eq 0 ] ; then 91 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 92 | MAX_FD="$MAX_FD_LIMIT" 93 | fi 94 | ulimit -n $MAX_FD 95 | if [ $? -ne 0 ] ; then 96 | warn "Could not set maximum file descriptor limit: $MAX_FD" 97 | fi 98 | else 99 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 100 | fi 101 | fi 102 | 103 | # For Darwin, add options to specify how the application appears in the dock 104 | if $darwin; then 105 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 106 | fi 107 | 108 | # For Cygwin, switch paths to Windows format before running java 109 | if $cygwin ; then 110 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 111 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 112 | JAVACMD=`cygpath --unix "$JAVACMD"` 113 | 114 | # We build the pattern for arguments to be converted via cygpath 115 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 116 | SEP="" 117 | for dir in $ROOTDIRSRAW ; do 118 | ROOTDIRS="$ROOTDIRS$SEP$dir" 119 | SEP="|" 120 | done 121 | OURCYGPATTERN="(^($ROOTDIRS))" 122 | # Add a user-defined pattern to the cygpath arguments 123 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 124 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 125 | fi 126 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 127 | i=0 128 | for arg in "$@" ; do 129 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 130 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 131 | 132 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 133 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 134 | else 135 | eval `echo args$i`="\"$arg\"" 136 | fi 137 | i=$((i+1)) 138 | done 139 | case $i in 140 | (0) set -- ;; 141 | (1) set -- "$args0" ;; 142 | (2) set -- "$args0" "$args1" ;; 143 | (3) set -- "$args0" "$args1" "$args2" ;; 144 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 145 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 146 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 147 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 148 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 149 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 150 | esac 151 | fi 152 | 153 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 154 | function splitJvmOpts() { 155 | JVM_OPTS=("$@") 156 | } 157 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 158 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 159 | 160 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 161 | -------------------------------------------------------------------------------- /example/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 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() 4 | 5 | def plugins = new Properties() 6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') 7 | if (pluginsFile.exists()) { 8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } 9 | } 10 | 11 | plugins.each { name, path -> 12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() 13 | include ":$name" 14 | project(":$name").projectDir = pluginDirectory 15 | } 16 | -------------------------------------------------------------------------------- /example/flutter_inapp_purchase_example.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /example/flutter_inapp_purchase_example_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/app.flx 37 | /Flutter/app.zip 38 | /Flutter/flutter_assets/ 39 | /Flutter/App.framework 40 | /Flutter/Flutter.framework 41 | /Flutter/Generated.xcconfig 42 | /ServiceDefinitions.json 43 | 44 | Pods/ 45 | .symlinks/ 46 | -------------------------------------------------------------------------------- /example/ios/Flutter/.last_build_id: -------------------------------------------------------------------------------- 1 | 64b7d48f089c80732795ca9f2fc67c24 -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Flutter.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # This podspec is NOT to be published. It is only used as a local source! 3 | # This is a generated file; do not edit or check into version control. 4 | # 5 | 6 | Pod::Spec.new do |s| 7 | s.name = 'Flutter' 8 | s.version = '1.0.0' 9 | s.summary = 'A UI toolkit for beautiful and fast apps.' 10 | s.homepage = 'https://flutter.dev' 11 | s.license = { :type => 'BSD' } 12 | s.author = { 'Flutter Dev Team' => 'flutter-dev@googlegroups.com' } 13 | s.source = { :git => 'https://github.com/flutter/engine', :tag => s.version.to_s } 14 | s.ios.deployment_target = '11.0' 15 | # Framework linking is handled by Flutter tooling, not CocoaPods. 16 | # Add a placeholder to satisfy `s.dependency 'Flutter'` plugin podspecs. 17 | s.vendored_frameworks = 'path/to/nothing' 18 | end 19 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '11.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 32 | end 33 | 34 | post_install do |installer| 35 | installer.pods_project.targets.each do |target| 36 | flutter_additional_ios_build_settings(target) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - flutter_inapp_purchase (0.0.1): 4 | - Flutter 5 | 6 | DEPENDENCIES: 7 | - Flutter (from `Flutter`) 8 | - flutter_inapp_purchase (from `.symlinks/plugins/flutter_inapp_purchase/ios`) 9 | 10 | EXTERNAL SOURCES: 11 | Flutter: 12 | :path: Flutter 13 | flutter_inapp_purchase: 14 | :path: ".symlinks/plugins/flutter_inapp_purchase/ios" 15 | 16 | SPEC CHECKSUMS: 17 | Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 18 | flutter_inapp_purchase: 5c6a1ac3f11b11d0c8c0321c0c41c1f05805e4c8 19 | 20 | PODFILE CHECKSUM: 663715e941f9adb426e33bf9376914006f9ea95b 21 | 22 | COCOAPODS: 1.13.0 23 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 27672194C9D5FA8DDDF731B8 /* libPods-Runner.a in Frameworks */ = {isa = PBXBuildFile; fileRef = A72A37C715E27BACEF745D36 /* libPods-Runner.a */; }; 12 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 13 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB21CF90195004384FC /* Debug.xcconfig */; }; 14 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */ = {isa = PBXBuildFile; fileRef = 9740EEB31CF90195004384FC /* Generated.xcconfig */; }; 15 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */; }; 16 | 97C146F31CF9000F007C117D /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 97C146F21CF9000F007C117D /* main.m */; }; 17 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 18 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 19 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 20 | /* End PBXBuildFile section */ 21 | 22 | /* Begin PBXCopyFilesBuildPhase section */ 23 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 24 | isa = PBXCopyFilesBuildPhase; 25 | buildActionMask = 2147483647; 26 | dstPath = ""; 27 | dstSubfolderSpec = 10; 28 | files = ( 29 | ); 30 | name = "Embed Frameworks"; 31 | runOnlyForDeploymentPostprocessing = 0; 32 | }; 33 | /* End PBXCopyFilesBuildPhase section */ 34 | 35 | /* Begin PBXFileReference section */ 36 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 37 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 38 | 1D70DAC946071716E420D20E /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 39 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 40 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 41 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = AppDelegate.h; sourceTree = ""; }; 42 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = AppDelegate.m; sourceTree = ""; }; 43 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 44 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 45 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 46 | 97C146F21CF9000F007C117D /* main.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = main.m; sourceTree = ""; }; 47 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 48 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 49 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 50 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 51 | A72A37C715E27BACEF745D36 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; 52 | FF0FE51B5618DEE6A1B14EAD /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 53 | /* End PBXFileReference section */ 54 | 55 | /* Begin PBXFrameworksBuildPhase section */ 56 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 57 | isa = PBXFrameworksBuildPhase; 58 | buildActionMask = 2147483647; 59 | files = ( 60 | 27672194C9D5FA8DDDF731B8 /* libPods-Runner.a in Frameworks */, 61 | ); 62 | runOnlyForDeploymentPostprocessing = 0; 63 | }; 64 | /* End PBXFrameworksBuildPhase section */ 65 | 66 | /* Begin PBXGroup section */ 67 | 296F2EDE9AD48161597CA429 /* Frameworks */ = { 68 | isa = PBXGroup; 69 | children = ( 70 | A72A37C715E27BACEF745D36 /* libPods-Runner.a */, 71 | ); 72 | name = Frameworks; 73 | sourceTree = ""; 74 | }; 75 | 9740EEB11CF90186004384FC /* Flutter */ = { 76 | isa = PBXGroup; 77 | children = ( 78 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 79 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 80 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 81 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 82 | ); 83 | name = Flutter; 84 | sourceTree = ""; 85 | }; 86 | 97C146E51CF9000F007C117D = { 87 | isa = PBXGroup; 88 | children = ( 89 | 9740EEB11CF90186004384FC /* Flutter */, 90 | 97C146F01CF9000F007C117D /* Runner */, 91 | 97C146EF1CF9000F007C117D /* Products */, 92 | C7B32ECD7F7FE068F924AA78 /* Pods */, 93 | 296F2EDE9AD48161597CA429 /* Frameworks */, 94 | ); 95 | sourceTree = ""; 96 | }; 97 | 97C146EF1CF9000F007C117D /* Products */ = { 98 | isa = PBXGroup; 99 | children = ( 100 | 97C146EE1CF9000F007C117D /* Runner.app */, 101 | ); 102 | name = Products; 103 | sourceTree = ""; 104 | }; 105 | 97C146F01CF9000F007C117D /* Runner */ = { 106 | isa = PBXGroup; 107 | children = ( 108 | 7AFFD8ED1D35381100E5BB4D /* AppDelegate.h */, 109 | 7AFFD8EE1D35381100E5BB4D /* AppDelegate.m */, 110 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 111 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 112 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 113 | 97C147021CF9000F007C117D /* Info.plist */, 114 | 97C146F11CF9000F007C117D /* Supporting Files */, 115 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 116 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 117 | ); 118 | path = Runner; 119 | sourceTree = ""; 120 | }; 121 | 97C146F11CF9000F007C117D /* Supporting Files */ = { 122 | isa = PBXGroup; 123 | children = ( 124 | 97C146F21CF9000F007C117D /* main.m */, 125 | ); 126 | name = "Supporting Files"; 127 | sourceTree = ""; 128 | }; 129 | C7B32ECD7F7FE068F924AA78 /* Pods */ = { 130 | isa = PBXGroup; 131 | children = ( 132 | 1D70DAC946071716E420D20E /* Pods-Runner.debug.xcconfig */, 133 | FF0FE51B5618DEE6A1B14EAD /* Pods-Runner.release.xcconfig */, 134 | ); 135 | name = Pods; 136 | sourceTree = ""; 137 | }; 138 | /* End PBXGroup section */ 139 | 140 | /* Begin PBXNativeTarget section */ 141 | 97C146ED1CF9000F007C117D /* Runner */ = { 142 | isa = PBXNativeTarget; 143 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 144 | buildPhases = ( 145 | 6D038B862F639D466FC324F4 /* [CP] Check Pods Manifest.lock */, 146 | 9740EEB61CF901F6004384FC /* Run Script */, 147 | 97C146EA1CF9000F007C117D /* Sources */, 148 | 97C146EB1CF9000F007C117D /* Frameworks */, 149 | 97C146EC1CF9000F007C117D /* Resources */, 150 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 151 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 152 | ); 153 | buildRules = ( 154 | ); 155 | dependencies = ( 156 | ); 157 | name = Runner; 158 | productName = Runner; 159 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 160 | productType = "com.apple.product-type.application"; 161 | }; 162 | /* End PBXNativeTarget section */ 163 | 164 | /* Begin PBXProject section */ 165 | 97C146E61CF9000F007C117D /* Project object */ = { 166 | isa = PBXProject; 167 | attributes = { 168 | LastUpgradeCheck = 1430; 169 | ORGANIZATIONNAME = "The Chromium Authors"; 170 | TargetAttributes = { 171 | 97C146ED1CF9000F007C117D = { 172 | CreatedOnToolsVersion = 7.3.1; 173 | DevelopmentTeam = LCXV255WTL; 174 | }; 175 | }; 176 | }; 177 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 178 | compatibilityVersion = "Xcode 3.2"; 179 | developmentRegion = English; 180 | hasScannedForEncodings = 0; 181 | knownRegions = ( 182 | en, 183 | Base, 184 | ); 185 | mainGroup = 97C146E51CF9000F007C117D; 186 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 187 | projectDirPath = ""; 188 | projectRoot = ""; 189 | targets = ( 190 | 97C146ED1CF9000F007C117D /* Runner */, 191 | ); 192 | }; 193 | /* End PBXProject section */ 194 | 195 | /* Begin PBXResourcesBuildPhase section */ 196 | 97C146EC1CF9000F007C117D /* Resources */ = { 197 | isa = PBXResourcesBuildPhase; 198 | buildActionMask = 2147483647; 199 | files = ( 200 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 201 | 9740EEB51CF90195004384FC /* Generated.xcconfig in Resources */, 202 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 203 | 9740EEB41CF90195004384FC /* Debug.xcconfig in Resources */, 204 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 205 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 206 | ); 207 | runOnlyForDeploymentPostprocessing = 0; 208 | }; 209 | /* End PBXResourcesBuildPhase section */ 210 | 211 | /* Begin PBXShellScriptBuildPhase section */ 212 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 213 | isa = PBXShellScriptBuildPhase; 214 | alwaysOutOfDate = 1; 215 | buildActionMask = 2147483647; 216 | files = ( 217 | ); 218 | inputPaths = ( 219 | "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", 220 | ); 221 | name = "Thin Binary"; 222 | outputPaths = ( 223 | ); 224 | runOnlyForDeploymentPostprocessing = 0; 225 | shellPath = /bin/sh; 226 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 227 | }; 228 | 6D038B862F639D466FC324F4 /* [CP] Check Pods Manifest.lock */ = { 229 | isa = PBXShellScriptBuildPhase; 230 | buildActionMask = 2147483647; 231 | files = ( 232 | ); 233 | inputPaths = ( 234 | "${PODS_PODFILE_DIR_PATH}/Podfile.lock", 235 | "${PODS_ROOT}/Manifest.lock", 236 | ); 237 | name = "[CP] Check Pods Manifest.lock"; 238 | outputPaths = ( 239 | "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", 240 | ); 241 | runOnlyForDeploymentPostprocessing = 0; 242 | shellPath = /bin/sh; 243 | shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; 244 | showEnvVarsInLog = 0; 245 | }; 246 | 9740EEB61CF901F6004384FC /* Run Script */ = { 247 | isa = PBXShellScriptBuildPhase; 248 | alwaysOutOfDate = 1; 249 | buildActionMask = 2147483647; 250 | files = ( 251 | ); 252 | inputPaths = ( 253 | ); 254 | name = "Run Script"; 255 | outputPaths = ( 256 | ); 257 | runOnlyForDeploymentPostprocessing = 0; 258 | shellPath = /bin/sh; 259 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 260 | }; 261 | /* End PBXShellScriptBuildPhase section */ 262 | 263 | /* Begin PBXSourcesBuildPhase section */ 264 | 97C146EA1CF9000F007C117D /* Sources */ = { 265 | isa = PBXSourcesBuildPhase; 266 | buildActionMask = 2147483647; 267 | files = ( 268 | 978B8F6F1D3862AE00F588F7 /* AppDelegate.m in Sources */, 269 | 97C146F31CF9000F007C117D /* main.m in Sources */, 270 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 271 | ); 272 | runOnlyForDeploymentPostprocessing = 0; 273 | }; 274 | /* End PBXSourcesBuildPhase section */ 275 | 276 | /* Begin PBXVariantGroup section */ 277 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 278 | isa = PBXVariantGroup; 279 | children = ( 280 | 97C146FB1CF9000F007C117D /* Base */, 281 | ); 282 | name = Main.storyboard; 283 | sourceTree = ""; 284 | }; 285 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 286 | isa = PBXVariantGroup; 287 | children = ( 288 | 97C147001CF9000F007C117D /* Base */, 289 | ); 290 | name = LaunchScreen.storyboard; 291 | sourceTree = ""; 292 | }; 293 | /* End PBXVariantGroup section */ 294 | 295 | /* Begin XCBuildConfiguration section */ 296 | 97C147031CF9000F007C117D /* Debug */ = { 297 | isa = XCBuildConfiguration; 298 | buildSettings = { 299 | ALWAYS_SEARCH_USER_PATHS = NO; 300 | CLANG_ANALYZER_NONNULL = YES; 301 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 302 | CLANG_CXX_LIBRARY = "libc++"; 303 | CLANG_ENABLE_MODULES = YES; 304 | CLANG_ENABLE_OBJC_ARC = YES; 305 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 306 | CLANG_WARN_BOOL_CONVERSION = YES; 307 | CLANG_WARN_COMMA = YES; 308 | CLANG_WARN_CONSTANT_CONVERSION = YES; 309 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 310 | CLANG_WARN_EMPTY_BODY = YES; 311 | CLANG_WARN_ENUM_CONVERSION = YES; 312 | CLANG_WARN_INFINITE_RECURSION = YES; 313 | CLANG_WARN_INT_CONVERSION = YES; 314 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 315 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 316 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 317 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 318 | CLANG_WARN_STRICT_PROTOTYPES = YES; 319 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 320 | CLANG_WARN_UNREACHABLE_CODE = YES; 321 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 322 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 323 | COPY_PHASE_STRIP = NO; 324 | DEBUG_INFORMATION_FORMAT = dwarf; 325 | ENABLE_STRICT_OBJC_MSGSEND = YES; 326 | ENABLE_TESTABILITY = YES; 327 | GCC_C_LANGUAGE_STANDARD = gnu99; 328 | GCC_DYNAMIC_NO_PIC = NO; 329 | GCC_NO_COMMON_BLOCKS = YES; 330 | GCC_OPTIMIZATION_LEVEL = 0; 331 | GCC_PREPROCESSOR_DEFINITIONS = ( 332 | "DEBUG=1", 333 | "$(inherited)", 334 | ); 335 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 336 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 337 | GCC_WARN_UNDECLARED_SELECTOR = YES; 338 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 339 | GCC_WARN_UNUSED_FUNCTION = YES; 340 | GCC_WARN_UNUSED_VARIABLE = YES; 341 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 342 | MTL_ENABLE_DEBUG_INFO = YES; 343 | ONLY_ACTIVE_ARCH = YES; 344 | SDKROOT = iphoneos; 345 | TARGETED_DEVICE_FAMILY = "1,2"; 346 | }; 347 | name = Debug; 348 | }; 349 | 97C147041CF9000F007C117D /* Release */ = { 350 | isa = XCBuildConfiguration; 351 | buildSettings = { 352 | ALWAYS_SEARCH_USER_PATHS = NO; 353 | CLANG_ANALYZER_NONNULL = YES; 354 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 355 | CLANG_CXX_LIBRARY = "libc++"; 356 | CLANG_ENABLE_MODULES = YES; 357 | CLANG_ENABLE_OBJC_ARC = YES; 358 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 359 | CLANG_WARN_BOOL_CONVERSION = YES; 360 | CLANG_WARN_COMMA = YES; 361 | CLANG_WARN_CONSTANT_CONVERSION = YES; 362 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 363 | CLANG_WARN_EMPTY_BODY = YES; 364 | CLANG_WARN_ENUM_CONVERSION = YES; 365 | CLANG_WARN_INFINITE_RECURSION = YES; 366 | CLANG_WARN_INT_CONVERSION = YES; 367 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 368 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 369 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 370 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 371 | CLANG_WARN_STRICT_PROTOTYPES = YES; 372 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 373 | CLANG_WARN_UNREACHABLE_CODE = YES; 374 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 375 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 376 | COPY_PHASE_STRIP = NO; 377 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 378 | ENABLE_NS_ASSERTIONS = NO; 379 | ENABLE_STRICT_OBJC_MSGSEND = YES; 380 | GCC_C_LANGUAGE_STANDARD = gnu99; 381 | GCC_NO_COMMON_BLOCKS = YES; 382 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 383 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 384 | GCC_WARN_UNDECLARED_SELECTOR = YES; 385 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 386 | GCC_WARN_UNUSED_FUNCTION = YES; 387 | GCC_WARN_UNUSED_VARIABLE = YES; 388 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 389 | MTL_ENABLE_DEBUG_INFO = NO; 390 | SDKROOT = iphoneos; 391 | TARGETED_DEVICE_FAMILY = "1,2"; 392 | VALIDATE_PRODUCT = YES; 393 | }; 394 | name = Release; 395 | }; 396 | 97C147061CF9000F007C117D /* Debug */ = { 397 | isa = XCBuildConfiguration; 398 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 399 | buildSettings = { 400 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 401 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 402 | DEVELOPMENT_TEAM = LCXV255WTL; 403 | ENABLE_BITCODE = NO; 404 | FRAMEWORK_SEARCH_PATHS = ( 405 | "$(inherited)", 406 | "$(PROJECT_DIR)/Flutter", 407 | ); 408 | INFOPLIST_FILE = Runner/Info.plist; 409 | LD_RUNPATH_SEARCH_PATHS = ( 410 | "$(inherited)", 411 | "@executable_path/Frameworks", 412 | ); 413 | LIBRARY_SEARCH_PATHS = ( 414 | "$(inherited)", 415 | "$(PROJECT_DIR)/Flutter", 416 | ); 417 | PRODUCT_BUNDLE_IDENTIFIER = com.dooboolab.test; 418 | PRODUCT_NAME = "$(TARGET_NAME)"; 419 | VERSIONING_SYSTEM = "apple-generic"; 420 | }; 421 | name = Debug; 422 | }; 423 | 97C147071CF9000F007C117D /* Release */ = { 424 | isa = XCBuildConfiguration; 425 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 426 | buildSettings = { 427 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 428 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 429 | DEVELOPMENT_TEAM = LCXV255WTL; 430 | ENABLE_BITCODE = NO; 431 | FRAMEWORK_SEARCH_PATHS = ( 432 | "$(inherited)", 433 | "$(PROJECT_DIR)/Flutter", 434 | ); 435 | INFOPLIST_FILE = Runner/Info.plist; 436 | LD_RUNPATH_SEARCH_PATHS = ( 437 | "$(inherited)", 438 | "@executable_path/Frameworks", 439 | ); 440 | LIBRARY_SEARCH_PATHS = ( 441 | "$(inherited)", 442 | "$(PROJECT_DIR)/Flutter", 443 | ); 444 | PRODUCT_BUNDLE_IDENTIFIER = com.dooboolab.test; 445 | PRODUCT_NAME = "$(TARGET_NAME)"; 446 | VERSIONING_SYSTEM = "apple-generic"; 447 | }; 448 | name = Release; 449 | }; 450 | /* End XCBuildConfiguration section */ 451 | 452 | /* Begin XCConfigurationList section */ 453 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 454 | isa = XCConfigurationList; 455 | buildConfigurations = ( 456 | 97C147031CF9000F007C117D /* Debug */, 457 | 97C147041CF9000F007C117D /* Release */, 458 | ); 459 | defaultConfigurationIsVisible = 0; 460 | defaultConfigurationName = Release; 461 | }; 462 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 463 | isa = XCConfigurationList; 464 | buildConfigurations = ( 465 | 97C147061CF9000F007C117D /* Debug */, 466 | 97C147071CF9000F007C117D /* Release */, 467 | ); 468 | defaultConfigurationIsVisible = 0; 469 | defaultConfigurationName = Release; 470 | }; 471 | /* End XCConfigurationList section */ 472 | }; 473 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 474 | } 475 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 32 | 33 | 39 | 40 | 41 | 42 | 43 | 44 | 54 | 56 | 62 | 63 | 64 | 65 | 68 | 69 | 70 | 71 | 72 | 73 | 79 | 81 | 87 | 88 | 89 | 90 | 92 | 93 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface AppDelegate : FlutterAppDelegate 5 | 6 | @end 7 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.m: -------------------------------------------------------------------------------- 1 | #include "AppDelegate.h" 2 | #include "GeneratedPluginRegistrant.h" 3 | 4 | @implementation AppDelegate 5 | 6 | - (BOOL)application:(UIApplication *)application 7 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions { 8 | [GeneratedPluginRegistrant registerWithRegistry:self]; 9 | // Override point for customization after application launch. 10 | return [super application:application didFinishLaunchingWithOptions:launchOptions]; 11 | } 12 | 13 | @end 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | flutter_inapp_purchase_example 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | $(FLUTTER_BUILD_NAME) 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIMainStoryboardFile 28 | Main 29 | UISupportedInterfaceOrientations 30 | 31 | UIInterfaceOrientationPortrait 32 | UIInterfaceOrientationLandscapeLeft 33 | UIInterfaceOrientationLandscapeRight 34 | 35 | UISupportedInterfaceOrientations~ipad 36 | 37 | UIInterfaceOrientationPortrait 38 | UIInterfaceOrientationPortraitUpsideDown 39 | UIInterfaceOrientationLandscapeLeft 40 | UIInterfaceOrientationLandscapeRight 41 | 42 | UIViewControllerBasedStatusBarAppearance 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/ios/Runner/main.m: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import "AppDelegate.h" 4 | 5 | int main(int argc, char* argv[]) { 6 | @autoreleasepool { 7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'dart:async'; 3 | import 'dart:io'; 4 | 5 | import 'package:flutter_inapp_purchase/flutter_inapp_purchase.dart'; 6 | 7 | void main() => runApp(new MyApp()); 8 | 9 | class MyApp extends StatelessWidget { 10 | @override 11 | Widget build(BuildContext context) { 12 | return MaterialApp( 13 | title: 'Flutter Demo', 14 | home: Scaffold( 15 | appBar: AppBar( 16 | title: Text('Flutter Inapp Plugin by dooboolab'), 17 | ), 18 | body: InApp()), 19 | ); 20 | } 21 | } 22 | 23 | class InApp extends StatefulWidget { 24 | @override 25 | _InAppState createState() => new _InAppState(); 26 | } 27 | 28 | class _InAppState extends State { 29 | late dynamic _purchaseUpdatedSubscription; 30 | late dynamic _purchaseErrorSubscription; 31 | late dynamic _connectionSubscription; 32 | final List _productLists = Platform.isAndroid 33 | ? [ 34 | 'android.test.purchased', 35 | 'point_1000', 36 | '5000_point', 37 | 'android.test.canceled', 38 | ] 39 | : ['com.cooni.point1000', 'com.cooni.point5000']; 40 | 41 | List _items = []; 42 | List _purchases = []; 43 | 44 | @override 45 | void initState() { 46 | super.initState(); 47 | initPlatformState(); 48 | } 49 | 50 | @override 51 | void dispose() { 52 | if (_connectionSubscription != null) { 53 | _connectionSubscription.cancel(); 54 | _connectionSubscription = null; 55 | } 56 | super.dispose(); 57 | } 58 | 59 | // Platform messages are asynchronous, so we initialize in an async method. 60 | Future initPlatformState() async { 61 | // prepare 62 | var result = await FlutterInappPurchase.instance.initialize(); 63 | print('result: $result'); 64 | 65 | // If the widget was removed from the tree while the asynchronous platform 66 | // message was in flight, we want to discard the reply rather than calling 67 | // setState to update our non-existent appearance. 68 | if (!mounted) return; 69 | 70 | // refresh items for android 71 | try { 72 | String msg = await FlutterInappPurchase.instance.consumeAll(); 73 | print('consumeAllItems: $msg'); 74 | } catch (err) { 75 | print('consumeAllItems error: $err'); 76 | } 77 | 78 | _connectionSubscription = 79 | FlutterInappPurchase.connectionUpdated.listen((connected) { 80 | print('connected: $connected'); 81 | }); 82 | 83 | _purchaseUpdatedSubscription = 84 | FlutterInappPurchase.purchaseUpdated.listen((productItem) { 85 | print('purchase-updated: $productItem'); 86 | }); 87 | 88 | _purchaseErrorSubscription = 89 | FlutterInappPurchase.purchaseError.listen((purchaseError) { 90 | print('purchase-error: $purchaseError'); 91 | }); 92 | } 93 | 94 | void _requestPurchase(IAPItem item) { 95 | FlutterInappPurchase.instance.requestPurchase(item.productId!); 96 | } 97 | 98 | Future _getProduct() async { 99 | List items = 100 | await FlutterInappPurchase.instance.getProducts(_productLists); 101 | for (var item in items) { 102 | print('${item.toString()}'); 103 | this._items.add(item); 104 | } 105 | 106 | setState(() { 107 | this._items = items; 108 | this._purchases = []; 109 | }); 110 | } 111 | 112 | Future _getPurchases() async { 113 | List? items = 114 | await FlutterInappPurchase.instance.getAvailablePurchases(); 115 | for (var item in items!) { 116 | print('${item.toString()}'); 117 | this._purchases.add(item); 118 | } 119 | 120 | setState(() { 121 | this._items = []; 122 | this._purchases = items; 123 | }); 124 | } 125 | 126 | Future _getPurchaseHistory() async { 127 | List? items = 128 | await FlutterInappPurchase.instance.getPurchaseHistory(); 129 | for (var item in items!) { 130 | print('${item.toString()}'); 131 | this._purchases.add(item); 132 | } 133 | 134 | setState(() { 135 | this._items = []; 136 | this._purchases = items; 137 | }); 138 | } 139 | 140 | List _renderInApps() { 141 | List widgets = this 142 | ._items 143 | .map((item) => Container( 144 | margin: EdgeInsets.symmetric(vertical: 10.0), 145 | child: Container( 146 | child: Column( 147 | children: [ 148 | Container( 149 | margin: EdgeInsets.only(bottom: 5.0), 150 | child: Text( 151 | item.toString(), 152 | style: TextStyle( 153 | fontSize: 18.0, 154 | color: Colors.black, 155 | ), 156 | ), 157 | ), 158 | MaterialButton( 159 | color: Colors.orange, 160 | onPressed: () { 161 | print("---------- Buy Item Button Pressed"); 162 | this._requestPurchase(item); 163 | }, 164 | child: Row( 165 | children: [ 166 | Expanded( 167 | child: Container( 168 | height: 48.0, 169 | alignment: Alignment(-1.0, 0.0), 170 | child: Text('Buy Item'), 171 | ), 172 | ), 173 | ], 174 | ), 175 | ), 176 | ], 177 | ), 178 | ), 179 | )) 180 | .toList(); 181 | return widgets; 182 | } 183 | 184 | List _renderPurchases() { 185 | List widgets = this 186 | ._purchases 187 | .map((item) => Container( 188 | margin: EdgeInsets.symmetric(vertical: 10.0), 189 | child: Container( 190 | child: Column( 191 | children: [ 192 | Container( 193 | margin: EdgeInsets.only(bottom: 5.0), 194 | child: Text( 195 | item.toString(), 196 | style: TextStyle( 197 | fontSize: 18.0, 198 | color: Colors.black, 199 | ), 200 | ), 201 | ) 202 | ], 203 | ), 204 | ), 205 | )) 206 | .toList(); 207 | return widgets; 208 | } 209 | 210 | @override 211 | Widget build(BuildContext context) { 212 | double screenWidth = MediaQuery.of(context).size.width - 20; 213 | double buttonWidth = (screenWidth / 3) - 20; 214 | 215 | return Container( 216 | padding: EdgeInsets.all(10.0), 217 | child: ListView( 218 | children: [ 219 | Column( 220 | crossAxisAlignment: CrossAxisAlignment.start, 221 | mainAxisAlignment: MainAxisAlignment.start, 222 | children: [ 223 | Container( 224 | child: Text( 225 | 'Running on: ${Platform.operatingSystem} - ${Platform.operatingSystemVersion}\n', 226 | style: TextStyle(fontSize: 18.0), 227 | ), 228 | ), 229 | Column( 230 | children: [ 231 | Row( 232 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 233 | children: [ 234 | Container( 235 | width: buttonWidth, 236 | height: 60.0, 237 | margin: EdgeInsets.all(7.0), 238 | child: MaterialButton( 239 | color: Colors.amber, 240 | padding: EdgeInsets.all(0.0), 241 | onPressed: () async { 242 | print("---------- Connect Billing Button Pressed"); 243 | await FlutterInappPurchase.instance.initialize(); 244 | }, 245 | child: Container( 246 | padding: EdgeInsets.symmetric(horizontal: 20.0), 247 | alignment: Alignment(0.0, 0.0), 248 | child: Text( 249 | 'Connect Billing', 250 | style: TextStyle( 251 | fontSize: 16.0, 252 | ), 253 | ), 254 | ), 255 | ), 256 | ), 257 | Container( 258 | width: buttonWidth, 259 | height: 60.0, 260 | margin: EdgeInsets.all(7.0), 261 | child: MaterialButton( 262 | color: Colors.amber, 263 | padding: EdgeInsets.all(0.0), 264 | onPressed: () async { 265 | print("---------- End Connection Button Pressed"); 266 | await FlutterInappPurchase.instance.finalize(); 267 | if (_purchaseUpdatedSubscription != null) { 268 | _purchaseUpdatedSubscription.cancel(); 269 | _purchaseUpdatedSubscription = null; 270 | } 271 | if (_purchaseErrorSubscription != null) { 272 | _purchaseErrorSubscription.cancel(); 273 | _purchaseErrorSubscription = null; 274 | } 275 | setState(() { 276 | this._items = []; 277 | this._purchases = []; 278 | }); 279 | }, 280 | child: Container( 281 | padding: EdgeInsets.symmetric(horizontal: 20.0), 282 | alignment: Alignment(0.0, 0.0), 283 | child: Text( 284 | 'End Connection', 285 | style: TextStyle( 286 | fontSize: 16.0, 287 | ), 288 | ), 289 | ), 290 | ), 291 | ), 292 | ], 293 | ), 294 | Row( 295 | mainAxisAlignment: MainAxisAlignment.spaceEvenly, 296 | children: [ 297 | Container( 298 | width: buttonWidth, 299 | height: 60.0, 300 | margin: EdgeInsets.all(7.0), 301 | child: MaterialButton( 302 | color: Colors.green, 303 | padding: EdgeInsets.all(0.0), 304 | onPressed: () { 305 | print("---------- Get Items Button Pressed"); 306 | this._getProduct(); 307 | }, 308 | child: Container( 309 | padding: EdgeInsets.symmetric(horizontal: 20.0), 310 | alignment: Alignment(0.0, 0.0), 311 | child: Text( 312 | 'Get Items', 313 | style: TextStyle( 314 | fontSize: 16.0, 315 | ), 316 | ), 317 | ), 318 | )), 319 | Container( 320 | width: buttonWidth, 321 | height: 60.0, 322 | margin: EdgeInsets.all(7.0), 323 | child: MaterialButton( 324 | color: Colors.green, 325 | padding: EdgeInsets.all(0.0), 326 | onPressed: () { 327 | print( 328 | "---------- Get Purchases Button Pressed"); 329 | this._getPurchases(); 330 | }, 331 | child: Container( 332 | padding: EdgeInsets.symmetric(horizontal: 20.0), 333 | alignment: Alignment(0.0, 0.0), 334 | child: Text( 335 | 'Get Purchases', 336 | style: TextStyle( 337 | fontSize: 16.0, 338 | ), 339 | ), 340 | ), 341 | )), 342 | Container( 343 | width: buttonWidth, 344 | height: 60.0, 345 | margin: EdgeInsets.all(7.0), 346 | child: MaterialButton( 347 | color: Colors.green, 348 | padding: EdgeInsets.all(0.0), 349 | onPressed: () { 350 | print( 351 | "---------- Get Purchase History Button Pressed"); 352 | this._getPurchaseHistory(); 353 | }, 354 | child: Container( 355 | padding: EdgeInsets.symmetric(horizontal: 20.0), 356 | alignment: Alignment(0.0, 0.0), 357 | child: Text( 358 | 'Get Purchase History', 359 | style: TextStyle( 360 | fontSize: 16.0, 361 | ), 362 | ), 363 | ), 364 | )), 365 | ]), 366 | ], 367 | ), 368 | Column( 369 | children: this._renderInApps(), 370 | ), 371 | Column( 372 | children: this._renderPurchases(), 373 | ), 374 | ], 375 | ), 376 | ], 377 | ), 378 | ); 379 | } 380 | } 381 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_inapp_purchase_example 2 | description: Demonstrates how to use the flutter_inapp_purchase plugin. 3 | 4 | # The following defines the version and build number for your application. 5 | # A version number is three numbers separated by dots, like 1.2.43 6 | # followed by an optional build number separated by a +. 7 | # Both the version and the builder number may be overridden in flutter 8 | # build by specifying --build-name and --build-number, respectively. 9 | # Read more about versioning at semver.org. 10 | version: 1.0.0+1 11 | 12 | environment: 13 | sdk: ">=2.12.0 <3.0.0" 14 | flutter: ">=2.0.0" 15 | 16 | dependencies: 17 | flutter: 18 | sdk: flutter 19 | 20 | # The following adds the Cupertino Icons font to your application. 21 | # Use with the CupertinoIcons class for iOS style icons. 22 | cupertino_icons: ^1.0.0 23 | 24 | dev_dependencies: 25 | flutter_test: 26 | sdk: flutter 27 | 28 | flutter_inapp_purchase: 29 | path: ../ 30 | 31 | # For information on the generic Dart part of this file, see the 32 | # following page: https://www.dartlang.org/tools/pub/pubspec 33 | 34 | # The following section is specific to Flutter. 35 | flutter: 36 | 37 | # The following line ensures that the Material Icons font is 38 | # included with your application, so that you can use the icons in 39 | # the material Icons class. 40 | uses-material-design: true 41 | 42 | # To add assets to your application, add an assets section, like this: 43 | # assets: 44 | # - images/a_dot_burr.jpeg 45 | # - images/a_dot_ham.jpeg 46 | 47 | # An image asset can refer to one or more resolution-specific "variants", see 48 | # https://flutter.io/assets-and-images/#resolution-aware. 49 | 50 | # For details regarding adding assets from package dependencies, see 51 | # https://flutter.io/assets-and-images/#from-packages 52 | 53 | # To add custom fonts to your application, add a fonts section here, 54 | # in this "flutter" section. Each entry in this list should have a 55 | # "family" key with the font family name, and a "fonts" key with a 56 | # list giving the asset and other descriptors for the font. For 57 | # example: 58 | # fonts: 59 | # - family: Schyler 60 | # fonts: 61 | # - asset: fonts/Schyler-Regular.ttf 62 | # - asset: fonts/Schyler-Italic.ttf 63 | # style: italic 64 | # - family: Trajan Pro 65 | # fonts: 66 | # - asset: fonts/TrajanPro.ttf 67 | # - asset: fonts/TrajanPro_Bold.ttf 68 | # weight: 700 69 | # 70 | # For details regarding fonts from package dependencies, 71 | # see https://flutter.io/custom-fonts/#from-packages 72 | -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // To perform an interaction with a widget in your test, use the WidgetTester utility that Flutter 3 | // provides. For example, you can send tap and scroll gestures. You can also use WidgetTester to 4 | // find child widgets in the widget tree, read text, and verify that the values of widget properties 5 | // are correct. 6 | 7 | // import 'package:flutter/material.dart'; 8 | // import 'package:flutter_test/flutter_test.dart'; 9 | 10 | // import 'package:flutter_inapp_purchase_example/main.dart'; 11 | 12 | void main() { 13 | // testWidgets('Verify Platform version', (WidgetTester tester) async { 14 | // // Build our app and trigger a frame. 15 | // await tester.pumpWidget(new MyApp()); 16 | 17 | // // Verify that platform version is retrieved. 18 | // expect( 19 | // find.byWidgetPredicate( 20 | // (Widget widget) => 21 | // widget is Text && widget.data.startsWith('Running on:'), 22 | // ), 23 | // findsOneWidget); 24 | // }); 25 | } 26 | -------------------------------------------------------------------------------- /flutter_inapp_purchase_android.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /ios/.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | .vagrant/ 3 | .sconsign.dblite 4 | .svn/ 5 | 6 | .DS_Store 7 | *.swp 8 | profile 9 | 10 | DerivedData/ 11 | build/ 12 | GeneratedPluginRegistrant.h 13 | GeneratedPluginRegistrant.m 14 | 15 | .generated/ 16 | 17 | *.pbxuser 18 | *.mode1v3 19 | *.mode2v3 20 | *.perspectivev3 21 | 22 | !default.pbxuser 23 | !default.mode1v3 24 | !default.mode2v3 25 | !default.perspectivev3 26 | 27 | xcuserdata 28 | 29 | *.moved-aside 30 | 31 | *.pyc 32 | *sync/ 33 | Icon? 34 | .tags* 35 | 36 | /Flutter/Generated.xcconfig 37 | -------------------------------------------------------------------------------- /ios/Assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyochan/flutter_inapp_purchase/e93bc132ae5a28a2f20f98786384a04f88bb588e/ios/Assets/.gitkeep -------------------------------------------------------------------------------- /ios/Classes/FlutterInappPurchasePlugin.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface FlutterInappPurchasePlugin : NSObject{ 5 | SKProductsRequest *productsRequest; 6 | NSMutableArray *validProducts; 7 | } 8 | @end 9 | -------------------------------------------------------------------------------- /ios/Classes/IAPPromotionObserver.h: -------------------------------------------------------------------------------- 1 | #import 2 | 3 | @protocol IAPPromotionObserverDelegate; 4 | 5 | @interface IAPPromotionObserver: NSObject 6 | 7 | @property (strong, nonatomic, readonly) SKPayment *payment; 8 | @property (strong, nonatomic, readonly) SKProduct *product; 9 | @property (weak, nonatomic) id delegate; 10 | 11 | + (instancetype)sharedObserver; 12 | + (void)startObserving; 13 | 14 | @end 15 | 16 | @protocol IAPPromotionObserverDelegate 17 | 18 | @required 19 | - (BOOL)shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product; 20 | 21 | @end 22 | -------------------------------------------------------------------------------- /ios/Classes/IAPPromotionObserver.m: -------------------------------------------------------------------------------- 1 | #import "IAPPromotionObserver.h" 2 | 3 | @interface IAPPromotionObserver () { 4 | SKPayment *_promotedPayment; 5 | SKProduct *_promotedProduct; 6 | } 7 | 8 | @end 9 | 10 | @implementation IAPPromotionObserver 11 | 12 | + (instancetype)sharedObserver { 13 | static IAPPromotionObserver *sharedInstance = nil; 14 | static dispatch_once_t onceToken; 15 | 16 | dispatch_once(&onceToken, ^{ 17 | sharedInstance = [[IAPPromotionObserver alloc] init]; 18 | }); 19 | return sharedInstance; 20 | } 21 | 22 | + (void)startObserving { 23 | [IAPPromotionObserver sharedObserver]; 24 | } 25 | 26 | - (SKPayment *)payment { 27 | return _promotedPayment; 28 | } 29 | 30 | - (SKProduct *)product { 31 | return _promotedProduct; 32 | } 33 | 34 | - (void)paymentQueue:(SKPaymentQueue *)queue updatedTransactions:(NSArray *)transactions { 35 | } 36 | 37 | - (BOOL)paymentQueue:(SKPaymentQueue *)queue shouldAddStorePayment:(SKPayment *)payment forProduct:(SKProduct *)product { 38 | _promotedProduct = product; 39 | _promotedPayment = payment; 40 | 41 | if (self.delegate != nil && [self.delegate respondsToSelector:@selector(shouldAddStorePayment:forProduct:)]) { 42 | return [self.delegate shouldAddStorePayment:payment forProduct:product]; 43 | } 44 | return NO; 45 | } 46 | 47 | @end 48 | -------------------------------------------------------------------------------- /ios/flutter_inapp_purchase.podspec: -------------------------------------------------------------------------------- 1 | # 2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html 3 | # 4 | Pod::Spec.new do |s| 5 | s.name = 'flutter_inapp_purchase' 6 | s.version = '0.0.1' 7 | s.summary = 'In App Purchase plugin for flutter. This project has been forked by react-native-iap and we are willing to share same experience with that on react-native.' 8 | s.description = <<-DESC 9 | In App Purchase plugin for flutter. This project has been forked by react-native-iap and we are willing to share same experience with that on react-native. 10 | DESC 11 | s.homepage = 'http://example.com' 12 | s.license = { :file => '../LICENSE' } 13 | s.author = { 'Your Company' => 'email@example.com' } 14 | s.source = { :path => '.' } 15 | s.source_files = 'Classes/**/*' 16 | s.public_header_files = 'Classes/**/*.h' 17 | s.dependency 'Flutter' 18 | 19 | s.ios.deployment_target = '8.0' 20 | end 21 | 22 | -------------------------------------------------------------------------------- /issue_template.md: -------------------------------------------------------------------------------- 1 | ### Version of flutter_inapp_purchase 2 | 3 | ### Platforms you faced the error (IOS or Android or both?) 4 | 5 | ### Expected behavior 6 | 7 | ### Actual behavior 8 | 9 | ### Tested environment (Emulator? Real Device?) 10 | 11 | ### Steps to reproduce the behavior 12 | -------------------------------------------------------------------------------- /lib/Store.dart: -------------------------------------------------------------------------------- 1 | enum Store { none, playStore, amazon, appStore } 2 | -------------------------------------------------------------------------------- /lib/modules.dart: -------------------------------------------------------------------------------- 1 | enum ResponseCodeAndroid { 2 | BILLING_RESPONSE_RESULT_OK, 3 | BILLING_RESPONSE_RESULT_USER_CANCELED, 4 | BILLING_RESPONSE_RESULT_SERVICE_UNAVAILABLE, 5 | BILLING_RESPONSE_RESULT_BILLING_UNAVAILABLE, 6 | BILLING_RESPONSE_RESULT_ITEM_UNAVAILABLE, 7 | BILLING_RESPONSE_RESULT_DEVELOPER_ERROR, 8 | BILLING_RESPONSE_RESULT_ERROR, 9 | BILLING_RESPONSE_RESULT_ITEM_ALREADY_OWNED, 10 | BILLING_RESPONSE_RESULT_ITEM_NOT_OWNED, 11 | UNKNOWN, 12 | } 13 | 14 | /// An item available for purchase from either the `Google Play Store` or `iOS AppStore` 15 | class IAPItem { 16 | final String? productId; 17 | final String? price; 18 | final String? currency; 19 | final String? localizedPrice; 20 | final String? title; 21 | final String? description; 22 | final String? introductoryPrice; 23 | 24 | /// ios only 25 | final String? subscriptionPeriodNumberIOS; 26 | final String? subscriptionPeriodUnitIOS; 27 | final String? introductoryPriceNumberIOS; 28 | final String? introductoryPricePaymentModeIOS; 29 | final String? introductoryPriceNumberOfPeriodsIOS; 30 | final String? introductoryPriceSubscriptionPeriodIOS; 31 | final List? discountsIOS; 32 | 33 | /// android only 34 | final String? signatureAndroid; 35 | final List? subscriptionOffersAndroid; 36 | final String? subscriptionPeriodAndroid; 37 | 38 | final String? iconUrl; 39 | final String? originalJson; 40 | final String originalPrice; 41 | 42 | /// Create [IAPItem] from a Map that was previously JSON formatted 43 | IAPItem.fromJSON(Map json) 44 | : productId = json['productId'] as String?, 45 | price = json['price'] as String?, 46 | currency = json['currency'] as String?, 47 | localizedPrice = json['localizedPrice'] as String?, 48 | title = json['title'] as String?, 49 | description = json['description'] as String?, 50 | introductoryPrice = json['introductoryPrice'] as String?, 51 | introductoryPricePaymentModeIOS = 52 | json['introductoryPricePaymentModeIOS'] as String?, 53 | introductoryPriceNumberOfPeriodsIOS = 54 | json['introductoryPriceNumberOfPeriodsIOS'] as String?, 55 | introductoryPriceSubscriptionPeriodIOS = 56 | json['introductoryPriceSubscriptionPeriodIOS'] as String?, 57 | introductoryPriceNumberIOS = 58 | json['introductoryPriceNumberIOS'] as String?, 59 | subscriptionPeriodNumberIOS = 60 | json['subscriptionPeriodNumberIOS'] as String?, 61 | subscriptionPeriodUnitIOS = 62 | json['subscriptionPeriodUnitIOS'] as String?, 63 | subscriptionPeriodAndroid = 64 | json['subscriptionPeriodAndroid'] as String?, 65 | signatureAndroid = json['signatureAndroid'] as String?, 66 | iconUrl = json['iconUrl'] as String?, 67 | originalJson = json['originalJson'] as String?, 68 | originalPrice = json['originalPrice'].toString(), 69 | discountsIOS = _extractDiscountIOS(json['discounts']), 70 | subscriptionOffersAndroid = 71 | _extractSubscriptionOffersAndroid(json['subscriptionOffers']); 72 | 73 | /// wow, i find if i want to save a IAPItem, there is not "toJson" to cast it into String... 74 | /// i'm sorry to see that... so, 75 | /// 76 | /// you can cast a IAPItem to json(Map) via invoke this method. 77 | /// for example: 78 | /// String str = convert.jsonEncode(item) 79 | /// 80 | /// and then get IAPItem from "str" above 81 | /// IAPItem item = IAPItem.fromJSON(convert.jsonDecode(str)); 82 | Map toJson() { 83 | final Map data = Map(); 84 | data['productId'] = this.productId; 85 | data['price'] = this.price; 86 | data['currency'] = this.currency; 87 | data['localizedPrice'] = this.localizedPrice; 88 | data['title'] = this.title; 89 | data['description'] = this.description; 90 | data['introductoryPrice'] = this.introductoryPrice; 91 | 92 | data['subscriptionPeriodNumberIOS'] = this.subscriptionPeriodNumberIOS; 93 | data['subscriptionPeriodUnitIOS'] = this.subscriptionPeriodUnitIOS; 94 | data['introductoryPricePaymentModeIOS'] = 95 | this.introductoryPricePaymentModeIOS; 96 | data['introductoryPriceNumberOfPeriodsIOS'] = 97 | this.introductoryPriceNumberOfPeriodsIOS; 98 | data['introductoryPriceSubscriptionPeriodIOS'] = 99 | this.introductoryPriceSubscriptionPeriodIOS; 100 | data['subscriptionPeriodAndroid'] = this.subscriptionPeriodAndroid; 101 | data['signatureAndroid'] = this.signatureAndroid; 102 | 103 | data['iconUrl'] = this.iconUrl; 104 | data['originalJson'] = this.originalJson; 105 | data['originalPrice'] = this.originalPrice; 106 | data['discounts'] = this.discountsIOS; 107 | return data; 108 | } 109 | 110 | /// Return the contents of this class as a string 111 | @override 112 | String toString() { 113 | return 'productId: $productId, ' 114 | 'price: $price, ' 115 | 'currency: $currency, ' 116 | 'localizedPrice: $localizedPrice, ' 117 | 'title: $title, ' 118 | 'description: $description, ' 119 | 'introductoryPrice: $introductoryPrice, ' 120 | 'subscriptionPeriodNumberIOS: $subscriptionPeriodNumberIOS, ' 121 | 'subscriptionPeriodUnitIOS: $subscriptionPeriodUnitIOS, ' 122 | 'introductoryPricePaymentModeIOS: $introductoryPricePaymentModeIOS, ' 123 | 'introductoryPriceNumberOfPeriodsIOS: $introductoryPriceNumberOfPeriodsIOS, ' 124 | 'introductoryPriceSubscriptionPeriodIOS: $introductoryPriceSubscriptionPeriodIOS, ' 125 | 'subscriptionPeriodAndroid: $subscriptionPeriodAndroid, ' 126 | 'iconUrl: $iconUrl, ' 127 | 'originalJson: $originalJson, ' 128 | 'originalPrice: $originalPrice, ' 129 | 'discounts: $discountsIOS, '; 130 | } 131 | 132 | static List? _extractDiscountIOS(dynamic json) { 133 | List? list = json as List?; 134 | List? discounts; 135 | 136 | if (list != null) { 137 | discounts = list 138 | .map( 139 | (dynamic discount) => 140 | DiscountIOS.fromJSON(discount as Map), 141 | ) 142 | .toList(); 143 | } 144 | 145 | return discounts; 146 | } 147 | 148 | static List? _extractSubscriptionOffersAndroid( 149 | dynamic json) { 150 | List? list = json as List?; 151 | List? offers; 152 | 153 | if (list != null) { 154 | offers = list 155 | .map( 156 | (dynamic offer) => SubscriptionOfferAndroid.fromJSON( 157 | offer as Map), 158 | ) 159 | .toList(); 160 | } 161 | 162 | return offers; 163 | } 164 | } 165 | 166 | class SubscriptionOfferAndroid { 167 | String? offerId; 168 | String? basePlanId; 169 | String? offerToken; 170 | List? pricingPhases; 171 | 172 | SubscriptionOfferAndroid.fromJSON(Map json) 173 | : offerId = json["offerId"] as String?, 174 | basePlanId = json["basePlanId"] as String?, 175 | offerToken = json["offerToken"] as String?, 176 | pricingPhases = _extractAndroidPricingPhase(json["pricingPhases"]); 177 | 178 | static List? _extractAndroidPricingPhase(dynamic json) { 179 | List? list = json as List?; 180 | List? phases; 181 | 182 | if (list != null) { 183 | phases = list 184 | .map( 185 | (dynamic phase) => 186 | PricingPhaseAndroid.fromJSON(phase as Map), 187 | ) 188 | .toList(); 189 | } 190 | 191 | return phases; 192 | } 193 | } 194 | 195 | class PricingPhaseAndroid { 196 | String? price; 197 | String? formattedPrice; 198 | String? billingPeriod; 199 | String? currencyCode; 200 | int? recurrenceMode; 201 | int? billingCycleCount; 202 | 203 | PricingPhaseAndroid.fromJSON(Map json) 204 | : price = json["price"] as String?, 205 | formattedPrice = json["formattedPrice"] as String?, 206 | billingPeriod = json["billingPeriod"] as String?, 207 | currencyCode = json["currencyCode"] as String?, 208 | recurrenceMode = json["recurrenceMode"] as int?, 209 | billingCycleCount = json["billingCycleCount"] as int?; 210 | } 211 | 212 | class DiscountIOS { 213 | String? identifier; 214 | String? type; 215 | String? numberOfPeriods; 216 | double? price; 217 | String? localizedPrice; 218 | String? paymentMode; 219 | String? subscriptionPeriod; 220 | 221 | /// Create [DiscountIOS] from a Map that was previously JSON formatted 222 | DiscountIOS.fromJSON(Map json) 223 | : identifier = json['identifier'] as String?, 224 | type = json['type'] as String?, 225 | numberOfPeriods = json['numberOfPeriods'] as String?, 226 | price = json['price'] as double?, 227 | localizedPrice = json['localizedPrice'] as String?, 228 | paymentMode = json['paymentMode'] as String?, 229 | subscriptionPeriod = json['subscriptionPeriod'] as String?; 230 | 231 | Map toJson() { 232 | final Map data = Map(); 233 | data['identifier'] = this.identifier; 234 | data['type'] = this.type; 235 | data['numberOfPeriods'] = this.numberOfPeriods; 236 | data['price'] = this.price; 237 | data['localizedPrice'] = this.localizedPrice; 238 | data['paymentMode'] = this.paymentMode; 239 | data['subscriptionPeriod'] = this.subscriptionPeriod; 240 | return data; 241 | } 242 | 243 | /// Return the contents of this class as a string 244 | @override 245 | String toString() { 246 | return 'identifier: $identifier, ' 247 | 'type: $type, ' 248 | 'numberOfPeriods: $numberOfPeriods, ' 249 | 'price: $price, ' 250 | 'localizedPrice: $localizedPrice, ' 251 | 'paymentMode: $paymentMode, ' 252 | 'subscriptionPeriod: $subscriptionPeriod, '; 253 | } 254 | } 255 | 256 | /// An item which was purchased from either the `Google Play Store` or `iOS AppStore` 257 | class PurchasedItem { 258 | final String? productId; 259 | final String? transactionId; 260 | final DateTime? transactionDate; 261 | final String? transactionReceipt; 262 | final String? purchaseToken; 263 | 264 | // Android only 265 | final String? dataAndroid; 266 | final String? signatureAndroid; 267 | final bool? autoRenewingAndroid; 268 | final bool? isAcknowledgedAndroid; 269 | final PurchaseState? purchaseStateAndroid; 270 | 271 | // iOS only 272 | final DateTime? originalTransactionDateIOS; 273 | final String? originalTransactionIdentifierIOS; 274 | final TransactionState? transactionStateIOS; 275 | 276 | /// Create [PurchasedItem] from a Map that was previously JSON formatted 277 | PurchasedItem.fromJSON(Map json) 278 | : productId = json['productId'] as String?, 279 | transactionId = json['transactionId'] as String?, 280 | transactionDate = _extractDate(json['transactionDate']), 281 | transactionReceipt = json['transactionReceipt'] as String?, 282 | purchaseToken = json['purchaseToken'] as String?, 283 | dataAndroid = json['dataAndroid'] as String?, 284 | signatureAndroid = json['signatureAndroid'] as String?, 285 | isAcknowledgedAndroid = json['isAcknowledgedAndroid'] as bool?, 286 | autoRenewingAndroid = json['autoRenewingAndroid'] as bool?, 287 | purchaseStateAndroid = 288 | _decodePurchaseStateAndroid(json['purchaseStateAndroid'] as int?), 289 | originalTransactionDateIOS = 290 | _extractDate(json['originalTransactionDateIOS']), 291 | originalTransactionIdentifierIOS = 292 | json['originalTransactionIdentifierIOS'] as String?, 293 | transactionStateIOS = 294 | _decodeTransactionStateIOS(json['transactionStateIOS'] as int?); 295 | 296 | /// This returns transaction dates in ISO 8601 format. 297 | @override 298 | String toString() { 299 | return 'productId: $productId, ' 300 | 'transactionId: $transactionId, ' 301 | 'transactionDate: ${transactionDate?.toIso8601String()}, ' 302 | 'transactionReceipt: $transactionReceipt, ' 303 | 'purchaseToken: $purchaseToken, ' 304 | 305 | /// android specific 306 | 'dataAndroid: $dataAndroid, ' 307 | 'signatureAndroid: $signatureAndroid, ' 308 | 'isAcknowledgedAndroid: $isAcknowledgedAndroid, ' 309 | 'autoRenewingAndroid: $autoRenewingAndroid, ' 310 | 'purchaseStateAndroid: $purchaseStateAndroid, ' 311 | 312 | /// ios specific 313 | 'originalTransactionDateIOS: ${originalTransactionDateIOS?.toIso8601String()}, ' 314 | 'originalTransactionIdentifierIOS: $originalTransactionIdentifierIOS, ' 315 | 'transactionStateIOS: $transactionStateIOS'; 316 | } 317 | 318 | /// Coerce miliseconds since epoch in double, int, or String into DateTime format 319 | static DateTime? _extractDate(dynamic timestamp) { 320 | if (timestamp == null) return null; 321 | 322 | int _toInt() => double.parse(timestamp.toString()).toInt(); 323 | return DateTime.fromMillisecondsSinceEpoch(_toInt()); 324 | } 325 | } 326 | 327 | class PurchaseResult { 328 | final int? responseCode; 329 | final String? debugMessage; 330 | final String? code; 331 | final String? message; 332 | 333 | PurchaseResult({ 334 | this.responseCode, 335 | this.debugMessage, 336 | this.code, 337 | this.message, 338 | }); 339 | 340 | PurchaseResult.fromJSON(Map json) 341 | : responseCode = json['responseCode'] as int?, 342 | debugMessage = json['debugMessage'] as String?, 343 | code = json['code'] as String?, 344 | message = json['message'] as String?; 345 | 346 | Map toJson() => { 347 | "responseCode": responseCode ?? 0, 348 | "debugMessage": debugMessage ?? '', 349 | "code": code ?? '', 350 | "message": message ?? '', 351 | }; 352 | 353 | @override 354 | String toString() { 355 | return 'responseCode: $responseCode, ' 356 | 'debugMessage: $debugMessage, ' 357 | 'code: $code, ' 358 | 'message: $message'; 359 | } 360 | } 361 | 362 | class ConnectionResult { 363 | final bool? connected; 364 | 365 | ConnectionResult({ 366 | this.connected, 367 | }); 368 | 369 | ConnectionResult.fromJSON(Map json) 370 | : connected = json['connected'] as bool?; 371 | 372 | Map toJson() => { 373 | "connected": connected ?? false, 374 | }; 375 | 376 | @override 377 | String toString() { 378 | return 'connected: $connected'; 379 | } 380 | } 381 | 382 | /// See also https://developer.apple.com/documentation/storekit/skpaymenttransactionstate 383 | enum TransactionState { 384 | /// A transaction that is being processed by the App Store. 385 | purchasing, 386 | 387 | /// A successfully processed transaction. 388 | purchased, 389 | 390 | /// A failed transaction. 391 | failed, 392 | 393 | /// A transaction that restores content previously purchased by the user. 394 | restored, 395 | 396 | /// A transaction that is in the queue, but its final status is pending external action such as Ask to Buy. 397 | deferred, 398 | } 399 | 400 | TransactionState? _decodeTransactionStateIOS(int? rawValue) { 401 | switch (rawValue) { 402 | case 0: 403 | return TransactionState.purchasing; 404 | case 1: 405 | return TransactionState.purchased; 406 | case 2: 407 | return TransactionState.failed; 408 | case 3: 409 | return TransactionState.restored; 410 | case 4: 411 | return TransactionState.deferred; 412 | default: 413 | return null; 414 | } 415 | } 416 | 417 | /// See also https://developer.android.com/reference/com/android/billingclient/api/Purchase.PurchaseState 418 | enum PurchaseState { 419 | pending, 420 | 421 | purchased, 422 | 423 | unspecified, 424 | } 425 | 426 | PurchaseState? _decodePurchaseStateAndroid(int? rawValue) { 427 | switch (rawValue) { 428 | case 0: 429 | return PurchaseState.unspecified; 430 | case 1: 431 | return PurchaseState.purchased; 432 | case 2: 433 | return PurchaseState.pending; 434 | default: 435 | return null; 436 | } 437 | } 438 | -------------------------------------------------------------------------------- /lib/utils.dart: -------------------------------------------------------------------------------- 1 | import 'modules.dart'; 2 | import 'dart:convert'; 3 | 4 | List extractItems(dynamic result) { 5 | List list = json.decode(result.toString()); 6 | List products = list 7 | .map( 8 | (dynamic product) => IAPItem.fromJSON(product as Map), 9 | ) 10 | .toList(); 11 | 12 | return products; 13 | } 14 | 15 | List? extractPurchased(dynamic result) { 16 | List? decoded = json 17 | .decode(result.toString()) 18 | .map( 19 | (dynamic product) => 20 | PurchasedItem.fromJSON(product as Map), 21 | ) 22 | .toList(); 23 | 24 | return decoded; 25 | } 26 | 27 | List? extractResult(dynamic result) { 28 | List? decoded = json 29 | .decode(result.toString()) 30 | .map( 31 | (dynamic product) => 32 | PurchaseResult.fromJSON(product as Map), 33 | ) 34 | .toList(); 35 | 36 | return decoded; 37 | } 38 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: flutter_inapp_purchase 2 | description: In App Purchase plugin for flutter. This project has been forked by 3 | react-native-iap and we are willing to share same experience with that on 4 | react-native. 5 | version: 5.6.2 6 | homepage: https://github.com/dooboolab/flutter_inapp_purchase/blob/main/pubspec.yaml 7 | environment: 8 | sdk: ">=2.15.0 <4.0.0" 9 | flutter: ">=2.0.0" 10 | 11 | dependencies: 12 | flutter: 13 | sdk: flutter 14 | http: ^1.1.0 15 | meta: ^1.9.1 16 | platform: ^3.1.3 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | mockito: ^5.4.2 22 | test: ^1.24.3 23 | 24 | # For information on the generic Dart part of this file, see the 25 | # following page: https://www.dartlang.org/tools/pub/pubspec 26 | # The following section is specific to Flutter. 27 | flutter: 28 | plugin: 29 | platforms: 30 | android: 31 | package: com.dooboolab.flutterinapppurchase 32 | pluginClass: FlutterInappPurchasePlugin 33 | mainClass: FlutterInappPurchasePlugin 34 | ios: 35 | pluginClass: FlutterInappPurchasePlugin 36 | -------------------------------------------------------------------------------- /test/utils_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | 3 | enum _TestEnum { Hoge } 4 | 5 | void main() { 6 | group('utils', () { 7 | test('EnumUtil.getValueString', () async { 8 | String value = _TestEnum.Hoge.name; 9 | expect(value, "Hoge"); 10 | }); 11 | }); 12 | } 13 | --------------------------------------------------------------------------------