diff --git a/CHANGELOG.md b/CHANGELOG.md index 196cc9b9..258fc111 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,10 @@ a beta or production release, they must be documented here). ## [Unreleased] +### Added +- Creation of the structure for implementing paid decks. +- Now the deck can be paid for, just add the `productInfo` and also if it is `isPremium`. + ### Added - Visually Opinionated Buttons (Primary, Secondary and Text). diff --git a/README.md b/README.md index 0e56eca7..34439425 100644 --- a/README.md +++ b/README.md @@ -74,3 +74,7 @@ This project was built with the help of the sponsors below: - [Maratona Discover](https://bit.ly/lucas-montano-maratonadiscover): Discover is a free way of learning how to code. - [Startup Life Podcast](https://bit.ly/lucas-montano-startup-life): Your tech, business, and innovation Podcast. - [Pingback](https://bit.ly/lucas-montano-pingback): Total freedom to create content. + +## Revenue + +We are currently exploring new approaches to generate revenue with the app. The implementation of the paid collection feature can be found in the code, along with an example of a paid collection. Other collections will not be visible on GitHub to ensure we can properly validate this revenue model. diff --git a/README_ptbr.md b/README_ptbr.md index f4bd1ca1..64cbddde 100644 --- a/README_ptbr.md +++ b/README_ptbr.md @@ -74,3 +74,7 @@ Este projeto foi construído com a ajuda dos patrocinadores abaixo: - [Maratona Discover](https://bit.ly/lucas-montano-maratonadiscover): Aprenda programação na prática. E de graça. - [Startup Life Podcast](https://bit.ly/lucas-montano-startup-life): O seu podcast sobre negócios, tecnologia e inovação. - [Pingback](https://bit.ly/lucas-montano-pingback): Crie conteúdo com total liberdade. + +## Receita + +No momento, estamos explorando novas abordagens para gerar receita com o aplicativo. A implementação do recurso de deck pago pode ser encontrada no código, junto com um exemplo de deck pago. Outros decks não estarão visíveis no GitHub para garantir que possamos validar adequadamente este modelo de receita. diff --git a/android/app/build.gradle b/android/app/build.gradle index f965613f..cda1c8cc 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -31,7 +31,7 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 31 + compileSdkVersion 34 sourceSets { main.java.srcDirs += 'src/main/kotlin' @@ -40,7 +40,7 @@ android { defaultConfig { applicationId "com.olmps.memoClient" minSdkVersion 21 - targetSdkVersion 31 + targetSdkVersion 34 versionCode 7 versionName flutterVersionName } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 5bf788fc..99e00323 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,7 @@ android:icon="@mipmap/ic_launcher"> 8.11.0) - - Firebase/CoreOnly (8.11.0): - - FirebaseCore (= 8.11.0) - - Firebase/Crashlytics (8.11.0): + - FirebaseAnalytics/WithoutAdIdSupport (~> 10.25.0) + - Firebase/CoreOnly (10.25.0): + - FirebaseCore (= 10.25.0) + - Firebase/Crashlytics (10.25.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 8.11.0) - - firebase_analytics (9.1.2): - - Firebase/AnalyticsWithoutAdIdSupport (= 8.11.0) + - FirebaseCrashlytics (~> 10.25.0) + - firebase_analytics (10.10.7): + - Firebase/AnalyticsWithoutAdIdSupport (= 10.25.0) - firebase_core - Flutter - - firebase_core (1.13.1): - - Firebase/CoreOnly (= 8.11.0) + - firebase_core (2.32.0): + - Firebase/CoreOnly (= 10.25.0) - Flutter - - firebase_crashlytics (2.5.3): - - Firebase/Crashlytics (= 8.11.0) + - firebase_crashlytics (3.5.7): + - Firebase/Crashlytics (= 10.25.0) - firebase_core - Flutter - - FirebaseAnalytics/WithoutAdIdSupport (8.11.0): - - FirebaseCore (~> 8.0) - - FirebaseInstallations (~> 8.0) - - GoogleAppMeasurement/WithoutAdIdSupport (= 8.11.0) - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - FirebaseCore (8.11.0): - - FirebaseCoreDiagnostics (~> 8.0) - - GoogleUtilities/Environment (~> 7.7) - - GoogleUtilities/Logger (~> 7.7) - - FirebaseCoreDiagnostics (8.13.0): - - GoogleDataTransport (~> 9.1) - - GoogleUtilities/Environment (~> 7.7) - - GoogleUtilities/Logger (~> 7.7) - - nanopb (~> 2.30908.0) - - FirebaseCrashlytics (8.11.0): - - FirebaseCore (~> 8.0) - - FirebaseInstallations (~> 8.0) - - GoogleDataTransport (~> 9.1) - - GoogleUtilities/Environment (~> 7.7) - - nanopb (~> 2.30908.0) - - PromisesObjC (< 3.0, >= 1.2) - - FirebaseInstallations (8.13.0): - - FirebaseCore (~> 8.0) - - GoogleUtilities/Environment (~> 7.7) - - GoogleUtilities/UserDefaults (~> 7.7) - - PromisesObjC (< 3.0, >= 1.2) + - FirebaseAnalytics/WithoutAdIdSupport (10.25.0): + - FirebaseCore (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.25.0) + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30911.0, >= 2.30908.0) + - FirebaseCore (10.25.0): + - FirebaseCoreInternal (~> 10.0) + - GoogleUtilities/Environment (~> 7.12) + - GoogleUtilities/Logger (~> 7.12) + - FirebaseCoreExtension (10.29.0): + - FirebaseCore (~> 10.0) + - FirebaseCoreInternal (10.29.0): + - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseCrashlytics (10.25.0): + - FirebaseCore (~> 10.5) + - FirebaseInstallations (~> 10.0) + - FirebaseRemoteConfigInterop (~> 10.23) + - FirebaseSessions (~> 10.5) + - GoogleDataTransport (~> 9.2) + - GoogleUtilities/Environment (~> 7.8) + - nanopb (< 2.30911.0, >= 2.30908.0) + - PromisesObjC (~> 2.1) + - FirebaseInstallations (10.29.0): + - FirebaseCore (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/UserDefaults (~> 7.8) + - PromisesObjC (~> 2.1) + - FirebaseRemoteConfigInterop (10.29.0) + - FirebaseSessions (10.29.0): + - FirebaseCore (~> 10.5) + - FirebaseCoreExtension (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleDataTransport (~> 9.2) + - GoogleUtilities/Environment (~> 7.13) + - GoogleUtilities/UserDefaults (~> 7.13) + - nanopb (< 2.30911.0, >= 2.30908.0) + - PromisesSwift (~> 2.1) - Flutter (1.0.0) - - flutter_inappwebview (0.0.1): - - Flutter - - flutter_inappwebview/Core (= 0.0.1) - - OrderedSet (~> 5.0) - - flutter_inappwebview/Core (0.0.1): - - Flutter - - OrderedSet (~> 5.0) - flutter_keyboard_visibility (0.0.1): - Flutter - - gallery_saver (0.0.1): + - flutter_native_splash (0.0.1): - Flutter - - GoogleAppMeasurement/WithoutAdIdSupport (8.11.0): - - GoogleUtilities/AppDelegateSwizzler (~> 7.7) - - GoogleUtilities/MethodSwizzler (~> 7.7) - - GoogleUtilities/Network (~> 7.7) - - "GoogleUtilities/NSData+zlib (~> 7.7)" - - nanopb (~> 2.30908.0) - - GoogleDataTransport (9.1.2): - - GoogleUtilities/Environment (~> 7.2) - - nanopb (~> 2.30908.0) + - GoogleAppMeasurement/WithoutAdIdSupport (10.25.0): + - GoogleUtilities/AppDelegateSwizzler (~> 7.11) + - GoogleUtilities/MethodSwizzler (~> 7.11) + - GoogleUtilities/Network (~> 7.11) + - "GoogleUtilities/NSData+zlib (~> 7.11)" + - nanopb (< 2.30911.0, >= 2.30908.0) + - GoogleDataTransport (9.4.1): + - GoogleUtilities/Environment (~> 7.7) + - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.7.0): + - GoogleUtilities/AppDelegateSwizzler (7.13.3): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.7.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Environment (7.13.3): + - GoogleUtilities/Privacy - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/Logger (7.7.0): + - GoogleUtilities/Logger (7.13.3): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.7.0): + - GoogleUtilities/Privacy + - GoogleUtilities/MethodSwizzler (7.13.3): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.7.0): + - GoogleUtilities/Privacy + - GoogleUtilities/Network (7.13.3): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" + - GoogleUtilities/Privacy - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.7.0)" - - GoogleUtilities/Reachability (7.7.0): + - "GoogleUtilities/NSData+zlib (7.13.3)": + - GoogleUtilities/Privacy + - GoogleUtilities/Privacy (7.13.3) + - GoogleUtilities/Reachability (7.13.3): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.7.0): + - GoogleUtilities/Privacy + - GoogleUtilities/UserDefaults (7.13.3): - GoogleUtilities/Logger - - image_picker (0.0.1): - - Flutter - - nanopb (2.30908.0): - - nanopb/decode (= 2.30908.0) - - nanopb/encode (= 2.30908.0) - - nanopb/decode (2.30908.0) - - nanopb/encode (2.30908.0) - - OrderedSet (5.0.0) + - GoogleUtilities/Privacy + - nanopb (2.30910.0): + - nanopb/decode (= 2.30910.0) + - nanopb/encode (= 2.30910.0) + - nanopb/decode (2.30910.0) + - nanopb/encode (2.30910.0) - package_info_plus (0.4.5): - Flutter - path_provider_ios (0.0.1): - Flutter - - PromisesObjC (2.0.0) - - url_launcher_ios (0.0.1): + - PromisesObjC (2.4.0) + - PromisesSwift (2.4.0): + - PromisesObjC (= 2.4.0) + - purchases_flutter (6.30.2): - Flutter - - video_player_avfoundation (0.0.1): + - PurchasesHybridCommon (= 11.1.0) + - PurchasesHybridCommon (11.1.0): + - RevenueCat (= 4.43.2) + - RevenueCat (4.43.2) + - url_launcher_ios (0.0.1): - Flutter DEPENDENCIES: @@ -115,29 +131,32 @@ DEPENDENCIES: - firebase_core (from `.symlinks/plugins/firebase_core/ios`) - firebase_crashlytics (from `.symlinks/plugins/firebase_crashlytics/ios`) - Flutter (from `Flutter`) - - flutter_inappwebview (from `.symlinks/plugins/flutter_inappwebview/ios`) - flutter_keyboard_visibility (from `.symlinks/plugins/flutter_keyboard_visibility/ios`) - - gallery_saver (from `.symlinks/plugins/gallery_saver/ios`) - - image_picker (from `.symlinks/plugins/image_picker/ios`) + - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - path_provider_ios (from `.symlinks/plugins/path_provider_ios/ios`) + - purchases_flutter (from `.symlinks/plugins/purchases_flutter/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) - - video_player_avfoundation (from `.symlinks/plugins/video_player_avfoundation/ios`) SPEC REPOS: trunk: - Firebase - FirebaseAnalytics - FirebaseCore - - FirebaseCoreDiagnostics + - FirebaseCoreExtension + - FirebaseCoreInternal - FirebaseCrashlytics - FirebaseInstallations + - FirebaseRemoteConfigInterop + - FirebaseSessions - GoogleAppMeasurement - GoogleDataTransport - GoogleUtilities - nanopb - - OrderedSet - PromisesObjC + - PromisesSwift + - PurchasesHybridCommon + - RevenueCat EXTERNAL SOURCES: device_info_plus: @@ -150,50 +169,49 @@ EXTERNAL SOURCES: :path: ".symlinks/plugins/firebase_crashlytics/ios" Flutter: :path: Flutter - flutter_inappwebview: - :path: ".symlinks/plugins/flutter_inappwebview/ios" flutter_keyboard_visibility: :path: ".symlinks/plugins/flutter_keyboard_visibility/ios" - gallery_saver: - :path: ".symlinks/plugins/gallery_saver/ios" - image_picker: - :path: ".symlinks/plugins/image_picker/ios" + flutter_native_splash: + :path: ".symlinks/plugins/flutter_native_splash/ios" package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_ios: :path: ".symlinks/plugins/path_provider_ios/ios" + purchases_flutter: + :path: ".symlinks/plugins/purchases_flutter/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" - video_player_avfoundation: - :path: ".symlinks/plugins/video_player_avfoundation/ios" SPEC CHECKSUMS: - device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed - Firebase: 44dd9724c84df18b486639e874f31436eaa9a20c - firebase_analytics: 816a4f02c5a0dbd67c5ee281944aa321b4445816 - firebase_core: 08f6a85f62060111de5e98d6a214810d11365de9 - firebase_crashlytics: 28149c943342a73fefe1afef224d345cfb35e1ee - FirebaseAnalytics: 4e4b13031034e6561ed3bd1d47b6fdabbd6487c6 - FirebaseCore: 2f4f85b453cc8fea4bb2b37e370007d2bcafe3f0 - FirebaseCoreDiagnostics: c2836d254a8f0bbb4121ff18f2c2ea39d118fd08 - FirebaseCrashlytics: 62268addefae79601057818156e8bc69d71fee41 - FirebaseInstallations: 60edbf7e11d91ae4c751d26c200dfd037099abe0 - Flutter: 50d75fe2f02b26cc09d224853bb45737f8b3214a - flutter_inappwebview: bfd58618f49dc62f2676de690fc6dcda1d6c3721 + device_info_plus: 97af1d7e84681a90d0693e63169a5d50e0839a0d + Firebase: 0312a2352584f782ea56f66d91606891d4607f06 + firebase_analytics: 1e318eb0b35f3fb5b203766a75add0b0b90d03ff + firebase_core: a626d00494efa398e7c54f25f1454a64c8abf197 + firebase_crashlytics: 17e856fabec68d993662abaf2f6fe2413f0abece + FirebaseAnalytics: ec00fe8b93b41dc6fe4a28784b8e51da0647a248 + FirebaseCore: 7ec4d0484817f12c3373955bc87762d96842d483 + FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f + FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 + FirebaseCrashlytics: 4b96efb0ce73b38b2a85e8b8bd1bd8f63f09d015 + FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd + FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d + FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc + Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 - gallery_saver: 9fc173c9f4fcc48af53b2a9ebea1b643255be542 - GoogleAppMeasurement: aa3cb422fab2b05d2efac543a5720d1a85b9dea5 - GoogleDataTransport: 629c20a4d363167143f30ea78320d5a7eb8bd940 - GoogleUtilities: e0913149f6b0625b553d70dae12b49fc62914fd1 - image_picker: 541dcbb3b9cf32d87eacbd957845d8651d6c62c3 - nanopb: a0ba3315591a9ae0a16a309ee504766e90db0c96 - OrderedSet: aaeb196f7fef5a9edf55d89760da9176ad40b93c - package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e + flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 + GoogleAppMeasurement: 9abf64b682732fed36da827aa2a68f0221fd2356 + GoogleDataTransport: 6c09b596d841063d76d4288cc2d2f42cc36e1e2a + GoogleUtilities: ea963c370a38a8069cc5f7ba4ca849a60b6d7d15 + nanopb: 438bc412db1928dac798aa6fd75726007be04262 + package_info_plus: 58f0028419748fad15bf008b270aaa8e54380b1c path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 - PromisesObjC: 68159ce6952d93e17b2dfe273b8c40907db5ba58 - url_launcher_ios: 839c58cdb4279282219f5e248c3321761ff3c4de - video_player_avfoundation: e489aac24ef5cf7af82702979ed16f2a5ef84cff + PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 + PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 + purchases_flutter: 42d5544e7730ea89a88cc2f008b7c700fd147052 + PurchasesHybridCommon: 4022d5944cb30ec44ba5159e42aa161fe0e30175 + RevenueCat: 3d934653b7e8b09af88fd47e9e84cfaf5d0a89ba + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe -PODFILE CHECKSUM: 93c13a6b510094ca189e0993ce1f80877faea644 +PODFILE CHECKSUM: 80a6f1651e14576cbf8f071404d32bc9269538e5 -COCOAPODS: 1.11.2 +COCOAPODS: 1.15.2 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index e061390a..d206708f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -3,7 +3,7 @@ archiveVersion = 1; classes = { }; - objectVersion = 51; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ @@ -165,7 +165,7 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; + LastUpgradeCheck = 1510; ORGANIZATIONNAME = ""; TargetAttributes = { 97C146ED1CF9000F007C117D = { @@ -212,10 +212,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -266,6 +268,7 @@ }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -379,7 +382,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -461,7 +464,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -512,7 +515,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 12.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; diff --git a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index c87d15a3..5e31d3d3 100644 --- a/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/lib/application/app.dart b/lib/application/app.dart index 2a56db4e..bdaada7c 100644 --- a/lib/application/app.dart +++ b/lib/application/app.dart @@ -38,6 +38,7 @@ class AppRoot extends StatelessWidget { executionServices.overrideWithValue(state.executionServices), progressServices.overrideWithValue(state.progressServices), resourceServices.overrideWithValue(state.resourceServices), + purchaseServices.overrideWithValue(state.collectionPurchaseServices) ], child: _LoadedAppRoot(), ), diff --git a/lib/application/constants/dimensions.dart b/lib/application/constants/dimensions.dart index c1f2606c..b60158db 100644 --- a/lib/application/constants/dimensions.dart +++ b/lib/application/constants/dimensions.dart @@ -11,6 +11,8 @@ const double genericBorderHeight = 2; const textTagBorderRadius = BorderRadius.all(Radius.circular(2)); const double cardBorderWidth = 4; +const double collectionsBlurSize = 5; + const double resourceLinkEmojiTextSize = 20; const double iconSize = 24; @@ -22,6 +24,7 @@ const double smallIconSize = 16; // const double collectionsLinearProgressLineWidth = 12; const double collectionsEmptyStateSize = 96; +const double collectionActionBarMaxHeight = 56; // // ProgressPage diff --git a/lib/application/constants/exception_strings.dart b/lib/application/constants/exception_strings.dart index fff6844b..477be320 100644 --- a/lib/application/constants/exception_strings.dart +++ b/lib/application/constants/exception_strings.dart @@ -14,6 +14,9 @@ String descriptionForException(BaseException exception) { case ExceptionType.failedToOpenUrl: return 'Algo deu errado ao tentar abrir o link!'; + + case ExceptionType.failedToPurchase: + return 'Algo deu errado ao tentar realizar a compra!'; default: return 'Algo deu errado. Por favor tente novamente'; } diff --git a/lib/application/constants/strings.dart b/lib/application/constants/strings.dart index ceb78d6b..9920d27e 100644 --- a/lib/application/constants/strings.dart +++ b/lib/application/constants/strings.dart @@ -30,6 +30,10 @@ const collectionsSectionHeaderSeeAll = 'Ver todos'; const collectionsStartNow = 'Começar Agora'; +const collectionPurchase = 'Deseja comprar este Deck?'; +const collectionSuccessPurchase = 'Deck comprado com sucesso!'; +String collectionPurchaseDeck(double price) => 'Comprar deck - R\$ ${price.toStringAsFixed(2)}'; + String collectionsEmptyTitleSegment(CollectionsSegment segment) { switch (segment) { case CollectionsSegment.explore: @@ -94,6 +98,7 @@ const jumpTo = 'Pular para'; // Tags Component // const tags = 'Tags'; +const premium = 'Premium'; const tagsHint = 'Adicione as tags...'; const suggestions = 'Sugestões'; const addTags = 'Adicionar Tags'; @@ -186,6 +191,7 @@ const memos = 'Memos'; const next = 'Próximo'; const cancel = 'Cancelar'; const remove = 'Remover'; +const purchase = 'Comprar'; const recallLevel = 'Nível de Fixação'; diff --git a/lib/application/pages/details/collection_details_page.dart b/lib/application/pages/details/collection_details_page.dart index 03ee7516..01a1b578 100644 --- a/lib/application/pages/details/collection_details_page.dart +++ b/lib/application/pages/details/collection_details_page.dart @@ -1,11 +1,15 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:layoutr/common_layout.dart'; +import 'package:memo/application/constants/dimensions.dart' as dimensions; import 'package:memo/application/constants/strings.dart' as strings; import 'package:memo/application/coordinator/routes_coordinator.dart'; +import 'package:memo/application/pages/details/collection_purchase_vm.dart'; import 'package:memo/application/pages/details/contributor_view.dart'; import 'package:memo/application/pages/details/details_providers.dart'; import 'package:memo/application/theme/theme_controller.dart'; +import 'package:memo/application/utils/bottom_sheet.dart'; +import 'package:memo/application/utils/scaffold_messenger.dart'; import 'package:memo/application/view-models/details/collection_details_vm.dart'; import 'package:memo/application/widgets/theme/custom_button.dart'; import 'package:memo/application/widgets/theme/item_collection_card.dart'; @@ -18,6 +22,28 @@ class CollectionDetailsPage extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final memoTheme = ref.watch(themeController); final state = watchCollectionDetailsState(ref); + final id = ref.read(detailsCollectionId); + + ref.listen(collectionPurchaseVM(id), (_, state) { + if (state is PurchaseInfoLoadingFailed) { + showExceptionSnackBar(ref, state.exception); + } + + if (state is ProcessingPurchase) { + Navigator.of(context).pop(); + } + + if (state is PurchaseFailed) { + showExceptionSnackBar(ref, state.exception); + } + + if (state is PurchaseSuccess) { + showSnackBar( + ref, + const SnackBar(content: Text(strings.collectionSuccessPurchase)), + ); + } + }); if (state is LoadedCollectionDetailsState) { final sections = []; @@ -66,7 +92,7 @@ class CollectionDetailsPage extends ConsumerWidget { context.verticalBox(Spacing.small), Text( strings.detailsResourcesWarning, - style: Theme.of(context).textTheme.caption?.copyWith(color: memoTheme.neutralSwatch.shade300), + style: Theme.of(context).textTheme.bodySmall?.copyWith(color: memoTheme.neutralSwatch.shade300), ), context.verticalBox(Spacing.small), ResourcesList( @@ -81,16 +107,18 @@ class CollectionDetailsPage extends ConsumerWidget { sections.add(resourcesSection); final fixedBottomAction = ThemedBottomContainer( - child: Container( + child: ColoredBox( color: memoTheme.neutralSwatch.shade800, child: SafeArea( - child: PrimaryElevatedButton( - onPressed: () { - final id = ref.read(detailsCollectionId); - readCoordinator(ref).navigateToCollectionExecution(id, isNestedNavigation: false); - }, - text: strings.detailsStudyNow.toUpperCase(), - ).withSymmetricalPadding(context, vertical: Spacing.small, horizontal: Spacing.medium), + child: ConstrainedBox( + constraints: BoxConstraints.tight( + const Size.fromHeight(dimensions.collectionActionBarMaxHeight), + ), + child: _CollectionAction(id: id, isPremium: state.metadata.isPremium, price: state.metadata.price)), + ).withSymmetricalPadding( + context, + vertical: Spacing.small, + horizontal: Spacing.medium, ), ), ); @@ -128,6 +156,76 @@ class CollectionDetailsPage extends ConsumerWidget { Widget _buildSectionTitle(BuildContext context, WidgetRef ref, String text) => Text( text, style: - Theme.of(context).textTheme.subtitle1?.copyWith(color: ref.watch(themeController).neutralSwatch.shade300), + Theme.of(context).textTheme.titleMedium?.copyWith(color: ref.watch(themeController).neutralSwatch.shade300), + ); +} + +class _CollectionAction extends ConsumerWidget { + const _CollectionAction({ + required this.id, + required this.isPremium, + this.price, + }); + + final String id; + final bool isPremium; + final double? price; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final memoTheme = ref.watch(themeController); + final collectionExecutionAction = PrimaryElevatedButton( + onPressed: () => readCoordinator(ref).navigateToCollectionExecution(id, isNestedNavigation: false), + text: strings.detailsStudyNow.toUpperCase(), + ); + + if (!isPremium) { + return collectionExecutionAction; + } + + final purchaseState = ref.watch(collectionPurchaseVM(id)); + + if (purchaseState is PurchaseInfoLoading || purchaseState is ProcessingPurchase) { + return const Center(child: CircularProgressIndicator()); + } + + // TODO(lucasbiancogs): Not the best approach to null-check the price here, + // should be revisited in the future along with the other purchase implementations. + Widget collectionPurchaseAction(VoidCallback? onPressed) => SecondaryElevatedButton( + backgroundColor: memoTheme.secondarySwatch, + text: strings.collectionPurchaseDeck(price!), + onPressed: onPressed, + ); + + if (purchaseState is PurchaseInfoLoadingFailed) { + return collectionPurchaseAction(null); + } + + final currentState = purchaseState as PurchaseInfoLoaded; + + return currentState.isPurchased + ? collectionExecutionAction + : collectionPurchaseAction( + () async => _collectionPurchaseBottomSheet( + context, + ref.read(collectionPurchaseVM(id).notifier).purchase, + ), + ); + } + + /// This Modal Bottom Sheet representing the option to purchase a specific `Collection`. + Future _collectionPurchaseBottomSheet(BuildContext context, VoidCallback? onPressed) => + showSnappableDraggableModalBottomSheet( + context, + title: strings.collectionPurchase, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + context.verticalBox(Spacing.xLarge), + PrimaryElevatedButton(text: strings.purchase, onPressed: onPressed), + context.verticalBox(Spacing.medium), + SecondaryElevatedButton(text: strings.cancel, onPressed: Navigator.of(context).pop), + ], + ).withAllPadding(context, Spacing.medium), ); } diff --git a/lib/application/pages/details/collection_purchase_vm.dart b/lib/application/pages/details/collection_purchase_vm.dart new file mode 100644 index 00000000..b7ea9ffc --- /dev/null +++ b/lib/application/pages/details/collection_purchase_vm.dart @@ -0,0 +1,96 @@ +import 'package:equatable/equatable.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:memo/application/view-models/app_vm.dart'; +import 'package:memo/core/faults/exceptions/base_exception.dart'; +import 'package:memo/domain/services/collection_purchase_services.dart'; + +final collectionPurchaseVM = StateNotifierProvider.family.autoDispose( + (ref, collectionId) => CollectionPurchaseVMImpl( + collectionId: collectionId, + collectionPurchaseServices: ref.read(purchaseServices), + ), +); + +abstract class CollectionPurchaseVM extends StateNotifier { + CollectionPurchaseVM(super._state); + + /// Purchases a collection. + Future purchase(); +} + +class CollectionPurchaseVMImpl extends CollectionPurchaseVM { + CollectionPurchaseVMImpl({ + required this.collectionId, + required this.collectionPurchaseServices, + }) : super(PurchaseInfoLoading()) { + _loadDependencies(); + } + + final String collectionId; + final CollectionPurchaseServices collectionPurchaseServices; + + Future _loadDependencies() async { + state = PurchaseInfoLoading(); + + try { + final isPurchased = await collectionPurchaseServices.isPurchased(id: collectionId); + state = PurchaseInfoLoaded(isPurchased: isPurchased); + } on BaseException catch (exception) { + state = PurchaseInfoLoadingFailed(exception); + } + } + + @override + Future purchase() async { + state = ProcessingPurchase(); + + try { + await collectionPurchaseServices.purchaseCollection(id: collectionId); + state = PurchaseSuccess(); + } on BaseException catch (exception) { + state = PurchaseFailed(exception); + } + } +} + +abstract class PurchaseState extends Equatable { + @override + List get props => []; +} + +class PurchaseInfoLoading extends PurchaseState {} + +class PurchaseInfoLoadingFailed extends PurchaseState { + PurchaseInfoLoadingFailed(this.exception); + + final BaseException exception; + + @override + List get props => [exception]; +} + +class PurchaseInfoLoaded extends PurchaseState { + PurchaseInfoLoaded({required this.isPurchased}); + + final bool isPurchased; + + @override + List get props => [isPurchased]; +} + +class ProcessingPurchase extends PurchaseInfoLoaded { + ProcessingPurchase() : super(isPurchased: false); +} + +class PurchaseSuccess extends PurchaseInfoLoaded { + PurchaseSuccess() : super(isPurchased: true); +} + +class PurchaseFailed extends PurchaseInfoLoaded { + PurchaseFailed(this.exception) : super(isPurchased: false); + + final BaseException exception; + + @override + List get props => [exception, ...super.props]; +} diff --git a/lib/application/pages/details/contributor_view.dart b/lib/application/pages/details/contributor_view.dart index b0712ad6..e53d85a8 100644 --- a/lib/application/pages/details/contributor_view.dart +++ b/lib/application/pages/details/contributor_view.dart @@ -70,7 +70,7 @@ class MultiContributorsView extends ConsumerWidget { size: dimens.contributorImageSize, child: Text( '+${contributorsAmount - _visibleContributorsLimit}', - style: Theme.of(context).textTheme.subtitle2, + style: Theme.of(context).textTheme.titleSmall, ), ), ); diff --git a/lib/application/pages/execution/completed_execution_contents.dart b/lib/application/pages/execution/completed_execution_contents.dart index 0eaaa3e8..dae22ddd 100644 --- a/lib/application/pages/execution/completed_execution_contents.dart +++ b/lib/application/pages/execution/completed_execution_contents.dart @@ -51,7 +51,7 @@ class CompletedExecutionContents extends ConsumerWidget { Widget _buildSectionTitle(BuildContext context, MemoThemeData theme, String text) => Text( text, - style: Theme.of(context).textTheme.subtitle1?.copyWith(color: theme.neutralSwatch.shade300), + style: Theme.of(context).textTheme.titleMedium?.copyWith(color: theme.neutralSwatch.shade300), ); Widget _wrapInVerticalSection(Widget child, MemoThemeData theme) { @@ -146,19 +146,19 @@ class _Header extends ConsumerWidget { context.verticalBox(Spacing.xLarge), Text( strings.executionWellDone, - style: textTheme.subtitle1?.copyWith(color: memoTheme.primarySwatch.shade400), + style: textTheme.titleMedium?.copyWith(color: memoTheme.primarySwatch.shade400), textAlign: TextAlign.center, ), context.verticalBox(Spacing.small), Text( strings.executionImprovedKnowledgeDescription, - style: textTheme.subtitle1?.copyWith(color: memoTheme.neutralSwatch.shade400), + style: textTheme.titleMedium?.copyWith(color: memoTheme.neutralSwatch.shade400), textAlign: TextAlign.center, ), context.verticalBox(Spacing.large), Text( '# $collectionName', - style: textTheme.headline6, + style: textTheme.headlineSmall, textAlign: TextAlign.center, ), ], @@ -199,13 +199,13 @@ class _PerformanceIndicators extends ConsumerWidget { context.verticalBox(Spacing.small), Text( readableAnswersForDifficulty(difficulty) + strings.percentSymbol, - style: textTheme.subtitle1?.copyWith(color: memoTheme.secondarySwatch.shade400), + style: textTheme.titleMedium?.copyWith(color: memoTheme.secondarySwatch.shade400), textAlign: TextAlign.center, ), context.verticalBox(Spacing.xxSmall), Text( strings.answeredMemos(difficulty).toUpperCase(), - style: textTheme.caption, + style: textTheme.bodySmall, textAlign: TextAlign.center, ), ], diff --git a/lib/application/pages/execution/execution_terminal.dart b/lib/application/pages/execution/execution_terminal.dart index 1fc46fab..badb5096 100644 --- a/lib/application/pages/execution/execution_terminal.dart +++ b/lib/application/pages/execution/execution_terminal.dart @@ -264,7 +264,6 @@ class _TerminalQuillEditor extends StatelessWidget { padding: EdgeInsets.symmetric( vertical: dimens.terminalWindowHeaderHeight, horizontal: context.rawSpacing(Spacing.medium)), autoFocus: false, - readOnly: true, showCursor: false, expands: false, enableInteractiveSelection: false, @@ -372,7 +371,9 @@ class _TerminalActions extends HookWidget { Expanded( child: Text( strings.memoDifficulty(difficulty), - style: Theme.of(context).textTheme.bodyText2?.copyWith(color: isMarkedAnswer ? highlightColor : null), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: isMarkedAnswer ? highlightColor : null, + ), ), ), ], diff --git a/lib/application/pages/home/collections/collections_list_view.dart b/lib/application/pages/home/collections/collections_list_view.dart index 2070c830..9b5954ac 100644 --- a/lib/application/pages/home/collections/collections_list_view.dart +++ b/lib/application/pages/home/collections/collections_list_view.dart @@ -3,6 +3,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:layoutr/common_layout.dart'; import 'package:memo/application/coordinator/routes_coordinator.dart'; import 'package:memo/application/theme/theme_controller.dart'; +import 'package:memo/application/view-models/home/collections_vm.dart'; import 'package:memo/application/view-models/item_metadata.dart'; import 'package:memo/application/widgets/theme/item_collection_card.dart'; import 'package:memo/core/faults/errors/inconsistent_state_error.dart'; @@ -14,25 +15,30 @@ class CollectionsListView extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - return ListView.builder( - itemCount: items.length, - itemBuilder: (context, index) { - // Builds the respective widget based on the item's `ItemMetadata` subtype. - final item = items[index]; - - if (item is CollectionsCategoryMetadata) { - return _CollectionsSectionHeader(title: item.name) - .withOnlyPadding(context, top: Spacing.xLarge, bottom: Spacing.small); - } else if (item is CollectionItem) { - return buildCollectionCardFromItem( - item, - padding: context.symmetricInsets(vertical: Spacing.large, horizontal: Spacing.small), - onTap: () => readCoordinator(ref).navigateToCollectionDetails(item.id), - ).withOnlyPadding(context, bottom: Spacing.medium); - } - - throw InconsistentStateError.layout('Unsupported subtype (${item.runtimeType}) of `CollectionItemMetadata`'); - }, + final vm = ref.watch(collectionsVM.notifier); + + return RefreshIndicator( + onRefresh: vm.onRefresh, + child: ListView.builder( + itemCount: items.length, + itemBuilder: (context, index) { + // Builds the respective widget based on the item's `ItemMetadata` subtype. + final item = items[index]; + + if (item is CollectionsCategoryMetadata) { + return _CollectionsSectionHeader(title: item.name) + .withOnlyPadding(context, top: Spacing.xLarge, bottom: Spacing.small); + } else if (item is CollectionItem) { + return buildCollectionCardFromItem( + item, + padding: context.symmetricInsets(vertical: Spacing.large, horizontal: Spacing.small), + onTap: () => readCoordinator(ref).navigateToCollectionDetails(item.id), + ).withOnlyPadding(context, bottom: Spacing.medium); + } + + throw InconsistentStateError.layout('Unsupported subtype (${item.runtimeType}) of `CollectionItemMetadata`'); + }, + ), ); } } @@ -46,7 +52,7 @@ class _CollectionsSectionHeader extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { final titleColor = ref.watch(themeController).neutralSwatch.shade300; - final sectionTitleStyle = Theme.of(context).textTheme.headline6?.copyWith(color: titleColor); + final sectionTitleStyle = Theme.of(context).textTheme.headlineSmall?.copyWith(color: titleColor); return Text(title, style: sectionTitleStyle); } } diff --git a/lib/application/pages/home/collections/collections_page.dart b/lib/application/pages/home/collections/collections_page.dart index fbf39f1d..8defe82c 100644 --- a/lib/application/pages/home/collections/collections_page.dart +++ b/lib/application/pages/home/collections/collections_page.dart @@ -134,11 +134,11 @@ class _CollectionsEmptyState extends ConsumerWidget { color: theme.neutralSwatch.shade700, ), context.verticalBox(Spacing.xLarge), - Text(title, style: textTheme.headline6, textAlign: TextAlign.center), + Text(title, style: textTheme.headlineSmall, textAlign: TextAlign.center), context.verticalBox(Spacing.medium), Text( description, - style: textTheme.bodyText2?.copyWith(color: theme.neutralSwatch.shade400), + style: textTheme.bodyMedium?.copyWith(color: theme.neutralSwatch.shade400), textAlign: TextAlign.center, ), context.verticalBox(Spacing.xLarge), diff --git a/lib/application/pages/home/collections/update/update_collection_details.dart b/lib/application/pages/home/collections/update/update_collection_details.dart index 7c6e5427..064481f4 100644 --- a/lib/application/pages/home/collections/update/update_collection_details.dart +++ b/lib/application/pages/home/collections/update/update_collection_details.dart @@ -148,7 +148,7 @@ class _DescriptionField extends HookConsumerWidget { controller: controller, modalTitle: Text( strings.detailsDescription, - style: textTheme.bodyText1?.copyWith(color: theme.primarySwatch.shade400), + style: textTheme.bodyLarge?.copyWith(color: theme.primarySwatch.shade400), ), placeholder: strings.collectionDescription, helperText: strings.fieldCharactersAmount(descriptionLength, validators.collectionDescriptionMaxLength), diff --git a/lib/application/pages/home/collections/update/update_collection_memos.dart b/lib/application/pages/home/collections/update/update_collection_memos.dart index 307fe4c5..1068d640 100644 --- a/lib/application/pages/home/collections/update/update_collection_memos.dart +++ b/lib/application/pages/home/collections/update/update_collection_memos.dart @@ -184,7 +184,7 @@ class _NavigationIndicator extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, children: [ AssetIconButton(images.chevronLeftAsset, onPressed: onLeftTapped), - Text('$currentPage/$pagesAmount', style: textTheme.subtitle2), + Text('$currentPage/$pagesAmount', style: textTheme.titleSmall), AssetIconButton(images.chevronRightAsset, onPressed: onRightTapped), ], ); @@ -209,7 +209,7 @@ class _CreateMemoEmptyState extends ConsumerWidget { context.verticalBox(Spacing.small), Text( strings.newMemo.toUpperCase(), - style: textTheme.button?.copyWith(color: theme.primarySwatch.shade400), + style: textTheme.labelLarge?.copyWith(color: theme.primarySwatch.shade400), textAlign: TextAlign.center, ) ], diff --git a/lib/application/pages/home/collections/update/update_collection_page.dart b/lib/application/pages/home/collections/update/update_collection_page.dart index 0728b294..a95b2fe0 100644 --- a/lib/application/pages/home/collections/update/update_collection_page.dart +++ b/lib/application/pages/home/collections/update/update_collection_page.dart @@ -260,7 +260,7 @@ class _MemosReorderableList extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final textTheme = Theme.of(context).textTheme; - final listHeader = Text('${strings.jumpTo}...', style: textTheme.subtitle1, textAlign: TextAlign.center); + final listHeader = Text('${strings.jumpTo}...', style: textTheme.titleMedium, textAlign: TextAlign.center); return Theme( // Overrides theme to remove canvasColor and shadowColor when dragging a Memo card @@ -321,7 +321,7 @@ class _MemosReorderableListRow extends ConsumerWidget { children: [ Text( strings.updateMemoQuestionTitle(index + 1), - style: textTheme.bodyText1?.copyWith(color: theme.secondarySwatch), + style: textTheme.bodyLarge?.copyWith(color: theme.secondarySwatch), ), context.verticalBox(Spacing.xSmall), Text(metadata.question.plainContent, maxLines: 3), diff --git a/lib/application/pages/home/collections/update/update_memo_terminal.dart b/lib/application/pages/home/collections/update/update_memo_terminal.dart index 8060e1a4..168157a5 100644 --- a/lib/application/pages/home/collections/update/update_memo_terminal.dart +++ b/lib/application/pages/home/collections/update/update_memo_terminal.dart @@ -40,7 +40,7 @@ class UpdateMemoTerminal extends HookConsumerWidget { final questionTitle = Text( strings.updateMemoQuestionTitle(memoIndex), - style: textTheme.bodyText1?.copyWith(color: theme.secondarySwatch), + style: textTheme.bodyLarge?.copyWith(color: theme.secondarySwatch), ); final questionField = Column( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -57,7 +57,7 @@ class UpdateMemoTerminal extends HookConsumerWidget { final answerTitle = Text( strings.updateMemoAnswer, - style: textTheme.bodyText1?.copyWith(color: theme.primarySwatch), + style: textTheme.bodyLarge?.copyWith(color: theme.primarySwatch), ); final answerField = Column( crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/application/pages/home/progress/progress_page.dart b/lib/application/pages/home/progress/progress_page.dart index 467c911d..13314b9c 100644 --- a/lib/application/pages/home/progress/progress_page.dart +++ b/lib/application/pages/home/progress/progress_page.dart @@ -114,8 +114,8 @@ class ProgressPage extends ConsumerWidget { TextSpan( text: texts[index], style: index.isOdd - ? textTheme.headline4?.copyWith(color: titleColor) - : textTheme.headline3?.copyWith(color: titleColor), + ? textTheme.headlineLarge?.copyWith(color: titleColor) + : textTheme.displaySmall?.copyWith(color: titleColor), ), ); } @@ -147,7 +147,7 @@ class _ProgressContainer extends ConsumerWidget { children: [ title, context.verticalBox(Spacing.xSmall), - Text(description, style: textTheme.caption), + Text(description, style: textTheme.bodySmall), ], ); diff --git a/lib/application/pages/settings/settings_page.dart b/lib/application/pages/settings/settings_page.dart index a4327190..ff0e4f1f 100644 --- a/lib/application/pages/settings/settings_page.dart +++ b/lib/application/pages/settings/settings_page.dart @@ -27,7 +27,7 @@ class SettingsPage extends ConsumerWidget { if (item is SettingsSectionItem) { return Text( strings.settingsDescriptionForSection(item.section), - style: Theme.of(context).textTheme.subtitle1, + style: Theme.of(context).textTheme.titleMedium, ).withOnlyPadding(context, top: Spacing.xLarge, bottom: Spacing.xxSmall); } else if (item is LinkSettingsItem) { return UrlLinkButton( diff --git a/lib/application/theme/material_theme_data.dart b/lib/application/theme/material_theme_data.dart index ffa5aac9..c01f2541 100644 --- a/lib/application/theme/material_theme_data.dart +++ b/lib/application/theme/material_theme_data.dart @@ -55,7 +55,7 @@ ThemeData buildThemeData({ elevation: 0, iconTheme: iconTheme, backgroundColor: Colors.transparent, - titleTextStyle: textTheme.subtitle1, + titleTextStyle: textTheme.titleMedium, foregroundColor: Colors.white, ); @@ -70,13 +70,13 @@ ThemeData buildThemeData({ labelPadding: tabBarLabelPadding, labelColor: secondarySwatch.shade400, unselectedLabelColor: neutralSwatch.shade300, - labelStyle: textTheme.subtitle2, - unselectedLabelStyle: textTheme.subtitle2, + labelStyle: textTheme.titleSmall, + unselectedLabelStyle: textTheme.titleSmall, ); final snackBarTheme = SnackBarThemeData( backgroundColor: neutralSwatch.shade800, - contentTextStyle: textTheme.bodyText2, + contentTextStyle: textTheme.bodyMedium, actionTextColor: secondarySwatch.shade400, ); @@ -120,91 +120,91 @@ ThemeData buildThemeData({ // TextTheme _buildTextTheme(String fontFamily, {required Color textColor}) { return TextTheme( - headline1: TextStyle( + displayLarge: TextStyle( fontFamily: fontFamily, fontSize: 96, height: 1.17, fontWeight: FontWeight.w400, color: textColor, ), - headline2: TextStyle( + displayMedium: TextStyle( fontFamily: fontFamily, fontSize: 60, height: 1.2, fontWeight: FontWeight.w500, color: textColor, ), - headline3: TextStyle( + displaySmall: TextStyle( fontFamily: fontFamily, fontSize: 48, height: 1, fontWeight: FontWeight.w500, color: textColor, ), - headline4: TextStyle( + headlineLarge: TextStyle( fontFamily: fontFamily, fontSize: 32, height: 1.19, fontWeight: FontWeight.w400, color: textColor, ), - headline5: TextStyle( + headlineMedium: TextStyle( fontFamily: fontFamily, fontSize: 24, height: 1.17, fontWeight: FontWeight.w400, color: textColor, ), - headline6: TextStyle( + headlineSmall: TextStyle( fontFamily: fontFamily, fontSize: 20, height: 1.2, fontWeight: FontWeight.w400, color: textColor, ), - subtitle1: TextStyle( + titleMedium: TextStyle( fontFamily: fontFamily, fontSize: 16, height: 1.25, fontWeight: FontWeight.w700, color: textColor, ), - subtitle2: TextStyle( + titleSmall: TextStyle( fontFamily: fontFamily, fontSize: 14, height: 1.14, fontWeight: FontWeight.w500, color: textColor, ), - bodyText1: TextStyle( + bodyLarge: TextStyle( fontFamily: fontFamily, fontSize: 16, height: 1.5, fontWeight: FontWeight.w300, color: textColor, ), - bodyText2: TextStyle( + bodyMedium: TextStyle( fontFamily: fontFamily, fontSize: 14, height: 1.57, fontWeight: FontWeight.w400, color: textColor, ), - button: TextStyle( + labelLarge: TextStyle( fontFamily: fontFamily, fontSize: 16, height: 1.25, fontWeight: FontWeight.w700, color: textColor, ), - caption: TextStyle( + bodySmall: TextStyle( fontFamily: fontFamily, fontSize: 12, height: 1.33, fontWeight: FontWeight.w400, color: textColor, ), - overline: TextStyle( + labelSmall: TextStyle( fontFamily: fontFamily, fontSize: 10, height: 1, diff --git a/lib/application/theme/memo_theme_colors.dart b/lib/application/theme/memo_theme_colors.dart index b6a44251..c148464a 100644 --- a/lib/application/theme/memo_theme_colors.dart +++ b/lib/application/theme/memo_theme_colors.dart @@ -83,3 +83,22 @@ MaterialColor buildClassicNeutralSwatch() { }, ); } + +MaterialColor buildPremiumSwatch() { + const defaultPremium = Color(0xFFA87B05); + return MaterialColor( + defaultPremium.value, + const { + 50: Color(0xFFFFF8E0), + 100: Color(0xFFFFE6B3), + 200: Color(0xFFFFD280), + 300: Color(0xFFFFC04D), + 400: Color(0xFFFFB02A), + 500: defaultPremium, + 600: Color(0xFF8C5E03), + 700: Color(0xFF6B4A02), + 800: Color(0xFF4A3601), + 900: Color(0xFF2A2300), + }, + ); +} diff --git a/lib/application/theme/memo_theme_data.dart b/lib/application/theme/memo_theme_data.dart index ebbefc3d..4c3b81d9 100644 --- a/lib/application/theme/memo_theme_data.dart +++ b/lib/application/theme/memo_theme_data.dart @@ -15,6 +15,7 @@ class MemoThemeData { required this.secondarySwatch, required this.neutralSwatch, required this.destructiveSwatch, + required this.premiumSwatch, }); final MemoTheme theme; @@ -23,4 +24,5 @@ class MemoThemeData { final MaterialColor secondarySwatch; final MaterialColor neutralSwatch; final MaterialColor destructiveSwatch; + final MaterialColor premiumSwatch; } diff --git a/lib/application/theme/theme_controller.dart b/lib/application/theme/theme_controller.dart index a21ed4ff..e551712b 100644 --- a/lib/application/theme/theme_controller.dart +++ b/lib/application/theme/theme_controller.dart @@ -30,6 +30,7 @@ class ThemeController extends StateNotifier { secondarySwatch: _secondarySwatchFor(MemoTheme.classic), neutralSwatch: _neutralSwatchFor(MemoTheme.classic), destructiveSwatch: _destructiveSwatchFor(MemoTheme.classic), + premiumSwatch: _premiumSwatchFor(MemoTheme.classic), ); /// Updates the current [state] with a new instance of [MemoThemeData], using the [theme] argument. @@ -46,6 +47,7 @@ class ThemeController extends StateNotifier { secondarySwatch: _secondarySwatchFor(theme), neutralSwatch: _neutralSwatchFor(theme), destructiveSwatch: _destructiveSwatchFor(theme), + premiumSwatch: _premiumSwatchFor(theme), ); } @@ -116,3 +118,10 @@ MaterialColor _destructiveSwatchFor(MemoTheme theme) { return colors.buildClassicDestructiveSwatch(); } } + +MaterialColor _premiumSwatchFor(MemoTheme theme) { + switch (theme) { + case MemoTheme.classic: + return colors.buildPremiumSwatch(); + } +} diff --git a/lib/application/utils/bottom_sheet.dart b/lib/application/utils/bottom_sheet.dart index 1094f9b8..75f115cd 100644 --- a/lib/application/utils/bottom_sheet.dart +++ b/lib/application/utils/bottom_sheet.dart @@ -41,7 +41,7 @@ Future showSnappableDraggableModalBottomSheet( Text( title, textAlign: TextAlign.center, - style: Theme.of(context).textTheme.headline6, + style: Theme.of(context).textTheme.headlineSmall, maxLines: 1, overflow: TextOverflow.ellipsis, ).withSymmetricalPadding(context, vertical: Spacing.small, horizontal: Spacing.medium) @@ -92,9 +92,9 @@ Future showDestructiveOperationModalBottomSheet( child: Column( mainAxisSize: MainAxisSize.min, children: [ - Text(title, style: textTheme.subtitle1), + Text(title, style: textTheme.titleMedium), context.verticalBox(Spacing.xLarge), - Text(message, style: textTheme.bodyText1), + Text(message, style: textTheme.bodyLarge), context.verticalBox(Spacing.xxxLarge), DestructiveButton(onPressed: onDestructiveTapped, text: destructiveActionTitle), context.verticalBox(Spacing.medium), diff --git a/lib/application/view-models/app_vm.dart b/lib/application/view-models/app_vm.dart index 5c092678..bd019520 100644 --- a/lib/application/view-models/app_vm.dart +++ b/lib/application/view-models/app_vm.dart @@ -1,9 +1,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:memo/core/env.dart'; import 'package:memo/data/gateways/application_bundle.dart'; +import 'package:memo/data/gateways/purchase_gateway.dart'; import 'package:memo/data/gateways/sembast.dart' as sembast; import 'package:memo/data/gateways/sembast_database.dart'; +import 'package:memo/data/repositories/purchase_repository.dart'; import 'package:memo/data/repositories/collection_repository.dart'; import 'package:memo/data/repositories/memo_execution_repository.dart'; import 'package:memo/data/repositories/memo_repository.dart'; @@ -12,6 +15,7 @@ import 'package:memo/data/repositories/transaction_handler.dart'; import 'package:memo/data/repositories/user_repository.dart'; import 'package:memo/data/repositories/version_repository.dart'; import 'package:memo/domain/isolated_services/memory_recall_services.dart'; +import 'package:memo/domain/services/collection_purchase_services.dart'; import 'package:memo/domain/services/collection_services.dart'; import 'package:memo/domain/services/execution_services.dart'; import 'package:memo/domain/services/progress_services.dart'; @@ -53,6 +57,8 @@ class AppVMImpl extends AppVM { } _hasRequestedLoading = true; + final env = envMetadata(); + // Set a minimum (reasonable) duration for this first load, as it may simply flick a splash screen if too fast. final splashMinDuration = Future.delayed(const Duration(milliseconds: 500)); @@ -64,6 +70,7 @@ class AppVMImpl extends AppVM { // Gateways final dbRepo = SembastDatabaseImpl(firstClassDependencies[0] as Database); final appBundle = ApplicationBundleImpl(assetBundle); + final purchaseGateway = PurchaseGatewayImpl(env); // Repositories final collectionRepo = CollectionRepositoryImpl(dbRepo, appBundle); @@ -72,6 +79,7 @@ class AppVMImpl extends AppVM { final userRepo = UserRepositoryImpl(dbRepo); final versionRepo = VersionRepositoryImpl(dbRepo); final resourceRepo = ResourceRepositoryImpl(dbRepo, appBundle); + final purchaseRepo = PurchaseRepositoryImpl(dbRepo, purchaseGateway); final transactionHandler = TransactionHandlerImpl(dbRepo); @@ -79,10 +87,18 @@ class AppVMImpl extends AppVM { final memoryServices = MemoryRecallServicesImpl(); // Services + final collectionPurchaseServices = CollectionPurchaseServicesImpl( + env: env, + purchaseRepo: purchaseRepo, + collectionRepo: collectionRepo, + ); + final collectionServices = CollectionServicesImpl( collectionRepo: collectionRepo, memoRepo: memoRepo, memoryServices: memoryServices, + purchaseRepo: purchaseRepo, + collectionPurchaseServices: collectionPurchaseServices, ); final executionServices = ExecutionServicesImpl( @@ -103,6 +119,7 @@ class AppVMImpl extends AppVM { executionServices: executionServices, progressServices: progressServices, resourceServices: resourceServices, + collectionPurchaseServices: collectionPurchaseServices, ); // Scope-specific Services @@ -122,6 +139,9 @@ class AppVMImpl extends AppVM { splashMinDuration, ]); + // This dependency needs to be loaded after the "dependent dependencies" as it depends on checking purchases that cannot be loaded in parallel. + await collectionPurchaseServices.updatePurchasesIfNeeded(); + value = AsyncValue.data(appState); } } @@ -132,12 +152,14 @@ class AppState { required this.executionServices, required this.progressServices, required this.resourceServices, + required this.collectionPurchaseServices, }); final CollectionServices collectionServices; final ExecutionServices executionServices; final ProgressServices progressServices; final ResourceServices resourceServices; + final CollectionPurchaseServices collectionPurchaseServices; } // Creates uninitialized Provider for all services, which MUST BE overriden in the root `ProviderScope.overrides`. @@ -153,3 +175,6 @@ final progressServices = Provider((_) { final resourceServices = Provider((_) { throw UnimplementedError('resourceServices Provider must be overridden'); }); +final purchaseServices = Provider((_) { + throw UnimplementedError('PurchaseServices Provider must be overridden'); +}); diff --git a/lib/application/view-models/home/collections_vm.dart b/lib/application/view-models/home/collections_vm.dart index e7247b02..09423a98 100644 --- a/lib/application/view-models/home/collections_vm.dart +++ b/lib/application/view-models/home/collections_vm.dart @@ -14,11 +14,15 @@ final collectionsVM = StateNotifierProvider((re /// Segment used to filter the current state of the [CollectionsVM]. enum CollectionsSegment { explore, review } + const availableSegments = CollectionsSegment.values; abstract class CollectionsVM extends StateNotifier { CollectionsVM(CollectionsState state) : super(state); + /// Updates the collections list. + Future onRefresh(); + /// Updates the current [state] with [segment]. /// /// Changing this [segment] also updates the displayed collections based on this [CollectionsSegment]. @@ -35,6 +39,12 @@ class CollectionsVMImpl extends CollectionsVM { StreamSubscription>? _statusListener; List _cachedCollectionItems = []; + @override + Future onRefresh() async { + state = LoadingCollectionsState(state.currentSegment); + await _addCollectionsListeners(); + } + @override Future updateCollectionsSegment(CollectionsSegment segment) async { if (state is LoadingCollectionsState) { @@ -66,12 +76,28 @@ class CollectionsVMImpl extends CollectionsVM { case CollectionsSegment.review: return metadata is CompletedCollectionItem; } - }).toList(); + }).toList() + ..sort(_premiumSort); final items = _mapMetadataToItems(filteredMetadata); state = LoadedCollectionsState(items, currentSegment: normalizedSegment); } + /// Sorts the [CollectionItem]s by their premium status. + int _premiumSort(CollectionItem a, CollectionItem b) { + { + if (a.isPremium && b.isPremium) { + return 0; + } + + if (b.isPremium) { + return 1; + } + + return -1; + } + } + /// Maps all [metadata] to its sorted [ItemMetadata] list. /// /// To sort its contents, a `Map` is created to segment the [metadata] using the categories, and then flatten this diff --git a/lib/application/view-models/home/update_collection_vm.dart b/lib/application/view-models/home/update_collection_vm.dart index f3b7da27..6700f6f6 100644 --- a/lib/application/view-models/home/update_collection_vm.dart +++ b/lib/application/view-models/home/update_collection_vm.dart @@ -94,7 +94,6 @@ class UpdateCollectionVMImpl extends UpdateCollectionVM { final loadedState = state as UpdateCollectionLoaded; try { // TODO(ggirotto): Call services to save the collection - } on BaseException catch (exception) { state = UpdateCollectionFailedSaving( exception, diff --git a/lib/application/view-models/item_metadata.dart b/lib/application/view-models/item_metadata.dart index f5bbd0d8..281d1a45 100644 --- a/lib/application/view-models/item_metadata.dart +++ b/lib/application/view-models/item_metadata.dart @@ -15,15 +15,25 @@ class CollectionsCategoryMetadata extends ItemMetadata { } abstract class CollectionItem extends ItemMetadata { - CollectionItem({required this.id, required this.name, required this.category, required this.tags}); + CollectionItem({ + required this.id, + required this.name, + required this.category, + required this.tags, + required this.isPremium, + this.price, + }); final String id; final String name; final String category; final List tags; + final bool isPremium; + final double? price; + @override - List get props => [id, name, category, tags]; + List get props => [id, name, category, tags, isPremium]; } /// Represents a collection that have been fully executed - where no pristine memos are left. @@ -34,7 +44,16 @@ class CompletedCollectionItem extends CollectionItem { required String name, required String category, required List tags, - }) : super(id: id, name: name, category: category, tags: tags); + required bool isPremium, + double? price, + }) : super( + id: id, + name: name, + category: category, + tags: tags, + isPremium: isPremium, + price: price, + ); final double recallLevel; String get readableRecall => (recallLevel * 100).round().toString(); @@ -52,7 +71,16 @@ class IncompleteCollectionItem extends CollectionItem { required String name, required String category, required List tags, - }) : super(id: id, name: name, category: category, tags: tags); + required bool isPremium, + double? price, + }) : super( + id: id, + name: name, + category: category, + tags: tags, + isPremium: isPremium, + price: price, + ); final int executedUniqueMemos; final int totalUniqueMemos; @@ -75,6 +103,8 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) { name: collection.name, category: collection.category, tags: collection.tags, + isPremium: collection.isPremium, + price: collection.productInfo?.price, ); } else { return IncompleteCollectionItem( @@ -84,6 +114,8 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) { name: collection.name, category: collection.category, tags: collection.tags, + isPremium: collection.isPremium, + price: collection.productInfo?.price, ); } } diff --git a/lib/application/widgets/theme/collection_card.dart b/lib/application/widgets/theme/collection_card.dart index 8c98f350..05fc7ee9 100644 --- a/lib/application/widgets/theme/collection_card.dart +++ b/lib/application/widgets/theme/collection_card.dart @@ -6,6 +6,7 @@ import 'package:layoutr/common_layout.dart'; import 'package:memo/application/constants/animations.dart' as anims; import 'package:memo/application/constants/colors.dart' as colors; import 'package:memo/application/constants/dimensions.dart' as dimens; +import 'package:memo/application/constants/strings.dart' as strings; import 'package:memo/application/theme/memo_theme_data.dart'; import 'package:memo/application/theme/theme_controller.dart'; import 'package:memo/application/widgets/animatable_progress.dart'; @@ -20,6 +21,7 @@ class CollectionCard extends ConsumerWidget { required this.name, required this.tags, required this.padding, + required this.isPremium, this.hasBorder = true, this.progressDescription, this.progressValue, @@ -35,6 +37,9 @@ class CollectionCard extends ConsumerWidget { /// List of tags associated with this collection. final List tags; + /// `true` If the collection is available for purchase. + final bool isPremium; + /// If this widget should draw a border for this card. final bool hasBorder; @@ -59,7 +64,7 @@ class CollectionCard extends ConsumerWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(name, style: Theme.of(context).textTheme.headline6), + Text(name, style: Theme.of(context).textTheme.headlineSmall), context.verticalBox(Spacing.xSmall), Flexible(child: _buildTagsWrap(context)), ], @@ -78,13 +83,15 @@ class CollectionCard extends ConsumerWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Flexible(child: firstRowElements), + Flexible( + child: firstRowElements, + ), if (progressDescription != null && progressValue != null) ...[ context.verticalBox(Spacing.large), _buildMemoryRecallTitle(context, theme), context.verticalBox(Spacing.xSmall), _buildMemoryRecallProgress(theme), - ] + ], ], ), ), @@ -126,13 +133,16 @@ class CollectionCard extends ConsumerWidget { crossAxisAlignment: WrapCrossAlignment.center, spacing: tagsSpacing, runSpacing: tagsSpacing, - children: tags.map((tag) => PrimaryTextTag(tag.toUpperCase())).toList(), + children: [ + ...tags.map((tag) => PrimaryTextTag(tag.toUpperCase())), + if (isPremium) SecondaryTextTag(strings.premium.toUpperCase()), + ], ); } Text _buildMemoryRecallTitle(BuildContext context, MemoThemeData theme) { final captionColor = theme.neutralSwatch.shade200; - final captionStyle = Theme.of(context).textTheme.caption; + final captionStyle = Theme.of(context).textTheme.bodySmall; return Text(progressDescription!, style: captionStyle?.copyWith(color: captionColor)); } diff --git a/lib/application/widgets/theme/custom_button.dart b/lib/application/widgets/theme/custom_button.dart index 3d24d260..9dc98283 100644 --- a/lib/application/widgets/theme/custom_button.dart +++ b/lib/application/widgets/theme/custom_button.dart @@ -112,7 +112,7 @@ class CustomTextButton extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final theme = ref.watch(themeController); final buttonColorSwatch = color ?? theme.primarySwatch; - final textTheme = Theme.of(context).textTheme.button!; + final textTheme = Theme.of(context).textTheme.labelLarge!; Color? buttonColor(_ButtonState state) { switch (state) { @@ -173,7 +173,7 @@ class _CustomElevatedButton extends StatelessWidget { @override Widget build(BuildContext context) { - final textTheme = Theme.of(context).textTheme.button!; + final textTheme = Theme.of(context).textTheme.labelLarge!; Widget leadingAssetBuilder(_ButtonState state) => Image.asset(leadingAsset!, color: textTheme.color); diff --git a/lib/application/widgets/theme/custom_text_field.dart b/lib/application/widgets/theme/custom_text_field.dart index f6ba43a3..24823c85 100644 --- a/lib/application/widgets/theme/custom_text_field.dart +++ b/lib/application/widgets/theme/custom_text_field.dart @@ -113,8 +113,8 @@ class CustomTextField extends HookConsumerWidget { }); final labelStyle = hasFocus.value || hasText.value - ? textTheme.caption?.copyWith(color: neutralSwatch.shade300) - : textTheme.subtitle1; + ? textTheme.bodySmall?.copyWith(color: neutralSwatch.shade300) + : textTheme.titleMedium; final textField = DecoratedBox( decoration: BoxDecoration( @@ -129,7 +129,7 @@ class CustomTextField extends HookConsumerWidget { focusNode: focusNode, enabled: enabled, textAlign: textAlign, - style: textTheme.bodyText2, + style: textTheme.bodyMedium, cursorColor: theme.secondarySwatch.shade400, decoration: InputDecoration( labelText: labelText, @@ -152,7 +152,8 @@ class CustomTextField extends HookConsumerWidget { if (_hasErrorText || _hasHelperText) Text( _hasErrorText ? errorText! : helperText!, - style: textTheme.caption?.copyWith(color: _hasErrorText ? theme.destructiveSwatch : neutralSwatch.shade400), + style: + textTheme.bodySmall?.copyWith(color: _hasErrorText ? theme.destructiveSwatch : neutralSwatch.shade400), ).withOnlyPadding(context, left: Spacing.small) ], ); diff --git a/lib/application/widgets/theme/exception_retry_container.dart b/lib/application/widgets/theme/exception_retry_container.dart index 505be32d..7619368c 100644 --- a/lib/application/widgets/theme/exception_retry_container.dart +++ b/lib/application/widgets/theme/exception_retry_container.dart @@ -21,11 +21,11 @@ class ExceptionRetryContainer extends StatelessWidget { return Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - Text(strings.oops, style: textTheme.headline4, textAlign: TextAlign.center), + Text(strings.oops, style: textTheme.headlineLarge, textAlign: TextAlign.center), context.verticalBox(Spacing.xSmall), Text( descriptionForException(exception), - style: textTheme.headline6, + style: textTheme.headlineSmall, textAlign: TextAlign.center, ), context.verticalBox(Spacing.large), diff --git a/lib/application/widgets/theme/item_collection_card.dart b/lib/application/widgets/theme/item_collection_card.dart index 57f617f8..2b2b6722 100644 --- a/lib/application/widgets/theme/item_collection_card.dart +++ b/lib/application/widgets/theme/item_collection_card.dart @@ -41,5 +41,6 @@ Widget buildCollectionCardFromItem( progressValue: progressValue, progressSemanticLabel: progressSemanticLabel, onTap: onTap, + isPremium: item.isPremium, ); } diff --git a/lib/application/widgets/theme/link.dart b/lib/application/widgets/theme/link.dart index 831ac054..01cd993a 100644 --- a/lib/application/widgets/theme/link.dart +++ b/lib/application/widgets/theme/link.dart @@ -186,7 +186,7 @@ class UnderlinedUrlLink extends ConsumerWidget { onTap: isEnabled ? () => _handleUrlLaunch(url, onFailLaunchingUrl) : null, child: Text( text ?? url, - style: Theme.of(context).textTheme.caption?.copyWith( + style: Theme.of(context).textTheme.bodySmall?.copyWith( color: ref.watch(themeController).neutralSwatch.shade300, decoration: TextDecoration.underline, ), diff --git a/lib/application/widgets/theme/rich_text_field.dart b/lib/application/widgets/theme/rich_text_field.dart index 0195ac50..274e13c5 100644 --- a/lib/application/widgets/theme/rich_text_field.dart +++ b/lib/application/widgets/theme/rich_text_field.dart @@ -169,11 +169,11 @@ class RichTextField extends HookConsumerWidget { collapsedEditor, if (errorText != null) ...[ context.verticalBox(Spacing.xxxSmall), - Text(errorText!, style: textTheme.caption?.copyWith(color: theme.destructiveSwatch)) + Text(errorText!, style: textTheme.bodySmall?.copyWith(color: theme.destructiveSwatch)) .withOnlyPadding(context, left: Spacing.small) ] else if (helperText != null) ...[ context.verticalBox(Spacing.xxxSmall), - Text(helperText!, style: textTheme.caption?.copyWith(color: theme.neutralSwatch.shade400)) + Text(helperText!, style: textTheme.bodySmall?.copyWith(color: theme.neutralSwatch.shade400)) .withOnlyPadding(context, left: Spacing.small) ] ], @@ -239,7 +239,7 @@ class _CollapsedEditor extends ConsumerWidget { mainAxisSize: MainAxisSize.min, children: [ if (hasContent) ...[ - Text(placeholder, style: textTheme.caption?.copyWith(color: theme.neutralSwatch.shade400)), + Text(placeholder, style: textTheme.bodySmall?.copyWith(color: theme.neutralSwatch.shade400)), context.verticalBox(Spacing.small), Flexible( child: AbsorbPointer( @@ -252,7 +252,7 @@ class _CollapsedEditor extends ConsumerWidget { ), ), ] else - Text(placeholder, style: textTheme.subtitle1) + Text(placeholder, style: textTheme.titleMedium) ], ).withSymmetricalPadding(context, vertical: Spacing.small, horizontal: Spacing.medium), ); @@ -388,21 +388,20 @@ class _ThemedEditor extends ConsumerWidget { scrollable: true, padding: EdgeInsets.zero, autoFocus: !readOnly, - readOnly: readOnly, expands: false, enableInteractiveSelection: !readOnly, showCursor: !readOnly, placeholder: placeholder, customStyles: quill.DefaultStyles( - paragraph: quill.DefaultTextBlockStyle(textTheme.bodyText2!, zeroTuple, zeroTuple, null), + paragraph: quill.DefaultTextBlockStyle(textTheme.bodyMedium!, zeroTuple, zeroTuple, null), placeHolder: quill.DefaultTextBlockStyle( - textTheme.bodyText1!.copyWith(color: theme.neutralSwatch.shade400), + textTheme.bodyLarge!.copyWith(color: theme.neutralSwatch.shade400), zeroTuple, zeroTuple, null, ), code: quill.DefaultTextBlockStyle( - textTheme.bodyText1!, + textTheme.bodyLarge!, zeroTuple, zeroTuple, BoxDecoration(color: codeBackgroundColor), diff --git a/lib/application/widgets/theme/tags_field.dart b/lib/application/widgets/theme/tags_field.dart index dbd284a6..7c446fe0 100644 --- a/lib/application/widgets/theme/tags_field.dart +++ b/lib/application/widgets/theme/tags_field.dart @@ -144,14 +144,14 @@ class _TagsFieldContainer extends ConsumerWidget { // Simulates `TextField.helperText` animating the label when the field is focused or not. final helperTitle = AnimatedDefaultTextStyle( - style: hasFocus ? textTheme.caption!.copyWith(color: theme.neutralSwatch.shade300) : textTheme.subtitle1!, + style: hasFocus ? textTheme.bodySmall!.copyWith(color: theme.neutralSwatch.shade300) : textTheme.titleMedium!, duration: anims.textFieldHelperTextDuration, child: const Text(strings.addTags), ); final helperText = Text( errorText != null ? errorText! : strings.tagsAmount(tagsAmount, maxTags), - style: textTheme.caption?.copyWith( + style: textTheme.bodySmall?.copyWith( color: errorText != null ? theme.destructiveSwatch : theme.neutralSwatch.shade400, ), ); @@ -231,10 +231,10 @@ class _TagsTextField extends HookConsumerWidget { contentPadding: EdgeInsets.zero, fillColor: Colors.transparent, hintText: strings.tagsHint, - hintStyle: textTheme.bodyText2?.copyWith(color: theme.neutralSwatch.shade600), + hintStyle: textTheme.bodyMedium?.copyWith(color: theme.neutralSwatch.shade600), ), textAlignVertical: TextAlignVertical.center, - style: textTheme.bodyText2, + style: textTheme.bodyMedium, keyboardType: TextInputType.text, textInputAction: TextInputAction.done, inputFormatters: [_TagFieldFormatter()], @@ -280,7 +280,7 @@ class _SelectedTag extends ConsumerWidget { child: Row( mainAxisSize: MainAxisSize.min, children: [ - Text(tag, style: textTheme.caption), + Text(tag, style: textTheme.bodySmall), context.horizontalBox(Spacing.xxxSmall), Image.asset(images.closeAsset, height: dimens.tagsRemoveIconSize, width: dimens.tagsRemoveIconSize), ], diff --git a/lib/application/widgets/theme/themed_text_tag.dart b/lib/application/widgets/theme/themed_text_tag.dart index 127481fa..ac083f8e 100644 --- a/lib/application/widgets/theme/themed_text_tag.dart +++ b/lib/application/widgets/theme/themed_text_tag.dart @@ -15,7 +15,7 @@ class PrimaryTextTag extends ConsumerWidget { text, backgroundColor: ref.watch(themeController).primarySwatch.shade600, padding: context.allInsets(Spacing.xxSmall), - textStyle: Theme.of(context).textTheme.overline, + textStyle: Theme.of(context).textTheme.labelSmall, ); } @@ -28,9 +28,9 @@ class SecondaryTextTag extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) => TextTag( text, - backgroundColor: ref.watch(themeController).secondarySwatch.shade600, + backgroundColor: ref.watch(themeController).premiumSwatch, padding: context.allInsets(Spacing.xxSmall), - textStyle: Theme.of(context).textTheme.overline, + textStyle: Theme.of(context).textTheme.labelSmall, ); } @@ -45,6 +45,6 @@ class NeutralTextTag extends ConsumerWidget { text, backgroundColor: ref.watch(themeController).neutralSwatch.shade800, padding: context.allInsets(Spacing.xxSmall), - textStyle: Theme.of(context).textTheme.overline, + textStyle: Theme.of(context).textTheme.labelSmall, ); } diff --git a/lib/core/env.dart b/lib/core/env.dart index bb506877..6894e97f 100644 --- a/lib/core/env.dart +++ b/lib/core/env.dart @@ -13,6 +13,9 @@ abstract class EnvMetadata { /// `true` when running in [Env.dev]. bool get isDev; + + /// RevenueCat SDK API Key. + String get inAppPurchaseKey; } class EnvMetadataImpl implements EnvMetadata { @@ -34,6 +37,16 @@ class EnvMetadataImpl implements EnvMetadata { throw InconsistentStateError('Unsupported platform - ${Platform.operatingSystem}'); } + + @override + String get inAppPurchaseKey { + switch (platform) { + case SupportedPlatform.ios: + return 'appl_edKVhziuBuXDpmVPASASRdEJhKc'; + case SupportedPlatform.android: + return 'goog_PlRbIRkgyhwGbBiUugCHBjzXsTL'; + } + } } /// Application's supported environments. diff --git a/lib/core/faults/exceptions/base_exception.dart b/lib/core/faults/exceptions/base_exception.dart index 49f86b8f..a66f5627 100644 --- a/lib/core/faults/exceptions/base_exception.dart +++ b/lib/core/faults/exceptions/base_exception.dart @@ -43,4 +43,8 @@ enum ExceptionType { // Validation emptyField, fieldLengthExceeded, + + // PurchaseException + purchaseProductFailed, + failedToPurchase, } diff --git a/lib/core/faults/exceptions/purchase_exception.dart b/lib/core/faults/exceptions/purchase_exception.dart new file mode 100644 index 00000000..8d34cc35 --- /dev/null +++ b/lib/core/faults/exceptions/purchase_exception.dart @@ -0,0 +1,9 @@ +import 'package:memo/core/faults/exceptions/base_exception.dart'; + +/// Failed to purchase a deck for any specific reason. +class PurchaseException extends BaseException { + PurchaseException.purchaseProductFailed({String? debugInfo}) + : super(type: ExceptionType.purchaseProductFailed, debugInfo: debugInfo); + + PurchaseException.failedPurchase() : super(type: ExceptionType.failedToPurchase); +} diff --git a/lib/data/gateways/purchase_gateway.dart b/lib/data/gateways/purchase_gateway.dart new file mode 100644 index 00000000..0034a048 --- /dev/null +++ b/lib/data/gateways/purchase_gateway.dart @@ -0,0 +1,76 @@ +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:memo/core/env.dart'; +import 'package:memo/core/faults/exceptions/purchase_exception.dart'; +import 'package:purchases_flutter/purchases_flutter.dart'; + +/// Handles in-app purchases. +abstract class PurchaseGateway { + /// Purchase the product using a unique [identifier] per product. + /// + /// Throws a [PurchaseException.failedPurchase] if a purchase failed. + /// Throws a [PurchaseException.purchaseProductFailed] if anything wrong happens during the purchase flow. + Future purchase({required String identifier}); + + /// Get all purchases the user has made. + Future> purchasesInfo(); + + /// Check which products are available for purchase. + Future> getAvailableProducts(); +} + +class PurchaseGatewayImpl extends PurchaseGateway { + PurchaseGatewayImpl(this._env); + + final EnvMetadata _env; + + bool _hasInitialized = false; + + FutureOr _init() async { + PurchasesConfiguration? configuration; + configuration = PurchasesConfiguration(_env.inAppPurchaseKey); + + if (!_hasInitialized) { + await Purchases.setLogLevel(LogLevel.debug); + await Purchases.configure(configuration); + + _hasInitialized = true; + } + } + + @override + Future purchase({required String identifier}) async { + try { + await _init(); + await Purchases.purchaseProduct(identifier, type: PurchaseType.inapp); + } on PlatformException catch (exception) { + final errorCode = PurchasesErrorHelper.getErrorCode(exception); + if (errorCode == PurchasesErrorCode.offlineConnectionError) { + throw PurchaseException.failedPurchase(); + } + + throw PurchaseException.purchaseProductFailed(debugInfo: exception.toString()); + } + } + + @override + Future> purchasesInfo() async { + await _init(); + final customerInfo = await Purchases.getCustomerInfo(); + + final purchases = customerInfo.allPurchasedProductIdentifiers; + + return purchases; + } + + @override + Future> getAvailableProducts() async { + await _init(); + final offerings = await Purchases.getOfferings(); + + final productIdentifiers = offerings.current!.availablePackages; + + return productIdentifiers; + } +} diff --git a/lib/data/gateways/sembast_database.dart b/lib/data/gateways/sembast_database.dart index 26a5000c..f64fd4aa 100644 --- a/lib/data/gateways/sembast_database.dart +++ b/lib/data/gateways/sembast_database.dart @@ -133,7 +133,7 @@ class SembastDatabaseImpl extends SembastDatabase { } @override - Future?> get({required String id, required String store}) { + Future?> get({required String id, required String store}) async { final storeMap = sembast.stringMapStoreFactory.store(store); return storeMap.record(id).get(currentTransaction ?? db); } diff --git a/lib/data/repositories/collection_repository.dart b/lib/data/repositories/collection_repository.dart index d6b5ee9d..29002e85 100644 --- a/lib/data/repositories/collection_repository.dart +++ b/lib/data/repositories/collection_repository.dart @@ -7,6 +7,7 @@ import 'package:memo/data/serializers/collection_memos_serializer.dart'; import 'package:memo/data/serializers/collection_serializer.dart'; import 'package:memo/data/serializers/contributor_serializer.dart'; import 'package:memo/data/serializers/memo_difficulty_parser.dart'; +import 'package:memo/data/serializers/product_info_serializer.dart'; import 'package:memo/domain/enums/memo_difficulty.dart'; import 'package:memo/domain/models/collection.dart'; import 'package:memo/domain/transients/collection_memos.dart'; @@ -52,6 +53,7 @@ class CollectionRepositoryImpl implements CollectionRepository { final _collectionsMemosSerializer = CollectionMemosSerializer(); final _collectionSerializer = CollectionSerializer(); final _contributorSerializer = ContributorSerializer(); + final _productInfoSerializer = ProductInfoSerializer(); @override Future getCollection({required String id}) async { @@ -90,6 +92,8 @@ class CollectionRepositoryImpl implements CollectionRepository { CollectionKeys.contributors: collection.contributors.map(_contributorSerializer.to), CollectionKeys.uniqueMemosAmount: collection.uniqueMemosAmount, CollectionKeys.uniqueMemoExecutionsAmount: collection.uniqueMemoExecutionsAmount, + CollectionKeys.isPremium: collection.isPremium, + if (collection.isPremium) CollectionKeys.productInfo: _productInfoSerializer.to(collection.productInfo!), }, ) .toList(), diff --git a/lib/data/repositories/purchase_repository.dart b/lib/data/repositories/purchase_repository.dart new file mode 100644 index 00000000..8ab3adfc --- /dev/null +++ b/lib/data/repositories/purchase_repository.dart @@ -0,0 +1,67 @@ +import 'package:memo/data/gateways/purchase_gateway.dart'; +import 'package:memo/data/gateways/sembast_database.dart'; + +abstract class PurchaseRepository { + /// Purchase products in the app with the store ID [storeId] for the local user. + Future purchaseInApp({required String storeId}); + + /// Fetches a list of RevenueCat purchase information strings by user id. + /// + /// This function retrieves purchase information asynchronously + /// and returns a list of strings, where each string represents a purchase. + Future> getUserPurchases(); + + /// Check which products are available for purchase. + Future> isAvailable(); + + /// Updates the collection with the [purchaseId] to be premium or not. + Future updatePurchase({required String purchaseId}); + + /// Fetches a list of purchased product IDs from the local database. + /// + /// This function asynchronously retrieves all the purchases stored in the local database + /// and extracts the product IDs from each purchase record. + Future> getPurchasedProductsIds(); +} + +class PurchaseRepositoryImpl implements PurchaseRepository { + PurchaseRepositoryImpl(this._db, this._purchaseGateway); + + final SembastDatabase _db; + final _purchasesStore = 'purchases'; + final _purchaseIdKey = 'purchasesId'; + + final PurchaseGateway _purchaseGateway; + + @override + Future purchaseInApp({required String storeId}) => _purchaseGateway.purchase( + identifier: storeId, + ); + + @override + Future> getUserPurchases() async { + final info = await _purchaseGateway.purchasesInfo(); + return info.map((purchase) => purchase).toList(); + } + + @override + Future> isAvailable() async { + final products = await _purchaseGateway.getAvailableProducts(); + return products.map((product) => product.storeProduct.identifier).toList(); + } + + @override + Future updatePurchase({required String purchaseId}) => _db.put( + id: purchaseId, + object: { + _purchaseIdKey: purchaseId, + }, + store: _purchasesStore, + ); + + @override + Future> getPurchasedProductsIds() async { + final purchases = await _db.getAll(store: _purchasesStore); + return purchases.map((purchase) => purchase[_purchaseIdKey] as String).toList(); + } +} diff --git a/lib/data/serializers/collection_memos_serializer.dart b/lib/data/serializers/collection_memos_serializer.dart index 805295b7..71bfb9b0 100644 --- a/lib/data/serializers/collection_memos_serializer.dart +++ b/lib/data/serializers/collection_memos_serializer.dart @@ -1,5 +1,6 @@ import 'package:memo/data/serializers/contributor_serializer.dart'; import 'package:memo/data/serializers/memo_collection_metadata_serializer.dart'; +import 'package:memo/data/serializers/product_info_serializer.dart'; import 'package:memo/data/serializers/serializer.dart'; import 'package:memo/domain/transients/collection_memos.dart'; @@ -11,11 +12,14 @@ class CollectionMemosKeys { static const contributors = 'contributors'; static const tags = 'tags'; static const memosMetadata = 'memos'; + static const isPremium = 'isPremium'; + static const productInfo = 'productInfo'; } class CollectionMemosSerializer implements Serializer> { final memoMetadataSerializer = MemoCollectionMetadataSerializer(); final contributorSerializer = ContributorSerializer(); + final productInfoSerializer = ProductInfoSerializer(); @override CollectionMemos from(Map json) { @@ -23,6 +27,10 @@ class CollectionMemosSerializer implements Serializer?; + final productInfo = rawProductInfo != null ? productInfoSerializer.from(rawProductInfo) : null; final tags = List.from(json[CollectionMemosKeys.tags] as List); @@ -40,6 +48,8 @@ class CollectionMemosSerializer implements Serializer> { final contributorSerializer = ContributorSerializer(); + final productInfoSerializar = ProductInfoSerializer(); @override Collection from(Map json) { @@ -41,6 +45,11 @@ class CollectionSerializer implements Serializer>.from(json[CollectionKeys.contributors] as List); final contributors = rawContributors.map(contributorSerializer.from).toList(); + final isPremium = json[CollectionKeys.isPremium] as bool?; + + final rawProductInfo = json[CollectionKeys.productInfo] as Map?; + final productInfo = rawProductInfo != null ? productInfoSerializar.from(rawProductInfo) : null; + return Collection( id: id, name: name, @@ -52,6 +61,8 @@ class CollectionSerializer implements Serializer MapEntry(key.raw, value)), CollectionKeys.contributors: collection.contributors.map(contributorSerializer.to), CollectionKeys.timeSpentInMillis: collection.timeSpentInMillis, + CollectionKeys.isPremium: collection.isPremium, + if (collection.isPremium) CollectionKeys.productInfo: productInfoSerializar.to(collection.productInfo!), }; } diff --git a/lib/data/serializers/product_info_serializer.dart b/lib/data/serializers/product_info_serializer.dart new file mode 100644 index 00000000..55332505 --- /dev/null +++ b/lib/data/serializers/product_info_serializer.dart @@ -0,0 +1,26 @@ +import 'package:memo/data/serializers/serializer.dart'; +import 'package:memo/domain/models/product_info.dart'; + +class ProductInfoKeys { + static const id = 'id'; + static const price = 'price'; +} + +class ProductInfoSerializer implements Serializer> { + @override + ProductInfo from(Map json) { + final id = json[ProductInfoKeys.id] as String?; + final price = json[ProductInfoKeys.price] as double?; + + return ProductInfo( + id: id ?? '', + price: price ?? 0.0, + ); + } + + @override + Map to(ProductInfo productInfo) => { + ProductInfoKeys.id: productInfo.id, + ProductInfoKeys.price: productInfo.price, + }; +} diff --git a/lib/domain/models/collection.dart b/lib/domain/models/collection.dart index 2e12a7f1..42f0b407 100644 --- a/lib/domain/models/collection.dart +++ b/lib/domain/models/collection.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:memo/domain/enums/memo_difficulty.dart'; import 'package:memo/domain/models/memo_execution.dart'; +import 'package:memo/domain/models/product_info.dart'; import 'package:meta/meta.dart'; /// Metadata for a collection (group) of its associated `Memo`s. @@ -16,6 +17,8 @@ class Collection extends MemoExecutionsMetadata with EquatableMixin implements C required this.tags, required this.uniqueMemosAmount, required this.contributors, + required this.isPremium, + this.productInfo, this.uniqueMemoExecutionsAmount = 0, Map executionsAmounts = const {}, int timeSpentInMillis = 0, @@ -47,6 +50,13 @@ class Collection extends MemoExecutionsMetadata with EquatableMixin implements C @override final List contributors; + /// `true` if this [Collection] is a premium. + @override + final bool isPremium; + + @override + final ProductInfo? productInfo; + @override final int uniqueMemosAmount; @@ -67,6 +77,8 @@ class Collection extends MemoExecutionsMetadata with EquatableMixin implements C category, tags, contributors, + isPremium, + productInfo, uniqueMemoExecutionsAmount, uniqueMemosAmount, ...super.props, @@ -86,6 +98,12 @@ abstract class CollectionMetadata { /// Contributors (or owners) that have created (or made changes) to this collection. List get contributors; + /// Informs whether the collection is premium or not. + bool get isPremium; + + /// Informs the `id` and `price` of the product associated with this collection. + ProductInfo? get productInfo; + /// Total amount of unique `Memo`s associated with this collection. int get uniqueMemosAmount; diff --git a/lib/domain/models/product_info.dart b/lib/domain/models/product_info.dart new file mode 100644 index 00000000..a30c96df --- /dev/null +++ b/lib/domain/models/product_info.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; + +class ProductInfo with EquatableMixin { + ProductInfo({ + required this.id, + required this.price, + }); + + final String id; + final double price; + + @override + List get props => [id, price]; +} diff --git a/lib/domain/services/collection_purchase_services.dart b/lib/domain/services/collection_purchase_services.dart new file mode 100644 index 00000000..dbf8b3c8 --- /dev/null +++ b/lib/domain/services/collection_purchase_services.dart @@ -0,0 +1,75 @@ +import 'dart:async'; + +import 'package:memo/core/env.dart'; +import 'package:memo/data/repositories/collection_repository.dart'; +import 'package:memo/data/repositories/purchase_repository.dart'; + +abstract class CollectionPurchaseServices { + /// Purchases the collection - from [id]. + Future purchaseCollection({required String id}); + + /// Verifies if the collection - from [id] - is visible to the user. + Future isPurchased({required String id}); + + /// Compares all file-based collections (`CollectionMemos`) with collections stored in the user database (`Collection`). + /// + /// Updates purchased collections based on the RevenueCat backend. + Future updatePurchasesIfNeeded(); +} + +class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { + CollectionPurchaseServicesImpl({ + required this.env, + required this.purchaseRepo, + required this.collectionRepo, + }); + + final EnvMetadata env; + + final PurchaseRepository purchaseRepo; + + final CollectionRepository collectionRepo; + + @override + Future purchaseCollection({required String id}) async { + final collection = await collectionRepo.getCollection(id: id); + + await purchaseRepo.purchaseInApp(storeId: collection.productInfo!.id); + await _updatePurchaseCollection(id: id); + } + + Future _updatePurchaseCollection({required String id}) async { + final collection = await collectionRepo.getCollection(id: id); + final purchasesIds = await purchaseRepo.getUserPurchases(); + + if (purchasesIds.contains(collection.productInfo!.id)) { + await purchaseRepo.updatePurchase(purchaseId: collection.productInfo!.id); + } + } + + @override + Future isPurchased({required String id}) async { + final collection = await collectionRepo.getCollection(id: id); + + if (!collection.isPremium) { + return true; + } + + final storeId = collection.productInfo!.id; + + final purchasedProductsList = await purchaseRepo.getPurchasedProductsIds(); + + return purchasedProductsList.contains(storeId); + } + + @override + Future updatePurchasesIfNeeded() async { + final collections = await collectionRepo.getAllCollectionMemos(); + + for (final collection in collections) { + if (collection.isPremium) { + await _updatePurchaseCollection(id: collection.id); + } + } + } +} diff --git a/lib/domain/services/collection_services.dart b/lib/domain/services/collection_services.dart index 796b4cf1..d9bbb218 100644 --- a/lib/domain/services/collection_services.dart +++ b/lib/domain/services/collection_services.dart @@ -1,8 +1,10 @@ import 'package:memo/core/faults/errors/inconsistent_state_error.dart'; +import 'package:memo/data/repositories/purchase_repository.dart'; import 'package:memo/data/repositories/collection_repository.dart'; import 'package:memo/data/repositories/memo_repository.dart'; import 'package:memo/domain/isolated_services/memory_recall_services.dart'; import 'package:memo/domain/models/collection.dart'; +import 'package:memo/domain/services/collection_purchase_services.dart'; import 'package:memo/domain/transients/collection_status.dart'; /// Handles all domain-specific operations associated with [Collection]s. @@ -26,16 +28,26 @@ abstract class CollectionServices { } class CollectionServicesImpl implements CollectionServices { - CollectionServicesImpl({required this.collectionRepo, required this.memoRepo, required this.memoryServices}); + CollectionServicesImpl({ + required this.collectionRepo, + required this.memoRepo, + required this.memoryServices, + required this.purchaseRepo, + required this.collectionPurchaseServices, + }); final CollectionRepository collectionRepo; final MemoRepository memoRepo; final MemoryRecallServices memoryServices; + final PurchaseRepository purchaseRepo; + final CollectionPurchaseServices collectionPurchaseServices; + @override Future>> listenToAllCollectionsStatus() async { final collectionsStream = await collectionRepo.listenToAllCollections(); + // Asynchronously transform the stream due to the async calculations. return collectionsStream.asyncMap( (collections) { @@ -68,7 +80,6 @@ class CollectionServicesImpl implements CollectionServices { if (collection.isCompleted) { memoryRecall = await _getMemosAverageMemoryRecall(collectionId: collection.id); } - return CollectionStatus(collection, memoryRecall); } diff --git a/lib/domain/transients/collection_memos.dart b/lib/domain/transients/collection_memos.dart index d65fa97d..3e55488e 100644 --- a/lib/domain/transients/collection_memos.dart +++ b/lib/domain/transients/collection_memos.dart @@ -1,6 +1,7 @@ import 'package:equatable/equatable.dart'; import 'package:memo/domain/models/collection.dart'; import 'package:memo/domain/models/memo_collection_metadata.dart'; +import 'package:memo/domain/models/product_info.dart'; /// Groups a [CollectionMetadata] with its [memosMetadata]. /// @@ -14,6 +15,8 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { required this.tags, required this.contributors, required this.memosMetadata, + required this.isPremium, + this.productInfo, int uniqueMemoExecutionsAmount = 0, }) : _uniqueMemoExecutionsAmount = uniqueMemoExecutionsAmount, assert(memosMetadata.isNotEmpty, 'must not be an empty list of memos'), @@ -40,6 +43,12 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { @override final List contributors; + @override + final bool isPremium; + + @override + final ProductInfo? productInfo; + @override int get uniqueMemosAmount => memosMetadata.length; @@ -64,5 +73,7 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { _uniqueMemoExecutionsAmount, uniqueMemosAmount, memosMetadata, + isPremium, + productInfo, ]; } diff --git a/pubspec.lock b/pubspec.lock index 35466b23..40d163aa 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,251 +1,262 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: - _fe_analyzer_shared: + _flutterfire_internals: dependency: transitive description: - name: _fe_analyzer_shared - url: "https://pub.dartlang.org" + name: _flutterfire_internals + sha256: "37a42d06068e2fe3deddb2da079a8c4d105f241225ba27b7122b37e9865fd8f7" + url: "https://pub.dev" source: hosted - version: "31.0.0" - analyzer: + version: "1.3.35" + ansicolor: dependency: transitive description: - name: analyzer - url: "https://pub.dartlang.org" + name: ansicolor + sha256: "8bf17a8ff6ea17499e40a2d2542c2f481cd7615760c6d34065cb22bfd22e6880" + url: "https://pub.dev" source: hosted - version: "2.8.0" + version: "2.0.2" archive: dependency: transitive description: name: archive - url: "https://pub.dartlang.org" + sha256: "20071638cbe4e5964a427cfa0e86dce55d060bc7d82d56f3554095d7239a8765" + url: "https://pub.dev" source: hosted - version: "3.2.2" + version: "3.4.2" args: dependency: transitive description: name: args - url: "https://pub.dartlang.org" + sha256: "7cf60b9f0cc88203c5a190b4cd62a99feea42759a7fa695010eb5de1c0b2252a" + url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.5.0" async: dependency: transitive description: name: async - url: "https://pub.dartlang.org" + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + url: "https://pub.dev" source: hosted - version: "2.8.2" + version: "2.11.0" boolean_selector: dependency: transitive description: name: boolean_selector - url: "https://pub.dartlang.org" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.1" characters: dependency: transitive description: name: characters - url: "https://pub.dartlang.org" + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.0" charcode: dependency: transitive description: name: charcode - url: "https://pub.dartlang.org" + sha256: fb98c0f6d12c920a02ee2d998da788bca066ca5f148492b7085ee23372b12306 + url: "https://pub.dev" source: hosted version: "1.3.1" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" cli_util: dependency: transitive description: name: cli_util - url: "https://pub.dartlang.org" + sha256: c05b7406fdabc7a49a3929d4af76bcaccbbffcbcdcf185b082e1ae07da323d19 + url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.4.1" clock: dependency: transitive description: name: clock - url: "https://pub.dartlang.org" + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + url: "https://pub.dev" source: hosted - version: "1.1.0" + version: "1.1.1" collection: dependency: "direct main" description: name: collection - url: "https://pub.dartlang.org" + sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.18.0" convert: dependency: transitive description: name: convert - url: "https://pub.dartlang.org" + sha256: f08428ad63615f96a27e34221c65e1a451439b5f26030f78d790f461c686d65d + url: "https://pub.dev" source: hosted version: "3.0.1" - coverage: - dependency: transitive - description: - name: coverage - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.3" - cross_file: - dependency: transitive - description: - name: cross_file - url: "https://pub.dartlang.org" - source: hosted - version: "0.3.2" crypto: dependency: transitive description: name: crypto - url: "https://pub.dartlang.org" + sha256: cf75650c66c0316274e21d7c43d3dea246273af5955bd94e8184837cd577575c + url: "https://pub.dev" source: hosted version: "3.0.1" csslib: dependency: transitive description: name: csslib - url: "https://pub.dartlang.org" + sha256: d1cd6d6e4b39a4ad295204722b8608f19981677b223f3e942c0b5a33dcf57ec0 + url: "https://pub.dev" source: hosted version: "0.17.1" - device_info_plus: - dependency: transitive - description: - name: device_info_plus - url: "https://pub.dartlang.org" - source: hosted - version: "3.2.2" - device_info_plus_linux: + dart_quill_delta: dependency: transitive description: - name: device_info_plus_linux - url: "https://pub.dartlang.org" + name: dart_quill_delta + sha256: "6aa89f0903ca3e70f5ceeb1d75d722f6ca583e87a2a8893c7b9f42f7a947f6e5" + url: "https://pub.dev" source: hosted - version: "2.1.1" - device_info_plus_macos: + version: "9.6.0" + device_info_plus: dependency: transitive description: - name: device_info_plus_macos - url: "https://pub.dartlang.org" + name: device_info_plus + sha256: eead12d1a1ed83d8283ab4c2f3fca23ac4082f29f25f29dff0f758f57d06ec91 + url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "10.1.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.0+1" - device_info_plus_web: - dependency: transitive - description: - name: device_info_plus_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - device_info_plus_windows: - dependency: transitive - description: - name: device_info_plus_windows - url: "https://pub.dartlang.org" + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "7.0.0" diff_match_patch: dependency: transitive description: name: diff_match_patch - url: "https://pub.dartlang.org" + sha256: "2efc9e6e8f449d0abe15be240e2c2a3bcd977c8d126cfd70598aee60af35c0a4" + url: "https://pub.dev" source: hosted version: "0.4.1" equatable: dependency: "direct main" description: name: equatable - url: "https://pub.dartlang.org" + sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + url: "https://pub.dev" source: hosted - version: "2.0.3" + version: "2.0.5" fake_async: dependency: transitive description: name: fake_async - url: "https://pub.dartlang.org" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.3.1" ffi: dependency: transitive description: name: ffi - url: "https://pub.dartlang.org" + sha256: "493f37e7df1804778ff3a53bd691d8692ddf69702cf4c1c1096a2e41b4779e21" + url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "2.1.2" file: dependency: transitive description: name: file - url: "https://pub.dartlang.org" + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + url: "https://pub.dev" source: hosted - version: "6.1.2" + version: "7.0.0" firebase_analytics: dependency: "direct main" description: name: firebase_analytics - url: "https://pub.dartlang.org" + sha256: dbf1e7ab22cfb1f4a4adb103b46a26276b4edc593d4a78ef6fb942bafc92e035 + url: "https://pub.dev" source: hosted - version: "9.1.2" + version: "10.10.7" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - url: "https://pub.dartlang.org" + sha256: "3729b74f8cf1d974a27ba70332ecb55ff5ff560edc8164a6469f4a055b429c37" + url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.10.8" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - url: "https://pub.dartlang.org" + sha256: "019cd7eee74254d33fbd2e29229367ce33063516bf6b3258a341d89e3b0f1655" + url: "https://pub.dev" source: hosted - version: "0.4.0+8" + version: "0.5.7+7" firebase_core: dependency: "direct main" description: name: firebase_core - url: "https://pub.dartlang.org" + sha256: "26de145bb9688a90962faec6f838247377b0b0d32cc0abecd9a4e43525fc856c" + url: "https://pub.dev" source: hosted - version: "1.13.1" + version: "2.32.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - url: "https://pub.dartlang.org" + sha256: "1003a5a03a61fc9a22ef49f37cbcb9e46c86313a7b2e7029b9390cf8c6fc32cb" + url: "https://pub.dev" source: hosted - version: "4.2.5" + version: "5.1.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - url: "https://pub.dartlang.org" + sha256: "23509cb3cddfb3c910c143279ac3f07f06d3120f7d835e4a5d4b42558e978712" + url: "https://pub.dev" source: hosted - version: "1.6.1" + version: "2.17.3" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - url: "https://pub.dartlang.org" + sha256: "9897c01efaa950d2f6da8317d12452749a74dc45f33b46390a14cfe28067f271" + url: "https://pub.dev" source: hosted - version: "2.5.3" + version: "3.5.7" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - url: "https://pub.dartlang.org" + sha256: "16a71e08fbf6e00382816e1b13397898c29a54fa0ad969c2c2a3b82a704877f0" + url: "https://pub.dev" source: hosted - version: "3.2.1" + version: "3.6.35" + fixnum: + dependency: transitive + description: + name: fixnum + sha256: "25517a4deb0c03aa0f32fd12db525856438902d9c16536311e76cdc57b31d7d1" + url: "https://pub.dev" + source: hosted + version: "1.1.0" flutter: dependency: "direct main" description: flutter @@ -255,79 +266,111 @@ packages: dependency: transitive description: name: flutter_colorpicker - url: "https://pub.dartlang.org" + sha256: "969de5f6f9e2a570ac660fb7b501551451ea2a1ab9e2097e89475f60e07816ea" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.1.0" flutter_hooks: dependency: "direct main" description: name: flutter_hooks - url: "https://pub.dartlang.org" + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" source: hosted - version: "0.18.2+1" - flutter_inappwebview: + version: "0.20.5" + flutter_keyboard_visibility: dependency: transitive description: - name: flutter_inappwebview - url: "https://pub.dartlang.org" + name: flutter_keyboard_visibility + sha256: "98664be7be0e3ffca00de50f7f6a287ab62c763fc8c762e0a21584584a3ff4f8" + url: "https://pub.dev" source: hosted - version: "5.3.2" - flutter_keyboard_visibility: + version: "6.0.0" + flutter_keyboard_visibility_linux: dependency: transitive description: - name: flutter_keyboard_visibility - url: "https://pub.dartlang.org" + name: flutter_keyboard_visibility_linux + sha256: "6fba7cd9bb033b6ddd8c2beb4c99ad02d728f1e6e6d9b9446667398b2ac39f08" + url: "https://pub.dev" source: hosted - version: "5.2.0" + version: "1.0.0" + flutter_keyboard_visibility_macos: + dependency: transitive + description: + name: flutter_keyboard_visibility_macos + sha256: c5c49b16fff453dfdafdc16f26bdd8fb8d55812a1d50b0ce25fc8d9f2e53d086 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_keyboard_visibility_platform_interface: dependency: transitive description: name: flutter_keyboard_visibility_platform_interface - url: "https://pub.dartlang.org" + sha256: e43a89845873f7be10cb3884345ceb9aebf00a659f479d1c8f4293fcb37022a4 + url: "https://pub.dev" source: hosted version: "2.0.0" flutter_keyboard_visibility_web: dependency: transitive description: name: flutter_keyboard_visibility_web - url: "https://pub.dartlang.org" + sha256: d3771a2e752880c79203f8d80658401d0c998e4183edca05a149f5098ce6e3d1 + url: "https://pub.dev" source: hosted version: "2.0.0" + flutter_keyboard_visibility_windows: + dependency: transitive + description: + name: flutter_keyboard_visibility_windows + sha256: fc4b0f0b6be9b93ae527f3d527fb56ee2d918cd88bbca438c478af7bcfd0ef73 + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_launcher_icons: dependency: "direct dev" description: name: flutter_launcher_icons - url: "https://pub.dartlang.org" + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" + url: "https://pub.dev" source: hosted - version: "0.9.2" + version: "0.13.1" + flutter_localizations: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" flutter_native_splash: dependency: "direct dev" description: name: flutter_native_splash - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.3" - flutter_plugin_android_lifecycle: - dependency: transitive - description: - name: flutter_plugin_android_lifecycle - url: "https://pub.dartlang.org" + sha256: aa06fec78de2190f3db4319dd60fdc8d12b2626e93ef9828633928c2dcaea840 + url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.4.1" flutter_quill: dependency: "direct main" description: name: flutter_quill - url: "https://pub.dartlang.org" + sha256: "7c37e0761e7101809af8e7994d6aef58519cb8035aa307be66867c710d64abf0" + url: "https://pub.dev" + source: hosted + version: "9.6.0" + flutter_quill_delta_from_html: + dependency: transitive + description: + name: flutter_quill_delta_from_html + sha256: "3cddb27325e005459fd2bbd6af4d813525d5b2bcb6e9828ab2b449f70eaf1147" + url: "https://pub.dev" source: hosted - version: "4.0.10" + version: "1.3.12" flutter_riverpod: dependency: "direct main" description: name: flutter_riverpod - url: "https://pub.dartlang.org" + sha256: "0f1974eff5bbe774bf1d870e406fc6f29e3d6f1c46bd9c58e7172ff68a785d7d" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.5.1" flutter_test: dependency: "direct dev" description: flutter @@ -338,697 +381,547 @@ packages: description: flutter source: sdk version: "0.0.0" - frontend_server_client: - dependency: transitive - description: - name: frontend_server_client - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.2" - gallery_saver: + freezed_annotation: dependency: transitive description: - name: gallery_saver - url: "https://pub.dartlang.org" + name: freezed_annotation + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 + url: "https://pub.dev" source: hosted - version: "2.3.2" - gettext_parser: - dependency: transitive - description: - name: gettext_parser - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0" - glob: - dependency: transitive - description: - name: glob - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" + version: "2.4.4" hooks_riverpod: dependency: "direct main" description: name: hooks_riverpod - url: "https://pub.dartlang.org" + sha256: "45b2030a18bcd6dbd680c2c91bc3b33e3fe7c323e3acb5ecec93a613e2fbaa8a" + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.5.1" html: dependency: transitive description: name: html - url: "https://pub.dartlang.org" + sha256: "3a7812d5bcd2894edf53dfaf8cd640876cf6cef50a8f238745c8b8120ea74d3a" + url: "https://pub.dev" source: hosted - version: "0.15.0" + version: "0.15.4" http: dependency: transitive description: name: http - url: "https://pub.dartlang.org" + sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 + url: "https://pub.dev" source: hosted - version: "0.13.4" - http_multi_server: - dependency: transitive - description: - name: http_multi_server - url: "https://pub.dartlang.org" - source: hosted - version: "3.2.0" + version: "1.2.2" http_parser: dependency: transitive description: name: http_parser - url: "https://pub.dartlang.org" + sha256: e362d639ba3bc07d5a71faebb98cde68c05bfbcfbbb444b60b6f60bb67719185 + url: "https://pub.dev" source: hosted version: "4.0.0" - i18n_extension: - dependency: transitive - description: - name: i18n_extension - url: "https://pub.dartlang.org" - source: hosted - version: "4.2.0" image: dependency: transitive description: name: image - url: "https://pub.dartlang.org" + sha256: "2237616a36c0d69aef7549ab439b833fb7f9fb9fc861af2cc9ac3eedddd69ca8" + url: "https://pub.dev" source: hosted - version: "3.1.3" - image_picker: + version: "4.2.0" + intl: dependency: transitive description: - name: image_picker - url: "https://pub.dartlang.org" + name: intl + sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + url: "https://pub.dev" source: hosted - version: "0.8.4+11" - image_picker_for_web: + version: "0.19.0" + js: dependency: transitive description: - name: image_picker_for_web - url: "https://pub.dartlang.org" + name: js + sha256: d9bdfd70d828eeb352390f81b18d6a354ef2044aa28ef25682079797fa7cd174 + url: "https://pub.dev" source: hosted - version: "2.1.6" - image_picker_platform_interface: + version: "0.6.3" + json_annotation: dependency: transitive description: - name: image_picker_platform_interface - url: "https://pub.dartlang.org" + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" source: hosted - version: "2.4.4" - intl: - dependency: transitive + version: "4.9.0" + layoutr: + dependency: "direct main" description: - name: intl - url: "https://pub.dartlang.org" + name: layoutr + sha256: "7617513c48cc71e5d4d1f7a80ac37272280df9f2193e3c577fad99b69c2c2613" + url: "https://pub.dev" source: hosted - version: "0.17.0" - io: + version: "1.0.0" + leak_tracker: dependency: transitive description: - name: io - url: "https://pub.dartlang.org" + name: leak_tracker + sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + url: "https://pub.dev" source: hosted - version: "1.0.3" - js: + version: "10.0.4" + leak_tracker_flutter_testing: dependency: transitive description: - name: js - url: "https://pub.dartlang.org" + name: leak_tracker_flutter_testing + sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + url: "https://pub.dev" source: hosted - version: "0.6.3" - layoutr: - dependency: "direct main" + version: "3.0.3" + leak_tracker_testing: + dependency: transitive description: - name: layoutr - url: "https://pub.dartlang.org" + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" source: hosted - version: "1.0.0" - logging: + version: "3.0.1" + markdown: dependency: transitive description: - name: logging - url: "https://pub.dartlang.org" + name: markdown + sha256: ef2a1298144e3f985cc736b22e0ccdaf188b5b3970648f2d9dc13efd1d9df051 + url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "7.2.2" matcher: dependency: transitive description: name: matcher - url: "https://pub.dartlang.org" + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + url: "https://pub.dev" source: hosted - version: "0.12.11" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: name: material_color_utilities - url: "https://pub.dartlang.org" + sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + url: "https://pub.dev" source: hosted - version: "0.1.3" + version: "0.8.0" meta: dependency: "direct main" description: name: meta - url: "https://pub.dartlang.org" + sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + url: "https://pub.dev" source: hosted - version: "1.7.0" - mime: - dependency: transitive - description: - name: mime - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" + version: "1.12.0" mocktail: dependency: "direct dev" description: name: mocktail - url: "https://pub.dartlang.org" + sha256: "890df3f9688106f25755f26b1c60589a92b3ab91a22b8b224947ad041bf172d8" + url: "https://pub.dev" source: hosted - version: "0.3.0" - node_preamble: - dependency: transitive - description: - name: node_preamble - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - package_config: - dependency: transitive - description: - name: package_config - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.2" + version: "1.0.4" package_info_plus: dependency: "direct main" description: name: package_info_plus - url: "https://pub.dartlang.org" - source: hosted - version: "1.4.0" - package_info_plus_linux: - dependency: transitive - description: - name: package_info_plus_linux - url: "https://pub.dartlang.org" + sha256: cb44f49b6e690fa766f023d5b22cac6b9affe741dd792b6ac7ad4fabe0d7b097 + url: "https://pub.dev" source: hosted - version: "1.0.3" - package_info_plus_macos: - dependency: transitive - description: - name: package_info_plus_macos - url: "https://pub.dartlang.org" - source: hosted - version: "1.3.0" + version: "6.0.0" package_info_plus_platform_interface: dependency: transitive description: name: package_info_plus_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.2" - package_info_plus_web: - dependency: transitive - description: - name: package_info_plus_web - url: "https://pub.dartlang.org" + sha256: "9bc8ba46813a4cc42c66ab781470711781940780fd8beddd0c3da62506d3a6c6" + url: "https://pub.dev" source: hosted - version: "1.0.4" - package_info_plus_windows: - dependency: transitive - description: - name: package_info_plus_windows - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.4" + version: "2.0.1" path: dependency: "direct main" description: name: path - url: "https://pub.dartlang.org" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.0" path_provider: dependency: "direct main" description: name: path_provider - url: "https://pub.dartlang.org" + sha256: e92dee4d38a9044605cb3fb253e9b46eb9375dfcad4515d0379b44ac90797568 + url: "https://pub.dev" source: hosted version: "2.0.9" path_provider_android: dependency: transitive description: name: path_provider_android - url: "https://pub.dartlang.org" + sha256: "8b759fb6c74955931e87f550cc9e890b0cccb7ef8e710943973efeaa9695c54d" + url: "https://pub.dev" source: hosted version: "2.0.12" path_provider_ios: dependency: transitive description: name: path_provider_ios - url: "https://pub.dartlang.org" + sha256: "943b76e54056386432cdc2731cb303e2f580346b61a1fc73819721767be72309" + url: "https://pub.dev" source: hosted version: "2.0.8" path_provider_linux: dependency: transitive description: name: path_provider_linux - url: "https://pub.dartlang.org" + sha256: ffbb8cc9ed2c9ec0e4b7a541e56fd79b138e8f47d2fb86815f15358a349b3b57 + url: "https://pub.dev" source: hosted - version: "2.1.5" + version: "2.1.11" path_provider_macos: dependency: transitive description: name: path_provider_macos - url: "https://pub.dartlang.org" + sha256: "0adeb313e1f2c3fc52baeeee59b0fe9c2d1f7da56fd96a9234e1702ec653a453" + url: "https://pub.dev" source: hosted version: "2.0.5" path_provider_platform_interface: dependency: transitive description: name: path_provider_platform_interface - url: "https://pub.dartlang.org" + sha256: "3dc0d51b07f85fec3746d9f4e8d31c73bb173cafa2e763f03f8df2e8d1878882" + url: "https://pub.dev" source: hosted version: "2.0.3" path_provider_windows: dependency: transitive description: name: path_provider_windows - url: "https://pub.dartlang.org" + sha256: "1cb68ba4cd3a795033de62ba1b7b4564dace301f952de6bfb3cd91b202b6ee96" + url: "https://pub.dev" source: hosted - version: "2.0.5" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.dartlang.org" - source: hosted - version: "1.11.1" + version: "2.1.7" petitparser: dependency: transitive description: name: petitparser - url: "https://pub.dartlang.org" + sha256: c15605cd28af66339f8eb6fbe0e541bfe2d1b72d5825efc6598f3e0a31b9ad27 + url: "https://pub.dev" source: hosted - version: "4.4.0" - photo_view: - dependency: transitive - description: - name: photo_view - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.0" + version: "6.0.2" platform: dependency: transitive description: name: platform - url: "https://pub.dartlang.org" + sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" + url: "https://pub.dev" source: hosted version: "3.1.0" plugin_platform_interface: dependency: transitive description: name: plugin_platform_interface - url: "https://pub.dartlang.org" + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" source: hosted - version: "2.1.2" - pool: + version: "2.1.8" + pointycastle: dependency: transitive description: - name: pool - url: "https://pub.dartlang.org" + name: pointycastle + sha256: "4be0097fcf3fd3e8449e53730c631200ebc7b88016acecab2b0da2f0149222fe" + url: "https://pub.dev" source: hosted - version: "1.5.0" - process: - dependency: transitive - description: - name: process - url: "https://pub.dartlang.org" - source: hosted - version: "4.2.4" - pub_semver: - dependency: transitive + version: "3.9.1" + purchases_flutter: + dependency: "direct main" description: - name: pub_semver - url: "https://pub.dartlang.org" + name: purchases_flutter + sha256: "83f58c4ef331c06af0548b009632ec2c45b119b87bf02a74f8f77a40e0f64bad" + url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "6.30.2" quiver: dependency: transitive description: name: quiver - url: "https://pub.dartlang.org" + sha256: b1c1ac5ce6688d77f65f3375a9abb9319b3cb32486bdc7a1e0fdf004d7ba4e47 + url: "https://pub.dev" source: hosted - version: "3.0.1+1" + version: "3.2.1" riverpod: dependency: transitive description: name: riverpod - url: "https://pub.dartlang.org" + sha256: f21b32ffd26a36555e501b04f4a5dca43ed59e16343f1a30c13632b2351dfa4d + url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "2.5.1" rxdart: dependency: "direct main" description: name: rxdart - url: "https://pub.dartlang.org" + sha256: bc2d2b17b87fab32e2dca53ca3066d3147de6f96c74d76cfe1a379a24239c46d + url: "https://pub.dev" source: hosted version: "0.27.3" sembast: dependency: "direct main" description: name: sembast - url: "https://pub.dartlang.org" + sha256: "208d4f37cdd4fb86f43239a07bdadc444ae12a46370d36848bee019b56443ff1" + url: "https://pub.dev" source: hosted version: "3.2.0" - shelf: - dependency: transitive - description: - name: shelf - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - shelf_packages_handler: - dependency: transitive - description: - name: shelf_packages_handler - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.0" - shelf_static: - dependency: transitive - description: - name: shelf_static - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - shelf_web_socket: - dependency: transitive - description: - name: shelf_web_socket - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.1" sky_engine: dependency: transitive description: flutter source: sdk version: "0.0.99" - source_map_stack_trace: - dependency: transitive - description: - name: source_map_stack_trace - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.0" - source_maps: - dependency: transitive - description: - name: source_maps - url: "https://pub.dartlang.org" - source: hosted - version: "0.10.10" source_span: dependency: transitive description: name: source_span - url: "https://pub.dartlang.org" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + url: "https://pub.dev" source: hosted - version: "1.8.1" + version: "1.10.0" sprintf: dependency: transitive description: name: sprintf - url: "https://pub.dartlang.org" + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "7.0.0" stack_trace: dependency: transitive description: name: stack_trace - url: "https://pub.dartlang.org" + sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.11.1" state_notifier: dependency: transitive description: name: state_notifier - url: "https://pub.dartlang.org" + sha256: "8fe42610f179b843b12371e40db58c9444f8757f8b69d181c97e50787caed289" + url: "https://pub.dev" source: hosted version: "0.7.2+1" stream_channel: dependency: transitive description: name: stream_channel - url: "https://pub.dartlang.org" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + url: "https://pub.dev" source: hosted - version: "2.1.0" + version: "2.1.2" strict: dependency: "direct dev" description: name: strict - url: "https://pub.dartlang.org" + sha256: "23bc2d1c00afa1685324b547c9a63e42f312668e737d6096142258c383bcb456" + url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "2.1.0" string_scanner: dependency: transitive description: name: string_scanner - url: "https://pub.dartlang.org" - source: hosted - version: "1.1.0" - string_validator: - dependency: transitive - description: - name: string_validator - url: "https://pub.dartlang.org" + sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "1.2.0" synchronized: dependency: transitive description: name: synchronized - url: "https://pub.dartlang.org" + sha256: "271977ff1e9e82ceefb4f08424b8839f577c1852e0726b5ce855311b46d3ef83" + url: "https://pub.dev" source: hosted version: "3.0.0" term_glyph: dependency: transitive description: name: term_glyph - url: "https://pub.dartlang.org" - source: hosted - version: "1.2.0" - test: - dependency: transitive - description: - name: test - url: "https://pub.dartlang.org" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + url: "https://pub.dev" source: hosted - version: "1.19.5" + version: "1.2.1" test_api: dependency: transitive description: name: test_api - url: "https://pub.dartlang.org" + sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + url: "https://pub.dev" source: hosted - version: "0.4.8" - test_core: - dependency: transitive - description: - name: test_core - url: "https://pub.dartlang.org" - source: hosted - version: "0.4.9" + version: "0.7.0" tuple: dependency: "direct main" description: name: tuple - url: "https://pub.dartlang.org" + sha256: fe3ae4f0dca3f9aac0888e2e0d117b642ce283a82d7017b54136290c0a3b0dd3 + url: "https://pub.dev" source: hosted version: "2.0.0" typed_data: dependency: transitive description: name: typed_data - url: "https://pub.dartlang.org" + sha256: "53bdf7e979cfbf3e28987552fd72f637e63f3c8724c9e56d9246942dc2fa36ee" + url: "https://pub.dev" source: hosted version: "1.3.0" universal_io: dependency: transitive description: name: universal_io - url: "https://pub.dartlang.org" + sha256: "1722b2dcc462b4b2f3ee7d188dad008b6eb4c40bbd03a3de451d82c78bba9aad" + url: "https://pub.dev" source: hosted - version: "2.0.4" + version: "2.2.2" url_launcher: dependency: "direct main" description: name: url_launcher - url: "https://pub.dartlang.org" + sha256: "21b704ce5fa560ea9f3b525b43601c678728ba46725bab9b01187b4831377ed3" + url: "https://pub.dev" source: hosted - version: "6.0.20" + version: "6.3.0" url_launcher_android: dependency: transitive description: name: url_launcher_android - url: "https://pub.dartlang.org" + sha256: "95d8027db36a0e52caf55680f91e33ea6aa12a3ce608c90b06f4e429a21067ac" + url: "https://pub.dev" source: hosted - version: "6.0.15" + version: "6.3.5" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - url: "https://pub.dartlang.org" + sha256: e43b677296fadce447e987a2f519dcf5f6d1e527dc35d01ffab4fff5b8a7063e + url: "https://pub.dev" source: hosted - version: "6.0.15" + version: "6.3.1" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - url: "https://pub.dartlang.org" + sha256: ab360eb661f8879369acac07b6bb3ff09d9471155357da8443fd5d3cf7363811 + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.1" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - url: "https://pub.dartlang.org" + sha256: "9a1a42d5d2d95400c795b2914c36fdcb525870c752569438e4ebb09a2b5d90de" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.2.0" url_launcher_platform_interface: dependency: transitive description: name: url_launcher_platform_interface - url: "https://pub.dartlang.org" + sha256: "552f8a1e663569be95a8190206a38187b531910283c3e982193e4f2733f01029" + url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.3.2" url_launcher_web: dependency: transitive description: name: url_launcher_web - url: "https://pub.dartlang.org" + sha256: "8d9e750d8c9338601e709cd0885f95825086bd8b642547f26bda435aade95d8a" + url: "https://pub.dev" source: hosted - version: "2.0.9" + version: "2.3.1" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - url: "https://pub.dartlang.org" + sha256: "49c10f879746271804767cb45551ec5592cdab00ee105c06dddde1a98f73b185" + url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "3.1.2" uuid: dependency: "direct main" description: name: uuid - url: "https://pub.dartlang.org" + sha256: "83d37c7ad7aaf9aa8e275490669535c8080377cfa7a7004c24dfac53afffaa90" + url: "https://pub.dev" source: hosted - version: "3.0.6" + version: "4.4.2" vector_math: dependency: transitive description: name: vector_math - url: "https://pub.dartlang.org" + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" source: hosted - version: "2.1.1" - video_player: - dependency: transitive - description: - name: video_player - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.0" - video_player_android: - dependency: transitive - description: - name: video_player_android - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.1" - video_player_avfoundation: - dependency: transitive - description: - name: video_player_avfoundation - url: "https://pub.dartlang.org" - source: hosted - version: "2.3.1" - video_player_platform_interface: - dependency: transitive - description: - name: video_player_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "5.1.0" - video_player_web: - dependency: transitive - description: - name: video_player_web - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.7" + version: "2.1.4" vm_service: dependency: transitive description: name: vm_service - url: "https://pub.dartlang.org" + sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + url: "https://pub.dev" source: hosted - version: "7.5.0" - watcher: + version: "14.2.1" + web: dependency: transitive description: - name: watcher - url: "https://pub.dartlang.org" + name: web + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" + url: "https://pub.dev" source: hosted - version: "1.0.1" - web_socket_channel: + version: "0.5.1" + win32: dependency: transitive description: - name: web_socket_channel - url: "https://pub.dartlang.org" + name: win32 + sha256: a79dbe579cb51ecd6d30b17e0cae4e0ea15e2c0e66f69ad4198f22a6789e94f4 + url: "https://pub.dev" source: hosted - version: "2.1.0" - webkit_inspection_protocol: + version: "5.5.1" + win32_registry: dependency: transitive description: - name: webkit_inspection_protocol - url: "https://pub.dartlang.org" + name: win32_registry + sha256: "10589e0d7f4e053f2c61023a31c9ce01146656a70b7b7f0828c0b46d7da2a9bb" + url: "https://pub.dev" source: hosted - version: "1.0.0" - win32: - dependency: transitive - description: - name: win32 - url: "https://pub.dartlang.org" - source: hosted - version: "2.4.2" + version: "1.1.3" xdg_directories: dependency: transitive description: name: xdg_directories - url: "https://pub.dartlang.org" + sha256: faea9dee56b520b55a566385b84f2e8de55e7496104adada9962e0bd11bcff1d + url: "https://pub.dev" source: hosted - version: "0.2.0+1" + version: "1.0.4" xml: dependency: transitive description: name: xml - url: "https://pub.dartlang.org" + sha256: b015a8ad1c488f66851d762d3090a21c600e479dc75e68328c52774040cf9226 + url: "https://pub.dev" source: hosted - version: "5.3.1" + version: "6.5.0" yaml: dependency: transitive description: name: yaml - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" - youtube_player_flutter: - dependency: transitive - description: - name: youtube_player_flutter - url: "https://pub.dartlang.org" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" + url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "3.1.2" sdks: - dart: ">=2.16.1 <3.0.0" - flutter: ">=2.10.3" + dart: ">=3.4.3 <4.0.0" + flutter: ">=3.22.0" diff --git a/pubspec.yaml b/pubspec.yaml index b7ad03b1..c205db54 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ publish_to: "none" version: 0.2.0+0 environment: - sdk: ">=2.16.0 <3.0.0" + sdk: ">=2.17.0 <3.0.0" flutter: 2.10.3 dependencies: @@ -51,6 +51,7 @@ dependencies: # Keep dependency locked, as we need it to be the exact same in `memo-editor` flutter_quill: ^9.3.3 + purchases_flutter: ^6.30.1 dev_dependencies: flutter_test: diff --git a/test/application/widgets/theme/custom_button_test.dart b/test/application/widgets/theme/custom_button_test.dart index eafa7e5b..80ad3fe1 100644 --- a/test/application/widgets/theme/custom_button_test.dart +++ b/test/application/widgets/theme/custom_button_test.dart @@ -179,7 +179,7 @@ void main() { await pumpProviderScoped(tester, textButton); final buttonRenderBox = find.byType(CustomTextButton).first.evaluate().single.renderObject! as RenderBox; - expect(buttonRenderBox.size.width, expectedWidth); + expect(buttonRenderBox.size.width.round(), expectedWidth); }); }); } diff --git a/test/data/serializers/collection_memos_serializer_test.dart b/test/data/serializers/collection_memos_serializer_test.dart index ec788b29..d9e2a3d6 100644 --- a/test/data/serializers/collection_memos_serializer_test.dart +++ b/test/data/serializers/collection_memos_serializer_test.dart @@ -2,6 +2,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:memo/data/serializers/collection_memos_serializer.dart'; import 'package:memo/domain/models/collection.dart'; import 'package:memo/domain/models/memo_collection_metadata.dart'; +import 'package:memo/domain/models/product_info.dart'; import 'package:memo/domain/transients/collection_memos.dart'; import '../../fixtures/fixtures.dart' as fixtures; @@ -15,19 +16,22 @@ void main() { description: 'This collection represents a collection.', category: 'Category', tags: const ['Tag 1', 'Tag 2'], + isPremium: true, + productInfo: ProductInfo(id: '', price: 0.0), contributors: [const Contributor(name: 'name')], memosMetadata: [ MemoCollectionMetadata( uniqueId: '1', rawQuestion: fakes.question, rawAnswer: fakes.answer, - ) + ), ], ); Map completeFixture() => fixtures.collectionMemos() ..[CollectionMemosKeys.memosMetadata] = [fixtures.memoCollectionMetadata()] - ..[CollectionMemosKeys.contributors] = [fixtures.contributor()]; + ..[CollectionMemosKeys.contributors] = [fixtures.contributor()] + ..[CollectionMemosKeys.productInfo] = fixtures.productInfo(); test('CollectionMemosSerializer should correctly encode/decode a CollectionMemos', () { final rawCollection = completeFixture(); diff --git a/test/data/serializers/collection_serializer_test.dart b/test/data/serializers/collection_serializer_test.dart index ed7de15f..8b8a0e35 100644 --- a/test/data/serializers/collection_serializer_test.dart +++ b/test/data/serializers/collection_serializer_test.dart @@ -3,6 +3,7 @@ import 'package:memo/data/serializers/collection_serializer.dart'; import 'package:memo/data/serializers/memo_difficulty_parser.dart'; import 'package:memo/domain/enums/memo_difficulty.dart'; import 'package:memo/domain/models/collection.dart'; +import 'package:memo/domain/models/product_info.dart'; import '../../fixtures/fixtures.dart' as fixtures; @@ -13,13 +14,16 @@ void main() { name: 'My Collection', description: 'This collection represents a collection.', category: 'Category', + isPremium: true, + productInfo: ProductInfo(id: '', price: 0.0), contributors: const [Contributor(name: 'name')], tags: const ['Tag 1', 'Tag 2'], uniqueMemosAmount: 1, ); - Map completeFixture() => - fixtures.collection()..[CollectionKeys.contributors] = [fixtures.contributor()]; + Map completeFixture() => fixtures.collection() + ..[CollectionKeys.contributors] = [fixtures.contributor()] + ..[CollectionKeys.productInfo] = fixtures.productInfo(); test('CollectionSerializer should correctly encode/decode a Collection', () { final rawCollection = completeFixture(); @@ -102,6 +106,8 @@ void main() { category: 'Category', contributors: const [Contributor(name: 'name')], tags: const ['Tag 1', 'Tag 2'], + isPremium: true, + productInfo: ProductInfo(price: 0.0, id: ''), uniqueMemosAmount: 1, uniqueMemoExecutionsAmount: 1, executionsAmounts: const {MemoDifficulty.easy: 1}, diff --git a/test/data/serializers/product_info_serializar_test.dart b/test/data/serializers/product_info_serializar_test.dart new file mode 100644 index 00000000..85864f8c --- /dev/null +++ b/test/data/serializers/product_info_serializar_test.dart @@ -0,0 +1,36 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:memo/data/serializers/product_info_serializer.dart'; +import 'package:memo/domain/models/product_info.dart'; + +import '../../fixtures/fixtures.dart' as fixtures; + +void main() { + final serializer = ProductInfoSerializer(); + final testProductInfo = ProductInfo( + id: '', + price: 0.0, + ); + + test('ProductInfoSerializer should correctly encode/decode a ProductInfo', () { + final rawProductInfo = fixtures.productInfo(); + + final decodedProductInfo = serializer.from(rawProductInfo); + expect(decodedProductInfo, testProductInfo); + + final encodedProductInfo = serializer.to(decodedProductInfo); + expect(encodedProductInfo, rawProductInfo); + }); + + test('ProductInfoSerializer should fail to decode without required properties', () { + final rawProductInfo = fixtures.productInfo() + ..[ProductInfoKeys.id] = '' + ..[ProductInfoKeys.price] = 0.0; + + final decodedProductInfo = serializer.from(rawProductInfo); + + final allPropsProductInfo = ProductInfo(id: '', price: 0.0); + + expect(decodedProductInfo, allPropsProductInfo); + expect(rawProductInfo, serializer.to(decodedProductInfo)); + }); +} diff --git a/test/domain/models/collection_test.dart b/test/domain/models/collection_test.dart index 095d599b..0c26fce3 100644 --- a/test/domain/models/collection_test.dart +++ b/test/domain/models/collection_test.dart @@ -1,6 +1,7 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:memo/domain/enums/memo_difficulty.dart'; import 'package:memo/domain/models/collection.dart'; +import 'package:memo/domain/models/product_info.dart'; void main() { Collection newCollection({ @@ -9,6 +10,7 @@ void main() { int uniqueMemoExecutionsAmount = 0, int timeSpentInMillis = 0, List? contributors, + ProductInfo? productInfo, }) { return Collection( id: 'id', @@ -16,6 +18,8 @@ void main() { description: 'description', category: 'category', tags: const [], + isPremium: false, + productInfo: productInfo ?? ProductInfo(price: 0.1, id: 'id'), contributors: contributors ?? const [Contributor(name: 'name')], uniqueMemosAmount: uniqueMemosAmount, executionsAmounts: executionsAmounts, diff --git a/test/domain/transients/collection_memos_test.dart b/test/domain/transients/collection_memos_test.dart index 52e43016..308a44db 100644 --- a/test/domain/transients/collection_memos_test.dart +++ b/test/domain/transients/collection_memos_test.dart @@ -1,12 +1,14 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:memo/domain/models/collection.dart'; import 'package:memo/domain/models/memo_collection_metadata.dart'; +import 'package:memo/domain/models/product_info.dart'; import 'package:memo/domain/transients/collection_memos.dart'; void main() { CollectionMemos newCollectionMemos({ List? memosMetadata, List? contributors, + ProductInfo? productInfo, int uniqueMemoExecutionsAmount = 0, }) { return CollectionMemos( @@ -15,6 +17,8 @@ void main() { description: 'description', category: 'category', tags: const [], + isPremium: false, + productInfo: productInfo ?? ProductInfo(price: 0.1, id: 'id'), contributors: contributors ?? const [Contributor(name: 'name')], memosMetadata: memosMetadata ?? [MemoCollectionMetadata(uniqueId: '1', rawAnswer: const [], rawQuestion: const [])], diff --git a/test/fixtures/collection.json b/test/fixtures/collection.json index 13fd5913..9b458af7 100644 --- a/test/fixtures/collection.json +++ b/test/fixtures/collection.json @@ -4,7 +4,12 @@ "description": "This collection represents a collection.", "category": "Category", "contributors": [], - "tags": ["Tag 1", "Tag 2"], + "tags": [ + "Tag 1", + "Tag 2" + ], + "isPremium": true, + "productInfo": [], "uniqueMemosAmount": 1, "uniqueMemoExecutionsAmount": 0, "executionsAmounts": { diff --git a/test/fixtures/collection_memos.json b/test/fixtures/collection_memos.json index 74e1c2ff..6e5ff748 100644 --- a/test/fixtures/collection_memos.json +++ b/test/fixtures/collection_memos.json @@ -4,6 +4,11 @@ "description": "This collection represents a collection.", "category": "Category", "contributors": [], - "tags": ["Tag 1", "Tag 2"], + "tags": [ + "Tag 1", + "Tag 2" + ], + "isPremium": true, + "productInfo": [], "memos": [] } \ No newline at end of file diff --git a/test/fixtures/fixtures.dart b/test/fixtures/fixtures.dart index c65df73b..dcd9c810 100644 --- a/test/fixtures/fixtures.dart +++ b/test/fixtures/fixtures.dart @@ -14,3 +14,4 @@ Map memo() => _readFixture('memo.json'); Map contributor() => _readFixture('contributor.json'); Map resource() => _readFixture('resource.json'); Map user() => _readFixture('user.json'); +Map productInfo() => _readFixture('product_info.json'); diff --git a/test/fixtures/product_info.json b/test/fixtures/product_info.json new file mode 100644 index 00000000..bb9bbed1 --- /dev/null +++ b/test/fixtures/product_info.json @@ -0,0 +1,4 @@ +{ + "id": "", + "price": 0.0 +} \ No newline at end of file