fix profile

This commit is contained in:
2026-08-18 15:09:53 +03:00
parent 80a95ece5b
commit 6513a004d9
16 changed files with 268 additions and 26 deletions

View File

@@ -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<void> 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<List<PaymentCard>> getPaymentCards() async {
final response = await _dio.get(
"$baseUrl/client/me",

View File

@@ -27,6 +27,11 @@ class UserProfileRepositoryImpl implements UserProfileRepository {
return await _apiService.getProfile() ?? _cachedProfile;
}
@override
Future<void> deleteProfile() async {
return await _apiService.deleteProfile();
}
@override
Future<UserProfile?> updateProfile(UserProfile profile) async {
//await Future.delayed(const Duration(milliseconds: 300));

View File

@@ -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<void> setupDependencies() async {
getIt.registerSingleton<GetProfileUseCase>(GetProfileUseCase(getIt()));
getIt.registerSingleton<DeleteProfileUsecase>(DeleteProfileUsecase(getIt(), getIt(), getIt()));
getIt.registerSingleton<UpdateProfileUseCase>(UpdateProfileUseCase(getIt()));
getIt.registerSingleton<UploadProfilePhotoUsecase>(
@@ -319,7 +323,7 @@ Future<void> setupDependencies() async {
);
getIt.registerLazySingleton<ProfileBloc>(
() => ProfileBloc(getIt(), getIt(), getIt()),
() => ProfileBloc(getIt(), getIt(), getIt(), getIt(), getIt(), getIt()),
);
getIt.registerFactory<EditProfileBloc>(

View File

@@ -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,

View File

@@ -5,6 +5,7 @@ import '../entities/user_profile.dart';
abstract class UserProfileRepository {
Future<UserProfile> getProfile();
Future<void> deleteProfile();
Future<UserProfile?> updateProfile(UserProfile profile);
Future<int?> uploadProfilePhoto(File imageFile);
Future<UserCheckFlags?> checkUser();

View File

@@ -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<void> call() {
_pinRepository.removePin();
repository.deleteProfile();
return _authRepository.logout();
}
}

View File

@@ -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,
),

View File

@@ -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),
),
),
),
],
),
),
);
}
}

View File

@@ -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<TariffInfoSheet> {
),
child: Column(
children: [
// Полоска сверху (Handle)
const SizedBox(height: 12),
Container(
width: 40,
@@ -81,9 +82,9 @@ class _TariffInfoSheetState extends State<TariffInfoSheet> {
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<TariffInfoSheet> {
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<TariffInfoSheet> {
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),
],
),

View File

@@ -281,7 +281,7 @@ class _TariffSheetState extends State<TariffSheet> {
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,

View File

@@ -8,6 +8,8 @@ class ProfileStarted extends ProfileEvent {}
class ProfileUpdated extends ProfileEvent {}
class DeleteProfilePressed extends ProfileEvent {}
class ProfilePhotoUpdated extends ProfileEvent{
final File imageFile;

View File

@@ -77,6 +77,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
initialDate: _selectedBirthDate ?? DateTime.now(),
firstDate: DateTime(DateTime.now().year - 100),
lastDate: DateTime.now(),
locale: const Locale('ru', 'RU'),
);
if (picked != null && picked != _selectedBirthDate) {

View File

@@ -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<ProfileScreen> {
}
Future<void> _openEditProfile(UserProfile profile) async {
context.go("/home/profile/edit");
context.go("/home/profile/edit");
}
Future<void> _confirmDeleteAccount(BuildContext context) async {
final confirmed = await showDialog<bool>(
context: context,
barrierColor: Colors.black.withOpacity(0.6),
builder: (dialogContext) => const DeleteAccountDialog(),
);
if (confirmed == true && context.mounted) {
context.read<ProfileBloc>().add(DeleteProfilePressed());
}
}
@override
@@ -136,12 +149,17 @@ class _ProfileScreenState extends State<ProfileScreen> {
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 {
),
);
}
}
}

View File

@@ -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<ProfileEvent, ProfileState> {
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<ProfileStarted>(_onStarted);
on<ProfileUpdated>(_onUpdated);
on<ProfilePhotoUpdated>(_onPhotoUploaded);
on<DeleteProfilePressed>(_onDeleteProfilePressed);
}
Future<void> _onStarted(
@@ -55,7 +68,6 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
final avatarId = await uploadProfilePhotoUsecase(event.imageFile);
if (avatarId != null) {
// Отправляем PATCH запрос с новым avatarId
await updateProfileUseCase(UserProfile(
name: '',
birthDate: '',
@@ -72,4 +84,14 @@ class ProfileBloc extends Bloc<ProfileEvent, ProfileState> {
emit(state.copyWith(isLoading: false, error: "Ошибка загрузки: ${e.toString()}"));
}
}
FutureOr<void> _onDeleteProfilePressed(DeleteProfilePressed event, Emitter<ProfileState> emit) async {
try {
await deleteProfileUsecase();
splashBloc.add(AuthLogoutRequested());
} catch (e) {
emit(state.copyWith(isLoading: false, error: "Ошибка удаления профиля: ${e.toString()}"));
}
}
}

View File

@@ -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:

View File

@@ -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