From 9e98536e4f9ac2af2991578eacefdc2e6120ea5f Mon Sep 17 00:00:00 2001 From: Joaoaraujo97 Date: Wed, 19 Jun 2024 15:28:17 -0300 Subject: [PATCH 01/12] Initial implementation of purchase collection. --- assets/collections/0_chatGPT_premium.json | 44 ++++++++++ assets/collections/bdd_fundamentos_01.json | 5 +- assets/collections/comecando_com_git.json | 5 +- .../collections/ecossistema_do_flutter.json | 6 +- assets/collections/fundamentos_scrum.json | 5 +- assets/collections/guia_scrum.json | 5 +- assets/collections/kotlin_fundamentos_01.json | 5 +- assets/collections/manifesto_agil.json | 4 +- assets/collections/swift_fundamentos_01.json | 5 +- assets/images/icons/2.0x/lock.png | Bin 0 -> 446 bytes assets/images/icons/3.0x/lock.png | Bin 0 -> 658 bytes assets/images/icons/lock.png | Bin 0 -> 262 bytes ios/Runner.xcodeproj/project.pbxproj | 13 +-- .../xcshareddata/xcschemes/Runner.xcscheme | 2 +- lib/application/app.dart | 1 + lib/application/constants/dimensions.dart | 2 + .../constants/exception_strings.dart | 3 + lib/application/constants/images.dart | 4 + lib/application/constants/strings.dart | 4 + .../pages/execution/execution_terminal.dart | 1 - .../collections/collections_list_view.dart | 81 ++++++++++++++---- lib/application/view-models/app_vm.dart | 22 +++++ .../view-models/home/collections_vm.dart | 43 +++++++++- .../home/update_collection_vm.dart | 1 - .../view-models/item_metadata.dart | 32 ++++++- .../widgets/theme/collection_card.dart | 31 ++++++- .../widgets/theme/item_collection_card.dart | 2 + .../widgets/theme/rich_text_field.dart | 1 - lib/core/env.dart | 14 +++ .../faults/exceptions/base_exception.dart | 4 + .../faults/exceptions/purchase_exception.dart | 9 ++ lib/data/gateways/purchase_gateway.dart | 76 ++++++++++++++++ lib/data/gateways/sembast_database.dart | 3 +- .../collection_purchase_repository.dart | 55 ++++++++++++ .../repositories/collection_repository.dart | 2 + .../collection_memos_serializer.dart | 12 +++ .../serializers/collection_serializer.dart | 10 +++ lib/domain/models/collection.dart | 17 ++++ .../collection_purchase_services.dart | 70 +++++++++++++++ lib/domain/services/collection_services.dart | 50 +++++++++-- lib/domain/transients/collection_memos.dart | 14 +++ lib/domain/transients/collection_status.dart | 4 +- pubspec.yaml | 1 + .../widgets/theme/custom_button_test.dart | 2 +- .../collection_memos_serializer_test.dart | 4 +- .../collection_serializer_test.dart | 4 + test/domain/models/collection_test.dart | 2 + .../transients/collection_memos_test.dart | 2 + test/fixtures/collection.json | 7 +- test/fixtures/collection_memos.json | 8 +- 50 files changed, 638 insertions(+), 59 deletions(-) create mode 100644 assets/collections/0_chatGPT_premium.json create mode 100644 assets/images/icons/2.0x/lock.png create mode 100644 assets/images/icons/3.0x/lock.png create mode 100644 assets/images/icons/lock.png create mode 100644 lib/core/faults/exceptions/purchase_exception.dart create mode 100644 lib/data/gateways/purchase_gateway.dart create mode 100644 lib/data/repositories/collection_purchase_repository.dart create mode 100644 lib/domain/services/collection_purchase_services.dart diff --git a/assets/collections/0_chatGPT_premium.json b/assets/collections/0_chatGPT_premium.json new file mode 100644 index 00000000..05f357c5 --- /dev/null +++ b/assets/collections/0_chatGPT_premium.json @@ -0,0 +1,44 @@ +{ + "id": "0_chatGPT_premium", + "name": "teste", + "description": "teste", + "category": "Premium", + "tags": [ + "ChatGPT" + ], + "isPremium": true, + "appStoreId": "com.olmps.memo_099_in_app_purchase_deck", + "playStoreId": null, + "contributors": [ + { + "name": "chatGPT", + "url": "https://www.linkedin.com/", + "imageUrl": "https://media-exp1.licdn.com/dms/image/C4E03AQFYmdJJeE9gmA/profile-displayphoto-shrink_400_400/0/1517688960443?e=1631750400&v=beta&t=bpxRB_CL0-3CaT4nzadf1PKpSblm2I_Z7yjm6gEdaBM" + } + ], + "memos": [ + { + "uniqueId": "03c9a8d5-9e27-4ec2-8a3c-7bf20431b890", + "question": [ + { + "insert": "Testes e mais testes\n" + } + ], + "answer": [ + { + "insert": "Isso é mais conhecido como " + }, + { + "insert": "testes", + "attributes": { + "bold": true, + "underline": true + } + }, + { + "insert": ".\n" + } + ] + } + ] +} \ No newline at end of file diff --git a/assets/collections/bdd_fundamentos_01.json b/assets/collections/bdd_fundamentos_01.json index 9a1f50f6..837f6fe6 100644 --- a/assets/collections/bdd_fundamentos_01.json +++ b/assets/collections/bdd_fundamentos_01.json @@ -3,7 +3,10 @@ "name": "BDD - Fundamentos", "description": "Existem diversos paradigmas de desenvolvimento de software. Dentre estes, está o desenvolvimento orientado a comportamento (BDD). Neste deck, vamos conhecer um pouco sobre o BDD, uma das melhores formas de otimizar tanto seu processo de desenvolvimento quanto o produto final gerado.", "category": "Testes", - "tags": ["tests", "bdd"], + "tags": [ + "tests", + "bdd" + ], "contributors": [ { "name": "Nicolas Nascimento", diff --git a/assets/collections/comecando_com_git.json b/assets/collections/comecando_com_git.json index 50bb78ae..52ee6fd2 100644 --- a/assets/collections/comecando_com_git.json +++ b/assets/collections/comecando_com_git.json @@ -3,7 +3,10 @@ "name": "Começando com Git", "description": "\"Git é um sistema de controle de versões distribuído, usado principalmente no desenvolvimento de software, mas pode ser usado para registrar o histórico de edições de qualquer tipo de arquivo. O Git foi inicialmente projetado e desenvolvido por Linus Torvalds para o desenvolvimento do kernel Linux, mas foi adotado por muitos outros projetos.\" - Wikipedia, 2021.", "category": "Versionamento", - "tags": ["git", "versionamento"], + "tags": [ + "git", + "versionamento" + ], "contributors": [ { "name": "@matuella", diff --git a/assets/collections/ecossistema_do_flutter.json b/assets/collections/ecossistema_do_flutter.json index 7e8a0cf1..c95da844 100644 --- a/assets/collections/ecossistema_do_flutter.json +++ b/assets/collections/ecossistema_do_flutter.json @@ -3,7 +3,11 @@ "name": "Ecossistema do Flutter - Fundamentos", "description": "\"Flutter é um kit de desenvolvimento de interface de usuário (UI toolkit), de código aberto, criado pelo Google, que possibilita a criação de aplicativos compilados nativamente. Atualmente pode compilar para Android, iOS, Windows, Mac, Linux, Google Fuchsia e Web.\" - Wikipedia, 2021.", "category": "Flutter", - "tags": ["flutter", "framework", "cross-platform"], + "tags": [ + "flutter", + "framework", + "cross-platform" + ], "contributors": [ { "name": "@matuella", diff --git a/assets/collections/fundamentos_scrum.json b/assets/collections/fundamentos_scrum.json index b01a87b7..1fd2dd10 100644 --- a/assets/collections/fundamentos_scrum.json +++ b/assets/collections/fundamentos_scrum.json @@ -3,7 +3,10 @@ "name": "Fundamentos do Scrum", "description": "O Scrum é um framework de gerenciamento de projetos, da organização ao desenvolvimento ágil de produtos complexos e adaptativos com o mais alto valor possível, através de várias técnicas, utilizado desde o início de 1990 e que atualmente é utilizado em mais de 60% dos projetos ágeis em todo o mundo. - Wikipedia, 2021", "category": "Scrum", - "tags": ["agile", "scrum"], + "tags": [ + "agile", + "scrum" + ], "contributors": [ { "name": "Olympus", diff --git a/assets/collections/guia_scrum.json b/assets/collections/guia_scrum.json index 9c687274..59351432 100644 --- a/assets/collections/guia_scrum.json +++ b/assets/collections/guia_scrum.json @@ -3,7 +3,10 @@ "name": "Guia do Scrum", "description": "O guia do scrum é um documento pequeno, que descreve tudo o que existe no Scrum. Muitas pessoas que dizem conhecer o Scrum, nunca leram o documento. Não pode ser pela quantidade de páginas, que são menos de 20. O guia do scrum estabelece pilares, valores, artefatos, papéis e responsabilidades para que uma equipe possa organizar o seu fluxo de trabalho, encontrando formas de priorizar o trabalho a ser realizado, acompanhar dificuldades e progresso, revisar o trabalho feito e ainda garantir ações de melhoria ao longo do tempo.", "category": "Scrum", - "tags": ["agile", "scrum"], + "tags": [ + "agile", + "scrum" + ], "contributors": [ { "name": "Daniel Wildt", diff --git a/assets/collections/kotlin_fundamentos_01.json b/assets/collections/kotlin_fundamentos_01.json index afbdb937..91237957 100644 --- a/assets/collections/kotlin_fundamentos_01.json +++ b/assets/collections/kotlin_fundamentos_01.json @@ -3,7 +3,10 @@ "name": "Kotlin - Fundamentos", "description": "Nessa coleção de memórias você vai ser introduzido na linguagem de programação Kotlin. Embora ler a documentação do Kotlin seja fácil e agradável, aprender por exemplos é um tipo diferente de diversão (confira os links abaixo).", "category": "Kotlin", - "tags": ["kotlin", "linguagem de programação"], + "tags": [ + "kotlin", + "linguagem de programação" + ], "contributors": [ { "name": "Lucas Montano", diff --git a/assets/collections/manifesto_agil.json b/assets/collections/manifesto_agil.json index 0e470997..28d888e0 100644 --- a/assets/collections/manifesto_agil.json +++ b/assets/collections/manifesto_agil.json @@ -3,7 +3,9 @@ "name": "Manifesto Ágil", "description": "Em Fevereiro de 2001, no Snowbird ski resort em Utah, 17 pessoas se apresentam para falar, se divertir e encontrar caminhos comuns nas práticas de engenharia e organização de projetos que vinham testando, validando e aprendendo. O que emerge deste encontro de 2 dias foi o Agile ‘Software Development’ Manifesto. Representantes de diferentes metodologias estavam presentes nesta data: Extreme Programming, SCRUM, DSDM, Adaptive Software Development, Crystal, Feature-Driven Development, Pragmatic Programming, e outras pessoas que eram simpáticas a necessidade de alternativas para projetos direcionados por documentação, normalmente direcionados por processos de desenvolvimento de software pesados. Todos signatários terminam o encontro com o termo Agile, termo que aparece em cena por indicação de Martin Fowler, que já aparecia na cena de eXtreme Programming anos anteriores.", "category": "Metodologia Ágil", - "tags": ["agile"], + "tags": [ + "agile" + ], "contributors": [ { "name": "Daniel Wildt", diff --git a/assets/collections/swift_fundamentos_01.json b/assets/collections/swift_fundamentos_01.json index bcd10d28..a128adf4 100644 --- a/assets/collections/swift_fundamentos_01.json +++ b/assets/collections/swift_fundamentos_01.json @@ -3,7 +3,10 @@ "name": "Fundamentos de Swift I", "description": "\"Swift é uma linguagem de programação desenvolvida pela Apple para desenvolvimento no iOS, macOS, watchOS, tvOS e Linux. O compilador usa a infraestrutura do LLVM e é distribuído junto do Xcode desde a versão 6.\" - Wikipedia, 2021", "category": "Swift", - "tags": ["swift", "linguagem de programação"], + "tags": [ + "swift", + "linguagem de programação" + ], "contributors": [ { "name": "@matuella", diff --git a/assets/images/icons/2.0x/lock.png b/assets/images/icons/2.0x/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..3734573cc6cdfd8b4fb45c4b37dbf1a8f0d53f53 GIT binary patch literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?2=RS;(M3{v?36l5$8 za(7}_cTVOdki(Mh=F{Fa=?Tp^Q!v;L9@;(7(CpZ$DH+Y&a zkT#jyAb5(cCtwBxQ0yh!qo+xI*QVUyJK_~xTfQqUTKm%Gr~4V&5+%f&J9IL%Z*i}k zm@oDHwoq~FN!v?NoR`n|aR}e?vhtt79IIqBPuFDPzMPs}MyoQPI_{qIO=r1x4lA$3 zjtP%Dwcb@vd~vxmO2M3GmyhSa1M4TuSDsieB2n^OHBkM>wR!LPc3NKSPHoD6I9<@K zsCi;ZV85Kpgah$^_wP75Gnr}2PC3Ro#Z#lLqU)J<{h$2f=>Aeetv~8D528M8?mMZw zqr`LnzWvVa?CaL8cjis<(paUgrX*pV##J^wfuM9A_89ZJ6T-G@yGywpsg0TAl literal 0 HcmV?d00001 diff --git a/assets/images/icons/3.0x/lock.png b/assets/images/icons/3.0x/lock.png new file mode 100644 index 0000000000000000000000000000000000000000..27ecd175faed3b236457e701e733441ec40fcc5a GIT binary patch literal 658 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9FJ79h;%I?XTvD9BhG zjkW98v+=k7$X=<9IP7nuI=5K+{WMXlut=#-~BUvd=n<@*=eo!pFs%-7TH$q z5D?sX-M)5h`r5{@rKR)l-OJG3Dz)}~-1_gW|CLx4n_h}K{V^;mc`es=pYjj7#xrUi z_ddS1B+Pi)XG!lji~cPw5G$(dp3UJ|o}_hm<{{(G=8dV#OsC9cjQIaPdvC^7cb)ge zK3vv0Zz5hO{Z+oc*=obQtrdStCm(Ju>h*EU{?(vfwBxHzfJah!PPS_uTTW-P*KDrV zo(I;F-R~~+KA(0y+cqa7KttC$v+_4%oX77y=?%iIrCZiiv+6JTTkz;fq0^%y#q&+A zE}XnrbK_R+`fPE_)89+?{)>(6bu@2gl$4mz2qpaZG<@$IvYdal%&F+X=JdiNzpiJ_ zZrdT!dEC`_XPx2-uGTKKu$SEF9n-UHGPcN-%I$Pqc1h2yX?{VI*XkEva>}?TRUEKv z_V>Tl^~{oc%9aJ;Kc?<|tI9a@`NZ`%7-o9ynsH#`9pySd5ni4X2O4;YA_9a0nD=%?g%;l?pb*H<{@#WT^1>3WBB=2kD=Y14V%s@(7 z;wZ<&fO6X@UZ>^%zAI{fQJs02VWx;c;NgqKK3kN|vuP$Q?E6#F;kasxQ&8pUz?%l= z@_4pGB<{86%)tikRc}pIp5NBoy=TqJ!sBY|o?Q6w z=!nC86Ty@_-``hH;Qg0q;Bda+cf0jT|8|{>NygP_a$b&Y7h}!71D(m>>FVdQ&MBb@ E04tASkpKVy literal 0 HcmV?d00001 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 @@ 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); + + ref.listen(collectionsVM, (_, state) { + if (state is PurchaseCollectionFailed) { + Navigator.of(context).pop(); + showExceptionSnackBar(ref, state.exception); + } + if (state is PurchaseCollectionSuccess) { + Navigator.of(context).pop(); + showSnackBar( + ref, + const SnackBar(content: Text(strings.collectionSuccessPurchase)), + ); + } + }); + + 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: item.isVisible + ? () async => collectionPurchaseBottomSheet(context, () => vm.purchaseCollection(item.id)) + : () => readCoordinator(ref).navigateToCollectionDetails(item.id), + isVisible: item.isVisible, + ).withOnlyPadding(context, bottom: Spacing.medium); + } + + throw InconsistentStateError.layout('Unsupported subtype (${item.runtimeType}) of `CollectionItemMetadata`'); + }, + ), ); } } @@ -50,3 +77,19 @@ class _CollectionsSectionHeader extends ConsumerWidget { return Text(title, style: sectionTitleStyle); } } + +/// 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/view-models/app_vm.dart b/lib/application/view-models/app_vm.dart index 5c092678..b88f9fb2 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/collection_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 collectionPurchaseRepo = CollectionPurchaseRepositoryImpl(dbRepo, purchaseGateway, collectionRepo); final transactionHandler = TransactionHandlerImpl(dbRepo); @@ -79,10 +87,18 @@ class AppVMImpl extends AppVM { final memoryServices = MemoryRecallServicesImpl(); // Services + final collectionPurchaseServices = CollectionPurchaseServicesImpl( + env: env, + collectionPurchaseRepo: collectionPurchaseRepo, + collectionRepo: collectionRepo, + ); + final collectionServices = CollectionServicesImpl( collectionRepo: collectionRepo, memoRepo: memoRepo, memoryServices: memoryServices, + collectionPurchaseRepo: collectionPurchaseRepo, + collectionPurchaseServices: collectionPurchaseServices, ); final executionServices = ExecutionServicesImpl( @@ -103,6 +119,7 @@ class AppVMImpl extends AppVM { executionServices: executionServices, progressServices: progressServices, resourceServices: resourceServices, + collectionPurchaseServices: collectionPurchaseServices, ); // Scope-specific Services @@ -132,12 +149,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 +172,6 @@ final progressServices = Provider((_) { final resourceServices = Provider((_) { throw UnimplementedError('resourceServices Provider must be overridden'); }); +final collectionPurchaseServices = Provider((_) { + throw UnimplementedError('collectionPurchaseServices 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..329205e8 100644 --- a/lib/application/view-models/home/collections_vm.dart +++ b/lib/application/view-models/home/collections_vm.dart @@ -4,37 +4,63 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:memo/application/view-models/app_vm.dart'; import 'package:memo/application/view-models/item_metadata.dart'; +import 'package:memo/core/faults/exceptions/base_exception.dart'; +import 'package:memo/domain/services/collection_purchase_services.dart'; import 'package:memo/domain/services/collection_services.dart'; import 'package:memo/domain/transients/collection_status.dart'; import 'package:meta/meta.dart'; final collectionsVM = StateNotifierProvider((ref) { - return CollectionsVMImpl(ref.read(collectionServices)); + return CollectionsVMImpl(ref.read(collectionServices), ref.read(collectionPurchaseServices)); }); /// 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 when connected to the internet. + Future onRefresh(); + /// Updates the current [state] with [segment]. /// /// Changing this [segment] also updates the displayed collections based on this [CollectionsSegment]. Future updateCollectionsSegment(CollectionsSegment segment); + + /// Purchase a deck, comparing its [id] and updating the isAvailable state to `true`. + Future purchaseCollection(String id); } class CollectionsVMImpl extends CollectionsVM { - CollectionsVMImpl(this._services) : super(LoadingCollectionsState(availableSegments.first)) { + CollectionsVMImpl(this._services, this._purchaseServices) : super(LoadingCollectionsState(availableSegments.first)) { _addCollectionsListeners(); } final CollectionServices _services; + final CollectionPurchaseServices _purchaseServices; StreamSubscription>? _statusListener; List _cachedCollectionItems = []; + @override + Future onRefresh() async { + await _addCollectionsListeners(); + } + + @override + Future purchaseCollection(String id) async { + try { + await _purchaseServices.purchaseCollection(id: id); + state = PurchaseCollectionSuccess(state.currentSegment); + } on BaseException catch (exception) { + state = PurchaseCollectionFailed(exception, state.currentSegment); + } + _updateToLoadedStateWithCachedMetadata(); + } + @override Future updateCollectionsSegment(CollectionsSegment segment) async { if (state is LoadingCollectionsState) { @@ -132,3 +158,16 @@ class LoadedCollectionsState extends CollectionsState { @override List get props => [collectionItems, ...super.props]; } + +class PurchaseCollectionFailed extends CollectionsState { + const PurchaseCollectionFailed(this.exception, CollectionsSegment currentSegment) : super(currentSegment); + + final BaseException exception; + + @override + List get props => [exception, ...super.props]; +} + +class PurchaseCollectionSuccess extends CollectionsState { + const PurchaseCollectionSuccess(CollectionsSegment currentSegment) : super(currentSegment); +} 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..b9e1af9a 100644 --- a/lib/application/view-models/item_metadata.dart +++ b/lib/application/view-models/item_metadata.dart @@ -15,15 +15,23 @@ 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.isVisible, + }); final String id; final String name; final String category; final List tags; + final bool isVisible; + @override - List get props => [id, name, category, tags]; + List get props => [id, name, category, tags, isVisible]; } /// Represents a collection that have been fully executed - where no pristine memos are left. @@ -34,7 +42,14 @@ class CompletedCollectionItem extends CollectionItem { required String name, required String category, required List tags, - }) : super(id: id, name: name, category: category, tags: tags); + required bool isVisible, + }) : super( + id: id, + name: name, + category: category, + tags: tags, + isVisible: isVisible, + ); final double recallLevel; String get readableRecall => (recallLevel * 100).round().toString(); @@ -52,7 +67,14 @@ class IncompleteCollectionItem extends CollectionItem { required String name, required String category, required List tags, - }) : super(id: id, name: name, category: category, tags: tags); + required bool isVisibile, + }) : super( + id: id, + name: name, + category: category, + tags: tags, + isVisible: isVisibile, + ); final int executedUniqueMemos; final int totalUniqueMemos; @@ -75,6 +97,7 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) { name: collection.name, category: collection.category, tags: collection.tags, + isVisible: status.isVisible, ); } else { return IncompleteCollectionItem( @@ -84,6 +107,7 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) { name: collection.name, category: collection.category, tags: collection.tags, + isVisibile: status.isVisible, ); } } diff --git a/lib/application/widgets/theme/collection_card.dart b/lib/application/widgets/theme/collection_card.dart index 8c98f350..89b2e784 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/images.dart' as images; 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.isVisible, this.hasBorder = true, this.progressDescription, this.progressValue, @@ -52,6 +54,9 @@ class CollectionCard extends ConsumerWidget { final VoidCallback? onTap; + /// Indicator if deck is available. + final bool isVisible; + @override Widget build(BuildContext context, WidgetRef ref) { final theme = ref.watch(themeController); @@ -78,13 +83,15 @@ class CollectionCard extends ConsumerWidget { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Flexible(child: firstRowElements), + Flexible( + child: isVisible ? _buildLockedCollection(context, firstRowElements, theme) : firstRowElements, + ), if (progressDescription != null && progressValue != null) ...[ context.verticalBox(Spacing.large), _buildMemoryRecallTitle(context, theme), context.verticalBox(Spacing.xSmall), _buildMemoryRecallProgress(theme), - ] + ], ], ), ), @@ -149,6 +156,26 @@ class CollectionCard extends ConsumerWidget { semanticLabel: progressSemanticLabel, ); } + + Widget _buildLockedCollection(BuildContext context, Widget child, MemoThemeData theme) { + return Stack( + children: [ + Center( + child: Transform.scale( + scale: 2, + child: Image.asset(images.lockAsset, color: theme.neutralSwatch), + ), + ).withAllPadding(context, Spacing.small), + ImageFiltered( + imageFilter: ui.ImageFilter.blur( + sigmaX: dimens.collectionsBlurSize, + sigmaY: dimens.collectionsBlurSize, + ), + child: child, + ), + ], + ); + } } /// Custom background painter for a Card that represents a `Collection`. diff --git a/lib/application/widgets/theme/item_collection_card.dart b/lib/application/widgets/theme/item_collection_card.dart index 57f617f8..f30a8ce6 100644 --- a/lib/application/widgets/theme/item_collection_card.dart +++ b/lib/application/widgets/theme/item_collection_card.dart @@ -9,6 +9,7 @@ Widget buildCollectionCardFromItem( required EdgeInsets padding, bool hasBorder = true, VoidCallback? onTap, + bool isVisible = false, }) { String? progressDescription; double? progressValue; @@ -41,5 +42,6 @@ Widget buildCollectionCardFromItem( progressValue: progressValue, progressSemanticLabel: progressSemanticLabel, onTap: onTap, + isVisible: isVisible, ); } diff --git a/lib/application/widgets/theme/rich_text_field.dart b/lib/application/widgets/theme/rich_text_field.dart index 0195ac50..e9b62e5d 100644 --- a/lib/application/widgets/theme/rich_text_field.dart +++ b/lib/application/widgets/theme/rich_text_field.dart @@ -388,7 +388,6 @@ class _ThemedEditor extends ConsumerWidget { scrollable: true, padding: EdgeInsets.zero, autoFocus: !readOnly, - readOnly: readOnly, expands: false, enableInteractiveSelection: !readOnly, showCursor: !readOnly, diff --git a/lib/core/env.dart b/lib/core/env.dart index bb506877..f2d91695 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,17 @@ class EnvMetadataImpl implements EnvMetadata { throw InconsistentStateError('Unsupported platform - ${Platform.operatingSystem}'); } + + @override + String get inAppPurchaseKey { + switch (platform) { + case SupportedPlatform.ios: + return 'appl_edKVhziuBuXDpmVPASASRdEJhKc'; + default: + // TODO joao: Replace with the correct key for Android + return 'appl_edKVhziuBuXDpmVPASASRdEJhKc'; + } + } } /// Application's supported environments. diff --git a/lib/core/faults/exceptions/base_exception.dart b/lib/core/faults/exceptions/base_exception.dart index 49f86b8f..46f77458 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, + failedPurchase, } diff --git a/lib/core/faults/exceptions/purchase_exception.dart b/lib/core/faults/exceptions/purchase_exception.dart new file mode 100644 index 00000000..6aab49c7 --- /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.failedPurchase); +} 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..7cd8f0c2 100644 --- a/lib/data/gateways/sembast_database.dart +++ b/lib/data/gateways/sembast_database.dart @@ -31,6 +31,7 @@ abstract class SembastTransactionHandler implements DatabaseTransactionHandler { await db.transaction((transaction) async { currentTransaction = transaction; await run(); + currentTransaction = null; }); // ignore: avoid_catches_without_on_clauses } catch (error, stack) { @@ -133,7 +134,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_purchase_repository.dart b/lib/data/repositories/collection_purchase_repository.dart new file mode 100644 index 00000000..bb26e60d --- /dev/null +++ b/lib/data/repositories/collection_purchase_repository.dart @@ -0,0 +1,55 @@ +import 'package:memo/data/gateways/purchase_gateway.dart'; +import 'package:memo/data/gateways/sembast_database.dart'; +import 'package:memo/data/repositories/collection_repository.dart'; +import 'package:memo/data/serializers/collection_serializer.dart'; + +abstract class CollectionPurchaseRepository { + /// Purchase products in the app with the store ID [storeId] for the local user. + Future purchaseInApp({required String storeId}); + + /// Receives purchase information made by the user. + Future> getPurchasesInfo(); + + /// Check which products are available for purchase. + Future> isAvailable(); + + /// Updates the collection with the [id] to be premium or not. + Future updatePurchaseCollection({required String id, required bool isPremium}); +} + +class CollectionPurchaseRepositoryImpl implements CollectionPurchaseRepository { + CollectionPurchaseRepositoryImpl(this._db, this._purchaseGateway, this.collectionRepo); + + final SembastDatabase _db; + final _collectionStore = 'collections'; + + final PurchaseGateway _purchaseGateway; + + final CollectionRepository collectionRepo; + + @override + Future purchaseInApp({required String storeId}) => _purchaseGateway.purchase( + identifier: storeId, + ); + + @override + Future> getPurchasesInfo() 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 updatePurchaseCollection({required String id, required bool isPremium}) => _db.put( + id: id, + object: { + CollectionKeys.isPremium: isPremium, + }, + store: _collectionStore, + ); +} diff --git a/lib/data/repositories/collection_repository.dart b/lib/data/repositories/collection_repository.dart index d6b5ee9d..eaf3a7c1 100644 --- a/lib/data/repositories/collection_repository.dart +++ b/lib/data/repositories/collection_repository.dart @@ -90,6 +90,8 @@ class CollectionRepositoryImpl implements CollectionRepository { CollectionKeys.contributors: collection.contributors.map(_contributorSerializer.to), CollectionKeys.uniqueMemosAmount: collection.uniqueMemosAmount, CollectionKeys.uniqueMemoExecutionsAmount: collection.uniqueMemoExecutionsAmount, + CollectionKeys.isPremium: collection.isPremium, + CollectionKeys.appStoreId: collection.appStoreId, }, ) .toList(), diff --git a/lib/data/serializers/collection_memos_serializer.dart b/lib/data/serializers/collection_memos_serializer.dart index 805295b7..814d7dd2 100644 --- a/lib/data/serializers/collection_memos_serializer.dart +++ b/lib/data/serializers/collection_memos_serializer.dart @@ -11,6 +11,9 @@ class CollectionMemosKeys { static const contributors = 'contributors'; static const tags = 'tags'; static const memosMetadata = 'memos'; + static const isPremium = 'isPremium'; + static const appStoreId = 'appStoreId'; + static const playStoreId = 'playStoreId'; } class CollectionMemosSerializer implements Serializer> { @@ -23,6 +26,9 @@ class CollectionMemosSerializer implements Serializer.from(json[CollectionMemosKeys.tags] as List); @@ -40,6 +46,9 @@ class CollectionMemosSerializer implements Serializer> { @@ -41,6 +43,10 @@ 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 appStoreId = json[CollectionKeys.appStoreId] as String?; + return Collection( id: id, name: name, @@ -52,6 +58,8 @@ class CollectionSerializer implements Serializer MapEntry(key.raw, value)), CollectionKeys.contributors: collection.contributors.map(contributorSerializer.to), CollectionKeys.timeSpentInMillis: collection.timeSpentInMillis, + CollectionKeys.isPremium: collection.isPremium, + CollectionKeys.appStoreId: collection.appStoreId, }; } diff --git a/lib/domain/models/collection.dart b/lib/domain/models/collection.dart index 2e12a7f1..8d93cb4f 100644 --- a/lib/domain/models/collection.dart +++ b/lib/domain/models/collection.dart @@ -16,6 +16,8 @@ class Collection extends MemoExecutionsMetadata with EquatableMixin implements C required this.tags, required this.uniqueMemosAmount, required this.contributors, + required this.isPremium, + required this.appStoreId, this.uniqueMemoExecutionsAmount = 0, Map executionsAmounts = const {}, int timeSpentInMillis = 0, @@ -47,6 +49,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 String appStoreId; + @override final int uniqueMemosAmount; @@ -67,6 +76,8 @@ class Collection extends MemoExecutionsMetadata with EquatableMixin implements C category, tags, contributors, + isPremium, + appStoreId, uniqueMemoExecutionsAmount, uniqueMemosAmount, ...super.props, @@ -86,6 +97,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; + + /// App store id for this collection. + String get appStoreId; + /// Total amount of unique `Memo`s associated with this collection. int get uniqueMemosAmount; diff --git a/lib/domain/services/collection_purchase_services.dart b/lib/domain/services/collection_purchase_services.dart new file mode 100644 index 00000000..b34adb05 --- /dev/null +++ b/lib/domain/services/collection_purchase_services.dart @@ -0,0 +1,70 @@ +import 'dart:async'; + +import 'package:memo/core/env.dart'; +import 'package:memo/data/repositories/collection_purchase_repository.dart'; +import 'package:memo/data/repositories/collection_repository.dart'; +import 'package:memo/domain/models/collection.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 isVisible({required String id}); +} + +class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { + CollectionPurchaseServicesImpl({ + required this.env, + required this.collectionPurchaseRepo, + required this.collectionRepo, + }); + + final EnvMetadata env; + + final CollectionPurchaseRepository collectionPurchaseRepo; + + final CollectionRepository collectionRepo; + + @override + Future purchaseCollection({required String id}) async { + final collection = await collectionRepo.getCollection(id: id); + + await _purchaseInAppCollection(collection); + await _updatePurchaseCollection(id: id, isPremium: false); + } + + Future _updatePurchaseCollection({required String id, required bool isPremium}) async { + final collection = await collectionRepo.getCollection(id: id); + final isPurchased = await collectionPurchaseRepo.getPurchasesInfo(); + + if (isPurchased.contains(collection.appStoreId)) { + await collectionPurchaseRepo.updatePurchaseCollection(id: id, isPremium: isPremium); + } + } + + Future _purchaseInAppCollection(Collection collection) async { + switch (env.platform) { + case SupportedPlatform.ios: + await collectionPurchaseRepo.purchaseInApp(storeId: collection.appStoreId); + break; + case SupportedPlatform.android: + break; + } + } + + @override + Future isVisible({required String id}) async { + final collection = await collectionRepo.getCollection(id: id); + + if (collection.isPremium) { + final isAvailable = await collectionPurchaseRepo.isAvailable(); + final isPurchased = await collectionPurchaseRepo.getPurchasesInfo(); + + if (isAvailable.contains(collection.appStoreId)) { + return !isPurchased.contains(collection.appStoreId); + } + } + return collection.isPremium; + } +} diff --git a/lib/domain/services/collection_services.dart b/lib/domain/services/collection_services.dart index 796b4cf1..c9d841f0 100644 --- a/lib/domain/services/collection_services.dart +++ b/lib/domain/services/collection_services.dart @@ -1,9 +1,13 @@ +import 'package:flutter/services.dart'; import 'package:memo/core/faults/errors/inconsistent_state_error.dart'; +import 'package:memo/data/repositories/collection_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'; +import 'package:purchases_flutter/purchases_flutter.dart'; /// Handles all domain-specific operations associated with [Collection]s. abstract class CollectionServices { @@ -26,23 +30,50 @@ 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.collectionPurchaseRepo, + required this.collectionPurchaseServices, + }); final CollectionRepository collectionRepo; final MemoRepository memoRepo; final MemoryRecallServices memoryServices; + final CollectionPurchaseRepository collectionPurchaseRepo; + 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) { - final mappedStatuses = collections.map(_mapCollectionToCollectionStatus).toList(); - return Future.wait(mappedStatuses); - }, - ); + try { + final isAvailbleList = await collectionPurchaseRepo.isAvailable(); + if (isAvailbleList.isNotEmpty) { + return collectionsStream.asyncMap( + (collections) { + final mappedStatuses = collections.map(_mapCollectionToCollectionStatus).toList(); + return Future.wait(mappedStatuses); + }, + ); + } + } on PlatformException catch (exception) { + final errorCode = PurchasesErrorHelper.getErrorCode(exception); + if (errorCode == PurchasesErrorCode.offlineConnectionError) { + return collectionsStream.asyncMap( + (collections) { + final offlineCollections = collections.where((collection) => !collection.isPremium).toList(); + final mappedStatuses = offlineCollections.map(_mapCollectionToCollectionStatus).toList(); + return Future.wait(mappedStatuses); + }, + ); + } + } + throw InconsistentStateError.service('Missing required collection purchase information'); } @override @@ -65,11 +96,12 @@ class CollectionServicesImpl implements CollectionServices { Future _mapCollectionToCollectionStatus(Collection collection) async { double? memoryRecall; + bool isVisible; if (collection.isCompleted) { memoryRecall = await _getMemosAverageMemoryRecall(collectionId: collection.id); } - - return CollectionStatus(collection, memoryRecall); + isVisible = await collectionPurchaseServices.isVisible(id: collection.id); + return CollectionStatus(collection, memoryRecall, isVisible: isVisible); } @override diff --git a/lib/domain/transients/collection_memos.dart b/lib/domain/transients/collection_memos.dart index d65fa97d..ed06fabe 100644 --- a/lib/domain/transients/collection_memos.dart +++ b/lib/domain/transients/collection_memos.dart @@ -14,6 +14,9 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { required this.tags, required this.contributors, required this.memosMetadata, + required this.isPremium, + required this.appStoreId, + this.playStoreId, int uniqueMemoExecutionsAmount = 0, }) : _uniqueMemoExecutionsAmount = uniqueMemoExecutionsAmount, assert(memosMetadata.isNotEmpty, 'must not be an empty list of memos'), @@ -40,6 +43,14 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { @override final List contributors; + @override + final bool isPremium; + + @override + final String appStoreId; + + final String? playStoreId; + @override int get uniqueMemosAmount => memosMetadata.length; @@ -64,5 +75,8 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { _uniqueMemoExecutionsAmount, uniqueMemosAmount, memosMetadata, + isPremium, + appStoreId, + playStoreId, ]; } diff --git a/lib/domain/transients/collection_status.dart b/lib/domain/transients/collection_status.dart index 1a7f87d0..a930d66e 100644 --- a/lib/domain/transients/collection_status.dart +++ b/lib/domain/transients/collection_status.dart @@ -2,7 +2,7 @@ import 'package:memo/domain/models/collection.dart'; /// Groups a [Collection] with its memory recall. class CollectionStatus { - CollectionStatus(this.collection, this.memoryRecall); + CollectionStatus(this.collection, this.memoryRecall, {required this.isVisible}); final Collection collection; @@ -10,4 +10,6 @@ class CollectionStatus { /// /// Should be `null` if [Collection.isCompleted] is `false`. final double? memoryRecall; + + final bool isVisible; } diff --git a/pubspec.yaml b/pubspec.yaml index b7ad03b1..d2e967b8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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.29.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..40c07c40 100644 --- a/test/application/widgets/theme/custom_button_test.dart +++ b/test/application/widgets/theme/custom_button_test.dart @@ -173,7 +173,7 @@ void main() { }); testWidgets('must shrink based on its content', (tester) async { - const expectedWidth = 64.0; + const expectedWidth = 64.4000015258789; const textButton = CustomTextButton(text: 'Text'); await pumpProviderScoped(tester, textButton); diff --git a/test/data/serializers/collection_memos_serializer_test.dart b/test/data/serializers/collection_memos_serializer_test.dart index ec788b29..349be89c 100644 --- a/test/data/serializers/collection_memos_serializer_test.dart +++ b/test/data/serializers/collection_memos_serializer_test.dart @@ -15,13 +15,15 @@ void main() { description: 'This collection represents a collection.', category: 'Category', tags: const ['Tag 1', 'Tag 2'], + isPremium: false, + appStoreId: 'appStoreId', contributors: [const Contributor(name: 'name')], memosMetadata: [ MemoCollectionMetadata( uniqueId: '1', rawQuestion: fakes.question, rawAnswer: fakes.answer, - ) + ), ], ); diff --git a/test/data/serializers/collection_serializer_test.dart b/test/data/serializers/collection_serializer_test.dart index ed7de15f..e9035128 100644 --- a/test/data/serializers/collection_serializer_test.dart +++ b/test/data/serializers/collection_serializer_test.dart @@ -13,6 +13,8 @@ void main() { name: 'My Collection', description: 'This collection represents a collection.', category: 'Category', + isPremium: false, + appStoreId: 'appStoreId', contributors: const [Contributor(name: 'name')], tags: const ['Tag 1', 'Tag 2'], uniqueMemosAmount: 1, @@ -102,6 +104,8 @@ void main() { category: 'Category', contributors: const [Contributor(name: 'name')], tags: const ['Tag 1', 'Tag 2'], + isPremium: false, + appStoreId: 'appStoreId', uniqueMemosAmount: 1, uniqueMemoExecutionsAmount: 1, executionsAmounts: const {MemoDifficulty.easy: 1}, diff --git a/test/domain/models/collection_test.dart b/test/domain/models/collection_test.dart index 095d599b..b23c12b7 100644 --- a/test/domain/models/collection_test.dart +++ b/test/domain/models/collection_test.dart @@ -16,6 +16,8 @@ void main() { description: 'description', category: 'category', tags: const [], + isPremium: false, + appStoreId: 'appStoreId', 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..536a46d9 100644 --- a/test/domain/transients/collection_memos_test.dart +++ b/test/domain/transients/collection_memos_test.dart @@ -15,6 +15,8 @@ void main() { description: 'description', category: 'category', tags: const [], + isPremium: false, + appStoreId: 'appStoreId', 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..8b67431b 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": false, + "appStoreId": "appStoreId", "uniqueMemosAmount": 1, "uniqueMemoExecutionsAmount": 0, "executionsAmounts": { diff --git a/test/fixtures/collection_memos.json b/test/fixtures/collection_memos.json index 74e1c2ff..91f220b4 100644 --- a/test/fixtures/collection_memos.json +++ b/test/fixtures/collection_memos.json @@ -4,6 +4,12 @@ "description": "This collection represents a collection.", "category": "Category", "contributors": [], - "tags": ["Tag 1", "Tag 2"], + "tags": [ + "Tag 1", + "Tag 2" + ], + "isPremium": false, + "appStoreId": "appStoreId", + "playStoreId": null, "memos": [] } \ No newline at end of file From 37b64a1e81a776feadf61c185ff4828ae66282f2 Mon Sep 17 00:00:00 2001 From: Joaoaraujo97 Date: Wed, 26 Jun 2024 12:11:19 -0300 Subject: [PATCH 02/12] Fixes for new purchasing approach. --- assets/collections/0_chatGPT_premium.json | 3 +- assets/images/icons/2.0x/lock.png | Bin 446 -> 0 bytes assets/images/icons/3.0x/lock.png | Bin 658 -> 0 bytes assets/images/icons/lock.png | Bin 262 -> 0 bytes lib/application/constants/images.dart | 4 -- lib/application/constants/strings.dart | 2 + .../details/collection_details_page.dart | 58 +++++++++++++++--- .../collections/collections_list_view.dart | 39 +----------- lib/application/theme/memo_theme_colors.dart | 19 ++++++ lib/application/theme/memo_theme_data.dart | 2 + lib/application/theme/theme_controller.dart | 9 +++ .../details/collection_details_vm.dart | 32 +++++++++- .../view-models/home/collections_vm.dart | 34 +--------- .../widgets/theme/collection_card.dart | 36 +++-------- .../widgets/theme/item_collection_card.dart | 2 - .../widgets/theme/themed_text_tag.dart | 2 +- lib/core/env.dart | 5 +- 17 files changed, 131 insertions(+), 116 deletions(-) delete mode 100644 assets/images/icons/2.0x/lock.png delete mode 100644 assets/images/icons/3.0x/lock.png delete mode 100644 assets/images/icons/lock.png diff --git a/assets/collections/0_chatGPT_premium.json b/assets/collections/0_chatGPT_premium.json index 05f357c5..eb1b57e8 100644 --- a/assets/collections/0_chatGPT_premium.json +++ b/assets/collections/0_chatGPT_premium.json @@ -4,7 +4,8 @@ "description": "teste", "category": "Premium", "tags": [ - "ChatGPT" + "ChatGPT", + "Premium" ], "isPremium": true, "appStoreId": "com.olmps.memo_099_in_app_purchase_deck", diff --git a/assets/images/icons/2.0x/lock.png b/assets/images/icons/2.0x/lock.png deleted file mode 100644 index 3734573cc6cdfd8b4fb45c4b37dbf1a8f0d53f53..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^1|ZDA1|-9oezpTC&H|6fVg?2=RS;(M3{v?36l5$8 za(7}_cTVOdki(Mh=F{Fa=?Tp^Q!v;L9@;(7(CpZ$DH+Y&a zkT#jyAb5(cCtwBxQ0yh!qo+xI*QVUyJK_~xTfQqUTKm%Gr~4V&5+%f&J9IL%Z*i}k zm@oDHwoq~FN!v?NoR`n|aR}e?vhtt79IIqBPuFDPzMPs}MyoQPI_{qIO=r1x4lA$3 zjtP%Dwcb@vd~vxmO2M3GmyhSa1M4TuSDsieB2n^OHBkM>wR!LPc3NKSPHoD6I9<@K zsCi;ZV85Kpgah$^_wP75Gnr}2PC3Ro#Z#lLqU)J<{h$2f=>Aeetv~8D528M8?mMZw zqr`LnzWvVa?CaL8cjis<(paUgrX*pV##J^wfuM9A_89ZJ6T-G@yGywpsg0TAl diff --git a/assets/images/icons/3.0x/lock.png b/assets/images/icons/3.0x/lock.png deleted file mode 100644 index 27ecd175faed3b236457e701e733441ec40fcc5a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 658 zcmeAS@N?(olHy`uVBq!ia0vp^9w5xY1|&n@ZgvM!oCO|{#S9FJ79h;%I?XTvD9BhG zjkW98v+=k7$X=<9IP7nuI=5K+{WMXlut=#-~BUvd=n<@*=eo!pFs%-7TH$q z5D?sX-M)5h`r5{@rKR)l-OJG3Dz)}~-1_gW|CLx4n_h}K{V^;mc`es=pYjj7#xrUi z_ddS1B+Pi)XG!lji~cPw5G$(dp3UJ|o}_hm<{{(G=8dV#OsC9cjQIaPdvC^7cb)ge zK3vv0Zz5hO{Z+oc*=obQtrdStCm(Ju>h*EU{?(vfwBxHzfJah!PPS_uTTW-P*KDrV zo(I;F-R~~+KA(0y+cqa7KttC$v+_4%oX77y=?%iIrCZiiv+6JTTkz;fq0^%y#q&+A zE}XnrbK_R+`fPE_)89+?{)>(6bu@2gl$4mz2qpaZG<@$IvYdal%&F+X=JdiNzpiJ_ zZrdT!dEC`_XPx2-uGTKKu$SEF9n-UHGPcN-%I$Pqc1h2yX?{VI*XkEva>}?TRUEKv z_V>Tl^~{oc%9aJ;Kc?<|tI9a@`NZ`%7-o9ynsH#`9pySd5ni4X2O4;YA_9a0nD=%?g%;l?pb*H<{@#WT^1>3WBB=2kD=Y14V%s@(7 z;wZ<&fO6X@UZ>^%zAI{fQJs02VWx;c;NgqKK3kN|vuP$Q?E6#F;kasxQ&8pUz?%l= z@_4pGB<{86%)tikRc}pIp5NBoy=TqJ!sBY|o?Q6w z=!nC86Ty@_-``hH;Qg0q;Bda+cf0jT|8|{>NygP_a$b&Y7h}!71D(m>>FVdQ&MBb@ E04tASkpKVy diff --git a/lib/application/constants/images.dart b/lib/application/constants/images.dart index 4e7cb536..bd17715c 100644 --- a/lib/application/constants/images.dart +++ b/lib/application/constants/images.dart @@ -20,7 +20,6 @@ enum ImageKey { folder, italic, link, - lock, organize, settings, trash, @@ -62,8 +61,6 @@ extension ImageKeyPath on ImageKey { return '$_editorIconsRoot/italic.png'; case ImageKey.link: return '$_iconsRoot/link.png'; - case ImageKey.lock: - return '$_iconsRoot/lock.png'; case ImageKey.organize: return '$_iconsRoot/organize.png'; case ImageKey.settings: @@ -103,7 +100,6 @@ final dragAsset = ImageKey.drag.path; final folderAsset = ImageKey.folder.path; final italicAsset = ImageKey.italic.path; final linkAsset = ImageKey.link.path; -final lockAsset = ImageKey.lock.path; final organizeAsset = ImageKey.organize.path; final settingsAsset = ImageKey.settings.path; final trashAsset = ImageKey.trash.path; diff --git a/lib/application/constants/strings.dart b/lib/application/constants/strings.dart index cf525c79..d908dfe9 100644 --- a/lib/application/constants/strings.dart +++ b/lib/application/constants/strings.dart @@ -30,6 +30,7 @@ const collectionsSectionHeaderSeeAll = 'Ver todos'; const collectionsStartNow = 'Começar Agora'; +const collectionPurchaseDeck = r'Comprar deck - R$ 0.99'; const collectionPurchase = 'Deseja comprar este Deck?'; const collectionSuccessPurchase = 'Deck comprado com sucesso!'; @@ -97,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'; diff --git a/lib/application/pages/details/collection_details_page.dart b/lib/application/pages/details/collection_details_page.dart index 03ee7516..c5c9e112 100644 --- a/lib/application/pages/details/collection_details_page.dart +++ b/lib/application/pages/details/collection_details_page.dart @@ -6,6 +6,8 @@ import 'package:memo/application/coordinator/routes_coordinator.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 +20,21 @@ 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(collectionDetailsVM(id), (_, state) { + if (state is PurchaseCollectionFailed) { + Navigator.of(context).pop(); + showExceptionSnackBar(ref, state.exception); + } + if (state is PurchaseCollectionSuccess) { + Navigator.of(context).pop(); + showSnackBar( + ref, + const SnackBar(content: Text(strings.collectionSuccessPurchase)), + ); + } + }); if (state is LoadedCollectionDetailsState) { final sections = []; @@ -84,13 +101,24 @@ class CollectionDetailsPage extends ConsumerWidget { child: Container( 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: !state.metadata.isVisible + ? PrimaryElevatedButton( + onPressed: () { + readCoordinator(ref).navigateToCollectionExecution(id, isNestedNavigation: false); + }, + text: strings.detailsStudyNow.toUpperCase(), + ).withSymmetricalPadding(context, vertical: Spacing.small, horizontal: Spacing.medium) + : SecondaryElevatedButton( + backgroundColor: memoTheme.secondarySwatch, + text: strings.collectionPurchaseDeck, + onPressed: () async => collectionPurchaseBottomSheet(context, () { + ref.watch(collectionDetailsVM(id).notifier).purchaseCollection(state.metadata.id); + }), + ).withSymmetricalPadding( + context, + vertical: Spacing.small, + horizontal: Spacing.medium, + ), ), ), ); @@ -131,3 +159,19 @@ class CollectionDetailsPage extends ConsumerWidget { Theme.of(context).textTheme.subtitle1?.copyWith(color: ref.watch(themeController).neutralSwatch.shade300), ); } + +/// 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/home/collections/collections_list_view.dart b/lib/application/pages/home/collections/collections_list_view.dart index 69443b69..77bdc95d 100644 --- a/lib/application/pages/home/collections/collections_list_view.dart +++ b/lib/application/pages/home/collections/collections_list_view.dart @@ -1,14 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:layoutr/common_layout.dart'; -import 'package:memo/application/constants/strings.dart' as strings; import 'package:memo/application/coordinator/routes_coordinator.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/home/collections_vm.dart'; import 'package:memo/application/view-models/item_metadata.dart'; -import 'package:memo/application/widgets/theme/custom_button.dart'; import 'package:memo/application/widgets/theme/item_collection_card.dart'; import 'package:memo/core/faults/errors/inconsistent_state_error.dart'; @@ -21,20 +17,6 @@ class CollectionsListView extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final vm = ref.watch(collectionsVM.notifier); - ref.listen(collectionsVM, (_, state) { - if (state is PurchaseCollectionFailed) { - Navigator.of(context).pop(); - showExceptionSnackBar(ref, state.exception); - } - if (state is PurchaseCollectionSuccess) { - Navigator.of(context).pop(); - showSnackBar( - ref, - const SnackBar(content: Text(strings.collectionSuccessPurchase)), - ); - } - }); - return RefreshIndicator( onRefresh: vm.onRefresh, child: ListView.builder( @@ -50,10 +32,7 @@ class CollectionsListView extends ConsumerWidget { return buildCollectionCardFromItem( item, padding: context.symmetricInsets(vertical: Spacing.large, horizontal: Spacing.small), - onTap: item.isVisible - ? () async => collectionPurchaseBottomSheet(context, () => vm.purchaseCollection(item.id)) - : () => readCoordinator(ref).navigateToCollectionDetails(item.id), - isVisible: item.isVisible, + onTap: () => readCoordinator(ref).navigateToCollectionDetails(item.id), ).withOnlyPadding(context, bottom: Spacing.medium); } @@ -77,19 +56,3 @@ class _CollectionsSectionHeader extends ConsumerWidget { return Text(title, style: sectionTitleStyle); } } - -/// 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/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/view-models/details/collection_details_vm.dart b/lib/application/view-models/details/collection_details_vm.dart index 888b9e39..7e4c9ed9 100644 --- a/lib/application/view-models/details/collection_details_vm.dart +++ b/lib/application/view-models/details/collection_details_vm.dart @@ -4,8 +4,10 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:memo/application/view-models/app_vm.dart'; import 'package:memo/application/view-models/item_metadata.dart'; +import 'package:memo/core/faults/exceptions/base_exception.dart'; import 'package:memo/domain/enums/resource_type.dart'; import 'package:memo/domain/models/resource.dart'; +import 'package:memo/domain/services/collection_purchase_services.dart'; import 'package:memo/domain/services/collection_services.dart'; import 'package:memo/domain/services/resource_services.dart'; import 'package:memo/domain/transients/collection_status.dart'; @@ -15,11 +17,15 @@ final collectionDetailsVM = StateNotifierProvider.family { CollectionDetailsVM(CollectionDetailsState state) : super(state); + + /// Purchase a deck, comparing its [id] and updating the isVisible state to `true`. + Future purchaseCollection(String id); } class CollectionDetailsVMImpl extends CollectionDetailsVM { @@ -27,6 +33,7 @@ class CollectionDetailsVMImpl extends CollectionDetailsVM { required this.collectionId, required this.collectionServices, required this.resourceServices, + required this.purchaseServices, }) : super(LoadingCollectionDetailsState()) { _loadCollection(); } @@ -34,9 +41,10 @@ class CollectionDetailsVMImpl extends CollectionDetailsVM { final String collectionId; final CollectionServices collectionServices; final ResourceServices resourceServices; + final CollectionPurchaseServices purchaseServices; List? _associatedResources; - late final StreamSubscription _listener; + late StreamSubscription _listener; Future _loadCollection() async { final stream = await collectionServices.listenToCollectionStatus(collectionId: collectionId); @@ -83,6 +91,17 @@ class CollectionDetailsVMImpl extends CollectionDetailsVM { _listener.cancel(); super.dispose(); } + + @override + Future purchaseCollection(String id) async { + try { + await purchaseServices.purchaseCollection(id: id); + state = PurchaseCollectionSuccess(); + } on BaseException catch (exception) { + state = PurchaseCollectionFailed(exception); + } + await _loadCollection(); + } } abstract class CollectionDetailsState extends Equatable { @@ -129,3 +148,14 @@ class ContributorInfo extends Equatable { @override List get props => [name, imageUrl, url]; } + +class PurchaseCollectionFailed extends CollectionDetailsState { + PurchaseCollectionFailed(this.exception); + + final BaseException exception; + + @override + List get props => [exception, ...super.props]; +} + +class PurchaseCollectionSuccess extends CollectionDetailsState {} diff --git a/lib/application/view-models/home/collections_vm.dart b/lib/application/view-models/home/collections_vm.dart index 329205e8..862a9ebe 100644 --- a/lib/application/view-models/home/collections_vm.dart +++ b/lib/application/view-models/home/collections_vm.dart @@ -4,14 +4,12 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:memo/application/view-models/app_vm.dart'; import 'package:memo/application/view-models/item_metadata.dart'; -import 'package:memo/core/faults/exceptions/base_exception.dart'; -import 'package:memo/domain/services/collection_purchase_services.dart'; import 'package:memo/domain/services/collection_services.dart'; import 'package:memo/domain/transients/collection_status.dart'; import 'package:meta/meta.dart'; final collectionsVM = StateNotifierProvider((ref) { - return CollectionsVMImpl(ref.read(collectionServices), ref.read(collectionPurchaseServices)); + return CollectionsVMImpl(ref.read(collectionServices)); }); /// Segment used to filter the current state of the [CollectionsVM]. @@ -29,18 +27,14 @@ abstract class CollectionsVM extends StateNotifier { /// /// Changing this [segment] also updates the displayed collections based on this [CollectionsSegment]. Future updateCollectionsSegment(CollectionsSegment segment); - - /// Purchase a deck, comparing its [id] and updating the isAvailable state to `true`. - Future purchaseCollection(String id); } class CollectionsVMImpl extends CollectionsVM { - CollectionsVMImpl(this._services, this._purchaseServices) : super(LoadingCollectionsState(availableSegments.first)) { + CollectionsVMImpl(this._services) : super(LoadingCollectionsState(availableSegments.first)) { _addCollectionsListeners(); } final CollectionServices _services; - final CollectionPurchaseServices _purchaseServices; StreamSubscription>? _statusListener; List _cachedCollectionItems = []; @@ -50,17 +44,6 @@ class CollectionsVMImpl extends CollectionsVM { await _addCollectionsListeners(); } - @override - Future purchaseCollection(String id) async { - try { - await _purchaseServices.purchaseCollection(id: id); - state = PurchaseCollectionSuccess(state.currentSegment); - } on BaseException catch (exception) { - state = PurchaseCollectionFailed(exception, state.currentSegment); - } - _updateToLoadedStateWithCachedMetadata(); - } - @override Future updateCollectionsSegment(CollectionsSegment segment) async { if (state is LoadingCollectionsState) { @@ -158,16 +141,3 @@ class LoadedCollectionsState extends CollectionsState { @override List get props => [collectionItems, ...super.props]; } - -class PurchaseCollectionFailed extends CollectionsState { - const PurchaseCollectionFailed(this.exception, CollectionsSegment currentSegment) : super(currentSegment); - - final BaseException exception; - - @override - List get props => [exception, ...super.props]; -} - -class PurchaseCollectionSuccess extends CollectionsState { - const PurchaseCollectionSuccess(CollectionsSegment currentSegment) : super(currentSegment); -} diff --git a/lib/application/widgets/theme/collection_card.dart b/lib/application/widgets/theme/collection_card.dart index 89b2e784..368203d7 100644 --- a/lib/application/widgets/theme/collection_card.dart +++ b/lib/application/widgets/theme/collection_card.dart @@ -6,7 +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/images.dart' as images; +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'; @@ -21,7 +21,6 @@ class CollectionCard extends ConsumerWidget { required this.name, required this.tags, required this.padding, - required this.isVisible, this.hasBorder = true, this.progressDescription, this.progressValue, @@ -54,9 +53,6 @@ class CollectionCard extends ConsumerWidget { final VoidCallback? onTap; - /// Indicator if deck is available. - final bool isVisible; - @override Widget build(BuildContext context, WidgetRef ref) { final theme = ref.watch(themeController); @@ -84,7 +80,7 @@ class CollectionCard extends ConsumerWidget { crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Flexible( - child: isVisible ? _buildLockedCollection(context, firstRowElements, theme) : firstRowElements, + child: firstRowElements, ), if (progressDescription != null && progressValue != null) ...[ context.verticalBox(Spacing.large), @@ -133,7 +129,13 @@ class CollectionCard extends ConsumerWidget { crossAxisAlignment: WrapCrossAlignment.center, spacing: tagsSpacing, runSpacing: tagsSpacing, - children: tags.map((tag) => PrimaryTextTag(tag.toUpperCase())).toList(), + children: tags + .map( + (tag) => tag.toUpperCase().contains(strings.premium) + ? SecondaryTextTag(tag.toUpperCase()) + : PrimaryTextTag(tag.toUpperCase()), + ) + .toList(), ); } @@ -156,26 +158,6 @@ class CollectionCard extends ConsumerWidget { semanticLabel: progressSemanticLabel, ); } - - Widget _buildLockedCollection(BuildContext context, Widget child, MemoThemeData theme) { - return Stack( - children: [ - Center( - child: Transform.scale( - scale: 2, - child: Image.asset(images.lockAsset, color: theme.neutralSwatch), - ), - ).withAllPadding(context, Spacing.small), - ImageFiltered( - imageFilter: ui.ImageFilter.blur( - sigmaX: dimens.collectionsBlurSize, - sigmaY: dimens.collectionsBlurSize, - ), - child: child, - ), - ], - ); - } } /// Custom background painter for a Card that represents a `Collection`. diff --git a/lib/application/widgets/theme/item_collection_card.dart b/lib/application/widgets/theme/item_collection_card.dart index f30a8ce6..57f617f8 100644 --- a/lib/application/widgets/theme/item_collection_card.dart +++ b/lib/application/widgets/theme/item_collection_card.dart @@ -9,7 +9,6 @@ Widget buildCollectionCardFromItem( required EdgeInsets padding, bool hasBorder = true, VoidCallback? onTap, - bool isVisible = false, }) { String? progressDescription; double? progressValue; @@ -42,6 +41,5 @@ Widget buildCollectionCardFromItem( progressValue: progressValue, progressSemanticLabel: progressSemanticLabel, onTap: onTap, - isVisible: isVisible, ); } diff --git a/lib/application/widgets/theme/themed_text_tag.dart b/lib/application/widgets/theme/themed_text_tag.dart index 127481fa..4c9f2e21 100644 --- a/lib/application/widgets/theme/themed_text_tag.dart +++ b/lib/application/widgets/theme/themed_text_tag.dart @@ -28,7 +28,7 @@ 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, ); diff --git a/lib/core/env.dart b/lib/core/env.dart index f2d91695..6894e97f 100644 --- a/lib/core/env.dart +++ b/lib/core/env.dart @@ -43,9 +43,8 @@ class EnvMetadataImpl implements EnvMetadata { switch (platform) { case SupportedPlatform.ios: return 'appl_edKVhziuBuXDpmVPASASRdEJhKc'; - default: - // TODO joao: Replace with the correct key for Android - return 'appl_edKVhziuBuXDpmVPASASRdEJhKc'; + case SupportedPlatform.android: + return 'goog_PlRbIRkgyhwGbBiUugCHBjzXsTL'; } } } From 31ca12d78b87da9f6c09dd016960b35885dc1e7a Mon Sep 17 00:00:00 2001 From: Joaoaraujo97 Date: Wed, 26 Jun 2024 17:23:39 -0300 Subject: [PATCH 03/12] Theme adjustments. --- .../details/collection_details_page.dart | 4 +-- .../pages/details/contributor_view.dart | 2 +- .../completed_execution_contents.dart | 12 +++---- .../pages/execution/execution_terminal.dart | 3 +- .../collections/collections_list_view.dart | 2 +- .../home/collections/collections_page.dart | 4 +-- .../update/update_collection_details.dart | 2 +- .../update/update_collection_memos.dart | 4 +-- .../update/update_collection_page.dart | 4 +-- .../update/update_memo_terminal.dart | 4 +-- .../pages/home/progress/progress_page.dart | 6 ++-- .../pages/settings/settings_page.dart | 2 +- .../theme/material_theme_data.dart | 34 +++++++++---------- lib/application/utils/bottom_sheet.dart | 6 ++-- .../widgets/theme/collection_card.dart | 4 +-- .../widgets/theme/custom_button.dart | 4 +-- .../widgets/theme/custom_text_field.dart | 9 ++--- .../theme/exception_retry_container.dart | 4 +-- lib/application/widgets/theme/link.dart | 2 +- .../widgets/theme/rich_text_field.dart | 14 ++++---- lib/application/widgets/theme/tags_field.dart | 10 +++--- .../widgets/theme/themed_text_tag.dart | 6 ++-- 22 files changed, 72 insertions(+), 70 deletions(-) diff --git a/lib/application/pages/details/collection_details_page.dart b/lib/application/pages/details/collection_details_page.dart index c5c9e112..4f52f576 100644 --- a/lib/application/pages/details/collection_details_page.dart +++ b/lib/application/pages/details/collection_details_page.dart @@ -83,7 +83,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( @@ -156,7 +156,7 @@ 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), ); } 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 27297ad3..9fbab2f4 100644 --- a/lib/application/pages/execution/execution_terminal.dart +++ b/lib/application/pages/execution/execution_terminal.dart @@ -371,7 +371,8 @@ 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 77bdc95d..9b5954ac 100644 --- a/lib/application/pages/home/collections/collections_list_view.dart +++ b/lib/application/pages/home/collections/collections_list_view.dart @@ -52,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/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/widgets/theme/collection_card.dart b/lib/application/widgets/theme/collection_card.dart index 368203d7..bdfbab1f 100644 --- a/lib/application/widgets/theme/collection_card.dart +++ b/lib/application/widgets/theme/collection_card.dart @@ -60,7 +60,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)), ], @@ -141,7 +141,7 @@ class CollectionCard extends ConsumerWidget { 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/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 e9b62e5d..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), ); @@ -393,15 +393,15 @@ class _ThemedEditor extends ConsumerWidget { 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 4c9f2e21..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, ); } @@ -30,7 +30,7 @@ class SecondaryTextTag extends ConsumerWidget { text, 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, ); } From 0face034909b50f86d85cc3ff72c3ec1a03cb1c1 Mon Sep 17 00:00:00 2001 From: Joaoaraujo97 Date: Mon, 1 Jul 2024 15:19:33 -0300 Subject: [PATCH 04/12] Adjustments and inclusion of the playStoreId. --- android/app/build.gradle | 4 ++-- android/app/src/debug/AndroidManifest.xml | 9 ++++++++- android/build.gradle | 6 +++--- .../gradle/wrapper/gradle-wrapper.properties | 2 +- assets/collections/0_chatGPT_premium.json | 2 +- .../repositories/collection_repository.dart | 1 + .../collection_memos_serializer.dart | 2 +- .../serializers/collection_serializer.dart | 4 ++++ lib/domain/models/collection.dart | 8 ++++++++ .../collection_purchase_services.dart | 7 ++++--- lib/domain/services/collection_services.dart | 20 ++++++++----------- lib/domain/transients/collection_memos.dart | 5 +++-- 12 files changed, 44 insertions(+), 26 deletions(-) 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/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 0350eafe..802ea3ec 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,11 @@ to allow setting breakpoints, to provide hot reload, etc. --> - + + + + + + \ No newline at end of file diff --git a/android/build.gradle b/android/build.gradle index 98a47a44..e2e4a4f7 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,12 +1,12 @@ buildscript { - ext.kotlin_version = '1.6.20' + ext.kotlin_version = '1.8.22' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' + classpath 'com.android.tools.build:gradle:7.2.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'com.google.gms:google-services:4.3.5' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.7.1' @@ -28,6 +28,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties index bc6a58af..cfe88f69 100644 --- a/android/gradle/wrapper/gradle-wrapper.properties +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-all.zip diff --git a/assets/collections/0_chatGPT_premium.json b/assets/collections/0_chatGPT_premium.json index eb1b57e8..4581be38 100644 --- a/assets/collections/0_chatGPT_premium.json +++ b/assets/collections/0_chatGPT_premium.json @@ -9,7 +9,7 @@ ], "isPremium": true, "appStoreId": "com.olmps.memo_099_in_app_purchase_deck", - "playStoreId": null, + "playStoreId": "com.olmps.memo_099_in_app_purchase", "contributors": [ { "name": "chatGPT", diff --git a/lib/data/repositories/collection_repository.dart b/lib/data/repositories/collection_repository.dart index eaf3a7c1..d5ff69e8 100644 --- a/lib/data/repositories/collection_repository.dart +++ b/lib/data/repositories/collection_repository.dart @@ -92,6 +92,7 @@ class CollectionRepositoryImpl implements CollectionRepository { CollectionKeys.uniqueMemoExecutionsAmount: collection.uniqueMemoExecutionsAmount, CollectionKeys.isPremium: collection.isPremium, CollectionKeys.appStoreId: collection.appStoreId, + CollectionKeys.playStoreId: collection.playStoreId, }, ) .toList(), diff --git a/lib/data/serializers/collection_memos_serializer.dart b/lib/data/serializers/collection_memos_serializer.dart index 814d7dd2..b199e5f8 100644 --- a/lib/data/serializers/collection_memos_serializer.dart +++ b/lib/data/serializers/collection_memos_serializer.dart @@ -48,7 +48,7 @@ class CollectionMemosSerializer implements Serializer> { @@ -47,6 +48,8 @@ class CollectionSerializer implements Serializer executionsAmounts = const {}, int timeSpentInMillis = 0, @@ -56,6 +57,9 @@ class Collection extends MemoExecutionsMetadata with EquatableMixin implements C @override final String appStoreId; + @override + final String playStoreId; + @override final int uniqueMemosAmount; @@ -78,6 +82,7 @@ class Collection extends MemoExecutionsMetadata with EquatableMixin implements C contributors, isPremium, appStoreId, + playStoreId, uniqueMemoExecutionsAmount, uniqueMemosAmount, ...super.props, @@ -103,6 +108,9 @@ abstract class CollectionMetadata { /// App store id for this collection. String get appStoreId; + /// Play store id for this collection. + String get playStoreId; + /// Total amount of unique `Memo`s associated with this collection. int get uniqueMemosAmount; diff --git a/lib/domain/services/collection_purchase_services.dart b/lib/domain/services/collection_purchase_services.dart index b34adb05..fbfdece5 100644 --- a/lib/domain/services/collection_purchase_services.dart +++ b/lib/domain/services/collection_purchase_services.dart @@ -38,7 +38,7 @@ class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { final collection = await collectionRepo.getCollection(id: id); final isPurchased = await collectionPurchaseRepo.getPurchasesInfo(); - if (isPurchased.contains(collection.appStoreId)) { + if (isPurchased.contains(collection.appStoreId) || isPurchased.contains(collection.playStoreId)) { await collectionPurchaseRepo.updatePurchaseCollection(id: id, isPremium: isPremium); } } @@ -49,6 +49,7 @@ class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { await collectionPurchaseRepo.purchaseInApp(storeId: collection.appStoreId); break; case SupportedPlatform.android: + await collectionPurchaseRepo.purchaseInApp(storeId: collection.playStoreId); break; } } @@ -61,8 +62,8 @@ class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { final isAvailable = await collectionPurchaseRepo.isAvailable(); final isPurchased = await collectionPurchaseRepo.getPurchasesInfo(); - if (isAvailable.contains(collection.appStoreId)) { - return !isPurchased.contains(collection.appStoreId); + if (isAvailable.contains(collection.appStoreId) || isAvailable.contains(collection.playStoreId)) { + return !isPurchased.contains(collection.appStoreId) || !isPurchased.contains(collection.playStoreId); } } return collection.isPremium; diff --git a/lib/domain/services/collection_services.dart b/lib/domain/services/collection_services.dart index c9d841f0..3c39acdd 100644 --- a/lib/domain/services/collection_services.dart +++ b/lib/domain/services/collection_services.dart @@ -7,7 +7,6 @@ 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'; -import 'package:purchases_flutter/purchases_flutter.dart'; /// Handles all domain-specific operations associated with [Collection]s. abstract class CollectionServices { @@ -61,17 +60,14 @@ class CollectionServicesImpl implements CollectionServices { }, ); } - } on PlatformException catch (exception) { - final errorCode = PurchasesErrorHelper.getErrorCode(exception); - if (errorCode == PurchasesErrorCode.offlineConnectionError) { - return collectionsStream.asyncMap( - (collections) { - final offlineCollections = collections.where((collection) => !collection.isPremium).toList(); - final mappedStatuses = offlineCollections.map(_mapCollectionToCollectionStatus).toList(); - return Future.wait(mappedStatuses); - }, - ); - } + } on PlatformException catch (_) { + return collectionsStream.asyncMap( + (collections) { + final offlineCollections = collections.where((collection) => !collection.isPremium).toList(); + final mappedStatuses = offlineCollections.map(_mapCollectionToCollectionStatus).toList(); + return Future.wait(mappedStatuses); + }, + ); } throw InconsistentStateError.service('Missing required collection purchase information'); } diff --git a/lib/domain/transients/collection_memos.dart b/lib/domain/transients/collection_memos.dart index ed06fabe..387c624b 100644 --- a/lib/domain/transients/collection_memos.dart +++ b/lib/domain/transients/collection_memos.dart @@ -16,7 +16,7 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { required this.memosMetadata, required this.isPremium, required this.appStoreId, - this.playStoreId, + required this.playStoreId, int uniqueMemoExecutionsAmount = 0, }) : _uniqueMemoExecutionsAmount = uniqueMemoExecutionsAmount, assert(memosMetadata.isNotEmpty, 'must not be an empty list of memos'), @@ -49,7 +49,8 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { @override final String appStoreId; - final String? playStoreId; + @override + final String playStoreId; @override int get uniqueMemosAmount => memosMetadata.length; From 09bb6d70027adf2f5bf8efcaf9229fe71053a557 Mon Sep 17 00:00:00 2001 From: Joaoaraujo97 Date: Fri, 5 Jul 2024 14:54:05 -0300 Subject: [PATCH 05/12] Fixes on purchase collections. --- android/app/src/debug/AndroidManifest.xml | 7 -- android/app/src/main/AndroidManifest.xml | 1 + assets/collections/0_chatGPT_premium.json | 3 +- lib/application/app.dart | 2 +- .../constants/exception_strings.dart | 2 +- lib/application/constants/strings.dart | 4 +- .../details/collection_details_page.dart | 71 ++++++++++--------- .../pages/execution/execution_terminal.dart | 5 +- lib/application/view-models/app_vm.dart | 12 ++-- .../details/collection_details_vm.dart | 11 ++- .../view-models/home/collections_vm.dart | 3 +- .../view-models/item_metadata.dart | 18 ++--- .../widgets/theme/collection_card.dart | 15 ++-- .../widgets/theme/item_collection_card.dart | 1 + .../faults/exceptions/base_exception.dart | 2 +- .../faults/exceptions/purchase_exception.dart | 2 +- lib/data/gateways/sembast_database.dart | 1 - ...pository.dart => purchase_repository.dart} | 30 ++++---- .../serializers/collection_serializer.dart | 1 + .../collection_purchase_services.dart | 46 ++++++------ lib/domain/services/collection_services.dart | 37 +++------- lib/domain/transients/collection_status.dart | 4 +- .../widgets/theme/custom_button_test.dart | 4 +- .../collection_memos_serializer_test.dart | 1 + .../collection_serializer_test.dart | 2 + test/domain/models/collection_test.dart | 1 + .../transients/collection_memos_test.dart | 1 + test/fixtures/collection.json | 1 + test/fixtures/collection_memos.json | 2 +- 29 files changed, 144 insertions(+), 146 deletions(-) rename lib/data/repositories/{collection_purchase_repository.dart => purchase_repository.dart} (62%) diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 802ea3ec..3b8f6617 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -4,11 +4,4 @@ to allow setting breakpoints, to provide hot reload, etc. --> - - - - - \ No newline at end of file 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"> 'Comprar deck - R\$ ${price.toStringAsFixed(2)}'; +// TODO(joao): check the best place to store the const double. +const double collectionPrice = 0.99; String collectionsEmptyTitleSegment(CollectionsSegment segment) { switch (segment) { diff --git a/lib/application/pages/details/collection_details_page.dart b/lib/application/pages/details/collection_details_page.dart index 4f52f576..31ce7bc7 100644 --- a/lib/application/pages/details/collection_details_page.dart +++ b/lib/application/pages/details/collection_details_page.dart @@ -97,28 +97,29 @@ class CollectionDetailsPage extends ConsumerWidget { sections.add(resourcesSection); + final studyNowButton = PrimaryElevatedButton( + onPressed: () => readCoordinator(ref).navigateToCollectionExecution(id, isNestedNavigation: false), + text: strings.detailsStudyNow.toUpperCase(), + ); + + final purchaseDeckButton = SecondaryElevatedButton( + backgroundColor: memoTheme.secondarySwatch, + text: strings.collectionPurchaseDeck(strings.collectionPrice), + onPressed: () async => _collectionPurchaseBottomSheet( + context, + () => ref.watch(collectionDetailsVM(id).notifier).purchaseCollection(state.metadata.id), + ), + ); + final fixedBottomAction = ThemedBottomContainer( - child: Container( + child: ColoredBox( color: memoTheme.neutralSwatch.shade800, child: SafeArea( - child: !state.metadata.isVisible - ? PrimaryElevatedButton( - onPressed: () { - readCoordinator(ref).navigateToCollectionExecution(id, isNestedNavigation: false); - }, - text: strings.detailsStudyNow.toUpperCase(), - ).withSymmetricalPadding(context, vertical: Spacing.small, horizontal: Spacing.medium) - : SecondaryElevatedButton( - backgroundColor: memoTheme.secondarySwatch, - text: strings.collectionPurchaseDeck, - onPressed: () async => collectionPurchaseBottomSheet(context, () { - ref.watch(collectionDetailsVM(id).notifier).purchaseCollection(state.metadata.id); - }), - ).withSymmetricalPadding( - context, - vertical: Spacing.small, - horizontal: Spacing.medium, - ), + child: (state.isPurchased ? studyNowButton : purchaseDeckButton), + ).withSymmetricalPadding( + context, + vertical: Spacing.small, + horizontal: Spacing.medium, ), ), ); @@ -158,20 +159,20 @@ class CollectionDetailsPage extends ConsumerWidget { style: Theme.of(context).textTheme.titleMedium?.copyWith(color: ref.watch(themeController).neutralSwatch.shade300), ); -} -/// 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), - ); + /// 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/execution/execution_terminal.dart b/lib/application/pages/execution/execution_terminal.dart index 9fbab2f4..badb5096 100644 --- a/lib/application/pages/execution/execution_terminal.dart +++ b/lib/application/pages/execution/execution_terminal.dart @@ -371,8 +371,9 @@ class _TerminalActions extends HookWidget { Expanded( child: Text( strings.memoDifficulty(difficulty), - style: - Theme.of(context).textTheme.bodyMedium?.copyWith(color: isMarkedAnswer ? highlightColor : null), + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: isMarkedAnswer ? highlightColor : null, + ), ), ), ], diff --git a/lib/application/view-models/app_vm.dart b/lib/application/view-models/app_vm.dart index b88f9fb2..393df401 100644 --- a/lib/application/view-models/app_vm.dart +++ b/lib/application/view-models/app_vm.dart @@ -6,7 +6,7 @@ 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/collection_purchase_repository.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'; @@ -79,7 +79,7 @@ class AppVMImpl extends AppVM { final userRepo = UserRepositoryImpl(dbRepo); final versionRepo = VersionRepositoryImpl(dbRepo); final resourceRepo = ResourceRepositoryImpl(dbRepo, appBundle); - final collectionPurchaseRepo = CollectionPurchaseRepositoryImpl(dbRepo, purchaseGateway, collectionRepo); + final purchaseRepo = PurchaseRepositoryImpl(dbRepo, purchaseGateway); final transactionHandler = TransactionHandlerImpl(dbRepo); @@ -89,7 +89,7 @@ class AppVMImpl extends AppVM { // Services final collectionPurchaseServices = CollectionPurchaseServicesImpl( env: env, - collectionPurchaseRepo: collectionPurchaseRepo, + purchaseRepo: purchaseRepo, collectionRepo: collectionRepo, ); @@ -97,7 +97,7 @@ class AppVMImpl extends AppVM { collectionRepo: collectionRepo, memoRepo: memoRepo, memoryServices: memoryServices, - collectionPurchaseRepo: collectionPurchaseRepo, + purchaseRepo: purchaseRepo, collectionPurchaseServices: collectionPurchaseServices, ); @@ -172,6 +172,6 @@ final progressServices = Provider((_) { final resourceServices = Provider((_) { throw UnimplementedError('resourceServices Provider must be overridden'); }); -final collectionPurchaseServices = Provider((_) { - throw UnimplementedError('collectionPurchaseServices Provider must be overridden'); +final purchaseServices = Provider((_) { + throw UnimplementedError('PurchaseServices Provider must be overridden'); }); diff --git a/lib/application/view-models/details/collection_details_vm.dart b/lib/application/view-models/details/collection_details_vm.dart index 7e4c9ed9..8e0ce860 100644 --- a/lib/application/view-models/details/collection_details_vm.dart +++ b/lib/application/view-models/details/collection_details_vm.dart @@ -17,14 +17,14 @@ final collectionDetailsVM = StateNotifierProvider.family { CollectionDetailsVM(CollectionDetailsState state) : super(state); - /// Purchase a deck, comparing its [id] and updating the isVisible state to `true`. + /// Purchase a deck. Future purchaseCollection(String id); } @@ -47,6 +47,8 @@ class CollectionDetailsVMImpl extends CollectionDetailsVM { late StreamSubscription _listener; Future _loadCollection() async { + final isPurchased = await purchaseServices.isPurchased(id: collectionId); + final stream = await collectionServices.listenToCollectionStatus(collectionId: collectionId); _listener = stream.listen((collectionStatus) async { @@ -82,6 +84,7 @@ class CollectionDetailsVMImpl extends CollectionDetailsVM { memosAmount: memosAmount, resources: mappedResources, contributors: contributors, + isPurchased: isPurchased, ); }); } @@ -97,10 +100,10 @@ class CollectionDetailsVMImpl extends CollectionDetailsVM { try { await purchaseServices.purchaseCollection(id: id); state = PurchaseCollectionSuccess(); + await _loadCollection(); } on BaseException catch (exception) { state = PurchaseCollectionFailed(exception); } - await _loadCollection(); } } @@ -118,6 +121,7 @@ class LoadedCollectionDetailsState extends CollectionDetailsState { required this.memosAmount, required this.resources, required this.contributors, + required this.isPurchased, }); final CollectionItem metadata; @@ -125,6 +129,7 @@ class LoadedCollectionDetailsState extends CollectionDetailsState { final int memosAmount; final List resources; final List contributors; + final bool isPurchased; } class ResourceInfo extends Equatable { diff --git a/lib/application/view-models/home/collections_vm.dart b/lib/application/view-models/home/collections_vm.dart index 862a9ebe..5759dba4 100644 --- a/lib/application/view-models/home/collections_vm.dart +++ b/lib/application/view-models/home/collections_vm.dart @@ -20,7 +20,7 @@ const availableSegments = CollectionsSegment.values; abstract class CollectionsVM extends StateNotifier { CollectionsVM(CollectionsState state) : super(state); - /// Updates the collections list when connected to the internet. + /// Updates the collections list. Future onRefresh(); /// Updates the current [state] with [segment]. @@ -41,6 +41,7 @@ class CollectionsVMImpl extends CollectionsVM { @override Future onRefresh() async { + state = LoadingCollectionsState(state.currentSegment); await _addCollectionsListeners(); } diff --git a/lib/application/view-models/item_metadata.dart b/lib/application/view-models/item_metadata.dart index b9e1af9a..28173896 100644 --- a/lib/application/view-models/item_metadata.dart +++ b/lib/application/view-models/item_metadata.dart @@ -20,7 +20,7 @@ abstract class CollectionItem extends ItemMetadata { required this.name, required this.category, required this.tags, - required this.isVisible, + required this.isPremium, }); final String id; @@ -28,10 +28,10 @@ abstract class CollectionItem extends ItemMetadata { final String category; final List tags; - final bool isVisible; + final bool isPremium; @override - List get props => [id, name, category, tags, isVisible]; + List get props => [id, name, category, tags, isPremium]; } /// Represents a collection that have been fully executed - where no pristine memos are left. @@ -42,13 +42,13 @@ class CompletedCollectionItem extends CollectionItem { required String name, required String category, required List tags, - required bool isVisible, + required bool isPremium, }) : super( id: id, name: name, category: category, tags: tags, - isVisible: isVisible, + isPremium: isPremium, ); final double recallLevel; @@ -67,13 +67,13 @@ class IncompleteCollectionItem extends CollectionItem { required String name, required String category, required List tags, - required bool isVisibile, + required bool isPremium, }) : super( id: id, name: name, category: category, tags: tags, - isVisible: isVisibile, + isPremium: isPremium, ); final int executedUniqueMemos; @@ -97,7 +97,7 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) { name: collection.name, category: collection.category, tags: collection.tags, - isVisible: status.isVisible, + isPremium: collection.isPremium, ); } else { return IncompleteCollectionItem( @@ -107,7 +107,7 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) { name: collection.name, category: collection.category, tags: collection.tags, - isVisibile: status.isVisible, + isPremium: collection.isPremium, ); } } diff --git a/lib/application/widgets/theme/collection_card.dart b/lib/application/widgets/theme/collection_card.dart index bdfbab1f..1168a583 100644 --- a/lib/application/widgets/theme/collection_card.dart +++ b/lib/application/widgets/theme/collection_card.dart @@ -21,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, @@ -36,6 +37,9 @@ class CollectionCard extends ConsumerWidget { /// List of tags associated with this collection. final List tags; + /// + final bool isPremium; + /// If this widget should draw a border for this card. final bool hasBorder; @@ -129,13 +133,10 @@ class CollectionCard extends ConsumerWidget { crossAxisAlignment: WrapCrossAlignment.center, spacing: tagsSpacing, runSpacing: tagsSpacing, - children: tags - .map( - (tag) => tag.toUpperCase().contains(strings.premium) - ? SecondaryTextTag(tag.toUpperCase()) - : PrimaryTextTag(tag.toUpperCase()), - ) - .toList(), + children: [ + ...tags.map((tag) => PrimaryTextTag(tag.toUpperCase())), + if (isPremium) SecondaryTextTag(strings.premium.toUpperCase()), + ], ); } 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/core/faults/exceptions/base_exception.dart b/lib/core/faults/exceptions/base_exception.dart index 46f77458..a66f5627 100644 --- a/lib/core/faults/exceptions/base_exception.dart +++ b/lib/core/faults/exceptions/base_exception.dart @@ -46,5 +46,5 @@ enum ExceptionType { // PurchaseException purchaseProductFailed, - failedPurchase, + failedToPurchase, } diff --git a/lib/core/faults/exceptions/purchase_exception.dart b/lib/core/faults/exceptions/purchase_exception.dart index 6aab49c7..8d34cc35 100644 --- a/lib/core/faults/exceptions/purchase_exception.dart +++ b/lib/core/faults/exceptions/purchase_exception.dart @@ -5,5 +5,5 @@ class PurchaseException extends BaseException { PurchaseException.purchaseProductFailed({String? debugInfo}) : super(type: ExceptionType.purchaseProductFailed, debugInfo: debugInfo); - PurchaseException.failedPurchase() : super(type: ExceptionType.failedPurchase); + PurchaseException.failedPurchase() : super(type: ExceptionType.failedToPurchase); } diff --git a/lib/data/gateways/sembast_database.dart b/lib/data/gateways/sembast_database.dart index 7cd8f0c2..f64fd4aa 100644 --- a/lib/data/gateways/sembast_database.dart +++ b/lib/data/gateways/sembast_database.dart @@ -31,7 +31,6 @@ abstract class SembastTransactionHandler implements DatabaseTransactionHandler { await db.transaction((transaction) async { currentTransaction = transaction; await run(); - currentTransaction = null; }); // ignore: avoid_catches_without_on_clauses } catch (error, stack) { diff --git a/lib/data/repositories/collection_purchase_repository.dart b/lib/data/repositories/purchase_repository.dart similarity index 62% rename from lib/data/repositories/collection_purchase_repository.dart rename to lib/data/repositories/purchase_repository.dart index bb26e60d..fae667ef 100644 --- a/lib/data/repositories/collection_purchase_repository.dart +++ b/lib/data/repositories/purchase_repository.dart @@ -1,9 +1,7 @@ import 'package:memo/data/gateways/purchase_gateway.dart'; import 'package:memo/data/gateways/sembast_database.dart'; -import 'package:memo/data/repositories/collection_repository.dart'; -import 'package:memo/data/serializers/collection_serializer.dart'; -abstract class CollectionPurchaseRepository { +abstract class PurchaseRepository { /// Purchase products in the app with the store ID [storeId] for the local user. Future purchaseInApp({required String storeId}); @@ -14,19 +12,19 @@ abstract class CollectionPurchaseRepository { Future> isAvailable(); /// Updates the collection with the [id] to be premium or not. - Future updatePurchaseCollection({required String id, required bool isPremium}); + Future updatePurchase({required String purchaseId}); + + Future> getPurchaseProducts(); } -class CollectionPurchaseRepositoryImpl implements CollectionPurchaseRepository { - CollectionPurchaseRepositoryImpl(this._db, this._purchaseGateway, this.collectionRepo); +class PurchaseRepositoryImpl implements PurchaseRepository { + PurchaseRepositoryImpl(this._db, this._purchaseGateway); final SembastDatabase _db; - final _collectionStore = 'collections'; + final _purchasesStore = 'purchases'; final PurchaseGateway _purchaseGateway; - final CollectionRepository collectionRepo; - @override Future purchaseInApp({required String storeId}) => _purchaseGateway.purchase( identifier: storeId, @@ -45,11 +43,17 @@ class CollectionPurchaseRepositoryImpl implements CollectionPurchaseRepository { } @override - Future updatePurchaseCollection({required String id, required bool isPremium}) => _db.put( - id: id, + Future updatePurchase({required String purchaseId}) => _db.put( + id: purchaseId, object: { - CollectionKeys.isPremium: isPremium, + 'purchasesId': purchaseId, }, - store: _collectionStore, + store: _purchasesStore, ); + + @override + Future> getPurchaseProducts() async { + final purchases = await _db.getAll(store: _purchasesStore); + return purchases.map((purchase) => purchase['purchasesId'] as String).toList(); + } } diff --git a/lib/data/serializers/collection_serializer.dart b/lib/data/serializers/collection_serializer.dart index 29b0e894..63491695 100644 --- a/lib/data/serializers/collection_serializer.dart +++ b/lib/data/serializers/collection_serializer.dart @@ -81,5 +81,6 @@ class CollectionSerializer implements Serializer purchaseCollection({required String id}); /// Verifies if the collection - from [id] - is visible to the user. - Future isVisible({required String id}); + Future isPurchased({required String id}); } class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { CollectionPurchaseServicesImpl({ required this.env, - required this.collectionPurchaseRepo, + required this.purchaseRepo, required this.collectionRepo, }); final EnvMetadata env; - final CollectionPurchaseRepository collectionPurchaseRepo; + final PurchaseRepository purchaseRepo; final CollectionRepository collectionRepo; @@ -31,41 +31,43 @@ class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { final collection = await collectionRepo.getCollection(id: id); await _purchaseInAppCollection(collection); - await _updatePurchaseCollection(id: id, isPremium: false); + await _updatePurchaseCollection(id: id); } - Future _updatePurchaseCollection({required String id, required bool isPremium}) async { + Future _updatePurchaseCollection({required String id}) async { final collection = await collectionRepo.getCollection(id: id); - final isPurchased = await collectionPurchaseRepo.getPurchasesInfo(); + final isPurchased = await purchaseRepo.getPurchasesInfo(); - if (isPurchased.contains(collection.appStoreId) || isPurchased.contains(collection.playStoreId)) { - await collectionPurchaseRepo.updatePurchaseCollection(id: id, isPremium: isPremium); + if (isPurchased.contains(_collectionStore(collection))) { + await purchaseRepo.updatePurchase(purchaseId: _collectionStore(collection)); } } Future _purchaseInAppCollection(Collection collection) async { + await purchaseRepo.purchaseInApp(storeId: _collectionStore(collection)); + } + + String _collectionStore(Collection collection) { switch (env.platform) { case SupportedPlatform.ios: - await collectionPurchaseRepo.purchaseInApp(storeId: collection.appStoreId); - break; + return collection.appStoreId; case SupportedPlatform.android: - await collectionPurchaseRepo.purchaseInApp(storeId: collection.playStoreId); - break; + return collection.playStoreId; } } @override - Future isVisible({required String id}) async { + Future isPurchased({required String id}) async { final collection = await collectionRepo.getCollection(id: id); - if (collection.isPremium) { - final isAvailable = await collectionPurchaseRepo.isAvailable(); - final isPurchased = await collectionPurchaseRepo.getPurchasesInfo(); - - if (isAvailable.contains(collection.appStoreId) || isAvailable.contains(collection.playStoreId)) { - return !isPurchased.contains(collection.appStoreId) || !isPurchased.contains(collection.playStoreId); - } + if (!collection.isPremium) { + return true; } - return collection.isPremium; + + final storeId = _collectionStore(collection); + + final purchasedProductsList = await purchaseRepo.getPurchaseProducts(); + + return purchasedProductsList.contains(storeId); } } diff --git a/lib/domain/services/collection_services.dart b/lib/domain/services/collection_services.dart index 3c39acdd..d9bbb218 100644 --- a/lib/domain/services/collection_services.dart +++ b/lib/domain/services/collection_services.dart @@ -1,6 +1,5 @@ -import 'package:flutter/services.dart'; import 'package:memo/core/faults/errors/inconsistent_state_error.dart'; -import 'package:memo/data/repositories/collection_purchase_repository.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'; @@ -33,7 +32,7 @@ class CollectionServicesImpl implements CollectionServices { required this.collectionRepo, required this.memoRepo, required this.memoryServices, - required this.collectionPurchaseRepo, + required this.purchaseRepo, required this.collectionPurchaseServices, }); @@ -42,7 +41,7 @@ class CollectionServicesImpl implements CollectionServices { final MemoryRecallServices memoryServices; - final CollectionPurchaseRepository collectionPurchaseRepo; + final PurchaseRepository purchaseRepo; final CollectionPurchaseServices collectionPurchaseServices; @override @@ -50,26 +49,12 @@ class CollectionServicesImpl implements CollectionServices { final collectionsStream = await collectionRepo.listenToAllCollections(); // Asynchronously transform the stream due to the async calculations. - try { - final isAvailbleList = await collectionPurchaseRepo.isAvailable(); - if (isAvailbleList.isNotEmpty) { - return collectionsStream.asyncMap( - (collections) { - final mappedStatuses = collections.map(_mapCollectionToCollectionStatus).toList(); - return Future.wait(mappedStatuses); - }, - ); - } - } on PlatformException catch (_) { - return collectionsStream.asyncMap( - (collections) { - final offlineCollections = collections.where((collection) => !collection.isPremium).toList(); - final mappedStatuses = offlineCollections.map(_mapCollectionToCollectionStatus).toList(); - return Future.wait(mappedStatuses); - }, - ); - } - throw InconsistentStateError.service('Missing required collection purchase information'); + return collectionsStream.asyncMap( + (collections) { + final mappedStatuses = collections.map(_mapCollectionToCollectionStatus).toList(); + return Future.wait(mappedStatuses); + }, + ); } @override @@ -92,12 +77,10 @@ class CollectionServicesImpl implements CollectionServices { Future _mapCollectionToCollectionStatus(Collection collection) async { double? memoryRecall; - bool isVisible; if (collection.isCompleted) { memoryRecall = await _getMemosAverageMemoryRecall(collectionId: collection.id); } - isVisible = await collectionPurchaseServices.isVisible(id: collection.id); - return CollectionStatus(collection, memoryRecall, isVisible: isVisible); + return CollectionStatus(collection, memoryRecall); } @override diff --git a/lib/domain/transients/collection_status.dart b/lib/domain/transients/collection_status.dart index a930d66e..1a7f87d0 100644 --- a/lib/domain/transients/collection_status.dart +++ b/lib/domain/transients/collection_status.dart @@ -2,7 +2,7 @@ import 'package:memo/domain/models/collection.dart'; /// Groups a [Collection] with its memory recall. class CollectionStatus { - CollectionStatus(this.collection, this.memoryRecall, {required this.isVisible}); + CollectionStatus(this.collection, this.memoryRecall); final Collection collection; @@ -10,6 +10,4 @@ class CollectionStatus { /// /// Should be `null` if [Collection.isCompleted] is `false`. final double? memoryRecall; - - final bool isVisible; } diff --git a/test/application/widgets/theme/custom_button_test.dart b/test/application/widgets/theme/custom_button_test.dart index 40c07c40..80ad3fe1 100644 --- a/test/application/widgets/theme/custom_button_test.dart +++ b/test/application/widgets/theme/custom_button_test.dart @@ -173,13 +173,13 @@ void main() { }); testWidgets('must shrink based on its content', (tester) async { - const expectedWidth = 64.4000015258789; + const expectedWidth = 64.0; const textButton = CustomTextButton(text: 'Text'); 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 349be89c..ac59c4dc 100644 --- a/test/data/serializers/collection_memos_serializer_test.dart +++ b/test/data/serializers/collection_memos_serializer_test.dart @@ -17,6 +17,7 @@ void main() { tags: const ['Tag 1', 'Tag 2'], isPremium: false, appStoreId: 'appStoreId', + playStoreId: 'playStoreId', contributors: [const Contributor(name: 'name')], memosMetadata: [ MemoCollectionMetadata( diff --git a/test/data/serializers/collection_serializer_test.dart b/test/data/serializers/collection_serializer_test.dart index e9035128..5703a7f6 100644 --- a/test/data/serializers/collection_serializer_test.dart +++ b/test/data/serializers/collection_serializer_test.dart @@ -15,6 +15,7 @@ void main() { category: 'Category', isPremium: false, appStoreId: 'appStoreId', + playStoreId: 'playStoreId', contributors: const [Contributor(name: 'name')], tags: const ['Tag 1', 'Tag 2'], uniqueMemosAmount: 1, @@ -106,6 +107,7 @@ void main() { tags: const ['Tag 1', 'Tag 2'], isPremium: false, appStoreId: 'appStoreId', + playStoreId: 'playStoreId', uniqueMemosAmount: 1, uniqueMemoExecutionsAmount: 1, executionsAmounts: const {MemoDifficulty.easy: 1}, diff --git a/test/domain/models/collection_test.dart b/test/domain/models/collection_test.dart index b23c12b7..fc3236e9 100644 --- a/test/domain/models/collection_test.dart +++ b/test/domain/models/collection_test.dart @@ -18,6 +18,7 @@ void main() { tags: const [], isPremium: false, appStoreId: 'appStoreId', + playStoreId: 'playStoreId', 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 536a46d9..21924e15 100644 --- a/test/domain/transients/collection_memos_test.dart +++ b/test/domain/transients/collection_memos_test.dart @@ -17,6 +17,7 @@ void main() { tags: const [], isPremium: false, appStoreId: 'appStoreId', + playStoreId: 'playStoreId', 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 8b67431b..3ded61a6 100644 --- a/test/fixtures/collection.json +++ b/test/fixtures/collection.json @@ -10,6 +10,7 @@ ], "isPremium": false, "appStoreId": "appStoreId", + "playStoreId": "playStoreId", "uniqueMemosAmount": 1, "uniqueMemoExecutionsAmount": 0, "executionsAmounts": { diff --git a/test/fixtures/collection_memos.json b/test/fixtures/collection_memos.json index 91f220b4..c7d11444 100644 --- a/test/fixtures/collection_memos.json +++ b/test/fixtures/collection_memos.json @@ -10,6 +10,6 @@ ], "isPremium": false, "appStoreId": "appStoreId", - "playStoreId": null, + "playStoreId": "playStoreId", "memos": [] } \ No newline at end of file From 9b137c95c9fe65cd5fa294975548066cf21ce790 Mon Sep 17 00:00:00 2001 From: Joaoaraujo97 Date: Fri, 5 Jul 2024 14:58:30 -0300 Subject: [PATCH 06/12] Minimal fix on manifest. --- android/app/src/debug/AndroidManifest.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/android/app/src/debug/AndroidManifest.xml b/android/app/src/debug/AndroidManifest.xml index 3b8f6617..0350eafe 100644 --- a/android/app/src/debug/AndroidManifest.xml +++ b/android/app/src/debug/AndroidManifest.xml @@ -4,4 +4,4 @@ to allow setting breakpoints, to provide hot reload, etc. --> - \ No newline at end of file + From dd26b55d5f65a8912032d016acdf47ac19ea0929 Mon Sep 17 00:00:00 2001 From: Joaoaraujo97 Date: Mon, 8 Jul 2024 18:56:35 -0300 Subject: [PATCH 07/12] More fixes on Purchase Collections. --- assets/collections/0_chatGPT_premium.json | 6 ++-- assets/collections/bdd_fundamentos_01.json | 4 +++ assets/collections/comecando_com_git.json | 4 +++ .../collections/ecossistema_do_flutter.json | 4 +++ assets/collections/fundamentos_scrum.json | 4 +++ assets/collections/guia_scrum.json | 4 +++ assets/collections/kotlin_fundamentos_01.json | 4 +++ assets/collections/manifesto_agil.json | 4 +++ assets/collections/swift_fundamentos_01.json | 4 +++ .../details/collection_details_page.dart | 2 +- lib/application/view-models/app_vm.dart | 4 +++ .../view-models/item_metadata.dart | 8 +++++ .../repositories/collection_repository.dart | 5 +-- .../collection_memos_serializer.dart | 15 ++++---- .../serializers/collection_serializer.dart | 15 ++++---- .../serializers/product_info_serializer.dart | 26 ++++++++++++++ lib/domain/models/collection.dart | 19 ++++------ lib/domain/models/product_info.dart | 14 ++++++++ .../collection_purchase_services.dart | 31 +++++++++------- lib/domain/transients/collection_memos.dart | 12 +++---- .../collection_memos_serializer_test.dart | 14 ++++++-- .../collection_serializer_test.dart | 19 ++++++---- .../product_info_serializar_test.dart | 36 +++++++++++++++++++ test/domain/models/collection_test.dart | 5 +-- .../transients/collection_memos_test.dart | 5 +-- test/fixtures/collection.json | 3 +- test/fixtures/collection_memos.json | 3 +- test/fixtures/fixtures.dart | 1 + test/fixtures/product_info.json | 4 +++ 29 files changed, 206 insertions(+), 73 deletions(-) create mode 100644 lib/data/serializers/product_info_serializer.dart create mode 100644 lib/domain/models/product_info.dart create mode 100644 test/data/serializers/product_info_serializar_test.dart create mode 100644 test/fixtures/product_info.json diff --git a/assets/collections/0_chatGPT_premium.json b/assets/collections/0_chatGPT_premium.json index 0b5ddf25..94a4f474 100644 --- a/assets/collections/0_chatGPT_premium.json +++ b/assets/collections/0_chatGPT_premium.json @@ -7,8 +7,10 @@ "ChatGPT" ], "isPremium": true, - "appStoreId": "com.olmps.memo_099_in_app_purchase_deck", - "playStoreId": "com.olmps.memo_099_in_app_purchase", + "productInfo": { + "price": 0.99, + "productId": "com.olmps.memo_099_in_app_purchase_deck" + }, "contributors": [ { "name": "chatGPT", diff --git a/assets/collections/bdd_fundamentos_01.json b/assets/collections/bdd_fundamentos_01.json index 837f6fe6..16c33ed5 100644 --- a/assets/collections/bdd_fundamentos_01.json +++ b/assets/collections/bdd_fundamentos_01.json @@ -7,6 +7,10 @@ "tests", "bdd" ], + "productInfo": { + "price": null, + "productId": null + }, "contributors": [ { "name": "Nicolas Nascimento", diff --git a/assets/collections/comecando_com_git.json b/assets/collections/comecando_com_git.json index 52ee6fd2..5e875114 100644 --- a/assets/collections/comecando_com_git.json +++ b/assets/collections/comecando_com_git.json @@ -7,6 +7,10 @@ "git", "versionamento" ], + "productInfo": { + "price": null, + "productId": null + }, "contributors": [ { "name": "@matuella", diff --git a/assets/collections/ecossistema_do_flutter.json b/assets/collections/ecossistema_do_flutter.json index c95da844..379218fe 100644 --- a/assets/collections/ecossistema_do_flutter.json +++ b/assets/collections/ecossistema_do_flutter.json @@ -8,6 +8,10 @@ "framework", "cross-platform" ], + "productInfo": { + "price": null, + "productId": null + }, "contributors": [ { "name": "@matuella", diff --git a/assets/collections/fundamentos_scrum.json b/assets/collections/fundamentos_scrum.json index 1fd2dd10..b60977d8 100644 --- a/assets/collections/fundamentos_scrum.json +++ b/assets/collections/fundamentos_scrum.json @@ -7,6 +7,10 @@ "agile", "scrum" ], + "productInfo": { + "price": null, + "productId": null + }, "contributors": [ { "name": "Olympus", diff --git a/assets/collections/guia_scrum.json b/assets/collections/guia_scrum.json index 59351432..0ad3a192 100644 --- a/assets/collections/guia_scrum.json +++ b/assets/collections/guia_scrum.json @@ -7,6 +7,10 @@ "agile", "scrum" ], + "productInfo": { + "price": null, + "productId": null + }, "contributors": [ { "name": "Daniel Wildt", diff --git a/assets/collections/kotlin_fundamentos_01.json b/assets/collections/kotlin_fundamentos_01.json index 91237957..b1e09d4a 100644 --- a/assets/collections/kotlin_fundamentos_01.json +++ b/assets/collections/kotlin_fundamentos_01.json @@ -7,6 +7,10 @@ "kotlin", "linguagem de programação" ], + "productInfo": { + "price": null, + "productId": null + }, "contributors": [ { "name": "Lucas Montano", diff --git a/assets/collections/manifesto_agil.json b/assets/collections/manifesto_agil.json index 28d888e0..b8806b8a 100644 --- a/assets/collections/manifesto_agil.json +++ b/assets/collections/manifesto_agil.json @@ -6,6 +6,10 @@ "tags": [ "agile" ], + "productInfo": { + "price": null, + "productId": null + }, "contributors": [ { "name": "Daniel Wildt", diff --git a/assets/collections/swift_fundamentos_01.json b/assets/collections/swift_fundamentos_01.json index a128adf4..14ef3103 100644 --- a/assets/collections/swift_fundamentos_01.json +++ b/assets/collections/swift_fundamentos_01.json @@ -7,6 +7,10 @@ "swift", "linguagem de programação" ], + "productInfo": { + "price": null, + "productId": null + }, "contributors": [ { "name": "@matuella", diff --git a/lib/application/pages/details/collection_details_page.dart b/lib/application/pages/details/collection_details_page.dart index 31ce7bc7..63891aaa 100644 --- a/lib/application/pages/details/collection_details_page.dart +++ b/lib/application/pages/details/collection_details_page.dart @@ -104,7 +104,7 @@ class CollectionDetailsPage extends ConsumerWidget { final purchaseDeckButton = SecondaryElevatedButton( backgroundColor: memoTheme.secondarySwatch, - text: strings.collectionPurchaseDeck(strings.collectionPrice), + text: strings.collectionPurchaseDeck(state.metadata.price), onPressed: () async => _collectionPurchaseBottomSheet( context, () => ref.watch(collectionDetailsVM(id).notifier).purchaseCollection(state.metadata.id), diff --git a/lib/application/view-models/app_vm.dart b/lib/application/view-models/app_vm.dart index 393df401..93a1ea73 100644 --- a/lib/application/view-models/app_vm.dart +++ b/lib/application/view-models/app_vm.dart @@ -139,6 +139,10 @@ class AppVMImpl extends AppVM { splashMinDuration, ]); + await Future.wait([ + collectionPurchaseServices.updatePurchasesIfNeeded(), + ]); + value = AsyncValue.data(appState); } } diff --git a/lib/application/view-models/item_metadata.dart b/lib/application/view-models/item_metadata.dart index 28173896..ab335cb9 100644 --- a/lib/application/view-models/item_metadata.dart +++ b/lib/application/view-models/item_metadata.dart @@ -21,6 +21,7 @@ abstract class CollectionItem extends ItemMetadata { required this.category, required this.tags, required this.isPremium, + required this.price, }); final String id; @@ -29,6 +30,7 @@ abstract class CollectionItem extends ItemMetadata { final List tags; final bool isPremium; + final double price; @override List get props => [id, name, category, tags, isPremium]; @@ -43,12 +45,14 @@ class CompletedCollectionItem extends CollectionItem { required String category, required List tags, required bool isPremium, + required double price, }) : super( id: id, name: name, category: category, tags: tags, isPremium: isPremium, + price: price, ); final double recallLevel; @@ -68,12 +72,14 @@ class IncompleteCollectionItem extends CollectionItem { required String category, required List tags, required bool isPremium, + required double price, }) : super( id: id, name: name, category: category, tags: tags, isPremium: isPremium, + price: price, ); final int executedUniqueMemos; @@ -98,6 +104,7 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) { category: collection.category, tags: collection.tags, isPremium: collection.isPremium, + price: collection.productInfo.price, ); } else { return IncompleteCollectionItem( @@ -108,6 +115,7 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) { category: collection.category, tags: collection.tags, isPremium: collection.isPremium, + price: collection.productInfo.price, ); } } diff --git a/lib/data/repositories/collection_repository.dart b/lib/data/repositories/collection_repository.dart index d5ff69e8..ff93cf6f 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 { @@ -91,8 +93,7 @@ class CollectionRepositoryImpl implements CollectionRepository { CollectionKeys.uniqueMemosAmount: collection.uniqueMemosAmount, CollectionKeys.uniqueMemoExecutionsAmount: collection.uniqueMemoExecutionsAmount, CollectionKeys.isPremium: collection.isPremium, - CollectionKeys.appStoreId: collection.appStoreId, - CollectionKeys.playStoreId: collection.playStoreId, + CollectionKeys.productInfo: _productInfoSerializer.to(collection.productInfo), }, ) .toList(), diff --git a/lib/data/serializers/collection_memos_serializer.dart b/lib/data/serializers/collection_memos_serializer.dart index b199e5f8..fce9b15d 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'; @@ -12,13 +13,13 @@ class CollectionMemosKeys { static const tags = 'tags'; static const memosMetadata = 'memos'; static const isPremium = 'isPremium'; - static const appStoreId = 'appStoreId'; - static const playStoreId = 'playStoreId'; + static const productInfo = 'productInfo'; } class CollectionMemosSerializer implements Serializer> { final memoMetadataSerializer = MemoCollectionMetadataSerializer(); final contributorSerializer = ContributorSerializer(); + final productInfoSerializer = ProductInfoSerializer(); @override CollectionMemos from(Map json) { @@ -27,8 +28,8 @@ class CollectionMemosSerializer implements Serializer); final tags = List.from(json[CollectionMemosKeys.tags] as List); @@ -47,8 +48,7 @@ class CollectionMemosSerializer implements Serializer> { final contributorSerializer = ContributorSerializer(); + final productInfoSerializar = ProductInfoSerializer(); @override Collection from(Map json) { @@ -46,9 +47,7 @@ class CollectionSerializer implements Serializer); return Collection( id: id, @@ -62,8 +61,7 @@ class CollectionSerializer 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 50f34694..9c1f659c 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. @@ -17,8 +18,7 @@ class Collection extends MemoExecutionsMetadata with EquatableMixin implements C required this.uniqueMemosAmount, required this.contributors, required this.isPremium, - required this.appStoreId, - required this.playStoreId, + required this.productInfo, this.uniqueMemoExecutionsAmount = 0, Map executionsAmounts = const {}, int timeSpentInMillis = 0, @@ -55,10 +55,7 @@ class Collection extends MemoExecutionsMetadata with EquatableMixin implements C final bool isPremium; @override - final String appStoreId; - - @override - final String playStoreId; + final ProductInfo productInfo; @override final int uniqueMemosAmount; @@ -81,8 +78,7 @@ class Collection extends MemoExecutionsMetadata with EquatableMixin implements C tags, contributors, isPremium, - appStoreId, - playStoreId, + productInfo, uniqueMemoExecutionsAmount, uniqueMemosAmount, ...super.props, @@ -105,11 +101,8 @@ abstract class CollectionMetadata { /// Informs whether the collection is premium or not. bool get isPremium; - /// App store id for this collection. - String get appStoreId; - - /// Play store id for this collection. - String get playStoreId; + /// Informs the `productId` 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 index f2ad23b2..c85106ed 100644 --- a/lib/domain/services/collection_purchase_services.dart +++ b/lib/domain/services/collection_purchase_services.dart @@ -11,6 +11,11 @@ abstract class CollectionPurchaseServices { /// 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 { @@ -38,22 +43,13 @@ class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { final collection = await collectionRepo.getCollection(id: id); final isPurchased = await purchaseRepo.getPurchasesInfo(); - if (isPurchased.contains(_collectionStore(collection))) { - await purchaseRepo.updatePurchase(purchaseId: _collectionStore(collection)); + if (isPurchased.contains(collection.productInfo.id)) { + await purchaseRepo.updatePurchase(purchaseId: collection.productInfo.id); } } Future _purchaseInAppCollection(Collection collection) async { - await purchaseRepo.purchaseInApp(storeId: _collectionStore(collection)); - } - - String _collectionStore(Collection collection) { - switch (env.platform) { - case SupportedPlatform.ios: - return collection.appStoreId; - case SupportedPlatform.android: - return collection.playStoreId; - } + await purchaseRepo.purchaseInApp(storeId: collection.productInfo.id); } @override @@ -64,10 +60,19 @@ class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { return true; } - final storeId = _collectionStore(collection); + final storeId = collection.productInfo.id; final purchasedProductsList = await purchaseRepo.getPurchaseProducts(); return purchasedProductsList.contains(storeId); } + + @override + Future updatePurchasesIfNeeded() async { + final collections = await collectionRepo.getAllCollectionMemos(); + + for (final collection in collections) { + await _updatePurchaseCollection(id: collection.id); + } + } } diff --git a/lib/domain/transients/collection_memos.dart b/lib/domain/transients/collection_memos.dart index 387c624b..9ca9f078 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]. /// @@ -15,8 +16,7 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { required this.contributors, required this.memosMetadata, required this.isPremium, - required this.appStoreId, - required this.playStoreId, + required this.productInfo, int uniqueMemoExecutionsAmount = 0, }) : _uniqueMemoExecutionsAmount = uniqueMemoExecutionsAmount, assert(memosMetadata.isNotEmpty, 'must not be an empty list of memos'), @@ -47,10 +47,7 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { final bool isPremium; @override - final String appStoreId; - - @override - final String playStoreId; + final ProductInfo productInfo; @override int get uniqueMemosAmount => memosMetadata.length; @@ -77,7 +74,6 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { uniqueMemosAmount, memosMetadata, isPremium, - appStoreId, - playStoreId, + productInfo, ]; } diff --git a/test/data/serializers/collection_memos_serializer_test.dart b/test/data/serializers/collection_memos_serializer_test.dart index ac59c4dc..24b6c4a6 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; @@ -16,8 +17,7 @@ void main() { category: 'Category', tags: const ['Tag 1', 'Tag 2'], isPremium: false, - appStoreId: 'appStoreId', - playStoreId: 'playStoreId', + productInfo: ProductInfo(id: '', price: 0.0), contributors: [const Contributor(name: 'name')], memosMetadata: [ MemoCollectionMetadata( @@ -30,7 +30,8 @@ void main() { 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(); @@ -85,6 +86,13 @@ void main() { }, throwsA(isA()), ); + expect( + () { + final rawCollection = completeFixture()..remove(CollectionMemosKeys.productInfo); + serializer.from(rawCollection); + }, + throwsA(isA()), + ); expect( () { final rawCollection = completeFixture()..remove(CollectionMemosKeys.memosMetadata); diff --git a/test/data/serializers/collection_serializer_test.dart b/test/data/serializers/collection_serializer_test.dart index 5703a7f6..d63f00e5 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; @@ -14,15 +15,15 @@ void main() { description: 'This collection represents a collection.', category: 'Category', isPremium: false, - appStoreId: 'appStoreId', - playStoreId: 'playStoreId', + productInfo: ProductInfo(price: 0.0, id: ''), 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(); @@ -77,6 +78,13 @@ void main() { }, throwsA(isA()), ); + expect( + () { + final rawCollection = completeFixture()..remove(CollectionKeys.productInfo); + serializer.from(rawCollection); + }, + throwsA(isA()), + ); expect( () { final rawCollection = completeFixture()..remove(CollectionKeys.uniqueMemosAmount); @@ -106,8 +114,7 @@ void main() { contributors: const [Contributor(name: 'name')], tags: const ['Tag 1', 'Tag 2'], isPremium: false, - appStoreId: 'appStoreId', - playStoreId: 'playStoreId', + 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 fc3236e9..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', @@ -17,8 +19,7 @@ void main() { category: 'category', tags: const [], isPremium: false, - appStoreId: 'appStoreId', - playStoreId: 'playStoreId', + 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 21924e15..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( @@ -16,8 +18,7 @@ void main() { category: 'category', tags: const [], isPremium: false, - appStoreId: 'appStoreId', - playStoreId: 'playStoreId', + 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 3ded61a6..cb05ea23 100644 --- a/test/fixtures/collection.json +++ b/test/fixtures/collection.json @@ -4,13 +4,12 @@ "description": "This collection represents a collection.", "category": "Category", "contributors": [], + "productInfo": {}, "tags": [ "Tag 1", "Tag 2" ], "isPremium": false, - "appStoreId": "appStoreId", - "playStoreId": "playStoreId", "uniqueMemosAmount": 1, "uniqueMemoExecutionsAmount": 0, "executionsAmounts": { diff --git a/test/fixtures/collection_memos.json b/test/fixtures/collection_memos.json index c7d11444..bf394441 100644 --- a/test/fixtures/collection_memos.json +++ b/test/fixtures/collection_memos.json @@ -4,12 +4,11 @@ "description": "This collection represents a collection.", "category": "Category", "contributors": [], + "productInfo": {}, "tags": [ "Tag 1", "Tag 2" ], "isPremium": false, - "appStoreId": "appStoreId", - "playStoreId": "playStoreId", "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..e1e65708 --- /dev/null +++ b/test/fixtures/product_info.json @@ -0,0 +1,4 @@ +{ + "productId": "", + "price": 0.0 +} \ No newline at end of file From 49abade7e6cf9ba0099ff92060e3807993c0828c Mon Sep 17 00:00:00 2001 From: Joaoaraujo97 Date: Tue, 9 Jul 2024 12:12:38 -0300 Subject: [PATCH 08/12] Some more fixes in collection purchases. --- CHANGELOG.md | 4 ++ assets/collections/0_chatGPT_premium.json | 46 ------------------- assets/collections/bdd_fundamentos_01.json | 4 -- assets/collections/comecando_com_git.json | 4 -- .../collections/ecossistema_do_flutter.json | 4 -- assets/collections/fundamentos_scrum.json | 4 -- assets/collections/guia_scrum.json | 4 -- assets/collections/kotlin_fundamentos_01.json | 4 -- assets/collections/manifesto_agil.json | 4 -- assets/collections/swift_fundamentos_01.json | 4 -- lib/application/constants/strings.dart | 4 +- .../details/collection_details_page.dart | 32 +++++++------ lib/application/view-models/app_vm.dart | 5 +- .../view-models/item_metadata.dart | 12 ++--- .../widgets/theme/collection_card.dart | 2 +- .../repositories/collection_repository.dart | 2 +- .../repositories/purchase_repository.dart | 24 ++++++---- .../collection_memos_serializer.dart | 5 +- .../serializers/collection_serializer.dart | 9 ++-- .../serializers/product_info_serializer.dart | 2 +- lib/domain/models/collection.dart | 8 ++-- .../collection_purchase_services.dart | 21 ++++----- lib/domain/transients/collection_memos.dart | 4 +- .../collection_memos_serializer_test.dart | 12 +---- .../collection_serializer_test.dart | 16 ++----- .../product_info_serializar_test.dart | 36 --------------- test/fixtures/collection.json | 1 - test/fixtures/collection_memos.json | 1 - test/fixtures/product_info.json | 4 -- 29 files changed, 77 insertions(+), 205 deletions(-) delete mode 100644 assets/collections/0_chatGPT_premium.json delete mode 100644 test/data/serializers/product_info_serializar_test.dart delete mode 100644 test/fixtures/product_info.json 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/assets/collections/0_chatGPT_premium.json b/assets/collections/0_chatGPT_premium.json deleted file mode 100644 index 94a4f474..00000000 --- a/assets/collections/0_chatGPT_premium.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "id": "0_chatGPT_premium", - "name": "teste", - "description": "teste", - "category": "Premium", - "tags": [ - "ChatGPT" - ], - "isPremium": true, - "productInfo": { - "price": 0.99, - "productId": "com.olmps.memo_099_in_app_purchase_deck" - }, - "contributors": [ - { - "name": "chatGPT", - "url": "https://www.linkedin.com/", - "imageUrl": "https://media-exp1.licdn.com/dms/image/C4E03AQFYmdJJeE9gmA/profile-displayphoto-shrink_400_400/0/1517688960443?e=1631750400&v=beta&t=bpxRB_CL0-3CaT4nzadf1PKpSblm2I_Z7yjm6gEdaBM" - } - ], - "memos": [ - { - "uniqueId": "03c9a8d5-9e27-4ec2-8a3c-7bf20431b890", - "question": [ - { - "insert": "Testes e mais testes\n" - } - ], - "answer": [ - { - "insert": "Isso é mais conhecido como " - }, - { - "insert": "testes", - "attributes": { - "bold": true, - "underline": true - } - }, - { - "insert": ".\n" - } - ] - } - ] -} \ No newline at end of file diff --git a/assets/collections/bdd_fundamentos_01.json b/assets/collections/bdd_fundamentos_01.json index 16c33ed5..837f6fe6 100644 --- a/assets/collections/bdd_fundamentos_01.json +++ b/assets/collections/bdd_fundamentos_01.json @@ -7,10 +7,6 @@ "tests", "bdd" ], - "productInfo": { - "price": null, - "productId": null - }, "contributors": [ { "name": "Nicolas Nascimento", diff --git a/assets/collections/comecando_com_git.json b/assets/collections/comecando_com_git.json index 5e875114..52ee6fd2 100644 --- a/assets/collections/comecando_com_git.json +++ b/assets/collections/comecando_com_git.json @@ -7,10 +7,6 @@ "git", "versionamento" ], - "productInfo": { - "price": null, - "productId": null - }, "contributors": [ { "name": "@matuella", diff --git a/assets/collections/ecossistema_do_flutter.json b/assets/collections/ecossistema_do_flutter.json index 379218fe..c95da844 100644 --- a/assets/collections/ecossistema_do_flutter.json +++ b/assets/collections/ecossistema_do_flutter.json @@ -8,10 +8,6 @@ "framework", "cross-platform" ], - "productInfo": { - "price": null, - "productId": null - }, "contributors": [ { "name": "@matuella", diff --git a/assets/collections/fundamentos_scrum.json b/assets/collections/fundamentos_scrum.json index b60977d8..1fd2dd10 100644 --- a/assets/collections/fundamentos_scrum.json +++ b/assets/collections/fundamentos_scrum.json @@ -7,10 +7,6 @@ "agile", "scrum" ], - "productInfo": { - "price": null, - "productId": null - }, "contributors": [ { "name": "Olympus", diff --git a/assets/collections/guia_scrum.json b/assets/collections/guia_scrum.json index 0ad3a192..59351432 100644 --- a/assets/collections/guia_scrum.json +++ b/assets/collections/guia_scrum.json @@ -7,10 +7,6 @@ "agile", "scrum" ], - "productInfo": { - "price": null, - "productId": null - }, "contributors": [ { "name": "Daniel Wildt", diff --git a/assets/collections/kotlin_fundamentos_01.json b/assets/collections/kotlin_fundamentos_01.json index b1e09d4a..91237957 100644 --- a/assets/collections/kotlin_fundamentos_01.json +++ b/assets/collections/kotlin_fundamentos_01.json @@ -7,10 +7,6 @@ "kotlin", "linguagem de programação" ], - "productInfo": { - "price": null, - "productId": null - }, "contributors": [ { "name": "Lucas Montano", diff --git a/assets/collections/manifesto_agil.json b/assets/collections/manifesto_agil.json index b8806b8a..28d888e0 100644 --- a/assets/collections/manifesto_agil.json +++ b/assets/collections/manifesto_agil.json @@ -6,10 +6,6 @@ "tags": [ "agile" ], - "productInfo": { - "price": null, - "productId": null - }, "contributors": [ { "name": "Daniel Wildt", diff --git a/assets/collections/swift_fundamentos_01.json b/assets/collections/swift_fundamentos_01.json index 14ef3103..a128adf4 100644 --- a/assets/collections/swift_fundamentos_01.json +++ b/assets/collections/swift_fundamentos_01.json @@ -7,10 +7,6 @@ "swift", "linguagem de programação" ], - "productInfo": { - "price": null, - "productId": null - }, "contributors": [ { "name": "@matuella", diff --git a/lib/application/constants/strings.dart b/lib/application/constants/strings.dart index 0ee1659a..9920d27e 100644 --- a/lib/application/constants/strings.dart +++ b/lib/application/constants/strings.dart @@ -33,8 +33,6 @@ 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)}'; -// TODO(joao): check the best place to store the const double. -const double collectionPrice = 0.99; String collectionsEmptyTitleSegment(CollectionsSegment segment) { switch (segment) { @@ -100,7 +98,7 @@ const jumpTo = 'Pular para'; // Tags Component // const tags = 'Tags'; -const premium = 'PREMIUM'; +const premium = 'Premium'; const tagsHint = 'Adicione as tags...'; const suggestions = 'Sugestões'; const addTags = 'Adicionar Tags'; diff --git a/lib/application/pages/details/collection_details_page.dart b/lib/application/pages/details/collection_details_page.dart index 63891aaa..8f087bb6 100644 --- a/lib/application/pages/details/collection_details_page.dart +++ b/lib/application/pages/details/collection_details_page.dart @@ -97,25 +97,29 @@ class CollectionDetailsPage extends ConsumerWidget { sections.add(resourcesSection); - final studyNowButton = PrimaryElevatedButton( - onPressed: () => readCoordinator(ref).navigateToCollectionExecution(id, isNestedNavigation: false), - text: strings.detailsStudyNow.toUpperCase(), - ); - - final purchaseDeckButton = SecondaryElevatedButton( - backgroundColor: memoTheme.secondarySwatch, - text: strings.collectionPurchaseDeck(state.metadata.price), - onPressed: () async => _collectionPurchaseBottomSheet( - context, - () => ref.watch(collectionDetailsVM(id).notifier).purchaseCollection(state.metadata.id), - ), - ); + Widget actionButton() { + if (state.isPurchased) { + return PrimaryElevatedButton( + onPressed: () => readCoordinator(ref).navigateToCollectionExecution(id, isNestedNavigation: false), + text: strings.detailsStudyNow.toUpperCase(), + ); + } else { + return SecondaryElevatedButton( + backgroundColor: memoTheme.secondarySwatch, + text: strings.collectionPurchaseDeck(state.metadata.price!), + onPressed: () async => _collectionPurchaseBottomSheet( + context, + () => ref.watch(collectionDetailsVM(id).notifier).purchaseCollection(state.metadata.id), + ), + ); + } + } final fixedBottomAction = ThemedBottomContainer( child: ColoredBox( color: memoTheme.neutralSwatch.shade800, child: SafeArea( - child: (state.isPurchased ? studyNowButton : purchaseDeckButton), + child: actionButton(), ).withSymmetricalPadding( context, vertical: Spacing.small, diff --git a/lib/application/view-models/app_vm.dart b/lib/application/view-models/app_vm.dart index 93a1ea73..bd019520 100644 --- a/lib/application/view-models/app_vm.dart +++ b/lib/application/view-models/app_vm.dart @@ -139,9 +139,8 @@ class AppVMImpl extends AppVM { splashMinDuration, ]); - await Future.wait([ - collectionPurchaseServices.updatePurchasesIfNeeded(), - ]); + // 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); } diff --git a/lib/application/view-models/item_metadata.dart b/lib/application/view-models/item_metadata.dart index ab335cb9..281d1a45 100644 --- a/lib/application/view-models/item_metadata.dart +++ b/lib/application/view-models/item_metadata.dart @@ -21,7 +21,7 @@ abstract class CollectionItem extends ItemMetadata { required this.category, required this.tags, required this.isPremium, - required this.price, + this.price, }); final String id; @@ -30,7 +30,7 @@ abstract class CollectionItem extends ItemMetadata { final List tags; final bool isPremium; - final double price; + final double? price; @override List get props => [id, name, category, tags, isPremium]; @@ -45,7 +45,7 @@ class CompletedCollectionItem extends CollectionItem { required String category, required List tags, required bool isPremium, - required double price, + double? price, }) : super( id: id, name: name, @@ -72,7 +72,7 @@ class IncompleteCollectionItem extends CollectionItem { required String category, required List tags, required bool isPremium, - required double price, + double? price, }) : super( id: id, name: name, @@ -104,7 +104,7 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) { category: collection.category, tags: collection.tags, isPremium: collection.isPremium, - price: collection.productInfo.price, + price: collection.productInfo?.price, ); } else { return IncompleteCollectionItem( @@ -115,7 +115,7 @@ CollectionItem mapStatusToMetadata(CollectionStatus status) { category: collection.category, tags: collection.tags, isPremium: collection.isPremium, - price: collection.productInfo.price, + price: collection.productInfo?.price, ); } } diff --git a/lib/application/widgets/theme/collection_card.dart b/lib/application/widgets/theme/collection_card.dart index 1168a583..05fc7ee9 100644 --- a/lib/application/widgets/theme/collection_card.dart +++ b/lib/application/widgets/theme/collection_card.dart @@ -37,7 +37,7 @@ 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. diff --git a/lib/data/repositories/collection_repository.dart b/lib/data/repositories/collection_repository.dart index ff93cf6f..29002e85 100644 --- a/lib/data/repositories/collection_repository.dart +++ b/lib/data/repositories/collection_repository.dart @@ -93,7 +93,7 @@ class CollectionRepositoryImpl implements CollectionRepository { CollectionKeys.uniqueMemosAmount: collection.uniqueMemosAmount, CollectionKeys.uniqueMemoExecutionsAmount: collection.uniqueMemoExecutionsAmount, CollectionKeys.isPremium: collection.isPremium, - CollectionKeys.productInfo: _productInfoSerializer.to(collection.productInfo), + 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 index fae667ef..8ab3adfc 100644 --- a/lib/data/repositories/purchase_repository.dart +++ b/lib/data/repositories/purchase_repository.dart @@ -5,16 +5,23 @@ abstract class PurchaseRepository { /// Purchase products in the app with the store ID [storeId] for the local user. Future purchaseInApp({required String storeId}); - /// Receives purchase information made by the user. - Future> getPurchasesInfo(); + /// 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 [id] to be premium or not. + /// Updates the collection with the [purchaseId] to be premium or not. Future updatePurchase({required String purchaseId}); - Future> getPurchaseProducts(); + /// 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 { @@ -22,6 +29,7 @@ class PurchaseRepositoryImpl implements PurchaseRepository { final SembastDatabase _db; final _purchasesStore = 'purchases'; + final _purchaseIdKey = 'purchasesId'; final PurchaseGateway _purchaseGateway; @@ -31,7 +39,7 @@ class PurchaseRepositoryImpl implements PurchaseRepository { ); @override - Future> getPurchasesInfo() async { + Future> getUserPurchases() async { final info = await _purchaseGateway.purchasesInfo(); return info.map((purchase) => purchase).toList(); } @@ -46,14 +54,14 @@ class PurchaseRepositoryImpl implements PurchaseRepository { Future updatePurchase({required String purchaseId}) => _db.put( id: purchaseId, object: { - 'purchasesId': purchaseId, + _purchaseIdKey: purchaseId, }, store: _purchasesStore, ); @override - Future> getPurchaseProducts() async { + Future> getPurchasedProductsIds() async { final purchases = await _db.getAll(store: _purchasesStore); - return purchases.map((purchase) => purchase['purchasesId'] as String).toList(); + 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 fce9b15d..71bfb9b0 100644 --- a/lib/data/serializers/collection_memos_serializer.dart +++ b/lib/data/serializers/collection_memos_serializer.dart @@ -29,7 +29,8 @@ class CollectionMemosSerializer implements Serializer); + final rawProductInfo = json[CollectionMemosKeys.productInfo] as Map?; + final productInfo = rawProductInfo != null ? productInfoSerializer.from(rawProductInfo) : null; final tags = List.from(json[CollectionMemosKeys.tags] as List); @@ -62,6 +63,6 @@ class CollectionMemosSerializer implements Serializer>.from(json[CollectionKeys.contributors] as List); final contributors = rawContributors.map(contributorSerializer.from).toList(); - final isPremium = json[CollectionKeys.isPremium] as bool; + final isPremium = json[CollectionKeys.isPremium] as bool?; - final productInfo = productInfoSerializar.from(json[CollectionKeys.productInfo] as Map); + final rawProductInfo = json[CollectionKeys.productInfo] as Map?; + final productInfo = rawProductInfo != null ? productInfoSerializar.from(rawProductInfo) : null; return Collection( id: id, @@ -60,7 +61,7 @@ class CollectionSerializer implements Serializer executionsAmounts = const {}, int timeSpentInMillis = 0, @@ -55,7 +55,7 @@ class Collection extends MemoExecutionsMetadata with EquatableMixin implements C final bool isPremium; @override - final ProductInfo productInfo; + final ProductInfo? productInfo; @override final int uniqueMemosAmount; @@ -101,8 +101,8 @@ abstract class CollectionMetadata { /// Informs whether the collection is premium or not. bool get isPremium; - /// Informs the `productId` and `price` of the product associated with this collection. - ProductInfo get productInfo; + /// 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/services/collection_purchase_services.dart b/lib/domain/services/collection_purchase_services.dart index c85106ed..912ccac7 100644 --- a/lib/domain/services/collection_purchase_services.dart +++ b/lib/domain/services/collection_purchase_services.dart @@ -3,7 +3,6 @@ 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'; -import 'package:memo/domain/models/collection.dart'; abstract class CollectionPurchaseServices { /// Purchases the collection - from [id]. @@ -35,23 +34,19 @@ class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { Future purchaseCollection({required String id}) async { final collection = await collectionRepo.getCollection(id: id); - await _purchaseInAppCollection(collection); + 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 isPurchased = await purchaseRepo.getPurchasesInfo(); + final purchasesId = await purchaseRepo.getUserPurchases(); - if (isPurchased.contains(collection.productInfo.id)) { - await purchaseRepo.updatePurchase(purchaseId: collection.productInfo.id); + if (purchasesId.contains(collection.productInfo!.id)) { + await purchaseRepo.updatePurchase(purchaseId: collection.productInfo!.id); } } - Future _purchaseInAppCollection(Collection collection) async { - await purchaseRepo.purchaseInApp(storeId: collection.productInfo.id); - } - @override Future isPurchased({required String id}) async { final collection = await collectionRepo.getCollection(id: id); @@ -60,9 +55,9 @@ class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { return true; } - final storeId = collection.productInfo.id; + final storeId = collection.productInfo!.id; - final purchasedProductsList = await purchaseRepo.getPurchaseProducts(); + final purchasedProductsList = await purchaseRepo.getPurchasedProductsIds(); return purchasedProductsList.contains(storeId); } @@ -72,7 +67,9 @@ class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { final collections = await collectionRepo.getAllCollectionMemos(); for (final collection in collections) { - await _updatePurchaseCollection(id: collection.id); + if (collection.isPremium) { + await _updatePurchaseCollection(id: collection.id); + } } } } diff --git a/lib/domain/transients/collection_memos.dart b/lib/domain/transients/collection_memos.dart index 9ca9f078..3e55488e 100644 --- a/lib/domain/transients/collection_memos.dart +++ b/lib/domain/transients/collection_memos.dart @@ -16,7 +16,7 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { required this.contributors, required this.memosMetadata, required this.isPremium, - required this.productInfo, + this.productInfo, int uniqueMemoExecutionsAmount = 0, }) : _uniqueMemoExecutionsAmount = uniqueMemoExecutionsAmount, assert(memosMetadata.isNotEmpty, 'must not be an empty list of memos'), @@ -47,7 +47,7 @@ class CollectionMemos extends CollectionMetadata with EquatableMixin { final bool isPremium; @override - final ProductInfo productInfo; + final ProductInfo? productInfo; @override int get uniqueMemosAmount => memosMetadata.length; diff --git a/test/data/serializers/collection_memos_serializer_test.dart b/test/data/serializers/collection_memos_serializer_test.dart index 24b6c4a6..b8a22256 100644 --- a/test/data/serializers/collection_memos_serializer_test.dart +++ b/test/data/serializers/collection_memos_serializer_test.dart @@ -2,7 +2,6 @@ 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; @@ -17,7 +16,6 @@ void main() { category: 'Category', tags: const ['Tag 1', 'Tag 2'], isPremium: false, - productInfo: ProductInfo(id: '', price: 0.0), contributors: [const Contributor(name: 'name')], memosMetadata: [ MemoCollectionMetadata( @@ -30,8 +28,7 @@ void main() { Map completeFixture() => fixtures.collectionMemos() ..[CollectionMemosKeys.memosMetadata] = [fixtures.memoCollectionMetadata()] - ..[CollectionMemosKeys.contributors] = [fixtures.contributor()] - ..[CollectionMemosKeys.productInfo] = fixtures.productInfo(); + ..[CollectionMemosKeys.contributors] = [fixtures.contributor()]; test('CollectionMemosSerializer should correctly encode/decode a CollectionMemos', () { final rawCollection = completeFixture(); @@ -86,13 +83,6 @@ void main() { }, throwsA(isA()), ); - expect( - () { - final rawCollection = completeFixture()..remove(CollectionMemosKeys.productInfo); - serializer.from(rawCollection); - }, - throwsA(isA()), - ); expect( () { final rawCollection = completeFixture()..remove(CollectionMemosKeys.memosMetadata); diff --git a/test/data/serializers/collection_serializer_test.dart b/test/data/serializers/collection_serializer_test.dart index d63f00e5..4bdc70f8 100644 --- a/test/data/serializers/collection_serializer_test.dart +++ b/test/data/serializers/collection_serializer_test.dart @@ -3,7 +3,6 @@ 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; @@ -15,15 +14,13 @@ void main() { description: 'This collection represents a collection.', category: 'Category', isPremium: false, - productInfo: ProductInfo(price: 0.0, id: ''), contributors: const [Contributor(name: 'name')], tags: const ['Tag 1', 'Tag 2'], uniqueMemosAmount: 1, ); - Map completeFixture() => fixtures.collection() - ..[CollectionKeys.contributors] = [fixtures.contributor()] - ..[CollectionKeys.productInfo] = fixtures.productInfo(); + Map completeFixture() => + fixtures.collection()..[CollectionKeys.contributors] = [fixtures.contributor()]; test('CollectionSerializer should correctly encode/decode a Collection', () { final rawCollection = completeFixture(); @@ -78,13 +75,6 @@ void main() { }, throwsA(isA()), ); - expect( - () { - final rawCollection = completeFixture()..remove(CollectionKeys.productInfo); - serializer.from(rawCollection); - }, - throwsA(isA()), - ); expect( () { final rawCollection = completeFixture()..remove(CollectionKeys.uniqueMemosAmount); @@ -114,7 +104,7 @@ void main() { contributors: const [Contributor(name: 'name')], tags: const ['Tag 1', 'Tag 2'], isPremium: false, - productInfo: ProductInfo(price: 0.0, id: ''), + // 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 deleted file mode 100644 index 85864f8c..00000000 --- a/test/data/serializers/product_info_serializar_test.dart +++ /dev/null @@ -1,36 +0,0 @@ -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/fixtures/collection.json b/test/fixtures/collection.json index cb05ea23..d017aa32 100644 --- a/test/fixtures/collection.json +++ b/test/fixtures/collection.json @@ -4,7 +4,6 @@ "description": "This collection represents a collection.", "category": "Category", "contributors": [], - "productInfo": {}, "tags": [ "Tag 1", "Tag 2" diff --git a/test/fixtures/collection_memos.json b/test/fixtures/collection_memos.json index bf394441..0211cca4 100644 --- a/test/fixtures/collection_memos.json +++ b/test/fixtures/collection_memos.json @@ -4,7 +4,6 @@ "description": "This collection represents a collection.", "category": "Category", "contributors": [], - "productInfo": {}, "tags": [ "Tag 1", "Tag 2" diff --git a/test/fixtures/product_info.json b/test/fixtures/product_info.json deleted file mode 100644 index e1e65708..00000000 --- a/test/fixtures/product_info.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "productId": "", - "price": 0.0 -} \ No newline at end of file From f3597bca34b43be6e72c0b13fe47a3e409a22386 Mon Sep 17 00:00:00 2001 From: Joaoaraujo97 Date: Thu, 11 Jul 2024 15:57:13 -0300 Subject: [PATCH 09/12] Inclusion of the README and the productInfo serialization test. --- README.md | 4 + README_ptbr.md | 4 + ios/Podfile.lock | 252 ++++++++++-------- .../collection_purchase_services.dart | 4 +- .../collection_memos_serializer_test.dart | 7 +- .../collection_serializer_test.dart | 13 +- .../product_info_serializar_test.dart | 36 +++ test/fixtures/collection.json | 3 +- test/fixtures/collection_memos.json | 3 +- test/fixtures/product_info.json | 4 + 10 files changed, 202 insertions(+), 128 deletions(-) create mode 100644 test/data/serializers/product_info_serializar_test.dart create mode 100644 test/fixtures/product_info.json diff --git a/README.md b/README.md index 0e56eca7..a4b4f34d 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 deck feature can be found in the code, along with an example of a paid deck. Other decks 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/ios/Podfile.lock b/ios/Podfile.lock index 62675245..7f6997bc 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,112 +1,128 @@ PODS: - device_info_plus (0.0.1): - Flutter - - Firebase/AnalyticsWithoutAdIdSupport (8.11.0): + - Firebase/AnalyticsWithoutAdIdSupport (10.25.0): - Firebase/CoreOnly - - FirebaseAnalytics/WithoutAdIdSupport (~> 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.27.0): + - FirebaseCore (~> 10.0) + - FirebaseCoreInternal (10.27.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.27.0): + - FirebaseCore (~> 10.0) + - GoogleUtilities/Environment (~> 7.8) + - GoogleUtilities/UserDefaults (~> 7.8) + - PromisesObjC (~> 2.1) + - FirebaseRemoteConfigInterop (10.27.0) + - FirebaseSessions (10.27.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.29.4): - Flutter - - video_player_avfoundation (0.0.1): + - PurchasesHybridCommon (= 10.9.0) + - PurchasesHybridCommon (10.9.0): + - RevenueCat (= 4.43.0) + - RevenueCat (4.43.0) + - 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: 4ec89dd0c6de93d6becde32122d68b7c35f6bf5d + FirebaseCoreInternal: 4b297a2d56063dbea2c1d0d04222d44a8d058862 + FirebaseCrashlytics: 4b96efb0ce73b38b2a85e8b8bd1bd8f63f09d015 + FirebaseInstallations: 766dabca09fd94aef922538aaf144cc4a6fb6869 + FirebaseRemoteConfigInterop: c55a739f5ab121792776e191d9fd437dc624a541 + FirebaseSessions: 2fdf949f9e58295a57703ae8f2efc44f9fa3aa16 + 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: a10ab0c4bb4effbc9b78be6513658ae659e67a20 + PurchasesHybridCommon: 0e157d11d04fdfdd434b5c6d05acb58fd1cc9db8 + RevenueCat: f7b90c52f4b7e322bc9e7adaf734030523534b06 + url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe -PODFILE CHECKSUM: 93c13a6b510094ca189e0993ce1f80877faea644 +PODFILE CHECKSUM: 80a6f1651e14576cbf8f071404d32bc9269538e5 -COCOAPODS: 1.11.2 +COCOAPODS: 1.15.2 diff --git a/lib/domain/services/collection_purchase_services.dart b/lib/domain/services/collection_purchase_services.dart index 912ccac7..dbf8b3c8 100644 --- a/lib/domain/services/collection_purchase_services.dart +++ b/lib/domain/services/collection_purchase_services.dart @@ -40,9 +40,9 @@ class CollectionPurchaseServicesImpl implements CollectionPurchaseServices { Future _updatePurchaseCollection({required String id}) async { final collection = await collectionRepo.getCollection(id: id); - final purchasesId = await purchaseRepo.getUserPurchases(); + final purchasesIds = await purchaseRepo.getUserPurchases(); - if (purchasesId.contains(collection.productInfo!.id)) { + if (purchasesIds.contains(collection.productInfo!.id)) { await purchaseRepo.updatePurchase(purchaseId: collection.productInfo!.id); } } diff --git a/test/data/serializers/collection_memos_serializer_test.dart b/test/data/serializers/collection_memos_serializer_test.dart index b8a22256..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,7 +16,8 @@ void main() { description: 'This collection represents a collection.', category: 'Category', tags: const ['Tag 1', 'Tag 2'], - isPremium: false, + isPremium: true, + productInfo: ProductInfo(id: '', price: 0.0), contributors: [const Contributor(name: 'name')], memosMetadata: [ MemoCollectionMetadata( @@ -28,7 +30,8 @@ void main() { 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 4bdc70f8..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,14 +14,16 @@ void main() { name: 'My Collection', description: 'This collection represents a collection.', category: 'Category', - isPremium: false, + 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(); @@ -103,8 +106,8 @@ void main() { category: 'Category', contributors: const [Contributor(name: 'name')], tags: const ['Tag 1', 'Tag 2'], - isPremium: false, - // productInfo: ProductInfo(price: 0.0, id: ''), + 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/fixtures/collection.json b/test/fixtures/collection.json index d017aa32..9b458af7 100644 --- a/test/fixtures/collection.json +++ b/test/fixtures/collection.json @@ -8,7 +8,8 @@ "Tag 1", "Tag 2" ], - "isPremium": false, + "isPremium": true, + "productInfo": [], "uniqueMemosAmount": 1, "uniqueMemoExecutionsAmount": 0, "executionsAmounts": { diff --git a/test/fixtures/collection_memos.json b/test/fixtures/collection_memos.json index 0211cca4..6e5ff748 100644 --- a/test/fixtures/collection_memos.json +++ b/test/fixtures/collection_memos.json @@ -8,6 +8,7 @@ "Tag 1", "Tag 2" ], - "isPremium": false, + "isPremium": true, + "productInfo": [], "memos": [] } \ No newline at end of file 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 From 81ac7a9d89c906c4277d9cf4151cafc5c285d19b Mon Sep 17 00:00:00 2001 From: Joaoaraujo97 Date: Mon, 15 Jul 2024 14:32:50 -0300 Subject: [PATCH 10/12] Fix in collection details. --- README.md | 2 +- lib/application/view-models/details/collection_details_vm.dart | 1 + pubspec.yaml | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a4b4f34d..34439425 100644 --- a/README.md +++ b/README.md @@ -77,4 +77,4 @@ This project was built with the help of the sponsors below: ## Revenue -We are currently exploring new approaches to generate revenue with the app. The implementation of the paid deck feature can be found in the code, along with an example of a paid deck. Other decks will not be visible on GitHub to ensure we can properly validate this revenue model. +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/lib/application/view-models/details/collection_details_vm.dart b/lib/application/view-models/details/collection_details_vm.dart index 8e0ce860..7ddda199 100644 --- a/lib/application/view-models/details/collection_details_vm.dart +++ b/lib/application/view-models/details/collection_details_vm.dart @@ -103,6 +103,7 @@ class CollectionDetailsVMImpl extends CollectionDetailsVM { await _loadCollection(); } on BaseException catch (exception) { state = PurchaseCollectionFailed(exception); + await _loadCollection(); } } } diff --git a/pubspec.yaml b/pubspec.yaml index d2e967b8..d7d730de 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -51,7 +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.29.1 + purchases_flutter: ^6.30.1 dev_dependencies: flutter_test: From 051e7bf2a5aaaffabea5703d122852e30a9f63cb Mon Sep 17 00:00:00 2001 From: Lucas Bianco Date: Wed, 24 Jul 2024 12:34:27 -0300 Subject: [PATCH 11/12] Implements new collection purchase view model --- ios/Podfile.lock | 36 +- ios/Runner/Info.plist | 4 + lib/application/constants/dimensions.dart | 1 + .../details/collection_details_page.dart | 95 +- .../pages/details/collection_purchase_vm.dart | 96 ++ .../details/collection_details_vm.dart | 38 +- pubspec.lock | 889 ++++++++---------- pubspec.yaml | 2 +- 8 files changed, 584 insertions(+), 577 deletions(-) create mode 100644 lib/application/pages/details/collection_purchase_vm.dart diff --git a/ios/Podfile.lock b/ios/Podfile.lock index 7f6997bc..1d727568 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -33,9 +33,9 @@ PODS: - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.12) - GoogleUtilities/Logger (~> 7.12) - - FirebaseCoreExtension (10.27.0): + - FirebaseCoreExtension (10.29.0): - FirebaseCore (~> 10.0) - - FirebaseCoreInternal (10.27.0): + - FirebaseCoreInternal (10.29.0): - "GoogleUtilities/NSData+zlib (~> 7.8)" - FirebaseCrashlytics (10.25.0): - FirebaseCore (~> 10.5) @@ -46,13 +46,13 @@ PODS: - GoogleUtilities/Environment (~> 7.8) - nanopb (< 2.30911.0, >= 2.30908.0) - PromisesObjC (~> 2.1) - - FirebaseInstallations (10.27.0): + - FirebaseInstallations (10.29.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebaseRemoteConfigInterop (10.27.0) - - FirebaseSessions (10.27.0): + - FirebaseRemoteConfigInterop (10.29.0) + - FirebaseSessions (10.29.0): - FirebaseCore (~> 10.5) - FirebaseCoreExtension (~> 10.0) - FirebaseInstallations (~> 10.0) @@ -116,12 +116,12 @@ PODS: - PromisesObjC (2.4.0) - PromisesSwift (2.4.0): - PromisesObjC (= 2.4.0) - - purchases_flutter (6.29.4): + - purchases_flutter (6.30.2): - Flutter - - PurchasesHybridCommon (= 10.9.0) - - PurchasesHybridCommon (10.9.0): - - RevenueCat (= 4.43.0) - - RevenueCat (4.43.0) + - PurchasesHybridCommon (= 11.1.0) + - PurchasesHybridCommon (11.1.0): + - RevenueCat (= 4.43.2) + - RevenueCat (4.43.2) - url_launcher_ios (0.0.1): - Flutter @@ -190,12 +190,12 @@ SPEC CHECKSUMS: firebase_crashlytics: 17e856fabec68d993662abaf2f6fe2413f0abece FirebaseAnalytics: ec00fe8b93b41dc6fe4a28784b8e51da0647a248 FirebaseCore: 7ec4d0484817f12c3373955bc87762d96842d483 - FirebaseCoreExtension: 4ec89dd0c6de93d6becde32122d68b7c35f6bf5d - FirebaseCoreInternal: 4b297a2d56063dbea2c1d0d04222d44a8d058862 + FirebaseCoreExtension: 705ca5b14bf71d2564a0ddc677df1fc86ffa600f + FirebaseCoreInternal: df84dd300b561c27d5571684f389bf60b0a5c934 FirebaseCrashlytics: 4b96efb0ce73b38b2a85e8b8bd1bd8f63f09d015 - FirebaseInstallations: 766dabca09fd94aef922538aaf144cc4a6fb6869 - FirebaseRemoteConfigInterop: c55a739f5ab121792776e191d9fd437dc624a541 - FirebaseSessions: 2fdf949f9e58295a57703ae8f2efc44f9fa3aa16 + FirebaseInstallations: 913cf60d0400ebd5d6b63a28b290372ab44590dd + FirebaseRemoteConfigInterop: 6efda51fb5e2f15b16585197e26eaa09574e8a4d + FirebaseSessions: dbd14adac65ce996228652c1fc3a3f576bdf3ecc Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 flutter_keyboard_visibility: 0339d06371254c3eb25eeb90ba8d17dca8f9c069 flutter_native_splash: edf599c81f74d093a4daf8e17bd7a018854bc778 @@ -207,9 +207,9 @@ SPEC CHECKSUMS: path_provider_ios: 14f3d2fd28c4fdb42f44e0f751d12861c43cee02 PromisesObjC: f5707f49cb48b9636751c5b2e7d227e43fba9f47 PromisesSwift: 9d77319bbe72ebf6d872900551f7eeba9bce2851 - purchases_flutter: a10ab0c4bb4effbc9b78be6513658ae659e67a20 - PurchasesHybridCommon: 0e157d11d04fdfdd434b5c6d05acb58fd1cc9db8 - RevenueCat: f7b90c52f4b7e322bc9e7adaf734030523534b06 + purchases_flutter: 42d5544e7730ea89a88cc2f008b7c700fd147052 + PurchasesHybridCommon: 4022d5944cb30ec44ba5159e42aa161fe0e30175 + RevenueCat: 3d934653b7e8b09af88fd47e9e84cfaf5d0a89ba url_launcher_ios: 5334b05cef931de560670eeae103fd3e431ac3fe PODFILE CHECKSUM: 80a6f1651e14576cbf8f071404d32bc9269538e5 diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 6163df8c..839a878e 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -53,5 +53,9 @@ UIViewControllerBasedStatusBarAppearance + CADisableMinimumFrameDurationOnPhone + + UIApplicationSupportsIndirectInputEvents + diff --git a/lib/application/constants/dimensions.dart b/lib/application/constants/dimensions.dart index dcc0cf8c..b60158db 100644 --- a/lib/application/constants/dimensions.dart +++ b/lib/application/constants/dimensions.dart @@ -24,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/pages/details/collection_details_page.dart b/lib/application/pages/details/collection_details_page.dart index 8f087bb6..01a1b578 100644 --- a/lib/application/pages/details/collection_details_page.dart +++ b/lib/application/pages/details/collection_details_page.dart @@ -1,8 +1,10 @@ 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'; @@ -22,13 +24,20 @@ class CollectionDetailsPage extends ConsumerWidget { final state = watchCollectionDetailsState(ref); final id = ref.read(detailsCollectionId); - ref.listen(collectionDetailsVM(id), (_, state) { - if (state is PurchaseCollectionFailed) { - Navigator.of(context).pop(); + ref.listen(collectionPurchaseVM(id), (_, state) { + if (state is PurchaseInfoLoadingFailed) { showExceptionSnackBar(ref, state.exception); } - if (state is PurchaseCollectionSuccess) { + + 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)), @@ -97,29 +106,15 @@ class CollectionDetailsPage extends ConsumerWidget { sections.add(resourcesSection); - Widget actionButton() { - if (state.isPurchased) { - return PrimaryElevatedButton( - onPressed: () => readCoordinator(ref).navigateToCollectionExecution(id, isNestedNavigation: false), - text: strings.detailsStudyNow.toUpperCase(), - ); - } else { - return SecondaryElevatedButton( - backgroundColor: memoTheme.secondarySwatch, - text: strings.collectionPurchaseDeck(state.metadata.price!), - onPressed: () async => _collectionPurchaseBottomSheet( - context, - () => ref.watch(collectionDetailsVM(id).notifier).purchaseCollection(state.metadata.id), - ), - ); - } - } - final fixedBottomAction = ThemedBottomContainer( child: ColoredBox( color: memoTheme.neutralSwatch.shade800, child: SafeArea( - child: actionButton(), + 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, @@ -163,6 +158,60 @@ class CollectionDetailsPage extends ConsumerWidget { style: 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) => 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/view-models/details/collection_details_vm.dart b/lib/application/view-models/details/collection_details_vm.dart index 7ddda199..888b9e39 100644 --- a/lib/application/view-models/details/collection_details_vm.dart +++ b/lib/application/view-models/details/collection_details_vm.dart @@ -4,10 +4,8 @@ import 'package:equatable/equatable.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:memo/application/view-models/app_vm.dart'; import 'package:memo/application/view-models/item_metadata.dart'; -import 'package:memo/core/faults/exceptions/base_exception.dart'; import 'package:memo/domain/enums/resource_type.dart'; import 'package:memo/domain/models/resource.dart'; -import 'package:memo/domain/services/collection_purchase_services.dart'; import 'package:memo/domain/services/collection_services.dart'; import 'package:memo/domain/services/resource_services.dart'; import 'package:memo/domain/transients/collection_status.dart'; @@ -17,15 +15,11 @@ final collectionDetailsVM = StateNotifierProvider.family { CollectionDetailsVM(CollectionDetailsState state) : super(state); - - /// Purchase a deck. - Future purchaseCollection(String id); } class CollectionDetailsVMImpl extends CollectionDetailsVM { @@ -33,7 +27,6 @@ class CollectionDetailsVMImpl extends CollectionDetailsVM { required this.collectionId, required this.collectionServices, required this.resourceServices, - required this.purchaseServices, }) : super(LoadingCollectionDetailsState()) { _loadCollection(); } @@ -41,14 +34,11 @@ class CollectionDetailsVMImpl extends CollectionDetailsVM { final String collectionId; final CollectionServices collectionServices; final ResourceServices resourceServices; - final CollectionPurchaseServices purchaseServices; List? _associatedResources; - late StreamSubscription _listener; + late final StreamSubscription _listener; Future _loadCollection() async { - final isPurchased = await purchaseServices.isPurchased(id: collectionId); - final stream = await collectionServices.listenToCollectionStatus(collectionId: collectionId); _listener = stream.listen((collectionStatus) async { @@ -84,7 +74,6 @@ class CollectionDetailsVMImpl extends CollectionDetailsVM { memosAmount: memosAmount, resources: mappedResources, contributors: contributors, - isPurchased: isPurchased, ); }); } @@ -94,18 +83,6 @@ class CollectionDetailsVMImpl extends CollectionDetailsVM { _listener.cancel(); super.dispose(); } - - @override - Future purchaseCollection(String id) async { - try { - await purchaseServices.purchaseCollection(id: id); - state = PurchaseCollectionSuccess(); - await _loadCollection(); - } on BaseException catch (exception) { - state = PurchaseCollectionFailed(exception); - await _loadCollection(); - } - } } abstract class CollectionDetailsState extends Equatable { @@ -122,7 +99,6 @@ class LoadedCollectionDetailsState extends CollectionDetailsState { required this.memosAmount, required this.resources, required this.contributors, - required this.isPurchased, }); final CollectionItem metadata; @@ -130,7 +106,6 @@ class LoadedCollectionDetailsState extends CollectionDetailsState { final int memosAmount; final List resources; final List contributors; - final bool isPurchased; } class ResourceInfo extends Equatable { @@ -154,14 +129,3 @@ class ContributorInfo extends Equatable { @override List get props => [name, imageUrl, url]; } - -class PurchaseCollectionFailed extends CollectionDetailsState { - PurchaseCollectionFailed(this.exception); - - final BaseException exception; - - @override - List get props => [exception, ...super.props]; -} - -class PurchaseCollectionSuccess extends CollectionDetailsState {} 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 d7d730de..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: From 2f2c0b5f811ddc7a79ab01f900f72cf88a394387 Mon Sep 17 00:00:00 2001 From: Lucas Bianco Date: Wed, 24 Jul 2024 15:51:16 -0300 Subject: [PATCH 12/12] Sorts collections by their premium status --- .../view-models/home/collections_vm.dart | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/application/view-models/home/collections_vm.dart b/lib/application/view-models/home/collections_vm.dart index 5759dba4..09423a98 100644 --- a/lib/application/view-models/home/collections_vm.dart +++ b/lib/application/view-models/home/collections_vm.dart @@ -76,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