From cbb4ff447210b52394e26379d0b5de93421a64f7 Mon Sep 17 00:00:00 2001 From: Frederik Martini <39860858+fremartini@users.noreply.github.com> Date: Tue, 14 Feb 2023 19:56:18 +0100 Subject: [PATCH] Hide open indicator on API error; Move opening hours code into feature (Clean Architecture); Use dartz package for Either class (#394) Co-authored-by: Omid Marfavi <21163286+marfavi@users.noreply.github.com> --- lib/core/errors/exceptions.dart | 11 ++ lib/core/errors/failures.dart | 19 ++ lib/core/usecases/usecase.dart | 12 ++ lib/cubits/contributor/contributor_cubit.dart | 4 +- lib/cubits/environment/environment_cubit.dart | 2 +- lib/cubits/form/form_bloc.dart | 32 ++-- lib/cubits/login/login_cubit.dart | 2 +- lib/cubits/occupation/occupation_cubit.dart | 2 +- .../opening_hours/opening_hours_cubit.dart | 29 --- lib/cubits/products/products_cubit.dart | 2 +- lib/cubits/purchase/purchase_cubit.dart | 66 +++---- lib/cubits/receipt/receipt_cubit.dart | 2 +- lib/cubits/register/register_cubit.dart | 4 +- lib/cubits/statistics/statistics_cubit.dart | 79 ++++---- lib/cubits/tickets/tickets_cubit.dart | 5 +- lib/cubits/user/user_cubit.dart | 41 ++-- lib/cubits/voucher/voucher_cubit.dart | 9 +- .../external/contributor_repository.dart | 7 +- .../shared/account_repository.dart | 2 +- .../opening_hours_repository.dart | 62 ------ lib/data/repositories/utils/executor.dart | 3 +- .../v1/occupation_repository.dart | 2 +- .../repositories/v1/product_repository.dart | 2 +- .../repositories/v1/receipt_repository.dart | 29 +-- .../repositories/v1/ticket_repository.dart | 2 +- .../repositories/v1/voucher_repository.dart | 2 +- .../v2/app_config_repository.dart | 2 +- .../v2/leaderboard_repository.dart | 2 +- .../repositories/v2/purchase_repository.dart | 2 +- .../opening_hours_remote_data_source.dart | 44 +++++ .../opening_hours_repository_impl.dart | 89 +++++++++ .../domain/entities/opening_hours.dart | 14 ++ .../opening_hours_repository.dart | 8 + .../domain/usecases/check_open_status.dart | 15 ++ .../domain/usecases/get_opening_hours.dart | 16 ++ lib/features/opening_hours/opening_hours.dart | 7 + .../cubit/opening_hours_cubit.dart | 38 ++++ .../cubit}/opening_hours_state.dart | 18 +- .../pages}/opening_hours_page.dart | 2 +- .../widgets}/opening_hours_indicator.dart | 7 +- lib/payment/mobilepay_service.dart | 6 +- lib/payment/payment_handler.dart | 2 +- lib/service_locator.dart | 41 +++- lib/utils/either.dart | 29 --- lib/utils/input_validator.dart | 2 +- lib/utils/reactivation_authenticator.dart | 20 +- .../forgot_passcode/forgot_passcode_form.dart | 30 +-- .../components/forms/form_text_field.dart | 2 +- .../forms/register/register_email_form.dart | 14 +- .../forms/settings/change_email_form.dart | 14 +- lib/widgets/pages/home_page.dart | 7 +- lib/widgets/pages/settings/settings_page.dart | 54 ++---- lib/widgets/pages/tickets/tickets_page.dart | 2 +- pubspec.lock | 7 + pubspec.yaml | 3 + .../contributor/contributor_cubit_test.dart | 2 +- .../environment/environment_cubit_test.dart | 2 +- test/cubits/login/login_cubit_test.dart | 2 +- .../opening_hours_cubit_test.dart | 82 -------- test/cubits/products/products_cubit_test.dart | 2 +- test/cubits/receipt/receipt_cubit_test.dart | 2 +- .../statistics/statistics_cubit_test.dart | 2 +- test/cubits/tickets/tickets_cubit_test.dart | 2 +- .../account_repository_test.dart | 7 +- .../ticket_repository_test.dart | 8 +- ...opening_hours_remote_data_source_test.dart | 85 +++++++++ .../opening_hours_repository_impl_test.dart | 176 ++++++++++++++++++ .../usecases/check_open_status_test.dart | 31 +++ .../usecases/get_opening_hours_test.dart | 38 ++++ .../cubit/opening_hours_cubit_test.dart | 88 +++++++++ test/response_factory.dart | 11 ++ 71 files changed, 1004 insertions(+), 465 deletions(-) create mode 100644 lib/core/errors/exceptions.dart create mode 100644 lib/core/errors/failures.dart create mode 100644 lib/core/usecases/usecase.dart delete mode 100644 lib/cubits/opening_hours/opening_hours_cubit.dart delete mode 100644 lib/data/repositories/shiftplanning/opening_hours_repository.dart create mode 100644 lib/features/opening_hours/data/datasources/opening_hours_remote_data_source.dart create mode 100644 lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart create mode 100644 lib/features/opening_hours/domain/entities/opening_hours.dart create mode 100644 lib/features/opening_hours/domain/repositories/opening_hours_repository.dart create mode 100644 lib/features/opening_hours/domain/usecases/check_open_status.dart create mode 100644 lib/features/opening_hours/domain/usecases/get_opening_hours.dart create mode 100644 lib/features/opening_hours/opening_hours.dart create mode 100644 lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart rename lib/{cubits/opening_hours => features/opening_hours/presentation/cubit}/opening_hours_state.dart (67%) rename lib/{widgets/pages/settings => features/opening_hours/presentation/pages}/opening_hours_page.dart (97%) rename lib/{widgets/components/tickets => features/opening_hours/presentation/widgets}/opening_hours_indicator.dart (90%) delete mode 100644 lib/utils/either.dart delete mode 100644 test/cubits/opening_hours/opening_hours_cubit_test.dart create mode 100644 test/features/opening_hours/data/datasources/opening_hours_remote_data_source_test.dart create mode 100644 test/features/opening_hours/data/repositories/opening_hours_repository_impl_test.dart create mode 100644 test/features/opening_hours/domain/usecases/check_open_status_test.dart create mode 100644 test/features/opening_hours/domain/usecases/get_opening_hours_test.dart create mode 100644 test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart create mode 100644 test/response_factory.dart diff --git a/lib/core/errors/exceptions.dart b/lib/core/errors/exceptions.dart new file mode 100644 index 000000000..3d1a9c98c --- /dev/null +++ b/lib/core/errors/exceptions.dart @@ -0,0 +1,11 @@ +import 'package:chopper/chopper.dart'; + +class ServerException implements Exception { + final String error; + + ServerException({required this.error}); + + ServerException.fromResponse(Response response) : error = response.bodyString; +} + +class LocalStorageException implements Exception {} diff --git a/lib/core/errors/failures.dart b/lib/core/errors/failures.dart new file mode 100644 index 000000000..f57aaaae3 --- /dev/null +++ b/lib/core/errors/failures.dart @@ -0,0 +1,19 @@ +import 'package:equatable/equatable.dart'; + +abstract class Failure extends Equatable { + final String reason; + + const Failure(this.reason); + + @override + List get props => [reason]; +} + +// General failures +class ServerFailure extends Failure { + const ServerFailure(super.reason); +} + +class LocalStorageFailure extends Failure { + const LocalStorageFailure(super.reason); +} diff --git a/lib/core/usecases/usecase.dart b/lib/core/usecases/usecase.dart new file mode 100644 index 000000000..9d839e2c3 --- /dev/null +++ b/lib/core/usecases/usecase.dart @@ -0,0 +1,12 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:dartz/dartz.dart'; +import 'package:equatable/equatable.dart'; + +abstract class UseCase { + Future> call(Params params); +} + +class NoParams extends Equatable { + @override + List get props => []; +} diff --git a/lib/cubits/contributor/contributor_cubit.dart b/lib/cubits/contributor/contributor_cubit.dart index eb7a52cb6..992a3c679 100644 --- a/lib/cubits/contributor/contributor_cubit.dart +++ b/lib/cubits/contributor/contributor_cubit.dart @@ -11,8 +11,10 @@ class ContributorCubit extends Cubit { Future getContributors() async { emit(const ContributorLoading()); + final either = await _repository.getContributors(); - either.caseOf( + + either.fold( (error) => emit(ContributorError(error.message)), (contributors) => emit(ContributorLoaded(contributors)), ); diff --git a/lib/cubits/environment/environment_cubit.dart b/lib/cubits/environment/environment_cubit.dart index 79e265b76..d833601b7 100644 --- a/lib/cubits/environment/environment_cubit.dart +++ b/lib/cubits/environment/environment_cubit.dart @@ -13,7 +13,7 @@ class EnvironmentCubit extends Cubit { Future getConfig() async { final either = await _configRepository.getEnvironmentType(); - either.caseOf( + either.fold( (error) => emit(EnvironmentError(error.message)), (env) => emit(EnvironmentLoaded(env: env)), ); diff --git a/lib/cubits/form/form_bloc.dart b/lib/cubits/form/form_bloc.dart index 5a0607b94..406ed2296 100644 --- a/lib/cubits/form/form_bloc.dart +++ b/lib/cubits/form/form_bloc.dart @@ -1,7 +1,7 @@ import 'package:bloc/bloc.dart'; import 'package:coffeecard/utils/debouncing.dart'; -import 'package:coffeecard/utils/either.dart'; import 'package:coffeecard/utils/input_validator.dart'; +import 'package:dartz/dartz.dart'; import 'package:equatable/equatable.dart'; part 'form_event.dart'; @@ -18,27 +18,27 @@ class FormBloc extends Bloc { (event, emit) async { final text = event.input.trim(); for (final validator in validators) { - final result = await validator.validate(text); - if (result.isLeft) { - emit( + final either = await validator.validate(text); + + either.fold( + (errorMessage) => emit( state.copyWith( loading: false, canSubmit: false, - error: Left(result.left), + error: Left(errorMessage), shouldDisplayError: validator.forceErrorMessage ? true : null, ), - ); - return; - } + ), + (_) => emit( + state.copyWith( + loading: false, + text: text, + canSubmit: true, + error: const Right(null), + ), + ), + ); } - emit( - state.copyWith( - loading: false, - text: text, - canSubmit: true, - error: const Right(null), - ), - ); }, transformer: debounce ? debouncing() : null, ); diff --git a/lib/cubits/login/login_cubit.dart b/lib/cubits/login/login_cubit.dart index 7631cc5ec..beb520be0 100644 --- a/lib/cubits/login/login_cubit.dart +++ b/lib/cubits/login/login_cubit.dart @@ -48,7 +48,7 @@ class LoginCubit extends Cubit { final either = await accountRepository.login(email, encodedPasscode); - either.caseOf( + either.fold( (error) => emit(LoginError(formatErrorMessage(error.message))), (user) { sl().loginEvent(); diff --git a/lib/cubits/occupation/occupation_cubit.dart b/lib/cubits/occupation/occupation_cubit.dart index bfd61bd17..28c3dd0b0 100644 --- a/lib/cubits/occupation/occupation_cubit.dart +++ b/lib/cubits/occupation/occupation_cubit.dart @@ -14,7 +14,7 @@ class OccupationCubit extends Cubit { Future getOccupations() async { final either = await occupationRepository.getOccupations(); - either.caseOf( + either.fold( (error) => emit(OccupationError(error.message)), (occupations) => emit(OccupationLoaded(occupations: occupations)), ); diff --git a/lib/cubits/opening_hours/opening_hours_cubit.dart b/lib/cubits/opening_hours/opening_hours_cubit.dart deleted file mode 100644 index 40f2696d6..000000000 --- a/lib/cubits/opening_hours/opening_hours_cubit.dart +++ /dev/null @@ -1,29 +0,0 @@ -import 'package:bloc/bloc.dart'; -import 'package:coffeecard/data/repositories/shiftplanning/opening_hours_repository.dart'; -import 'package:equatable/equatable.dart'; - -part 'opening_hours_state.dart'; - -class OpeningHoursCubit extends Cubit { - OpeningHoursCubit(this._repo) : super(const OpeningHoursLoading()); - final OpeningHoursRepository _repo; - - Future getOpeninghours() async { - emit(const OpeningHoursLoading()); - final isOpenResult = await _repo.isOpen(); - final openingHoursResult = await _repo.getOpeningHours(); - - if (isOpenResult.isLeft) { - emit(OpeningHoursError(isOpenResult.left.message)); - } else if (openingHoursResult.isLeft) { - emit(OpeningHoursError(openingHoursResult.left.message)); - } else { - emit( - OpeningHoursLoaded( - isOpen: isOpenResult.right, - openingHours: openingHoursResult.right, - ), - ); - } - } -} diff --git a/lib/cubits/products/products_cubit.dart b/lib/cubits/products/products_cubit.dart index c4593e9f9..e6b295491 100644 --- a/lib/cubits/products/products_cubit.dart +++ b/lib/cubits/products/products_cubit.dart @@ -14,7 +14,7 @@ class ProductsCubit extends Cubit { emit(const ProductsLoading()); final either = await _repository.getProducts(); - either.caseOf( + either.fold( (error) => emit(ProductsError(error.message)), (products) { final ticketProducts = products.where((p) => p.amount > 1); diff --git a/lib/cubits/purchase/purchase_cubit.dart b/lib/cubits/purchase/purchase_cubit.dart index 9cfe98030..cc9b126b9 100644 --- a/lib/cubits/purchase/purchase_cubit.dart +++ b/lib/cubits/purchase/purchase_cubit.dart @@ -22,18 +22,19 @@ class PurchaseCubit extends Cubit { emit(const PurchaseStarted()); final either = await paymentHandler.initPurchase(product.id); - if (either.isRight) { - final Payment payment = either.right; - if (payment.status != PaymentStatus.error) { - emit(PurchaseProcessing(payment)); - await paymentHandler.invokePaymentMethod(Uri.parse(payment.deeplink)); - } else { - emit(PurchasePaymentRejected(payment)); - } - } else { - emit(PurchaseError(either.left.message)); - } + either.fold( + (error) => emit(PurchaseError(error.message)), + (payment) async { + if (payment.status != PaymentStatus.error) { + emit(PurchaseProcessing(payment)); + await paymentHandler + .invokePaymentMethod(Uri.parse(payment.deeplink)); + } else { + emit(PurchasePaymentRejected(payment)); + } + }, + ); } } @@ -45,28 +46,29 @@ class PurchaseCubit extends Cubit { emit(PurchaseVerifying(payment)); final either = await paymentHandler.verifyPurchase(payment.id); - either.caseOf((error) { - emit(PurchaseError(either.left.message)); - }, (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 + either.fold( + (error) => emit(PurchaseError(error.message)), + (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))); - } - }); + // 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/cubits/receipt/receipt_cubit.dart b/lib/cubits/receipt/receipt_cubit.dart index 4ef200a1d..63e79bf7f 100644 --- a/lib/cubits/receipt/receipt_cubit.dart +++ b/lib/cubits/receipt/receipt_cubit.dart @@ -14,7 +14,7 @@ class ReceiptCubit extends Cubit { Future fetchReceipts() async { final either = await _repository.getUserReceipts(); - either.caseOf( + either.fold( (error) => emit( state.copyWith( status: ReceiptStatus.failure, diff --git a/lib/cubits/register/register_cubit.dart b/lib/cubits/register/register_cubit.dart index ec25f78cf..9aeb39a96 100644 --- a/lib/cubits/register/register_cubit.dart +++ b/lib/cubits/register/register_cubit.dart @@ -24,9 +24,9 @@ class RegisterCubit extends Cubit { occupationId, ); - either.caseOf( + either.fold( (error) => emit(RegisterError(error.message)), - (user) { + (_) { emit(RegisterSuccess()); sl().signUpEvent(); }, diff --git a/lib/cubits/statistics/statistics_cubit.dart b/lib/cubits/statistics/statistics_cubit.dart index 70d1b49a1..e0c75badd 100644 --- a/lib/cubits/statistics/statistics_cubit.dart +++ b/lib/cubits/statistics/statistics_cubit.dart @@ -19,52 +19,51 @@ class LeaderboardCubit extends Cubit { Future fetch() async { final filter = state.filter; - final maybeLeaderboard = await _repo.getLeaderboard(filter); final maybeUser = await _repo.getLeaderboardUser(filter); - if (maybeUser.isLeft) { - emit(StatisticsError(maybeUser.left.message, filter: filter)); - return; - } + maybeUser.fold( + (l) => emit(StatisticsError(l.message, filter: filter)), + (user) async { + final maybeLeaderboard = await _repo.getLeaderboard(filter); - if (maybeLeaderboard.isLeft) { - emit(StatisticsError(maybeLeaderboard.left.message, filter: filter)); - return; - } + maybeLeaderboard.fold( + (l) => emit(StatisticsError(l.message, filter: filter)), + (leaderboard) { + var userInLeaderboard = false; + final List users = + leaderboard.map((leaderboardUser) { + final isCurrentUser = leaderboardUser.id == user.id; - final user = maybeUser.right; + // set the 'found' flag if this is the current user + if (!userInLeaderboard && isCurrentUser) { + userInLeaderboard = true; + } - var userInLeaderboard = false; - final List leaderboard = - maybeLeaderboard.right.map((leaderboardUser) { - final isCurrentUser = leaderboardUser.id == user.id; + return LeaderboardUser( + id: leaderboardUser.id, + name: leaderboardUser.name, + score: leaderboardUser.score, + highlight: isCurrentUser, + rank: leaderboardUser.rank, + ); + }).toList(); - // set the 'found' flag if this is the current user - if (!userInLeaderboard && isCurrentUser) { - userInLeaderboard = true; - } + if (!userInLeaderboard) { + users.add( + LeaderboardUser( + id: user.id, + name: user.name, + highlight: true, + score: user.score, + rank: user.rank, + ), + ); + } - return LeaderboardUser( - id: leaderboardUser.id, - name: leaderboardUser.name, - score: leaderboardUser.score, - highlight: isCurrentUser, - rank: leaderboardUser.rank, - ); - }).toList(); - - if (!userInLeaderboard) { - leaderboard.add( - LeaderboardUser( - id: user.id, - name: user.name, - highlight: true, - score: user.score, - rank: user.rank, - ), - ); - } - - emit(StatisticsLoaded(leaderboard, filter: filter)); + emit(StatisticsLoaded(users, filter: filter)); + }, + ); + }, + ); } } diff --git a/lib/cubits/tickets/tickets_cubit.dart b/lib/cubits/tickets/tickets_cubit.dart index 216368daa..ccf79b81b 100644 --- a/lib/cubits/tickets/tickets_cubit.dart +++ b/lib/cubits/tickets/tickets_cubit.dart @@ -23,7 +23,7 @@ class TicketsCubit extends Cubit { emit(TicketUsing(st.tickets)); final either = await _ticketRepository.useTicket(productId); - either.caseOf( + either.fold( (error) => emit(TicketsUseError(error.message)), (receipt) => emit(TicketUsed(receipt, st.tickets)), ); @@ -32,7 +32,8 @@ class TicketsCubit extends Cubit { Future refreshTickets() async { final either = await _ticketRepository.getUserTickets(); - either.caseOf( + + either.fold( (error) => emit(TicketsLoadError(error.message)), (tickets) => emit(TicketsLoaded(tickets)), ); diff --git a/lib/cubits/user/user_cubit.dart b/lib/cubits/user/user_cubit.dart index cb655ffdd..209db1ebe 100644 --- a/lib/cubits/user/user_cubit.dart +++ b/lib/cubits/user/user_cubit.dart @@ -24,11 +24,12 @@ class UserCubit extends Cubit { Future refreshUserDetails() async { final either = await _accountRepository.getUser(); - if (either.isRight) { - _enrichUserWithOccupations(either.right); - } else { - emit(UserError(either.left.message)); - } + either.fold( + (l) => emit(UserError(l.message)), + (r) { + _enrichUserWithOccupations(r); + }, + ); } Future _updateUser(UpdateUser user) async { @@ -45,32 +46,32 @@ class UserCubit extends Cubit { final either = await _accountRepository.updateUser(user); - either.caseOf((error) { - emit(UserError(either.left.message)); - }, (user) async { - // Refreshes twice as a work-around for - // a backend bug that returns a user object with all ranks set to 0. - await _enrichUserWithOccupations(either.right); + either.fold( + (l) => emit(UserError(l.message)), + (user) async { + // Refreshes twice as a work-around for + // a backend bug that returns a user object with all ranks set to 0. + await _enrichUserWithOccupations(user); - // TODO(marfavi): remove fetchUserDetails when backend bug is fixed, https://github.com/AnalogIO/coffeecard_app/issues/378 - fetchUserDetails(); - }); + // TODO(marfavi): remove fetchUserDetails when backend bug is fixed, https://github.com/AnalogIO/coffeecard_app/issues/378 + fetchUserDetails(); + }, + ); } Future _enrichUserWithOccupations(User user) async { - final List occupations; + List occupations = []; if (state is UserUpdating) { occupations = (state as UserUpdating).occupations; } else if (state is UserLoaded) { occupations = (state as UserLoaded).occupations; } else { - // Fetches occupation info, if we have not cached it beforehand + // Fetches the programme info, if we have not cached it beforehand final either = await _occupationRepository.getOccupations(); - if (either.isRight) { - occupations = either.right; - } else { - emit(UserError(either.left.message)); + either.fold((l) => emit(UserError(l.message)), (r) => occupations = r); + + if (either.isLeft()) { return; } } diff --git a/lib/cubits/voucher/voucher_cubit.dart b/lib/cubits/voucher/voucher_cubit.dart index 834f0e5d2..89ca7c4b1 100644 --- a/lib/cubits/voucher/voucher_cubit.dart +++ b/lib/cubits/voucher/voucher_cubit.dart @@ -14,10 +14,9 @@ class VoucherCubit extends Cubit { emit(VoucherLoading()); final either = await _voucherRepository.redeemVoucher(voucher); - if (either.isRight) { - emit(VoucherSuccess(either.right)); - } else { - emit(VoucherError(either.left.message)); - } + either.fold( + (l) => emit(VoucherError(l.message)), + (r) => emit(VoucherSuccess(r)), + ); } } diff --git a/lib/data/repositories/external/contributor_repository.dart b/lib/data/repositories/external/contributor_repository.dart index 3eb6f75a3..0d9f3607a 100644 --- a/lib/data/repositories/external/contributor_repository.dart +++ b/lib/data/repositories/external/contributor_repository.dart @@ -1,6 +1,6 @@ import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/models/contributor.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; class ContributorRepository { Future>> getContributors() async { @@ -21,11 +21,6 @@ class ContributorRepository { githubUrl: 'https://github.com/fredpetersen', avatarUrl: 'https://avatars.githubusercontent.com/u/43568735?v=4', ), - Contributor( - name: 'Hjalte Sorgenfrei Mac Dalland', - githubUrl: 'https://github.com/Hjaltesorgenfrei', - avatarUrl: 'https://avatars.githubusercontent.com/u/8939023?v=4', - ), Contributor( name: 'Jonas Anker Rasmussen', githubUrl: 'https://github.com/jonasanker', diff --git a/lib/data/repositories/shared/account_repository.dart b/lib/data/repositories/shared/account_repository.dart index 20e5558de..ec4046c69 100644 --- a/lib/data/repositories/shared/account_repository.dart +++ b/lib/data/repositories/shared/account_repository.dart @@ -7,7 +7,7 @@ import 'package:coffeecard/models/account/authenticated_user.dart'; import 'package:coffeecard/models/account/update_user.dart'; import 'package:coffeecard/models/account/user.dart'; import 'package:coffeecard/utils/api_uri_constants.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; class AccountRepository { AccountRepository({ diff --git a/lib/data/repositories/shiftplanning/opening_hours_repository.dart b/lib/data/repositories/shiftplanning/opening_hours_repository.dart deleted file mode 100644 index 03be31f9a..000000000 --- a/lib/data/repositories/shiftplanning/opening_hours_repository.dart +++ /dev/null @@ -1,62 +0,0 @@ -import 'package:coffeecard/base/strings.dart'; -import 'package:coffeecard/data/repositories/utils/executor.dart'; -import 'package:coffeecard/data/repositories/utils/request_types.dart'; -import 'package:coffeecard/generated/api/shiftplanning_api.swagger.dart'; -import 'package:coffeecard/models/opening_hours_day.dart'; -import 'package:coffeecard/utils/either.dart'; -import 'package:collection/collection.dart'; - -class OpeningHoursRepository { - OpeningHoursRepository({ - required this.api, - required this.executor, - }); - - final ShiftplanningApi api; - final Executor executor; - - Future> isOpen() async { - return executor.execute( - () => api.apiOpenShortKeyGet(shortKey: 'analog'), - (dto) => dto.open, - ); - } - - Future>> getOpeningHours() async { - return executor.execute( - () => api.apiShiftsShortKeyGet(shortKey: 'analog'), - _transformOpeningHours, - ); - } - - Map _transformOpeningHours(List dtoList) { - final content = dtoList..sortBy((dto) => dto.start); - - final openingHoursPerWeekday = - groupBy(content, (dto) => dto.start.weekday); - - // create map associating each weekday to its opening hours: - // { - // 0: 8 - 16, - // 1: 8 - 16, ... - // } - final weekDayOpeningHours = openingHoursPerWeekday.map( - (day, value) => MapEntry( - day, - OpeningHoursDay(value.first.start, value.last.end).toString(), - ), - ); - - // closed string is not capitalized - var closed = Strings.closed; - closed = closed.replaceFirst(closed[0], closed[0].toUpperCase()); - - // the previous map only contains weekdays, mark weekends as closed - weekDayOpeningHours.addAll({ - DateTime.saturday: closed, - DateTime.sunday: closed, - }); - - return weekDayOpeningHours; - } -} diff --git a/lib/data/repositories/utils/executor.dart b/lib/data/repositories/utils/executor.dart index 2413b7f6b..251046c8f 100644 --- a/lib/data/repositories/utils/executor.dart +++ b/lib/data/repositories/utils/executor.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:chopper/chopper.dart' show Response; import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; import 'package:http/http.dart' show ClientException; import 'package:logger/logger.dart'; @@ -25,7 +25,6 @@ class Executor { if (response.isSuccessful) { return Right(transformer(response.body as Dto)); } else { - logger.e('API failure: (${response.statusCode}) ${response.error}'); return Left(RequestHttpFailure.fromResponse(response)); } } on SocketException catch (e) { diff --git a/lib/data/repositories/v1/occupation_repository.dart b/lib/data/repositories/v1/occupation_repository.dart index bc4bbe8d7..e26b4f4fe 100644 --- a/lib/data/repositories/v1/occupation_repository.dart +++ b/lib/data/repositories/v1/occupation_repository.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/data/repositories/utils/executor.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/models/occupation.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; class OccupationRepository { OccupationRepository({ diff --git a/lib/data/repositories/v1/product_repository.dart b/lib/data/repositories/v1/product_repository.dart index 5dbe6d5e1..f30d86233 100644 --- a/lib/data/repositories/v1/product_repository.dart +++ b/lib/data/repositories/v1/product_repository.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/data/repositories/utils/executor.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/models/ticket/product.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; class ProductRepository { ProductRepository({ diff --git a/lib/data/repositories/v1/receipt_repository.dart b/lib/data/repositories/v1/receipt_repository.dart index 4c478f7f5..8104cc294 100644 --- a/lib/data/repositories/v1/receipt_repository.dart +++ b/lib/data/repositories/v1/receipt_repository.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/data/repositories/utils/executor.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/models/receipts/receipt.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; class ReceiptRepository { ReceiptRepository({ @@ -26,18 +26,21 @@ class ReceiptRepository { ); final usedTicketsEither = await usedTicketsFutureEither; - final purchasedTicketsEither = await purchasedTicketsFutureEither; - if (usedTicketsEither.isLeft) { - return Left(usedTicketsEither.left); - } else if (purchasedTicketsEither.isLeft) { - return Left(purchasedTicketsEither.left); - } else { - final usedTickets = usedTicketsEither.right; - final purchasedTickets = purchasedTicketsEither.right; - final allTickets = [...usedTickets, ...purchasedTickets]; - allTickets.sort((a, b) => b.timeUsed.compareTo(a.timeUsed)); - return Right(allTickets); - } + return usedTicketsEither.fold( + (l) => Left(l), + (usedTickets) async { + final purchasedTicketsEither = await purchasedTicketsFutureEither; + + return purchasedTicketsEither.fold( + (l) => Left(l), + (purchasedTickets) { + final allTickets = [...usedTickets, ...purchasedTickets]; + allTickets.sort((a, b) => b.timeUsed.compareTo(a.timeUsed)); + return Right(allTickets); + }, + ); + }, + ); } } diff --git a/lib/data/repositories/v1/ticket_repository.dart b/lib/data/repositories/v1/ticket_repository.dart index b490f8464..1dbd1b755 100644 --- a/lib/data/repositories/v1/ticket_repository.dart +++ b/lib/data/repositories/v1/ticket_repository.dart @@ -4,8 +4,8 @@ import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:coffeecard/models/receipts/receipt.dart'; import 'package:coffeecard/models/ticket/ticket_count.dart'; -import 'package:coffeecard/utils/either.dart'; import 'package:collection/collection.dart'; +import 'package:dartz/dartz.dart'; class TicketRepository { TicketRepository({ diff --git a/lib/data/repositories/v1/voucher_repository.dart b/lib/data/repositories/v1/voucher_repository.dart index 7353e9198..a0cc3287c 100644 --- a/lib/data/repositories/v1/voucher_repository.dart +++ b/lib/data/repositories/v1/voucher_repository.dart @@ -2,7 +2,7 @@ import 'package:coffeecard/data/repositories/utils/executor.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/models/voucher/redeemed_voucher.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.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 cee330842..47bd18686 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/data/repositories/utils/executor.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:coffeecard/models/environment.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; class AppConfigRepository { AppConfigRepository({ diff --git a/lib/data/repositories/v2/leaderboard_repository.dart b/lib/data/repositories/v2/leaderboard_repository.dart index 3a8109bb0..608d161ee 100644 --- a/lib/data/repositories/v2/leaderboard_repository.dart +++ b/lib/data/repositories/v2/leaderboard_repository.dart @@ -3,7 +3,7 @@ import 'package:coffeecard/data/repositories/utils/executor.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:coffeecard/models/leaderboard/leaderboard_user.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.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 86b91ee14..f68b72670 100644 --- a/lib/data/repositories/v2/purchase_repository.dart +++ b/lib/data/repositories/v2/purchase_repository.dart @@ -3,7 +3,7 @@ import 'package:coffeecard/data/repositories/utils/request_types.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:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; class PurchaseRepository { PurchaseRepository({ 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 new file mode 100644 index 000000000..38efd68c8 --- /dev/null +++ b/lib/features/opening_hours/data/datasources/opening_hours_remote_data_source.dart @@ -0,0 +1,44 @@ +import 'package:coffeecard/core/errors/exceptions.dart'; +import 'package:coffeecard/generated/api/shiftplanning_api.swagger.dart'; + +abstract class OpeningHoursRemoteDataSource { + /// Check if the cafe is open. + /// + /// Throws a [ServerException] if the api call failed. + Future isOpen(); + + /// Get the opening hours of the cafe. + /// + /// Throws a [ServerException] if the api call failed. + Future> getOpeningHours(); +} + +class OpeningHoursRemoteDataSourceImpl implements OpeningHoursRemoteDataSource { + final ShiftplanningApi api; + + OpeningHoursRemoteDataSourceImpl({required this.api}); + + final shortkey = 'analog'; + + @override + Future isOpen() async { + final response = await api.apiOpenShortKeyGet(shortKey: shortkey); + + if (!response.isSuccessful) { + throw ServerException.fromResponse(response); + } + + return response.body!.open; + } + + @override + Future> getOpeningHours() async { + final response = await api.apiShiftsShortKeyGet(shortKey: shortkey); + + if (!response.isSuccessful) { + throw ServerException.fromResponse(response); + } + + return response.body!; + } +} 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 new file mode 100644 index 000000000..40fbc7cf3 --- /dev/null +++ b/lib/features/opening_hours/data/repositories/opening_hours_repository_impl.dart @@ -0,0 +1,89 @@ +import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/core/errors/exceptions.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:coffeecard/generated/api/shiftplanning_api.swagger.dart'; +import 'package:coffeecard/models/opening_hours_day.dart'; +import 'package:collection/collection.dart'; +import 'package:dartz/dartz.dart'; + +class OpeningHoursRepositoryImpl implements OpeningHoursRepository { + final OpeningHoursRemoteDataSource dataSource; + + OpeningHoursRepositoryImpl({required this.dataSource}); + + @override + Future> getIsOpen() async { + try { + final isOpen = await dataSource.isOpen(); + + return Right(isOpen); + } on ServerException catch (e) { + return Left(ServerFailure(e.error)); + } + } + + @override + Future> getOpeningHours(int weekday) async { + try { + final openingHours = await dataSource.getOpeningHours(); + + final openingHoursMap = transformOpeningHours(openingHours); + + return Right( + OpeningHours( + allOpeningHours: openingHoursMap, + todaysOpeningHours: calculateTodaysOpeningHours( + weekday, + openingHoursMap, + ), + ), + ); + } on ServerException catch (e) { + return Left(ServerFailure(e.error)); + } + } + + Map transformOpeningHours(List dtoList) { + final content = dtoList..sortBy((dto) => dto.start); + + final openingHoursPerWeekday = + groupBy(content, (dto) => dto.start.weekday); + + // create map associating each weekday to its opening hours: + // { + // 0: 8 - 16, + // 1: 8 - 16, ... + // } + final weekDayOpeningHours = openingHoursPerWeekday.map( + (day, value) => MapEntry( + day, + OpeningHoursDay(value.first.start, value.last.end).toString(), + ), + ); + + // closed string is not capitalized + var closed = Strings.closed; + closed = closed.replaceFirst(closed[0], closed[0].toUpperCase()); + + // the previous map only contains weekdays, mark weekends as closed + weekDayOpeningHours.addAll({ + DateTime.saturday: closed, + DateTime.sunday: closed, + }); + + return weekDayOpeningHours; + } + + /// Return the current weekday and the corresponding opening hours e.g + /// 'Monday: 8 - 16' + String calculateTodaysOpeningHours( + int weekday, + Map openingHours, + ) { + final weekdayPlural = Strings.weekdaysPlural[weekday]!; + final hours = openingHours[weekday]; + return '$weekdayPlural: $hours'; + } +} diff --git a/lib/features/opening_hours/domain/entities/opening_hours.dart b/lib/features/opening_hours/domain/entities/opening_hours.dart new file mode 100644 index 000000000..93c378fd3 --- /dev/null +++ b/lib/features/opening_hours/domain/entities/opening_hours.dart @@ -0,0 +1,14 @@ +import 'package:equatable/equatable.dart'; + +class OpeningHours extends Equatable { + final Map allOpeningHours; + final String todaysOpeningHours; + + const OpeningHours({ + required this.allOpeningHours, + required this.todaysOpeningHours, + }); + + @override + List get props => [allOpeningHours, todaysOpeningHours]; +} diff --git a/lib/features/opening_hours/domain/repositories/opening_hours_repository.dart b/lib/features/opening_hours/domain/repositories/opening_hours_repository.dart new file mode 100644 index 000000000..d4cf8eb63 --- /dev/null +++ b/lib/features/opening_hours/domain/repositories/opening_hours_repository.dart @@ -0,0 +1,8 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/features/opening_hours/domain/entities/opening_hours.dart'; +import 'package:dartz/dartz.dart'; + +abstract class OpeningHoursRepository { + Future> getIsOpen(); + 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 new file mode 100644 index 000000000..179d3f817 --- /dev/null +++ b/lib/features/opening_hours/domain/usecases/check_open_status.dart @@ -0,0 +1,15 @@ +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'; + +class CheckOpenStatus implements UseCase { + final OpeningHoursRepository repository; + + CheckOpenStatus({required this.repository}); + + @override + Future> call(NoParams params) async { + return repository.getIsOpen(); + } +} diff --git a/lib/features/opening_hours/domain/usecases/get_opening_hours.dart b/lib/features/opening_hours/domain/usecases/get_opening_hours.dart new file mode 100644 index 000000000..3f2144dc1 --- /dev/null +++ b/lib/features/opening_hours/domain/usecases/get_opening_hours.dart @@ -0,0 +1,16 @@ +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'; + +class GetOpeningHours implements UseCase { + final OpeningHoursRepository repository; + + GetOpeningHours({required this.repository}); + + @override + Future> call(NoParams params) async { + return repository.getOpeningHours(DateTime.now().weekday); + } +} diff --git a/lib/features/opening_hours/opening_hours.dart b/lib/features/opening_hours/opening_hours.dart new file mode 100644 index 000000000..e6cccc5b2 --- /dev/null +++ b/lib/features/opening_hours/opening_hours.dart @@ -0,0 +1,7 @@ +export 'data/datasources/opening_hours_remote_data_source.dart'; +export 'data/repositories/opening_hours_repository_impl.dart'; +export 'domain/repositories/opening_hours_repository.dart'; +export 'domain/usecases/check_open_status.dart'; +export 'domain/usecases/get_opening_hours.dart'; +export 'presentation/cubit/opening_hours_cubit.dart'; +export 'presentation/widgets/opening_hours_indicator.dart'; diff --git a/lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart b/lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart new file mode 100644 index 000000000..0bcf38d7f --- /dev/null +++ b/lib/features/opening_hours/presentation/cubit/opening_hours_cubit.dart @@ -0,0 +1,38 @@ +import 'package:bloc/bloc.dart'; +import 'package:coffeecard/core/usecases/usecase.dart'; +import 'package:coffeecard/features/opening_hours/opening_hours.dart'; +import 'package:equatable/equatable.dart'; + +part 'opening_hours_state.dart'; + +class OpeningHoursCubit extends Cubit { + final GetOpeningHours fetchOpeningHours; + final CheckOpenStatus isOpen; + + OpeningHoursCubit({required this.fetchOpeningHours, required this.isOpen}) + : super(const OpeningHoursLoading()); + + Future getOpeninghours() async { + emit(const OpeningHoursLoading()); + + final either = await isOpen(NoParams()); + + either.fold( + (error) => emit(OpeningHoursError(error: error.reason)), + (isOpen) async { + final openingHoursResult = await fetchOpeningHours(NoParams()); + + openingHoursResult.fold( + (error) => emit(OpeningHoursError(error: error.reason)), + (openingHours) => emit( + OpeningHoursLoaded( + isOpen: isOpen, + openingHours: openingHours.allOpeningHours, + todaysOpeningHours: openingHours.todaysOpeningHours, + ), + ), + ); + }, + ); + } +} diff --git a/lib/cubits/opening_hours/opening_hours_state.dart b/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart similarity index 67% rename from lib/cubits/opening_hours/opening_hours_state.dart rename to lib/features/opening_hours/presentation/cubit/opening_hours_state.dart index 899baa175..b707d37a9 100644 --- a/lib/cubits/opening_hours/opening_hours_state.dart +++ b/lib/features/opening_hours/presentation/cubit/opening_hours_state.dart @@ -12,20 +12,26 @@ class OpeningHoursLoading extends OpeningHoursState { } class OpeningHoursLoaded extends OpeningHoursState { - const OpeningHoursLoaded({required this.isOpen, required this.openingHours}); - final bool isOpen; - /// Opening hours in the format of Map final Map openingHours; + final bool isOpen; + final String todaysOpeningHours; + + const OpeningHoursLoaded({ + required this.isOpen, + required this.openingHours, + required this.todaysOpeningHours, + }); @override List get props => [isOpen, openingHours]; } class OpeningHoursError extends OpeningHoursState { - const OpeningHoursError(this.message); - final String message; + final String error; + + const OpeningHoursError({required this.error}); @override - List get props => [message]; + List get props => [error]; } diff --git a/lib/widgets/pages/settings/opening_hours_page.dart b/lib/features/opening_hours/presentation/pages/opening_hours_page.dart similarity index 97% rename from lib/widgets/pages/settings/opening_hours_page.dart rename to lib/features/opening_hours/presentation/pages/opening_hours_page.dart index 689bd3efe..220ad980a 100644 --- a/lib/widgets/pages/settings/opening_hours_page.dart +++ b/lib/features/opening_hours/presentation/pages/opening_hours_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/opening_hours/opening_hours_cubit.dart'; +import 'package:coffeecard/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:flutter/material.dart'; import 'package:gap/gap.dart'; diff --git a/lib/widgets/components/tickets/opening_hours_indicator.dart b/lib/features/opening_hours/presentation/widgets/opening_hours_indicator.dart similarity index 90% rename from lib/widgets/components/tickets/opening_hours_indicator.dart rename to lib/features/opening_hours/presentation/widgets/opening_hours_indicator.dart index 5a14fa1f9..865899be4 100644 --- a/lib/widgets/components/tickets/opening_hours_indicator.dart +++ b/lib/features/opening_hours/presentation/widgets/opening_hours_indicator.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/opening_hours/opening_hours_cubit.dart'; +import 'package:coffeecard/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/utils/analog_icons.dart'; import 'package:coffeecard/widgets/components/helpers/shimmer_builder.dart'; import 'package:flutter/widgets.dart'; @@ -15,10 +15,15 @@ class OpeningHoursIndicator extends StatelessWidget { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { + if (state is OpeningHoursError) { + return const SizedBox.shrink(); + } + var isOpen = false; if (state is OpeningHoursLoaded) { isOpen = state.isOpen; } + final openOrClosed = isOpen ? Strings.open : Strings.closed; final color = isOpen ? AppColor.success : AppColor.errorOnBright; final textStyle = diff --git a/lib/payment/mobilepay_service.dart b/lib/payment/mobilepay_service.dart index 33855c34d..2dcdf4b27 100644 --- a/lib/payment/mobilepay_service.dart +++ b/lib/payment/mobilepay_service.dart @@ -8,8 +8,8 @@ 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/either.dart'; import 'package:coffeecard/utils/launch.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter/widgets.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -31,7 +31,7 @@ class MobilePayService implements PaymentHandler { return Left(RequestFailure(e.toString())); } - return response.caseOf( + return response.fold( (error) => Left(error), (response) { final paymentDetails = MobilePayPaymentDetails.fromJsonFactory( @@ -81,7 +81,7 @@ class MobilePayService implements PaymentHandler { // Call API endpoint, receive PaymentStatus final either = await _repository.getPurchase(purchaseId); - return either.caseOf((error) { + return either.fold((error) { return Left(error); }, (purchase) { final paymentDetails = diff --git a/lib/payment/payment_handler.dart b/lib/payment/payment_handler.dart index df87e7a5e..ab9a1562d 100644 --- a/lib/payment/payment_handler.dart +++ b/lib/payment/payment_handler.dart @@ -4,7 +4,7 @@ import 'package:coffeecard/models/purchase/payment.dart'; import 'package:coffeecard/models/purchase/payment_status.dart'; import 'package:coffeecard/payment/mobilepay_service.dart'; import 'package:coffeecard/service_locator.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter/widgets.dart'; enum InternalPaymentType { diff --git a/lib/service_locator.dart b/lib/service_locator.dart index e1ecdf447..7d00ac1d2 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -3,7 +3,6 @@ import 'package:coffeecard/cubits/authentication/authentication_cubit.dart'; import 'package:coffeecard/data/api/interceptors/authentication_interceptor.dart'; import 'package:coffeecard/data/repositories/external/contributor_repository.dart'; import 'package:coffeecard/data/repositories/shared/account_repository.dart'; -import 'package:coffeecard/data/repositories/shiftplanning/opening_hours_repository.dart'; import 'package:coffeecard/data/repositories/utils/executor.dart'; import 'package:coffeecard/data/repositories/v1/occupation_repository.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; @@ -14,6 +13,7 @@ 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/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart'; import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart' hide $JsonSerializableConverter; @@ -47,6 +47,9 @@ void configureServices() { () => ReactivationAuthenticator(sl), ); + // Features + initFeatures(); + // Rest Client, Chopper client final coffeCardChopper = ChopperClient( baseUrl: ApiUriConstants.getCoffeeCardUrl(), @@ -145,14 +148,6 @@ void configureServices() { ), ); - // shiftplanning - sl.registerFactory( - () => OpeningHoursRepository( - api: sl(), - executor: sl(), - ), - ); - // external sl.registerFactory( ContributorRepository.new, @@ -162,3 +157,31 @@ void configureServices() { FirebaseAnalyticsEventLogging(FirebaseAnalytics.instance), ); } + +void initFeatures() { + initOpeningHours(); +} + +void initOpeningHours() { + // bloc + sl.registerFactory( + () => OpeningHoursCubit( + fetchOpeningHours: sl(), + isOpen: sl(), + ), + ); + + // use case + sl.registerFactory(() => GetOpeningHours(repository: sl())); + sl.registerFactory(() => CheckOpenStatus(repository: sl())); + + // repository + sl.registerLazySingleton( + () => OpeningHoursRepositoryImpl(dataSource: sl()), + ); + + // data source + sl.registerLazySingleton( + () => OpeningHoursRemoteDataSourceImpl(api: sl()), + ); +} diff --git a/lib/utils/either.dart b/lib/utils/either.dart deleted file mode 100644 index bbd81c516..000000000 --- a/lib/utils/either.dart +++ /dev/null @@ -1,29 +0,0 @@ -abstract class Either { - const Either(); - - bool get isRight => this is Right; - bool get isLeft => this is Left; - - L get left => (this as Left)._l; - R get right => (this as Right)._r; - - T caseOf(T Function(L) left, T Function(R) right) { - if (this is Left) { - return left((this as Left)._l); - } else { - return right((this as Right)._r); - } - } -} - -class Left extends Either { - final L _l; - - const Left(this._l); -} - -class Right extends Either { - final R _r; - - const Right(this._r); -} diff --git a/lib/utils/input_validator.dart b/lib/utils/input_validator.dart index 767456784..ae14efe2e 100644 --- a/lib/utils/input_validator.dart +++ b/lib/utils/input_validator.dart @@ -1,7 +1,7 @@ import 'dart:async'; -import 'package:coffeecard/utils/either.dart'; import 'package:coffeecard/utils/email_is_valid.dart'; +import 'package:dartz/dartz.dart'; part 'input_validator_helpers.dart'; diff --git a/lib/utils/reactivation_authenticator.dart b/lib/utils/reactivation_authenticator.dart index 5f842f33c..2a580e119 100644 --- a/lib/utils/reactivation_authenticator.dart +++ b/lib/utils/reactivation_authenticator.dart @@ -6,6 +6,7 @@ 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'; +import 'package:logger/logger.dart'; class ReactivationAuthenticator extends Authenticator { final GetIt serviceLocator; @@ -16,10 +17,12 @@ class ReactivationAuthenticator extends Authenticator { late SecureStorage secureStorage; late AuthenticationCubit authenticationCubit; + late Logger logger; ReactivationAuthenticator(this.serviceLocator) { secureStorage = serviceLocator.get(); authenticationCubit = serviceLocator.get(); + logger = serviceLocator.get(); } bool _canRefreshToken() => @@ -46,6 +49,9 @@ class ReactivationAuthenticator extends Authenticator { Response response, [ Request? originalRequest, ]) async { + logger.d( + '${request.url} ${response.statusCode}\n${response.bodyString}', + ); if (response.statusCode != 401) { return null; } @@ -89,21 +95,21 @@ class ReactivationAuthenticator extends Authenticator { try { final either = await accountRepository.login(email, encodedPasscode); - if (either.isRight) { + either.fold((l) { + // refresh failed, sign the user out + _evict(); + }, (r) async { // refresh succeeded, update the token in secure storage tokenRefreshedAt = DateTime.now(); - final token = either.right.token; - final bearerToken = 'Bearer ${either.right.token}'; + final token = r.token; + final bearerToken = 'Bearer ${r.token}'; await secureStorage.updateToken(token); return request.copyWith( headers: _updateHeadersWithToken(request.headers, bearerToken), ); - } else { - // refresh failed, sign the user out - _evict(); - } + }); } finally { mutex.unlock(); } 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 5f498b279..88de6b2dd 100644 --- a/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart +++ b/lib/widgets/components/forms/forgot_passcode/forgot_passcode_form.dart @@ -1,11 +1,11 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/service_locator.dart'; -import 'package:coffeecard/utils/either.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:dartz/dartz.dart'; import 'package:flutter/material.dart'; class ForgotPasscodeForm extends StatelessWidget { @@ -28,13 +28,13 @@ class ForgotPasscodeForm extends StatelessWidget { forceErrorMessage: true, validate: (text) async { final either = await sl().emailExists(text); - if (either.isRight) { - return (either.right == false) + + return either.fold( + (l) => const Left(Strings.emailValidationError), + (r) => r ? const Left(Strings.forgotPasscodeNoAccountExists) - : const Right(null); - } else { - return const Left(Strings.emailValidationError); - } + : const Right(null), + ); }, ), ], @@ -53,12 +53,16 @@ class ForgotPasscodeForm extends StatelessWidget { final either = await sl().requestPasscodeReset(email); - final title = (either.isRight) - ? Strings.forgotPasscodeLinkSent - : Strings.forgotPasscodeError; - final body = (either.isRight) - ? Strings.forgotPasscodeSentRequestTo(email) - : either.left.message; + var title = Strings.forgotPasscodeLinkSent; + var body = Strings.forgotPasscodeSentRequestTo(email); + + either.fold( + (l) { + title = Strings.forgotPasscodeError; + body = l.message; + }, + (r) => null, + ); appDialog( context: context, diff --git a/lib/widgets/components/forms/form_text_field.dart b/lib/widgets/components/forms/form_text_field.dart index 2aaaddfc2..1060fa5e5 100644 --- a/lib/widgets/components/forms/form_text_field.dart +++ b/lib/widgets/components/forms/form_text_field.dart @@ -94,7 +94,7 @@ class _FormTextFieldState extends State<_FormTextField> { Widget build(BuildContext context) { return BlocBuilder( builder: (context, state) { - final maybeError = state.error.isLeft ? state.error.left : null; + final maybeError = state.error.fold((l) => l, (r) => null); return Container( margin: const EdgeInsets.only(bottom: 12), diff --git a/lib/widgets/components/forms/register/register_email_form.dart b/lib/widgets/components/forms/register/register_email_form.dart index d27903e35..39340ac2f 100644 --- a/lib/widgets/components/forms/register/register_email_form.dart +++ b/lib/widgets/components/forms/register/register_email_form.dart @@ -1,9 +1,9 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/service_locator.dart'; -import 'package:coffeecard/utils/either.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'; class RegisterEmailForm extends StatelessWidget { @@ -24,13 +24,13 @@ class RegisterEmailForm extends StatelessWidget { forceErrorMessage: true, validate: (text) async { final either = await sl().emailExists(text); - if (either.isRight) { - return (either.right == true) + + return either.fold( + (l) => const Left(Strings.emailValidationError), + (r) => r ? Left(Strings.registerEmailInUse(text)) - : const Right(null); - } else { - return const Left(Strings.emailValidationError); - } + : const Right(null), + ); }, ), ], diff --git a/lib/widgets/components/forms/settings/change_email_form.dart b/lib/widgets/components/forms/settings/change_email_form.dart index a480bb560..42954e2f1 100644 --- a/lib/widgets/components/forms/settings/change_email_form.dart +++ b/lib/widgets/components/forms/settings/change_email_form.dart @@ -1,9 +1,9 @@ import 'package:coffeecard/base/strings.dart'; import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/service_locator.dart'; -import 'package:coffeecard/utils/either.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'; class ChangeEmailForm extends StatelessWidget { @@ -31,13 +31,13 @@ class ChangeEmailForm extends StatelessWidget { forceErrorMessage: true, validate: (text) async { final either = await sl().emailExists(text); - if (either.isRight) { - return (either.right == true) + + return either.fold( + (l) => const Left(Strings.emailValidationError), + (r) => r ? Left(Strings.registerEmailInUse(text)) - : const Right(null); - } else { - return const Left(Strings.emailValidationError); - } + : const Right(null), + ); }, ), ], diff --git a/lib/widgets/pages/home_page.dart b/lib/widgets/pages/home_page.dart index 5253167b3..a8365d8a7 100644 --- a/lib/widgets/pages/home_page.dart +++ b/lib/widgets/pages/home_page.dart @@ -3,17 +3,16 @@ 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/opening_hours/opening_hours_cubit.dart'; import 'package:coffeecard/cubits/receipt/receipt_cubit.dart'; import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; import 'package:coffeecard/cubits/tickets/tickets_cubit.dart'; import 'package:coffeecard/cubits/user/user_cubit.dart'; import 'package:coffeecard/data/repositories/shared/account_repository.dart'; -import 'package:coffeecard/data/repositories/shiftplanning/opening_hours_repository.dart'; import 'package:coffeecard/data/repositories/v1/occupation_repository.dart'; import 'package:coffeecard/data/repositories/v1/receipt_repository.dart'; import 'package:coffeecard/data/repositories/v1/ticket_repository.dart'; import 'package:coffeecard/data/repositories/v2/leaderboard_repository.dart'; +import 'package:coffeecard/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/service_locator.dart'; import 'package:coffeecard/widgets/components/helpers/lazy_indexed_stack.dart'; import 'package:coffeecard/widgets/pages/receipts/receipts_page.dart'; @@ -147,9 +146,7 @@ class _HomePageState extends State { )..fetch(), ), BlocProvider( - create: (_) => OpeningHoursCubit( - sl(), - )..getOpeninghours(), + create: (_) => sl()..getOpeninghours(), ) ], child: WillPopScope( diff --git a/lib/widgets/pages/settings/settings_page.dart b/lib/widgets/pages/settings/settings_page.dart index 7af93417d..8f6a268b4 100644 --- a/lib/widgets/pages/settings/settings_page.dart +++ b/lib/widgets/pages/settings/settings_page.dart @@ -2,8 +2,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/cubits/authentication/authentication_cubit.dart'; -import 'package:coffeecard/cubits/opening_hours/opening_hours_cubit.dart'; import 'package:coffeecard/cubits/user/user_cubit.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/utils/api_uri_constants.dart'; import 'package:coffeecard/utils/launch.dart'; import 'package:coffeecard/widgets/components/dialog.dart'; @@ -17,7 +18,6 @@ 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/credits_page.dart'; import 'package:coffeecard/widgets/pages/settings/faq_page.dart'; -import 'package:coffeecard/widgets/pages/settings/opening_hours_page.dart'; import 'package:coffeecard/widgets/pages/settings/setting_value_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -42,14 +42,6 @@ class SettingsPage extends StatelessWidget { return (state is! UserLoaded) ? null : () => callback(state); } - /// Tappable only if opening hours data has been loaded. - void Function()? _ifOpeningHoursLoaded( - OpeningHoursState state, - void Function(OpeningHoursLoaded) callback, - ) { - return (state is! OpeningHoursLoaded) ? null : () => callback(state); - } - @override Widget build(BuildContext context) { final openingHoursState = context.watch().state; @@ -132,34 +124,24 @@ class SettingsPage extends StatelessWidget { ), SettingListEntry( name: Strings.openingHours, - onTap: _ifOpeningHoursLoaded( - openingHoursState, - (st) => Navigator.push( - context, - OpeningHoursPage.routeWith(state: st), - ), - ), + onTap: openingHoursState is OpeningHoursLoaded + ? () => Navigator.push( + context, + OpeningHoursPage.routeWith(state: openingHoursState), + ) + : null, valueWidget: ShimmerBuilder( showShimmer: openingHoursState is OpeningHoursLoading, - builder: (context, colorIfShimmer) { - final today = DateTime.now().weekday; - final weekdayPlural = Strings.weekdaysPlural[today]!; - final String text; - - if (openingHoursState is OpeningHoursLoaded) { - final hours = openingHoursState.openingHours[today]!; - text = '$weekdayPlural: $hours'; - } else if (openingHoursState is OpeningHoursLoading) { - text = Strings.openingHoursShimmerText; - } else { - text = ''; - } - - return ColoredBox( - color: colorIfShimmer, - child: SettingValueText(value: text), - ); - }, + builder: (context, colorIfShimmer) => ColoredBox( + color: colorIfShimmer, + child: SettingValueText( + value: openingHoursState is OpeningHoursLoaded + ? openingHoursState.todaysOpeningHours + : openingHoursState is OpeningHoursLoading + ? Strings.openingHoursShimmerText + : '', + ), + ), ), ), SettingListEntry( diff --git a/lib/widgets/pages/tickets/tickets_page.dart b/lib/widgets/pages/tickets/tickets_page.dart index 2dda4c7cf..afc397909 100644 --- a/lib/widgets/pages/tickets/tickets_page.dart +++ b/lib/widgets/pages/tickets/tickets_page.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/base/strings.dart'; +import 'package:coffeecard/features/opening_hours/opening_hours.dart'; import 'package:coffeecard/widgets/components/scaffold.dart'; import 'package:coffeecard/widgets/components/section_title.dart'; -import 'package:coffeecard/widgets/components/tickets/opening_hours_indicator.dart'; import 'package:coffeecard/widgets/components/tickets/shop_section.dart'; import 'package:coffeecard/widgets/components/tickets/tickets_section.dart'; import 'package:flutter/material.dart'; diff --git a/pubspec.lock b/pubspec.lock index 7f7e74362..c277bd1e9 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -232,6 +232,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.3" + dartz: + dependency: "direct main" + description: + name: dartz + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.1" diff_match_patch: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 6608b71b9..d1bea3d1d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -78,6 +78,9 @@ dependencies: firebase_crashlytics: 2.8.9 firebase_performance: 0.8.2+4 + # functional programming thingies + dartz: 0.10.1 + dev_dependencies: flutter_test: sdk: flutter diff --git a/test/cubits/contributor/contributor_cubit_test.dart b/test/cubits/contributor/contributor_cubit_test.dart index 97ffecfa8..f97010afe 100644 --- a/test/cubits/contributor/contributor_cubit_test.dart +++ b/test/cubits/contributor/contributor_cubit_test.dart @@ -3,7 +3,7 @@ import 'package:coffeecard/cubits/contributor/contributor_cubit.dart'; import 'package:coffeecard/data/repositories/external/contributor_repository.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/models/contributor.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/cubits/environment/environment_cubit_test.dart b/test/cubits/environment/environment_cubit_test.dart index 140ed2bc5..40902c99d 100644 --- a/test/cubits/environment/environment_cubit_test.dart +++ b/test/cubits/environment/environment_cubit_test.dart @@ -3,7 +3,7 @@ import 'package:coffeecard/cubits/environment/environment_cubit.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/data/repositories/v2/app_config_repository.dart'; import 'package:coffeecard/models/environment.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.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 4b9ef0ae7..d984d8899 100644 --- a/test/cubits/login/login_cubit_test.dart +++ b/test/cubits/login/login_cubit_test.dart @@ -3,7 +3,7 @@ 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/data/repositories/utils/request_types.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/cubits/opening_hours/opening_hours_cubit_test.dart b/test/cubits/opening_hours/opening_hours_cubit_test.dart deleted file mode 100644 index 2b0cecc9b..000000000 --- a/test/cubits/opening_hours/opening_hours_cubit_test.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:bloc_test/bloc_test.dart'; -import 'package:coffeecard/cubits/opening_hours/opening_hours_cubit.dart'; -import 'package:coffeecard/data/repositories/shiftplanning/opening_hours_repository.dart'; -import 'package:coffeecard/data/repositories/utils/request_types.dart'; -import 'package:coffeecard/utils/either.dart'; -import 'package:flutter_test/flutter_test.dart'; -import 'package:mockito/annotations.dart'; -import 'package:mockito/mockito.dart'; - -import 'opening_hours_cubit_test.mocks.dart'; - -const dummyOpeningHours = { - DateTime.monday: '00:00-00:00', - DateTime.tuesday: '00:00-00:00', - DateTime.wednesday: '00:00-00:00', - DateTime.thursday: '00:00-00:00', - DateTime.friday: '00:00-00:00', - DateTime.saturday: '00:00-00:00', - DateTime.sunday: '00:00-00:00', -}; - -@GenerateMocks([OpeningHoursRepository]) -void main() { - group('openinghours cubit tests', () { - late OpeningHoursCubit openinghoursCubit; - final repo = MockOpeningHoursRepository(); - - setUp(() { - openinghoursCubit = OpeningHoursCubit(repo); - }); - - blocTest( - 'should emit loading and loaded when data is fetched successfully', - build: () { - when(repo.isOpen()).thenAnswer((_) async => const Right(true)); - when(repo.getOpeningHours()) - .thenAnswer((_) async => const Right(dummyOpeningHours)); - return openinghoursCubit; - }, - act: (cubit) => cubit.getOpeninghours(), - expect: () => [ - const OpeningHoursLoading(), - const OpeningHoursLoaded(isOpen: true, openingHours: dummyOpeningHours), - ], - ); - - blocTest( - 'should emit loading and error when fetching repo.isOpen fails', - build: () { - when(repo.isOpen()) - .thenAnswer((_) async => Left(RequestHttpFailure('ERROR', 0))); - when(repo.getOpeningHours()) - .thenAnswer((_) async => const Right(dummyOpeningHours)); - return openinghoursCubit; - }, - act: (cubit) => cubit.getOpeninghours(), - expect: () => [ - const OpeningHoursLoading(), - const OpeningHoursError('ERROR'), - ], - ); - - blocTest( - 'should emit loading and error when fetching repo.getOpeningHours fails', - build: () { - when(repo.isOpen()).thenAnswer((_) async => const Right(true)); - when(repo.getOpeningHours()) - .thenAnswer((_) async => Left(RequestFailure('ERROR'))); - return openinghoursCubit; - }, - act: (cubit) => cubit.getOpeninghours(), - expect: () => [ - const OpeningHoursLoading(), - const OpeningHoursError('ERROR'), - ], - ); - - tearDown(() { - openinghoursCubit.close(); - }); - }); -} diff --git a/test/cubits/products/products_cubit_test.dart b/test/cubits/products/products_cubit_test.dart index 0d9c04861..a5070bbc6 100644 --- a/test/cubits/products/products_cubit_test.dart +++ b/test/cubits/products/products_cubit_test.dart @@ -3,7 +3,7 @@ import 'package:coffeecard/cubits/products/products_cubit.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/data/repositories/v1/product_repository.dart'; import 'package:coffeecard/models/ticket/product.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/cubits/receipt/receipt_cubit_test.dart b/test/cubits/receipt/receipt_cubit_test.dart index 7758a11a5..027ed5275 100644 --- a/test/cubits/receipt/receipt_cubit_test.dart +++ b/test/cubits/receipt/receipt_cubit_test.dart @@ -4,7 +4,7 @@ import 'package:coffeecard/cubits/receipt/receipt_cubit.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/data/repositories/v1/receipt_repository.dart'; import 'package:coffeecard/models/receipts/receipt.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.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 c4fbc4d2b..a39170de4 100644 --- a/test/cubits/statistics/statistics_cubit_test.dart +++ b/test/cubits/statistics/statistics_cubit_test.dart @@ -3,7 +3,7 @@ import 'package:coffeecard/cubits/statistics/statistics_cubit.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/data/repositories/v2/leaderboard_repository.dart'; import 'package:coffeecard/models/leaderboard/leaderboard_user.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:mockito/annotations.dart'; import 'package:mockito/mockito.dart'; diff --git a/test/cubits/tickets/tickets_cubit_test.dart b/test/cubits/tickets/tickets_cubit_test.dart index 72d0c4b59..43864756a 100644 --- a/test/cubits/tickets/tickets_cubit_test.dart +++ b/test/cubits/tickets/tickets_cubit_test.dart @@ -3,7 +3,7 @@ import 'package:coffeecard/cubits/tickets/tickets_cubit.dart'; import 'package:coffeecard/data/repositories/utils/request_types.dart'; import 'package:coffeecard/data/repositories/v1/ticket_repository.dart'; import 'package:coffeecard/models/receipts/receipt.dart'; -import 'package:coffeecard/utils/either.dart'; +import 'package:dartz/dartz.dart'; import 'package:flutter_test/flutter_test.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 53c72b817..cfb5cd268 100644 --- a/test/data/repositories/v2/account_repository/account_repository_test.dart +++ b/test/data/repositories/v2/account_repository/account_repository_test.dart @@ -3,7 +3,8 @@ import 'package:coffeecard/data/repositories/shared/account_repository.dart'; import 'package:coffeecard/data/repositories/utils/executor.dart'; import 'package:coffeecard/generated/api/coffeecard_api.swagger.dart' hide MessageResponseDto; -import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart' + show CoffeecardApiV2, MessageResponseDto; import 'package:flutter_test/flutter_test.dart'; import 'package:logger/logger.dart'; import 'package:mockito/annotations.dart'; @@ -38,7 +39,7 @@ void main() { ); final actual = await repo.register('name', 'email', 'passcode', 0); - expectLater(actual.isRight, isTrue); + expectLater(actual.isRight(), isTrue); }); test('register given unsuccessful api response returns left', () async { @@ -47,6 +48,6 @@ void main() { ); final actual = await repo.register('name', 'email', 'passcode', 0); - expect(actual.isLeft, isTrue); + expect(actual.isLeft(), isTrue); }); } diff --git a/test/data/repositories/v2/ticket_repository/ticket_repository_test.dart b/test/data/repositories/v2/ticket_repository/ticket_repository_test.dart index 71e18f614..0c70d6656 100644 --- a/test/data/repositories/v2/ticket_repository/ticket_repository_test.dart +++ b/test/data/repositories/v2/ticket_repository/ticket_repository_test.dart @@ -30,13 +30,17 @@ void main() { }); test('getUserTickets given successfull api response returns right', () async { + // arrange when(apiV2.apiV2TicketsGet(includeUsed: anyNamed('includeUsed'))) .thenAnswer( (_) async => chopper.Response(Responses.succeeding(), const []), ); + // act final actual = await repo.getUserTickets(); - expect(actual.isRight, isTrue); + + // assert + expect(actual.isRight(), isTrue); }); test('getUserTickets given unsuccessfull api response returns left', @@ -47,6 +51,6 @@ void main() { ); final actual = await repo.getUserTickets(); - expect(actual.isLeft, isTrue); + expect(actual.isLeft(), isTrue); }); } 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 new file mode 100644 index 000000000..e9a887171 --- /dev/null +++ b/test/features/opening_hours/data/datasources/opening_hours_remote_data_source_test.dart @@ -0,0 +1,85 @@ +import 'package:coffeecard/core/errors/exceptions.dart'; +import 'package:coffeecard/features/opening_hours/opening_hours.dart'; +import 'package:coffeecard/generated/api/shiftplanning_api.swagger.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import '../../../../response_factory.dart'; +import 'opening_hours_remote_data_source_test.mocks.dart'; + +@GenerateMocks([ShiftplanningApi]) +void main() { + late MockShiftplanningApi api; + late OpeningHoursRemoteDataSource dataSource; + + setUp(() { + api = MockShiftplanningApi(); + dataSource = OpeningHoursRemoteDataSourceImpl(api: api); + }); + + group('isOpen', () { + test('should throw [ServerException] if api call fails', () async { + // arrange + when(api.apiOpenShortKeyGet(shortKey: anyNamed('shortKey'))).thenAnswer( + (_) async => ResponseFactory.fromStatusCode(500), + ); + + // act + final call = dataSource.isOpen; + + // assert + expect( + () async => call(), + throwsA(const TypeMatcher()), + ); + }); + + test('should return bool if api call succeeds', () async { + // arrange + when(api.apiOpenShortKeyGet(shortKey: anyNamed('shortKey'))).thenAnswer( + (_) async => ResponseFactory.fromStatusCode( + 200, + body: IsOpenDTO(open: true), + ), + ); + + // act + final actual = await dataSource.isOpen(); + + // assert + expect(actual, true); + }); + }); + + group('getOpeningHours', () { + test('should throw [ServerException] if api call fails', () async { + // arrange + when(api.apiShiftsShortKeyGet(shortKey: anyNamed('shortKey'))).thenAnswer( + (_) async => ResponseFactory.fromStatusCode(500), + ); + + // act + final call = dataSource.getOpeningHours; + + // assert + expect( + () async => call(), + throwsA(const TypeMatcher()), + ); + }); + + test('should return opening hours if api call succeeds', () async { + // arrange + when(api.apiShiftsShortKeyGet(shortKey: anyNamed('shortKey'))).thenAnswer( + (_) async => ResponseFactory.fromStatusCode(200, body: []), + ); + + // act + final actual = await dataSource.getOpeningHours(); + + // assert + expect(actual, []); + }); + }); +} 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 new file mode 100644 index 000000000..aee285902 --- /dev/null +++ b/test/features/opening_hours/data/repositories/opening_hours_repository_impl_test.dart @@ -0,0 +1,176 @@ +import 'package:coffeecard/core/errors/exceptions.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:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'opening_hours_repository_impl_test.mocks.dart'; + +@GenerateMocks([OpeningHoursRemoteDataSource]) +void main() { + late MockOpeningHoursRemoteDataSource dataSource; + late OpeningHoursRepositoryImpl repository; + + setUp(() { + dataSource = MockOpeningHoursRemoteDataSource(); + repository = OpeningHoursRepositoryImpl(dataSource: dataSource); + }); + + group('getOpeningHours', () { + test('should return ServerFailure if data source call fails', () async { + // arrange + when(dataSource.isOpen()).thenThrow(ServerException(error: 'some error')); + + // act + final actual = await repository.getIsOpen(); + + // assert + expect(actual, const Left(ServerFailure('some error'))); + }); + + test('should return bool if data source call succeeds', () async { + // arrange + when(dataSource.isOpen()).thenAnswer((_) async => true); + + // act + final actual = await repository.getIsOpen(); + + // assert + expect(actual, const Right(true)); + }); + }); + + group('getIsOpen', () { + test('should return ServerFailure if data source call fails', () async { + // arrange + when(dataSource.getOpeningHours()).thenThrow( + ServerException(error: 'some error'), + ); + + // act + final actual = await repository.getOpeningHours(0); + + // assert + expect(actual, const Left(ServerFailure('some error'))); + }); + + test('should return map if data source call succeeds', () async { + // arrange + when(dataSource.getOpeningHours()).thenAnswer((_) async => []); + + // act + final actual = await repository.getOpeningHours(DateTime.monday); + + // assert + expect( + actual, + const Right( + OpeningHours( + allOpeningHours: { + 6: 'Closed', + 7: 'Closed', + }, + todaysOpeningHours: 'Mondays: null', + ), + ), + ); + }); + }); + + group('calculateTodaysOpeningHours', () { + test('should return correct hours given Monday', () { + // arrange + final openingHours = {DateTime.monday: '8 - 16'}; + + // act + final actual = + repository.calculateTodaysOpeningHours(DateTime.monday, openingHours); + + // assert + expect(actual, 'Mondays: 8 - 16'); + }); + + test('should return correct hours given Tuesday', () { + // arrange + final openingHours = {DateTime.tuesday: '8 - 16'}; + + // act + final actual = repository.calculateTodaysOpeningHours( + DateTime.tuesday, + openingHours, + ); + + // assert + expect(actual, 'Tuesdays: 8 - 16'); + }); + + test('should return correct hours given Wednesday', () { + // arrange + final openingHours = {DateTime.wednesday: '8 - 16'}; + + // act + final actual = repository.calculateTodaysOpeningHours( + DateTime.wednesday, + openingHours, + ); + + // assert + expect(actual, 'Wednesdays: 8 - 16'); + }); + + test('should return correct hours given Thursday', () { + // arrange + final openingHours = {DateTime.thursday: '8 - 16'}; + + // act + final actual = repository.calculateTodaysOpeningHours( + DateTime.thursday, + openingHours, + ); + + // assert + expect(actual, 'Thursdays: 8 - 16'); + }); + + test('should return correct hours given Fridays', () { + // arrange + final openingHours = {DateTime.friday: '8 - 16'}; + + // act + final actual = + repository.calculateTodaysOpeningHours(DateTime.friday, openingHours); + + // assert + expect(actual, 'Fridays: 8 - 16'); + }); + + test('should return correct hours given Saturday', () { + // arrange + final openingHours = {DateTime.saturday: '8 - 16'}; + + // act + final actual = repository.calculateTodaysOpeningHours( + DateTime.saturday, + openingHours, + ); + + // assert + expect(actual, 'Saturdays: 8 - 16'); + }); + + test('should return correct hours given Monday', () { + // arrange + final openingHours = {DateTime.sunday: '8 - 16'}; + + // act + final actual = + repository.calculateTodaysOpeningHours(DateTime.sunday, openingHours); + + // assert + expect(actual, 'Sundays: 8 - 16'); + }); + }); +} 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 new file mode 100644 index 000000000..8902fbbf0 --- /dev/null +++ b/test/features/opening_hours/domain/usecases/check_open_status_test.dart @@ -0,0 +1,31 @@ +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:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'check_open_status_test.mocks.dart'; + +@GenerateMocks([OpeningHoursRepository]) +void main() { + late MockOpeningHoursRepository repository; + late CheckOpenStatus getIsOpen; + + setUp(() { + repository = MockOpeningHoursRepository(); + getIsOpen = CheckOpenStatus(repository: repository); + }); + + test('should call repository', () async { + // arrange + when(repository.getIsOpen()).thenAnswer((_) async => const Right(true)); + + // act + await getIsOpen(NoParams()); + + // assert + verify(repository.getIsOpen()); + verifyNoMoreInteractions(repository); + }); +} 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 new file mode 100644 index 000000000..89d249648 --- /dev/null +++ b/test/features/opening_hours/domain/usecases/get_opening_hours_test.dart @@ -0,0 +1,38 @@ +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:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'get_opening_hours_test.mocks.dart'; + +@GenerateMocks([OpeningHoursRepository]) +void main() { + late MockOpeningHoursRepository repository; + late GetOpeningHours fetchOpeningHours; + + setUp(() { + repository = MockOpeningHoursRepository(); + fetchOpeningHours = GetOpeningHours(repository: repository); + }); + + test('should call repository', () async { + const theOpeningHours = OpeningHours( + allOpeningHours: {}, + todaysOpeningHours: '', + ); + + // arrange + when(repository.getOpeningHours(any)) + .thenAnswer((_) async => const Right(theOpeningHours)); + + // act + await fetchOpeningHours(NoParams()); + + // assert + verify(repository.getOpeningHours(any)); + verifyNoMoreInteractions(repository); + }); +} 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 new file mode 100644 index 000000000..2e24bcee1 --- /dev/null +++ b/test/features/opening_hours/presentation/cubit/opening_hours_cubit_test.dart @@ -0,0 +1,88 @@ +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:mockito/annotations.dart'; +import 'package:mockito/mockito.dart'; + +import 'opening_hours_cubit_test.mocks.dart'; + +@GenerateMocks([GetOpeningHours, CheckOpenStatus]) +void main() { + late MockGetOpeningHours getOpeningHours; + late MockCheckOpenStatus checkOpenStatus; + late OpeningHoursCubit cubit; + + setUp(() { + getOpeningHours = MockGetOpeningHours(); + checkOpenStatus = MockCheckOpenStatus(); + cubit = OpeningHoursCubit( + isOpen: checkOpenStatus, + fetchOpeningHours: getOpeningHours, + ); + }); + + group('getOpeninghours', () { + const theOpeningHours = OpeningHours( + allOpeningHours: {}, + todaysOpeningHours: '', + ); + + blocTest( + 'should emit [Loading, Error] when isOpen fails', + build: () => cubit, + setUp: () => { + when(checkOpenStatus(any)).thenAnswer( + (_) => Future.value(const Left(ServerFailure('some error'))), + ) + }, + act: (_) async => cubit.getOpeninghours(), + expect: () => [ + const OpeningHoursLoading(), + const OpeningHoursError(error: 'some error'), + ], + ); + + blocTest( + 'should emit [Loading, Error] when fetchOpeningHours fails', + build: () => cubit, + setUp: () { + when(checkOpenStatus(any)).thenAnswer( + (_) => Future.value(const Right(false)), + ); + when(getOpeningHours(any)).thenAnswer( + (_) => Future.value(const Left(ServerFailure('some error'))), + ); + }, + act: (_) async => cubit.getOpeninghours(), + expect: () => [ + const OpeningHoursLoading(), + const OpeningHoursError(error: 'some error'), + ], + ); + + blocTest( + 'should emit [Loading, Loaded] when isOpen and openingHours succeeds', + build: () => cubit, + setUp: () { + when(checkOpenStatus(any)).thenAnswer( + (_) => Future.value(const Right(true)), + ); + when(getOpeningHours(any)).thenAnswer( + (_) => Future.value(const Right(theOpeningHours)), + ); + }, + act: (_) async => cubit.getOpeninghours(), + expect: () => [ + const OpeningHoursLoading(), + const OpeningHoursLoaded( + isOpen: true, + openingHours: {}, + todaysOpeningHours: '', + ) + ], + ); + }); +} diff --git a/test/response_factory.dart b/test/response_factory.dart new file mode 100644 index 000000000..77cc7eac0 --- /dev/null +++ b/test/response_factory.dart @@ -0,0 +1,11 @@ +import 'package:chopper/chopper.dart' as chopper; +import 'package:http/http.dart' as http; + +class ResponseFactory { + static chopper.Response fromStatusCode(int statusCode, {T? body}) { + return chopper.Response( + http.Response('', statusCode), + body, + ); + } +}