diff --git a/.gitignore b/.gitignore index 73ee12395d8a..e89cf1187f48 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ ServiceDefinitions.json xcuserdata/ local.properties +keystore.properties .gradle/ gradlew gradlew.bat @@ -31,3 +32,4 @@ GeneratedPluginRegistrant.m GeneratedPluginRegistrant.java build/ .flutter-plugins + diff --git a/packages/in_app_purchase/README.md b/packages/in_app_purchase/README.md index 826c24146c16..30ca2bdead57 100644 --- a/packages/in_app_purchase/README.md +++ b/packages/in_app_purchase/README.md @@ -1,5 +1,32 @@ # In App Purchase +A Flutter plugin for in-app purchases. + +## Getting Started + This plugin is not ready to be used yet. Follow [flutter/flutter#9591](https://github.com/flutter/flutter/issues/9591) for more -updates. \ No newline at end of file +updates. + +There's a significant amount of setup required for testing in app purchases +successfully, including registering new app IDs and store entries to use for +testing in both the Play Developer Console and App Store Connect. Both Google +Play and the App Store require developers to configure an app with in-app items +for purchase to call their in-app-purchase APIs. You can check out the [example +app](example/README.md) for an example on configuring both. + +## Design + +The API surface is stacked into 2 main layers. + +1. [in_app_purchase.dart](lib/in_app_purchase.dart), the generic idiommatic + Flutter API. This exposes the most basic IAP-related functionality. The goal + is that Flutter apps should be able to use this API surface on its own for + the vast majority of cases. + +2. The dart wrappers around the platform specific IAP APIs and their platform + specific implementations of the generic interface. See + [google_play.dart](lib/google_play.dart) and + [app_store.dart](lib/app_store.dart). These API surfaces should expose all + the platform-specific behavior and allow for more fine-tuned control when + needed. diff --git a/packages/in_app_purchase/android/build.gradle b/packages/in_app_purchase/android/build.gradle index 4252427e4485..1c5afd35a3fb 100644 --- a/packages/in_app_purchase/android/build.gradle +++ b/packages/in_app_purchase/android/build.gradle @@ -32,3 +32,7 @@ android { disable 'InvalidPackage' } } + +dependencies { + implementation 'com.android.billingclient:billing:1.2' +} diff --git a/packages/in_app_purchase/example/README.md b/packages/in_app_purchase/example/README.md index 72cfe4f2d990..4c21b987b627 100644 --- a/packages/in_app_purchase/example/README.md +++ b/packages/in_app_purchase/example/README.md @@ -1,8 +1,85 @@ -# in_app_purchase_example +# In App Purchase Example -Demonstrates how to use the in_app_purchase plugin. +Demonstrates how to use the In App Purchase (IAP) Plugin. ## Getting Started -For help getting started with Flutter, view our online -[documentation](https://flutter.io/). +WARNING: This plugin and example are not ready to be used. Right now the example +app doesn't actually demonstrate any IAP functionality yet. Follow +[flutter/flutter#9591](https://github.com/flutter/flutter/issues/9591) for more +updates. + +There's a significant amount of setup required for testing in app purchases +successfully, including registering new app IDs and store entries to use for +testing in both the Play Developer Console and App Store Connect. Both Google +Play and the App Store require developers to configure an app with in-app items +for purchase to call their in-app-purchase APIs. + +### Android + +1. Create a new app in the [Play Developer + Console](https://play.google.com/apps/publish/) (PDC). + +2. Sign up for a merchant's account in the PDC. + +3. Create IAPs in the PDC available for purchase in the app. The example assumes + the following SKU IDs exist: + + - `consumable`: A managed product. + - `upgrade`: A managed product. + - `subscription`: A subscription. + + Make sure that all of the products are set to `ACTIVE`. + +4. Update `APP_ID` in `example/android/app/build.gradle` to match your package + ID in the PDC. + +5. Create an `example/android/keystore.properties` file with all your signing + information. `keystore.example.properties` exists as an example to follow. + It's impossible to use any of the `BillingClient` APIs from an unsigned APK. + See + [here](https://developer.android.com/studio/publish/app-signing#secure-shared-keystore) + and [here](https://developer.android.com/studio/publish/app-signing#sign-apk) + for more information. + +6. Build a signed apk. `flutter build apk` will work for this, the gradle files + in this project have been configured to sign even debug builds. + +7. Upload the signed APK from step 6 to the PDC, and publish that to the alpha + test channel. Add your test account as an approved tester. The + `BillingClient` APIs won't work unless the app has been fully published to + the alpha channel and is being used by an authorized test account. See + [here](https://support.google.com/googleplay/android-developer/answer/3131213) + for more info. + +8. Sign in to the test device with the test account from step #7. Then use + `flutter run` to install the app to the device and test like normal. + +### iOS + +1. Follow ["Workflow for configuring in-app + purchases"](https://help.apple.com/app-store-connect/#/devb57be10e7), a + detailed guide on all the steps needed to enable IAPs for an app. Complete + steps 1 ("Sign a Paid Applications Agreement") and 2 ("Configure in-app + purchases"). + + For step #2, "Configure in-app purchases in App Store Connect," you'll want + to create the following products: + + - A consumable with product ID `consumable` + - An upgrade with product ID `upgrade` + - An auto-renewing subscription with product ID `subscription` + +2. In XCode, `File > Open File` `example/ios/Runner.xcworkspace`. Update the + Bundle ID to match the Bundle ID of the app created in step #1. + +3. [Create a Sandbox tester + account](https://help.apple.com/app-store-connect/#/dev8b997bee1) to test the + in-app purchases with. + +4. Use `flutter run` to install the app and test it. Note that you need to test + it on a real device instead of a simulator, and signing into any production + service (including iTunes!) with the test account will permanently invalidate + it. Sign in to the test account in the example app following the steps in the + [*In-App Purchase Programming + Guide*](https://developer.apple.com/library/archive/documentation/NetworkingInternet/Conceptual/StoreKitGuide/Chapters/ShowUI.html#//apple_ref/doc/uid/TP40008267-CH3-SW11). \ No newline at end of file diff --git a/packages/in_app_purchase/example/android/app/build.gradle b/packages/in_app_purchase/example/android/app/build.gradle index 11eb3dd9798e..7e1672b876fc 100644 --- a/packages/in_app_purchase/example/android/app/build.gradle +++ b/packages/in_app_purchase/example/android/app/build.gradle @@ -6,6 +6,43 @@ if (localPropertiesFile.exists()) { } } +// Load the build signing secrets from a local `keystore.properties` file. +// TODO(YOU): Create release keys and a `keystore.properties` file. See +// `example/README.md` for more info and `keystore.example.properties` for an +// example. +def keystorePropertiesFile = rootProject.file("keystore.properties") +def keystoreProperties = new Properties() +def configured = true +try { + keystoreProperties.load(new FileInputStream(keystorePropertiesFile)) +} catch (IOException e) { + configured = false + logger.error('Release signing information not found.') +} + +project.ext { + // TODO(YOU): Set this to match your package ID in the Play Developer Console (see example/README.md). + APP_ID = "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE" + KEYSTORE_STORE_FILE = configured ? rootProject.file(keystoreProperties['storeFile']) : null + KEYSTORE_STORE_PASSWORD = keystoreProperties['storePassword'] + KEYSTORE_KEY_ALIAS = keystoreProperties['keyAlias'] + KEYSTORE_KEY_PASSWORD = keystoreProperties['keyPassword'] + VERSION_CODE = 1 + VERSION_NAME = "0.1" +} + +if (project.APP_ID == "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE") { + configured = false + logger.error('Unique package name not set, defaulting to "io.flutter.plugins.inapppurchaseexample.DEFAULT_DO_NOT_USE".') +} + +// Log a final error message if we're unable to create a release key signed +// build for an app configured in the Play Developer Console. Apks built in this +// condition won't be able to call any of the BillingClient APIs. +if (!configured) { + logger.error('The app could not be configured for release signing. In app purchases will not be testable. See `example/README.md` for more info and instructions.') +} + def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") @@ -15,6 +52,15 @@ apply plugin: 'com.android.application' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { + signingConfigs { + release { + storeFile project.KEYSTORE_STORE_FILE + storePassword project.KEYSTORE_STORE_PASSWORD + keyAlias project.KEYSTORE_KEY_ALIAS + keyPassword project.KEYSTORE_KEY_PASSWORD + } + } + compileSdkVersion 27 lintOptions { @@ -22,20 +68,29 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "io.flutter.plugins.inapppurchaseexample" + applicationId project.APP_ID minSdkVersion 16 targetSdkVersion 27 - versionCode 1 - versionName "1.0" + versionCode Integer.valueOf(VERSION_CODE) + versionName VERSION_NAME testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" } buildTypes { + // Google Play Billing APIs only work with apps signed for production. + debug { + if (configured) { + signingConfig signingConfigs.release + } else { + signingConfig signingConfigs.debug + } + } release { - // TODO: Add your own signing config for the release build. - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug + if (configured) { + signingConfig signingConfigs.release + } else { + signingConfig signingConfigs.debug + } } } } diff --git a/packages/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj b/packages/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj index 868b8889c7d8..0feca7ca869d 100644 --- a/packages/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj +++ b/packages/in_app_purchase/example/ios/Runner.xcodeproj/project.pbxproj @@ -22,6 +22,7 @@ 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; + A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = A5279297219369C600FF69E6 /* StoreKit.framework */; }; /* End PBXBuildFile section */ /* Begin PBXCopyFilesBuildPhase section */ @@ -57,6 +58,7 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + A5279297219369C600FF69E6 /* StoreKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = StoreKit.framework; path = System/Library/Frameworks/StoreKit.framework; sourceTree = SDKROOT; }; B2AB6BE1D4E2232AB5D4A002 /* libPods-Runner.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = "libPods-Runner.a"; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ @@ -68,6 +70,7 @@ 9705A1C61CF904A100538489 /* Flutter.framework in Frameworks */, 3B80C3941E831B6300D905FE /* App.framework in Frameworks */, 861D0D93B0757D95C8A69620 /* libPods-Runner.a in Frameworks */, + A5279298219369C600FF69E6 /* StoreKit.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -141,6 +144,7 @@ E4DB99639FAD8ADED6B572FC /* Frameworks */ = { isa = PBXGroup; children = ( + A5279297219369C600FF69E6 /* StoreKit.framework */, B2AB6BE1D4E2232AB5D4A002 /* libPods-Runner.a */, ); name = Frameworks; @@ -182,6 +186,11 @@ TargetAttributes = { 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; + SystemCapabilities = { + com.apple.InAppPurchase = { + enabled = 1; + }; + }; }; }; }; diff --git a/packages/in_app_purchase/example/keystore.example.properties b/packages/in_app_purchase/example/keystore.example.properties new file mode 100644 index 000000000000..07be54f3118e --- /dev/null +++ b/packages/in_app_purchase/example/keystore.example.properties @@ -0,0 +1,4 @@ +storePassword=??? +keyPassword=??? +keyAlias=??? +storeFile=??? \ No newline at end of file