diff --git a/.circleci/config.yml b/.circleci/config.yml index 28f463ad..036b467e 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -166,9 +166,14 @@ jobs: project: revenuecat_examples/purchase_tester - flutter-get-dependencies: project: api_tester + - flutter-get-dependencies: + project: purchases_ui_flutter - run: name: Analyze code command: flutter analyze + - run: + name: Analyze code for purchases_ui_flutter + command: cd purchases_ui_flutter && flutter analyze test: description: "Run tests for Flutter" @@ -180,6 +185,9 @@ jobs: - run: name: Run tests command: flutter test + - run: + name: Run tests for purchases_ui_flutter + command: cd purchases_ui_flutter && flutter test api_test: description: "Run API tests for Flutter" @@ -316,20 +324,9 @@ jobs: - run: name: Dry Run Release command: flutter pub publish --dry-run - - prepare-next-version: - description: "Creates a PR with the new snapshot version" - docker: - - image: cimg/ruby:3.1.2 - steps: - - checkout - - revenuecat/install-gem-unix-dependencies: - cache-version: v1 - - revenuecat/trust-github-key - - revenuecat/setup-git-credentials - run: - name: Prepare next version - command: bundle exec fastlane prepare_next_version + name: Dry Run Release for purchases_ui_flutter + command: cd purchases_ui_flutter && flutter pub publish --dry-run update-hybrid-common-versions: description: "Creates a PR updating purchases-hybrid-common to latest release" @@ -349,8 +346,9 @@ jobs: version:<< pipeline.parameters.version >> \ open_pr:true \ automatic_release:<< pipeline.parameters.automatic >> + make-release: - description: "Publishes the new version to pub.dev and creates a github release" + description: "Publishes a new version of purchases-flutter to pub.dev and creates a github release" docker: - image: ghcr.io/cirruslabs/flutter:stable steps: @@ -362,6 +360,32 @@ jobs: name: release command: bundle exec fastlane release + make-release-purchases-flutter-ui: + description: "Publishes a new version of purchases-flutter-ui to pub.dev and creates a github release" + docker: + - image: ghcr.io/cirruslabs/flutter:stable + steps: + - checkout + - revenuecat/install-gem-unix-dependencies: + cache-version: v1 + - revenuecat/trust-github-key + - run: + name: release + command: bundle exec fastlane release_purchases_ui_flutter + + github_release: + description: "Creates a github release with the current version" + docker: + - image: cimg/ruby:3.1.2 + steps: + - checkout + - revenuecat/install-gem-unix-dependencies: + cache-version: v1 + - revenuecat/trust-github-key + - run: + name: release + command: bundle exec fastlane github_release_current_version + workflows: danger: jobs: @@ -403,13 +427,12 @@ workflows: - hold <<: *release-branches - make-release: *release-tags - snapshot-bump: - when: - not: - equal: [ scheduled_pipeline, << pipeline.trigger_source >> ] - jobs: - - prepare-next-version: - <<: *only-main-branch + - make-release-purchases-flutter-ui: *release-tags + - github_release: + requires: + - make-release + - make-release-purchases-flutter-ui + <<: *release-tags weekly-run-workflow: when: and: diff --git a/.pubignore b/.pubignore new file mode 100644 index 00000000..555c62b6 --- /dev/null +++ b/.pubignore @@ -0,0 +1,26 @@ +revenuecat_examples +scripts +migrations +fastlane +api_tester +codegen.sh +Dangerfile +Gemfile +Gemfile.lock +pubspec.lock + +# Imported from .gitignore +vendor +.bundle +flutter +**/Podfile.lock +*.bck +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks diff --git a/.version b/.version index c2d76ffc..b52662a4 100644 --- a/.version +++ b/.version @@ -1 +1 @@ -6.4.0-SNAPSHOT +6.15.0-beta.4 diff --git a/CHANGELOG-LATEST.md b/CHANGELOG-LATEST.md index 88a36c2a..802a380f 100644 --- a/CHANGELOG-LATEST.md +++ b/CHANGELOG-LATEST.md @@ -1,23 +1,36 @@ -### Dependency Updates -* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 7.3.3 (#864) via RevenueCat Git Bot (@RCGitBot) - * [Android 7.2.3](https://github.com/RevenueCat/purchases-android/releases/tag/7.2.3) - * [Android 7.2.2](https://github.com/RevenueCat/purchases-android/releases/tag/7.2.2) - * [Android 7.2.1](https://github.com/RevenueCat/purchases-android/releases/tag/7.2.1) - * [Android 7.2.0](https://github.com/RevenueCat/purchases-android/releases/tag/7.2.0) - * [Android 7.1.1](https://github.com/RevenueCat/purchases-android/releases/tag/7.1.1) - * [Android 7.1.0](https://github.com/RevenueCat/purchases-android/releases/tag/7.1.0) - * [Android 7.1.0-beta.2](https://github.com/RevenueCat/purchases-android/releases/tag/7.1.0-beta.2) - * [Android 7.1.0-beta.1](https://github.com/RevenueCat/purchases-android/releases/tag/7.1.0-beta.1) - * [iOS 4.30.5](https://github.com/RevenueCat/purchases-ios/releases/tag/4.30.5) - * [iOS 4.30.4](https://github.com/RevenueCat/purchases-ios/releases/tag/4.30.4) - * [iOS 4.30.3](https://github.com/RevenueCat/purchases-ios/releases/tag/4.30.3) - * [iOS 4.30.2](https://github.com/RevenueCat/purchases-ios/releases/tag/4.30.2) - * [iOS 4.30.1](https://github.com/RevenueCat/purchases-ios/releases/tag/4.30.1) - * [iOS 4.30.0](https://github.com/RevenueCat/purchases-ios/releases/tag/4.30.0) -* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 7.3.2 (#859) via RevenueCat Git Bot (@RCGitBot) -* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 7.3.0 (#850) via RevenueCat Git Bot (@RCGitBot) +### New features +* 📱 Initial support for cross-platform RevenueCat Paywalls 🐾 🧱 (#852) + +#### Instructions: +- For Android, you need to change your `MainActivity` to subclass `FlutterFragmentActivity` instead of `FlutterActivity`. Also, the min sdk version of the new package is 24. Please make sure your app's android/build.gradle minSdkVersion has that or a higher version. +- Add `purchases-ui-flutter` in your `pubspec.yaml`: +```yaml +dependencies: + purchases_ui_flutter: 6.15.0-beta.4 +``` + +#### Usage: +```dart +import 'package:purchases_ui_flutter/purchases_ui_flutter.dart'; + +await RevenueCatUI.presentPaywallIfNeeded("pro"); +``` + +#### Limitations: +- Currently only full screen paywalls are supported +- There is no way to detect paywall events other than using `addCustomerInfoUpdateListener` + +#### Breaking changes from previous beta: +- Paywalls functionality has been extracted into a new dependency called purchases_ui_flutter. Add purchases_ui_flutter to your pubspec.yaml. Before this release, it was part of the main RevenueCat SDK `purchases_flutter`. +- A new import is required to use this functionality: `import 'package:purchases_ui_flutter/purchases_ui_flutter.dart';` +- Usage has changed the class from `Purchases.presentPaywall` to `RevenueCatUI.presentPaywall`. +- `presentPaywall` and `presentPaywallIfNeeded` now don't return a result. To detect purchases, please use `Purchases.getCustomerInfo` from the `purchases_flutter` SDK. A return value will be added in future releases. + ### Other Changes -* `Purchase Tester`: remove unused `Activity` (#860) via NachoSoto (@NachoSoto) -* Remove `.common_version` (#861) via NachoSoto (@NachoSoto) -* Add `3.10.1` to `VERSIONS` (#858) via NachoSoto (@NachoSoto) -* Remove unused GoogleProrationMode import (#848) via Toni Rico (@tonidero) +* Fix pub score for `purchases-ui-flutter` (#924) via Toni Rico (@tonidero) +* Update `paywalls` to latest `main` (#923) via Toni Rico (@tonidero) +* Rename `purchases_flutter_ui` to `purchases_ui_flutter` (#921) via Toni Rico (@tonidero) +* Separate paywalls into a different package (#919) via Toni Rico (@tonidero) +* Remove result from presentPaywall methods (#916) via Toni Rico (@tonidero) +* Update to use PHC PaywallHelpers instead of custom activity (#903) via Toni Rico (@tonidero) + diff --git a/CHANGELOG.md b/CHANGELOG.md index 080bb7b0..015f19a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,122 @@ +## 6.15.0-beta.4 +### New features +* 📱 Initial support for cross-platform RevenueCat Paywalls 🐾 🧱 (#852) + +#### Instructions: +- For Android, you need to change your `MainActivity` to subclass `FlutterFragmentActivity` instead of `FlutterActivity`. Also, the min sdk version of the new package is 24. Please make sure your app's android/build.gradle minSdkVersion has that or a higher version. +- Add `purchases-ui-flutter` in your `pubspec.yaml`: +```yaml +dependencies: + purchases_ui_flutter: 6.15.0-beta.4 +``` + +#### Usage: +```dart +import 'package:purchases_ui_flutter/purchases_ui_flutter.dart'; + +await RevenueCatUI.presentPaywallIfNeeded("pro"); +``` + +#### Limitations: +- Currently only full screen paywalls are supported +- There is no way to detect paywall events other than using `addCustomerInfoUpdateListener` + +#### Breaking changes from previous beta: +- Paywalls functionality has been extracted into a new dependency called purchases_ui_flutter. Add purchases_ui_flutter to your pubspec.yaml. Before this release, it was part of the main RevenueCat SDK `purchases_flutter`. +- A new import is required to use this functionality: `import 'package:purchases_ui_flutter/purchases_ui_flutter.dart';` +- Usage has changed the class from `Purchases.presentPaywall` to `RevenueCatUI.presentPaywall`. +- `presentPaywall` and `presentPaywallIfNeeded` now don't return a result. To detect purchases, please use `Purchases.getCustomerInfo` from the `purchases_flutter` SDK. A return value will be added in future releases. + +### Other Changes +* Fix pub score for `purchases-ui-flutter` (#924) via Toni Rico (@tonidero) +* Update `paywalls` to latest `main` (#923) via Toni Rico (@tonidero) +* Rename `purchases_flutter_ui` to `purchases_ui_flutter` (#921) via Toni Rico (@tonidero) +* Separate paywalls into a different package (#919) via Toni Rico (@tonidero) +* Remove result from presentPaywall methods (#916) via Toni Rico (@tonidero) +* Update to use PHC PaywallHelpers instead of custom activity (#903) via Toni Rico (@tonidero) + + +## 6.6.0-beta.3 +### New Features +* 📱 Initial support for cross-platform RevenueCat Paywalls 🐾 🧱 (#852) + +#### Instructions: +- For Android, you need to change your `MainActivity` to subclass `FlutterFragmentActivity` instead of `FlutterActivity`. +- Update `purchases-flutter` in your `pubspec.yaml`: +```yaml +dependencies: + purchases_flutter: 6.6.0-beta.3 +``` + +#### Usage: +```dart +await Purchases.presentPaywallIfNeeded("pro"); +``` + +#### Limitations: + +- Currently only full screen paywalls are supported +- There is no way to detect paywall events other than using `addCustomerInfoUpdateListener` +- Android's `minSdkVersion` is temporarily increased from `19` to `24` to support paywalls. This will be reverted in a future release as we split `purchases_flutter` and `purchases_ui_flutter` + +### Breaking changes from previous beta + +- `PurchasesFlutterActivity` has been removed. Use `FlutterFragmentActivity` provided by Flutter instead + +### Fixes from previous beta +* Fix `presentPaywallIfNeeded` (#904) via Toni Rico (@tonidero) + +### Other Changes +* Update paywalls latest main (#906) via Toni Rico (@tonidero) +* Update to use PHC PaywallHelpers instead of custom activity (#903) via Toni Rico (@tonidero) +* Fix flutter analyze deprecation warnings (#872) + +## 6.6.0 +### Dependency Updates +* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 8.2.1 (#912) via RevenueCat Git Bot (@RCGitBot) + * [Android 7.3.1](https://github.com/RevenueCat/purchases-android/releases/tag/7.3.1) + * [Android 7.3.0](https://github.com/RevenueCat/purchases-android/releases/tag/7.3.0) + * [Android 7.2.9](https://github.com/RevenueCat/purchases-android/releases/tag/7.2.9) + * [Android 7.2.8](https://github.com/RevenueCat/purchases-android/releases/tag/7.2.8) + * [iOS 4.31.6](https://github.com/RevenueCat/purchases-ios/releases/tag/4.31.6) + * [iOS 4.31.5](https://github.com/RevenueCat/purchases-ios/releases/tag/4.31.5) + * [iOS 4.31.4](https://github.com/RevenueCat/purchases-ios/releases/tag/4.31.4) + * [iOS 4.31.3](https://github.com/RevenueCat/purchases-ios/releases/tag/4.31.3) +* Bump fastlane from 2.217.0 to 2.218.0 (#918) via dependabot[bot] (@dependabot[bot]) +* Bump danger from 9.4.1 to 9.4.2 (#896) via dependabot[bot] (@dependabot[bot]) +### Other Changes +* Fix freezed tests after latest update (#899) via Toni Rico (@tonidero) + +## 6.5.1 +### Dependency Updates +* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 8.1.1 (#892) via RevenueCat Git Bot (@RCGitBot) + * [Android 7.2.7](https://github.com/RevenueCat/purchases-android/releases/tag/7.2.7) + * [iOS 4.31.2](https://github.com/RevenueCat/purchases-ios/releases/tag/4.31.2) +* Bump danger from 9.4.0 to 9.4.1 (#889) via dependabot[bot] (@dependabot[bot]) + +## 6.5.0 +### Dependency Updates +* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 8.1.0 (#890) via RevenueCat Git Bot (@RCGitBot) + * [Android 7.2.6](https://github.com/RevenueCat/purchases-android/releases/tag/7.2.6) + * [Android 7.2.5](https://github.com/RevenueCat/purchases-android/releases/tag/7.2.5) + * [iOS 4.31.1](https://github.com/RevenueCat/purchases-ios/releases/tag/4.31.1) + * [iOS 4.31.0](https://github.com/RevenueCat/purchases-ios/releases/tag/4.31.0) +* Bump cocoapods from 1.14.2 to 1.14.3 (#876) via dependabot[bot] (@dependabot[bot]) + +## 6.4.0 +### New Features +* `Trusted Entitlements`: add support for setting `EntitlementVerificationMode` and getting verification result in `EntitlementInfos` and `EntitlementInfo` (#753) via NachoSoto (@NachoSoto) +### Dependency Updates +* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 8.0.0 (#878) via RevenueCat Git Bot (@RCGitBot) + * [Android 7.2.4](https://github.com/RevenueCat/purchases-android/releases/tag/7.2.4) +* [AUTOMATIC BUMP] Updates purchases-hybrid-common to 7.4.0 (#871) via RevenueCat Git Bot (@RCGitBot) + * [Android 7.2.4](https://github.com/RevenueCat/purchases-android/releases/tag/7.2.4) +* Bump fastlane from 2.216.0 to 2.217.0 (#865) via dependabot[bot] (@dependabot[bot]) +* Bump danger from 9.3.2 to 9.4.0 (#866) via dependabot[bot] (@dependabot[bot]) +### Other Changes +* Fix flutter analyze deprecation warnings (#872) via Toni Rico (@tonidero) +* `CI`: disable `prepare-next-version` (#869) via NachoSoto (@NachoSoto) + ## 6.3.0 ### Dependency Updates * [AUTOMATIC BUMP] Updates purchases-hybrid-common to 7.3.3 (#864) via RevenueCat Git Bot (@RCGitBot) diff --git a/Gemfile.lock b/Gemfile.lock index c659d205..a6e20519 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,7 +9,7 @@ GEM specs: CFPropertyList (3.0.6) rexml - activesupport (7.1.1) + activesupport (7.1.2) base64 bigdecimal concurrent-ruby (~> 1.0, >= 1.0.2) @@ -19,43 +19,43 @@ GEM minitest (>= 5.1) mutex_m tzinfo (~> 2.0) - addressable (2.8.5) + addressable (2.8.6) public_suffix (>= 2.0.2, < 6.0) algoliasearch (1.27.5) httpclient (~> 2.8, >= 2.8.3) json (>= 1.5.1) artifactory (3.0.15) atomos (0.1.3) - aws-eventstream (1.2.0) - aws-partitions (1.850.0) - aws-sdk-core (3.186.0) - aws-eventstream (~> 1, >= 1.0.2) + aws-eventstream (1.3.0) + aws-partitions (1.877.0) + aws-sdk-core (3.190.1) + aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.651.0) - aws-sigv4 (~> 1.5) + aws-sigv4 (~> 1.8) jmespath (~> 1, >= 1.6.1) - aws-sdk-kms (1.72.0) - aws-sdk-core (~> 3, >= 3.184.0) + aws-sdk-kms (1.75.0) + aws-sdk-core (~> 3, >= 3.188.0) aws-sigv4 (~> 1.1) - aws-sdk-s3 (1.136.0) - aws-sdk-core (~> 3, >= 3.181.0) + aws-sdk-s3 (1.142.0) + aws-sdk-core (~> 3, >= 3.189.0) aws-sdk-kms (~> 1) - aws-sigv4 (~> 1.6) - aws-sigv4 (1.6.1) + aws-sigv4 (~> 1.8) + aws-sigv4 (1.8.0) aws-eventstream (~> 1, >= 1.0.2) babosa (1.0.4) - base64 (0.1.1) + base64 (0.2.0) bigdecimal (3.1.4) claide (1.1.0) claide-plugins (0.9.2) cork nap open4 (~> 1.3) - cocoapods (1.14.2) + cocoapods (1.14.3) addressable (~> 2.8) claide (>= 1.0.2, < 2.0) - cocoapods-core (= 1.14.2) + cocoapods-core (= 1.14.3) cocoapods-deintegrate (>= 1.0.3, < 2.0) - cocoapods-downloader (>= 2.0) + cocoapods-downloader (>= 2.1, < 3.0) cocoapods-plugins (>= 1.0.0, < 2.0) cocoapods-search (>= 1.0.0, < 2.0) cocoapods-trunk (>= 1.6.0, < 2.0) @@ -68,7 +68,7 @@ GEM nap (~> 1.0) ruby-macho (>= 2.3.0, < 3.0) xcodeproj (>= 1.23.0, < 2.0) - cocoapods-core (1.14.2) + cocoapods-core (1.14.3) activesupport (>= 5.0, < 8) addressable (~> 2.8) algoliasearch (~> 1.0) @@ -79,7 +79,7 @@ GEM public_suffix (~> 4.0) typhoeus (~> 1.0) cocoapods-deintegrate (1.0.5) - cocoapods-downloader (2.0) + cocoapods-downloader (2.1) cocoapods-plugins (1.0.0) nap cocoapods-search (1.0.1) @@ -95,7 +95,7 @@ GEM connection_pool (2.4.1) cork (0.3.0) colored2 (~> 3.1) - danger (9.4.0) + danger (9.4.2) claide (~> 1.0) claide-plugins (>= 0.9.2) colored2 (~> 3.1) @@ -106,20 +106,20 @@ GEM kramdown (~> 2.3) kramdown-parser-gfm (~> 1.0) no_proxy_fix - octokit (>= 6.0, < 8.0) + octokit (>= 4.0) terminal-table (>= 1, < 4) declarative (0.0.20) digest-crc (0.6.5) rake (>= 12.0.0, < 14.0.0) domain_name (0.6.20231109) dotenv (2.8.1) - drb (2.1.1) + drb (2.2.0) ruby2_keywords emoji_regex (3.2.3) escape (0.0.4) ethon (0.16.0) ffi (>= 1.15.0) - excon (0.104.0) + excon (0.109.0) faraday (1.10.3) faraday-em_http (~> 1.0) faraday-em_synchrony (~> 1.0) @@ -150,8 +150,8 @@ GEM faraday-retry (1.0.3) faraday_middleware (1.2.0) faraday (~> 1.0) - fastimage (2.2.7) - fastlane (2.217.0) + fastimage (2.3.0) + fastlane (2.219.0) CFPropertyList (>= 2.3, < 4.0.0) addressable (>= 2.8, < 3.0.0) artifactory (~> 3.0) @@ -170,6 +170,7 @@ GEM gh_inspector (>= 1.1.2, < 2.0.0) google-apis-androidpublisher_v3 (~> 0.3) google-apis-playcustomapp_v1 (~> 0.1) + google-cloud-env (>= 1.6.0, < 2.0.0) google-cloud-storage (~> 1.31) highline (~> 2.0) http-cookie (~> 1.0.5) @@ -178,7 +179,7 @@ GEM mini_magick (>= 4.9.4, < 5.0.0) multipart-post (>= 2.0.0, < 3.0.0) naturally (~> 2.2) - optparse (~> 0.1.1) + optparse (>= 0.1.1) plist (>= 3.1.0, < 4.0.0) rubyzip (>= 2.0.0, < 3.0.0) security (= 0.1.3) @@ -198,7 +199,7 @@ GEM git (1.18.0) addressable (~> 2.8) rchardet (~> 1.8) - google-apis-androidpublisher_v3 (0.52.0) + google-apis-androidpublisher_v3 (0.54.0) google-apis-core (>= 0.11.0, < 2.a) google-apis-core (0.11.2) addressable (~> 2.5, >= 2.5.1) @@ -215,8 +216,8 @@ GEM google-apis-core (>= 0.11.0, < 2.a) google-apis-storage_v1 (0.29.0) google-apis-core (>= 0.11.0, < 2.a) - google-cloud-core (1.6.0) - google-cloud-env (~> 1.0) + google-cloud-core (1.6.1) + google-cloud-env (>= 1.0, < 3.a) google-cloud-errors (~> 1.0) google-cloud-env (1.6.0) faraday (>= 0.17.3, < 3.0) @@ -242,7 +243,7 @@ GEM i18n (1.14.1) concurrent-ruby (~> 1.0) jmespath (1.6.2) - json (2.6.3) + json (2.7.1) jwt (2.7.1) kramdown (2.4.0) rexml @@ -254,19 +255,19 @@ GEM molinillo (0.8.0) multi_json (1.15.0) multipart-post (2.3.0) - mutex_m (0.1.2) + mutex_m (0.2.0) nanaimo (0.3.0) nap (1.1.0) naturally (2.2.1) netrc (0.11.0) no_proxy_fix (0.1.2) - octokit (7.2.0) + octokit (8.0.0) faraday (>= 1, < 3) sawyer (~> 0.9) open4 (1.3.4) - optparse (0.1.1) + optparse (0.4.0) os (1.1.4) - plist (3.7.0) + plist (3.7.1) public_suffix (4.0.7) rake (13.1.0) rchardet (1.8.0) @@ -297,10 +298,10 @@ GEM unicode-display_width (>= 1.1.1, < 3) trailblazer-option (0.1.2) tty-cursor (0.7.1) - tty-screen (0.8.1) + tty-screen (0.8.2) tty-spinner (0.9.3) tty-cursor (~> 0.7) - typhoeus (1.4.0) + typhoeus (1.4.1) ethon (>= 0.9.0) tzinfo (2.0.6) concurrent-ruby (~> 1.0) diff --git a/VERSIONS.md b/VERSIONS.md index 55dff872..698a3f62 100644 --- a/VERSIONS.md +++ b/VERSIONS.md @@ -1,5 +1,9 @@ | Version | iOS version | Android version | Common files version | |---------|-------------|-----------------|----------------------| +| 6.6.0 | 4.31.6 | 7.3.1 | 8.2.1 | +| 6.5.1 | 4.31.2 | 7.2.7 | 8.1.1 | +| 6.5.0 | 4.31.1 | 7.2.6 | 8.1.0 | +| 6.4.0 | 4.30.5 | 7.2.4 | 8.0.0 | | 6.3.0 | 4.30.5 | 7.2.3 | 7.3.3 | | 6.2.0 | 4.29.0 | 7.0.1 | 7.2.0 | | 6.1.0 | 4.28.0 | 7.0.1 | 7.1.0 | diff --git a/android/build.gradle b/android/build.gradle index 693b3dbf..cc72c9c1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,9 +1,9 @@ group 'com.revenuecat.purchases_flutter' -version '6.4.0-SNAPSHOT' +version '6.15.0-beta.4' buildscript { ext.kotlin_version = '1.7.21' - ext.common_version = '7.3.3' + ext.common_version = '8.10.0-beta.10' repositories { google() mavenCentral() @@ -63,6 +63,7 @@ android { } dependencies { + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation "com.revenuecat.purchases:purchases-hybrid-common:$common_version" } diff --git a/android/src/main/java/com/revenuecat/purchases_flutter/PurchasesFlutterPlugin.java b/android/src/main/java/com/revenuecat/purchases_flutter/PurchasesFlutterPlugin.java index 02fdb14e..46140970 100644 --- a/android/src/main/java/com/revenuecat/purchases_flutter/PurchasesFlutterPlugin.java +++ b/android/src/main/java/com/revenuecat/purchases_flutter/PurchasesFlutterPlugin.java @@ -8,6 +8,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.OptIn; import com.revenuecat.purchases.DangerousSettings; import com.revenuecat.purchases.Purchases; @@ -29,6 +30,7 @@ import java.util.List; import java.util.Map; +import io.flutter.embedding.android.FlutterFragmentActivity; import io.flutter.embedding.engine.plugins.FlutterPlugin; import io.flutter.embedding.engine.plugins.activity.ActivityAware; import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding; @@ -43,7 +45,8 @@ * PurchasesFlutterPlugin */ public class PurchasesFlutterPlugin implements FlutterPlugin, MethodCallHandler, ActivityAware { - private String INVALID_ARGS_ERROR_CODE = "invalidArgs"; + private static final String TAG = "PurchasesFlutter"; + private static final String INVALID_ARGS_ERROR_CODE = "invalidArgs"; private static final String CUSTOMER_INFO_UPDATED = "Purchases-CustomerInfoUpdated"; protected static final String LOG_HANDLER_EVENT = "Purchases-LogHandlerEvent"; @@ -59,7 +62,7 @@ public class PurchasesFlutterPlugin implements FlutterPlugin, MethodCallHandler, private final Handler handler = new Handler(Looper.getMainLooper()); private static final String PLATFORM_NAME = "flutter"; - private static final String PLUGIN_VERSION = "6.4.0-SNAPSHOT"; + private static final String PLUGIN_VERSION = "6.15.0-beta.4"; /** * Plugin registration. @@ -139,7 +142,9 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { //noinspection unused Boolean usesStoreKit2IfAvailable = call.argument("usesStoreKit2IfAvailable"); // iOS-only, unused. Boolean shouldShowInAppMessagesAutomatically = call.argument("shouldShowInAppMessagesAutomatically"); - setupPurchases(apiKey, appUserId, observerMode, useAmazon, shouldShowInAppMessagesAutomatically, result); + String verificationMode = call.argument("entitlementVerificationMode"); + setupPurchases(apiKey, appUserId, observerMode, useAmazon, + shouldShowInAppMessagesAutomatically, verificationMode, result); break; case "setFinishTransactions": Boolean finishTransactions = call.argument("finishTransactions"); @@ -357,7 +362,8 @@ public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) { } private void setupPurchases(String apiKey, String appUserID, @Nullable Boolean observerMode, - @Nullable Boolean useAmazon, @Nullable Boolean shouldShowInAppMessagesAutomatically, final Result result) { + @Nullable Boolean useAmazon, @Nullable Boolean shouldShowInAppMessagesAutomatically, + @Nullable String verificationMode, final Result result) { if (this.applicationContext != null) { PlatformInfo platformInfo = new PlatformInfo(PLATFORM_NAME, PLUGIN_VERSION); Store store = Store.PLAY_STORE; @@ -365,7 +371,8 @@ private void setupPurchases(String apiKey, String appUserID, @Nullable Boolean o store = Store.AMAZON; } CommonKt.configure(this.applicationContext, apiKey, appUserID, observerMode, - platformInfo, store, new DangerousSettings(),shouldShowInAppMessagesAutomatically); + platformInfo, store, new DangerousSettings(), + shouldShowInAppMessagesAutomatically, verificationMode); setUpdatedCustomerInfoListener(); result.success(null); } else { @@ -708,7 +715,7 @@ private void showInAppMessages(final ArrayList messageTypes, final Resu if (messageType != null) { messageTypesList.add(messageType); } else { - Log.e("RNPurchases", "Unsupported in-app message type: " + messageTypeInt); + Log.e(TAG, "Unsupported in-app message type: " + messageTypeInt); } } CommonKt.showInAppMessagesIfNeeded(activity, messageTypesList); diff --git a/api_tester/android/app/build.gradle b/api_tester/android/app/build.gradle index 235dcb0b..a3053ce2 100644 --- a/api_tester/android/app/build.gradle +++ b/api_tester/android/app/build.gradle @@ -43,11 +43,12 @@ android { } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "com.revenuecat.api_tester" // You can update the following values to match your application needs. // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. - minSdkVersion flutter.minSdkVersion + // TODO: change + // minSdkVersion flutter.minSdkVersion + minSdkVersion 24 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/api_tester/lib/api_tests/models/entitlement_info_wrapper_api_test.dart b/api_tester/lib/api_tests/models/entitlement_info_wrapper_api_test.dart index b04df84e..62bb81f6 100644 --- a/api_tester/lib/api_tests/models/entitlement_info_wrapper_api_test.dart +++ b/api_tester/lib/api_tests/models/entitlement_info_wrapper_api_test.dart @@ -43,7 +43,8 @@ class _EntitlementInfoApiTest { PeriodType periodType, String? expirationDate, String? unsubscribeDetectedAt, - String? billingIssueDetectedAt) { + String? billingIssueDetectedAt, + VerificationResult verificationResult) { EntitlementInfo entitlementInfo = EntitlementInfo( identifier, isActive, @@ -59,7 +60,8 @@ class _EntitlementInfoApiTest { periodType: periodType, expirationDate: expirationDate, unsubscribeDetectedAt: unsubscribeDetectedAt, - billingIssueDetectedAt: billingIssueDetectedAt); + billingIssueDetectedAt: billingIssueDetectedAt, + verification: verificationResult); } void _checkProperties(EntitlementInfo info) { @@ -76,5 +78,6 @@ class _EntitlementInfoApiTest { String? expirationDate = info.expirationDate; String? unsubscribeDetectedAt = info.unsubscribeDetectedAt; String? billingIssueDetectedAt = info.billingIssueDetectedAt; + VerificationResult verificationResult = info.verification; } } diff --git a/api_tester/lib/api_tests/models/entitlement_infos_wrapper_api_test.dart b/api_tester/lib/api_tests/models/entitlement_infos_wrapper_api_test.dart index 04550a69..b04e685e 100644 --- a/api_tester/lib/api_tests/models/entitlement_infos_wrapper_api_test.dart +++ b/api_tester/lib/api_tests/models/entitlement_infos_wrapper_api_test.dart @@ -12,12 +12,17 @@ class _EntitlementInfosApiTest { } void _checkConstructor( - Map all, Map active) { + Map all, + Map active, + VerificationResult verificationResult) { EntitlementInfos entitlementInfos = EntitlementInfos(all, active); + EntitlementInfos entitlementInfos2 = EntitlementInfos(all, active, + verification: verificationResult); } void _checkProperties(EntitlementInfos entitlementInfos) { Map all = entitlementInfos.all; Map active = entitlementInfos.active; + VerificationResult verificationResult = entitlementInfos.verification; } } diff --git a/api_tester/lib/api_tests/purchases_ui_flutter_api_test.dart b/api_tester/lib/api_tests/purchases_ui_flutter_api_test.dart new file mode 100644 index 00000000..8bd117dd --- /dev/null +++ b/api_tester/lib/api_tests/purchases_ui_flutter_api_test.dart @@ -0,0 +1,21 @@ +import 'package:purchases_ui_flutter/purchases_ui_flutter.dart'; + +// ignore_for_file: unused_element +// ignore_for_file: unused_local_variable +class _PurchasesFlutterApiTest { + void _checkPresentPaywall() async { + Future future1 = RevenueCatUI.presentPaywall(); + Future future2 = RevenueCatUI.presentPaywallIfNeeded("test"); + } + + void _checkPaywallResult(PaywallResult result) { + switch (result) { + case PaywallResult.notPresented: + case PaywallResult.cancelled: + case PaywallResult.error: + case PaywallResult.purchased: + case PaywallResult.restored: + break; + } + } +} diff --git a/api_tester/lib/api_tests_import.dart b/api_tester/lib/api_tests_import.dart index 8de337f1..dc3906b6 100644 --- a/api_tester/lib/api_tests_import.dart +++ b/api_tester/lib/api_tests_import.dart @@ -22,3 +22,4 @@ import 'package:api_tester/api_tests/models/store_product_wrapper_api_test.dart' import 'package:api_tester/api_tests/models/store_transaction_api_test.dart'; import 'package:api_tester/api_tests/models/subscription_option_wrapper_api_test.dart'; import 'package:api_tester/api_tests/purchases_flutter_api_test.dart'; +import 'package:api_tester/api_tests/purchases_ui_flutter_api_test.dart'; diff --git a/api_tester/pubspec.yaml b/api_tester/pubspec.yaml index 0388213f..8f6a22ed 100644 --- a/api_tester/pubspec.yaml +++ b/api_tester/pubspec.yaml @@ -44,6 +44,8 @@ dev_dependencies: purchases_flutter: path: ../ + purchases_ui_flutter: + path: ../purchases_ui_flutter # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/fastlane/Fastfile b/fastlane/Fastfile index 8e9c32c7..03aaef45 100644 --- a/fastlane/Fastfile +++ b/fastlane/Fastfile @@ -25,6 +25,10 @@ files_with_version_number = { './ios/Classes/PurchasesFlutterPlugin.m' => ['return @"{x}"'], './android/build.gradle' => ['version \'{x}\''], './android/src/main/java/com/revenuecat/purchases_flutter/PurchasesFlutterPlugin.java' => ['PLUGIN_VERSION = "{x}"'], + './purchases_ui_flutter/pubspec.yaml' => ['version: {x}'], + './purchases_ui_flutter/ios/purchases_ui_flutter.podspec' => ['s.version = \'{x}\''], + './purchases_ui_flutter/macos/purchases_ui_flutter.podspec' => ['s.version = \'{x}\''], + './purchases_ui_flutter/android/build.gradle' => ['version \'{x}\''], } repo_name = 'purchases-flutter' @@ -85,16 +89,35 @@ end desc "Create release" lane :release do |options| + # Remove purchases_ui_flutter so it's not included in the release of the main SDK + # Can't use .pubignore because it causes files to not be found when trying to + # deploy purchases_ui_flutter + sh('rm', '-rf', 'purchases_ui_flutter') Dir.chdir('..') do sh('flutter', 'pub', 'publish', '--dry-run') end - github_release(version: current_version_number) - setup_credentials_file + setup_credentials_file Dir.chdir('..') do sh('flutter', 'pub', 'publish', '--force') end end +desc "Create purchases_ui_flutter release" +lane :release_purchases_ui_flutter do |options| + Dir.chdir('../purchases_ui_flutter') do + sh('flutter', 'pub', 'publish', '--dry-run') + end + setup_credentials_file + Dir.chdir('../purchases_ui_flutter') do + sh('flutter', 'pub', 'publish', '--force') + end +end + +desc "Make github release with current version number" +lane :github_release_current_version do |options| + github_release(version: current_version_number) +end + desc "Make github release" lane :github_release do |options| create_github_release( @@ -106,17 +129,6 @@ lane :github_release do |options| ) end -desc "Creates PR changing version to next minor adding a -SNAPSHOT suffix" -lane :prepare_next_version do |options| - create_next_snapshot_version( - current_version: current_version_number, - repo_name: repo_name, - github_pr_token: ENV["GITHUB_PULL_REQUEST_API_TOKEN"], - files_to_update: files_with_version_number, - files_to_update_without_prerelease_modifiers: {} - ) -end - desc "Builds and analyzes the api_tester project to make sure APIs are expected" lane :run_api_tests do |options| check_api_tester_imports_up_to_date @@ -152,6 +164,9 @@ lane :update_hybrid_common do |options| 'ios/purchases_flutter.podspec' => ['s.dependency \'PurchasesHybridCommon\', \'{x}\''], 'macos/purchases_flutter.podspec' => ['s.dependency \'PurchasesHybridCommon\', \'{x}\''], 'android/build.gradle' => ['ext.common_version = \'{x}\''], + 'purchases_ui_flutter/ios/purchases_ui_flutter.podspec' => ['s.dependency \'PurchasesHybridCommon\', \'{x}\''], + 'purchases_ui_flutter/macos/purchases_ui_flutter.podspec' => ['s.dependency \'PurchasesHybridCommon\', \'{x}\''], + 'purchases_ui_flutter/android/build.gradle' => ['ext.common_version = \'{x}\''], } if dry_run diff --git a/fastlane/README.md b/fastlane/README.md index c9390694..652db02b 100644 --- a/fastlane/README.md +++ b/fastlane/README.md @@ -37,21 +37,29 @@ Automatically bumps version, edit changelog, and create pull request Create release -### github_release +### release_purchases_ui_flutter ```sh -[bundle exec] fastlane github_release +[bundle exec] fastlane release_purchases_ui_flutter ``` -Make github release +Create purchases_ui_flutter release -### prepare_next_version +### github_release_current_version ```sh -[bundle exec] fastlane prepare_next_version +[bundle exec] fastlane github_release_current_version ``` -Creates PR changing version to next minor adding a -SNAPSHOT suffix +Make github release with current version number + +### github_release + +```sh +[bundle exec] fastlane github_release +``` + +Make github release ### run_api_tests diff --git a/ios/Classes/PurchasesFlutterPlugin.m b/ios/Classes/PurchasesFlutterPlugin.m index fb749633..a9a2a89b 100644 --- a/ios/Classes/PurchasesFlutterPlugin.m +++ b/ios/Classes/PurchasesFlutterPlugin.m @@ -30,6 +30,7 @@ - (instancetype)initWithChannel:(FlutterMethodChannel *)channel NSAssert(self, @"super init cannot be nil"); self.channel = channel; self.registrar = registrar; + return self; } @@ -53,6 +54,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call if (object != [NSNull null] && object != nil) { shouldShowInAppMessagesAutomatically = [object boolValue]; } + NSString * _Nullable verificationMode = arguments[@"entitlementVerificationMode"]; NSString * _Nullable userDefaultsSuiteName = arguments[@"userDefaultsSuiteName"]; [self setupPurchases:apiKey appUserID:appUserID @@ -60,6 +62,7 @@ - (void)handleMethodCall:(FlutterMethodCall *)call userDefaultsSuiteName:userDefaultsSuiteName usesStoreKit2IfAvailable:usesStoreKit2IfAvailable shouldShowInAppMessagesAutomatically: shouldShowInAppMessagesAutomatically + verificationMode:verificationMode result:result]; } else if ([@"setAllowSharingStoreAccount" isEqualToString:call.method]) { [self setAllowSharingStoreAccount:[arguments[@"allowSharing"] boolValue] result:result]; @@ -228,6 +231,7 @@ - (void)setupPurchases:(NSString *)apiKey userDefaultsSuiteName:(nullable NSString *)userDefaultsSuiteName usesStoreKit2IfAvailable:(BOOL)usesStoreKit2IfAvailable shouldShowInAppMessagesAutomatically:(BOOL)shouldShowInAppMessagesAutomatically + verificationMode:(nullable NSString *)verificationMode result:(FlutterResult)result { if ([appUserID isKindOfClass:NSNull.class]) { appUserID = nil; @@ -244,7 +248,8 @@ - (void)setupPurchases:(NSString *)apiKey platformFlavorVersion:self.platformFlavorVersion usesStoreKit2IfAvailable:usesStoreKit2IfAvailable dangerousSettings:nil - shouldShowInAppMessagesAutomatically:shouldShowInAppMessagesAutomatically]; + shouldShowInAppMessagesAutomatically:shouldShowInAppMessagesAutomatically + verificationMode:verificationMode]; purchases.delegate = self; result(nil); @@ -654,7 +659,7 @@ - (NSString *)platformFlavor { } - (NSString *)platformFlavorVersion { - return @"6.4.0-SNAPSHOT"; + return @"6.15.0-beta.4"; } @end diff --git a/ios/purchases_flutter.podspec b/ios/purchases_flutter.podspec index 8edaef4c..78ce2f6d 100644 --- a/ios/purchases_flutter.podspec +++ b/ios/purchases_flutter.podspec @@ -3,7 +3,7 @@ # Pod::Spec.new do |s| s.name = 'purchases_flutter' - s.version = '6.4.0-SNAPSHOT' + s.version = '6.15.0-beta.4' s.summary = 'Cross-platform subscriptions framework for Flutter.' s.description = <<-DESC Client for the RevenueCat subscription and purchase tracking system, making implementing in-app subscriptions in Flutter easy - receipt validation and status tracking included! @@ -15,7 +15,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'Flutter' - s.dependency 'PurchasesHybridCommon', '7.3.3' + s.dependency 'PurchasesHybridCommon', '8.10.0-beta.10' s.ios.deployment_target = '11.0' s.swift_version = '5.0' diff --git a/lib/models/customer_info_wrapper.freezed.dart b/lib/models/customer_info_wrapper.freezed.dart index 88d9b172..c6ddf3ae 100644 --- a/lib/models/customer_info_wrapper.freezed.dart +++ b/lib/models/customer_info_wrapper.freezed.dart @@ -440,7 +440,7 @@ class _$CustomerInfoImpl implements _CustomerInfo { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$CustomerInfoImpl && diff --git a/lib/models/entitlement_info_wrapper.dart b/lib/models/entitlement_info_wrapper.dart index adec7b9a..007272e1 100644 --- a/lib/models/entitlement_info_wrapper.dart +++ b/lib/models/entitlement_info_wrapper.dart @@ -1,5 +1,6 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'store.dart'; +import 'verification_result.dart'; part 'entitlement_info_wrapper.freezed.dart'; @@ -111,6 +112,11 @@ class EntitlementInfo with _$EntitlementInfo { /// @note: Entitlement may still be active even if there is a billing issue. /// Check the [isActive] property. String? billingIssueDetectedAt, + + /// If entitlement verification was enabled, the result of that verification. + /// If not, `VerificationResult.NOT_REQUESTED`. + @Default(VerificationResult.notRequested) + VerificationResult verification, }) = _EntitlementInfo; factory EntitlementInfo.fromJson(Map json) => diff --git a/lib/models/entitlement_info_wrapper.freezed.dart b/lib/models/entitlement_info_wrapper.freezed.dart index 2a933369..db09fa3c 100644 --- a/lib/models/entitlement_info_wrapper.freezed.dart +++ b/lib/models/entitlement_info_wrapper.freezed.dart @@ -74,6 +74,10 @@ mixin _$EntitlementInfo { /// Check the [isActive] property. String? get billingIssueDetectedAt => throw _privateConstructorUsedError; + /// If entitlement verification was enabled, the result of that verification. + /// If not, `VerificationResult.NOT_REQUESTED`. + VerificationResult get verification => throw _privateConstructorUsedError; + Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) $EntitlementInfoCopyWith get copyWith => @@ -101,7 +105,8 @@ abstract class $EntitlementInfoCopyWith<$Res> { PeriodType periodType, String? expirationDate, String? unsubscribeDetectedAt, - String? billingIssueDetectedAt}); + String? billingIssueDetectedAt, + VerificationResult verification}); } /// @nodoc @@ -130,6 +135,7 @@ class _$EntitlementInfoCopyWithImpl<$Res, $Val extends EntitlementInfo> Object? expirationDate = freezed, Object? unsubscribeDetectedAt = freezed, Object? billingIssueDetectedAt = freezed, + Object? verification = null, }) { return _then(_value.copyWith( identifier: null == identifier @@ -184,6 +190,10 @@ class _$EntitlementInfoCopyWithImpl<$Res, $Val extends EntitlementInfo> ? _value.billingIssueDetectedAt : billingIssueDetectedAt // ignore: cast_nullable_to_non_nullable as String?, + verification: null == verification + ? _value.verification + : verification // ignore: cast_nullable_to_non_nullable + as VerificationResult, ) as $Val); } } @@ -211,7 +221,8 @@ abstract class _$$EntitlementInfoImplCopyWith<$Res> PeriodType periodType, String? expirationDate, String? unsubscribeDetectedAt, - String? billingIssueDetectedAt}); + String? billingIssueDetectedAt, + VerificationResult verification}); } /// @nodoc @@ -238,6 +249,7 @@ class __$$EntitlementInfoImplCopyWithImpl<$Res> Object? expirationDate = freezed, Object? unsubscribeDetectedAt = freezed, Object? billingIssueDetectedAt = freezed, + Object? verification = null, }) { return _then(_$EntitlementInfoImpl( null == identifier @@ -292,6 +304,10 @@ class __$$EntitlementInfoImplCopyWithImpl<$Res> ? _value.billingIssueDetectedAt : billingIssueDetectedAt // ignore: cast_nullable_to_non_nullable as String?, + verification: null == verification + ? _value.verification + : verification // ignore: cast_nullable_to_non_nullable + as VerificationResult, )); } } @@ -315,7 +331,8 @@ class _$EntitlementInfoImpl implements _EntitlementInfo { this.periodType = PeriodType.unknown, this.expirationDate, this.unsubscribeDetectedAt, - this.billingIssueDetectedAt}); + this.billingIssueDetectedAt, + this.verification = VerificationResult.notRequested}); factory _$EntitlementInfoImpl.fromJson(Map json) => _$$EntitlementInfoImplFromJson(json); @@ -387,13 +404,19 @@ class _$EntitlementInfoImpl implements _EntitlementInfo { @override final String? billingIssueDetectedAt; + /// If entitlement verification was enabled, the result of that verification. + /// If not, `VerificationResult.NOT_REQUESTED`. + @override + @JsonKey() + final VerificationResult verification; + @override String toString() { - return 'EntitlementInfo(identifier: $identifier, isActive: $isActive, willRenew: $willRenew, latestPurchaseDate: $latestPurchaseDate, originalPurchaseDate: $originalPurchaseDate, productIdentifier: $productIdentifier, isSandbox: $isSandbox, ownershipType: $ownershipType, store: $store, periodType: $periodType, expirationDate: $expirationDate, unsubscribeDetectedAt: $unsubscribeDetectedAt, billingIssueDetectedAt: $billingIssueDetectedAt)'; + return 'EntitlementInfo(identifier: $identifier, isActive: $isActive, willRenew: $willRenew, latestPurchaseDate: $latestPurchaseDate, originalPurchaseDate: $originalPurchaseDate, productIdentifier: $productIdentifier, isSandbox: $isSandbox, ownershipType: $ownershipType, store: $store, periodType: $periodType, expirationDate: $expirationDate, unsubscribeDetectedAt: $unsubscribeDetectedAt, billingIssueDetectedAt: $billingIssueDetectedAt, verification: $verification)'; } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$EntitlementInfoImpl && @@ -421,7 +444,9 @@ class _$EntitlementInfoImpl implements _EntitlementInfo { (identical(other.unsubscribeDetectedAt, unsubscribeDetectedAt) || other.unsubscribeDetectedAt == unsubscribeDetectedAt) && (identical(other.billingIssueDetectedAt, billingIssueDetectedAt) || - other.billingIssueDetectedAt == billingIssueDetectedAt)); + other.billingIssueDetectedAt == billingIssueDetectedAt) && + (identical(other.verification, verification) || + other.verification == verification)); } @JsonKey(ignore: true) @@ -440,7 +465,8 @@ class _$EntitlementInfoImpl implements _EntitlementInfo { periodType, expirationDate, unsubscribeDetectedAt, - billingIssueDetectedAt); + billingIssueDetectedAt, + verification); @JsonKey(ignore: true) @override @@ -474,7 +500,8 @@ abstract class _EntitlementInfo implements EntitlementInfo { final PeriodType periodType, final String? expirationDate, final String? unsubscribeDetectedAt, - final String? billingIssueDetectedAt}) = _$EntitlementInfoImpl; + final String? billingIssueDetectedAt, + final VerificationResult verification}) = _$EntitlementInfoImpl; factory _EntitlementInfo.fromJson(Map json) = _$EntitlementInfoImpl.fromJson; @@ -547,6 +574,11 @@ abstract class _EntitlementInfo implements EntitlementInfo { /// Check the [isActive] property. String? get billingIssueDetectedAt; @override + + /// If entitlement verification was enabled, the result of that verification. + /// If not, `VerificationResult.NOT_REQUESTED`. + VerificationResult get verification; + @override @JsonKey(ignore: true) _$$EntitlementInfoImplCopyWith<_$EntitlementInfoImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/models/entitlement_info_wrapper.g.dart b/lib/models/entitlement_info_wrapper.g.dart index bcd56aa8..2476b8f8 100644 --- a/lib/models/entitlement_info_wrapper.g.dart +++ b/lib/models/entitlement_info_wrapper.g.dart @@ -28,6 +28,9 @@ _$EntitlementInfoImpl _$$EntitlementInfoImplFromJson(Map json) => expirationDate: json['expirationDate'] as String?, unsubscribeDetectedAt: json['unsubscribeDetectedAt'] as String?, billingIssueDetectedAt: json['billingIssueDetectedAt'] as String?, + verification: $enumDecodeNullable( + _$VerificationResultEnumMap, json['verification']) ?? + VerificationResult.notRequested, ); Map _$$EntitlementInfoImplToJson( @@ -46,6 +49,7 @@ Map _$$EntitlementInfoImplToJson( 'expirationDate': instance.expirationDate, 'unsubscribeDetectedAt': instance.unsubscribeDetectedAt, 'billingIssueDetectedAt': instance.billingIssueDetectedAt, + 'verification': _$VerificationResultEnumMap[instance.verification]!, }; const _$OwnershipTypeEnumMap = { @@ -70,3 +74,10 @@ const _$PeriodTypeEnumMap = { PeriodType.trial: 'TRIAL', PeriodType.unknown: 'unknown', }; + +const _$VerificationResultEnumMap = { + VerificationResult.notRequested: 'NOT_REQUESTED', + VerificationResult.verified: 'VERIFIED', + VerificationResult.verifiedOnDevice: 'VERIFIED_ON_DEVICE', + VerificationResult.failed: 'FAILED', +}; diff --git a/lib/models/entitlement_infos_wrapper.dart b/lib/models/entitlement_infos_wrapper.dart index 8eee7187..89f5e7c9 100644 --- a/lib/models/entitlement_infos_wrapper.dart +++ b/lib/models/entitlement_infos_wrapper.dart @@ -1,6 +1,7 @@ import 'package:freezed_annotation/freezed_annotation.dart'; import 'entitlement_info_wrapper.dart'; +import 'verification_result.dart'; part 'entitlement_infos_wrapper.freezed.dart'; part 'entitlement_infos_wrapper.g.dart'; @@ -16,8 +17,13 @@ class EntitlementInfos with _$EntitlementInfos { /// Map of active EntitlementInfo (`EntitlementInfo`) objects keyed by /// entitlement identifier. - final Map active, - ) = _EntitlementInfos; + final Map active, { + + /// If entitlement verification was enabled, the result of that verification. + /// If not, `VerificationResult.NOT_REQUESTED`. + @Default(VerificationResult.notRequested) + final VerificationResult verification, + }) = _EntitlementInfos; factory EntitlementInfos.fromJson(Map json) => _$EntitlementInfosFromJson(json); diff --git a/lib/models/entitlement_infos_wrapper.freezed.dart b/lib/models/entitlement_infos_wrapper.freezed.dart index a82001f7..29b8929e 100644 --- a/lib/models/entitlement_infos_wrapper.freezed.dart +++ b/lib/models/entitlement_infos_wrapper.freezed.dart @@ -28,6 +28,10 @@ mixin _$EntitlementInfos { /// entitlement identifier. Map get active => throw _privateConstructorUsedError; + /// If entitlement verification was enabled, the result of that verification. + /// If not, `VerificationResult.NOT_REQUESTED`. + VerificationResult get verification => throw _privateConstructorUsedError; + Map toJson() => throw _privateConstructorUsedError; @JsonKey(ignore: true) $EntitlementInfosCopyWith get copyWith => @@ -41,7 +45,9 @@ abstract class $EntitlementInfosCopyWith<$Res> { _$EntitlementInfosCopyWithImpl<$Res, EntitlementInfos>; @useResult $Res call( - {Map all, Map active}); + {Map all, + Map active, + VerificationResult verification}); } /// @nodoc @@ -59,6 +65,7 @@ class _$EntitlementInfosCopyWithImpl<$Res, $Val extends EntitlementInfos> $Res call({ Object? all = null, Object? active = null, + Object? verification = null, }) { return _then(_value.copyWith( all: null == all @@ -69,6 +76,10 @@ class _$EntitlementInfosCopyWithImpl<$Res, $Val extends EntitlementInfos> ? _value.active : active // ignore: cast_nullable_to_non_nullable as Map, + verification: null == verification + ? _value.verification + : verification // ignore: cast_nullable_to_non_nullable + as VerificationResult, ) as $Val); } } @@ -82,7 +93,9 @@ abstract class _$$EntitlementInfosImplCopyWith<$Res> @override @useResult $Res call( - {Map all, Map active}); + {Map all, + Map active, + VerificationResult verification}); } /// @nodoc @@ -98,6 +111,7 @@ class __$$EntitlementInfosImplCopyWithImpl<$Res> $Res call({ Object? all = null, Object? active = null, + Object? verification = null, }) { return _then(_$EntitlementInfosImpl( null == all @@ -108,6 +122,10 @@ class __$$EntitlementInfosImplCopyWithImpl<$Res> ? _value._active : active // ignore: cast_nullable_to_non_nullable as Map, + verification: null == verification + ? _value.verification + : verification // ignore: cast_nullable_to_non_nullable + as VerificationResult, )); } } @@ -116,7 +134,8 @@ class __$$EntitlementInfosImplCopyWithImpl<$Res> @JsonSerializable() class _$EntitlementInfosImpl implements _EntitlementInfos { const _$EntitlementInfosImpl(final Map all, - final Map active) + final Map active, + {this.verification = VerificationResult.notRequested}) : _all = all, _active = active; @@ -149,18 +168,26 @@ class _$EntitlementInfosImpl implements _EntitlementInfos { return EqualUnmodifiableMapView(_active); } + /// If entitlement verification was enabled, the result of that verification. + /// If not, `VerificationResult.NOT_REQUESTED`. + @override + @JsonKey() + final VerificationResult verification; + @override String toString() { - return 'EntitlementInfos(all: $all, active: $active)'; + return 'EntitlementInfos(all: $all, active: $active, verification: $verification)'; } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$EntitlementInfosImpl && const DeepCollectionEquality().equals(other._all, _all) && - const DeepCollectionEquality().equals(other._active, _active)); + const DeepCollectionEquality().equals(other._active, _active) && + (identical(other.verification, verification) || + other.verification == verification)); } @JsonKey(ignore: true) @@ -168,7 +195,8 @@ class _$EntitlementInfosImpl implements _EntitlementInfos { int get hashCode => Object.hash( runtimeType, const DeepCollectionEquality().hash(_all), - const DeepCollectionEquality().hash(_active)); + const DeepCollectionEquality().hash(_active), + verification); @JsonKey(ignore: true) @override @@ -187,7 +215,8 @@ class _$EntitlementInfosImpl implements _EntitlementInfos { abstract class _EntitlementInfos implements EntitlementInfos { const factory _EntitlementInfos(final Map all, - final Map active) = _$EntitlementInfosImpl; + final Map active, + {final VerificationResult verification}) = _$EntitlementInfosImpl; factory _EntitlementInfos.fromJson(Map json) = _$EntitlementInfosImpl.fromJson; @@ -203,6 +232,11 @@ abstract class _EntitlementInfos implements EntitlementInfos { /// entitlement identifier. Map get active; @override + + /// If entitlement verification was enabled, the result of that verification. + /// If not, `VerificationResult.NOT_REQUESTED`. + VerificationResult get verification; + @override @JsonKey(ignore: true) _$$EntitlementInfosImplCopyWith<_$EntitlementInfosImpl> get copyWith => throw _privateConstructorUsedError; diff --git a/lib/models/entitlement_infos_wrapper.g.dart b/lib/models/entitlement_infos_wrapper.g.dart index 024084a7..589dfd4b 100644 --- a/lib/models/entitlement_infos_wrapper.g.dart +++ b/lib/models/entitlement_infos_wrapper.g.dart @@ -16,6 +16,9 @@ _$EntitlementInfosImpl _$$EntitlementInfosImplFromJson(Map json) => (k, e) => MapEntry(k as String, EntitlementInfo.fromJson(Map.from(e as Map))), ), + verification: $enumDecodeNullable( + _$VerificationResultEnumMap, json['verification']) ?? + VerificationResult.notRequested, ); Map _$$EntitlementInfosImplToJson( @@ -23,4 +26,12 @@ Map _$$EntitlementInfosImplToJson( { 'all': instance.all.map((k, e) => MapEntry(k, e.toJson())), 'active': instance.active.map((k, e) => MapEntry(k, e.toJson())), + 'verification': _$VerificationResultEnumMap[instance.verification]!, }; + +const _$VerificationResultEnumMap = { + VerificationResult.notRequested: 'NOT_REQUESTED', + VerificationResult.verified: 'VERIFIED', + VerificationResult.verifiedOnDevice: 'VERIFIED_ON_DEVICE', + VerificationResult.failed: 'FAILED', +}; diff --git a/lib/models/entitlement_verification_mode.dart b/lib/models/entitlement_verification_mode.dart new file mode 100644 index 00000000..97b0a266 --- /dev/null +++ b/lib/models/entitlement_verification_mode.dart @@ -0,0 +1,29 @@ +/// Enum of entitlement verification modes +enum EntitlementVerificationMode { + /// The SDK will not perform any entitlement verification. + disabled, + + /// Enable entitlement verification. + /// + /// If verification fails, this will be indicated with [VerificationResult.FAILED] in + /// the [EntitlementInfos.verification] and [EntitlementInfo.verification] properties but parsing will not fail + /// (i.e. Entitlements will still be granted). + /// + /// This can be useful if you want to handle verification failures to display an error/warning to the user + /// or to track this situation but still grant access. + informational, + + // Disabled temporarily until ENFORCED is supported. + // enforced +} + +extension EntitlementVerificationModeExtension on EntitlementVerificationMode { + String get name { + switch (this) { + case EntitlementVerificationMode.disabled: + return 'DISABLED'; + case EntitlementVerificationMode.informational: + return 'INFORMATIONAL'; + } + } +} diff --git a/lib/models/introductory_price.freezed.dart b/lib/models/introductory_price.freezed.dart index 3aeb3fc7..a2fcc8b9 100644 --- a/lib/models/introductory_price.freezed.dart +++ b/lib/models/introductory_price.freezed.dart @@ -231,7 +231,7 @@ class _$IntroductoryPriceImpl implements _IntroductoryPrice { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$IntroductoryPriceImpl && diff --git a/lib/models/offering_wrapper.freezed.dart b/lib/models/offering_wrapper.freezed.dart index 422759a9..4f4b68a5 100644 --- a/lib/models/offering_wrapper.freezed.dart +++ b/lib/models/offering_wrapper.freezed.dart @@ -437,7 +437,7 @@ class _$OfferingImpl extends _Offering { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$OfferingImpl && diff --git a/lib/models/offerings_wrapper.freezed.dart b/lib/models/offerings_wrapper.freezed.dart index 3eb7c9e0..b79a02e4 100644 --- a/lib/models/offerings_wrapper.freezed.dart +++ b/lib/models/offerings_wrapper.freezed.dart @@ -155,7 +155,7 @@ class _$OfferingsImpl extends _Offerings { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$OfferingsImpl && diff --git a/lib/models/package_wrapper.freezed.dart b/lib/models/package_wrapper.freezed.dart index e0b208b4..c5c530df 100644 --- a/lib/models/package_wrapper.freezed.dart +++ b/lib/models/package_wrapper.freezed.dart @@ -199,7 +199,7 @@ class _$PackageImpl implements _Package { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PackageImpl && diff --git a/lib/models/period_wrapper.freezed.dart b/lib/models/period_wrapper.freezed.dart index 22636b03..fa1466d6 100644 --- a/lib/models/period_wrapper.freezed.dart +++ b/lib/models/period_wrapper.freezed.dart @@ -148,7 +148,7 @@ class _$PeriodImpl implements _Period { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PeriodImpl && diff --git a/lib/models/price_wrapper.freezed.dart b/lib/models/price_wrapper.freezed.dart index 1b110c73..eaad3af0 100644 --- a/lib/models/price_wrapper.freezed.dart +++ b/lib/models/price_wrapper.freezed.dart @@ -158,7 +158,7 @@ class _$PriceImpl implements _Price { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PriceImpl && diff --git a/lib/models/pricing_phase_wrapper.freezed.dart b/lib/models/pricing_phase_wrapper.freezed.dart index 4422e6d0..506d908f 100644 --- a/lib/models/pricing_phase_wrapper.freezed.dart +++ b/lib/models/pricing_phase_wrapper.freezed.dart @@ -222,7 +222,7 @@ class _$PricingPhaseImpl implements _PricingPhase { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PricingPhaseImpl && diff --git a/lib/models/promotional_offer.freezed.dart b/lib/models/promotional_offer.freezed.dart index e896463b..734d7fcc 100644 --- a/lib/models/promotional_offer.freezed.dart +++ b/lib/models/promotional_offer.freezed.dart @@ -194,7 +194,7 @@ class _$PromotionalOfferImpl implements _PromotionalOffer { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$PromotionalOfferImpl && diff --git a/lib/models/purchases_configuration.dart b/lib/models/purchases_configuration.dart index 2e620be9..32c97624 100644 --- a/lib/models/purchases_configuration.dart +++ b/lib/models/purchases_configuration.dart @@ -1,5 +1,4 @@ import '../purchases_flutter.dart'; -import 'store.dart'; /// Used when calling [configure] to configure the RevenueCat plugin class PurchasesConfiguration { @@ -50,6 +49,10 @@ class PurchasesConfiguration { /// Required to configure the plugin to be used in the Amazon Appstore. /// Values different to [Store.amazon] don't have any effect. Store? store; + + /// Verification strictness levels for [EntitlementInfo]. + /// See https://rev.cat/trusted-entitlements for more info. + EntitlementVerificationMode entitlementVerificationMode = EntitlementVerificationMode.disabled; } /// A [PurchasesConfiguration] convenience object that diff --git a/lib/models/store_product_discount.freezed.dart b/lib/models/store_product_discount.freezed.dart index 494d8843..b22c05c0 100644 --- a/lib/models/store_product_discount.freezed.dart +++ b/lib/models/store_product_discount.freezed.dart @@ -237,7 +237,7 @@ class _$StoreProductDiscountImpl implements _StoreProductDiscount { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$StoreProductDiscountImpl && diff --git a/lib/models/store_product_wrapper.freezed.dart b/lib/models/store_product_wrapper.freezed.dart index 11e18f24..69e6c4ab 100644 --- a/lib/models/store_product_wrapper.freezed.dart +++ b/lib/models/store_product_wrapper.freezed.dart @@ -421,7 +421,7 @@ class _$StoreProductImpl implements _StoreProduct { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$StoreProductImpl && diff --git a/lib/models/store_transaction.freezed.dart b/lib/models/store_transaction.freezed.dart index 565df43b..b7375f10 100644 --- a/lib/models/store_transaction.freezed.dart +++ b/lib/models/store_transaction.freezed.dart @@ -238,7 +238,7 @@ class _$StoreTransactionImpl implements _StoreTransaction { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$StoreTransactionImpl && diff --git a/lib/models/subscription_option_wrapper.freezed.dart b/lib/models/subscription_option_wrapper.freezed.dart index 882785ba..f2df311b 100644 --- a/lib/models/subscription_option_wrapper.freezed.dart +++ b/lib/models/subscription_option_wrapper.freezed.dart @@ -430,7 +430,7 @@ class _$SubscriptionOptionImpl implements _SubscriptionOption { } @override - bool operator ==(dynamic other) { + bool operator ==(Object other) { return identical(this, other) || (other.runtimeType == runtimeType && other is _$SubscriptionOptionImpl && diff --git a/lib/models/verification_result.dart b/lib/models/verification_result.dart new file mode 100644 index 00000000..658666cc --- /dev/null +++ b/lib/models/verification_result.dart @@ -0,0 +1,17 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +/// Enum of entitlement verification modes +enum VerificationResult { + /// The SDK will not perform any entitlement verification. + @JsonValue('NOT_REQUESTED') + notRequested, + + @JsonValue('VERIFIED') + verified, + + @JsonValue('VERIFIED_ON_DEVICE') + verifiedOnDevice, + + @JsonValue('FAILED') + failed +} diff --git a/lib/object_wrappers.dart b/lib/object_wrappers.dart index f1bbc97e..0513e517 100644 --- a/lib/object_wrappers.dart +++ b/lib/object_wrappers.dart @@ -2,6 +2,7 @@ export 'errors.dart'; export 'models/customer_info_wrapper.dart'; export 'models/entitlement_info_wrapper.dart'; export 'models/entitlement_infos_wrapper.dart'; +export 'models/entitlement_verification_mode.dart'; export 'models/google_product_change.dart'; export 'models/introductory_price.dart'; export 'models/offering_wrapper.dart'; @@ -20,3 +21,4 @@ export 'models/store_product_wrapper.dart'; export 'models/store_transaction.dart'; export 'models/subscription_option_wrapper.dart'; export 'models/upgrade_info.dart'; +export 'models/verification_result.dart'; diff --git a/lib/purchases_flutter.dart b/lib/purchases_flutter.dart index 3eac718e..2b8821b5 100644 --- a/lib/purchases_flutter.dart +++ b/lib/purchases_flutter.dart @@ -144,6 +144,7 @@ class Purchases { // ignore: deprecated_member_use_from_same_package purchasesConfiguration.usesStoreKit2IfAvailable, 'shouldShowInAppMessagesAutomatically': purchasesConfiguration.shouldShowInAppMessagesAutomatically, + 'entitlementVerificationMode': purchasesConfiguration.entitlementVerificationMode.name, }, ); @@ -259,9 +260,9 @@ class Purchases { String typeString; // ignore: deprecated_member_use_from_same_package if (type != PurchaseType.subs) { - typeString = describeEnum(type); + typeString = type.name; } else { - typeString = describeEnum(productCategory); + typeString = productCategory.name; } final List result = await _channel.invokeMethod('getProductInfo', { @@ -304,7 +305,7 @@ class Purchases { final prorationMode = upgradeInfo?.prorationMode; final customerInfo = await _invokeReturningCustomerInfo('purchaseProduct', { 'productIdentifier': productIdentifier, - 'type': describeEnum(type), + 'type': type.name, 'googleOldProductIdentifier': upgradeInfo?.oldSKU, 'googleProrationMode': prorationMode?.index, 'googleIsPersonalizedPrice': null, @@ -339,9 +340,7 @@ class Purchases { final prorationMode = googleProductChangeInfo?.prorationMode?.value; final customerInfo = await _invokeReturningCustomerInfo('purchaseProduct', { 'productIdentifier': storeProduct.identifier, - 'type': storeProduct.productCategory != null - ? describeEnum(storeProduct.productCategory!) - : null, + 'type': storeProduct.productCategory?.name, 'googleOldProductIdentifier': googleProductChangeInfo?.oldProductIdentifier, 'googleProrationMode': prorationMode, @@ -540,7 +539,7 @@ class Purchases { /// The default is {LOG_LEVEL.INFO} in release builds and {LOG_LEVEL.DEBUG} in debug builds. static Future setLogLevel(LogLevel level) => _channel.invokeMethod( 'setLogLevel', - {'level': describeEnum(level).toUpperCase()}, + {'level': level.name.toUpperCase()}, ); /// @@ -929,7 +928,7 @@ class Purchases { final args = Map.from(call.arguments); final logLevelName = args['logLevel']; final logLevel = LogLevel.values.firstWhere( - (e) => describeEnum(e).toUpperCase() == logLevelName, + (e) => e.name.toUpperCase() == logLevelName, orElse: () => LogLevel.info, ); final msg = args['message']; diff --git a/macos/purchases_flutter.podspec b/macos/purchases_flutter.podspec index 809a0c7d..be6d3cc3 100644 --- a/macos/purchases_flutter.podspec +++ b/macos/purchases_flutter.podspec @@ -4,7 +4,7 @@ # Pod::Spec.new do |s| s.name = 'purchases_flutter' - s.version = '6.4.0-SNAPSHOT' + s.version = '6.15.0-beta.4' s.summary = 'Cross-platform subscriptions framework for Flutter.' s.description = <<-DESC Client for the RevenueCat subscription and purchase tracking system, making implementing in-app subscriptions in Flutter easy - receipt validation and status tracking included! @@ -16,7 +16,7 @@ Pod::Spec.new do |s| s.source_files = 'Classes/**/*' s.public_header_files = 'Classes/**/*.h' s.dependency 'FlutterMacOS' - s.dependency 'PurchasesHybridCommon', '7.3.3' + s.dependency 'PurchasesHybridCommon', '8.10.0-beta.10' s.platform = :osx, '10.12' s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } s.swift_version = '5.0' diff --git a/pubspec.yaml b/pubspec.yaml index adc2c210..17f5935f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: purchases_flutter description: Flutter in-app purchases and subscriptions made easy. The plugin supports iOS, macOS and Android. -version: 6.4.0-SNAPSHOT +version: 6.15.0-beta.4 homepage: https://www.revenuecat.com/ repository: https://github.com/RevenueCat/purchases-flutter issue_tracker: https://github.com/RevenueCat/purchases-flutter/issues diff --git a/purchases_ui_flutter/.gitignore b/purchases_ui_flutter/.gitignore new file mode 100644 index 00000000..ac5aa989 --- /dev/null +++ b/purchases_ui_flutter/.gitignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/purchases_ui_flutter/.metadata b/purchases_ui_flutter/.metadata new file mode 100644 index 00000000..de6974f8 --- /dev/null +++ b/purchases_ui_flutter/.metadata @@ -0,0 +1,36 @@ +# This file tracks properties of this Flutter project. +# Used by Flutter tool to assess capabilities and perform upgrades etc. +# +# This file should be version controlled and should not be manually edited. + +version: + revision: "78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9" + channel: "stable" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + - platform: android + create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + - platform: ios + create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + - platform: macos + create_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + base_revision: 78666c8dc57e9f7548ca9f8dd0740fbf0c658dc9 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/purchases_ui_flutter/.pubignore b/purchases_ui_flutter/.pubignore new file mode 100644 index 00000000..ac5aa989 --- /dev/null +++ b/purchases_ui_flutter/.pubignore @@ -0,0 +1,29 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +build/ diff --git a/purchases_ui_flutter/CHANGELOG.md b/purchases_ui_flutter/CHANGELOG.md new file mode 120000 index 00000000..04c99a55 --- /dev/null +++ b/purchases_ui_flutter/CHANGELOG.md @@ -0,0 +1 @@ +../CHANGELOG.md \ No newline at end of file diff --git a/purchases_ui_flutter/LICENSE b/purchases_ui_flutter/LICENSE new file mode 100644 index 00000000..f0a02b1c --- /dev/null +++ b/purchases_ui_flutter/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2019 RevenueCat + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/purchases_ui_flutter/README.md b/purchases_ui_flutter/README.md new file mode 100644 index 00000000..b09f88a3 --- /dev/null +++ b/purchases_ui_flutter/README.md @@ -0,0 +1,31 @@ +

