Compare commits
12 Commits
fix/ios-la
...
release
| Author | SHA1 | Date | |
|---|---|---|---|
| 1323de9f79 | |||
| 0a6c40181a | |||
| fcb23e0d5f | |||
| 11c00ad2cc | |||
| 419b54d5dd | |||
| a6e8b9d070 | |||
| 4a919a5bcc | |||
| e91f373258 | |||
| 902c6d27a4 | |||
| 98c5a7a1d1 | |||
| 9eda119527 | |||
| 939b5be7d8 |
@@ -8,6 +8,7 @@
|
|||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.VIEW" />
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
</intent>
|
</intent>
|
||||||
<intent>
|
<intent>
|
||||||
@@ -49,6 +50,14 @@
|
|||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
<intent-filter android:label="card_binding_deep_link">
|
||||||
|
<action android:name="android.intent.action.VIEW" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
<category android:name="android.intent.category.BROWSABLE" />
|
||||||
|
|
||||||
|
<data android:scheme="com.behappy.app" android:host="card-result" />
|
||||||
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
|
|||||||
@@ -2,3 +2,7 @@ org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m
|
|||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
yandexMapkit.variant=full
|
yandexMapkit.variant=full
|
||||||
|
# This builtInKotlin flag was added automatically by Flutter migrator
|
||||||
|
android.builtInKotlin=false
|
||||||
|
# This newDsl flag was added automatically by Flutter migrator
|
||||||
|
android.newDsl=false
|
||||||
|
|||||||
BIN
assets/icons/card_result_screen.png
Normal file
BIN
assets/icons/card_result_screen.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 275 KiB |
BIN
assets/icons/failed.png
Normal file
BIN
assets/icons/failed.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
BIN
assets/icons/success.png
Normal file
BIN
assets/icons/success.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.7 KiB |
3
devtools_options.yaml
Normal file
3
devtools_options.yaml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
description: This file stores settings for Dart & Flutter DevTools.
|
||||||
|
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
|
||||||
|
extensions:
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import '../../domain/entities/client_notification.dart';
|
import '../../domain/entities/client_notification.dart';
|
||||||
|
|
||||||
class ClientNotificationDto {
|
class ClientNotificationDto {
|
||||||
@@ -43,12 +42,19 @@ class ClientNotificationDto {
|
|||||||
clientId: clientId,
|
clientId: clientId,
|
||||||
type: _parseType(type),
|
type: _parseType(type),
|
||||||
category: _parseCategory(category),
|
category: _parseCategory(category),
|
||||||
createdAt: DateTime.parse(createdAt),
|
createdAt: _parseUtcDate(createdAt),
|
||||||
canceledAt: canceledAt != null ? DateTime.parse(canceledAt!) : null,
|
canceledAt: canceledAt != null ? _parseUtcDate(canceledAt!) : null,
|
||||||
readAt: readAt != null ? DateTime.parse(readAt!) : null,
|
readAt: readAt != null ? _parseUtcDate(readAt!) : null,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
DateTime _parseUtcDate(String dateString) {
|
||||||
|
if (dateString.endsWith('Z') || dateString.contains('+') || dateString.contains('-')) {
|
||||||
|
return DateTime.parse(dateString).toLocal();
|
||||||
|
}
|
||||||
|
return DateTime.parse('${dateString}Z').toLocal();
|
||||||
|
}
|
||||||
|
|
||||||
NotificationType _parseType(String type) {
|
NotificationType _parseType(String type) {
|
||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case 'info':
|
case 'info':
|
||||||
@@ -80,4 +86,4 @@ class ClientNotificationDto {
|
|||||||
return NotificationCategory.companyInfo;
|
return NotificationCategory.companyInfo;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ import '../../domain/entities/payment_card.dart';
|
|||||||
|
|
||||||
class PaymentCardResponseDto {
|
class PaymentCardResponseDto {
|
||||||
final int id;
|
final int id;
|
||||||
final int clientId;
|
final String bindingId;
|
||||||
final int expirationMonth;
|
final int expirationMonth;
|
||||||
final int expirationYear;
|
final int expirationYear;
|
||||||
final String cardHolder;
|
final String cardHolder;
|
||||||
@@ -12,7 +12,7 @@ class PaymentCardResponseDto {
|
|||||||
|
|
||||||
PaymentCardResponseDto({
|
PaymentCardResponseDto({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.clientId,
|
required this.bindingId,
|
||||||
required this.expirationMonth,
|
required this.expirationMonth,
|
||||||
required this.expirationYear,
|
required this.expirationYear,
|
||||||
required this.cardHolder,
|
required this.cardHolder,
|
||||||
@@ -24,7 +24,7 @@ class PaymentCardResponseDto {
|
|||||||
factory PaymentCardResponseDto.fromJson(Map<String, dynamic> json) {
|
factory PaymentCardResponseDto.fromJson(Map<String, dynamic> json) {
|
||||||
return PaymentCardResponseDto(
|
return PaymentCardResponseDto(
|
||||||
id: json['id'] as int,
|
id: json['id'] as int,
|
||||||
clientId: json['clientId'] as int,
|
bindingId: json['bindingId'] as String,
|
||||||
expirationMonth: json['expirationMonth'] as int,
|
expirationMonth: json['expirationMonth'] as int,
|
||||||
expirationYear: json['expirationYear'] as int,
|
expirationYear: json['expirationYear'] as int,
|
||||||
cardHolder: json['cardHolder'] as String,
|
cardHolder: json['cardHolder'] as String,
|
||||||
@@ -37,13 +37,9 @@ class PaymentCardResponseDto {
|
|||||||
PaymentCard toEntity(String? fullCardNumber) {
|
PaymentCard toEntity(String? fullCardNumber) {
|
||||||
return PaymentCard(
|
return PaymentCard(
|
||||||
id: id,
|
id: id,
|
||||||
clientId: clientId,
|
bindingId: bindingId,
|
||||||
expirationMonth: expirationMonth,
|
|
||||||
expirationYear: expirationYear,
|
|
||||||
cardHolder: cardHolder,
|
|
||||||
cardLastNumber: cardLastNumber,
|
cardLastNumber: cardLastNumber,
|
||||||
isMain: isMain,
|
isMain: isMain,
|
||||||
type: type,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,11 @@ class ApiService {
|
|||||||
try {
|
try {
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
"$baseUrl/auth/client/login",
|
"$baseUrl/auth/client/login",
|
||||||
data: {"phone": phone, "model": model, "systemId": systemId},
|
data: {
|
||||||
|
"phone": phone,
|
||||||
|
"model": model,
|
||||||
|
"systemId": systemId
|
||||||
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
@@ -143,11 +147,12 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dynamic balanceRaw = profileData["balance"];
|
dynamic balanceRaw = profileData["balance"];
|
||||||
int? balance;
|
double? balance;
|
||||||
if (balanceRaw is int) {
|
|
||||||
balance = balanceRaw;
|
if (balanceRaw is num) {
|
||||||
|
balance = balanceRaw.toDouble();
|
||||||
} else if (balanceRaw is String) {
|
} else if (balanceRaw is String) {
|
||||||
balance = int.tryParse(balanceRaw);
|
balance = double.tryParse(balanceRaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
return UserProfile(
|
return UserProfile(
|
||||||
@@ -177,13 +182,9 @@ class ApiService {
|
|||||||
cardsJson.map((cardJson) async {
|
cardsJson.map((cardJson) async {
|
||||||
return PaymentCard(
|
return PaymentCard(
|
||||||
id: cardJson['id'],
|
id: cardJson['id'],
|
||||||
clientId: cardJson['clientId'],
|
bindingId: cardJson['bindingId'],
|
||||||
expirationMonth: cardJson['expirationMonth'],
|
|
||||||
expirationYear: cardJson['expirationYear'],
|
|
||||||
cardHolder: cardJson['cardHolder'],
|
|
||||||
cardLastNumber: cardJson['cardLastNumber'],
|
cardLastNumber: cardJson['cardLastNumber'],
|
||||||
isMain: cardJson['isMain'],
|
isMain: cardJson['isMain'],
|
||||||
type: cardJson['cardType'],
|
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
@@ -238,11 +239,11 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dynamic balanceRaw = data["balance"];
|
dynamic balanceRaw = data["balance"];
|
||||||
int? balance;
|
double? balance;
|
||||||
if (balanceRaw is int) {
|
if (balanceRaw is double) {
|
||||||
balance = balanceRaw;
|
balance = balanceRaw;
|
||||||
} else if (balanceRaw is String) {
|
} else if (balanceRaw is String) {
|
||||||
balance = int.tryParse(balanceRaw);
|
balance = double.tryParse(balanceRaw);
|
||||||
}
|
}
|
||||||
|
|
||||||
return UserProfile(
|
return UserProfile(
|
||||||
@@ -420,7 +421,7 @@ class ApiService {
|
|||||||
int page = 1,
|
int page = 1,
|
||||||
int pageSize = 500,
|
int pageSize = 500,
|
||||||
}) async {
|
}) async {
|
||||||
const path = "$baseUrl/zone/available";
|
const path = "$baseUrl/zone/available2";
|
||||||
|
|
||||||
final queryParams = {
|
final queryParams = {
|
||||||
'readAll': 'true',
|
'readAll': 'true',
|
||||||
@@ -517,13 +518,13 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<bool> activateSubscription({required int optionId}) async {
|
Future<bool> activateSubscription({required int optionId, required int cardId}) async {
|
||||||
const url = "$baseUrl/scootersubscription/client";
|
const url = "$baseUrl/scootersubscription/client";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
url,
|
url,
|
||||||
data: {"optionId": optionId},
|
data: {"optionId": optionId, "cardId": cardId},
|
||||||
options: await _getAuthOptions(),
|
options: await _getAuthOptions(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -539,31 +540,25 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> addPaymentCard({
|
Future<Map<String, dynamic>> addPaymentCard() async {
|
||||||
required String cardNumber,
|
|
||||||
required String cardHolder,
|
|
||||||
required int expirationMonth,
|
|
||||||
required int expirationYear,
|
|
||||||
required String cvv,
|
|
||||||
}) async {
|
|
||||||
const url = "$baseUrl/client/card";
|
const url = "$baseUrl/client/card";
|
||||||
final cleanCardNumber = cardNumber.replaceAll(' ', '');
|
/*final cleanCardNumber = cardNumber.replaceAll(' ', '');*/
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final response = await _dio.post(
|
final response = await _dio.post(
|
||||||
url,
|
url,
|
||||||
data: {
|
/*data: {
|
||||||
"cardNumber": cleanCardNumber,
|
"cardNumber": cleanCardNumber,
|
||||||
"cardHolder": cardHolder,
|
"cardHolder": cardHolder,
|
||||||
"expirationMonth": expirationMonth,
|
"expirationMonth": expirationMonth,
|
||||||
"expirationYear": expirationYear,
|
"expirationYear": expirationYear,
|
||||||
"cvv": cvv,
|
"cvv": cvv,
|
||||||
},
|
},*/
|
||||||
options: await _getAuthOptions(),
|
options: await _getAuthOptions(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
if (response.statusCode == 200 || response.statusCode == 201) {
|
||||||
return;
|
return response.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
throw AuthException('Непредвиденный статус: ${response.statusCode}', 0);
|
throw AuthException('Непредвиденный статус: ${response.statusCode}', 0);
|
||||||
@@ -714,17 +709,13 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ScooterOrder?> cancelRide(int orderId) async {
|
Future<void> cancelRide(int orderId) async {
|
||||||
try {
|
try {
|
||||||
final response = await _dio.put(
|
final response = await _dio.put(
|
||||||
"$baseUrl/scooterorder/$orderId/cancel",
|
"$baseUrl/scooterorder/$orderId/cancel",
|
||||||
options: await _getAuthOptions(),
|
options: await _getAuthOptions(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
||||||
return ScooterOrder.fromJson(response.data);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
_handleDioError(e);
|
_handleDioError(e);
|
||||||
return null;
|
return null;
|
||||||
@@ -765,7 +756,7 @@ class ApiService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ScooterOrder?> finishRide({
|
Future<void> finishRide({
|
||||||
required int orderId,
|
required int orderId,
|
||||||
required List<int> filesId,
|
required List<int> filesId,
|
||||||
}) async {
|
}) async {
|
||||||
@@ -776,10 +767,6 @@ class ApiService {
|
|||||||
options: await _getAuthOptions(),
|
options: await _getAuthOptions(),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.statusCode == 200 || response.statusCode == 201) {
|
|
||||||
return ScooterOrder.fromJson(response.data);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
} on DioException catch (e) {
|
} on DioException catch (e) {
|
||||||
if (e.response?.statusCode == 400) {
|
if (e.response?.statusCode == 400) {
|
||||||
final data = e.response?.data;
|
final data = e.response?.data;
|
||||||
@@ -1019,10 +1006,10 @@ class ApiService {
|
|||||||
if (event.data != null && event.data!.isNotEmpty) {
|
if (event.data != null && event.data!.isNotEmpty) {
|
||||||
try {
|
try {
|
||||||
final Map<String, dynamic> jsonData = jsonDecode(event.data!);
|
final Map<String, dynamic> jsonData = jsonDecode(event.data!);
|
||||||
print("✅ [SSE] Parsed JSON: $jsonData");
|
print("[SSE] Parsed JSON: $jsonData");
|
||||||
controller.add(jsonData);
|
controller.add(jsonData);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("❌ [SSE] JSON Parse Error: $e | Raw: ${event.data}");
|
print("[SSE] JSON Parse Error: $e | Raw: ${event.data}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -1040,17 +1027,19 @@ class ApiService {
|
|||||||
return controller.stream;
|
return controller.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<List<Map<String, dynamic>>> getNotifications() async {
|
Future<Map<String, dynamic>> getNotifications({
|
||||||
final url = Uri.parse('$baseUrl/notification/client');
|
int page = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
}) async {
|
||||||
|
final url = Uri.parse('$baseUrl/notification/client').replace(
|
||||||
|
queryParameters: {
|
||||||
|
'page': page.toString(),
|
||||||
|
'pageSize': pageSize.toString(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
final accessToken = await _securityService.getAccessToken();
|
final accessToken = await _securityService.getAccessToken();
|
||||||
if (accessToken == null) {
|
if (accessToken == null) throw UnauthorizedException();
|
||||||
print("APISERVICE Error: Access token is null.");
|
|
||||||
throw UnauthorizedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
print("GET NOTIFICATIONS REQUEST:");
|
|
||||||
print("URL: $url");
|
|
||||||
|
|
||||||
final response = await http.get(
|
final response = await http.get(
|
||||||
url,
|
url,
|
||||||
@@ -1060,34 +1049,17 @@ class ApiService {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
print("GET NOTIFICATIONS RESPONSE:");
|
|
||||||
print("STATUS: ${response.statusCode}");
|
|
||||||
print("BODY: ${response.body}");
|
|
||||||
|
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
final data = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
|
if (data is Map<String, dynamic>) {
|
||||||
// ✅ Проверяем, является ли ответ массивом или объектом с data[]
|
return data;
|
||||||
if (data is List) {
|
|
||||||
return data.cast<Map<String, dynamic>>();
|
|
||||||
} else if (data is Map<String, dynamic>) {
|
|
||||||
final list = data['data'];
|
|
||||||
if (list is List) {
|
|
||||||
return list.cast<Map<String, dynamic>>();
|
|
||||||
} else {
|
|
||||||
throw Exception(
|
|
||||||
'Expected a List under "data" but got ${list.runtimeType}',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
throw Exception('Expected a List or Map but got ${data.runtimeType}');
|
|
||||||
}
|
}
|
||||||
|
throw Exception('Expected Map with data and pagination');
|
||||||
} else if (response.statusCode == 401) {
|
} else if (response.statusCode == 401) {
|
||||||
throw UnauthorizedException();
|
throw UnauthorizedException();
|
||||||
} else if (response.statusCode == 403) {
|
} else if (response.statusCode == 403) {
|
||||||
throw AuthBlockException();
|
throw AuthBlockException();
|
||||||
}
|
}
|
||||||
|
|
||||||
throw Exception('Ошибка сервера: ${response.statusCode}');
|
throw Exception('Ошибка сервера: ${response.statusCode}');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import 'package:be_happy/domain/entities/client_notification.dart';
|
import 'package:be_happy/domain/entities/client_notification.dart';
|
||||||
|
|
||||||
import '../../data/network/api_service.dart';
|
import '../../data/network/api_service.dart';
|
||||||
|
import '../../domain/entities/pagination_meta.dart';
|
||||||
import '../../domain/repositories/notification_repository.dart';
|
import '../../domain/repositories/notification_repository.dart';
|
||||||
|
import '../../domain/usecase/get_notifications_usecase.dart';
|
||||||
import '../models/client_notification_dto.dart';
|
import '../models/client_notification_dto.dart';
|
||||||
|
|
||||||
class NotificationRepositoryImpl implements NotificationRepository {
|
class NotificationRepositoryImpl implements NotificationRepository {
|
||||||
@@ -35,19 +37,27 @@ class NotificationRepositoryImpl implements NotificationRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<ClientNotification>> getNotifications() async {
|
Future<NotificationsResult> getNotifications({
|
||||||
|
int page = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
}) async {
|
||||||
try {
|
try {
|
||||||
final List<Map<String, dynamic>> data = await _apiService.getNotifications();
|
final response = await _apiService.getNotifications(
|
||||||
final notifications = data.map((json) {
|
page: page,
|
||||||
final dto = ClientNotificationDto.fromJson(json);
|
pageSize: pageSize,
|
||||||
return dto.toEntity();
|
);
|
||||||
}).toList();
|
|
||||||
|
|
||||||
// dev.log('NotificationRepository: Загружено ${notifications.length} уведомлений');
|
final notifications = (response['data'] as List)
|
||||||
|
.map((json) => ClientNotificationDto.fromJson(json).toEntity())
|
||||||
|
.toList();
|
||||||
|
|
||||||
return notifications;
|
final pagination = PaginationMeta.fromJson(response['pagination']);
|
||||||
|
|
||||||
|
return NotificationsResult(
|
||||||
|
notifications: notifications,
|
||||||
|
pagination: pagination,
|
||||||
|
);
|
||||||
} catch (e, stackTrace) {
|
} catch (e, stackTrace) {
|
||||||
// dev.log('NotificationRepository: Ошибка: $e', stackTrace: stackTrace);
|
|
||||||
throw Exception('Не удалось загрузить уведомления: $e');
|
throw Exception('Не удалось загрузить уведомления: $e');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,25 +32,25 @@ class PaymentRepositoryImpl implements PaymentRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<void>> addPaymentCard({
|
Future<Result<Map<String, dynamic>>> addPaymentCard(
|
||||||
required String cardNumber,
|
/*required String cardNumber,
|
||||||
required String cardHolder,
|
required String cardHolder,
|
||||||
required String expiryMonth,
|
required String expiryMonth,
|
||||||
required String expiryYear,
|
required String expiryYear,
|
||||||
required String cvv,
|
required String cvv,*/
|
||||||
}) async {
|
) async {
|
||||||
try {
|
try {
|
||||||
await apiService.addPaymentCard(
|
final result = await apiService.addPaymentCard(
|
||||||
cardNumber: cardNumber,
|
/*cardNumber: cardNumber,
|
||||||
cardHolder: cardHolder,
|
cardHolder: cardHolder,
|
||||||
expirationMonth: int.parse(expiryMonth),
|
expirationMonth: int.parse(expiryMonth),
|
||||||
expirationYear: int.parse(expiryYear),
|
expirationYear: int.parse(expiryYear),
|
||||||
cvv: cvv,
|
cvv: cvv,*/
|
||||||
);
|
);
|
||||||
|
|
||||||
// await securityService.saveCardFullNumber(cardId, cardNumber);
|
// await securityService.saveCardFullNumber(cardId, cardNumber);
|
||||||
|
|
||||||
return Success(null);
|
return Success(result);
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
return Failure(AuthFailure(e.attemptsLeft));
|
return Failure(AuthFailure(e.attemptsLeft));
|
||||||
} on AuthBlockException catch (_) {
|
} on AuthBlockException catch (_) {
|
||||||
@@ -98,9 +98,9 @@ class PaymentRepositoryImpl implements PaymentRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<bool>> activateSubscription(int optionId) async {
|
Future<Result<bool>> activateSubscription(int optionId, int cardId) async {
|
||||||
try {
|
try {
|
||||||
final success = await apiService.activateSubscription(optionId: optionId);
|
final success = await apiService.activateSubscription(optionId: optionId, cardId: cardId);
|
||||||
return Success(success);
|
return Success(success);
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
return Failure(AuthFailure(e.attemptsLeft));
|
return Failure(AuthFailure(e.attemptsLeft));
|
||||||
|
|||||||
@@ -192,15 +192,11 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<ScooterOrder>> cancelRide(int orderId) async {
|
Future<Result<void>> cancelRide(int orderId) async {
|
||||||
late final Result<ScooterOrder> result;
|
late final Result<void> result;
|
||||||
try {
|
try {
|
||||||
final order = await _apiService.cancelRide(orderId);
|
await _apiService.cancelRide(orderId);
|
||||||
if (order != null) {
|
result = Success(null);
|
||||||
result = Success(order);
|
|
||||||
} else {
|
|
||||||
result = Failure(UnknownFailure("Неизвестная ошибка"));
|
|
||||||
}
|
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
result = Failure(AuthFailure(e.attemptsLeft));
|
result = Failure(AuthFailure(e.attemptsLeft));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -246,18 +242,11 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<Result<ScooterOrder>> finishRide(int orderId, List<int> files) async {
|
Future<Result<void>> finishRide(int orderId, List<int> files) async {
|
||||||
late final Result<ScooterOrder> result;
|
late final Result<ScooterOrder> result;
|
||||||
try {
|
try {
|
||||||
final order = await _apiService.finishRide(
|
await _apiService.finishRide(orderId: orderId, filesId: files);
|
||||||
orderId: orderId,
|
result = Success(null);
|
||||||
filesId: files,
|
|
||||||
);
|
|
||||||
if (order != null) {
|
|
||||||
result = Success(order);
|
|
||||||
} else {
|
|
||||||
result = Failure(UnknownFailure("Неизвестная ошибка"));
|
|
||||||
}
|
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
result = Failure(AuthFailure(e.attemptsLeft));
|
result = Failure(AuthFailure(e.attemptsLeft));
|
||||||
} on WrongZoneException catch (e) {
|
} on WrongZoneException catch (e) {
|
||||||
@@ -274,7 +263,6 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
|||||||
try {
|
try {
|
||||||
await _apiService.payRide(orderId);
|
await _apiService.payRide(orderId);
|
||||||
result = Success(null);
|
result = Success(null);
|
||||||
|
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
result = Failure(AuthFailure(e.attemptsLeft));
|
result = Failure(AuthFailure(e.attemptsLeft));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -345,7 +333,7 @@ class ScooterRepositoryImpl extends ScooterRepository {
|
|||||||
cardId: cardId,
|
cardId: cardId,
|
||||||
isBalance: isBalance,
|
isBalance: isBalance,
|
||||||
);
|
);
|
||||||
result = Success(null);
|
result = Success(null);
|
||||||
} on AuthException catch (e) {
|
} on AuthException catch (e) {
|
||||||
result = Failure(AuthFailure(e.attemptsLeft));
|
result = Failure(AuthFailure(e.attemptsLeft));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -305,9 +305,9 @@ Future<void> setupDependencies() async {
|
|||||||
|
|
||||||
getIt.registerFactory<PhoneAuthBloc>(() => PhoneAuthBloc(getIt()));
|
getIt.registerFactory<PhoneAuthBloc>(() => PhoneAuthBloc(getIt()));
|
||||||
|
|
||||||
getIt.registerFactory<VerifyCodeBloc>(() => VerifyCodeBloc(getIt()));
|
getIt.registerFactory<VerifyCodeBloc>(() => VerifyCodeBloc(getIt(), getIt(), getIt()));
|
||||||
|
|
||||||
getIt.registerFactory<ProfileBloc>(() => ProfileBloc(getIt(), getIt(), getIt()));
|
getIt.registerLazySingleton<ProfileBloc>(() => ProfileBloc(getIt(), getIt(), getIt()));
|
||||||
|
|
||||||
getIt.registerFactory<EditProfileBloc>(
|
getIt.registerFactory<EditProfileBloc>(
|
||||||
() => EditProfileBloc(getIt(), getIt()),
|
() => EditProfileBloc(getIt(), getIt()),
|
||||||
@@ -337,7 +337,7 @@ Future<void> setupDependencies() async {
|
|||||||
|
|
||||||
|
|
||||||
getIt.registerFactory<PaymentMethodSheetBloc>(
|
getIt.registerFactory<PaymentMethodSheetBloc>(
|
||||||
() => PaymentMethodSheetBloc(getIt()),
|
() => PaymentMethodSheetBloc(getIt(), getIt(), getIt(), getIt()),
|
||||||
);
|
);
|
||||||
|
|
||||||
getIt.registerFactory<CurrentRidesBloc>(() => CurrentRidesBloc(getIt()));
|
getIt.registerFactory<CurrentRidesBloc>(() => CurrentRidesBloc(getIt()));
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class ClientNotification {
|
|||||||
required this.createdAt,
|
required this.createdAt,
|
||||||
this.canceledAt,
|
this.canceledAt,
|
||||||
this.readAt,
|
this.readAt,
|
||||||
});
|
}) ;
|
||||||
|
|
||||||
ClientNotification copyWith({
|
ClientNotification copyWith({
|
||||||
int? id,
|
int? id,
|
||||||
|
|||||||
28
lib/domain/entities/pagination_meta.dart
Normal file
28
lib/domain/entities/pagination_meta.dart
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
class PaginationMeta {
|
||||||
|
final int total;
|
||||||
|
final int lastPage;
|
||||||
|
final int currentPage;
|
||||||
|
final int totalPerPage;
|
||||||
|
final int? prevPage;
|
||||||
|
final int? nextPage;
|
||||||
|
|
||||||
|
PaginationMeta({
|
||||||
|
required this.total,
|
||||||
|
required this.lastPage,
|
||||||
|
required this.currentPage,
|
||||||
|
required this.totalPerPage,
|
||||||
|
this.prevPage,
|
||||||
|
this.nextPage,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory PaginationMeta.fromJson(Map<String, dynamic> json) {
|
||||||
|
return PaginationMeta(
|
||||||
|
total: json['total'] as int,
|
||||||
|
lastPage: json['lastPage'] as int,
|
||||||
|
currentPage: json['currentPage'] as int,
|
||||||
|
totalPerPage: json['totalPerPage'] as int,
|
||||||
|
prevPage: json['prevPage'] as int?,
|
||||||
|
nextPage: json['nextPage'] as int?,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,43 +1,27 @@
|
|||||||
class PaymentCard {
|
class PaymentCard {
|
||||||
final int id;
|
final int id;
|
||||||
final int clientId;
|
final String? bindingId;
|
||||||
final int expirationMonth;
|
|
||||||
final int expirationYear;
|
|
||||||
final String cardHolder;
|
|
||||||
final String cardLastNumber;
|
final String cardLastNumber;
|
||||||
final bool isMain;
|
final bool isMain;
|
||||||
final String type;
|
|
||||||
|
|
||||||
PaymentCard({
|
PaymentCard({
|
||||||
required this.id,
|
required this.id,
|
||||||
required this.clientId,
|
required this.bindingId,
|
||||||
required this.expirationMonth,
|
|
||||||
required this.expirationYear,
|
|
||||||
required this.cardHolder,
|
|
||||||
required this.cardLastNumber,
|
required this.cardLastNumber,
|
||||||
required this.isMain,
|
required this.isMain,
|
||||||
required this.type,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
PaymentCard copyWith({
|
PaymentCard copyWith({
|
||||||
int? id,
|
int? id,
|
||||||
int? clientId,
|
String? bindingId,
|
||||||
int? expirationMonth,
|
|
||||||
int? expirationYear,
|
|
||||||
String? cardHolder,
|
|
||||||
String? cardLastNumber,
|
String? cardLastNumber,
|
||||||
bool? isMain,
|
bool? isMain,
|
||||||
String? type,
|
|
||||||
}) {
|
}) {
|
||||||
return PaymentCard(
|
return PaymentCard(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
clientId: clientId ?? this.clientId,
|
bindingId: bindingId ?? this.bindingId,
|
||||||
expirationMonth: expirationMonth ?? this.expirationMonth,
|
|
||||||
expirationYear: expirationYear ?? this.expirationYear,
|
|
||||||
cardHolder: cardHolder ?? this.cardHolder,
|
|
||||||
cardLastNumber: cardLastNumber ?? this.cardLastNumber,
|
cardLastNumber: cardLastNumber ?? this.cardLastNumber,
|
||||||
isMain: isMain ?? this.isMain,
|
isMain: isMain ?? this.isMain,
|
||||||
type: type ?? this.type,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -29,6 +29,9 @@ class ScooterOrder {
|
|||||||
final String? cancelDescription;
|
final String? cancelDescription;
|
||||||
final double mileage; //эту
|
final double mileage; //эту
|
||||||
final double avgSpeed; //эту и снизу еще 4 есть
|
final double avgSpeed; //эту и снизу еще 4 есть
|
||||||
|
final String payTotalPricePrint;
|
||||||
|
final String unpayTotalPricePrint;
|
||||||
|
final String totalOrderPricePrint;
|
||||||
|
|
||||||
ScooterOrder({
|
ScooterOrder({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -57,11 +60,16 @@ class ScooterOrder {
|
|||||||
this.expiresAt,
|
this.expiresAt,
|
||||||
this.cancelAt,
|
this.cancelAt,
|
||||||
this.cancelDescription,
|
this.cancelDescription,
|
||||||
required this.mileage, //эту
|
required this.mileage,
|
||||||
required this.avgSpeed, //эту и снизу еще 2 есть
|
required this.avgSpeed,
|
||||||
|
required this.payTotalPricePrint, // оплачено
|
||||||
|
required this.unpayTotalPricePrint, //неоплачено
|
||||||
|
required this.totalOrderPricePrint, //общая
|
||||||
});
|
});
|
||||||
|
|
||||||
factory ScooterOrder.fromJson(Map<String, dynamic> json) {
|
factory ScooterOrder.fromJson(Map<String, dynamic> json) {
|
||||||
|
final paymentInfo = json['paymentInfo'] as Map<String, dynamic>?;
|
||||||
|
|
||||||
return ScooterOrder(
|
return ScooterOrder(
|
||||||
id: json['id'] ?? 0,
|
id: json['id'] ?? 0,
|
||||||
scooterId: json['scooterId'] ?? 0,
|
scooterId: json['scooterId'] ?? 0,
|
||||||
@@ -101,8 +109,11 @@ class ScooterOrder {
|
|||||||
? DateTime.parse(json['cancelAt'])
|
? DateTime.parse(json['cancelAt'])
|
||||||
: null,
|
: null,
|
||||||
cancelDescription: json['cancelDescription'],
|
cancelDescription: json['cancelDescription'],
|
||||||
mileage: (json['mileage'] as num?)?.toDouble() ?? 0.0, //эту
|
mileage: (json['mileage'] as num?)?.toDouble() ?? 0.0,
|
||||||
avgSpeed: (json['avgSpeed'] as num?)?.toDouble() ?? 0.0, //эту
|
avgSpeed: (json['avgSpeed'] as num?)?.toDouble() ?? 0.0,
|
||||||
|
payTotalPricePrint: paymentInfo?['payTotalPricePrint'] ?? '',
|
||||||
|
unpayTotalPricePrint: paymentInfo?['unpayTotalPricePrint'] ?? '',
|
||||||
|
totalOrderPricePrint: paymentInfo?['totalOrderPricePrint'] ?? '',
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ class UserProfile {
|
|||||||
String birthDate;
|
String birthDate;
|
||||||
String phone;
|
String phone;
|
||||||
String email;
|
String email;
|
||||||
int? balance;
|
double? balance;
|
||||||
int? avatarId;
|
int? avatarId;
|
||||||
String? avatarUrl;
|
String? avatarUrl;
|
||||||
|
|
||||||
@@ -21,7 +21,7 @@ class UserProfile {
|
|||||||
String? name,
|
String? name,
|
||||||
String? birthDate,
|
String? birthDate,
|
||||||
String? email,
|
String? email,
|
||||||
int? balance,
|
double? balance,
|
||||||
int? avatarId,
|
int? avatarId,
|
||||||
String? avatarUrl,
|
String? avatarUrl,
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import 'dart:convert';
|
import 'dart:convert';
|
||||||
|
|
||||||
import 'package:be_happy/domain/entities/point.dart';
|
import 'package:be_happy/domain/entities/point.dart';
|
||||||
|
|
||||||
class Zone {
|
class Zone {
|
||||||
@@ -9,7 +8,10 @@ class Zone {
|
|||||||
final String type;
|
final String type;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
final String shapeType;
|
final String shapeType;
|
||||||
final List<Point> points;
|
// Трехмерная структура для поддержки MultiPolygon и дыр (holes):
|
||||||
|
// geometry[индекс_полигона][индекс_кольца][индекс_точки]
|
||||||
|
// индекс_кольца == 0 — внешняя граница, > 0 — внутренние вырезы (дыры)
|
||||||
|
final List<List<List<Point>>> geometry;
|
||||||
final String speedLimit;
|
final String speedLimit;
|
||||||
|
|
||||||
Zone({
|
Zone({
|
||||||
@@ -19,38 +21,84 @@ class Zone {
|
|||||||
required this.type,
|
required this.type,
|
||||||
required this.isActive,
|
required this.isActive,
|
||||||
required this.shapeType,
|
required this.shapeType,
|
||||||
required this.points,
|
required this.geometry,
|
||||||
required this.speedLimit,
|
required this.speedLimit,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Геттер для обратной совместимости (возвращает внешнее кольцо первого полигона)
|
||||||
|
List<Point> get points {
|
||||||
|
if (geometry.isNotEmpty && geometry[0].isNotEmpty) {
|
||||||
|
return geometry[0][0];
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
factory Zone.fromJson(Map<String, dynamic> json) {
|
factory Zone.fromJson(Map<String, dynamic> json) {
|
||||||
final zoneCoordinates = json['coordinates'] as Map<String, dynamic>? ?? {};
|
final zoneCoordinates = json['coordinates'] as Map<String, dynamic>? ?? {};
|
||||||
final String coordsString = zoneCoordinates['coordinates'] ?? '[]';
|
final String coordsString = zoneCoordinates['coordinates'] ?? '[]';
|
||||||
final String shapeType = zoneCoordinates['type'] ?? 'Polygon';
|
final String shapeType = zoneCoordinates['type'] ?? 'Polygon';
|
||||||
|
|
||||||
List<Point> points = [];
|
List<List<List<Point>>> parsedGeometry = [];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final dynamic decoded = jsonDecode(coordsString);
|
final dynamic decoded = jsonDecode(coordsString);
|
||||||
|
|
||||||
if (decoded is List && decoded.isNotEmpty) {
|
if (decoded is List && decoded.isNotEmpty) {
|
||||||
List<dynamic> targetList = [];
|
|
||||||
|
|
||||||
if (shapeType == 'Polygon') {
|
if (shapeType == 'Polygon') {
|
||||||
// У полигона структура [[[lat, lon], ...]] -> уходим на 1 уровень вглубь
|
// Структура: [[[lon, lat], [lon, lat], ...], [hole1], ...]
|
||||||
targetList = decoded[0] as List<dynamic>;
|
List<List<Point>> polygonRings = [];
|
||||||
} else {
|
for (var ring in decoded) {
|
||||||
// У LineString структура [[lat, lon], ...] -> используем как есть
|
if (ring is List) {
|
||||||
targetList = decoded;
|
List<Point> ringPoints = [];
|
||||||
}
|
for (var coord in ring) {
|
||||||
|
if (coord is List && coord.length >= 2) {
|
||||||
|
// В GeoJSON сначала идет Longitude, затем Latitude
|
||||||
|
ringPoints.add(Point(
|
||||||
|
(coord[1] as num).toDouble(),
|
||||||
|
(coord[0] as num).toDouble(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ringPoints.isNotEmpty) polygonRings.add(ringPoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (polygonRings.isNotEmpty) parsedGeometry.add(polygonRings);
|
||||||
|
|
||||||
points = targetList.map((item) {
|
} else if (shapeType == 'MultiPolygon') {
|
||||||
final List<dynamic> coords = item as List<dynamic>;
|
// Структура: [[[[lon, lat], ...]], [[[lon, lat], ...]]]
|
||||||
return Point(
|
for (var poly in decoded) {
|
||||||
(coords[1] as num).toDouble(),
|
if (poly is List) {
|
||||||
(coords[0] as num).toDouble(),
|
List<List<Point>> polygonRings = [];
|
||||||
);
|
for (var ring in poly) {
|
||||||
}).toList();
|
if (ring is List) {
|
||||||
|
List<Point> ringPoints = [];
|
||||||
|
for (var coord in ring) {
|
||||||
|
if (coord is List && coord.length >= 2) {
|
||||||
|
ringPoints.add(Point(
|
||||||
|
(coord[1] as num).toDouble(),
|
||||||
|
(coord[0] as num).toDouble(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ringPoints.isNotEmpty) polygonRings.add(ringPoints);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (polygonRings.isNotEmpty) parsedGeometry.add(polygonRings);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Fallback для LineString или плоских списков
|
||||||
|
List<Point> ringPoints = [];
|
||||||
|
for (var coord in decoded) {
|
||||||
|
if (coord is List && coord.length >= 2) {
|
||||||
|
ringPoints.add(Point(
|
||||||
|
(coord[1] as num).toDouble(),
|
||||||
|
(coord[0] as num).toDouble(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (ringPoints.isNotEmpty) parsedGeometry.add([ringPoints]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("PARSE ERROR for Zone ID ${json['id']}: $e");
|
print("PARSE ERROR for Zone ID ${json['id']}: $e");
|
||||||
@@ -64,11 +112,12 @@ class Zone {
|
|||||||
isActive: json['isActive'] ?? false,
|
isActive: json['isActive'] ?? false,
|
||||||
speedLimit: json['speedLimit'] ?? '',
|
speedLimit: json['speedLimit'] ?? '',
|
||||||
shapeType: shapeType,
|
shapeType: shapeType,
|
||||||
points: points,
|
geometry: parsedGeometry,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
String toString() {
|
String toString() {
|
||||||
return 'Zone{id: $id, title: $title, type: $type, points: $points}';
|
return 'Zone{id: $id, title: $title, type: $type, polygonsCount: ${geometry.length}}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:be_happy/domain/entities/client_notification.dart';
|
import 'package:be_happy/domain/entities/client_notification.dart';
|
||||||
|
|
||||||
|
import '../usecase/get_notifications_usecase.dart';
|
||||||
|
|
||||||
abstract class NotificationRepository {
|
abstract class NotificationRepository {
|
||||||
/// Устанавливает постоянное SSE-соединение и возвращает поток уведомлений
|
/// Устанавливает постоянное SSE-соединение и возвращает поток уведомлений
|
||||||
Stream<ClientNotification> getNotificationsStream();
|
Stream<ClientNotification> getNotificationsStream();
|
||||||
@@ -11,5 +13,7 @@ abstract class NotificationRepository {
|
|||||||
void closeStream();
|
void closeStream();
|
||||||
|
|
||||||
/// получить список уведомлений
|
/// получить список уведомлений
|
||||||
Future<List<ClientNotification>> getNotifications();
|
Future<NotificationsResult> getNotifications({
|
||||||
}
|
int page = 1,
|
||||||
|
int pageSize = 10,
|
||||||
|
});}
|
||||||
|
|||||||
@@ -4,17 +4,17 @@ import '../entities/payment_card.dart';
|
|||||||
abstract class PaymentRepository {
|
abstract class PaymentRepository {
|
||||||
Future<Result<List<PaymentCard>>> getPaymentCards();
|
Future<Result<List<PaymentCard>>> getPaymentCards();
|
||||||
|
|
||||||
Future<Result<void>> addPaymentCard({
|
Future<Result<Map<String, dynamic>>> addPaymentCard(
|
||||||
required String cardNumber,
|
/*required String cardNumber,
|
||||||
required String cardHolder,
|
required String cardHolder,
|
||||||
required String expiryMonth,
|
required String expiryMonth,
|
||||||
required String expiryYear,
|
required String expiryYear,
|
||||||
required String cvv,
|
required String cvv,*/
|
||||||
});
|
);
|
||||||
|
|
||||||
Future<Result<void>> setMainPaymentCard(int cardId);
|
Future<Result<void>> setMainPaymentCard(int cardId);
|
||||||
|
|
||||||
Future<Result<void>> removePaymentCard(int cardId);
|
Future<Result<void>> removePaymentCard(int cardId);
|
||||||
|
|
||||||
Future<Result<bool>> activateSubscription(int optionId);
|
Future<Result<bool>> activateSubscription(int optionId, int cardId);
|
||||||
}
|
}
|
||||||
@@ -27,10 +27,10 @@ abstract class ScooterRepository {
|
|||||||
required bool isInsurance,
|
required bool isInsurance,
|
||||||
});
|
});
|
||||||
Future<Result<ScooterOrder>> startRide(int orderId);
|
Future<Result<ScooterOrder>> startRide(int orderId);
|
||||||
Future<Result<ScooterOrder>> cancelRide(int orderId);
|
Future<Result<void>> cancelRide(int orderId);
|
||||||
Future<Result<ScooterOrder>> pauseRide(int orderId);
|
Future<Result<ScooterOrder>> pauseRide(int orderId);
|
||||||
Future<Result<ScooterOrder>> resumeRide(int orderId);
|
Future<Result<ScooterOrder>> resumeRide(int orderId);
|
||||||
Future<Result<ScooterOrder>> finishRide(int orderId, List<int> files);
|
Future<Result<void>> finishRide(int orderId, List<int> files);
|
||||||
Future<Result<void>> payRide(int orderId);
|
Future<Result<void>> payRide(int orderId);
|
||||||
Future<Result<List<ScooterOrder>>> getClientOrders();
|
Future<Result<List<ScooterOrder>>> getClientOrders();
|
||||||
Future<Result<List<int>>> uploadScooterPhotos(List<File> images);
|
Future<Result<List<int>>> uploadScooterPhotos(List<File> images);
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ class ActivateSubscriptionUsecase {
|
|||||||
|
|
||||||
ActivateSubscriptionUsecase(this.repository);
|
ActivateSubscriptionUsecase(this.repository);
|
||||||
|
|
||||||
Future<Result<bool>> call(int optionId) {
|
Future<Result<bool>> call(int optionId, int cardId) {
|
||||||
return repository.activateSubscription(optionId);
|
return repository.activateSubscription(optionId, cardId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,19 +6,19 @@ class AddPaymentCardUsecase {
|
|||||||
|
|
||||||
AddPaymentCardUsecase(this.repository);
|
AddPaymentCardUsecase(this.repository);
|
||||||
|
|
||||||
Future<Result<void>> call({
|
Future<Result<Map<String, dynamic>>> call(
|
||||||
required String cardNumber,
|
/*required String cardNumber,
|
||||||
required String cardHolder,
|
required String cardHolder,
|
||||||
required String expiryMonth,
|
required String expiryMonth,
|
||||||
required String expiryYear,
|
required String expiryYear,
|
||||||
required String cvv,
|
required String cvv,*/
|
||||||
}) {
|
) {
|
||||||
return repository.addPaymentCard(
|
return repository.addPaymentCard(
|
||||||
cardNumber: cardNumber,
|
/*cardNumber: cardNumber,
|
||||||
cardHolder: cardHolder,
|
cardHolder: cardHolder,
|
||||||
expiryMonth: expiryMonth,
|
expiryMonth: expiryMonth,
|
||||||
expiryYear: expiryYear,
|
expiryYear: expiryYear,
|
||||||
cvv: cvv,
|
cvv: cvv,*/
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,7 +7,7 @@ class CancelRideUsecase {
|
|||||||
|
|
||||||
CancelRideUsecase(this.repository);
|
CancelRideUsecase(this.repository);
|
||||||
|
|
||||||
Future<Result<ScooterOrder>> call(int orderId) {
|
Future<Result<void>> call(int orderId) {
|
||||||
return repository.cancelRide(orderId);
|
return repository.cancelRide(orderId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ class FinishRideUsecase {
|
|||||||
|
|
||||||
FinishRideUsecase(this.repository);
|
FinishRideUsecase(this.repository);
|
||||||
|
|
||||||
Future<Result<ScooterOrder>> call(int orderId, List<int> files) {
|
Future<Result<void>> call(int orderId, List<int> files) {
|
||||||
return repository.finishRide(orderId, files);
|
return repository.finishRide(orderId, files);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import '../entities/client_notification.dart';
|
import '../entities/client_notification.dart';
|
||||||
|
import '../entities/pagination_meta.dart';
|
||||||
import '../repositories/notification_repository.dart';
|
import '../repositories/notification_repository.dart';
|
||||||
|
|
||||||
class GetNotificationsUsecase {
|
class GetNotificationsUsecase {
|
||||||
@@ -6,7 +7,17 @@ class GetNotificationsUsecase {
|
|||||||
|
|
||||||
GetNotificationsUsecase(this.repository);
|
GetNotificationsUsecase(this.repository);
|
||||||
|
|
||||||
Future<List<ClientNotification>> call() {
|
Future<NotificationsResult> call({int page = 1, int pageSize = 10}) {
|
||||||
return repository.getNotifications();
|
return repository.getNotifications(page: page, pageSize: pageSize);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationsResult {
|
||||||
|
final List<ClientNotification> notifications;
|
||||||
|
final PaginationMeta pagination;
|
||||||
|
|
||||||
|
NotificationsResult({
|
||||||
|
required this.notifications,
|
||||||
|
required this.pagination,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -57,6 +57,7 @@ class MyApp extends StatelessWidget {
|
|||||||
BlocProvider(create: (context) => getIt<VerifyCodeBloc>()),
|
BlocProvider(create: (context) => getIt<VerifyCodeBloc>()),
|
||||||
BlocProvider(create: (context) => getIt<PinBloc>()),
|
BlocProvider(create: (context) => getIt<PinBloc>()),
|
||||||
BlocProvider(create: (context) => getIt<MapBloc>()),
|
BlocProvider(create: (context) => getIt<MapBloc>()),
|
||||||
|
BlocProvider(create: (context) => getIt<ProfileBloc>()),
|
||||||
],
|
],
|
||||||
child: BlocListener<SplashBloc, SplashState>(
|
child: BlocListener<SplashBloc, SplashState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
|
|||||||
@@ -2,20 +2,83 @@ import 'package:flutter/material.dart';
|
|||||||
import 'scooter_info_item.dart';
|
import 'scooter_info_item.dart';
|
||||||
|
|
||||||
class ScooterInfoSection extends StatelessWidget {
|
class ScooterInfoSection extends StatelessWidget {
|
||||||
const ScooterInfoSection({super.key});
|
final double distance;
|
||||||
|
final String location;
|
||||||
|
final int batteryLevel;
|
||||||
|
|
||||||
|
const ScooterInfoSection({
|
||||||
|
super.key,
|
||||||
|
required this.distance,
|
||||||
|
required this.location,
|
||||||
|
required this.batteryLevel,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
// 1. Расчет оставшегося расстояния (макс. 62 км)
|
||||||
|
final double remainingKm = (62 * batteryLevel) / 100;
|
||||||
|
|
||||||
|
// Красиво форматируем: если число целое — убираем точку (например, 31 км вместо 31.0 км)
|
||||||
|
final String kmText = remainingKm % 1 == 0
|
||||||
|
? '${remainingKm.toInt()} км'
|
||||||
|
: '${remainingKm.toStringAsFixed(1)} км';
|
||||||
|
|
||||||
|
// 2. Расчет оставшегося времени (макс. 2.5 часа = 150 минут)
|
||||||
|
final int totalMinutes = ((150 * batteryLevel) / 100).round();
|
||||||
|
final int hours = totalMinutes ~/ 60;
|
||||||
|
final int minutes = totalMinutes % 60;
|
||||||
|
|
||||||
|
// Формируем строку времени в зависимости от получившихся значений
|
||||||
|
String timeText = '';
|
||||||
|
if (hours > 0 && minutes > 0) {
|
||||||
|
timeText = '$hours ч $minutes мин';
|
||||||
|
} else if (hours > 0) {
|
||||||
|
timeText = '$hours ч';
|
||||||
|
} else {
|
||||||
|
timeText = '$minutes мин';
|
||||||
|
}
|
||||||
|
|
||||||
|
final int walkingMinutes = (distance / 80).round().clamp(1, 999);
|
||||||
|
final int walkHours = walkingMinutes ~/ 60;
|
||||||
|
final int walkMins = walkingMinutes % 60;
|
||||||
|
|
||||||
|
String walkingText = '';
|
||||||
|
if (walkHours > 0 && walkMins > 0) {
|
||||||
|
walkingText = '$walkHours ч $walkMins мин';
|
||||||
|
} else if (walkHours > 0) {
|
||||||
|
walkingText = '$walkHours ч';
|
||||||
|
} else {
|
||||||
|
walkingText = '$walkingMinutes мин';
|
||||||
|
}
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
children: const [
|
children: [
|
||||||
ScooterInfoItem(icon: 'assets/icons/bolt.png', text: '47 км или 4 часа 17 минут'),
|
// Динамическая строка с зарядом
|
||||||
ScooterInfoItem(icon: 'assets/icons/speed.png', text: 'max = 25 км/ч'),
|
ScooterInfoItem(
|
||||||
ScooterInfoItem(icon: 'assets/icons/location.png', text: 'пр. Московский, 33'),
|
icon: 'assets/icons/bolt.png',
|
||||||
|
text: '$kmText или $timeText',
|
||||||
|
),
|
||||||
|
ScooterInfoItem(
|
||||||
|
icon: 'assets/icons/speed.png',
|
||||||
|
text: 'max = 25 км/ч',
|
||||||
|
),
|
||||||
|
ScooterInfoItem(
|
||||||
|
icon: 'assets/icons/location.png',
|
||||||
|
text: location,
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
ScooterInfoItem(icon: 'assets/icons/person.png', text: '120 м'),
|
ScooterInfoItem(
|
||||||
SizedBox(width: 16),
|
icon: 'assets/icons/person.png',
|
||||||
ScooterInfoItem(icon: 'assets/icons/time.png', text: '1 минута'),
|
text: distance.toInt() >= 1000
|
||||||
|
? '${((distance.toInt()) / 1000).toStringAsFixed(1)} km'
|
||||||
|
: '${distance.toInt()} m',
|
||||||
|
),
|
||||||
|
const SizedBox(width: 16),
|
||||||
|
ScooterInfoItem(
|
||||||
|
icon: 'assets/icons/time.png',
|
||||||
|
text: walkingText,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
|||||||
child: _ScooterCard(
|
child: _ScooterCard(
|
||||||
scooter: scooter,
|
scooter: scooter,
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
|
address: state.address ?? "Unknown address",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -151,8 +152,9 @@ class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
|||||||
class _ScooterCard extends StatelessWidget {
|
class _ScooterCard extends StatelessWidget {
|
||||||
final Scooter scooter;
|
final Scooter scooter;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
|
final String address;
|
||||||
|
|
||||||
const _ScooterCard({required this.scooter, required this.isActive});
|
const _ScooterCard({required this.scooter, required this.isActive, required this.address});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -186,7 +188,9 @@ class _ScooterCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
"${(scooter.distance?.toInt()) ?? 0}m",
|
(scooter.distance?.toInt() ?? 0) >= 1000
|
||||||
|
? '${((scooter.distance?.toInt() ?? 0) / 1000).toStringAsFixed(1)} km'
|
||||||
|
: '${scooter.distance?.toInt() ?? 0} m',
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -229,7 +233,7 @@ class _ScooterCard extends StatelessWidget {
|
|||||||
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.go('/home/scooter/${scooter.id}');
|
context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]);
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
height: 42,
|
height: 42,
|
||||||
@@ -241,7 +245,7 @@ class _ScooterCard extends StatelessWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
context.go('/home/scooter/${scooter.id}');
|
context.go('/home/scooter/${scooter.id}', extra: [address, scooter.distance]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -218,6 +218,9 @@ class _RideCardState extends State<_RideCard> {
|
|||||||
} else {
|
} else {
|
||||||
displayTime = _elapsedTime;
|
displayTime = _elapsedTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final isExpired = isReserved && (_reservationTime - _elapsedTime).isNegative;
|
||||||
|
|
||||||
final timeString = _formatDuration(displayTime);
|
final timeString = _formatDuration(displayTime);
|
||||||
final statusText = widget.order.status == 'Booking' ? 'Забронирован' : "Активный";
|
final statusText = widget.order.status == 'Booking' ? 'Забронирован' : "Активный";
|
||||||
final statusColor = widget.order.status == 'Booking' ? Color(0xFFFFCC00) : Color(0xFF8bffaa);
|
final statusColor = widget.order.status == 'Booking' ? Color(0xFFFFCC00) : Color(0xFF8bffaa);
|
||||||
@@ -347,7 +350,8 @@ class _RideCardState extends State<_RideCard> {
|
|||||||
height: 32,
|
height: 32,
|
||||||
width: 100,
|
width: 100,
|
||||||
fontSize: 11,
|
fontSize: 11,
|
||||||
onTap: () {
|
enabled: !isExpired,
|
||||||
|
onTap: !isExpired ? () {
|
||||||
if (isReserved) {
|
if (isReserved) {
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
@@ -374,7 +378,7 @@ class _RideCardState extends State<_RideCard> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
} : null,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ class PaymentMethodSheet extends StatefulWidget {
|
|||||||
|
|
||||||
class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
||||||
int? _selectedPaymentMethod = -2;
|
int? _selectedPaymentMethod = -2;
|
||||||
|
int? _processingCardId; // <-- Переменная для отслеживания отправленного запроса
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -36,56 +37,20 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocBuilder<PaymentMethodSheetBloc, PaymentMethodSheetState>(
|
return BlocConsumer<PaymentMethodSheetBloc, PaymentMethodSheetState>(
|
||||||
|
listener: (context, state) {
|
||||||
|
if (state.status == PaymentMethodSheetStatus.success && _processingCardId != null) {
|
||||||
|
final updatedCard = state.cards.cast<PaymentCard?>().firstWhere(
|
||||||
|
(c) => c?.id == _processingCardId,
|
||||||
|
orElse: () => state.cards.cast<PaymentCard?>().firstWhere((c) => c?.isMain == true, orElse: () => null),
|
||||||
|
);
|
||||||
|
|
||||||
|
_processingCardId = null;
|
||||||
|
Navigator.pop(context, updatedCard);
|
||||||
|
}
|
||||||
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
|
|
||||||
if (state.status == PaymentMethodSheetStatus.loading) {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
|
||||||
child: BackdropFilter(
|
|
||||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
|
||||||
child: Container(
|
|
||||||
height: 450,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0x00000032).withOpacity(0.6),
|
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: CircularProgressIndicator(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.status == PaymentMethodSheetStatus.failure) {
|
|
||||||
return Align(
|
|
||||||
alignment: Alignment.bottomCenter,
|
|
||||||
child: ClipRRect(
|
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
|
||||||
child: BackdropFilter(
|
|
||||||
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
|
|
||||||
child: Container(
|
|
||||||
height: 450,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0x00000032).withOpacity(0.6),
|
|
||||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
state.errorMessage ?? 'Ошибка загрузки карт',
|
|
||||||
style: const TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (state.status == PaymentMethodSheetStatus.success && _selectedPaymentMethod == -2) {
|
if (state.status == PaymentMethodSheetStatus.success && _selectedPaymentMethod == -2) {
|
||||||
if (widget.initialSelectedCard != null) {
|
if (widget.initialSelectedCard != null) {
|
||||||
final initialIndex = state.cards.indexWhere(
|
final initialIndex = state.cards.indexWhere(
|
||||||
@@ -98,14 +63,6 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Находим карту с isMain: true при загрузке
|
|
||||||
/*if (_selectedPaymentMethod == null) {
|
|
||||||
final mainCardIndex = state.cards.indexWhere((card) => card.isMain);
|
|
||||||
if (mainCardIndex != -1) {
|
|
||||||
_selectedPaymentMethod = mainCardIndex;
|
|
||||||
}
|
|
||||||
}*/
|
|
||||||
|
|
||||||
return Align(
|
return Align(
|
||||||
alignment: Alignment.bottomCenter,
|
alignment: Alignment.bottomCenter,
|
||||||
child: ClipRRect(
|
child: ClipRRect(
|
||||||
@@ -122,47 +79,6 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
GestureDetector(
|
|
||||||
onTap: () => Navigator.pop(context),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_back_ios_sharp,
|
|
||||||
color: const Color(0x99FFFFFF),
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_back_ios_sharp,
|
|
||||||
color: const Color(0x66FFFFFF),
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_back_ios_sharp,
|
|
||||||
color: const Color(0x22FFFFFF),
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
'Выберите способ оплаты',
|
|
||||||
style: const TextStyle(
|
|
||||||
color: Colors.white,
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
@@ -174,7 +90,7 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
|||||||
if (widget.showBalance) ...[
|
if (widget.showBalance) ...[
|
||||||
PaymentOption(
|
PaymentOption(
|
||||||
title: 'Баланс',
|
title: 'Баланс',
|
||||||
subtitle: '${state.balance.toStringAsFixed(2)} BYN',
|
subtitle: '${state.balance} баллов',
|
||||||
isSelected: _selectedPaymentMethod == -1,
|
isSelected: _selectedPaymentMethod == -1,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
@@ -192,14 +108,20 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
|||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: PaymentOption(
|
child: PaymentOption(
|
||||||
title: card.type,
|
title: card.isMain ? "Основная" : "Карта",
|
||||||
subtitle: '****${card.cardLastNumber}',
|
subtitle: '${card.cardLastNumber}',
|
||||||
isSelected: _selectedPaymentMethod == index,
|
isSelected: _selectedPaymentMethod == index,
|
||||||
onTap: () {
|
onTap: () async {
|
||||||
setState(() {
|
setState(() {
|
||||||
_selectedPaymentMethod = index;
|
_selectedPaymentMethod = index;
|
||||||
|
_processingCardId = card.id;
|
||||||
});
|
});
|
||||||
Navigator.pop(context, card);
|
|
||||||
|
// Отправляем событие в Блок
|
||||||
|
context.read<PaymentMethodSheetBloc>().add(
|
||||||
|
SelectPaymentCardRequested(card),
|
||||||
|
);
|
||||||
|
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -218,8 +140,7 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
|
|||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context);
|
context.read<PaymentMethodSheetBloc>().add(AddPaymentCardRequested());
|
||||||
context.go('/home/payment-methods/add-card');
|
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
child: Center(
|
child: Center(
|
||||||
|
|||||||
@@ -151,6 +151,7 @@ class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
|||||||
child: _ScooterCard(
|
child: _ScooterCard(
|
||||||
scooter: scooter,
|
scooter: scooter,
|
||||||
isActive: isActive,
|
isActive: isActive,
|
||||||
|
address: state.address ?? "Unknown address",
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -174,9 +175,10 @@ class _ScooterBottomSheetState extends State<ScooterBottomSheet> {
|
|||||||
|
|
||||||
class _ScooterCard extends StatelessWidget {
|
class _ScooterCard extends StatelessWidget {
|
||||||
final Scooter scooter;
|
final Scooter scooter;
|
||||||
|
final String address;
|
||||||
final bool isActive;
|
final bool isActive;
|
||||||
|
|
||||||
const _ScooterCard({required this.scooter, required this.isActive});
|
const _ScooterCard({required this.scooter, required this.isActive, required this.address});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -210,7 +212,9 @@ class _ScooterCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 6),
|
const SizedBox(width: 6),
|
||||||
Text(
|
Text(
|
||||||
"${(scooter.distance?.toInt()) ?? 0}m",
|
(scooter.distance?.toInt() ?? 0) >= 1000
|
||||||
|
? '${((scooter.distance?.toInt() ?? 0) / 1000).toStringAsFixed(1)} km'
|
||||||
|
: '${scooter.distance?.toInt() ?? 0} m',
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -262,7 +266,7 @@ class _ScooterCard extends StatelessWidget {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
onTap: () {
|
onTap: () {
|
||||||
Navigator.pop(context, scooter);
|
Navigator.pop(context, [scooter, address]);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -11,7 +11,10 @@ import '../../../di/service_locator.dart';
|
|||||||
import '../../../domain/entities/payment_card.dart';
|
import '../../../domain/entities/payment_card.dart';
|
||||||
import '../../../domain/entities/scooter.dart';
|
import '../../../domain/entities/scooter.dart';
|
||||||
import '../../../domain/entities/tariff.dart';
|
import '../../../domain/entities/tariff.dart';
|
||||||
|
import '../../../domain/usecase/add_payment_card_usecase.dart';
|
||||||
import '../../../domain/usecase/get_payment_cards_usecase.dart';
|
import '../../../domain/usecase/get_payment_cards_usecase.dart';
|
||||||
|
import '../../../domain/usecase/get_profile_usecase.dart';
|
||||||
|
import '../../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||||
import '../../event/payment_method_sheet_event.dart';
|
import '../../event/payment_method_sheet_event.dart';
|
||||||
import '../../event/tariff_sheet_event.dart';
|
import '../../event/tariff_sheet_event.dart';
|
||||||
import '../../state/tariff_sheet_state.dart';
|
import '../../state/tariff_sheet_state.dart';
|
||||||
@@ -39,6 +42,24 @@ class _TariffSheetState extends State<TariffSheet> {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final double remainingKm = (62 * widget.scooter.batteryLevel) / 100;
|
||||||
|
|
||||||
|
final String kmText = remainingKm % 1 == 0
|
||||||
|
? '${remainingKm.toInt()} км'
|
||||||
|
: '${remainingKm.toStringAsFixed(1)} км';
|
||||||
|
|
||||||
|
final int totalMinutes = ((150 * widget.scooter.batteryLevel) / 100).round();
|
||||||
|
final int hours = totalMinutes ~/ 60;
|
||||||
|
final int minutes = totalMinutes % 60;
|
||||||
|
|
||||||
|
String timeText = '';
|
||||||
|
if (hours > 0 && minutes > 0) {
|
||||||
|
timeText = '$hours ч $minutes мин';
|
||||||
|
} else if (hours > 0) {
|
||||||
|
timeText = '$hours ч';
|
||||||
|
} else {
|
||||||
|
timeText = '$minutes мин';
|
||||||
|
}
|
||||||
return BlocConsumer<TariffSheetBloc, TariffSheetState>(
|
return BlocConsumer<TariffSheetBloc, TariffSheetState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.status == TariffSheetStatus.failure && state.errorMessage != null) {
|
if (state.status == TariffSheetStatus.failure && state.errorMessage != null) {
|
||||||
@@ -134,7 +155,6 @@ class _TariffSheetState extends State<TariffSheet> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 🔹 HEADER
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Row(
|
child: Row(
|
||||||
@@ -226,7 +246,7 @@ class _TariffSheetState extends State<TariffSheet> {
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Text(
|
child: Text(
|
||||||
'Заряда хватит на 4 часа 17 минут\nили 47 км',
|
'Заряда хватит на $kmText или $timeText',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
@@ -288,10 +308,10 @@ class _TariffSheetState extends State<TariffSheet> {
|
|||||||
child: PaymentOption(
|
child: PaymentOption(
|
||||||
title: state.useBalance
|
title: state.useBalance
|
||||||
? 'Баланс'
|
? 'Баланс'
|
||||||
: state.selectedCard!.type,
|
: "Карта",
|
||||||
subtitle: state.useBalance
|
subtitle: state.useBalance
|
||||||
? '${state.userBalance.toStringAsFixed(2)} BYN'
|
? '${state.userBalance.toStringAsFixed(2)} BYN'
|
||||||
: '****${state.selectedCard!.cardLastNumber}',
|
: '${state.selectedCard!.cardLastNumber}',
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final result = await showModalBottomSheet<dynamic>(
|
final result = await showModalBottomSheet<dynamic>(
|
||||||
@@ -301,6 +321,9 @@ class _TariffSheetState extends State<TariffSheet> {
|
|||||||
builder: (innerContext) => BlocProvider(
|
builder: (innerContext) => BlocProvider(
|
||||||
create: (context) => PaymentMethodSheetBloc(
|
create: (context) => PaymentMethodSheetBloc(
|
||||||
getIt<GetPaymentCardsUsecase>(),
|
getIt<GetPaymentCardsUsecase>(),
|
||||||
|
getIt<SetMainPaymentCardUsecase>(),
|
||||||
|
getIt<GetProfileUseCase>(),
|
||||||
|
getIt<AddPaymentCardUsecase>(),
|
||||||
)..add(PaymentMethodSheetStarted()),
|
)..add(PaymentMethodSheetStarted()),
|
||||||
child: PaymentMethodSheet(
|
child: PaymentMethodSheet(
|
||||||
initialSelectedCard: state.useBalance ? null : state.selectedCard,
|
initialSelectedCard: state.useBalance ? null : state.selectedCard,
|
||||||
@@ -381,7 +404,6 @@ class _TariffSheetState extends State<TariffSheet> {
|
|||||||
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
// 🔹 КНОПКА "ЗАБРОНИРОВАТЬ"
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: GradientButton(
|
child: GradientButton(
|
||||||
@@ -494,13 +516,10 @@ class _TariffCard extends StatelessWidget {
|
|||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Резервируем место под иконку инфо справа,
|
|
||||||
// чтобы текст не залез под неё
|
|
||||||
const SizedBox(width: 24),
|
const SizedBox(width: 24),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
// Цена + текст рядом
|
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.baseline,
|
crossAxisAlignment: CrossAxisAlignment.baseline,
|
||||||
textBaseline: TextBaseline.alphabetic,
|
textBaseline: TextBaseline.alphabetic,
|
||||||
@@ -540,7 +559,6 @@ class _TariffCard extends StatelessWidget {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// Кнопка-иконка в верхнем правом углу
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 0,
|
top: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
@@ -550,7 +568,7 @@ class _TariffCard extends StatelessWidget {
|
|||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
builder: (context) => TariffInfoSheet(
|
builder: (_) => TariffInfoSheet(
|
||||||
tariff: tariff,
|
tariff: tariff,
|
||||||
isInsurance: isInsurance,
|
isInsurance: isInsurance,
|
||||||
onChanged: (val) {
|
onChanged: (val) {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class SideMenu extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Баланс ${state.balance} руб.',
|
'Баланс ${state.balance} баллов',
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white70,
|
color: Colors.white70,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
@@ -63,68 +63,90 @@ class SideMenu extends StatelessWidget {
|
|||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Пункты меню
|
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: 'assets/icons/person_icon.png',
|
icon: 'assets/icons/person_icon.png',
|
||||||
title: 'Профиль',
|
title: 'Профиль',
|
||||||
onTap: () => {
|
onTap: () {
|
||||||
context.go('/home/profile'),
|
Navigator.pop(context);
|
||||||
|
context.go('/home/profile');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: 'assets/icons/list_star_icon.png',
|
icon: 'assets/icons/list_star_icon.png',
|
||||||
title: 'История поездок',
|
title: 'История поездок',
|
||||||
onTap: () => context.go("/home/order-history"),
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.go("/home/order-history");
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Divider(color: Colors.white30, height: 1),
|
const Divider(color: Colors.white30, height: 1),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: 'assets/icons/creditcard_icon.png',
|
icon: 'assets/icons/creditcard_icon.png',
|
||||||
title: 'Способ оплаты',
|
title: 'Способ оплаты',
|
||||||
onTap: () => context.go("/home/payment-methods"),
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.go("/home/payment-methods");
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: 'assets/icons/promo_icon.png',
|
icon: 'assets/icons/promo_icon.png',
|
||||||
title: 'Промокоды',
|
title: 'Промокоды',
|
||||||
onTap: () => context.go("/home/promo"),
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.go("/home/promo");
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: 'assets/icons/plans_icon.png',
|
icon: 'assets/icons/plans_icon.png',
|
||||||
title: 'Абонементы',
|
title: 'Абонементы',
|
||||||
onTap: () => context.push("/home/subscriptions"),
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.push("/home/subscriptions");
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Divider(color: Colors.white30, height: 1),
|
const Divider(color: Colors.white30, height: 1),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: 'assets/icons/magazine_icon.png',
|
icon: 'assets/icons/magazine_icon.png',
|
||||||
title: 'Правила пользования самокатом',
|
title: 'Правила пользования самокатом',
|
||||||
onTap: () => context.go("/home/rules")
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.go("/home/rules");
|
||||||
|
}
|
||||||
),
|
),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: 'assets/icons/headphones_icon.png',
|
icon: 'assets/icons/headphones_icon.png',
|
||||||
title: 'Техподдержка',
|
title: 'Техподдержка',
|
||||||
onTap: () => {
|
onTap: () {
|
||||||
context.go('/home/support'),
|
Navigator.pop(context);
|
||||||
|
context.go('/home/support');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: 'assets/icons/doc_icon.png',
|
icon: 'assets/icons/doc_icon.png',
|
||||||
title: 'Документы',
|
title: 'Документы',
|
||||||
onTap: () => context.go("/home/documents"),
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.go("/home/documents");
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: 'assets/icons/news_icon.png',
|
icon: 'assets/icons/news_icon.png',
|
||||||
title: 'Новости',
|
title: 'Новости',
|
||||||
onTap: () => context.go("/home/news"),
|
onTap: () {
|
||||||
|
Navigator.pop(context);
|
||||||
|
context.go("/home/news");
|
||||||
|
},
|
||||||
),
|
),
|
||||||
const Divider(color: Colors.white30, height: 1),
|
const Divider(color: Colors.white30, height: 1),
|
||||||
_MenuItem(
|
_MenuItem(
|
||||||
icon: 'assets/icons/logout_icon.png',
|
icon: 'assets/icons/logout_icon.png',
|
||||||
title: 'Выход',
|
title: 'Выход',
|
||||||
onTap: () => {
|
onTap: () {
|
||||||
context.go('/login'),
|
context.read<MapBloc>().stopNotificationStream();
|
||||||
context.read<MapBloc>().stopNotificationStream(),
|
context.read<MapBloc>().add(LogoutPressed());
|
||||||
context.read<MapBloc>().add(LogoutPressed()),
|
context.go('/login');
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,3 +1,8 @@
|
|||||||
sealed class NotificationsEvent {}
|
sealed class NotificationsEvent {}
|
||||||
|
|
||||||
class NotificationsFetchRequested extends NotificationsEvent {}
|
class NotificationsFetchRequested extends NotificationsEvent {
|
||||||
|
final int page;
|
||||||
|
NotificationsFetchRequested({this.page = 1});
|
||||||
|
}
|
||||||
|
|
||||||
|
class NotificationsLoadMoreRequested extends NotificationsEvent {}
|
||||||
@@ -1,3 +1,12 @@
|
|||||||
|
import '../../domain/entities/payment_card.dart';
|
||||||
|
|
||||||
sealed class PaymentMethodSheetEvent {}
|
sealed class PaymentMethodSheetEvent {}
|
||||||
|
|
||||||
class PaymentMethodSheetStarted extends PaymentMethodSheetEvent {}
|
class PaymentMethodSheetStarted extends PaymentMethodSheetEvent {}
|
||||||
|
|
||||||
|
class SelectPaymentCardRequested extends PaymentMethodSheetEvent {
|
||||||
|
final PaymentCard card;
|
||||||
|
SelectPaymentCardRequested(this.card);
|
||||||
|
}
|
||||||
|
|
||||||
|
class AddPaymentCardRequested extends PaymentMethodSheetEvent {}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ sealed class PaymentMethodsEvent {}
|
|||||||
|
|
||||||
class PaymentMethodsStarted extends PaymentMethodsEvent {}
|
class PaymentMethodsStarted extends PaymentMethodsEvent {}
|
||||||
|
|
||||||
|
class AddPayemntCardPressed extends PaymentMethodsEvent {}
|
||||||
|
|
||||||
class PaymentMethodsDeleteCard extends PaymentMethodsEvent {
|
class PaymentMethodsDeleteCard extends PaymentMethodsEvent {
|
||||||
final int cardId;
|
final int cardId;
|
||||||
|
|
||||||
|
|||||||
@@ -13,3 +13,5 @@ class AuthCheckRequested extends SplashEvent {}
|
|||||||
class AuthStarted extends SplashEvent {}
|
class AuthStarted extends SplashEvent {}
|
||||||
|
|
||||||
class PinVerificationSuccess extends SplashEvent {}
|
class PinVerificationSuccess extends SplashEvent {}
|
||||||
|
|
||||||
|
class AuthLogoutRequested extends SplashEvent {}
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import '../../domain/entities/payment_card.dart';
|
||||||
import '../../domain/entities/subscription_period.dart';
|
import '../../domain/entities/subscription_period.dart';
|
||||||
|
|
||||||
abstract class SubscriptionDetailsEvent {}
|
abstract class SubscriptionDetailsEvent {}
|
||||||
@@ -17,4 +18,10 @@ class ToggleAgreementEvent extends SubscriptionDetailsEvent {
|
|||||||
ToggleAgreementEvent(this.value);
|
ToggleAgreementEvent(this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
class ActivateSubscriptionPressed extends SubscriptionDetailsEvent {}
|
class ActivateSubscriptionPressed extends SubscriptionDetailsEvent {}
|
||||||
|
|
||||||
|
class SelectCardEvent extends SubscriptionDetailsEvent {
|
||||||
|
final PaymentCard card;
|
||||||
|
|
||||||
|
SelectCardEvent(this.card);
|
||||||
|
}
|
||||||
@@ -20,4 +20,6 @@ class SelectCard extends TopUpEvent {
|
|||||||
class ToggleAgreement extends TopUpEvent {
|
class ToggleAgreement extends TopUpEvent {
|
||||||
final bool value;
|
final bool value;
|
||||||
ToggleAgreement(this.value);
|
ToggleAgreement(this.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TopUpButtonPressed extends TopUpEvent {}
|
||||||
@@ -13,6 +13,11 @@ class CodeChanged extends VerifyCodeEvent {
|
|||||||
CodeChanged(this.code);
|
CodeChanged(this.code);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class TimerTicked extends VerifyCodeEvent {
|
||||||
|
final int secondsLeft;
|
||||||
|
TimerTicked(this.secondsLeft);
|
||||||
|
}
|
||||||
|
|
||||||
class ResendCodePressed extends VerifyCodeEvent {}
|
class ResendCodePressed extends VerifyCodeEvent {}
|
||||||
|
|
||||||
class VerifyCodeSubmitted extends VerifyCodeEvent {}
|
class VerifyCodeSubmitted extends VerifyCodeEvent {}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:be_happy/presentation/screens/card_binding_result_screen.dart';
|
||||||
import 'package:bot_toast/bot_toast.dart';
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
import 'package:be_happy/core/app_colors.dart';
|
import 'package:be_happy/core/app_colors.dart';
|
||||||
import 'package:be_happy/di/service_locator.dart';
|
import 'package:be_happy/di/service_locator.dart';
|
||||||
@@ -43,6 +44,7 @@ import 'package:path/path.dart';
|
|||||||
|
|
||||||
import '../../domain/entities/scooter.dart';
|
import '../../domain/entities/scooter.dart';
|
||||||
import '../../domain/usecase/activate_subscription_usecase.dart';
|
import '../../domain/usecase/activate_subscription_usecase.dart';
|
||||||
|
import '../../domain/usecase/add_payment_card_usecase.dart';
|
||||||
import '../../domain/usecase/book_scooter_usecase.dart';
|
import '../../domain/usecase/book_scooter_usecase.dart';
|
||||||
import '../../domain/usecase/create_pin_usecase.dart';
|
import '../../domain/usecase/create_pin_usecase.dart';
|
||||||
import '../../domain/usecase/get_address_by_point_usecase.dart';
|
import '../../domain/usecase/get_address_by_point_usecase.dart';
|
||||||
@@ -139,7 +141,8 @@ class AppRouter {
|
|||||||
createPinUseCase: getIt<CreatePinUseCase>(),
|
createPinUseCase: getIt<CreatePinUseCase>(),
|
||||||
verifyPinUsecase: getIt<VerifyPinUseCase>(),
|
verifyPinUsecase: getIt<VerifyPinUseCase>(),
|
||||||
isPinSetUsecase: getIt<IsPinSetUsecase>(),
|
isPinSetUsecase: getIt<IsPinSetUsecase>(),
|
||||||
)..add(PinScreenStarted()),
|
)
|
||||||
|
..add(PinScreenStarted()),
|
||||||
child: const PinLoginScreen(),
|
child: const PinLoginScreen(),
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
@@ -151,6 +154,26 @@ class AppRouter {
|
|||||||
path: '/license-agreement',
|
path: '/license-agreement',
|
||||||
builder: (context, state) => const LicenseAgreementScreen(),
|
builder: (context, state) => const LicenseAgreementScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/card-result',
|
||||||
|
builder: (context, state) {
|
||||||
|
final modeParam = state.uri.queryParameters['status'];
|
||||||
|
|
||||||
|
CardBindingStatus status = CardBindingStatus.success;
|
||||||
|
String title = 'Карта успешно\nпривязана!';
|
||||||
|
|
||||||
|
if (modeParam == 'failed') {
|
||||||
|
status = CardBindingStatus.failed;
|
||||||
|
title = 'Извините, что-то\nпошло не так';
|
||||||
|
}
|
||||||
|
|
||||||
|
return CardBindingResultScreen(
|
||||||
|
status: status,
|
||||||
|
title: title,
|
||||||
|
cardImagePath: 'assets/icons/card_result_screen.png',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/home',
|
path: '/home',
|
||||||
builder: (context, state) => const MapScreen(),
|
builder: (context, state) => const MapScreen(),
|
||||||
@@ -237,7 +260,12 @@ class AppRouter {
|
|||||||
type: MaterialType.transparency,
|
type: MaterialType.transparency,
|
||||||
child: BlocProvider(
|
child: BlocProvider(
|
||||||
create: (context) =>
|
create: (context) =>
|
||||||
PaymentMethodSheetBloc(getIt<GetPaymentCardsUsecase>()),
|
PaymentMethodSheetBloc(
|
||||||
|
getIt<GetPaymentCardsUsecase>(),
|
||||||
|
getIt<SetMainPaymentCardUsecase>(),
|
||||||
|
getIt<GetProfileUseCase>(),
|
||||||
|
getIt<AddPaymentCardUsecase>(),
|
||||||
|
),
|
||||||
child: PaymentMethodSheet(),
|
child: PaymentMethodSheet(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -297,12 +325,20 @@ class AppRouter {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'scooter/:id',
|
path: 'scooter/:id',
|
||||||
builder: (context, state) =>
|
builder: (context, state) {
|
||||||
BlocProvider(
|
List<dynamic> extras = [];
|
||||||
|
if (state.extra != null) {
|
||||||
|
extras = state.extra as List<dynamic>;
|
||||||
|
}
|
||||||
|
String address = extras[0];
|
||||||
|
double distance = extras[1];
|
||||||
|
|
||||||
|
return BlocProvider(
|
||||||
create: (context) => getIt<ScooterDetailBloc>(),
|
create: (context) => getIt<ScooterDetailBloc>(),
|
||||||
child: ScooterDetailScreen(),
|
child: ScooterDetailScreen(address: address, distance: distance),
|
||||||
),
|
);
|
||||||
|
}
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'order-photos/:orderId',
|
path: 'order-photos/:orderId',
|
||||||
@@ -371,6 +407,7 @@ class AppRouter {
|
|||||||
create: (context) =>
|
create: (context) =>
|
||||||
SubscriptionDetailsBloc(
|
SubscriptionDetailsBloc(
|
||||||
getIt<GetSubscriptionByIdUsecase>(),
|
getIt<GetSubscriptionByIdUsecase>(),
|
||||||
|
getIt<GetPaymentCardsUsecase>(),
|
||||||
getIt<ActivateSubscriptionUsecase>(),
|
getIt<ActivateSubscriptionUsecase>(),
|
||||||
getIt<GetClientSubscriptionsUsecase>(),
|
getIt<GetClientSubscriptionsUsecase>(),
|
||||||
)
|
)
|
||||||
@@ -394,39 +431,44 @@ class AppRouter {
|
|||||||
builder: (context, state) => const NewsScreen(),
|
builder: (context, state) => const NewsScreen(),
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'qr-info',
|
path: 'qr-info',
|
||||||
builder: (context, state) => const QRScanInfoScreen(),
|
builder: (context, state) => const QRScanInfoScreen(),
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'qr-scan',
|
path: 'qr-scan',
|
||||||
builder: (context, state) => const QrScanScreen(),
|
builder: (context, state) => const QrScanScreen(),
|
||||||
),
|
|
||||||
GoRoute(
|
|
||||||
path: 'qr-input',
|
|
||||||
builder: (context, state) => BlocProvider(
|
|
||||||
create: (context) =>
|
|
||||||
ScooterCodeBloc(
|
|
||||||
getScooterByTitleUsecase: getIt<GetScooterByTitleUsecase>(),
|
|
||||||
),
|
|
||||||
child: ScooterCodeInputScreen(),
|
|
||||||
),
|
),
|
||||||
),
|
GoRoute(
|
||||||
]
|
path: 'qr-input',
|
||||||
|
builder: (context, state) =>
|
||||||
|
BlocProvider(
|
||||||
|
create: (context) =>
|
||||||
|
ScooterCodeBloc(
|
||||||
|
getScooterByTitleUsecase: getIt<
|
||||||
|
GetScooterByTitleUsecase>(),
|
||||||
|
),
|
||||||
|
child: ScooterCodeInputScreen(),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'payment-methods',
|
path: 'payment-methods',
|
||||||
builder: (context, state) =>
|
builder: (context, state) {
|
||||||
BlocProvider(
|
final refreshId = state.uri.queryParameters['refreshId'];
|
||||||
create: (context) =>
|
|
||||||
PaymentMethodsBloc(
|
return BlocProvider(
|
||||||
getIt<GetPaymentCardsUsecase>(),
|
key: refreshId != null ? ValueKey(refreshId) : null,
|
||||||
getIt<RemovePaymentCardUsecase>(),
|
create: (context) => PaymentMethodsBloc(
|
||||||
getIt<SetMainPaymentCardUsecase>(),
|
getIt<GetPaymentCardsUsecase>(),
|
||||||
getIt<GetProfileUseCase>(),
|
getIt<RemovePaymentCardUsecase>(),
|
||||||
)
|
getIt<SetMainPaymentCardUsecase>(),
|
||||||
..add(PaymentMethodsStarted()),
|
getIt<GetProfileUseCase>(),
|
||||||
child: PaymentMethodsScreen(),
|
getIt<AddPaymentCardUsecase>(),
|
||||||
),
|
)..add(PaymentMethodsStarted()),
|
||||||
|
child: const PaymentMethodsScreen(),
|
||||||
|
);
|
||||||
|
},
|
||||||
routes: [
|
routes: [
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'add-card',
|
path: 'add-card',
|
||||||
@@ -450,13 +492,35 @@ class AppRouter {
|
|||||||
..add(LoadTopUpData()),
|
..add(LoadTopUpData()),
|
||||||
child: TopUpScreen(),
|
child: TopUpScreen(),
|
||||||
),
|
),
|
||||||
|
routes: [
|
||||||
|
GoRoute(
|
||||||
|
path: 'result/:status',
|
||||||
|
builder: (context, state) {
|
||||||
|
final modeParam = state.pathParameters['status'];
|
||||||
|
|
||||||
|
CardBindingStatus status = CardBindingStatus.success;
|
||||||
|
String title = 'Успешно!';
|
||||||
|
|
||||||
|
if (modeParam == 'failed') {
|
||||||
|
status = CardBindingStatus.failed;
|
||||||
|
title = 'Извините, что-то\nпошло не так';
|
||||||
|
}
|
||||||
|
|
||||||
|
return CardBindingResultScreen(
|
||||||
|
status: status,
|
||||||
|
title: title,
|
||||||
|
cardImagePath: 'assets/icons/card_result_screen.png',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'order-history',
|
path: 'order-history',
|
||||||
builder: (context, state) => const OrderHistoryScreen(),
|
builder: (context, state) => const OrderHistoryScreen(),
|
||||||
routes: []
|
routes: []
|
||||||
),
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: 'notifications',
|
path: 'notifications',
|
||||||
@@ -466,6 +530,24 @@ class AppRouter {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
onException: (context, state, router) {
|
||||||
|
final location = state.uri.toString();
|
||||||
|
|
||||||
|
if (location.startsWith('com.behappy.app://')) {
|
||||||
|
|
||||||
|
String normalizedPath = location
|
||||||
|
.replaceFirst('com.behappy.app://', '/')
|
||||||
|
.replaceAll('/?', '?')
|
||||||
|
.replaceAll(' ', '');
|
||||||
|
|
||||||
|
print("Нормализованный путь диплинка: $normalizedPath");
|
||||||
|
|
||||||
|
router.go(normalizedPath);
|
||||||
|
} else {
|
||||||
|
print('GoRouter Exception: ${state.error}');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
observers: [BotToastNavigatorObserver()],
|
observers: [BotToastNavigatorObserver()],
|
||||||
|
|
||||||
redirect: (BuildContext context, GoRouterState state) {
|
redirect: (BuildContext context, GoRouterState state) {
|
||||||
@@ -476,45 +558,59 @@ class AppRouter {
|
|||||||
print(splashBloc.state);
|
print(splashBloc.state);
|
||||||
print(state.uri.toString());
|
print(state.uri.toString());
|
||||||
|
|
||||||
if (authState is AuthInitial && currentLocation != '/splash') {
|
if (authState is AuthInitial) {
|
||||||
print("splash");
|
if (currentLocation != '/splash') return '/splash';
|
||||||
return '/splash';
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authState is AuthFirstLaunch &&
|
// 2. Первый запуск или пользователь не авторизован
|
||||||
currentLocation != '/login' &&
|
if (authState is AuthFirstLaunch || authState is AuthUnauthenticated) {
|
||||||
currentLocation != '/privacy-policy' &&
|
final isAllowed = currentLocation == '/login' ||
|
||||||
currentLocation != '/license-agreement') {
|
currentLocation.startsWith('/verify') ||
|
||||||
print("login");
|
currentLocation == '/privacy-policy' ||
|
||||||
return '/login';
|
currentLocation == '/license-agreement';
|
||||||
|
|
||||||
|
if (!isAllowed) return '/login';
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authState is AuthUnauthenticated &&
|
// 3. Авторизация в процессе (переход от ввода телефона/SMS к PIN-коду)
|
||||||
currentLocation != '/login' &&
|
if (authState is AuthInProgress) {
|
||||||
currentLocation != '/privacy-policy' &&
|
final isAllowed = currentLocation == '/login' ||
|
||||||
currentLocation != '/license-agreement') {
|
currentLocation.startsWith('/verify') ||
|
||||||
print("login2");
|
currentLocation == '/pin' || // ← Теперь роутер пустит сюда!
|
||||||
return '/login';
|
currentLocation == '/privacy-policy' ||
|
||||||
|
currentLocation == '/license-agreement';
|
||||||
|
|
||||||
|
if (!isAllowed) return '/login';
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. Пользователь авторизован, но PIN еще не проверен
|
||||||
if (authState is AuthAuthenticated) {
|
if (authState is AuthAuthenticated) {
|
||||||
final isComingFromStart =
|
final isComingFromStart = currentLocation == '/splash' ||
|
||||||
currentLocation == '/splash' ||
|
currentLocation == '/login' ||
|
||||||
currentLocation == '/login' ||
|
currentLocation.startsWith('/verify') ||
|
||||||
currentLocation == '/phone';
|
currentLocation == '/phone';
|
||||||
|
|
||||||
if (isComingFromStart) {
|
if (isComingFromStart) {
|
||||||
print("redirecting to pin check");
|
print("redirecting to pin check");
|
||||||
return '/pin';
|
return '/pin';
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 5. Пользователь полностью авторизован и PIN успешно верифицирован
|
||||||
if (authState is AuthPinVerified) {
|
if (authState is AuthPinVerified) {
|
||||||
if (currentLocation == '/splash' ||
|
final isComingFromAuth = currentLocation == '/splash' ||
|
||||||
currentLocation == '/login' ||
|
currentLocation == '/login' ||
|
||||||
currentLocation == '/pin') {
|
currentLocation.startsWith('/verify') ||
|
||||||
|
currentLocation == '/pin';
|
||||||
|
|
||||||
|
if (isComingFromAuth) {
|
||||||
return '/home';
|
return '/home';
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
173
lib/presentation/screens/card_binding_result_screen.dart
Normal file
173
lib/presentation/screens/card_binding_result_screen.dart
Normal file
@@ -0,0 +1,173 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
|
import '../components/gradient_button.dart';
|
||||||
|
|
||||||
|
enum CardBindingStatus { success, failed }
|
||||||
|
|
||||||
|
class CardBindingResultScreen extends StatefulWidget {
|
||||||
|
final CardBindingStatus status;
|
||||||
|
final String title;
|
||||||
|
final String cardImagePath;
|
||||||
|
|
||||||
|
const CardBindingResultScreen({
|
||||||
|
super.key,
|
||||||
|
required this.status,
|
||||||
|
required this.title,
|
||||||
|
required this.cardImagePath,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<CardBindingResultScreen> createState() => _CardBindingResultScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _CardBindingResultScreenState extends State<CardBindingResultScreen> {
|
||||||
|
Timer? _autoPopTimer;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
if (widget.status == CardBindingStatus.success) {
|
||||||
|
_autoPopTimer = Timer(const Duration(seconds: 5), () {
|
||||||
|
if (mounted) {
|
||||||
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
context.go("/home/payment-methods?refreshId=$timestamp");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_autoPopTimer?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final isSuccess = widget.status == CardBindingStatus.success;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
body: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
begin: Alignment.topCenter,
|
||||||
|
end: Alignment.bottomCenter,
|
||||||
|
colors: [
|
||||||
|
Color(0xFF1B284F),
|
||||||
|
Color(0xFF0F172A),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: SafeArea(
|
||||||
|
child: Stack(
|
||||||
|
children: [
|
||||||
|
// Positioned(
|
||||||
|
// bottom: 50,
|
||||||
|
// left: 0,
|
||||||
|
// right: 0,
|
||||||
|
// child: Image.asset(
|
||||||
|
// 'assets/wave.png',
|
||||||
|
// fit: BoxFit.cover,
|
||||||
|
// ),
|
||||||
|
// ),
|
||||||
|
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
// const Spacer(),
|
||||||
|
|
||||||
|
Text(
|
||||||
|
widget.title,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
Image.asset(
|
||||||
|
isSuccess
|
||||||
|
? 'assets/icons/success.png'
|
||||||
|
: 'assets/icons/failed.png',
|
||||||
|
width: 140,
|
||||||
|
height: 140,
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
if (isSuccess) ...[
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 40.0),
|
||||||
|
// child: Image.asset(
|
||||||
|
// widget.cardImagePath,
|
||||||
|
// width: double.infinity,
|
||||||
|
// fit: BoxFit.contain,
|
||||||
|
// ),
|
||||||
|
),
|
||||||
|
] else ...[
|
||||||
|
const Text(
|
||||||
|
'Проверьте подключение к\nинтернету и попробуйте снова',
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
color: Color(0xFF94A3B8),
|
||||||
|
fontSize: 16,
|
||||||
|
height: 1.4,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
|
||||||
|
GradientButton(
|
||||||
|
text: "Попробовать снова",
|
||||||
|
onTap: () {
|
||||||
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
context.go("/home/payment-methods?refreshId=$timestamp");
|
||||||
|
},
|
||||||
|
enabled: true,
|
||||||
|
showArrows: true,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
|
||||||
|
SizedBox(
|
||||||
|
width: double.infinity,
|
||||||
|
height: 56,
|
||||||
|
child: TextButton(
|
||||||
|
onPressed: ()
|
||||||
|
{
|
||||||
|
final timestamp = DateTime.now().millisecondsSinceEpoch;
|
||||||
|
context.go("/home/payment-methods?refreshId=$timestamp");
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
),
|
||||||
|
backgroundColor: const Color(0xFF0F172A).withOpacity(0.4),
|
||||||
|
),
|
||||||
|
child: const Text(
|
||||||
|
'Закрыть',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ import 'package:go_router/go_router.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import '../../core/app_colors.dart';
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../di/service_locator.dart';
|
||||||
import '../../domain/entities/user_profile.dart';
|
import '../../domain/entities/user_profile.dart';
|
||||||
import '../components/custom_app_bar.dart';
|
import '../components/custom_app_bar.dart';
|
||||||
import '../components/gradient_button.dart';
|
import '../components/gradient_button.dart';
|
||||||
@@ -106,7 +107,7 @@ class _EditProfileScreenState extends State<EditProfileScreen> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.isSuccess) {
|
if (state.isSuccess) {
|
||||||
context.read<ProfileBloc>().add(ProfileUpdated());
|
getIt<ProfileBloc>().add(ProfileUpdated());
|
||||||
context.pop();
|
context.pop();
|
||||||
}
|
}
|
||||||
if (state.error != null) {
|
if (state.error != null) {
|
||||||
|
|||||||
@@ -4,24 +4,9 @@ import 'dart:ui' as ui;
|
|||||||
|
|
||||||
import 'package:bot_toast/bot_toast.dart';
|
import 'package:bot_toast/bot_toast.dart';
|
||||||
import 'package:be_happy/domain/entities/client_notification.dart';
|
import 'package:be_happy/domain/entities/client_notification.dart';
|
||||||
import 'package:be_happy/domain/usecase/book_scooter_usecase.dart';
|
|
||||||
import 'package:be_happy/domain/usecase/get_client_orders_usecase.dart';
|
|
||||||
import 'package:be_happy/domain/usecase/get_map_settings_usecase.dart';
|
|
||||||
import 'package:be_happy/domain/usecase/get_payment_cards_usecase.dart';
|
|
||||||
import 'package:be_happy/domain/usecase/get_pedestrian_routes_usecase.dart';
|
|
||||||
import 'package:be_happy/domain/usecase/get_scooter_usecase.dart';
|
|
||||||
import 'package:be_happy/domain/usecase/save_map_settings_usecase.dart';
|
|
||||||
import 'package:be_happy/presentation/components/fine_notification_card.dart';
|
import 'package:be_happy/presentation/components/fine_notification_card.dart';
|
||||||
import 'package:be_happy/presentation/components/map_icon_painter/clusterized_icon_painter.dart';
|
import 'package:be_happy/presentation/components/map_icon_painter/clusterized_icon_painter.dart';
|
||||||
import 'package:be_happy/presentation/components/payment_notification_card.dart';
|
import 'package:be_happy/presentation/components/payment_notification_card.dart';
|
||||||
import 'package:be_happy/presentation/components/sheet/current_rides_sheet.dart';
|
|
||||||
import 'package:be_happy/presentation/components/sheet/map_settings_sheet.dart';
|
|
||||||
import 'package:be_happy/presentation/components/sheet/tariff_sheet.dart';
|
|
||||||
import 'package:be_happy/presentation/event/current_rides_event.dart';
|
|
||||||
import 'package:be_happy/presentation/event/map_settings_modal_event.dart';
|
|
||||||
import 'package:be_happy/presentation/viewmodel/current_rides_bloc.dart';
|
|
||||||
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
|
|
||||||
import 'package:be_happy/presentation/viewmodel/tariff_sheet_bloc.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:geolocator/geolocator.dart';
|
import 'package:geolocator/geolocator.dart';
|
||||||
@@ -32,18 +17,12 @@ import '../../core/app_colors.dart';
|
|||||||
import '../../di/service_locator.dart';
|
import '../../di/service_locator.dart';
|
||||||
import '../../domain/entities/scooter.dart';
|
import '../../domain/entities/scooter.dart';
|
||||||
import '../../domain/entities/zone.dart';
|
import '../../domain/entities/zone.dart';
|
||||||
import '../../domain/usecase/get_address_by_point_usecase.dart';
|
|
||||||
import '../../domain/usecase/get_available_tariffs_usecase.dart';
|
|
||||||
import '../../domain/usecase/get_notifications_stream_usecase.dart';
|
import '../../domain/usecase/get_notifications_stream_usecase.dart';
|
||||||
import '../components/notification_toast.dart';
|
|
||||||
import '../components/sheet/scooter_bottom_sheet.dart';
|
|
||||||
import '../components/side_menu.dart';
|
import '../components/side_menu.dart';
|
||||||
import '../components/unpaid_order_notification_card.dart';
|
import '../components/unpaid_order_notification_card.dart';
|
||||||
import '../event/map_event.dart';
|
import '../event/map_event.dart';
|
||||||
import '../event/scooter_detail_modal_event.dart';
|
|
||||||
import '../state/map_state.dart';
|
import '../state/map_state.dart';
|
||||||
import '../viewmodel/map_bloc.dart';
|
import '../viewmodel/map_bloc.dart';
|
||||||
import '../viewmodel/scooter_detail_modal_bloc.dart';
|
|
||||||
|
|
||||||
class MapScreen extends StatefulWidget {
|
class MapScreen extends StatefulWidget {
|
||||||
const MapScreen({super.key});
|
const MapScreen({super.key});
|
||||||
@@ -609,28 +588,33 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
if (zones == null || zones.isEmpty) return [];
|
if (zones == null || zones.isEmpty) return [];
|
||||||
|
|
||||||
List<MapObject> objects = [];
|
List<MapObject> objects = [];
|
||||||
|
List<LinearRing> driveOuterRings = [];
|
||||||
List<LinearRing> allZoneHoles = [];
|
|
||||||
|
|
||||||
for (var zone in zones) {
|
for (var zone in zones) {
|
||||||
var points = zone.points.map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
|
if (zone.type == "Drive") {
|
||||||
|
for (var polygonData in zone.geometry) {
|
||||||
|
if (polygonData.isEmpty) continue;
|
||||||
|
|
||||||
var cleanPoints = <Point>[];
|
var outerPoints = polygonData[0].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
|
||||||
for (var p in points) {
|
|
||||||
if (cleanPoints.isEmpty || cleanPoints.last != p) {
|
var cleanPoints = <Point>[];
|
||||||
cleanPoints.add(p);
|
for (var p in outerPoints) {
|
||||||
|
if (cleanPoints.isEmpty || cleanPoints.last != p) {
|
||||||
|
cleanPoints.add(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cleanPoints.length > 2) {
|
||||||
|
if (cleanPoints.first != cleanPoints.last) {
|
||||||
|
cleanPoints.add(cleanPoints.first);
|
||||||
|
}
|
||||||
|
driveOuterRings.add(LinearRing(points: cleanPoints));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cleanPoints.length > 2) {
|
|
||||||
if (cleanPoints.first != cleanPoints.last) {
|
|
||||||
cleanPoints.add(cleanPoints.first);
|
|
||||||
}
|
|
||||||
|
|
||||||
allZoneHoles.add(LinearRing(points: cleanPoints));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Добавляем глобальную маску (все, что НЕ Drive — затеняется красным)
|
||||||
objects.add(
|
objects.add(
|
||||||
PolygonMapObject(
|
PolygonMapObject(
|
||||||
mapId: const MapObjectId('global_inverse_mask'),
|
mapId: const MapObjectId('global_inverse_mask'),
|
||||||
@@ -641,7 +625,7 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
Point(latitude: -85, longitude: 179.9),
|
Point(latitude: -85, longitude: 179.9),
|
||||||
Point(latitude: -85, longitude: -179.9),
|
Point(latitude: -85, longitude: -179.9),
|
||||||
]),
|
]),
|
||||||
innerRings: allZoneHoles,
|
innerRings: driveOuterRings, // Вырезаем только зоны Drive
|
||||||
),
|
),
|
||||||
strokeWidth: 0,
|
strokeWidth: 0,
|
||||||
fillColor: Colors.red.withOpacity(0.15),
|
fillColor: Colors.red.withOpacity(0.15),
|
||||||
@@ -649,27 +633,63 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 2. Отрисовываем каждую зону (включая MultiPolygon и вложенные NotDrive/Finish)
|
||||||
for (var zone in zones) {
|
for (var zone in zones) {
|
||||||
Color borderColor;
|
Color borderColor;
|
||||||
|
Color fillColor;
|
||||||
|
|
||||||
|
// Стилизация в зависимости от типа зоны
|
||||||
if (zone.type == "Drive") {
|
if (zone.type == "Drive") {
|
||||||
borderColor = const Color(0xFF5ECD4C);
|
borderColor = const Color(0xFF5ECD4C); // Зеленый бордюр
|
||||||
|
fillColor = Colors.transparent; // Прозрачная внутри маски
|
||||||
} else if (zone.type == "NotDrive") {
|
} else if (zone.type == "NotDrive") {
|
||||||
borderColor = const Color(0xFFEF4444);
|
borderColor = const Color(0xFFEF4444); // Красный бордюр
|
||||||
|
fillColor = const Color(0xFFEF4444).withOpacity(0.3); // Красная полупрозрачная заливка для наглядности внутри Drive
|
||||||
} else {
|
} else {
|
||||||
borderColor = const Color(0xFFA78BFA);
|
borderColor = const Color(0xFFA78BFA); // Фиолетовый бордюр (Парковка)
|
||||||
|
fillColor = const Color(0xFFA78BFA).withOpacity(0.25); // Фиолетовая заливка
|
||||||
}
|
}
|
||||||
|
|
||||||
objects.add(
|
int polyIndex = 0;
|
||||||
PolylineMapObject(
|
for (var polygonData in zone.geometry) {
|
||||||
mapId: MapObjectId('zone_contour_${zone.id}'),
|
if (polygonData.isEmpty) continue;
|
||||||
polyline: Polyline(
|
|
||||||
points: zone.points.map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList(),
|
// Строим внешнее кольцо текущего полигона
|
||||||
|
var outerPoints = polygonData[0].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
|
||||||
|
if (outerPoints.first != outerPoints.last) {
|
||||||
|
outerPoints.add(outerPoints.first);
|
||||||
|
}
|
||||||
|
LinearRing outerRing = LinearRing(points: outerPoints);
|
||||||
|
|
||||||
|
// Строим внутренние кольца (дыры), если они есть у этого полигона
|
||||||
|
List<LinearRing> innerRings = [];
|
||||||
|
for (int i = 1; i < polygonData.length; i++) {
|
||||||
|
var innerPoints = polygonData[i].map((p) => Point(latitude: p.latitude, longitude: p.longitude)).toList();
|
||||||
|
if (innerPoints.isNotEmpty) {
|
||||||
|
if (innerPoints.first != innerPoints.last) {
|
||||||
|
innerPoints.add(innerPoints.first);
|
||||||
|
}
|
||||||
|
innerRings.add(LinearRing(points: innerPoints));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Рендерим полноценный PolygonMapObject (он поддерживает и заливку, и обводку)
|
||||||
|
objects.add(
|
||||||
|
PolygonMapObject(
|
||||||
|
mapId: MapObjectId('zone_poly_${zone.type}_${zone.id}_$polyIndex'),
|
||||||
|
polygon: Polygon(
|
||||||
|
outerRing: outerRing,
|
||||||
|
innerRings: innerRings,
|
||||||
|
),
|
||||||
|
strokeColor: borderColor,
|
||||||
|
strokeWidth: 2.0,
|
||||||
|
fillColor: fillColor,
|
||||||
|
// Поднимаем зоны ограничений и парковок выше маски и контуров Drive
|
||||||
|
zIndex: zone.type == "Drive" ? 1 : 2,
|
||||||
),
|
),
|
||||||
strokeColor: borderColor,
|
);
|
||||||
strokeWidth: 2.0,
|
polyIndex++;
|
||||||
zIndex: 1,
|
}
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return objects;
|
return objects;
|
||||||
@@ -684,7 +704,10 @@ class _MapScreenState extends State<MapScreen> {
|
|||||||
child: Builder(
|
child: Builder(
|
||||||
builder: (innerContext) => _RoundIconButton(
|
builder: (innerContext) => _RoundIconButton(
|
||||||
icon: Icons.menu,
|
icon: Icons.menu,
|
||||||
onPressed: () => {Scaffold.of(innerContext).openDrawer()},
|
onPressed: () {
|
||||||
|
context.read<MapBloc>().add(FetchProfileData());
|
||||||
|
Scaffold.of(innerContext).openDrawer();
|
||||||
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -23,7 +23,10 @@ class NotificationsScreen extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return _NotificationsScreenContent();
|
return BlocProvider(
|
||||||
|
create: (context) => getIt<NotificationsBloc>()..add(NotificationsFetchRequested(page: 1)),
|
||||||
|
child: _NotificationsScreenContent(),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,21 +39,31 @@ class _NotificationsScreenContent extends StatefulWidget {
|
|||||||
|
|
||||||
class _NotificationsScreenContentState extends State<_NotificationsScreenContent> {
|
class _NotificationsScreenContentState extends State<_NotificationsScreenContent> {
|
||||||
NotificationFilter _filter = NotificationFilter.all;
|
NotificationFilter _filter = NotificationFilter.all;
|
||||||
|
final ScrollController _scrollController = ScrollController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_scrollController.dispose();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
void _setFilter(NotificationFilter filter) {
|
void _setFilter(NotificationFilter filter) {
|
||||||
setState(() {
|
setState(() => _filter = filter);
|
||||||
_filter = filter;
|
_scrollController.jumpTo(0);
|
||||||
});
|
context.read<NotificationsBloc>().add(NotificationsFetchRequested(page: 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider(
|
return NotificationsView(
|
||||||
create: (context) => getIt<NotificationsBloc>()..add(NotificationsFetchRequested()),
|
filter: _filter,
|
||||||
child: NotificationsView(
|
onFilterChanged: _setFilter,
|
||||||
filter: _filter,
|
scrollController: _scrollController,
|
||||||
onFilterChanged: _setFilter,
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,11 +71,13 @@ class _NotificationsScreenContentState extends State<_NotificationsScreenContent
|
|||||||
class NotificationsView extends StatelessWidget {
|
class NotificationsView extends StatelessWidget {
|
||||||
final NotificationFilter filter;
|
final NotificationFilter filter;
|
||||||
final ValueChanged<NotificationFilter> onFilterChanged;
|
final ValueChanged<NotificationFilter> onFilterChanged;
|
||||||
|
final ScrollController? scrollController;
|
||||||
|
|
||||||
const NotificationsView({
|
const NotificationsView({
|
||||||
super.key,
|
super.key,
|
||||||
this.filter = NotificationFilter.all,
|
this.filter = NotificationFilter.all,
|
||||||
required this.onFilterChanged,
|
required this.onFilterChanged,
|
||||||
|
this.scrollController,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -119,7 +134,7 @@ class NotificationsView extends StatelessWidget {
|
|||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
context.read<NotificationsBloc>().add(NotificationsFetchRequested());
|
context.read<NotificationsBloc>().add(NotificationsFetchRequested(page: 1));
|
||||||
},
|
},
|
||||||
child: const Text('Повторить'),
|
child: const Text('Повторить'),
|
||||||
),
|
),
|
||||||
@@ -138,9 +153,6 @@ class NotificationsView extends StatelessWidget {
|
|||||||
return n.category == NotificationCategory.payment;
|
return n.category == NotificationCategory.payment;
|
||||||
case NotificationFilter.order:
|
case NotificationFilter.order:
|
||||||
return n.category == NotificationCategory.scooter;
|
return n.category == NotificationCategory.scooter;
|
||||||
// || n.category == NotificationCategory.adminInfo
|
|
||||||
// || n.category == NotificationCategory.companyInfo;
|
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -152,14 +164,39 @@ class NotificationsView extends StatelessWidget {
|
|||||||
|
|
||||||
return RefreshIndicator(
|
return RefreshIndicator(
|
||||||
onRefresh: () async {
|
onRefresh: () async {
|
||||||
context.read<NotificationsBloc>().add(NotificationsFetchRequested());
|
context.read<NotificationsBloc>().add(NotificationsFetchRequested(page: 1));
|
||||||
},
|
},
|
||||||
child: ListView.builder(
|
child: NotificationListener<ScrollNotification>(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
onNotification: (scrollInfo) {
|
||||||
itemCount: filtered.length,
|
if (scrollInfo.metrics.pixels >= scrollInfo.metrics.maxScrollExtent - 50 &&
|
||||||
itemBuilder: (context, index) {
|
scrollInfo.metrics.maxScrollExtent > 0 &&
|
||||||
return _NotificationCard(notification: filtered[index]);
|
state.hasMore &&
|
||||||
|
!state.isLoadingMore &&
|
||||||
|
state.status == NotificationsStatus.success) {
|
||||||
|
context.read<NotificationsBloc>().add(NotificationsLoadMoreRequested());
|
||||||
|
}
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
|
child: ListView.builder(
|
||||||
|
controller: scrollController,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
itemCount: filtered.length + (state.hasMore && filtered.length >= 10 ? 1 : 0),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
if (index == filtered.length && state.hasMore && filtered.length >= 10) {
|
||||||
|
return const Padding(
|
||||||
|
padding: EdgeInsets.symmetric(vertical: 16),
|
||||||
|
child: Center(
|
||||||
|
child: SizedBox(
|
||||||
|
width: 24,
|
||||||
|
height: 24,
|
||||||
|
child: CircularProgressIndicator(color: Colors.white, strokeWidth: 2),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return _NotificationCard(notification: filtered[index]);
|
||||||
|
},
|
||||||
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@@ -308,16 +345,23 @@ class _NotificationCard extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _formatDate(DateTime date) {
|
String _formatDate(DateTime date) {
|
||||||
|
final localDate = date.toLocal();
|
||||||
final now = DateTime.now();
|
final now = DateTime.now();
|
||||||
final difference = now.difference(date);
|
|
||||||
|
|
||||||
if (difference.inDays == 0) {
|
if (localDate.year == now.year &&
|
||||||
return 'Сегодня, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
localDate.month == now.month &&
|
||||||
} else if (difference.inDays == 1) {
|
localDate.day == now.day) {
|
||||||
return 'Вчера, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
|
return 'Сегодня, ${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}';
|
||||||
} else {
|
|
||||||
return '${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}.${date.year}';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final yesterday = now.subtract(const Duration(days: 1));
|
||||||
|
if (localDate.year == yesterday.year &&
|
||||||
|
localDate.month == yesterday.month &&
|
||||||
|
localDate.day == yesterday.day) {
|
||||||
|
return 'Вчера, ${localDate.hour.toString().padLeft(2, '0')}:${localDate.minute.toString().padLeft(2, '0')}';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '${localDate.day.toString().padLeft(2, '0')}.${localDate.month.toString().padLeft(2, '0')}.${localDate.year}';
|
||||||
}
|
}
|
||||||
|
|
||||||
String _getTypeLabel(NotificationType type) {
|
String _getTypeLabel(NotificationType type) {
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import 'package:go_router/go_router.dart';
|
|||||||
import '../../core/app_colors.dart';
|
import '../../core/app_colors.dart';
|
||||||
import '../../di/service_locator.dart';
|
import '../../di/service_locator.dart';
|
||||||
import '../../domain/entities/payment_card.dart';
|
import '../../domain/entities/payment_card.dart';
|
||||||
|
import '../../domain/usecase/add_payment_card_usecase.dart';
|
||||||
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
||||||
|
import '../../domain/usecase/get_profile_usecase.dart';
|
||||||
|
import '../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||||
import '../components/custom_app_bar.dart';
|
import '../components/custom_app_bar.dart';
|
||||||
import '../components/gradient_button.dart';
|
import '../components/gradient_button.dart';
|
||||||
import '../components/payment_option.dart';
|
import '../components/payment_option.dart';
|
||||||
@@ -133,11 +136,6 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
|||||||
|
|
||||||
const SizedBox(height: 24),
|
const SizedBox(height: 24),
|
||||||
|
|
||||||
_buildInfoLabel('Начислено баллов:'),
|
|
||||||
_buildValueRow('points_icon.png', '2 балла'),
|
|
||||||
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
@@ -148,7 +146,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
|||||||
_buildInfoLabel('Стоимость поездки:'),
|
_buildInfoLabel('Стоимость поездки:'),
|
||||||
_buildValueRow(
|
_buildValueRow(
|
||||||
'money_icon.png',
|
'money_icon.png',
|
||||||
'17,17 BYN',
|
order?.totalOrderPricePrint ?? '- BYN',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -161,7 +159,7 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
|||||||
_buildInfoLabel('Оплачено:'),
|
_buildInfoLabel('Оплачено:'),
|
||||||
_buildValueRow(
|
_buildValueRow(
|
||||||
'money_icon.png',
|
'money_icon.png',
|
||||||
'10,00 BYN',
|
order?.payTotalPricePrint ?? '0,00 BYN',
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -192,9 +190,9 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
|||||||
width: 40,
|
width: 40,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
const Text(
|
Text(
|
||||||
'7,17 BYN',
|
order?.unpayTotalPricePrint ?? '0,00 BYN',
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
fontSize: 32,
|
fontSize: 32,
|
||||||
fontWeight: FontWeight.bold,
|
fontWeight: FontWeight.bold,
|
||||||
@@ -212,10 +210,10 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
|||||||
(state.useBalance || state.selectedCard != null)
|
(state.useBalance || state.selectedCard != null)
|
||||||
? Padding(padding: const EdgeInsets.symmetric(horizontal: 20),
|
? Padding(padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: PaymentOption(
|
child: PaymentOption(
|
||||||
title: state.useBalance ? 'Баланс' : state.selectedCard!.type,
|
title: state.useBalance ? 'Баланс' : "",
|
||||||
subtitle: state.useBalance
|
subtitle: state.useBalance
|
||||||
? '${state.userBalance.toStringAsFixed(2)} BYN'
|
? '${state.userBalance.toStringAsFixed(2)} BYN'
|
||||||
: '****${state.selectedCard!.cardLastNumber}',
|
: '${state.selectedCard!.cardLastNumber}',
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final result = await showModalBottomSheet<dynamic>(
|
final result = await showModalBottomSheet<dynamic>(
|
||||||
@@ -225,6 +223,9 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
|
|||||||
builder: (innerContext) => BlocProvider(
|
builder: (innerContext) => BlocProvider(
|
||||||
create: (context) => PaymentMethodSheetBloc(
|
create: (context) => PaymentMethodSheetBloc(
|
||||||
getIt<GetPaymentCardsUsecase>(),
|
getIt<GetPaymentCardsUsecase>(),
|
||||||
|
getIt<SetMainPaymentCardUsecase>(),
|
||||||
|
getIt<GetProfileUseCase>(),
|
||||||
|
getIt<AddPaymentCardUsecase>(),
|
||||||
)..add(PaymentMethodSheetStarted()),
|
)..add(PaymentMethodSheetStarted()),
|
||||||
child: PaymentMethodSheet(
|
child: PaymentMethodSheet(
|
||||||
initialSelectedCard: state.useBalance ? null : state.selectedCard,
|
initialSelectedCard: state.useBalance ? null : state.selectedCard,
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ class PaymentMethodsScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildBalanceCard(BuildContext context, int balance) {
|
Widget _buildBalanceCard(BuildContext context, double balance) {
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.all(20),
|
padding: EdgeInsets.all(20),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@@ -184,11 +184,14 @@ class PaymentMethodsScreen extends StatelessWidget {
|
|||||||
color: Colors.transparent,
|
color: Colors.transparent,
|
||||||
child: InkWell(
|
child: InkWell(
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final isCardAdded = await context.push<bool>('/home/payment-methods/add-card');
|
context.read<PaymentMethodsBloc>().add(AddPayemntCardPressed());
|
||||||
|
/*final isCardAdded = await context.push<bool>('/home/payment-methods/add-card');
|
||||||
|
|
||||||
if (isCardAdded == true && context.mounted) {
|
if (isCardAdded == true && context.mounted) {
|
||||||
context.read<PaymentMethodsBloc>().add(PaymentMethodsStarted());
|
context.read<PaymentMethodsBloc>().add(PaymentMethodsStarted());
|
||||||
}
|
}*/
|
||||||
|
|
||||||
|
|
||||||
},
|
},
|
||||||
borderRadius: BorderRadius.circular(24),
|
borderRadius: BorderRadius.circular(24),
|
||||||
child: const Padding(
|
child: const Padding(
|
||||||
@@ -264,12 +267,7 @@ class _CardItem extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
Image.asset(
|
Icon(Icons.credit_card, color: Color(0xFFFFFFFF), size: 40),
|
||||||
_getDefaultIconPath(card.type),
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
fit: BoxFit.contain,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: GestureDetector(
|
child: GestureDetector(
|
||||||
@@ -279,7 +277,7 @@ class _CardItem extends StatelessWidget {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
'${card.type} ****${card.cardLastNumber}',
|
'Карта ${card.cardLastNumber}',
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600),
|
style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
|
|||||||
@@ -77,19 +77,20 @@ class _PhoneLoginScreenState extends State<PhoneLoginScreen> {
|
|||||||
return BlocConsumer<VerifyCodeBloc, VerifyCodeState>(
|
return BlocConsumer<VerifyCodeBloc, VerifyCodeState>(
|
||||||
listener: (context, state) {
|
listener: (context, state) {
|
||||||
if (state.isSuccess) {
|
if (state.isSuccess) {
|
||||||
context.go("/pin");
|
|
||||||
} else if (state.error != null) {
|
} else if (state.error != null) {
|
||||||
setState(() {
|
setState(() { _isFirstTry = false; });
|
||||||
_isFirstTry = false;
|
|
||||||
});
|
|
||||||
codeController.clear();
|
codeController.clear();
|
||||||
FocusScope.of(context).requestFocus(codeFocusNode);
|
FocusScope.of(context).requestFocus(codeFocusNode);
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(state.error!)));
|
||||||
context,
|
|
||||||
).showSnackBar(SnackBar(content: Text(state.error!)));
|
|
||||||
} else if (state.isBlocked) {
|
} else if (state.isBlocked) {
|
||||||
context.go("/block");
|
context.go("/block");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.secondsLeft == 0 && codeFocusNode.hasFocus) {
|
||||||
|
codeFocusNode.unfocus();
|
||||||
|
codeController.clear();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -126,12 +127,21 @@ class _PhoneLoginScreenState extends State<PhoneLoginScreen> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 35),
|
const SizedBox(height: 35),
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: openKeyboard,
|
onTap: state.secondsLeft > 0 ? openKeyboard : null,
|
||||||
child: CodeDots(code: state.code, length: 6),
|
child: CodeDots(code: state.code, length: 6),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
if (!_isFirstTry)
|
if (state.secondsLeft == 0)
|
||||||
|
const Text(
|
||||||
|
"Время действия кода истекло. Запросите новый.",
|
||||||
|
style: TextStyle(
|
||||||
|
color: AppColors.pinError,
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else if (!_isFirstTry)
|
||||||
Text(
|
Text(
|
||||||
"Осталось ${state.attemptsLeft} попытк${state.attemptsLeft == 1 ? 'а' : 'и'}",
|
"Осталось ${state.attemptsLeft} попытк${state.attemptsLeft == 1 ? 'а' : 'и'}",
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
@@ -145,14 +155,14 @@ class _PhoneLoginScreenState extends State<PhoneLoginScreen> {
|
|||||||
text: state.secondsLeft == 0
|
text: state.secondsLeft == 0
|
||||||
? "Отправить код повторно"
|
? "Отправить код повторно"
|
||||||
: "Отправить код повторно\nчерез 00:${state.secondsLeft.toString().padLeft(2, '0')} сек.",
|
: "Отправить код повторно\nчерез 00:${state.secondsLeft.toString().padLeft(2, '0')} сек.",
|
||||||
onTap: state.secondsLeft == 0
|
onTap: (state.secondsLeft == 0 && !state.isSubmitting)
|
||||||
? () {
|
? () {
|
||||||
context.read<VerifyCodeBloc>().add(
|
context.read<VerifyCodeBloc>().add(
|
||||||
ResendCodePressed(),
|
ResendCodePressed(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
: () {},
|
: null,
|
||||||
enabled: state.secondsLeft == 0,
|
enabled: state.secondsLeft == 0 && !state.isSubmitting,
|
||||||
width: 250,
|
width: 250,
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
@@ -167,6 +177,7 @@ class _PhoneLoginScreenState extends State<PhoneLoginScreen> {
|
|||||||
keyboardType: TextInputType.number,
|
keyboardType: TextInputType.number,
|
||||||
maxLength: 6,
|
maxLength: 6,
|
||||||
autofocus: true,
|
autofocus: true,
|
||||||
|
enabled: state.secondsLeft > 0,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -22,6 +22,43 @@ class PhoneScreen extends StatefulWidget {
|
|||||||
class _PhoneScreenState extends State<PhoneScreen> {
|
class _PhoneScreenState extends State<PhoneScreen> {
|
||||||
final TextEditingController _phoneController = TextEditingController();
|
final TextEditingController _phoneController = TextEditingController();
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
// Вызываем диалог сразу после отрисовки первого кадра экрана
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
_showTestModeDialog();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Метод для отображения диалогового окна
|
||||||
|
void _showTestModeDialog() {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false, // Запрещает закрывать окно кликом по фону
|
||||||
|
builder: (BuildContext context) {
|
||||||
|
return AlertDialog(
|
||||||
|
title: const Text(
|
||||||
|
'Внимание!',
|
||||||
|
style: TextStyle(fontWeight: FontWeight.bold),
|
||||||
|
),
|
||||||
|
content: const Text('Приложение находится в тестовом режиме!'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop(); // Закрывает диалоговое окно
|
||||||
|
},
|
||||||
|
child: const Text(
|
||||||
|
'Продолжить',
|
||||||
|
style: TextStyle(color: Colors.blueAccent),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_phoneController.dispose();
|
_phoneController.dispose();
|
||||||
@@ -219,7 +256,6 @@ class _PhoneScreenState extends State<PhoneScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 🔹 Кликабельный текст для политики
|
|
||||||
class ClickableText extends StatelessWidget {
|
class ClickableText extends StatelessWidget {
|
||||||
final String text;
|
final String text;
|
||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
@@ -237,12 +273,12 @@ class ClickableText extends StatelessWidget {
|
|||||||
child: Text(
|
child: Text(
|
||||||
text,
|
text,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
color: Colors.blueAccent,
|
color: Colors.blueAccent,
|
||||||
decorationColor: Colors.blueAccent,
|
decorationColor: Colors.blueAccent,
|
||||||
decoration: TextDecoration.underline,
|
decoration: TextDecoration.underline,
|
||||||
fontSize: 12
|
fontSize: 12
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,7 +12,9 @@ import '../state/scooter_detail_state.dart';
|
|||||||
import '../viewmodel/scooter_detail_bloc.dart';
|
import '../viewmodel/scooter_detail_bloc.dart';
|
||||||
|
|
||||||
class ScooterDetailScreen extends StatelessWidget {
|
class ScooterDetailScreen extends StatelessWidget {
|
||||||
const ScooterDetailScreen({super.key});
|
final String address;
|
||||||
|
final double distance;
|
||||||
|
const ScooterDetailScreen({super.key, required this.address, required this.distance});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
@@ -83,7 +85,7 @@ class ScooterDetailScreen extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const Expanded(child: ScooterInfoSection()),
|
Expanded(child: ScooterInfoSection(distance: distance, location: address, batteryLevel: scooter?.batteryLevel ?? 0,)),
|
||||||
const SizedBox(width: 20),
|
const SizedBox(width: 20),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -59,14 +59,14 @@ class _SendPhotoViewState extends State<SendPhotoView> {
|
|||||||
_getImage(ImageSource.camera);
|
_getImage(ImageSource.camera);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
// ListTile(
|
||||||
leading: const Icon(Icons.photo_library),
|
// leading: const Icon(Icons.photo_library),
|
||||||
title: const Text('Выбрать из галереи'),
|
// title: const Text('Выбрать из галереи'),
|
||||||
onTap: () {
|
// onTap: () {
|
||||||
Navigator.pop(context);
|
// Navigator.pop(context);
|
||||||
_getImage(ImageSource.gallery);
|
// _getImage(ImageSource.gallery);
|
||||||
},
|
// },
|
||||||
),
|
// ),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.cancel),
|
leading: const Icon(Icons.cancel),
|
||||||
title: const Text('Отмена'),
|
title: const Text('Отмена'),
|
||||||
|
|||||||
@@ -3,13 +3,23 @@ import 'package:flutter_bloc/flutter_bloc.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
|
|
||||||
import '../../core/app_colors.dart';
|
import '../../core/app_colors.dart';
|
||||||
|
import '../../di/service_locator.dart';
|
||||||
|
import '../../domain/entities/payment_card.dart';
|
||||||
|
import '../../domain/usecase/add_payment_card_usecase.dart';
|
||||||
|
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
||||||
|
import '../../domain/usecase/get_profile_usecase.dart';
|
||||||
|
import '../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||||
import '../components/app_checkbox.dart';
|
import '../components/app_checkbox.dart';
|
||||||
import '../components/custom_app_bar.dart';
|
import '../components/custom_app_bar.dart';
|
||||||
import '../components/gradient_button.dart';
|
import '../components/gradient_button.dart';
|
||||||
|
import '../components/payment_option.dart';
|
||||||
import '../components/period_selector.dart';
|
import '../components/period_selector.dart';
|
||||||
import '../components/content_parser.dart'; // ✅ Парсер контента
|
import '../components/content_parser.dart'; // ✅ Парсер контента
|
||||||
|
import '../components/sheet/payment_method_sheet.dart';
|
||||||
|
import '../event/payment_method_sheet_event.dart';
|
||||||
import '../event/subscription_details_event.dart';
|
import '../event/subscription_details_event.dart';
|
||||||
import '../state/susbcription_details_state.dart';
|
import '../state/susbcription_details_state.dart';
|
||||||
|
import '../viewmodel/payment_method_sheet_bloc.dart';
|
||||||
import '../viewmodel/susbcription_details_bloc.dart';
|
import '../viewmodel/susbcription_details_bloc.dart';
|
||||||
|
|
||||||
class SubscriptionDetailsScreen extends StatelessWidget {
|
class SubscriptionDetailsScreen extends StatelessWidget {
|
||||||
@@ -50,11 +60,9 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
|
|
||||||
// 🔹 Контент
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
// Волна снизу
|
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
@@ -108,10 +116,9 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// 🔹 Обёртка для растягивания контента на всю ширину
|
|
||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch, // ✅ Растягивает детей на всю ширину
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
mainAxisSize: MainAxisSize.min, // ✅ Не занимает лишнее место по вертикали
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: ContentParser.parseContent(
|
children: ContentParser.parseContent(
|
||||||
textJson: state.subscription.fullDescriptionJson,
|
textJson: state.subscription.fullDescriptionJson,
|
||||||
text: state.subscription.fullDescription,
|
text: state.subscription.fullDescription,
|
||||||
@@ -121,6 +128,14 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
|||||||
if (isAvailableForPurchase) ...[
|
if (isAvailableForPurchase) ...[
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
_ActionCard(state: state),
|
_ActionCard(state: state),
|
||||||
|
const SizedBox(height: 24),
|
||||||
|
const Text(
|
||||||
|
'Способ оплаты',
|
||||||
|
style: TextStyle(color: Colors.white70, fontSize: 14),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
_buildCardSelector(state, context),
|
||||||
|
|
||||||
const SizedBox(height: 30),
|
const SizedBox(height: 30),
|
||||||
GradientButton(
|
GradientButton(
|
||||||
text: state.isAlreadyPurchased ? 'Продлить' : 'Активировать',
|
text: state.isAlreadyPurchased ? 'Продлить' : 'Активировать',
|
||||||
@@ -129,7 +144,7 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
|||||||
ActivateSubscriptionPressed(),
|
ActivateSubscriptionPressed(),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
enabled: state.isAgreed,
|
enabled: state.isAgreed && state.selectedCard != null,
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 56,
|
height: 56,
|
||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
@@ -141,8 +156,79 @@ class SubscriptionDetailsScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Widget _buildCardSelector(DetailsContentState state, BuildContext context) {
|
||||||
|
if (state.selectedCard != null) {
|
||||||
|
return PaymentOption(
|
||||||
|
title: "Карта",
|
||||||
|
subtitle: state.selectedCard.cardLastNumber,
|
||||||
|
isSelected: true,
|
||||||
|
onTap: () async {
|
||||||
|
final selectedCard = await showModalBottomSheet<PaymentCard>(
|
||||||
|
context: context,
|
||||||
|
isScrollControlled: true,
|
||||||
|
backgroundColor: Colors.transparent,
|
||||||
|
builder: (innerContext) => BlocProvider(
|
||||||
|
create: (context) => PaymentMethodSheetBloc(
|
||||||
|
getIt<GetPaymentCardsUsecase>(),
|
||||||
|
getIt<SetMainPaymentCardUsecase>(),
|
||||||
|
getIt<GetProfileUseCase>(),
|
||||||
|
getIt<AddPaymentCardUsecase>(),
|
||||||
|
)..add(PaymentMethodSheetStarted()),
|
||||||
|
child: const PaymentMethodSheet(showBalance: false),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (selectedCard != null && context.mounted) {
|
||||||
|
context.read<SubscriptionDetailsBloc>().add(SelectCardEvent(selectedCard));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Container(
|
||||||
|
height: 56,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
border: Border.all(
|
||||||
|
color: Colors.white.withOpacity(0.4),
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
onTap: () {
|
||||||
|
context.pushReplacement('/home/payment-method-sheet');
|
||||||
|
},
|
||||||
|
borderRadius: BorderRadius.circular(24),
|
||||||
|
child: Center(
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
const Text(
|
||||||
|
'Добавить способ оплаты',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.6)),
|
||||||
|
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.4)),
|
||||||
|
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.2)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class _ActionCard extends StatelessWidget {
|
class _ActionCard extends StatelessWidget {
|
||||||
final DetailsContentState state;
|
final DetailsContentState state;
|
||||||
|
|
||||||
@@ -216,6 +302,7 @@ class _ActionCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _PriceRow extends StatelessWidget {
|
class _PriceRow extends StatelessWidget {
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ class SupportScreen extends StatelessWidget {
|
|||||||
LinkRow(
|
LinkRow(
|
||||||
icon: 'assets/icons/call.png',
|
icon: 'assets/icons/call.png',
|
||||||
title: 'Позвонить',
|
title: 'Позвонить',
|
||||||
onTap: () => openLink('tel:+375000000000'),
|
onTap: () => openLink('tel:+375257110909'),
|
||||||
),
|
),
|
||||||
const Divider(height: 1, color: Colors.white24),
|
const Divider(height: 1, color: Colors.white24),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import 'package:be_happy/domain/usecase/add_payment_card_usecase.dart';
|
||||||
|
import 'package:be_happy/presentation/viewmodel/payment_methods_bloc.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
@@ -6,18 +8,23 @@ import '../../core/app_colors.dart';
|
|||||||
import '../../di/service_locator.dart';
|
import '../../di/service_locator.dart';
|
||||||
import '../../domain/entities/payment_card.dart';
|
import '../../domain/entities/payment_card.dart';
|
||||||
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
||||||
|
import '../../domain/usecase/get_profile_usecase.dart';
|
||||||
|
import '../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||||
import '../components/app_checkbox.dart';
|
import '../components/app_checkbox.dart';
|
||||||
import '../components/custom_app_bar.dart'; // ✅ Уже есть
|
import '../components/custom_app_bar.dart';
|
||||||
import '../components/gradient_button.dart';
|
import '../components/gradient_button.dart';
|
||||||
import '../components/payment_option.dart';
|
import '../components/payment_option.dart';
|
||||||
import '../components/sheet/payment_method_sheet.dart';
|
import '../components/sheet/payment_method_sheet.dart';
|
||||||
import '../event/payment_method_sheet_event.dart';
|
import '../event/payment_method_sheet_event.dart';
|
||||||
|
import '../event/payment_methods_event.dart';
|
||||||
import '../event/top_up_event.dart';
|
import '../event/top_up_event.dart';
|
||||||
import '../state/top_up_state.dart';
|
import '../state/top_up_state.dart';
|
||||||
import '../viewmodel/payment_method_sheet_bloc.dart';
|
import '../viewmodel/payment_method_sheet_bloc.dart';
|
||||||
import '../viewmodel/top_up_bloc.dart';
|
import '../viewmodel/top_up_bloc.dart';
|
||||||
|
|
||||||
class TopUpScreen extends StatelessWidget {
|
class TopUpScreen extends StatelessWidget {
|
||||||
|
const TopUpScreen({Key? key}) : super(key: key);
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -26,55 +33,77 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
child: SafeArea(
|
child: SafeArea(
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Column(
|
child: BlocListener<TopUpBloc, TopUpState>(
|
||||||
children: [
|
listener: (context, state) {
|
||||||
const SizedBox(height: 16),
|
if (state is TopUpPaymentSuccess) {
|
||||||
const CustomAppBar(title: 'Пополнение баланса'),
|
context.go('/home/payment-methods/top-up/result/success');
|
||||||
const SizedBox(height: 20),
|
} else if (state is TopUpPaymentFailure) {
|
||||||
|
context.go('/home/payment-methods/top-up/result/failed');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const CustomAppBar(title: 'Пополнение баланса'),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
BlocBuilder<TopUpBloc, TopUpState>(
|
||||||
|
builder: (context, state) {
|
||||||
|
if (state is TopUpLoading) {
|
||||||
|
return const Expanded(
|
||||||
|
child: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
BlocBuilder<TopUpBloc, TopUpState>(
|
if (state is TopUpLoaded) {
|
||||||
builder: (context, state) {
|
if (state.isProcessingPayment) {
|
||||||
if (state.isLoading) {
|
return const Expanded(
|
||||||
return const Center(child: CircularProgressIndicator());
|
child: Center(child: CircularProgressIndicator()),
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return Expanded(
|
return Expanded(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
children: [
|
||||||
_buildTariffList(state, context),
|
Column(
|
||||||
const SizedBox(height: 30),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
_buildPriceInfo(state),
|
children: [
|
||||||
const SizedBox(height: 40),
|
_buildTariffList(state, context),
|
||||||
const Text(
|
const SizedBox(height: 30),
|
||||||
'Способ оплаты',
|
_buildPriceInfo(state),
|
||||||
style: TextStyle(color: Colors.white70),
|
const SizedBox(height: 40),
|
||||||
|
const Text(
|
||||||
|
'Способ оплаты',
|
||||||
|
style: TextStyle(color: Colors.white70),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 15),
|
||||||
|
_buildCardSelector(state, context),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
_buildAgreement(state, context),
|
||||||
|
const Spacer(),
|
||||||
|
_buildPayButton(state, () {
|
||||||
|
context.read<TopUpBloc>().add(TopUpButtonPressed());
|
||||||
|
}),
|
||||||
|
const SizedBox(height: 30),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 15),
|
|
||||||
_buildCardSelector(state, context),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
_buildAgreement(state, context),
|
|
||||||
const Spacer(),
|
|
||||||
_buildPayButton(state),
|
|
||||||
const SizedBox(height: 30),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
);
|
||||||
),
|
}
|
||||||
);
|
|
||||||
},
|
return const SizedBox.shrink();
|
||||||
),
|
},
|
||||||
],
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
)
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildTariffList(TopUpState state, BuildContext context) {
|
Widget _buildTariffList(TopUpLoaded state, BuildContext context) {
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 120,
|
height: 120,
|
||||||
child: ListView.separated(
|
child: ListView.separated(
|
||||||
@@ -85,14 +114,11 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
final tariff = state.certificates[index];
|
final tariff = state.certificates[index];
|
||||||
final isSelected = state.selectedTariff == tariff;
|
final isSelected = state.selectedTariff == tariff;
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () =>
|
onTap: () => context.read<TopUpBloc>().add(SelectCertificate(tariff)),
|
||||||
context.read<TopUpBloc>().add(SelectCertificate(tariff)),
|
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 140,
|
width: 140,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isSelected
|
color: isSelected ? const Color(0xFF80FFC1) : Colors.white.withOpacity(0.1),
|
||||||
? const Color(0xFF80FFC1)
|
|
||||||
: Colors.white.withOpacity(0.1),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: Colors.white24),
|
border: Border.all(color: Colors.white24),
|
||||||
),
|
),
|
||||||
@@ -123,7 +149,7 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPriceInfo(TopUpState state) {
|
Widget _buildPriceInfo(TopUpLoaded state) {
|
||||||
if (state.selectedTariff == null) return const SizedBox.shrink();
|
if (state.selectedTariff == null) return const SizedBox.shrink();
|
||||||
return Center(
|
return Center(
|
||||||
child: Column(
|
child: Column(
|
||||||
@@ -153,13 +179,13 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildCardSelector(TopUpState state, BuildContext context) {
|
Widget _buildCardSelector(TopUpLoaded state, BuildContext context) {
|
||||||
return state.selectedCard != null
|
return state.selectedCard != null
|
||||||
? Padding(
|
? Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: PaymentOption(
|
child: PaymentOption(
|
||||||
title: state.selectedCard!.type,
|
title: "Карта",
|
||||||
subtitle: '****${state.selectedCard!.cardLastNumber}',
|
subtitle: '${state.selectedCard!.cardLastNumber}',
|
||||||
isSelected: true,
|
isSelected: true,
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
final selectedCard = await showModalBottomSheet<PaymentCard>(
|
final selectedCard = await showModalBottomSheet<PaymentCard>(
|
||||||
@@ -167,10 +193,13 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
backgroundColor: Colors.transparent,
|
backgroundColor: Colors.transparent,
|
||||||
builder: (innerContext) => BlocProvider(
|
builder: (innerContext) => BlocProvider(
|
||||||
create: (context) =>
|
create: (context) => PaymentMethodSheetBloc(
|
||||||
PaymentMethodSheetBloc(getIt<GetPaymentCardsUsecase>())
|
getIt<GetPaymentCardsUsecase>(),
|
||||||
..add(PaymentMethodSheetStarted()),
|
getIt<SetMainPaymentCardUsecase>(),
|
||||||
child: const PaymentMethodSheet(showBalance: false,),
|
getIt<GetProfileUseCase>(),
|
||||||
|
getIt<AddPaymentCardUsecase>(),
|
||||||
|
)..add(PaymentMethodSheetStarted()),
|
||||||
|
child: const PaymentMethodSheet(showBalance: false),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -202,7 +231,6 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
|
|
||||||
const Text(
|
const Text(
|
||||||
'Способ оплаты',
|
'Способ оплаты',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -212,21 +240,9 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
Icon(
|
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.6)),
|
||||||
Icons.arrow_forward_ios,
|
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.4)),
|
||||||
size: 12,
|
Icon(Icons.arrow_forward_ios, size: 12, color: Colors.white.withOpacity(0.2)),
|
||||||
color: Colors.white.withOpacity(0.6),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 12,
|
|
||||||
color: Colors.white.withOpacity(0.4),
|
|
||||||
),
|
|
||||||
Icon(
|
|
||||||
Icons.arrow_forward_ios,
|
|
||||||
size: 12,
|
|
||||||
color: Colors.white.withOpacity(0.2),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -236,20 +252,19 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildAgreement(TopUpState state, BuildContext context) {
|
Widget _buildAgreement(TopUpLoaded state, BuildContext context) {
|
||||||
return Row(
|
return Row(
|
||||||
children: [
|
children: [
|
||||||
AppCheckbox(
|
AppCheckbox(
|
||||||
value: state.isAgreed,
|
value: state.isAgreed,
|
||||||
onChanged: (v) =>
|
onChanged: (v) => context.read<TopUpBloc>().add(ToggleAgreement(v ?? false)),
|
||||||
context.read<TopUpBloc>().add(ToggleAgreement(v ?? false)),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Flexible(
|
const Flexible(
|
||||||
child: Text.rich(
|
child: Text.rich(
|
||||||
TextSpan(
|
TextSpan(
|
||||||
text: 'Я принимаю условия покупки бонусного пакета, при котором денежные средства не подлежат возврату... ',
|
text: 'Я принимаю условия покупки бонусного пакека, при котором денежные средства не подлежат возврату... ',
|
||||||
style: const TextStyle(
|
style: TextStyle(
|
||||||
color: AppColors.white70,
|
color: AppColors.white70,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
),
|
),
|
||||||
@@ -260,10 +275,10 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildPayButton(TopUpState state) {
|
Widget _buildPayButton(TopUpLoaded state, VoidCallback onTap) {
|
||||||
return GradientButton(
|
return GradientButton(
|
||||||
text: 'Оплатить',
|
text: 'Оплатить',
|
||||||
onTap: state.isAgreed ? () {} : null,
|
onTap: state.isAgreed ? onTap : null,
|
||||||
enabled: state.isAgreed,
|
enabled: state.isAgreed,
|
||||||
showArrows: true,
|
showArrows: true,
|
||||||
height: 56,
|
height: 56,
|
||||||
@@ -271,5 +286,4 @@ class TopUpScreen extends StatelessWidget {
|
|||||||
fontSize: 16,
|
fontSize: 16,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -19,7 +19,7 @@ class ScooterState {
|
|||||||
final String? address;
|
final String? address;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final String phoneNumber;
|
final String phoneNumber;
|
||||||
final int balance;
|
final double balance;
|
||||||
final UserCheckFlags flags;
|
final UserCheckFlags flags;
|
||||||
final ClientNotification? lastNotification;
|
final ClientNotification? lastNotification;
|
||||||
|
|
||||||
@@ -52,7 +52,7 @@ class ScooterState {
|
|||||||
String? address,
|
String? address,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
String? phoneNumber,
|
String? phoneNumber,
|
||||||
int? balance,
|
double? balance,
|
||||||
UserCheckFlags? flags,
|
UserCheckFlags? flags,
|
||||||
ClientNotification? lastNotification,
|
ClientNotification? lastNotification,
|
||||||
}) {
|
}) {
|
||||||
|
|||||||
@@ -1,27 +1,43 @@
|
|||||||
import '../../domain/entities/client_notification.dart';
|
import '../../domain/entities/client_notification.dart';
|
||||||
|
|
||||||
enum NotificationsStatus { initial, loading, success, failure }
|
enum NotificationsStatus { initial, loading, loadingMore, success, failure }
|
||||||
|
|
||||||
class NotificationsState {
|
class NotificationsState {
|
||||||
final NotificationsStatus status;
|
final NotificationsStatus status;
|
||||||
final List<ClientNotification> notifications;
|
final List<ClientNotification> notifications;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
final int currentPage;
|
||||||
|
final int totalPages;
|
||||||
|
final bool hasMore;
|
||||||
|
final bool isLoadingMore;
|
||||||
|
|
||||||
const NotificationsState({
|
const NotificationsState({
|
||||||
this.status = NotificationsStatus.initial,
|
this.status = NotificationsStatus.initial,
|
||||||
this.notifications = const [],
|
this.notifications = const [],
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.currentPage = 1,
|
||||||
|
this.totalPages = 1,
|
||||||
|
this.hasMore = true,
|
||||||
|
this.isLoadingMore = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
NotificationsState copyWith({
|
NotificationsState copyWith({
|
||||||
NotificationsStatus? status,
|
NotificationsStatus? status,
|
||||||
List<ClientNotification>? notifications,
|
List<ClientNotification>? notifications,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
|
int? currentPage,
|
||||||
|
int? totalPages,
|
||||||
|
bool? hasMore,
|
||||||
|
bool? isLoadingMore,
|
||||||
}) {
|
}) {
|
||||||
return NotificationsState(
|
return NotificationsState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
notifications: notifications ?? this.notifications,
|
notifications: notifications ?? this.notifications,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
|
currentPage: currentPage ?? this.currentPage,
|
||||||
|
totalPages: totalPages ?? this.totalPages,
|
||||||
|
hasMore: hasMore ?? this.hasMore,
|
||||||
|
isLoadingMore: isLoadingMore ?? this.isLoadingMore,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,7 +11,7 @@ class PaymentConfirmState {
|
|||||||
final bool paymentCompleted;
|
final bool paymentCompleted;
|
||||||
final ScooterOrder? order;
|
final ScooterOrder? order;
|
||||||
final bool useBalance;
|
final bool useBalance;
|
||||||
final int userBalance;
|
final double userBalance;
|
||||||
|
|
||||||
const PaymentConfirmState({
|
const PaymentConfirmState({
|
||||||
this.status = PaymentConfirmStatus.initial,
|
this.status = PaymentConfirmStatus.initial,
|
||||||
@@ -30,7 +30,7 @@ class PaymentConfirmState {
|
|||||||
bool? paymentCompleted,
|
bool? paymentCompleted,
|
||||||
ScooterOrder? order,
|
ScooterOrder? order,
|
||||||
bool? useBalance,
|
bool? useBalance,
|
||||||
int? userBalance,
|
double? userBalance,
|
||||||
}) {
|
}) {
|
||||||
return PaymentConfirmState(
|
return PaymentConfirmState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ class PaymentMethodSheetState {
|
|||||||
PaymentMethodSheetState({
|
PaymentMethodSheetState({
|
||||||
this.status = PaymentMethodSheetStatus.initial,
|
this.status = PaymentMethodSheetStatus.initial,
|
||||||
this.cards = const [],
|
this.cards = const [],
|
||||||
this.balance = 0.0,
|
this.balance = 0,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ enum PaymentMethodsStatus { initial, loading, success, failure }
|
|||||||
class PaymentMethodsState {
|
class PaymentMethodsState {
|
||||||
final PaymentMethodsStatus status;
|
final PaymentMethodsStatus status;
|
||||||
final List<PaymentCard> cards;
|
final List<PaymentCard> cards;
|
||||||
final int balance;
|
final double balance;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final bool isDeleting;
|
final bool isDeleting;
|
||||||
final bool isSettingMain;
|
final bool isSettingMain;
|
||||||
@@ -22,7 +22,7 @@ class PaymentMethodsState {
|
|||||||
PaymentMethodsState copyWith({
|
PaymentMethodsState copyWith({
|
||||||
PaymentMethodsStatus? status,
|
PaymentMethodsStatus? status,
|
||||||
List<PaymentCard>? cards,
|
List<PaymentCard>? cards,
|
||||||
int? balance,
|
double? balance,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
bool? isDeleting,
|
bool? isDeleting,
|
||||||
bool? isSettingMain,
|
bool? isSettingMain,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import 'package:be_happy/domain/entities/payment_card.dart';
|
||||||
import 'package:be_happy/domain/entities/subscription.dart';
|
import 'package:be_happy/domain/entities/subscription.dart';
|
||||||
|
|
||||||
import '../../domain/entities/subscription_period.dart';
|
import '../../domain/entities/subscription_period.dart';
|
||||||
@@ -8,19 +9,24 @@ class DetailsLoading extends SubscriptionDetailsState {}
|
|||||||
|
|
||||||
class DetailsError extends SubscriptionDetailsState {
|
class DetailsError extends SubscriptionDetailsState {
|
||||||
final String message;
|
final String message;
|
||||||
|
|
||||||
DetailsError(this.message);
|
DetailsError(this.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
class DetailsContentState extends SubscriptionDetailsState {
|
class DetailsContentState extends SubscriptionDetailsState {
|
||||||
final Subscription subscription;
|
final Subscription subscription;
|
||||||
final SubscriptionPeriod selectedPeriod;
|
final SubscriptionPeriod selectedPeriod;
|
||||||
|
final List<PaymentCard> cards;
|
||||||
|
final PaymentCard selectedCard;
|
||||||
final bool isAgreed;
|
final bool isAgreed;
|
||||||
final bool isAlreadyPurchased; // ✅ Куплена ли эта подписка сейчас
|
final bool isAlreadyPurchased;
|
||||||
final bool isSuccess; // ✅ Сигнал для навигатора назад
|
final bool isSuccess;
|
||||||
|
|
||||||
DetailsContentState({
|
DetailsContentState({
|
||||||
required this.subscription,
|
required this.subscription,
|
||||||
required this.selectedPeriod,
|
required this.selectedPeriod,
|
||||||
|
required this.selectedCard,
|
||||||
|
this.cards = const [],
|
||||||
this.isAgreed = false,
|
this.isAgreed = false,
|
||||||
this.isAlreadyPurchased = false,
|
this.isAlreadyPurchased = false,
|
||||||
this.isSuccess = false,
|
this.isSuccess = false,
|
||||||
@@ -28,6 +34,7 @@ class DetailsContentState extends SubscriptionDetailsState {
|
|||||||
|
|
||||||
DetailsContentState copyWith({
|
DetailsContentState copyWith({
|
||||||
SubscriptionPeriod? selectedPeriod,
|
SubscriptionPeriod? selectedPeriod,
|
||||||
|
PaymentCard? selectedCard,
|
||||||
bool? isAgreed,
|
bool? isAgreed,
|
||||||
bool? isAlreadyPurchased,
|
bool? isAlreadyPurchased,
|
||||||
bool? isSuccess,
|
bool? isSuccess,
|
||||||
@@ -35,9 +42,10 @@ class DetailsContentState extends SubscriptionDetailsState {
|
|||||||
return DetailsContentState(
|
return DetailsContentState(
|
||||||
subscription: this.subscription,
|
subscription: this.subscription,
|
||||||
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
selectedPeriod: selectedPeriod ?? this.selectedPeriod,
|
||||||
|
selectedCard: selectedCard ?? this.selectedCard,
|
||||||
isAgreed: isAgreed ?? this.isAgreed,
|
isAgreed: isAgreed ?? this.isAgreed,
|
||||||
isAlreadyPurchased: isAlreadyPurchased ?? this.isAlreadyPurchased,
|
isAlreadyPurchased: isAlreadyPurchased ?? this.isAlreadyPurchased,
|
||||||
isSuccess: isSuccess ?? this.isSuccess,
|
isSuccess: isSuccess ?? this.isSuccess,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ class TariffSheetState {
|
|||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
final PaymentCard? selectedCard;
|
final PaymentCard? selectedCard;
|
||||||
final bool isInsurance;
|
final bool isInsurance;
|
||||||
final int userBalance;
|
final double userBalance;
|
||||||
final bool useBalance ;
|
final bool useBalance ;
|
||||||
|
|
||||||
TariffSheetState({
|
TariffSheetState({
|
||||||
@@ -29,7 +29,7 @@ class TariffSheetState {
|
|||||||
PaymentCard? selectedCard,
|
PaymentCard? selectedCard,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
bool? isInsurance,
|
bool? isInsurance,
|
||||||
int? userBalance,
|
double? userBalance,
|
||||||
bool? useBalance,
|
bool? useBalance,
|
||||||
|
|
||||||
}) => TariffSheetState(
|
}) => TariffSheetState(
|
||||||
|
|||||||
@@ -1,40 +1,55 @@
|
|||||||
import 'package:be_happy/domain/entities/certificate.dart';
|
import 'package:be_happy/domain/entities/certificate.dart';
|
||||||
import 'package:be_happy/domain/entities/payment_card.dart';
|
import 'package:be_happy/domain/entities/payment_card.dart';
|
||||||
|
|
||||||
import '../../domain/entities/top_up_tariff.dart';
|
sealed class TopUpState {
|
||||||
|
const TopUpState();
|
||||||
|
}
|
||||||
|
|
||||||
class TopUpState {
|
class TopUpLoading extends TopUpState {
|
||||||
|
const TopUpLoading();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TopUpLoaded extends TopUpState {
|
||||||
final List<Certificate> certificates;
|
final List<Certificate> certificates;
|
||||||
final List<PaymentCard> cards;
|
final List<PaymentCard> cards;
|
||||||
final Certificate? selectedTariff;
|
final Certificate? selectedTariff;
|
||||||
final PaymentCard? selectedCard;
|
final PaymentCard? selectedCard;
|
||||||
final bool isLoading;
|
|
||||||
final bool isAgreed;
|
final bool isAgreed;
|
||||||
|
final bool isProcessingPayment;
|
||||||
|
|
||||||
TopUpState({
|
const TopUpLoaded({
|
||||||
this.certificates = const [],
|
this.certificates = const [],
|
||||||
this.cards = const [],
|
this.cards = const [],
|
||||||
this.selectedTariff,
|
this.selectedTariff,
|
||||||
this.selectedCard,
|
this.selectedCard,
|
||||||
this.isLoading = false,
|
|
||||||
this.isAgreed = false,
|
this.isAgreed = false,
|
||||||
|
this.isProcessingPayment = false,
|
||||||
});
|
});
|
||||||
|
|
||||||
TopUpState copyWith({
|
TopUpLoaded copyWith({
|
||||||
List<Certificate>? certificates,
|
List<Certificate>? certificates,
|
||||||
List<PaymentCard>? cards,
|
List<PaymentCard>? cards,
|
||||||
Certificate? selectedTariff,
|
Certificate? selectedTariff,
|
||||||
PaymentCard? selectedCard,
|
PaymentCard? selectedCard,
|
||||||
bool? isLoading,
|
|
||||||
bool? isAgreed,
|
bool? isAgreed,
|
||||||
|
bool? isProcessingPayment,
|
||||||
}) {
|
}) {
|
||||||
return TopUpState(
|
return TopUpLoaded(
|
||||||
certificates: certificates ?? this.certificates,
|
certificates: certificates ?? this.certificates,
|
||||||
cards: cards ?? this.cards,
|
cards: cards ?? this.cards,
|
||||||
selectedTariff: selectedTariff ?? this.selectedTariff,
|
selectedTariff: selectedTariff ?? this.selectedTariff,
|
||||||
selectedCard: selectedCard ?? this.selectedCard,
|
selectedCard: selectedCard ?? this.selectedCard,
|
||||||
isLoading: isLoading ?? this.isLoading,
|
|
||||||
isAgreed: isAgreed ?? this.isAgreed,
|
isAgreed: isAgreed ?? this.isAgreed,
|
||||||
|
isProcessingPayment: isProcessingPayment ?? this.isProcessingPayment,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class TopUpPaymentSuccess extends TopUpState {
|
||||||
|
const TopUpPaymentSuccess();
|
||||||
|
}
|
||||||
|
|
||||||
|
class TopUpPaymentFailure extends TopUpState {
|
||||||
|
final String errorMessage;
|
||||||
|
const TopUpPaymentFailure(this.errorMessage);
|
||||||
}
|
}
|
||||||
@@ -52,17 +52,17 @@ class AddCardBloc extends Bloc<AddCardEvent, AddCardState> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
final result = await addPaymentCardUsecase(
|
final result = await addPaymentCardUsecase(
|
||||||
cardNumber: state.cardNumber.replaceAll(' ', ''),
|
/*cardNumber: state.cardNumber.replaceAll(' ', ''),
|
||||||
expiryMonth: expiryParts[0],
|
expiryMonth: expiryParts[0],
|
||||||
expiryYear: expiryParts[1],
|
expiryYear: expiryParts[1],
|
||||||
cardHolder: state.cardHolder.trim(),
|
cardHolder: state.cardHolder.trim(),
|
||||||
cvv: state.cvv,
|
cvv: state.cvv,*/
|
||||||
);
|
);
|
||||||
|
|
||||||
if (result is Success) {
|
if (result is Success) {
|
||||||
emit(state.copyWith(status: AddCardStatus.success));
|
emit(state.copyWith(status: AddCardStatus.success));
|
||||||
} else if (result is Failure) {
|
} else if (result is Failure) {
|
||||||
final failure = result.failure;
|
/*final failure = result.failure;
|
||||||
|
|
||||||
String errorMessage = 'Ошибка добавления карты';
|
String errorMessage = 'Ошибка добавления карты';
|
||||||
|
|
||||||
@@ -72,12 +72,12 @@ class AddCardBloc extends Bloc<AddCardEvent, AddCardState> {
|
|||||||
errorMessage = 'Доступ заблокирован';
|
errorMessage = 'Доступ заблокирован';
|
||||||
} else if (failure is UnknownFailure) {
|
} else if (failure is UnknownFailure) {
|
||||||
errorMessage = failure.message ?? 'Неизвестная ошибка';
|
errorMessage = failure.message ?? 'Неизвестная ошибка';
|
||||||
}
|
}*/
|
||||||
|
|
||||||
emit(state.copyWith(
|
/* emit(state.copyWith(
|
||||||
status: AddCardStatus.failure,
|
status: AddCardStatus.failure,
|
||||||
errorMessage: errorMessage,
|
errorMessage: errorMessage,
|
||||||
));
|
));*/
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ class PhoneAuthBloc extends Bloc<PhoneAuthEvent, PhoneAuthState> {
|
|||||||
|
|
||||||
PhoneAuthBloc(this._loginUseCase) : super(PhoneAuthState.initial()) {
|
PhoneAuthBloc(this._loginUseCase) : super(PhoneAuthState.initial()) {
|
||||||
on<PhoneChanged>((event, emit) {
|
on<PhoneChanged>((event, emit) {
|
||||||
emit(state.copyWith(phone: event.phone));
|
emit(state.copyWith(
|
||||||
|
phone: event.phone,
|
||||||
|
isSuccess: false,
|
||||||
|
error: null,
|
||||||
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
on<IsAdultChanged>((event, emit) {
|
on<IsAdultChanged>((event, emit) {
|
||||||
|
|||||||
@@ -264,9 +264,10 @@ class MapBloc extends Bloc<ScooterEvent, ScooterState> {
|
|||||||
FutureOr<void> _onLogoutPressed(
|
FutureOr<void> _onLogoutPressed(
|
||||||
LogoutPressed event,
|
LogoutPressed event,
|
||||||
Emitter<ScooterState> emit,
|
Emitter<ScooterState> emit,
|
||||||
) {
|
) async {
|
||||||
logoutUseCase();
|
await logoutUseCase();
|
||||||
splashBloc.add(AuthCheckRequested());
|
stopNotificationStream();
|
||||||
|
splashBloc.add(AuthLogoutRequested());
|
||||||
}
|
}
|
||||||
|
|
||||||
FutureOr<void> _onCheckUser(
|
FutureOr<void> _onCheckUser(
|
||||||
|
|||||||
@@ -9,26 +9,53 @@ class NotificationsBloc extends Bloc<NotificationsEvent, NotificationsState> {
|
|||||||
|
|
||||||
NotificationsBloc(this._getNotificationsUsecase) : super(const NotificationsState()) {
|
NotificationsBloc(this._getNotificationsUsecase) : super(const NotificationsState()) {
|
||||||
on<NotificationsFetchRequested>(_onFetchRequested);
|
on<NotificationsFetchRequested>(_onFetchRequested);
|
||||||
|
on<NotificationsLoadMoreRequested>(_onLoadMoreRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onFetchRequested(
|
Future<void> _onFetchRequested(
|
||||||
NotificationsFetchRequested event,
|
NotificationsFetchRequested event,
|
||||||
Emitter<NotificationsState> emit,
|
Emitter<NotificationsState> emit,
|
||||||
) async {
|
) async {
|
||||||
emit(state.copyWith(status: NotificationsStatus.loading));
|
if (event.page == 1) {
|
||||||
|
emit(state.copyWith(status: NotificationsStatus.loading));
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final notifications = await _getNotificationsUsecase();
|
final result = await _getNotificationsUsecase(
|
||||||
|
page: event.page,
|
||||||
|
pageSize: 100,
|
||||||
|
);
|
||||||
|
|
||||||
|
final newNotifications = event.page == 1
|
||||||
|
? result.notifications
|
||||||
|
: [...state.notifications, ...result.notifications];
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: NotificationsStatus.success,
|
status: NotificationsStatus.success,
|
||||||
notifications: notifications,
|
notifications: newNotifications,
|
||||||
|
currentPage: result.pagination.currentPage,
|
||||||
|
totalPages: result.pagination.lastPage,
|
||||||
|
hasMore: result.pagination.currentPage < result.pagination.lastPage,
|
||||||
|
isLoadingMore: false,
|
||||||
));
|
));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
dev.log('NotificationsBloc: Ошибка загрузки: $e');
|
dev.log('NotificationsBloc: Ошибка загрузки: $e');
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: NotificationsStatus.failure,
|
status: event.page == 1 ? NotificationsStatus.failure : NotificationsStatus.success,
|
||||||
errorMessage: e.toString(),
|
errorMessage: event.page == 1 ? e.toString() : null,
|
||||||
|
isLoadingMore: false,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadMoreRequested(
|
||||||
|
NotificationsLoadMoreRequested event,
|
||||||
|
Emitter<NotificationsState> emit,
|
||||||
|
) async {
|
||||||
|
if (!state.hasMore || state.isLoadingMore) return;
|
||||||
|
|
||||||
|
emit(state.copyWith(isLoadingMore: true));
|
||||||
|
final nextPage = state.currentPage + 1;
|
||||||
|
add(NotificationsFetchRequested(page: nextPage));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,17 +1,26 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../core/result.dart';
|
import '../../core/result.dart';
|
||||||
import '../../domain/entities/payment_card.dart';
|
import '../../domain/entities/payment_card.dart';
|
||||||
|
import '../../domain/usecase/add_payment_card_usecase.dart';
|
||||||
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
import '../../domain/usecase/get_payment_cards_usecase.dart';
|
||||||
|
import '../../domain/usecase/get_profile_usecase.dart';
|
||||||
|
import '../../domain/usecase/set_main_payment_card_usecase.dart';
|
||||||
import '../event/payment_method_sheet_event.dart';
|
import '../event/payment_method_sheet_event.dart';
|
||||||
import '../state/payment_method_sheet_state.dart';
|
import '../state/payment_method_sheet_state.dart';
|
||||||
|
|
||||||
class PaymentMethodSheetBloc extends Bloc<PaymentMethodSheetEvent, PaymentMethodSheetState> {
|
class PaymentMethodSheetBloc extends Bloc<PaymentMethodSheetEvent, PaymentMethodSheetState> {
|
||||||
final GetPaymentCardsUsecase _getPaymentCardsUsecase;
|
final GetPaymentCardsUsecase _getPaymentCardsUsecase;
|
||||||
|
final GetProfileUseCase _getProfileUseCase;
|
||||||
|
final SetMainPaymentCardUsecase _setMainPaymentCardUsecase;
|
||||||
|
final AddPaymentCardUsecase _addPaymentCardUsecase;
|
||||||
|
|
||||||
PaymentMethodSheetBloc(this._getPaymentCardsUsecase)
|
PaymentMethodSheetBloc(this._getPaymentCardsUsecase, this._setMainPaymentCardUsecase, this._getProfileUseCase, this._addPaymentCardUsecase)
|
||||||
: super(PaymentMethodSheetState(status: PaymentMethodSheetStatus.initial)) {
|
: super(PaymentMethodSheetState(status: PaymentMethodSheetStatus.initial)) {
|
||||||
on<PaymentMethodSheetStarted>(_onStarted);
|
on<PaymentMethodSheetStarted>(_onStarted);
|
||||||
|
on<SelectPaymentCardRequested>(_onSelectCard);
|
||||||
|
on<AddPaymentCardRequested>(_onAddPaymentCardRequested);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onStarted(
|
Future<void> _onStarted(
|
||||||
@@ -22,7 +31,57 @@ class PaymentMethodSheetBloc extends Bloc<PaymentMethodSheetEvent, PaymentMethod
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
final result = await _getPaymentCardsUsecase();
|
final result = await _getPaymentCardsUsecase();
|
||||||
|
final profile = await _getProfileUseCase();
|
||||||
|
|
||||||
|
|
||||||
|
if (result is Success<List<PaymentCard>>) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodSheetStatus.success,
|
||||||
|
balance: profile.balance,
|
||||||
|
cards: result.data ?? [],
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodSheetStatus.failure,
|
||||||
|
errorMessage: 'Failed to load payment cards',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodSheetStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Future<void> _onSelectCard(
|
||||||
|
SelectPaymentCardRequested event,
|
||||||
|
Emitter<PaymentMethodSheetState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: PaymentMethodSheetStatus.loading));
|
||||||
|
|
||||||
|
final result = await _setMainPaymentCardUsecase(event.card.id);
|
||||||
|
|
||||||
|
if (result is Success<void>) {
|
||||||
|
final cardsResult = await _getPaymentCardsUsecase();
|
||||||
|
if (cardsResult is Success<List<PaymentCard>>) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodSheetStatus.success,
|
||||||
|
cards: cardsResult.data ?? [],
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(status: PaymentMethodSheetStatus.failure, errorMessage: 'Ошибка обновления списка карт'));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodSheetStatus.failure,
|
||||||
|
errorMessage: 'Не удалось установить карту основной',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _loadCards(Emitter<PaymentMethodSheetState> emit) async {
|
||||||
|
try {
|
||||||
|
final result = await _getPaymentCardsUsecase();
|
||||||
if (result is Success<List<PaymentCard>>) {
|
if (result is Success<List<PaymentCard>>) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: PaymentMethodSheetStatus.success,
|
status: PaymentMethodSheetStatus.success,
|
||||||
@@ -41,4 +100,51 @@ class PaymentMethodSheetBloc extends Bloc<PaymentMethodSheetEvent, PaymentMethod
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onAddPaymentCardRequested(
|
||||||
|
AddPaymentCardRequested event,
|
||||||
|
Emitter<PaymentMethodSheetState> emit,
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: PaymentMethodSheetStatus.loading));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await _addPaymentCardUsecase();
|
||||||
|
|
||||||
|
if (result is Success<Map<String, dynamic>>) {
|
||||||
|
final formUrlString = result.data?['formUrl'] as String?;
|
||||||
|
|
||||||
|
if (formUrlString != null && formUrlString.isNotEmpty) {
|
||||||
|
final url = Uri.parse(formUrlString);
|
||||||
|
|
||||||
|
if (await canLaunchUrl(url)) {
|
||||||
|
await launchUrl(
|
||||||
|
url,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
emit(state.copyWith(status: PaymentMethodSheetStatus.success));
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodSheetStatus.failure,
|
||||||
|
errorMessage: 'Не удалось открыть ссылку для оплаты',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodSheetStatus.failure,
|
||||||
|
errorMessage: 'Ссылка для привязки карты не получена от сервера',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else if (result is Failure) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodSheetStatus.failure,
|
||||||
|
errorMessage: 'Ошибка при добавлении карты',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodSheetStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:be_happy/domain/usecase/add_payment_card_usecase.dart';
|
||||||
import 'package:be_happy/domain/usecase/get_profile_usecase.dart';
|
import 'package:be_happy/domain/usecase/get_profile_usecase.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
import '../../core/result.dart';
|
import '../../core/result.dart';
|
||||||
import '../../core/failures.dart';
|
import '../../core/failures.dart';
|
||||||
@@ -15,16 +19,19 @@ class PaymentMethodsBloc extends Bloc<PaymentMethodsEvent, PaymentMethodsState>
|
|||||||
final RemovePaymentCardUsecase _removePaymentCardUsecase;
|
final RemovePaymentCardUsecase _removePaymentCardUsecase;
|
||||||
final SetMainPaymentCardUsecase _setMainPaymentCardUsecase;
|
final SetMainPaymentCardUsecase _setMainPaymentCardUsecase;
|
||||||
final GetProfileUseCase _getProfileUseCase;
|
final GetProfileUseCase _getProfileUseCase;
|
||||||
|
final AddPaymentCardUsecase _addPaymentCardUsecase;
|
||||||
|
|
||||||
PaymentMethodsBloc(
|
PaymentMethodsBloc(
|
||||||
this._getPaymentCardsUsecase,
|
this._getPaymentCardsUsecase,
|
||||||
this._removePaymentCardUsecase,
|
this._removePaymentCardUsecase,
|
||||||
this._setMainPaymentCardUsecase,
|
this._setMainPaymentCardUsecase,
|
||||||
this._getProfileUseCase,
|
this._getProfileUseCase,
|
||||||
|
this._addPaymentCardUsecase,
|
||||||
) : super(PaymentMethodsState(status: PaymentMethodsStatus.initial)) {
|
) : super(PaymentMethodsState(status: PaymentMethodsStatus.initial)) {
|
||||||
on<PaymentMethodsStarted>(_onStarted);
|
on<PaymentMethodsStarted>(_onStarted);
|
||||||
on<PaymentMethodsDeleteCard>(_onDeleteCard);
|
on<PaymentMethodsDeleteCard>(_onDeleteCard);
|
||||||
on<PaymentMethodsSetMainCard>(_onSetMainCard);
|
on<PaymentMethodsSetMainCard>(_onSetMainCard);
|
||||||
|
on<AddPayemntCardPressed>(_onAddPayemntCardPressed);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onStarted(
|
Future<void> _onStarted(
|
||||||
@@ -39,6 +46,8 @@ class PaymentMethodsBloc extends Bloc<PaymentMethodsEvent, PaymentMethodsState>
|
|||||||
|
|
||||||
|
|
||||||
if (result is Success<List<PaymentCard>>) {
|
if (result is Success<List<PaymentCard>>) {
|
||||||
|
|
||||||
|
print("BALANCE: ${profile.balance} BYN");
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: PaymentMethodsStatus.success,
|
status: PaymentMethodsStatus.success,
|
||||||
cards: result.data ?? [],
|
cards: result.data ?? [],
|
||||||
@@ -136,4 +145,54 @@ class PaymentMethodsBloc extends Bloc<PaymentMethodsEvent, PaymentMethodsState>
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
FutureOr<void> _onAddPayemntCardPressed(
|
||||||
|
AddPayemntCardPressed event,
|
||||||
|
Emitter<PaymentMethodsState> emit
|
||||||
|
) async {
|
||||||
|
emit(state.copyWith(status: PaymentMethodsStatus.loading));
|
||||||
|
|
||||||
|
try {
|
||||||
|
final result = await _addPaymentCardUsecase();
|
||||||
|
|
||||||
|
if (result is Success<Map<String, dynamic>>) {
|
||||||
|
final formUrlString = result.data?['formUrl'] as String?;
|
||||||
|
|
||||||
|
if (formUrlString != null && formUrlString.isNotEmpty) {
|
||||||
|
final url = Uri.parse(formUrlString);
|
||||||
|
|
||||||
|
if (await canLaunchUrl(url)) {
|
||||||
|
launchUrl(
|
||||||
|
url,
|
||||||
|
mode: LaunchMode.externalApplication,
|
||||||
|
);
|
||||||
|
emit(state.copyWith(status: PaymentMethodsStatus.success));
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodsStatus.failure,
|
||||||
|
errorMessage: 'Не удалось открыть ссылку для оплаты',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodsStatus.failure,
|
||||||
|
errorMessage: 'Ссылка для привязки карты не получена от сервера',
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if (result is Failure) {
|
||||||
|
String errorMessage = 'Ошибка при добавлении карты';
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodsStatus.failure,
|
||||||
|
errorMessage: errorMessage,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
status: PaymentMethodsStatus.failure,
|
||||||
|
errorMessage: e.toString(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class ReservedRideBloc extends Bloc<ReservedRideEvent, ReservedRideState> {
|
|||||||
|
|
||||||
final result = await _cancelRideUsecase(event.orderId);
|
final result = await _cancelRideUsecase(event.orderId);
|
||||||
|
|
||||||
if (result is Success<ScooterOrder>) {
|
if (result is Success) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: ReservedRideStatus.success,
|
status: ReservedRideStatus.success,
|
||||||
rideCancelled: true,
|
rideCancelled: true,
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ class SendPhotoBloc extends Bloc<SendPhotoEvent, SendPhotoState> {
|
|||||||
|
|
||||||
switch (result) {
|
switch (result) {
|
||||||
case Success<List<int>>():
|
case Success<List<int>>():
|
||||||
// ✅ Защита: если data null или пустой — не вызываем finish
|
|
||||||
if (result.data == null || result.data!.isEmpty) {
|
if (result.data == null || result.data!.isEmpty) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: SendPhotoStatus.failure,
|
status: SendPhotoStatus.failure,
|
||||||
@@ -49,20 +48,19 @@ class SendPhotoBloc extends Bloc<SendPhotoEvent, SendPhotoState> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ✅ Передаём два параметра в UseCase
|
|
||||||
final finishResult = await _finishRideUsecase(
|
final finishResult = await _finishRideUsecase(
|
||||||
event.orderId,
|
event.orderId,
|
||||||
result.data! // ✅ ! т.к. проверили выше
|
result.data!
|
||||||
);
|
);
|
||||||
|
|
||||||
switch (finishResult) {
|
switch (finishResult) {
|
||||||
case Success<ScooterOrder>():
|
case Success():
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: SendPhotoStatus.success,
|
status: SendPhotoStatus.success,
|
||||||
recievedPhotoIds: result.data,
|
recievedPhotoIds: result.data,
|
||||||
));
|
));
|
||||||
case Failure():
|
case Failure():
|
||||||
print("❌ FINISH ERROR: ${finishResult.failure.message}");
|
print("FINISH ERROR: ${finishResult.failure.message}");
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: SendPhotoStatus.failure,
|
status: SendPhotoStatus.failure,
|
||||||
errorMessage: 'Ошибка завершения поездки',
|
errorMessage: 'Ошибка завершения поездки',
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ class SplashBloc extends Bloc<SplashEvent, SplashState> {
|
|||||||
on<PinVerificationSuccess>((event, emit) {
|
on<PinVerificationSuccess>((event, emit) {
|
||||||
emit(AuthPinVerified());
|
emit(AuthPinVerified());
|
||||||
});
|
});
|
||||||
|
on<AuthLogoutRequested>((event, emit) {
|
||||||
|
emit(AuthUnauthenticated());
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAuthCheckRequested(
|
Future<void> _onAuthCheckRequested(
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import 'package:be_happy/core/result.dart';
|
import 'package:be_happy/core/result.dart';
|
||||||
|
import 'package:be_happy/domain/entities/payment_card.dart';
|
||||||
import 'package:be_happy/domain/entities/subscription.dart';
|
import 'package:be_happy/domain/entities/subscription.dart';
|
||||||
import 'package:be_happy/domain/usecase/activate_subscription_usecase.dart';
|
import 'package:be_happy/domain/usecase/activate_subscription_usecase.dart';
|
||||||
|
import 'package:be_happy/domain/usecase/get_payment_cards_usecase.dart';
|
||||||
import 'package:be_happy/domain/usecase/get_subscription_by_id_usecase.dart';
|
import 'package:be_happy/domain/usecase/get_subscription_by_id_usecase.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
@@ -12,27 +14,35 @@ import '../state/susbcription_details_state.dart';
|
|||||||
class SubscriptionDetailsBloc
|
class SubscriptionDetailsBloc
|
||||||
extends Bloc<SubscriptionDetailsEvent, SubscriptionDetailsState> {
|
extends Bloc<SubscriptionDetailsEvent, SubscriptionDetailsState> {
|
||||||
final GetSubscriptionByIdUsecase getSubscriptionByIdUsecase;
|
final GetSubscriptionByIdUsecase getSubscriptionByIdUsecase;
|
||||||
|
final GetPaymentCardsUsecase getPaymentCardsUsecase;
|
||||||
final ActivateSubscriptionUsecase activateSubscriptionUsecase;
|
final ActivateSubscriptionUsecase activateSubscriptionUsecase;
|
||||||
final GetClientSubscriptionsUsecase getClientSubscriptionsUsecase;
|
final GetClientSubscriptionsUsecase getClientSubscriptionsUsecase;
|
||||||
|
|
||||||
SubscriptionDetailsBloc(
|
SubscriptionDetailsBloc(
|
||||||
this.getSubscriptionByIdUsecase,
|
this.getSubscriptionByIdUsecase,
|
||||||
|
this.getPaymentCardsUsecase,
|
||||||
this.activateSubscriptionUsecase,
|
this.activateSubscriptionUsecase,
|
||||||
this.getClientSubscriptionsUsecase,
|
this.getClientSubscriptionsUsecase,
|
||||||
) : super(DetailsLoading()) {
|
) : super(DetailsLoading()) {
|
||||||
on<LoadDetailsEvent>((event, emit) async {
|
on<LoadDetailsEvent>((event, emit) async {
|
||||||
emit(DetailsLoading());
|
emit(DetailsLoading());
|
||||||
try {
|
try {
|
||||||
final result = await getSubscriptionByIdUsecase(event.subscriptionId);
|
final cardsResult = await getPaymentCardsUsecase();
|
||||||
|
|
||||||
|
final subsResult = await getSubscriptionByIdUsecase(
|
||||||
|
event.subscriptionId,
|
||||||
|
);
|
||||||
|
|
||||||
final clientSubsResult = await getClientSubscriptionsUsecase();
|
final clientSubsResult = await getClientSubscriptionsUsecase();
|
||||||
bool isPurchased = false;
|
bool isPurchased = false;
|
||||||
|
|
||||||
switch (clientSubsResult) {
|
switch (clientSubsResult) {
|
||||||
case Success<List<ClientSubscription>>():
|
case Success<List<ClientSubscription>>():
|
||||||
isPurchased = clientSubsResult.data?.any(
|
isPurchased =
|
||||||
|
clientSubsResult.data?.any(
|
||||||
(element) => element.subscriptionId == event.subscriptionId,
|
(element) => element.subscriptionId == event.subscriptionId,
|
||||||
) ?? false;
|
) ??
|
||||||
|
false;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case Failure<List<ClientSubscription>>():
|
case Failure<List<ClientSubscription>>():
|
||||||
@@ -40,16 +50,30 @@ class SubscriptionDetailsBloc
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (result) {
|
switch (subsResult) {
|
||||||
case Success<Subscription>():
|
case Success<Subscription>():
|
||||||
final sub = result.data;
|
final sub = subsResult.data;
|
||||||
if (sub == null) return;
|
if (sub == null) return;
|
||||||
|
|
||||||
|
List<PaymentCard> loadedCards = [];
|
||||||
|
PaymentCard? defaultCard;
|
||||||
|
|
||||||
|
if (cardsResult is Success<List<PaymentCard>>) {
|
||||||
|
loadedCards = cardsResult.data ?? [];
|
||||||
|
if (loadedCards.isNotEmpty) {
|
||||||
|
defaultCard = loadedCards.firstWhere(
|
||||||
|
(element) => element.isMain,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
emit(
|
emit(
|
||||||
DetailsContentState(
|
DetailsContentState(
|
||||||
subscription: sub,
|
subscription: sub,
|
||||||
selectedPeriod: sub.options.first,
|
selectedPeriod: sub.options.first,
|
||||||
isAlreadyPurchased: isPurchased,
|
isAlreadyPurchased: isPurchased,
|
||||||
|
cards: loadedCards,
|
||||||
|
selectedCard: defaultCard ?? loadedCards.first,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
break;
|
break;
|
||||||
@@ -70,6 +94,12 @@ class SubscriptionDetailsBloc
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
on<SelectCardEvent>((event, emit) {
|
||||||
|
if (state is DetailsContentState) {
|
||||||
|
emit((state as DetailsContentState).copyWith(selectedCard: event.card));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
on<ToggleAgreementEvent>((event, emit) {
|
on<ToggleAgreementEvent>((event, emit) {
|
||||||
if (state is DetailsContentState) {
|
if (state is DetailsContentState) {
|
||||||
emit((state as DetailsContentState).copyWith(isAgreed: event.value));
|
emit((state as DetailsContentState).copyWith(isAgreed: event.value));
|
||||||
@@ -79,16 +109,18 @@ class SubscriptionDetailsBloc
|
|||||||
on<ActivateSubscriptionPressed>((event, emit) async {
|
on<ActivateSubscriptionPressed>((event, emit) async {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case DetailsContentState contentState:
|
case DetailsContentState contentState:
|
||||||
if (!contentState.isAgreed) {
|
if (!contentState.isAgreed || contentState.selectedCard == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
final result = await activateSubscriptionUsecase(contentState.selectedPeriod.id);
|
final result = await activateSubscriptionUsecase(
|
||||||
|
contentState.selectedPeriod.id,
|
||||||
|
contentState.selectedCard.id,
|
||||||
|
);
|
||||||
|
|
||||||
if (result is Success) {
|
if (result is Success) {
|
||||||
emit(contentState.copyWith(isSuccess: true));
|
emit(contentState.copyWith(isSuccess: true));
|
||||||
} else {
|
} else {}
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -18,11 +18,11 @@ class TopUpBloc extends Bloc<TopUpEvent, TopUpState> {
|
|||||||
required this.getCertificatesUsecase,
|
required this.getCertificatesUsecase,
|
||||||
required this.purchaseCertificateUsecase,
|
required this.purchaseCertificateUsecase,
|
||||||
required this.getUserCards,
|
required this.getUserCards,
|
||||||
}) : super(TopUpState(isLoading: true)) {
|
}) : super(const TopUpLoading()) {
|
||||||
on<LoadTopUpData>((event, emit) async {
|
|
||||||
emit(state.copyWith(isLoading: true));
|
on<LoadTopUpData>((event, emit) async {
|
||||||
|
emit(const TopUpLoading());
|
||||||
|
|
||||||
// Запускаем оба запроса параллельно
|
|
||||||
final results = await Future.wait([
|
final results = await Future.wait([
|
||||||
getCertificatesUsecase(),
|
getCertificatesUsecase(),
|
||||||
getUserCards(),
|
getUserCards(),
|
||||||
@@ -34,48 +34,68 @@ class TopUpBloc extends Bloc<TopUpEvent, TopUpState> {
|
|||||||
switch ((certResult, cardResult)) {
|
switch ((certResult, cardResult)) {
|
||||||
case (Success(data: final certs), Success(data: final cards)):
|
case (Success(data: final certs), Success(data: final cards)):
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
TopUpLoaded(
|
||||||
certificates: certs,
|
certificates: certs ?? [],
|
||||||
cards: cards,
|
cards: cards ?? [],
|
||||||
selectedTariff: certs!.length > 1
|
selectedTariff: certs != null && certs.length > 1
|
||||||
? certs[1]
|
? certs[1]
|
||||||
: (certs.isNotEmpty ? certs.first : null),
|
: (certs != null && certs.isNotEmpty ? certs.first : null),
|
||||||
|
selectedCard: cards == null || cards.isEmpty
|
||||||
selectedCard: cards!.isEmpty
|
|
||||||
? null
|
? null
|
||||||
: cards.firstWhere(
|
: cards.firstWhere(
|
||||||
(c) => c.isMain,
|
(c) => c.isMain,
|
||||||
orElse: () => cards.first,
|
orElse: () => cards.first,
|
||||||
),
|
),
|
||||||
isLoading: false,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
default:
|
||||||
case (
|
emit(const TopUpPaymentFailure('Не удалось загрузить данные'));
|
||||||
Success<List<Certificate>>(data: null),
|
}
|
||||||
Failure<List<PaymentCard>>(),
|
});
|
||||||
):
|
|
||||||
// TODO: Handle this case.
|
on<SelectCertificate>((event, emit) {
|
||||||
throw UnimplementedError();
|
if (state is TopUpLoaded) {
|
||||||
case (
|
final currentState = state as TopUpLoaded;
|
||||||
Success<List<Certificate>>(data: [...]),
|
emit(currentState.copyWith(selectedTariff: event.certificate));
|
||||||
Failure<List<PaymentCard>>(),
|
}
|
||||||
):
|
});
|
||||||
// TODO: Handle this case.
|
|
||||||
throw UnimplementedError();
|
on<SelectCard>((event, emit) {
|
||||||
case (Failure<List<Certificate>>(), _):
|
if (state is TopUpLoaded) {
|
||||||
// TODO: Handle this case.
|
final currentState = state as TopUpLoaded;
|
||||||
throw UnimplementedError();
|
emit(currentState.copyWith(selectedCard: event.card));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
on<ToggleAgreement>((event, emit) {
|
||||||
|
if (state is TopUpLoaded) {
|
||||||
|
final currentState = state as TopUpLoaded;
|
||||||
|
emit(currentState.copyWith(isAgreed: event.value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
on<TopUpButtonPressed>((event, emit) async {
|
||||||
|
if (state is TopUpLoaded) {
|
||||||
|
final currentState = state as TopUpLoaded;
|
||||||
|
|
||||||
|
if (currentState.isAgreed &&
|
||||||
|
currentState.selectedTariff != null &&
|
||||||
|
currentState.selectedCard != null) {
|
||||||
|
|
||||||
|
emit(currentState.copyWith(isProcessingPayment: true));
|
||||||
|
|
||||||
|
final result = await purchaseCertificateUsecase(
|
||||||
|
certificateId: currentState.selectedTariff!.id,
|
||||||
|
cardId: currentState.selectedCard!.id,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (result is Success) {
|
||||||
|
emit(const TopUpPaymentSuccess());
|
||||||
|
} else {
|
||||||
|
emit(const TopUpPaymentFailure('Ошибка при оплате'));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
on<SelectCertificate>(
|
|
||||||
(event, emit) => emit(state.copyWith(selectedTariff: event.certificate)),
|
|
||||||
);
|
|
||||||
on<SelectCard>(
|
|
||||||
(event, emit) => emit(state.copyWith(selectedCard: event.card)),
|
|
||||||
);
|
|
||||||
on<ToggleAgreement>(
|
|
||||||
(event, emit) => emit(state.copyWith(isAgreed: event.value)),
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,35 @@
|
|||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
|
import 'package:be_happy/core/failures.dart';
|
||||||
|
import 'package:be_happy/core/result.dart';
|
||||||
|
import 'package:be_happy/presentation/viewmodel/splash_bloc.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
|
import '../../domain/usecase/verify_code_usecase.dart';
|
||||||
|
import '../event/spalsh_event.dart';
|
||||||
|
import '../event/verify_code_event.dart';
|
||||||
|
import '../state/verify_code_state.dart';
|
||||||
|
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:be_happy/core/failures.dart';
|
import 'package:be_happy/core/failures.dart';
|
||||||
import 'package:be_happy/core/result.dart';
|
import 'package:be_happy/core/result.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
|
||||||
import '../../domain/usecase/verify_code_usecase.dart';
|
import '../../domain/usecase/verify_code_usecase.dart';
|
||||||
|
import '../../domain/usecase/login_usecase.dart'; // 🔹 Импортируем LoginUseCase
|
||||||
import '../event/verify_code_event.dart';
|
import '../event/verify_code_event.dart';
|
||||||
import '../state/verify_code_state.dart';
|
import '../state/verify_code_state.dart';
|
||||||
|
|
||||||
class VerifyCodeBloc extends Bloc<VerifyCodeEvent, VerifyCodeState> {
|
class VerifyCodeBloc extends Bloc<VerifyCodeEvent, VerifyCodeState> {
|
||||||
final VerifyCodeUseCase _verifyCodeUseCase;
|
final VerifyCodeUseCase _verifyCodeUseCase;
|
||||||
|
final LoginUseCase _loginUseCase;
|
||||||
|
final SplashBloc _splashBloc;
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
|
|
||||||
VerifyCodeBloc(this._verifyCodeUseCase)
|
VerifyCodeBloc(this._verifyCodeUseCase, this._loginUseCase, this._splashBloc)
|
||||||
: super(VerifyCodeState.initial()) {
|
: super(VerifyCodeState.initial()) {
|
||||||
|
|
||||||
on<VerifyCodeStarted>((event, emit) {
|
on<VerifyCodeStarted>((event, emit) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
phoneNumber: event.phoneNumber,
|
phoneNumber: event.phoneNumber,
|
||||||
@@ -24,56 +40,91 @@ class VerifyCodeBloc extends Bloc<VerifyCodeEvent, VerifyCodeState> {
|
|||||||
});
|
});
|
||||||
|
|
||||||
on<CodeChanged>((event, emit) {
|
on<CodeChanged>((event, emit) {
|
||||||
emit(state.copyWith(code: event.code));
|
emit(state.copyWith(
|
||||||
|
code: event.code,
|
||||||
|
error: null,
|
||||||
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
on<ResendCodePressed>((event, emit) {
|
on<ResendCodePressed>((event, emit) async {
|
||||||
if (state.secondsLeft == 0) {
|
if (state.secondsLeft == 0 && !state.isSubmitting) {
|
||||||
emit(state.copyWith(secondsLeft: 60));
|
emit(state.copyWith(isSubmitting: true, error: null));
|
||||||
_startTimer();
|
|
||||||
|
try {
|
||||||
|
final newTempToken = await _loginUseCase.execute("+${state.phoneNumber}");
|
||||||
|
|
||||||
|
emit(state.copyWith(
|
||||||
|
isSubmitting: false,
|
||||||
|
tempToken: newTempToken,
|
||||||
|
secondsLeft: 60,
|
||||||
|
code: '',
|
||||||
|
error: null,
|
||||||
|
isBlocked: false,
|
||||||
|
));
|
||||||
|
|
||||||
|
_startTimer();
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
isSubmitting: false,
|
||||||
|
error: 'Не удалось повторно отправить код. Попробуйте позже.',
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
on<TimerTicked>((event, emit) {
|
||||||
|
emit(state.copyWith(secondsLeft: event.secondsLeft));
|
||||||
|
});
|
||||||
|
|
||||||
on<VerifyCodeSubmitted>(_onVerifyCodeSubmitted);
|
on<VerifyCodeSubmitted>(_onVerifyCodeSubmitted);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _startTimer() {
|
void _startTimer() {
|
||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
|
|
||||||
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
_timer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
if (state.secondsLeft <= 1) {
|
final currentSeconds = state.secondsLeft;
|
||||||
|
if (currentSeconds <= 1) {
|
||||||
timer.cancel();
|
timer.cancel();
|
||||||
emit(state.copyWith(secondsLeft: 0));
|
add(TimerTicked(0));
|
||||||
} else {
|
} else {
|
||||||
emit(state.copyWith(secondsLeft: state.secondsLeft - 1));
|
add(TimerTicked(currentSeconds - 1));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onVerifyCodeSubmitted(VerifyCodeSubmitted event,
|
Future<void> _onVerifyCodeSubmitted(VerifyCodeSubmitted event,
|
||||||
Emitter<VerifyCodeState> emit,) async {
|
Emitter<VerifyCodeState> emit,) async {
|
||||||
if (state.isSubmitting || state.code.length != 6) return;
|
if (state.isSubmitting || state.code.length != 6 || state.secondsLeft == 0) return;
|
||||||
|
|
||||||
emit(state.copyWith(isSubmitting: true, error: null));
|
emit(state.copyWith(isSubmitting: true, error: null));
|
||||||
|
|
||||||
final result = await _verifyCodeUseCase.execute(state.code, state.tempToken);
|
final result = await _verifyCodeUseCase.execute(state.code, state.tempToken);
|
||||||
|
|
||||||
final newState = switch (result) {
|
final newState = switch (result) {
|
||||||
Success() => state.copyWith(
|
Success() => (() {
|
||||||
isSubmitting: false,
|
_timer?.cancel();
|
||||||
isSuccess: true,
|
|
||||||
),
|
_splashBloc.add(AuthCheckRequested());
|
||||||
|
|
||||||
|
return state.copyWith(
|
||||||
|
isSubmitting: false,
|
||||||
|
isSuccess: true,
|
||||||
|
);
|
||||||
|
})(),
|
||||||
Failure(failure: final f) => switch (f) {
|
Failure(failure: final f) => switch (f) {
|
||||||
AuthFailure(attemptsLeft: final count) => state.copyWith(
|
AuthFailure(attemptsLeft: final count) => state.copyWith(
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
attemptsLeft: count,
|
attemptsLeft: count,
|
||||||
error: 'Неверный код. Осталось попыток: $count',
|
error: 'Неверный код. Осталось попыток: $count',
|
||||||
),
|
),
|
||||||
AuthBlockFailure() => state.copyWith(
|
AuthBlockFailure() => (() {
|
||||||
isSubmitting: false,
|
_timer?.cancel();
|
||||||
isBlocked: true,
|
return state.copyWith(
|
||||||
error: 'Вы заблокированы за слишком частые попытки',
|
isSubmitting: false,
|
||||||
),
|
isBlocked: true,
|
||||||
|
error: 'Вы заблокированы за слишком частые попытки',
|
||||||
|
);
|
||||||
|
})(),
|
||||||
_ => state.copyWith(
|
_ => state.copyWith(
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
error: 'Произошла ошибка. Попробуйте позже',
|
error: 'Произошла ошибка. Попробуйте позже',
|
||||||
@@ -89,4 +140,4 @@ class VerifyCodeBloc extends Bloc<VerifyCodeEvent, VerifyCodeState> {
|
|||||||
_timer?.cancel();
|
_timer?.cancel();
|
||||||
return super.close();
|
return super.close();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
236
pubspec.lock
236
pubspec.lock
@@ -29,18 +29,18 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: async
|
name: async
|
||||||
sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb"
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.13.0"
|
version: "2.13.1"
|
||||||
bloc:
|
bloc:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: bloc
|
name: bloc
|
||||||
sha256: a48653a82055a900b88cd35f92429f068c5a8057ae9b136d197b3d56c57efb81
|
sha256: e03b235924e4f509c27b5d6b2f949200e0a91149a9818b4f65eeb56662b75413
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "9.2.0"
|
version: "9.2.1"
|
||||||
boolean_selector:
|
boolean_selector:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -61,10 +61,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: characters
|
name: characters
|
||||||
sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.4.0"
|
version: "1.4.1"
|
||||||
checked_yaml:
|
checked_yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -89,6 +89,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.1.2"
|
version: "1.1.2"
|
||||||
|
code_assets:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: code_assets
|
||||||
|
sha256: bf394f466ba9205f1812a0433b392d6af280f155f56651eda7c18cc32ed493b8
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.1"
|
||||||
collection:
|
collection:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -101,10 +109,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: cross_file
|
name: cross_file
|
||||||
sha256: "28bb3ae56f117b5aec029d702a90f57d285cd975c3c5c281eaca38dbc47c5937"
|
sha256: "92c9c43c383bfa1c32079d3bc492d55d6d4318044b7b47edaff8971cbb555c51"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.3.5+2"
|
version: "0.3.5+4"
|
||||||
crypto:
|
crypto:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -125,10 +133,10 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: cupertino_icons
|
name: cupertino_icons
|
||||||
sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6
|
sha256: "41e005c33bd814be4d3096aff55b1908d419fde52ca656c8c47719ec745873cd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.8"
|
version: "1.0.9"
|
||||||
device_info_plus:
|
device_info_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -149,26 +157,26 @@ packages:
|
|||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: dio
|
name: dio
|
||||||
sha256: aff32c08f92787a557dd5c0145ac91536481831a01b4648136373cddb0e64f8c
|
sha256: ea2bad3c89a27635ce2d85cce4d6b199da49a5a48ec77b03e45b65a3b90922b0
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "5.9.2"
|
version: "5.10.0"
|
||||||
dio_web_adapter:
|
dio_web_adapter:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: dio_web_adapter
|
name: dio_web_adapter
|
||||||
sha256: "2f9e64323a7c3c7ef69567d5c800424a11f8337b8b228bad02524c9fb3c1f340"
|
sha256: dd58dc3861eb36edb13b217efc006a1c21e5bbc341de8c229b85634fa5e362e4
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.2.0"
|
||||||
equatable:
|
equatable:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: equatable
|
name: equatable
|
||||||
sha256: "3e0141505477fd8ad55d6eb4e7776d3fe8430be8e497ccb1521370c3f21a3e2b"
|
sha256: "3bce007a596ff8b3119c45d68aaef631272537c03d30e5d4534dd24bf4c5eaa2"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.8"
|
version: "2.1.0"
|
||||||
fake_async:
|
fake_async:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -205,10 +213,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: file_selector_macos
|
name: file_selector_macos
|
||||||
sha256: "19124ff4a3d8864fdc62072b6a2ef6c222d55a3404fe14893a3c02744907b60c"
|
sha256: "5e0bbe9c312416f1787a68259ea1505b52f258c587f12920422671807c4d618a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.9.4+4"
|
version: "0.9.5"
|
||||||
file_selector_platform_interface:
|
file_selector_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -274,10 +282,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: flutter_plugin_android_lifecycle
|
name: flutter_plugin_android_lifecycle
|
||||||
sha256: c2fe1001710127dfa7da89977a08d591398370d099aacdaa6d44da7eb14b8476
|
sha256: "3854fe5e3bff0b113c658f260b90c95dea17c92db0f2addeac2e343dd9969785"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.0.31"
|
version: "2.0.35"
|
||||||
flutter_secure_storage:
|
flutter_secure_storage:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -356,18 +364,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: geolocator_apple
|
name: geolocator_apple
|
||||||
sha256: dbdd8789d5aaf14cf69f74d4925ad1336b4433a6efdf2fce91e8955dc921bf22
|
sha256: "853803d6bb1713c094e935b4a5ae5f19c0308acf81da13fa9ff84fb4c70c0b73"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.3.13"
|
version: "2.3.14"
|
||||||
geolocator_platform_interface:
|
geolocator_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: geolocator_platform_interface
|
name: geolocator_platform_interface
|
||||||
sha256: "30cb64f0b9adcc0fb36f628b4ebf4f731a2961a0ebd849f4b56200205056fe67"
|
sha256: cdb082e4f048b69da244117b7914cc60d2a8897546ffaa4f2529c786ded7aee2
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.6"
|
version: "4.2.8"
|
||||||
geolocator_web:
|
geolocator_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -400,6 +408,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "14.8.1"
|
version: "14.8.1"
|
||||||
|
hooks:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: hooks
|
||||||
|
sha256: "9a62a50b50b769a737bc0a8ff381f333529df3ab746b2f6b02e83760231455ba"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.0.2"
|
||||||
html:
|
html:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -428,26 +444,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image
|
name: image
|
||||||
sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce
|
sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.8.0"
|
version: "4.9.1"
|
||||||
image_picker:
|
image_picker:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: image_picker
|
name: image_picker
|
||||||
sha256: "784210112be18ea55f69d7076e2c656a4e24949fa9e76429fe53af0c0f4fa320"
|
sha256: d8402284df184bc05f4a2210c6c23983b0720f4cd87cbd05c5390a78af602667
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.2.1"
|
version: "1.2.3"
|
||||||
image_picker_android:
|
image_picker_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_android
|
name: image_picker_android
|
||||||
sha256: "28f3987ca0ec702d346eae1d90eda59603a2101b52f1e234ded62cff1d5cfa6e"
|
sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.13+1"
|
version: "0.8.13+19"
|
||||||
image_picker_for_web:
|
image_picker_for_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -460,10 +476,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_ios
|
name: image_picker_ios
|
||||||
sha256: eb06fe30bab4c4497bad449b66448f50edcc695f1c59408e78aa3a8059eb8f0e
|
sha256: b9c4a438a9ff4f60808c9cf0039b93a42bb6c2211ef6ebb647394b2b3fa84588
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.8.13"
|
version: "0.8.13+6"
|
||||||
image_picker_linux:
|
image_picker_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -476,10 +492,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: image_picker_macos
|
name: image_picker_macos
|
||||||
sha256: d58cd9d67793d52beefd6585b12050af0a7663c0c2a6ece0fb110a35d6955e04
|
sha256: "86f0f15a309de7e1a552c12df9ce5b59fe927e71385329355aec4776c6a8ec91"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.2.2"
|
version: "0.2.2+1"
|
||||||
image_picker_platform_interface:
|
image_picker_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -504,6 +520,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.19.0"
|
version: "0.19.0"
|
||||||
|
jni:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni
|
||||||
|
sha256: c2230682d5bc2362c1c9e8d3c7f406d9cbba23ab3f2e203a025dd47e0fb2e68f
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.0"
|
||||||
|
jni_flutter:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: jni_flutter
|
||||||
|
sha256: "8b59e590786050b1cd866677dddaf76b1ade5e7bc751abe04b86e84d379d3ba6"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.0.1"
|
||||||
js:
|
js:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -516,34 +548,34 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: json_annotation
|
name: json_annotation
|
||||||
sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1"
|
sha256: "2a743920d81b7910627f68ee2c9ac1fc0bfee32b9fc3403587d7c6791ca12f80"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "4.9.0"
|
version: "4.12.0"
|
||||||
leak_tracker:
|
leak_tracker:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker
|
name: leak_tracker
|
||||||
sha256: "6bb818ecbdffe216e81182c2f0714a2e62b593f4a4f13098713ff1685dfb6ab0"
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "10.0.9"
|
version: "11.0.2"
|
||||||
leak_tracker_flutter_testing:
|
leak_tracker_flutter_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_flutter_testing
|
name: leak_tracker_flutter_testing
|
||||||
sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.9"
|
version: "3.0.10"
|
||||||
leak_tracker_testing:
|
leak_tracker_testing:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: leak_tracker_testing
|
name: leak_tracker_testing
|
||||||
sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3"
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.0.1"
|
version: "3.0.2"
|
||||||
lints:
|
lints:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -572,26 +604,26 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: matcher
|
name: matcher
|
||||||
sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.12.17"
|
version: "0.12.19"
|
||||||
material_color_utilities:
|
material_color_utilities:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: material_color_utilities
|
name: material_color_utilities
|
||||||
sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.11.1"
|
version: "0.13.0"
|
||||||
meta:
|
meta:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: meta
|
name: meta
|
||||||
sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c
|
sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.16.0"
|
version: "1.18.0"
|
||||||
mime:
|
mime:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -616,6 +648,22 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.0.0"
|
version: "1.0.0"
|
||||||
|
objective_c:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: objective_c
|
||||||
|
sha256: "6cb691c686fa2838c6deb34980d426145c2a5d537491cb83d463c33cdbc726ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "9.4.1"
|
||||||
|
package_config:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: package_config
|
||||||
|
sha256: f096c55ebb7deb7e384101542bfba8c52696c1b56fca2eb62827989ef2353bbc
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
path:
|
path:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -628,42 +676,42 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider
|
name: path_provider
|
||||||
sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd"
|
sha256: a7f4874f987173da295a61c181b8ee71dab59b332a486b391babf26a1b884825
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.5"
|
version: "2.1.6"
|
||||||
path_provider_android:
|
path_provider_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_android
|
name: path_provider_android
|
||||||
sha256: "3b4c1fc3aa55ddc9cd4aa6759984330d5c8e66aa7702a6223c61540dc6380c37"
|
sha256: "69cbd515a62b94d32a7944f086b2f82b4ac40a1d45bebfc00813a430ab2dabcd"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.19"
|
version: "2.3.1"
|
||||||
path_provider_foundation:
|
path_provider_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_foundation
|
name: path_provider_foundation
|
||||||
sha256: "16eef174aacb07e09c351502740fa6254c165757638eba1e9116b0a781201bbd"
|
sha256: "2a376b7d6392d80cd3705782d2caa734ca4727776db0b6ec36ef3f1855197699"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.2"
|
version: "2.6.0"
|
||||||
path_provider_linux:
|
path_provider_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_linux
|
name: path_provider_linux
|
||||||
sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279
|
sha256: "58c2005f147315b11e9b4a7bc889cd5203e250cba8e3f012dae259b4972b5c16"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.2.1"
|
version: "2.2.2"
|
||||||
path_provider_platform_interface:
|
path_provider_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: path_provider_platform_interface
|
name: path_provider_platform_interface
|
||||||
sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334"
|
sha256: "484838772624c3a4b94f1e44a3e19897fee738f2d5c4ce448443b0417f7c9dda"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.2"
|
version: "2.1.3"
|
||||||
path_provider_windows:
|
path_provider_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -712,30 +760,46 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.1.5+1"
|
version: "6.1.5+1"
|
||||||
|
pub_semver:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: pub_semver
|
||||||
|
sha256: "5bfcf68ca79ef689f8990d1160781b4bad40a3bd5e5218ad4076ddb7f4081585"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
record_use:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: record_use
|
||||||
|
sha256: "2551bd8eecfe95d14ae75f6021ad0248be5c27f138c2ec12fcb52b500b3ba1ed"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.6.0"
|
||||||
shared_preferences:
|
shared_preferences:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
name: shared_preferences
|
name: shared_preferences
|
||||||
sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5"
|
sha256: c3025c5534b01739267eb7d76959bbc25a6d10f6988e1c2a3036940133dd10bf
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.3"
|
version: "2.5.5"
|
||||||
shared_preferences_android:
|
shared_preferences_android:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_android
|
name: shared_preferences_android
|
||||||
sha256: bd14436108211b0d4ee5038689a56d4ae3620fd72fd6036e113bf1345bc74d9e
|
sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.13"
|
version: "2.4.27"
|
||||||
shared_preferences_foundation:
|
shared_preferences_foundation:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_foundation
|
name: shared_preferences_foundation
|
||||||
sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03"
|
sha256: "4e7eaffc2b17ba398759f1151415869a34771ba11ebbccd1b0145472a619a64f"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.5.4"
|
version: "2.5.6"
|
||||||
shared_preferences_linux:
|
shared_preferences_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -748,10 +812,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: shared_preferences_platform_interface
|
name: shared_preferences_platform_interface
|
||||||
sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80"
|
sha256: "649dc798a33931919ea356c4305c2d1f81619ea6e92244070b520187b5140ef9"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.2"
|
||||||
shared_preferences_web:
|
shared_preferences_web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -777,10 +841,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: source_span
|
name: source_span
|
||||||
sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c"
|
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.10.1"
|
version: "1.10.2"
|
||||||
stack_trace:
|
stack_trace:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -817,10 +881,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: test_api
|
name: test_api
|
||||||
sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd
|
sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "0.7.4"
|
version: "0.7.11"
|
||||||
typed_data:
|
typed_data:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -841,18 +905,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_android
|
name: url_launcher_android
|
||||||
sha256: "81777b08c498a292d93ff2feead633174c386291e35612f8da438d6e92c4447e"
|
sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.20"
|
version: "6.3.32"
|
||||||
url_launcher_ios:
|
url_launcher_ios:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_ios
|
name: url_launcher_ios
|
||||||
sha256: d80b3f567a617cb923546034cc94bfe44eb15f989fe670b37f26abdb9d939cb7
|
sha256: "580fe5dfb51671ae38191d316e027f6b76272b026370708c2d898799750a02b0"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.3.4"
|
version: "6.4.1"
|
||||||
url_launcher_linux:
|
url_launcher_linux:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -865,10 +929,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_macos
|
name: url_launcher_macos
|
||||||
sha256: c043a77d6600ac9c38300567f33ef12b0ef4f4783a2c1f00231d2b1941fea13f
|
sha256: "368adf46f71ad3c21b8f06614adb38346f193f3a59ba8fe9a2fd74133070ba18"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.2.3"
|
version: "3.2.5"
|
||||||
url_launcher_platform_interface:
|
url_launcher_platform_interface:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -881,10 +945,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: url_launcher_web
|
name: url_launcher_web
|
||||||
sha256: "4bd2b7b4dc4d4d0b94e5babfffbca8eac1a126c7f3d6ecbc1a11013faa3abba2"
|
sha256: "85c81589622fbc87c1c683aaea164d3604a7777495a79d91e39ffcdec39ddb34"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.4.1"
|
version: "2.4.3"
|
||||||
url_launcher_windows:
|
url_launcher_windows:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -905,18 +969,18 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vector_math
|
name: vector_math
|
||||||
sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803"
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "2.1.4"
|
version: "2.2.0"
|
||||||
vm_service:
|
vm_service:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: vm_service
|
name: vm_service
|
||||||
sha256: ddfa8d30d89985b96407efce8acbdd124701f96741f2d981ca860662f1c0dc02
|
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "15.0.0"
|
version: "15.2.0"
|
||||||
web:
|
web:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -953,10 +1017,10 @@ packages:
|
|||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
name: xml
|
name: xml
|
||||||
sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025"
|
sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4"
|
||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "6.6.1"
|
version: "7.0.1"
|
||||||
yaml:
|
yaml:
|
||||||
dependency: transitive
|
dependency: transitive
|
||||||
description:
|
description:
|
||||||
@@ -974,5 +1038,5 @@ packages:
|
|||||||
source: hosted
|
source: hosted
|
||||||
version: "4.2.1"
|
version: "4.2.1"
|
||||||
sdks:
|
sdks:
|
||||||
dart: ">=3.8.1 <4.0.0"
|
dart: ">=3.12.0 <4.0.0"
|
||||||
flutter: ">=3.32.0"
|
flutter: ">=3.44.0"
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: be_happy
|
|||||||
description: ""
|
description: ""
|
||||||
publish_to: 'none'
|
publish_to: 'none'
|
||||||
|
|
||||||
version: 1.0.0+12
|
version: 1.0.0+16
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.8.1
|
sdk: ^3.8.1
|
||||||
@@ -26,7 +26,7 @@ dependencies:
|
|||||||
go_router: ^14.0.0
|
go_router: ^14.0.0
|
||||||
android_id: ^0.4.0
|
android_id: ^0.4.0
|
||||||
device_info_plus: ^12.3.0
|
device_info_plus: ^12.3.0
|
||||||
mobile_scanner: ^7.0.0
|
mobile_scanner: ^7.2.0
|
||||||
yandex_mapkit: ^4.2.1
|
yandex_mapkit: ^4.2.1
|
||||||
geolocator: ^11.0.0
|
geolocator: ^11.0.0
|
||||||
crypto: ^3.0.3
|
crypto: ^3.0.3
|
||||||
|
|||||||
Reference in New Issue
Block a user