From 6513a004d9aca2158c805617115edc44111ee3f8 Mon Sep 17 00:00:00 2001 From: Polyanka1 Date: Tue, 18 Aug 2026 15:09:53 +0300 Subject: [PATCH] fix profile --- lib/data/network/api_service.dart | 50 ++++++++++ .../repositories/profile_repository_impl.dart | 5 + lib/di/service_locator.dart | 6 +- lib/domain/entities/tariff.dart | 3 + .../repositories/profile_repository.dart | 1 + .../usecase/delete_profile_usecase.dart | 21 +++++ lib/main.dart | 11 ++- .../dialog/delete_account_dialog.dart | 94 +++++++++++++++++++ .../components/sheet/tariff_info_sheet.dart | 23 +++-- .../components/sheet/tariff_sheet.dart | 2 +- lib/presentation/event/profile_event.dart | 2 + .../screens/edit_profile_screen.dart | 1 + lib/presentation/screens/profile_screen.dart | 34 +++++-- lib/presentation/viewmodel/profile_bloc.dart | 26 ++++- pubspec.lock | 9 +- pubspec.yaml | 6 +- 16 files changed, 268 insertions(+), 26 deletions(-) create mode 100644 lib/domain/usecase/delete_profile_usecase.dart create mode 100644 lib/presentation/components/dialog/delete_account_dialog.dart diff --git a/lib/data/network/api_service.dart b/lib/data/network/api_service.dart index 0aff771..7897d52 100644 --- a/lib/data/network/api_service.dart +++ b/lib/data/network/api_service.dart @@ -39,6 +39,9 @@ class ApiService { static const String baseUrl = "https://api.behappybel.by/api/v1"; static const String fileBaseUrl = "https://api.behappybel.by/"; + /*static const String baseUrl = "https://sharing-api.sparkit.by/api/v1"; + static const String fileBaseUrl = "https://sharing-api.sparkit.by"; +*/ final SecurityService _securityService; final Dio _dio; @@ -172,6 +175,53 @@ class ApiService { } } + Future deleteProfile() async { + try { + final response = await _dio.delete( + "$baseUrl/client/me", + options: await _getAuthOptions(), + ); + + if (response.statusCode == 200) { + final data = response.data; + final profileData = data["profile"]; + if (profileData == null) return null; + + String? parsedDate; + if (profileData["dob"] != null) { + try { + parsedDate = DateFormat( + 'dd.MM.yyyy', + ).format(DateTime.parse(profileData["dob"])); + } catch (_) {} + } + + final int? avatarId = profileData["avatarId"]; + String? avatarUrl; + + if (avatarId != null && profileData["avatar"] != null) { + final String? avatarPath = profileData["avatar"]["path"]; + if (avatarPath != null && avatarPath.isNotEmpty) { + avatarUrl = Uri.parse(fileBaseUrl).resolve(avatarPath).toString(); + } + } + + dynamic balanceRaw = profileData["balance"]; + double? balance; + + if (balanceRaw is num) { + balance = balanceRaw.toDouble(); + } else if (balanceRaw is String) { + balance = double.tryParse(balanceRaw); + } + + } + return null; + } catch (e) { + return null; + } + } + Future> getPaymentCards() async { final response = await _dio.get( "$baseUrl/client/me", diff --git a/lib/data/repositories/profile_repository_impl.dart b/lib/data/repositories/profile_repository_impl.dart index 0ec3d70..5160c99 100644 --- a/lib/data/repositories/profile_repository_impl.dart +++ b/lib/data/repositories/profile_repository_impl.dart @@ -27,6 +27,11 @@ class UserProfileRepositoryImpl implements UserProfileRepository { return await _apiService.getProfile() ?? _cachedProfile; } + @override + Future deleteProfile() async { + return await _apiService.deleteProfile(); + } + @override Future updateProfile(UserProfile profile) async { //await Future.delayed(const Duration(milliseconds: 300)); diff --git a/lib/di/service_locator.dart b/lib/di/service_locator.dart index 3b61f41..a6545c0 100644 --- a/lib/di/service_locator.dart +++ b/lib/di/service_locator.dart @@ -17,6 +17,7 @@ import 'package:be_happy/domain/repositories/scooter_repository.dart'; import 'package:be_happy/domain/repositories/zone_repository.dart'; import 'package:be_happy/domain/service/security_service.dart'; import 'package:be_happy/domain/usecase/add_payment_card_usecase.dart'; // ← новый +import 'package:be_happy/domain/usecase/delete_profile_usecase.dart'; import 'package:be_happy/domain/usecase/get_certificates_usecase.dart'; import 'package:be_happy/domain/usecase/get_scooter_order_route_history_usecase.dart'; import 'package:be_happy/domain/usecase/is_pin_set_usecase.dart'; @@ -54,6 +55,7 @@ import 'package:be_happy/domain/usecase/finish_ride_usecase.dart'; import 'package:be_happy/domain/usecase/pause_ride_usecase.dart'; import 'package:be_happy/domain/usecase/resume_ride_usecase.dart'; import 'package:be_happy/domain/usecase/verify_pin_usecase.dart'; +import 'package:be_happy/presentation/event/profile_event.dart'; import 'package:be_happy/presentation/viewmodel/active_ride_bloc.dart'; import 'package:be_happy/presentation/viewmodel/add_card_bloc.dart'; // ← новый import 'package:be_happy/presentation/viewmodel/current_rides_bloc.dart'; @@ -205,6 +207,8 @@ Future setupDependencies() async { getIt.registerSingleton(GetProfileUseCase(getIt())); + getIt.registerSingleton(DeleteProfileUsecase(getIt(), getIt(), getIt())); + getIt.registerSingleton(UpdateProfileUseCase(getIt())); getIt.registerSingleton( @@ -319,7 +323,7 @@ Future setupDependencies() async { ); getIt.registerLazySingleton( - () => ProfileBloc(getIt(), getIt(), getIt()), + () => ProfileBloc(getIt(), getIt(), getIt(), getIt(), getIt(), getIt()), ); getIt.registerFactory( diff --git a/lib/domain/entities/tariff.dart b/lib/domain/entities/tariff.dart index a999e32..ea07259 100644 --- a/lib/domain/entities/tariff.dart +++ b/lib/domain/entities/tariff.dart @@ -2,6 +2,7 @@ class Tariff { final int id; final String title; final String description; + final String descriptionJson; final bool isActive; final String currency; final double holdPrice; // Старт / бронь @@ -15,6 +16,7 @@ class Tariff { required this.id, required this.title, required this.description, + required this.descriptionJson, required this.isActive, required this.currency, required this.holdPrice, @@ -33,6 +35,7 @@ class Tariff { id: json['id'] ?? 0, title: json['title'] ?? 'Unknown', description: json['description'] ?? '', + descriptionJson: json['descriptionJson'] ?? '', isActive: json['isActive'] ?? false, currency: currency['currency'] ?? 'BYN', holdPrice: (planPrice['hold'] as num?)?.toDouble() ?? 0.0, diff --git a/lib/domain/repositories/profile_repository.dart b/lib/domain/repositories/profile_repository.dart index 6f831a4..e7686cd 100644 --- a/lib/domain/repositories/profile_repository.dart +++ b/lib/domain/repositories/profile_repository.dart @@ -5,6 +5,7 @@ import '../entities/user_profile.dart'; abstract class UserProfileRepository { Future getProfile(); + Future deleteProfile(); Future updateProfile(UserProfile profile); Future uploadProfilePhoto(File imageFile); Future checkUser(); diff --git a/lib/domain/usecase/delete_profile_usecase.dart b/lib/domain/usecase/delete_profile_usecase.dart new file mode 100644 index 0000000..1fe31db --- /dev/null +++ b/lib/domain/usecase/delete_profile_usecase.dart @@ -0,0 +1,21 @@ +import '../entities/user_profile.dart'; +import '../repositories/auth_repository.dart'; +import '../repositories/pin_repository.dart'; +import '../repositories/profile_repository.dart'; + +class DeleteProfileUsecase { + final UserProfileRepository repository; + final PinRepository _pinRepository; + final AuthRepository _authRepository; + + + + DeleteProfileUsecase(this.repository, this._pinRepository, this._authRepository); + + Future call() { + _pinRepository.removePin(); + repository.deleteProfile(); + return _authRepository.logout(); + } +} + diff --git a/lib/main.dart b/lib/main.dart index be2e620..b179b4d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; // 🔹 1. Добавлен импорт +import 'package:flutter/services.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:bot_toast/bot_toast.dart'; import 'package:be_happy/presentation/event/edit_profile_event.dart'; import 'package:be_happy/presentation/event/profile_event.dart'; @@ -69,6 +70,14 @@ class MyApp extends StatelessWidget { title: 'BeHappy', debugShowCheckedModeBanner: false, theme: ThemeData.dark(), + localizationsDelegates: const [ + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: const [ + Locale('ru', 'RU'), + ], builder: BotToastInit(), routerConfig: appRouter.router, ), diff --git a/lib/presentation/components/dialog/delete_account_dialog.dart b/lib/presentation/components/dialog/delete_account_dialog.dart new file mode 100644 index 0000000..5c267b1 --- /dev/null +++ b/lib/presentation/components/dialog/delete_account_dialog.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; + +class DeleteAccountDialog extends StatelessWidget { + const DeleteAccountDialog({super.key}); + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + insetPadding: const EdgeInsets.symmetric(horizontal: 40), + child: Container( + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: const Color(0xFF2E3253).withOpacity(0.95), + borderRadius: BorderRadius.circular(28), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + "Удалить\nучётную запись?", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.w600, + height: 1.2, + ), + ), + const SizedBox(height: 16), + const Text( + "Это действие необратимо. Все ваши\nданные будут удалены безвозвратно", + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white70, + fontSize: 14, + height: 1.4, + ), + ), + const SizedBox(height: 24), + + Container( + width: double.infinity, + height: 52, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(26), + gradient: const LinearGradient( + colors: [Color(0xFFFF8E8E), Color(0xFFFE5F86)], + ), + ), + child: ElevatedButton( + onPressed: () => Navigator.pop(context, true), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.transparent, + shadowColor: Colors.transparent, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26), + ), + ), + child: const Text( + "Удалить", + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ), + ), + const SizedBox(height: 12), + + SizedBox( + width: double.infinity, + height: 52, + child: ElevatedButton( + onPressed: () => Navigator.pop(context, false), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF0D1024), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(26), + ), + ), + child: const Text( + "Отмена", + style: TextStyle(color: Colors.white, fontSize: 16), + ), + ), + ), + ], + ), + ), + ); + } +} \ No newline at end of file diff --git a/lib/presentation/components/sheet/tariff_info_sheet.dart b/lib/presentation/components/sheet/tariff_info_sheet.dart index 00b194e..c882a8b 100644 --- a/lib/presentation/components/sheet/tariff_info_sheet.dart +++ b/lib/presentation/components/sheet/tariff_info_sheet.dart @@ -3,6 +3,8 @@ import 'package:be_happy/domain/entities/tariff.dart'; import 'package:flutter/material.dart'; import 'package:flutter/cupertino.dart'; +import '../content_parser.dart'; + class TariffInfoSheet extends StatefulWidget { final Tariff tariff; final bool isInsurance; @@ -42,7 +44,6 @@ class _TariffInfoSheetState extends State { ), child: Column( children: [ - // Полоска сверху (Handle) const SizedBox(height: 12), Container( width: 40, @@ -81,9 +82,9 @@ class _TariffInfoSheetState extends State { const SizedBox(height: 30), // Таблица цен - _buildPriceRow('Старт поездки', '${widget.tariff.startPrice} BYN'), - _buildPriceRow('Последующая минута', '${widget.tariff.drivePrice} BYN'), - _buildPriceRow('Пауза', '${widget.tariff.pausePrice} BYN/мин'), + _buildPriceRow('Старт поездки', '${widget.tariff.startPrice} руб.'), + _buildPriceRow('Последующая минута', '${widget.tariff.drivePrice} руб.'), + _buildPriceRow('Пауза', '${widget.tariff.pausePrice} руб./мин'), _buildPriceRow('КЕШБЭК', '${widget.tariff.cashback * 100}%', isAccent: true), const Divider(color: Colors.white24, height: 40), @@ -98,7 +99,7 @@ class _TariffInfoSheetState extends State { Image.asset('assets/icons/info_icon.png', width: 18, height: 18), const Spacer(), Text( - '${widget.tariff.insurance} BYN', + '${widget.tariff.insurance} руб.', style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.bold), ), const SizedBox(width: 12), @@ -118,10 +119,14 @@ class _TariffInfoSheetState extends State { const Divider(color: Colors.white24, height: 40), - _buildInfoBullet('Оплата страховки осуществляется только по банковской карте отдельным платежом'), - _buildInfoBullet('В режиме паузы время тарифа приостанавливается'), - _buildInfoBullet('При старте заказа будет заблокирована сумма в размере 7 рублей для проверки платежеспособности. Сумма разблокируется по факту списания средств за поездку.'), - + Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: ContentParser.parseContent( + textJson: widget.tariff.description, + text: widget.tariff.descriptionJson, + ), + ), const SizedBox(height: 30), ], ), diff --git a/lib/presentation/components/sheet/tariff_sheet.dart b/lib/presentation/components/sheet/tariff_sheet.dart index d5bae7b..d8e8658 100644 --- a/lib/presentation/components/sheet/tariff_sheet.dart +++ b/lib/presentation/components/sheet/tariff_sheet.dart @@ -281,7 +281,7 @@ class _TariffSheetState extends State { subtitle: 'Старт поездки', details: [ 'Далее ${tariff.drivePrice.toStringAsFixed(2)} ${tariff.currency}/мин.', - 'Минута на паузе ${tariff.pausePrice.toStringAsFixed(0)} ${tariff.currency}', + 'Минута на паузе ${tariff.pausePrice.toStringAsFixed(2)} ${tariff.currency}', ], isSelected: _selectedTariffIndex == index, isInsurance: state.isInsurance, diff --git a/lib/presentation/event/profile_event.dart b/lib/presentation/event/profile_event.dart index ce1e57c..2c7fcec 100644 --- a/lib/presentation/event/profile_event.dart +++ b/lib/presentation/event/profile_event.dart @@ -8,6 +8,8 @@ class ProfileStarted extends ProfileEvent {} class ProfileUpdated extends ProfileEvent {} +class DeleteProfilePressed extends ProfileEvent {} + class ProfilePhotoUpdated extends ProfileEvent{ final File imageFile; diff --git a/lib/presentation/screens/edit_profile_screen.dart b/lib/presentation/screens/edit_profile_screen.dart index 4a02e95..25b0c67 100644 --- a/lib/presentation/screens/edit_profile_screen.dart +++ b/lib/presentation/screens/edit_profile_screen.dart @@ -77,6 +77,7 @@ class _EditProfileScreenState extends State { initialDate: _selectedBirthDate ?? DateTime.now(), firstDate: DateTime(DateTime.now().year - 100), lastDate: DateTime.now(), + locale: const Locale('ru', 'RU'), ); if (picked != null && picked != _selectedBirthDate) { diff --git a/lib/presentation/screens/profile_screen.dart b/lib/presentation/screens/profile_screen.dart index 52504ea..793cf91 100644 --- a/lib/presentation/screens/profile_screen.dart +++ b/lib/presentation/screens/profile_screen.dart @@ -6,6 +6,7 @@ import '../../core/app_colors.dart'; import '../../domain/entities/user_profile.dart'; import '../components/custom_app_bar.dart'; import '../components/gradient_button.dart'; +import '../components/dialog/delete_account_dialog.dart'; import '../viewmodel/profile_bloc.dart'; import 'edit_profile_screen.dart'; @@ -43,7 +44,19 @@ class _ProfileScreenState extends State { } Future _openEditProfile(UserProfile profile) async { - context.go("/home/profile/edit"); + context.go("/home/profile/edit"); + } + + Future _confirmDeleteAccount(BuildContext context) async { + final confirmed = await showDialog( + context: context, + barrierColor: Colors.black.withOpacity(0.6), + builder: (dialogContext) => const DeleteAccountDialog(), + ); + + if (confirmed == true && context.mounted) { + context.read().add(DeleteProfilePressed()); + } } @override @@ -136,12 +149,17 @@ class _ProfileScreenState extends State { onEditTap: () => context.go("/home/profile/edit"), ), - // const SizedBox(height: 24), - // _SettingsBlock( - // notificationsEnabled: notificationsEnabled, - // onNotificationsChanged: (v) => - // setState(() => notificationsEnabled = v), - // ), + Padding( + padding: const EdgeInsets.all(20), + child: GradientButton( + text: 'Удалить учётную запись', + onTap: () => _confirmDeleteAccount(context), + showArrows: true, + height: 56, + width: double.infinity, + fontSize: 16, + ), + ), ], ), ), @@ -370,4 +388,4 @@ class _SettingsBlock extends StatelessWidget { ), ); } -} +} \ No newline at end of file diff --git a/lib/presentation/viewmodel/profile_bloc.dart b/lib/presentation/viewmodel/profile_bloc.dart index f846820..57d11b8 100644 --- a/lib/presentation/viewmodel/profile_bloc.dart +++ b/lib/presentation/viewmodel/profile_bloc.dart @@ -1,25 +1,38 @@ +import 'dart:async'; + +import 'package:be_happy/domain/usecase/delete_profile_usecase.dart'; import 'package:be_happy/domain/usecase/upload_profile_photo_usecase.dart'; +import 'package:be_happy/presentation/viewmodel/splash_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../domain/entities/user_profile.dart'; import '../../domain/usecase/get_profile_usecase.dart'; +import '../../domain/usecase/logout_usecase.dart'; import '../../domain/usecase/update_profile_usecase.dart'; import '../event/profile_event.dart'; +import '../event/spalsh_event.dart'; import '../state/profile_state.dart'; class ProfileBloc extends Bloc { final GetProfileUseCase getProfileUseCase; final UploadProfilePhotoUsecase uploadProfilePhotoUsecase; final UpdateProfileUseCase updateProfileUseCase; + final DeleteProfileUsecase deleteProfileUsecase; + final LogoutUseCase logoutUseCase; + final SplashBloc splashBloc; ProfileBloc(this.getProfileUseCase, this.uploadProfilePhotoUsecase, - this.updateProfileUseCase) + this.updateProfileUseCase, + this.deleteProfileUsecase, + this.logoutUseCase, + this.splashBloc) : super(ProfileState.initial()) { on(_onStarted); on(_onUpdated); on(_onPhotoUploaded); + on(_onDeleteProfilePressed); } Future _onStarted( @@ -55,7 +68,6 @@ class ProfileBloc extends Bloc { final avatarId = await uploadProfilePhotoUsecase(event.imageFile); if (avatarId != null) { - // Отправляем PATCH запрос с новым avatarId await updateProfileUseCase(UserProfile( name: '', birthDate: '', @@ -72,4 +84,14 @@ class ProfileBloc extends Bloc { emit(state.copyWith(isLoading: false, error: "Ошибка загрузки: ${e.toString()}")); } } + + FutureOr _onDeleteProfilePressed(DeleteProfilePressed event, Emitter emit) async { + try { + + await deleteProfileUsecase(); + splashBloc.add(AuthLogoutRequested()); + } catch (e) { + emit(state.copyWith(isLoading: false, error: "Ошибка удаления профиля: ${e.toString()}")); + } + } } diff --git a/pubspec.lock b/pubspec.lock index ebb6cd5..dd176cc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -294,6 +294,11 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_localizations: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" flutter_native_splash: dependency: "direct dev" description: @@ -540,10 +545,10 @@ packages: dependency: "direct main" description: name: intl - sha256: d6f56758b7d3014a48af9701c085700aac781a92a87a62b1333b46d8879661cf + sha256: "3df61194eb431efc39c4ceba583b95633a403f46c9fd341e550ce0bfa50e9aa5" url: "https://pub.dev" source: hosted - version: "0.19.0" + version: "0.20.2" jni: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index a1dd423..290f5ab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -2,7 +2,7 @@ name: be_happy description: "" publish_to: 'none' -version: 1.0.0+22 +version: 1.0.0+23 environment: sdk: ^3.8.1 @@ -10,6 +10,8 @@ environment: dependencies: flutter: sdk: flutter + flutter_localizations: + sdk: flutter cupertino_icons: ^1.0.8 flutter_secure_storage: ^9.2.4 shared_preferences: ^2.5.3 @@ -21,7 +23,7 @@ dependencies: path: ^1.9.0 async: ^2.11.0 mime: ^1.0.5 - intl: ^0.19.0 + intl: ^0.20.2 url_launcher: ^6.2.5 go_router: ^14.0.0 android_id: ^0.4.0