From aa8bc2ad10e4ef2431c5a8f6d83ae25fb083015a Mon Sep 17 00:00:00 2001 From: Jonas Anker Rasmussen Date: Sat, 13 May 2023 21:54:18 +0200 Subject: [PATCH 01/32] Update Coffee Card API Server Url (#453) --- .env.develop | 2 +- .env.production | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.develop b/.env.develop index 4e1790d95..4e12fea45 100644 --- a/.env.develop +++ b/.env.develop @@ -1 +1 @@ -coffeeCardUrl="https://beta.analogio.dk/api/clippy" \ No newline at end of file +coffeeCardUrl="https://core.dev.analogio.dk" \ No newline at end of file diff --git a/.env.production b/.env.production index 5c9d47b68..1ddabf2a0 100644 --- a/.env.production +++ b/.env.production @@ -1 +1 @@ -coffeeCardUrl="https://analogio.dk/clippy" \ No newline at end of file +coffeeCardUrl="https://core.prd.analogio.dk" \ No newline at end of file From fa38bc2e948583200186e642124c43e504aea66d Mon Sep 17 00:00:00 2001 From: Jonas Anker Rasmussen Date: Sun, 14 May 2023 09:06:57 +0200 Subject: [PATCH 02/32] Trigger release when .env files are changes (#454) --- .github/workflows/release.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8828822b..9255b7d52 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,6 +13,8 @@ on: - .github/workflows/** - pubspec.lock - pubspec.yml + - .env.develop + - .env.production jobs: build_and_test: From 76979cace45488eacb649fb0018944e90cc7c0f3 Mon Sep 17 00:00:00 2001 From: Jonas Anker Rasmussen Date: Sun, 14 May 2023 14:22:25 +0200 Subject: [PATCH 03/32] Update Privacy Policy url (#456) --- lib/utils/api_uri_constants.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/utils/api_uri_constants.dart b/lib/utils/api_uri_constants.dart index 0f898f5a5..0cd0afa58 100644 --- a/lib/utils/api_uri_constants.dart +++ b/lib/utils/api_uri_constants.dart @@ -14,7 +14,7 @@ class ApiUriConstants { // Settings static Uri privacyPolicyUri = - Uri.parse('https://analogio.dk/policies/privacypolicy.pdf'); + Uri.parse('https://cafeanalog.dk/privacy-policy/'); static Uri feedbackFormUri = Uri.parse('https://www.cognitoforms.com/CafeAnalog1/BugReport'); From f81055acd557398740b12db49bbe66e3d2bd5e4c Mon Sep 17 00:00:00 2001 From: Jonas Anker Rasmussen Date: Sun, 14 May 2023 19:52:21 +0200 Subject: [PATCH 04/32] Disable showing opening hours (#458) Closes #458 --- .../presentation/pages/tickets_page.dart | 2 -- lib/widgets/pages/settings/settings_page.dart | 29 ++----------------- 2 files changed, 3 insertions(+), 28 deletions(-) diff --git a/lib/features/ticket/presentation/pages/tickets_page.dart b/lib/features/ticket/presentation/pages/tickets_page.dart index bdd06c7a1..f071820ac 100644 --- a/lib/features/ticket/presentation/pages/tickets_page.dart +++ b/lib/features/ticket/presentation/pages/tickets_page.dart @@ -1,6 +1,5 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/core/widgets/upgrade_alert.dart'; -import 'package:coffeecard/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/features/ticket/presentation/widgets/shop_section.dart'; import 'package:coffeecard/features/ticket/presentation/widgets/tickets_section.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; @@ -38,7 +37,6 @@ class TicketsPage extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: const [ SectionTitle(Strings.ticketsMyTickets), - OpeningHoursIndicator(), ], ), const TicketSection(), diff --git a/lib/widgets/pages/settings/settings_page.dart b/lib/widgets/pages/settings/settings_page.dart index 6db39e98f..3a93b4fbb 100644 --- a/lib/widgets/pages/settings/settings_page.dart +++ b/lib/widgets/pages/settings/settings_page.dart @@ -5,8 +5,6 @@ import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/features/contributor/presentation/pages/credits_page.dart'; -import 'package:coffeecard/features/opening_hours/opening_hours.dart'; -import 'package:coffeecard/features/opening_hours/presentation/pages/opening_hours_page.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/utils/api_uri_constants.dart'; import 'package:coffeecard/utils/launch.dart'; @@ -46,7 +44,6 @@ class SettingsPage extends StatelessWidget { @override Widget build(BuildContext context) { - final openingHoursState = context.watch().state; final userState = context.watch().state; return AppScaffold.withTitle( @@ -124,30 +121,10 @@ class SettingsPage extends StatelessWidget { name: Strings.frequentlyAskedQuestions, onTap: () => Navigator.push(context, FAQPage.route), ), - SettingListEntry( + const SettingListEntry( name: Strings.openingHours, - onTap: openingHoursState is OpeningHoursLoaded - ? () => Navigator.push( - context, - OpeningHoursPage.routeWith(state: openingHoursState), - ) - : null, - valueWidget: ShimmerBuilder( - showShimmer: openingHoursState is OpeningHoursLoading, - builder: (context, colorIfShimmer) { - final loadingText = openingHoursState is OpeningHoursLoading - ? Strings.openingHoursShimmerText - : ''; - - return ColoredBox( - color: colorIfShimmer, - child: SettingValueText( - value: openingHoursState is OpeningHoursLoaded - ? openingHoursState.todaysOpeningHours - : loadingText, - ), - ); - }, + valueWidget: SettingValueText( + value: 'Not available', ), ), SettingListEntry( From f3a072bb3c2bece764e9ac247e6b542c74a785e0 Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Tue, 16 May 2023 12:39:18 +0200 Subject: [PATCH 05/32] migrate to fpdart (#457) * remove dartz * replace dartz with fpdart --------- Co-authored-by: Omid Marfavi <21163286+marfavi@users.noreply.github.com> --- lib/core/extensions/either_extensions.dart | 2 +- lib/core/network/network_request_executor.dart | 2 +- lib/core/usecases/usecase.dart | 2 +- lib/cubits/form/form_bloc.dart | 2 +- .../repositories/shared/account_repository.dart | 2 +- lib/data/repositories/v1/product_repository.dart | 2 +- lib/data/repositories/v1/voucher_repository.dart | 2 +- .../repositories/v2/app_config_repository.dart | 2 +- .../repositories/v2/leaderboard_repository.dart | 2 +- .../repositories/v2/purchase_repository.dart | 2 +- .../domain/usecases/fetch_contributors.dart | 2 +- .../occupation_remote_data_source.dart | 2 +- .../domain/usecases/get_occupations.dart | 2 +- .../opening_hours_remote_data_source.dart | 2 +- .../opening_hours_repository_impl.dart | 2 +- .../repositories/opening_hours_repository.dart | 2 +- .../domain/usecases/check_open_status.dart | 2 +- .../domain/usecases/get_opening_hours.dart | 2 +- .../datasources/receipt_remote_data_source.dart | 2 +- .../repositories/receipt_repository_impl.dart | 2 +- .../domain/repositories/receipt_repository.dart | 2 +- .../receipt/domain/usecases/get_receipts.dart | 2 +- .../datasources/ticket_remote_data_source.dart | 2 +- .../ticket/domain/usecases/consume_ticket.dart | 2 +- .../ticket/domain/usecases/load_tickets.dart | 2 +- .../datasources/user_remote_data_source.dart | 2 +- lib/features/user/domain/usecases/get_user.dart | 2 +- .../usecases/request_account_deletion.dart | 2 +- .../domain/usecases/update_user_details.dart | 2 +- lib/payment/free_product_service.dart | 2 +- lib/payment/mobilepay_service.dart | 2 +- lib/payment/payment_handler.dart | 2 +- lib/utils/input_validator.dart | 2 +- .../forgot_passcode/forgot_passcode_form.dart | 2 +- .../forms/register/register_email_form.dart | 2 +- .../forms/settings/change_email_form.dart | 2 +- pubspec.lock | 16 ++++++++-------- pubspec.yaml | 2 +- .../network/network_request_executor_test.dart | 2 +- .../environment/environment_cubit_test.dart | 2 +- test/cubits/login/login_cubit_test.dart | 2 +- test/cubits/products/products_cubit_test.dart | 2 +- .../cubits/statistics/statistics_cubit_test.dart | 2 +- .../account_repository_test.dart | 2 +- .../cubit/contributor_cubit_test.dart | 2 +- .../occupation_remote_data_source_test.dart | 2 +- .../domain/usecase/get_occupations_test.dart | 2 +- .../presentation/occupation_cubit_test.dart | 2 +- .../opening_hours_remote_data_source_test.dart | 2 +- .../opening_hours_repository_impl_test.dart | 2 +- .../domain/usecases/check_open_status_test.dart | 2 +- .../domain/usecases/get_opening_hours_test.dart | 2 +- .../cubit/opening_hours_cubit_test.dart | 2 +- .../receipt_remote_data_source_test.dart | 2 +- .../receipt_repository_impl_test.dart | 2 +- .../domain/usecases/get_receipts_test.dart | 2 +- .../presentation/cubit/receipt_cubit_test.dart | 2 +- .../ticket_remote_data_source_test.dart | 2 +- .../domain/usecases/consume_ticket_test.dart | 2 +- .../domain/usecases/load_tickets_test.dart | 2 +- .../presentation/cubit/tickets_cubit_test.dart | 2 +- .../user_remote_data_source_test.dart | 2 +- .../user/domain/usecases/get_user_test.dart | 2 +- .../usecases/request_account_deletion_test.dart | 2 +- .../usecases/update_user_details_test.dart | 2 +- .../user/presentation/cubit/user_cubit_test.dart | 2 +- 66 files changed, 73 insertions(+), 73 deletions(-) diff --git a/lib/core/extensions/either_extensions.dart b/lib/core/extensions/either_extensions.dart index 0a1dd434b..b14650c9a 100644 --- a/lib/core/extensions/either_extensions.dart +++ b/lib/core/extensions/either_extensions.dart @@ -1,4 +1,4 @@ -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; extension EitherExtensionsAsync on Future> { Future> bindFuture(R2 Function(R) f) async { diff --git a/lib/core/network/network_request_executor.dart b/lib/core/network/network_request_executor.dart index 4200dc0fe..0170f2629 100644 --- a/lib/core/network/network_request_executor.dart +++ b/lib/core/network/network_request_executor.dart @@ -1,7 +1,7 @@ import 'package:chopper/chopper.dart' show Response; import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:logger/logger.dart'; class NetworkRequestExecutor { diff --git a/lib/core/usecases/usecase.dart b/lib/core/usecases/usecase.dart index 9d839e2c3..ebd097b34 100644 --- a/lib/core/usecases/usecase.dart +++ b/lib/core/usecases/usecase.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/core/errors/failures.dart'; -import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; +import 'package:fpdart/fpdart.dart'; abstract class UseCase { Future> call(Params params); diff --git a/lib/cubits/form/form_bloc.dart b/lib/cubits/form/form_bloc.dart index 2d9879096..d7a4094dd 100644 --- a/lib/cubits/form/form_bloc.dart +++ b/lib/cubits/form/form_bloc.dart @@ -1,8 +1,8 @@ import 'package:bloc/bloc.dart'; import 'package:coffeecard/utils/debouncing.dart'; import 'package:coffeecard/utils/input_validator.dart'; -import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; +import 'package:fpdart/fpdart.dart'; part 'form_event.dart'; part 'form_state.dart'; diff --git a/lib/data/repositories/shared/account_repository.dart b/lib/data/repositories/shared/account_repository.dart index 8aaa5a5cc..b2e9a7b14 100644 --- a/lib/data/repositories/shared/account_repository.dart +++ b/lib/data/repositories/shared/account_repository.dart @@ -8,7 +8,7 @@ import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart' import 'package:coffeecard/models/account/authenticated_user.dart'; import 'package:coffeecard/models/account/update_user.dart'; import 'package:coffeecard/utils/api_uri_constants.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class AccountRepository { AccountRepository({ diff --git a/lib/data/repositories/v1/product_repository.dart b/lib/data/repositories/v1/product_repository.dart index 19e810d8d..89fe8ce00 100644 --- a/lib/data/repositories/v1/product_repository.dart +++ b/lib/data/repositories/v1/product_repository.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/models/ticket/product.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class ProductRepository { ProductRepository({ diff --git a/lib/data/repositories/v1/voucher_repository.dart b/lib/data/repositories/v1/voucher_repository.dart index 4bd6bbd23..69b513d95 100644 --- a/lib/data/repositories/v1/voucher_repository.dart +++ b/lib/data/repositories/v1/voucher_repository.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/models/voucher/redeemed_voucher.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class VoucherRepository { VoucherRepository({ diff --git a/lib/data/repositories/v2/app_config_repository.dart b/lib/data/repositories/v2/app_config_repository.dart index 26b76db3e..bc84944b0 100644 --- a/lib/data/repositories/v2/app_config_repository.dart +++ b/lib/data/repositories/v2/app_config_repository.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:coffeecard/models/environment.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class AppConfigRepository { AppConfigRepository({ diff --git a/lib/data/repositories/v2/leaderboard_repository.dart b/lib/data/repositories/v2/leaderboard_repository.dart index 12547d353..f5aad5a1f 100644 --- a/lib/data/repositories/v2/leaderboard_repository.dart +++ b/lib/data/repositories/v2/leaderboard_repository.dart @@ -3,7 +3,7 @@ import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:coffeecard/models/leaderboard/leaderboard_user.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; extension _FilterCategoryToPresetString on LeaderboardFilter { String get label { diff --git a/lib/data/repositories/v2/purchase_repository.dart b/lib/data/repositories/v2/purchase_repository.dart index d581a7d75..78cff68f2 100644 --- a/lib/data/repositories/v2/purchase_repository.dart +++ b/lib/data/repositories/v2/purchase_repository.dart @@ -3,7 +3,7 @@ import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:coffeecard/models/purchase/initiate_purchase.dart'; import 'package:coffeecard/models/purchase/single_purchase.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class PurchaseRepository { PurchaseRepository({ diff --git a/lib/features/contributor/domain/usecases/fetch_contributors.dart b/lib/features/contributor/domain/usecases/fetch_contributors.dart index b06a5f8ad..253821dc2 100644 --- a/lib/features/contributor/domain/usecases/fetch_contributors.dart +++ b/lib/features/contributor/domain/usecases/fetch_contributors.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/contributor/data/datasources/contributor_local_data_source.dart'; import 'package:coffeecard/features/contributor/domain/entities/contributor.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class FetchContributors implements UseCase, NoParams> { final ContributorLocalDataSource dataSource; diff --git a/lib/features/occupation/data/datasources/occupation_remote_data_source.dart b/lib/features/occupation/data/datasources/occupation_remote_data_source.dart index 574f65e60..bb6970f49 100644 --- a/lib/features/occupation/data/datasources/occupation_remote_data_source.dart +++ b/lib/features/occupation/data/datasources/occupation_remote_data_source.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/features/occupation/data/models/occupation_model.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class OccupationRemoteDataSource { final CoffeecardApi api; diff --git a/lib/features/occupation/domain/usecases/get_occupations.dart b/lib/features/occupation/domain/usecases/get_occupations.dart index a1885f320..988a6ab88 100644 --- a/lib/features/occupation/domain/usecases/get_occupations.dart +++ b/lib/features/occupation/domain/usecases/get_occupations.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/occupation/data/datasources/occupation_remote_data_source.dart'; import 'package:coffeecard/features/occupation/domain/entities/occupation.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class GetOccupations implements UseCase, NoParams> { final OccupationRemoteDataSource dataSource; diff --git a/lib/features/opening_hours/data/datasources/opening_hours_remote_data_source.dart b/lib/features/opening_hours/data/datasources/opening_hours_remote_data_source.dart index 5aaff476c..11d567d16 100644 --- a/lib/features/opening_hours/data/datasources/opening_hours_remote_data_source.dart +++ b/lib/features/opening_hours/data/datasources/opening_hours_remote_data_source.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/generated/api/shiftplanning_api.swagger.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class OpeningHoursRemoteDataSource { final ShiftplanningApi api; diff --git a/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart b/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart index 44234cba9..ab1f774fa 100644 --- a/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart +++ b/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart @@ -4,7 +4,7 @@ import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours. import 'package:coffeecard/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/generated/api/shiftplanning_api.swagger.dart'; import 'package:coffeecard/models/opening_hours_day.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class OpeningHoursRepositoryImpl implements OpeningHoursRepository { final OpeningHoursRemoteDataSource dataSource; diff --git a/lib/features/opening_hours/domain/repositories/opening_hours_repository.dart b/lib/features/opening_hours/domain/repositories/opening_hours_repository.dart index e0fba51d0..e2cf4f9cb 100644 --- a/lib/features/opening_hours/domain/repositories/opening_hours_repository.dart +++ b/lib/features/opening_hours/domain/repositories/opening_hours_repository.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; abstract class OpeningHoursRepository { Future> getOpeningHours(int weekday); diff --git a/lib/features/opening_hours/domain/usecases/check_open_status.dart b/lib/features/opening_hours/domain/usecases/check_open_status.dart index a354c9561..ad70b2825 100644 --- a/lib/features/opening_hours/domain/usecases/check_open_status.dart +++ b/lib/features/opening_hours/domain/usecases/check_open_status.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class CheckOpenStatus implements UseCase { final OpeningHoursRemoteDataSource dataSource; diff --git a/lib/features/opening_hours/domain/usecases/get_opening_hours.dart b/lib/features/opening_hours/domain/usecases/get_opening_hours.dart index 3f2144dc1..58c752dfe 100644 --- a/lib/features/opening_hours/domain/usecases/get_opening_hours.dart +++ b/lib/features/opening_hours/domain/usecases/get_opening_hours.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class GetOpeningHours implements UseCase { final OpeningHoursRepository repository; diff --git a/lib/features/receipt/data/datasources/receipt_remote_data_source.dart b/lib/features/receipt/data/datasources/receipt_remote_data_source.dart index b10e48d08..0c98ec81a 100644 --- a/lib/features/receipt/data/datasources/receipt_remote_data_source.dart +++ b/lib/features/receipt/data/datasources/receipt_remote_data_source.dart @@ -5,7 +5,7 @@ import 'package:coffeecard/features/receipt/data/models/purchase_receipt_model.d import 'package:coffeecard/features/receipt/data/models/swipe_receipt_model.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class ReceiptRemoteDataSource { final CoffeecardApiV2 apiV2; diff --git a/lib/features/receipt/data/repositories/receipt_repository_impl.dart b/lib/features/receipt/data/repositories/receipt_repository_impl.dart index ba0272f54..a7419ab09 100644 --- a/lib/features/receipt/data/repositories/receipt_repository_impl.dart +++ b/lib/features/receipt/data/repositories/receipt_repository_impl.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/receipt/data/datasources/receipt_remote_data_source.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/domain/repositories/receipt_repository.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class ReceiptRepositoryImpl implements ReceiptRepository { final ReceiptRemoteDataSource remoteDataSource; diff --git a/lib/features/receipt/domain/repositories/receipt_repository.dart b/lib/features/receipt/domain/repositories/receipt_repository.dart index 73d07ea7e..3fbe56e76 100644 --- a/lib/features/receipt/domain/repositories/receipt_repository.dart +++ b/lib/features/receipt/domain/repositories/receipt_repository.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; abstract class ReceiptRepository { Future>> getUserReceipts(); diff --git a/lib/features/receipt/domain/usecases/get_receipts.dart b/lib/features/receipt/domain/usecases/get_receipts.dart index 37f6a2c8d..054e2eca0 100644 --- a/lib/features/receipt/domain/usecases/get_receipts.dart +++ b/lib/features/receipt/domain/usecases/get_receipts.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/domain/repositories/receipt_repository.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class GetReceipts implements UseCase, NoParams> { final ReceiptRepository repository; diff --git a/lib/features/ticket/data/datasources/ticket_remote_data_source.dart b/lib/features/ticket/data/datasources/ticket_remote_data_source.dart index 8596529c5..faac65a43 100644 --- a/lib/features/ticket/data/datasources/ticket_remote_data_source.dart +++ b/lib/features/ticket/data/datasources/ticket_remote_data_source.dart @@ -7,7 +7,7 @@ import 'package:coffeecard/features/ticket/data/models/ticket_count_model.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:collection/collection.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class TicketRemoteDataSource { TicketRemoteDataSource({ diff --git a/lib/features/ticket/domain/usecases/consume_ticket.dart b/lib/features/ticket/domain/usecases/consume_ticket.dart index c7da43056..ccd26724f 100644 --- a/lib/features/ticket/domain/usecases/consume_ticket.dart +++ b/lib/features/ticket/domain/usecases/consume_ticket.dart @@ -2,8 +2,8 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/ticket/data/datasources/ticket_remote_data_source.dart'; -import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; +import 'package:fpdart/fpdart.dart'; class ConsumeTicket implements UseCase { final TicketRemoteDataSource ticketRemoteDataSource; diff --git a/lib/features/ticket/domain/usecases/load_tickets.dart b/lib/features/ticket/domain/usecases/load_tickets.dart index fd7ee3e19..d748f351d 100644 --- a/lib/features/ticket/domain/usecases/load_tickets.dart +++ b/lib/features/ticket/domain/usecases/load_tickets.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/ticket/data/datasources/ticket_remote_data_source.dart'; import 'package:coffeecard/features/ticket/domain/entities/ticket_count.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class LoadTickets implements UseCase, NoParams> { final TicketRemoteDataSource ticketRemoteDataSource; diff --git a/lib/features/user/data/datasources/user_remote_data_source.dart b/lib/features/user/data/datasources/user_remote_data_source.dart index 822b290b4..506c7df1b 100644 --- a/lib/features/user/data/datasources/user_remote_data_source.dart +++ b/lib/features/user/data/datasources/user_remote_data_source.dart @@ -3,7 +3,7 @@ import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/features/user/data/models/user_model.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:coffeecard/models/account/update_user.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class UserRemoteDataSource { final CoffeecardApiV2 apiV2; diff --git a/lib/features/user/domain/usecases/get_user.dart b/lib/features/user/domain/usecases/get_user.dart index 5faef5592..b69cc7614 100644 --- a/lib/features/user/domain/usecases/get_user.dart +++ b/lib/features/user/domain/usecases/get_user.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/user/data/datasources/user_remote_data_source.dart'; import 'package:coffeecard/features/user/domain/entities/user.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class GetUser implements UseCase { final UserRemoteDataSource dataSource; diff --git a/lib/features/user/domain/usecases/request_account_deletion.dart b/lib/features/user/domain/usecases/request_account_deletion.dart index 987480b19..e161c6a0f 100644 --- a/lib/features/user/domain/usecases/request_account_deletion.dart +++ b/lib/features/user/domain/usecases/request_account_deletion.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/user/data/datasources/user_remote_data_source.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class RequestAccountDeletion implements UseCase { final UserRemoteDataSource dataSource; diff --git a/lib/features/user/domain/usecases/update_user_details.dart b/lib/features/user/domain/usecases/update_user_details.dart index ec5ad939c..dd451c828 100644 --- a/lib/features/user/domain/usecases/update_user_details.dart +++ b/lib/features/user/domain/usecases/update_user_details.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/user/data/datasources/user_remote_data_source.dart'; import 'package:coffeecard/features/user/domain/entities/user.dart'; import 'package:coffeecard/models/account/update_user.dart'; -import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; +import 'package:fpdart/fpdart.dart'; class UpdateUserDetails implements UseCase { final UserRemoteDataSource dataSource; diff --git a/lib/payment/free_product_service.dart b/lib/payment/free_product_service.dart index a0f7715df..ac96dbcb9 100644 --- a/lib/payment/free_product_service.dart +++ b/lib/payment/free_product_service.dart @@ -3,7 +3,7 @@ import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; import 'package:coffeecard/models/purchase/payment.dart'; import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/payment/payment_handler.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; class FreeProductService extends PaymentHandler { const FreeProductService({ diff --git a/lib/payment/mobilepay_service.dart b/lib/payment/mobilepay_service.dart index 92f8e5da6..43e3725f0 100644 --- a/lib/payment/mobilepay_service.dart +++ b/lib/payment/mobilepay_service.dart @@ -8,7 +8,7 @@ import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/payment/payment_handler.dart'; import 'package:coffeecard/utils/api_uri_constants.dart'; import 'package:coffeecard/utils/launch.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:url_launcher/url_launcher.dart'; class MobilePayService extends PaymentHandler { diff --git a/lib/payment/payment_handler.dart b/lib/payment/payment_handler.dart index 20cb53f74..3d2275485 100644 --- a/lib/payment/payment_handler.dart +++ b/lib/payment/payment_handler.dart @@ -5,8 +5,8 @@ import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/payment/free_product_service.dart'; import 'package:coffeecard/payment/mobilepay_service.dart'; import 'package:coffeecard/service_locator.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter/widgets.dart'; +import 'package:fpdart/fpdart.dart'; abstract class PaymentHandler { final PurchaseRepository purchaseRepository; diff --git a/lib/utils/input_validator.dart b/lib/utils/input_validator.dart index ae14efe2e..ab703a3cf 100644 --- a/lib/utils/input_validator.dart +++ b/lib/utils/input_validator.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:coffeecard/utils/email_is_valid.dart'; -import 'package:dartz/dartz.dart'; +import 'package:fpdart/fpdart.dart'; part 'input_validator_helpers.dart'; diff --git a/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart b/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart index c604ddc2a..b69f7cd62 100644 --- a/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart +++ b/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart @@ -5,8 +5,8 @@ import 'package:coffeecard/utils/input_validator.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; import 'package:coffeecard/widgets/components/forms/form.dart'; import 'package:coffeecard/widgets/components/loading_overlay.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; +import 'package:fpdart/fpdart.dart'; class ForgotPasscodeForm extends StatelessWidget { const ForgotPasscodeForm({required this.initialValue}); diff --git a/lib/widgets/components/forms/register/register_email_form.dart b/lib/widgets/components/forms/register/register_email_form.dart index 39340ac2f..d0a85b852 100644 --- a/lib/widgets/components/forms/register/register_email_form.dart +++ b/lib/widgets/components/forms/register/register_email_form.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/input_validator.dart'; import 'package:coffeecard/widgets/components/forms/form.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; +import 'package:fpdart/fpdart.dart'; class RegisterEmailForm extends StatelessWidget { const RegisterEmailForm({required this.onSubmit}); diff --git a/lib/widgets/components/forms/settings/change_email_form.dart b/lib/widgets/components/forms/settings/change_email_form.dart index 42954e2f1..2153242a6 100644 --- a/lib/widgets/components/forms/settings/change_email_form.dart +++ b/lib/widgets/components/forms/settings/change_email_form.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/input_validator.dart'; import 'package:coffeecard/widgets/components/forms/form.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter/material.dart'; +import 'package:fpdart/fpdart.dart'; class ChangeEmailForm extends StatelessWidget { const ChangeEmailForm({required this.currentEmail, required this.onSubmit}); diff --git a/pubspec.lock b/pubspec.lock index 47b183283..5ed762104 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -305,14 +305,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.2.5" - dartz: - dependency: "direct main" - description: - name: dartz - sha256: e6acf34ad2e31b1eb00948692468c30ab48ac8250e0f0df661e29f12dd252168 - url: "https://pub.dev" - source: hosted - version: "0.10.1" device_info_plus: dependency: transitive description: @@ -632,6 +624,14 @@ packages: description: flutter source: sdk version: "0.0.0" + fpdart: + dependency: "direct main" + description: + name: fpdart + sha256: "4a0d047c3359a4bdd19e4941603bbe394bd133b3772a38049e17f9534d47106e" + url: "https://pub.dev" + source: hosted + version: "0.6.0" frontend_server_client: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 2c7225b19..d3ba80e8e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,7 +64,7 @@ dependencies: firebase_performance: 0.9.0+14 # functional programming thingies - dartz: 0.10.1 + fpdart: 0.6.0 # Upgrade notifier upgrader: 4.11.1 diff --git a/test/core/network/network_request_executor_test.dart b/test/core/network/network_request_executor_test.dart index c185aa48c..f93d915c7 100644 --- a/test/core/network/network_request_executor_test.dart +++ b/test/core/network/network_request_executor_test.dart @@ -2,8 +2,8 @@ import 'package:chopper/chopper.dart' as chopper; import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:http/http.dart' as http; import 'package:logger/logger.dart'; import 'package:mockito/annotations.dart'; diff --git a/test/cubits/environment/environment_cubit_test.dart b/test/cubits/environment/environment_cubit_test.dart index 1a908b85e..9f0b60b64 100644 --- a/test/cubits/environment/environment_cubit_test.dart +++ b/test/cubits/environment/environment_cubit_test.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/cubits/environment/environment_cubit.dart'; import 'package:coffeecard/data/repositories/v2/app_config_repository.dart'; import 'package:coffeecard/models/environment.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/cubits/login/login_cubit_test.dart b/test/cubits/login/login_cubit_test.dart index 0d7a0d44a..80664efec 100644 --- a/test/cubits/login/login_cubit_test.dart +++ b/test/cubits/login/login_cubit_test.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/cubits/login/login_cubit.dart'; import 'package:coffeecard/data/repositories/shared/account_repository.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/cubits/products/products_cubit_test.dart b/test/cubits/products/products_cubit_test.dart index cc40b2975..8bba1e716 100644 --- a/test/cubits/products/products_cubit_test.dart +++ b/test/cubits/products/products_cubit_test.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/cubits/products/products_cubit.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; import 'package:coffeecard/models/ticket/product.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/cubits/statistics/statistics_cubit_test.dart b/test/cubits/statistics/statistics_cubit_test.dart index 1cba5731b..fbd489d0f 100644 --- a/test/cubits/statistics/statistics_cubit_test.dart +++ b/test/cubits/statistics/statistics_cubit_test.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; import 'package:coffeecard/data/repositories/v2/leaderboard_repository.dart'; import 'package:coffeecard/models/leaderboard/leaderboard_user.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/data/repositories/v2/account_repository/account_repository_test.dart b/test/data/repositories/v2/account_repository/account_repository_test.dart index a794a7775..62aa01dc5 100644 --- a/test/data/repositories/v2/account_repository/account_repository_test.dart +++ b/test/data/repositories/v2/account_repository/account_repository_test.dart @@ -4,8 +4,8 @@ import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart' hide MessageResponseDto; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart' show CoffeecardApiV2, MessageResponseDto; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/contributor/presentation/cubit/contributor_cubit_test.dart b/test/features/contributor/presentation/cubit/contributor_cubit_test.dart index a64d0fcbe..dad356cda 100644 --- a/test/features/contributor/presentation/cubit/contributor_cubit_test.dart +++ b/test/features/contributor/presentation/cubit/contributor_cubit_test.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/contributor/domain/entities/contributor.dart'; import 'package:coffeecard/features/contributor/domain/usecases/fetch_contributors.dart'; import 'package:coffeecard/features/contributor/presentation/cubit/contributor_cubit.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/occupation/data/datasources/occupation_remote_data_source_test.dart b/test/features/occupation/data/datasources/occupation_remote_data_source_test.dart index b3d233cb8..218350f02 100644 --- a/test/features/occupation/data/datasources/occupation_remote_data_source_test.dart +++ b/test/features/occupation/data/datasources/occupation_remote_data_source_test.dart @@ -2,8 +2,8 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/features/occupation/data/datasources/occupation_remote_data_source.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/occupation/domain/usecase/get_occupations_test.dart b/test/features/occupation/domain/usecase/get_occupations_test.dart index 7d6be5cd6..db9a01a93 100644 --- a/test/features/occupation/domain/usecase/get_occupations_test.dart +++ b/test/features/occupation/domain/usecase/get_occupations_test.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/occupation/data/datasources/occupation_remote_data_source.dart'; import 'package:coffeecard/features/occupation/domain/usecases/get_occupations.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/occupation/presentation/occupation_cubit_test.dart b/test/features/occupation/presentation/occupation_cubit_test.dart index 6676cc76b..b08286b23 100644 --- a/test/features/occupation/presentation/occupation_cubit_test.dart +++ b/test/features/occupation/presentation/occupation_cubit_test.dart @@ -2,8 +2,8 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/occupation/domain/usecases/get_occupations.dart'; import 'package:coffeecard/features/occupation/presentation/cubit/occupation_cubit.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/opening_hours/data/datasources/opening_hours_remote_data_source_test.dart b/test/features/opening_hours/data/datasources/opening_hours_remote_data_source_test.dart index 4697a7157..2f8af6509 100644 --- a/test/features/opening_hours/data/datasources/opening_hours_remote_data_source_test.dart +++ b/test/features/opening_hours/data/datasources/opening_hours_remote_data_source_test.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/generated/api/shiftplanning_api.swagger.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/opening_hours/data/repositories/opening_hours_repository_impl_test.dart b/test/features/opening_hours/data/repositories/opening_hours_repository_impl_test.dart index 7a612640f..a13420e92 100644 --- a/test/features/opening_hours/data/repositories/opening_hours_repository_impl_test.dart +++ b/test/features/opening_hours/data/repositories/opening_hours_repository_impl_test.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/opening_hours/domain/usecases/check_open_status_test.dart b/test/features/opening_hours/domain/usecases/check_open_status_test.dart index 3f7dcfe48..4779ce9c1 100644 --- a/test/features/opening_hours/domain/usecases/check_open_status_test.dart +++ b/test/features/opening_hours/domain/usecases/check_open_status_test.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/opening_hours/domain/usecases/get_opening_hours_test.dart b/test/features/opening_hours/domain/usecases/get_opening_hours_test.dart index 89d249648..024ca69ee 100644 --- a/test/features/opening_hours/domain/usecases/get_opening_hours_test.dart +++ b/test/features/opening_hours/domain/usecases/get_opening_hours_test.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart b/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart index 03baa31de..a0cddee29 100644 --- a/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart +++ b/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart @@ -2,8 +2,8 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/receipt/data/datasources/receipt_remote_data_source_test.dart b/test/features/receipt/data/datasources/receipt_remote_data_source_test.dart index 71f87c63f..50f1744ca 100644 --- a/test/features/receipt/data/datasources/receipt_remote_data_source_test.dart +++ b/test/features/receipt/data/datasources/receipt_remote_data_source_test.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; import 'package:coffeecard/features/receipt/data/datasources/receipt_remote_data_source.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/receipt/data/repositories/receipt_repository_impl_test.dart b/test/features/receipt/data/repositories/receipt_repository_impl_test.dart index f8916ab7b..4d75a5da6 100644 --- a/test/features/receipt/data/repositories/receipt_repository_impl_test.dart +++ b/test/features/receipt/data/repositories/receipt_repository_impl_test.dart @@ -4,8 +4,8 @@ import 'package:coffeecard/features/receipt/data/repositories/receipt_repository import 'package:coffeecard/features/receipt/domain/entities/purchase_receipt.dart'; import 'package:coffeecard/features/receipt/domain/entities/swipe_receipt.dart'; import 'package:coffeecard/features/receipt/domain/repositories/receipt_repository.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/receipt/domain/usecases/get_receipts_test.dart b/test/features/receipt/domain/usecases/get_receipts_test.dart index 06d4fbb58..d0d866bfa 100644 --- a/test/features/receipt/domain/usecases/get_receipts_test.dart +++ b/test/features/receipt/domain/usecases/get_receipts_test.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/receipt/domain/repositories/receipt_repository.dart'; import 'package:coffeecard/features/receipt/domain/usecases/get_receipts.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/receipt/presentation/cubit/receipt_cubit_test.dart b/test/features/receipt/presentation/cubit/receipt_cubit_test.dart index beab5bc2f..7035ffd6e 100644 --- a/test/features/receipt/presentation/cubit/receipt_cubit_test.dart +++ b/test/features/receipt/presentation/cubit/receipt_cubit_test.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/receipt/domain/entities/swipe_receipt.dart'; import 'package:coffeecard/features/receipt/domain/usecases/get_receipts.dart'; import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/ticket/data/datasources/ticket_remote_data_source_test.dart b/test/features/ticket/data/datasources/ticket_remote_data_source_test.dart index 8ea83600c..3d57aa426 100644 --- a/test/features/ticket/data/datasources/ticket_remote_data_source_test.dart +++ b/test/features/ticket/data/datasources/ticket_remote_data_source_test.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/features/ticket/data/datasources/ticket_remote_data_source.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/ticket/domain/usecases/consume_ticket_test.dart b/test/features/ticket/domain/usecases/consume_ticket_test.dart index 8da2809e3..86770831d 100644 --- a/test/features/ticket/domain/usecases/consume_ticket_test.dart +++ b/test/features/ticket/domain/usecases/consume_ticket_test.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/ticket/data/datasources/ticket_remote_data_source.dart'; import 'package:coffeecard/features/ticket/domain/usecases/consume_ticket.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/ticket/domain/usecases/load_tickets_test.dart b/test/features/ticket/domain/usecases/load_tickets_test.dart index 56c5c40bc..f8a6d9cfc 100644 --- a/test/features/ticket/domain/usecases/load_tickets_test.dart +++ b/test/features/ticket/domain/usecases/load_tickets_test.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/ticket/data/datasources/ticket_remote_data_source.dart'; import 'package:coffeecard/features/ticket/domain/usecases/load_tickets.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/ticket/presentation/cubit/tickets_cubit_test.dart b/test/features/ticket/presentation/cubit/tickets_cubit_test.dart index b0317e909..382ebd0bf 100644 --- a/test/features/ticket/presentation/cubit/tickets_cubit_test.dart +++ b/test/features/ticket/presentation/cubit/tickets_cubit_test.dart @@ -4,8 +4,8 @@ import 'package:coffeecard/features/receipt/domain/entities/placeholder_receipt. import 'package:coffeecard/features/ticket/domain/usecases/consume_ticket.dart'; import 'package:coffeecard/features/ticket/domain/usecases/load_tickets.dart'; import 'package:coffeecard/features/ticket/presentation/cubit/tickets_cubit.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/user/data/datasources/user_remote_data_source_test.dart b/test/features/user/data/datasources/user_remote_data_source_test.dart index e64e1d8e8..e8ce72eff 100644 --- a/test/features/user/data/datasources/user_remote_data_source_test.dart +++ b/test/features/user/data/datasources/user_remote_data_source_test.dart @@ -8,8 +8,8 @@ import 'package:coffeecard/features/user/data/models/user_model.dart'; import 'package:coffeecard/features/user/domain/entities/role.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:coffeecard/models/account/update_user.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/user/domain/usecases/get_user_test.dart b/test/features/user/domain/usecases/get_user_test.dart index a165215e9..c3cdf948f 100644 --- a/test/features/user/domain/usecases/get_user_test.dart +++ b/test/features/user/domain/usecases/get_user_test.dart @@ -4,8 +4,8 @@ import 'package:coffeecard/features/user/data/datasources/user_remote_data_sourc import 'package:coffeecard/features/user/data/models/user_model.dart'; import 'package:coffeecard/features/user/domain/entities/role.dart'; import 'package:coffeecard/features/user/domain/usecases/get_user.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/user/domain/usecases/request_account_deletion_test.dart b/test/features/user/domain/usecases/request_account_deletion_test.dart index 09a686b4f..6419ae4a9 100644 --- a/test/features/user/domain/usecases/request_account_deletion_test.dart +++ b/test/features/user/domain/usecases/request_account_deletion_test.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/core/usecases/usecase.dart'; import 'package:coffeecard/features/user/data/datasources/user_remote_data_source.dart'; import 'package:coffeecard/features/user/domain/usecases/request_account_deletion.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/user/domain/usecases/update_user_details_test.dart b/test/features/user/domain/usecases/update_user_details_test.dart index 037a30484..bcd954c79 100644 --- a/test/features/user/domain/usecases/update_user_details_test.dart +++ b/test/features/user/domain/usecases/update_user_details_test.dart @@ -3,8 +3,8 @@ import 'package:coffeecard/features/user/data/datasources/user_remote_data_sourc import 'package:coffeecard/features/user/data/models/user_model.dart'; import 'package:coffeecard/features/user/domain/entities/role.dart'; import 'package:coffeecard/features/user/domain/usecases/update_user_details.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/features/user/presentation/cubit/user_cubit_test.dart b/test/features/user/presentation/cubit/user_cubit_test.dart index 2225222dc..9110cb4fd 100644 --- a/test/features/user/presentation/cubit/user_cubit_test.dart +++ b/test/features/user/presentation/cubit/user_cubit_test.dart @@ -8,8 +8,8 @@ import 'package:coffeecard/features/user/domain/usecases/request_account_deletio import 'package:coffeecard/features/user/domain/usecases/update_user_details.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/models/account/update_user.dart'; -import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; From c6a06d993c0578742f675459c51b07e30a8a64b8 Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Tue, 16 May 2023 17:06:34 +0200 Subject: [PATCH 06/32] ServerFailure returns an unknown error occured, if unable to decode response as json (#463) --- lib/base/strings.dart | 2 +- lib/core/errors/failures.dart | 2 +- test/core/network/network_request_executor_test.dart | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/base/strings.dart b/lib/base/strings.dart index 6ce0162ef..53a3cab53 100644 --- a/lib/base/strings.dart +++ b/lib/base/strings.dart @@ -360,5 +360,5 @@ abstract class Strings { static const String noInternet = "Can't connect to Analog. Are you connected to the internet?"; static const String retry = 'Retry'; - static const String unknownErrorOccured = 'an unknown error occured'; + static const String unknownErrorOccured = 'An unknown error occured'; } diff --git a/lib/core/errors/failures.dart b/lib/core/errors/failures.dart index 7c5c5466e..81ce26632 100644 --- a/lib/core/errors/failures.dart +++ b/lib/core/errors/failures.dart @@ -33,7 +33,7 @@ class ServerFailure extends NetworkFailure { return ServerFailure(message); } on Exception { - return ServerFailure(response.bodyString); + return const ServerFailure(Strings.unknownErrorOccured); } } } diff --git a/test/core/network/network_request_executor_test.dart b/test/core/network/network_request_executor_test.dart index f93d915c7..fd23ff419 100644 --- a/test/core/network/network_request_executor_test.dart +++ b/test/core/network/network_request_executor_test.dart @@ -1,4 +1,5 @@ import 'package:chopper/chopper.dart' as chopper; +import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; @@ -43,7 +44,7 @@ void main() { final actual = await executor(() async => tResponse); // assert - expect(actual, const Left(ServerFailure(''))); + expect(actual, const Left(ServerFailure(Strings.unknownErrorOccured))); }); test('should return response body if api call succeeds', () async { From 4c7d183cd9d7dbb90fb9adef198b021dc8383ac9 Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Tue, 16 May 2023 18:48:32 +0200 Subject: [PATCH 07/32] Added awaits for useTicket and FetchReceipts (#464) --- .../presentation/widgets/swipe_ticket_confirm.dart | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart b/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart index 898ba42c8..9a8cf403e 100644 --- a/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart +++ b/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart @@ -128,13 +128,14 @@ class _ModalContentState extends State<_ModalContent> sliderButtonIconPadding: 0, innerColor: AppColor.white, outerColor: AppColor.primary, - onSubmit: () { + onSubmit: () async { // Disable hero animation in the reverse direction setState(() => _heroTag = -1); - widget.context - .read() - .useTicket(widget.productId); - widget.context.read().fetchReceipts(); + final ticketCubit = widget.context.read(); + final receiptCubit = + widget.context.read(); + await ticketCubit.useTicket(widget.productId); + await receiptCubit.fetchReceipts(); }, ), ), From bddf0ca1b9e5c562300af4b6f5acf1d113a7381b Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Wed, 17 May 2023 12:08:26 +0200 Subject: [PATCH 08/32] Refactor and test Leaderboard (#443) --- lib/cubits/statistics/statistics_cubit.dart | 69 --------- lib/cubits/statistics/statistics_state.dart | 31 ---- .../leaderboard_remote_data_source.dart} | 31 ++-- .../data/models/leaderboard_user_model.dart | 22 +++ .../domain/entities}/leaderboard_user.dart | 12 +- .../domain/usecases/get_leaderboard.dart | 65 +++++++++ .../presentation/cubit/leaderboard_cubit.dart | 29 ++++ .../presentation/cubit/leaderboard_state.dart | 31 ++++ .../presentation/pages/leaderboard_page.dart} | 30 ++-- .../widgets}/leaderboard_list_entry.dart | 0 .../widgets}/leaderboard_list_view.dart | 6 +- .../leaderboard_list_view_placeholder.dart | 2 +- .../widgets}/leaderboard_section.dart | 14 +- .../widgets/statistics_card.dart} | 0 .../widgets}/statistics_dropdown.dart | 8 +- .../widgets/statistics_section.dart} | 6 +- lib/service_locator.dart | 27 ++-- lib/widgets/pages/home_page.dart | 11 +- .../network_request_executor_test.dart | 12 +- .../statistics/statistics_cubit_test.dart | 100 ------------- .../cubit/contributor_cubit_test.dart | 6 +- .../leaderboard_remote_data_source_test.dart | 113 +++++++++++++++ .../models/leaderboard_user_model_test.dart | 30 ++++ .../domain/usecases/get_leaderboard_test.dart | 136 ++++++++++++++++++ .../cubit/leaderboard_cubit_test.dart | 90 ++++++++++++ ...opening_hours_remote_data_source_test.dart | 4 +- .../receipt_repository_impl_test.dart | 12 +- .../cubit/receipt_cubit_test.dart | 6 +- .../cubit/tickets_cubit_test.dart | 8 +- .../user_remote_data_source_test.dart | 6 +- .../presentation/cubit/user_cubit_test.dart | 22 +-- .../stats/leaderboard_list_entry_test.dart | 2 +- .../components/stats/stat_card_test.dart | 2 +- 33 files changed, 630 insertions(+), 313 deletions(-) delete mode 100644 lib/cubits/statistics/statistics_cubit.dart delete mode 100644 lib/cubits/statistics/statistics_state.dart rename lib/{data/repositories/v2/leaderboard_repository.dart => features/leaderboard/data/datasources/leaderboard_remote_data_source.dart} (63%) create mode 100644 lib/features/leaderboard/data/models/leaderboard_user_model.dart rename lib/{models/leaderboard => features/leaderboard/domain/entities}/leaderboard_user.dart (51%) create mode 100644 lib/features/leaderboard/domain/usecases/get_leaderboard.dart create mode 100644 lib/features/leaderboard/presentation/cubit/leaderboard_cubit.dart create mode 100644 lib/features/leaderboard/presentation/cubit/leaderboard_state.dart rename lib/{widgets/pages/stats_page.dart => features/leaderboard/presentation/pages/leaderboard_page.dart} (61%) rename lib/{widgets/components/stats => features/leaderboard/presentation/widgets}/leaderboard_list_entry.dart (100%) rename lib/{widgets/components/stats => features/leaderboard/presentation/widgets}/leaderboard_list_view.dart (75%) rename lib/{widgets/components/stats => features/leaderboard/presentation/widgets}/leaderboard_list_view_placeholder.dart (78%) rename lib/{widgets/components/stats => features/leaderboard/presentation/widgets}/leaderboard_section.dart (69%) rename lib/{widgets/components/stats/stat_card.dart => features/leaderboard/presentation/widgets/statistics_card.dart} (100%) rename lib/{widgets/components/stats => features/leaderboard/presentation/widgets}/statistics_dropdown.dart (76%) rename lib/{widgets/components/stats/stats_section.dart => features/leaderboard/presentation/widgets/statistics_section.dart} (91%) delete mode 100644 test/cubits/statistics/statistics_cubit_test.dart create mode 100644 test/features/leaderboard/data/datasources/leaderboard_remote_data_source_test.dart create mode 100644 test/features/leaderboard/domain/models/leaderboard_user_model_test.dart create mode 100644 test/features/leaderboard/domain/usecases/get_leaderboard_test.dart create mode 100644 test/features/leaderboard/presentation/cubit/leaderboard_cubit_test.dart diff --git a/lib/cubits/statistics/statistics_cubit.dart b/lib/cubits/statistics/statistics_cubit.dart deleted file mode 100644 index b2b0be214..000000000 --- a/lib/cubits/statistics/statistics_cubit.dart +++ /dev/null @@ -1,69 +0,0 @@ -import 'package:coffeecard/data/repositories/v2/leaderboard_repository.dart'; -import 'package:coffeecard/models/leaderboard/leaderboard_user.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -part 'statistics_state.dart'; - -class LeaderboardCubit extends Cubit { - LeaderboardCubit(this._repo) - : super(const StatisticsLoading(filter: LeaderboardFilter.month)); - - final LeaderboardRepository _repo; - - Future setFilter(LeaderboardFilter filter) async { - emit(StatisticsLoading(filter: filter)); - fetch(); - } - - Future fetch() async { - final filter = state.filter; - - final maybeUser = await _repo.getLeaderboardUser(filter); - - maybeUser.fold( - (error) => emit(StatisticsError(error.reason, filter: filter)), - (user) async { - final maybeLeaderboard = await _repo.getLeaderboard(filter); - - maybeLeaderboard.fold( - (error) => emit(StatisticsError(error.reason, filter: filter)), - (leaderboard) { - var userInLeaderboard = false; - final List users = - leaderboard.map((leaderboardUser) { - final isCurrentUser = leaderboardUser.id == user.id; - - // set the 'found' flag if this is the current user - if (!userInLeaderboard && isCurrentUser) { - userInLeaderboard = true; - } - - return LeaderboardUser( - id: leaderboardUser.id, - name: leaderboardUser.name, - score: leaderboardUser.score, - highlight: isCurrentUser, - rank: leaderboardUser.rank, - ); - }).toList(); - - if (!userInLeaderboard) { - users.add( - LeaderboardUser( - id: user.id, - name: user.name, - highlight: true, - score: user.score, - rank: user.rank, - ), - ); - } - - emit(StatisticsLoaded(users, filter: filter)); - }, - ); - }, - ); - } -} diff --git a/lib/cubits/statistics/statistics_state.dart b/lib/cubits/statistics/statistics_state.dart deleted file mode 100644 index 68c81e32f..000000000 --- a/lib/cubits/statistics/statistics_state.dart +++ /dev/null @@ -1,31 +0,0 @@ -part of 'statistics_cubit.dart'; - -enum LeaderboardFilter { semester, month, total } - -abstract class StatisticsState extends Equatable { - const StatisticsState({required this.filter}); - final LeaderboardFilter filter; - - @override - List get props => [filter]; -} - -class StatisticsLoading extends StatisticsState { - const StatisticsLoading({required super.filter}); -} - -class StatisticsLoaded extends StatisticsState { - const StatisticsLoaded(this.leaderboard, {required super.filter}); - final List leaderboard; - - @override - List get props => [filter, leaderboard]; -} - -class StatisticsError extends StatisticsState { - const StatisticsError(this.errorMessage, {required super.filter}); - final String errorMessage; - - @override - List get props => [filter, errorMessage]; -} diff --git a/lib/data/repositories/v2/leaderboard_repository.dart b/lib/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart similarity index 63% rename from lib/data/repositories/v2/leaderboard_repository.dart rename to lib/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart index f5aad5a1f..34576d7f9 100644 --- a/lib/data/repositories/v2/leaderboard_repository.dart +++ b/lib/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart @@ -1,8 +1,10 @@ import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/either_extensions.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; -import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; +import 'package:coffeecard/features/leaderboard/data/models/leaderboard_user_model.dart'; +import 'package:coffeecard/features/leaderboard/domain/entities/leaderboard_user.dart'; +import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; -import 'package:coffeecard/models/leaderboard/leaderboard_user.dart'; import 'package:fpdart/fpdart.dart'; extension _FilterCategoryToPresetString on LeaderboardFilter { @@ -18,32 +20,31 @@ extension _FilterCategoryToPresetString on LeaderboardFilter { } } -class LeaderboardRepository { - LeaderboardRepository({ +class LeaderboardRemoteDataSource { + final CoffeecardApiV2 apiV2; + final NetworkRequestExecutor executor; + + LeaderboardRemoteDataSource({ required this.apiV2, required this.executor, }); - final CoffeecardApiV2 apiV2; - final NetworkRequestExecutor executor; - Future>> getLeaderboard( LeaderboardFilter category, + int top, ) async { - final result = await executor( - () => apiV2.apiV2LeaderboardTopGet(preset: category.label, top: 10), + return executor( + () => apiV2.apiV2LeaderboardTopGet(preset: category.label, top: top), + ).bindFuture( + (result) => result.map(LeaderboardUserModel.fromDTO).toList(), ); - - return result.map((result) => result.map(LeaderboardUser.fromDTO).toList()); } Future> getLeaderboardUser( LeaderboardFilter category, ) async { - final result = await executor( + return executor( () => apiV2.apiV2LeaderboardGet(preset: category.label), - ); - - return result.map(LeaderboardUser.fromDTO); + ).bindFuture(LeaderboardUserModel.fromDTO); } } diff --git a/lib/features/leaderboard/data/models/leaderboard_user_model.dart b/lib/features/leaderboard/data/models/leaderboard_user_model.dart new file mode 100644 index 000000000..a5b9d1788 --- /dev/null +++ b/lib/features/leaderboard/data/models/leaderboard_user_model.dart @@ -0,0 +1,22 @@ +import 'package:coffeecard/features/leaderboard/domain/entities/leaderboard_user.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; + +class LeaderboardUserModel extends LeaderboardUser { + const LeaderboardUserModel({ + required super.id, + required super.rank, + required super.score, + required super.name, + required super.highlight, + }); + + factory LeaderboardUserModel.fromDTO(LeaderboardEntry dto) { + return LeaderboardUserModel( + id: dto.id!, + rank: dto.rank!, + score: dto.score!, + name: dto.name!, + highlight: false, + ); + } +} diff --git a/lib/models/leaderboard/leaderboard_user.dart b/lib/features/leaderboard/domain/entities/leaderboard_user.dart similarity index 51% rename from lib/models/leaderboard/leaderboard_user.dart rename to lib/features/leaderboard/domain/entities/leaderboard_user.dart index 7a159cb82..c8121ceeb 100644 --- a/lib/models/leaderboard/leaderboard_user.dart +++ b/lib/features/leaderboard/domain/entities/leaderboard_user.dart @@ -1,4 +1,3 @@ -import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; import 'package:equatable/equatable.dart'; class LeaderboardUser extends Equatable { @@ -16,15 +15,6 @@ class LeaderboardUser extends Equatable { required this.highlight, }); - LeaderboardUser.fromDTO(LeaderboardEntry dto) - : id = dto.id!, - name = dto.name!, - rank = dto.rank!, - score = dto.score!, - highlight = false; - @override - List get props { - return [id, rank, score, name, highlight]; - } + List get props => [id, rank, score, name, highlight]; } diff --git a/lib/features/leaderboard/domain/usecases/get_leaderboard.dart b/lib/features/leaderboard/domain/usecases/get_leaderboard.dart new file mode 100644 index 000000000..47804f283 --- /dev/null +++ b/lib/features/leaderboard/domain/usecases/get_leaderboard.dart @@ -0,0 +1,65 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart'; +import 'package:coffeecard/features/leaderboard/domain/entities/leaderboard_user.dart'; +import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; +import 'package:fpdart/fpdart.dart'; + +class GetLeaderboard + implements UseCase, LeaderboardFilter> { + final LeaderboardRemoteDataSource remoteDataSource; + + GetLeaderboard({required this.remoteDataSource}); + + @override + Future>> call( + LeaderboardFilter filter, + ) async { + final leaderboardUser = await remoteDataSource.getLeaderboardUser(filter); + + return leaderboardUser.fold( + (error) => Left(error), + (user) async { + final leaderboardEither = + await remoteDataSource.getLeaderboard(filter, 10); + + return leaderboardEither.fold( + (error) => Left(error), + (leaderboard) => Right(buildLeaderboard(leaderboard, user)), + ); + }, + ); + } + + List buildLeaderboard( + List leaderboard, + LeaderboardUser user, + ) { + final users = leaderboard + .map( + (leaderboardUser) => LeaderboardUser( + id: leaderboardUser.id, + name: leaderboardUser.name, + score: leaderboardUser.score, + highlight: leaderboardUser.id == user.id, // is current user + rank: leaderboardUser.rank, + ), + ) + .toList(); + + if (!users.any((leaderboardUser) => leaderboardUser.id == user.id)) { + // user is not in the leaderboard, highlight them at the bottom + users.add( + LeaderboardUser( + id: user.id, + name: user.name, + highlight: true, + score: user.score, + rank: user.rank, + ), + ); + } + + return users; + } +} diff --git a/lib/features/leaderboard/presentation/cubit/leaderboard_cubit.dart b/lib/features/leaderboard/presentation/cubit/leaderboard_cubit.dart new file mode 100644 index 000000000..85d191941 --- /dev/null +++ b/lib/features/leaderboard/presentation/cubit/leaderboard_cubit.dart @@ -0,0 +1,29 @@ +import 'package:coffeecard/features/leaderboard/domain/entities/leaderboard_user.dart'; +import 'package:coffeecard/features/leaderboard/domain/usecases/get_leaderboard.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'leaderboard_state.dart'; + +class LeaderboardCubit extends Cubit { + final GetLeaderboard getLeaderboard; + + LeaderboardCubit({required this.getLeaderboard}) + : super(const LeaderboardLoading(filter: LeaderboardFilter.month)); + + Future setFilter(LeaderboardFilter filter) async { + emit(LeaderboardLoading(filter: filter)); + loadLeaderboard(); + } + + Future loadLeaderboard() async { + final filter = state.filter; + + final maybeLeaderboard = await getLeaderboard(filter); + + maybeLeaderboard.fold( + (error) => emit(LeaderboardError(error.reason, filter: filter)), + (leaderboard) => emit(LeaderboardLoaded(leaderboard, filter: filter)), + ); + } +} diff --git a/lib/features/leaderboard/presentation/cubit/leaderboard_state.dart b/lib/features/leaderboard/presentation/cubit/leaderboard_state.dart new file mode 100644 index 000000000..995f94082 --- /dev/null +++ b/lib/features/leaderboard/presentation/cubit/leaderboard_state.dart @@ -0,0 +1,31 @@ +part of 'leaderboard_cubit.dart'; + +enum LeaderboardFilter { semester, month, total } + +abstract class LeaderboardState extends Equatable { + const LeaderboardState({required this.filter}); + final LeaderboardFilter filter; + + @override + List get props => [filter]; +} + +class LeaderboardLoading extends LeaderboardState { + const LeaderboardLoading({required super.filter}); +} + +class LeaderboardLoaded extends LeaderboardState { + const LeaderboardLoaded(this.leaderboard, {required super.filter}); + final List leaderboard; + + @override + List get props => [filter, leaderboard]; +} + +class LeaderboardError extends LeaderboardState { + const LeaderboardError(this.errorMessage, {required super.filter}); + final String errorMessage; + + @override + List get props => [filter, errorMessage]; +} diff --git a/lib/widgets/pages/stats_page.dart b/lib/features/leaderboard/presentation/pages/leaderboard_page.dart similarity index 61% rename from lib/widgets/pages/stats_page.dart rename to lib/features/leaderboard/presentation/pages/leaderboard_page.dart index 00ba1c593..cc0b1b59f 100644 --- a/lib/widgets/pages/stats_page.dart +++ b/lib/features/leaderboard/presentation/pages/leaderboard_page.dart @@ -1,31 +1,33 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; +import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; +import 'package:coffeecard/features/leaderboard/presentation/widgets/leaderboard_section.dart'; +import 'package:coffeecard/features/leaderboard/presentation/widgets/statistics_section.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/widgets/components/error_section.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; -import 'package:coffeecard/widgets/components/stats/leaderboard_section.dart'; -import 'package:coffeecard/widgets/components/stats/stats_section.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -class StatsPage extends StatelessWidget { - const StatsPage({required this.scrollController}); +class StatisticsPage extends StatelessWidget { + const StatisticsPage({required this.scrollController}); final ScrollController scrollController; static Route routeWith({required ScrollController scrollController}) { return MaterialPageRoute( - builder: (_) => StatsPage(scrollController: scrollController), + builder: (_) => StatisticsPage(scrollController: scrollController), ); } @override Widget build(BuildContext context) { - Future refresh() async => context.read().fetch(); + Future refresh() async => + context.read().loadLeaderboard(); final userState = context.watch().state; - final statsState = context.watch().state; - final loading = userState is! UserLoaded || statsState is! StatisticsLoaded; + final leaderboardState = context.watch().state; + final loading = + userState is! UserLoaded || leaderboardState is! LeaderboardLoaded; if (userState is UserError) { return ErrorSection( @@ -35,11 +37,11 @@ class StatsPage extends StatelessWidget { ); } - if (statsState is StatisticsError) { + if (leaderboardState is LeaderboardError) { return ErrorSection( center: true, - error: statsState.errorMessage, - retry: () => context.read().fetch(), + error: leaderboardState.errorMessage, + retry: () => context.read().loadLeaderboard(), ); } @@ -55,11 +57,11 @@ class StatsPage extends StatelessWidget { controller: scrollController, physics: loading ? const NeverScrollableScrollPhysics() : null, children: [ - const StatsSection(), + const StatisticsSection(), LeaderboardSection( loading: loading, userState: userState, - statsState: statsState, + leaderboardState: leaderboardState, ), ], ), diff --git a/lib/widgets/components/stats/leaderboard_list_entry.dart b/lib/features/leaderboard/presentation/widgets/leaderboard_list_entry.dart similarity index 100% rename from lib/widgets/components/stats/leaderboard_list_entry.dart rename to lib/features/leaderboard/presentation/widgets/leaderboard_list_entry.dart diff --git a/lib/widgets/components/stats/leaderboard_list_view.dart b/lib/features/leaderboard/presentation/widgets/leaderboard_list_view.dart similarity index 75% rename from lib/widgets/components/stats/leaderboard_list_view.dart rename to lib/features/leaderboard/presentation/widgets/leaderboard_list_view.dart index ab98f422c..dfe2adac7 100644 --- a/lib/widgets/components/stats/leaderboard_list_view.dart +++ b/lib/features/leaderboard/presentation/widgets/leaderboard_list_view.dart @@ -1,10 +1,10 @@ -import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; +import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; +import 'package:coffeecard/features/leaderboard/presentation/widgets/leaderboard_list_entry.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; -import 'package:coffeecard/widgets/components/stats/leaderboard_list_entry.dart'; import 'package:flutter/material.dart'; class LeaderboardListView extends StatelessWidget { - final StatisticsLoaded statsState; + final LeaderboardLoaded statsState; final UserLoaded userState; const LeaderboardListView({ diff --git a/lib/widgets/components/stats/leaderboard_list_view_placeholder.dart b/lib/features/leaderboard/presentation/widgets/leaderboard_list_view_placeholder.dart similarity index 78% rename from lib/widgets/components/stats/leaderboard_list_view_placeholder.dart rename to lib/features/leaderboard/presentation/widgets/leaderboard_list_view_placeholder.dart index 41a242ff5..740e3ecf0 100644 --- a/lib/widgets/components/stats/leaderboard_list_view_placeholder.dart +++ b/lib/features/leaderboard/presentation/widgets/leaderboard_list_view_placeholder.dart @@ -1,4 +1,4 @@ -import 'package:coffeecard/widgets/components/stats/leaderboard_list_entry.dart'; +import 'package:coffeecard/features/leaderboard/presentation/widgets/leaderboard_list_entry.dart'; import 'package:flutter/material.dart'; final _placeholderListEntries = diff --git a/lib/widgets/components/stats/leaderboard_section.dart b/lib/features/leaderboard/presentation/widgets/leaderboard_section.dart similarity index 69% rename from lib/widgets/components/stats/leaderboard_section.dart rename to lib/features/leaderboard/presentation/widgets/leaderboard_section.dart index 1414c0d7c..768cd5b11 100644 --- a/lib/widgets/components/stats/leaderboard_section.dart +++ b/lib/features/leaderboard/presentation/widgets/leaderboard_section.dart @@ -1,23 +1,23 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; +import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; +import 'package:coffeecard/features/leaderboard/presentation/widgets/leaderboard_list_view.dart'; +import 'package:coffeecard/features/leaderboard/presentation/widgets/leaderboard_list_view_placeholder.dart'; +import 'package:coffeecard/features/leaderboard/presentation/widgets/statistics_dropdown.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/filter_bar.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/widgets/components/section_title.dart'; -import 'package:coffeecard/widgets/components/stats/leaderboard_list_view.dart'; -import 'package:coffeecard/widgets/components/stats/leaderboard_list_view_placeholder.dart'; -import 'package:coffeecard/widgets/components/stats/statistics_dropdown.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; class LeaderboardSection extends StatelessWidget { const LeaderboardSection({ required this.loading, - required this.statsState, + required this.leaderboardState, required this.userState, }); final bool loading; - final StatisticsState statsState; + final LeaderboardState leaderboardState; final UserState userState; @override @@ -39,7 +39,7 @@ class LeaderboardSection extends StatelessWidget { else LeaderboardListView( // if not loading, then these states must be loaded - statsState: statsState as StatisticsLoaded, + statsState: leaderboardState as LeaderboardLoaded, userState: userState as UserLoaded, ), ], diff --git a/lib/widgets/components/stats/stat_card.dart b/lib/features/leaderboard/presentation/widgets/statistics_card.dart similarity index 100% rename from lib/widgets/components/stats/stat_card.dart rename to lib/features/leaderboard/presentation/widgets/statistics_card.dart diff --git a/lib/widgets/components/stats/statistics_dropdown.dart b/lib/features/leaderboard/presentation/widgets/statistics_dropdown.dart similarity index 76% rename from lib/widgets/components/stats/statistics_dropdown.dart rename to lib/features/leaderboard/presentation/widgets/statistics_dropdown.dart index 41a26e3fd..b47d36be6 100644 --- a/lib/widgets/components/stats/statistics_dropdown.dart +++ b/lib/features/leaderboard/presentation/widgets/statistics_dropdown.dart @@ -1,4 +1,4 @@ -import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; +import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; import 'package:coffeecard/widgets/components/dropdown.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -9,12 +9,12 @@ class StatisticsDropdown extends StatelessWidget { void onChanged(LeaderboardFilter? filter) => context.read().setFilter(filter!); - return BlocBuilder( + return BlocBuilder( buildWhen: (previous, current) => - current is StatisticsLoaded || current is StatisticsLoading, + current is LeaderboardLoaded || current is LeaderboardLoading, builder: (_, state) { return Dropdown( - loading: state is StatisticsLoading, + loading: state is LeaderboardLoading, onChanged: onChanged, value: state.filter, items: _menuItems, diff --git a/lib/widgets/components/stats/stats_section.dart b/lib/features/leaderboard/presentation/widgets/statistics_section.dart similarity index 91% rename from lib/widgets/components/stats/stats_section.dart rename to lib/features/leaderboard/presentation/widgets/statistics_section.dart index 960f66168..28d72feb8 100644 --- a/lib/widgets/components/stats/stats_section.dart +++ b/lib/features/leaderboard/presentation/widgets/statistics_section.dart @@ -1,15 +1,15 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/features/leaderboard/presentation/widgets/statistics_card.dart'; import 'package:coffeecard/features/user/domain/entities/user.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/widgets/components/helpers/grid.dart'; -import 'package:coffeecard/widgets/components/stats/stat_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; -class StatsSection extends StatelessWidget { - const StatsSection(); +class StatisticsSection extends StatelessWidget { + const StatisticsSection(); @override Widget build(BuildContext context) { diff --git a/lib/service_locator.dart b/lib/service_locator.dart index 9faab89ca..ff4503026 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -6,13 +6,15 @@ import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; import 'package:coffeecard/data/repositories/v1/voucher_repository.dart'; import 'package:coffeecard/data/repositories/v2/app_config_repository.dart'; -import 'package:coffeecard/data/repositories/v2/leaderboard_repository.dart'; import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; import 'package:coffeecard/data/storage/secure_storage.dart'; import 'package:coffeecard/env/env.dart'; import 'package:coffeecard/features/contributor/data/datasources/contributor_local_data_source.dart'; import 'package:coffeecard/features/contributor/domain/usecases/fetch_contributors.dart'; import 'package:coffeecard/features/contributor/presentation/cubit/contributor_cubit.dart'; +import 'package:coffeecard/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart'; +import 'package:coffeecard/features/leaderboard/domain/usecases/get_leaderboard.dart'; +import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; import 'package:coffeecard/features/occupation/data/datasources/occupation_remote_data_source.dart'; import 'package:coffeecard/features/occupation/domain/usecases/get_occupations.dart'; import 'package:coffeecard/features/occupation/presentation/cubit/occupation_cubit.dart'; @@ -125,13 +127,6 @@ void configureServices() { ); // v2 - sl.registerFactory( - () => LeaderboardRepository( - apiV2: sl(), - executor: sl(), - ), - ); - sl.registerFactory( () => PurchaseRepository( apiV2: sl(), @@ -159,6 +154,7 @@ void initFeatures() { initUser(); initReceipt(); initContributor(); + initLeaderboard(); } void initOpeningHours() { @@ -272,3 +268,18 @@ void initContributor() { // data source sl.registerLazySingleton(() => ContributorLocalDataSource()); } + +void initLeaderboard() { + // bloc + sl.registerFactory( + () => LeaderboardCubit(getLeaderboard: sl()), + ); + + // use case + sl.registerFactory(() => GetLeaderboard(remoteDataSource: sl())); + + // data source + sl.registerLazySingleton( + () => LeaderboardRemoteDataSource(apiV2: sl(), executor: sl()), + ); +} diff --git a/lib/widgets/pages/home_page.dart b/lib/widgets/pages/home_page.dart index a1ada12f9..16ee089f1 100644 --- a/lib/widgets/pages/home_page.dart +++ b/lib/widgets/pages/home_page.dart @@ -3,8 +3,8 @@ import 'dart:math'; import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; -import 'package:coffeecard/data/repositories/v2/leaderboard_repository.dart'; +import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; +import 'package:coffeecard/features/leaderboard/presentation/pages/leaderboard_page.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart'; import 'package:coffeecard/features/receipt/presentation/pages/receipts_page.dart'; @@ -14,7 +14,6 @@ import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/widgets/components/helpers/lazy_indexed_stack.dart'; import 'package:coffeecard/widgets/pages/settings/settings_page.dart'; -import 'package:coffeecard/widgets/pages/stats_page.dart'; import 'package:coffeecard/widgets/routers/app_flow.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -104,7 +103,7 @@ class _HomePageState extends State { ), AppFlow( navigatorKey: _pages[2].navigatorKey, - initialRoute: StatsPage.routeWith( + initialRoute: StatisticsPage.routeWith( scrollController: _pages[2].scrollController, ), ), @@ -130,9 +129,7 @@ class _HomePageState extends State { create: (_) => sl()..fetchReceipts(), ), BlocProvider( - create: (_) => LeaderboardCubit( - sl.get(), - )..fetch(), + create: (_) => sl()..loadLeaderboard(), ), BlocProvider( create: (_) => sl()..getOpeninghours(), diff --git a/test/core/network/network_request_executor_test.dart b/test/core/network/network_request_executor_test.dart index fd23ff419..32ba84905 100644 --- a/test/core/network/network_request_executor_test.dart +++ b/test/core/network/network_request_executor_test.dart @@ -38,10 +38,10 @@ void main() { test('should return [ServerFailure] if api call fails', () async { // arrange - final tResponse = responseFromStatusCode(500, body: ''); + final testResponse = responseFromStatusCode(500, body: ''); // act - final actual = await executor(() async => tResponse); + final actual = await executor(() async => testResponse); // assert expect(actual, const Left(ServerFailure(Strings.unknownErrorOccured))); @@ -49,10 +49,10 @@ void main() { test('should return response body if api call succeeds', () async { // arrange - final tResponse = responseFromStatusCode(200, body: 'some string'); + final testResponse = responseFromStatusCode(200, body: 'some string'); // act - final actual = await executor(() async => tResponse); + final actual = await executor(() async => testResponse); // assert expect(actual, const Right('some string')); @@ -60,10 +60,10 @@ void main() { test('should return [ServerFailure] if call throws [Exception]', () async { // arrange - final tException = Exception('some error'); + final testException = Exception('some error'); // act - final actual = await executor(() async => throw tException); + final actual = await executor(() async => throw testException); // assert expect(actual, const Left(ConnectionFailure())); diff --git a/test/cubits/statistics/statistics_cubit_test.dart b/test/cubits/statistics/statistics_cubit_test.dart deleted file mode 100644 index fbd489d0f..000000000 --- a/test/cubits/statistics/statistics_cubit_test.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; -import 'package:coffeecard/data/repositories/v2/leaderboard_repository.dart'; -import 'package:coffeecard/models/leaderboard/leaderboard_user.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'statistics_cubit_test.mocks.dart'; - -@GenerateMocks([LeaderboardRepository]) -void main() { - const dummyLeaderboardUser = LeaderboardUser( - id: 0, - name: 'name', - score: 0, - rank: 0, - highlight: true, - ); - group('statistics cubit tests', () { - late LeaderboardCubit statisticsCubit; - final leaderboardRepository = MockLeaderboardRepository(); - - setUp(() { - statisticsCubit = LeaderboardCubit(leaderboardRepository); - }); - - blocTest( - 'fetch emits StatisticsLoaded after successful fetch', - build: () { - when(leaderboardRepository.getLeaderboard(any)) - .thenAnswer((_) async => const Right([])); - when(leaderboardRepository.getLeaderboardUser(any)) - .thenAnswer((_) async => const Right(dummyLeaderboardUser)); - return statisticsCubit; - }, - act: (cubit) => cubit.fetch(), - expect: () => [ - const StatisticsLoaded( - [dummyLeaderboardUser], - filter: LeaderboardFilter.month, - ), - ], - ); - - blocTest( - 'fetch emits StatisticsError after failed fetch', - build: () { - when(leaderboardRepository.getLeaderboard(any)).thenAnswer( - (_) async => const Left(ServerFailure('some error')), - ); - return statisticsCubit; - }, - act: (cubit) => cubit.fetch(), - expect: () => [ - const StatisticsError('some error', filter: LeaderboardFilter.month), - ], - ); - - blocTest( - 'setFilter emits StatisticsLoading with correct filter and then emits StatisticsLoaded after successful fetch', - build: () { - when(leaderboardRepository.getLeaderboard(any)) - .thenAnswer((_) async => const Right([])); - when(leaderboardRepository.getLeaderboardUser(any)) - .thenAnswer((_) async => const Right(dummyLeaderboardUser)); - return statisticsCubit; - }, - act: (cubit) => cubit.setFilter(LeaderboardFilter.semester), - expect: () => [ - const StatisticsLoading(filter: LeaderboardFilter.semester), - const StatisticsLoaded( - [dummyLeaderboardUser], - filter: LeaderboardFilter.semester, - ), - ], - ); - - blocTest( - 'setFilter emits StatisticsLoading with correct filter and then emits StatisticsError after failed fetch', - build: () { - when(leaderboardRepository.getLeaderboard(any)).thenAnswer( - (_) async => const Left(ServerFailure('some error')), - ); - return statisticsCubit; - }, - act: (cubit) => cubit.setFilter(LeaderboardFilter.total), - expect: () => [ - const StatisticsLoading(filter: LeaderboardFilter.total), - const StatisticsError('some error', filter: LeaderboardFilter.total), - ], - ); - - tearDown(() { - statisticsCubit.close(); - }); - }); -} diff --git a/test/features/contributor/presentation/cubit/contributor_cubit_test.dart b/test/features/contributor/presentation/cubit/contributor_cubit_test.dart index dad356cda..e266d38ae 100644 --- a/test/features/contributor/presentation/cubit/contributor_cubit_test.dart +++ b/test/features/contributor/presentation/cubit/contributor_cubit_test.dart @@ -27,7 +27,7 @@ void main() { group( 'getContributors', () { - const tContributors = [ + const testContributors = [ Contributor( name: 'name', avatarUrl: 'avatarUrl', @@ -38,10 +38,10 @@ void main() { 'should emit [Loaded] with data when use case succeeds', build: () => cubit, setUp: () => when(fetchContributors(any)) - .thenAnswer((_) async => const Right(tContributors)), + .thenAnswer((_) async => const Right(testContributors)), act: (_) => cubit.getContributors(), expect: () => [ - const ContributorLoaded(tContributors), + const ContributorLoaded(testContributors), ], ); diff --git a/test/features/leaderboard/data/datasources/leaderboard_remote_data_source_test.dart b/test/features/leaderboard/data/datasources/leaderboard_remote_data_source_test.dart new file mode 100644 index 000000000..46ebd98aa --- /dev/null +++ b/test/features/leaderboard/data/datasources/leaderboard_remote_data_source_test.dart @@ -0,0 +1,113 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart'; +import 'package:coffeecard/features/leaderboard/data/models/leaderboard_user_model.dart'; +import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'leaderboard_remote_data_source_test.mocks.dart'; + +@GenerateMocks([CoffeecardApiV2, NetworkRequestExecutor]) +void main() { + late MockCoffeecardApiV2 apiV2; + late MockNetworkRequestExecutor executor; + late LeaderboardRemoteDataSource dataSource; + + setUp(() { + apiV2 = MockCoffeecardApiV2(); + executor = MockNetworkRequestExecutor(); + dataSource = LeaderboardRemoteDataSource(apiV2: apiV2, executor: executor); + }); + + const testErrorMessage = 'some error'; + + group('getLeaderboard', () { + test('should return [Right] when executor succeeds', () async { + // arrange + when(executor.call>(any)).thenAnswer( + (_) async => const Right([]), + ); + + // act + final actual = await dataSource.getLeaderboard( + LeaderboardFilter.total, + 10, + ); + + // assert + expect(actual.isRight(), true); + actual.map( + (response) => expect(response, []), + ); + }); + + test('should return [Left] when executor fails', () async { + // arrange + when(executor.call>(any)).thenAnswer( + (_) async => const Left(ServerFailure(testErrorMessage)), + ); + + // act + final actual = await dataSource.getLeaderboard( + LeaderboardFilter.total, + 10, + ); + + // assert + expect(actual, const Left(ServerFailure(testErrorMessage))); + }); + }); + + group('getLeaderboardUser', () { + test('should return [Right] when executor succeeds', () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => Right( + LeaderboardEntry( + id: 0, + rank: 0, + score: 0, + name: 'name', + ), + ), + ); + + // act + final actual = + await dataSource.getLeaderboardUser(LeaderboardFilter.total); + + // assert + expect(actual.isRight(), true); + actual.map( + (response) => expect( + response, + const LeaderboardUserModel( + id: 0, + rank: 0, + score: 0, + name: 'name', + highlight: false, + ), + ), + ); + }); + + test('should return [Left] when executor fails', () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => const Left(ServerFailure(testErrorMessage)), + ); + + // act + final actual = + await dataSource.getLeaderboardUser(LeaderboardFilter.total); + + // assert + expect(actual, const Left(ServerFailure(testErrorMessage))); + }); + }); +} diff --git a/test/features/leaderboard/domain/models/leaderboard_user_model_test.dart b/test/features/leaderboard/domain/models/leaderboard_user_model_test.dart new file mode 100644 index 000000000..1917ac401 --- /dev/null +++ b/test/features/leaderboard/domain/models/leaderboard_user_model_test.dart @@ -0,0 +1,30 @@ +import 'package:coffeecard/features/leaderboard/data/models/leaderboard_user_model.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + test('should map LeaderboardEntry', () { + // arrange + final entry = LeaderboardEntry( + id: 0, + rank: 0, + score: 0, + name: 'name', + ); + + // act + final actual = LeaderboardUserModel.fromDTO(entry); + + // assert + expect( + actual, + const LeaderboardUserModel( + id: 0, + rank: 0, + score: 0, + name: 'name', + highlight: false, + ), + ); + }); +} diff --git a/test/features/leaderboard/domain/usecases/get_leaderboard_test.dart b/test/features/leaderboard/domain/usecases/get_leaderboard_test.dart new file mode 100644 index 000000000..d4e478ed6 --- /dev/null +++ b/test/features/leaderboard/domain/usecases/get_leaderboard_test.dart @@ -0,0 +1,136 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart'; +import 'package:coffeecard/features/leaderboard/domain/entities/leaderboard_user.dart'; +import 'package:coffeecard/features/leaderboard/domain/usecases/get_leaderboard.dart'; +import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'get_leaderboard_test.mocks.dart'; + +@GenerateMocks([LeaderboardRemoteDataSource]) +void main() { + late MockLeaderboardRemoteDataSource remoteDataSource; + late GetLeaderboard usecase; + + setUp(() { + remoteDataSource = MockLeaderboardRemoteDataSource(); + usecase = GetLeaderboard(remoteDataSource: remoteDataSource); + }); + + group('call', () { + const testUserLeaderboard = LeaderboardUser( + id: 0, + rank: 0, + score: 0, + name: 'name', + highlight: true, + ); + + test('should return [Left] if getLeaderboardUser fails', () async { + // arrange + when(remoteDataSource.getLeaderboardUser(any)) + .thenAnswer((_) async => const Left(ServerFailure('some error'))); + + // act + final actual = await usecase(LeaderboardFilter.total); + + // assert + expect(actual, const Left(ServerFailure('some error'))); + }); + test('should return [Left] if getLeaderboard fails', () async { + // arrange + when(remoteDataSource.getLeaderboardUser(any)).thenAnswer( + (_) async => const Right( + testUserLeaderboard, + ), + ); + when(remoteDataSource.getLeaderboard(any, any)) + .thenAnswer((_) async => const Left(ServerFailure('some error'))); + + // act + final actual = await usecase(LeaderboardFilter.total); + + // assert + expect(actual, const Left(ServerFailure('some error'))); + }); + test('should return [Right] if api calls succeed', () async { + // arrange + when(remoteDataSource.getLeaderboardUser(any)).thenAnswer( + (_) async => const Right(testUserLeaderboard), + ); + when(remoteDataSource.getLeaderboard(any, any)) + .thenAnswer((_) async => const Right([])); + + // act + final actual = await usecase(LeaderboardFilter.total); + + // assert + expect(actual.isRight(), true); + actual.map( + (response) => expect( + response, + [testUserLeaderboard], + ), + ); + }); + }); + + group('buildLeaderboard', () { + test('should highlight user if present in leaderboard', () { + // arrange + const testUser = LeaderboardUser( + id: 0, + rank: 0, + score: 0, + name: 'name', + highlight: false, + ); + + final testLeaderboard = [testUser]; + + // act + final actual = usecase.buildLeaderboard(testLeaderboard, testUser); + + // assert + expect(actual, [ + const LeaderboardUser( + id: 0, + rank: 0, + score: 0, + name: 'name', + highlight: true, + ), + ]); + }); + + test('should append user if not present in leaderboard', () { + // arrange + const testUser = LeaderboardUser( + id: 0, + rank: 0, + score: 0, + name: 'name', + highlight: false, + ); + + final List testLeaderboard = []; + + // act + final actual = usecase.buildLeaderboard(testLeaderboard, testUser); + + // assert + expect(actual, [ + const LeaderboardUser( + id: 0, + rank: 0, + score: 0, + name: 'name', + highlight: true, + ), + ]); + }); + }); +} diff --git a/test/features/leaderboard/presentation/cubit/leaderboard_cubit_test.dart b/test/features/leaderboard/presentation/cubit/leaderboard_cubit_test.dart new file mode 100644 index 000000000..9423fbfb7 --- /dev/null +++ b/test/features/leaderboard/presentation/cubit/leaderboard_cubit_test.dart @@ -0,0 +1,90 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/features/leaderboard/data/models/leaderboard_user_model.dart'; +import 'package:coffeecard/features/leaderboard/domain/usecases/get_leaderboard.dart'; +import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'leaderboard_cubit_test.mocks.dart'; + +@GenerateMocks([GetLeaderboard]) +void main() { + late MockGetLeaderboard getLeaderboard; + late LeaderboardCubit cubit; + + setUp(() { + getLeaderboard = MockGetLeaderboard(); + cubit = LeaderboardCubit(getLeaderboard: getLeaderboard); + }); + + const testLeaderboard = [ + LeaderboardUserModel( + id: 0, + name: 'name', + score: 0, + rank: 0, + highlight: true, + ), + ]; + + group('setFilter', () { + blocTest( + 'should emit [LeaderboardLoading, LeaderboardLoaded] with correct filter', + setUp: () => when(getLeaderboard(any)) + .thenAnswer((_) async => const Right(testLeaderboard)), + build: () => cubit, + act: (_) => cubit.setFilter(LeaderboardFilter.semester), + expect: () => [ + const LeaderboardLoading(filter: LeaderboardFilter.semester), + const LeaderboardLoaded( + testLeaderboard, + filter: LeaderboardFilter.semester, + ), + ], + ); + + blocTest( + 'should emit [LeaderboardLoading, LeaderboardError] with correct filter', + setUp: () => when(getLeaderboard(any)).thenAnswer( + (_) async => const Left(ServerFailure('some error')), + ), + build: () => cubit, + act: (_) => cubit.setFilter(LeaderboardFilter.total), + expect: () => [ + const LeaderboardLoading(filter: LeaderboardFilter.total), + const LeaderboardError('some error', filter: LeaderboardFilter.total), + ], + ); + }); + + group('loadLeaderboard', () { + blocTest( + 'should emit [LeaderboardLoaded] when usecase succeeds', + setUp: () => when(getLeaderboard(any)) + .thenAnswer((_) async => const Right(testLeaderboard)), + build: () => cubit, + act: (_) => cubit.loadLeaderboard(), + expect: () => [ + const LeaderboardLoaded( + testLeaderboard, + filter: LeaderboardFilter.month, + ), + ], + ); + + blocTest( + 'should emit [LeaderboardError] when usecase fails', + setUp: () => when(getLeaderboard(any)).thenAnswer( + (_) async => const Left(ServerFailure('some error')), + ), + build: () => cubit, + act: (_) => cubit.loadLeaderboard(), + expect: () => [ + const LeaderboardError('some error', filter: LeaderboardFilter.month), + ], + ); + }); +} diff --git a/test/features/opening_hours/data/datasources/opening_hours_remote_data_source_test.dart b/test/features/opening_hours/data/datasources/opening_hours_remote_data_source_test.dart index 2f8af6509..9969fae5f 100644 --- a/test/features/opening_hours/data/datasources/opening_hours_remote_data_source_test.dart +++ b/test/features/opening_hours/data/datasources/opening_hours_remote_data_source_test.dart @@ -38,10 +38,10 @@ void main() { group('getOpeningHours', () { test('should call executor', () async { // arrange - final List tOpeningHours = []; + final List testOpeningHours = []; when(executor>(any)).thenAnswer( - (_) async => Right(tOpeningHours), + (_) async => Right(testOpeningHours), ); // act diff --git a/test/features/receipt/data/repositories/receipt_repository_impl_test.dart b/test/features/receipt/data/repositories/receipt_repository_impl_test.dart index 4d75a5da6..2332377fe 100644 --- a/test/features/receipt/data/repositories/receipt_repository_impl_test.dart +++ b/test/features/receipt/data/repositories/receipt_repository_impl_test.dart @@ -52,13 +52,13 @@ void main() { 'should return [Right>] if api calls succeed', () async { // arrange - final tSwipedReceipt = SwipeReceipt( + final testSwipedReceipt = SwipeReceipt( productName: 'productName', timeUsed: DateTime.parse('2023-04-23'), id: 0, ); - final tPurchasedReceipt = PurchaseReceipt( + final testPurchasedReceipt = PurchaseReceipt( productName: 'productName', timeUsed: DateTime.parse('2023-04-24'), // note this is a day later id: 0, @@ -68,12 +68,12 @@ void main() { when(remoteDataSource.getUsersUsedTicketsReceipts()).thenAnswer( (_) async => Right([ - tSwipedReceipt, + testSwipedReceipt, ]), ); when(remoteDataSource.getUserPurchasesReceipts()).thenAnswer( (_) async => Right([ - tPurchasedReceipt, + testPurchasedReceipt, ]), ); @@ -85,8 +85,8 @@ void main() { (response) => expect( response, [ - tPurchasedReceipt, - tSwipedReceipt, + testPurchasedReceipt, + testSwipedReceipt, ], // note that it is sorted from oldest --> newest ), ); diff --git a/test/features/receipt/presentation/cubit/receipt_cubit_test.dart b/test/features/receipt/presentation/cubit/receipt_cubit_test.dart index 7035ffd6e..f807ff4cb 100644 --- a/test/features/receipt/presentation/cubit/receipt_cubit_test.dart +++ b/test/features/receipt/presentation/cubit/receipt_cubit_test.dart @@ -48,7 +48,7 @@ void main() { }); group('filterReceipts', () { - final tReceipts = [ + final testReceipts = [ SwipeReceipt( id: 1, productName: 'Coffee', @@ -71,11 +71,11 @@ void main() { blocTest( 'should emit new state with filter applied', build: () => cubit, - seed: () => ReceiptState(receipts: tReceipts), + seed: () => ReceiptState(receipts: testReceipts), act: (_) => cubit.filterReceipts(ReceiptFilterCategory.purchases), expect: () => [ ReceiptState( - receipts: tReceipts, + receipts: testReceipts, filteredReceipts: const [], filterBy: ReceiptFilterCategory.purchases, ), diff --git a/test/features/ticket/presentation/cubit/tickets_cubit_test.dart b/test/features/ticket/presentation/cubit/tickets_cubit_test.dart index 382ebd0bf..5072d714b 100644 --- a/test/features/ticket/presentation/cubit/tickets_cubit_test.dart +++ b/test/features/ticket/presentation/cubit/tickets_cubit_test.dart @@ -53,14 +53,14 @@ void main() { ); }); group('useTicket', () { - final tReceipt = PlaceholderReceipt(); + final testReceipt = PlaceholderReceipt(); blocTest( 'should not emit new state when state is not [Loaded]', build: () => cubit, setUp: () { when(loadTickets(any)).thenAnswer((_) async => const Right([])); - when(consumeTicket(any)).thenAnswer((_) async => Right(tReceipt)); + when(consumeTicket(any)).thenAnswer((_) async => Right(testReceipt)); }, act: (cubit) => cubit.useTicket(0), expect: () => [], @@ -71,7 +71,7 @@ void main() { build: () => cubit, setUp: () { when(loadTickets(any)).thenAnswer((_) async => const Right([])); - when(consumeTicket(any)).thenAnswer((_) async => Right(tReceipt)); + when(consumeTicket(any)).thenAnswer((_) async => Right(testReceipt)); }, act: (_) async { await cubit.getTickets(); @@ -81,7 +81,7 @@ void main() { skip: 2, expect: () => [ const TicketUsing([]), - TicketUsed(tReceipt, const []), + TicketUsed(testReceipt, const []), const TicketsLoaded([]), ], ); diff --git a/test/features/user/data/datasources/user_remote_data_source_test.dart b/test/features/user/data/datasources/user_remote_data_source_test.dart index e8ce72eff..14fc3a9c3 100644 --- a/test/features/user/data/datasources/user_remote_data_source_test.dart +++ b/test/features/user/data/datasources/user_remote_data_source_test.dart @@ -30,7 +30,7 @@ void main() { ); }); - const tUserModel = UserModel( + const testUserModel = UserModel( id: 0, name: 'name', email: 'email', @@ -84,7 +84,7 @@ void main() { final actual = await dataSource.getUser(); // assert - expect(actual, const Right(tUserModel)); + expect(actual, const Right(testUserModel)); }); }); @@ -126,7 +126,7 @@ void main() { final actual = await dataSource.updateUserDetails(const UpdateUser()); // assert - expect(actual, const Right(tUserModel)); + expect(actual, const Right(testUserModel)); }); }); diff --git a/test/features/user/presentation/cubit/user_cubit_test.dart b/test/features/user/presentation/cubit/user_cubit_test.dart index 9110cb4fd..09ec84eba 100644 --- a/test/features/user/presentation/cubit/user_cubit_test.dart +++ b/test/features/user/presentation/cubit/user_cubit_test.dart @@ -33,7 +33,7 @@ void main() { ); }); - const tUser = User( + const testUser = User( id: 0, name: 'name', email: 'email', @@ -65,12 +65,12 @@ void main() { 'should emit [Loading, Loaded] when use case succeeds', build: () => cubit, setUp: () => when(getUser(any)).thenAnswer( - (_) async => const Right(tUser), + (_) async => const Right(testUser), ), act: (_) => cubit.fetchUserDetails(), expect: () => [ UserLoading(), - UserLoaded(user: tUser), + UserLoaded(user: testUser), ], ); }); @@ -96,7 +96,7 @@ void main() { 'should not update state if state is [Updating]', build: () => cubit, act: (_) => cubit.updateUser(const UpdateUser()), - seed: () => UserUpdating(user: tUser), + seed: () => UserUpdating(user: testUser), expect: () => [], ); @@ -109,9 +109,9 @@ void main() { ), ), act: (_) => cubit.updateUser(const UpdateUser()), - seed: () => UserLoaded(user: tUser), + seed: () => UserLoaded(user: testUser), expect: () => [ - UserUpdating(user: tUser), + UserUpdating(user: testUser), UserError('some error'), ], ); @@ -120,19 +120,19 @@ void main() { 'should emit [Updating, Loaded] if use case succeeds', build: () => cubit, setUp: () => when(updateUserDetails(any)).thenAnswer( - (_) async => const Right(tUser), + (_) async => const Right(testUser), ), act: (_) => cubit.updateUser(const UpdateUser()), - seed: () => UserLoaded(user: tUser), + seed: () => UserLoaded(user: testUser), expect: () => [ - UserUpdating(user: tUser), - UserLoaded(user: tUser), + UserUpdating(user: testUser), + UserLoaded(user: testUser), ], ); }); group('requestUserAccountDeletion', () { - test('shoul call use case', () { + test('should call use case', () { // arrange when(requestAccountDeletion(any)).thenAnswer( (_) async => const Right(null), diff --git a/test/widgets/components/stats/leaderboard_list_entry_test.dart b/test/widgets/components/stats/leaderboard_list_entry_test.dart index cca3a2474..6a19f23b7 100644 --- a/test/widgets/components/stats/leaderboard_list_entry_test.dart +++ b/test/widgets/components/stats/leaderboard_list_entry_test.dart @@ -1,4 +1,4 @@ -import 'package:coffeecard/widgets/components/stats/leaderboard_list_entry.dart'; +import 'package:coffeecard/features/leaderboard/presentation/widgets/leaderboard_list_entry.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test/widgets/components/stats/stat_card_test.dart b/test/widgets/components/stats/stat_card_test.dart index 833ccd7cd..dc2e7b72b 100644 --- a/test/widgets/components/stats/stat_card_test.dart +++ b/test/widgets/components/stats/stat_card_test.dart @@ -1,4 +1,4 @@ -import 'package:coffeecard/widgets/components/stats/stat_card.dart'; +import 'package:coffeecard/features/leaderboard/presentation/widgets/statistics_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; From dbbec71922e12fd5ba9351f8d14761c2997d54d5 Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Fri, 19 May 2023 14:01:22 +0200 Subject: [PATCH 09/32] Migrate to Dart 3 and Flutter 3.10.0 (#452) * Bump packages and versions * Upgrade pods * Update Kotlin Gradle plugin * Update workflow commands to use `dart run` instead of `flutter pub run` among others * Update goldens Dart 3 changes: * Add class modifiers (sealed, final, interface) * Convert most switch statements to expressions * Improve pattern matching on class type on sealed classes Misc changes: * Fix Receipt typos * Improve code structure of ReceiptsListView * Fix whitespace and unused rule ignores --------- Co-authored-by: Thomas Andersen --- .github/workflows/build.yml | 12 +- README.md | 10 +- analysis_options.yaml | 11 +- android/build.gradle | 2 +- ios/Podfile | 1 - ios/Podfile.lock | 164 +++--- ios/Runner.xcodeproj/project.pbxproj | 1 + lib/base/strings.dart | 2 +- lib/base/strings_environment.dart | 2 +- lib/base/style/colors.dart | 2 +- lib/base/style/text_styles.dart | 8 +- lib/core/errors/failures.dart | 12 +- lib/core/usecases/usecase.dart | 4 +- lib/cubits/environment/environment_state.dart | 2 +- lib/cubits/form/form_event.dart | 2 +- lib/cubits/login/login_state.dart | 2 +- lib/cubits/products/products_state.dart | 2 +- lib/cubits/purchase/purchase_state.dart | 2 +- lib/cubits/register/register_state.dart | 2 +- lib/cubits/voucher/voucher_state.dart | 2 +- .../v2/app_config_repository.dart | 17 +- lib/env/env.dart | 2 +- .../presentation/cubit/contributor_state.dart | 2 +- .../widgets/contributor_card.dart | 2 +- .../leaderboard_remote_data_source.dart | 15 +- .../presentation/cubit/leaderboard_state.dart | 2 +- .../presentation/widgets/statistics_card.dart | 16 +- .../presentation/cubit/occupation_state.dart | 2 +- .../opening_hours_repository_impl.dart | 2 +- .../opening_hours_repository.dart | 2 +- .../cubit/opening_hours_state.dart | 2 +- .../pages/opening_hours_page.dart | 2 +- .../data/models/purchase_receipt_model.dart | 2 +- .../data/models/swipe_receipt_model.dart | 2 +- .../domain/entities/placeholder_receipt.dart | 3 +- .../domain/entities/purchase_receipt.dart | 2 +- .../receipt/domain/entities/receipt.dart | 7 +- .../domain/entities/swipe_receipt.dart | 2 +- .../repositories/receipt_repository.dart | 2 +- .../presentation/cubit/receipt_cubit.dart | 17 +- .../presentation/cubit/receipt_state.dart | 15 +- .../purchase_receipt_list_entry.dart | 2 +- .../list_entry/receipt_list_entry.dart | 6 +- .../list_entry/swipe_receipt_list_entry.dart | 2 +- .../presentation/widgets/receipt_card.dart | 2 +- .../widgets/receipt_list_entry_factory.dart | 25 +- .../widgets/receipts_list_view.dart | 89 ++-- .../presentation/cubit/tickets_state.dart | 2 +- .../presentation/pages/tickets_page.dart | 12 +- .../presentation/widgets/tickets_section.dart | 2 +- lib/features/user/domain/entities/role.dart | 31 +- .../user/presentation/cubit/user_state.dart | 4 +- lib/firebase_options.dart | 4 +- lib/models/purchase/payment_status.dart | 19 +- lib/payment/payment_handler.dart | 17 +- lib/service_locator.dart | 39 +- lib/utils/analog_icons.dart | 5 +- lib/utils/ignore_value.dart | 8 + .../helpers/lazy_indexed_stack.dart | 2 + .../components/images/analog_logo.dart | 2 +- .../components/purchase/purchase_process.dart | 4 +- lib/widgets/components/user/user_card.dart | 2 +- .../pages/settings/your_profile_page.dart | 4 +- makefile | 14 +- pubspec.lock | 488 ++++++++---------- pubspec.yaml | 60 +-- .../receipt_repository_impl_test.dart | 3 +- .../cubit/receipt_cubit_test.dart | 2 +- .../cubit/tickets_cubit_test.dart | 2 +- .../components/goldens/error_section.png | Bin 4873 -> 4848 bytes .../goldens/setting_list_entry_truncated.png | Bin 3503 -> 3498 bytes .../goldens/settings_list_entry_normal.png | Bin 3436 -> 3420 bytes .../stats/goldens/leaderboard_list_entry.png | Bin 4896 -> 4882 bytes .../leaderboard_list_entry_highlighted.png | Bin 4846 -> 4827 bytes .../components/stats/goldens/stat_card.png | Bin 3490 -> 3490 bytes .../tickets/goldens/buy_tickets_card.png | Bin 4879 -> 4855 bytes 76 files changed, 585 insertions(+), 630 deletions(-) create mode 100644 lib/utils/ignore_value.dart diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index de21df0ee..5f207cd31 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ on: value: ${{ jobs.version.outputs.version_tag }} env: - FLUTTER_VERSION: 3.7.8 + FLUTTER_VERSION: 3.10.0 JAVA_VERSION: 11.x jobs: @@ -109,7 +109,7 @@ jobs: run: sed -i '' 's/.env.develop/.env.production/' lib/env/env.dart - name: Generate code - run: flutter pub run build_runner build + run: dart run build_runner build - name: Build iOS (dev) if: github.ref_name != 'production' @@ -176,7 +176,7 @@ jobs: run: sed -i 's/.env.develop/.env.production/' lib/env/env.dart - name: Generate code - run: flutter pub run build_runner build + run: dart run build_runner build - name: Build appbundle (dev) if: github.ref_name != 'production' @@ -211,16 +211,16 @@ jobs: run: flutter pub get - name: Generate code - run: flutter pub run build_runner build + run: dart run build_runner build - name: Check formatting - run: flutter format --set-exit-if-changed . + run: dart format --set-exit-if-changed . - name: Static Analysis run: flutter analyze - name: Run Code Metrics - run: flutter pub run dart_code_metrics:metrics --reporter=github lib + run: dart run dart_code_metrics:metrics --reporter=github lib - name: Run tests run: flutter test --coverage diff --git a/README.md b/README.md index e215e6392..c4597bafc 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ allows users to buy and use clip cards in Cafe Analog @ IT University of Copenha We are building the Flutter app with these SDK versions -| SDK | Version | -| ------- | ------- | -| Dart | >=2.18.0 <3.0.0 | -| Flutter | 3.7.8 | +| SDK | Version | +| ------- | -------------- | +| Dart | >=3.0.0 <4.0.0 | +| Flutter | 3.10.0 | ## Relevant READMEs @@ -26,4 +26,4 @@ We are building the Flutter app with these SDK versions This project relies on autogenerated files for environment configuration and testing. To generate these files run `make generate`. -A **.env.develop** should be present in the root directory before running code generation. This file should contain the URI of the backend API in the format `coffeeCardUrl="https://the-url"`. \ No newline at end of file +A **.env.develop** should be present in the root directory before running code generation. This file should contain the URI of the backend API in the format `coffeeCardUrl="https://the-url"`. diff --git a/analysis_options.yaml b/analysis_options.yaml index 394c7758e..9b35447c0 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -12,8 +12,6 @@ linter: sort_pub_dependencies: false analyzer: - strong-mode: - implicit-casts: false exclude: - lib/generated/** - test/**.mocks.dart @@ -26,25 +24,24 @@ analyzer: fixme: warning plugins: - dart_code_metrics + language: + strict-casts: true dart_code_metrics: extends: - package:dart_code_metrics_presets/all.yaml metrics: - cyclomatic-complexity: 20 + cyclomatic-complexity: 15 number-of-parameters: 6 maximum-nesting-level: 4 rules: + - list-all-equatable-fields: true - avoid-ignoring-return-values: exclude: - test/** - - no-empty-block: exclude: - test/** - - - list-all-equatable-fields - - arguments-ordering: false - format-comment: false - member-ordering: false diff --git a/android/build.gradle b/android/build.gradle index 0ff3f4fc4..9b38488bb 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -1,5 +1,5 @@ buildscript { - ext.kotlin_version = '1.6.0' + ext.kotlin_version = '1.8.21' repositories { google() jcenter() diff --git a/ios/Podfile b/ios/Podfile index c2f708945..010b0c3ea 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -1,4 +1,3 @@ -# Uncomment this line to define a global platform for your project platform :ios, '12.0' # CocoaPods analytics sends network stats synchronously affecting flutter build latency. diff --git a/ios/Podfile.lock b/ios/Podfile.lock index d22a85b6c..e21e321d8 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -1,38 +1,38 @@ PODS: - device_info_plus (0.0.1): - Flutter - - Firebase/Analytics (10.3.0): + - Firebase/Analytics (10.7.0): - Firebase/Core - - Firebase/Core (10.3.0): + - Firebase/Core (10.7.0): - Firebase/CoreOnly - - FirebaseAnalytics (~> 10.3.0) - - Firebase/CoreOnly (10.3.0): - - FirebaseCore (= 10.3.0) - - Firebase/Crashlytics (10.3.0): + - FirebaseAnalytics (~> 10.7.0) + - Firebase/CoreOnly (10.7.0): + - FirebaseCore (= 10.7.0) + - Firebase/Crashlytics (10.7.0): - Firebase/CoreOnly - - FirebaseCrashlytics (~> 10.3.0) - - Firebase/Performance (10.3.0): + - FirebaseCrashlytics (~> 10.7.0) + - Firebase/Performance (10.7.0): - Firebase/CoreOnly - - FirebasePerformance (~> 10.3.0) - - firebase_analytics (10.1.4): - - Firebase/Analytics (= 10.3.0) + - FirebasePerformance (~> 10.7.0) + - firebase_analytics (10.4.0): + - Firebase/Analytics (= 10.7.0) - firebase_core - Flutter - - firebase_core (2.7.0): - - Firebase/CoreOnly (= 10.3.0) + - firebase_core (2.12.0): + - Firebase/CoreOnly (= 10.7.0) - Flutter - - firebase_crashlytics (3.0.15): - - Firebase/Crashlytics (= 10.3.0) + - firebase_crashlytics (3.3.0): + - Firebase/Crashlytics (= 10.7.0) - firebase_core - Flutter - - firebase_performance (0.9.0-14): - - Firebase/Performance (= 10.3.0) + - firebase_performance (0.9.2): + - Firebase/Performance (= 10.7.0) - firebase_core - Flutter - - FirebaseABTesting (10.6.0): + - FirebaseABTesting (10.9.0): - FirebaseCore (~> 10.0) - - FirebaseAnalytics (10.3.0): - - FirebaseAnalytics/AdIdSupport (= 10.3.0) + - FirebaseAnalytics (10.7.0): + - FirebaseAnalytics/AdIdSupport (= 10.7.0) - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.8) @@ -40,48 +40,60 @@ PODS: - GoogleUtilities/Network (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseAnalytics/AdIdSupport (10.3.0): + - FirebaseAnalytics/AdIdSupport (10.7.0): - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - - GoogleAppMeasurement (= 10.3.0) + - GoogleAppMeasurement (= 10.7.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - GoogleUtilities/MethodSwizzler (~> 7.8) - GoogleUtilities/Network (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseCore (10.3.0): + - FirebaseCore (10.7.0): - FirebaseCoreInternal (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/Logger (~> 7.8) - - FirebaseCoreInternal (10.6.0): - - "GoogleUtilities/NSData+zlib (~> 7.8)" - - FirebaseCrashlytics (10.3.0): + - FirebaseCoreExtension (10.9.0): - FirebaseCore (~> 10.0) + - FirebaseCoreInternal (10.9.0): + - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseCrashlytics (10.7.0): + - FirebaseCore (~> 10.5) - FirebaseInstallations (~> 10.0) + - FirebaseSessions (~> 10.5) - GoogleDataTransport (~> 9.2) - GoogleUtilities/Environment (~> 7.8) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (~> 2.1) - - FirebaseInstallations (10.6.0): + - FirebaseInstallations (10.9.0): - FirebaseCore (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/UserDefaults (~> 7.8) - PromisesObjC (~> 2.1) - - FirebasePerformance (10.3.0): - - FirebaseCore (~> 10.0) + - FirebasePerformance (10.7.0): + - FirebaseCore (~> 10.5) - FirebaseInstallations (~> 10.0) - FirebaseRemoteConfig (~> 10.0) + - FirebaseSessions (~> 10.5) - GoogleDataTransport (~> 9.2) - GoogleUtilities/Environment (~> 7.8) - GoogleUtilities/ISASwizzler (~> 7.8) - GoogleUtilities/MethodSwizzler (~> 7.8) - nanopb (< 2.30910.0, >= 2.30908.0) - - FirebaseRemoteConfig (10.6.0): + - FirebaseRemoteConfig (10.9.0): - FirebaseABTesting (~> 10.0) - FirebaseCore (~> 10.0) - FirebaseInstallations (~> 10.0) - GoogleUtilities/Environment (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" + - FirebaseSessions (10.9.0): + - FirebaseCore (~> 10.5) + - FirebaseCoreExtension (~> 10.0) + - FirebaseInstallations (~> 10.0) + - GoogleDataTransport (~> 9.2) + - GoogleUtilities/Environment (~> 7.10) + - nanopb (< 2.30910.0, >= 2.30908.0) + - PromisesSwift (~> 2.1) - Flutter (1.0.0) - flutter_native_splash (0.0.1): - Flutter @@ -90,49 +102,49 @@ PODS: - FMDB (2.7.5): - FMDB/standard (= 2.7.5) - FMDB/standard (2.7.5) - - GoogleAppMeasurement (10.3.0): - - GoogleAppMeasurement/AdIdSupport (= 10.3.0) + - GoogleAppMeasurement (10.7.0): + - GoogleAppMeasurement/AdIdSupport (= 10.7.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - GoogleUtilities/MethodSwizzler (~> 7.8) - GoogleUtilities/Network (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleAppMeasurement/AdIdSupport (10.3.0): - - GoogleAppMeasurement/WithoutAdIdSupport (= 10.3.0) + - GoogleAppMeasurement/AdIdSupport (10.7.0): + - GoogleAppMeasurement/WithoutAdIdSupport (= 10.7.0) - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - GoogleUtilities/MethodSwizzler (~> 7.8) - GoogleUtilities/Network (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleAppMeasurement/WithoutAdIdSupport (10.3.0): + - GoogleAppMeasurement/WithoutAdIdSupport (10.7.0): - GoogleUtilities/AppDelegateSwizzler (~> 7.8) - GoogleUtilities/MethodSwizzler (~> 7.8) - GoogleUtilities/Network (~> 7.8) - "GoogleUtilities/NSData+zlib (~> 7.8)" - nanopb (< 2.30910.0, >= 2.30908.0) - - GoogleDataTransport (9.2.1): + - GoogleDataTransport (9.2.3): - GoogleUtilities/Environment (~> 7.7) - nanopb (< 2.30910.0, >= 2.30908.0) - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/AppDelegateSwizzler (7.11.0): + - GoogleUtilities/AppDelegateSwizzler (7.11.1): - GoogleUtilities/Environment - GoogleUtilities/Logger - GoogleUtilities/Network - - GoogleUtilities/Environment (7.11.0): + - GoogleUtilities/Environment (7.11.1): - PromisesObjC (< 3.0, >= 1.2) - - GoogleUtilities/ISASwizzler (7.11.0) - - GoogleUtilities/Logger (7.11.0): + - GoogleUtilities/ISASwizzler (7.11.1) + - GoogleUtilities/Logger (7.11.1): - GoogleUtilities/Environment - - GoogleUtilities/MethodSwizzler (7.11.0): + - GoogleUtilities/MethodSwizzler (7.11.1): - GoogleUtilities/Logger - - GoogleUtilities/Network (7.11.0): + - GoogleUtilities/Network (7.11.1): - GoogleUtilities/Logger - "GoogleUtilities/NSData+zlib" - GoogleUtilities/Reachability - - "GoogleUtilities/NSData+zlib (7.11.0)" - - GoogleUtilities/Reachability (7.11.0): + - "GoogleUtilities/NSData+zlib (7.11.1)" + - GoogleUtilities/Reachability (7.11.1): - GoogleUtilities/Logger - - GoogleUtilities/UserDefaults (7.11.0): + - GoogleUtilities/UserDefaults (7.11.1): - GoogleUtilities/Logger - nanopb (2.30909.0): - nanopb/decode (= 2.30909.0) @@ -145,12 +157,14 @@ PODS: - Flutter - FlutterMacOS - PromisesObjC (2.2.0) + - PromisesSwift (2.2.0): + - PromisesObjC (= 2.2.0) - screen_brightness_ios (0.1.0): - Flutter - shared_preferences_foundation (0.0.1): - Flutter - FlutterMacOS - - sqflite (0.0.2): + - sqflite (0.0.3): - Flutter - FMDB (>= 2.7.5) - url_launcher_ios (0.0.1): @@ -166,9 +180,9 @@ DEPENDENCIES: - flutter_native_splash (from `.symlinks/plugins/flutter_native_splash/ios`) - flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`) - package_info_plus (from `.symlinks/plugins/package_info_plus/ios`) - - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/ios`) + - path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`) - screen_brightness_ios (from `.symlinks/plugins/screen_brightness_ios/ios`) - - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/ios`) + - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) - sqflite (from `.symlinks/plugins/sqflite/ios`) - url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`) @@ -178,17 +192,20 @@ SPEC REPOS: - FirebaseABTesting - FirebaseAnalytics - FirebaseCore + - FirebaseCoreExtension - FirebaseCoreInternal - FirebaseCrashlytics - FirebaseInstallations - FirebasePerformance - FirebaseRemoteConfig + - FirebaseSessions - FMDB - GoogleAppMeasurement - GoogleDataTransport - GoogleUtilities - nanopb - PromisesObjC + - PromisesSwift EXTERNAL SOURCES: device_info_plus: @@ -210,47 +227,50 @@ EXTERNAL SOURCES: package_info_plus: :path: ".symlinks/plugins/package_info_plus/ios" path_provider_foundation: - :path: ".symlinks/plugins/path_provider_foundation/ios" + :path: ".symlinks/plugins/path_provider_foundation/darwin" screen_brightness_ios: :path: ".symlinks/plugins/screen_brightness_ios/ios" shared_preferences_foundation: - :path: ".symlinks/plugins/shared_preferences_foundation/ios" + :path: ".symlinks/plugins/shared_preferences_foundation/darwin" sqflite: :path: ".symlinks/plugins/sqflite/ios" url_launcher_ios: :path: ".symlinks/plugins/url_launcher_ios/ios" SPEC CHECKSUMS: - device_info_plus: e5c5da33f982a436e103237c0c85f9031142abed - Firebase: f92fc551ead69c94168d36c2b26188263860acd9 - firebase_analytics: 4f57c422c9c6225cd5ddd5fca28f92bde5fb21b5 - firebase_core: 128d8c43c3a453a4a67463314fc3761bedff860b - firebase_crashlytics: 0a99d06e38030d769021340b551d8ac4b27345fc - firebase_performance: 3c1458dca5341be250ed91b704c9e424690ca46e - FirebaseABTesting: cec558d2e670053b1e862c98cfecae64aecfd629 - FirebaseAnalytics: 036232b6a1e2918e5f67572417be1173576245f3 - FirebaseCore: 988754646ab3bd4bdcb740f1bfe26b9f6c0d5f2a - FirebaseCoreInternal: c7cd505e2136811096b225ac388d6254a2622362 - FirebaseCrashlytics: f20d956f8229010b645e534693c39e0b7843c268 - FirebaseInstallations: 13dde135fa0524e15bddb133ccc8465c53a1b3f3 - FirebasePerformance: 8f1c8e5a4fcc5a68400835518ee63a6d63dbff0c - FirebaseRemoteConfig: 7ef4dd164f3066c64ca1efab0f990152a6b94fc0 + device_info_plus: 7545d84d8d1b896cb16a4ff98c19f07ec4b298ea + Firebase: 0219acf760880eeec8ce479895bd7767466d9f81 + firebase_analytics: 843839cfcab326b37cca084009045a8fc287ef74 + firebase_core: 312d0d81b346ec20540822c8498e626d6918ef48 + firebase_crashlytics: 869c43dd16f650004bb312ff9eff8d068406b939 + firebase_performance: 2a0f7dcea97db730990e16a1e94eecc3280160a8 + FirebaseABTesting: 005b70969e2817e2a1e631e8dba29134a04c0622 + FirebaseAnalytics: f8133442ee6f8512e28ff19e62ce15398bfaeace + FirebaseCore: e317665b9d744727a97e623edbbed009320afdd7 + FirebaseCoreExtension: d3e9bba2930a8033042112397cd9f006a1bb203d + FirebaseCoreInternal: d2b4acb827908e72eca47a9fd896767c3053921e + FirebaseCrashlytics: 35fdd1a433b31e28adcf5c8933f4c526691a1e0b + FirebaseInstallations: c58489c9caacdbf27d1da60891a87318e20218e0 + FirebasePerformance: 8281bbaf08aad194001018b932115b7d58a6f00b + FirebaseRemoteConfig: 5ea5834e8c518f377bf1af2d97ebd611914ebf2d + FirebaseSessions: 44a6782502eb279a214d4adca20891353278760c Flutter: f04841e97a9d0b0a8025694d0796dd46242b2854 flutter_native_splash: 52501b97d1c0a5f898d687f1646226c1f93c56ef flutter_secure_storage: 23fc622d89d073675f2eaa109381aefbcf5a49be FMDB: 2ce00b547f966261cd18927a3ddb07cb6f3db82a - GoogleAppMeasurement: c7d6fff39bf2d829587d74088d582e32d75133c3 - GoogleDataTransport: ea169759df570f4e37bdee1623ec32a7e64e67c4 - GoogleUtilities: c2bdc4cf2ce786c4d2e6b3bcfd599a25ca78f06f + GoogleAppMeasurement: fe17c92a32207dd5cdd4e8d742767f2da74857f6 + GoogleDataTransport: f0308f5905a745f94fb91fea9c6cbaf3831cb1bd + GoogleUtilities: 9aa0ad5a7bc171f8bae016300bfcfa3fb8425749 nanopb: b552cce312b6c8484180ef47159bc0f65a1f0431 - package_info_plus: 6c92f08e1f853dc01228d6f553146438dafcd14e - path_provider_foundation: c68054786f1b4f3343858c1e1d0caaded73f0be9 + package_info_plus: fd030dabf36271f146f1f3beacd48f564b0f17f7 + path_provider_foundation: eaf5b3e458fc0e5fbb9940fb09980e853fe058b8 PromisesObjC: 09985d6d70fbe7878040aa746d78236e6946d2ef + PromisesSwift: cf9eb58666a43bbe007302226e510b16c1e10959 screen_brightness_ios: 715ca807df953bf676d339f11464e438143ee625 - shared_preferences_foundation: 986fc17f3d3251412d18b0265f9c64113a8c2472 - sqflite: 6d358c025f5b867b29ed92fc697fd34924e11904 + shared_preferences_foundation: e2dae3258e06f44cc55f49d42024fd8dd03c590c + sqflite: 31f7eba61e3074736dff8807a9b41581e4f7f15a url_launcher_ios: 08a3dfac5fb39e8759aeb0abbd5d9480f30fc8b4 -PODFILE CHECKSUM: 1fad2ee47de2c4ba65439e1c912e36e1883244af +PODFILE CHECKSUM: 8143e5b5aaa759b3655ac1120358665a1a0c2811 COCOAPODS: 1.11.3 diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index d8238d18e..80ee0e73f 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -270,6 +270,7 @@ files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( diff --git a/lib/base/strings.dart b/lib/base/strings.dart index 53a3cab53..e78158b60 100644 --- a/lib/base/strings.dart +++ b/lib/base/strings.dart @@ -1,4 +1,4 @@ -abstract class Strings { +abstract final class Strings { static const appTitle = 'Cafe Analog'; // Titles for the app bar. diff --git a/lib/base/strings_environment.dart b/lib/base/strings_environment.dart index f78582c64..3fd613448 100644 --- a/lib/base/strings_environment.dart +++ b/lib/base/strings_environment.dart @@ -1,4 +1,4 @@ -abstract class TestEnvironmentStrings { +abstract final class TestEnvironmentStrings { static const title = 'Connected to test environment'; static const description = [ 'The functionality of this app is for testing purposes only.', diff --git a/lib/base/style/colors.dart b/lib/base/style/colors.dart index 2939dc2c9..5947950ee 100644 --- a/lib/base/style/colors.dart +++ b/lib/base/style/colors.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -abstract class AppColor { +abstract final class AppColor { static const primary = Color(0xff362619); static const secondary = Color(0xff785B38); static const background = Color(0xffE5E2D7); diff --git a/lib/base/style/text_styles.dart b/lib/base/style/text_styles.dart index cab1243e5..28e796a17 100644 --- a/lib/base/style/text_styles.dart +++ b/lib/base/style/text_styles.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_style_builder.dart'; import 'package:flutter/material.dart'; -abstract class AppTextStyle { +abstract final class AppTextStyle { static final _heading = TextStyleBuilder.heading; static final _body = TextStyleBuilder.body; static final _mono = TextStyleBuilder.mono; @@ -43,10 +43,10 @@ abstract class AppTextStyle { static final settingValue = settingKey.copyWith(color: AppColor.secondary); - static final recieptItemKey = + static final receiptItemKey = _body.size(14).color(AppColor.primary).bold().style; - static final recieptItemValue = settingKey; + static final receiptItemValue = settingKey; static final loginExplainer = settingKey.copyWith(color: AppColor.white); @@ -105,7 +105,7 @@ abstract class AppTextStyle { static final leaderboardScore = _mono.size(14).color(AppColor.primary).style; - static final recieptItemDate = _mono.size(12).color(AppColor.secondary).style; + static final receiptItemDate = _mono.size(12).color(AppColor.secondary).style; static final rankingNumber = _mono.size(12).color(AppColor.primary).bold().style; diff --git a/lib/core/errors/failures.dart b/lib/core/errors/failures.dart index 81ce26632..c73fcb885 100644 --- a/lib/core/errors/failures.dart +++ b/lib/core/errors/failures.dart @@ -4,7 +4,7 @@ import 'package:chopper/chopper.dart'; import 'package:coffeecard/base/strings.dart'; import 'package:equatable/equatable.dart'; -abstract class Failure extends Equatable { +sealed class Failure extends Equatable { final String reason; const Failure(this.reason); @@ -13,7 +13,11 @@ abstract class Failure extends Equatable { List get props => [reason]; } -abstract class NetworkFailure extends Failure { +class LocalStorageFailure extends Failure { + const LocalStorageFailure(super.reason); +} + +sealed class NetworkFailure extends Failure { const NetworkFailure(super.reason); } @@ -41,7 +45,3 @@ class ServerFailure extends NetworkFailure { class ConnectionFailure extends NetworkFailure { const ConnectionFailure() : super('connection refused'); } - -class LocalStorageFailure extends Failure { - const LocalStorageFailure(super.reason); -} diff --git a/lib/core/usecases/usecase.dart b/lib/core/usecases/usecase.dart index ebd097b34..c3ca921db 100644 --- a/lib/core/usecases/usecase.dart +++ b/lib/core/usecases/usecase.dart @@ -2,11 +2,11 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:equatable/equatable.dart'; import 'package:fpdart/fpdart.dart'; -abstract class UseCase { +abstract interface class UseCase { Future> call(Params params); } -class NoParams extends Equatable { +final class NoParams extends Equatable { @override List get props => []; } diff --git a/lib/cubits/environment/environment_state.dart b/lib/cubits/environment/environment_state.dart index d1547631f..76ce03750 100644 --- a/lib/cubits/environment/environment_state.dart +++ b/lib/cubits/environment/environment_state.dart @@ -1,6 +1,6 @@ part of 'environment_cubit.dart'; -abstract class EnvironmentState extends Equatable { +sealed class EnvironmentState extends Equatable { const EnvironmentState(); } diff --git a/lib/cubits/form/form_event.dart b/lib/cubits/form/form_event.dart index 682843cbb..31edc58f0 100644 --- a/lib/cubits/form/form_event.dart +++ b/lib/cubits/form/form_event.dart @@ -1,6 +1,6 @@ part of 'form_bloc.dart'; -abstract class FormEvent extends Equatable {} +sealed class FormEvent extends Equatable {} /// The form wants to validate itself and should show a loading indicator. class FormValidateRequested extends FormEvent { diff --git a/lib/cubits/login/login_state.dart b/lib/cubits/login/login_state.dart index 3a821ed81..88a0cbdfb 100644 --- a/lib/cubits/login/login_state.dart +++ b/lib/cubits/login/login_state.dart @@ -1,6 +1,6 @@ part of 'login_cubit.dart'; -abstract class LoginState extends Equatable { +sealed class LoginState extends Equatable { const LoginState(); @override diff --git a/lib/cubits/products/products_state.dart b/lib/cubits/products/products_state.dart index fe9790c2e..c3a88ff71 100644 --- a/lib/cubits/products/products_state.dart +++ b/lib/cubits/products/products_state.dart @@ -1,6 +1,6 @@ part of 'products_cubit.dart'; -abstract class ProductsState extends Equatable { +sealed class ProductsState extends Equatable { const ProductsState(); } diff --git a/lib/cubits/purchase/purchase_state.dart b/lib/cubits/purchase/purchase_state.dart index 52ab329c1..b5a85396a 100644 --- a/lib/cubits/purchase/purchase_state.dart +++ b/lib/cubits/purchase/purchase_state.dart @@ -1,6 +1,6 @@ part of 'purchase_cubit.dart'; -abstract class PurchaseState extends Equatable { +sealed class PurchaseState extends Equatable { const PurchaseState(); } diff --git a/lib/cubits/register/register_state.dart b/lib/cubits/register/register_state.dart index ce35e8a62..924c419ee 100644 --- a/lib/cubits/register/register_state.dart +++ b/lib/cubits/register/register_state.dart @@ -1,6 +1,6 @@ part of 'register_cubit.dart'; -abstract class RegisterState {} +sealed class RegisterState {} class RegisterInitial extends RegisterState {} diff --git a/lib/cubits/voucher/voucher_state.dart b/lib/cubits/voucher/voucher_state.dart index 0c9524ec9..cf2cc2487 100644 --- a/lib/cubits/voucher/voucher_state.dart +++ b/lib/cubits/voucher/voucher_state.dart @@ -1,6 +1,6 @@ part of 'voucher_cubit.dart'; -abstract class VoucherState extends Equatable { +sealed class VoucherState extends Equatable { const VoucherState(); @override diff --git a/lib/data/repositories/v2/app_config_repository.dart b/lib/data/repositories/v2/app_config_repository.dart index bc84944b0..0901677b4 100644 --- a/lib/data/repositories/v2/app_config_repository.dart +++ b/lib/data/repositories/v2/app_config_repository.dart @@ -14,16 +14,13 @@ class AppConfigRepository { final NetworkRequestExecutor executor; Environment _onSuccessfulRequest(AppConfig dto) { - switch (environmentTypeFromJson(dto.environmentType as String)) { - case EnvironmentType.production: - return Environment.production; - // both test and localdevelopment are treated as test - case EnvironmentType.test: - case EnvironmentType.localdevelopment: - return Environment.test; - case EnvironmentType.swaggerGeneratedUnknown: - return Environment.unknown; - } + return switch (environmentTypeFromJson(dto.environmentType as String)) { + EnvironmentType.production => Environment.production, + EnvironmentType.test || + EnvironmentType.localdevelopment => + Environment.test, + EnvironmentType.swaggerGeneratedUnknown => Environment.unknown, + }; } Future> getEnvironmentType() async { diff --git a/lib/env/env.dart b/lib/env/env.dart index b4e0ed58d..3e6b60fc7 100644 --- a/lib/env/env.dart +++ b/lib/env/env.dart @@ -3,7 +3,7 @@ import 'package:envied/envied.dart'; part 'env.g.dart'; @Envied(path: '.env.develop') -abstract class Env { +abstract final class Env { @EnviedField(varName: 'coffeeCardUrl') static const coffeeCardUrl = _Env.coffeeCardUrl; } diff --git a/lib/features/contributor/presentation/cubit/contributor_state.dart b/lib/features/contributor/presentation/cubit/contributor_state.dart index 1326df6fa..6190d5e72 100644 --- a/lib/features/contributor/presentation/cubit/contributor_state.dart +++ b/lib/features/contributor/presentation/cubit/contributor_state.dart @@ -1,6 +1,6 @@ part of 'contributor_cubit.dart'; -abstract class ContributorState extends Equatable { +sealed class ContributorState extends Equatable { const ContributorState(); } diff --git a/lib/features/contributor/presentation/widgets/contributor_card.dart b/lib/features/contributor/presentation/widgets/contributor_card.dart index f14a1f6f8..7b80dbd69 100644 --- a/lib/features/contributor/presentation/widgets/contributor_card.dart +++ b/lib/features/contributor/presentation/widgets/contributor_card.dart @@ -58,7 +58,7 @@ class ContributorCard extends StatelessWidget { contributor.name, maxLines: 1, overflow: TextOverflow.ellipsis, - style: AppTextStyle.recieptItemKey, + style: AppTextStyle.receiptItemKey, ), const Gap(3), Text( diff --git a/lib/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart b/lib/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart index 34576d7f9..71a1f42b8 100644 --- a/lib/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart +++ b/lib/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart @@ -8,16 +8,11 @@ import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:fpdart/fpdart.dart'; extension _FilterCategoryToPresetString on LeaderboardFilter { - String get label { - switch (this) { - case LeaderboardFilter.semester: - return 'Semester'; - case LeaderboardFilter.month: - return 'Month'; - case LeaderboardFilter.total: - return 'Total'; - } - } + String get label => switch (this) { + LeaderboardFilter.semester => 'Semester', + LeaderboardFilter.month => 'Month', + LeaderboardFilter.total => 'Total', + }; } class LeaderboardRemoteDataSource { diff --git a/lib/features/leaderboard/presentation/cubit/leaderboard_state.dart b/lib/features/leaderboard/presentation/cubit/leaderboard_state.dart index 995f94082..695f8c8a3 100644 --- a/lib/features/leaderboard/presentation/cubit/leaderboard_state.dart +++ b/lib/features/leaderboard/presentation/cubit/leaderboard_state.dart @@ -2,7 +2,7 @@ part of 'leaderboard_cubit.dart'; enum LeaderboardFilter { semester, month, total } -abstract class LeaderboardState extends Equatable { +sealed class LeaderboardState extends Equatable { const LeaderboardState({required this.filter}); final LeaderboardFilter filter; diff --git a/lib/features/leaderboard/presentation/widgets/statistics_card.dart b/lib/features/leaderboard/presentation/widgets/statistics_card.dart index d210181ce..bd8d6d5f2 100644 --- a/lib/features/leaderboard/presentation/widgets/statistics_card.dart +++ b/lib/features/leaderboard/presentation/widgets/statistics_card.dart @@ -60,14 +60,10 @@ String formatLeaderboardPostfix(int rank) { if (rank > 10 && rank < 20) return def; - switch (rank % 10) { - case 1: - return 'st'; - case 2: - return 'nd'; - case 3: - return 'rd'; - default: - return def; - } + return switch (rank % 10) { + 1 => 'st', + 2 => 'nd', + 3 => 'rd', + _ => def, + }; } diff --git a/lib/features/occupation/presentation/cubit/occupation_state.dart b/lib/features/occupation/presentation/cubit/occupation_state.dart index e77e3e704..13be1381c 100644 --- a/lib/features/occupation/presentation/cubit/occupation_state.dart +++ b/lib/features/occupation/presentation/cubit/occupation_state.dart @@ -1,6 +1,6 @@ part of 'occupation_cubit.dart'; -abstract class OccupationState extends Equatable { +sealed class OccupationState extends Equatable { const OccupationState(); } diff --git a/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart b/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart index ab1f774fa..757281fca 100644 --- a/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart +++ b/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart @@ -49,7 +49,7 @@ class OpeningHoursRepositoryImpl implements OpeningHoursRepository { // capitalize the closed string final closedString = // Closed string is const, and does not contain an emoji - //ignore: avoid-substring + // ignore: avoid-substring Strings.closed[0].toUpperCase() + Strings.closed.substring(1); return shiftsByWeekday.map( diff --git a/lib/features/opening_hours/domain/repositories/opening_hours_repository.dart b/lib/features/opening_hours/domain/repositories/opening_hours_repository.dart index e2cf4f9cb..acb4b2aca 100644 --- a/lib/features/opening_hours/domain/repositories/opening_hours_repository.dart +++ b/lib/features/opening_hours/domain/repositories/opening_hours_repository.dart @@ -2,6 +2,6 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours.dart'; import 'package:fpdart/fpdart.dart'; -abstract class OpeningHoursRepository { +abstract interface class OpeningHoursRepository { Future> getOpeningHours(int weekday); } diff --git a/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart b/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart index 69b36b340..a0342c846 100644 --- a/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart +++ b/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart @@ -1,6 +1,6 @@ part of 'opening_hours_cubit.dart'; -abstract class OpeningHoursState extends Equatable { +sealed class OpeningHoursState extends Equatable { const OpeningHoursState(); } diff --git a/lib/features/opening_hours/presentation/pages/opening_hours_page.dart b/lib/features/opening_hours/presentation/pages/opening_hours_page.dart index 0ca508b1c..ff10632c8 100644 --- a/lib/features/opening_hours/presentation/pages/opening_hours_page.dart +++ b/lib/features/opening_hours/presentation/pages/opening_hours_page.dart @@ -84,7 +84,7 @@ class _OpeningHoursView extends StatelessWidget { Strings.weekdaysPlural[weekday]!, style: AppTextStyle.settingKey, ), - Text(hours, style: AppTextStyle.recieptItemKey), + Text(hours, style: AppTextStyle.receiptItemKey), ], ); }, diff --git a/lib/features/receipt/data/models/purchase_receipt_model.dart b/lib/features/receipt/data/models/purchase_receipt_model.dart index 942dff058..942cf8292 100644 --- a/lib/features/receipt/data/models/purchase_receipt_model.dart +++ b/lib/features/receipt/data/models/purchase_receipt_model.dart @@ -1,4 +1,4 @@ -import 'package:coffeecard/features/receipt/domain/entities/purchase_receipt.dart'; +import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; class PurchaseReceiptModel extends PurchaseReceipt { diff --git a/lib/features/receipt/data/models/swipe_receipt_model.dart b/lib/features/receipt/data/models/swipe_receipt_model.dart index c60cfa11a..52ec9738a 100644 --- a/lib/features/receipt/data/models/swipe_receipt_model.dart +++ b/lib/features/receipt/data/models/swipe_receipt_model.dart @@ -1,4 +1,4 @@ -import 'package:coffeecard/features/receipt/domain/entities/swipe_receipt.dart'; +import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/generated/api/coffeecard_api.models.swagger.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; diff --git a/lib/features/receipt/domain/entities/placeholder_receipt.dart b/lib/features/receipt/domain/entities/placeholder_receipt.dart index 52cc528ff..19cdc45d4 100644 --- a/lib/features/receipt/domain/entities/placeholder_receipt.dart +++ b/lib/features/receipt/domain/entities/placeholder_receipt.dart @@ -1,5 +1,4 @@ -import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; +part of 'receipt.dart'; class PlaceholderReceipt extends Receipt { PlaceholderReceipt() diff --git a/lib/features/receipt/domain/entities/purchase_receipt.dart b/lib/features/receipt/domain/entities/purchase_receipt.dart index 991e85754..57b92409c 100644 --- a/lib/features/receipt/domain/entities/purchase_receipt.dart +++ b/lib/features/receipt/domain/entities/purchase_receipt.dart @@ -1,4 +1,4 @@ -import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; +part of 'receipt.dart'; class PurchaseReceipt extends Receipt { final int price; diff --git a/lib/features/receipt/domain/entities/receipt.dart b/lib/features/receipt/domain/entities/receipt.dart index ab660942e..dd41039f4 100644 --- a/lib/features/receipt/domain/entities/receipt.dart +++ b/lib/features/receipt/domain/entities/receipt.dart @@ -1,7 +1,12 @@ +import 'package:coffeecard/base/strings.dart'; import 'package:equatable/equatable.dart'; +part 'placeholder_receipt.dart'; +part 'purchase_receipt.dart'; +part 'swipe_receipt.dart'; + /// A receipt for either a used ticket, or a purchase -abstract class Receipt extends Equatable { +sealed class Receipt extends Equatable { final String productName; final DateTime timeUsed; final int id; diff --git a/lib/features/receipt/domain/entities/swipe_receipt.dart b/lib/features/receipt/domain/entities/swipe_receipt.dart index 40e3ceb37..023d38495 100644 --- a/lib/features/receipt/domain/entities/swipe_receipt.dart +++ b/lib/features/receipt/domain/entities/swipe_receipt.dart @@ -1,4 +1,4 @@ -import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; +part of 'receipt.dart'; class SwipeReceipt extends Receipt { const SwipeReceipt({ diff --git a/lib/features/receipt/domain/repositories/receipt_repository.dart b/lib/features/receipt/domain/repositories/receipt_repository.dart index 3fbe56e76..62dad4c2f 100644 --- a/lib/features/receipt/domain/repositories/receipt_repository.dart +++ b/lib/features/receipt/domain/repositories/receipt_repository.dart @@ -2,6 +2,6 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:fpdart/fpdart.dart'; -abstract class ReceiptRepository { +abstract interface class ReceiptRepository { Future>> getUserReceipts(); } diff --git a/lib/features/receipt/presentation/cubit/receipt_cubit.dart b/lib/features/receipt/presentation/cubit/receipt_cubit.dart index bcfed8fc5..1bc6cd56b 100644 --- a/lib/features/receipt/presentation/cubit/receipt_cubit.dart +++ b/lib/features/receipt/presentation/cubit/receipt_cubit.dart @@ -1,8 +1,6 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/core/usecases/usecase.dart'; -import 'package:coffeecard/features/receipt/domain/entities/purchase_receipt.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; -import 'package:coffeecard/features/receipt/domain/entities/swipe_receipt.dart'; import 'package:coffeecard/features/receipt/domain/usecases/get_receipts.dart'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -48,13 +46,12 @@ class ReceiptCubit extends Cubit { List receipts, ReceiptFilterCategory filterBy, ) { - switch (filterBy) { - case ReceiptFilterCategory.all: - return receipts; - case ReceiptFilterCategory.swipes: - return receipts.whereType().toList(); - case ReceiptFilterCategory.purchases: - return receipts.whereType().toList(); - } + return switch (filterBy) { + ReceiptFilterCategory.all => receipts, + ReceiptFilterCategory.swipes => + receipts.whereType().toList(), + ReceiptFilterCategory.purchases => + receipts.whereType().toList(), + }; } } diff --git a/lib/features/receipt/presentation/cubit/receipt_state.dart b/lib/features/receipt/presentation/cubit/receipt_state.dart index ecb7a22e2..460114043 100644 --- a/lib/features/receipt/presentation/cubit/receipt_state.dart +++ b/lib/features/receipt/presentation/cubit/receipt_state.dart @@ -5,16 +5,11 @@ enum ReceiptFilterCategory { all, swipes, purchases } enum ReceiptStatus { initial, success, failure } extension DropdownName on ReceiptFilterCategory { - String get name { - switch (this) { - case ReceiptFilterCategory.all: - return Strings.receiptFilterAll; - case ReceiptFilterCategory.swipes: - return Strings.receiptFilterSwipes; - case ReceiptFilterCategory.purchases: - return Strings.receiptFilterPurchases; - } - } + String get name => switch (this) { + ReceiptFilterCategory.all => Strings.receiptFilterAll, + ReceiptFilterCategory.swipes => Strings.receiptFilterSwipes, + ReceiptFilterCategory.purchases => Strings.receiptFilterPurchases + }; } class ReceiptState extends Equatable { diff --git a/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart index 508c62309..b4b170335 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; -import 'package:coffeecard/features/receipt/domain/entities/purchase_receipt.dart'; +import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart'; import 'package:flutter/material.dart'; diff --git a/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart index a69e543d6..131a39030 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart @@ -53,14 +53,14 @@ class ReceiptListEntry extends StatelessWidget { color: colorIfShimmer, child: Text( topText, - style: AppTextStyle.recieptItemKey, + style: AppTextStyle.receiptItemKey, ), ), ColoredBox( color: colorIfShimmer, child: Text( _formatter.format(time), - style: AppTextStyle.recieptItemDate, + style: AppTextStyle.receiptItemDate, ), ), ], @@ -69,7 +69,7 @@ class ReceiptListEntry extends StatelessWidget { color: colorIfShimmer, child: Text( rightText, - style: AppTextStyle.recieptItemValue, + style: AppTextStyle.receiptItemValue, ), ), backgroundColor: backgroundColor, diff --git a/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart index 5d4e88ecd..1242d848b 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; -import 'package:coffeecard/features/receipt/domain/entities/swipe_receipt.dart'; +import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart'; import 'package:flutter/material.dart'; diff --git a/lib/features/receipt/presentation/widgets/receipt_card.dart b/lib/features/receipt/presentation/widgets/receipt_card.dart index 402ce9f38..e315f6367 100644 --- a/lib/features/receipt/presentation/widgets/receipt_card.dart +++ b/lib/features/receipt/presentation/widgets/receipt_card.dart @@ -63,7 +63,7 @@ class ReceiptCard extends StatelessWidget { left: isInOverlay ? Text(Strings.receiptCardNote, style: AppTextStyle.explainer) : const SizedBox.shrink(), - right: AnalogRecieptLogo(), + right: AnalogReceiptLogo(), ), ), ); diff --git a/lib/features/receipt/presentation/widgets/receipt_list_entry_factory.dart b/lib/features/receipt/presentation/widgets/receipt_list_entry_factory.dart index 5ed547510..38ff08b15 100644 --- a/lib/features/receipt/presentation/widgets/receipt_list_entry_factory.dart +++ b/lib/features/receipt/presentation/widgets/receipt_list_entry_factory.dart @@ -1,26 +1,13 @@ -import 'package:coffeecard/features/receipt/domain/entities/placeholder_receipt.dart'; -import 'package:coffeecard/features/receipt/domain/entities/purchase_receipt.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; -import 'package:coffeecard/features/receipt/domain/entities/swipe_receipt.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/list_entry/placeholder_receipt_list_entry.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart'; import 'package:flutter/material.dart'; -abstract class ReceiptListEntryFactory { - static Widget create(Receipt receipt) { - if (receipt is PlaceholderReceipt) { - return const PlaceholderReceiptListEntry(); - } - - if (receipt is PurchaseReceipt) { - return PurchaseReceiptListEntry(receipt: receipt); - } - - if (receipt is SwipeReceipt) { - return SwipeReceiptListEntry(receipt: receipt); - } - - throw ArgumentError('unknown receipt type $receipt'); - } +abstract final class ReceiptListEntryFactory { + static Widget create(Receipt receipt) => switch (receipt) { + PlaceholderReceipt() => const PlaceholderReceiptListEntry(), + PurchaseReceipt() => PurchaseReceiptListEntry(receipt: receipt), + SwipeReceipt() => SwipeReceiptListEntry(receipt: receipt), + }; } diff --git a/lib/features/receipt/presentation/widgets/receipts_list_view.dart b/lib/features/receipt/presentation/widgets/receipts_list_view.dart index 9d4348cda..ccc0f6ce7 100644 --- a/lib/features/receipt/presentation/widgets/receipts_list_view.dart +++ b/lib/features/receipt/presentation/widgets/receipts_list_view.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/features/receipt/domain/entities/placeholder_receipt.dart'; +import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/receipt_list_entry_factory.dart'; import 'package:coffeecard/widgets/components/error_section.dart'; @@ -17,40 +17,61 @@ class ReceiptsListView extends StatelessWidget { return Expanded( child: BlocBuilder( builder: (context, state) { - switch (state.status) { - case ReceiptStatus.initial: - return _ReceiptsPlaceholder(); - case ReceiptStatus.success: - return RefreshIndicator( - displacement: 24, - onRefresh: context.read().fetchReceipts, - child: state.filteredReceipts.isEmpty - ? _ReceiptsEmptyIndicator( - hasNoReceipts: state.receipts.isEmpty, - filterCategory: state.filterBy, - ) - : ListView.builder( - controller: scrollController, - itemCount: state.filteredReceipts.length, - itemBuilder: (_, index) { - final receipt = state.filteredReceipts[index]; - return ReceiptListEntryFactory.create(receipt); - }, - ), - ); - case ReceiptStatus.failure: - return ErrorSection( - center: true, - error: state.error!, - retry: context.read().fetchReceipts, - ); - } + return switch (state.status) { + ReceiptStatus.initial => const _ReceiptsPlaceholder(), + ReceiptStatus.success => _ReceiptsLoadedView(scrollController), + ReceiptStatus.failure => const _ReceiptsErrorView(), + }; }, ), ); } } +class _ReceiptsErrorView extends StatelessWidget { + const _ReceiptsErrorView(); + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + return ErrorSection( + center: true, + error: cubit.state.error!, + retry: cubit.fetchReceipts, + ); + } +} + +class _ReceiptsLoadedView extends StatelessWidget { + const _ReceiptsLoadedView(this.scrollController); + + final ScrollController scrollController; + + @override + Widget build(BuildContext context) { + final cubit = context.read(); + final state = cubit.state; + + return RefreshIndicator( + displacement: 24, + onRefresh: cubit.fetchReceipts, + child: state.filteredReceipts.isEmpty + ? _ReceiptsEmptyIndicator( + hasNoReceipts: state.receipts.isEmpty, + filterCategory: state.filterBy, + ) + : ListView.builder( + controller: scrollController, + itemCount: state.filteredReceipts.length, + itemBuilder: (_, index) { + final receipt = state.filteredReceipts[index]; + return ReceiptListEntryFactory.create(receipt); + }, + ), + ); + } +} + class _ReceiptsEmptyIndicator extends StatelessWidget { const _ReceiptsEmptyIndicator({ required this.hasNoReceipts, @@ -105,10 +126,12 @@ class _ReceiptsEmptyIndicator extends StatelessWidget { } class _ReceiptsPlaceholder extends StatelessWidget { - final placeholderListEntries = List.generate( - 20, - (_) => ReceiptListEntryFactory.create(PlaceholderReceipt()), - ); + const _ReceiptsPlaceholder(); + + List get placeholderListEntries => List.generate( + 20, + (_) => ReceiptListEntryFactory.create(PlaceholderReceipt()), + ); @override Widget build(BuildContext context) { diff --git a/lib/features/ticket/presentation/cubit/tickets_state.dart b/lib/features/ticket/presentation/cubit/tickets_state.dart index c311c7aec..42fbb5d53 100644 --- a/lib/features/ticket/presentation/cubit/tickets_state.dart +++ b/lib/features/ticket/presentation/cubit/tickets_state.dart @@ -1,6 +1,6 @@ part of 'tickets_cubit.dart'; -abstract class TicketsState extends Equatable { +sealed class TicketsState extends Equatable { const TicketsState(); } diff --git a/lib/features/ticket/presentation/pages/tickets_page.dart b/lib/features/ticket/presentation/pages/tickets_page.dart index f071820ac..4e8a22a9d 100644 --- a/lib/features/ticket/presentation/pages/tickets_page.dart +++ b/lib/features/ticket/presentation/pages/tickets_page.dart @@ -31,18 +31,18 @@ class TicketsPage extends StatelessWidget { controller: scrollController, shrinkWrap: true, padding: const EdgeInsets.all(16.0), - children: [ + children: const [ Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, crossAxisAlignment: CrossAxisAlignment.start, - children: const [ + children: [ SectionTitle(Strings.ticketsMyTickets), ], ), - const TicketSection(), - const Gap(24), - const SectionTitle(Strings.shopText), - const ShopSection(), + TicketSection(), + Gap(24), + SectionTitle(Strings.shopText), + ShopSection(), ], ), ), diff --git a/lib/features/ticket/presentation/widgets/tickets_section.dart b/lib/features/ticket/presentation/widgets/tickets_section.dart index 395473ad6..4979392e2 100644 --- a/lib/features/ticket/presentation/widgets/tickets_section.dart +++ b/lib/features/ticket/presentation/widgets/tickets_section.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/text_styles.dart'; import 'package:coffeecard/cubits/environment/environment_cubit.dart'; -import 'package:coffeecard/features/receipt/domain/entities/purchase_receipt.dart'; +import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/receipt_overlay.dart'; import 'package:coffeecard/features/ticket/presentation/cubit/tickets_cubit.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; diff --git a/lib/features/user/domain/entities/role.dart b/lib/features/user/domain/entities/role.dart index 297df09d3..fba700fc4 100644 --- a/lib/features/user/domain/entities/role.dart +++ b/lib/features/user/domain/entities/role.dart @@ -1,32 +1,19 @@ import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; -enum Role { - customer, - barista, - manager, - board, -} +enum Role { customer, barista, manager, board } extension RoleExtension on Role { // json is a dynamic object by its very nature - //ignore: avoid-dynamic + // ignore: avoid-dynamic static Role fromJson(dynamic json) { final role = userRoleFromJson(json); - - switch (role) { - case UserRole.customer: - return Role.customer; - case UserRole.barista: - return Role.barista; - case UserRole.manager: - return Role.manager; - case UserRole.board: - return Role.board; - case UserRole.swaggerGeneratedUnknown: - break; - } - - throw ArgumentError('unknown role $role'); + return switch (role) { + UserRole.customer => Role.customer, + UserRole.barista => Role.barista, + UserRole.manager => Role.manager, + UserRole.board => Role.board, + UserRole.swaggerGeneratedUnknown => throw ArgumentError.value(role), + }; } } diff --git a/lib/features/user/presentation/cubit/user_state.dart b/lib/features/user/presentation/cubit/user_state.dart index b96b3f299..217891869 100644 --- a/lib/features/user/presentation/cubit/user_state.dart +++ b/lib/features/user/presentation/cubit/user_state.dart @@ -1,6 +1,6 @@ part of 'user_cubit.dart'; -abstract class UserState extends Equatable { +sealed class UserState extends Equatable { @override List get props => []; } @@ -13,7 +13,7 @@ class UserError extends UserState { class UserLoading extends UserState {} -abstract class UserWithData extends UserState { +sealed class UserWithData extends UserState { final User user; UserWithData({required this.user}); diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart index 8544b7342..a8cae729a 100644 --- a/lib/firebase_options.dart +++ b/lib/firebase_options.dart @@ -1,8 +1,8 @@ import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; import 'package:flutter/foundation.dart' - show defaultTargetPlatform, kIsWeb, TargetPlatform; + show TargetPlatform, defaultTargetPlatform, kIsWeb; -abstract class DefaultFirebaseOptions { +abstract final class DefaultFirebaseOptions { static FirebaseOptions get production { if (kIsWeb) throw UnsupportedError('Web is not supported'); switch (defaultTargetPlatform) { diff --git a/lib/models/purchase/payment_status.dart b/lib/models/purchase/payment_status.dart index 3a8ae8350..2e1813a9a 100644 --- a/lib/models/purchase/payment_status.dart +++ b/lib/models/purchase/payment_status.dart @@ -20,17 +20,12 @@ enum PaymentStatus { refunded; static PaymentStatus fromPurchaseStatus(PurchaseStatus status) { - switch (status) { - case PurchaseStatus.swaggerGeneratedUnknown: - return PaymentStatus.error; - case PurchaseStatus.completed: - return PaymentStatus.completed; - case PurchaseStatus.cancelled: - return PaymentStatus.rejectedPayment; - case PurchaseStatus.pendingpayment: - return PaymentStatus.awaitingPayment; - case PurchaseStatus.refunded: - return PaymentStatus.refunded; - } + return switch (status) { + PurchaseStatus.completed => PaymentStatus.completed, + PurchaseStatus.cancelled => PaymentStatus.rejectedPayment, + PurchaseStatus.pendingpayment => PaymentStatus.awaitingPayment, + PurchaseStatus.refunded => PaymentStatus.refunded, + PurchaseStatus.swaggerGeneratedUnknown => PaymentStatus.error, + }; } } diff --git a/lib/payment/payment_handler.dart b/lib/payment/payment_handler.dart index 3d2275485..ddd60badb 100644 --- a/lib/payment/payment_handler.dart +++ b/lib/payment/payment_handler.dart @@ -11,7 +11,6 @@ import 'package:fpdart/fpdart.dart'; abstract class PaymentHandler { final PurchaseRepository purchaseRepository; // Certain implementations of the payment handler require access to the build context, even if it does not do so itself. - // ignore: unused_field final BuildContext context; const PaymentHandler({ @@ -25,20 +24,16 @@ abstract class PaymentHandler { ) { final repository = sl.get(); - switch (paymentType) { - case InternalPaymentType.mobilePay: - return MobilePayService( + return switch (paymentType) { + InternalPaymentType.mobilePay => MobilePayService( purchaseRepository: repository, context: context, - ); - case InternalPaymentType.free: - return FreeProductService( + ), + InternalPaymentType.free => FreeProductService( purchaseRepository: repository, context: context, - ); - default: - throw UnimplementedError(); - } + ), + }; } Future> initPurchase(int productId); diff --git a/lib/service_locator.dart b/lib/service_locator.dart index ff4503026..ee6a75b54 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -40,6 +40,7 @@ import 'package:coffeecard/generated/api/shiftplanning_api.swagger.dart' hide $JsonSerializableConverter; import 'package:coffeecard/utils/api_uri_constants.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; +import 'package:coffeecard/utils/ignore_value.dart'; import 'package:coffeecard/utils/reactivation_authenticator.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; import 'package:get_it/get_it.dart'; @@ -49,7 +50,9 @@ final GetIt sl = GetIt.instance; void configureServices() { // Logger - sl.registerSingleton(Logger()); + ignoreValue( + sl.registerSingleton(Logger()), + ); // Executor sl.registerLazySingleton( @@ -60,11 +63,15 @@ void configureServices() { ); // Storage - sl.registerSingleton(SecureStorage(sl())); + ignoreValue( + sl.registerSingleton(SecureStorage(sl())), + ); // Authentication - sl.registerSingleton( - AuthenticationCubit(sl.get()), + ignoreValue( + sl.registerSingleton( + AuthenticationCubit(sl()), + ), ); sl.registerFactory( @@ -92,14 +99,20 @@ void configureServices() { services: [ShiftplanningApi.create()], ); - sl.registerSingleton( - coffeCardChopper.getService(), + ignoreValue( + sl.registerSingleton( + coffeCardChopper.getService(), + ), ); - sl.registerSingleton( - coffeCardChopper.getService(), + ignoreValue( + sl.registerSingleton( + coffeCardChopper.getService(), + ), ); - sl.registerSingleton( - shiftplanningChopper.getService(), + ignoreValue( + sl.registerSingleton( + shiftplanningChopper.getService(), + ), ); // Repositories @@ -142,8 +155,10 @@ void configureServices() { ); // external - sl.registerSingleton( - FirebaseAnalyticsEventLogging(FirebaseAnalytics.instance), + ignoreValue( + sl.registerSingleton( + FirebaseAnalyticsEventLogging(FirebaseAnalytics.instance), + ), ); } diff --git a/lib/utils/analog_icons.dart b/lib/utils/analog_icons.dart index c74f3d37e..1951c17be 100644 --- a/lib/utils/analog_icons.dart +++ b/lib/utils/analog_icons.dart @@ -10,9 +10,8 @@ /// - family: AnalogIcons /// fonts: /// - asset: fonts/AnalogIcons.ttf -/// -/// -/// +library; + import 'package:flutter/widgets.dart'; class AnalogIcons { diff --git a/lib/utils/ignore_value.dart b/lib/utils/ignore_value.dart new file mode 100644 index 000000000..800cef489 --- /dev/null +++ b/lib/utils/ignore_value.dart @@ -0,0 +1,8 @@ +/// Ignore the return value of a function. +/// +/// This function is only needed if you need to ignore multiple values in the +/// same scope. Assigning to the underscore (`_`) variable is a better solution +/// in other cases. +void ignoreValue(T _) { + return; +} diff --git a/lib/widgets/components/helpers/lazy_indexed_stack.dart b/lib/widgets/components/helpers/lazy_indexed_stack.dart index 6af39c4eb..193baf047 100644 --- a/lib/widgets/components/helpers/lazy_indexed_stack.dart +++ b/lib/widgets/components/helpers/lazy_indexed_stack.dart @@ -1,5 +1,7 @@ /// [Author] Alex (https://github.com/AlexV525) /// [Date] 2020-12-26 14:08 +library; + import 'package:flutter/material.dart'; typedef LazyWidgetBuilder = Widget Function(BuildContext context); diff --git a/lib/widgets/components/images/analog_logo.dart b/lib/widgets/components/images/analog_logo.dart index 30bff2158..60a75031b 100644 --- a/lib/widgets/components/images/analog_logo.dart +++ b/lib/widgets/components/images/analog_logo.dart @@ -16,7 +16,7 @@ class AnalogLogo extends StatelessWidget { } } -class AnalogRecieptLogo extends StatelessWidget { +class AnalogReceiptLogo extends StatelessWidget { @override Widget build(BuildContext context) { return SizedBox(height: 26, child: Image.asset('assets/logo-dark.png')); diff --git a/lib/widgets/components/purchase/purchase_process.dart b/lib/widgets/components/purchase/purchase_process.dart index d30f23bd5..d70ac75cc 100644 --- a/lib/widgets/components/purchase/purchase_process.dart +++ b/lib/widgets/components/purchase/purchase_process.dart @@ -98,9 +98,9 @@ class _PurchaseProcessState extends State return SimpleDialog( shape: _getShape(), title: _getTitleWidget(title), - children: [ + children: const [ Column( - children: const [ + children: [ Padding( padding: EdgeInsets.all(16), child: CircularProgressIndicator(color: AppColor.primary), diff --git a/lib/widgets/components/user/user_card.dart b/lib/widgets/components/user/user_card.dart index 83223220c..2b13ec604 100644 --- a/lib/widgets/components/user/user_card.dart +++ b/lib/widgets/components/user/user_card.dart @@ -57,7 +57,7 @@ class UserCard extends StatelessWidget { name, maxLines: 1, overflow: TextOverflow.ellipsis, - style: AppTextStyle.recieptItemKey, + style: AppTextStyle.receiptItemKey, ), ), const Gap(3), diff --git a/lib/widgets/pages/settings/your_profile_page.dart b/lib/widgets/pages/settings/your_profile_page.dart index fdb7cda3b..f873d0ea2 100644 --- a/lib/widgets/pages/settings/your_profile_page.dart +++ b/lib/widgets/pages/settings/your_profile_page.dart @@ -102,8 +102,8 @@ class _EditProfile extends StatelessWidget { .setUserPrivacy(privacyActivated: !user.privacyActivated), valueWidget: Switch( value: user.privacyActivated, - //No action needed on change, only tap - //ignore: no-empty-block + // No action needed on change, only tap + // ignore: no-empty-block onChanged: (_) {}, ), ), diff --git a/makefile b/makefile index c04cf8e25..82c8dd027 100644 --- a/makefile +++ b/makefile @@ -1,23 +1,23 @@ get: flutter pub get generate: - flutter pub run build_runner build --delete-conflicting-outputs + dart run build_runner build --delete-conflicting-outputs format: dart format . && dart analyze icon: - flutter pub run flutter_launcher_icons:main + dart run flutter_launcher_icons:main splash: - flutter pub run flutter_native_splash:create + dart run flutter_native_splash:create analyze: flutter analyze && \ - flutter pub run dart_code_metrics:metrics analyze lib + dart run dart_code_metrics:metrics analyze lib update_icon: - flutter pub run flutter_launcher_icons:main + dart run flutter_launcher_icons:main update_name: - flutter pub run flutter_app_name + dart run flutter_app_name cov: flutter test --coverage && lcov --remove coverage/lcov.info 'lib/base/*' 'lib/widgets/*' 'lib/features/*/presentation/widgets/*' 'lib/service_locator.dart' 'lib/generated/*' -o coverage/lcov.info && genhtml coverage/lcov.info -o coverage/html clean: - flutter clean && flutter pub get && flutter pub run build_runner build --delete-conflicting-outputs + flutter clean && flutter pub get && dart run build_runner build --delete-conflicting-outputs upgrade: flutter pub upgrade --major-versions \ No newline at end of file diff --git a/pubspec.lock b/pubspec.lock index 5ed762104..4d4dcfa13 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,26 +5,26 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: "98d1d33ed129b372846e862de23a0fc365745f4d7b5e786ce667fcbbb7ac5c07" + sha256: "8880b4cfe7b5b17d57c052a5a3a8cc1d4f546261c7cc8fbd717bd53f48db0568" url: "https://pub.dev" source: hosted - version: "55.0.0" + version: "59.0.0" _flutterfire_internals: dependency: transitive description: name: _flutterfire_internals - sha256: "64fcb0dbca4386356386c085142fa6e79c00a3326ceaa778a2d25f5d9ba61441" + sha256: d687576bb973e8d2539d0b4615d985336269a0dbe1b3984e937fd22d31406fb8 url: "https://pub.dev" source: hosted - version: "1.0.16" + version: "1.3.0" analyzer: dependency: transitive description: name: analyzer - sha256: "881348aed9b0b425882c97732629a6a31093c8ff20fc4b3b03fb9d3d50a3a126" + sha256: a89627f49b0e70e068130a36571409726b04dab12da7e5625941d2c8ec278b96 url: "https://pub.dev" source: hosted - version: "5.7.1" + version: "5.11.1" analyzer_plugin: dependency: transitive description: @@ -53,26 +53,26 @@ packages: dependency: transitive description: name: archive - sha256: d6347d54a2d8028e0437e3c099f66fdb8ae02c4720c1e7534c9f24c10351f85d + sha256: "0c8368c9b3f0abbc193b9d6133649a614204b528982bebc7026372d61677ce3a" url: "https://pub.dev" source: hosted - version: "3.3.6" + version: "3.3.7" args: dependency: transitive description: name: args - sha256: "4cab82a83ffef80b262ddedf47a0a8e56ee6fbf7fe21e6e768b02792034dd440" + sha256: c372bb384f273f0c2a8aaaa226dad84dc27c8519a691b888725dec59518ad53a url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" async: dependency: transitive description: name: async - sha256: bfe67ef28df125b7dddcea62755991f807aa39a2492a23e1550161692950bbe0 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.10.0" + version: "2.11.0" bloc: dependency: "direct main" description: @@ -101,10 +101,10 @@ packages: dependency: transitive description: name: build - sha256: "3fbda25365741f8251b39f3917fb3c8e286a96fd068a5a242e11c2012d495777" + sha256: "43865b79fbb78532e4bff7c33087aa43b1d488c4fdef014eaef568af6d8016dc" url: "https://pub.dev" source: hosted - version: "2.3.1" + version: "2.4.0" build_config: dependency: transitive description: @@ -117,10 +117,10 @@ packages: dependency: transitive description: name: build_daemon - sha256: "757153e5d9cd88253cb13f28c2fb55a537dc31fefd98137549895b5beb7c6169" + sha256: "5f02d73eb2ba16483e693f80bee4f088563a820e47d1027d4cdfe62b5bb43e65" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "4.0.0" build_resolvers: dependency: transitive description: @@ -133,18 +133,18 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: b0a8a7b8a76c493e85f1b84bffa0588859a06197863dba8c9036b15581fd9727 + sha256: "220ae4553e50d7c21a17c051afc7b183d28a24a420502e842f303f8e4e6edced" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.4.4" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "14febe0f5bac5ae474117a36099b4de6f1dbc52df6c5e55534b3da9591bf4292" + sha256: "30859c90e9ddaccc484f56303931f477b1f1ba2bab74aa32ed5d6ce15870f8cf" url: "https://pub.dev" source: hosted - version: "7.2.7" + version: "7.2.8" built_collection: dependency: transitive description: @@ -157,10 +157,10 @@ packages: dependency: transitive description: name: built_value - sha256: "31b7c748fd4b9adf8d25d72a4c4a59ef119f12876cf414f94f8af5131d5fa2b0" + sha256: "2f17434bd5d52a26762043d6b43bb53b3acd029b4d9071a329f46d67ef297e6d" url: "https://pub.dev" source: hosted - version: "8.4.4" + version: "8.5.0" cached_network_image: dependency: "direct main" description: @@ -189,42 +189,42 @@ packages: dependency: transitive description: name: characters - sha256: e6a326c8af69605aec75ed6c187d06b349707a27fbff8222ca9cc2cff167975c + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.3.0" checked_yaml: dependency: transitive description: name: checked_yaml - sha256: "3d1505d91afa809d177efd4eed5bb0eb65805097a1463abdd2add076effae311" + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff url: "https://pub.dev" source: hosted - version: "2.0.2" + version: "2.0.3" chopper: dependency: "direct main" description: name: chopper - sha256: b2645618fa760df06d7609c96b092d7b3e7a8f23639d34269f62f45a5edabc7d + sha256: "946bf5a8712140032d71683b95c959d1b6b2ae48d4ace32ec0675cbef39e51dc" url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.2" chopper_generator: dependency: "direct dev" description: name: chopper_generator - sha256: "62360a1a3536645aaee030dd7719089838a478682867d46dbcf30a3c0e5305b4" + sha256: b3860ae94129012b636c3b68af25ad9a326aa2203afa118ab7b6fbc0ca98144d url: "https://pub.dev" source: hosted - version: "6.0.0" + version: "6.0.1" cli_util: dependency: transitive description: name: cli_util - sha256: "66f86e916d285c1a93d3b79587d94bd71984a66aac4ff74e524cfa7877f1395c" + sha256: b8db3080e59b2503ca9e7922c3df2072cf13992354d5e944074ffa836fba43b7 url: "https://pub.dev" source: hosted - version: "0.3.5" + version: "0.4.0" clock: dependency: transitive description: @@ -245,10 +245,10 @@ packages: dependency: "direct main" description: name: collection - sha256: cfc915e6923fe5ce6e153b0723c753045de46de1b4d63771530504004a45fae0 + sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.17.1" convert: dependency: transitive description: @@ -269,10 +269,10 @@ packages: dependency: "direct main" description: name: crypto - sha256: aa274aa7774f8964e4f4f38cc994db7b6158dd36e9187aaceaddc994b35c6c67 + sha256: ff625774173754681d66daaf4a448684fb04b78f902da9cb3d308c19cc5e8bab url: "https://pub.dev" source: hosted - version: "3.0.2" + version: "3.0.3" csslib: dependency: transitive description: @@ -285,74 +285,42 @@ packages: dependency: "direct dev" description: name: dart_code_metrics - sha256: "728b1835a8c5b994ae8111148b427236f2f89954eebcb62baf4c476909b3d02d" + sha256: "162c81dbd0a2ba182f38ca615335f3e8878f212ec7beea83d6bfad4e99eb541a" url: "https://pub.dev" source: hosted - version: "5.7.0" + version: "5.7.3" dart_code_metrics_presets: dependency: "direct dev" description: name: dart_code_metrics_presets - sha256: "1b841b3ab3e7b942683a706cfbcef55fe9f38e7d01b690dbcd2e9f3c8e268525" + sha256: "22e27f98e8c7d8b11cca43d2656a822935280747050ae65e8cd03c52d09c0d1c" url: "https://pub.dev" source: hosted - version: "1.5.0" + version: "1.7.0" dart_style: dependency: transitive description: name: dart_style - sha256: "5be16bf1707658e4c03078d4a9b90208ded217fb02c163e207d334082412f2fb" + sha256: f4f1f73ab3fd2afcbcca165ee601fe980d966af6a21b5970c6c9376955c528ad url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.3.1" device_info_plus: dependency: transitive description: name: device_info_plus - sha256: "717a1d782b01ecbd0890b9a8994cb018ae2a9a617dddf681217567189008915c" - url: "https://pub.dev" - source: hosted - version: "6.0.0" - device_info_plus_linux: - dependency: transitive - description: - name: device_info_plus_linux - sha256: ebefd15fa43df3a8b222d6a120f23771c18b62b8306b07f72f546927475b2bd6 + sha256: "9b1a0c32b2a503f8fe9f8764fac7b5fcd4f6bd35d8f49de5350bccf9e2a33b8a" url: "https://pub.dev" source: hosted - version: "5.0.0" - device_info_plus_macos: - dependency: transitive - description: - name: device_info_plus_macos - sha256: "9e540a74d7f307e640db2caa059d4b55676ffe7f2bc5cf3d103f6e7fdb6ac153" - url: "https://pub.dev" - source: hosted - version: "5.0.0" + version: "9.0.0" device_info_plus_platform_interface: dependency: transitive description: name: device_info_plus_platform_interface - sha256: adac28cb9101434260279bf36adeac8cafac8d120d0d942dabb2c6bd9a03b4c7 + sha256: d3b01d5868b50ae571cd1dc6e502fc94d956b665756180f7b16ead09e836fd64 url: "https://pub.dev" source: hosted - version: "5.0.0" - device_info_plus_web: - dependency: transitive - description: - name: device_info_plus_web - sha256: de7e651339a77b003af7cd77d83afbbd4f4848fd8892dbc80910278a8cf45f7b - url: "https://pub.dev" - source: hosted - version: "5.0.0" - device_info_plus_windows: - dependency: transitive - description: - name: device_info_plus_windows - sha256: "803371e6c0ed01b7cda9ae7c8628ea1a6646a6f2b8fb8e1c9376fda19fa123b3" - url: "https://pub.dev" - source: hosted - version: "6.0.0" + version: "7.0.0" diff_match_patch: dependency: transitive description: @@ -373,18 +341,18 @@ packages: dependency: "direct main" description: name: envied - sha256: d5d978fbd578b5c00123003609c39185e0b1ddf9d2ac460d710dd0eb2fc223d7 + sha256: "60d3f5606c7b35bc6ef493e650d916b34351d8af2e58b7ac45881ba59dfcf039" url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.0+3" envied_generator: dependency: "direct dev" description: name: envied_generator - sha256: "6c5a98c27c5eae925807692eb252ccac2b8e81f09bace1f07207c47dfb6a4eb0" + sha256: dfdbe5dc52863e54c036a4c4042afbdf1bd528cb4c1e638ecba26228ba72e9e5 url: "https://pub.dev" source: hosted - version: "0.3.0" + version: "0.3.0+3" equatable: dependency: "direct main" description: @@ -405,10 +373,10 @@ packages: dependency: transitive description: name: ffi - sha256: a38574032c5f1dd06c4aee541789906c12ccaab8ba01446e800d9c5b79c4a978 + sha256: ed5337a5660c506388a9f012be0288fb38b49020ce2b45fe1f8b8323fe429f99 url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.2" file: dependency: transitive description: @@ -421,90 +389,90 @@ packages: dependency: "direct main" description: name: firebase_analytics - sha256: "4d7829b265823ae6aff81656d5ab9d89f61edfa5a6647843ca978bd1d6ec1ae6" + sha256: "0dfe8a3da3052400a266e8d929d2940545aac4c37ec1f1c5e11c0df3175ce9ad" url: "https://pub.dev" source: hosted - version: "10.1.4" + version: "10.4.0" firebase_analytics_platform_interface: dependency: transitive description: name: firebase_analytics_platform_interface - sha256: b18475c1367622b3a86585d970a755a73d00f320efa8977a46b4768e7752c037 + sha256: "45a023037fbb7d081bee5f6b0c421b33a48e2471ab27810bdbcc185c878bd317" url: "https://pub.dev" source: hosted - version: "3.3.21" + version: "3.6.0" firebase_analytics_web: dependency: transitive description: name: firebase_analytics_web - sha256: "30e5631f14bce70592bf9f26672302aafe62fe50b194c4c633c4b977051581ec" + sha256: c60b1042cacf2c8075569717fe3b5f9e26936d2063bc8d749ca8b102fe6cfec0 url: "https://pub.dev" source: hosted - version: "0.5.1+12" + version: "0.5.4" firebase_core: dependency: "direct main" description: name: firebase_core - sha256: fe30ac230f12f8836bb97e6e09197340d3c584526825b1746ea362a82e1e43f7 + sha256: "4491238f4fddc885bc994e304a035eb8aba2c935816b2c0b31d87f3ec6e96682" url: "https://pub.dev" source: hosted - version: "2.7.0" + version: "2.12.0" firebase_core_platform_interface: dependency: transitive description: name: firebase_core_platform_interface - sha256: "5615b30c36f55b2777d0533771deda7e5730e769e5d3cb7fda79e9bed86cfa55" + sha256: b63e3be6c96ef5c33bdec1aab23c91eb00696f6452f0519401d640938c94cba2 url: "https://pub.dev" source: hosted - version: "4.5.3" + version: "4.8.0" firebase_core_web: dependency: transitive description: name: firebase_core_web - sha256: "0c1cf1f1022d2245ac117443bb95207952ca770281524d2908e323bc063fb8ff" + sha256: "8c0f4c87d20e2d001a5915df238c1f9c88704231f591324205f5a5d2a7740a45" url: "https://pub.dev" source: hosted - version: "2.2.2" + version: "2.5.0" firebase_crashlytics: dependency: "direct main" description: name: firebase_crashlytics - sha256: "816bbb920316c8fe257b460b8856b01e274e867a729961bf7a3be6322cdf13e1" + sha256: cc6cab05d4de3257408c167ec1e2e410f21c0764aea22f78481314496a1cc4ca url: "https://pub.dev" source: hosted - version: "3.0.15" + version: "3.3.0" firebase_crashlytics_platform_interface: dependency: transitive description: name: firebase_crashlytics_platform_interface - sha256: "120e47b9bac3654848d1bdc60b8027f3574b53ee0b81b1a2e5e76ddaa58f6645" + sha256: b3b6017045866b981b566896a0de6114181bf9e91465221f2e89996f47a160b4 url: "https://pub.dev" source: hosted - version: "3.3.15" + version: "3.6.0" firebase_performance: dependency: "direct main" description: name: firebase_performance - sha256: a908b58c102fc2fddd3bfcdf05efaad73cf96a0988c2bbd06c63dad6a26f955e + sha256: "460f48e4a32f898e777692d0b1278fcee74c14b8f3f7482c0fa551d115bdf79d" url: "https://pub.dev" source: hosted - version: "0.9.0+14" + version: "0.9.2" firebase_performance_platform_interface: dependency: transitive description: name: firebase_performance_platform_interface - sha256: ef847cb2b5e1f9ab901c9144688987b0c749f609c8c6add76e12e7f6c1862ec7 + sha256: "48dca6a9cbdab834ea3efa715f446d381797e98b0f6e6ddf5d2891d8cd79f545" url: "https://pub.dev" source: hosted - version: "0.1.1+33" + version: "0.1.4" firebase_performance_web: dependency: transitive description: name: firebase_performance_web - sha256: "5f0085691745a28555aac7f129f236bf92537d8da41d84d25a7f71155c3482d0" + sha256: b4111036bdccaffc7fa57047876bcb55325120a17b517f0ba75e4a39adfdef63 url: "https://pub.dev" source: hosted - version: "0.1.1+22" + version: "0.1.4" fixnum: dependency: transitive description: @@ -546,26 +514,26 @@ packages: dependency: "direct dev" description: name: flutter_launcher_icons - sha256: a9de6706cd844668beac27c0aed5910fa0534832b3c2cad61a5fd977fce82a5d + sha256: "526faf84284b86a4cb36d20a5e45147747b7563d921373d4ee0559c54fcdbcea" url: "https://pub.dev" source: hosted - version: "0.10.0" + version: "0.13.1" flutter_layout_grid: dependency: "direct main" description: name: flutter_layout_grid - sha256: "1df27a2e9cd34faa0c0a33148c8bb9d9259e87cc5b934c989a59a77e1a4c0d6b" + sha256: "31728ce6e9c8eaa48f8a3f928a7c308da4b74ab867aec2dc5b80b0c9f9b79d0d" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.0.3" flutter_native_splash: dependency: "direct dev" description: name: flutter_native_splash - sha256: "6777a3abb974021a39b5fdd2d46a03ca390e03903b6351f21d10e7ecc969f12d" + sha256: af665ef80a213a9ed502845a3d7a61b9acca4100ee7e9f067a7440bc3acd6730 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.19" flutter_secure_storage: dependency: "direct main" description: @@ -644,18 +612,18 @@ packages: dependency: "direct main" description: name: gap - sha256: "6e35ee60d5bbc61b0bec97cc5ba7ed32f62f1f62449e281ea77677479418a15d" + sha256: "976fa1e405d7d8249b3d2ec0e18a1eac116f0a86430c29a7485dff6f5326f03a" url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "3.0.0" get_it: dependency: "direct main" description: name: get_it - sha256: "290fde3a86072e4b37dbb03c07bec6126f0ecc28dad403c12ffe2e5a2d751ab7" + sha256: "529de303c739fca98cd7ece5fca500d8ff89649f1bb4b4e94fb20954abcd7468" url: "https://pub.dev" source: hosted - version: "7.2.0" + version: "7.6.0" glob: dependency: transitive description: @@ -668,26 +636,26 @@ packages: dependency: transitive description: name: graphs - sha256: f9e130f3259f52d26f0cfc0e964513796dafed572fa52e45d2f8d6ca14db39b2 + sha256: "772db3d53d23361d4ffcf5a9bb091cf3ee9b22f2be52cd107cd7a2683a89ba0e" url: "https://pub.dev" source: hosted - version: "2.2.0" + version: "2.3.0" html: dependency: transitive description: name: html - sha256: d9793e10dbe0e6c364f4c59bf3e01fb33a9b2a674bc7a1081693dba0614b6269 + sha256: "58e3491f7bf0b6a4ea5110c0c688877460d1a6366731155c4a4580e7ded773e8" url: "https://pub.dev" source: hosted - version: "0.15.1" + version: "0.15.3" http: dependency: "direct main" description: name: http - sha256: "6aa2946395183537c8b880962d935877325d6a09a2867c3970c05c0fed6ac482" + sha256: "5895291c13fa8a3bd82e76d5627f69e0d85ca6a30dcac95c4ea19a5d555879c2" url: "https://pub.dev" source: hosted - version: "0.13.5" + version: "0.13.6" http_multi_server: dependency: transitive description: @@ -708,18 +676,18 @@ packages: dependency: transitive description: name: image - sha256: "8e9d133755c3e84c73288363e6343157c383a0c6c56fc51afcc5d4d7180306d6" + sha256: a72242c9a0ffb65d03de1b7113bc4e189686fc07c7147b8b41811d0dd0e0d9bf url: "https://pub.dev" source: hosted - version: "3.3.0" + version: "4.0.17" intl: dependency: "direct main" description: name: intl - sha256: a3715e3bc90294e971cb7dc063fbf3cd9ee0ebf8604ffeafabd9e6f16abbdbe6 + sha256: "3bc132a9dbce73a7e4a21a17d06e1878839ffbf975568bc875c60537824b0c4d" url: "https://pub.dev" source: hosted - version: "0.18.0" + version: "0.18.1" io: dependency: transitive description: @@ -732,42 +700,42 @@ packages: dependency: transitive description: name: js - sha256: "5528c2f391ededb7775ec1daa69e65a2d61276f7552de2b5f7b8d34ee9fd4ab7" + sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 url: "https://pub.dev" source: hosted - version: "0.6.5" + version: "0.6.7" json_annotation: dependency: transitive description: name: json_annotation - sha256: c33da08e136c3df0190bd5bbe51ae1df4a7d96e7954d1d7249fea2968a72d317 + sha256: b10a7b2ff83d83c777edba3c6a0f97045ddadd56c944e1a23a3fdf43a1bf4467 url: "https://pub.dev" source: hosted - version: "4.8.0" + version: "4.8.1" json_serializable: dependency: "direct dev" description: name: json_serializable - sha256: dadc08bd61f72559f938dd08ec20dbfec6c709bba83515085ea943d2078d187a + sha256: "43793352f90efa5d8b251893a63d767b2f7c833120e3cc02adad55eefec04dc7" url: "https://pub.dev" source: hosted - version: "6.6.1" + version: "6.6.2" lint: dependency: "direct dev" description: name: lint - sha256: "3e9343b1cededcfb1e8b40d0dbd3592b7a1c6c0121545663a991433390c2bc97" + sha256: f4bd4dbaa39f4ae8836f2d1275f2f32bc68b3a8cce0a0735dd1f7a601f06682a url: "https://pub.dev" source: hosted - version: "2.0.1" + version: "2.1.2" logger: dependency: "direct main" description: name: logger - sha256: c40f9ef51e5bffb4ce69ad2d8c8aad7bd47ec109c090521109b63a4e2bc27191 + sha256: db2ff852ed77090ba9f62d3611e4208a3d11dfa35991a81ae724c113fcb3e3f7 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.3.0" logging: dependency: transitive description: @@ -780,18 +748,18 @@ packages: dependency: transitive description: name: markdown - sha256: b3c60dee8c2af50ad0e6e90cceba98e47718a6ee0a7a6772c77846a0cc21f78b + sha256: "8e332924094383133cee218b676871f42db2514f1f6ac617b6cf6152a7faab8e" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.1.0" matcher: dependency: transitive description: name: matcher - sha256: "16db949ceee371e9b99d22f88fa3a73c4e59fd0afed0bd25fc336eb76c198b72" + sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" url: "https://pub.dev" source: hosted - version: "0.12.13" + version: "0.12.15" material_color_utilities: dependency: transitive description: @@ -804,10 +772,10 @@ packages: dependency: "direct main" description: name: meta - sha256: "6c268b42ed578a53088d834796959e4a1814b5e9e164f147f580a386e5decf42" + sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" url: "https://pub.dev" source: hosted - version: "1.8.0" + version: "1.9.1" mime: dependency: transitive description: @@ -820,10 +788,10 @@ packages: dependency: "direct dev" description: name: mockito - sha256: "2a8a17b82b1bde04d514e75d90d634a0ac23f6cb4991f6098009dd56836aeafe" + sha256: dd61809f04da1838a680926de50a9e87385c1de91c6579629c3d1723946e8059 url: "https://pub.dev" source: hosted - version: "5.3.2" + version: "5.4.0" mocktail: dependency: transitive description: @@ -876,26 +844,10 @@ packages: dependency: transitive description: name: package_info_plus - sha256: a6eb9db6f8bfadcc95b04f53c9f6ba2b112b8e8fbf430ae34a0aa0a64b1022c8 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - package_info_plus_linux: - dependency: transitive - description: - name: package_info_plus_linux - sha256: "15a66f80b52245e14af7be939bbd1959961f09cfc1d58523cc4e8d5b740efb21" - url: "https://pub.dev" - source: hosted - version: "2.0.0" - package_info_plus_macos: - dependency: transitive - description: - name: package_info_plus_macos - sha256: b774a2d8b31c4575a9a9c56f86a2cb542f0ef1d08be8623b94af0f7c962a2845 + sha256: d39e8fbff4c5aef4592737e25ad6ac500df006ce7a7a8e1f838ce1256e167542 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "4.0.0" package_info_plus_platform_interface: dependency: transitive description: @@ -904,30 +856,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.1" - package_info_plus_web: - dependency: transitive - description: - name: package_info_plus_web - sha256: fb408fed17c00ed4b4b963d124a83bcbf25de990dea0c43a73d48b20339f62a4 - url: "https://pub.dev" - source: hosted - version: "2.0.0" - package_info_plus_windows: - dependency: transitive - description: - name: package_info_plus_windows - sha256: ffb1f96a57ff7680c06a66ae12f62140350beddc5478ef18494901c74067f122 - url: "https://pub.dev" - source: hosted - version: "3.0.0" path: dependency: transitive description: name: path - sha256: db9d4f58c908a4ba5953fcee2ae317c94889433e5024c27ce74a37f94267945b + sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" url: "https://pub.dev" source: hosted - version: "1.8.2" + version: "1.8.3" path_drawing: dependency: transitive description: @@ -948,34 +884,34 @@ packages: dependency: transitive description: name: path_provider - sha256: "04890b994ee89bfa80bf3080bfec40d5a92c5c7a785ebb02c13084a099d2b6f9" + sha256: "3087813781ab814e4157b172f1a11c46be20179fcc9bea043e0fba36bc0acaa2" url: "https://pub.dev" source: hosted - version: "2.0.13" + version: "2.0.15" path_provider_android: dependency: transitive description: name: path_provider_android - sha256: "7623b7d4be0f0f7d9a8b5ee6879fc13e4522d4c875ab86801dee4af32b54b83e" + sha256: "2cec049d282c7f13c594b4a73976b0b4f2d7a1838a6dd5aaf7bd9719196bee86" url: "https://pub.dev" source: hosted - version: "2.0.23" + version: "2.0.27" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: eec003594f19fe2456ea965ae36b3fc967bc5005f508890aafe31fa75e41d972 + sha256: "1995d88ec2948dac43edf8fe58eb434d35d22a2940ecee1a9fefcd62beee6eb3" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.2.3" path_provider_linux: dependency: transitive description: name: path_provider_linux - sha256: "525ad5e07622d19447ad740b1ed5070031f7a5437f44355ae915ff56e986429a" + sha256: "2ae08f2216225427e64ad224a24354221c2c7907e448e6e0e8b57b1eb9f10ad1" url: "https://pub.dev" source: hosted - version: "2.1.9" + version: "2.1.10" path_provider_platform_interface: dependency: transitive description: @@ -988,10 +924,10 @@ packages: dependency: transitive description: name: path_provider_windows - sha256: "642ddf65fde5404f83267e8459ddb4556316d3ee6d511ed193357e25caa3632d" + sha256: d3f80b32e83ec208ac95253e0cd4d298e104fbc63cb29c5c69edaed43b0c69d6 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.6" pedantic: dependency: transitive description: @@ -1004,10 +940,10 @@ packages: dependency: transitive description: name: petitparser - sha256: "49392a45ced973e8d94a85fdb21293fbb40ba805fc49f2965101ae748a3683b4" + sha256: cb3798bef7fc021ac45b308f4b51208a152792445cce0448c9a4ba5879dd8750 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.4.0" platform: dependency: transitive description: @@ -1028,10 +964,10 @@ packages: dependency: transitive description: name: pointycastle - sha256: db7306cf0249f838d1a24af52b5a5887c5bf7f31d8bb4e827d071dc0939ad346 + sha256: "7c1e5f0d23c9016c5bbd8b1473d0d3fb3fc851b876046039509e18e0c7485f2c" url: "https://pub.dev" source: hosted - version: "3.6.2" + version: "3.7.3" pool: dependency: transitive description: @@ -1060,26 +996,26 @@ packages: dependency: transitive description: name: pub_semver - sha256: "307de764d305289ff24ad257ad5c5793ce56d04947599ad68b3baa124105fc17" + sha256: "40d3ab1bbd474c4c2328c91e3a7df8c6dd629b79ece4c4bd04bee496a224fb0c" url: "https://pub.dev" source: hosted - version: "2.1.3" + version: "2.1.4" pub_updater: dependency: transitive description: name: pub_updater - sha256: "42890302ab2672adf567dc2b20e55b4ecc29d7e19c63b6b98143ab68dd717d3a" + sha256: "05ae70703e06f7fdeb05f7f02dd680b8aad810e87c756a618f33e1794635115c" url: "https://pub.dev" source: hosted - version: "0.2.4" + version: "0.3.0" pubspec_parse: dependency: transitive description: name: pubspec_parse - sha256: ec85d7d55339d85f44ec2b682a82fea340071e8978257e5a43e69f79e98ef50c + sha256: c63b2876e58e194e4b0828fcb080ad0e06d051cb607a6be51a9e084f47cb9367 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.3" quiver: dependency: transitive description: @@ -1116,10 +1052,10 @@ packages: dependency: transitive description: name: screen_brightness_android - sha256: "4e4ba0c44b5c24be20030733ada0c844aa0e8f1963f5d7cd72f5b2fe54a61495" + sha256: "3df10961e3a9e968a5e076fe27e7f4741fa8a1d3950bdeb48cf121ed529d0caf" url: "https://pub.dev" source: hosted - version: "0.1.0" + version: "0.1.0+2" screen_brightness_ios: dependency: transitive description: @@ -1156,90 +1092,90 @@ packages: dependency: transitive description: name: shared_preferences - sha256: ee6257848f822b8481691f20c3e6d2bfee2e9eccb2a3d249907fcfb198c55b41 + sha256: "16d3fb6b3692ad244a695c0183fca18cf81fd4b821664394a781de42386bf022" url: "https://pub.dev" source: hosted - version: "2.0.18" + version: "2.1.1" shared_preferences_android: dependency: transitive description: name: shared_preferences_android - sha256: a51a4f9375097f94df1c6e0a49c0374440d31ab026b59d58a7e7660675879db4 + sha256: "6478c6bbbecfe9aced34c483171e90d7c078f5883558b30ec3163cf18402c749" url: "https://pub.dev" source: hosted - version: "2.0.16" + version: "2.1.4" shared_preferences_foundation: dependency: transitive description: name: shared_preferences_foundation - sha256: "6b84fdf06b32bb336f972d373cd38b63734f3461ba56ac2ba01b56d052796259" + sha256: e014107bb79d6d3297196f4f2d0db54b5d1f85b8ea8ff63b8e8b391a02700feb url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.2" shared_preferences_linux: dependency: transitive description: name: shared_preferences_linux - sha256: d7fb71e6e20cd3dfffcc823a28da3539b392e53ed5fc5c2b90b55fdaa8a7e8fa + sha256: "9d387433ca65717bbf1be88f4d5bb18f10508917a8fa2fb02e0fd0d7479a9afa" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" shared_preferences_platform_interface: dependency: transitive description: name: shared_preferences_platform_interface - sha256: "824bfd02713e37603b2bdade0842e47d56e7db32b1dcdd1cae533fb88e2913fc" + sha256: fb5cf25c0235df2d0640ac1b1174f6466bd311f621574997ac59018a6664548d url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.2.0" shared_preferences_web: dependency: transitive description: name: shared_preferences_web - sha256: "6737b757e49ba93de2a233df229d0b6a87728cea1684da828cbc718b65dcf9d7" + sha256: "74083203a8eae241e0de4a0d597dbedab3b8fef5563f33cf3c12d7e93c655ca5" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.1.0" shared_preferences_windows: dependency: transitive description: name: shared_preferences_windows - sha256: bd014168e8484837c39ef21065b78f305810ceabc1d4f90be6e3b392ce81b46d + sha256: "5e588e2efef56916a3b229c3bfe81e6a525665a454519ca51dbcc4236a274173" url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.2.0" shelf: dependency: transitive description: name: shelf - sha256: c24a96135a2ccd62c64b69315a14adc5c3419df63b4d7c05832a346fdb73682c + sha256: ad29c505aee705f41a4d8963641f91ac4cee3c8fad5947e033390a7bd8180fa4 url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" shelf_packages_handler: dependency: transitive description: name: shelf_packages_handler - sha256: aef74dc9195746a384843102142ab65b6a4735bb3beea791e63527b88cc83306 + sha256: "89f967eca29607c933ba9571d838be31d67f53f6e4ee15147d5dc2934fee1b1e" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.0.2" shelf_static: dependency: transitive description: name: shelf_static - sha256: e792b76b96a36d4a41b819da593aff4bdd413576b3ba6150df5d8d9996d2e74c + sha256: a41d3f53c4adf0f57480578c1d61d90342cd617de7fc8077b1304643c2d85c1e url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" shelf_web_socket: dependency: transitive description: name: shelf_web_socket - sha256: a988c0e8d8ffbdb8a28aa7ec8e449c260f3deb808781fe1284d22c5bba7156e8 + sha256: "9ca081be41c60190ebcb4766b2486a7d50261db7bd0f5d9615f2d653637a84c1" url: "https://pub.dev" source: hosted - version: "1.0.3" + version: "1.0.4" shimmer: dependency: "direct main" description: @@ -1257,10 +1193,10 @@ packages: dependency: transitive description: name: source_gen - sha256: c2bea18c95cfa0276a366270afaa2850b09b4a76db95d546f3d003dcc7011298 + sha256: "373f96cf5a8744bc9816c1ff41cf5391bbdbe3d7a96fe98c622b6738a8a7bd33" url: "https://pub.dev" source: hosted - version: "1.2.7" + version: "1.3.2" source_helper: dependency: transitive description: @@ -1297,18 +1233,18 @@ packages: dependency: transitive description: name: sqflite - sha256: "851d5040552cf911f4cabda08d003eca76b27da3ed0002978272e27c8fbf8ecc" + sha256: b4d6710e1200e96845747e37338ea8a819a12b51689a3bcf31eff0003b37a0b9 url: "https://pub.dev" source: hosted - version: "2.2.5" + version: "2.2.8+4" sqflite_common: dependency: transitive description: name: sqflite_common - sha256: bfd6973aaeeb93475bc0d875ac9aefddf7965ef22ce09790eb963992ffc5183f + sha256: e77abf6ff961d69dfef41daccbb66b51e9983cdd5cb35bf30733598057401555 url: "https://pub.dev" source: hosted - version: "2.4.2+2" + version: "2.4.5" stack_trace: dependency: transitive description: @@ -1345,18 +1281,18 @@ packages: dependency: "direct dev" description: name: swagger_dart_code_generator - sha256: cd2efc34dbc3f63cb81ba49048fa56c6307a580376104eb756496e08604e27bc + sha256: de3846fca5f0ac1ea0fbd30ec0fbf0e835b917dd5e039afa4222c40101715960 url: "https://pub.dev" source: hosted - version: "2.10.2" + version: "2.11.0" synchronized: dependency: transitive description: name: synchronized - sha256: "33b31b6beb98100bf9add464a36a8dd03eb10c7a8cf15aeec535e9b054aaf04b" + sha256: "5fcbd27688af6082f5abd611af56ee575342c30e87541d0245f7ff99faa02c60" url: "https://pub.dev" source: hosted - version: "3.0.1" + version: "3.1.0" term_glyph: dependency: transitive description: @@ -1369,26 +1305,26 @@ packages: dependency: transitive description: name: test - sha256: a5fcd2d25eeadbb6589e80198a47d6a464ba3e2049da473943b8af9797900c2d + sha256: "3dac9aecf2c3991d09b9cdde4f98ded7b30804a88a0d7e4e7e1678e78d6b97f4" url: "https://pub.dev" source: hosted - version: "1.22.0" + version: "1.24.1" test_api: dependency: transitive description: name: test_api - sha256: ad540f65f92caa91bf21dfc8ffb8c589d6e4dc0c2267818b4cc2792857706206 + sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb url: "https://pub.dev" source: hosted - version: "0.4.16" + version: "0.5.1" test_core: dependency: transitive description: name: test_core - sha256: "0ef9755ec6d746951ba0aabe62f874b707690b5ede0fecc818b138fcc9b14888" + sha256: "5138dbffb77b2289ecb12b81c11ba46036590b72a64a7a90d6ffb880f1a29e93" url: "https://pub.dev" source: hosted - version: "0.4.20" + version: "0.5.1" timing: dependency: transitive description: @@ -1401,10 +1337,10 @@ packages: dependency: transitive description: name: typed_data - sha256: "26f87ade979c47a150c9eaab93ccd2bebe70a27dc0b4b29517f2904f04eb11a5" + sha256: facc8d6582f16042dd49f2463ff1bd6e2c9ef9f3d5da3d9b087e244a7b564b3c url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" universal_io: dependency: transitive description: @@ -1417,50 +1353,50 @@ packages: dependency: "direct main" description: name: upgrader - sha256: "661d8f4510a626924b1b64e84a3d6694b893a0c8573445e5bef549d43020ebd7" + sha256: "0e5d309a4045a340098ecf48a6c225b484e1cf6939de90839d51dc42612fdc7d" url: "https://pub.dev" source: hosted - version: "4.11.1" + version: "7.0.0" url_launcher: dependency: "direct main" description: name: url_launcher - sha256: "4f0d5f9bf7efba3da5a7ff03bd33cc898c84bac978c068e1c94483828e709592" + sha256: eb1e00ab44303d50dd487aab67ebc575456c146c6af44422f9c13889984c00f3 url: "https://pub.dev" source: hosted - version: "6.1.5" + version: "6.1.11" url_launcher_android: dependency: transitive description: name: url_launcher_android - sha256: "1f4d9ebe86f333c15d318f81dcdc08b01d45da44af74552608455ebdc08d9732" + sha256: "7aac14be5f4731b923cc697ae2d42043945076cd0dbb8806baecc92c1dc88891" url: "https://pub.dev" source: hosted - version: "6.0.24" + version: "6.0.33" url_launcher_ios: dependency: transitive description: name: url_launcher_ios - sha256: c9cd648d2f7ab56968e049d4e9116f96a85517f1dd806b96a86ea1018a3a82e5 + sha256: "9af7ea73259886b92199f9e42c116072f05ff9bea2dcb339ab935dfc957392c2" url: "https://pub.dev" source: hosted - version: "6.1.1" + version: "6.1.4" url_launcher_linux: dependency: transitive description: name: url_launcher_linux - sha256: e29039160ab3730e42f3d811dc2a6d5f2864b90a70fb765ea60144b03307f682 + sha256: "207f4ddda99b95b4d4868320a352d374b0b7e05eefad95a4a26f57da413443f5" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" url_launcher_macos: dependency: transitive description: name: url_launcher_macos - sha256: "2dddb3291a57b074dade66b5e07e64401dd2487caefd4e9e2f467138d8c7eb06" + sha256: "91ee3e75ea9dadf38036200c5d3743518f4a5eb77a8d13fda1ee5764373f185e" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" url_launcher_platform_interface: dependency: transitive description: @@ -1473,18 +1409,18 @@ packages: dependency: transitive description: name: url_launcher_web - sha256: "574cfbe2390666003c3a1d129bdc4574aaa6728f0c00a4829a81c316de69dd9b" + sha256: "81fe91b6c4f84f222d186a9d23c73157dc4c8e1c71489c4d08be1ad3b228f1aa" url: "https://pub.dev" source: hosted - version: "2.0.15" + version: "2.0.16" url_launcher_windows: dependency: transitive description: name: url_launcher_windows - sha256: "97c9067950a0d09cbd93e2e3f0383d1403989362b97102fbf446473a48079a4b" + sha256: "254708f17f7c20a9c8c471f67d86d76d4a3f9c1591aad1e15292008aceb82771" url: "https://pub.dev" source: hosted - version: "3.0.4" + version: "3.0.6" uuid: dependency: transitive description: @@ -1513,26 +1449,26 @@ packages: dependency: transitive description: name: vm_service - sha256: e7fb6c2282f7631712b69c19d1bff82f3767eea33a2321c14fa59ad67ea391c7 + sha256: f3743ca475e0c9ef71df4ba15eb2d7684eecd5c8ba20a462462e4e8b561b2e11 url: "https://pub.dev" source: hosted - version: "9.4.0" + version: "11.6.0" watcher: dependency: transitive description: name: watcher - sha256: "6a7f46926b01ce81bfc339da6a7f20afbe7733eff9846f6d6a5466aa4c6667c0" + sha256: "3d2ad6751b3c16cf07c7fca317a1413b3f26530319181b37e3b9039b84fc01d8" url: "https://pub.dev" source: hosted - version: "1.0.2" + version: "1.1.0" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: ca49c0bc209c687b887f30527fb6a9d80040b072cc2990f34b9bec3e7663101b + sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b url: "https://pub.dev" source: hosted - version: "2.3.0" + version: "2.4.0" webkit_inspection_protocol: dependency: transitive description: @@ -1545,10 +1481,18 @@ packages: dependency: transitive description: name: win32 - sha256: c9ebe7ee4ab0c2194e65d3a07d8c54c5d00bb001b76081c4a04cdb8448b59e46 + sha256: "5a751eddf9db89b3e5f9d50c20ab8612296e4e8db69009788d6c8b060a84191c" url: "https://pub.dev" source: hosted - version: "3.1.3" + version: "4.1.4" + win32_registry: + dependency: transitive + description: + name: win32_registry + sha256: "1c52f994bdccb77103a6231ad4ea331a244dbcef5d1f37d8462f713143b0bfae" + url: "https://pub.dev" + source: hosted + version: "1.1.0" xdg_directories: dependency: transitive description: @@ -1561,18 +1505,18 @@ packages: dependency: transitive description: name: xml - sha256: "979ee37d622dec6365e2efa4d906c37470995871fe9ae080d967e192d88286b5" + sha256: "5bc72e1e45e941d825fd7468b9b4cc3b9327942649aeb6fc5cdbf135f0a86e84" url: "https://pub.dev" source: hosted - version: "6.2.2" + version: "6.3.0" yaml: dependency: transitive description: name: yaml - sha256: "23812a9b125b48d4007117254bca50abb6c712352927eece9e155207b1db2370" + sha256: "75769501ea3489fca56601ff33454fe45507ea3bfb014161abc3b43ae25989d5" url: "https://pub.dev" source: hosted - version: "3.1.1" + version: "3.1.2" sdks: - dart: ">=2.19.0 <3.0.0" - flutter: ">=3.7.8" + dart: ">=3.0.0 <4.0.0" + flutter: ">=3.10.0" diff --git a/pubspec.yaml b/pubspec.yaml index d3ba80e8e..f30102197 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -7,30 +7,30 @@ publish_to: "none" version: 0.0.0+1 environment: - sdk: ">=2.18.0 <3.0.0" - flutter: 3.7.8 + sdk: ">=3.0.0 <4.0.0" + flutter: 3.10.0 dependencies: flutter: sdk: flutter # ui - gap: 2.0.1 + gap: 3.0.0 shimmer: 2.0.0 animations: 2.0.7 dotted_border: 2.0.0+3 - flutter_layout_grid: 2.0.1 + flutter_layout_grid: 2.0.3 cached_network_image: 3.2.3 # Chopper api and rest client - logger: 1.2.2 - chopper: 6.1.1 + logger: 1.3.0 + chopper: 6.1.2 # storage flutter_secure_storage: 8.0.0 # dependency injection - get_it: 7.2.0 + get_it: 7.6.0 # State management flutter_bloc: 8.1.2 @@ -38,65 +38,65 @@ dependencies: stream_transform: 2.1.0 # Utility - meta: 1.8.0 + meta: 1.9.1 equatable: 2.0.5 - collection: 1.17.0 + collection: 1.17.1 # Internationalization and date formatting - intl: 0.18.0 + intl: 0.18.1 # Url Launcher - url_launcher: 6.1.5 + url_launcher: 6.1.11 # crypto - crypto: 3.0.2 + crypto: 3.0.3 # rest - http: 0.13.5 + http: 0.13.6 # brightness screen_brightness: 0.2.2 # firebase - firebase_core: 2.7.0 - firebase_analytics: 10.1.4 - firebase_crashlytics: 3.0.15 - firebase_performance: 0.9.0+14 + firebase_core: 2.12.0 + firebase_analytics: 10.4.0 + firebase_crashlytics: 3.3.0 + firebase_performance: 0.9.2 # functional programming thingies fpdart: 0.6.0 # Upgrade notifier - upgrader: 4.11.1 + upgrader: 7.0.0 # env - envied: 0.3.0 + envied: 0.3.0+3 dev_dependencies: flutter_test: sdk: flutter - build_runner: 2.3.3 + build_runner: 2.4.4 - flutter_launcher_icons: 0.10.0 - flutter_native_splash: 2.2.16 + flutter_launcher_icons: 0.13.1 + flutter_native_splash: 2.2.19 # unit testing - mockito: 5.3.2 + mockito: 5.4.0 bloc_test: 9.1.1 # Chopper api and rest client - chopper_generator: 6.0.0 - json_serializable: 6.6.1 - swagger_dart_code_generator: 2.10.2 + chopper_generator: 6.0.1 + json_serializable: 6.6.2 + swagger_dart_code_generator: 2.11.0 # linter - lint: 2.0.1 - dart_code_metrics: 5.7.0 - dart_code_metrics_presets: 1.5.0 + lint: 2.1.2 + dart_code_metrics: 5.7.3 + dart_code_metrics_presets: 1.7.0 # env - envied_generator: 0.3.0 + envied_generator: 0.3.0+3 # update splash screen: # flutter pub run flutter_native_splash:create diff --git a/test/features/receipt/data/repositories/receipt_repository_impl_test.dart b/test/features/receipt/data/repositories/receipt_repository_impl_test.dart index 2332377fe..08b6fbc51 100644 --- a/test/features/receipt/data/repositories/receipt_repository_impl_test.dart +++ b/test/features/receipt/data/repositories/receipt_repository_impl_test.dart @@ -1,8 +1,7 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/features/receipt/data/datasources/receipt_remote_data_source.dart'; import 'package:coffeecard/features/receipt/data/repositories/receipt_repository_impl.dart'; -import 'package:coffeecard/features/receipt/domain/entities/purchase_receipt.dart'; -import 'package:coffeecard/features/receipt/domain/entities/swipe_receipt.dart'; +import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/domain/repositories/receipt_repository.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fpdart/fpdart.dart'; diff --git a/test/features/receipt/presentation/cubit/receipt_cubit_test.dart b/test/features/receipt/presentation/cubit/receipt_cubit_test.dart index f807ff4cb..3f95edad1 100644 --- a/test/features/receipt/presentation/cubit/receipt_cubit_test.dart +++ b/test/features/receipt/presentation/cubit/receipt_cubit_test.dart @@ -1,6 +1,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/features/receipt/domain/entities/swipe_receipt.dart'; +import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/domain/usecases/get_receipts.dart'; import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart'; import 'package:flutter_test/flutter_test.dart'; diff --git a/test/features/ticket/presentation/cubit/tickets_cubit_test.dart b/test/features/ticket/presentation/cubit/tickets_cubit_test.dart index 5072d714b..bde4ecc91 100644 --- a/test/features/ticket/presentation/cubit/tickets_cubit_test.dart +++ b/test/features/ticket/presentation/cubit/tickets_cubit_test.dart @@ -1,6 +1,6 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/features/receipt/domain/entities/placeholder_receipt.dart'; +import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/ticket/domain/usecases/consume_ticket.dart'; import 'package:coffeecard/features/ticket/domain/usecases/load_tickets.dart'; import 'package:coffeecard/features/ticket/presentation/cubit/tickets_cubit.dart'; diff --git a/test/widgets/components/goldens/error_section.png b/test/widgets/components/goldens/error_section.png index 4fdda1d577c44e0d1142c2af162a5554dfc07bd6..e637d9a641040030fca640a0aa865df215eed4eb 100644 GIT binary patch delta 1843 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 iy!AVpkA_k{npvMvH}fL9-q%zHAnt7l*t zR1jgoX0^~l3&H+tV&ws30{0YiUHy}JL#ms}Zq9ia0001i8*3i`003-7a z0D#QdsUPt9(|-K;{_W`9lc&$e*Q+nt?)>8MqB~F7zIy#;+`M&r(K7*nZMnYsF#i7e zb@cB3qs#I8=XJKbbN9ibJ5Sjzz4iR^*|_)c;^^HNV|;x7Zqcs(j%)mmFJ8V~wENE+ zlg|MzlfD5J46~ba9@ahr007tqZXWOMhRF5wmyQLjZbU0 zh$4ZYO|&pVECL3_W}3CNoeg|b>^c2E+{;uSGplKuHUj_vAaG~t0{{R3yF-)V0#kox z@3s&4^kF@|fBQOnclhXNT>ZG5y?c84e0(`S%XW{S9B*~MF~)fP=IvHH0ATlAoWCEx zuCHeA?(ZLr-#6E@cMp$F#?Q-(Z1>>caI5>>X8Y{Lt8w?<{_Ne~KR4s^$9J=LOWbjZ z_jq#pa`x_jzJLG#02u)S003kJ2mq7r0yYhq)ih0;r4IoB0PF@|lTiUaldl048MC)u zua`ap006KfR?{?XmOcak0I(bUf(5ft1N8~B@B&K#7XATN=u6RK7Aju=0000u8&Vio} zFpkr^PMe)^0`tV8VksvKT~!FlN%2}j<*L!QDWuG zC$U>lB8{?MJlPh}#IE%5kzA3)n-7A+vlbY&0VRTn}ltW8|mRx54)j@JT z^`8V3fWi_t+D>EgrKR zxVj>`n7Z}U5-~vG4Xwmx&KFy_fLtJLs z`}W!e09Fr8q3(BEy$Ec@ZPR@Yw~nn#s82PH${sZIEoid_5u_+zo?r`0#?Q-Q2M>Ag zwgv#%c=6%GU_*_MiRIrwaofB{B9rJZ7vyXn_2u&>jhbsrX#QWV_qbLO7 z+}W5Zc0)rrR+~y-GwV!icW!YJUngNnyH-vr58+OuVT{`Z{qOn(RlcDJHLTUq?&Edy zUivnpQ1NqyaoC(rFkR@;8fe1=oTk^yT}{RUz)Feb#>V(*L$_8?NenfKj#G?_iDQnkM>A|u9S!wLZUq02PO0ga{! zhtpIh$o&_*RLVi&clmREmZAsMzwH2^cLIv$s>-4|gb`dnQVI_jF`LJdoek92ZJw?D z0Dw?EhS7*}#5dZF-Ip#Uaw1Vq%nIe^jp@SfZUzCsj z)RuZBo7|~wWwNGLL?JQ~IyM@#G|F_}p!kG_6ahiiY zMz+3HWz43%kksglWtB*{Vu8+hG`=XBa*&D|1F_mFlh2Qbi7D~Ck&Ku<1AL40n{wzL zYGLOZc3p@zICO`abiG`3=@kiUkSvC5c@Dt06s*a-8V_f82J=@PM8D9f@wN#Q6O)O< zElGjOfhkyqca+I{AC{`4X!|KqO$4vIZS`A?RBL=UUaJ*6kilr>+WsJYIKsLwLk`)4!@mafU`mmKhK>x;0uw*z`UsfK3K+|aG% zG>$$Tuc&^!b1$4w7qhuuDQsnpwmSkvh2l-#xQA)-Hj(&7w-u!{hu~7Y92U_C3wY-` zd;=s{?>>m#z+by91|F6E7G@jb4_U;b>;3*k`LKD2)DK(uWgaUA9*E_q{ zH0^v8WV#-61F{;TT=Q1_c$@f4Xj>G!BXrQD;qba(=s N{zvdh&WYG7{|BTg?t1_L delta 1899 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 NIU9b4@m<2t{{eGuytx1X delta 1842 zcmV-22hI4~CGI7VL4TM@L_t(|obBCTh@E$x$MNrT=FFei2ns1QHi1N1utF)gxao z{F(FYMLN^iOfu)pnS7>mp4STklQZWz&o2iiocH{m-}7rG7Y~2i761T1KySYX0001V zhm%nVAbD9>`eafsjyLM1jg1<&ZH?QuE>0#@Rdu-6tA5i|ziH~tOG|a6zf@~I zEe8MqJ85i0_^NT+*7?z>UKowHVq;o|d%e1OxnIYYm#g12H)wdmp=$JybqUL22ieLMgF9CV!tkj-@;v)Y{fsHz%|$6K=?ul?S%Zq%Z8 z001}$Iujtf+2iBNz-ZgHMx#-+ZF^OOe1G%vUz_?h$8B5ZhSRS<0RVtS(un}s+-PuG zAAQ>gx8md8d+*%}j{X&UdR!SIk3IaunPx-IkLKU-5C8xUqPZ5KU%Iqj&z-+8_45aV zK~-C|y0$v?F}EH&T89p;?PhpeuC?&|1IOM~8yg#Qz4mKOyD)v$1?gXT;J@m}kAMDb zx5KJW-}kBd?&*i;IR2CM-P7OS_3hVJNH*T?_Imp)S;{>TUC8pA)mW4`14EcjWdT8y6sx@WF^K%P5) zp+52P`}S%q+xo#Hr|Y)cPtNrk*MD00z1Q(Eix3w*8cm>(>|;001nih2IJNh4J-gIUc|8MBRP#R9(7s>GCCFbNblZ@Av1v}WDa{Ce&> z^^U2Y=T2Av0Cdg52gocR^1pEU%QtW&R}&xr0Cdg52gtP+pl|2BA9?=`9Ld#Y85;lq zEP{n!guYu%(_HuOJ3S5n0DoL<;b%Y&_h$Pie)C7)+4Z>|{o~KKzJC0|6FaUenRRRc z0C3PP{0vBc#u%Pf_t8@`?I&_`Uw{ArusbUT@}G-nMOf)eOkynvm@tpY?87hkLzRvj`mk zfR0)COR%agUxsyYIDg#nc~AcOOjT8#Idf*_bKHOb{q^9357x%UMlDToY1NT_C*FDh zfCKC4Cx5cg=i4vM)d_o8=w1HRb6}Y!7JBwJoC&m+dk)^haRe`s%L+{{r9V? z?pRwj`vCwzXLVu?$d#TQ4D0MR3(_~&f9#d*)`6T@S*eq&D|>n#001~hIx_>Z>E!B4 z-Lic39jN}{-GA@gvvd6Wx4wAQW4A2##RmWYo!7Y;kgCgf#d>NmsOLxHou9l1ZdvZv z?W-=m0sw$cZbor=J;++EHQQf~wU=CW1polA>STayI=Q-1hk8vtH5l%=N64%;7ronX z5C8yP)%j1yw=6H$k$%6<4M%l;G@3g^HuoHv_qM+P09vq!I=KeqTE=Z#=Z2$taXhYz zlgUiS9`5z(NWU*U007`R1jse4PbT%kc%xq4*r;*a*0^oAqU3O|SN*1`e$&*OmX_+~ z{!*>=I(I`c005Yo00961V*J`3lW_+g81#Sfo0s={3;+N?M>NCX&^{ml06>IGu(PoT g6bq9u2vHXPA6zS~D0zpI4*&oF07*qoM6N<$f|*;)(*OVf diff --git a/test/widgets/components/stats/goldens/stat_card.png b/test/widgets/components/stats/goldens/stat_card.png index 5e15da5899ad9d840921b515104dc660eacec28a..a0a978f087ecc4dcb5ce1b253afeab93c77a8167 100644 GIT binary patch literal 3490 zcmeAS@N?(olHy`uVBq!ia0y~yU{+vYV2a>i1B%QlYbpRzjKx9jP7LeL$-D$|Sc;uI zLpXq-h9ji|sL99E#WAE}&f7bReldX}4G*n(trZnjTc$2B$XT#ym9GGY2$SO_EtN%L zfg+yb2lyHsJOpwc&s@1qoB6YU!~4(a?8D{`Bb2&Eoidb<*j}?Zs#OwmaunZ&RI@ zUY@_u_PE63SI;sR?mXT7xv*x(|2I2BZ5}V$od2>ursUi8_x8!P$;Ate85w>|mlj}P zP*8RNIv&t8_f@0BXZlLvVz^BRRAGpe2JL^T?8qd~_&r2=X+ zEsdt7(X>Q)ML(L4M)T2VJ{s_RbS-QzFc$WN_w$!O{`akFWqQrMTXL_ZXM5W|Hsu61 zl_MtozWo~5%-(;+n6J<8UPwa)D>vR&G`zw5z;@Rji}+vZ71`nD{`#}ZnW@n`0K0a> zj^@RaH*Vg&{gvsz*@#B|nVeb6ey*AhY^r~M@$q5ao%?b6Uwh6+-v)IER;KS!19wmM kGyyv-ly*|UW!Fz`UbUD5A&)BO0t1l2)78&qol`;+0P|P^*#H0l literal 3490 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 From ceebec7039a13b9246b9fea442b439044a849ac9 Mon Sep 17 00:00:00 2001 From: "codesee-maps[bot]" <86324825+codesee-maps[bot]@users.noreply.github.com> Date: Sun, 21 May 2023 20:58:40 +0000 Subject: [PATCH 10/32] Install the CodeSee workflow (#467) Learn more at https://docs.codesee.io Co-authored-by: codesee-maps[bot] <86324825+codesee-maps[bot]@users.noreply.github.com> --- .github/workflows/codesee-arch-diagram.yml | 23 ++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 .github/workflows/codesee-arch-diagram.yml diff --git a/.github/workflows/codesee-arch-diagram.yml b/.github/workflows/codesee-arch-diagram.yml new file mode 100644 index 000000000..008661e2d --- /dev/null +++ b/.github/workflows/codesee-arch-diagram.yml @@ -0,0 +1,23 @@ +# This workflow was added by CodeSee. Learn more at https://codesee.io/ +# This is v2.0 of this workflow file +on: + push: + branches: + - develop + pull_request_target: + types: [opened, synchronize, reopened] + +name: CodeSee + +permissions: read-all + +jobs: + codesee: + runs-on: ubuntu-latest + continue-on-error: true + name: Analyze the repo with CodeSee + steps: + - uses: Codesee-io/codesee-action@v2 + with: + codesee-token: ${{ secrets.CODESEE_ARCH_DIAG_API_TOKEN }} + codesee-url: https://app.codesee.io From aafdb5624a363694f9cecd542767da839d473810 Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Tue, 23 May 2023 17:07:46 +0200 Subject: [PATCH 11/32] Refactor payment (#465) * Fixed an error in the MobilePay payment flow Refactored payment to a feature Added testing --------- Co-authored-by: Omid Marfavi <21163286+marfavi@users.noreply.github.com> --- android/build.gradle | 2 +- lib/base/strings.dart | 3 + lib/core/external/external_url_launcher.dart | 35 +++ lib/cubits/purchase/purchase_cubit.dart | 75 ----- .../presentation/pages/credits_page.dart | 5 +- .../widgets/contributor_card.dart | 5 +- .../purchase_remote_data_source.dart} | 23 +- .../data/models/initiate_purchase_model.dart | 28 ++ .../data/models/single_purchase_model.dart | 23 ++ .../repositories/free_product_service.dart | 34 +++ .../data/repositories/mobilepay_service.dart | 78 +++++ .../data/repositories/payment_handler.dart | 51 ++++ .../domain/entities}/initiate_purchase.dart | 23 +- .../entities/internal_payment_type.dart | 4 + .../purchase/domain/entities}/payment.dart | 18 +- .../domain/entities}/payment_status.dart | 0 .../domain/entities/single_purchase.dart | 24 ++ .../domain/usecases/init_purchase.dart | 16 ++ .../usecases/verify_purchase_status.dart | 16 ++ .../presentation/cubit/purchase_cubit.dart | 125 ++++++++ .../presentation/cubit}/purchase_state.dart | 9 + .../pages/buy_single_drink_page.dart | 4 +- .../presentation/pages/buy_tickets_page.dart | 4 +- .../presentation/widgets/error_dialog.dart | 28 ++ .../presentation/widgets/loading_dialog.dart | 28 ++ .../widgets}/purchase_overlay.dart | 29 +- .../widgets}/purchase_process.dart | 72 ++--- .../presentation/widgets/shop_section.dart | 4 +- lib/features/user/data/models/user_model.dart | 2 +- lib/features/user/domain/entities/role.dart | 7 +- lib/models/purchase/single_purchase.dart | 27 -- lib/payment/free_product_service.dart | 36 --- lib/payment/mobilepay_service.dart | 85 ------ lib/payment/payment_handler.dart | 56 ---- lib/service_locator.dart | 25 +- .../firebase_analytics_event_logging.dart | 2 +- lib/utils/launch.dart | 23 -- .../buy_ticket_bottom_modal_sheet.dart | 6 +- lib/widgets/pages/settings/settings_page.dart | 9 +- .../purchase_remote_data_source_test.dart | 114 ++++++++ .../free_product_service_test.dart | 74 +++++ .../repositories/mobilepay_service_test.dart | 113 ++++++++ .../domain/usecases/init_purchase_test.dart | 45 +++ .../usecases/verify_purchase_status_test.dart | 34 +++ .../cubit/purchase_cubit_test.dart | 266 ++++++++++++++++++ 45 files changed, 1270 insertions(+), 420 deletions(-) create mode 100644 lib/core/external/external_url_launcher.dart delete mode 100644 lib/cubits/purchase/purchase_cubit.dart rename lib/{data/repositories/v2/purchase_repository.dart => features/purchase/data/datasources/purchase_remote_data_source.dart} (62%) create mode 100644 lib/features/purchase/data/models/initiate_purchase_model.dart create mode 100644 lib/features/purchase/data/models/single_purchase_model.dart create mode 100644 lib/features/purchase/data/repositories/free_product_service.dart create mode 100644 lib/features/purchase/data/repositories/mobilepay_service.dart create mode 100644 lib/features/purchase/data/repositories/payment_handler.dart rename lib/{models/purchase => features/purchase/domain/entities}/initiate_purchase.dart (52%) create mode 100644 lib/features/purchase/domain/entities/internal_payment_type.dart rename lib/{models/purchase => features/purchase/domain/entities}/payment.dart (71%) rename lib/{models/purchase => features/purchase/domain/entities}/payment_status.dart (100%) create mode 100644 lib/features/purchase/domain/entities/single_purchase.dart create mode 100644 lib/features/purchase/domain/usecases/init_purchase.dart create mode 100644 lib/features/purchase/domain/usecases/verify_purchase_status.dart create mode 100644 lib/features/purchase/presentation/cubit/purchase_cubit.dart rename lib/{cubits/purchase => features/purchase/presentation/cubit}/purchase_state.dart (87%) rename lib/features/{ticket => purchase}/presentation/pages/buy_single_drink_page.dart (96%) rename lib/features/{ticket => purchase}/presentation/pages/buy_tickets_page.dart (96%) create mode 100644 lib/features/purchase/presentation/widgets/error_dialog.dart create mode 100644 lib/features/purchase/presentation/widgets/loading_dialog.dart rename lib/{widgets/components/purchase => features/purchase/presentation/widgets}/purchase_overlay.dart (51%) rename lib/{widgets/components/purchase => features/purchase/presentation/widgets}/purchase_process.dart (58%) delete mode 100644 lib/models/purchase/single_purchase.dart delete mode 100644 lib/payment/free_product_service.dart delete mode 100644 lib/payment/mobilepay_service.dart delete mode 100644 lib/payment/payment_handler.dart delete mode 100644 lib/utils/launch.dart create mode 100644 test/features/purchase/data/datasources/purchase_remote_data_source_test.dart create mode 100644 test/features/purchase/data/repositories/free_product_service_test.dart create mode 100644 test/features/purchase/data/repositories/mobilepay_service_test.dart create mode 100644 test/features/purchase/domain/usecases/init_purchase_test.dart create mode 100644 test/features/purchase/domain/usecases/verify_purchase_status_test.dart create mode 100644 test/features/purchase/presentation/cubit/purchase_cubit_test.dart diff --git a/android/build.gradle b/android/build.gradle index 9b38488bb..ae1a8e354 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -29,6 +29,6 @@ subprojects { project.evaluationDependsOn(':app') } -task clean(type: Delete) { +tasks.register("clean", Delete) { delete rootProject.buildDir } diff --git a/lib/base/strings.dart b/lib/base/strings.dart index e78158b60..2f651868e 100644 --- a/lib/base/strings.dart +++ b/lib/base/strings.dart @@ -169,6 +169,9 @@ abstract final class Strings { static const purchaseRejectedOrCanceledMessage = 'The payment was rejected or cancelled. No tickets have been added to your account'; static const purchaseError = "Uh oh, we couldn't complete that purchase"; + static const purchaseTimeout = 'Purchase timed out'; + static const purchaseTimeoutMessage = + 'The payment confirmation was not received in time. If you have completed the purchase in MobilePay, please wait a few minutes'; // Receipts static const receiptsPageTitle = 'Receipts'; diff --git a/lib/core/external/external_url_launcher.dart b/lib/core/external/external_url_launcher.dart new file mode 100644 index 000000000..96617a840 --- /dev/null +++ b/lib/core/external/external_url_launcher.dart @@ -0,0 +1,35 @@ +import 'package:coffeecard/base/strings.dart'; +import 'package:flutter/material.dart'; +import 'package:url_launcher/url_launcher.dart' as url_launch; + +class ExternalUrlLauncher { + Future canLaunch(Uri uri) async => url_launch.canLaunchUrl(uri); + + Future launch(Uri uri) async => url_launch.launchUrl( + uri, + mode: url_launch.LaunchMode.externalApplication, + ); + + Future launchUrlExternalApplication( + Uri url, + BuildContext context, + ) async { + if (await canLaunch(url)) { + final _ = launch(url); + return; + } + + if (context.mounted) { + final _ = showDialog( + context: context, + barrierDismissible: true, + builder: (BuildContext context) { + return const AlertDialog( + title: Text(Strings.error), + content: Text(Strings.cantLaunchUrl), + ); + }, + ); + } + } +} diff --git a/lib/cubits/purchase/purchase_cubit.dart b/lib/cubits/purchase/purchase_cubit.dart deleted file mode 100644 index dc09bd6b1..000000000 --- a/lib/cubits/purchase/purchase_cubit.dart +++ /dev/null @@ -1,75 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:coffeecard/models/purchase/payment.dart'; -import 'package:coffeecard/models/purchase/payment_status.dart'; -import 'package:coffeecard/models/ticket/product.dart'; -import 'package:coffeecard/payment/payment_handler.dart'; -import 'package:coffeecard/service_locator.dart'; -import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; -import 'package:equatable/equatable.dart'; - -part 'purchase_state.dart'; - -class PurchaseCubit extends Cubit { - final PaymentHandler paymentHandler; - final Product product; - - PurchaseCubit({required this.paymentHandler, required this.product}) - : super(const PurchaseInitial()); - - Future pay() async { - sl().beginCheckoutEvent(product); - - if (state is PurchaseInitial) { - emit(const PurchaseStarted()); - - final either = await paymentHandler.initPurchase(product.id); - - either.fold( - (error) => emit(PurchaseError(error.reason)), - (payment) { - if (payment.status == PaymentStatus.completed) { - emit(PurchaseCompleted(payment)); - } else if (payment.status == PaymentStatus.awaitingPayment) { - emit(PurchaseProcessing(payment)); - } else { - emit(PurchasePaymentRejected(payment)); - } - }, - ); - } - } - - /// Verifies the status of the current purchase - /// Only checks the status of the purchase if the state is PurchaseProcessing - Future verifyPurchase() async { - if (state is PurchaseProcessing) { - final payment = (state as PurchaseProcessing).payment; - emit(PurchaseVerifying(payment)); - final either = await paymentHandler.verifyPurchase(payment.id); - - either.fold( - (error) => emit(PurchaseError(error.reason)), - (status) { - if (status == PaymentStatus.completed) { - sl().purchaseCompletedEvent(payment); - emit(PurchaseCompleted(payment.copyWith(status: status))); - } else if (status == PaymentStatus.reserved) { - // NOTE, recursive call, potentially infinite. - // If payment has been reserved, i.e. approved by user - // we will keep checking the backend to verify payment has been captured - - // Emit processing state to allow the verifyPurchase process again - emit( - PurchaseProcessing( - payment.copyWith(status: status), - ), - ); - verifyPurchase(); - } else { - emit(PurchasePaymentRejected(payment.copyWith(status: status))); - } - }, - ); - } - } -} diff --git a/lib/features/contributor/presentation/pages/credits_page.dart b/lib/features/contributor/presentation/pages/credits_page.dart index 5f31c6dfc..97c7856db 100644 --- a/lib/features/contributor/presentation/pages/credits_page.dart +++ b/lib/features/contributor/presentation/pages/credits_page.dart @@ -1,11 +1,11 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/core/external/external_url_launcher.dart'; import 'package:coffeecard/features/contributor/presentation/cubit/contributor_cubit.dart'; import 'package:coffeecard/features/contributor/presentation/widgets/contributor_card.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/api_uri_constants.dart'; -import 'package:coffeecard/utils/launch.dart'; import 'package:coffeecard/widgets/components/images/analogio_logo.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:coffeecard/widgets/components/section_title.dart'; @@ -76,7 +76,8 @@ class CreditsPage extends StatelessWidget { ), SettingListEntry( name: Strings.github, - onTap: () => launchUrlExternalApplication( + onTap: () => + sl().launchUrlExternalApplication( ApiUriConstants.analogIOGitHub, context, ), diff --git a/lib/features/contributor/presentation/widgets/contributor_card.dart b/lib/features/contributor/presentation/widgets/contributor_card.dart index 7b80dbd69..cd3265a39 100644 --- a/lib/features/contributor/presentation/widgets/contributor_card.dart +++ b/lib/features/contributor/presentation/widgets/contributor_card.dart @@ -1,8 +1,9 @@ import 'package:cached_network_image/cached_network_image.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/core/external/external_url_launcher.dart'; import 'package:coffeecard/features/contributor/domain/entities/contributor.dart'; -import 'package:coffeecard/utils/launch.dart'; +import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/widgets/components/helpers/tappable.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; @@ -15,7 +16,7 @@ class ContributorCard extends StatelessWidget { @override Widget build(BuildContext context) { return Tappable( - onTap: () => launchUrlExternalApplication( + onTap: () => sl().launchUrlExternalApplication( Uri.parse(contributor.githubUrl), context, ), diff --git a/lib/data/repositories/v2/purchase_repository.dart b/lib/features/purchase/data/datasources/purchase_remote_data_source.dart similarity index 62% rename from lib/data/repositories/v2/purchase_repository.dart rename to lib/features/purchase/data/datasources/purchase_remote_data_source.dart index 78cff68f2..07e01269d 100644 --- a/lib/data/repositories/v2/purchase_repository.dart +++ b/lib/features/purchase/data/datasources/purchase_remote_data_source.dart @@ -1,12 +1,15 @@ import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/either_extensions.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/purchase/data/models/initiate_purchase_model.dart'; +import 'package:coffeecard/features/purchase/data/models/single_purchase_model.dart'; +import 'package:coffeecard/features/purchase/domain/entities/initiate_purchase.dart'; +import 'package:coffeecard/features/purchase/domain/entities/single_purchase.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; -import 'package:coffeecard/models/purchase/initiate_purchase.dart'; -import 'package:coffeecard/models/purchase/single_purchase.dart'; import 'package:fpdart/fpdart.dart'; -class PurchaseRepository { - PurchaseRepository({ +class PurchaseRemoteDataSource { + PurchaseRemoteDataSource({ required this.apiV2, required this.executor, }); @@ -20,26 +23,22 @@ class PurchaseRepository { int productId, PaymentType paymentType, ) async { - final result = await executor( + return executor( () => apiV2.apiV2PurchasesPost( body: InitiatePurchaseRequest( productId: productId, paymentType: paymentTypeToJson(paymentType), ), ), - ); - - return result.map(InitiatePurchase.fromDto); + ).bindFuture(InitiatePurchaseModel.fromDto); } /// Get a purchase by its purchase id Future> getPurchase( int purchaseId, ) async { - final result = await executor( + return executor( () => apiV2.apiV2PurchasesIdGet(id: purchaseId), - ); - - return result.map(SinglePurchase.fromDto); + ).bindFuture(SinglePurchaseModel.fromDto); } } diff --git a/lib/features/purchase/data/models/initiate_purchase_model.dart b/lib/features/purchase/data/models/initiate_purchase_model.dart new file mode 100644 index 000000000..856b75e1d --- /dev/null +++ b/lib/features/purchase/data/models/initiate_purchase_model.dart @@ -0,0 +1,28 @@ +import 'package:coffeecard/features/purchase/domain/entities/initiate_purchase.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; + +class InitiatePurchaseModel extends InitiatePurchase { + const InitiatePurchaseModel({ + required super.id, + required super.totalAmount, + required super.paymentDetails, + required super.productId, + required super.productName, + required super.purchaseStatus, + required super.dateCreated, + }); + + factory InitiatePurchaseModel.fromDto(InitiatePurchaseResponse dto) { + return InitiatePurchaseModel( + id: dto.id, + totalAmount: dto.totalAmount, + paymentDetails: MobilePayPaymentDetails.fromJsonFactory( + dto.paymentDetails as Map, + ), + productId: dto.productId, + productName: dto.productName, + purchaseStatus: dto.purchaseStatus as String, + dateCreated: dto.dateCreated, + ); + } +} diff --git a/lib/features/purchase/data/models/single_purchase_model.dart b/lib/features/purchase/data/models/single_purchase_model.dart new file mode 100644 index 000000000..409e8dae8 --- /dev/null +++ b/lib/features/purchase/data/models/single_purchase_model.dart @@ -0,0 +1,23 @@ +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/features/purchase/domain/entities/single_purchase.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; + +class SinglePurchaseModel extends SinglePurchase { + const SinglePurchaseModel({ + required super.id, + required super.totalAmount, + required super.status, + required super.dateCreated, + }); + + factory SinglePurchaseModel.fromDto(SinglePurchaseResponse dto) { + return SinglePurchaseModel( + id: dto.id, + totalAmount: dto.totalAmount, + status: PaymentStatus.fromPurchaseStatus( + purchaseStatusFromJson(dto.purchaseStatus), + ), + dateCreated: dto.dateCreated, + ); + } +} diff --git a/lib/features/purchase/data/repositories/free_product_service.dart b/lib/features/purchase/data/repositories/free_product_service.dart new file mode 100644 index 000000000..df9d2818d --- /dev/null +++ b/lib/features/purchase/data/repositories/free_product_service.dart @@ -0,0 +1,34 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/either_extensions.dart'; +import 'package:coffeecard/features/purchase/data/repositories/payment_handler.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; +import 'package:fpdart/fpdart.dart'; + +class FreeProductService extends PaymentHandler { + const FreeProductService({ + required super.remoteDataSource, + required super.buildContext, + }); + + @override + Future> initPurchase(int productId) async { + return remoteDataSource + .initiatePurchase( + productId, + PaymentType.freepurchase, + ) + .bindFuture( + (purchase) => Payment( + id: purchase.id, + status: PaymentStatus.completed, + deeplink: '', + purchaseTime: purchase.dateCreated, + price: purchase.totalAmount, + productId: purchase.productId, + productName: purchase.productName, + ), + ); + } +} diff --git a/lib/features/purchase/data/repositories/mobilepay_service.dart b/lib/features/purchase/data/repositories/mobilepay_service.dart new file mode 100644 index 000000000..01eeb64db --- /dev/null +++ b/lib/features/purchase/data/repositories/mobilepay_service.dart @@ -0,0 +1,78 @@ +import 'dart:io'; + +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/either_extensions.dart'; +import 'package:coffeecard/core/external/external_url_launcher.dart'; +import 'package:coffeecard/features/purchase/data/repositories/payment_handler.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:coffeecard/utils/api_uri_constants.dart'; +import 'package:fpdart/fpdart.dart'; + +class MobilePayService extends PaymentHandler { + final ExternalUrlLauncher externalUrlLauncher; + + MobilePayService({ + required this.externalUrlLauncher, + required super.remoteDataSource, + required super.buildContext, + }); + @override + Future> initPurchase(int productId) async { + final either = await remoteDataSource + .initiatePurchase(productId, PaymentType.mobilepay) + .bindFuture( + (purchase) => Payment( + id: purchase.id, + status: PaymentStatus.awaitingPayment, + deeplink: purchase.paymentDetails.mobilePayAppRedirectUri, + purchaseTime: purchase.dateCreated, + price: purchase.totalAmount, + productId: purchase.productId, + productName: purchase.productName, + ), + ); + + return either.fold( + (error) => Left(error), + (payment) async { + await launchMobilePay(payment); + + return Right(payment); + }, + ); + } + + Future launchMobilePay(Payment payment) async { + final Uri mobilepayLink = Uri.parse(payment.deeplink); + + final canLaunch = await externalUrlLauncher.canLaunch(mobilepayLink); + + if (!canLaunch) { + final Uri url = _getAppStoreUri(); + + // MobilePay not installed, send user to appstore + if (buildContext.mounted) { + await externalUrlLauncher.launchUrlExternalApplication( + url, + buildContext, + ); + } + + return; + } + + await externalUrlLauncher.launch(mobilepayLink); + } + + Uri _getAppStoreUri() { + if (Platform.isAndroid) { + return ApiUriConstants.mobilepayAndroid; + } else if (Platform.isIOS) { + return ApiUriConstants.mobilepayIOS; + } else { + throw UnsupportedError('Unsupported platform'); + } + } +} diff --git a/lib/features/purchase/data/repositories/payment_handler.dart b/lib/features/purchase/data/repositories/payment_handler.dart new file mode 100644 index 000000000..98c854d5b --- /dev/null +++ b/lib/features/purchase/data/repositories/payment_handler.dart @@ -0,0 +1,51 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/either_extensions.dart'; +import 'package:coffeecard/features/purchase/data/datasources/purchase_remote_data_source.dart'; +import 'package:coffeecard/features/purchase/data/repositories/free_product_service.dart'; +import 'package:coffeecard/features/purchase/data/repositories/mobilepay_service.dart'; +import 'package:coffeecard/features/purchase/domain/entities/internal_payment_type.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/service_locator.dart'; +import 'package:flutter/widgets.dart'; +import 'package:fpdart/fpdart.dart'; + +abstract class PaymentHandler { + final PurchaseRemoteDataSource remoteDataSource; + final BuildContext buildContext; + + const PaymentHandler({ + required this.remoteDataSource, + required this.buildContext, + }); + + Future> initPurchase(int productId); + + static PaymentHandler createPaymentHandler( + InternalPaymentType paymentType, + BuildContext buildContext, + ) { + final repository = sl.get(); + + return switch (paymentType) { + InternalPaymentType.mobilePay => MobilePayService( + externalUrlLauncher: sl(), + remoteDataSource: repository, + buildContext: buildContext, + ), + InternalPaymentType.free => FreeProductService( + remoteDataSource: repository, + buildContext: buildContext, + ), + }; + } + + Future> verifyPurchase( + int purchaseId, + ) async { + // Call API endpoint, receive PaymentStatus + return remoteDataSource + .getPurchase(purchaseId) + .bindFuture((purchase) => purchase.status); + } +} diff --git a/lib/models/purchase/initiate_purchase.dart b/lib/features/purchase/domain/entities/initiate_purchase.dart similarity index 52% rename from lib/models/purchase/initiate_purchase.dart rename to lib/features/purchase/domain/entities/initiate_purchase.dart index 0aab6c04c..5cf434632 100644 --- a/lib/models/purchase/initiate_purchase.dart +++ b/lib/features/purchase/domain/entities/initiate_purchase.dart @@ -1,9 +1,10 @@ import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:equatable/equatable.dart'; -class InitiatePurchase { +class InitiatePurchase extends Equatable { final int id; final int totalAmount; - final Map paymentDetails; + final MobilePayPaymentDetails paymentDetails; final int productId; final String productName; final String purchaseStatus; @@ -19,12 +20,14 @@ class InitiatePurchase { required this.dateCreated, }); - InitiatePurchase.fromDto(InitiatePurchaseResponse dto) - : id = dto.id, - totalAmount = dto.totalAmount, - paymentDetails = dto.paymentDetails as Map, - productId = dto.productId, - productName = dto.productName, - purchaseStatus = dto.purchaseStatus as String, - dateCreated = dto.dateCreated; + @override + List get props => [ + id, + totalAmount, + paymentDetails, + productId, + productName, + purchaseStatus, + dateCreated, + ]; } diff --git a/lib/features/purchase/domain/entities/internal_payment_type.dart b/lib/features/purchase/domain/entities/internal_payment_type.dart new file mode 100644 index 000000000..3dddba272 --- /dev/null +++ b/lib/features/purchase/domain/entities/internal_payment_type.dart @@ -0,0 +1,4 @@ +enum InternalPaymentType { + mobilePay, + free, +} diff --git a/lib/models/purchase/payment.dart b/lib/features/purchase/domain/entities/payment.dart similarity index 71% rename from lib/models/purchase/payment.dart rename to lib/features/purchase/domain/entities/payment.dart index f6c5b08cd..a6b6ab9cc 100644 --- a/lib/models/purchase/payment.dart +++ b/lib/features/purchase/domain/entities/payment.dart @@ -1,6 +1,7 @@ -import 'package:coffeecard/models/purchase/payment_status.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:equatable/equatable.dart'; -class Payment { +class Payment extends Equatable { final int id; final PaymentStatus status; final String deeplink; @@ -9,7 +10,7 @@ class Payment { final int productId; final String productName; - Payment({ + const Payment({ required this.id, required this.price, required this.purchaseTime, @@ -38,4 +39,15 @@ class Payment { productName: productName ?? this.productName, ); } + + @override + List get props => [ + id, + status, + deeplink, + price, + purchaseTime, + productId, + productName, + ]; } diff --git a/lib/models/purchase/payment_status.dart b/lib/features/purchase/domain/entities/payment_status.dart similarity index 100% rename from lib/models/purchase/payment_status.dart rename to lib/features/purchase/domain/entities/payment_status.dart diff --git a/lib/features/purchase/domain/entities/single_purchase.dart b/lib/features/purchase/domain/entities/single_purchase.dart new file mode 100644 index 000000000..6a47b6574 --- /dev/null +++ b/lib/features/purchase/domain/entities/single_purchase.dart @@ -0,0 +1,24 @@ +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:equatable/equatable.dart'; + +class SinglePurchase extends Equatable { + final int id; + final int totalAmount; + final PaymentStatus status; + final DateTime dateCreated; + + const SinglePurchase({ + required this.id, + required this.totalAmount, + required this.status, + required this.dateCreated, + }); + + @override + List get props => [ + id, + totalAmount, + status, + dateCreated, + ]; +} diff --git a/lib/features/purchase/domain/usecases/init_purchase.dart b/lib/features/purchase/domain/usecases/init_purchase.dart new file mode 100644 index 000000000..4ffe1cbcf --- /dev/null +++ b/lib/features/purchase/domain/usecases/init_purchase.dart @@ -0,0 +1,16 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/features/purchase/data/repositories/payment_handler.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:fpdart/fpdart.dart'; + +class InitPurchase implements UseCase { + final PaymentHandler paymentHandler; + + InitPurchase({required this.paymentHandler}); + + @override + Future> call(int productId) { + return paymentHandler.initPurchase(productId); + } +} diff --git a/lib/features/purchase/domain/usecases/verify_purchase_status.dart b/lib/features/purchase/domain/usecases/verify_purchase_status.dart new file mode 100644 index 000000000..b58d09d90 --- /dev/null +++ b/lib/features/purchase/domain/usecases/verify_purchase_status.dart @@ -0,0 +1,16 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/features/purchase/data/repositories/payment_handler.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:fpdart/fpdart.dart'; + +class VerifyPurchaseStatus implements UseCase { + final PaymentHandler paymentHandler; + + VerifyPurchaseStatus({required this.paymentHandler}); + + @override + Future> call(int paymentId) { + return paymentHandler.verifyPurchase(paymentId); + } +} diff --git a/lib/features/purchase/presentation/cubit/purchase_cubit.dart b/lib/features/purchase/presentation/cubit/purchase_cubit.dart new file mode 100644 index 000000000..77cd947f0 --- /dev/null +++ b/lib/features/purchase/presentation/cubit/purchase_cubit.dart @@ -0,0 +1,125 @@ +import 'package:bloc/bloc.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/features/purchase/domain/usecases/init_purchase.dart'; +import 'package:coffeecard/features/purchase/domain/usecases/verify_purchase_status.dart'; +import 'package:coffeecard/models/ticket/product.dart'; +import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; +import 'package:equatable/equatable.dart'; + +part 'purchase_state.dart'; + +class PurchaseCubit extends Cubit { + final Product product; + final InitPurchase initPurchase; + final VerifyPurchaseStatus verifyPurchaseStatus; + final FirebaseAnalyticsEventLogging firebaseAnalyticsEventLogging; + + PurchaseCubit({ + required this.product, + required this.initPurchase, + required this.verifyPurchaseStatus, + required this.firebaseAnalyticsEventLogging, + }) : super(const PurchaseInitial()); + + /// Initialise a purchase of [product] + Future pay() async { + firebaseAnalyticsEventLogging.beginCheckoutEvent(product); + + if (state is! PurchaseInitial) { + return; + } + + emit(const PurchaseStarted()); + + final either = await initPurchase(product.id); + + either.fold( + (error) => emit(PurchaseError(error.reason)), + (payment) { + if (payment.status == PaymentStatus.completed) { + emit(PurchaseCompleted(payment)); + } else if (payment.status == PaymentStatus.awaitingPayment) { + emit(PurchaseProcessing(payment)); + } else { + emit(PurchasePaymentRejected(payment)); + } + }, + ); + } + + /// Verify the status of the current purchase + Future verifyPurchase() async { + if (state is! PurchaseProcessing) { + return; + } + + final payment = (state as PurchaseProcessing).payment; + + emit(PurchaseVerifying(payment)); + + checkPurchaseStatus( + payment, + () => validatePurchaseStatusAtInterval(payment), + ); + } + + /// Check the status of the current purchase at an interval and aborts + /// with a timeout if too long has passed + Future validatePurchaseStatusAtInterval( + Payment payment, { + int iteration = 0, + int maxIterations = 3, + Duration delay = const Duration(seconds: 1), + }) async { + if (iteration >= maxIterations) { + emit(PurchaseTimeout(payment)); + return; + } + + // don't wait on the first iteration + if (iteration != 0) { + final _ = await Future.delayed(delay); + } + + // If payment has been reserved, i.e. approved by user + // we will keep checking the backend to verify payment has been captured + checkPurchaseStatus( + payment, + () => validatePurchaseStatusAtInterval( + payment, + iteration: iteration + 1, + maxIterations: maxIterations, + delay: delay, + ), + ); + } + + /// Check the status of [payment] and invoke [onPending] + /// if the payment is still pending + Future checkPurchaseStatus( + Payment payment, + Future Function() onPending, + ) async { + final either = await verifyPurchaseStatus(payment.id); + + either.fold( + (error) => emit(PurchaseError(error.reason)), + (status) async { + switch (status) { + case PaymentStatus.completed: + firebaseAnalyticsEventLogging.purchaseCompletedEvent(payment); + emit(PurchaseCompleted(payment.copyWith(status: status))); + case PaymentStatus.error: + emit(PurchasePaymentRejected(payment.copyWith(status: status))); + case PaymentStatus.reserved: + case PaymentStatus.awaitingPayment: + await onPending(); + case PaymentStatus.rejectedPayment: + case PaymentStatus.refunded: + emit(PurchasePaymentRejected(payment)); + } + }, + ); + } +} diff --git a/lib/cubits/purchase/purchase_state.dart b/lib/features/purchase/presentation/cubit/purchase_state.dart similarity index 87% rename from lib/cubits/purchase/purchase_state.dart rename to lib/features/purchase/presentation/cubit/purchase_state.dart index b5a85396a..e3afa2a59 100644 --- a/lib/cubits/purchase/purchase_state.dart +++ b/lib/features/purchase/presentation/cubit/purchase_state.dart @@ -54,6 +54,15 @@ class PurchasePaymentRejected extends PurchaseState { List get props => [payment]; } +class PurchaseTimeout extends PurchaseState { + final Payment payment; + + const PurchaseTimeout(this.payment); + + @override + List get props => [payment]; +} + class PurchaseError extends PurchaseState { final String message; diff --git a/lib/features/ticket/presentation/pages/buy_single_drink_page.dart b/lib/features/purchase/presentation/pages/buy_single_drink_page.dart similarity index 96% rename from lib/features/ticket/presentation/pages/buy_single_drink_page.dart rename to lib/features/purchase/presentation/pages/buy_single_drink_page.dart index 4c7749dcb..93bf89028 100644 --- a/lib/features/ticket/presentation/pages/buy_single_drink_page.dart +++ b/lib/features/purchase/presentation/pages/buy_single_drink_page.dart @@ -2,10 +2,10 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/cubits/products/products_cubit.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart'; import 'package:coffeecard/features/ticket/presentation/cubit/tickets_cubit.dart'; -import 'package:coffeecard/models/purchase/payment.dart'; -import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/models/ticket/product.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; diff --git a/lib/features/ticket/presentation/pages/buy_tickets_page.dart b/lib/features/purchase/presentation/pages/buy_tickets_page.dart similarity index 96% rename from lib/features/ticket/presentation/pages/buy_tickets_page.dart rename to lib/features/purchase/presentation/pages/buy_tickets_page.dart index 8825174ef..656545700 100644 --- a/lib/features/ticket/presentation/pages/buy_tickets_page.dart +++ b/lib/features/purchase/presentation/pages/buy_tickets_page.dart @@ -3,12 +3,12 @@ import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/cubits/environment/environment_cubit.dart'; import 'package:coffeecard/cubits/products/products_cubit.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/receipt_overlay.dart'; import 'package:coffeecard/features/ticket/presentation/cubit/tickets_cubit.dart'; import 'package:coffeecard/models/environment.dart'; -import 'package:coffeecard/models/purchase/payment.dart'; -import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/models/ticket/product.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; diff --git a/lib/features/purchase/presentation/widgets/error_dialog.dart b/lib/features/purchase/presentation/widgets/error_dialog.dart new file mode 100644 index 000000000..14aa55451 --- /dev/null +++ b/lib/features/purchase/presentation/widgets/error_dialog.dart @@ -0,0 +1,28 @@ +import 'package:coffeecard/base/strings.dart'; +import 'package:flutter/material.dart'; + +class ErrorDialog extends StatelessWidget { + final String title; + final Widget child; + + const ErrorDialog({required this.title, required this.child}); + + @override + Widget build(BuildContext context) { + return AlertDialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + title: Text(title), + content: child, + actions: [ + TextButton( + child: const Text(Strings.purchaseErrorOk), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + } +} diff --git a/lib/features/purchase/presentation/widgets/loading_dialog.dart b/lib/features/purchase/presentation/widgets/loading_dialog.dart new file mode 100644 index 000000000..43922e40e --- /dev/null +++ b/lib/features/purchase/presentation/widgets/loading_dialog.dart @@ -0,0 +1,28 @@ +import 'package:coffeecard/base/style/colors.dart'; +import 'package:flutter/material.dart'; + +class LoadingDialog extends StatelessWidget { + final String title; + + const LoadingDialog({required this.title}); + + @override + Widget build(BuildContext context) { + return SimpleDialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(16)), + ), + title: Text(title), + children: const [ + Column( + children: [ + Padding( + padding: EdgeInsets.all(16), + child: CircularProgressIndicator(color: AppColor.primary), + ), + ], + ), + ], + ); + } +} diff --git a/lib/widgets/components/purchase/purchase_overlay.dart b/lib/features/purchase/presentation/widgets/purchase_overlay.dart similarity index 51% rename from lib/widgets/components/purchase/purchase_overlay.dart rename to lib/features/purchase/presentation/widgets/purchase_overlay.dart index d2b6a7228..1c37a337a 100644 --- a/lib/widgets/components/purchase/purchase_overlay.dart +++ b/lib/features/purchase/presentation/widgets/purchase_overlay.dart @@ -1,9 +1,13 @@ import 'package:coffeecard/base/style/colors.dart'; -import 'package:coffeecard/cubits/purchase/purchase_cubit.dart'; -import 'package:coffeecard/models/purchase/payment.dart'; +import 'package:coffeecard/features/purchase/data/repositories/payment_handler.dart'; +import 'package:coffeecard/features/purchase/domain/entities/internal_payment_type.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/usecases/init_purchase.dart'; +import 'package:coffeecard/features/purchase/domain/usecases/verify_purchase_status.dart'; +import 'package:coffeecard/features/purchase/presentation/cubit/purchase_cubit.dart'; +import 'package:coffeecard/features/purchase/presentation/widgets/purchase_process.dart'; import 'package:coffeecard/models/ticket/product.dart'; -import 'package:coffeecard/payment/payment_handler.dart'; -import 'package:coffeecard/widgets/components/purchase/purchase_process.dart'; +import 'package:coffeecard/service_locator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -21,11 +25,18 @@ Future showPurchaseOverlay({ // Will prevent Android back button from closing overlay. onWillPop: () async => false, child: BlocProvider( - create: (context) => PurchaseCubit( - paymentHandler: - PaymentHandler.createPaymentHandler(paymentType, context), - product: product, - ), + create: (context) { + final paymentHandler = + PaymentHandler.createPaymentHandler(paymentType, context); + + return PurchaseCubit( + firebaseAnalyticsEventLogging: sl(), + product: product, + initPurchase: InitPurchase(paymentHandler: paymentHandler), + verifyPurchaseStatus: + VerifyPurchaseStatus(paymentHandler: paymentHandler), + ); + }, child: BlocListener( listener: (context, state) async { if (state is PurchaseCompleted) { diff --git a/lib/widgets/components/purchase/purchase_process.dart b/lib/features/purchase/presentation/widgets/purchase_process.dart similarity index 58% rename from lib/widgets/components/purchase/purchase_process.dart rename to lib/features/purchase/presentation/widgets/purchase_process.dart index d70ac75cc..20ec60879 100644 --- a/lib/widgets/components/purchase/purchase_process.dart +++ b/lib/features/purchase/presentation/widgets/purchase_process.dart @@ -1,6 +1,7 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/base/style/colors.dart'; -import 'package:coffeecard/cubits/purchase/purchase_cubit.dart'; +import 'package:coffeecard/features/purchase/presentation/cubit/purchase_cubit.dart'; +import 'package:coffeecard/features/purchase/presentation/widgets/error_dialog.dart'; +import 'package:coffeecard/features/purchase/presentation/widgets/loading_dialog.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -45,36 +46,41 @@ class _PurchaseProcessState extends State if (_notification == AppLifecycleState.resumed) { cubit.verifyPurchase(); } + if (state is PurchaseInitial) { - // Not related to previous check, hence a separate if statement cubit.pay(); - return makeLoadingDialog( + return const LoadingDialog( title: Strings.purchaseTalking, ); } else if (state is PurchaseProcessing || state is PurchaseStarted) { - return makeLoadingDialog( + return const LoadingDialog( title: Strings.purchaseTalking, ); } else if (state is PurchaseVerifying) { - return makeLoadingDialog( + return const LoadingDialog( title: Strings.purchaseCompleting, ); } else if (state is PurchaseCompleted) { - return makeLoadingDialog( + return const LoadingDialog( title: Strings.purchaseSuccess, ); } else if (state is PurchasePaymentRejected) { - return makeErrorDialog( + return const ErrorDialog( title: Strings.purchaseRejectedOrCanceled, - content: const Text( + child: Text( Strings.purchaseRejectedOrCanceledMessage, ), ); } else if (state is PurchaseError) { - return makeErrorDialog( + return ErrorDialog( title: Strings.purchaseError, - content: Text(state.message), + child: Text(state.message), + ); + } else if (state is PurchaseTimeout) { + return const ErrorDialog( + title: Strings.purchaseTimeout, + child: Text(Strings.purchaseTimeoutMessage), ); } @@ -85,48 +91,4 @@ class _PurchaseProcessState extends State ), ); } - - StatelessWidget _getTitleWidget(String title) => Text(title); - - RoundedRectangleBorder _getShape() => const RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(16)), - ); - - StatelessWidget makeLoadingDialog({ - required String title, - }) { - return SimpleDialog( - shape: _getShape(), - title: _getTitleWidget(title), - children: const [ - Column( - children: [ - Padding( - padding: EdgeInsets.all(16), - child: CircularProgressIndicator(color: AppColor.primary), - ), - ], - ), - ], - ); - } - - StatelessWidget makeErrorDialog({ - required String title, - required Widget content, - }) { - return AlertDialog( - shape: _getShape(), - title: _getTitleWidget(title), - content: content, - actions: [ - TextButton( - child: const Text(Strings.purchaseErrorOk), - onPressed: () { - Navigator.of(context).pop(); - }, - ), - ], - ); - } } diff --git a/lib/features/ticket/presentation/widgets/shop_section.dart b/lib/features/ticket/presentation/widgets/shop_section.dart index f2172d7e8..574689038 100644 --- a/lib/features/ticket/presentation/widgets/shop_section.dart +++ b/lib/features/ticket/presentation/widgets/shop_section.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/features/ticket/presentation/pages/buy_single_drink_page.dart'; -import 'package:coffeecard/features/ticket/presentation/pages/buy_tickets_page.dart'; +import 'package:coffeecard/features/purchase/presentation/pages/buy_single_drink_page.dart'; +import 'package:coffeecard/features/purchase/presentation/pages/buy_tickets_page.dart'; import 'package:coffeecard/features/ticket/presentation/pages/redeem_voucher_page.dart'; import 'package:coffeecard/widgets/components/helpers/grid.dart'; import 'package:coffeecard/widgets/components/tickets/shop_card.dart'; diff --git a/lib/features/user/data/models/user_model.dart b/lib/features/user/data/models/user_model.dart index b81bbcf5d..b5cc81470 100644 --- a/lib/features/user/data/models/user_model.dart +++ b/lib/features/user/data/models/user_model.dart @@ -29,7 +29,7 @@ class UserModel extends User { rankMonth: response.rankMonth, rankSemester: response.rankSemester, rankTotal: response.rankAllTime, - role: RoleExtension.fromJson(response.role), + role: Role.fromJson(response.role), ); } } diff --git a/lib/features/user/domain/entities/role.dart b/lib/features/user/domain/entities/role.dart index fba700fc4..2e4514d34 100644 --- a/lib/features/user/domain/entities/role.dart +++ b/lib/features/user/domain/entities/role.dart @@ -1,9 +1,12 @@ import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; -enum Role { customer, barista, manager, board } +enum Role { + customer, + barista, + manager, + board; -extension RoleExtension on Role { // json is a dynamic object by its very nature // ignore: avoid-dynamic static Role fromJson(dynamic json) { diff --git a/lib/models/purchase/single_purchase.dart b/lib/models/purchase/single_purchase.dart deleted file mode 100644 index 2f62ca7e5..000000000 --- a/lib/models/purchase/single_purchase.dart +++ /dev/null @@ -1,27 +0,0 @@ -import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; -import 'package:coffeecard/models/purchase/payment_status.dart'; - -class SinglePurchase { - final int id; - final int totalAmount; - final Map paymentDetails; - final PaymentStatus status; - final DateTime dateCreated; - - const SinglePurchase({ - required this.id, - required this.totalAmount, - required this.paymentDetails, - required this.status, - required this.dateCreated, - }); - - SinglePurchase.fromDto(SinglePurchaseResponse dto) - : id = dto.id, - totalAmount = dto.totalAmount, - paymentDetails = dto.paymentDetails as Map, - status = PaymentStatus.fromPurchaseStatus( - purchaseStatusFromJson(dto.purchaseStatus), - ), - dateCreated = dto.dateCreated; -} diff --git a/lib/payment/free_product_service.dart b/lib/payment/free_product_service.dart deleted file mode 100644 index ac96dbcb9..000000000 --- a/lib/payment/free_product_service.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; -import 'package:coffeecard/models/purchase/payment.dart'; -import 'package:coffeecard/models/purchase/payment_status.dart'; -import 'package:coffeecard/payment/payment_handler.dart'; -import 'package:fpdart/fpdart.dart'; - -class FreeProductService extends PaymentHandler { - const FreeProductService({ - required super.purchaseRepository, - required super.context, - }); - - @override - Future> initPurchase(int productId) async { - final response = await purchaseRepository.initiatePurchase( - productId, - PaymentType.freepurchase, - ); - - return response.fold( - (error) => Left(error), - (purchase) => Right( - Payment( - id: purchase.id, - status: PaymentStatus.completed, - deeplink: '', - purchaseTime: purchase.dateCreated, - price: purchase.totalAmount, - productId: purchase.productId, - productName: purchase.productName, - ), - ), - ); - } -} diff --git a/lib/payment/mobilepay_service.dart b/lib/payment/mobilepay_service.dart deleted file mode 100644 index 43e3725f0..000000000 --- a/lib/payment/mobilepay_service.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'dart:io'; - -import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; -import 'package:coffeecard/models/purchase/initiate_purchase.dart'; -import 'package:coffeecard/models/purchase/payment.dart'; -import 'package:coffeecard/models/purchase/payment_status.dart'; -import 'package:coffeecard/payment/payment_handler.dart'; -import 'package:coffeecard/utils/api_uri_constants.dart'; -import 'package:coffeecard/utils/launch.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:url_launcher/url_launcher.dart'; - -class MobilePayService extends PaymentHandler { - MobilePayService({ - required super.purchaseRepository, - required super.context, - }); - @override - Future> initPurchase(int productId) async { - final Either response; - response = await purchaseRepository.initiatePurchase( - productId, - PaymentType.mobilepay, - ); - - final either = response.map( - (response) { - final paymentDetails = MobilePayPaymentDetails.fromJsonFactory( - response.paymentDetails, - ); - - return Payment( - id: response.id, - status: PaymentStatus.awaitingPayment, - deeplink: paymentDetails.mobilePayAppRedirectUri, - purchaseTime: response.dateCreated, - price: response.totalAmount, - productId: response.productId, - productName: response.productName, - ); - }, - ); - - await _invokeMobilePayApp(either); // Sideeffect - - return either; - } - - Future _invokeMobilePayApp( - Either paymentEither, - ) async { - final _ = paymentEither.map( - (payment) async { - final Uri mobilepayLink = Uri.parse(payment.deeplink); - - if (await canLaunchUrl(mobilepayLink)) { - final _ = await launchUrl( - mobilepayLink, - mode: LaunchMode.externalApplication, - ); - - return; - } else { - final Uri url = _getAppStoreUri(); - - // MobilePay not installed, send user to appstore - if (context.mounted) { - await launchUrlExternalApplication(url, context); - } - } - }, - ); - } - - Uri _getAppStoreUri() { - if (Platform.isAndroid) { - return ApiUriConstants.mobilepayAndroid; - } else if (Platform.isIOS) { - return ApiUriConstants.mobilepayIOS; - } else { - throw UnsupportedError('Unsupported platform'); - } - } -} diff --git a/lib/payment/payment_handler.dart b/lib/payment/payment_handler.dart deleted file mode 100644 index ddd60badb..000000000 --- a/lib/payment/payment_handler.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; -import 'package:coffeecard/models/purchase/payment.dart'; -import 'package:coffeecard/models/purchase/payment_status.dart'; -import 'package:coffeecard/payment/free_product_service.dart'; -import 'package:coffeecard/payment/mobilepay_service.dart'; -import 'package:coffeecard/service_locator.dart'; -import 'package:flutter/widgets.dart'; -import 'package:fpdart/fpdart.dart'; - -abstract class PaymentHandler { - final PurchaseRepository purchaseRepository; - // Certain implementations of the payment handler require access to the build context, even if it does not do so itself. - final BuildContext context; - - const PaymentHandler({ - required this.purchaseRepository, - required this.context, - }); - - static PaymentHandler createPaymentHandler( - InternalPaymentType paymentType, - BuildContext context, - ) { - final repository = sl.get(); - - return switch (paymentType) { - InternalPaymentType.mobilePay => MobilePayService( - purchaseRepository: repository, - context: context, - ), - InternalPaymentType.free => FreeProductService( - purchaseRepository: repository, - context: context, - ), - }; - } - - Future> initPurchase(int productId); - - Future> verifyPurchase( - int purchaseId, - ) async { - // Call API endpoint, receive PaymentStatus - final either = await purchaseRepository.getPurchase(purchaseId); - - return either.map( - (purchase) => purchase.status, - ); - } -} - -enum InternalPaymentType { - mobilePay, - free, -} diff --git a/lib/service_locator.dart b/lib/service_locator.dart index ee6a75b54..a39cbc14c 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -1,4 +1,5 @@ import 'package:chopper/chopper.dart'; +import 'package:coffeecard/core/external/external_url_launcher.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/data/api/interceptors/authentication_interceptor.dart'; @@ -6,7 +7,6 @@ import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; import 'package:coffeecard/data/repositories/v1/voucher_repository.dart'; import 'package:coffeecard/data/repositories/v2/app_config_repository.dart'; -import 'package:coffeecard/data/repositories/v2/purchase_repository.dart'; import 'package:coffeecard/data/storage/secure_storage.dart'; import 'package:coffeecard/env/env.dart'; import 'package:coffeecard/features/contributor/data/datasources/contributor_local_data_source.dart'; @@ -19,6 +19,7 @@ import 'package:coffeecard/features/occupation/data/datasources/occupation_remot import 'package:coffeecard/features/occupation/domain/usecases/get_occupations.dart'; import 'package:coffeecard/features/occupation/presentation/cubit/occupation_cubit.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; +import 'package:coffeecard/features/purchase/data/datasources/purchase_remote_data_source.dart'; import 'package:coffeecard/features/receipt/data/datasources/receipt_remote_data_source.dart'; import 'package:coffeecard/features/receipt/data/repositories/receipt_repository_impl.dart'; import 'package:coffeecard/features/receipt/domain/repositories/receipt_repository.dart'; @@ -140,13 +141,6 @@ void configureServices() { ); // v2 - sl.registerFactory( - () => PurchaseRepository( - apiV2: sl(), - executor: sl(), - ), - ); - sl.registerFactory( () => AppConfigRepository( apiV2: sl(), @@ -160,6 +154,8 @@ void configureServices() { FirebaseAnalyticsEventLogging(FirebaseAnalytics.instance), ), ); + + ignoreValue(sl.registerLazySingleton(() => ExternalUrlLauncher())); } void initFeatures() { @@ -169,6 +165,7 @@ void initFeatures() { initUser(); initReceipt(); initContributor(); + initPayment(); initLeaderboard(); } @@ -284,6 +281,18 @@ void initContributor() { sl.registerLazySingleton(() => ContributorLocalDataSource()); } +void initPayment() { + // bloc + + // data source + sl.registerFactory( + () => PurchaseRemoteDataSource( + apiV2: sl(), + executor: sl(), + ), + ); +} + void initLeaderboard() { // bloc sl.registerFactory( diff --git a/lib/utils/firebase_analytics_event_logging.dart b/lib/utils/firebase_analytics_event_logging.dart index 6857a6e64..16c19da9d 100644 --- a/lib/utils/firebase_analytics_event_logging.dart +++ b/lib/utils/firebase_analytics_event_logging.dart @@ -1,4 +1,4 @@ -import 'package:coffeecard/models/purchase/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; import 'package:coffeecard/models/ticket/product.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; diff --git a/lib/utils/launch.dart b/lib/utils/launch.dart deleted file mode 100644 index e09456ea4..000000000 --- a/lib/utils/launch.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:coffeecard/base/strings.dart'; -import 'package:flutter/material.dart'; -import 'package:url_launcher/url_launcher.dart'; - -Future launchUrlExternalApplication(Uri url, BuildContext context) async { - if (await canLaunchUrl(url)) { - final _ = launchUrl(url, mode: LaunchMode.externalApplication); - return; - } - - if (context.mounted) { - final _ = showDialog( - context: context, - barrierDismissible: true, - builder: (BuildContext context) { - return const AlertDialog( - title: Text(Strings.error), - content: Text(Strings.cantLaunchUrl), - ); - }, - ); - } -} diff --git a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart b/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart index 3955a28d2..59b2154fd 100644 --- a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart +++ b/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart @@ -1,10 +1,10 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/models/purchase/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/internal_payment_type.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/presentation/widgets/purchase_overlay.dart'; import 'package:coffeecard/models/ticket/product.dart'; -import 'package:coffeecard/payment/payment_handler.dart'; -import 'package:coffeecard/widgets/components/purchase/purchase_overlay.dart'; import 'package:coffeecard/widgets/components/tickets/bottom_modal_sheet_helper.dart'; import 'package:coffeecard/widgets/components/tickets/rounded_button.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/pages/settings/settings_page.dart b/lib/widgets/pages/settings/settings_page.dart index 3a93b4fbb..8ad7f543d 100644 --- a/lib/widgets/pages/settings/settings_page.dart +++ b/lib/widgets/pages/settings/settings_page.dart @@ -3,11 +3,12 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/core/external/external_url_launcher.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/features/contributor/presentation/pages/credits_page.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; +import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/api_uri_constants.dart'; -import 'package:coffeecard/utils/launch.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; import 'package:coffeecard/widgets/components/helpers/shimmer_builder.dart'; import 'package:coffeecard/widgets/components/images/analogio_logo.dart'; @@ -129,14 +130,16 @@ class SettingsPage extends StatelessWidget { ), SettingListEntry( name: Strings.privacyPolicy, - onTap: () => launchUrlExternalApplication( + onTap: () => + sl().launchUrlExternalApplication( ApiUriConstants.privacyPolicyUri, context, ), ), SettingListEntry( name: Strings.provideFeedback, - onTap: () => launchUrlExternalApplication( + onTap: () => + sl().launchUrlExternalApplication( ApiUriConstants.feedbackFormUri, context, ), diff --git a/test/features/purchase/data/datasources/purchase_remote_data_source_test.dart b/test/features/purchase/data/datasources/purchase_remote_data_source_test.dart new file mode 100644 index 000000000..e5a5d8371 --- /dev/null +++ b/test/features/purchase/data/datasources/purchase_remote_data_source_test.dart @@ -0,0 +1,114 @@ +import 'dart:convert'; + +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/purchase/data/datasources/purchase_remote_data_source.dart'; +import 'package:coffeecard/features/purchase/data/models/initiate_purchase_model.dart'; +import 'package:coffeecard/features/purchase/data/models/single_purchase_model.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'purchase_remote_data_source_test.mocks.dart'; + +@GenerateMocks([CoffeecardApiV2, NetworkRequestExecutor]) +void main() { + late PurchaseRemoteDataSource purchaseRemoteDataSource; + late MockCoffeecardApiV2 apiV2; + late MockNetworkRequestExecutor executor; + + setUp(() { + apiV2 = MockCoffeecardApiV2(); + executor = MockNetworkRequestExecutor(); + purchaseRemoteDataSource = PurchaseRemoteDataSource( + apiV2: apiV2, + executor: executor, + ); + }); + + group('initiatePurchase', () { + test('should call executor', () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => Right( + InitiatePurchaseResponse( + dateCreated: DateTime.parse('2023-05-19'), + id: 0, + paymentDetails: json.decode( + '{"mobilePayAppRedirectUri": "mobilePayAppRedirectUri", "paymentId": "paymentId", "state": "state", "discriminator": "discriminator", "paymentType": "paymentType", "orderId": "orderId"}', + ), + productId: 0, + productName: 'productName', + purchaseStatus: 'purchaseStatus', + totalAmount: 0, + ), + ), + ); + + // act + final actual = await purchaseRemoteDataSource.initiatePurchase( + 0, + PaymentType.mobilepay, + ); + + // assert + expect( + actual, + Right( + InitiatePurchaseModel( + dateCreated: DateTime.parse('2023-05-19'), + id: 0, + paymentDetails: MobilePayPaymentDetails( + mobilePayAppRedirectUri: 'mobilePayAppRedirectUri', + paymentId: 'paymentId', + state: 'state', + discriminator: 'discriminator', + paymentType: 'paymentType', + orderId: 'orderId', + ), + productId: 0, + productName: 'productName', + purchaseStatus: 'purchaseStatus', + totalAmount: 0, + ), + ), + ); + }); + }); + + group('getPurchase', () { + test('should call executor', () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => Right( + SinglePurchaseResponse( + dateCreated: DateTime.parse('2023-05-19'), + id: 0, + purchaseStatus: 'Completed', + totalAmount: 0, + productId: 0, + paymentDetails: null, + ), + ), + ); + + // act + final actual = await purchaseRemoteDataSource.getPurchase(0); + + // assert + expect( + actual, + Right( + SinglePurchaseModel( + dateCreated: DateTime.parse('2023-05-19'), + id: 0, + status: PaymentStatus.completed, + totalAmount: 0, + ), + ), + ); + }); + }); +} diff --git a/test/features/purchase/data/repositories/free_product_service_test.dart b/test/features/purchase/data/repositories/free_product_service_test.dart new file mode 100644 index 000000000..83e48d3e0 --- /dev/null +++ b/test/features/purchase/data/repositories/free_product_service_test.dart @@ -0,0 +1,74 @@ +import 'package:coffeecard/features/purchase/data/datasources/purchase_remote_data_source.dart'; +import 'package:coffeecard/features/purchase/data/repositories/free_product_service.dart'; +import 'package:coffeecard/features/purchase/domain/entities/initiate_purchase.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'free_product_service_test.mocks.dart'; + +@GenerateMocks([PurchaseRemoteDataSource, BuildContext]) +void main() { + late FreeProductService freeProductService; + late MockPurchaseRemoteDataSource remoteDataSource; + late MockBuildContext buildContext; + + setUp(() { + remoteDataSource = MockPurchaseRemoteDataSource(); + buildContext = MockBuildContext(); + freeProductService = FreeProductService( + remoteDataSource: remoteDataSource, + buildContext: buildContext, + ); + }); + + group('initPurchase', () { + test('should call remote data source', () async { + // arrange + when(remoteDataSource.initiatePurchase(any, any)).thenAnswer( + (_) async => Right( + InitiatePurchase( + id: 0, + totalAmount: 0, + paymentDetails: MobilePayPaymentDetails( + mobilePayAppRedirectUri: 'mobilePayAppRedirectUri', + paymentId: 'paymentId', + state: 'state', + discriminator: 'discriminator', + paymentType: 'paymentType', + orderId: 'orderId', + ), + productId: 0, + productName: 'productName', + purchaseStatus: 'purchaseStatus', + dateCreated: DateTime.parse('2023-05-19'), + ), + ), + ); + + // act + final actual = await freeProductService.initPurchase(0); + + // assert + expect( + actual, + Right( + Payment( + id: 0, + price: 0, + purchaseTime: DateTime.parse('2023-05-19'), + status: PaymentStatus.completed, + deeplink: '', + productId: 0, + productName: 'productName', + ), + ), + ); + }); + }); +} diff --git a/test/features/purchase/data/repositories/mobilepay_service_test.dart b/test/features/purchase/data/repositories/mobilepay_service_test.dart new file mode 100644 index 000000000..13b6c8b7b --- /dev/null +++ b/test/features/purchase/data/repositories/mobilepay_service_test.dart @@ -0,0 +1,113 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/external/external_url_launcher.dart'; +import 'package:coffeecard/features/purchase/data/datasources/purchase_remote_data_source.dart'; +import 'package:coffeecard/features/purchase/data/repositories/mobilepay_service.dart'; +import 'package:coffeecard/features/purchase/domain/entities/initiate_purchase.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'mobilepay_service_test.mocks.dart'; + +@GenerateMocks([ExternalUrlLauncher, PurchaseRemoteDataSource, BuildContext]) +void main() { + late MobilePayService mobilePayService; + late MockExternalUrlLauncher externalUrlLauncher; + late MockPurchaseRemoteDataSource remoteDataSource; + late MockBuildContext buildContext; + + setUp(() { + externalUrlLauncher = MockExternalUrlLauncher(); + remoteDataSource = MockPurchaseRemoteDataSource(); + buildContext = MockBuildContext(); + mobilePayService = MobilePayService( + externalUrlLauncher: externalUrlLauncher, + remoteDataSource: remoteDataSource, + buildContext: buildContext, + ); + }); + + const testError = 'some error'; + + final testPayment = Payment( + id: 0, + price: 0, + purchaseTime: DateTime.parse('2023-05-19'), + status: PaymentStatus.awaitingPayment, + deeplink: 'mobilePayAppRedirectUri', + productId: 0, + productName: 'productName', + ); + + group('initPurchase', () { + test('should return [Left] if remote data source fails', () async { + // arrange + when(remoteDataSource.initiatePurchase(any, any)) + .thenAnswer((_) async => const Left(ServerFailure(testError))); + + // act + final actual = await mobilePayService.initPurchase(0); + + // assert + expect(actual, const Left(ServerFailure(testError))); + }); + + test('should return [Right] if remote data source succeeds', () async { + // arrange + when(externalUrlLauncher.canLaunch(any)).thenAnswer((_) async => true); + //when(externalUrlLauncher.launch(any)).thenAnswer((_) async {}); + when(remoteDataSource.initiatePurchase(any, any)).thenAnswer( + (_) async => Right( + InitiatePurchase( + id: 0, + totalAmount: 0, + paymentDetails: MobilePayPaymentDetails( + mobilePayAppRedirectUri: 'mobilePayAppRedirectUri', + paymentId: 'paymentId', + state: 'state', + discriminator: 'discriminator', + paymentType: 'paymentType', + orderId: 'orderId', + ), + productId: 0, + productName: 'productName', + purchaseStatus: 'purchaseStatus', + dateCreated: DateTime.parse('2023-05-19'), + ), + ), + ); + + // act + final actual = await mobilePayService.initPurchase(0); + + // assert + expect( + actual, + Right( + testPayment, + ), + ); + }); + }); + + group('launchMobilePay', () { + test( + 'should launch mobilepay link if application is able to launch', + () async { + // arrange + when(externalUrlLauncher.canLaunch(any)).thenAnswer((_) async => true); + + // act + await mobilePayService.launchMobilePay(testPayment); + + // assert + verify(externalUrlLauncher.launch(any)); + }, + ); + }); +} diff --git a/test/features/purchase/domain/usecases/init_purchase_test.dart b/test/features/purchase/domain/usecases/init_purchase_test.dart new file mode 100644 index 000000000..2077e2f33 --- /dev/null +++ b/test/features/purchase/domain/usecases/init_purchase_test.dart @@ -0,0 +1,45 @@ +import 'package:coffeecard/features/purchase/data/repositories/payment_handler.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/features/purchase/domain/usecases/init_purchase.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'init_purchase_test.mocks.dart'; + +@GenerateMocks([PaymentHandler]) +void main() { + late MockPaymentHandler paymentHandler; + late InitPurchase usecase; + + setUp(() { + paymentHandler = MockPaymentHandler(); + usecase = InitPurchase(paymentHandler: paymentHandler); + }); + + test('should call repository', () async { + // arrange + when(paymentHandler.initPurchase(any)).thenAnswer( + (_) async => Right( + Payment( + id: 0, + price: 0, + purchaseTime: DateTime.now(), + status: PaymentStatus.completed, + deeplink: 'deeplink', + productId: 0, + productName: 'productName', + ), + ), + ); + + // act + await usecase(0); + + // assert + verify(paymentHandler.initPurchase(any)); + verifyNoMoreInteractions(paymentHandler); + }); +} diff --git a/test/features/purchase/domain/usecases/verify_purchase_status_test.dart b/test/features/purchase/domain/usecases/verify_purchase_status_test.dart new file mode 100644 index 000000000..af2c67c3e --- /dev/null +++ b/test/features/purchase/domain/usecases/verify_purchase_status_test.dart @@ -0,0 +1,34 @@ +import 'package:coffeecard/features/purchase/data/repositories/payment_handler.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/features/purchase/domain/usecases/verify_purchase_status.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'verify_purchase_status_test.mocks.dart'; + +@GenerateMocks([PaymentHandler]) +void main() { + late MockPaymentHandler paymentHandler; + late VerifyPurchaseStatus usecase; + + setUp(() { + paymentHandler = MockPaymentHandler(); + usecase = VerifyPurchaseStatus(paymentHandler: paymentHandler); + }); + + test('should call repository', () async { + // arrange + when(paymentHandler.verifyPurchase(any)).thenAnswer( + (_) async => const Right(PaymentStatus.completed), + ); + + // act + await usecase(0); + + // assert + verify(paymentHandler.verifyPurchase(any)); + verifyNoMoreInteractions(paymentHandler); + }); +} diff --git a/test/features/purchase/presentation/cubit/purchase_cubit_test.dart b/test/features/purchase/presentation/cubit/purchase_cubit_test.dart new file mode 100644 index 000000000..2854c6d0a --- /dev/null +++ b/test/features/purchase/presentation/cubit/purchase_cubit_test.dart @@ -0,0 +1,266 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/features/purchase/domain/usecases/init_purchase.dart'; +import 'package:coffeecard/features/purchase/domain/usecases/verify_purchase_status.dart'; +import 'package:coffeecard/features/purchase/presentation/cubit/purchase_cubit.dart'; +import 'package:coffeecard/models/ticket/product.dart'; +import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'purchase_cubit_test.mocks.dart'; + +@GenerateMocks( + [InitPurchase, VerifyPurchaseStatus, FirebaseAnalyticsEventLogging], +) +void main() { + late MockInitPurchase initPurchase; + late MockVerifyPurchaseStatus verifyPurchaseStatus; + late MockFirebaseAnalyticsEventLogging firebaseAnalyticsEventLogging; + late PurchaseCubit cubit; + + const testProduct = Product( + price: 0, + amount: 0, + name: 'name', + id: 0, + description: 'description', + ); + + setUp(() { + initPurchase = MockInitPurchase(); + verifyPurchaseStatus = MockVerifyPurchaseStatus(); + firebaseAnalyticsEventLogging = MockFirebaseAnalyticsEventLogging(); + cubit = PurchaseCubit( + product: testProduct, + initPurchase: initPurchase, + verifyPurchaseStatus: verifyPurchaseStatus, + firebaseAnalyticsEventLogging: firebaseAnalyticsEventLogging, + ); + }); + + const testError = 'some error'; + + Payment createTestPayment(PaymentStatus status) => Payment( + status: status, + deeplink: 'deeplink', + id: 0, + price: 0, + productId: 0, + productName: 'productName', + purchaseTime: DateTime.parse('2023-05-19'), + ); + + group('pay', () { + blocTest( + 'should not emit new state if state is not [Initial]', + build: () => cubit, + setUp: () { + when(firebaseAnalyticsEventLogging.beginCheckoutEvent(any)) + .thenReturn(null); + }, + seed: () => const PurchaseError(testError), + act: (_) => cubit.pay(), + expect: () => [], + ); + + blocTest( + 'should emit [Started, Error] if use case fails', + build: () => cubit, + setUp: () { + when(firebaseAnalyticsEventLogging.beginCheckoutEvent(any)) + .thenReturn(null); + when(initPurchase(any)) + .thenAnswer((_) async => const Left(ServerFailure(testError))); + }, + act: (_) => cubit.pay(), + expect: () => [ + const PurchaseStarted(), + const PurchaseError(testError), + ], + ); + + blocTest( + 'should emit [Started, Completed] if payment is completed', + build: () => cubit, + setUp: () { + when(firebaseAnalyticsEventLogging.beginCheckoutEvent(any)) + .thenReturn(null); + when(initPurchase(any)).thenAnswer( + (_) async => Right(createTestPayment(PaymentStatus.completed)), + ); + }, + act: (_) => cubit.pay(), + expect: () => [ + const PurchaseStarted(), + PurchaseCompleted(createTestPayment(PaymentStatus.completed)), + ], + ); + + blocTest( + 'should emit [Started, Processing] if payment is awaiting', + build: () => cubit, + setUp: () { + when(firebaseAnalyticsEventLogging.beginCheckoutEvent(any)) + .thenReturn(null); + when(initPurchase(any)).thenAnswer( + (_) async => Right(createTestPayment(PaymentStatus.awaitingPayment)), + ); + }, + act: (_) => cubit.pay(), + expect: () => [ + const PurchaseStarted(), + PurchaseProcessing(createTestPayment(PaymentStatus.awaitingPayment)), + ], + ); + + blocTest( + 'should emit [Started, Rejected] if payment is rejected', + build: () => cubit, + setUp: () { + when(firebaseAnalyticsEventLogging.beginCheckoutEvent(any)) + .thenReturn(null); + when(initPurchase(any)).thenAnswer( + (_) async => Right(createTestPayment(PaymentStatus.rejectedPayment)), + ); + }, + act: (_) => cubit.pay(), + expect: () => [ + const PurchaseStarted(), + PurchasePaymentRejected( + createTestPayment(PaymentStatus.rejectedPayment), + ), + ], + ); + }); + + group('verifyPurchase', () { + blocTest( + 'should not emit new state if state is not [Processing]', + build: () => cubit, + seed: () => const PurchaseError(testError), + act: (_) => cubit.verifyPurchase(), + expect: () => [], + ); + + blocTest( + 'should emit [Verifying] and check purchase status', + build: () => cubit, + seed: () => PurchaseProcessing( + createTestPayment(PaymentStatus.awaitingPayment), + ), + setUp: () { + when(verifyPurchaseStatus(any)) + .thenAnswer((_) async => const Left(ServerFailure(testError))); + }, + act: (_) => cubit.verifyPurchase(), + expect: () => [ + PurchaseVerifying(createTestPayment(PaymentStatus.awaitingPayment)), + const PurchaseError(testError), + ], + ); + }); + + group('validatePurchaseStatusAtInterval', () { + blocTest( + 'should emit [Timeout] if iterations exceed maxIterations', + build: () => cubit, + setUp: () { + when(verifyPurchaseStatus(any)).thenAnswer( + (_) async => const Right(PaymentStatus.reserved), + ); + }, + act: (_) async => cubit.validatePurchaseStatusAtInterval( + createTestPayment(PaymentStatus.completed), + maxIterations: 1, + ), + expect: () => + [PurchaseTimeout(createTestPayment(PaymentStatus.completed))], + ); + }); + + group('checkPurchaseStatus', () { + blocTest( + 'should emit [Error] when use case fails', + build: () => cubit, + setUp: () { + when(verifyPurchaseStatus(any)).thenAnswer( + (_) async => const Left(ServerFailure(testError)), + ); + }, + act: (_) async => cubit.checkPurchaseStatus( + createTestPayment(PaymentStatus.completed), + () async {}, + ), + expect: () => [const PurchaseError(testError)], + ); + + blocTest( + 'should emit [Completed] when status is Completed', + build: () => cubit, + setUp: () { + when(verifyPurchaseStatus(any)).thenAnswer( + (_) async => const Right(PaymentStatus.completed), + ); + }, + act: (_) async => cubit.checkPurchaseStatus( + createTestPayment(PaymentStatus.completed), + () async {}, + ), + expect: () => + [PurchaseCompleted(createTestPayment(PaymentStatus.completed))], + ); + + blocTest( + 'should emit [Rejected] when status is Error', + build: () => cubit, + setUp: () { + when(verifyPurchaseStatus(any)).thenAnswer( + (_) async => const Right(PaymentStatus.error), + ); + }, + act: (_) async => cubit.checkPurchaseStatus( + createTestPayment(PaymentStatus.completed), + () async {}, + ), + expect: () => + [PurchasePaymentRejected(createTestPayment(PaymentStatus.error))], + ); + + blocTest( + 'should emit [Rejected] when status is Rejected', + build: () => cubit, + setUp: () { + when(verifyPurchaseStatus(any)).thenAnswer( + (_) async => const Right(PaymentStatus.refunded), + ); + }, + act: (_) async => cubit.checkPurchaseStatus( + createTestPayment(PaymentStatus.completed), + () async {}, + ), + expect: () => + [PurchasePaymentRejected(createTestPayment(PaymentStatus.completed))], + ); + + blocTest( + 'should emit [Rejected] when status is Refunded', + build: () => cubit, + setUp: () { + when(verifyPurchaseStatus(any)).thenAnswer( + (_) async => const Right(PaymentStatus.refunded), + ); + }, + act: (_) async => cubit.checkPurchaseStatus( + createTestPayment(PaymentStatus.completed), + () async {}, + ), + expect: () => + [PurchasePaymentRejected(createTestPayment(PaymentStatus.completed))], + ); + }); +} From 7070c6f04000ccbc061db62a3049ec09f41ec06e Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Tue, 23 May 2023 20:09:47 +0200 Subject: [PATCH 12/32] show status on receipts (#468) --- lib/core/extensions/string_extensions.dart | 8 ++++++++ .../repositories/opening_hours_repository_impl.dart | 9 ++------- .../purchase/domain/entities/payment_status.dart | 12 ++++++++++++ .../presentation/pages/buy_tickets_page.dart | 2 +- .../receipt/data/models/purchase_receipt_model.dart | 5 +++++ .../data/repositories/receipt_repository_impl.dart | 4 ++-- .../receipt/domain/entities/purchase_receipt.dart | 4 +++- lib/features/receipt/domain/entities/receipt.dart | 1 + .../presentation/pages/view_receipt_page.dart | 6 +++--- .../list_entry/placeholder_receipt_list_entry.dart | 1 + .../list_entry/purchase_receipt_list_entry.dart | 5 ++--- .../widgets/list_entry/receipt_list_entry.dart | 6 ++++-- .../widgets/list_entry/swipe_receipt_list_entry.dart | 3 ++- .../receipt/presentation/widgets/receipt_card.dart | 8 +++----- .../presentation/widgets/receipt_overlay.dart | 4 ++-- .../ticket/presentation/widgets/tickets_section.dart | 6 +++++- .../repositories/receipt_repository_impl_test.dart | 2 ++ 17 files changed, 58 insertions(+), 28 deletions(-) create mode 100644 lib/core/extensions/string_extensions.dart diff --git a/lib/core/extensions/string_extensions.dart b/lib/core/extensions/string_extensions.dart new file mode 100644 index 000000000..d3facdbfe --- /dev/null +++ b/lib/core/extensions/string_extensions.dart @@ -0,0 +1,8 @@ +extension StringExtensions on String { + /// capitalize the first letter of the string + String capitalize() { + // TODO: check for emojis + // ignore: avoid-substring + return this[0].toUpperCase() + substring(1); + } +} diff --git a/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart b/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart index 757281fca..1edaa5d6e 100644 --- a/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart +++ b/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart @@ -1,5 +1,6 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/string_extensions.dart'; import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/generated/api/shiftplanning_api.swagger.dart'; @@ -46,17 +47,11 @@ class OpeningHoursRepositoryImpl implements OpeningHoursRepository { shiftsByWeekday[weekday]!.add(shift); } - // capitalize the closed string - final closedString = - // Closed string is const, and does not contain an emoji - // ignore: avoid-substring - Strings.closed[0].toUpperCase() + Strings.closed.substring(1); - return shiftsByWeekday.map( (day, shifts) => MapEntry( day, shifts.isEmpty - ? closedString + ? Strings.closed.capitalize() : OpeningHoursDay(shifts.first.start, shifts.last.end).toString(), ), ); diff --git a/lib/features/purchase/domain/entities/payment_status.dart b/lib/features/purchase/domain/entities/payment_status.dart index 2e1813a9a..a0daa5cd0 100644 --- a/lib/features/purchase/domain/entities/payment_status.dart +++ b/lib/features/purchase/domain/entities/payment_status.dart @@ -28,4 +28,16 @@ enum PaymentStatus { PurchaseStatus.swaggerGeneratedUnknown => PaymentStatus.error, }; } + + @override + String toString() { + return switch (this) { + PaymentStatus.completed => 'Completed', + PaymentStatus.rejectedPayment => 'Rejected', + PaymentStatus.awaitingPayment => 'Pending', + PaymentStatus.refunded => 'Refunded', + PaymentStatus.error => 'Error', + PaymentStatus.reserved => 'Reserved', + }; + } } diff --git a/lib/features/purchase/presentation/pages/buy_tickets_page.dart b/lib/features/purchase/presentation/pages/buy_tickets_page.dart index 656545700..4855c7181 100644 --- a/lib/features/purchase/presentation/pages/buy_tickets_page.dart +++ b/lib/features/purchase/presentation/pages/buy_tickets_page.dart @@ -121,7 +121,7 @@ class BuyTicketsPage extends StatelessWidget { ReceiptOverlay.of(context).show( isTestEnvironment: envState is EnvironmentLoaded && envState.env.isTest, - isPurchase: true, + paymentStatus: Strings.purchased, productName: payment.productName, timeUsed: payment.purchaseTime, ); diff --git a/lib/features/receipt/data/models/purchase_receipt_model.dart b/lib/features/receipt/data/models/purchase_receipt_model.dart index 942cf8292..0305efa47 100644 --- a/lib/features/receipt/data/models/purchase_receipt_model.dart +++ b/lib/features/receipt/data/models/purchase_receipt_model.dart @@ -1,3 +1,4 @@ +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; @@ -8,6 +9,7 @@ class PurchaseReceiptModel extends PurchaseReceipt { required super.id, required super.price, required super.amountPurchased, + required super.paymentStatus, }); /// Creates a receipt from a purchase DTO @@ -20,6 +22,9 @@ class PurchaseReceiptModel extends PurchaseReceipt { price: dto.totalAmount, amountPurchased: dto.numberOfTickets, id: dto.id, + paymentStatus: PaymentStatus.fromPurchaseStatus( + purchaseStatusFromJson(dto.purchaseStatus), + ), ); } } diff --git a/lib/features/receipt/data/repositories/receipt_repository_impl.dart b/lib/features/receipt/data/repositories/receipt_repository_impl.dart index a7419ab09..5598daa0f 100644 --- a/lib/features/receipt/data/repositories/receipt_repository_impl.dart +++ b/lib/features/receipt/data/repositories/receipt_repository_impl.dart @@ -15,13 +15,13 @@ class ReceiptRepositoryImpl implements ReceiptRepository { await remoteDataSource.getUsersUsedTicketsReceipts(); return userReceiptsEither.fold( - (l) => Left(l), + (error) => Left(error), (userReceipts) async { final userPurchasesEither = await remoteDataSource.getUserPurchasesReceipts(); return userPurchasesEither.fold( - (l) => Left(l), + (error) => Left(error), (userPurchases) async { final allTickets = [...userReceipts, ...userPurchases]; allTickets.sort((a, b) => b.timeUsed.compareTo(a.timeUsed)); diff --git a/lib/features/receipt/domain/entities/purchase_receipt.dart b/lib/features/receipt/domain/entities/purchase_receipt.dart index 57b92409c..ef6c00152 100644 --- a/lib/features/receipt/domain/entities/purchase_receipt.dart +++ b/lib/features/receipt/domain/entities/purchase_receipt.dart @@ -3,6 +3,7 @@ part of 'receipt.dart'; class PurchaseReceipt extends Receipt { final int price; final int amountPurchased; + final PaymentStatus paymentStatus; const PurchaseReceipt({ required super.productName, @@ -10,9 +11,10 @@ class PurchaseReceipt extends Receipt { required super.id, required this.price, required this.amountPurchased, + required this.paymentStatus, }); @override List get props => - [productName, timeUsed, id, price, amountPurchased]; + [productName, timeUsed, id, price, amountPurchased, paymentStatus]; } diff --git a/lib/features/receipt/domain/entities/receipt.dart b/lib/features/receipt/domain/entities/receipt.dart index dd41039f4..dc0ebdf79 100644 --- a/lib/features/receipt/domain/entities/receipt.dart +++ b/lib/features/receipt/domain/entities/receipt.dart @@ -1,4 +1,5 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; import 'package:equatable/equatable.dart'; part 'placeholder_receipt.dart'; diff --git a/lib/features/receipt/presentation/pages/view_receipt_page.dart b/lib/features/receipt/presentation/pages/view_receipt_page.dart index 1418edad3..24f815ec6 100644 --- a/lib/features/receipt/presentation/pages/view_receipt_page.dart +++ b/lib/features/receipt/presentation/pages/view_receipt_page.dart @@ -10,12 +10,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class ViewReceiptPage extends StatelessWidget { final String name; final DateTime time; - final bool isPurchase; + final String paymentStatus; const ViewReceiptPage({ required this.name, required this.time, - required this.isPurchase, + required this.paymentStatus, }); @override @@ -31,10 +31,10 @@ class ViewReceiptPage extends StatelessWidget { ReceiptCard( productName: name, time: time, - isPurchase: isPurchase, isInOverlay: false, isTestEnvironment: state is EnvironmentLoaded && state.env.isTest, + paymentStatus: paymentStatus, ), ], ), diff --git a/lib/features/receipt/presentation/widgets/list_entry/placeholder_receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/placeholder_receipt_list_entry.dart index e9f9c3389..3980328db 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/placeholder_receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/placeholder_receipt_list_entry.dart @@ -16,6 +16,7 @@ class PlaceholderReceiptListEntry extends StatelessWidget { topText: Strings.receiptPlaceholderName, rightText: Strings.oneTicket, backgroundColor: Colors.transparent, + purchaseStatus: '', ); } } diff --git a/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart index b4b170335..067ced6da 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart @@ -1,4 +1,3 @@ -import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart'; @@ -19,10 +18,10 @@ class PurchaseReceiptListEntry extends StatelessWidget { time: receipt.timeUsed, isPurchase: true, showShimmer: false, - topText: - '${Strings.purchased} ${receipt.amountPurchased} ${receipt.productName}', + topText: '${receipt.amountPurchased} ${receipt.productName}', rightText: '${receipt.price},-', backgroundColor: AppColor.slightlyHighlighted, + purchaseStatus: '${receipt.paymentStatus}', ); } } diff --git a/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart index 131a39030..55c02c189 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart @@ -17,6 +17,7 @@ class ReceiptListEntry extends StatelessWidget { final String topText; final String rightText; final Color backgroundColor; + final String purchaseStatus; const ReceiptListEntry({ required this.tappable, @@ -27,6 +28,7 @@ class ReceiptListEntry extends StatelessWidget { required this.topText, required this.rightText, required this.backgroundColor, + required this.purchaseStatus, }); @override @@ -37,7 +39,7 @@ class ReceiptListEntry extends StatelessWidget { return ViewReceiptPage( name: name, time: time, - isPurchase: isPurchase, + paymentStatus: purchaseStatus, ); }, closedBuilder: (context, openContainer) { @@ -59,7 +61,7 @@ class ReceiptListEntry extends StatelessWidget { ColoredBox( color: colorIfShimmer, child: Text( - _formatter.format(time), + '$purchaseStatus ${_formatter.format(time)}', style: AppTextStyle.receiptItemDate, ), ), diff --git a/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart index 1242d848b..e8883ceaa 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart @@ -17,9 +17,10 @@ class SwipeReceiptListEntry extends StatelessWidget { time: receipt.timeUsed, isPurchase: false, showShimmer: false, - topText: '${Strings.swiped} ${receipt.productName}', + topText: receipt.productName, rightText: Strings.oneTicket, backgroundColor: AppColor.white, + purchaseStatus: Strings.swiped, ); } } diff --git a/lib/features/receipt/presentation/widgets/receipt_card.dart b/lib/features/receipt/presentation/widgets/receipt_card.dart index e315f6367..8a3a111d8 100644 --- a/lib/features/receipt/presentation/widgets/receipt_card.dart +++ b/lib/features/receipt/presentation/widgets/receipt_card.dart @@ -14,16 +14,16 @@ DateFormat get _formatter => DateFormat('EEEE d/M/y HH:mm'); class ReceiptCard extends StatelessWidget { final String productName; final DateTime time; - final bool isPurchase; final bool isInOverlay; final bool isTestEnvironment; + final String paymentStatus; const ReceiptCard({ required this.productName, required this.time, - required this.isPurchase, required this.isInOverlay, required this.isTestEnvironment, + required this.paymentStatus, }); @override @@ -39,9 +39,7 @@ class ReceiptCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - isPurchase - ? Strings.receiptCardPurchased - : Strings.receiptCardSwiped, + paymentStatus, style: AppTextStyle.textField, ), const Gap(16), diff --git a/lib/features/receipt/presentation/widgets/receipt_overlay.dart b/lib/features/receipt/presentation/widgets/receipt_overlay.dart index 391353a10..5809e8446 100644 --- a/lib/features/receipt/presentation/widgets/receipt_overlay.dart +++ b/lib/features/receipt/presentation/widgets/receipt_overlay.dart @@ -18,8 +18,8 @@ class ReceiptOverlay { Future show({ required String productName, required DateTime timeUsed, - required bool isPurchase, required bool isTestEnvironment, + required String paymentStatus, }) async { await ScreenBrightness().setScreenBrightness(1); if (_context.mounted) { @@ -36,9 +36,9 @@ class ReceiptOverlay { ReceiptCard( productName: productName, time: timeUsed, - isPurchase: isPurchase, isInOverlay: true, isTestEnvironment: isTestEnvironment, + paymentStatus: paymentStatus, ), const Gap(12), Text( diff --git a/lib/features/ticket/presentation/widgets/tickets_section.dart b/lib/features/ticket/presentation/widgets/tickets_section.dart index 4979392e2..6ee5c4d7a 100644 --- a/lib/features/ticket/presentation/widgets/tickets_section.dart +++ b/lib/features/ticket/presentation/widgets/tickets_section.dart @@ -49,7 +49,11 @@ class TicketSection extends StatelessWidget { ReceiptOverlay.of(context).show( isTestEnvironment: envState is EnvironmentLoaded && envState.env.isTest, - isPurchase: state.receipt is PurchaseReceipt, + paymentStatus: state.receipt is PurchaseReceipt + ? (state.receipt as PurchaseReceipt) + .paymentStatus + .toString() + : Strings.swiped, productName: state.receipt.productName, timeUsed: state.receipt.timeUsed, ); diff --git a/test/features/receipt/data/repositories/receipt_repository_impl_test.dart b/test/features/receipt/data/repositories/receipt_repository_impl_test.dart index 08b6fbc51..60ea0267b 100644 --- a/test/features/receipt/data/repositories/receipt_repository_impl_test.dart +++ b/test/features/receipt/data/repositories/receipt_repository_impl_test.dart @@ -1,4 +1,5 @@ import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; import 'package:coffeecard/features/receipt/data/datasources/receipt_remote_data_source.dart'; import 'package:coffeecard/features/receipt/data/repositories/receipt_repository_impl.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; @@ -63,6 +64,7 @@ void main() { id: 0, price: 0, amountPurchased: 0, + paymentStatus: PaymentStatus.completed, ); when(remoteDataSource.getUsersUsedTicketsReceipts()).thenAnswer( From 1ec4e30d93c7815f9ebf07fad2f2aa1b7de3f745 Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Tue, 23 May 2023 20:38:47 +0200 Subject: [PATCH 13/32] refactor environment logic (#469) --- lib/app.dart | 10 +- lib/cubits/environment/environment_cubit.dart | 21 ---- .../v2/app_config_repository.dart | 33 ------- .../environment_remote_data_source.dart | 22 +++++ .../domain/entities/environment.dart | 25 +++++ .../domain/usecases/get_environment_type.dart | 16 ++++ .../presentation/cubit/environment_cubit.dart | 23 +++++ .../cubit}/environment_state.dart | 0 .../widgets/environment_banner.dart | 15 +++ .../widgets/environment_button.dart | 80 ++++++++++++++++ .../presentation/pages/buy_tickets_page.dart | 4 +- .../presentation/pages/view_receipt_page.dart | 4 +- .../presentation/widgets/tickets_section.dart | 4 +- lib/models/environment.dart | 11 --- lib/service_locator.dart | 29 ++++-- lib/widgets/components/scaffold.dart | 96 ++----------------- .../pages/splash/splash_error_page.dart | 2 +- lib/widgets/routers/splash_router.dart | 2 +- .../environment/environment_cubit_test.dart | 54 ----------- .../environment_remote_data_source_test.dart | 35 +++++++ .../domain/entities/environment_test.dart | 56 +++++++++++ .../usecases/get_environment_type_test.dart | 33 +++++++ .../cubit/environment_cubit_test.dart | 53 ++++++++++ 23 files changed, 394 insertions(+), 234 deletions(-) delete mode 100644 lib/cubits/environment/environment_cubit.dart delete mode 100644 lib/data/repositories/v2/app_config_repository.dart create mode 100644 lib/features/environment/data/datasources/environment_remote_data_source.dart create mode 100644 lib/features/environment/domain/entities/environment.dart create mode 100644 lib/features/environment/domain/usecases/get_environment_type.dart create mode 100644 lib/features/environment/presentation/cubit/environment_cubit.dart rename lib/{cubits/environment => features/environment/presentation/cubit}/environment_state.dart (100%) create mode 100644 lib/features/environment/presentation/widgets/environment_banner.dart create mode 100644 lib/features/environment/presentation/widgets/environment_button.dart delete mode 100644 lib/models/environment.dart delete mode 100644 test/cubits/environment/environment_cubit_test.dart create mode 100644 test/features/environment/data/datasources/environment_remote_data_source_test.dart create mode 100644 test/features/environment/domain/entities/environment_test.dart create mode 100644 test/features/environment/domain/usecases/get_environment_type_test.dart create mode 100644 test/features/environment/presentation/cubit/environment_cubit_test.dart diff --git a/lib/app.dart b/lib/app.dart index 6b7a794b5..31d2e2bf3 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,8 +1,7 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/theme.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; -import 'package:coffeecard/cubits/environment/environment_cubit.dart'; -import 'package:coffeecard/data/repositories/v2/app_config_repository.dart'; +import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/widgets/pages/splash/splash_error_page.dart'; import 'package:coffeecard/widgets/pages/splash/splash_loading_page.dart'; @@ -14,11 +13,6 @@ import 'package:flutter_bloc/flutter_bloc.dart'; class App extends StatelessWidget { final _navigatorKey = GlobalKey(); - EnvironmentCubit _createEnvironmentCubit(BuildContext _) { - final repo = sl.get(); - return EnvironmentCubit(repo)..getConfig(); - } - @override Widget build(BuildContext context) { // Force screen orientation to portrait @@ -27,7 +21,7 @@ class App extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider.value(value: sl()..appStarted()), - BlocProvider(create: _createEnvironmentCubit), + BlocProvider.value(value: sl()..getConfig()), ], child: SplashRouter( navigatorKey: _navigatorKey, diff --git a/lib/cubits/environment/environment_cubit.dart b/lib/cubits/environment/environment_cubit.dart deleted file mode 100644 index 1a6b2dc1c..000000000 --- a/lib/cubits/environment/environment_cubit.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:coffeecard/data/repositories/v2/app_config_repository.dart'; -import 'package:coffeecard/models/environment.dart'; -import 'package:equatable/equatable.dart'; - -part 'environment_state.dart'; - -class EnvironmentCubit extends Cubit { - EnvironmentCubit(this._configRepository) : super(const EnvironmentInitial()); - - final AppConfigRepository _configRepository; - - Future getConfig() async { - final either = await _configRepository.getEnvironmentType(); - - either.fold( - (error) => emit(EnvironmentError(error.reason)), - (env) => emit(EnvironmentLoaded(env: env)), - ); - } -} diff --git a/lib/data/repositories/v2/app_config_repository.dart b/lib/data/repositories/v2/app_config_repository.dart deleted file mode 100644 index 0901677b4..000000000 --- a/lib/data/repositories/v2/app_config_repository.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/core/network/network_request_executor.dart'; -import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; -import 'package:coffeecard/models/environment.dart'; -import 'package:fpdart/fpdart.dart'; - -class AppConfigRepository { - AppConfigRepository({ - required this.apiV2, - required this.executor, - }); - - final CoffeecardApiV2 apiV2; - final NetworkRequestExecutor executor; - - Environment _onSuccessfulRequest(AppConfig dto) { - return switch (environmentTypeFromJson(dto.environmentType as String)) { - EnvironmentType.production => Environment.production, - EnvironmentType.test || - EnvironmentType.localdevelopment => - Environment.test, - EnvironmentType.swaggerGeneratedUnknown => Environment.unknown, - }; - } - - Future> getEnvironmentType() async { - final result = await executor( - apiV2.apiV2AppconfigGet, - ); - - return result.map(_onSuccessfulRequest); - } -} diff --git a/lib/features/environment/data/datasources/environment_remote_data_source.dart b/lib/features/environment/data/datasources/environment_remote_data_source.dart new file mode 100644 index 000000000..ca74fa2a8 --- /dev/null +++ b/lib/features/environment/data/datasources/environment_remote_data_source.dart @@ -0,0 +1,22 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/either_extensions.dart'; +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:fpdart/fpdart.dart'; + +class EnvironmentRemoteDataSource { + EnvironmentRemoteDataSource({ + required this.apiV2, + required this.executor, + }); + + final CoffeecardApiV2 apiV2; + final NetworkRequestExecutor executor; + + Future> getEnvironmentType() async { + return executor( + apiV2.apiV2AppconfigGet, + ).bindFuture(Environment.fromAppConfig); + } +} diff --git a/lib/features/environment/domain/entities/environment.dart b/lib/features/environment/domain/entities/environment.dart new file mode 100644 index 000000000..9d5d6db51 --- /dev/null +++ b/lib/features/environment/domain/entities/environment.dart @@ -0,0 +1,25 @@ +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; + +enum Environment { + test, + production, + unknown; + + static Environment fromAppConfig(AppConfig config) { + final type = environmentTypeFromJson(config.environmentType as String); + + return switch (type) { + EnvironmentType.production => Environment.production, + EnvironmentType.test || + EnvironmentType.localdevelopment => + Environment.test, + EnvironmentType.swaggerGeneratedUnknown => Environment.unknown, + }; + } +} + +extension EnvironmentExtensions on Environment { + bool get isTest => this == Environment.test; + bool get isProduction => this == Environment.production; + bool get isUnknown => this == Environment.unknown; +} diff --git a/lib/features/environment/domain/usecases/get_environment_type.dart b/lib/features/environment/domain/usecases/get_environment_type.dart new file mode 100644 index 000000000..34f5de9de --- /dev/null +++ b/lib/features/environment/domain/usecases/get_environment_type.dart @@ -0,0 +1,16 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/features/environment/data/datasources/environment_remote_data_source.dart'; +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:fpdart/fpdart.dart'; + +class GetEnvironmentType implements UseCase { + final EnvironmentRemoteDataSource remoteDataSource; + + GetEnvironmentType({required this.remoteDataSource}); + + @override + Future> call(NoParams params) { + return remoteDataSource.getEnvironmentType(); + } +} diff --git a/lib/features/environment/presentation/cubit/environment_cubit.dart b/lib/features/environment/presentation/cubit/environment_cubit.dart new file mode 100644 index 000000000..0207dd0ee --- /dev/null +++ b/lib/features/environment/presentation/cubit/environment_cubit.dart @@ -0,0 +1,23 @@ +import 'package:bloc/bloc.dart'; +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:coffeecard/features/environment/domain/usecases/get_environment_type.dart'; +import 'package:equatable/equatable.dart'; + +part 'environment_state.dart'; + +class EnvironmentCubit extends Cubit { + final GetEnvironmentType getEnvironmentType; + + EnvironmentCubit({required this.getEnvironmentType}) + : super(const EnvironmentInitial()); + + Future getConfig() async { + final either = await getEnvironmentType(NoParams()); + + either.fold( + (error) => emit(EnvironmentError(error.reason)), + (environment) => emit(EnvironmentLoaded(env: environment)), + ); + } +} diff --git a/lib/cubits/environment/environment_state.dart b/lib/features/environment/presentation/cubit/environment_state.dart similarity index 100% rename from lib/cubits/environment/environment_state.dart rename to lib/features/environment/presentation/cubit/environment_state.dart diff --git a/lib/features/environment/presentation/widgets/environment_banner.dart b/lib/features/environment/presentation/widgets/environment_banner.dart new file mode 100644 index 000000000..d614fa73f --- /dev/null +++ b/lib/features/environment/presentation/widgets/environment_banner.dart @@ -0,0 +1,15 @@ +import 'package:coffeecard/base/style/colors.dart'; +import 'package:coffeecard/features/environment/presentation/widgets/environment_button.dart'; +import 'package:flutter/material.dart'; + +class EnvironmentBanner extends StatelessWidget { + const EnvironmentBanner(); + + @override + Widget build(BuildContext context) { + return const ColoredBox( + color: AppColor.primary, + child: Center(child: EnvironmentButton()), + ); + } +} diff --git a/lib/features/environment/presentation/widgets/environment_button.dart b/lib/features/environment/presentation/widgets/environment_button.dart new file mode 100644 index 000000000..c97bb472e --- /dev/null +++ b/lib/features/environment/presentation/widgets/environment_button.dart @@ -0,0 +1,80 @@ +import 'package:coffeecard/base/strings_environment.dart'; +import 'package:coffeecard/base/style/colors.dart'; +import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; +import 'package:coffeecard/widgets/components/dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; + +class EnvironmentButton extends StatelessWidget { + const EnvironmentButton(); + + @override + Widget build(BuildContext context) { + return BlocBuilder( + builder: (context, state) { + final bool isTestEnvironment = + state is EnvironmentLoaded && state.env.isTest; + + if (!isTestEnvironment) { + return const SizedBox.shrink(); + } + return Padding( + padding: const EdgeInsets.only(bottom: 2), + child: TextButton( + onPressed: () => appDialog( + context: context, + title: TestEnvironmentStrings.title, + children: [ + Text( + TestEnvironmentStrings.description.first, + style: AppTextStyle.settingKey, + ), + const Gap(8), + Text( + TestEnvironmentStrings.description[1], + style: AppTextStyle.settingKey, + ), + const Gap(8), + Text( + TestEnvironmentStrings.description[2], + style: AppTextStyle.settingKey, + ), + ], + actions: [ + TextButton( + child: const Text(TestEnvironmentStrings.understood), + onPressed: () => closeAppDialog(context), + ), + ], + dismissible: true, + ), + style: TextButton.styleFrom( + backgroundColor: AppColor.white, + padding: const EdgeInsets.only(left: 16, right: 12), + shape: const StadiumBorder(), + visualDensity: VisualDensity.comfortable, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + TestEnvironmentStrings.title, + style: AppTextStyle.environmentNotifier, + ), + const Gap(8), + const Icon( + Icons.info_outline, + color: AppColor.primary, + size: 18, + ), + ], + ), + ), + ); + }, + ); + } +} diff --git a/lib/features/purchase/presentation/pages/buy_tickets_page.dart b/lib/features/purchase/presentation/pages/buy_tickets_page.dart index 4855c7181..3d0cdba6e 100644 --- a/lib/features/purchase/presentation/pages/buy_tickets_page.dart +++ b/lib/features/purchase/presentation/pages/buy_tickets_page.dart @@ -1,14 +1,14 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; -import 'package:coffeecard/cubits/environment/environment_cubit.dart'; import 'package:coffeecard/cubits/products/products_cubit.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/receipt_overlay.dart'; import 'package:coffeecard/features/ticket/presentation/cubit/tickets_cubit.dart'; -import 'package:coffeecard/models/environment.dart'; import 'package:coffeecard/models/ticket/product.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; diff --git a/lib/features/receipt/presentation/pages/view_receipt_page.dart b/lib/features/receipt/presentation/pages/view_receipt_page.dart index 24f815ec6..77a589b70 100644 --- a/lib/features/receipt/presentation/pages/view_receipt_page.dart +++ b/lib/features/receipt/presentation/pages/view_receipt_page.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/cubits/environment/environment_cubit.dart'; +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/receipt_card.dart'; -import 'package:coffeecard/models/environment.dart'; import 'package:coffeecard/utils/responsive.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:flutter/material.dart'; diff --git a/lib/features/ticket/presentation/widgets/tickets_section.dart b/lib/features/ticket/presentation/widgets/tickets_section.dart index 6ee5c4d7a..774bf2330 100644 --- a/lib/features/ticket/presentation/widgets/tickets_section.dart +++ b/lib/features/ticket/presentation/widgets/tickets_section.dart @@ -1,11 +1,11 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/cubits/environment/environment_cubit.dart'; +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/receipt_overlay.dart'; import 'package:coffeecard/features/ticket/presentation/cubit/tickets_cubit.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; -import 'package:coffeecard/models/environment.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; import 'package:coffeecard/widgets/components/error_section.dart'; import 'package:coffeecard/widgets/components/helpers/shimmer_builder.dart'; diff --git a/lib/models/environment.dart b/lib/models/environment.dart deleted file mode 100644 index 9c0350c44..000000000 --- a/lib/models/environment.dart +++ /dev/null @@ -1,11 +0,0 @@ -enum Environment { - test, - production, - unknown, -} - -extension EnvironmentIs on Environment { - bool get isTest => this == Environment.test; - bool get isProduction => this == Environment.production; - bool get isUnknown => this == Environment.unknown; -} diff --git a/lib/service_locator.dart b/lib/service_locator.dart index a39cbc14c..8b65682b2 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -6,12 +6,14 @@ import 'package:coffeecard/data/api/interceptors/authentication_interceptor.dart import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; import 'package:coffeecard/data/repositories/v1/voucher_repository.dart'; -import 'package:coffeecard/data/repositories/v2/app_config_repository.dart'; import 'package:coffeecard/data/storage/secure_storage.dart'; import 'package:coffeecard/env/env.dart'; import 'package:coffeecard/features/contributor/data/datasources/contributor_local_data_source.dart'; import 'package:coffeecard/features/contributor/domain/usecases/fetch_contributors.dart'; import 'package:coffeecard/features/contributor/presentation/cubit/contributor_cubit.dart'; +import 'package:coffeecard/features/environment/data/datasources/environment_remote_data_source.dart'; +import 'package:coffeecard/features/environment/domain/usecases/get_environment_type.dart'; +import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; import 'package:coffeecard/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart'; import 'package:coffeecard/features/leaderboard/domain/usecases/get_leaderboard.dart'; import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; @@ -140,14 +142,6 @@ void configureServices() { ), ); - // v2 - sl.registerFactory( - () => AppConfigRepository( - apiV2: sl(), - executor: sl(), - ), - ); - // external ignoreValue( sl.registerSingleton( @@ -167,6 +161,7 @@ void initFeatures() { initContributor(); initPayment(); initLeaderboard(); + initEnvironment(); } void initOpeningHours() { @@ -307,3 +302,19 @@ void initLeaderboard() { () => LeaderboardRemoteDataSource(apiV2: sl(), executor: sl()), ); } + +void initEnvironment() { + // bloc + sl.registerLazySingleton(() => EnvironmentCubit(getEnvironmentType: sl())); + + // use case + sl.registerFactory(() => GetEnvironmentType(remoteDataSource: sl())); + + // data source + sl.registerLazySingleton( + () => EnvironmentRemoteDataSource( + apiV2: sl(), + executor: sl(), + ), + ); +} diff --git a/lib/widgets/components/scaffold.dart b/lib/widgets/components/scaffold.dart index 0c4a13030..2ebfdf4e4 100644 --- a/lib/widgets/components/scaffold.dart +++ b/lib/widgets/components/scaffold.dart @@ -1,12 +1,11 @@ -import 'package:coffeecard/base/strings_environment.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/cubits/environment/environment_cubit.dart'; -import 'package:coffeecard/models/environment.dart'; -import 'package:coffeecard/widgets/components/dialog.dart'; +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; +import 'package:coffeecard/features/environment/presentation/widgets/environment_banner.dart'; +import 'package:coffeecard/features/environment/presentation/widgets/environment_button.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:gap/gap.dart'; class AppScaffold extends StatelessWidget { final Widget? title; @@ -58,7 +57,7 @@ class AppScaffold extends StatelessWidget { // in the child of the Expanded widget below. backgroundColor: AppColor.primary, appBar: AppBar( - title: hasTitle ? title : const _EnvironmentButton(), + title: hasTitle ? title : const EnvironmentButton(), centerTitle: hasTitle ? null : true, toolbarHeight: appBarHeight, ), @@ -70,7 +69,7 @@ class AppScaffold extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - if (hasTitle && isTestEnvironment) const _EnvironmentBanner(), + if (hasTitle && isTestEnvironment) const EnvironmentBanner(), Expanded( child: Container( padding: applyPadding ? const EdgeInsets.all(16) : null, @@ -85,86 +84,3 @@ class AppScaffold extends StatelessWidget { ); } } - -class _EnvironmentBanner extends StatelessWidget { - const _EnvironmentBanner(); - - @override - Widget build(BuildContext context) { - return const ColoredBox( - color: AppColor.primary, - child: Center(child: _EnvironmentButton()), - ); - } -} - -class _EnvironmentButton extends StatelessWidget { - const _EnvironmentButton(); - - @override - Widget build(BuildContext context) { - return BlocBuilder( - builder: (context, state) { - final bool isTestEnvironment = - state is EnvironmentLoaded && state.env.isTest; - - if (!isTestEnvironment) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.only(bottom: 2), - child: TextButton( - onPressed: () => appDialog( - context: context, - title: TestEnvironmentStrings.title, - children: [ - Text( - TestEnvironmentStrings.description.first, - style: AppTextStyle.settingKey, - ), - const Gap(8), - Text( - TestEnvironmentStrings.description[1], - style: AppTextStyle.settingKey, - ), - const Gap(8), - Text( - TestEnvironmentStrings.description[2], - style: AppTextStyle.settingKey, - ), - ], - actions: [ - TextButton( - child: const Text(TestEnvironmentStrings.understood), - onPressed: () => closeAppDialog(context), - ), - ], - dismissible: true, - ), - style: TextButton.styleFrom( - backgroundColor: AppColor.white, - padding: const EdgeInsets.only(left: 16, right: 12), - shape: const StadiumBorder(), - visualDensity: VisualDensity.comfortable, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - TestEnvironmentStrings.title, - style: AppTextStyle.environmentNotifier, - ), - const Gap(8), - const Icon( - Icons.info_outline, - color: AppColor.primary, - size: 18, - ), - ], - ), - ), - ); - }, - ); - } -} diff --git a/lib/widgets/pages/splash/splash_error_page.dart b/lib/widgets/pages/splash/splash_error_page.dart index 7a86a9234..81438c593 100644 --- a/lib/widgets/pages/splash/splash_error_page.dart +++ b/lib/widgets/pages/splash/splash_error_page.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/cubits/environment/environment_cubit.dart'; +import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; import 'package:coffeecard/widgets/components/loading_overlay.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/widgets/routers/splash_router.dart b/lib/widgets/routers/splash_router.dart index d0aaaef12..25cd0c0ea 100644 --- a/lib/widgets/routers/splash_router.dart +++ b/lib/widgets/routers/splash_router.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; -import 'package:coffeecard/cubits/environment/environment_cubit.dart'; +import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; import 'package:coffeecard/widgets/pages/home_page.dart'; import 'package:coffeecard/widgets/pages/login/login_page_email.dart'; import 'package:flutter/material.dart'; diff --git a/test/cubits/environment/environment_cubit_test.dart b/test/cubits/environment/environment_cubit_test.dart deleted file mode 100644 index 9f0b60b64..000000000 --- a/test/cubits/environment/environment_cubit_test.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/cubits/environment/environment_cubit.dart'; -import 'package:coffeecard/data/repositories/v2/app_config_repository.dart'; -import 'package:coffeecard/models/environment.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'environment_cubit_test.mocks.dart'; - -@GenerateMocks([AppConfigRepository]) -void main() { - group('environment cubit tests', () { - late EnvironmentCubit environmentCubit; - final repo = MockAppConfigRepository(); - - setUp(() { - environmentCubit = EnvironmentCubit(repo); - }); - - test('initial state is EnvironmentInitial', () { - expect(environmentCubit.state, const EnvironmentInitial()); - }); - - blocTest( - 'getConfig emits Loaded when the repo returns a valid environment', - build: () { - when(repo.getEnvironmentType()) - .thenAnswer((_) async => const Right(Environment.production)); - return environmentCubit; - }, - act: (cubit) => cubit.getConfig(), - expect: () => [const EnvironmentLoaded(env: Environment.production)], - ); - - blocTest( - 'getConfig emits Error when the repo returns an error', - build: () { - when(repo.getEnvironmentType()).thenAnswer( - (_) async => const Left(ServerFailure('some error')), - ); - return environmentCubit; - }, - act: (cubit) => cubit.getConfig(), - expect: () => [const EnvironmentError('some error')], - ); - - tearDown(() { - environmentCubit.close(); - }); - }); -} diff --git a/test/features/environment/data/datasources/environment_remote_data_source_test.dart b/test/features/environment/data/datasources/environment_remote_data_source_test.dart new file mode 100644 index 000000000..f39696c27 --- /dev/null +++ b/test/features/environment/data/datasources/environment_remote_data_source_test.dart @@ -0,0 +1,35 @@ +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/environment/data/datasources/environment_remote_data_source.dart'; +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'environment_remote_data_source_test.mocks.dart'; + +@GenerateMocks([CoffeecardApiV2, NetworkRequestExecutor]) +void main() { + late EnvironmentRemoteDataSource environmentRemoteDataSource; + late MockCoffeecardApiV2 coffeecardApiV2; + late MockNetworkRequestExecutor executor; + + setUp(() { + coffeecardApiV2 = MockCoffeecardApiV2(); + executor = MockNetworkRequestExecutor(); + environmentRemoteDataSource = + EnvironmentRemoteDataSource(apiV2: coffeecardApiV2, executor: executor); + }); + test('should call executor', () async { + // arrange + when(executor.call(any)) + .thenAnswer((_) async => Right(AppConfig(environmentType: 'Test'))); + + // act + final actual = await environmentRemoteDataSource.getEnvironmentType(); + + // assert + expect(actual, const Right(Environment.test)); + }); +} diff --git a/test/features/environment/domain/entities/environment_test.dart b/test/features/environment/domain/entities/environment_test.dart new file mode 100644 index 000000000..adddef0e5 --- /dev/null +++ b/test/features/environment/domain/entities/environment_test.dart @@ -0,0 +1,56 @@ +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.models.swagger.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('fromAppConfig', () { + test( + 'should return [Production] when environment type is Production', + () { + // act + final actual = + Environment.fromAppConfig(AppConfig(environmentType: 'Production')); + + // assert + expect(actual, Environment.production); + }, + ); + + test( + 'should return [Test] when environment type is Test', + () { + // act + final actual = + Environment.fromAppConfig(AppConfig(environmentType: 'Test')); + + // assert + expect(actual, Environment.test); + }, + ); + + test( + 'should return [Test] when environment type is LocalDevelopment', + () { + // act + final actual = Environment.fromAppConfig( + AppConfig(environmentType: 'LocalDevelopment'), + ); + + // assert + expect(actual, Environment.test); + }, + ); + + test( + 'should return [Unknown] when environment type cannot be parsed', + () { + // act + final actual = + Environment.fromAppConfig(AppConfig(environmentType: '')); + + // assert + expect(actual, Environment.unknown); + }, + ); + }); +} diff --git a/test/features/environment/domain/usecases/get_environment_type_test.dart b/test/features/environment/domain/usecases/get_environment_type_test.dart new file mode 100644 index 000000000..28d9f2c31 --- /dev/null +++ b/test/features/environment/domain/usecases/get_environment_type_test.dart @@ -0,0 +1,33 @@ +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/features/environment/data/datasources/environment_remote_data_source.dart'; +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:coffeecard/features/environment/domain/usecases/get_environment_type.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'get_environment_type_test.mocks.dart'; + +@GenerateMocks([EnvironmentRemoteDataSource]) +void main() { + late MockEnvironmentRemoteDataSource remoteDataSource; + late GetEnvironmentType usecase; + + setUp(() { + remoteDataSource = MockEnvironmentRemoteDataSource(); + usecase = GetEnvironmentType(remoteDataSource: remoteDataSource); + }); + + test('should call data source', () async { + // arrange + when(remoteDataSource.getEnvironmentType()) + .thenAnswer((_) async => const Right(Environment.production)); + + // act + await usecase(NoParams()); + + // assert + verify(remoteDataSource.getEnvironmentType()); + }); +} diff --git a/test/features/environment/presentation/cubit/environment_cubit_test.dart b/test/features/environment/presentation/cubit/environment_cubit_test.dart new file mode 100644 index 000000000..76eb77c8c --- /dev/null +++ b/test/features/environment/presentation/cubit/environment_cubit_test.dart @@ -0,0 +1,53 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/features/environment/domain/entities/environment.dart'; +import 'package:coffeecard/features/environment/domain/usecases/get_environment_type.dart'; +import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'environment_cubit_test.mocks.dart'; + +@GenerateMocks([GetEnvironmentType]) +void main() { + group('environment cubit tests', () { + late EnvironmentCubit cubit; + late MockGetEnvironmentType getEnvironmentType; + + setUp(() { + getEnvironmentType = MockGetEnvironmentType(); + cubit = EnvironmentCubit(getEnvironmentType: getEnvironmentType); + }); + + test('initial state is [Initial]', () { + expect(cubit.state, const EnvironmentInitial()); + }); + + group('getConfig', () { + blocTest( + 'should emit [Loaded] when usecase suceeds', + build: () => cubit, + setUp: () { + when(getEnvironmentType(any)) + .thenAnswer((_) async => const Right(Environment.production)); + }, + act: (_) => cubit.getConfig(), + expect: () => [const EnvironmentLoaded(env: Environment.production)], + ); + + blocTest( + 'should emit [Error] when usecase fails', + build: () => cubit, + setUp: () { + when(getEnvironmentType(any)).thenAnswer( + (_) async => const Left(ServerFailure('some error')), + ); + }, + act: (cubit) => cubit.getConfig(), + expect: () => [const EnvironmentError('some error')], + ); + }); + }); +} From 355e296e83f22eb269d8ef5cfbe46d1943f62767 Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Fri, 26 May 2023 17:49:24 +0200 Subject: [PATCH 14/32] chore: update Bloc packages (#475) --- pubspec.lock | 12 ++++++------ pubspec.yaml | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/pubspec.lock b/pubspec.lock index 4d4dcfa13..cd934f5b6 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -77,18 +77,18 @@ packages: dependency: "direct main" description: name: bloc - sha256: "658a5ae59edcf1e58aac98b000a71c762ad8f46f1394c34a52050cafb3e11a80" + sha256: "3820f15f502372d979121de1f6b97bfcf1630ebff8fe1d52fb2b0bfa49be5b49" url: "https://pub.dev" source: hosted - version: "8.1.1" + version: "8.1.2" bloc_test: dependency: "direct dev" description: name: bloc_test - sha256: ffbb60c17ee3d8e3784cb78071088e353199057233665541e8ac6cd438dca8ad + sha256: "5f41a3e391c89ccdade81a96233e1e5e5d01564e29e5fe180741fb23579399b9" url: "https://pub.dev" source: hosted - version: "9.1.1" + version: "9.1.2" boolean_selector: dependency: transitive description: @@ -490,10 +490,10 @@ packages: dependency: "direct main" description: name: flutter_bloc - sha256: "434951eea948dbe87f737b674281465f610b8259c16c097b8163ce138749a775" + sha256: e74efb89ee6945bcbce74a5b3a5a3376b088e5f21f55c263fc38cbdc6237faae url: "https://pub.dev" source: hosted - version: "8.1.2" + version: "8.1.3" flutter_blurhash: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index f30102197..02352f576 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -33,8 +33,8 @@ dependencies: get_it: 7.6.0 # State management - flutter_bloc: 8.1.2 - bloc: 8.1.1 + flutter_bloc: 8.1.3 + bloc: 8.1.2 stream_transform: 2.1.0 # Utility @@ -83,7 +83,7 @@ dev_dependencies: # unit testing mockito: 5.4.0 - bloc_test: 9.1.1 + bloc_test: 9.1.2 # Chopper api and rest client chopper_generator: 6.0.1 From 9ff9f8cd04d04bdc14f90161bc2247bf5fafbeba Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Sat, 27 May 2023 00:01:08 +0200 Subject: [PATCH 15/32] test(utils): Improve String.capitalize() and add tests (#472) Now emoji-aware & empty-string aware! Closes #470 --- lib/core/extensions/string_extensions.dart | 13 ++++++----- .../extensions/string_extensions_test.dart | 23 +++++++++++++++++++ 2 files changed, 30 insertions(+), 6 deletions(-) create mode 100644 test/core/extensions/string_extensions_test.dart diff --git a/lib/core/extensions/string_extensions.dart b/lib/core/extensions/string_extensions.dart index d3facdbfe..a1457dd66 100644 --- a/lib/core/extensions/string_extensions.dart +++ b/lib/core/extensions/string_extensions.dart @@ -1,8 +1,9 @@ +import 'package:flutter/widgets.dart'; + extension StringExtensions on String { - /// capitalize the first letter of the string - String capitalize() { - // TODO: check for emojis - // ignore: avoid-substring - return this[0].toUpperCase() + substring(1); - } + /// Capitalize the first letter of the string. + /// + /// If the first letter is an emoji, the string will not be capitalized. + String capitalize() => + (characters.take(1).toUpperCase() + characters.skip(1)).string; } diff --git a/test/core/extensions/string_extensions_test.dart b/test/core/extensions/string_extensions_test.dart new file mode 100644 index 000000000..f98af7baf --- /dev/null +++ b/test/core/extensions/string_extensions_test.dart @@ -0,0 +1,23 @@ +import 'package:coffeecard/core/extensions/string_extensions.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('StringExtensions', () { + group('capitalize', () { + test( + 'should capitalize the first letter of a non-empty string', + () => expect('hello'.capitalize(), equals('Hello')), + ); + + test( + 'should return an empty string for an empty string', + () => expect(''.capitalize(), equals('')), + ); + + test( + 'should not capitalize the first letter of a string if it is an emoji', + () => expect('🌍hello'.capitalize(), equals('🌍hello')), + ); + }); + }); +} From 22054639215536367b4c0ab3eb40d7105c6a2a16 Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Sat, 27 May 2023 08:17:11 +0200 Subject: [PATCH 16/32] chore: bump flutter required version to 3.10.2 (#476) --- .github/workflows/build.yml | 2 +- README.md | 2 +- pubspec.lock | 2 +- pubspec.yaml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5f207cd31..009afbe9a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -17,7 +17,7 @@ on: value: ${{ jobs.version.outputs.version_tag }} env: - FLUTTER_VERSION: 3.10.0 + FLUTTER_VERSION: 3.10.2 JAVA_VERSION: 11.x jobs: diff --git a/README.md b/README.md index c4597bafc..9a62e4972 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ We are building the Flutter app with these SDK versions | SDK | Version | | ------- | -------------- | | Dart | >=3.0.0 <4.0.0 | -| Flutter | 3.10.0 | +| Flutter | 3.10.2 | ## Relevant READMEs diff --git a/pubspec.lock b/pubspec.lock index cd934f5b6..0dc0929ea 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1519,4 +1519,4 @@ packages: version: "3.1.2" sdks: dart: ">=3.0.0 <4.0.0" - flutter: ">=3.10.0" + flutter: ">=3.10.2" diff --git a/pubspec.yaml b/pubspec.yaml index 02352f576..b2cee57ea 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,7 +8,7 @@ version: 0.0.0+1 environment: sdk: ">=3.0.0 <4.0.0" - flutter: 3.10.0 + flutter: 3.10.2 dependencies: flutter: From 2b4d97804fd288c4eb29f5244a726dc5ee940654 Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Sat, 27 May 2023 08:42:58 +0200 Subject: [PATCH 17/32] fix(tickets): Fix multiple `CoffeeCard`s sharing the same product id (#471) `CoffeeCard`s are now grouped by id instead of name; if a group has tickets with different names, their names will be joined Closes #466 --- .../ticket_remote_data_source.dart | 18 ++- .../widgets/swipe_ticket_confirm.dart | 8 +- .../presentation/widgets/tickets_section.dart | 2 +- .../ticket_remote_data_source_test.dart | 153 ++++++++++++------ 4 files changed, 124 insertions(+), 57 deletions(-) diff --git a/lib/features/ticket/data/datasources/ticket_remote_data_source.dart b/lib/features/ticket/data/datasources/ticket_remote_data_source.dart index faac65a43..20ea7bd14 100644 --- a/lib/features/ticket/data/datasources/ticket_remote_data_source.dart +++ b/lib/features/ticket/data/datasources/ticket_remote_data_source.dart @@ -26,14 +26,20 @@ class TicketRemoteDataSource { () => apiV2.apiV2TicketsGet(includeUsed: false), ).bindFuture( (result) => result - .groupListsBy((t) => t.productName) + .groupListsBy((t) => t.productId) .entries .map( - (t) => TicketCountModel( - count: t.value.length, - productName: t.key, - productId: t.value.first.productId, - ), + (entry) { + final MapEntry(key: id, value: tickets) = entry; + // Join ticket names if they share the same product id + final ticketName = + tickets.map((t) => t.productName).toSet().join('/'); + return TicketCountModel( + count: tickets.length, + productName: ticketName, + productId: id, + ); + }, ) .sortedBy((t) => t.productId) .toList(), diff --git a/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart b/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart index 9a8cf403e..9e3287039 100644 --- a/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart +++ b/lib/features/ticket/presentation/widgets/swipe_ticket_confirm.dart @@ -53,7 +53,7 @@ class _ModalContentState extends State<_ModalContent> late AnimationController _controller; late Animation _animation; - late int _heroTag; + late (String, int)? _heroTag; @override void initState() { @@ -68,7 +68,7 @@ class _ModalContentState extends State<_ModalContent> ), ); - _heroTag = widget.productId; + _heroTag = (widget.productName, widget.productId); super.initState(); } @@ -101,7 +101,7 @@ class _ModalContentState extends State<_ModalContent> ], ), Hero( - tag: _heroTag, + tag: _heroTag ?? -1, // SingleChildScrollView to avoid the temporary overflow // error during the hero animation. child: SingleChildScrollView( @@ -130,7 +130,7 @@ class _ModalContentState extends State<_ModalContent> outerColor: AppColor.primary, onSubmit: () async { // Disable hero animation in the reverse direction - setState(() => _heroTag = -1); + setState(() => _heroTag = null); final ticketCubit = widget.context.read(); final receiptCubit = widget.context.read(); diff --git a/lib/features/ticket/presentation/widgets/tickets_section.dart b/lib/features/ticket/presentation/widgets/tickets_section.dart index 774bf2330..4fd711d62 100644 --- a/lib/features/ticket/presentation/widgets/tickets_section.dart +++ b/lib/features/ticket/presentation/widgets/tickets_section.dart @@ -93,7 +93,7 @@ class TicketSection extends StatelessWidget { (p) => Padding( padding: const EdgeInsets.only(bottom: 12.0), child: Hero( - tag: p.productId, + tag: (p.productName, p.productId), child: CoffeeCard( title: p.productName, amountOwned: p.count, diff --git a/test/features/ticket/data/datasources/ticket_remote_data_source_test.dart b/test/features/ticket/data/datasources/ticket_remote_data_source_test.dart index 3d57aa426..cd0fd46c4 100644 --- a/test/features/ticket/data/datasources/ticket_remote_data_source_test.dart +++ b/test/features/ticket/data/datasources/ticket_remote_data_source_test.dart @@ -28,62 +28,123 @@ void main() { }); group('getUserTickets', () { - test('should return [Right] when executor returns [Right]', () async { - // arrange - when(executor.call>(any)) - .thenAnswer((_) async => const Right([])); + test( + 'GIVEN executor returns a [Right] value ' + 'WHEN calling getUserTickets ' + 'THEN a [Right] value is returned', + () async { + // arrange + when(executor.call>(any)) + .thenAnswer((_) async => const Right([])); - // act - final actual = await dataSource.getUserTickets(); + // act + final actual = await dataSource.getUserTickets(); - // assert - expect(actual.isRight(), isTrue); - }); + // assert + expect(actual.isRight(), isTrue); + }, + ); - test('should return [Left] if executor returns [Left]', () async { - // arrange - when(executor.call>(any)) - .thenAnswer((_) async => const Left(ServerFailure('some error'))); + test( + 'GIVEN executor returns a [Left] value ' + 'WHEN calling getUserTickets ' + 'THEN a [Left] value is returned', + () async { + // arrange + when(executor.call>(any)) + .thenAnswer((_) async => const Left(ServerFailure('some error'))); - // act - final actual = await dataSource.getUserTickets(); + // act + final actual = await dataSource.getUserTickets(); - // assert - expect(actual, const Left(ServerFailure('some error'))); - }); + // assert + expect(actual.isLeft(), isTrue); + }, + ); + + test( + 'GIVEN executor returns two [TicketResponse] with the same product id but different product names ' + 'WHEN calling getUserTickets ' + 'THEN a [TicketCountModel] with the count of tickets and joined ticket names is returned', + () async { + // arrange + when(executor.call>(any)).thenAnswer( + (_) async => Right([ + TicketResponse( + id: 0, + dateCreated: DateTime.parse('2023-05-23'), + dateUsed: null, + productId: 0, + productName: 'A', + ), + TicketResponse( + id: 0, + dateCreated: DateTime.parse('2023-05-23'), + dateUsed: null, + productId: 0, + productName: 'B', + ), + ]), + ); + + // act + final actual = await dataSource.getUserTickets(); + + // assert + expect(actual.isRight(), isTrue); + final right = actual.getOrElse((_) => []); + expect(right, hasLength(1)); + expect(right.first.productId, equals(0)); + expect(right.first.count, equals(2)); + expect(right.first.productName, anyOf(['A/B', 'B/A'])); + }, + ); }); - group('useTicket', () { - test('should return [Right] when executor returns [Right]', () async { - // arrange - when(executor.call(any)).thenAnswer( - (_) async => Right( - TicketDto( - id: 0, - dateCreated: DateTime.parse('2023-04-11'), - dateUsed: DateTime.parse('2023-04-11'), - productName: 'productName', - ), - ), - ); + group( + 'useTicket', + () { + test( + 'GIVEN executor returns a [Right] value ' + 'WHEN calling useTicket ' + 'THEN a [Right] value is returned', + () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => Right( + TicketDto( + id: 0, + dateCreated: DateTime.parse('2023-04-11'), + dateUsed: DateTime.parse('2023-04-11'), + productName: 'productName', + ), + ), + ); - // act - final actual = await dataSource.useTicket(0); + // act + final actual = await dataSource.useTicket(0); - // assert - expect(actual.isRight(), isTrue); - }); + // assert + expect(actual.isRight(), isTrue); + }, + ); - test('should return [Left] if executor returns [Left]', () async { - // arrange - when(executor.call(any)) - .thenAnswer((_) async => const Left(ServerFailure('some error'))); + test( + 'GIVEN executor returns a [Left] value ' + 'WHEN calling useTicket ' + 'THEN a [Left] value is returned', + () async { + // arrange + when(executor.call(any)) + .thenAnswer((_) async => const Left(ServerFailure('some error'))); - // act - final actual = await dataSource.useTicket(0); + // act + final actual = await dataSource.useTicket(0); - // assert - expect(actual, const Left(ServerFailure('some error'))); - }); - }); + // assert + expect(actual.isLeft(), isTrue); + }, + ); + }, + ); } From 82c5bc3b2dddbe8fecabf1a84ec385ab685d1e3f Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Sat, 27 May 2023 08:56:45 +0200 Subject: [PATCH 18/32] refactor(settings): tidy up `settings_page.dart` (#473) --- lib/widgets/pages/settings/settings_page.dart | 332 +++++++++++------- 1 file changed, 199 insertions(+), 133 deletions(-) diff --git a/lib/widgets/pages/settings/settings_page.dart b/lib/widgets/pages/settings/settings_page.dart index 8ad7f543d..68c34c3fd 100644 --- a/lib/widgets/pages/settings/settings_page.dart +++ b/lib/widgets/pages/settings/settings_page.dart @@ -1,5 +1,3 @@ -//TODO(tta777): Refactor file, so rule does not need to be disabled -//ignore_for_file: prefer-moving-to-variable import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; @@ -24,159 +22,227 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; +/// Returns the given callback if the user data has been loaded, otherwise +/// returns null. +void Function()? _tappableIfUserLoaded( + BuildContext context, + void Function(BuildContext context, UserLoaded loadedState) callback, +) { + return switch (context.read().state) { + final UserLoaded loadedState => () => callback(context, loadedState), + _ => null, + }; +} + +// This callback is placed outside of any widgets because it is used by +// multiple widgets. +void _creditsTapCallback(BuildContext context) { + Navigator.push(context, CreditsPage.route).ignore(); +} + class SettingsPage extends StatelessWidget { - const SettingsPage({required this.scrollController}); + const SettingsPage._({required this.scrollController}); static Route routeWith({required ScrollController scrollController}) { return MaterialPageRoute( - builder: (_) => SettingsPage(scrollController: scrollController), + builder: (_) => SettingsPage._(scrollController: scrollController), ); } final ScrollController scrollController; - /// Tappable only if user data has been loaded. - void Function()? _ifUserStateLoaded( - UserState state, - void Function(UserLoaded) callback, - ) { - return (state is! UserLoaded) ? null : () => callback(state); - } - @override Widget build(BuildContext context) { - final userState = context.watch().state; - return AppScaffold.withTitle( title: Strings.settingsPageTitle, body: ListView( controller: scrollController, - children: [ - Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 16), - child: (userState is UserLoaded) - ? UserCard( - id: userState.user.id, - name: userState.user.name, - occupation: userState.user.occupation.fullName, - ) - : const UserCard.placeholder(), + children: const [ + _ProfileSection(), + _AccountSection(), + _AboutSection(), + _Footer(), + ], + ), + ); + } +} + +class _ProfileSection extends StatelessWidget { + const _ProfileSection(); + + @override + Widget build(BuildContext context) { + final userState = context.watch().state; + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 16), + child: switch (userState) { + UserLoaded(:final user) => UserCard( + id: user.id, + name: user.name, + occupation: user.occupation.fullName, ), - SettingsGroup( - title: Strings.settingsGroupAccount, - listItems: [ - SettingListEntry( - name: Strings.email, - valueWidget: ShimmerBuilder( - showShimmer: userState is! UserLoaded, - builder: (context, colorIfShimmer) { - return ColoredBox( - color: colorIfShimmer, - child: SettingValueText( - value: (userState is UserLoaded) - ? userState.user.email - : Strings.emailShimmerText, - ), - ); - }, - ), - onTap: _ifUserStateLoaded( - userState, - (st) => Navigator.push( - context, - ChangeEmailPage.routeWith(currentEmail: st.user.email), - ), - ), - // }, - ), - SettingListEntry( - name: Strings.passcode, - valueWidget: const SettingValueText( - value: Strings.change, - ), - onTap: _ifUserStateLoaded( - userState, - (_) => Navigator.push(context, ChangePasscodeFlow.route), - ), - ), - SettingListEntry( - name: Strings.logOut, - onTap: () { - context.read().unauthenticated(); - }, - ), - SettingListEntry( - name: Strings.deleteAccount, - destructive: true, - onTap: _ifUserStateLoaded( - userState, - (st) => _showDeleteAccountDialog(context, st.user.email), + _ => const UserCard.placeholder(), + }, + ); + } +} + +class _AccountSection extends StatelessWidget { + const _AccountSection(); + + void changeEmailTapCallback(BuildContext context, UserLoaded loadedState) { + Navigator.push( + context, + ChangeEmailPage.routeWith(currentEmail: loadedState.user.email), + ).ignore(); + } + + void changePasscodeTapCallback(BuildContext context, UserLoaded _) => + Navigator.push(context, ChangePasscodeFlow.route); + + void logoutTapCallback(BuildContext context) { + context.read().unauthenticated(); + } + + void deleteAccountTapCallback(BuildContext context, UserLoaded loadedState) { + _showDeleteAccountDialog(context, loadedState.user.email); + } + + @override + Widget build(BuildContext context) { + final userState = context.watch().state; + + return SettingsGroup( + title: Strings.settingsGroupAccount, + listItems: [ + SettingListEntry( + name: Strings.email, + valueWidget: ShimmerBuilder( + showShimmer: userState is! UserLoaded, + builder: (context, colorIfShimmer) { + return ColoredBox( + color: colorIfShimmer, + child: SettingValueText( + value: (userState is UserLoaded) + ? userState.user.email + : Strings.emailShimmerText, ), - ), - ], + ); + }, ), - SettingsGroup( - title: Strings.settingsGroupAbout, - listItems: [ - SettingListEntry( - name: Strings.frequentlyAskedQuestions, - onTap: () => Navigator.push(context, FAQPage.route), - ), - const SettingListEntry( - name: Strings.openingHours, - valueWidget: SettingValueText( - value: 'Not available', - ), - ), - SettingListEntry( - name: Strings.privacyPolicy, - onTap: () => - sl().launchUrlExternalApplication( - ApiUriConstants.privacyPolicyUri, - context, - ), - ), - SettingListEntry( - name: Strings.provideFeedback, - onTap: () => - sl().launchUrlExternalApplication( - ApiUriConstants.feedbackFormUri, - context, - ), + onTap: _tappableIfUserLoaded(context, changeEmailTapCallback), + // }, + ), + SettingListEntry( + name: Strings.passcode, + valueWidget: const SettingValueText( + value: Strings.change, + ), + onTap: _tappableIfUserLoaded(context, changePasscodeTapCallback), + ), + SettingListEntry( + name: Strings.logOut, + onTap: () => logoutTapCallback(context), + ), + SettingListEntry( + name: Strings.deleteAccount, + destructive: true, + onTap: _tappableIfUserLoaded(context, deleteAccountTapCallback), + ), + ], + ); + } +} + +class _AboutSection extends StatelessWidget { + const _AboutSection(); + + void faqTapCallback(BuildContext context) { + Navigator.push(context, FAQPage.route).ignore(); + } + + void privacyPolicyTapCallback(BuildContext context) { + sl().launchUrlExternalApplication( + ApiUriConstants.privacyPolicyUri, + context, + ); + } + + void provideFeedbackTapCallback(BuildContext context) { + sl().launchUrlExternalApplication( + ApiUriConstants.feedbackFormUri, + context, + ); + } + + @override + Widget build(BuildContext context) { + return SettingsGroup( + title: Strings.settingsGroupAbout, + listItems: [ + SettingListEntry( + name: Strings.frequentlyAskedQuestions, + onTap: () => faqTapCallback(context), + ), + const SettingListEntry( + name: Strings.openingHours, + valueWidget: SettingValueText( + value: 'Not available', + ), + ), + SettingListEntry( + name: Strings.privacyPolicy, + onTap: () => privacyPolicyTapCallback(context), + ), + SettingListEntry( + name: Strings.provideFeedback, + onTap: () => provideFeedbackTapCallback(context), + ), + SettingListEntry( + name: Strings.credits, + onTap: () => _creditsTapCallback(context), + ), + ], + ); + } +} + +class _Footer extends StatelessWidget { + const _Footer(); + + @override + Widget build(BuildContext context) { + final userId = switch (context.watch().state) { + UserLoaded(:final user) => user.id.toString(), + _ => '...', + }; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => _creditsTapCallback(context), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Text( + Strings.madeBy, + style: AppTextStyle.explainer, + textAlign: TextAlign.center, ), - SettingListEntry( - name: Strings.credits, - onTap: () => Navigator.push(context, CreditsPage.route), + const Gap(8), + Text( + '${Strings.userID}: $userId', + style: AppTextStyle.explainer, + textAlign: TextAlign.center, ), + const Gap(24), + const AnalogIOLogo.small(), ], ), - const Gap(24), - GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => Navigator.push(context, CreditsPage.route), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - Text( - Strings.madeBy, - style: AppTextStyle.explainer, - textAlign: TextAlign.center, - ), - const Gap(8), - Text( - '${Strings.userID}: ${userState is UserLoaded ? userState.user.id : '...'}', - style: AppTextStyle.explainer, - textAlign: TextAlign.center, - ), - const Gap(24), - const AnalogIOLogo.small(), - ], - ), - ), - ), - const Gap(24), - ], + ), ), ); } From 75a22dc6194bb547b21741b8ed452200a6c7acbf Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Sat, 27 May 2023 11:19:54 +0200 Subject: [PATCH 19/32] fix: Avoid type casting 204 return types from API (#479) --- .../datasources/receipt_remote_data_source.dart | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/lib/features/receipt/data/datasources/receipt_remote_data_source.dart b/lib/features/receipt/data/datasources/receipt_remote_data_source.dart index 0c98ec81a..0eb8f08c1 100644 --- a/lib/features/receipt/data/datasources/receipt_remote_data_source.dart +++ b/lib/features/receipt/data/datasources/receipt_remote_data_source.dart @@ -27,17 +27,9 @@ class ReceiptRemoteDataSource { /// Retrieves all of the users purchase receipts Future>> getUserPurchasesReceipts() async { - // The API CAN return null if the user has no tickets, - // but the generator doesn't pick up on this, hence the type parameter - return executor?>( - apiV2.apiV2PurchasesGet, - ).bindFuture((result) { - // If the user has no purchases, the API returns 204 No Content (body is - // null). The generator is bad and doesn't handle this case - if (result == null) return List.empty(); - return result - .map(PurchaseReceiptModel.fromSimplePurchaseResponse) - .toList(); - }); + return executor(apiV2.apiV2PurchasesGet).bindFuture( + (result) => + result.map(PurchaseReceiptModel.fromSimplePurchaseResponse).toList(), + ); } } From b30ed85911304fbf0ab5272823b30a239c405056 Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Sat, 27 May 2023 22:41:09 +0200 Subject: [PATCH 20/32] fix(receipts): Remove purchase receipt view & refactor payment status display (#478) This commit closes #477. - `PurchaseReceiptListEntry` widget now has 'tappable' set to false, disabling purchase receipts. - Updated `PaymentStatus` string values to provide clearer descriptions. - Renamed 'purchaseStatus' field to 'status' across multiple generic receipt widgets. - Refactored `PurchaseReceiptListEntry` to show price differently (or hide it), based on status. - misc: Updated `_formatter` to `_formatDate` in `ReceiptListEntry` and `ReceiptCard`. - misc: Removed rounded edges from `ReceiptListEntry`. --- lib/base/strings.dart | 7 +++++ .../domain/entities/payment_status.dart | 14 +++++---- .../presentation/pages/buy_tickets_page.dart | 2 +- .../presentation/pages/view_receipt_page.dart | 2 +- .../placeholder_receipt_list_entry.dart | 2 +- .../purchase_receipt_list_entry.dart | 24 ++++++++++++--- .../list_entry/receipt_list_entry.dart | 30 +++++++++++++++---- .../list_entry/swipe_receipt_list_entry.dart | 2 +- .../presentation/widgets/receipt_card.dart | 10 +++---- .../presentation/widgets/receipt_overlay.dart | 4 +-- .../presentation/widgets/tickets_section.dart | 2 +- 11 files changed, 71 insertions(+), 28 deletions(-) diff --git a/lib/base/strings.dart b/lib/base/strings.dart index 2f651868e..3b487436f 100644 --- a/lib/base/strings.dart +++ b/lib/base/strings.dart @@ -202,6 +202,13 @@ abstract final class Strings { static String noReceiptsOfTypeMessage(String buyOrSwipe) => 'When you $buyOrSwipe tickets, they will show up here.\nGo to the Tickets tab to $buyOrSwipe tickets.'; + // PaymentStatus enum + static const paymentStatusCompleted = 'Purchased'; + static const paymentStatusRefunded = 'Purchase refunded'; + static const paymentStatusAwaitingPayment = 'Payment pending'; + static const paymentStatusReserved = 'Payment reserved'; + static const paymentStatusFailed = 'Payment failed'; + // Statistics page static const statsYourStats = 'Your stats'; static const statsLeaderboards = 'Leaderboards'; diff --git a/lib/features/purchase/domain/entities/payment_status.dart b/lib/features/purchase/domain/entities/payment_status.dart index a0daa5cd0..1e59068b8 100644 --- a/lib/features/purchase/domain/entities/payment_status.dart +++ b/lib/features/purchase/domain/entities/payment_status.dart @@ -1,3 +1,4 @@ +import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.enums.swagger.dart'; enum PaymentStatus { @@ -32,12 +33,13 @@ enum PaymentStatus { @override String toString() { return switch (this) { - PaymentStatus.completed => 'Completed', - PaymentStatus.rejectedPayment => 'Rejected', - PaymentStatus.awaitingPayment => 'Pending', - PaymentStatus.refunded => 'Refunded', - PaymentStatus.error => 'Error', - PaymentStatus.reserved => 'Reserved', + PaymentStatus.completed => Strings.paymentStatusCompleted, + PaymentStatus.refunded => Strings.paymentStatusRefunded, + PaymentStatus.awaitingPayment => Strings.paymentStatusAwaitingPayment, + PaymentStatus.reserved => Strings.paymentStatusReserved, + PaymentStatus.rejectedPayment || + PaymentStatus.error => + Strings.paymentStatusFailed, }; } } diff --git a/lib/features/purchase/presentation/pages/buy_tickets_page.dart b/lib/features/purchase/presentation/pages/buy_tickets_page.dart index 3d0cdba6e..708a4f6ae 100644 --- a/lib/features/purchase/presentation/pages/buy_tickets_page.dart +++ b/lib/features/purchase/presentation/pages/buy_tickets_page.dart @@ -121,7 +121,7 @@ class BuyTicketsPage extends StatelessWidget { ReceiptOverlay.of(context).show( isTestEnvironment: envState is EnvironmentLoaded && envState.env.isTest, - paymentStatus: Strings.purchased, + status: Strings.purchased, productName: payment.productName, timeUsed: payment.purchaseTime, ); diff --git a/lib/features/receipt/presentation/pages/view_receipt_page.dart b/lib/features/receipt/presentation/pages/view_receipt_page.dart index 77a589b70..873db7b39 100644 --- a/lib/features/receipt/presentation/pages/view_receipt_page.dart +++ b/lib/features/receipt/presentation/pages/view_receipt_page.dart @@ -34,7 +34,7 @@ class ViewReceiptPage extends StatelessWidget { isInOverlay: false, isTestEnvironment: state is EnvironmentLoaded && state.env.isTest, - paymentStatus: paymentStatus, + status: paymentStatus, ), ], ), diff --git a/lib/features/receipt/presentation/widgets/list_entry/placeholder_receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/placeholder_receipt_list_entry.dart index 3980328db..8aaedfaea 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/placeholder_receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/placeholder_receipt_list_entry.dart @@ -16,7 +16,7 @@ class PlaceholderReceiptListEntry extends StatelessWidget { topText: Strings.receiptPlaceholderName, rightText: Strings.oneTicket, backgroundColor: Colors.transparent, - purchaseStatus: '', + status: '', ); } } diff --git a/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart index 067ced6da..5b3ed9f18 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/purchase_receipt_list_entry.dart @@ -1,4 +1,6 @@ +import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart'; import 'package:flutter/material.dart'; @@ -10,18 +12,32 @@ class PurchaseReceiptListEntry extends StatelessWidget { required this.receipt, }); + String get priceText { + final price = Strings.price(receipt.price); + return switch (receipt.paymentStatus) { + PaymentStatus.completed => price, + PaymentStatus.awaitingPayment || + PaymentStatus.reserved || + PaymentStatus.refunded => + '($price)', + PaymentStatus.error || PaymentStatus.rejectedPayment => '', + }; + } + @override Widget build(BuildContext context) { return ReceiptListEntry( - tappable: true, + tappable: false, name: receipt.productName, time: receipt.timeUsed, isPurchase: true, showShimmer: false, topText: '${receipt.amountPurchased} ${receipt.productName}', - rightText: '${receipt.price},-', - backgroundColor: AppColor.slightlyHighlighted, - purchaseStatus: '${receipt.paymentStatus}', + rightText: priceText, + backgroundColor: receipt.paymentStatus == PaymentStatus.completed + ? AppColor.slightlyHighlighted + : AppColor.lightGray.withOpacity(0.5), + status: '${receipt.paymentStatus}', ); } } diff --git a/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart index 55c02c189..13f1e3402 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart @@ -1,4 +1,5 @@ import 'package:animations/animations.dart'; +import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; import 'package:coffeecard/features/receipt/presentation/pages/view_receipt_page.dart'; import 'package:coffeecard/widgets/components/helpers/shimmer_builder.dart'; @@ -6,7 +7,7 @@ import 'package:coffeecard/widgets/components/list_entry.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; -final _formatter = DateFormat('dd.MM.yyyy'); +final _formatDateTime = DateFormat('dd/MM/y HH:mm').format; class ReceiptListEntry extends StatelessWidget { final bool tappable; @@ -17,7 +18,7 @@ class ReceiptListEntry extends StatelessWidget { final String topText; final String rightText; final Color backgroundColor; - final String purchaseStatus; + final String status; const ReceiptListEntry({ required this.tappable, @@ -28,18 +29,28 @@ class ReceiptListEntry extends StatelessWidget { required this.topText, required this.rightText, required this.backgroundColor, - required this.purchaseStatus, + required this.status, }); + TextStyle get statusTextStyle { + return tappable + ? AppTextStyle.receiptItemDate + : AppTextStyle.receiptItemDate.copyWith(color: AppColor.gray); + } + @override Widget build(BuildContext context) { + final time = this.time.toLocal(); + return OpenContainer( tappable: tappable, + // Remove rounded edges + closedShape: const RoundedRectangleBorder(), openBuilder: (context, _) { return ViewReceiptPage( name: name, time: time, - paymentStatus: purchaseStatus, + paymentStatus: status, ); }, closedBuilder: (context, openContainer) { @@ -61,8 +72,15 @@ class ReceiptListEntry extends StatelessWidget { ColoredBox( color: colorIfShimmer, child: Text( - '$purchaseStatus ${_formatter.format(time)}', - style: AppTextStyle.receiptItemDate, + status, + style: statusTextStyle, + ), + ), + ColoredBox( + color: colorIfShimmer, + child: Text( + _formatDateTime(time), + style: statusTextStyle, ), ), ], diff --git a/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart index e8883ceaa..6de282d79 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/swipe_receipt_list_entry.dart @@ -20,7 +20,7 @@ class SwipeReceiptListEntry extends StatelessWidget { topText: receipt.productName, rightText: Strings.oneTicket, backgroundColor: AppColor.white, - purchaseStatus: Strings.swiped, + status: Strings.swiped, ); } } diff --git a/lib/features/receipt/presentation/widgets/receipt_card.dart b/lib/features/receipt/presentation/widgets/receipt_card.dart index 8a3a111d8..226cbb55e 100644 --- a/lib/features/receipt/presentation/widgets/receipt_card.dart +++ b/lib/features/receipt/presentation/widgets/receipt_card.dart @@ -9,21 +9,21 @@ import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:intl/intl.dart'; -DateFormat get _formatter => DateFormat('EEEE d/M/y HH:mm'); +final _formatDate = DateFormat('EEEE d/M/y HH:mm').format; class ReceiptCard extends StatelessWidget { final String productName; final DateTime time; final bool isInOverlay; final bool isTestEnvironment; - final String paymentStatus; + final String status; const ReceiptCard({ required this.productName, required this.time, required this.isInOverlay, required this.isTestEnvironment, - required this.paymentStatus, + required this.status, }); @override @@ -39,7 +39,7 @@ class ReceiptCard extends StatelessWidget { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - paymentStatus, + status, style: AppTextStyle.textField, ), const Gap(16), @@ -50,7 +50,7 @@ class ReceiptCard extends StatelessWidget { style: AppTextStyle.textFieldBold, ), Text( - _formatter.format(localTime), + _formatDate(localTime), style: AppTextStyle.textField, ), ], diff --git a/lib/features/receipt/presentation/widgets/receipt_overlay.dart b/lib/features/receipt/presentation/widgets/receipt_overlay.dart index 5809e8446..8147e9cbc 100644 --- a/lib/features/receipt/presentation/widgets/receipt_overlay.dart +++ b/lib/features/receipt/presentation/widgets/receipt_overlay.dart @@ -19,7 +19,7 @@ class ReceiptOverlay { required String productName, required DateTime timeUsed, required bool isTestEnvironment, - required String paymentStatus, + required String status, }) async { await ScreenBrightness().setScreenBrightness(1); if (_context.mounted) { @@ -38,7 +38,7 @@ class ReceiptOverlay { time: timeUsed, isInOverlay: true, isTestEnvironment: isTestEnvironment, - paymentStatus: paymentStatus, + status: status, ), const Gap(12), Text( diff --git a/lib/features/ticket/presentation/widgets/tickets_section.dart b/lib/features/ticket/presentation/widgets/tickets_section.dart index 4fd711d62..9c3bca8e3 100644 --- a/lib/features/ticket/presentation/widgets/tickets_section.dart +++ b/lib/features/ticket/presentation/widgets/tickets_section.dart @@ -49,7 +49,7 @@ class TicketSection extends StatelessWidget { ReceiptOverlay.of(context).show( isTestEnvironment: envState is EnvironmentLoaded && envState.env.isTest, - paymentStatus: state.receipt is PurchaseReceipt + status: state.receipt is PurchaseReceipt ? (state.receipt as PurchaseReceipt) .paymentStatus .toString() From 3a1f57f8a49aef543c668c35b1151dd2d21dcc63 Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Sat, 27 May 2023 23:14:23 +0200 Subject: [PATCH 21/32] Restructure settings (#481) --- lib/base/strings.dart | 3 + .../widgets}/images/analog_logo.dart | 0 .../widgets}/images/analogio_logo.dart | 0 .../widgets}/images/coffee_image.dart | 0 .../widgets}/list_entry.dart | 0 .../presentation/pages/credits_page.dart | 6 +- .../widgets/leaderboard_list_entry.dart | 4 +- .../presentation/widgets/occupation_form.dart | 4 +- .../list_entry/receipt_list_entry.dart | 2 +- .../presentation/widgets/receipt_card.dart | 2 +- .../pages}/change_email_page.dart | 2 +- .../presentation/pages}/change_name_page.dart | 2 +- .../settings/presentation/pages/faq_page.dart | 21 ++ .../presentation/pages/settings_page.dart | 35 +++ .../presentation/pages/your_profile_page.dart | 41 +++ .../widgets}/change_passcode_flow.dart | 2 +- .../presentation/widgets/edit_profile.dart} | 48 +-- .../settings/presentation/widgets/faq.dart} | 19 -- .../widgets/forms}/change_email_form.dart | 0 .../widgets/forms}/change_name_form.dart | 0 .../widgets/forms}/change_passcode_form.dart | 2 +- .../forms}/change_passcode_repeat_form.dart | 0 .../widgets/sections/about_section.dart | 63 ++++ .../widgets/sections/account_section.dart | 133 ++++++++ .../presentation/widgets/sections/footer.dart | 48 +++ .../widgets/sections/profile_section.dart | 24 ++ .../widgets}/setting_value_text.dart | 0 .../presentation/widgets}/settings_group.dart | 0 .../widgets}/settings_list_entry.dart | 2 +- .../presentation/widgets}/user_card.dart | 9 +- .../presentation/widgets}/user_icon.dart | 2 +- lib/widgets/pages/home_page.dart | 2 +- lib/widgets/pages/login/login_page_base.dart | 2 +- lib/widgets/pages/settings/settings_page.dart | 290 ------------------ .../pages/splash/splash_loading_page.dart | 2 +- .../components/settings_list_entry_test.dart | 4 +- 36 files changed, 399 insertions(+), 375 deletions(-) rename lib/{widgets/components => core/widgets}/images/analog_logo.dart (100%) rename lib/{widgets/components => core/widgets}/images/analogio_logo.dart (100%) rename lib/{widgets/components => core/widgets}/images/coffee_image.dart (100%) rename lib/{widgets/components => core/widgets}/list_entry.dart (100%) rename lib/{widgets/pages/settings => features/settings/presentation/pages}/change_email_page.dart (94%) rename lib/{widgets/pages/settings => features/settings/presentation/pages}/change_name_page.dart (86%) create mode 100644 lib/features/settings/presentation/pages/faq_page.dart create mode 100644 lib/features/settings/presentation/pages/settings_page.dart create mode 100644 lib/features/settings/presentation/pages/your_profile_page.dart rename lib/{widgets/pages/settings => features/settings/presentation/widgets}/change_passcode_flow.dart (85%) rename lib/{widgets/pages/settings/your_profile_page.dart => features/settings/presentation/widgets/edit_profile.dart} (60%) rename lib/{widgets/pages/settings/faq_page.dart => features/settings/presentation/widgets/faq.dart} (74%) rename lib/{widgets/components/forms/settings => features/settings/presentation/widgets/forms}/change_email_form.dart (100%) rename lib/{widgets/components/forms/settings => features/settings/presentation/widgets/forms}/change_name_form.dart (100%) rename lib/{widgets/components/forms/settings => features/settings/presentation/widgets/forms}/change_passcode_form.dart (92%) rename lib/{widgets/components/forms/settings => features/settings/presentation/widgets/forms}/change_passcode_repeat_form.dart (100%) create mode 100644 lib/features/settings/presentation/widgets/sections/about_section.dart create mode 100644 lib/features/settings/presentation/widgets/sections/account_section.dart create mode 100644 lib/features/settings/presentation/widgets/sections/footer.dart create mode 100644 lib/features/settings/presentation/widgets/sections/profile_section.dart rename lib/{widgets/pages/settings => features/settings/presentation/widgets}/setting_value_text.dart (100%) rename lib/{widgets/components => features/settings/presentation/widgets}/settings_group.dart (100%) rename lib/{widgets/components => features/settings/presentation/widgets}/settings_list_entry.dart (97%) rename lib/{widgets/components/user => features/settings/presentation/widgets}/user_card.dart (89%) rename lib/{widgets/components/user => features/settings/presentation/widgets}/user_icon.dart (93%) delete mode 100644 lib/widgets/pages/settings/settings_page.dart diff --git a/lib/base/strings.dart b/lib/base/strings.dart index 3b487436f..a6e61e91b 100644 --- a/lib/base/strings.dart +++ b/lib/base/strings.dart @@ -279,10 +279,12 @@ abstract final class Strings { static const frequentlyAskedQuestions = 'Frequently Asked Questions'; static const faq = 'FAQ'; static const openingHours = 'Opening hours'; + static const notAvailable = 'Not available'; static const today = 'Today'; static const name = 'Name'; static const occupation = 'Occupation'; + static const occupationPlaceholder = 'Occupation name fullname'; static const appearAnonymous = 'Appear anonymous on leaderboard'; static const appearAnonymousSmall = 'Appear anonymous'; static const yourProfileDescription = @@ -371,4 +373,5 @@ abstract final class Strings { "Can't connect to Analog. Are you connected to the internet?"; static const String retry = 'Retry'; static const String unknownErrorOccured = 'An unknown error occured'; + static const String loading = 'Loading'; } diff --git a/lib/widgets/components/images/analog_logo.dart b/lib/core/widgets/images/analog_logo.dart similarity index 100% rename from lib/widgets/components/images/analog_logo.dart rename to lib/core/widgets/images/analog_logo.dart diff --git a/lib/widgets/components/images/analogio_logo.dart b/lib/core/widgets/images/analogio_logo.dart similarity index 100% rename from lib/widgets/components/images/analogio_logo.dart rename to lib/core/widgets/images/analogio_logo.dart diff --git a/lib/widgets/components/images/coffee_image.dart b/lib/core/widgets/images/coffee_image.dart similarity index 100% rename from lib/widgets/components/images/coffee_image.dart rename to lib/core/widgets/images/coffee_image.dart diff --git a/lib/widgets/components/list_entry.dart b/lib/core/widgets/list_entry.dart similarity index 100% rename from lib/widgets/components/list_entry.dart rename to lib/core/widgets/list_entry.dart diff --git a/lib/features/contributor/presentation/pages/credits_page.dart b/lib/features/contributor/presentation/pages/credits_page.dart index 97c7856db..7f274ac43 100644 --- a/lib/features/contributor/presentation/pages/credits_page.dart +++ b/lib/features/contributor/presentation/pages/credits_page.dart @@ -2,15 +2,15 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; import 'package:coffeecard/core/external/external_url_launcher.dart'; +import 'package:coffeecard/core/widgets/images/analogio_logo.dart'; import 'package:coffeecard/features/contributor/presentation/cubit/contributor_cubit.dart'; import 'package:coffeecard/features/contributor/presentation/widgets/contributor_card.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/settings_group.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/settings_list_entry.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/api_uri_constants.dart'; -import 'package:coffeecard/widgets/components/images/analogio_logo.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:coffeecard/widgets/components/section_title.dart'; -import 'package:coffeecard/widgets/components/settings_group.dart'; -import 'package:coffeecard/widgets/components/settings_list_entry.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; diff --git a/lib/features/leaderboard/presentation/widgets/leaderboard_list_entry.dart b/lib/features/leaderboard/presentation/widgets/leaderboard_list_entry.dart index c1b98ff8e..3c463fad1 100644 --- a/lib/features/leaderboard/presentation/widgets/leaderboard_list_entry.dart +++ b/lib/features/leaderboard/presentation/widgets/leaderboard_list_entry.dart @@ -1,9 +1,9 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/core/widgets/list_entry.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/user_icon.dart'; import 'package:coffeecard/widgets/components/helpers/shimmer_builder.dart'; -import 'package:coffeecard/widgets/components/list_entry.dart'; -import 'package:coffeecard/widgets/components/user/user_icon.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; diff --git a/lib/features/occupation/presentation/widgets/occupation_form.dart b/lib/features/occupation/presentation/widgets/occupation_form.dart index 7ac1c071d..4c528121b 100644 --- a/lib/features/occupation/presentation/widgets/occupation_form.dart +++ b/lib/features/occupation/presentation/widgets/occupation_form.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/core/widgets/list_entry.dart'; import 'package:coffeecard/features/occupation/domain/entities/occupation.dart'; -import 'package:coffeecard/widgets/components/list_entry.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/settings_list_entry.dart'; import 'package:coffeecard/widgets/components/section_title.dart'; -import 'package:coffeecard/widgets/components/settings_list_entry.dart'; import 'package:coffeecard/widgets/components/tickets/rounded_button.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; diff --git a/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart b/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart index 13f1e3402..d3ef65435 100644 --- a/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart +++ b/lib/features/receipt/presentation/widgets/list_entry/receipt_list_entry.dart @@ -1,9 +1,9 @@ import 'package:animations/animations.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/core/widgets/list_entry.dart'; import 'package:coffeecard/features/receipt/presentation/pages/view_receipt_page.dart'; import 'package:coffeecard/widgets/components/helpers/shimmer_builder.dart'; -import 'package:coffeecard/widgets/components/list_entry.dart'; import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; diff --git a/lib/features/receipt/presentation/widgets/receipt_card.dart b/lib/features/receipt/presentation/widgets/receipt_card.dart index 226cbb55e..18b99d151 100644 --- a/lib/features/receipt/presentation/widgets/receipt_card.dart +++ b/lib/features/receipt/presentation/widgets/receipt_card.dart @@ -1,10 +1,10 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/core/widgets/images/analog_logo.dart'; import 'package:coffeecard/utils/responsive.dart'; import 'package:coffeecard/utils/time_since.dart'; import 'package:coffeecard/widgets/components/card.dart'; -import 'package:coffeecard/widgets/components/images/analog_logo.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; import 'package:intl/intl.dart'; diff --git a/lib/widgets/pages/settings/change_email_page.dart b/lib/features/settings/presentation/pages/change_email_page.dart similarity index 94% rename from lib/widgets/pages/settings/change_email_page.dart rename to lib/features/settings/presentation/pages/change_email_page.dart index 80b5a9bb9..91399a346 100644 --- a/lib/widgets/pages/settings/change_email_page.dart +++ b/lib/features/settings/presentation/pages/change_email_page.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/forms/change_email_form.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; -import 'package:coffeecard/widgets/components/forms/settings/change_email_form.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/widgets/pages/settings/change_name_page.dart b/lib/features/settings/presentation/pages/change_name_page.dart similarity index 86% rename from lib/widgets/pages/settings/change_name_page.dart rename to lib/features/settings/presentation/pages/change_name_page.dart index aa947ae36..a43c75a4c 100644 --- a/lib/widgets/pages/settings/change_name_page.dart +++ b/lib/features/settings/presentation/pages/change_name_page.dart @@ -1,5 +1,5 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/widgets/components/forms/settings/change_name_form.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/forms/change_name_form.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:flutter/material.dart'; diff --git a/lib/features/settings/presentation/pages/faq_page.dart b/lib/features/settings/presentation/pages/faq_page.dart new file mode 100644 index 000000000..9b85a8c6b --- /dev/null +++ b/lib/features/settings/presentation/pages/faq_page.dart @@ -0,0 +1,21 @@ +import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/faq.dart'; +import 'package:coffeecard/widgets/components/scaffold.dart'; +import 'package:flutter/material.dart'; + +class FAQPage extends StatelessWidget { + static Route get route => MaterialPageRoute(builder: (_) => FAQPage()); + + @override + Widget build(BuildContext context) { + return AppScaffold.withTitle( + title: Strings.faq, + body: ListView( + padding: const EdgeInsets.all(16), + children: Strings.faqEntries.entries + .map((e) => FAQ(question: e.key, answer: e.value)) + .toList(), + ), + ); + } +} diff --git a/lib/features/settings/presentation/pages/settings_page.dart b/lib/features/settings/presentation/pages/settings_page.dart new file mode 100644 index 000000000..d21518b60 --- /dev/null +++ b/lib/features/settings/presentation/pages/settings_page.dart @@ -0,0 +1,35 @@ +import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/sections/about_section.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/sections/account_section.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/sections/footer.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/sections/profile_section.dart'; +import 'package:coffeecard/widgets/components/scaffold.dart'; +import 'package:flutter/material.dart'; + +class SettingsPage extends StatelessWidget { + const SettingsPage._({required this.scrollController}); + + static Route routeWith({required ScrollController scrollController}) { + return MaterialPageRoute( + builder: (_) => SettingsPage._(scrollController: scrollController), + ); + } + + final ScrollController scrollController; + + @override + Widget build(BuildContext context) { + return AppScaffold.withTitle( + title: Strings.settingsPageTitle, + body: ListView( + controller: scrollController, + children: const [ + ProfileSection(), + AccountSection(), + AboutSection(), + Footer(), + ], + ), + ); + } +} diff --git a/lib/features/settings/presentation/pages/your_profile_page.dart b/lib/features/settings/presentation/pages/your_profile_page.dart new file mode 100644 index 000000000..6772b129c --- /dev/null +++ b/lib/features/settings/presentation/pages/your_profile_page.dart @@ -0,0 +1,41 @@ +import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/edit_profile.dart'; +import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; +import 'package:coffeecard/widgets/components/loading.dart'; +import 'package:coffeecard/widgets/components/scaffold.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class YourProfilePage extends StatelessWidget { + const YourProfilePage(); + + static Route get route => + MaterialPageRoute(builder: (_) => const YourProfilePage()); + + @override + Widget build(BuildContext context) { + return AppScaffold.withTitle( + title: Strings.yourProfilePageTitle, + body: BlocBuilder( + buildWhen: (_, current) => current is UserLoaded, + builder: (_, userLoadedState) { + if (userLoadedState is! UserLoaded) return const SizedBox.shrink(); + + return BlocBuilder( + buildWhen: (previous, current) => + previous is UserUpdating || current is UserUpdating, + builder: (context, state) { + return Loading( + loading: state is UserUpdating, + child: SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 36), + child: EditProfile(user: userLoadedState.user), + ), + ); + }, + ); + }, + ), + ); + } +} diff --git a/lib/widgets/pages/settings/change_passcode_flow.dart b/lib/features/settings/presentation/widgets/change_passcode_flow.dart similarity index 85% rename from lib/widgets/pages/settings/change_passcode_flow.dart rename to lib/features/settings/presentation/widgets/change_passcode_flow.dart index d80df68cb..f206a86a9 100644 --- a/lib/widgets/pages/settings/change_passcode_flow.dart +++ b/lib/features/settings/presentation/widgets/change_passcode_flow.dart @@ -1,5 +1,5 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/widgets/components/forms/settings/change_passcode_form.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/forms/change_passcode_form.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:coffeecard/widgets/routers/app_flow.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/pages/settings/your_profile_page.dart b/lib/features/settings/presentation/widgets/edit_profile.dart similarity index 60% rename from lib/widgets/pages/settings/your_profile_page.dart rename to lib/features/settings/presentation/widgets/edit_profile.dart index f873d0ea2..fbe35878a 100644 --- a/lib/widgets/pages/settings/your_profile_page.dart +++ b/lib/features/settings/presentation/widgets/edit_profile.dart @@ -1,55 +1,19 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/text_styles.dart'; import 'package:coffeecard/features/occupation/presentation/pages/change_occupation_page.dart'; +import 'package:coffeecard/features/settings/presentation/pages/change_name_page.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/settings_group.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/settings_list_entry.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/user_icon.dart'; import 'package:coffeecard/features/user/domain/entities/user.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/utils/responsive.dart'; -import 'package:coffeecard/widgets/components/loading.dart'; -import 'package:coffeecard/widgets/components/scaffold.dart'; -import 'package:coffeecard/widgets/components/settings_group.dart'; -import 'package:coffeecard/widgets/components/settings_list_entry.dart'; -import 'package:coffeecard/widgets/components/user/user_icon.dart'; -import 'package:coffeecard/widgets/pages/settings/change_name_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; -class YourProfilePage extends StatelessWidget { - const YourProfilePage(); - - static Route get route => - MaterialPageRoute(builder: (_) => const YourProfilePage()); - - @override - Widget build(BuildContext context) { - return AppScaffold.withTitle( - title: Strings.yourProfilePageTitle, - body: BlocBuilder( - buildWhen: (_, current) => current is UserLoaded, - builder: (_, userLoadedState) { - if (userLoadedState is! UserLoaded) return const SizedBox.shrink(); - - return BlocBuilder( - buildWhen: (previous, current) => - previous is UserUpdating || current is UserUpdating, - builder: (context, state) { - return Loading( - loading: state is UserUpdating, - child: SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 36), - child: _EditProfile(user: userLoadedState.user), - ), - ); - }, - ); - }, - ), - ); - } -} - -class _EditProfile extends StatelessWidget { - const _EditProfile({required this.user}); +class EditProfile extends StatelessWidget { + const EditProfile({required this.user}); final User user; diff --git a/lib/widgets/pages/settings/faq_page.dart b/lib/features/settings/presentation/widgets/faq.dart similarity index 74% rename from lib/widgets/pages/settings/faq_page.dart rename to lib/features/settings/presentation/widgets/faq.dart index fc613d95d..a3c42087a 100644 --- a/lib/widgets/pages/settings/faq_page.dart +++ b/lib/features/settings/presentation/widgets/faq.dart @@ -1,26 +1,7 @@ -import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; -class FAQPage extends StatelessWidget { - static Route get route => MaterialPageRoute(builder: (_) => FAQPage()); - - @override - Widget build(BuildContext context) { - return AppScaffold.withTitle( - title: Strings.faq, - body: ListView( - padding: const EdgeInsets.all(16), - children: Strings.faqEntries.entries - .map((e) => FAQ(question: e.key, answer: e.value)) - .toList(), - ), - ); - } -} - class FAQ extends StatefulWidget { const FAQ({required this.question, required this.answer}); diff --git a/lib/widgets/components/forms/settings/change_email_form.dart b/lib/features/settings/presentation/widgets/forms/change_email_form.dart similarity index 100% rename from lib/widgets/components/forms/settings/change_email_form.dart rename to lib/features/settings/presentation/widgets/forms/change_email_form.dart diff --git a/lib/widgets/components/forms/settings/change_name_form.dart b/lib/features/settings/presentation/widgets/forms/change_name_form.dart similarity index 100% rename from lib/widgets/components/forms/settings/change_name_form.dart rename to lib/features/settings/presentation/widgets/forms/change_name_form.dart diff --git a/lib/widgets/components/forms/settings/change_passcode_form.dart b/lib/features/settings/presentation/widgets/forms/change_passcode_form.dart similarity index 92% rename from lib/widgets/components/forms/settings/change_passcode_form.dart rename to lib/features/settings/presentation/widgets/forms/change_passcode_form.dart index 31a4f6366..1dd2701e0 100644 --- a/lib/widgets/components/forms/settings/change_passcode_form.dart +++ b/lib/features/settings/presentation/widgets/forms/change_passcode_form.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; import 'package:coffeecard/utils/input_validator.dart'; import 'package:coffeecard/widgets/components/forms/form.dart'; -import 'package:coffeecard/widgets/components/forms/settings/change_passcode_repeat_form.dart'; import 'package:flutter/material.dart'; class ChangePasscodeForm extends StatelessWidget { diff --git a/lib/widgets/components/forms/settings/change_passcode_repeat_form.dart b/lib/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart similarity index 100% rename from lib/widgets/components/forms/settings/change_passcode_repeat_form.dart rename to lib/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart diff --git a/lib/features/settings/presentation/widgets/sections/about_section.dart b/lib/features/settings/presentation/widgets/sections/about_section.dart new file mode 100644 index 000000000..e2ebd3e61 --- /dev/null +++ b/lib/features/settings/presentation/widgets/sections/about_section.dart @@ -0,0 +1,63 @@ +import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/core/external/external_url_launcher.dart'; +import 'package:coffeecard/features/contributor/presentation/pages/credits_page.dart'; +import 'package:coffeecard/features/settings/presentation/pages/faq_page.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/setting_value_text.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/settings_group.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/settings_list_entry.dart'; +import 'package:coffeecard/service_locator.dart'; +import 'package:coffeecard/utils/api_uri_constants.dart'; +import 'package:flutter/material.dart'; + +class AboutSection extends StatelessWidget { + const AboutSection(); + + void faqTapCallback(BuildContext context) { + Navigator.push(context, FAQPage.route).ignore(); + } + + void privacyPolicyTapCallback(BuildContext context) { + sl().launchUrlExternalApplication( + ApiUriConstants.privacyPolicyUri, + context, + ); + } + + void provideFeedbackTapCallback(BuildContext context) { + sl().launchUrlExternalApplication( + ApiUriConstants.feedbackFormUri, + context, + ); + } + + @override + Widget build(BuildContext context) { + return SettingsGroup( + title: Strings.settingsGroupAbout, + listItems: [ + SettingListEntry( + name: Strings.frequentlyAskedQuestions, + onTap: () => faqTapCallback(context), + ), + const SettingListEntry( + name: Strings.openingHours, + valueWidget: SettingValueText( + value: Strings.notAvailable, + ), + ), + SettingListEntry( + name: Strings.privacyPolicy, + onTap: () => privacyPolicyTapCallback(context), + ), + SettingListEntry( + name: Strings.provideFeedback, + onTap: () => provideFeedbackTapCallback(context), + ), + SettingListEntry( + name: Strings.credits, + onTap: () => Navigator.push(context, CreditsPage.route).ignore(), + ), + ], + ); + } +} diff --git a/lib/features/settings/presentation/widgets/sections/account_section.dart b/lib/features/settings/presentation/widgets/sections/account_section.dart new file mode 100644 index 000000000..e57e850d0 --- /dev/null +++ b/lib/features/settings/presentation/widgets/sections/account_section.dart @@ -0,0 +1,133 @@ +import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/base/style/colors.dart'; +import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; +import 'package:coffeecard/features/settings/presentation/pages/change_email_page.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/change_passcode_flow.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/setting_value_text.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/settings_group.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/settings_list_entry.dart'; +import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; +import 'package:coffeecard/widgets/components/dialog.dart'; +import 'package:coffeecard/widgets/components/helpers/shimmer_builder.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class AccountSection extends StatelessWidget { + const AccountSection(); + + void changeEmailTapCallback(BuildContext context, UserLoaded loadedState) { + Navigator.push( + context, + ChangeEmailPage.routeWith(currentEmail: loadedState.user.email), + ).ignore(); + } + + void changePasscodeTapCallback(BuildContext context, UserLoaded _) => + Navigator.push(context, ChangePasscodeFlow.route); + + void logoutTapCallback(BuildContext context) { + context.read().unauthenticated(); + } + + void deleteAccountTapCallback(BuildContext context, UserLoaded loadedState) { + _showDeleteAccountDialog(context, loadedState.user.email); + } + + @override + Widget build(BuildContext context) { + final userState = context.watch().state; + + return SettingsGroup( + title: Strings.settingsGroupAccount, + listItems: [ + SettingListEntry( + name: Strings.email, + valueWidget: ShimmerBuilder( + showShimmer: userState is! UserLoaded, + builder: (context, colorIfShimmer) { + return ColoredBox( + color: colorIfShimmer, + child: SettingValueText( + value: (userState is UserLoaded) + ? userState.user.email + : Strings.emailShimmerText, + ), + ); + }, + ), + onTap: _tappableIfUserLoaded(context, changeEmailTapCallback), + // }, + ), + SettingListEntry( + name: Strings.passcode, + valueWidget: const SettingValueText( + value: Strings.change, + ), + onTap: _tappableIfUserLoaded(context, changePasscodeTapCallback), + ), + SettingListEntry( + name: Strings.logOut, + onTap: () => logoutTapCallback(context), + ), + SettingListEntry( + name: Strings.deleteAccount, + destructive: true, + onTap: _tappableIfUserLoaded(context, deleteAccountTapCallback), + ), + ], + ); + } +} + +void _showDeleteAccountDialog(BuildContext context, String email) { + appDialog( + context: context, + title: Strings.deleteAccount, + children: const [Text(Strings.deleteAccountText)], + actions: [ + TextButton( + child: const Text( + Strings.buttonUnderstand, + style: TextStyle(color: AppColor.errorOnBright), + ), + onPressed: () { + context.read().requestUserAccountDeletion(); + + closeAppDialog(context); + appDialog( + context: context, + title: Strings.deleteAccount, + children: [Text(Strings.deleteAccountEmailConfirmation(email))], + actions: [ + TextButton( + child: const Text(Strings.buttonOK), + onPressed: () { + closeAppDialog(context); + context.read().unauthenticated(); + }, + ), + ], + dismissible: false, + ); + }, + ), + TextButton( + onPressed: () => closeAppDialog(context), + child: const Text(Strings.buttonCancel), + ), + ], + dismissible: true, + ); +} + +/// Returns the given callback if the user data has been loaded, otherwise +/// returns null. +void Function()? _tappableIfUserLoaded( + BuildContext context, + void Function(BuildContext context, UserLoaded loadedState) callback, +) { + return switch (context.read().state) { + final UserLoaded loadedState => () => callback(context, loadedState), + _ => null, + }; +} diff --git a/lib/features/settings/presentation/widgets/sections/footer.dart b/lib/features/settings/presentation/widgets/sections/footer.dart new file mode 100644 index 000000000..26e6cff3e --- /dev/null +++ b/lib/features/settings/presentation/widgets/sections/footer.dart @@ -0,0 +1,48 @@ +import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/core/widgets/images/analogio_logo.dart'; +import 'package:coffeecard/features/contributor/presentation/pages/credits_page.dart'; +import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gap/gap.dart'; + +class Footer extends StatelessWidget { + const Footer(); + + @override + Widget build(BuildContext context) { + final userId = switch (context.watch().state) { + UserLoaded(:final user) => user.id.toString(), + _ => '...', + }; + + return Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () => Navigator.push(context, CreditsPage.route).ignore(), + child: Padding( + padding: const EdgeInsets.all(24), + child: Column( + children: [ + Text( + Strings.madeBy, + style: AppTextStyle.explainer, + textAlign: TextAlign.center, + ), + const Gap(8), + Text( + '${Strings.userID}: $userId', + style: AppTextStyle.explainer, + textAlign: TextAlign.center, + ), + const Gap(24), + const AnalogIOLogo.small(), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/settings/presentation/widgets/sections/profile_section.dart b/lib/features/settings/presentation/widgets/sections/profile_section.dart new file mode 100644 index 000000000..39e7e9397 --- /dev/null +++ b/lib/features/settings/presentation/widgets/sections/profile_section.dart @@ -0,0 +1,24 @@ +import 'package:coffeecard/features/settings/presentation/widgets/user_card.dart'; +import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class ProfileSection extends StatelessWidget { + const ProfileSection(); + + @override + Widget build(BuildContext context) { + final userState = context.watch().state; + return Padding( + padding: const EdgeInsets.only(left: 16, right: 16, top: 16), + child: switch (userState) { + UserLoaded(:final user) => UserCard( + id: user.id, + name: user.name, + occupation: user.occupation.fullName, + ), + _ => const UserCard.placeholder(), + }, + ); + } +} diff --git a/lib/widgets/pages/settings/setting_value_text.dart b/lib/features/settings/presentation/widgets/setting_value_text.dart similarity index 100% rename from lib/widgets/pages/settings/setting_value_text.dart rename to lib/features/settings/presentation/widgets/setting_value_text.dart diff --git a/lib/widgets/components/settings_group.dart b/lib/features/settings/presentation/widgets/settings_group.dart similarity index 100% rename from lib/widgets/components/settings_group.dart rename to lib/features/settings/presentation/widgets/settings_group.dart diff --git a/lib/widgets/components/settings_list_entry.dart b/lib/features/settings/presentation/widgets/settings_list_entry.dart similarity index 97% rename from lib/widgets/components/settings_list_entry.dart rename to lib/features/settings/presentation/widgets/settings_list_entry.dart index 883769bb2..d9ba1404a 100644 --- a/lib/widgets/components/settings_list_entry.dart +++ b/lib/features/settings/presentation/widgets/settings_list_entry.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/widgets/components/list_entry.dart'; +import 'package:coffeecard/core/widgets/list_entry.dart'; import 'package:flutter/material.dart'; class SettingListEntry extends StatelessWidget { diff --git a/lib/widgets/components/user/user_card.dart b/lib/features/settings/presentation/widgets/user_card.dart similarity index 89% rename from lib/widgets/components/user/user_card.dart rename to lib/features/settings/presentation/widgets/user_card.dart index 2b13ec604..aea442670 100644 --- a/lib/widgets/components/user/user_card.dart +++ b/lib/features/settings/presentation/widgets/user_card.dart @@ -1,9 +1,10 @@ +import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/features/settings/presentation/pages/your_profile_page.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/user_icon.dart'; import 'package:coffeecard/widgets/components/helpers/shimmer_builder.dart'; import 'package:coffeecard/widgets/components/helpers/tappable.dart'; -import 'package:coffeecard/widgets/components/user/user_icon.dart'; -import 'package:coffeecard/widgets/pages/settings/your_profile_page.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; @@ -17,8 +18,8 @@ class UserCard extends StatelessWidget { const UserCard.placeholder() : id = 0, isPlaceholder = true, - name = 'Loading', - occupation = 'Occupation name fullname'; + name = Strings.loading, + occupation = Strings.occupationPlaceholder; final int id; final String name; diff --git a/lib/widgets/components/user/user_icon.dart b/lib/features/settings/presentation/widgets/user_icon.dart similarity index 93% rename from lib/widgets/components/user/user_icon.dart rename to lib/features/settings/presentation/widgets/user_icon.dart index 662e781a9..7360cc4a9 100644 --- a/lib/widgets/components/user/user_icon.dart +++ b/lib/features/settings/presentation/widgets/user_icon.dart @@ -1,4 +1,4 @@ -import 'package:coffeecard/widgets/components/images/coffee_image.dart'; +import 'package:coffeecard/core/widgets/images/coffee_image.dart'; import 'package:flutter/material.dart'; class UserIcon extends StatelessWidget { diff --git a/lib/widgets/pages/home_page.dart b/lib/widgets/pages/home_page.dart index 16ee089f1..517ab4289 100644 --- a/lib/widgets/pages/home_page.dart +++ b/lib/widgets/pages/home_page.dart @@ -8,12 +8,12 @@ import 'package:coffeecard/features/leaderboard/presentation/pages/leaderboard_p import 'package:coffeecard/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart'; import 'package:coffeecard/features/receipt/presentation/pages/receipts_page.dart'; +import 'package:coffeecard/features/settings/presentation/pages/settings_page.dart'; import 'package:coffeecard/features/ticket/presentation/cubit/tickets_cubit.dart'; import 'package:coffeecard/features/ticket/presentation/pages/tickets_page.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/widgets/components/helpers/lazy_indexed_stack.dart'; -import 'package:coffeecard/widgets/pages/settings/settings_page.dart'; import 'package:coffeecard/widgets/routers/app_flow.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/widgets/pages/login/login_page_base.dart b/lib/widgets/pages/login/login_page_base.dart index 8c4ee4445..14fa1231e 100644 --- a/lib/widgets/pages/login/login_page_base.dart +++ b/lib/widgets/pages/login/login_page_base.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/core/widgets/images/analog_logo.dart'; import 'package:coffeecard/core/widgets/upgrade_alert.dart'; import 'package:coffeecard/utils/responsive.dart'; -import 'package:coffeecard/widgets/components/images/analog_logo.dart'; import 'package:coffeecard/widgets/components/login/login_input_hint.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/pages/settings/settings_page.dart b/lib/widgets/pages/settings/settings_page.dart deleted file mode 100644 index 68c34c3fd..000000000 --- a/lib/widgets/pages/settings/settings_page.dart +++ /dev/null @@ -1,290 +0,0 @@ -import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/base/style/colors.dart'; -import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/core/external/external_url_launcher.dart'; -import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; -import 'package:coffeecard/features/contributor/presentation/pages/credits_page.dart'; -import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; -import 'package:coffeecard/service_locator.dart'; -import 'package:coffeecard/utils/api_uri_constants.dart'; -import 'package:coffeecard/widgets/components/dialog.dart'; -import 'package:coffeecard/widgets/components/helpers/shimmer_builder.dart'; -import 'package:coffeecard/widgets/components/images/analogio_logo.dart'; -import 'package:coffeecard/widgets/components/scaffold.dart'; -import 'package:coffeecard/widgets/components/settings_group.dart'; -import 'package:coffeecard/widgets/components/settings_list_entry.dart'; -import 'package:coffeecard/widgets/components/user/user_card.dart'; -import 'package:coffeecard/widgets/pages/settings/change_email_page.dart'; -import 'package:coffeecard/widgets/pages/settings/change_passcode_flow.dart'; -import 'package:coffeecard/widgets/pages/settings/faq_page.dart'; -import 'package:coffeecard/widgets/pages/settings/setting_value_text.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:gap/gap.dart'; - -/// Returns the given callback if the user data has been loaded, otherwise -/// returns null. -void Function()? _tappableIfUserLoaded( - BuildContext context, - void Function(BuildContext context, UserLoaded loadedState) callback, -) { - return switch (context.read().state) { - final UserLoaded loadedState => () => callback(context, loadedState), - _ => null, - }; -} - -// This callback is placed outside of any widgets because it is used by -// multiple widgets. -void _creditsTapCallback(BuildContext context) { - Navigator.push(context, CreditsPage.route).ignore(); -} - -class SettingsPage extends StatelessWidget { - const SettingsPage._({required this.scrollController}); - - static Route routeWith({required ScrollController scrollController}) { - return MaterialPageRoute( - builder: (_) => SettingsPage._(scrollController: scrollController), - ); - } - - final ScrollController scrollController; - - @override - Widget build(BuildContext context) { - return AppScaffold.withTitle( - title: Strings.settingsPageTitle, - body: ListView( - controller: scrollController, - children: const [ - _ProfileSection(), - _AccountSection(), - _AboutSection(), - _Footer(), - ], - ), - ); - } -} - -class _ProfileSection extends StatelessWidget { - const _ProfileSection(); - - @override - Widget build(BuildContext context) { - final userState = context.watch().state; - return Padding( - padding: const EdgeInsets.only(left: 16, right: 16, top: 16), - child: switch (userState) { - UserLoaded(:final user) => UserCard( - id: user.id, - name: user.name, - occupation: user.occupation.fullName, - ), - _ => const UserCard.placeholder(), - }, - ); - } -} - -class _AccountSection extends StatelessWidget { - const _AccountSection(); - - void changeEmailTapCallback(BuildContext context, UserLoaded loadedState) { - Navigator.push( - context, - ChangeEmailPage.routeWith(currentEmail: loadedState.user.email), - ).ignore(); - } - - void changePasscodeTapCallback(BuildContext context, UserLoaded _) => - Navigator.push(context, ChangePasscodeFlow.route); - - void logoutTapCallback(BuildContext context) { - context.read().unauthenticated(); - } - - void deleteAccountTapCallback(BuildContext context, UserLoaded loadedState) { - _showDeleteAccountDialog(context, loadedState.user.email); - } - - @override - Widget build(BuildContext context) { - final userState = context.watch().state; - - return SettingsGroup( - title: Strings.settingsGroupAccount, - listItems: [ - SettingListEntry( - name: Strings.email, - valueWidget: ShimmerBuilder( - showShimmer: userState is! UserLoaded, - builder: (context, colorIfShimmer) { - return ColoredBox( - color: colorIfShimmer, - child: SettingValueText( - value: (userState is UserLoaded) - ? userState.user.email - : Strings.emailShimmerText, - ), - ); - }, - ), - onTap: _tappableIfUserLoaded(context, changeEmailTapCallback), - // }, - ), - SettingListEntry( - name: Strings.passcode, - valueWidget: const SettingValueText( - value: Strings.change, - ), - onTap: _tappableIfUserLoaded(context, changePasscodeTapCallback), - ), - SettingListEntry( - name: Strings.logOut, - onTap: () => logoutTapCallback(context), - ), - SettingListEntry( - name: Strings.deleteAccount, - destructive: true, - onTap: _tappableIfUserLoaded(context, deleteAccountTapCallback), - ), - ], - ); - } -} - -class _AboutSection extends StatelessWidget { - const _AboutSection(); - - void faqTapCallback(BuildContext context) { - Navigator.push(context, FAQPage.route).ignore(); - } - - void privacyPolicyTapCallback(BuildContext context) { - sl().launchUrlExternalApplication( - ApiUriConstants.privacyPolicyUri, - context, - ); - } - - void provideFeedbackTapCallback(BuildContext context) { - sl().launchUrlExternalApplication( - ApiUriConstants.feedbackFormUri, - context, - ); - } - - @override - Widget build(BuildContext context) { - return SettingsGroup( - title: Strings.settingsGroupAbout, - listItems: [ - SettingListEntry( - name: Strings.frequentlyAskedQuestions, - onTap: () => faqTapCallback(context), - ), - const SettingListEntry( - name: Strings.openingHours, - valueWidget: SettingValueText( - value: 'Not available', - ), - ), - SettingListEntry( - name: Strings.privacyPolicy, - onTap: () => privacyPolicyTapCallback(context), - ), - SettingListEntry( - name: Strings.provideFeedback, - onTap: () => provideFeedbackTapCallback(context), - ), - SettingListEntry( - name: Strings.credits, - onTap: () => _creditsTapCallback(context), - ), - ], - ); - } -} - -class _Footer extends StatelessWidget { - const _Footer(); - - @override - Widget build(BuildContext context) { - final userId = switch (context.watch().state) { - UserLoaded(:final user) => user.id.toString(), - _ => '...', - }; - - return Padding( - padding: const EdgeInsets.symmetric(vertical: 16), - child: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () => _creditsTapCallback(context), - child: Padding( - padding: const EdgeInsets.all(24), - child: Column( - children: [ - Text( - Strings.madeBy, - style: AppTextStyle.explainer, - textAlign: TextAlign.center, - ), - const Gap(8), - Text( - '${Strings.userID}: $userId', - style: AppTextStyle.explainer, - textAlign: TextAlign.center, - ), - const Gap(24), - const AnalogIOLogo.small(), - ], - ), - ), - ), - ); - } -} - -void _showDeleteAccountDialog(BuildContext context, String email) { - appDialog( - context: context, - title: Strings.deleteAccount, - children: const [Text(Strings.deleteAccountText)], - actions: [ - TextButton( - child: const Text( - Strings.buttonUnderstand, - style: TextStyle(color: AppColor.errorOnBright), - ), - onPressed: () { - context.read().requestUserAccountDeletion(); - - closeAppDialog(context); - appDialog( - context: context, - title: Strings.deleteAccount, - children: [Text(Strings.deleteAccountEmailConfirmation(email))], - actions: [ - TextButton( - child: const Text(Strings.buttonOK), - onPressed: () { - closeAppDialog(context); - context.read().unauthenticated(); - }, - ), - ], - dismissible: false, - ); - }, - ), - TextButton( - onPressed: () => closeAppDialog(context), - child: const Text(Strings.buttonCancel), - ), - ], - dismissible: true, - ); -} diff --git a/lib/widgets/pages/splash/splash_loading_page.dart b/lib/widgets/pages/splash/splash_loading_page.dart index 67ad70a18..0e3495f9a 100644 --- a/lib/widgets/pages/splash/splash_loading_page.dart +++ b/lib/widgets/pages/splash/splash_loading_page.dart @@ -1,5 +1,5 @@ import 'package:coffeecard/base/style/colors.dart'; -import 'package:coffeecard/widgets/components/images/analog_logo.dart'; +import 'package:coffeecard/core/widgets/images/analog_logo.dart'; import 'package:flutter/material.dart'; class SplashLoadingPage extends StatelessWidget { diff --git a/test/widgets/components/settings_list_entry_test.dart b/test/widgets/components/settings_list_entry_test.dart index f231dbaab..b5abcdc1b 100644 --- a/test/widgets/components/settings_list_entry_test.dart +++ b/test/widgets/components/settings_list_entry_test.dart @@ -1,5 +1,5 @@ -import 'package:coffeecard/widgets/components/settings_list_entry.dart'; -import 'package:coffeecard/widgets/pages/settings/setting_value_text.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/setting_value_text.dart'; +import 'package:coffeecard/features/settings/presentation/widgets/settings_list_entry.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; From bae775e1502239385db96a32413cdecebb271fe8 Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Sun, 28 May 2023 08:52:07 +0200 Subject: [PATCH 22/32] Refactor and test login (#483) --- .../account_remote_data_source.dart} | 58 ++---- lib/cubits/register/register_cubit.dart | 4 +- .../login/domain/usecases/login_user.dart | 27 +++ .../presentation/cubit}/login_cubit.dart | 22 ++- .../presentation/cubit}/login_state.dart | 0 .../pages}/forgot_passcode_page.dart | 0 .../presentation/pages}/login_page_base.dart | 2 +- .../presentation/pages}/login_page_email.dart | 8 +- .../pages}/login_page_passcode.dart | 16 +- .../presentation/widgets}/login_cta.dart | 0 .../widgets}/login_email_text_field.dart | 0 .../widgets}/login_input_hint.dart | 0 .../widgets/login_passcode_dots.dart | 27 +++ .../presentation/widgets/numpad/numpad.dart} | 71 ++----- .../widgets/numpad/numpad_button.dart | 23 +++ .../widgets/numpad/numpad_digit_button.dart | 26 +++ .../presentation/widgets/passcode_dot.dart} | 29 +-- .../widgets/forms/change_email_form.dart | 5 +- lib/service_locator.dart | 17 +- lib/utils/reactivation_authenticator.dart | 4 +- .../forgot_passcode/forgot_passcode_form.dart | 8 +- .../forms/register/register_email_form.dart | 5 +- lib/widgets/pages/register/register_flow.dart | 5 +- .../pages/register/register_page_name.dart | 4 +- lib/widgets/routers/splash_router.dart | 2 +- .../account_remote_data_source_test.dart | 173 ++++++++++++++++++ test/cubits/login/login_cubit_test.dart | 86 --------- .../account_repository_test.dart | 52 ------ .../domain/usecases/login_user_test.dart | 38 ++++ .../presentation/cubit/login_cubit_test.dart | 93 ++++++++++ 30 files changed, 497 insertions(+), 308 deletions(-) rename lib/{data/repositories/shared/account_repository.dart => core/data/datasources/account_remote_data_source.dart} (62%) create mode 100644 lib/features/login/domain/usecases/login_user.dart rename lib/{cubits/login => features/login/presentation/cubit}/login_cubit.dart (76%) rename lib/{cubits/login => features/login/presentation/cubit}/login_state.dart (100%) rename lib/{widgets/pages/login => features/login/presentation/pages}/forgot_passcode_page.dart (100%) rename lib/{widgets/pages/login => features/login/presentation/pages}/login_page_base.dart (95%) rename lib/{widgets/pages/login => features/login/presentation/pages}/login_page_email.dart (88%) rename lib/{widgets/pages/login => features/login/presentation/pages}/login_page_passcode.dart (76%) rename lib/{widgets/components/login => features/login/presentation/widgets}/login_cta.dart (100%) rename lib/{widgets/components/login => features/login/presentation/widgets}/login_email_text_field.dart (100%) rename lib/{widgets/components/login => features/login/presentation/widgets}/login_input_hint.dart (100%) create mode 100644 lib/features/login/presentation/widgets/login_passcode_dots.dart rename lib/{widgets/components/login/login_numpad.dart => features/login/presentation/widgets/numpad/numpad.dart} (65%) create mode 100644 lib/features/login/presentation/widgets/numpad/numpad_button.dart create mode 100644 lib/features/login/presentation/widgets/numpad/numpad_digit_button.dart rename lib/{widgets/components/login/login_passcode_dots.dart => features/login/presentation/widgets/passcode_dot.dart} (54%) create mode 100644 test/core/data/datasources/account_remote_data_source_test.dart delete mode 100644 test/cubits/login/login_cubit_test.dart delete mode 100644 test/data/repositories/v2/account_repository/account_repository_test.dart create mode 100644 test/features/login/domain/usecases/login_user_test.dart create mode 100644 test/features/login/presentation/cubit/login_cubit_test.dart diff --git a/lib/data/repositories/shared/account_repository.dart b/lib/core/data/datasources/account_remote_data_source.dart similarity index 62% rename from lib/data/repositories/shared/account_repository.dart rename to lib/core/data/datasources/account_remote_data_source.dart index b2e9a7b14..9eacd6b1a 100644 --- a/lib/data/repositories/shared/account_repository.dart +++ b/lib/core/data/datasources/account_remote_data_source.dart @@ -1,4 +1,5 @@ import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/either_extensions.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/features/user/data/models/user_model.dart'; import 'package:coffeecard/features/user/domain/entities/user.dart'; @@ -6,12 +7,11 @@ import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart' hide MessageResponseDto; import 'package:coffeecard/models/account/authenticated_user.dart'; -import 'package:coffeecard/models/account/update_user.dart'; import 'package:coffeecard/utils/api_uri_constants.dart'; import 'package:fpdart/fpdart.dart'; -class AccountRepository { - AccountRepository({ +class AccountRemoteDataSource { + AccountRemoteDataSource({ required this.apiV1, required this.apiV2, required this.executor, @@ -27,7 +27,7 @@ class AccountRepository { String encodedPasscode, int occupationId, ) async { - final result = await executor( + return executor( () => apiV2.apiV2AccountPost( body: RegisterAccountRequest( name: name, @@ -36,17 +36,14 @@ class AccountRepository { programmeId: occupationId, ), ), - ); - - return result.map((_) => const Right(null)); + ).bindFuture((_) => const Right(null)); } - /// Returns the user token or throws an error. Future> login( String email, String encodedPasscode, ) async { - final result = await executor( + return executor( () => apiV1.apiV1AccountLoginPost( body: LoginDto( email: email, @@ -54,9 +51,7 @@ class AccountRepository { version: ApiUriConstants.minAppVersion, ), ), - ); - - return result.map( + ).bindFuture( (result) => AuthenticatedUser( email: email, token: result.token!, @@ -65,28 +60,9 @@ class AccountRepository { } Future> getUser() async { - final result = await executor( + return executor( apiV2.apiV2AccountGet, - ); - - return result.map(UserModel.fromResponse); - } - - /// Update user information - Future> updateUser(UpdateUser user) async { - final result = await executor( - () => apiV2.apiV2AccountPut( - body: UpdateUserRequest( - name: user.name, - programmeId: user.occupationId, - email: user.email, - privacyActivated: user.privacyActivated, - password: user.encodedPasscode, - ), - ), - ); - - return result.map(UserModel.fromResponse); + ).bindFuture(UserModel.fromResponse); } Future> requestPasscodeReset( @@ -96,24 +72,14 @@ class AccountRepository { () => apiV1.apiV1AccountForgotpasswordPost(body: EmailDto(email: email)), ); - return result.bind((_) => const Right(null)); - } - - Future> requestAccountDeletion() async { - final result = await executor( - apiV2.apiV2AccountDelete, - ); - - return result.bind((_) => const Right(null)); + return result.pure(null); } Future> emailExists(String email) async { - final result = await executor( + return executor( () => apiV2.apiV2AccountEmailExistsPost( body: EmailExistsRequest(email: email), ), - ); - - return result.map((result) => result.emailExists); + ).bindFuture((result) => result.emailExists); } } diff --git a/lib/cubits/register/register_cubit.dart b/lib/cubits/register/register_cubit.dart index b71e1976d..b53083560 100644 --- a/lib/cubits/register/register_cubit.dart +++ b/lib/cubits/register/register_cubit.dart @@ -1,4 +1,4 @@ -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/encode_passcode.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; @@ -7,7 +7,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; part 'register_state.dart'; class RegisterCubit extends Cubit { - AccountRepository repository; + AccountRemoteDataSource repository; RegisterCubit({required this.repository}) : super(RegisterInitial()); diff --git a/lib/features/login/domain/usecases/login_user.dart b/lib/features/login/domain/usecases/login_user.dart new file mode 100644 index 000000000..ab989c429 --- /dev/null +++ b/lib/features/login/domain/usecases/login_user.dart @@ -0,0 +1,27 @@ +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/models/account/authenticated_user.dart'; +import 'package:equatable/equatable.dart'; +import 'package:fpdart/fpdart.dart'; + +class LoginUser implements UseCase { + final AccountRemoteDataSource remoteDataSource; + + LoginUser({required this.remoteDataSource}); + + @override + Future> call(Params params) { + return remoteDataSource.login(params.email, params.encodedPasscode); + } +} + +class Params extends Equatable { + final String email; + final String encodedPasscode; + + const Params({required this.email, required this.encodedPasscode}); + + @override + List get props => [email, encodedPasscode]; +} diff --git a/lib/cubits/login/login_cubit.dart b/lib/features/login/presentation/cubit/login_cubit.dart similarity index 76% rename from lib/cubits/login/login_cubit.dart rename to lib/features/login/presentation/cubit/login_cubit.dart index 520498ed8..48c1295c1 100644 --- a/lib/cubits/login/login_cubit.dart +++ b/lib/features/login/presentation/cubit/login_cubit.dart @@ -1,6 +1,5 @@ import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; -import 'package:coffeecard/service_locator.dart'; +import 'package:coffeecard/features/login/domain/usecases/login_user.dart'; import 'package:coffeecard/utils/encode_passcode.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; import 'package:equatable/equatable.dart'; @@ -9,18 +8,19 @@ import 'package:flutter_bloc/flutter_bloc.dart'; part 'login_state.dart'; class LoginCubit extends Cubit { + final String email; + final AuthenticationCubit authenticationCubit; + final LoginUser loginUser; + final FirebaseAnalyticsEventLogging firebaseAnalyticsEventLogging; + LoginCubit({ required this.email, required this.authenticationCubit, - required this.accountRepository, + required this.loginUser, + required this.firebaseAnalyticsEventLogging, }) : super(const LoginTypingPasscode('')); - final String email; - final AuthenticationCubit authenticationCubit; - final AccountRepository accountRepository; - void addPasscodeInput(String input) { - // used for type promotion final st = state; final String newPasscode; @@ -41,12 +41,14 @@ class LoginCubit extends Cubit { emit(const LoginLoading()); - final either = await accountRepository.login(email, encodedPasscode); + final either = await loginUser( + Params(email: email, encodedPasscode: encodedPasscode), + ); either.fold( (error) => emit(LoginError(error.reason)), (user) { - sl().loginEvent(); + firebaseAnalyticsEventLogging.loginEvent(); authenticationCubit.authenticated( user.email, diff --git a/lib/cubits/login/login_state.dart b/lib/features/login/presentation/cubit/login_state.dart similarity index 100% rename from lib/cubits/login/login_state.dart rename to lib/features/login/presentation/cubit/login_state.dart diff --git a/lib/widgets/pages/login/forgot_passcode_page.dart b/lib/features/login/presentation/pages/forgot_passcode_page.dart similarity index 100% rename from lib/widgets/pages/login/forgot_passcode_page.dart rename to lib/features/login/presentation/pages/forgot_passcode_page.dart diff --git a/lib/widgets/pages/login/login_page_base.dart b/lib/features/login/presentation/pages/login_page_base.dart similarity index 95% rename from lib/widgets/pages/login/login_page_base.dart rename to lib/features/login/presentation/pages/login_page_base.dart index 14fa1231e..036d65a32 100644 --- a/lib/widgets/pages/login/login_page_base.dart +++ b/lib/features/login/presentation/pages/login_page_base.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/base/style/text_styles.dart'; import 'package:coffeecard/core/widgets/images/analog_logo.dart'; import 'package:coffeecard/core/widgets/upgrade_alert.dart'; +import 'package:coffeecard/features/login/presentation/widgets/login_input_hint.dart'; import 'package:coffeecard/utils/responsive.dart'; -import 'package:coffeecard/widgets/components/login/login_input_hint.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; diff --git a/lib/widgets/pages/login/login_page_email.dart b/lib/features/login/presentation/pages/login_page_email.dart similarity index 88% rename from lib/widgets/pages/login/login_page_email.dart rename to lib/features/login/presentation/pages/login_page_email.dart index 609d4fc9e..30717295f 100644 --- a/lib/widgets/pages/login/login_page_email.dart +++ b/lib/features/login/presentation/pages/login_page_email.dart @@ -1,10 +1,10 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/features/login/presentation/pages/login_page_base.dart'; +import 'package:coffeecard/features/login/presentation/pages/login_page_passcode.dart'; +import 'package:coffeecard/features/login/presentation/widgets/login_cta.dart'; +import 'package:coffeecard/features/login/presentation/widgets/login_email_text_field.dart'; import 'package:coffeecard/utils/email_is_valid.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; -import 'package:coffeecard/widgets/components/login/login_cta.dart'; -import 'package:coffeecard/widgets/components/login/login_email_text_field.dart'; -import 'package:coffeecard/widgets/pages/login/login_page_base.dart'; -import 'package:coffeecard/widgets/pages/login/login_page_passcode.dart'; import 'package:coffeecard/widgets/pages/register/register_flow.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/pages/login/login_page_passcode.dart b/lib/features/login/presentation/pages/login_page_passcode.dart similarity index 76% rename from lib/widgets/pages/login/login_page_passcode.dart rename to lib/features/login/presentation/pages/login_page_passcode.dart index 589e5fa29..2a8125011 100644 --- a/lib/widgets/pages/login/login_page_passcode.dart +++ b/lib/features/login/presentation/pages/login_page_passcode.dart @@ -1,14 +1,15 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; -import 'package:coffeecard/cubits/login/login_cubit.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; +import 'package:coffeecard/features/login/domain/usecases/login_user.dart'; +import 'package:coffeecard/features/login/presentation/cubit/login_cubit.dart'; +import 'package:coffeecard/features/login/presentation/pages/forgot_passcode_page.dart'; +import 'package:coffeecard/features/login/presentation/pages/login_page_base.dart'; +import 'package:coffeecard/features/login/presentation/widgets/login_passcode_dots.dart'; +import 'package:coffeecard/features/login/presentation/widgets/numpad/numpad.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; +import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; import 'package:coffeecard/widgets/components/loading_overlay.dart'; -import 'package:coffeecard/widgets/components/login/login_numpad.dart'; -import 'package:coffeecard/widgets/components/login/login_passcode_dots.dart'; -import 'package:coffeecard/widgets/pages/login/forgot_passcode_page.dart'; -import 'package:coffeecard/widgets/pages/login/login_page_base.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -38,8 +39,9 @@ class _LoginPagePasscodeState extends State { return BlocProvider( create: (_) => LoginCubit( email: widget.email, - accountRepository: sl(), + loginUser: sl(), authenticationCubit: sl(), + firebaseAnalyticsEventLogging: sl(), ), child: BlocConsumer( listenWhen: (previous, current) => diff --git a/lib/widgets/components/login/login_cta.dart b/lib/features/login/presentation/widgets/login_cta.dart similarity index 100% rename from lib/widgets/components/login/login_cta.dart rename to lib/features/login/presentation/widgets/login_cta.dart diff --git a/lib/widgets/components/login/login_email_text_field.dart b/lib/features/login/presentation/widgets/login_email_text_field.dart similarity index 100% rename from lib/widgets/components/login/login_email_text_field.dart rename to lib/features/login/presentation/widgets/login_email_text_field.dart diff --git a/lib/widgets/components/login/login_input_hint.dart b/lib/features/login/presentation/widgets/login_input_hint.dart similarity index 100% rename from lib/widgets/components/login/login_input_hint.dart rename to lib/features/login/presentation/widgets/login_input_hint.dart diff --git a/lib/features/login/presentation/widgets/login_passcode_dots.dart b/lib/features/login/presentation/widgets/login_passcode_dots.dart new file mode 100644 index 000000000..d27734995 --- /dev/null +++ b/lib/features/login/presentation/widgets/login_passcode_dots.dart @@ -0,0 +1,27 @@ +import 'package:coffeecard/features/login/presentation/widgets/passcode_dot.dart'; +import 'package:flutter/material.dart'; + +class LoginPasscodeDots extends StatelessWidget { + const LoginPasscodeDots({ + required this.passcodeLength, + required this.hasError, + }); + + final int passcodeLength; + final bool hasError; + + @override + Widget build(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate( + 4, + (index) => PasscodeDot( + isLit: index < passcodeLength, + isError: hasError, + ), + growable: false, + ), + ); + } +} diff --git a/lib/widgets/components/login/login_numpad.dart b/lib/features/login/presentation/widgets/numpad/numpad.dart similarity index 65% rename from lib/widgets/components/login/login_numpad.dart rename to lib/features/login/presentation/widgets/numpad/numpad.dart index 7ec3ccbf4..e22336222 100644 --- a/lib/widgets/components/login/login_numpad.dart +++ b/lib/features/login/presentation/widgets/numpad/numpad.dart @@ -1,10 +1,10 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/cubits/login/login_cubit.dart'; -import 'package:coffeecard/utils/responsive.dart'; +import 'package:coffeecard/features/login/presentation/cubit/login_cubit.dart'; +import 'package:coffeecard/features/login/presentation/widgets/numpad/numpad_button.dart'; +import 'package:coffeecard/features/login/presentation/widgets/numpad/numpad_digit_button.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart' show HapticFeedback; import 'package:flutter_bloc/flutter_bloc.dart'; class Numpad extends StatefulWidget { @@ -63,30 +63,30 @@ class _NumpadState extends State with SingleTickerProviderStateMixin { children: [ const TableRow( children: [ - _NumpadDigitButton('1'), - _NumpadDigitButton('2'), - _NumpadDigitButton('3'), + NumpadDigitButton('1'), + NumpadDigitButton('2'), + NumpadDigitButton('3'), ], ), const TableRow( children: [ - _NumpadDigitButton('4'), - _NumpadDigitButton('5'), - _NumpadDigitButton('6'), + NumpadDigitButton('4'), + NumpadDigitButton('5'), + NumpadDigitButton('6'), ], ), const TableRow( children: [ - _NumpadDigitButton('7'), - _NumpadDigitButton('8'), - _NumpadDigitButton('9'), + NumpadDigitButton('7'), + NumpadDigitButton('8'), + NumpadDigitButton('9'), ], ), TableRow( children: [ TableCell( verticalAlignment: TableCellVerticalAlignment.fill, - child: _NumpadButton( + child: NumpadButton( onPressed: widget.forgotPasscodeAction, child: Text( Strings.loginForgot, @@ -94,10 +94,10 @@ class _NumpadState extends State with SingleTickerProviderStateMixin { ), ), ), - const _NumpadDigitButton('0'), + const NumpadDigitButton('0'), TableCell( verticalAlignment: TableCellVerticalAlignment.fill, - child: _NumpadButton( + child: NumpadButton( onPressed: (context) { context.read().clearPasscode(); }, @@ -115,44 +115,3 @@ class _NumpadState extends State with SingleTickerProviderStateMixin { ); } } - -class _NumpadButton extends StatelessWidget { - final void Function(BuildContext) onPressed; - final Widget child; - - const _NumpadButton({ - required this.child, - required this.onPressed, - }); - - @override - Widget build(BuildContext context) { - return TextButton( - onPressed: () { - HapticFeedback.lightImpact(); - onPressed(context); - }, - child: child, - ); - } -} - -class _NumpadDigitButton extends StatelessWidget { - const _NumpadDigitButton(this.digit); - final String digit; - - @override - Widget build(BuildContext context) { - return _NumpadButton( - onPressed: (BuildContext context) { - context.read().addPasscodeInput(digit); - }, - child: Text( - digit, - style: deviceIsSmall(context) - ? AppTextStyle.ticketsCount - : AppTextStyle.numpadDigit, - ), - ); - } -} diff --git a/lib/features/login/presentation/widgets/numpad/numpad_button.dart b/lib/features/login/presentation/widgets/numpad/numpad_button.dart new file mode 100644 index 000000000..685cdfeba --- /dev/null +++ b/lib/features/login/presentation/widgets/numpad/numpad_button.dart @@ -0,0 +1,23 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class NumpadButton extends StatelessWidget { + final void Function(BuildContext) onPressed; + final Widget child; + + const NumpadButton({ + required this.child, + required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return TextButton( + onPressed: () { + HapticFeedback.lightImpact(); + onPressed(context); + }, + child: child, + ); + } +} diff --git a/lib/features/login/presentation/widgets/numpad/numpad_digit_button.dart b/lib/features/login/presentation/widgets/numpad/numpad_digit_button.dart new file mode 100644 index 000000000..84c0ab43b --- /dev/null +++ b/lib/features/login/presentation/widgets/numpad/numpad_digit_button.dart @@ -0,0 +1,26 @@ +import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/features/login/presentation/cubit/login_cubit.dart'; +import 'package:coffeecard/features/login/presentation/widgets/numpad/numpad_button.dart'; +import 'package:coffeecard/utils/responsive.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class NumpadDigitButton extends StatelessWidget { + const NumpadDigitButton(this.digit); + final String digit; + + @override + Widget build(BuildContext context) { + return NumpadButton( + onPressed: (BuildContext context) { + context.read().addPasscodeInput(digit); + }, + child: Text( + digit, + style: deviceIsSmall(context) + ? AppTextStyle.ticketsCount + : AppTextStyle.numpadDigit, + ), + ); + } +} diff --git a/lib/widgets/components/login/login_passcode_dots.dart b/lib/features/login/presentation/widgets/passcode_dot.dart similarity index 54% rename from lib/widgets/components/login/login_passcode_dots.dart rename to lib/features/login/presentation/widgets/passcode_dot.dart index 00a5638ad..0aa26886d 100644 --- a/lib/widgets/components/login/login_passcode_dots.dart +++ b/lib/features/login/presentation/widgets/passcode_dot.dart @@ -1,35 +1,10 @@ import 'package:coffeecard/base/style/colors.dart'; import 'package:flutter/material.dart'; -class LoginPasscodeDots extends StatelessWidget { - const LoginPasscodeDots({ - required this.passcodeLength, - required this.hasError, - }); - - final int passcodeLength; - final bool hasError; - - @override - Widget build(BuildContext context) { - return Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - 4, - (index) => _PasscodeDot( - isLit: index < passcodeLength, - isError: hasError, - ), - growable: false, - ), - ); - } -} - -class _PasscodeDot extends StatelessWidget { +class PasscodeDot extends StatelessWidget { final bool isLit; final bool isError; - const _PasscodeDot({ + const PasscodeDot({ required this.isLit, required this.isError, }); diff --git a/lib/features/settings/presentation/widgets/forms/change_email_form.dart b/lib/features/settings/presentation/widgets/forms/change_email_form.dart index 2153242a6..dba429421 100644 --- a/lib/features/settings/presentation/widgets/forms/change_email_form.dart +++ b/lib/features/settings/presentation/widgets/forms/change_email_form.dart @@ -1,5 +1,5 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/input_validator.dart'; import 'package:coffeecard/widgets/components/forms/form.dart'; @@ -30,7 +30,8 @@ class ChangeEmailForm extends StatelessWidget { InputValidator( forceErrorMessage: true, validate: (text) async { - final either = await sl().emailExists(text); + final either = + await sl().emailExists(text); return either.fold( (l) => const Left(Strings.emailValidationError), diff --git a/lib/service_locator.dart b/lib/service_locator.dart index 8b65682b2..e9af7e39a 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -1,9 +1,9 @@ import 'package:chopper/chopper.dart'; +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; import 'package:coffeecard/core/external/external_url_launcher.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/data/api/interceptors/authentication_interceptor.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; import 'package:coffeecard/data/repositories/v1/voucher_repository.dart'; import 'package:coffeecard/data/storage/secure_storage.dart'; @@ -17,6 +17,7 @@ import 'package:coffeecard/features/environment/presentation/cubit/environment_c import 'package:coffeecard/features/leaderboard/data/datasources/leaderboard_remote_data_source.dart'; import 'package:coffeecard/features/leaderboard/domain/usecases/get_leaderboard.dart'; import 'package:coffeecard/features/leaderboard/presentation/cubit/leaderboard_cubit.dart'; +import 'package:coffeecard/features/login/domain/usecases/login_user.dart'; import 'package:coffeecard/features/occupation/data/datasources/occupation_remote_data_source.dart'; import 'package:coffeecard/features/occupation/domain/usecases/get_occupations.dart'; import 'package:coffeecard/features/occupation/presentation/cubit/occupation_cubit.dart'; @@ -134,8 +135,8 @@ void configureServices() { ); // v1 and v2 - sl.registerFactory( - () => AccountRepository( + sl.registerFactory( + () => AccountRemoteDataSource( apiV1: sl(), apiV2: sl(), executor: sl(), @@ -162,6 +163,7 @@ void initFeatures() { initPayment(); initLeaderboard(); initEnvironment(); + initLogin(); } void initOpeningHours() { @@ -318,3 +320,12 @@ void initEnvironment() { ), ); } + +void initLogin() { + // bloc + + // use case + sl.registerFactory(() => LoginUser(remoteDataSource: sl())); + + // data source +} diff --git a/lib/utils/reactivation_authenticator.dart b/lib/utils/reactivation_authenticator.dart index 412e00b2c..97008c46d 100644 --- a/lib/utils/reactivation_authenticator.dart +++ b/lib/utils/reactivation_authenticator.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:chopper/chopper.dart'; +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/data/storage/secure_storage.dart'; import 'package:coffeecard/utils/mutex.dart'; import 'package:get_it/get_it.dart'; @@ -99,7 +99,7 @@ class ReactivationAuthenticator extends Authenticator { mutex.lock(); try { - final accountRepository = serviceLocator.get(); + final accountRepository = serviceLocator.get(); // this call may return 401 which triggers a recursive call, use a guard final either = await accountRepository.login(email, encodedPasscode); diff --git a/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart b/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart index b69f7cd62..3efa51b16 100644 --- a/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart +++ b/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart @@ -1,5 +1,5 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/input_validator.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; @@ -27,7 +27,8 @@ class ForgotPasscodeForm extends StatelessWidget { InputValidator( forceErrorMessage: true, validate: (text) async { - final either = await sl().emailExists(text); + final either = + await sl().emailExists(text); return either.fold( (l) => const Left(Strings.emailValidationError), @@ -51,7 +52,8 @@ class ForgotPasscodeForm extends StatelessWidget { Future _onSubmit(BuildContext context, String email) async { showLoadingOverlay(context); - final either = await sl().requestPasscodeReset(email); + final either = + await sl().requestPasscodeReset(email); final title = either.fold( (_) => Strings.forgotPasscodeError, diff --git a/lib/widgets/components/forms/register/register_email_form.dart b/lib/widgets/components/forms/register/register_email_form.dart index d0a85b852..dea7af3e5 100644 --- a/lib/widgets/components/forms/register/register_email_form.dart +++ b/lib/widgets/components/forms/register/register_email_form.dart @@ -1,5 +1,5 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/input_validator.dart'; import 'package:coffeecard/widgets/components/forms/form.dart'; @@ -23,7 +23,8 @@ class RegisterEmailForm extends StatelessWidget { InputValidator( forceErrorMessage: true, validate: (text) async { - final either = await sl().emailExists(text); + final either = + await sl().emailExists(text); return either.fold( (l) => const Left(Strings.emailValidationError), diff --git a/lib/widgets/pages/register/register_flow.dart b/lib/widgets/pages/register/register_flow.dart index c035d87d2..07b4f89ef 100644 --- a/lib/widgets/pages/register/register_flow.dart +++ b/lib/widgets/pages/register/register_flow.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; import 'package:coffeecard/cubits/register/register_cubit.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/features/occupation/presentation/cubit/occupation_cubit.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; @@ -20,7 +20,8 @@ class RegisterFlow extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => RegisterCubit(repository: sl()), + create: (_) => + RegisterCubit(repository: sl()), ), BlocProvider( lazy: false, diff --git a/lib/widgets/pages/register/register_page_name.dart b/lib/widgets/pages/register/register_page_name.dart index 0fea5aab0..a75061145 100644 --- a/lib/widgets/pages/register/register_page_name.dart +++ b/lib/widgets/pages/register/register_page_name.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; import 'package:coffeecard/cubits/register/register_cubit.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; @@ -40,7 +40,7 @@ class RegisterPageName extends StatelessWidget { return Padding( padding: const EdgeInsets.all(16), child: BlocProvider( - create: (_) => RegisterCubit(repository: sl()), + create: (_) => RegisterCubit(repository: sl()), child: BlocListener( listener: (context, state) { if (state is RegisterSuccess) return _showSuccessDialog(context); diff --git a/lib/widgets/routers/splash_router.dart b/lib/widgets/routers/splash_router.dart index 25cd0c0ea..71c53a858 100644 --- a/lib/widgets/routers/splash_router.dart +++ b/lib/widgets/routers/splash_router.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; +import 'package:coffeecard/features/login/presentation/pages/login_page_email.dart'; import 'package:coffeecard/widgets/pages/home_page.dart'; -import 'package:coffeecard/widgets/pages/login/login_page_email.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/test/core/data/datasources/account_remote_data_source_test.dart b/test/core/data/datasources/account_remote_data_source_test.dart new file mode 100644 index 000000000..1b583c23d --- /dev/null +++ b/test/core/data/datasources/account_remote_data_source_test.dart @@ -0,0 +1,173 @@ +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/occupation/data/models/occupation_model.dart'; +import 'package:coffeecard/features/user/data/models/user_model.dart'; +import 'package:coffeecard/features/user/domain/entities/role.dart'; +import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart' as v1; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart' as v2; +import 'package:coffeecard/models/account/authenticated_user.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'account_remote_data_source_test.mocks.dart'; + +@GenerateMocks([v1.CoffeecardApi, v2.CoffeecardApiV2, NetworkRequestExecutor]) +void main() { + late MockCoffeecardApi apiV1; + late MockCoffeecardApiV2 apiV2; + late MockNetworkRequestExecutor executor; + late AccountRemoteDataSource dataSource; + + setUp(() { + apiV1 = MockCoffeecardApi(); + apiV2 = MockCoffeecardApiV2(); + + executor = MockNetworkRequestExecutor(); + dataSource = AccountRemoteDataSource( + apiV1: apiV1, + apiV2: apiV2, + executor: executor, + ); + }); + + const testError = 'some error'; + + group('register', () { + test('should call executor', () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => Right(v2.MessageResponseDto()), + ); + + // act + await dataSource.register( + 'name', + 'email', + 'passcode', + 0, + ); + + // assert + verify(executor.call(any)); + }); + }); + + group('login', () { + test( + 'should return [Right] when executor returns token', + () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => Right(v1.TokenDto(token: 'token')), + ); + + // act + final actual = await dataSource.login('email', 'encodedPasscode'); + + // assert + expect( + actual, + const Right(AuthenticatedUser(email: 'email', token: 'token')), + ); + }, + ); + }); + + group('getUser', () { + test('should return [Left] if executor fails', () async { + // arrange + when(executor.call(any)) + .thenAnswer((_) async => const Left(ServerFailure(testError))); + + // act + final actual = await dataSource.getUser(); + + // assert + expect(actual, const Left(ServerFailure(testError))); + }); + + test( + 'should return [Right] when executor returns user response', + () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => Right( + v2.UserResponse( + email: 'email', + id: 0, + name: 'name', + privacyActivated: true, + programme: { + 'id': 0, + 'shortName': 'shortName', + 'fullName': 'fullName', + }, + rankAllTime: 0, + rankMonth: 0, + rankSemester: 0, + role: 'Barista', + ), + ), + ); + + // act + final actual = await dataSource.getUser(); + + // assert + expect( + actual, + const Right( + UserModel( + id: 0, + name: 'name', + email: 'email', + privacyActivated: true, + occupation: OccupationModel( + id: 0, + shortName: 'shortName', + fullName: 'fullName', + ), + rankMonth: 0, + rankSemester: 0, + rankTotal: 0, + role: Role.barista, + ), + ), + ); + }, + ); + }); + + group('requestPasscodeReset', () { + test('should return [Right] when executor succeeds', () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => Right(v1.MessageResponseDto()), + ); + + // act + final actual = await dataSource.requestPasscodeReset('name'); + + // assert + expect(actual, const Right(null)); + }); + }); + + group('emailExists', () { + test('should return [Right] if executor succeeds', () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => Right(v2.EmailExistsResponse(emailExists: true)), + ); + + // act + final actual = await dataSource.emailExists('name'); + + // assert + expect(actual, const Right(true)); + }); + }); +} diff --git a/test/cubits/login/login_cubit_test.dart b/test/cubits/login/login_cubit_test.dart deleted file mode 100644 index 80664efec..000000000 --- a/test/cubits/login/login_cubit_test.dart +++ /dev/null @@ -1,86 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; -import 'package:coffeecard/cubits/login/login_cubit.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'login_cubit_test.mocks.dart'; - -@GenerateMocks([AuthenticationCubit]) -@GenerateMocks([AccountRepository]) -void main() { - group('login cubit tests', () { - late LoginCubit loginCubit; - final authenticationCubit = MockAuthenticationCubit(); - final accountRepository = MockAccountRepository(); - - setUp(() { - loginCubit = LoginCubit( - email: '', - authenticationCubit: authenticationCubit, - accountRepository: accountRepository, - ); - }); - - test('initial state is LoginTypingPasscode', () { - expect(loginCubit.state, const LoginTypingPasscode('')); - }); - - blocTest( - 'addPasscodeInput emits only TypingPasscode when passcode length is less than 4', - build: () => loginCubit, - act: (cubit) => cubit - ..addPasscodeInput('1') - ..addPasscodeInput('2') - ..addPasscodeInput('3'), - expect: () => [ - const LoginTypingPasscode('1'), - const LoginTypingPasscode('12'), - const LoginTypingPasscode('123'), - ], - ); - - blocTest( - 'addPasscodeInput emits TypingPasscode, Loading when passcode length is 4, then emits LoginError when login fails', - build: () { - when(accountRepository.login(any, any)).thenAnswer( - (_) async => const Left(ServerFailure('some error')), - ); - return loginCubit - ..addPasscodeInput('1') - ..addPasscodeInput('2') - ..addPasscodeInput('3'); - }, - act: (cubit) => cubit.addPasscodeInput('4'), - expect: () => [ - const LoginTypingPasscode('1234'), - const LoginLoading(), - const LoginError('some error'), - ], - ); - - blocTest( - 'clearPasscode emits TypingPasscode with empty string', - build: () => loginCubit, - act: (cubit) => cubit - ..addPasscodeInput('1') - ..addPasscodeInput('2') - ..addPasscodeInput('3') - ..clearPasscode(), - expect: () => [ - const LoginTypingPasscode('1'), - const LoginTypingPasscode('12'), - const LoginTypingPasscode('123'), - const LoginTypingPasscode(''), - ], - ); - - tearDown(() { - loginCubit.close(); - }); - }); -} diff --git a/test/data/repositories/v2/account_repository/account_repository_test.dart b/test/data/repositories/v2/account_repository/account_repository_test.dart deleted file mode 100644 index 62aa01dc5..000000000 --- a/test/data/repositories/v2/account_repository/account_repository_test.dart +++ /dev/null @@ -1,52 +0,0 @@ -import 'package:coffeecard/core/network/network_request_executor.dart'; -import 'package:coffeecard/data/repositories/shared/account_repository.dart'; -import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart' - hide MessageResponseDto; -import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart' - show CoffeecardApiV2, MessageResponseDto; -import 'package:flutter_test/flutter_test.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'account_repository_test.mocks.dart'; - -@GenerateMocks([CoffeecardApi, CoffeecardApiV2, NetworkRequestExecutor]) -void main() { - late MockCoffeecardApi apiV1; - late MockCoffeecardApiV2 apiV2; - late MockNetworkRequestExecutor executor; - late AccountRepository repository; - - setUp(() { - apiV1 = MockCoffeecardApi(); - apiV2 = MockCoffeecardApiV2(); - - executor = MockNetworkRequestExecutor(); - repository = AccountRepository( - apiV1: apiV1, - apiV2: apiV2, - executor: executor, - ); - }); - - group('register', () { - test('should call executor', () async { - // arrange - when(executor.call(any)).thenAnswer( - (_) async => Right(MessageResponseDto()), - ); - - // act - await repository.register( - 'name', - 'email', - 'passcode', - 0, - ); - - // assert - verify(executor.call(any)); - }); - }); -} diff --git a/test/features/login/domain/usecases/login_user_test.dart b/test/features/login/domain/usecases/login_user_test.dart new file mode 100644 index 000000000..685f24334 --- /dev/null +++ b/test/features/login/domain/usecases/login_user_test.dart @@ -0,0 +1,38 @@ +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/features/login/domain/usecases/login_user.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'login_user_test.mocks.dart'; + +@GenerateMocks([AccountRemoteDataSource]) +void main() { + late MockAccountRemoteDataSource remoteDataSource; + late LoginUser usecase; + + setUp(() { + remoteDataSource = MockAccountRemoteDataSource(); + usecase = LoginUser(remoteDataSource: remoteDataSource); + }); + + const testError = 'some error'; + + test('should call data source', () async { + // arrange + when(remoteDataSource.login(any, any)).thenAnswer( + (_) async => const Left(ServerFailure(testError)), + ); + + // act + final actual = await usecase( + const Params(email: 'email', encodedPasscode: 'encodedPasscode'), + ); + + // assert + verify(remoteDataSource.login(any, any)).called(1); + expect(actual, const Left(ServerFailure(testError))); + }); +} diff --git a/test/features/login/presentation/cubit/login_cubit_test.dart b/test/features/login/presentation/cubit/login_cubit_test.dart new file mode 100644 index 000000000..3932d3ea6 --- /dev/null +++ b/test/features/login/presentation/cubit/login_cubit_test.dart @@ -0,0 +1,93 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; +import 'package:coffeecard/features/login/domain/usecases/login_user.dart'; +import 'package:coffeecard/features/login/presentation/cubit/login_cubit.dart'; +import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'login_cubit_test.mocks.dart'; + +@GenerateMocks([ + AuthenticationCubit, + LoginUser, + FirebaseAnalyticsEventLogging, +]) +void main() { + late LoginCubit cubit; + late MockAuthenticationCubit authenticationCubit; + late MockLoginUser loginUser; + late MockFirebaseAnalyticsEventLogging firebaseAnalyticsEventLogging; + + setUp(() { + authenticationCubit = MockAuthenticationCubit(); + loginUser = MockLoginUser(); + firebaseAnalyticsEventLogging = MockFirebaseAnalyticsEventLogging(); + cubit = LoginCubit( + email: '', + authenticationCubit: authenticationCubit, + loginUser: loginUser, + firebaseAnalyticsEventLogging: firebaseAnalyticsEventLogging, + ); + }); + + test('initial state is [LoginTypingPasscode]', () { + expect(cubit.state, const LoginTypingPasscode('')); + }); + + group('addPasscodeInput', () { + blocTest( + 'should only emit [TypingPasscode] when passcode length is less than 4', + build: () => cubit, + act: (_) => cubit + ..addPasscodeInput('1') + ..addPasscodeInput('2') + ..addPasscodeInput('3'), + expect: () => [ + const LoginTypingPasscode('1'), + const LoginTypingPasscode('12'), + const LoginTypingPasscode('123'), + ], + ); + + blocTest( + 'should emit [TypingPasscode, Loading, Error] when passcode length is 4 and login fails', + build: () { + when(loginUser(any)).thenAnswer( + (_) async => const Left(ServerFailure('some error')), + ); + return cubit + ..addPasscodeInput('1') + ..addPasscodeInput('2') + ..addPasscodeInput('3'); + }, + act: (_) => cubit.addPasscodeInput('4'), + expect: () => [ + const LoginTypingPasscode('1234'), + const LoginLoading(), + const LoginError('some error'), + ], + ); + }); + + group('clearPasscode', () { + blocTest( + 'should emit [TypingPasscode] with empty string', + build: () => cubit, + act: (_) => cubit + ..addPasscodeInput('1') + ..addPasscodeInput('2') + ..addPasscodeInput('3') + ..clearPasscode(), + expect: () => [ + const LoginTypingPasscode('1'), + const LoginTypingPasscode('12'), + const LoginTypingPasscode('123'), + const LoginTypingPasscode(''), + ], + ); + }); +} From 7505c32ce0dcdbee1df5387eb75214a626c6355c Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Sun, 28 May 2023 09:09:00 +0200 Subject: [PATCH 23/32] refactor and test voucher (#480) --- .../forms => core/widgets/form}/form.dart | 0 .../widgets/form}/form_text_field.dart | 0 .../widgets/forms/change_email_form.dart | 2 +- .../widgets/forms/change_name_form.dart | 2 +- .../widgets/forms/change_passcode_form.dart | 2 +- .../forms/change_passcode_repeat_form.dart | 2 +- .../presentation/widgets/shop_section.dart | 2 +- .../voucher_remote_data_source.dart} | 20 +++---- .../data/models/redeemed_voucher_model.dart | 16 +++++ .../domain/entities}/redeemed_voucher.dart | 5 -- .../domain/usecases/redeem_voucher_code.dart | 16 +++++ .../presentation/cubit}/voucher_cubit.dart | 11 ++-- .../presentation/cubit}/voucher_state.dart | 3 + .../pages/redeem_voucher_page.dart | 7 +-- .../presentation/widgets}/voucher_form.dart | 4 +- lib/service_locator.dart | 25 +++++--- .../forgot_passcode/forgot_passcode_form.dart | 2 +- .../forms/register/register_email_form.dart | 2 +- .../forms/register/register_name_form.dart | 2 +- .../register/register_passcode_form.dart | 2 +- .../register_passcode_repeat_form.dart | 2 +- .../voucher_remote_data_source_test.dart | 59 +++++++++++++++++++ .../usecases/redeem_voucher_code_test.dart | 35 +++++++++++ .../cubit/voucher_cubit_test.dart | 55 +++++++++++++++++ 24 files changed, 232 insertions(+), 44 deletions(-) rename lib/{widgets/components/forms => core/widgets/form}/form.dart (100%) rename lib/{widgets/components/forms => core/widgets/form}/form_text_field.dart (100%) rename lib/{data/repositories/v1/voucher_repository.dart => features/voucher/data/datasources/voucher_remote_data_source.dart} (59%) create mode 100644 lib/features/voucher/data/models/redeemed_voucher_model.dart rename lib/{models/voucher => features/voucher/domain/entities}/redeemed_voucher.dart (60%) create mode 100644 lib/features/voucher/domain/usecases/redeem_voucher_code.dart rename lib/{cubits/voucher => features/voucher/presentation/cubit}/voucher_cubit.dart (52%) rename lib/{cubits/voucher => features/voucher/presentation/cubit}/voucher_state.dart (99%) rename lib/features/{ticket => voucher}/presentation/pages/redeem_voucher_page.dart (89%) rename lib/{widgets/components/forms/voucher => features/voucher/presentation/widgets}/voucher_form.dart (81%) create mode 100644 test/features/voucher/data/datasources/voucher_remote_data_source_test.dart create mode 100644 test/features/voucher/domain/usecases/redeem_voucher_code_test.dart create mode 100644 test/features/voucher/presentation/cubit/voucher_cubit_test.dart diff --git a/lib/widgets/components/forms/form.dart b/lib/core/widgets/form/form.dart similarity index 100% rename from lib/widgets/components/forms/form.dart rename to lib/core/widgets/form/form.dart diff --git a/lib/widgets/components/forms/form_text_field.dart b/lib/core/widgets/form/form_text_field.dart similarity index 100% rename from lib/widgets/components/forms/form_text_field.dart rename to lib/core/widgets/form/form_text_field.dart diff --git a/lib/features/settings/presentation/widgets/forms/change_email_form.dart b/lib/features/settings/presentation/widgets/forms/change_email_form.dart index dba429421..6996016bd 100644 --- a/lib/features/settings/presentation/widgets/forms/change_email_form.dart +++ b/lib/features/settings/presentation/widgets/forms/change_email_form.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; +import 'package:coffeecard/core/widgets/form/form.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/input_validator.dart'; -import 'package:coffeecard/widgets/components/forms/form.dart'; import 'package:flutter/material.dart'; import 'package:fpdart/fpdart.dart'; diff --git a/lib/features/settings/presentation/widgets/forms/change_name_form.dart b/lib/features/settings/presentation/widgets/forms/change_name_form.dart index 39dcb4412..b5d84de9e 100644 --- a/lib/features/settings/presentation/widgets/forms/change_name_form.dart +++ b/lib/features/settings/presentation/widgets/forms/change_name_form.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/core/widgets/form/form.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/utils/input_validator.dart'; -import 'package:coffeecard/widgets/components/forms/form.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/settings/presentation/widgets/forms/change_passcode_form.dart b/lib/features/settings/presentation/widgets/forms/change_passcode_form.dart index 1dd2701e0..1d8910474 100644 --- a/lib/features/settings/presentation/widgets/forms/change_passcode_form.dart +++ b/lib/features/settings/presentation/widgets/forms/change_passcode_form.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/core/widgets/form/form.dart'; import 'package:coffeecard/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; import 'package:coffeecard/utils/input_validator.dart'; -import 'package:coffeecard/widgets/components/forms/form.dart'; import 'package:flutter/material.dart'; class ChangePasscodeForm extends StatelessWidget { diff --git a/lib/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart b/lib/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart index d5b6d3f2f..a9738a547 100644 --- a/lib/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart +++ b/lib/features/settings/presentation/widgets/forms/change_passcode_repeat_form.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/core/widgets/form/form.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; import 'package:coffeecard/utils/input_validator.dart'; -import 'package:coffeecard/widgets/components/forms/form.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/features/ticket/presentation/widgets/shop_section.dart b/lib/features/ticket/presentation/widgets/shop_section.dart index 574689038..2213d936e 100644 --- a/lib/features/ticket/presentation/widgets/shop_section.dart +++ b/lib/features/ticket/presentation/widgets/shop_section.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/features/purchase/presentation/pages/buy_single_drink_page.dart'; import 'package:coffeecard/features/purchase/presentation/pages/buy_tickets_page.dart'; -import 'package:coffeecard/features/ticket/presentation/pages/redeem_voucher_page.dart'; +import 'package:coffeecard/features/voucher/presentation/pages/redeem_voucher_page.dart'; import 'package:coffeecard/widgets/components/helpers/grid.dart'; import 'package:coffeecard/widgets/components/tickets/shop_card.dart'; import 'package:flutter/material.dart'; diff --git a/lib/data/repositories/v1/voucher_repository.dart b/lib/features/voucher/data/datasources/voucher_remote_data_source.dart similarity index 59% rename from lib/data/repositories/v1/voucher_repository.dart rename to lib/features/voucher/data/datasources/voucher_remote_data_source.dart index 69b513d95..10f36b099 100644 --- a/lib/data/repositories/v1/voucher_repository.dart +++ b/lib/features/voucher/data/datasources/voucher_remote_data_source.dart @@ -1,25 +1,25 @@ import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/either_extensions.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/voucher/data/models/redeemed_voucher_model.dart'; +import 'package:coffeecard/features/voucher/domain/entities/redeemed_voucher.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; -import 'package:coffeecard/models/voucher/redeemed_voucher.dart'; import 'package:fpdart/fpdart.dart'; -class VoucherRepository { - VoucherRepository({ +class VoucherRemoteDataSource { + final CoffeecardApi apiV1; + final NetworkRequestExecutor executor; + + VoucherRemoteDataSource({ required this.apiV1, required this.executor, }); - final CoffeecardApi apiV1; - final NetworkRequestExecutor executor; - Future> redeemVoucher( String voucher, ) async { - final result = await executor( + return executor( () => apiV1.apiV1PurchasesRedeemvoucherPost(voucherCode: voucher), - ); - - return result.map(RedeemedVoucher.fromDTO); + ).bindFuture(RedeemedVoucherModel.fromDTO); } } diff --git a/lib/features/voucher/data/models/redeemed_voucher_model.dart b/lib/features/voucher/data/models/redeemed_voucher_model.dart new file mode 100644 index 000000000..13a80d1ff --- /dev/null +++ b/lib/features/voucher/data/models/redeemed_voucher_model.dart @@ -0,0 +1,16 @@ +import 'package:coffeecard/features/voucher/domain/entities/redeemed_voucher.dart'; +import 'package:coffeecard/generated/api/coffeecard_api.models.swagger.dart'; + +class RedeemedVoucherModel extends RedeemedVoucher { + const RedeemedVoucherModel({ + required super.numberOfTickets, + required super.productName, + }); + + factory RedeemedVoucherModel.fromDTO(PurchaseDto dto) { + return RedeemedVoucherModel( + numberOfTickets: dto.numberOfTickets, + productName: dto.productName, + ); + } +} diff --git a/lib/models/voucher/redeemed_voucher.dart b/lib/features/voucher/domain/entities/redeemed_voucher.dart similarity index 60% rename from lib/models/voucher/redeemed_voucher.dart rename to lib/features/voucher/domain/entities/redeemed_voucher.dart index a92a5a533..47a2b64d5 100644 --- a/lib/models/voucher/redeemed_voucher.dart +++ b/lib/features/voucher/domain/entities/redeemed_voucher.dart @@ -1,4 +1,3 @@ -import 'package:coffeecard/generated/api/coffeecard_api.models.swagger.dart'; import 'package:equatable/equatable.dart'; class RedeemedVoucher extends Equatable { @@ -10,10 +9,6 @@ class RedeemedVoucher extends Equatable { required this.productName, }); - RedeemedVoucher.fromDTO(PurchaseDto dto) - : numberOfTickets = dto.numberOfTickets, - productName = dto.productName; - @override List get props => [numberOfTickets, productName]; } diff --git a/lib/features/voucher/domain/usecases/redeem_voucher_code.dart b/lib/features/voucher/domain/usecases/redeem_voucher_code.dart new file mode 100644 index 000000000..12aab447c --- /dev/null +++ b/lib/features/voucher/domain/usecases/redeem_voucher_code.dart @@ -0,0 +1,16 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/features/voucher/data/datasources/voucher_remote_data_source.dart'; +import 'package:coffeecard/features/voucher/domain/entities/redeemed_voucher.dart'; +import 'package:fpdart/fpdart.dart'; + +class RedeemVoucherCode implements UseCase { + final VoucherRemoteDataSource dataSource; + + RedeemVoucherCode({required this.dataSource}); + + @override + Future> call(String voucher) { + return dataSource.redeemVoucher(voucher); + } +} diff --git a/lib/cubits/voucher/voucher_cubit.dart b/lib/features/voucher/presentation/cubit/voucher_cubit.dart similarity index 52% rename from lib/cubits/voucher/voucher_cubit.dart rename to lib/features/voucher/presentation/cubit/voucher_cubit.dart index 0b1ee8a45..c43507cab 100644 --- a/lib/cubits/voucher/voucher_cubit.dart +++ b/lib/features/voucher/presentation/cubit/voucher_cubit.dart @@ -1,18 +1,19 @@ import 'package:bloc/bloc.dart'; -import 'package:coffeecard/data/repositories/v1/voucher_repository.dart'; -import 'package:coffeecard/models/voucher/redeemed_voucher.dart'; +import 'package:coffeecard/features/voucher/domain/entities/redeemed_voucher.dart'; +import 'package:coffeecard/features/voucher/domain/usecases/redeem_voucher_code.dart'; import 'package:equatable/equatable.dart'; part 'voucher_state.dart'; class VoucherCubit extends Cubit { - VoucherCubit(this._voucherRepository) : super(VoucherInitial()); + final RedeemVoucherCode redeemVoucherCode; - final VoucherRepository _voucherRepository; + VoucherCubit({required this.redeemVoucherCode}) : super(VoucherInitial()); Future redeemVoucher(String voucher) async { emit(VoucherLoading()); - final either = await _voucherRepository.redeemVoucher(voucher); + + final either = await redeemVoucherCode(voucher); either.fold( (error) => emit(VoucherError(error.reason)), diff --git a/lib/cubits/voucher/voucher_state.dart b/lib/features/voucher/presentation/cubit/voucher_state.dart similarity index 99% rename from lib/cubits/voucher/voucher_state.dart rename to lib/features/voucher/presentation/cubit/voucher_state.dart index cf2cc2487..cb4a59fdb 100644 --- a/lib/cubits/voucher/voucher_state.dart +++ b/lib/features/voucher/presentation/cubit/voucher_state.dart @@ -13,13 +13,16 @@ class VoucherLoading extends VoucherState {} class VoucherError extends VoucherState { final String error; + const VoucherError(this.error); + @override List get props => [error]; } class VoucherSuccess extends VoucherState { final RedeemedVoucher redeemedVoucher; + const VoucherSuccess(this.redeemedVoucher); @override diff --git a/lib/features/ticket/presentation/pages/redeem_voucher_page.dart b/lib/features/voucher/presentation/pages/redeem_voucher_page.dart similarity index 89% rename from lib/features/ticket/presentation/pages/redeem_voucher_page.dart rename to lib/features/voucher/presentation/pages/redeem_voucher_page.dart index 1d0d8694c..e98c35f91 100644 --- a/lib/features/ticket/presentation/pages/redeem_voucher_page.dart +++ b/lib/features/voucher/presentation/pages/redeem_voucher_page.dart @@ -1,11 +1,10 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/cubits/voucher/voucher_cubit.dart'; -import 'package:coffeecard/data/repositories/v1/voucher_repository.dart'; import 'package:coffeecard/features/ticket/presentation/cubit/tickets_cubit.dart'; +import 'package:coffeecard/features/voucher/presentation/cubit/voucher_cubit.dart'; +import 'package:coffeecard/features/voucher/presentation/widgets/voucher_form.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; -import 'package:coffeecard/widgets/components/forms/voucher/voucher_form.dart'; import 'package:coffeecard/widgets/components/loading_overlay.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:flutter/material.dart'; @@ -23,7 +22,7 @@ class RedeemVoucherPage extends StatelessWidget { title: Strings.redeemVoucherPageTitle, applyPadding: true, body: BlocProvider( - create: (_) => VoucherCubit(sl()), + create: (_) => sl(), child: BlocListener( listener: (context, state) { if (state is VoucherLoading) return showLoadingOverlay(context); diff --git a/lib/widgets/components/forms/voucher/voucher_form.dart b/lib/features/voucher/presentation/widgets/voucher_form.dart similarity index 81% rename from lib/widgets/components/forms/voucher/voucher_form.dart rename to lib/features/voucher/presentation/widgets/voucher_form.dart index 84bcd696c..9cd9bc2d5 100644 --- a/lib/widgets/components/forms/voucher/voucher_form.dart +++ b/lib/features/voucher/presentation/widgets/voucher_form.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/cubits/voucher/voucher_cubit.dart'; +import 'package:coffeecard/core/widgets/form/form.dart'; +import 'package:coffeecard/features/voucher/presentation/cubit/voucher_cubit.dart'; import 'package:coffeecard/utils/input_validator.dart'; -import 'package:coffeecard/widgets/components/forms/form.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/service_locator.dart b/lib/service_locator.dart index e9af7e39a..86c9d93f6 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -5,7 +5,6 @@ import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/data/api/interceptors/authentication_interceptor.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; -import 'package:coffeecard/data/repositories/v1/voucher_repository.dart'; import 'package:coffeecard/data/storage/secure_storage.dart'; import 'package:coffeecard/env/env.dart'; import 'package:coffeecard/features/contributor/data/datasources/contributor_local_data_source.dart'; @@ -37,6 +36,9 @@ import 'package:coffeecard/features/user/domain/usecases/get_user.dart'; import 'package:coffeecard/features/user/domain/usecases/request_account_deletion.dart'; import 'package:coffeecard/features/user/domain/usecases/update_user_details.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; +import 'package:coffeecard/features/voucher/data/datasources/voucher_remote_data_source.dart'; +import 'package:coffeecard/features/voucher/domain/usecases/redeem_voucher_code.dart'; +import 'package:coffeecard/features/voucher/presentation/cubit/voucher_cubit.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart' hide $JsonSerializableConverter; @@ -127,13 +129,6 @@ void configureServices() { ), ); - sl.registerFactory( - () => VoucherRepository( - apiV1: sl(), - executor: sl(), - ), - ); - // v1 and v2 sl.registerFactory( () => AccountRemoteDataSource( @@ -163,6 +158,7 @@ void initFeatures() { initPayment(); initLeaderboard(); initEnvironment(); + initVoucher(); initLogin(); } @@ -321,6 +317,19 @@ void initEnvironment() { ); } +void initVoucher() { + // bloc + sl.registerFactory(() => VoucherCubit(redeemVoucherCode: sl())); + + // use case + sl.registerFactory(() => RedeemVoucherCode(dataSource: sl())); + + // data source + sl.registerLazySingleton( + () => VoucherRemoteDataSource(apiV1: sl(), executor: sl()), + ); +} + void initLogin() { // bloc diff --git a/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart b/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart index 3efa51b16..c8fe4bfd9 100644 --- a/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart +++ b/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart @@ -1,9 +1,9 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; +import 'package:coffeecard/core/widgets/form/form.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/input_validator.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; -import 'package:coffeecard/widgets/components/forms/form.dart'; import 'package:coffeecard/widgets/components/loading_overlay.dart'; import 'package:flutter/material.dart'; import 'package:fpdart/fpdart.dart'; diff --git a/lib/widgets/components/forms/register/register_email_form.dart b/lib/widgets/components/forms/register/register_email_form.dart index dea7af3e5..ce9803d66 100644 --- a/lib/widgets/components/forms/register/register_email_form.dart +++ b/lib/widgets/components/forms/register/register_email_form.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; +import 'package:coffeecard/core/widgets/form/form.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/input_validator.dart'; -import 'package:coffeecard/widgets/components/forms/form.dart'; import 'package:flutter/material.dart'; import 'package:fpdart/fpdart.dart'; diff --git a/lib/widgets/components/forms/register/register_name_form.dart b/lib/widgets/components/forms/register/register_name_form.dart index 08e4abcab..e6f02be1c 100644 --- a/lib/widgets/components/forms/register/register_name_form.dart +++ b/lib/widgets/components/forms/register/register_name_form.dart @@ -1,8 +1,8 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/core/widgets/form/form.dart'; import 'package:coffeecard/cubits/register/register_cubit.dart'; import 'package:coffeecard/utils/input_validator.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; -import 'package:coffeecard/widgets/components/forms/form.dart'; import 'package:coffeecard/widgets/components/helpers/unordered_list_builder.dart'; import 'package:coffeecard/widgets/components/loading_overlay.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/components/forms/register/register_passcode_form.dart b/lib/widgets/components/forms/register/register_passcode_form.dart index cbb74ee89..fca9958b9 100644 --- a/lib/widgets/components/forms/register/register_passcode_form.dart +++ b/lib/widgets/components/forms/register/register_passcode_form.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/core/widgets/form/form.dart'; import 'package:coffeecard/utils/input_validator.dart'; -import 'package:coffeecard/widgets/components/forms/form.dart'; import 'package:flutter/material.dart'; class RegisterPasscodeForm extends StatelessWidget { diff --git a/lib/widgets/components/forms/register/register_passcode_repeat_form.dart b/lib/widgets/components/forms/register/register_passcode_repeat_form.dart index 7d9ec01e3..63711968a 100644 --- a/lib/widgets/components/forms/register/register_passcode_repeat_form.dart +++ b/lib/widgets/components/forms/register/register_passcode_repeat_form.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/core/widgets/form/form.dart'; import 'package:coffeecard/utils/input_validator.dart'; -import 'package:coffeecard/widgets/components/forms/form.dart'; import 'package:flutter/material.dart'; class RegisterPasscodeRepeatForm extends StatelessWidget { diff --git a/test/features/voucher/data/datasources/voucher_remote_data_source_test.dart b/test/features/voucher/data/datasources/voucher_remote_data_source_test.dart new file mode 100644 index 000000000..42eacb6c4 --- /dev/null +++ b/test/features/voucher/data/datasources/voucher_remote_data_source_test.dart @@ -0,0 +1,59 @@ +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/voucher/data/datasources/voucher_remote_data_source.dart'; +import 'package:coffeecard/features/voucher/data/models/redeemed_voucher_model.dart'; +import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'voucher_remote_data_source_test.mocks.dart'; + +@GenerateMocks([CoffeecardApi, NetworkRequestExecutor]) +void main() { + late VoucherRemoteDataSource remoteDataSource; + late MockCoffeecardApi apiV1; + late MockNetworkRequestExecutor executor; + + setUp(() { + executor = MockNetworkRequestExecutor(); + apiV1 = MockCoffeecardApi(); + remoteDataSource = VoucherRemoteDataSource( + apiV1: apiV1, + executor: executor, + ); + }); + + group('redeemVoucher', () { + test('should call executor and map data', () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => Right( + PurchaseDto( + id: 0, + productName: 'productName', + productId: 0, + price: 0, + numberOfTickets: 0, + dateCreated: DateTime.parse('2023-27-05'), + completed: true, + orderId: 'orderId', + transactionId: 'transactionId', + ), + ), + ); + + // act + final actual = await remoteDataSource.redeemVoucher('voucher'); + + // assert + verify(executor.call(any)); + expect( + actual, + const Right( + RedeemedVoucherModel(numberOfTickets: 0, productName: 'productName'), + ), + ); + }); + }); +} diff --git a/test/features/voucher/domain/usecases/redeem_voucher_code_test.dart b/test/features/voucher/domain/usecases/redeem_voucher_code_test.dart new file mode 100644 index 000000000..c294c4e0c --- /dev/null +++ b/test/features/voucher/domain/usecases/redeem_voucher_code_test.dart @@ -0,0 +1,35 @@ +import 'package:coffeecard/features/voucher/data/datasources/voucher_remote_data_source.dart'; +import 'package:coffeecard/features/voucher/domain/entities/redeemed_voucher.dart'; +import 'package:coffeecard/features/voucher/domain/usecases/redeem_voucher_code.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'redeem_voucher_code_test.mocks.dart'; + +@GenerateMocks([VoucherRemoteDataSource]) +void main() { + late MockVoucherRemoteDataSource dataSource; + late RedeemVoucherCode usecase; + + setUp(() { + dataSource = MockVoucherRemoteDataSource(); + usecase = RedeemVoucherCode(dataSource: dataSource); + }); + + test('should call repository', () async { + // arrange + when(dataSource.redeemVoucher(any)).thenAnswer( + (_) async => const Right( + RedeemedVoucher(numberOfTickets: 0, productName: 'productName'), + ), + ); + + // act + await usecase('voucher'); + + // assert + verify(dataSource.redeemVoucher(any)); + }); +} diff --git a/test/features/voucher/presentation/cubit/voucher_cubit_test.dart b/test/features/voucher/presentation/cubit/voucher_cubit_test.dart new file mode 100644 index 000000000..d08bc1645 --- /dev/null +++ b/test/features/voucher/presentation/cubit/voucher_cubit_test.dart @@ -0,0 +1,55 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/features/voucher/domain/entities/redeemed_voucher.dart'; +import 'package:coffeecard/features/voucher/domain/usecases/redeem_voucher_code.dart'; +import 'package:coffeecard/features/voucher/presentation/cubit/voucher_cubit.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'voucher_cubit_test.mocks.dart'; + +@GenerateMocks([RedeemVoucherCode]) +void main() { + late MockRedeemVoucherCode redeemVoucherCode; + late VoucherCubit cubit; + + setUp(() { + redeemVoucherCode = MockRedeemVoucherCode(); + cubit = VoucherCubit(redeemVoucherCode: redeemVoucherCode); + }); + + const testErrorMessage = 'some error'; + + const testRedeemedVoucher = + RedeemedVoucher(numberOfTickets: 0, productName: 'productName'); + + group('redeemVoucher', () { + blocTest( + 'should emit [Loading, Error] when use case fails', + build: () => cubit, + setUp: () => when(redeemVoucherCode(any)).thenAnswer( + (_) async => const Left(ServerFailure(testErrorMessage)), + ), + act: (_) => cubit.redeemVoucher('voucher'), + expect: () => [ + VoucherLoading(), + const VoucherError(testErrorMessage), + ], + ); + + blocTest( + 'should emit [Loading, Success] when use case succeeds', + build: () => cubit, + setUp: () => when(redeemVoucherCode(any)).thenAnswer( + (_) async => const Right(testRedeemedVoucher), + ), + act: (_) => cubit.redeemVoucher('voucher'), + expect: () => [ + VoucherLoading(), + const VoucherSuccess(testRedeemedVoucher), + ], + ); + }); +} From b79b60e6cc18af62695eb55044d8373cb207c9b9 Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Mon, 29 May 2023 08:56:30 +0200 Subject: [PATCH 24/32] Update README.md to show coverage on develop branch (#484) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 9a62e4972..1929d2dfa 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Coffee card App [in development] -![Flutter build and test](https://github.com/AnalogIO/coffeecard_app/workflows/Flutter%20build%20and%20test/badge.svg) [![codecov](https://codecov.io/gh/AnalogIO/coffeecard_app/branch/master/graph/badge.svg)](https://codecov.io/gh/AnalogIO/coffeecard_app) +![Flutter build and test](https://github.com/AnalogIO/coffeecard_app/workflows/Flutter%20build%20and%20test/badge.svg) [![codecov](https://codecov.io/gh/AnalogIO/coffeecard_app/branch/develop/graph/badge.svg)](https://codecov.io/gh/AnalogIO/coffeecard_app) **Contact** AnalogIO at *support [at] analogio.dk* From 9eb0f34cdea4db56951581e7f1febd5a8c91b078 Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Tue, 30 May 2023 17:33:58 +0200 Subject: [PATCH 25/32] fix(purchase): Set type of `paymentDetails` to generic JSON (#489) --- .../purchase/data/models/initiate_purchase_model.dart | 4 +--- .../purchase/data/repositories/mobilepay_service.dart | 3 ++- lib/features/purchase/domain/entities/initiate_purchase.dart | 3 +-- .../data/datasources/purchase_remote_data_source_test.dart | 2 +- .../purchase/data/repositories/free_product_service_test.dart | 2 +- .../purchase/data/repositories/mobilepay_service_test.dart | 2 +- 6 files changed, 7 insertions(+), 9 deletions(-) diff --git a/lib/features/purchase/data/models/initiate_purchase_model.dart b/lib/features/purchase/data/models/initiate_purchase_model.dart index 856b75e1d..643922700 100644 --- a/lib/features/purchase/data/models/initiate_purchase_model.dart +++ b/lib/features/purchase/data/models/initiate_purchase_model.dart @@ -16,9 +16,7 @@ class InitiatePurchaseModel extends InitiatePurchase { return InitiatePurchaseModel( id: dto.id, totalAmount: dto.totalAmount, - paymentDetails: MobilePayPaymentDetails.fromJsonFactory( - dto.paymentDetails as Map, - ), + paymentDetails: dto.paymentDetails as Map, productId: dto.productId, productName: dto.productName, purchaseStatus: dto.purchaseStatus as String, diff --git a/lib/features/purchase/data/repositories/mobilepay_service.dart b/lib/features/purchase/data/repositories/mobilepay_service.dart index 01eeb64db..58354a145 100644 --- a/lib/features/purchase/data/repositories/mobilepay_service.dart +++ b/lib/features/purchase/data/repositories/mobilepay_service.dart @@ -26,7 +26,8 @@ class MobilePayService extends PaymentHandler { (purchase) => Payment( id: purchase.id, status: PaymentStatus.awaitingPayment, - deeplink: purchase.paymentDetails.mobilePayAppRedirectUri, + deeplink: MobilePayPaymentDetails.fromJson(purchase.paymentDetails) + .mobilePayAppRedirectUri, purchaseTime: purchase.dateCreated, price: purchase.totalAmount, productId: purchase.productId, diff --git a/lib/features/purchase/domain/entities/initiate_purchase.dart b/lib/features/purchase/domain/entities/initiate_purchase.dart index 5cf434632..32cec160c 100644 --- a/lib/features/purchase/domain/entities/initiate_purchase.dart +++ b/lib/features/purchase/domain/entities/initiate_purchase.dart @@ -1,10 +1,9 @@ -import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:equatable/equatable.dart'; class InitiatePurchase extends Equatable { final int id; final int totalAmount; - final MobilePayPaymentDetails paymentDetails; + final Map paymentDetails; final int productId; final String productName; final String purchaseStatus; diff --git a/test/features/purchase/data/datasources/purchase_remote_data_source_test.dart b/test/features/purchase/data/datasources/purchase_remote_data_source_test.dart index e5a5d8371..363170e0a 100644 --- a/test/features/purchase/data/datasources/purchase_remote_data_source_test.dart +++ b/test/features/purchase/data/datasources/purchase_remote_data_source_test.dart @@ -67,7 +67,7 @@ void main() { discriminator: 'discriminator', paymentType: 'paymentType', orderId: 'orderId', - ), + ).toJson(), productId: 0, productName: 'productName', purchaseStatus: 'purchaseStatus', diff --git a/test/features/purchase/data/repositories/free_product_service_test.dart b/test/features/purchase/data/repositories/free_product_service_test.dart index 83e48d3e0..4ad861ded 100644 --- a/test/features/purchase/data/repositories/free_product_service_test.dart +++ b/test/features/purchase/data/repositories/free_product_service_test.dart @@ -42,7 +42,7 @@ void main() { discriminator: 'discriminator', paymentType: 'paymentType', orderId: 'orderId', - ), + ).toJson(), productId: 0, productName: 'productName', purchaseStatus: 'purchaseStatus', diff --git a/test/features/purchase/data/repositories/mobilepay_service_test.dart b/test/features/purchase/data/repositories/mobilepay_service_test.dart index 13b6c8b7b..8ce5bfcfe 100644 --- a/test/features/purchase/data/repositories/mobilepay_service_test.dart +++ b/test/features/purchase/data/repositories/mobilepay_service_test.dart @@ -73,7 +73,7 @@ void main() { discriminator: 'discriminator', paymentType: 'paymentType', orderId: 'orderId', - ), + ).toJson(), productId: 0, productName: 'productName', purchaseStatus: 'purchaseStatus', From f7b48f7e254d99389ca48a991abc3ccb2e31ddcf Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Tue, 30 May 2023 17:53:43 +0200 Subject: [PATCH 26/32] Refactor and test register (#486) --- .../account_remote_data_source.dart | 18 ------- lib/cubits/register/register_cubit.dart | 35 ------------- lib/cubits/register/register_state.dart | 14 ----- .../pages/forgot_passcode_page.dart | 2 +- .../presentation/pages/login_page_email.dart | 2 +- .../widgets}/forgot_passcode_form.dart | 0 .../register_remote_data_source.dart | 33 ++++++++++++ .../domain/usecases/register_user.dart | 38 ++++++++++++++ .../presentation/cubit/register_cubit.dart | 41 +++++++++++++++ .../presentation/cubit/register_state.dart | 22 ++++++++ .../presentation/pages}/register_flow.dart | 8 ++- .../pages}/register_page_email.dart | 4 +- .../pages}/register_page_name.dart | 9 ++-- .../pages}/register_page_occupation.dart | 2 +- .../pages}/register_page_passcode.dart | 4 +- .../pages}/register_page_passcode_repeat.dart | 4 +- .../widgets/forms}/register_email_form.dart | 0 .../widgets/forms}/register_name_form.dart | 2 +- .../forms}/register_passcode_form.dart | 0 .../forms}/register_passcode_repeat_form.dart | 0 lib/service_locator.dart | 22 ++++++++ .../account_remote_data_source_test.dart | 20 -------- .../register_remote_data_source_test.dart | 45 ++++++++++++++++ .../domain/usecases/register_user_test.dart | 38 ++++++++++++++ .../cubit/register_cubit_test.dart | 51 +++++++++++++++++++ 25 files changed, 307 insertions(+), 107 deletions(-) delete mode 100644 lib/cubits/register/register_cubit.dart delete mode 100644 lib/cubits/register/register_state.dart rename lib/{widgets/components/forms/forgot_passcode => features/login/presentation/widgets}/forgot_passcode_form.dart (100%) create mode 100644 lib/features/register/data/datasources/register_remote_data_source.dart create mode 100644 lib/features/register/domain/usecases/register_user.dart create mode 100644 lib/features/register/presentation/cubit/register_cubit.dart create mode 100644 lib/features/register/presentation/cubit/register_state.dart rename lib/{widgets/pages/register => features/register/presentation/pages}/register_flow.dart (75%) rename lib/{widgets/pages/register => features/register/presentation/pages}/register_page_email.dart (75%) rename lib/{widgets/pages/register => features/register/presentation/pages}/register_page_name.dart (89%) rename lib/{widgets/pages/register => features/register/presentation/pages}/register_page_occupation.dart (96%) rename lib/{widgets/pages/register => features/register/presentation/pages}/register_page_passcode.dart (79%) rename lib/{widgets/pages/register => features/register/presentation/pages}/register_page_passcode_repeat.dart (81%) rename lib/{widgets/components/forms/register => features/register/presentation/widgets/forms}/register_email_form.dart (100%) rename lib/{widgets/components/forms/register => features/register/presentation/widgets/forms}/register_name_form.dart (96%) rename lib/{widgets/components/forms/register => features/register/presentation/widgets/forms}/register_passcode_form.dart (100%) rename lib/{widgets/components/forms/register => features/register/presentation/widgets/forms}/register_passcode_repeat_form.dart (100%) create mode 100644 test/features/register/data/datasources/register_remote_data_source_test.dart create mode 100644 test/features/register/domain/usecases/register_user_test.dart create mode 100644 test/features/register/presentation/cubit/register_cubit_test.dart diff --git a/lib/core/data/datasources/account_remote_data_source.dart b/lib/core/data/datasources/account_remote_data_source.dart index 9eacd6b1a..e91d8fbcd 100644 --- a/lib/core/data/datasources/account_remote_data_source.dart +++ b/lib/core/data/datasources/account_remote_data_source.dart @@ -21,24 +21,6 @@ class AccountRemoteDataSource { final CoffeecardApiV2 apiV2; final NetworkRequestExecutor executor; - Future> register( - String name, - String email, - String encodedPasscode, - int occupationId, - ) async { - return executor( - () => apiV2.apiV2AccountPost( - body: RegisterAccountRequest( - name: name, - email: email, - password: encodedPasscode, - programmeId: occupationId, - ), - ), - ).bindFuture((_) => const Right(null)); - } - Future> login( String email, String encodedPasscode, diff --git a/lib/cubits/register/register_cubit.dart b/lib/cubits/register/register_cubit.dart deleted file mode 100644 index b53083560..000000000 --- a/lib/cubits/register/register_cubit.dart +++ /dev/null @@ -1,35 +0,0 @@ -import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; -import 'package:coffeecard/service_locator.dart'; -import 'package:coffeecard/utils/encode_passcode.dart'; -import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -part 'register_state.dart'; - -class RegisterCubit extends Cubit { - AccountRemoteDataSource repository; - - RegisterCubit({required this.repository}) : super(RegisterInitial()); - - Future register( - String name, - String email, - String passcode, - int occupationId, - ) async { - final either = await repository.register( - name, - email, - encodePasscode(passcode), - occupationId, - ); - - either.fold( - (error) => emit(RegisterError(error.reason)), - (_) { - emit(RegisterSuccess()); - sl().signUpEvent(); - }, - ); - } -} diff --git a/lib/cubits/register/register_state.dart b/lib/cubits/register/register_state.dart deleted file mode 100644 index 924c419ee..000000000 --- a/lib/cubits/register/register_state.dart +++ /dev/null @@ -1,14 +0,0 @@ -part of 'register_cubit.dart'; - -sealed class RegisterState {} - -class RegisterInitial extends RegisterState {} - -/// The user has created their account. -class RegisterSuccess extends RegisterState {} - -/// An error occurred trying to create an account. -class RegisterError extends RegisterState { - RegisterError(this.errorMessage); - final String errorMessage; -} diff --git a/lib/features/login/presentation/pages/forgot_passcode_page.dart b/lib/features/login/presentation/pages/forgot_passcode_page.dart index b1c097d81..a2e4c1a51 100644 --- a/lib/features/login/presentation/pages/forgot_passcode_page.dart +++ b/lib/features/login/presentation/pages/forgot_passcode_page.dart @@ -1,5 +1,5 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart'; +import 'package:coffeecard/features/login/presentation/widgets/forgot_passcode_form.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:flutter/material.dart'; diff --git a/lib/features/login/presentation/pages/login_page_email.dart b/lib/features/login/presentation/pages/login_page_email.dart index 30717295f..74ae4852c 100644 --- a/lib/features/login/presentation/pages/login_page_email.dart +++ b/lib/features/login/presentation/pages/login_page_email.dart @@ -3,9 +3,9 @@ import 'package:coffeecard/features/login/presentation/pages/login_page_base.dar import 'package:coffeecard/features/login/presentation/pages/login_page_passcode.dart'; import 'package:coffeecard/features/login/presentation/widgets/login_cta.dart'; import 'package:coffeecard/features/login/presentation/widgets/login_email_text_field.dart'; +import 'package:coffeecard/features/register/presentation/pages/register_flow.dart'; import 'package:coffeecard/utils/email_is_valid.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; -import 'package:coffeecard/widgets/pages/register/register_flow.dart'; import 'package:flutter/material.dart'; class LoginPageEmail extends StatefulWidget { diff --git a/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart b/lib/features/login/presentation/widgets/forgot_passcode_form.dart similarity index 100% rename from lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart rename to lib/features/login/presentation/widgets/forgot_passcode_form.dart diff --git a/lib/features/register/data/datasources/register_remote_data_source.dart b/lib/features/register/data/datasources/register_remote_data_source.dart new file mode 100644 index 000000000..0c67264e9 --- /dev/null +++ b/lib/features/register/data/datasources/register_remote_data_source.dart @@ -0,0 +1,33 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/either_extensions.dart'; +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:fpdart/fpdart.dart'; + +class RegisterRemoteDataSource { + RegisterRemoteDataSource({ + required this.apiV2, + required this.executor, + }); + + final CoffeecardApiV2 apiV2; + final NetworkRequestExecutor executor; + + Future> register( + String name, + String email, + String encodedPasscode, + int occupationId, + ) async { + return executor( + () => apiV2.apiV2AccountPost( + body: RegisterAccountRequest( + name: name, + email: email, + password: encodedPasscode, + programmeId: occupationId, + ), + ), + ).bindFuture((_) => const Right(null)); + } +} diff --git a/lib/features/register/domain/usecases/register_user.dart b/lib/features/register/domain/usecases/register_user.dart new file mode 100644 index 000000000..49d00e425 --- /dev/null +++ b/lib/features/register/domain/usecases/register_user.dart @@ -0,0 +1,38 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/features/register/data/datasources/register_remote_data_source.dart'; +import 'package:equatable/equatable.dart'; +import 'package:fpdart/fpdart.dart'; + +class RegisterUser implements UseCase { + final RegisterRemoteDataSource remoteDataSource; + + RegisterUser({required this.remoteDataSource}); + + @override + Future> call(Params params) { + return remoteDataSource.register( + params.name, + params.email, + params.encodedPasscode, + params.occupationId, + ); + } +} + +class Params extends Equatable { + final String name; + final String email; + final String encodedPasscode; + final int occupationId; + + const Params({ + required this.name, + required this.email, + required this.encodedPasscode, + required this.occupationId, + }); + + @override + List get props => [name, email, encodedPasscode, occupationId]; +} diff --git a/lib/features/register/presentation/cubit/register_cubit.dart b/lib/features/register/presentation/cubit/register_cubit.dart new file mode 100644 index 000000000..6017ea0c2 --- /dev/null +++ b/lib/features/register/presentation/cubit/register_cubit.dart @@ -0,0 +1,41 @@ +import 'package:coffeecard/features/register/domain/usecases/register_user.dart'; +import 'package:coffeecard/utils/encode_passcode.dart'; +import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'register_state.dart'; + +class RegisterCubit extends Cubit { + final RegisterUser registerUser; + final FirebaseAnalyticsEventLogging firebaseAnalyticsEventLogging; + + RegisterCubit({ + required this.registerUser, + required this.firebaseAnalyticsEventLogging, + }) : super(RegisterInitial()); + + Future register( + String name, + String email, + String passcode, + int occupationId, + ) async { + final either = await registerUser( + Params( + name: name, + email: email, + encodedPasscode: encodePasscode(passcode), + occupationId: occupationId, + ), + ); + + either.fold( + (error) => emit(RegisterError(error.reason)), + (_) { + emit(RegisterSuccess()); + firebaseAnalyticsEventLogging.signUpEvent(); + }, + ); + } +} diff --git a/lib/features/register/presentation/cubit/register_state.dart b/lib/features/register/presentation/cubit/register_state.dart new file mode 100644 index 000000000..5bcdef7e3 --- /dev/null +++ b/lib/features/register/presentation/cubit/register_state.dart @@ -0,0 +1,22 @@ +part of 'register_cubit.dart'; + +sealed class RegisterState extends Equatable {} + +class RegisterInitial extends RegisterState { + @override + List get props => []; +} + +class RegisterSuccess extends RegisterState { + @override + List get props => []; +} + +class RegisterError extends RegisterState { + final String message; + + RegisterError(this.message); + + @override + List get props => [message]; +} diff --git a/lib/widgets/pages/register/register_flow.dart b/lib/features/register/presentation/pages/register_flow.dart similarity index 75% rename from lib/widgets/pages/register/register_flow.dart rename to lib/features/register/presentation/pages/register_flow.dart index 07b4f89ef..8fc330737 100644 --- a/lib/widgets/pages/register/register_flow.dart +++ b/lib/features/register/presentation/pages/register_flow.dart @@ -1,10 +1,9 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; -import 'package:coffeecard/cubits/register/register_cubit.dart'; import 'package:coffeecard/features/occupation/presentation/cubit/occupation_cubit.dart'; +import 'package:coffeecard/features/register/presentation/cubit/register_cubit.dart'; +import 'package:coffeecard/features/register/presentation/pages/register_page_email.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; -import 'package:coffeecard/widgets/pages/register/register_page_email.dart'; import 'package:coffeecard/widgets/routers/app_flow.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -20,8 +19,7 @@ class RegisterFlow extends StatelessWidget { return MultiBlocProvider( providers: [ BlocProvider( - create: (_) => - RegisterCubit(repository: sl()), + create: (_) => sl(), ), BlocProvider( lazy: false, diff --git a/lib/widgets/pages/register/register_page_email.dart b/lib/features/register/presentation/pages/register_page_email.dart similarity index 75% rename from lib/widgets/pages/register/register_page_email.dart rename to lib/features/register/presentation/pages/register_page_email.dart index 73c2e8310..ce605761e 100644 --- a/lib/widgets/pages/register/register_page_email.dart +++ b/lib/features/register/presentation/pages/register_page_email.dart @@ -1,6 +1,6 @@ +import 'package:coffeecard/features/register/presentation/pages/register_page_passcode.dart'; +import 'package:coffeecard/features/register/presentation/widgets/forms/register_email_form.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; -import 'package:coffeecard/widgets/components/forms/register/register_email_form.dart'; -import 'package:coffeecard/widgets/pages/register/register_page_passcode.dart'; import 'package:flutter/material.dart'; class RegisterPageEmail extends StatelessWidget { diff --git a/lib/widgets/pages/register/register_page_name.dart b/lib/features/register/presentation/pages/register_page_name.dart similarity index 89% rename from lib/widgets/pages/register/register_page_name.dart rename to lib/features/register/presentation/pages/register_page_name.dart index a75061145..b642994c6 100644 --- a/lib/widgets/pages/register/register_page_name.dart +++ b/lib/features/register/presentation/pages/register_page_name.dart @@ -1,10 +1,9 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; -import 'package:coffeecard/cubits/register/register_cubit.dart'; +import 'package:coffeecard/features/register/presentation/cubit/register_cubit.dart'; +import 'package:coffeecard/features/register/presentation/widgets/forms/register_name_form.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; -import 'package:coffeecard/widgets/components/forms/register/register_name_form.dart'; import 'package:coffeecard/widgets/components/loading_overlay.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -40,7 +39,7 @@ class RegisterPageName extends StatelessWidget { return Padding( padding: const EdgeInsets.all(16), child: BlocProvider( - create: (_) => RegisterCubit(repository: sl()), + create: (_) => sl(), child: BlocListener( listener: (context, state) { if (state is RegisterSuccess) return _showSuccessDialog(context); @@ -99,7 +98,7 @@ class RegisterPageName extends StatelessWidget { children: [ const Text(Strings.registerFailureBody), const Gap(12), - Text(state.errorMessage), + Text(state.message), ], actions: [ TextButton( diff --git a/lib/widgets/pages/register/register_page_occupation.dart b/lib/features/register/presentation/pages/register_page_occupation.dart similarity index 96% rename from lib/widgets/pages/register/register_page_occupation.dart rename to lib/features/register/presentation/pages/register_page_occupation.dart index 5a9331995..ab40726bf 100644 --- a/lib/widgets/pages/register/register_page_occupation.dart +++ b/lib/features/register/presentation/pages/register_page_occupation.dart @@ -2,9 +2,9 @@ import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/features/occupation/domain/entities/occupation.dart'; import 'package:coffeecard/features/occupation/presentation/cubit/occupation_cubit.dart'; import 'package:coffeecard/features/occupation/presentation/widgets/occupation_form.dart'; +import 'package:coffeecard/features/register/presentation/pages/register_page_name.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; import 'package:coffeecard/widgets/components/error_section.dart'; -import 'package:coffeecard/widgets/pages/register/register_page_name.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/widgets/pages/register/register_page_passcode.dart b/lib/features/register/presentation/pages/register_page_passcode.dart similarity index 79% rename from lib/widgets/pages/register/register_page_passcode.dart rename to lib/features/register/presentation/pages/register_page_passcode.dart index ddaa447fd..ba2975729 100644 --- a/lib/widgets/pages/register/register_page_passcode.dart +++ b/lib/features/register/presentation/pages/register_page_passcode.dart @@ -1,6 +1,6 @@ +import 'package:coffeecard/features/register/presentation/pages/register_page_passcode_repeat.dart'; +import 'package:coffeecard/features/register/presentation/widgets/forms/register_passcode_form.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; -import 'package:coffeecard/widgets/components/forms/register/register_passcode_form.dart'; -import 'package:coffeecard/widgets/pages/register/register_page_passcode_repeat.dart'; import 'package:flutter/material.dart'; class RegisterPagePasscode extends StatelessWidget { diff --git a/lib/widgets/pages/register/register_page_passcode_repeat.dart b/lib/features/register/presentation/pages/register_page_passcode_repeat.dart similarity index 81% rename from lib/widgets/pages/register/register_page_passcode_repeat.dart rename to lib/features/register/presentation/pages/register_page_passcode_repeat.dart index f2be30ee0..7631c84d0 100644 --- a/lib/widgets/pages/register/register_page_passcode_repeat.dart +++ b/lib/features/register/presentation/pages/register_page_passcode_repeat.dart @@ -1,6 +1,6 @@ +import 'package:coffeecard/features/register/presentation/pages/register_page_occupation.dart'; +import 'package:coffeecard/features/register/presentation/widgets/forms/register_passcode_repeat_form.dart'; import 'package:coffeecard/utils/fast_slide_transition.dart'; -import 'package:coffeecard/widgets/components/forms/register/register_passcode_repeat_form.dart'; -import 'package:coffeecard/widgets/pages/register/register_page_occupation.dart'; import 'package:flutter/material.dart'; class RegisterPagePasscodeRepeat extends StatelessWidget { diff --git a/lib/widgets/components/forms/register/register_email_form.dart b/lib/features/register/presentation/widgets/forms/register_email_form.dart similarity index 100% rename from lib/widgets/components/forms/register/register_email_form.dart rename to lib/features/register/presentation/widgets/forms/register_email_form.dart diff --git a/lib/widgets/components/forms/register/register_name_form.dart b/lib/features/register/presentation/widgets/forms/register_name_form.dart similarity index 96% rename from lib/widgets/components/forms/register/register_name_form.dart rename to lib/features/register/presentation/widgets/forms/register_name_form.dart index e6f02be1c..e4e32729f 100644 --- a/lib/widgets/components/forms/register/register_name_form.dart +++ b/lib/features/register/presentation/widgets/forms/register_name_form.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/core/widgets/form/form.dart'; -import 'package:coffeecard/cubits/register/register_cubit.dart'; +import 'package:coffeecard/features/register/presentation/cubit/register_cubit.dart'; import 'package:coffeecard/utils/input_validator.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; import 'package:coffeecard/widgets/components/helpers/unordered_list_builder.dart'; diff --git a/lib/widgets/components/forms/register/register_passcode_form.dart b/lib/features/register/presentation/widgets/forms/register_passcode_form.dart similarity index 100% rename from lib/widgets/components/forms/register/register_passcode_form.dart rename to lib/features/register/presentation/widgets/forms/register_passcode_form.dart diff --git a/lib/widgets/components/forms/register/register_passcode_repeat_form.dart b/lib/features/register/presentation/widgets/forms/register_passcode_repeat_form.dart similarity index 100% rename from lib/widgets/components/forms/register/register_passcode_repeat_form.dart rename to lib/features/register/presentation/widgets/forms/register_passcode_repeat_form.dart diff --git a/lib/service_locator.dart b/lib/service_locator.dart index 86c9d93f6..e6fe7a5f4 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -27,6 +27,9 @@ import 'package:coffeecard/features/receipt/data/repositories/receipt_repository import 'package:coffeecard/features/receipt/domain/repositories/receipt_repository.dart'; import 'package:coffeecard/features/receipt/domain/usecases/get_receipts.dart'; import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart'; +import 'package:coffeecard/features/register/data/datasources/register_remote_data_source.dart'; +import 'package:coffeecard/features/register/domain/usecases/register_user.dart'; +import 'package:coffeecard/features/register/presentation/cubit/register_cubit.dart'; import 'package:coffeecard/features/ticket/data/datasources/ticket_remote_data_source.dart'; import 'package:coffeecard/features/ticket/domain/usecases/consume_ticket.dart'; import 'package:coffeecard/features/ticket/domain/usecases/load_tickets.dart'; @@ -160,6 +163,7 @@ void initFeatures() { initEnvironment(); initVoucher(); initLogin(); + initRegister(); } void initOpeningHours() { @@ -338,3 +342,21 @@ void initLogin() { // data source } + +void initRegister() { + // bloc + sl.registerFactory( + () => RegisterCubit( + registerUser: sl(), + firebaseAnalyticsEventLogging: sl(), + ), + ); + + // use case + sl.registerFactory(() => RegisterUser(remoteDataSource: sl())); + + // data source + sl.registerLazySingleton( + () => RegisterRemoteDataSource(apiV2: sl(), executor: sl()), + ); +} diff --git a/test/core/data/datasources/account_remote_data_source_test.dart b/test/core/data/datasources/account_remote_data_source_test.dart index 1b583c23d..7bb791414 100644 --- a/test/core/data/datasources/account_remote_data_source_test.dart +++ b/test/core/data/datasources/account_remote_data_source_test.dart @@ -35,26 +35,6 @@ void main() { const testError = 'some error'; - group('register', () { - test('should call executor', () async { - // arrange - when(executor.call(any)).thenAnswer( - (_) async => Right(v2.MessageResponseDto()), - ); - - // act - await dataSource.register( - 'name', - 'email', - 'passcode', - 0, - ); - - // assert - verify(executor.call(any)); - }); - }); - group('login', () { test( 'should return [Right] when executor returns token', diff --git a/test/features/register/data/datasources/register_remote_data_source_test.dart b/test/features/register/data/datasources/register_remote_data_source_test.dart new file mode 100644 index 000000000..5e1b290f3 --- /dev/null +++ b/test/features/register/data/datasources/register_remote_data_source_test.dart @@ -0,0 +1,45 @@ +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/register/data/datasources/register_remote_data_source.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'register_remote_data_source_test.mocks.dart'; + +@GenerateMocks([CoffeecardApiV2, NetworkRequestExecutor]) +void main() { + late MockCoffeecardApiV2 apiV2; + late MockNetworkRequestExecutor executor; + late RegisterRemoteDataSource dataSource; + + setUp(() { + apiV2 = MockCoffeecardApiV2(); + executor = MockNetworkRequestExecutor(); + dataSource = RegisterRemoteDataSource( + apiV2: apiV2, + executor: executor, + ); + }); + + group('register', () { + test('should call executor', () async { + // arrange + when(executor.call(any)).thenAnswer( + (_) async => Right(MessageResponseDto()), + ); + + // act + await dataSource.register( + 'name', + 'email', + 'passcode', + 0, + ); + + // assert + verify(executor.call(any)); + }); + }); +} diff --git a/test/features/register/domain/usecases/register_user_test.dart b/test/features/register/domain/usecases/register_user_test.dart new file mode 100644 index 000000000..c3cbfba89 --- /dev/null +++ b/test/features/register/domain/usecases/register_user_test.dart @@ -0,0 +1,38 @@ +import 'package:coffeecard/features/register/data/datasources/register_remote_data_source.dart'; +import 'package:coffeecard/features/register/domain/usecases/register_user.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'register_user_test.mocks.dart'; + +@GenerateMocks([RegisterRemoteDataSource]) +void main() { + late MockRegisterRemoteDataSource remoteDataSource; + late RegisterUser usecase; + + setUp(() { + remoteDataSource = MockRegisterRemoteDataSource(); + usecase = RegisterUser(remoteDataSource: remoteDataSource); + }); + + test('should call data source', () async { + // arrange + when(remoteDataSource.register(any, any, any, any)) + .thenAnswer((_) async => const Right(null)); + + // act + await usecase( + const Params( + name: 'name', + email: 'email', + encodedPasscode: 'encodedPasscode', + occupationId: 0, + ), + ); + + // assert + verify(remoteDataSource.register(any, any, any, any)); + }); +} diff --git a/test/features/register/presentation/cubit/register_cubit_test.dart b/test/features/register/presentation/cubit/register_cubit_test.dart new file mode 100644 index 000000000..2c0e7a7f5 --- /dev/null +++ b/test/features/register/presentation/cubit/register_cubit_test.dart @@ -0,0 +1,51 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/features/register/domain/usecases/register_user.dart'; +import 'package:coffeecard/features/register/presentation/cubit/register_cubit.dart'; +import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'register_cubit_test.mocks.dart'; + +@GenerateMocks([RegisterUser, FirebaseAnalyticsEventLogging]) +void main() { + late MockRegisterUser registerUser; + late MockFirebaseAnalyticsEventLogging firebaseAnalyticsEventLogging; + late RegisterCubit cubit; + + setUp(() { + registerUser = MockRegisterUser(); + firebaseAnalyticsEventLogging = MockFirebaseAnalyticsEventLogging(); + cubit = RegisterCubit( + registerUser: registerUser, + firebaseAnalyticsEventLogging: firebaseAnalyticsEventLogging, + ); + }); + + group('register', () { + const testError = 'some error'; + + blocTest( + 'should emit [Error] if use case fails', + build: () => cubit, + setUp: () => when(registerUser(any)) + .thenAnswer((_) async => const Left(ServerFailure(testError))), + act: (_) => cubit.register('name', 'email', 'passcode', 0), + expect: () => [RegisterError(testError)], + ); + + blocTest( + 'should emit [Success] if use case succeeds', + build: () => cubit, + setUp: () { + when(registerUser(any)).thenAnswer((_) async => const Right(null)); + when(firebaseAnalyticsEventLogging.signUpEvent()); + }, + act: (_) => cubit.register('name', 'email', 'passcode', 0), + expect: () => [RegisterSuccess()], + ); + }); +} From 2bb7da1c32cf74c01aaffa5e25e3ce6bdf8054e5 Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Tue, 30 May 2023 18:16:14 +0200 Subject: [PATCH 27/32] Refactor and test products (#482) --- lib/cubits/products/products_cubit.dart | 31 -------- .../repositories/v1/product_repository.dart | 23 ------ .../product_remote_data_source.dart | 23 ++++++ .../product/data/models/product_model.dart | 22 ++++++ .../product/domain/entities}/product.dart | 14 ++-- .../domain/usecases/get_all_products.dart | 25 +++++++ .../presentation/cubit/product_cubit.dart | 31 ++++++++ .../presentation/cubit/product_state.dart} | 18 ++--- .../pages/buy_single_drink_page.dart | 16 ++--- .../presentation/pages/buy_tickets_page.dart | 16 ++--- .../buy_ticket_bottom_modal_sheet.dart | 2 +- .../widgets}/buy_tickets_card.dart | 2 +- .../presentation/cubit/purchase_cubit.dart | 2 +- .../widgets/purchase_overlay.dart | 2 +- .../presentation/widgets}/shop_card.dart | 0 .../presentation/widgets/shop_section.dart | 6 +- lib/service_locator.dart | 26 ++++--- .../firebase_analytics_event_logging.dart | 2 +- pubspec.yaml | 2 +- test/cubits/products/products_cubit_test.dart | 71 ------------------- .../product_remote_data_source_test.dart | 65 +++++++++++++++++ .../usecases/get_all_products_test.dart | 67 +++++++++++++++++ .../cubit/product_cubit_test.dart | 62 ++++++++++++++++ .../cubit/purchase_cubit_test.dart | 2 +- .../receipt_remote_data_source_test.dart | 6 +- .../tickets/buy_tickets_card_test.dart | 4 +- 26 files changed, 357 insertions(+), 183 deletions(-) delete mode 100644 lib/cubits/products/products_cubit.dart delete mode 100644 lib/data/repositories/v1/product_repository.dart create mode 100644 lib/features/product/data/datasources/product_remote_data_source.dart create mode 100644 lib/features/product/data/models/product_model.dart rename lib/{models/ticket => features/product/domain/entities}/product.dart (59%) create mode 100644 lib/features/product/domain/usecases/get_all_products.dart create mode 100644 lib/features/product/presentation/cubit/product_cubit.dart rename lib/{cubits/products/products_state.dart => features/product/presentation/cubit/product_state.dart} (64%) rename lib/features/{purchase => product}/presentation/pages/buy_single_drink_page.dart (86%) rename lib/features/{purchase => product}/presentation/pages/buy_tickets_page.dart (88%) rename lib/{widgets/components/tickets => features/product/presentation/widgets}/buy_ticket_bottom_modal_sheet.dart (98%) rename lib/{widgets/components/tickets => features/product/presentation/widgets}/buy_tickets_card.dart (96%) rename lib/{widgets/components/tickets => features/ticket/presentation/widgets}/shop_card.dart (100%) delete mode 100644 test/cubits/products/products_cubit_test.dart create mode 100644 test/features/product/data/datasources/product_remote_data_source_test.dart create mode 100644 test/features/product/domain/usecases/get_all_products_test.dart create mode 100644 test/features/product/presentation/cubit/product_cubit_test.dart diff --git a/lib/cubits/products/products_cubit.dart b/lib/cubits/products/products_cubit.dart deleted file mode 100644 index e82a9e645..000000000 --- a/lib/cubits/products/products_cubit.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:coffeecard/data/repositories/v1/product_repository.dart'; -import 'package:coffeecard/models/ticket/product.dart'; -import 'package:equatable/equatable.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; - -part 'products_state.dart'; - -class ProductsCubit extends Cubit { - final ProductRepository _repository; - - ProductsCubit(this._repository) : super(const ProductsLoading()); - - Future getProducts() async { - emit(const ProductsLoading()); - final either = await _repository.getProducts(); - - either.fold( - (error) => emit(ProductsError(error.reason)), - (products) { - final ticketProducts = products.where((p) => p.amount > 1); - final singleDrinkProducts = products.where((p) => p.amount == 1); - emit( - ProductsLoaded( - ticketProducts.toList(), - singleDrinkProducts.toList(), - ), - ); - }, - ); - } -} diff --git a/lib/data/repositories/v1/product_repository.dart b/lib/data/repositories/v1/product_repository.dart deleted file mode 100644 index 89fe8ce00..000000000 --- a/lib/data/repositories/v1/product_repository.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/core/network/network_request_executor.dart'; -import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; -import 'package:coffeecard/models/ticket/product.dart'; -import 'package:fpdart/fpdart.dart'; - -class ProductRepository { - ProductRepository({ - required this.apiV1, - required this.executor, - }); - - final CoffeecardApi apiV1; - final NetworkRequestExecutor executor; - - Future>> getProducts() async { - final result = await executor( - apiV1.apiV1ProductsGet, - ); - - return result.map((result) => result.map((e) => Product.fromDTO(e))); - } -} diff --git a/lib/features/product/data/datasources/product_remote_data_source.dart b/lib/features/product/data/datasources/product_remote_data_source.dart new file mode 100644 index 000000000..7038899d1 --- /dev/null +++ b/lib/features/product/data/datasources/product_remote_data_source.dart @@ -0,0 +1,23 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/either_extensions.dart'; +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/product/data/models/product_model.dart'; +import 'package:coffeecard/features/product/domain/entities/product.dart'; +import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; +import 'package:fpdart/fpdart.dart'; + +class ProductRemoteDataSource { + final CoffeecardApi apiV1; + final NetworkRequestExecutor executor; + + ProductRemoteDataSource({ + required this.apiV1, + required this.executor, + }); + + Future>> getProducts() async { + return executor( + apiV1.apiV1ProductsGet, + ).bindFuture((result) => result.map(ProductModel.fromDTO).toList()); + } +} diff --git a/lib/features/product/data/models/product_model.dart b/lib/features/product/data/models/product_model.dart new file mode 100644 index 000000000..7a725d1fd --- /dev/null +++ b/lib/features/product/data/models/product_model.dart @@ -0,0 +1,22 @@ +import 'package:coffeecard/features/product/domain/entities/product.dart'; +import 'package:coffeecard/generated/api/coffeecard_api.models.swagger.dart'; + +class ProductModel extends Product { + const ProductModel({ + required super.price, + required super.amount, + required super.name, + required super.id, + required super.description, + }); + + factory ProductModel.fromDTO(ProductDto dto) { + return ProductModel( + price: dto.price, + amount: dto.numberOfTickets, + name: dto.name, + id: dto.id, + description: dto.description, + ); + } +} diff --git a/lib/models/ticket/product.dart b/lib/features/product/domain/entities/product.dart similarity index 59% rename from lib/models/ticket/product.dart rename to lib/features/product/domain/entities/product.dart index 0a54aa46b..298f1a622 100644 --- a/lib/models/ticket/product.dart +++ b/lib/features/product/domain/entities/product.dart @@ -1,6 +1,6 @@ -import 'package:coffeecard/generated/api/coffeecard_api.models.swagger.dart'; +import 'package:equatable/equatable.dart'; -class Product { +class Product extends Equatable { final int id; final int amount; final int price; @@ -15,15 +15,11 @@ class Product { required this.description, }); - Product.fromDTO(ProductDto dto) - : id = dto.id, - name = dto.name, - price = dto.price, - amount = dto.numberOfTickets, - description = dto.description; - @override String toString() { return 'Product{id: $id, amount: $amount, price: $price, productName: $name, description> $description}'; } + + @override + List get props => [id, amount, price, name, description]; } diff --git a/lib/features/product/domain/usecases/get_all_products.dart b/lib/features/product/domain/usecases/get_all_products.dart new file mode 100644 index 000000000..26e38b058 --- /dev/null +++ b/lib/features/product/domain/usecases/get_all_products.dart @@ -0,0 +1,25 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/extensions/either_extensions.dart'; +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/features/product/data/datasources/product_remote_data_source.dart'; +import 'package:coffeecard/features/product/domain/entities/product.dart'; +import 'package:fpdart/fpdart.dart'; + +class GetAllProducts + implements UseCase<(Iterable, Iterable), NoParams> { + final ProductRemoteDataSource remoteDataSource; + + GetAllProducts({required this.remoteDataSource}); + + @override + Future, List)>> call( + NoParams params, + ) async { + return remoteDataSource.getProducts().bindFuture((products) { + final ticketProducts = products.where((p) => p.amount > 1).toList(); + final singleDrinkProducts = products.where((p) => p.amount == 1).toList(); + + return (ticketProducts, singleDrinkProducts); + }); + } +} diff --git a/lib/features/product/presentation/cubit/product_cubit.dart b/lib/features/product/presentation/cubit/product_cubit.dart new file mode 100644 index 000000000..4481e7ea0 --- /dev/null +++ b/lib/features/product/presentation/cubit/product_cubit.dart @@ -0,0 +1,31 @@ +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/features/product/domain/entities/product.dart'; +import 'package:coffeecard/features/product/domain/usecases/get_all_products.dart'; +import 'package:equatable/equatable.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +part 'product_state.dart'; + +class ProductCubit extends Cubit { + final GetAllProducts getAllProducts; + + ProductCubit({required this.getAllProducts}) : super(const ProductsLoading()); + + Future getProducts() async { + emit(const ProductsLoading()); + + final either = await getAllProducts(NoParams()); + + either.fold( + (error) => emit(ProductsError(error.reason)), + (ticketsAndSingleDrinks) { + emit( + ProductsLoaded( + ticketsAndSingleDrinks.$1.toList(), + ticketsAndSingleDrinks.$2.toList(), + ), + ); + }, + ); + } +} diff --git a/lib/cubits/products/products_state.dart b/lib/features/product/presentation/cubit/product_state.dart similarity index 64% rename from lib/cubits/products/products_state.dart rename to lib/features/product/presentation/cubit/product_state.dart index c3a88ff71..c35279c38 100644 --- a/lib/cubits/products/products_state.dart +++ b/lib/features/product/presentation/cubit/product_state.dart @@ -1,29 +1,31 @@ -part of 'products_cubit.dart'; +part of 'product_cubit.dart'; -sealed class ProductsState extends Equatable { - const ProductsState(); +sealed class ProductState extends Equatable { + const ProductState(); } -class ProductsLoading extends ProductsState { +class ProductsLoading extends ProductState { const ProductsLoading(); @override List get props => []; } -class ProductsLoaded extends ProductsState { - const ProductsLoaded(this.ticketProducts, this.singleDrinkProducts); +class ProductsLoaded extends ProductState { final List ticketProducts; final List singleDrinkProducts; + const ProductsLoaded(this.ticketProducts, this.singleDrinkProducts); + @override List get props => [ticketProducts, singleDrinkProducts]; } -class ProductsError extends ProductsState { - const ProductsError(this.error); +class ProductsError extends ProductState { final String error; + const ProductsError(this.error); + @override List get props => [error]; } diff --git a/lib/features/purchase/presentation/pages/buy_single_drink_page.dart b/lib/features/product/presentation/pages/buy_single_drink_page.dart similarity index 86% rename from lib/features/purchase/presentation/pages/buy_single_drink_page.dart rename to lib/features/product/presentation/pages/buy_single_drink_page.dart index 93bf89028..66e385ae5 100644 --- a/lib/features/purchase/presentation/pages/buy_single_drink_page.dart +++ b/lib/features/product/presentation/pages/buy_single_drink_page.dart @@ -1,20 +1,19 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; -import 'package:coffeecard/cubits/products/products_cubit.dart'; -import 'package:coffeecard/data/repositories/v1/product_repository.dart'; +import 'package:coffeecard/features/product/domain/entities/product.dart'; +import 'package:coffeecard/features/product/presentation/cubit/product_cubit.dart'; +import 'package:coffeecard/features/product/presentation/widgets/buy_ticket_bottom_modal_sheet.dart'; +import 'package:coffeecard/features/product/presentation/widgets/buy_tickets_card.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart'; import 'package:coffeecard/features/ticket/presentation/cubit/tickets_cubit.dart'; -import 'package:coffeecard/models/ticket/product.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; import 'package:coffeecard/widgets/components/error_section.dart'; import 'package:coffeecard/widgets/components/helpers/grid.dart'; import 'package:coffeecard/widgets/components/loading.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; -import 'package:coffeecard/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart'; -import 'package:coffeecard/widgets/components/tickets/buy_tickets_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -30,11 +29,10 @@ class BuySingleDrinkPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => - ProductsCubit(sl.get())..getProducts(), + create: (context) => sl()..getProducts(), child: AppScaffold.withTitle( title: Strings.buyOneDrink, - body: BlocBuilder( + body: BlocBuilder( builder: (context, state) { if (state is ProductsLoading) { return const Loading(loading: true); @@ -64,7 +62,7 @@ class BuySingleDrinkPage extends StatelessWidget { } else if (state is ProductsError) { return ErrorSection( error: state.error, - retry: context.read().getProducts, + retry: context.read().getProducts, ); } diff --git a/lib/features/purchase/presentation/pages/buy_tickets_page.dart b/lib/features/product/presentation/pages/buy_tickets_page.dart similarity index 88% rename from lib/features/purchase/presentation/pages/buy_tickets_page.dart rename to lib/features/product/presentation/pages/buy_tickets_page.dart index 708a4f6ae..b674180c9 100644 --- a/lib/features/purchase/presentation/pages/buy_tickets_page.dart +++ b/lib/features/product/presentation/pages/buy_tickets_page.dart @@ -1,23 +1,22 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; -import 'package:coffeecard/cubits/products/products_cubit.dart'; -import 'package:coffeecard/data/repositories/v1/product_repository.dart'; import 'package:coffeecard/features/environment/domain/entities/environment.dart'; import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; +import 'package:coffeecard/features/product/domain/entities/product.dart'; +import 'package:coffeecard/features/product/presentation/cubit/product_cubit.dart'; +import 'package:coffeecard/features/product/presentation/widgets/buy_ticket_bottom_modal_sheet.dart'; +import 'package:coffeecard/features/product/presentation/widgets/buy_tickets_card.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; import 'package:coffeecard/features/receipt/presentation/cubit/receipt_cubit.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/receipt_overlay.dart'; import 'package:coffeecard/features/ticket/presentation/cubit/tickets_cubit.dart'; -import 'package:coffeecard/models/ticket/product.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; import 'package:coffeecard/widgets/components/error_section.dart'; import 'package:coffeecard/widgets/components/helpers/grid.dart'; import 'package:coffeecard/widgets/components/loading.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; -import 'package:coffeecard/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart'; -import 'package:coffeecard/widgets/components/tickets/buy_tickets_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -33,11 +32,10 @@ class BuyTicketsPage extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => - ProductsCubit(sl.get())..getProducts(), + create: (context) => sl()..getProducts(), child: AppScaffold.withTitle( title: Strings.buyTickets, - body: BlocBuilder( + body: BlocBuilder( builder: (context, state) { if (state is ProductsLoading) { return const Loading(loading: true); @@ -68,7 +66,7 @@ class BuyTicketsPage extends StatelessWidget { return ErrorSection( center: true, error: state.error, - retry: context.read().getProducts, + retry: context.read().getProducts, ); } diff --git a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart b/lib/features/product/presentation/widgets/buy_ticket_bottom_modal_sheet.dart similarity index 98% rename from lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart rename to lib/features/product/presentation/widgets/buy_ticket_bottom_modal_sheet.dart index 59b2154fd..c2abd75c6 100644 --- a/lib/widgets/components/tickets/buy_ticket_bottom_modal_sheet.dart +++ b/lib/features/product/presentation/widgets/buy_ticket_bottom_modal_sheet.dart @@ -1,10 +1,10 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; +import 'package:coffeecard/features/product/domain/entities/product.dart'; import 'package:coffeecard/features/purchase/domain/entities/internal_payment_type.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; import 'package:coffeecard/features/purchase/presentation/widgets/purchase_overlay.dart'; -import 'package:coffeecard/models/ticket/product.dart'; import 'package:coffeecard/widgets/components/tickets/bottom_modal_sheet_helper.dart'; import 'package:coffeecard/widgets/components/tickets/rounded_button.dart'; import 'package:flutter/material.dart'; diff --git a/lib/widgets/components/tickets/buy_tickets_card.dart b/lib/features/product/presentation/widgets/buy_tickets_card.dart similarity index 96% rename from lib/widgets/components/tickets/buy_tickets_card.dart rename to lib/features/product/presentation/widgets/buy_tickets_card.dart index 33e390a56..53998ae4d 100644 --- a/lib/widgets/components/tickets/buy_tickets_card.dart +++ b/lib/features/product/presentation/widgets/buy_tickets_card.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/base/style/colors.dart'; import 'package:coffeecard/base/style/text_styles.dart'; -import 'package:coffeecard/models/ticket/product.dart'; +import 'package:coffeecard/features/product/domain/entities/product.dart'; import 'package:coffeecard/utils/responsive.dart'; import 'package:coffeecard/widgets/components/card.dart'; import 'package:flutter/material.dart'; diff --git a/lib/features/purchase/presentation/cubit/purchase_cubit.dart b/lib/features/purchase/presentation/cubit/purchase_cubit.dart index 77cd947f0..2e7adee94 100644 --- a/lib/features/purchase/presentation/cubit/purchase_cubit.dart +++ b/lib/features/purchase/presentation/cubit/purchase_cubit.dart @@ -1,9 +1,9 @@ import 'package:bloc/bloc.dart'; +import 'package:coffeecard/features/product/domain/entities/product.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; import 'package:coffeecard/features/purchase/domain/usecases/init_purchase.dart'; import 'package:coffeecard/features/purchase/domain/usecases/verify_purchase_status.dart'; -import 'package:coffeecard/models/ticket/product.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; import 'package:equatable/equatable.dart'; diff --git a/lib/features/purchase/presentation/widgets/purchase_overlay.dart b/lib/features/purchase/presentation/widgets/purchase_overlay.dart index 1c37a337a..98623d79e 100644 --- a/lib/features/purchase/presentation/widgets/purchase_overlay.dart +++ b/lib/features/purchase/presentation/widgets/purchase_overlay.dart @@ -1,4 +1,5 @@ import 'package:coffeecard/base/style/colors.dart'; +import 'package:coffeecard/features/product/domain/entities/product.dart'; import 'package:coffeecard/features/purchase/data/repositories/payment_handler.dart'; import 'package:coffeecard/features/purchase/domain/entities/internal_payment_type.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; @@ -6,7 +7,6 @@ import 'package:coffeecard/features/purchase/domain/usecases/init_purchase.dart' import 'package:coffeecard/features/purchase/domain/usecases/verify_purchase_status.dart'; import 'package:coffeecard/features/purchase/presentation/cubit/purchase_cubit.dart'; import 'package:coffeecard/features/purchase/presentation/widgets/purchase_process.dart'; -import 'package:coffeecard/models/ticket/product.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; diff --git a/lib/widgets/components/tickets/shop_card.dart b/lib/features/ticket/presentation/widgets/shop_card.dart similarity index 100% rename from lib/widgets/components/tickets/shop_card.dart rename to lib/features/ticket/presentation/widgets/shop_card.dart diff --git a/lib/features/ticket/presentation/widgets/shop_section.dart b/lib/features/ticket/presentation/widgets/shop_section.dart index 2213d936e..64fcf47d3 100644 --- a/lib/features/ticket/presentation/widgets/shop_section.dart +++ b/lib/features/ticket/presentation/widgets/shop_section.dart @@ -1,9 +1,9 @@ import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/features/purchase/presentation/pages/buy_single_drink_page.dart'; -import 'package:coffeecard/features/purchase/presentation/pages/buy_tickets_page.dart'; +import 'package:coffeecard/features/product/presentation/pages/buy_single_drink_page.dart'; +import 'package:coffeecard/features/product/presentation/pages/buy_tickets_page.dart'; +import 'package:coffeecard/features/ticket/presentation/widgets/shop_card.dart'; import 'package:coffeecard/features/voucher/presentation/pages/redeem_voucher_page.dart'; import 'package:coffeecard/widgets/components/helpers/grid.dart'; -import 'package:coffeecard/widgets/components/tickets/shop_card.dart'; import 'package:flutter/material.dart'; class ShopSection extends StatelessWidget { diff --git a/lib/service_locator.dart b/lib/service_locator.dart index e6fe7a5f4..2a42634c7 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -4,7 +4,6 @@ import 'package:coffeecard/core/external/external_url_launcher.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/data/api/interceptors/authentication_interceptor.dart'; -import 'package:coffeecard/data/repositories/v1/product_repository.dart'; import 'package:coffeecard/data/storage/secure_storage.dart'; import 'package:coffeecard/env/env.dart'; import 'package:coffeecard/features/contributor/data/datasources/contributor_local_data_source.dart'; @@ -21,6 +20,9 @@ import 'package:coffeecard/features/occupation/data/datasources/occupation_remot import 'package:coffeecard/features/occupation/domain/usecases/get_occupations.dart'; import 'package:coffeecard/features/occupation/presentation/cubit/occupation_cubit.dart'; import 'package:coffeecard/features/opening_hours/opening_hours.dart'; +import 'package:coffeecard/features/product/data/datasources/product_remote_data_source.dart'; +import 'package:coffeecard/features/product/domain/usecases/get_all_products.dart'; +import 'package:coffeecard/features/product/presentation/cubit/product_cubit.dart'; import 'package:coffeecard/features/purchase/data/datasources/purchase_remote_data_source.dart'; import 'package:coffeecard/features/receipt/data/datasources/receipt_remote_data_source.dart'; import 'package:coffeecard/features/receipt/data/repositories/receipt_repository_impl.dart'; @@ -124,14 +126,6 @@ void configureServices() { ), ); - // Repositories - sl.registerFactory( - () => ProductRepository( - apiV1: sl(), - executor: sl(), - ), - ); - // v1 and v2 sl.registerFactory( () => AccountRemoteDataSource( @@ -161,6 +155,7 @@ void initFeatures() { initPayment(); initLeaderboard(); initEnvironment(); + initProduct(); initVoucher(); initLogin(); initRegister(); @@ -321,6 +316,19 @@ void initEnvironment() { ); } +void initProduct() { + // bloc + sl.registerFactory(() => ProductCubit(getAllProducts: sl())); + + // use case + sl.registerFactory(() => GetAllProducts(remoteDataSource: sl())); + + // data source + sl.registerLazySingleton( + () => ProductRemoteDataSource(apiV1: sl(), executor: sl()), + ); +} + void initVoucher() { // bloc sl.registerFactory(() => VoucherCubit(redeemVoucherCode: sl())); diff --git a/lib/utils/firebase_analytics_event_logging.dart b/lib/utils/firebase_analytics_event_logging.dart index 16c19da9d..918368848 100644 --- a/lib/utils/firebase_analytics_event_logging.dart +++ b/lib/utils/firebase_analytics_event_logging.dart @@ -1,5 +1,5 @@ +import 'package:coffeecard/features/product/domain/entities/product.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; -import 'package:coffeecard/models/ticket/product.dart'; import 'package:firebase_analytics/firebase_analytics.dart'; class FirebaseAnalyticsEventLogging { diff --git a/pubspec.yaml b/pubspec.yaml index b2cee57ea..2ec7b2b76 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -146,4 +146,4 @@ flutter: weight: 700 - family: AnalogIcons fonts: - - asset: assets/fonts/AnalogIcons.ttf + - asset: assets/fonts/AnalogIcons.ttf \ No newline at end of file diff --git a/test/cubits/products/products_cubit_test.dart b/test/cubits/products/products_cubit_test.dart deleted file mode 100644 index 8bba1e716..000000000 --- a/test/cubits/products/products_cubit_test.dart +++ /dev/null @@ -1,71 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:coffeecard/core/errors/failures.dart'; -import 'package:coffeecard/cubits/products/products_cubit.dart'; -import 'package:coffeecard/data/repositories/v1/product_repository.dart'; -import 'package:coffeecard/models/ticket/product.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:fpdart/fpdart.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'products_cubit_test.mocks.dart'; - -@GenerateMocks([ProductRepository]) -void main() { - const dummyProducts = [ - Product( - id: 1, - name: 'test (bundle of 10)', - amount: 10, - price: 1, - description: 'test', - ), - Product( - id: 2, - name: 'test (single)', - amount: 1, - price: 1, - description: 'test', - ), - ]; - group('products cubit tests', () { - late ProductsCubit productsCubit; - final repo = MockProductRepository(); - - setUp(() { - productsCubit = ProductsCubit(repo); - }); - - blocTest( - 'should emit loading and loaded when repo.getProducts succeeds', - build: () { - when(repo.getProducts()) - .thenAnswer((_) async => const Right(dummyProducts)); - return productsCubit; - }, - act: (cubit) => cubit.getProducts(), - expect: () => [ - const ProductsLoading(), - ProductsLoaded([dummyProducts.first], [dummyProducts[1]]), - ], - ); - - blocTest( - 'should emit loading and error when repo.getProducts fails', - build: () { - when(repo.getProducts()) - .thenAnswer((_) async => const Left(ServerFailure('some error'))); - return productsCubit; - }, - act: (cubit) => cubit.getProducts(), - expect: () => [ - const ProductsLoading(), - const ProductsError('some error'), - ], - ); - - tearDown(() { - productsCubit.close(); - }); - }); -} diff --git a/test/features/product/data/datasources/product_remote_data_source_test.dart b/test/features/product/data/datasources/product_remote_data_source_test.dart new file mode 100644 index 000000000..b8f7270bb --- /dev/null +++ b/test/features/product/data/datasources/product_remote_data_source_test.dart @@ -0,0 +1,65 @@ +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/product/data/datasources/product_remote_data_source.dart'; +import 'package:coffeecard/features/product/data/models/product_model.dart'; +import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'product_remote_data_source_test.mocks.dart'; + +@GenerateMocks([CoffeecardApi, NetworkRequestExecutor]) +void main() { + late ProductRemoteDataSource remoteDataSource; + late MockCoffeecardApi apiV1; + late MockNetworkRequestExecutor executor; + + setUp(() { + executor = MockNetworkRequestExecutor(); + apiV1 = MockCoffeecardApi(); + remoteDataSource = ProductRemoteDataSource( + apiV1: apiV1, + executor: executor, + ); + }); + + group('getProducts', () { + test('should call executor', () async { + // arrange + when(executor.call>(any)).thenAnswer( + (_) async => Right([ + ProductDto( + id: 0, + price: 0, + numberOfTickets: 0, + name: 'name', + description: 'description', + ), + ]), + ); + + // act + final actual = await remoteDataSource.getProducts(); + + // assert + actual.fold( + (_) => throw Exception(), + (actual) { + expect( + actual, + [ + const ProductModel( + price: 0, + amount: 0, + name: 'name', + id: 0, + description: 'description', + ), + ], + ); + }, + ); + }); + }); +} diff --git a/test/features/product/domain/usecases/get_all_products_test.dart b/test/features/product/domain/usecases/get_all_products_test.dart new file mode 100644 index 000000000..0b132ce26 --- /dev/null +++ b/test/features/product/domain/usecases/get_all_products_test.dart @@ -0,0 +1,67 @@ +// TODO(fremartini): uncomment when Mockito supports records, https://github.com/AnalogIO/coffeecard_app/issues/488 +//import 'get_all_products_test.mocks.dart'; + +//@GenerateMocks([ProductRemoteDataSource]) +void main() { + /* + late GetAllProducts usecase; + late MockProductRemoteDataSource remoteDataSource; + + setUp(() { + remoteDataSource = MockProductRemoteDataSource(); + usecase = GetAllProducts(remoteDataSource: remoteDataSource); + }); + + const testError = 'some error'; + + test('should return [Left] if data source fails', () async { + // arrange + when(remoteDataSource.getProducts()) + .thenAnswer((_) async => const Left(ServerFailure(testError))); + + // act + final actual = await usecase(NoParams()); + + // assert + expect(actual, const Left(ServerFailure(testError))); + }); + + test( + 'should return [Right, Iterable>] if data source succeeds', + () async { + // arrange + const products = [ + ProductModel( + id: 1, + name: 'test (bundle of 10)', + amount: 10, + price: 1, + description: 'test', + ), + ProductModel( + id: 2, + name: 'test (single)', + amount: 1, + price: 1, + description: 'test', + ), + ]; + + when(remoteDataSource.getProducts()) + .thenAnswer((_) async => const Right(products)); + + // act + final actual = await usecase(NoParams()); + + // assert + actual.fold( + (_) => throw Exception(), + (actual) { + expect(actual.$1, [products.first]); + expect(actual.$2, [products[1]]); + }, + ); + }, + ); + */ +} diff --git a/test/features/product/presentation/cubit/product_cubit_test.dart b/test/features/product/presentation/cubit/product_cubit_test.dart new file mode 100644 index 000000000..2723ade84 --- /dev/null +++ b/test/features/product/presentation/cubit/product_cubit_test.dart @@ -0,0 +1,62 @@ +// TODO(fremartini): uncomment when Mockito supports records, https://github.com/AnalogIO/coffeecard_app/issues/488 +//import 'product_cubit_test.mocks.dart'; + +//@GenerateMocks([GetAllProducts]) +void main() { + /* + late ProductCubit cubit; + late MockGetAllProducts getAllProducts; + + setUp(() { + getAllProducts = MockGetAllProducts(); + cubit = ProductCubit(getAllProducts: getAllProducts); + }); + + const tickets = [ + Product( + id: 1, + name: 'test (bundle of 10)', + amount: 10, + price: 1, + description: 'test', + ), + ]; + const singleDrinks = [ + Product( + id: 2, + name: 'test (single)', + amount: 1, + price: 1, + description: 'test', + ), + ]; + + const testError = 'some error'; + + group('getProducts', () { + blocTest( + 'should emit [Loading, Loaded] use case succeeds', + build: () => cubit, + setUp: () => when(getAllProducts(any)) + .thenAnswer((_) async => const Right((tickets, singleDrinks))), + act: (cubit) => cubit.getProducts(), + expect: () => [ + const ProductsLoading(), + const ProductsLoaded(tickets, singleDrinks), + ], + ); + + blocTest( + 'should emit [Loading, Error] when use case fails', + build: () => cubit, + setUp: () => when(getAllProducts(any)) + .thenAnswer((_) async => const Left(ServerFailure(testError))), + act: (cubit) => cubit.getProducts(), + expect: () => [ + const ProductsLoading(), + const ProductsError(testError), + ], + ); + }); + */ +} diff --git a/test/features/purchase/presentation/cubit/purchase_cubit_test.dart b/test/features/purchase/presentation/cubit/purchase_cubit_test.dart index 2854c6d0a..2793ee3c9 100644 --- a/test/features/purchase/presentation/cubit/purchase_cubit_test.dart +++ b/test/features/purchase/presentation/cubit/purchase_cubit_test.dart @@ -1,11 +1,11 @@ import 'package:bloc_test/bloc_test.dart'; import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/features/product/domain/entities/product.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; import 'package:coffeecard/features/purchase/domain/usecases/init_purchase.dart'; import 'package:coffeecard/features/purchase/domain/usecases/verify_purchase_status.dart'; import 'package:coffeecard/features/purchase/presentation/cubit/purchase_cubit.dart'; -import 'package:coffeecard/models/ticket/product.dart'; import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:fpdart/fpdart.dart'; diff --git a/test/features/receipt/data/datasources/receipt_remote_data_source_test.dart b/test/features/receipt/data/datasources/receipt_remote_data_source_test.dart index 50f1744ca..90a4763c1 100644 --- a/test/features/receipt/data/datasources/receipt_remote_data_source_test.dart +++ b/test/features/receipt/data/datasources/receipt_remote_data_source_test.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/core/errors/failures.dart'; import 'package:coffeecard/core/network/network_request_executor.dart'; -import 'package:coffeecard/data/repositories/v1/product_repository.dart'; +import 'package:coffeecard/features/product/data/datasources/product_remote_data_source.dart'; import 'package:coffeecard/features/receipt/data/datasources/receipt_remote_data_source.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:flutter_test/flutter_test.dart'; @@ -10,7 +10,9 @@ import 'package:mockito/mockito.dart'; import 'receipt_remote_data_source_test.mocks.dart'; -@GenerateMocks([CoffeecardApiV2, ProductRepository, NetworkRequestExecutor]) +@GenerateMocks( + [CoffeecardApiV2, ProductRemoteDataSource, NetworkRequestExecutor], +) void main() { late ReceiptRemoteDataSource remoteDataSource; late MockCoffeecardApiV2 apiV2; diff --git a/test/widgets/components/tickets/buy_tickets_card_test.dart b/test/widgets/components/tickets/buy_tickets_card_test.dart index ce901de32..bd8a7f8ad 100644 --- a/test/widgets/components/tickets/buy_tickets_card_test.dart +++ b/test/widgets/components/tickets/buy_tickets_card_test.dart @@ -1,5 +1,5 @@ -import 'package:coffeecard/models/ticket/product.dart'; -import 'package:coffeecard/widgets/components/tickets/buy_tickets_card.dart'; +import 'package:coffeecard/features/product/domain/entities/product.dart'; +import 'package:coffeecard/features/product/presentation/widgets/buy_tickets_card.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; From 6a494ad4ed900869796720b2d02b9852d0fcfd00 Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Tue, 6 Jun 2023 17:25:52 +0200 Subject: [PATCH 28/32] fix(utils): Fix reactivation authenticator not evicting; refactor & test (#429) # Summary This pull request introduces changes to the `reactivation_authenticator.dart` file, including a bug fix. The utility class `mutex.dart` is also rewritten (but unused), and a new utility class `throttler.dart` is added. All these files have corresponding tests. Closes #416 Closes #492 # Bug fixes **Fixed a bug where the user was not evicted if the token was expired.** The method `ReactivationAuthenticator.authenticate()` now checks the request body type against the `LoginDto` type to check if a failed request was an attempt to refresh the token; if it was, we stop execution to avoid infinite recursion. This change fixes #492. # Utility classes - Rewritten `Mutex` utility class and added tests for it. - Added new utility class `Throttler`. This class allows you to limit the rate at which some tasks are executed by ensuring that only one task can run through the `Throttler` at any given time. If a task is already running through the `Throttler`, an attempt to throttle a task will instead return the currently running task. - Both classes utilize fpdart's `Task` class to support asynchronous operations, alongside other fpdart utilities such as `Option`, `Unit`, and the `bool.match()` extension where appropriate. - Both classes are tested with full coverage. # `ReactivationAuthenticator` The `ReactivationAuthenticator` class has been rewritten to use the new `Throttler` class instead of the `Mutex` class. The `ReactivationAuthenticator` class has also been simplified and cleaned up. For example, we no longer needs to maintain internal state about whether we should throttle a token refresh attempt, and we don't need to check whether a `Mutex` is locked, since the `Throttler` class handles both cases for us. The class is also tested with full coverage. This change closes #416. # Service locator * Moved the initialization of http clients, interceptors, and authenticators to a separate function `initHttp()` in `service_locator.dart`. * In `service_locator.dart`, the `ReactivationAuthenticator` class is now registered in two steps: 1. The `ReactivationAuthenticator` is registered as a singleton using the constructor `ReactivationAuthenticator.uninitialized()`. 2. The method `ReactivationAuthenticator.initialize()` is called to initialize the `ReactivationAuthenticator` with the `AccountRemoteDataSource` to use. --- lib/service_locator.dart | 83 ++-- lib/utils/mutex.dart | 96 ++++- lib/utils/reactivation_authenticator.dart | 219 ++++++----- lib/utils/throttler.dart | 60 +++ test/utils/mutex_test.dart | 148 ++++++++ .../reactivation_authenticator_test.dart | 354 ++++++++++++++++++ test/utils/throttler_test.dart | 101 +++++ 7 files changed, 911 insertions(+), 150 deletions(-) create mode 100644 lib/utils/throttler.dart create mode 100644 test/utils/mutex_test.dart create mode 100644 test/utils/reactivation_authenticator_test.dart create mode 100644 test/utils/throttler_test.dart diff --git a/lib/service_locator.dart b/lib/service_locator.dart index 2a42634c7..4599b9d56 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -85,47 +85,12 @@ void configureServices() { ), ); - sl.registerFactory( - () => ReactivationAuthenticator(sl), - ); + // Reactivation authenticator (uninitalized), http client and interceptors + initHttp(); // Features initFeatures(); - // Rest Client, Chopper client - final coffeCardChopper = ChopperClient( - baseUrl: Uri.parse(Env.coffeeCardUrl), - interceptors: [AuthenticationInterceptor(sl())], - converter: $JsonSerializableConverter(), - services: [ - CoffeecardApi.create(), - CoffeecardApiV2.create(), - ], - authenticator: sl.get(), - ); - - final shiftplanningChopper = ChopperClient( - baseUrl: ApiUriConstants.shiftyUrl, - converter: $JsonSerializableConverter(), - services: [ShiftplanningApi.create()], - ); - - ignoreValue( - sl.registerSingleton( - coffeCardChopper.getService(), - ), - ); - ignoreValue( - sl.registerSingleton( - coffeCardChopper.getService(), - ), - ); - ignoreValue( - sl.registerSingleton( - shiftplanningChopper.getService(), - ), - ); - // v1 and v2 sl.registerFactory( () => AccountRemoteDataSource( @@ -143,6 +108,9 @@ void configureServices() { ); ignoreValue(sl.registerLazySingleton(() => ExternalUrlLauncher())); + + // provide the account repository to the reactivation authenticator + sl().initialize(sl()); } void initFeatures() { @@ -368,3 +336,44 @@ void initRegister() { () => RegisterRemoteDataSource(apiV2: sl(), executor: sl()), ); } + +void initHttp() { + ignoreValue( + sl.registerSingleton( + ReactivationAuthenticator.uninitialized(serviceLocator: sl), + ), + ); + + final coffeCardChopper = ChopperClient( + baseUrl: Uri.parse(Env.coffeeCardUrl), + interceptors: [AuthenticationInterceptor(sl())], + converter: $JsonSerializableConverter(), + services: [ + CoffeecardApi.create(), + CoffeecardApiV2.create(), + ], + authenticator: sl.get(), + ); + + final shiftplanningChopper = ChopperClient( + baseUrl: ApiUriConstants.shiftyUrl, + converter: $JsonSerializableConverter(), + services: [ShiftplanningApi.create()], + ); + + ignoreValue( + sl.registerSingleton( + coffeCardChopper.getService(), + ), + ); + ignoreValue( + sl.registerSingleton( + coffeCardChopper.getService(), + ), + ); + ignoreValue( + sl.registerSingleton( + shiftplanningChopper.getService(), + ), + ); +} diff --git a/lib/utils/mutex.dart b/lib/utils/mutex.dart index 2a53497f4..8669026f4 100644 --- a/lib/utils/mutex.dart +++ b/lib/utils/mutex.dart @@ -1,24 +1,96 @@ import 'dart:async'; +import 'dart:collection'; +import 'package:fpdart/fpdart.dart'; + +/// A mutual exclusion lock that can be used to protect critical sections of +/// code from concurrent access. +/// +/// A [Mutex] can be acquired by calling the [protect] method with a [Task] that +/// represents the critical section of code to be protected. If the [Mutex] is +/// already locked, the [Task] will wait until the lock is released before +/// acquiring it and running the critical section. +/// +/// Example usage: +/// +/// ```dart +/// final mutex = Mutex(); +/// final criticalSection = Task(() => 42).delay(Duration(seconds: 1)); +/// +/// // Both tasks will run sequentially. +/// final task1 = mutex.protect(criticalSection).run(); +/// final task2 = mutex.protect(criticalSection).run(); +/// +/// print(await task1); +/// print(await task2); +/// // Output: +/// // 42 +/// // 42 +/// ``` class Mutex { - Future? _lock; - final _completer = Completer(); + final _waitQueue = Queue>(); + bool _isLocked = false; + + /// Indicates whether the lock is currently acquired. + bool get isLocked => _isLocked; + + /// Acquires the lock, runs the provided action, and then releases the lock. + /// + /// If the lock is already acquired, this method will wait until the lock is + /// released before acquiring it and running the action. + Task protect(Task criticalSection) { + return _lock().call(criticalSection).chainFirst((_) => _unlock()); + } - bool isLocked() => _lock != null; + /// Acquires the lock. + /// + /// If the lock is already acquired, the task will wait in the wait queue + /// until it is woke up by the previous task releasing the lock. + Task _lock() { + return _isLocked.match( + () => _setLocked(true), + () => _waitInQueue(), + ); + } - void lock() { - _lock = _completer.future; + /// Releases the lock. + /// + /// If the wait queue is not empty, wakes up the next task in the queue. + /// Otherwise, sets the lock to unlocked. + Task _unlock() { + return _waitQueue.isNotEmpty.match( + () => _setLocked(false), + () => _dequeue(), + ); } - void unlock() { - if (!_completer.isCompleted) { - _completer.complete(); - } + Task _setLocked(bool value) { + _isLocked = value; + return Task.of(unit); + } - _lock = null; + /// Adds a completer to the wait queue and waits for it to complete. + Task _waitInQueue() { + return Task(() async { + final completer = Completer(); + _waitQueue.add(completer); + await completer.future; + return unit; + }); } - Future wait() async { - await _lock; + /// Completes the first completer in the wait queue. + Task _dequeue() { + _waitQueue.removeFirst().complete(); + return Task.of(unit); } } + +extension TaskMutexX on Task { + /// Ensures the task will run in a critical section + /// protected by the given [Mutex]. + /// + /// If the mutex is already locked when the task is run, this method will wait + /// until the mutex is released before acquiring it and running the task. + Task protect(Mutex mutex) => mutex.protect(this); +} diff --git a/lib/utils/reactivation_authenticator.dart b/lib/utils/reactivation_authenticator.dart index 97008c46d..e2a0bc238 100644 --- a/lib/utils/reactivation_authenticator.dart +++ b/lib/utils/reactivation_authenticator.dart @@ -4,128 +4,145 @@ import 'package:chopper/chopper.dart'; import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/data/storage/secure_storage.dart'; -import 'package:coffeecard/utils/mutex.dart'; +import 'package:coffeecard/generated/api/coffeecard_api.models.swagger.dart' + show LoginDto; +import 'package:coffeecard/utils/throttler.dart'; +import 'package:fpdart/fpdart.dart'; import 'package:get_it/get_it.dart'; import 'package:logger/logger.dart'; class ReactivationAuthenticator extends Authenticator { - final GetIt serviceLocator; - static const Duration debounce = Duration(seconds: 2); - - DateTime? tokenRefreshedAt; - Mutex mutex = Mutex(); - - late SecureStorage secureStorage; - late AuthenticationCubit authenticationCubit; - late Logger logger; - - ReactivationAuthenticator(this.serviceLocator) { - secureStorage = serviceLocator.get(); - authenticationCubit = serviceLocator.get(); - logger = serviceLocator.get(); - } - - bool canRefreshToken() => - tokenRefreshedAt == null || - DateTime.now().difference(tokenRefreshedAt!) > debounce; - - Future evict() => authenticationCubit.unauthenticated(); - - Map updateHeadersWithToken( - Map headers, - String token, - ) { - final _ = headers.update( - 'Authorization', - (String _) => token, - ifAbsent: () => token, - ); - - return headers; - } - - void log(Request request, Response response) { - logger.d( - '${request.method} ${request.url} ${response.statusCode}\n${response.bodyString}', - ); + /// Whether [initialize] has been called. + bool _ready = false; + late final AccountRemoteDataSource _accountRemoteDataSource; + + final SecureStorage _secureStorage; + final AuthenticationCubit _authenticationCubit; + final Logger _logger; + + final _throttler = Throttler>(); + + /// Creates a new [ReactivationAuthenticator] instance. + /// + /// This instance is not ready to be used. Call [initialize] before using it. + ReactivationAuthenticator.uninitialized({required GetIt serviceLocator}) + : _secureStorage = serviceLocator(), + _authenticationCubit = serviceLocator(), + _logger = serviceLocator(); + + /// Initializes the [ReactivationAuthenticator] by providing the + /// [AccountRemoteDataSource] to use. + /// + /// This method must be called before the [ReactivationAuthenticator] is used. + void initialize(AccountRemoteDataSource accountRemoteDataSource) { + _ready = true; + _accountRemoteDataSource = accountRemoteDataSource; } @override - FutureOr authenticate( + Future authenticate( Request request, Response response, [ - Request? originalRequest, - ]) async { - log(request, response); + Request? _, + ]) { + // If the [ReactivationAuthenticator] is not ready, an error is thrown. + assert( + _ready, + 'ReactivationAuthenticator is not ready. ' + 'Call initialize() before using it.', + ); + // If the response is not unauthorized, we don't need to do anything. if (response.statusCode != 401) { - return null; + return Future.value(); } - if (mutex.isLocked()) { - // someone is updating the token, wait until they are done and read it - await mutex.wait(); - - final refreshedToken = await secureStorage.readToken(); - - if (refreshedToken == null) { - return null; - } - - return request.copyWith( - headers: updateHeadersWithToken(request.headers, refreshedToken), - ); + // If the request is a unauthorized login request (token refresh request), + // we should not try to refresh the token. + // Otherwise, we would end up in an infinite loop. + if (request.body is LoginDto) { + return Future.value(); } - // avoid refreshing the token multiple times if requests happen at the same time - if (canRefreshToken()) { - return await refreshToken(request); - } + // Try to refresh the token. + final maybeNewToken = Task(() async { + // Set a minimum duration for the token refresh to allow for throttling. + final minimumDuration = Future.delayed(const Duration(seconds: 1)); + final maybeToken = await _getNewToken(request).run(); + await minimumDuration; + // Side effect: save the new token or evict the current token. + final _ = _saveOrEvict(maybeToken).run(); + return maybeToken; + }).runThrottled(_throttler); + + // Convert the [maybeNewToken] to a [Request] with the new token, or null. + final maybeNewRequest = TaskOption(() => maybeNewToken).match( + () => null, + (token) => request..headers['Authorization'] = 'Bearer $token', + ); - return null; + return maybeNewRequest.run(); } - Future refreshToken( - Request request, - ) async { - final email = await secureStorage.readEmail(); - final encodedPasscode = await secureStorage.readEncodedPasscode(); - - if (email == null || encodedPasscode == null) { - //User is not logged in - return null; - } - - mutex.lock(); - - try { - final accountRepository = serviceLocator.get(); - - // this call may return 401 which triggers a recursive call, use a guard - final either = await accountRepository.login(email, encodedPasscode); - - return either.fold( - (_) { - // refresh failed, sign the user out - evict(); + /// Attempt to retrieve a new token by logging in with stored credentials. + Task> _getNewToken(Request request) { + _logRefreshingToken(request); + + return Task( + () async { + // Check if user credentials are stored; if not, return None. + final email = await _secureStorage.readEmail(); + final encodedPasscode = await _secureStorage.readEncodedPasscode(); + if (email == null || encodedPasscode == null) { + return none(); + } + + // Attempt to log in with the stored credentials. + // This login call may return 401 if the stored credentials are invalid; + // recursive calls to [authenticate] are blocked by a check in the + // [authenticate] method. + final either = + await _accountRemoteDataSource.login(email, encodedPasscode); + + return Option.fromEither(either).map((user) => user.token); + }, + ); + } - return null; + /// Saves the [token] in [SecureStorage] + /// or signs out the user if the [token] is [None]. + Task _saveOrEvict(Option token) { + return Task(() async { + return token.match( + () async { + _logRefreshTokenFailed(); + await _authenticationCubit.unauthenticated(); + return unit; }, - (user) async { - // refresh succeeded, update the token in secure storage - tokenRefreshedAt = DateTime.now(); - - final token = user.token; - final bearerToken = 'Bearer ${user.token}'; - await secureStorage.updateToken(token); - - return request.copyWith( - headers: updateHeadersWithToken(request.headers, bearerToken), - ); + (token) async { + _logRefreshTokenSucceeded(); + await _secureStorage.updateToken(token); + return unit; }, ); - } finally { - mutex.unlock(); - } + }); + } + + /// Logs that a token refresh was triggered by a request. + void _logRefreshingToken(Request request) { + _logger.d( + 'Token refresh triggered by request:\n' + '\t${request.method} ${request.url}', + ); + } + + /// Logs that the refresh token call failed. + void _logRefreshTokenFailed() { + _logger.e('Failed to refresh token. Signing out.'); + } + + /// Logs that the refresh token call succeeded. + void _logRefreshTokenSucceeded() { + _logger.d('Successfully refreshed token.'); } } diff --git a/lib/utils/throttler.dart b/lib/utils/throttler.dart new file mode 100644 index 000000000..044cd0abc --- /dev/null +++ b/lib/utils/throttler.dart @@ -0,0 +1,60 @@ +import 'package:fpdart/fpdart.dart'; + +/// A utility class for throttling the execution of asynchronous tasks. +/// +/// This class allows you to limit the rate at which a task is executed by +/// ensuring that only one task runs through the [Throttler] at any given time. +/// If a task is already running, subsequent calls to execute a task will +/// instead return the currently running task. +/// +/// To ensure type safety, the [Throttler] class is generic over the type of +/// the result of the task. +/// +/// Example usage: +/// +/// ```dart +/// Future example() async { +/// int x = 0; +/// final throttler = Throttler(); +/// // Create a task that will increment the counter by 5 after 1 second. +/// final task = Task(() async => x += 5).delay(const Duration(seconds: 1)); +/// +/// // Run the task through the throttler twice. +/// final firstResult = throttler.throttle(task); +/// final secondResult = throttler.throttle(task); +/// +/// // The two futures should be identical, since the second task was throttled +/// // while the first task was still running. +/// final isSame = identical(firstResult, secondResult); +/// print(isSame); +/// print(await firstResult); +/// print(await secondResult); +/// +/// // Output: +/// // true +/// // 5 +/// // 5 +/// } +/// ``` +class Throttler { + Throttler(); + + Future? _storedTask; + + /// If no task is currently running, + /// runs the given [task] and stores it until it completes. + /// Otherwise, returns the currently running task. + Future throttle(Task task) { + return _storedTask.toOption().getOrElse( + () => _storedTask = task.run().whenComplete(() => _storedTask = null), + ); + } +} + +extension ThrottlerX on Task { + /// Throttles this task using the given [throttler]. + /// + /// If no task is currently running through the [throttler], starts this task + /// and stores it. Otherwise, returns the currently running task. + Future runThrottled(Throttler throttler) => throttler.throttle(this); +} diff --git a/test/utils/mutex_test.dart b/test/utils/mutex_test.dart new file mode 100644 index 000000000..8c4fb93d4 --- /dev/null +++ b/test/utils/mutex_test.dart @@ -0,0 +1,148 @@ +import 'dart:async'; + +import 'package:coffeecard/utils/mutex.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; + +void main() { + late Mutex mutex; + + setUp(() async { + mutex = Mutex(); + + expect( + mutex.isLocked, + isFalse, + reason: 'Mutex should be initially unlocked', + ); + }); + + tearDown(() async { + expect( + mutex.isLocked, + isFalse, + reason: 'Mutex should be unlocked after test', + ); + }); + + test( + 'GIVEN an unlocked mutex and a counter ' + 'WHEN Mutex.protect is called with a task that increments the counter ' + 'THEN the counter should be incremented once after the task completes', + () async { + int counter = 0; + + // Create a task that will return the inverse of the boolean. + final task = mutex.protect( + Task(() async => counter = counter + 1), + ); + + // Start the task. + final result = await task.run(); + + // The task should complete with 1, and the counter should be 1. + expect(result, 1); + expect(counter, 1); + }, + ); + + test( + 'GIVEN an unlocked mutex and a counter ' + 'WHEN protect is called on a task that increments the counter ' + 'THEN the counter should be incremented once after the task completes', + () async { + int counter = 0; + + // Create a task that will return the inverse of the boolean. + final task = Task(() async => counter = counter + 1).protect(mutex); + + // Start the task. + final result = await task.run(); + + // The task should complete with 1, and the counter should be 1. + expect(result, 1); + expect(counter, 1); + }, + ); + + test( + 'GIVEN an unlocked mutex ' + 'WHEN protect is called ' + 'THEN it should lock while the task is running', + () { + final completer = Completer(); + + // Create a task that will return with 42 when the completer is completed. + final task = mutex.protect( + Task(() => completer.future).map((_) => 42), + ); + + // Start the task, which should lock the mutex. + final startedTask = task.run(); + + // The mutex should be locked now. + expect( + mutex.isLocked, + isTrue, + reason: 'Mutex should be locked when task has started', + ); + + // Allow the task to complete. + completer.complete(); + + // The task should complete with 42. + expect(startedTask, completion(42)); + }, + ); + + test( + 'GIVEN a mutex locked by one task ' + 'WHEN another task tries to lock it ' + 'THEN it should wait until the first task completes ' + 'and the mutex is unlocked', + () { + final firstTaskCompleter = Completer(); + final secondTaskCompleter = Completer(); + + // Create a task that will finish executing + // when firstTaskCompleter is completed. + final firstTask = mutex.protect( + Task(() => firstTaskCompleter.future).map((_) => unit), + ); + + // Create a task that will immediately complete secondTaskCompleter. + final secondTask = mutex.protect( + Task(() async => secondTaskCompleter.complete()).map((_) => unit), + ); + + // Start the first task, which will hold + // the lock until firstTaskCompleter completes. + final firstStartedTask = firstTask.run(); + + // The mutex should be locked now. + expect( + mutex.isLocked, + isTrue, + reason: 'Mutex should be locked when task has started', + ); + + // Start the second task, which will wait until the lock is released. + final secondStartedTask = secondTask.run(); + + // The second task shouldn't complete until we complete the first task. + expect( + secondTaskCompleter.isCompleted, + isFalse, + reason: 'Second task completed before firstTaskCompleter was completed', + ); + + // Complete the first task, which should release the lock and allow the + // second task to complete. + firstTaskCompleter.complete(); + + // The tasks should complete with unit. + expect(firstStartedTask, completion(unit)); + expect(secondStartedTask, completion(unit)); + }, + ); +} diff --git a/test/utils/reactivation_authenticator_test.dart b/test/utils/reactivation_authenticator_test.dart new file mode 100644 index 000000000..38bb8b70b --- /dev/null +++ b/test/utils/reactivation_authenticator_test.dart @@ -0,0 +1,354 @@ +import 'package:chopper/chopper.dart' as chopper; +import 'package:coffeecard/core/data/datasources/account_remote_data_source.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; +import 'package:coffeecard/data/storage/secure_storage.dart'; +import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; +import 'package:coffeecard/models/account/authenticated_user.dart'; +import 'package:coffeecard/utils/reactivation_authenticator.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; +import 'package:get_it/get_it.dart'; +import 'package:http/http.dart' as http; +import 'package:logger/logger.dart'; +import 'package:mockito/annotations.dart'; + +import 'package:mockito/mockito.dart'; + +import 'reactivation_authenticator_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), + MockSpec(), + MockSpec(), +]) +void main() { + late _FakeGetIt serviceLocator; + late MockAuthenticationCubit authenticationCubit; + late MockAccountRemoteDataSource accountRemoteDataSource; + late MockSecureStorage secureStorage; + + late ReactivationAuthenticator authenticator; + + setUp(() { + serviceLocator = _FakeGetIt.fromMockedObjects( + mockAuthenticationCubit: MockAuthenticationCubit(), + mockAccountRemoteDataSource: MockAccountRemoteDataSource(), + mockSecureStorage: MockSecureStorage(), + mockLogger: MockLogger(), + ); + + authenticationCubit = serviceLocator.getMock(); + accountRemoteDataSource = + serviceLocator.getMock(); + secureStorage = serviceLocator.getMock(); + + authenticator = + ReactivationAuthenticator.uninitialized(serviceLocator: serviceLocator); + authenticator.initialize(accountRemoteDataSource); + }); + + test( + 'GIVEN a response with status code other than 401 ' + 'WHEN authenticate is called ' + 'THEN it should return null', + () async { + // Arrange + final request = _requestFromMethod('GET'); + final response = _responseFromStatusCode(200); + + // Act + final result = await authenticator.authenticate(request, response); + + // Assert + expect(result, isNull); + }, + ); + + test( + 'GIVEN ' + '1) a response with status code 401, ' + '2) no prior calls to authenticate, ' + 'and 3) no stored login credentials ' + 'WHEN authenticate is called ' + 'THEN it should return null', + () async { + // Arrange + final request = _requestFromMethod('GET'); + final response = _responseFromStatusCode(401); + + // Act + final result = await authenticator.authenticate(request, response); + + // Assert + expect(result, isNull); + }, + ); + + test( + 'GIVEN ' + '1) response with status code 401, ' + '2) no prior calls to authenticate, ' + 'and 3) stored login credentials that are invalid ' + 'WHEN authenticate is called ' + 'THEN ' + '1) AccountRemoteDataSource.login should be called with the stored credentials, ' + '2) AuthenticationCubit.unauthenticated should be called, ' + 'and 3) it should return null', + () async { + // Arrange + const email = 'email'; + const encodedPasscode = 'encodedPasscode'; + const token = 'token'; + const reason = 'invalid credentials'; + final loginRequest = chopper.Request( + 'method', + Uri.parse('test'), + Uri.parse('basetest'), + body: LoginDto( + email: 'email', + password: 'encodedPasscode', + version: 'verison', + ), + ); + + when(secureStorage.readEmail()).thenAnswer( + (_) async => email, + ); + when(secureStorage.readEncodedPasscode()).thenAnswer( + (_) async => encodedPasscode, + ); + when(secureStorage.getAuthenticatedUser()).thenAnswer( + (_) async => const AuthenticatedUser(email: email, token: token), + ); + when(accountRemoteDataSource.login(email, encodedPasscode)).thenAnswer( + (_) async { + // Simulate a failed login attempt through the NetworkRequestExecutor + final _ = await authenticator.authenticate( + loginRequest, + _responseFromStatusCode(401), + ); + return left(const ServerFailure(reason)); + }, + ); + + final request = _requestFromMethod('GET'); + final response = _responseFromStatusCode(401); + + // Act + final result = await authenticator.authenticate(request, response); + + // Assert + verify(accountRemoteDataSource.login(email, encodedPasscode)).called(1); + verify(authenticationCubit.unauthenticated()).called(1); + expect(result, isNull); + verifyNoMoreInteractions(accountRemoteDataSource); + verifyNoMoreInteractions(authenticationCubit); + }, + ); + + test( + 'GIVEN ' + '1) a response with status code 401, ' + '2) no prior calls to authenticate, ' + 'and 3) valid stored login credentials ' + 'WHEN authenticate is called ' + 'THEN ' + '1) AccountRemoteDataSource.login should be called with the stored credentials, ' + '2) SecureStorage.updateToken should be called, ' + 'and 3) it should return a new request with the updated token', + () async { + // Arrange + const email = 'email'; + const encodedPasscode = 'encodedPasscode'; + const oldToken = 'oldToken'; + const newToken = 'newToken'; + + when(secureStorage.readEmail()).thenAnswer( + (_) async => email, + ); + when(secureStorage.readEncodedPasscode()).thenAnswer( + (_) async => encodedPasscode, + ); + when(secureStorage.readToken()).thenAnswer( + (_) async => oldToken, + ); + + when(accountRemoteDataSource.login(email, encodedPasscode)).thenAnswer( + (_) async => right( + const AuthenticatedUser(email: email, token: newToken), + ), + ); + + final request = _requestFromMethod('GET'); + final response = _responseFromStatusCode(401); + + // Act + final result = await authenticator.authenticate(request, response); + + // Assert + verify(accountRemoteDataSource.login(email, encodedPasscode)).called(1); + verifyNoMoreInteractions(accountRemoteDataSource); + + verify(secureStorage.readEmail()).called(1); + verify(secureStorage.readEncodedPasscode()).called(1); + verify(secureStorage.updateToken(newToken)).called(1); + verifyNoMoreInteractions(secureStorage); + + expect(result, isNotNull); + expect(result!.headers['Authorization'], 'Bearer $newToken'); + }, + ); + + test( + 'GIVEN ' + '1) a response with status code 401, ' + '2) a prior call to authenticate is running, ' + 'and 3) and stored valid login credentials exist ' + 'WHEN authenticate is called ' + 'THEN it should return a new request with the updated token', + () async { + // Arrange + const email = 'email'; + const encodedPasscode = 'encodedPasscode'; + const oldToken = 'oldToken'; + + int counter = 0; + String getNewToken() => '${++counter}'; + + when(secureStorage.readEmail()).thenAnswer( + (_) async => email, + ); + when(secureStorage.readEncodedPasscode()).thenAnswer( + (_) async => encodedPasscode, + ); + when(secureStorage.readToken()).thenAnswer( + (_) async => oldToken, + ); + + when(accountRemoteDataSource.login(email, encodedPasscode)).thenAnswer( + (_) async => + right(AuthenticatedUser(email: email, token: getNewToken())), + ); + + final request = _requestFromMethod('GET'); + final response = _responseFromStatusCode(401); + + // Simulate a prior call to authenticate is running + final call1 = authenticator.authenticate(request, response); + + // Act + final call2 = authenticator.authenticate(request, response); + + // Assert + final result1 = await call1; + expect(result1, isNotNull); + expect(result1!.headers['Authorization'], 'Bearer 1'); + + // Both calls should have the same new token + final result2 = await call2; + expect(result2, isNotNull); + expect(result2!.headers['Authorization'], 'Bearer 1'); + }, + ); + + test( + 'GIVEN a response with status code 401, ' + 'and stored valid login credentials exist ' + 'WHEN authenticate is called ' + 'THEN it should return a new request with the updated token', + () async { + // Arrange + const email = 'email'; + const encodedPasscode = 'encodedPasscode'; + const newToken = 'newToken'; + + when(secureStorage.readEmail()).thenAnswer( + (_) async => email, + ); + when(secureStorage.readEncodedPasscode()).thenAnswer( + (_) async => encodedPasscode, + ); + + when(accountRemoteDataSource.login(email, encodedPasscode)).thenAnswer( + (_) async => + right(const AuthenticatedUser(email: email, token: newToken)), + ); + + final request = _requestFromMethod('GET'); + final response = _responseFromStatusCode(401); + + // Act + final result = await authenticator.authenticate(request, response); + + // Assert + expect(result, isNotNull); + expect(result!.headers['Authorization'], 'Bearer $newToken'); + }, + ); +} + +chopper.Response _responseFromStatusCode( + int statusCode, { + T? body, +}) { + return chopper.Response( + http.Response('', statusCode), + body, + ); +} + +chopper.Request _requestFromMethod(String method) { + return chopper.Request(method, Uri.parse('test'), Uri.parse('basetest')); +} + +class _FakeGetIt extends Fake implements GetIt { + _FakeGetIt.fromMockedObjects({ + required this.mockAuthenticationCubit, + required this.mockAccountRemoteDataSource, + required this.mockSecureStorage, + required this.mockLogger, + }); + + final MockAuthenticationCubit mockAuthenticationCubit; + final MockAccountRemoteDataSource mockAccountRemoteDataSource; + final MockSecureStorage mockSecureStorage; + final MockLogger mockLogger; + + @override + // We don't care about the parameter types, so we can ignore the warning + // ignore: type_annotate_public_apis + T call({String? instanceName, param1, param2, Type? type}) { + return get( + instanceName: instanceName, + param1: param1, + param2: param2, + type: type, + ); + } + + @override + // We don't care about the parameter types, so we can ignore the warning + // ignore: type_annotate_public_apis + T get({String? instanceName, param1, param2, Type? type}) { + return switch (T) { + AuthenticationCubit => mockAuthenticationCubit, + AccountRemoteDataSource => mockAccountRemoteDataSource, + SecureStorage => mockSecureStorage, + Logger => mockLogger, + _ => throw UnimplementedError('Mock for $T not implemented.'), + } as T; + } + + /// Given a mocked type, get the mocked object for the given type. + T getMock() { + return switch (T) { + MockAuthenticationCubit => mockAuthenticationCubit, + MockAccountRemoteDataSource => mockAccountRemoteDataSource, + MockSecureStorage => mockSecureStorage, + MockLogger => mockLogger, + _ => throw UnimplementedError('Mock for $T not implemented.'), + } as T; + } +} diff --git a/test/utils/throttler_test.dart b/test/utils/throttler_test.dart new file mode 100644 index 000000000..bc017cd2a --- /dev/null +++ b/test/utils/throttler_test.dart @@ -0,0 +1,101 @@ +import 'dart:async'; + +import 'package:coffeecard/utils/throttler.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:fpdart/fpdart.dart'; + +void main() { + late int counter; + late Completer completer; + + final throttler = Throttler(); + final incrementTask = Task(() async => counter = counter + 1); + + Task incrementAndWaitTask(Completer completer) { + return Task(() async { + await completer.future; + return counter = counter + 1; + }); + } + + setUp(() { + counter = 0; + completer = Completer(); + }); + + test( + 'GIVEN a Throttler with no previous calls ' + 'WHEN a task is throttled ' + 'THEN it should execute the task', + () async { + await incrementTask.runThrottled(throttler); + expect(counter, 1); + }, + ); + + test( + 'GIVEN a throttler with no previous calls ' + 'WHEN a task is throttled twice in parallel ' + 'THEN the futures returned by runThrottled should be identical', + () async { + // Throttle a task that will wait for the completer to complete, + // and then increment the counter. + final task1 = incrementAndWaitTask(completer).runThrottled(throttler); + + // Throttle a task that will increment the counter immediately. + final task2 = incrementTask.runThrottled(throttler); + + // Since task2 was run while task1 was still running, + // their futures should be identical. + expect(task1, same(task2)); + + // Allow the task to complete. + completer.complete(); + }, + ); + + test( + 'GIVEN a Throttler with an ongoing running task ' + 'WHEN an additional two tasks are throttled ' + 'THEN it should execute only the first task', + () async { + // Throttle a task that will wait for the completer to complete, + // and then increment the counter. + final task1 = incrementAndWaitTask(completer).runThrottled(throttler); + + // Throttle another task that will increment the counter immediately. + final task2 = incrementTask.runThrottled(throttler); + + // Throttle one more task that will increment the counter immediately. + final task3 = incrementTask.runThrottled(throttler); + + // Since task2 and task3 were run while task1 was still running, + // all futures should be identical. + expect([task1, task2, task3], everyElement(same(task1))); + + // Allow the tasks to complete. + completer.complete(); + await task1; + await task2; + await task3; + + expect(counter, 1); + }, + ); + + test( + 'GIVEN a Throttler with no pending tasks ' + 'WHEN a task is throttled after the previous task is completed ' + 'THEN it should execute the task', + () async { + // Throttle a task and wait for completion. + await incrementTask.runThrottled(throttler); + + // Throttle another task and wait for completion. + await incrementTask.runThrottled(throttler); + + // The counter should have incremented twice. + expect(counter, 2); + }, + ); +} From 6cdca56029646ac96a8deefcb7a70667ed0b757d Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Tue, 6 Jun 2023 18:41:10 +0200 Subject: [PATCH 29/32] test: `firebase_analytics_event_logging.dart` (#493) --- ...firebase_analytics_event_logging_test.dart | 176 ++++++++++++++++++ 1 file changed, 176 insertions(+) create mode 100644 test/utils/firebase_analytics_event_logging_test.dart diff --git a/test/utils/firebase_analytics_event_logging_test.dart b/test/utils/firebase_analytics_event_logging_test.dart new file mode 100644 index 000000000..67e92042a --- /dev/null +++ b/test/utils/firebase_analytics_event_logging_test.dart @@ -0,0 +1,176 @@ +import 'package:coffeecard/features/product/domain/entities/product.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; +import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; +import 'package:coffeecard/utils/firebase_analytics_event_logging.dart'; +import 'package:firebase_analytics/firebase_analytics.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'firebase_analytics_event_logging_test.mocks.dart'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + late FirebaseAnalyticsEventLogging eventLogging; + late MockFirebaseAnalytics mockAnalytics; + + const amount = 10; + const price = 10; + const currency = 'DKK'; + + Product productWithId(int id) { + return Product( + id: id, + name: 'Product $id', + price: price, + amount: amount, + description: '', + ); + } + + Payment paymentWithProductId(int id) { + return Payment( + id: id, + productId: 0, + productName: 'Product 0', + price: price, + deeplink: '', + purchaseTime: DateTime.now(), + status: PaymentStatus.awaitingPayment, + ); + } + + setUp(() { + mockAnalytics = MockFirebaseAnalytics(); + eventLogging = FirebaseAnalyticsEventLogging(mockAnalytics); + }); + + group('FirebaseAnalyticsEventLogging', () { + test( + 'errorEvent should log application_error event with the given error', + () { + const error = 'Test error'; + eventLogging.errorEvent(error); + + verify( + mockAnalytics.logEvent( + name: 'application_error', + parameters: {'reason': error}, + ), + ).called(1); + }, + ); + + test( + 'selectProductFromListEvent should log SelectItem event with the given parameters', + () { + const listId = '12345'; + const listName = 'Test List'; + final product = productWithId(1); + + eventLogging.selectProductFromListEvent(product, listId, listName); + + verify( + mockAnalytics.logSelectItem( + itemListId: listId, + itemListName: listName, + items: anyNamed('items'), + ), + ).called(1); + }, + ); + + test( + 'viewProductsListEvent should log ViewItemList event with the given parameters', + () { + final products = [productWithId(1), productWithId(2)]; + const listId = '12345'; + const listName = 'Test List'; + + eventLogging.viewProductsListEvent(products, listId, listName); + + verify( + mockAnalytics.logViewItemList( + itemListId: listId, + itemListName: listName, + items: anyNamed('items'), + ), + ).called(1); + }, + ); + + test( + 'viewProductEvent should log ViewItem event with the given product', + () { + final product = productWithId(1); + + eventLogging.viewProductEvent(product); + + verify( + mockAnalytics.logViewItem( + currency: currency, + value: price.toDouble(), + items: anyNamed('items'), + ), + ).called(1); + }, + ); + + test( + 'beginCheckoutEvent should log BeginCheckout event with the given product', + () { + final product = productWithId(1); + + eventLogging.beginCheckoutEvent(product); + + verify( + mockAnalytics.logBeginCheckout( + currency: currency, + value: price.toDouble(), + items: anyNamed('items'), + ), + ).called(1); + }, + ); + + test( + 'purchaseCompletedEvent should log Purchase event with the given payment', + () { + final payment = paymentWithProductId(1); + + eventLogging.purchaseCompletedEvent(payment); + + verify( + mockAnalytics.logPurchase( + currency: currency, + value: price.toDouble(), + transactionId: payment.id.toString(), + items: anyNamed('items'), + ), + ).called(1); + }, + ); + + test( + 'loginEvent should log Login event with the specified login method', + () { + eventLogging.loginEvent(); + + verify( + mockAnalytics.logLogin(loginMethod: 'UsernamePassword'), + ).called(1); + }, + ); + + test( + 'signUpEvent should log SignUp event with the specified login method', + () { + eventLogging.signUpEvent(); + + verify( + mockAnalytics.logSignUp(signUpMethod: 'UsernamePassword'), + ).called(1); + }, + ); + }); +} From ce3431557ab804c8700c1cf17c94c7f6afda071f Mon Sep 17 00:00:00 2001 From: Thomas Andersen Date: Tue, 6 Jun 2023 19:19:34 +0200 Subject: [PATCH 30/32] Reduce minimum duration for reAuth from 1s to 250ms (#497) --- lib/utils/reactivation_authenticator.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/utils/reactivation_authenticator.dart b/lib/utils/reactivation_authenticator.dart index e2a0bc238..8f156233f 100644 --- a/lib/utils/reactivation_authenticator.dart +++ b/lib/utils/reactivation_authenticator.dart @@ -67,7 +67,8 @@ class ReactivationAuthenticator extends Authenticator { // Try to refresh the token. final maybeNewToken = Task(() async { // Set a minimum duration for the token refresh to allow for throttling. - final minimumDuration = Future.delayed(const Duration(seconds: 1)); + final minimumDuration = + Future.delayed(const Duration(milliseconds: 250)); final maybeToken = await _getNewToken(request).run(); await minimumDuration; // Side effect: save the new token or evict the current token. From cd069c011ae976dbaa3d3fd5c3304b221205d956 Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Tue, 27 Jun 2023 16:03:25 +0200 Subject: [PATCH 31/32] Update README, CONTRIBUTING.md and add issue templates (#499) --- .github/ISSUE_TEMPLATE/bug-report.md | 32 +++++++++ .../ISSUE_TEMPLATE/documentation-update.md | 17 +++++ .github/ISSUE_TEMPLATE/feature-request.md | 20 ++++++ .github/ISSUE_TEMPLATE/question-discussion.md | 14 ++++ CONTRIBUTING.md | 64 +++++++++++++----- README.md | 56 +++++++++++---- readme_logo.png | Bin 0 -> 24749 bytes 7 files changed, 171 insertions(+), 32 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/documentation-update.md create mode 100644 .github/ISSUE_TEMPLATE/feature-request.md create mode 100644 .github/ISSUE_TEMPLATE/question-discussion.md create mode 100644 readme_logo.png diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 000000000..598812dca --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,32 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '' +labels: 'bug' +assignees: '' + +--- + +**Describe the bug** +A clear and concise description of what the bug is. + +**To Reproduce** +Steps to reproduce the behavior: +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Device (please complete the following information):** + - Device: [e.g. iPhone6] + - OS: [e.g. iOS8.1] + - App Version [e.g. 1.0.0] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/documentation-update.md b/.github/ISSUE_TEMPLATE/documentation-update.md new file mode 100644 index 000000000..ff8b1e52e --- /dev/null +++ b/.github/ISSUE_TEMPLATE/documentation-update.md @@ -0,0 +1,17 @@ +--- +name: Documentation update +about: Propose a documentation improvement +title: '' +labels: 'documentation' +assignees: '' + +--- + +**Describe the change** +A clear and concise description of what the change is. + +**Why is this change necessary?** +Explain why the documentation needs to be updated and how the proposed change improves it. + +**Additional context** +Add any other context or screenshots about the proposed change here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 000000000..36014cde5 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,20 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '' +labels: 'enhancement' +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/question-discussion.md b/.github/ISSUE_TEMPLATE/question-discussion.md new file mode 100644 index 000000000..5c8b2b6c4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question-discussion.md @@ -0,0 +1,14 @@ +--- +name: Question/Discussion +about: Ask a question or start a discussion +title: '' +labels: 'discuss' +assignees: '' + +--- + +**Describe the topic** +Briefly describe what you would like to discuss. + +**Additional context** +Add any other context or details here. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bb6a2711c..780957b71 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,39 +2,67 @@ We invite everyone to report issues and create pull requests to the repository. +## Issues + +Issues are used to track bugs, feature requests and other tasks related to the +project. When creating an issue, please make sure to follow the issue template +and provide as much information as possible. + +## Development + +### Prerequisites + +- [Flutter](https://flutter.dev/docs/get-started/install) +- [Dart](https://dart.dev/get-dart) + +Opening this project in VSCode will prompt you to install the recommended +extensions: + +- [Dart extension](https://marketplace.visualstudio.com/items?itemName=Dart-Code.dart-code) +- [Flutter extension](https://marketplace.visualstudio.com/items?itemName=Dart-Code.flutter) +- [bloc extension](https://marketplace.visualstudio.com/items?itemName=FelixAngelov.bloc) + ## Git branch structure -We follow a [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/) inspired setup. The -branch structure is as following : +We follow a [Git Flow](https://nvie.com/posts/a-successful-git-branching-model/) +inspired setup. The branch structure is as following: -- `develop` **(Main branch)** All new branches must check out from `develop` into feature branches, - then merged back to `develop`. -- `production` Reflects the current deployment in production. The `production` branch is merged - with `develop` every time a new version is released to `production`. -- `feature/{author}/{feature-name}` New features are developed on feature branches following the * - feature / author name / feature name branch* structure. +- `develop` **(Main branch)** All new branches must check out from `develop` + into feature branches, then merged back to `develop`. Feature branches can + have any name, but should be named after the feature they implement. +- `production` Reflects the current deployment in production. The `production` + branch is merged with `develop` every time a new version is released to + `production`. ## Merging with the `develop` branch **A pull request must be created and approved before merging with `develop`!** -We use a **rebase** strategy when merging to `develop`. When a feature has been finished in -development, the feature branch must be rebased with `develop` before creating the pull request in order to avoid -merge commits. +When a feature branch is ready to be merged with `develop`, the following steps +should be taken: + +1. Make sure the feature branch is up to date with the `develop` branch. This + can be done by merging the `develop` branch into the feature branch, or by + rebasing the feature branch on top of the `develop` branch. -A rebase from `develop` to the feature branch can be done in command line like this: +A rebase from `develop` to the feature branch can be done in command line like +this: ```bash git fetch -git checkout feature/author/feature-name # checkout the feature branch -git rebase origin/develop # rebase with remote develop branch -# resolve any conflicts -# add or stage your changes +# checkout the feature branch +git checkout feature/author/feature-name + +# rebase with remote develop branch +git rebase origin/develop + +# (resolve any conflicts if necessary) # Then commit and push to the feature branch git commit -m "New Purchase button for coffee clip cards" git push ``` -Now go to github and create a pull request from the feature branch to the develop branch. -If the pull request closes an issue, make sure to tag this issue in the description for the pull request. \ No newline at end of file +From there, the feature branch can be merged with `develop` using a pull +request. If the pull request closes an issue, the issue should be referenced in +the pull request description using the `Closes #issue-number` syntax. diff --git a/README.md b/README.md index 1929d2dfa..5d8aed50a 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,57 @@ -# Coffee card App [in development] +

+ Coffee Card App Logo +

-![Flutter build and test](https://github.com/AnalogIO/coffeecard_app/workflows/Flutter%20build%20and%20test/badge.svg) [![codecov](https://codecov.io/gh/AnalogIO/coffeecard_app/branch/develop/graph/badge.svg)](https://codecov.io/gh/AnalogIO/coffeecard_app) +

+ + Build and test + + + codecov + + + License: MIT + +

-**Contact** AnalogIO at *support [at] analogio.dk* +--- -We are rewriting our Coffee card app for Cafe Analog to a new cross platform one in Flutter. The app -allows users to buy and use clip cards in Cafe Analog @ IT University of Copenhagen. +

+ Brew the best coffee experience at Cafe Analog with our new cross-platform app, designed and coded in Flutter. + Buy and use digital clip cards at our cafe located at the IT University of Copenhagen. +

-## SDKs +

+ Contact AnalogIO at: support [at] analogio.dk +

-We are building the Flutter app with these SDK versions +--- + +## 🛠️ SDKs + +The application is brewed using these SDK versions: | SDK | Version | | ------- | -------------- | | Dart | >=3.0.0 <4.0.0 | | Flutter | 3.10.2 | -## Relevant READMEs +## 📚 Documentation + +For more details, please refer to our documentation: + +- [Contribution Guidelines](CONTRIBUTING.md) +- [Testing Guidelines](test/README.md) +- [Generated CoffeeCard API](lib/data/api/README.md) + +## 🔧 Getting Started + +This project relies on autogenerated files for environment configuration and testing. Follow the steps below to get started: -- [Contribution guidelines](CONTRIBUTING.md) `CONTRIBUTING.md` -- [Testing guidelines](test/README.md) `test/README.md` -- [Generated CoffeeCardApi](lib/data/api/README.md) `lib/persistence/api/README.md` +1. Ensure a `.env.develop` file is present in the root directory. This file should contain the URI of the backend API in the format `coffeeCardUrl="https://the-url"`. -## Getting started +2. Run the command `make generate` to generate necessary files. -This project relies on autogenerated files for environment configuration and testing. To generate these files run `make generate`. +## 📬 Contact -A **.env.develop** should be present in the root directory before running code generation. This file should contain the URI of the backend API in the format `coffeeCardUrl="https://the-url"`. +For any queries, feel free to reach us at: support [at] analogio.dk diff --git a/readme_logo.png b/readme_logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d8bd51b94286325b5a304b3b1c9d453c43078a6f GIT binary patch literal 24749 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�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 Date: Tue, 27 Jun 2023 16:22:29 +0200 Subject: [PATCH 32/32] Add new mobilepay deep links depending on environment (#496) --- .vscode/launch.json | 13 +++++++++++++ android/app/build.gradle | 2 ++ android/app/src/main/AndroidManifest.xml | 2 +- ios/Runner.xcodeproj/project.pbxproj | 5 +++++ ios/Runner/Info.plist | 2 +- 5 files changed, 22 insertions(+), 2 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index aafb39036..2666d5e05 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -17,6 +17,19 @@ "lib/main_development.dart" ] }, + { + "name": "Development flavor (profile mode)", + "request": "launch", + "type": "dart", + "flutterMode": "profile", + "program": "lib/main_development.dart", + "args": [ + "--flavor", + "development", + "--target", + "lib/main_development.dart" + ] + }, { "name": "Production flavor (debug mode)", "request": "launch", diff --git a/android/app/build.gradle b/android/app/build.gradle index 064424146..fb93deb47 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -72,6 +72,7 @@ android { applicationId "dk.analogio.analog.dev" versionNameSuffix "-dev" resValue "string", "app_name", "[DEV] Analog" + resValue "string", "app_deep_link", "analogcoffeecard-dev" } production { dimension "app" @@ -79,6 +80,7 @@ android { applicationIdSuffix "" versionNameSuffix "-prod" resValue "string", "app_name", "Analog" + resValue "string", "app_deep_link", "analogcoffeecard" } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 934865073..67cc78670 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -21,7 +21,7 @@ - +