From cdc16b9a9cf43e97a0be498d6345558e0effde1d Mon Sep 17 00:00:00 2001 From: Username1548 Date: Fri, 21 Mar 2025 20:40:48 +0700 Subject: [PATCH] card adding and selecting default one --- .metadata | 25 +- assets/icons/add_card.svg | 3 + assets/icons/card.svg | 5 + ios/Runner.xcodeproj/project.pbxproj | 12 +- .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 0 -> 406 bytes lib/core/app_router.dart | 4 +- lib/core/const.dart | 4 +- lib/core/locator_service.dart | 6 + .../bloc/recipes_cubit/recipes_cubit.dart | 4 +- .../menu/presentation/pages/menu_screen.dart | 129 +++------- .../my_finance/models/card_model.dart | 23 ++ .../my_finance/models/card_model.freezed.dart | 232 ++++++++++++++++++ .../my_finance/models/card_model.g.dart | 21 ++ .../my_finance/models/payment_url_model.dart | 14 ++ .../models/payment_url_model.freezed.dart | 174 +++++++++++++ .../models/payment_url_model.g.dart | 15 ++ .../user_cards_cubit/user_cards_cubit.dart | 66 +++++ .../user_cards_cubit/user_cards_state.dart | 14 ++ .../user_cards_state.freezed.dart | 186 ++++++++++++++ .../presentation/pages/card_settings.dart | 115 ++++----- .../pages/user_finance_screen.dart | 160 +++++------- .../presentation/widgets/card_widget.dart | 68 +++++ .../repositories/cards_repository.dart | 39 +++ .../my_finance/services/cards_service.dart | 28 +++ .../my_finance/services/cards_service.g.dart | 141 +++++++++++ lib/generated/intl/messages_en.dart | 5 + lib/generated/l10n.dart | 20 ++ lib/l10n/intl_en.arb | 5 +- pubspec.yaml | 2 +- test/widget_test.dart | 30 +++ 30 files changed, 1254 insertions(+), 296 deletions(-) create mode 100644 assets/icons/add_card.svg create mode 100644 assets/icons/card.svg create mode 100644 ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png create mode 100644 lib/features/my_finance/models/card_model.dart create mode 100644 lib/features/my_finance/models/card_model.freezed.dart create mode 100644 lib/features/my_finance/models/card_model.g.dart create mode 100644 lib/features/my_finance/models/payment_url_model.dart create mode 100644 lib/features/my_finance/models/payment_url_model.freezed.dart create mode 100644 lib/features/my_finance/models/payment_url_model.g.dart create mode 100644 lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart create mode 100644 lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart create mode 100644 lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.freezed.dart create mode 100644 lib/features/my_finance/presentation/widgets/card_widget.dart create mode 100644 lib/features/my_finance/repositories/cards_repository.dart create mode 100644 lib/features/my_finance/services/cards_service.dart create mode 100644 lib/features/my_finance/services/cards_service.g.dart create mode 100644 test/widget_test.dart diff --git a/.metadata b/.metadata index aa90aa80..5d420ae5 100644 --- a/.metadata +++ b/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: "ba393198430278b6595976de84fe170f553cc728" + revision: "09de023485e95e6d1225c2baa44b8feb85e0d45f" channel: "stable" project_type: app @@ -13,26 +13,11 @@ project_type: app migration: platforms: - platform: root - create_revision: ba393198430278b6595976de84fe170f553cc728 - base_revision: ba393198430278b6595976de84fe170f553cc728 - - platform: android - create_revision: ba393198430278b6595976de84fe170f553cc728 - base_revision: ba393198430278b6595976de84fe170f553cc728 + create_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f + base_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f - platform: ios - create_revision: ba393198430278b6595976de84fe170f553cc728 - base_revision: ba393198430278b6595976de84fe170f553cc728 - - platform: linux - create_revision: ba393198430278b6595976de84fe170f553cc728 - base_revision: ba393198430278b6595976de84fe170f553cc728 - - platform: macos - create_revision: ba393198430278b6595976de84fe170f553cc728 - base_revision: ba393198430278b6595976de84fe170f553cc728 - - platform: web - create_revision: ba393198430278b6595976de84fe170f553cc728 - base_revision: ba393198430278b6595976de84fe170f553cc728 - - platform: windows - create_revision: ba393198430278b6595976de84fe170f553cc728 - base_revision: ba393198430278b6595976de84fe170f553cc728 + create_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f + base_revision: 09de023485e95e6d1225c2baa44b8feb85e0d45f # User provided section diff --git a/assets/icons/add_card.svg b/assets/icons/add_card.svg new file mode 100644 index 00000000..4acb17cd --- /dev/null +++ b/assets/icons/add_card.svg @@ -0,0 +1,3 @@ + + + diff --git a/assets/icons/card.svg b/assets/icons/card.svg new file mode 100644 index 00000000..38c03089 --- /dev/null +++ b/assets/icons/card.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index a0fd3539..7fbba8a2 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -365,7 +365,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 49; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -383,7 +383,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.47; + MARKETING_VERSION = 1.0.49; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -514,7 +514,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 49; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -532,7 +532,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.47; + MARKETING_VERSION = 1.0.49; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; @@ -551,7 +551,7 @@ CLANG_ENABLE_MODULES = YES; CODE_SIGN_IDENTITY = "Apple Development"; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 47; + CURRENT_PROJECT_VERSION = 49; DEVELOPMENT_TEAM = VL68C36HYS; ENABLE_BITCODE = NO; FRAMEWORK_SEARCH_PATHS = ( @@ -569,7 +569,7 @@ "$(inherited)", "$(PROJECT_DIR)/Flutter", ); - MARKETING_VERSION = 1.0.47; + MARKETING_VERSION = 1.0.49; PRODUCT_BUNDLE_IDENTIFIER = com.eatvioapp.app; PRODUCT_NAME = "$(TARGET_NAME)"; PROVISIONING_PROFILE_SPECIFIER = ""; diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..797d452e458972bab9d994556c8305db4c827017 GIT binary patch literal 406 zcmV;H0crk;P))>cdjpWt&rLJgVp-t?DREyuq1A%0Z4)6_WsQ7{nzjN zo!X zGXV)2i3kcZIL~_j>uIKPK_zib+3T+Nt3Mb&Br)s)UIaA}@p{wDda>7=Q|mGRp7pqY zkJ!7E{MNz$9nOwoVqpFb)}$IP24Wn2JJ=Cw(!`OXJBr45rP>>AQr$6c7slJWvbpNW z@KTwna6d?PP>hvXCcp=4F;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f*5nx ACIA2c literal 0 HcmV?d00001 diff --git a/lib/core/app_router.dart b/lib/core/app_router.dart index 1e8a78d0..527e58d7 100644 --- a/lib/core/app_router.dart +++ b/lib/core/app_router.dart @@ -158,8 +158,8 @@ class AppRouter { builder: (context, state) => UserFinanceScreen(), ), GoRoute( - path: '/card-settings-screen', - name: 'cardSettingsScreen', + path: CardSettingsScreen.path, + name: CardSettingsScreen.name, builder: (context, state) => CardSettingsScreen(), ), GoRoute( diff --git a/lib/core/const.dart b/lib/core/const.dart index 4f38a54e..31c47e9c 100644 --- a/lib/core/const.dart +++ b/lib/core/const.dart @@ -1,5 +1,5 @@ -// String baseUrl = 'https://eatvio.ru'; -String baseUrl = 'https://dev2.eatvio.com'; +String baseUrl = 'https://eatvio.ru'; +// String baseUrl = 'https://dev2.eatvio.com'; const List durationValues = [7, 14, 21]; diff --git a/lib/core/locator_service.dart b/lib/core/locator_service.dart index 99846f1a..3a3f5342 100644 --- a/lib/core/locator_service.dart +++ b/lib/core/locator_service.dart @@ -49,6 +49,9 @@ import 'package:eatvio_app/features/feed/presentation/bloc/plans_cubit/plans_cub import 'package:eatvio_app/features/my_diets/presentation/cubit/my_diets_cubit.dart'; import 'package:eatvio_app/features/my_diets/repositories/my_diets_repository.dart'; import 'package:eatvio_app/features/my_diets/services/my_diets_service.dart'; +import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; +import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; +import 'package:eatvio_app/features/my_finance/services/cards_service.dart'; import 'package:eatvio_app/features/my_plans/presentation/cubit/my_plans_cubit.dart'; import 'package:eatvio_app/features/my_plans/repositories/my_diets_repository.dart'; import 'package:eatvio_app/features/my_plans/services/my_plans_service.dart'; @@ -117,6 +120,7 @@ Future init() async { sl.registerFactory(() => AboutRecipeCubit(recipeRepository: sl(), profileRepository: sl())); sl.registerFactory(() => AuthCubit(authRepository: sl())); sl.registerFactory(() => ComplainingCubit(repository: sl())); + sl.registerFactory(() => UserCardsCubit(cardsRepository: sl())); // Repository sl.registerLazySingleton(() => ChatRepository(chatService: sl())); @@ -139,6 +143,7 @@ Future init() async { sl.registerLazySingleton(() => AboutRecipeRepository(recipeService: sl())); sl.registerLazySingleton(() => ComplainingRepository(service: sl())); sl.registerLazySingleton(() => AuthRepository(authService: sl())); + sl.registerLazySingleton(() => CardsRepository(cardsService: sl())); // Services sl.registerLazySingleton(() => ChatService(sl())); @@ -162,6 +167,7 @@ Future init() async { sl.registerLazySingleton(() => AboutRecipeService(sl())); sl.registerLazySingleton(() => ComplainingService(sl())); sl.registerLazySingleton(() => AuthService(sl())); + sl.registerLazySingleton(() => CardsService(sl())); // External sl.registerLazySingleton( diff --git a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart index 1eb1027f..312a27c3 100644 --- a/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart +++ b/lib/features/feed/presentation/bloc/recipes_cubit/recipes_cubit.dart @@ -63,7 +63,7 @@ class RecipesCubit extends Cubit { lastPage = currentPage; } catch (e, st) { log(e.toString(), stackTrace: st); - emit(state.copyWith(errorMessage: e.toString())); + emit(state.copyWith(errorMessage: e.toString(), isLoading: false)); } } @@ -94,7 +94,7 @@ class RecipesCubit extends Cubit { } lastPage = currentPage; } catch (e) { - emit(state.copyWith(errorMessage: e.toString())); + emit(state.copyWith(errorMessage: e.toString(), isLoading: false)); } } } diff --git a/lib/features/menu/presentation/pages/menu_screen.dart b/lib/features/menu/presentation/pages/menu_screen.dart index 41c9706c..a03bd3e3 100644 --- a/lib/features/menu/presentation/pages/menu_screen.dart +++ b/lib/features/menu/presentation/pages/menu_screen.dart @@ -16,54 +16,32 @@ import 'package:go_router/go_router.dart'; class MenuScreen extends StatelessWidget { final ProfileModel profileModel; - const MenuScreen({ - super.key, - required this.profileModel, - }); + const MenuScreen({super.key, required this.profileModel}); @override Widget build(BuildContext context) { return Scaffold( - appBar: AppBar( - centerTitle: false, - title: Text('Меню'), - ), + appBar: AppBar(centerTitle: false, title: Text('Меню')), body: SingleChildScrollView( child: Column( children: [ - _tile( - title: 'Настройки профиля', - leading: ImageConstant.icnProfileSettings, - onTap: () => GoRouter.of(context).push('/settings-screen'), - ), + _tile(title: 'Настройки профиля', leading: ImageConstant.icnProfileSettings, onTap: () => GoRouter.of(context).push('/settings-screen')), if (profileModel.roles.contains('consultant')) _divider(context), if (profileModel.roles.contains('consultant')) - _tile( - leading: ImageConstant.icnRecipes, - title: 'Мои рецепты', - onTap: () => GoRouter.of(context).push('/recipe-select-screen'), - ), + _tile(leading: ImageConstant.icnRecipes, title: 'Мои рецепты', onTap: () => GoRouter.of(context).push('/recipe-select-screen')), if (profileModel.roles.contains('consultant')) _divider(context), if (profileModel.roles.contains('consultant')) - _tile( - leading: 'assets/icons/diets.svg', - title: 'Мои диеты', - onTap: () => GoRouter.of(context).push(MyDietsScreen.path), - ), + _tile(leading: 'assets/icons/diets.svg', title: 'Мои диеты', onTap: () => GoRouter.of(context).push(MyDietsScreen.path)), if (profileModel.roles.contains('consultant')) _divider(context), if (profileModel.roles.contains('consultant')) - _tile( - leading: ImageConstant.icnPlans, - title: 'Мои планы', - onTap: () => GoRouter.of(context).push(MyPlansScreen.path), - ), + _tile(leading: ImageConstant.icnPlans, title: 'Мои планы', onTap: () => GoRouter.of(context).push(MyPlansScreen.path)), _divider(context), _tile( leading: 'assets/icons/finance.svg', title: 'Мои финансы', onTap: () { if (profileModel.roles.contains('consultant')) { - GoRouter.of(context).pushNamed('consultantFinanceScreen'); + GoRouter.of(context).pushNamed('userFinanceScreen'); } else { GoRouter.of(context).pushNamed('userFinanceScreen'); } @@ -76,17 +54,9 @@ class MenuScreen extends StatelessWidget { // onTap: () => GoRouter.of(context).push('/calendar-screen'), // ), _divider(context), - _tile( - leading: ImageConstant.icnGlossary, - title: 'Глоссарий', - onTap: () => GoRouter.of(context).push('/glossary-screen'), - ), + _tile(leading: ImageConstant.icnGlossary, title: 'Глоссарий', onTap: () => GoRouter.of(context).push('/glossary-screen')), _divider(context), - _tile( - leading: ImageConstant.icnSupport, - title: 'Поддержка', - onTap: () => GoRouter.of(context).push('/support-screen'), - ), + _tile(leading: ImageConstant.icnSupport, title: 'Поддержка', onTap: () => GoRouter.of(context).push('/support-screen')), _divider(context), _tile( leading: ImageConstant.icnDevTools, @@ -96,36 +66,19 @@ class MenuScreen extends StatelessWidget { }, ), _divider(context), - _tile( - leading: ImageConstant.icnExit, - title: 'Выход', - onTap: _onTapLogout(context), - ), + _tile(leading: ImageConstant.icnExit, title: 'Выход', onTap: _onTapLogout(context)), ], ), ), ); } - _tile({ - required String title, - required String leading, - required Function() onTap, - }) { + _tile({required String title, required String leading, required Function() onTap}) { return ListTile( onTap: onTap, - leading: SvgPicture.asset( - leading, - height: 24, - width: 24, - colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn), - ), + leading: SvgPicture.asset(leading, height: 24, width: 24, colorFilter: ColorFilter.mode(appTheme.blueA400, BlendMode.srcIn)), title: Text(title), - trailing: Icon( - Icons.arrow_forward_ios_rounded, - size: 12, - color: Colors.black38, - ), + trailing: Icon(Icons.arrow_forward_ios_rounded, size: 12, color: Colors.black38), ); } @@ -133,10 +86,7 @@ class MenuScreen extends StatelessWidget { return Container( height: 0.4, width: MediaQuery.of(context).size.width - 32, - decoration: BoxDecoration( - color: Colors.black26, - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(color: Colors.black26, borderRadius: BorderRadius.circular(16)), ); } @@ -144,33 +94,32 @@ class MenuScreen extends StatelessWidget { return () async { showDialog( context: context, - builder: (context) => CupertinoAlertDialog( - title: Text("Выход"), - actions: [ - CupertinoDialogAction( - onPressed: () async { - try { - await sl().post( - baseUrl + '/api/profile/logout', - ); - } catch (e) { - } finally { - await TokenStorage.removeTokenInStorage(); - GoRouter.of(context).go('/auth-screen', extra: true); - } - }, - child: Text("Выйти"), - isDestructiveAction: true, + builder: + (context) => CupertinoAlertDialog( + title: Text("Выход"), + actions: [ + CupertinoDialogAction( + onPressed: () async { + try { + await sl().post(baseUrl + '/api/profile/logout'); + } catch (e) { + } finally { + await TokenStorage.removeTokenInStorage(); + GoRouter.of(context).go('/auth-screen', extra: true); + } + }, + child: Text("Выйти"), + isDestructiveAction: true, + ), + CupertinoDialogAction( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text("Отмена"), + ), + ], + content: Text("Вы уверены что хотите выйти из аккаунта?"), ), - CupertinoDialogAction( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text("Отмена"), - ), - ], - content: Text("Вы уверены что хотите выйти из аккаунта?"), - ), ); }; } diff --git a/lib/features/my_finance/models/card_model.dart b/lib/features/my_finance/models/card_model.dart new file mode 100644 index 00000000..c923519a --- /dev/null +++ b/lib/features/my_finance/models/card_model.dart @@ -0,0 +1,23 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'card_model.freezed.dart'; +part 'card_model.g.dart'; + +@freezed +class CardModel with _$CardModel { + + factory CardModel({ + required int id, + @JsonKey(name: 'bank_pan') required String cardNumber, + @JsonKey(name: 'is_default', fromJson: parseBool) required bool isDefault, + }) = _CardModel; + + factory CardModel.fromJson(Map json) => _$CardModelFromJson(json); +} + +bool parseBool(json) { + if (json == 1 || json == true) { + return true; + } + return false; +} diff --git a/lib/features/my_finance/models/card_model.freezed.dart b/lib/features/my_finance/models/card_model.freezed.dart new file mode 100644 index 00000000..81179841 --- /dev/null +++ b/lib/features/my_finance/models/card_model.freezed.dart @@ -0,0 +1,232 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'card_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +CardModel _$CardModelFromJson(Map json) { + return _CardModel.fromJson(json); +} + +/// @nodoc +mixin _$CardModel { + int get id => throw _privateConstructorUsedError; + @JsonKey(name: 'bank_pan') + String get cardNumber => throw _privateConstructorUsedError; + @JsonKey(name: 'is_default', fromJson: parseBool) + bool get isDefault => throw _privateConstructorUsedError; + + /// Serializes this CardModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of CardModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $CardModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $CardModelCopyWith<$Res> { + factory $CardModelCopyWith(CardModel value, $Res Function(CardModel) then) = + _$CardModelCopyWithImpl<$Res, CardModel>; + @useResult + $Res call({ + int id, + @JsonKey(name: 'bank_pan') String cardNumber, + @JsonKey(name: 'is_default', fromJson: parseBool) bool isDefault, + }); +} + +/// @nodoc +class _$CardModelCopyWithImpl<$Res, $Val extends CardModel> + implements $CardModelCopyWith<$Res> { + _$CardModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of CardModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? cardNumber = null, + Object? isDefault = null, + }) { + return _then( + _value.copyWith( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + cardNumber: + null == cardNumber + ? _value.cardNumber + : cardNumber // ignore: cast_nullable_to_non_nullable + as String, + isDefault: + null == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$CardModelImplCopyWith<$Res> + implements $CardModelCopyWith<$Res> { + factory _$$CardModelImplCopyWith( + _$CardModelImpl value, + $Res Function(_$CardModelImpl) then, + ) = __$$CardModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({ + int id, + @JsonKey(name: 'bank_pan') String cardNumber, + @JsonKey(name: 'is_default', fromJson: parseBool) bool isDefault, + }); +} + +/// @nodoc +class __$$CardModelImplCopyWithImpl<$Res> + extends _$CardModelCopyWithImpl<$Res, _$CardModelImpl> + implements _$$CardModelImplCopyWith<$Res> { + __$$CardModelImplCopyWithImpl( + _$CardModelImpl _value, + $Res Function(_$CardModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of CardModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({ + Object? id = null, + Object? cardNumber = null, + Object? isDefault = null, + }) { + return _then( + _$CardModelImpl( + id: + null == id + ? _value.id + : id // ignore: cast_nullable_to_non_nullable + as int, + cardNumber: + null == cardNumber + ? _value.cardNumber + : cardNumber // ignore: cast_nullable_to_non_nullable + as String, + isDefault: + null == isDefault + ? _value.isDefault + : isDefault // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$CardModelImpl implements _CardModel { + _$CardModelImpl({ + required this.id, + @JsonKey(name: 'bank_pan') required this.cardNumber, + @JsonKey(name: 'is_default', fromJson: parseBool) required this.isDefault, + }); + + factory _$CardModelImpl.fromJson(Map json) => + _$$CardModelImplFromJson(json); + + @override + final int id; + @override + @JsonKey(name: 'bank_pan') + final String cardNumber; + @override + @JsonKey(name: 'is_default', fromJson: parseBool) + final bool isDefault; + + @override + String toString() { + return 'CardModel(id: $id, cardNumber: $cardNumber, isDefault: $isDefault)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$CardModelImpl && + (identical(other.id, id) || other.id == id) && + (identical(other.cardNumber, cardNumber) || + other.cardNumber == cardNumber) && + (identical(other.isDefault, isDefault) || + other.isDefault == isDefault)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, id, cardNumber, isDefault); + + /// Create a copy of CardModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$CardModelImplCopyWith<_$CardModelImpl> get copyWith => + __$$CardModelImplCopyWithImpl<_$CardModelImpl>(this, _$identity); + + @override + Map toJson() { + return _$$CardModelImplToJson(this); + } +} + +abstract class _CardModel implements CardModel { + factory _CardModel({ + required final int id, + @JsonKey(name: 'bank_pan') required final String cardNumber, + @JsonKey(name: 'is_default', fromJson: parseBool) + required final bool isDefault, + }) = _$CardModelImpl; + + factory _CardModel.fromJson(Map json) = + _$CardModelImpl.fromJson; + + @override + int get id; + @override + @JsonKey(name: 'bank_pan') + String get cardNumber; + @override + @JsonKey(name: 'is_default', fromJson: parseBool) + bool get isDefault; + + /// Create a copy of CardModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$CardModelImplCopyWith<_$CardModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/my_finance/models/card_model.g.dart b/lib/features/my_finance/models/card_model.g.dart new file mode 100644 index 00000000..30fcc614 --- /dev/null +++ b/lib/features/my_finance/models/card_model.g.dart @@ -0,0 +1,21 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'card_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$CardModelImpl _$$CardModelImplFromJson(Map json) => + _$CardModelImpl( + id: (json['id'] as num).toInt(), + cardNumber: json['bank_pan'] as String, + isDefault: parseBool(json['is_default']), + ); + +Map _$$CardModelImplToJson(_$CardModelImpl instance) => + { + 'id': instance.id, + 'bank_pan': instance.cardNumber, + 'is_default': instance.isDefault, + }; diff --git a/lib/features/my_finance/models/payment_url_model.dart b/lib/features/my_finance/models/payment_url_model.dart new file mode 100644 index 00000000..cad57efc --- /dev/null +++ b/lib/features/my_finance/models/payment_url_model.dart @@ -0,0 +1,14 @@ +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'payment_url_model.freezed.dart'; +part 'payment_url_model.g.dart'; + +@freezed +class PaymentUrlModel with _$PaymentUrlModel { + + factory PaymentUrlModel({ + required String paymentUrl, + }) = _PaymentUrlModel; + + factory PaymentUrlModel.fromJson(Map json) => _$PaymentUrlModelFromJson(json); +} diff --git a/lib/features/my_finance/models/payment_url_model.freezed.dart b/lib/features/my_finance/models/payment_url_model.freezed.dart new file mode 100644 index 00000000..2200800c --- /dev/null +++ b/lib/features/my_finance/models/payment_url_model.freezed.dart @@ -0,0 +1,174 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'payment_url_model.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +PaymentUrlModel _$PaymentUrlModelFromJson(Map json) { + return _PaymentUrlModel.fromJson(json); +} + +/// @nodoc +mixin _$PaymentUrlModel { + String get paymentUrl => throw _privateConstructorUsedError; + + /// Serializes this PaymentUrlModel to a JSON map. + Map toJson() => throw _privateConstructorUsedError; + + /// Create a copy of PaymentUrlModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $PaymentUrlModelCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $PaymentUrlModelCopyWith<$Res> { + factory $PaymentUrlModelCopyWith( + PaymentUrlModel value, + $Res Function(PaymentUrlModel) then, + ) = _$PaymentUrlModelCopyWithImpl<$Res, PaymentUrlModel>; + @useResult + $Res call({String paymentUrl}); +} + +/// @nodoc +class _$PaymentUrlModelCopyWithImpl<$Res, $Val extends PaymentUrlModel> + implements $PaymentUrlModelCopyWith<$Res> { + _$PaymentUrlModelCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of PaymentUrlModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? paymentUrl = null}) { + return _then( + _value.copyWith( + paymentUrl: + null == paymentUrl + ? _value.paymentUrl + : paymentUrl // ignore: cast_nullable_to_non_nullable + as String, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$PaymentUrlModelImplCopyWith<$Res> + implements $PaymentUrlModelCopyWith<$Res> { + factory _$$PaymentUrlModelImplCopyWith( + _$PaymentUrlModelImpl value, + $Res Function(_$PaymentUrlModelImpl) then, + ) = __$$PaymentUrlModelImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({String paymentUrl}); +} + +/// @nodoc +class __$$PaymentUrlModelImplCopyWithImpl<$Res> + extends _$PaymentUrlModelCopyWithImpl<$Res, _$PaymentUrlModelImpl> + implements _$$PaymentUrlModelImplCopyWith<$Res> { + __$$PaymentUrlModelImplCopyWithImpl( + _$PaymentUrlModelImpl _value, + $Res Function(_$PaymentUrlModelImpl) _then, + ) : super(_value, _then); + + /// Create a copy of PaymentUrlModel + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? paymentUrl = null}) { + return _then( + _$PaymentUrlModelImpl( + paymentUrl: + null == paymentUrl + ? _value.paymentUrl + : paymentUrl // ignore: cast_nullable_to_non_nullable + as String, + ), + ); + } +} + +/// @nodoc +@JsonSerializable() +class _$PaymentUrlModelImpl implements _PaymentUrlModel { + _$PaymentUrlModelImpl({required this.paymentUrl}); + + factory _$PaymentUrlModelImpl.fromJson(Map json) => + _$$PaymentUrlModelImplFromJson(json); + + @override + final String paymentUrl; + + @override + String toString() { + return 'PaymentUrlModel(paymentUrl: $paymentUrl)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$PaymentUrlModelImpl && + (identical(other.paymentUrl, paymentUrl) || + other.paymentUrl == paymentUrl)); + } + + @JsonKey(includeFromJson: false, includeToJson: false) + @override + int get hashCode => Object.hash(runtimeType, paymentUrl); + + /// Create a copy of PaymentUrlModel + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$PaymentUrlModelImplCopyWith<_$PaymentUrlModelImpl> get copyWith => + __$$PaymentUrlModelImplCopyWithImpl<_$PaymentUrlModelImpl>( + this, + _$identity, + ); + + @override + Map toJson() { + return _$$PaymentUrlModelImplToJson(this); + } +} + +abstract class _PaymentUrlModel implements PaymentUrlModel { + factory _PaymentUrlModel({required final String paymentUrl}) = + _$PaymentUrlModelImpl; + + factory _PaymentUrlModel.fromJson(Map json) = + _$PaymentUrlModelImpl.fromJson; + + @override + String get paymentUrl; + + /// Create a copy of PaymentUrlModel + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$PaymentUrlModelImplCopyWith<_$PaymentUrlModelImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/my_finance/models/payment_url_model.g.dart b/lib/features/my_finance/models/payment_url_model.g.dart new file mode 100644 index 00000000..9bef3b73 --- /dev/null +++ b/lib/features/my_finance/models/payment_url_model.g.dart @@ -0,0 +1,15 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'payment_url_model.dart'; + +// ************************************************************************** +// JsonSerializableGenerator +// ************************************************************************** + +_$PaymentUrlModelImpl _$$PaymentUrlModelImplFromJson( + Map json, +) => _$PaymentUrlModelImpl(paymentUrl: json['paymentUrl'] as String); + +Map _$$PaymentUrlModelImplToJson( + _$PaymentUrlModelImpl instance, +) => {'paymentUrl': instance.paymentUrl}; diff --git a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart new file mode 100644 index 00000000..a2c22389 --- /dev/null +++ b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart @@ -0,0 +1,66 @@ +import 'dart:developer'; + +import 'package:easy_debounce/easy_debounce.dart'; +import 'package:eatvio_app/features/my_finance/models/payment_url_model.dart'; +import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart'; +import 'package:eatvio_app/features/my_finance/repositories/cards_repository.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; + +class UserCardsCubit extends Cubit { + final CardsRepository cardsRepository; + UserCardsCubit({required this.cardsRepository}) : super(UserCardsState()); + + Future addCard() async { + try { + return await cardsRepository.addCard(); + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + return null; + } + + Future getCards() async { + try { + if (isClosed) return; + final cards = await cardsRepository.getCards(); + emit(state.copyWith(cards: cards)); + } catch (e, st) { + log(e.toString(), stackTrace: st); + } + } + + Future markAsDefault({required int id}) async { + final oldCards = [...state.cards]; + if (isClosed) return; + final cards = + oldCards.map((card) { + if (card.id == id) { + return card.copyWith(isDefault: true); + } + return card.copyWith(isDefault: false); + }).toList(); + emit(state.copyWith(cards: cards)); + + EasyDebounce.debounce('markAsDefault', Duration(milliseconds: id), () async { + try { + await cardsRepository.markAsDefault(id: id); + } catch (e, st) { + emit(state.copyWith(cards: oldCards)); + log(e.toString(), stackTrace: st); + } + }); + } + + Future deleteCard({required int id}) async { + final oldCards = [...state.cards]; + try { + if (isClosed) return; + final cards = oldCards.where((card) => card.id != id).toList(); + emit(state.copyWith(cards: cards)); + await cardsRepository.deleteCard(id: id); + } catch (e, st) { + emit(state.copyWith(cards: oldCards)); + log(e.toString(), stackTrace: st); + } + } +} diff --git a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart new file mode 100644 index 00000000..d9fcd376 --- /dev/null +++ b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart @@ -0,0 +1,14 @@ +import 'package:eatvio_app/features/my_finance/models/card_model.dart'; +import 'package:freezed_annotation/freezed_annotation.dart'; + +part 'user_cards_state.freezed.dart'; + +@freezed +class UserCardsState with _$UserCardsState { + + factory UserCardsState({ + @Default([]) List cards, + @Default(false) bool isLoading, + }) = _UserCardsState; + +} \ No newline at end of file diff --git a/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.freezed.dart b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.freezed.dart new file mode 100644 index 00000000..6af8348f --- /dev/null +++ b/lib/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.freezed.dart @@ -0,0 +1,186 @@ +// coverage:ignore-file +// GENERATED CODE - DO NOT MODIFY BY HAND +// ignore_for_file: type=lint +// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark + +part of 'user_cards_state.dart'; + +// ************************************************************************** +// FreezedGenerator +// ************************************************************************** + +T _$identity(T value) => value; + +final _privateConstructorUsedError = UnsupportedError( + 'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models', +); + +/// @nodoc +mixin _$UserCardsState { + List get cards => throw _privateConstructorUsedError; + bool get isLoading => throw _privateConstructorUsedError; + + /// Create a copy of UserCardsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + $UserCardsStateCopyWith get copyWith => + throw _privateConstructorUsedError; +} + +/// @nodoc +abstract class $UserCardsStateCopyWith<$Res> { + factory $UserCardsStateCopyWith( + UserCardsState value, + $Res Function(UserCardsState) then, + ) = _$UserCardsStateCopyWithImpl<$Res, UserCardsState>; + @useResult + $Res call({List cards, bool isLoading}); +} + +/// @nodoc +class _$UserCardsStateCopyWithImpl<$Res, $Val extends UserCardsState> + implements $UserCardsStateCopyWith<$Res> { + _$UserCardsStateCopyWithImpl(this._value, this._then); + + // ignore: unused_field + final $Val _value; + // ignore: unused_field + final $Res Function($Val) _then; + + /// Create a copy of UserCardsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? cards = null, Object? isLoading = null}) { + return _then( + _value.copyWith( + cards: + null == cards + ? _value.cards + : cards // ignore: cast_nullable_to_non_nullable + as List, + isLoading: + null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + ) + as $Val, + ); + } +} + +/// @nodoc +abstract class _$$UserCardsStateImplCopyWith<$Res> + implements $UserCardsStateCopyWith<$Res> { + factory _$$UserCardsStateImplCopyWith( + _$UserCardsStateImpl value, + $Res Function(_$UserCardsStateImpl) then, + ) = __$$UserCardsStateImplCopyWithImpl<$Res>; + @override + @useResult + $Res call({List cards, bool isLoading}); +} + +/// @nodoc +class __$$UserCardsStateImplCopyWithImpl<$Res> + extends _$UserCardsStateCopyWithImpl<$Res, _$UserCardsStateImpl> + implements _$$UserCardsStateImplCopyWith<$Res> { + __$$UserCardsStateImplCopyWithImpl( + _$UserCardsStateImpl _value, + $Res Function(_$UserCardsStateImpl) _then, + ) : super(_value, _then); + + /// Create a copy of UserCardsState + /// with the given fields replaced by the non-null parameter values. + @pragma('vm:prefer-inline') + @override + $Res call({Object? cards = null, Object? isLoading = null}) { + return _then( + _$UserCardsStateImpl( + cards: + null == cards + ? _value._cards + : cards // ignore: cast_nullable_to_non_nullable + as List, + isLoading: + null == isLoading + ? _value.isLoading + : isLoading // ignore: cast_nullable_to_non_nullable + as bool, + ), + ); + } +} + +/// @nodoc + +class _$UserCardsStateImpl implements _UserCardsState { + _$UserCardsStateImpl({ + final List cards = const [], + this.isLoading = false, + }) : _cards = cards; + + final List _cards; + @override + @JsonKey() + List get cards { + if (_cards is EqualUnmodifiableListView) return _cards; + // ignore: implicit_dynamic_type + return EqualUnmodifiableListView(_cards); + } + + @override + @JsonKey() + final bool isLoading; + + @override + String toString() { + return 'UserCardsState(cards: $cards, isLoading: $isLoading)'; + } + + @override + bool operator ==(Object other) { + return identical(this, other) || + (other.runtimeType == runtimeType && + other is _$UserCardsStateImpl && + const DeepCollectionEquality().equals(other._cards, _cards) && + (identical(other.isLoading, isLoading) || + other.isLoading == isLoading)); + } + + @override + int get hashCode => Object.hash( + runtimeType, + const DeepCollectionEquality().hash(_cards), + isLoading, + ); + + /// Create a copy of UserCardsState + /// with the given fields replaced by the non-null parameter values. + @JsonKey(includeFromJson: false, includeToJson: false) + @override + @pragma('vm:prefer-inline') + _$$UserCardsStateImplCopyWith<_$UserCardsStateImpl> get copyWith => + __$$UserCardsStateImplCopyWithImpl<_$UserCardsStateImpl>( + this, + _$identity, + ); +} + +abstract class _UserCardsState implements UserCardsState { + factory _UserCardsState({final List cards, final bool isLoading}) = + _$UserCardsStateImpl; + + @override + List get cards; + @override + bool get isLoading; + + /// Create a copy of UserCardsState + /// with the given fields replaced by the non-null parameter values. + @override + @JsonKey(includeFromJson: false, includeToJson: false) + _$$UserCardsStateImplCopyWith<_$UserCardsStateImpl> get copyWith => + throw _privateConstructorUsedError; +} diff --git a/lib/features/my_finance/presentation/pages/card_settings.dart b/lib/features/my_finance/presentation/pages/card_settings.dart index abba517d..caa6a6e0 100644 --- a/lib/features/my_finance/presentation/pages/card_settings.dart +++ b/lib/features/my_finance/presentation/pages/card_settings.dart @@ -1,21 +1,35 @@ +import 'package:eatvio_app/core/locator_service.dart'; +import 'package:eatvio_app/core/theme/custom_text_style.dart'; import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; +import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_state.dart'; +import 'package:eatvio_app/features/my_finance/presentation/widgets/card_widget.dart'; +import 'package:eatvio_app/generated/l10n.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:collection/collection.dart'; class CardSettingsScreen extends StatelessWidget { + static const path = '/card-settings-screen'; + static const name = 'cardSettingsScreen'; const CardSettingsScreen({super.key}); @override Widget build(BuildContext context) { + return BlocProvider(create: (context) => sl()..getCards(), child: CardsSettingsContent()); + } +} + +class CardsSettingsContent extends StatelessWidget { + CardsSettingsContent({super.key}); + @override + Widget build(BuildContext context) { + final s = S.of(context); return Scaffold( backgroundColor: Color(0xFFF0F2F5), appBar: AppBar( - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.vertical( - bottom: Radius.circular(12), - ), - ), - title: Text('Мои карты'), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.vertical(bottom: Radius.circular(12))), + title: Text(s.my_cards), centerTitle: true, ), body: Column( @@ -30,41 +44,42 @@ class CardSettingsScreen extends StatelessWidget { const SizedBox(height: 16), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Привязанные карты', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 18, - fontWeight: FontWeight.w600, - height: 1, - ), - ), + child: Text('Привязанные карты', style: TextStyle(color: Color(0xFF323F4B), fontSize: 18, fontWeight: FontWeight.w600, height: 1)), ), const SizedBox(height: 8), Padding( padding: const EdgeInsets.symmetric(horizontal: 16), - child: Text( - 'Выплату будут производиться\nна карту ** 0278', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 16, - fontWeight: FontWeight.w500, - height: 1, - ), + child: BlocBuilder( + builder: (context, state) { + final defaultCardNumber = state.cards.firstWhereOrNull((element) => element.isDefault)?.cardNumber; + if (defaultCardNumber != null) { + return Text( + 'Выплату будут производиться\nна карту ${defaultCardNumber.substring(defaultCardNumber.length - 7)}', + style: TextStyle(color: Color(0xFF323F4B), fontSize: 16, fontWeight: FontWeight.w500, height: 1), + ); + } + return Text(s.no_default_cards, style: TextStyles.textMMedium.copyWith(color: appTheme.black)); + }, ), ), const SizedBox(height: 16), SingleChildScrollView( padding: EdgeInsets.symmetric(horizontal: 16), scrollDirection: Axis.horizontal, - child: Row( - children: [ - _card(true), - const SizedBox(width: 8), - _card(), - const SizedBox(width: 8), - _card(), - ], + child: BlocBuilder( + builder: (context, state) { + if (state.isLoading) {} + return Row( + spacing: 12, + children: List.generate(state.cards.length + 1, (index) { + if (index < state.cards.length) { + final cardsNumber = state.cards[index].cardNumber.substring(state.cards[index].cardNumber.length - 7); + return CardWidget(isSelected: state.cards[index].isDefault, cardId: state.cards[index].id, cardNumber: cardsNumber); + } + return CardWidget(enabledAddCard: true); + }), + ); + }, ), ), const SizedBox(height: 16), @@ -75,40 +90,4 @@ class CardSettingsScreen extends StatelessWidget { ), ); } - - _card([bool isSelected = false]) { - return Container( - padding: EdgeInsets.all(4), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8), - border: Border.all( - color: isSelected ? appTheme.blueA400 : Color(0xFFF1F2F6), - width: 2, - ), - ), - child: Container( - width: 148, - padding: EdgeInsets.all(10), - decoration: BoxDecoration( - color: Color(0xFFF1F2F6), - borderRadius: BorderRadius.circular(4), - ), - child: Column( - children: [ - SvgPicture.asset('assets/icons/mir.svg'), - const SizedBox(height: 24), - Text( - '** 0278', - style: TextStyle( - color: Color(0xFF323F4B), - fontSize: 14, - height: 1, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ), - ); - } } diff --git a/lib/features/my_finance/presentation/pages/user_finance_screen.dart b/lib/features/my_finance/presentation/pages/user_finance_screen.dart index 2ac71c30..5bca9c4d 100644 --- a/lib/features/my_finance/presentation/pages/user_finance_screen.dart +++ b/lib/features/my_finance/presentation/pages/user_finance_screen.dart @@ -2,6 +2,7 @@ import 'package:eatvio_app/core/theme/theme_helper.dart'; import 'package:eatvio_app/core/utils/date_time_utils.dart'; import 'package:eatvio_app/core/widgets/custom_drop_down_button.dart'; import 'package:eatvio_app/features/my_finance/entities/consultant_finance_entity.dart'; +import 'package:eatvio_app/features/my_finance/presentation/pages/card_settings.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; @@ -13,11 +14,7 @@ class UserFinanceScreen extends StatelessWidget { return Scaffold( backgroundColor: Color(0xFFF0F2F5), appBar: AppBar( - shape: RoundedRectangleBorder( - borderRadius: BorderRadiusDirectional.vertical( - bottom: Radius.circular(12), - ), - ), + shape: RoundedRectangleBorder(borderRadius: BorderRadiusDirectional.vertical(bottom: Radius.circular(12))), title: Text('История операций'), centerTitle: true, ), @@ -28,26 +25,16 @@ class UserFinanceScreen extends StatelessWidget { const SizedBox(height: 8), InkWell( onTap: () { - GoRouter.of(context).pushNamed('cardSettingsScreen'); + GoRouter.of(context).pushNamed(CardSettingsScreen.name); }, borderRadius: BorderRadius.circular(20), child: Ink( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(20), - color: appTheme.blueA400, - ), + decoration: BoxDecoration(borderRadius: BorderRadius.circular(20), color: appTheme.blueA400), width: MediaQuery.of(context).size.width - 32, child: Center( child: Padding( padding: const EdgeInsets.all(16), - child: Text( - 'Управление картами', - style: TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.w600, - ), - ), + child: Text('Управление картами', style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600)), ), ), ), @@ -62,108 +49,73 @@ class UserFinanceScreen extends StatelessWidget { physics: const NeverScrollableScrollPhysics(), child: Column( children: [ - CustomDropDownButton( - items: [], - onChanged: (value) {}, - ), + CustomDropDownButton(items: [], onChanged: (value) {}), const SizedBox(height: 12), ListView.separated( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), - itemBuilder: (context, index) => Material( - color: Color(0xFFF6F6F6), - borderRadius: BorderRadius.circular(20), - child: Padding( - padding: const EdgeInsets.all(12), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, + itemBuilder: + (context, index) => Material( + color: Color(0xFFF6F6F6), + borderRadius: BorderRadius.circular(20), + child: Padding( + padding: const EdgeInsets.all(12), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - testFinanceEntity.consultationsList[index].id, - style: TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontWeight: FontWeight.w500, - ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + testFinanceEntity.consultationsList[index].id, + style: TextStyle(color: Color(0xFF323F4B), height: 1, fontWeight: FontWeight.w500), + ), + Text( + testFinanceEntity.consultationsList[index].date.format('dd.MM.yyy', 'RU'), + style: TextStyle(color: Color(0xFF6D6F71), height: 1, fontWeight: FontWeight.w500), + ), + ], ), + const SizedBox(height: 8), Text( - testFinanceEntity.consultationsList[index].date.format('dd.MM.yyy', 'RU'), - style: TextStyle( - color: Color(0xFF6D6F71), - height: 1, - fontWeight: FontWeight.w500, - ), + testFinanceEntity.consultationsList[index].title, + style: TextStyle(color: appTheme.blueA400, height: 1, fontSize: 18, fontWeight: FontWeight.w600), ), - ], - ), - const SizedBox(height: 8), - Text( - testFinanceEntity.consultationsList[index].title, - style: TextStyle( - color: appTheme.blueA400, - height: 1, - fontSize: 18, - fontWeight: FontWeight.w600, - ), - ), - const SizedBox(height: 8), - Text( - 'Покупатель: ' + testFinanceEntity.consultationsList[index].clientName, - style: TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - Text( - testFinanceEntity.consultationsList[index].planDuration.toString() + ' дней', - style: TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 14, - fontWeight: FontWeight.w500, - ), - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ + const SizedBox(height: 8), Text( - testFinanceEntity.consultationsList[index].price.toString() + ' ₽', - style: TextStyle( - color: Color(0xFF323F4B), - height: 1, - fontSize: 18, - fontWeight: FontWeight.w600, - ), + 'Покупатель: ' + testFinanceEntity.consultationsList[index].clientName, + style: TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500), ), - Material( - color: Color(0xFF0BB450).withAlpha(11), - borderRadius: BorderRadius.circular(8), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), - child: Text( - testFinanceEntity.consultationsList[index].status.name, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w400, - color: Color(0xFF0BB450), - height: 1, + const SizedBox(height: 8), + Text( + testFinanceEntity.consultationsList[index].planDuration.toString() + ' дней', + style: TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 14, fontWeight: FontWeight.w500), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + testFinanceEntity.consultationsList[index].price.toString() + ' ₽', + style: TextStyle(color: Color(0xFF323F4B), height: 1, fontSize: 18, fontWeight: FontWeight.w600), + ), + Material( + color: Color(0xFF0BB450).withAlpha(11), + borderRadius: BorderRadius.circular(8), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4), + child: Text( + testFinanceEntity.consultationsList[index].status.name, + style: TextStyle(fontSize: 14, fontWeight: FontWeight.w400, color: Color(0xFF0BB450), height: 1), + ), ), ), - ), + ], ), ], ), - ], + ), ), - ), - ), separatorBuilder: (context, index) => const SizedBox(height: 12), itemCount: testFinanceEntity.consultationsList.length, ), diff --git a/lib/features/my_finance/presentation/widgets/card_widget.dart b/lib/features/my_finance/presentation/widgets/card_widget.dart new file mode 100644 index 00000000..22789c21 --- /dev/null +++ b/lib/features/my_finance/presentation/widgets/card_widget.dart @@ -0,0 +1,68 @@ +import 'package:eatvio_app/core/theme/custom_text_style.dart'; +import 'package:eatvio_app/core/theme/theme_helper.dart'; +import 'package:eatvio_app/features/my_finance/presentation/cubit/user_cards_cubit/user_cards_cubit.dart'; +import 'package:eatvio_app/generated/l10n.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_svg/svg.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class CardWidget extends StatelessWidget { + const CardWidget({super.key, this.isSelected = false, this.enabledAddCard = false, this.cardId = 0, this.cardNumber = ''}); + final int cardId; + final bool isSelected; + final bool enabledAddCard; + final String cardNumber; + @override + Widget build(BuildContext context) { + final s = S.of(context); + return GestureDetector( + onTap: enabledAddCard + ? () async { + final addCardUrl = await context.read().addCard(); + + if (addCardUrl != null) { + final uri = Uri.parse(addCardUrl.paymentUrl); + if (await canLaunchUrl(uri)) { + await launchUrl(uri, mode: LaunchMode.externalApplication); + } + } + } + : () { + context.read().markAsDefault(id: cardId); + }, + child: Container( + padding: EdgeInsets.all(4), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all(color: isSelected ? appTheme.blueA400 : Color(0xFFF1F2F6), width: 2), + ), + child: enabledAddCard + ? Container( + width: 148, + padding: EdgeInsets.all(10), + decoration: BoxDecoration(color: Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(4)), + child: Column( + children: [ + SvgPicture.asset('assets/icons/add_card.svg'), + const SizedBox(height: 24), + Text(s.new_card, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)), + ], + ), + ) + : Container( + width: 148, + padding: EdgeInsets.all(10), + decoration: BoxDecoration(color: Color(0xFFF1F2F6), borderRadius: BorderRadius.circular(4)), + child: Column( + children: [ + SvgPicture.asset('assets/icons/card.svg'), + const SizedBox(height: 24), + Text(cardNumber, style: TextStyles.textSSemibold.copyWith(color: appTheme.black)), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/my_finance/repositories/cards_repository.dart b/lib/features/my_finance/repositories/cards_repository.dart new file mode 100644 index 00000000..221a3376 --- /dev/null +++ b/lib/features/my_finance/repositories/cards_repository.dart @@ -0,0 +1,39 @@ +import 'package:eatvio_app/features/my_finance/models/card_model.dart'; +import 'package:eatvio_app/features/my_finance/models/payment_url_model.dart'; +import 'package:eatvio_app/features/my_finance/services/cards_service.dart'; + +class CardsRepository { + final CardsService cardsService; + CardsRepository({required this.cardsService}); + Future addCard() async { + try { + return await cardsService.addCard(); + } catch (e) { + rethrow; + } + } + + Future> getCards() async { + try { + return await cardsService.getCards(); + } catch (e) { + rethrow; + } + } + + Future markAsDefault({required int id}) async { + try { + await cardsService.markAsDefault(id: id); + } catch (e) { + rethrow; + } + } + + Future deleteCard({required int id}) async { + try { + await cardsService.deleteCard(id: id); + } catch (e) { + rethrow; + } + } +} diff --git a/lib/features/my_finance/services/cards_service.dart b/lib/features/my_finance/services/cards_service.dart new file mode 100644 index 00000000..b42a8ed7 --- /dev/null +++ b/lib/features/my_finance/services/cards_service.dart @@ -0,0 +1,28 @@ +import 'package:dio/dio.dart'; +import 'package:eatvio_app/features/my_finance/models/card_model.dart'; +import 'package:eatvio_app/features/my_finance/models/payment_url_model.dart'; +import 'package:retrofit/retrofit.dart'; + +part 'cards_service.g.dart'; + +@RestApi() +abstract class CardsService { + factory CardsService(Dio dio, {String baseUrl}) = _CardsService; + + @GET('/api/finances/cards') + Future> getCards(); + + @POST('/api/finances/cards/add') + Future addCard(); + + @POST('/api/finances/cards/{id}/default') + Future markAsDefault({ + @Path('id') required int id, + }); + + @POST('/api/finances/cards/{id}/remove') + Future deleteCard({ + @Path('id') required int id, + }); +} + diff --git a/lib/features/my_finance/services/cards_service.g.dart b/lib/features/my_finance/services/cards_service.g.dart new file mode 100644 index 00000000..62b8d7ca --- /dev/null +++ b/lib/features/my_finance/services/cards_service.g.dart @@ -0,0 +1,141 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'cards_service.dart'; + +// ************************************************************************** +// RetrofitGenerator +// ************************************************************************** + +// ignore_for_file: unnecessary_brace_in_string_interps,no_leading_underscores_for_local_identifiers,unused_element,unnecessary_string_interpolations + +class _CardsService implements CardsService { + _CardsService(this._dio, {this.baseUrl, this.errorLogger}); + + final Dio _dio; + + String? baseUrl; + + final ParseErrorLogger? errorLogger; + + @override + Future> getCards() async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType>( + Options(method: 'GET', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/finances/cards', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late List _value; + try { + _value = + _result.data! + .map((dynamic i) => CardModel.fromJson(i as Map)) + .toList(); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future addCard() async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/finances/cards/add', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + final _result = await _dio.fetch>(_options); + late PaymentUrlModel _value; + try { + _value = PaymentUrlModel.fromJson(_result.data!); + } on Object catch (e, s) { + errorLogger?.logError(e, s, _options); + rethrow; + } + return _value; + } + + @override + Future markAsDefault({required int id}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/finances/cards/${id}/default', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + @override + Future deleteCard({required int id}) async { + final _extra = {}; + final queryParameters = {}; + final _headers = {}; + const Map? _data = null; + final _options = _setStreamType( + Options(method: 'POST', headers: _headers, extra: _extra) + .compose( + _dio.options, + '/api/finances/cards/${id}/remove', + queryParameters: queryParameters, + data: _data, + ) + .copyWith(baseUrl: _combineBaseUrls(_dio.options.baseUrl, baseUrl)), + ); + await _dio.fetch(_options); + } + + RequestOptions _setStreamType(RequestOptions requestOptions) { + if (T != dynamic && + !(requestOptions.responseType == ResponseType.bytes || + requestOptions.responseType == ResponseType.stream)) { + if (T == String) { + requestOptions.responseType = ResponseType.plain; + } else { + requestOptions.responseType = ResponseType.json; + } + } + return requestOptions; + } + + String _combineBaseUrls(String dioBaseUrl, String? baseUrl) { + if (baseUrl == null || baseUrl.trim().isEmpty) { + return dioBaseUrl; + } + + final url = Uri.parse(baseUrl); + + if (url.isAbsolute) { + return url.toString(); + } + + return Uri.parse(dioBaseUrl).resolveUri(url).toString(); + } +} diff --git a/lib/generated/intl/messages_en.dart b/lib/generated/intl/messages_en.dart index dfc173ae..0839370a 100644 --- a/lib/generated/intl/messages_en.dart +++ b/lib/generated/intl/messages_en.dart @@ -363,6 +363,7 @@ class MessageLookup extends MessageLookupByLibrary { "Что то пошло не так!", ), "my_blog": MessageLookupByLibrary.simpleMessage("Мой блог"), + "my_cards": MessageLookupByLibrary.simpleMessage("Мои карты"), "my_diets": MessageLookupByLibrary.simpleMessage("Мои диеты"), "my_plans": MessageLookupByLibrary.simpleMessage("Мои планы"), "my_settings": MessageLookupByLibrary.simpleMessage("Мои настройки"), @@ -372,9 +373,13 @@ class MessageLookup extends MessageLookupByLibrary { "name_short": MessageLookupByLibrary.simpleMessage( "В имени пользователя должно быть минимум 6 символов", ), + "new_card": MessageLookupByLibrary.simpleMessage("Добавить карту"), "nicotine": MessageLookupByLibrary.simpleMessage("Никотин"), "no": MessageLookupByLibrary.simpleMessage("Нет"), "no_account": MessageLookupByLibrary.simpleMessage("У меня нет аккаунта"), + "no_default_cards": MessageLookupByLibrary.simpleMessage( + "У вас нет привязанных карт. Что бы начать работу, привяжите карту", + ), "not_liked_groceries": MessageLookupByLibrary.simpleMessage( "Не любимые продукты", ), diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 32705c64..35ece7a4 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -2631,6 +2631,26 @@ class S { args: [], ); } + + /// `Мои карты` + String get my_cards { + return Intl.message('Мои карты', name: 'my_cards', desc: '', args: []); + } + + /// `Добавить карту` + String get new_card { + return Intl.message('Добавить карту', name: 'new_card', desc: '', args: []); + } + + /// `У вас нет привязанных карт. Что бы начать работу, привяжите карту` + String get no_default_cards { + return Intl.message( + 'У вас нет привязанных карт. Что бы начать работу, привяжите карту', + name: 'no_default_cards', + desc: '', + args: [], + ); + } } class AppLocalizationDelegate extends LocalizationsDelegate { diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index 9ba44b26..5d85eb52 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -324,7 +324,10 @@ "more_about_order": "Подробнее о заказе", "cancel_order": "Отменить заказ", "refuse_order":"Отказаться от заказа", - "user_info": "Анкета пользователя" + "user_info": "Анкета пользователя", + "my_cards": "Мои карты", + "new_card": "Добавить карту", + "no_default_cards": "У вас нет привязанных карт. Что бы начать работу, привяжите карту" } diff --git a/pubspec.yaml b/pubspec.yaml index 010bd422..422f0bb1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: eatvio_app description: A new Flutter project. -version: 1.0.47+47 +version: 1.0.49+50 publish_to: 'none' environment: diff --git a/test/widget_test.dart b/test/widget_test.dart new file mode 100644 index 00000000..da093caf --- /dev/null +++ b/test/widget_test.dart @@ -0,0 +1,30 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:eatvio_app/main.dart'; + +void main() { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(MyApp()); + + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); + + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); + + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); + }); +}