From 88c14b2e784ed1f3b04a1f2ddb62dbcde7eb08f5 Mon Sep 17 00:00:00 2001 From: Omid Marfavi <21163286+marfavi@users.noreply.github.com> Date: Wed, 7 Feb 2024 21:53:26 +0100 Subject: [PATCH] environment: remove & refactor --- lib/app.dart | 51 ++++++--- lib/core/errors/failures.dart | 5 + .../network/network_request_executor.dart | 1 + lib/core/widgets/components/scaffold.dart | 71 ++++++------ .../pages/splash/splash_error_page.dart | 6 +- lib/features/environment.dart | 1 + .../environment_remote_data_source.dart | 21 ---- .../domain/entities/environment.dart | 25 ----- .../domain/usecases/get_environment_type.dart | 14 --- .../presentation/cubit/environment_cubit.dart | 22 ---- .../presentation/cubit/environment_state.dart | 28 ----- .../widgets/environment_button.dart | 80 ------------- .../presentation/pages/view_receipt_page.dart | 33 +++--- .../redirection/redirection_router.dart | 2 +- .../presentation/widgets/tickets_section.dart | 5 +- lib/service_locator.dart | 21 ---- .../bloc/authentication_cubit.dart | 24 ++-- .../environment/bloc/environment_cubit.dart | 25 +++++ .../environment/bloc/environment_state.dart | 36 ++++++ lib/src/environment/environment.dart | 5 + lib/src/environment/environment_model.dart | 4 + .../environment/environment_repository.dart | 52 +++++++++ .../widgets/environment_banner.dart | 8 +- .../widgets/environment_button.dart | 67 +++++++++++ lib/src/product/functions.dart | 5 +- .../bloc/environment_cubit_test.dart | 72 ++++++++++++ .../environment_remote_data_source_test.dart | 43 ------- .../domain/entities/environment_test.dart | 57 ---------- .../usecases/get_environment_type_test.dart | 37 ------ .../environment_repository_test.dart | 105 ++++++++++++++++++ .../cubit/environment_cubit_test.dart | 55 --------- 31 files changed, 476 insertions(+), 505 deletions(-) create mode 100644 lib/features/environment.dart delete mode 100644 lib/features/environment/data/datasources/environment_remote_data_source.dart delete mode 100644 lib/features/environment/domain/entities/environment.dart delete mode 100644 lib/features/environment/domain/usecases/get_environment_type.dart delete mode 100644 lib/features/environment/presentation/cubit/environment_cubit.dart delete mode 100644 lib/features/environment/presentation/cubit/environment_state.dart delete mode 100644 lib/features/environment/presentation/widgets/environment_button.dart create mode 100644 lib/src/environment/bloc/environment_cubit.dart create mode 100644 lib/src/environment/bloc/environment_state.dart create mode 100644 lib/src/environment/environment.dart create mode 100644 lib/src/environment/environment_model.dart create mode 100644 lib/src/environment/environment_repository.dart rename lib/{features/environment/presentation => src/environment}/widgets/environment_banner.dart (50%) create mode 100644 lib/src/environment/widgets/environment_button.dart create mode 100644 test/features/environment/bloc/environment_cubit_test.dart delete mode 100644 test/features/environment/data/datasources/environment_remote_data_source_test.dart delete mode 100644 test/features/environment/domain/entities/environment_test.dart delete mode 100644 test/features/environment/domain/usecases/get_environment_type_test.dart create mode 100644 test/features/environment/environment_repository_test.dart delete mode 100644 test/features/environment/presentation/cubit/environment_cubit_test.dart diff --git a/lib/app.dart b/lib/app.dart index aa914cd92..36a35baa0 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -1,13 +1,15 @@ +import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/core/strings.dart'; import 'package:coffeecard/core/styles/theme.dart'; import 'package:coffeecard/core/widgets/pages/splash/splash_error_page.dart'; import 'package:coffeecard/core/widgets/pages/splash/splash_loading_page.dart'; import 'package:coffeecard/features/authentication.dart'; -import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; import 'package:coffeecard/features/product.dart'; import 'package:coffeecard/features/redirection/redirection_router.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; import 'package:coffeecard/service_locator.dart'; +import 'package:coffeecard/src/environment/environment.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; @@ -20,25 +22,40 @@ class App extends StatelessWidget { // Force screen orientation to portrait SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]); - return MultiBlocProvider( + return MultiRepositoryProvider( providers: [ - BlocProvider.value(value: sl()..appStarted()), - BlocProvider.value(value: sl()..getConfig()), - BlocProvider(create: (_) => sl()), - BlocProvider.value(value: sl()), + RepositoryProvider.value(value: sl()), + RepositoryProvider.value(value: sl()), + RepositoryProvider( + create: (context) => EnvironmentRepository( + apiV2: context.read(), + executor: context.read(), + ), + ), ], - child: MainRedirectionRouter( - navigatorKey: _navigatorKey, - child: MaterialApp( - title: Strings.appTitle, - theme: analogTheme, + child: MultiBlocProvider( + providers: [ + BlocProvider.value(value: sl()..appStarted()), + BlocProvider( + create: (context) => + EnvironmentCubit(repository: context.read())..loadEnvironment(), + ), + BlocProvider(create: (_) => sl()), + BlocProvider.value(value: sl()), + ], + child: MainRedirectionRouter( navigatorKey: _navigatorKey, - home: BlocBuilder( - builder: (_, state) { - return (state is EnvironmentError) - ? SplashErrorPage(errorMessage: state.message) - : const SplashLoadingPage(); - }, + child: MaterialApp( + title: Strings.appTitle, + theme: analogTheme, + navigatorKey: _navigatorKey, + home: BlocBuilder( + builder: (_, state) => switch (state) { + EnvironmentLoadError(:final message) => + SplashErrorPage(message), + _ => const SplashLoadingPage(), + }, + ), ), ), ), diff --git a/lib/core/errors/failures.dart b/lib/core/errors/failures.dart index 6a50c152a..937ef725c 100644 --- a/lib/core/errors/failures.dart +++ b/lib/core/errors/failures.dart @@ -53,3 +53,8 @@ class ServerFailure extends NetworkFailure { class ConnectionFailure extends NetworkFailure { const ConnectionFailure() : super('connection refused'); } + +/// An unknown failure that is not expected to occur. +class UnknownFailure extends Failure { + const UnknownFailure(super.reason); +} diff --git a/lib/core/network/network_request_executor.dart b/lib/core/network/network_request_executor.dart index b5904013c..e7f715659 100644 --- a/lib/core/network/network_request_executor.dart +++ b/lib/core/network/network_request_executor.dart @@ -8,6 +8,7 @@ part 'network_request_executor_mapping.dart'; typedef _NetworkRequest = Future> Function(); typedef _ExecutorResult = Future>; +// FIXME: Attempt to change back to NetworkFailure for Left type? typedef _ExecutorTaskEither = TaskEither; class NetworkRequestExecutor { diff --git a/lib/core/widgets/components/scaffold.dart b/lib/core/widgets/components/scaffold.dart index 06752f848..fb17d6b7b 100644 --- a/lib/core/widgets/components/scaffold.dart +++ b/lib/core/widgets/components/scaffold.dart @@ -1,26 +1,14 @@ import 'package:coffeecard/core/styles/app_colors.dart'; import 'package:coffeecard/core/styles/app_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/features/environment/presentation/widgets/environment_banner.dart'; -import 'package:coffeecard/features/environment/presentation/widgets/environment_button.dart'; +import 'package:coffeecard/features/environment.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; class AppScaffold extends StatelessWidget { - final Widget? title; - final Widget body; - final bool applyPadding; - final Color backgroundColor; - final bool resizeToAvoidBottomInset; - final double? appBarHeight; - - bool get hasTitle => title != null; - /// A Scaffold with a normal app bar. /// - /// If connected to test envrionment, an environment - /// warning button is shown below the title. + /// If connected to test environment, a [TestingEnvironmentBanner] + /// is shown below the title. /// /// The body's background color is always `AppColor.background`. AppScaffold.withTitle({ @@ -35,8 +23,8 @@ class AppScaffold extends StatelessWidget { /// A Scaffold with an empty, 48 dp tall app bar. /// - /// If connected to test envrionment, an environment warning - /// button is shown where the title would normally show. + /// If connected to test environment, an [TestingEnvironmentButton] + /// is shown where the title would normally show. /// /// The body's background color is, by default, the same as the app bar. const AppScaffold.withoutTitle({ @@ -47,8 +35,20 @@ class AppScaffold extends StatelessWidget { applyPadding = false, appBarHeight = 48; + final Widget? title; + final Widget body; + final bool applyPadding; + final Color backgroundColor; + final bool resizeToAvoidBottomInset; + final double? appBarHeight; + + bool get hasTitle => title != null; + @override Widget build(BuildContext context) { + final isTestEnvironment = + context.watch().state.isTestingEnvironment; + return Scaffold( resizeToAvoidBottomInset: resizeToAvoidBottomInset, // The background color is set to avoid a thin line @@ -57,31 +57,26 @@ class AppScaffold extends StatelessWidget { // in the child of the Expanded widget below. backgroundColor: AppColors.primary, appBar: AppBar( - title: hasTitle ? title : const EnvironmentButton(), - centerTitle: hasTitle ? null : true, + title: (!hasTitle && isTestEnvironment) + ? const TestingEnvironmentButton() + : title, + centerTitle: !hasTitle, toolbarHeight: appBarHeight, backgroundColor: AppColors.primary, iconTheme: const IconThemeData(color: AppColors.white), ), - body: BlocBuilder( - builder: (_, state) { - final bool isTestEnvironment = - state is EnvironmentLoaded && state.env.isTest; - - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - if (hasTitle && isTestEnvironment) const EnvironmentBanner(), - Expanded( - child: Container( - padding: applyPadding ? const EdgeInsets.all(16) : null, - color: backgroundColor, - child: body, - ), - ), - ], - ); - }, + body: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + if (hasTitle && isTestEnvironment) const TestingEnvironmentBanner(), + Expanded( + child: Container( + padding: applyPadding ? const EdgeInsets.all(16) : null, + color: backgroundColor, + child: body, + ), + ), + ], ), ); } diff --git a/lib/core/widgets/pages/splash/splash_error_page.dart b/lib/core/widgets/pages/splash/splash_error_page.dart index 4484282d6..bf69bb4f1 100644 --- a/lib/core/widgets/pages/splash/splash_error_page.dart +++ b/lib/core/widgets/pages/splash/splash_error_page.dart @@ -2,13 +2,13 @@ import 'package:coffeecard/core/strings.dart'; import 'package:coffeecard/core/styles/app_text_styles.dart'; import 'package:coffeecard/core/widgets/components/loading_overlay.dart'; import 'package:coffeecard/core/widgets/components/rounded_button.dart'; -import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; +import 'package:coffeecard/features/environment.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:gap/gap.dart'; class SplashErrorPage extends StatefulWidget { - const SplashErrorPage({required this.errorMessage}); + const SplashErrorPage(this.errorMessage); final String errorMessage; @override @@ -23,7 +23,7 @@ class _SplashErrorPageState extends State { // may not be obvious that a load is happening with no internet await Future.wait([ Future.delayed(const Duration(milliseconds: 200)), - context.read().getConfig(), + context.read().loadEnvironment(), ]); if (mounted) { diff --git a/lib/features/environment.dart b/lib/features/environment.dart new file mode 100644 index 000000000..58ad44cef --- /dev/null +++ b/lib/features/environment.dart @@ -0,0 +1 @@ +export 'package:coffeecard/src/environment/environment.dart'; diff --git a/lib/features/environment/data/datasources/environment_remote_data_source.dart b/lib/features/environment/data/datasources/environment_remote_data_source.dart deleted file mode 100644 index 444ff59a2..000000000 --- a/lib/features/environment/data/datasources/environment_remote_data_source.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:coffeecard/core/errors/failures.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() { - return executor - .execute(apiV2.apiV2AppconfigGet) - .map(Environment.fromAppConfig); - } -} diff --git a/lib/features/environment/domain/entities/environment.dart b/lib/features/environment/domain/entities/environment.dart deleted file mode 100644 index 9d5d6db51..000000000 --- a/lib/features/environment/domain/entities/environment.dart +++ /dev/null @@ -1,25 +0,0 @@ -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 deleted file mode 100644 index 29cca2fb3..000000000 --- a/lib/features/environment/domain/usecases/get_environment_type.dart +++ /dev/null @@ -1,14 +0,0 @@ -import 'package:coffeecard/core/errors/failures.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 { - final EnvironmentRemoteDataSource remoteDataSource; - - GetEnvironmentType({required this.remoteDataSource}); - - Future> call() { - return remoteDataSource.getEnvironmentType(); - } -} diff --git a/lib/features/environment/presentation/cubit/environment_cubit.dart b/lib/features/environment/presentation/cubit/environment_cubit.dart deleted file mode 100644 index daeb12805..000000000 --- a/lib/features/environment/presentation/cubit/environment_cubit.dart +++ /dev/null @@ -1,22 +0,0 @@ -import 'package:bloc/bloc.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(); - - either.fold( - (error) => emit(EnvironmentError(error.reason)), - (environment) => emit(EnvironmentLoaded(env: environment)), - ); - } -} diff --git a/lib/features/environment/presentation/cubit/environment_state.dart b/lib/features/environment/presentation/cubit/environment_state.dart deleted file mode 100644 index 76ce03750..000000000 --- a/lib/features/environment/presentation/cubit/environment_state.dart +++ /dev/null @@ -1,28 +0,0 @@ -part of 'environment_cubit.dart'; - -sealed class EnvironmentState extends Equatable { - const EnvironmentState(); -} - -class EnvironmentInitial extends EnvironmentState { - const EnvironmentInitial(); - - @override - List get props => []; -} - -class EnvironmentLoaded extends EnvironmentState { - const EnvironmentLoaded({required this.env}); - final Environment env; - - @override - List get props => [env]; -} - -class EnvironmentError extends EnvironmentState { - const EnvironmentError(this.message); - final String message; - - @override - List get props => [message]; -} diff --git a/lib/features/environment/presentation/widgets/environment_button.dart b/lib/features/environment/presentation/widgets/environment_button.dart deleted file mode 100644 index 3b8241034..000000000 --- a/lib/features/environment/presentation/widgets/environment_button.dart +++ /dev/null @@ -1,80 +0,0 @@ -import 'package:coffeecard/core/strings.dart'; -import 'package:coffeecard/core/styles/app_colors.dart'; -import 'package:coffeecard/core/styles/app_text_styles.dart'; -import 'package:coffeecard/core/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: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: Strings.environmentTitle, - children: [ - Text( - Strings.environmentDescription.first, - style: AppTextStyle.settingKey, - ), - const Gap(8), - Text( - Strings.environmentDescription[1], - style: AppTextStyle.settingKey, - ), - const Gap(8), - Text( - Strings.environmentDescription[2], - style: AppTextStyle.settingKey, - ), - ], - actions: [ - TextButton( - child: const Text(Strings.environmentUnderstood), - onPressed: () => closeAppDialog(context), - ), - ], - dismissible: true, - ), - style: TextButton.styleFrom( - backgroundColor: AppColors.white, - padding: const EdgeInsets.only(left: 16, right: 12), - shape: const StadiumBorder(), - visualDensity: VisualDensity.comfortable, - ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text( - Strings.environmentTitle, - style: AppTextStyle.environmentNotifier, - ), - const Gap(8), - const Icon( - Icons.info_outline, - color: AppColors.primary, - size: 18, - ), - ], - ), - ), - ); - }, - ); - } -} diff --git a/lib/features/receipt/presentation/pages/view_receipt_page.dart b/lib/features/receipt/presentation/pages/view_receipt_page.dart index a1f53855d..64e1abab6 100644 --- a/lib/features/receipt/presentation/pages/view_receipt_page.dart +++ b/lib/features/receipt/presentation/pages/view_receipt_page.dart @@ -1,8 +1,7 @@ import 'package:coffeecard/core/strings.dart'; import 'package:coffeecard/core/widgets/components/helpers/responsive.dart'; import 'package:coffeecard/core/widgets/components/scaffold.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.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/receipt_card.dart'; import 'package:flutter/material.dart'; @@ -17,24 +16,20 @@ class ViewReceiptPage extends StatelessWidget { Widget build(BuildContext context) { return AppScaffold.withTitle( title: Strings.singleReceiptPageTitle, - body: BlocBuilder( - builder: (context, state) { - return Padding( - padding: EdgeInsets.all(deviceIsSmall(context) ? 24 : 48), - child: Column( - children: [ - ReceiptCard( - productName: receipt.menuItemName, - time: receipt.timeUsed, - isInOverlay: false, - isTestEnvironment: - state is EnvironmentLoaded && state.env.isTest, - status: '${Strings.swiped} via ${receipt.productName} ticket', - ), - ], + body: Padding( + padding: EdgeInsets.all(deviceIsSmall(context) ? 24 : 48), + child: Column( + children: [ + ReceiptCard( + productName: receipt.menuItemName, + time: receipt.timeUsed, + isInOverlay: false, + isTestEnvironment: + context.watch().state.isTestingEnvironment, + status: '${Strings.swiped} via ${receipt.productName} ticket', ), - ); - }, + ], + ), ), ); } diff --git a/lib/features/redirection/redirection_router.dart b/lib/features/redirection/redirection_router.dart index a7315758b..0736f1bf6 100644 --- a/lib/features/redirection/redirection_router.dart +++ b/lib/features/redirection/redirection_router.dart @@ -1,7 +1,7 @@ import 'package:coffeecard/core/styles/app_colors.dart'; import 'package:coffeecard/core/widgets/pages/home_page.dart'; import 'package:coffeecard/features/authentication.dart'; -import 'package:coffeecard/features/environment/presentation/cubit/environment_cubit.dart'; +import 'package:coffeecard/features/environment.dart'; import 'package:coffeecard/features/login/presentation/pages/login_page_email.dart'; import 'package:coffeecard/features/product.dart'; import 'package:coffeecard/features/user/presentation/cubit/user_cubit.dart'; diff --git a/lib/features/ticket/presentation/widgets/tickets_section.dart b/lib/features/ticket/presentation/widgets/tickets_section.dart index e29655a78..1e314615e 100644 --- a/lib/features/ticket/presentation/widgets/tickets_section.dart +++ b/lib/features/ticket/presentation/widgets/tickets_section.dart @@ -5,8 +5,7 @@ import 'package:coffeecard/core/widgets/components/error_section.dart'; import 'package:coffeecard/core/widgets/components/loading_overlay.dart'; import 'package:coffeecard/core/widgets/components/section_title.dart'; import 'package:coffeecard/core/widgets/components/tickets_card.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.dart'; import 'package:coffeecard/features/opening_hours/presentation/widgets/opening_hours_indicator.dart'; import 'package:coffeecard/features/receipt/domain/entities/receipt.dart'; import 'package:coffeecard/features/receipt/presentation/widgets/receipt_overlay.dart'; @@ -100,7 +99,7 @@ class TicketSection extends StatelessWidget { productName: receipt is SwipeReceipt ? receipt.menuItemName : receipt.productName, timeUsed: receipt.timeUsed, - isTestEnvironment: envState is EnvironmentLoaded && envState.env.isTest, + isTestEnvironment: envState.isTestingEnvironment, status: receipt is PurchaseReceipt ? receipt.paymentStatus.toString() : '${Strings.swiped} via ${receipt.productName} ticket', diff --git a/lib/service_locator.dart b/lib/service_locator.dart index d0c154369..a5be04a56 100644 --- a/lib/service_locator.dart +++ b/lib/service_locator.dart @@ -9,9 +9,6 @@ import 'package:coffeecard/core/network/network_request_executor.dart'; import 'package:coffeecard/core/store/store.dart'; import 'package:coffeecard/env/env.dart'; import 'package:coffeecard/features/authentication.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'; @@ -117,7 +114,6 @@ Future configureServices() async { initTicket(); initPayment(); initLeaderboard(); - initEnvironment(); initProduct(); initVoucher(); initLogin(); @@ -160,7 +156,6 @@ Future initFeatures() async { initTicket(); initPayment(); initLeaderboard(); - initEnvironment(); initProduct(); initVoucher(); initLogin(); @@ -287,22 +282,6 @@ void initLeaderboard() { ); } -void initEnvironment() { - // bloc - sl.registerLazySingleton(() => EnvironmentCubit(getEnvironmentType: sl())); - - // use case - sl.registerFactory(() => GetEnvironmentType(remoteDataSource: sl())); - - // data source - sl.registerLazySingleton( - () => EnvironmentRemoteDataSource( - apiV2: sl(), - executor: sl(), - ), - ); -} - void initProduct() { // bloc sl.registerFactory(() => ProductCubit(productRepository: sl())); diff --git a/lib/src/authentication/bloc/authentication_cubit.dart b/lib/src/authentication/bloc/authentication_cubit.dart index 66939e288..bbe717d93 100644 --- a/lib/src/authentication/bloc/authentication_cubit.dart +++ b/lib/src/authentication/bloc/authentication_cubit.dart @@ -23,19 +23,15 @@ class AuthenticationCubit extends Cubit { .run(); } - Future authenticated(AuthenticationInfo authenticationInfo) async { - return repository - .saveAuthenticationInfo(authenticationInfo) - .map((_) => AuthenticationState.authenticated(authenticationInfo)) - .map(emit) - .run(); - } + Future authenticated(AuthenticationInfo info) => repository + .saveAuthenticationInfo(info) + .map((_) => AuthenticationState.authenticated(info)) + .map(emit) + .run(); - Future unauthenticated() async { - return repository - .clearAuthenticationInfo() - .map((_) => const AuthenticationState.unauthenticated()) - .map(emit) - .run(); - } + Future unauthenticated() => repository + .clearAuthenticationInfo() + .map((_) => const AuthenticationState.unauthenticated()) + .map(emit) + .run(); } diff --git a/lib/src/environment/bloc/environment_cubit.dart b/lib/src/environment/bloc/environment_cubit.dart new file mode 100644 index 000000000..405057c6e --- /dev/null +++ b/lib/src/environment/bloc/environment_cubit.dart @@ -0,0 +1,25 @@ +import 'package:bloc/bloc.dart'; +import 'package:coffeecard/features/environment.dart'; +import 'package:equatable/equatable.dart'; + +part 'environment_state.dart'; + +class EnvironmentCubit extends Cubit { + EnvironmentCubit({required EnvironmentRepository repository}) + : _repository = repository, + super(const EnvironmentLoading()); + + final EnvironmentRepository _repository; + + Future loadEnvironment() { + emit(const EnvironmentLoading()); + return _repository + .getEnvironment() + .match( + (failure) => EnvironmentLoadError(failure.reason), + EnvironmentLoaded.new, + ) + .map(emit) + .run(); + } +} diff --git a/lib/src/environment/bloc/environment_state.dart b/lib/src/environment/bloc/environment_state.dart new file mode 100644 index 000000000..5fd872cb4 --- /dev/null +++ b/lib/src/environment/bloc/environment_state.dart @@ -0,0 +1,36 @@ +part of 'environment_cubit.dart'; + +sealed class EnvironmentState extends Equatable { + const EnvironmentState(); + + /// Whether the app is running in a testing environment. + bool get isTestingEnvironment { + return switch (this) { + EnvironmentLoaded(env: Environment.testing) => true, + _ => false, + }; + } +} + +class EnvironmentLoading extends EnvironmentState { + const EnvironmentLoading(); + + @override + List get props => []; +} + +class EnvironmentLoaded extends EnvironmentState { + const EnvironmentLoaded(this.env); + final Environment env; + + @override + List get props => [env]; +} + +class EnvironmentLoadError extends EnvironmentState { + const EnvironmentLoadError(this.message); + final String message; + + @override + List get props => [message]; +} diff --git a/lib/src/environment/environment.dart b/lib/src/environment/environment.dart new file mode 100644 index 000000000..1c299f6da --- /dev/null +++ b/lib/src/environment/environment.dart @@ -0,0 +1,5 @@ +export 'bloc/environment_cubit.dart'; +export 'environment_model.dart'; +export 'environment_repository.dart'; +export 'widgets/environment_banner.dart'; +export 'widgets/environment_button.dart'; diff --git a/lib/src/environment/environment_model.dart b/lib/src/environment/environment_model.dart new file mode 100644 index 000000000..6e644aa9d --- /dev/null +++ b/lib/src/environment/environment_model.dart @@ -0,0 +1,4 @@ +enum Environment { + production, + testing, +} diff --git a/lib/src/environment/environment_repository.dart b/lib/src/environment/environment_repository.dart new file mode 100644 index 000000000..48542a8c6 --- /dev/null +++ b/lib/src/environment/environment_repository.dart @@ -0,0 +1,52 @@ +// FIXME: Add linter rule to allow function declarations over variables +// ignore_for_file: prefer_function_declarations_over_variables + +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/environment.dart'; +import 'package:coffeecard/generated/api/coffeecard_api_v2.swagger.dart'; +import 'package:fpdart/fpdart.dart'; + +class EnvironmentRepository { + EnvironmentRepository({ + required CoffeecardApiV2 apiV2, + required NetworkRequestExecutor executor, + }) : _executor = executor, + _apiV2 = apiV2; + + final CoffeecardApiV2 _apiV2; + final NetworkRequestExecutor _executor; + + TaskEither getEnvironment() { + return _executor + .executeAsTask(_apiV2.apiV2AppconfigGet) + .chainEither(_parseSwaggerEnvironmentType) + .chainEither(_getValidEnvironmentType); + } +} + +/// Safely parses the environment type from the API +/// response using the generated Swagger code. +final _parseSwaggerEnvironmentType = (AppConfig config) { + return Either.tryCatch( + () => environmentTypeFromJson(config.environmentType as String), + (_, __) => const UnknownFailure( + 'The response from the server did not contain valid ' + 'environment type information.', + ), + ); +}; + +/// Converts the Swagger-generated environment type +/// to a supported [Environment] type. +final _getValidEnvironmentType = (EnvironmentType swaggerEnvType) { + final r = (Environment env) => Right(env); + final l = (String msg) => Left(UnknownFailure(msg)); + + return switch (swaggerEnvType) { + EnvironmentType.production => r(Environment.production), + EnvironmentType.test => r(Environment.testing), + EnvironmentType.localdevelopment => l('Unsupported environment type.'), + EnvironmentType.swaggerGeneratedUnknown => l('Unknown environment type.'), + }; +}; diff --git a/lib/features/environment/presentation/widgets/environment_banner.dart b/lib/src/environment/widgets/environment_banner.dart similarity index 50% rename from lib/features/environment/presentation/widgets/environment_banner.dart rename to lib/src/environment/widgets/environment_banner.dart index 42f1554db..eebba3bd4 100644 --- a/lib/features/environment/presentation/widgets/environment_banner.dart +++ b/lib/src/environment/widgets/environment_banner.dart @@ -1,15 +1,15 @@ import 'package:coffeecard/core/styles/app_colors.dart'; -import 'package:coffeecard/features/environment/presentation/widgets/environment_button.dart'; +import 'package:coffeecard/features/environment.dart'; import 'package:flutter/material.dart'; -class EnvironmentBanner extends StatelessWidget { - const EnvironmentBanner(); +class TestingEnvironmentBanner extends StatelessWidget { + const TestingEnvironmentBanner(); @override Widget build(BuildContext context) { return const ColoredBox( color: AppColors.primary, - child: Center(child: EnvironmentButton()), + child: Center(child: TestingEnvironmentButton()), ); } } diff --git a/lib/src/environment/widgets/environment_button.dart b/lib/src/environment/widgets/environment_button.dart new file mode 100644 index 000000000..2a7479e04 --- /dev/null +++ b/lib/src/environment/widgets/environment_button.dart @@ -0,0 +1,67 @@ +import 'package:coffeecard/core/strings.dart'; +import 'package:coffeecard/core/styles/app_colors.dart'; +import 'package:coffeecard/core/styles/app_text_styles.dart'; +import 'package:coffeecard/core/widgets/components/dialog.dart'; +import 'package:flutter/material.dart'; +import 'package:gap/gap.dart'; + +class TestingEnvironmentButton extends StatelessWidget { + const TestingEnvironmentButton(); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: 2), + child: TextButton( + onPressed: () => appDialog( + context: context, + title: Strings.environmentTitle, + children: [ + Text( + Strings.environmentDescription.first, + style: AppTextStyle.settingKey, + ), + const Gap(8), + Text( + Strings.environmentDescription[1], + style: AppTextStyle.settingKey, + ), + const Gap(8), + Text( + Strings.environmentDescription[2], + style: AppTextStyle.settingKey, + ), + ], + actions: [ + TextButton( + child: const Text(Strings.environmentUnderstood), + onPressed: () => closeAppDialog(context), + ), + ], + dismissible: true, + ), + style: TextButton.styleFrom( + backgroundColor: AppColors.white, + padding: const EdgeInsets.only(left: 16, right: 12), + shape: const StadiumBorder(), + visualDensity: VisualDensity.comfortable, + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + Strings.environmentTitle, + style: AppTextStyle.environmentNotifier, + ), + const Gap(8), + const Icon( + Icons.info_outline, + color: AppColors.primary, + size: 18, + ), + ], + ), + ), + ); + } +} diff --git a/lib/src/product/functions.dart b/lib/src/product/functions.dart index bc65aa885..55aefeee7 100644 --- a/lib/src/product/functions.dart +++ b/lib/src/product/functions.dart @@ -1,7 +1,6 @@ import 'package:coffeecard/core/strings.dart'; import 'package:coffeecard/core/styles/app_colors.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.dart'; import 'package:coffeecard/features/product.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment.dart'; import 'package:coffeecard/features/purchase/domain/entities/payment_status.dart'; @@ -75,7 +74,7 @@ Future _afterPurchaseModal( ticketsCubit.getTickets(); ReceiptOverlay.show( context: context, - isTestEnvironment: envState is EnvironmentLoaded && envState.env.isTest, + isTestEnvironment: envState.isTestingEnvironment, status: Strings.purchased, productName: payment.productName, timeUsed: payment.purchaseTime, diff --git a/test/features/environment/bloc/environment_cubit_test.dart b/test/features/environment/bloc/environment_cubit_test.dart new file mode 100644 index 000000000..570faa406 --- /dev/null +++ b/test/features/environment/bloc/environment_cubit_test.dart @@ -0,0 +1,72 @@ +import 'package:bloc_test/bloc_test.dart'; +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/features/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'; + +@GenerateNiceMocks([MockSpec()]) +void main() { + late EnvironmentRepository repository; + late EnvironmentCubit cubit; + + setUp(() { + repository = MockEnvironmentRepository(); + cubit = EnvironmentCubit(repository: repository); + }); + + provideDummy( + TaskEither.left(const ConnectionFailure()), + ); + + blocTest( + 'GIVEN the repository returns a Testing environment type ' + 'WHEN loadEnvironment is called ' + 'THEN ' + '1) EnvironmentLoading is emitted ' + '2) EnvironmentLoaded is emitted with Environment.testing', + build: () => cubit, + setUp: () => when(repository.getEnvironment()) + .thenReturn(TaskEither.of(Environment.testing)), + act: (cubit) => cubit.loadEnvironment(), + expect: () => [ + const EnvironmentLoading(), + const EnvironmentLoaded(Environment.testing), + ], + ); + + blocTest( + 'GIVEN the repository returns a Production environment type ' + 'WHEN loadEnvironment is called ' + 'THEN ' + '1) EnvironmentLoading is emitted ' + '2) EnvironmentLoaded is emitted with Environment.production', + build: () => cubit, + setUp: () => when(repository.getEnvironment()) + .thenReturn(TaskEither.of(Environment.production)), + act: (cubit) => cubit.loadEnvironment(), + expect: () => [ + const EnvironmentLoading(), + const EnvironmentLoaded(Environment.production), + ], + ); + + blocTest( + 'GIVEN the repository returns a Failure ' + 'WHEN loadEnvironment is called ' + 'THEN ' + '1) EnvironmentLoading is emitted ' + '2) EnvironmentLoadError is emitted with the failure reason', + build: () => cubit, + setUp: () => when(repository.getEnvironment()) + .thenReturn(TaskEither.left(const UnknownFailure('a'))), + act: (cubit) => cubit.loadEnvironment(), + expect: () => [ + const EnvironmentLoading(), + const EnvironmentLoadError('a'), + ], + ); +} 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 deleted file mode 100644 index 0db38dd59..000000000 --- a/test/features/environment/data/datasources/environment_remote_data_source_test.dart +++ /dev/null @@ -1,43 +0,0 @@ -import 'package:coffeecard/core/errors/failures.dart'; -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, - ); - - provideDummy>( - const Left(ConnectionFailure()), - ); - }); - test('should call executor', () async { - // arrange - when(executor.execute(any)).thenAnswer( - (_) async => const 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 deleted file mode 100644 index da8bd3a91..000000000 --- a/test/features/environment/domain/entities/environment_test.dart +++ /dev/null @@ -1,57 +0,0 @@ -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( - const AppConfig(environmentType: 'Production'), - ); - - // assert - expect(actual, Environment.production); - }, - ); - - test( - 'should return [Test] when environment type is Test', - () { - // act - final actual = - Environment.fromAppConfig(const AppConfig(environmentType: 'Test')); - - // assert - expect(actual, Environment.test); - }, - ); - - test( - 'should return [Test] when environment type is LocalDevelopment', - () { - // act - final actual = Environment.fromAppConfig( - const AppConfig(environmentType: 'LocalDevelopment'), - ); - - // assert - expect(actual, Environment.test); - }, - ); - - test( - 'should return [Unknown] when environment type cannot be parsed', - () { - // act - final actual = - Environment.fromAppConfig(const 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 deleted file mode 100644 index 323e44575..000000000 --- a/test/features/environment/domain/usecases/get_environment_type_test.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:coffeecard/core/errors/failures.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); - - provideDummy>( - const Left(ConnectionFailure()), - ); - }); - - test('should call data source', () async { - // arrange - when(remoteDataSource.getEnvironmentType()) - .thenAnswer((_) async => const Right(Environment.production)); - - // act - await usecase(); - - // assert - verify(remoteDataSource.getEnvironmentType()); - }); -} diff --git a/test/features/environment/environment_repository_test.dart b/test/features/environment/environment_repository_test.dart new file mode 100644 index 000000000..e2c34d918 --- /dev/null +++ b/test/features/environment/environment_repository_test.dart @@ -0,0 +1,105 @@ +import 'package:coffeecard/core/errors/failures.dart'; +import 'package:coffeecard/core/network/network_request_executor.dart'; +import 'package:coffeecard/features/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_repository_test.mocks.dart'; + +@GenerateNiceMocks([ + MockSpec(), + MockSpec(), +]) +void main() { + late CoffeecardApiV2 apiV2; + late NetworkRequestExecutor executor; + late EnvironmentRepository repository; + + setUp(() { + apiV2 = MockCoffeecardApiV2(); + executor = MockNetworkRequestExecutor(); + repository = EnvironmentRepository(apiV2: apiV2, executor: executor); + }); + + provideDummy(TaskEither.left(const ConnectionFailure())); + + test( + 'GIVEN the API returns a Testing environment type ' + 'WHEN getEnvironment is called ' + 'THEN ' + '1) Executor.executeAsTask is called with apiV2.apiV2AppconfigGet, ' + '2) Environment.testing is returned in a Right', + () async { + // arrange + when(executor.executeAsTask(apiV2.apiV2AppconfigGet)) + .thenReturn(TaskEither.of(const AppConfig(environmentType: 'Test'))); + + // act + final result = await repository.getEnvironment().run(); + + // assert + verify(executor.executeAsTask(apiV2.apiV2AppconfigGet)).called(1); + verifyNoMoreInteractions(executor); + expect( + result, + isA>() + .having((r) => r.value, 'Environment', Environment.testing), + ); + }, + ); + + test( + 'GIVEN the API returns a Production environment type ' + 'WHEN getEnvironment is called ' + 'THEN ' + '1) Executor.executeAsTask is called with apiV2.apiV2AppconfigGet, ' + '2) Environment.production is returned in a Right', + () async { + // arrange + when(executor.executeAsTask(apiV2.apiV2AppconfigGet)).thenReturn( + TaskEither.of(const AppConfig(environmentType: 'Production')), + ); + + // act + final result = await repository.getEnvironment().run(); + + // assert + verify(executor.executeAsTask(apiV2.apiV2AppconfigGet)).called(1); + verifyNoMoreInteractions(executor); + expect( + result, + isA>() + .having((r) => r.value, 'Environment', Environment.production), + ); + }, + ); + + test( + 'GIVEN the API returns an unsupported environment type ' + 'WHEN getEnvironment is called ' + 'THEN ' + '1) Executor.executeAsTask is called with apiV2.apiV2AppconfigGet, ' + '2) UnknownFailure is returned in a Left', + () async { + // arrange + when(executor.executeAsTask(apiV2.apiV2AppconfigGet)).thenReturn( + TaskEither.of(const AppConfig(environmentType: 'LocalDevelopment')), + ); + + // act + final result = await repository.getEnvironment().run(); + + // assert + verify(executor.executeAsTask(apiV2.apiV2AppconfigGet)).called(1); + verifyNoMoreInteractions(executor); + expect( + result, + isA>() + .having((l) => l.value, 'Failure', isA()), + ); + }, + ); +} diff --git a/test/features/environment/presentation/cubit/environment_cubit_test.dart b/test/features/environment/presentation/cubit/environment_cubit_test.dart deleted file mode 100644 index 03f4b76a9..000000000 --- a/test/features/environment/presentation/cubit/environment_cubit_test.dart +++ /dev/null @@ -1,55 +0,0 @@ -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() { - late EnvironmentCubit cubit; - late MockGetEnvironmentType getEnvironmentType; - - setUp(() { - getEnvironmentType = MockGetEnvironmentType(); - cubit = EnvironmentCubit(getEnvironmentType: getEnvironmentType); - - provideDummy>( - const Left(ConnectionFailure()), - ); - }); - - test('initial state is [Initial]', () { - expect(cubit.state, const EnvironmentInitial()); - }); - - group('getConfig', () { - blocTest( - 'should emit [Loaded] when usecase suceeds', - build: () => cubit, - setUp: () { - when(getEnvironmentType()) - .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()).thenAnswer( - (_) async => const Left(ServerFailure('some error', 500)), - ); - }, - act: (cubit) => cubit.getConfig(), - expect: () => [const EnvironmentError('some error')], - ); - }); -}