+ RevenueCat +
+ +[![pub package](https://img.shields.io/pub/v/purchases_ui_flutter.svg)](https://pub.dartlang.org/packages/purchases_ui_flutter) + +## purchases_ui_flutter + +*purchases_ui_flutter* allows to use [RevenueCat](https://www.revenuecat.com/)'s paywalls in your Flutter application. Check our [main SDK](https://pub.dev/packages/purchases_flutter) for more information and our [paywalls documentation](https://www.revenuecat.com/docs/paywalls) + +## Installation +To use this plugin, add `purchases_flutter` and `purchases_ui_flutter` as a [dependency in your pubspec.yaml file](https://flutter.io/platform-plugins/). + +Then, you can present your paywall like: +```dart +await RevenueCatUI.presentPaywall(); +``` + +or, if you want to present it conditionally based on a user's entitlements: +```dart +await RevenueCatUI.presentPaywallIfNeeded("requiredEntitlementId"); +``` + +### Requirements +*purchases_ui_flutter* requires XCode 13.3.1+ and minimum targets iOS 11.0+ and Android 24+. Paywalls will only work on iOS 15.0+ and Android 24+. + +## SDK Reference +Our full SDK reference [can be found here](https://pub.dev/documentation/purchases_ui_flutter/latest/). + +## Getting Started +For more detailed information, you can view our complete documentation at [docs.revenuecat.com](https://docs.revenuecat.com/docs/flutter). diff --git a/purchases_ui_flutter/analysis_options.yaml b/purchases_ui_flutter/analysis_options.yaml new file mode 120000 index 00000000..c9e0d9fb --- /dev/null +++ b/purchases_ui_flutter/analysis_options.yaml @@ -0,0 +1 @@ +../analysis_options.yaml \ No newline at end of file diff --git a/purchases_ui_flutter/android/.gitignore b/purchases_ui_flutter/android/.gitignore new file mode 100644 index 00000000..161bdcda --- /dev/null +++ b/purchases_ui_flutter/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/purchases_ui_flutter/android/build.gradle b/purchases_ui_flutter/android/build.gradle new file mode 100644 index 00000000..c3a89818 --- /dev/null +++ b/purchases_ui_flutter/android/build.gradle @@ -0,0 +1,54 @@ +group 'com.revenuecat.purchases_ui_flutter' +version '6.15.0-beta.4' + +buildscript { + ext.kotlin_version = '1.9.20' + ext.common_version = '8.10.0-beta.10' + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.2.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +android { + if (project.android.hasProperty("namespace")) { + namespace 'com.revenuecat.purchases_ui_flutter' + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + kotlinOptions { + jvmTarget = '1.8' + } + + sourceSets { + main.java.srcDirs += 'src/main/kotlin' + } + + defaultConfig { + minSdkVersion 24 + compileSdk 33 + } + + dependencies { + implementation "com.revenuecat.purchases:purchases-hybrid-common-ui:$common_version" + } +} diff --git a/purchases_ui_flutter/android/gradle/wrapper/gradle-wrapper.properties b/purchases_ui_flutter/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 00000000..a3638774 --- /dev/null +++ b/purchases_ui_flutter/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,6 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.2-bin.zip +networkTimeout=10000 +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/purchases_ui_flutter/android/settings.gradle b/purchases_ui_flutter/android/settings.gradle new file mode 100644 index 00000000..cebf2619 --- /dev/null +++ b/purchases_ui_flutter/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'purchases_ui_flutter' diff --git a/purchases_ui_flutter/android/src/main/AndroidManifest.xml b/purchases_ui_flutter/android/src/main/AndroidManifest.xml new file mode 100644 index 00000000..bccb2abd --- /dev/null +++ b/purchases_ui_flutter/android/src/main/AndroidManifest.xml @@ -0,0 +1,3 @@ + + diff --git a/purchases_ui_flutter/android/src/main/kotlin/com/revenuecat/purchases_ui_flutter/PurchasesUiFlutterPlugin.kt b/purchases_ui_flutter/android/src/main/kotlin/com/revenuecat/purchases_ui_flutter/PurchasesUiFlutterPlugin.kt new file mode 100644 index 00000000..8d38d0b0 --- /dev/null +++ b/purchases_ui_flutter/android/src/main/kotlin/com/revenuecat/purchases_ui_flutter/PurchasesUiFlutterPlugin.kt @@ -0,0 +1,96 @@ +package com.revenuecat.purchases_ui_flutter + +import android.app.Activity +import android.util.Log +import com.revenuecat.purchases.PurchasesErrorCode +import com.revenuecat.purchases.hybridcommon.ui.PaywallResultListener +import com.revenuecat.purchases.hybridcommon.ui.presentPaywallFromFragment +import com.revenuecat.purchases.ui.revenuecatui.activity.PaywallResult +import io.flutter.embedding.android.FlutterFragmentActivity +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result + +class PurchasesUiFlutterPlugin: FlutterPlugin, MethodCallHandler, ActivityAware { + private val TAG = "PurchasesUIFlutter" + + private var activity: Activity? = null + + private lateinit var channel : MethodChannel + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "purchases_ui_flutter") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "presentPaywall" -> presentPaywall(result, null) + "presentPaywallIfNeeded" -> { + val requiredEntitlementIdentifier: String? = call.argument("requiredEntitlementIdentifier") + presentPaywall(result, requiredEntitlementIdentifier) + } + else -> { + result.notImplemented() + } + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + } + + override fun onDetachedFromActivityForConfigChanges() { + onDetachedFromActivity() + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + onAttachedToActivity(binding) + } + + override fun onDetachedFromActivity() { + activity = null + } + + private fun presentPaywall(result: Result, requiredEntitlementIdentifier: String?) { + val activity = getActivityFragment() + if (activity != null) { + presentPaywallFromFragment( + activity, + requiredEntitlementIdentifier, + paywallResultListener = object : PaywallResultListener { + override fun onPaywallResult(paywallResult: String) { + result.success(paywallResult) + } + } + ) + } else { + result.error(PurchasesErrorCode.UnknownError.code.toString(), + "Make sure your MainActivity inherits from FlutterFragmentActivity", + null + ) + } + } + + private fun getActivityFragment(): FlutterFragmentActivity? { + val activity: Activity? = this.activity + return if (activity is FlutterFragmentActivity) { + activity + } else { + Log.e( + TAG, + "Paywalls require your activity to subclass FlutterFragmentActivity" + ) + null + } + } + +} diff --git a/purchases_ui_flutter/example/README.md b/purchases_ui_flutter/example/README.md new file mode 100644 index 00000000..1f3c2e21 --- /dev/null +++ b/purchases_ui_flutter/example/README.md @@ -0,0 +1,6 @@ +# Examples +Our examples are located at [revenuecat_examples](https://github.com/RevenueCat/purchases-flutter/tree/main/revenuecat_examples). + +### [Purchase Tester](https://github.com/RevenueCat/purchases-flutter/tree/main/revenuecat_examples/purchase_tester) + +Contains an example app that showcases how to use `purchases_flutter` and `purchases_ui_flutter` to display a paywall and make purchases. diff --git a/purchases_ui_flutter/ios/.gitignore b/purchases_ui_flutter/ios/.gitignore new file mode 100644 index 00000000..0c885071 --- /dev/null +++ b/purchases_ui_flutter/ios/.gitignore @@ -0,0 +1,38 @@ +.idea/ +.vagrant/ +.sconsign.dblite +.svn/ + +.DS_Store +*.swp +profile + +DerivedData/ +build/ +GeneratedPluginRegistrant.h +GeneratedPluginRegistrant.m + +.generated/ + +*.pbxuser +*.mode1v3 +*.mode2v3 +*.perspectivev3 + +!default.pbxuser +!default.mode1v3 +!default.mode2v3 +!default.perspectivev3 + +xcuserdata + +*.moved-aside + +*.pyc +*sync/ +Icon? +.tags* + +/Flutter/Generated.xcconfig +/Flutter/ephemeral/ +/Flutter/flutter_export_environment.sh \ No newline at end of file diff --git a/purchases_ui_flutter/ios/Assets/.gitkeep b/purchases_ui_flutter/ios/Assets/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/purchases_ui_flutter/ios/Classes/PurchasesUiFlutterPlugin.swift b/purchases_ui_flutter/ios/Classes/PurchasesUiFlutterPlugin.swift new file mode 100644 index 00000000..6a7ed4ec --- /dev/null +++ b/purchases_ui_flutter/ios/Classes/PurchasesUiFlutterPlugin.swift @@ -0,0 +1,94 @@ +#if os(macOS) +import FlutterMacOS +#else +import Flutter +#endif +import PurchasesHybridCommon +import Foundation + +public class PurchasesUiFlutterPlugin: NSObject, FlutterPlugin { + + private static let BAD_ARGS_ERROR_CODE = "BAD_ARGS" + + public static func register(with registrar: FlutterPluginRegistrar) { + + #if os(macOS) + let messenger = registrar.messenger + #else + let messenger = registrar.messenger() + #endif + let channel = FlutterMethodChannel(name: "purchases_ui_flutter", binaryMessenger: messenger) + let instance = PurchasesUiFlutterPlugin() + registrar.addMethodCallDelegate(instance, channel: channel) + } + + private var _paywallProxy: Any? + + #if os(iOS) + @available(iOS 15.0, *) + private var paywallProxy: PaywallProxy { + get { + // swiftlint:disable:next force_cast + return self._paywallProxy as! PaywallProxy + } + + set { + self._paywallProxy = newValue + } + } + #endif + + override init() { + #if os(iOS) + if #available(iOS 15.0, *) { + self._paywallProxy = PaywallProxy() + } + #endif + super.init() + } + + public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) { + switch call.method { + case "presentPaywall": + self.presentPaywall(result, requiredEntitlementIdentifier: nil) + case "presentPaywallIfNeeded": + guard let args = call.arguments as? Dictionary else { + result(FlutterError(code: PurchasesUiFlutterPlugin.BAD_ARGS_ERROR_CODE, + message: "Invalid arguments type", + details: nil)) + return + } + guard let requiredEntitlementIdentifier = args["requiredEntitlementIdentifier"] as? String? else { + result(FlutterError(code: PurchasesUiFlutterPlugin.BAD_ARGS_ERROR_CODE, + message: "Missing requiredEntitlementIdentifier in presentPaywallIfNeeded", + details: nil)) + return + } + self.presentPaywall(result, requiredEntitlementIdentifier: requiredEntitlementIdentifier) + default: + result(FlutterMethodNotImplemented) + } + } + + private func presentPaywall(_ result: @escaping FlutterResult, requiredEntitlementIdentifier: String?) { + #if os(iOS) + if #available(iOS 15.0, *) { + if let requiredEntitlementIdentifier { + self.paywallProxy.presentPaywallIfNeeded(requiredEntitlementIdentifier: + requiredEntitlementIdentifier) { paywallResultString in + result(paywallResultString) + } + } else { + self.paywallProxy.presentPaywall { paywallResultString in + result(paywallResultString) + } + } + } else { + NSLog("Presenting paywall requires iOS 15+") + } + #else + NSLog("Presenting paywall requires iOS") + #endif + } + +} diff --git a/purchases_ui_flutter/ios/purchases_ui_flutter.podspec b/purchases_ui_flutter/ios/purchases_ui_flutter.podspec new file mode 100644 index 00000000..749f00b1 --- /dev/null +++ b/purchases_ui_flutter/ios/purchases_ui_flutter.podspec @@ -0,0 +1,24 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint purchases_ui_flutter.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'purchases_ui_flutter' + s.version = '6.15.0-beta.4' + s.summary = 'Flutter plugin that integrates RevenueCat Paywalls' + s.description = <<-DESC +Flutter plugin that integrates RevenueCat Paywalls + DESC + s.homepage = 'http://revenuecat.com' + s.license = { :file => '../LICENSE' } + s.author = { 'RevenueCat' => 'support@revenuecat.com' } + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'Flutter' + s.dependency 'PurchasesHybridCommon', '8.10.0-beta.10' + s.platform = :ios, '11.0' + + # Flutter.framework does not contain a i386 slice. + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'EXCLUDED_ARCHS[sdk=iphonesimulator*]' => 'i386' } + s.swift_version = '5.0' +end diff --git a/purchases_ui_flutter/lib/paywall_result.dart b/purchases_ui_flutter/lib/paywall_result.dart new file mode 100644 index 00000000..614bda39 --- /dev/null +++ b/purchases_ui_flutter/lib/paywall_result.dart @@ -0,0 +1,18 @@ +/// Possible values for the result of a paywall presentation. +enum PaywallResult { + /// Only returned when using [RevenueCatUI.presentPaywallIfNeeded]. Returned + /// if the paywall was not presented. + notPresented, + /// Returned when the paywall was presented and the user cancelled without + /// executing an action. + cancelled, + /// Returned when the paywall was presented and an error occurred performing + /// an operation. + error, + /// Returned when the paywall was presented and the user successfully + /// purchased. + purchased, + /// Returned when the paywall was presented and the user successfully + /// restored. + restored +} diff --git a/purchases_ui_flutter/lib/purchases_ui_flutter.dart b/purchases_ui_flutter/lib/purchases_ui_flutter.dart new file mode 100644 index 00000000..1db93ade --- /dev/null +++ b/purchases_ui_flutter/lib/purchases_ui_flutter.dart @@ -0,0 +1,46 @@ +import 'package:flutter/services.dart'; +import 'paywall_result.dart'; + +export 'paywall_result.dart'; + +class RevenueCatUI { + static const _methodChannel = MethodChannel('purchases_ui_flutter'); + + /// Presents the paywall as an activity on android or a modal in iOS. + /// Returns a [PaywallResult] indicating the result of the paywall presentation. + static Future presentPaywall() async { + final result = await _methodChannel.invokeMethod('presentPaywall'); + return _parseStringToResult(result); + } + + + /// Presents the paywall as an activity on android or a modal in iOS as long + /// as the user does not have the given entitlement identifier active. + /// Returns a [PaywallResult] indicating the result of the paywall presentation. + /// + /// @param [requiredEntitlementIdentifier] Entitlement identifier to check if the user has access to before presenting the paywall. + static Future presentPaywallIfNeeded(String requiredEntitlementIdentifier) async { + final result = await _methodChannel.invokeMethod( + 'presentPaywallIfNeeded', + {'requiredEntitlementIdentifier': requiredEntitlementIdentifier}, + ); + return _parseStringToResult(result); + } + + static PaywallResult _parseStringToResult(String paywallResultString) { + switch (paywallResultString) { + case 'NOT_PRESENTED': + return PaywallResult.notPresented; + case 'CANCELLED': + return PaywallResult.cancelled; + case 'ERROR': + return PaywallResult.error; + case 'PURCHASED': + return PaywallResult.purchased; + case 'RESTORED': + return PaywallResult.restored; + default: + return PaywallResult.error; + } + } +} diff --git a/purchases_ui_flutter/macos/Classes/PurchasesUiFlutterPlugin.swift b/purchases_ui_flutter/macos/Classes/PurchasesUiFlutterPlugin.swift new file mode 120000 index 00000000..a8bf8d22 --- /dev/null +++ b/purchases_ui_flutter/macos/Classes/PurchasesUiFlutterPlugin.swift @@ -0,0 +1 @@ +../../ios/Classes/PurchasesUiFlutterPlugin.swift \ No newline at end of file diff --git a/purchases_ui_flutter/macos/purchases_ui_flutter.podspec b/purchases_ui_flutter/macos/purchases_ui_flutter.podspec new file mode 100644 index 00000000..c042938a --- /dev/null +++ b/purchases_ui_flutter/macos/purchases_ui_flutter.podspec @@ -0,0 +1,24 @@ +# +# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html. +# Run `pod lib lint purchases_ui_flutter.podspec` to validate before publishing. +# +Pod::Spec.new do |s| + s.name = 'purchases_ui_flutter' + s.version = '6.15.0-beta.4' + s.summary = 'Flutter plugin that integrates RevenueCat Paywalls' + s.description = <<-DESC +Flutter plugin that integrates RevenueCat Paywalls + DESC + s.homepage = 'http://revenuecat.com' + s.license = { :file => '../LICENSE' } + s.author = { 'RevenueCat' => 'support@revenuecat.com' } + + s.source = { :path => '.' } + s.source_files = 'Classes/**/*' + s.dependency 'FlutterMacOS' + s.dependency 'PurchasesHybridCommon', '8.10.0-beta.10' + + s.platform = :osx, '10.11' + s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES' } + s.swift_version = '5.0' +end diff --git a/purchases_ui_flutter/pubspec.yaml b/purchases_ui_flutter/pubspec.yaml new file mode 100644 index 00000000..7e0dc172 --- /dev/null +++ b/purchases_ui_flutter/pubspec.yaml @@ -0,0 +1,77 @@ +name: purchases_ui_flutter +description: "Flutter plugin that integrates RevenueCat Paywalls. This plugin supports iOS and Android." +version: 6.15.0-beta.4 +homepage: https://www.revenuecat.com/ +issue_tracker: https://github.com/RevenueCat/purchases-flutter/issues +documentation: https://docs.revenuecat.com/docs + +environment: + # TODO: Check constraints are ok + sdk: '>=3.2.3 <4.0.0' + flutter: '>=3.3.0' + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # This section identifies this Flutter project as a plugin project. + # The 'pluginClass' specifies the class (in Java, Kotlin, Swift, Objective-C, etc.) + # which should be registered in the plugin registry. This is required for + # using method channels. + # The Android 'package' specifies package in which the registered class is. + # This is required for using method channels on Android. + # The 'ffiPlugin' specifies that native code should be built and bundled. + # This is required for using `dart:ffi`. + # All these are used by the tooling to maintain consistency when + # adding or updating assets for this project. + plugin: + platforms: + android: + package: com.revenuecat.purchases_ui_flutter + pluginClass: PurchasesUiFlutterPlugin + ios: + pluginClass: PurchasesUiFlutterPlugin + macos: + pluginClass: PurchasesUiFlutterPlugin + + # To add assets to your plugin package, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + # + # For details regarding assets in packages, see + # https://flutter.dev/assets-and-images/#from-packages + # + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # To add custom fonts to your plugin package, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts in packages, see + # https://flutter.dev/custom-fonts/#from-packages diff --git a/purchases_ui_flutter/test/purchases_ui_flutter_test.dart b/purchases_ui_flutter/test/purchases_ui_flutter_test.dart new file mode 100644 index 00000000..cdd95161 --- /dev/null +++ b/purchases_ui_flutter/test/purchases_ui_flutter_test.dart @@ -0,0 +1,63 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:purchases_ui_flutter/purchases_ui_flutter.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + const channel = MethodChannel('purchases_ui_flutter'); + final log = []; + dynamic response; + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (call) async { + log.add(call); + return response; + }); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, null); + log.clear(); + response = null; + }); + + test('presentPaywall', () async { + response = 'NOT_PRESENTED'; + await RevenueCatUI.presentPaywall(); + expect(log, [ + isMethodCall('presentPaywall', arguments: null), + ]); + }); + + test('presentPaywallIfNeeded', () async { + response = 'NOT_PRESENTED'; + await RevenueCatUI.presentPaywallIfNeeded('entitlement'); + expect(log, [ + isMethodCall( + 'presentPaywallIfNeeded', + arguments: {'requiredEntitlementIdentifier': 'entitlement'}, + ), + ]); + }); + + test('presentPaywall parses response correctly', () async { + response = 'NOT_PRESENTED'; + var paywallResult = await RevenueCatUI.presentPaywall(); + expect(paywallResult, PaywallResult.notPresented); + response = 'CANCELLED'; + paywallResult = await RevenueCatUI.presentPaywall(); + expect(paywallResult, PaywallResult.cancelled); + response = 'ERROR'; + paywallResult = await RevenueCatUI.presentPaywall(); + expect(paywallResult, PaywallResult.error); + response = 'PURCHASED'; + paywallResult = await RevenueCatUI.presentPaywall(); + expect(paywallResult, PaywallResult.purchased); + response = 'RESTORED'; + paywallResult = await RevenueCatUI.presentPaywall(); + expect(paywallResult, PaywallResult.restored); + }); +} diff --git a/revenuecat_examples/purchase_tester/android/app/build.gradle b/revenuecat_examples/purchase_tester/android/app/build.gradle index 51c5ca13..147a2672 100644 --- a/revenuecat_examples/purchase_tester/android/app/build.gradle +++ b/revenuecat_examples/purchase_tester/android/app/build.gradle @@ -36,7 +36,8 @@ android { defaultConfig { applicationId "com.revenuecat.purchases_sample" - minSdkVersion flutter.minSdkVersion + // TODO: change + minSdkVersion 24 targetSdkVersion flutter.targetSdkVersion versionCode flutterVersionCode.toInteger() versionName flutterVersionName diff --git a/revenuecat_examples/purchase_tester/android/app/src/main/AndroidManifest.xml b/revenuecat_examples/purchase_tester/android/app/src/main/AndroidManifest.xml index 68100f52..ff9eb130 100644 --- a/revenuecat_examples/purchase_tester/android/app/src/main/AndroidManifest.xml +++ b/revenuecat_examples/purchase_tester/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ FlutterApplication and put your custom class here. --> _configureSDK() async { } else { configuration = PurchasesConfiguration(StoreConfig.instance.apiKey); } + + configuration.entitlementVerificationMode = EntitlementVerificationMode.informational; await Purchases.configure(configuration); await Purchases.enableAdServicesAttributionTokenCollection(); diff --git a/revenuecat_examples/purchase_tester/lib/src/app.dart b/revenuecat_examples/purchase_tester/lib/src/app.dart index 8ee2657a..d35ed2c6 100644 --- a/revenuecat_examples/purchase_tester/lib/src/app.dart +++ b/revenuecat_examples/purchase_tester/lib/src/app.dart @@ -1,8 +1,10 @@ import 'dart:async'; +import 'dart:developer'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:purchases_flutter/purchases_flutter.dart'; +import 'package:purchases_ui_flutter/purchases_ui_flutter.dart'; import './constant.dart'; @@ -144,6 +146,14 @@ class _UpsellScreenState extends State { .expand((i) => i) .toList(); + buttonThings.add(ElevatedButton( + onPressed: () async { + final paywallResult = await RevenueCatUI.presentPaywall(); + log('Paywall result: $paywallResult'); + }, + child: const Text('Present paywall'), + )); + return Scaffold( appBar: AppBar(title: const Text('Upsell Screen')), body: Center( diff --git a/revenuecat_examples/purchase_tester/macos/Flutter/GeneratedPluginRegistrant.swift b/revenuecat_examples/purchase_tester/macos/Flutter/GeneratedPluginRegistrant.swift index 1ac0400e..b4e78a3e 100644 --- a/revenuecat_examples/purchase_tester/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/revenuecat_examples/purchase_tester/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,9 @@ import FlutterMacOS import Foundation import purchases_flutter +import purchases_ui_flutter func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PurchasesFlutterPlugin.register(with: registry.registrar(forPlugin: "PurchasesFlutterPlugin")) + PurchasesUiFlutterPlugin.register(with: registry.registrar(forPlugin: "PurchasesUiFlutterPlugin")) } diff --git a/revenuecat_examples/purchase_tester/macos/Runner.xcodeproj/project.pbxproj b/revenuecat_examples/purchase_tester/macos/Runner.xcodeproj/project.pbxproj index f6c0f509..292ecfba 100644 --- a/revenuecat_examples/purchase_tester/macos/Runner.xcodeproj/project.pbxproj +++ b/revenuecat_examples/purchase_tester/macos/Runner.xcodeproj/project.pbxproj @@ -202,7 +202,7 @@ isa = PBXProject; attributes = { LastSwiftUpdateCheck = 0920; - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1430; ORGANIZATIONNAME = "The Flutter Authors"; TargetAttributes = { 33CC10EC2044A3C60003C045 = { @@ -322,11 +322,15 @@ "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh", "${BUILT_PRODUCTS_DIR}/PurchasesHybridCommon/PurchasesHybridCommon.framework", "${BUILT_PRODUCTS_DIR}/RevenueCat/RevenueCat.framework", + "${BUILT_PRODUCTS_DIR}/RevenueCatUI/RevenueCatUI.framework", + "${BUILT_PRODUCTS_DIR}/purchases_ui_flutter/purchases_ui_flutter.framework", ); name = "[CP] Embed Pods Frameworks"; outputPaths = ( "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/PurchasesHybridCommon.framework", "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RevenueCat.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/RevenueCatUI.framework", + "${TARGET_BUILD_DIR}/${FRAMEWORKS_FOLDER_PATH}/purchases_ui_flutter.framework", ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; diff --git a/revenuecat_examples/purchase_tester/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/revenuecat_examples/purchase_tester/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index d758a322..c42d3bdb 100644 --- a/revenuecat_examples/purchase_tester/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/revenuecat_examples/purchase_tester/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ generateEntitlementInfoJSON(String store) => { @@ -21,6 +22,7 @@ void main() { 'billingIssueDetectedAt': null, 'billingIssueDetectedAtMillis': null, 'store': store, + 'verification': 'VERIFIED', }; test('unknown period if missing from json', () { @@ -41,6 +43,7 @@ void main() { 'unsubscribeDetectedAtMillis': null, 'billingIssueDetectedAt': null, 'billingIssueDetectedAtMillis': null, + 'verification': 'VERIFIED', }; final entitlementInfo = EntitlementInfo.fromJson(entitlementInfoJson); @@ -65,12 +68,37 @@ void main() { 'unsubscribeDetectedAtMillis': null, 'billingIssueDetectedAt': null, 'billingIssueDetectedAtMillis': null, + 'verification': 'VERIFIED', }; final entitlementInfo = EntitlementInfo.fromJson(entitlementInfoJson); expect(entitlementInfo.store, Store.unknownStore); }); + test('not requested verification result if missing from json', () { + final entitlementInfoJson = { + 'identifier': 'almost_pro', + 'isActive': true, + 'willRenew': true, + 'periodType': 'NORMAL', + 'latestPurchaseDateMillis': 1.58759855E9, + 'latestPurchaseDate': '2020-04-22T23:35:50.000Z', + 'originalPurchaseDateMillis': 1.591725245E9, + 'originalPurchaseDate': '2020-06-09T17:54:05.000Z', + 'expirationDateMillis': null, + 'expirationDate': null, + 'productIdentifier': 'consumable', + 'isSandbox': true, + 'unsubscribeDetectedAt': null, + 'unsubscribeDetectedAtMillis': null, + 'billingIssueDetectedAt': null, + 'billingIssueDetectedAtMillis': null, + }; + final entitlementInfo = EntitlementInfo.fromJson(entitlementInfoJson); + + expect(entitlementInfo.verification, VerificationResult.notRequested); + }); + test('app store gets parsed if present in json', () { final entitlementInfoJson = generateEntitlementInfoJSON('APP_STORE'); final entitlementInfo = EntitlementInfo.fromJson(entitlementInfoJson); @@ -125,6 +153,7 @@ void main() { 'unsubscribeDetectedAtMillis': null, 'billingIssueDetectedAt': null, 'billingIssueDetectedAtMillis': null, + 'verification': 'VERIFIED', }; final entitlementInfo = EntitlementInfo.fromJson(entitlementInfoJson); @@ -149,6 +178,7 @@ void main() { 'unsubscribeDetectedAtMillis': null, 'billingIssueDetectedAt': null, 'billingIssueDetectedAtMillis': null, + 'verification': 'VERIFIED', }; entitlementInfoJson['ownershipType'] = 'PURCHASED'; var entitlementInfo = EntitlementInfo.fromJson(entitlementInfoJson); diff --git a/test/purchases_flutter_test.dart b/test/purchases_flutter_test.dart index f2f987b4..0c0d9b36 100644 --- a/test/purchases_flutter_test.dart +++ b/test/purchases_flutter_test.dart @@ -30,7 +30,7 @@ void main() { final randomGenerator = Random(DateTime.now().microsecondsSinceEpoch); final mockCustomerInfoResponse = { 'originalAppUserId': 'pepe', - 'entitlements': {'all': {}, 'active': {}}, + 'entitlements': {'all': {}, 'active': {}, 'verification': 'NOT_REQUESTED'}, 'activeSubscriptions': [], 'latestExpirationDate': '2021-04-09T14:48:00.000Z', 'allExpirationDates': {}, @@ -76,6 +76,7 @@ void main() { 'useAmazon': false, 'usesStoreKit2IfAvailable': false, 'shouldShowInAppMessagesAutomatically': true, + 'entitlementVerificationMode': 'DISABLED', }, ), ], @@ -958,6 +959,7 @@ void main() { 'useAmazon': true, 'usesStoreKit2IfAvailable': false, 'shouldShowInAppMessagesAutomatically': true, + 'entitlementVerificationMode': 'DISABLED', }, ), ], @@ -983,6 +985,7 @@ void main() { 'useAmazon': true, 'usesStoreKit2IfAvailable': false, 'shouldShowInAppMessagesAutomatically': true, + 'entitlementVerificationMode': 'DISABLED', }, ), ], @@ -1008,6 +1011,7 @@ void main() { 'useAmazon': false, 'usesStoreKit2IfAvailable': false, 'shouldShowInAppMessagesAutomatically': true, + 'entitlementVerificationMode': 'DISABLED', }, ), ], @@ -1034,6 +1038,7 @@ void main() { 'useAmazon': true, 'usesStoreKit2IfAvailable': false, 'shouldShowInAppMessagesAutomatically': true, + 'entitlementVerificationMode': 'DISABLED', }, ), ],