3 Commits

Author SHA1 Message Date
bb49bbd0f7 docs(ios): update README for simulator setup and build instructions
- Translated and reorganized instructions for running the app on the iOS simulator.
- Added specific commands for terminal usage and emphasized the importance of executing commands from the correct project directory.
- Removed outdated sections and streamlined the content for clarity and relevance.

This update enhances the documentation for developers working with the iOS simulator.
2026-05-25 15:33:37 +03:00
7779803ccc chore: update dependencies and configurations for iOS build
- Updated mobile_scanner dependency version to 7.0.0 in pubspec.yaml.
- Adjusted versions of several packages in pubspec.lock.
- Added exclusion of arm64 architecture for iOS simulator in Podfile and xcconfig files to support Apple Silicon.
- Cleaned up Podfile.lock by removing unused dependencies.

This update ensures compatibility with the latest mobile_scanner and improves build stability on M1/M4 devices.
2026-05-25 15:23:13 +03:00
713cc2520b fix(ios): запуск на iOS — MapKit, ключ из xcconfig, CocoaPods
- AppDelegate: YMKMapKit до super, регистрация плагинов по шаблону Flutter 3.41
- Info.plist: YandexMapKitApiKey из $(YANDEX_MAPKIT_API_KEY)
- Debug/Release/Profile.xcconfig: ключ, include Pods и опциональный Secrets
- Podfile, workspace, pbxproj; .gitignore для Secrets.xcconfig, пример Secrets
- ios/README.md

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-05-14 10:32:18 +03:00
124 changed files with 1682 additions and 2394 deletions

View File

@@ -44,7 +44,7 @@ android {
defaultConfig { defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.sparkit.behappy" applicationId = "com.sparkit.be_happy"
// You can update the following values to match your application needs. // You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config. // For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = 26 minSdk = 26

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.0 KiB

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 26 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -20,7 +20,5 @@
<string>????</string> <string>????</string>
<key>CFBundleVersion</key> <key>CFBundleVersion</key>
<string>1.0</string> <string>1.0</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
</dict> </dict>
</plist> </plist>

View File

@@ -1 +1,6 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"
#include? "Secrets.xcconfig"
YANDEX_MAPKIT_API_KEY=a0ef1404-2650-4f28-9891-c965ecc09174
// Переопределение Pods/ML Kit: arm64 для симулятора на Apple Silicon (M1/M4)
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386

View File

@@ -0,0 +1,5 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"
#include "Generated.xcconfig"
#include? "Secrets.xcconfig"
YANDEX_MAPKIT_API_KEY=a0ef1404-2650-4f28-9891-c965ecc09174
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386

View File

@@ -1 +1,5 @@
#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
#include "Generated.xcconfig" #include "Generated.xcconfig"
#include? "Secrets.xcconfig"
YANDEX_MAPKIT_API_KEY=a0ef1404-2650-4f28-9891-c965ecc09174
EXCLUDED_ARCHS[sdk=iphonesimulator*]=i386

View File

@@ -0,0 +1,2 @@
// Локальные переопределения (опционально). Не задавайте YANDEX_MAPKIT_API_KEY плейсхолдером —
// рабочий ключ задаётся в конце Debug/Release/Profile.xcconfig после этого include.

View File

@@ -0,0 +1,3 @@
// Скопируй в ios/Flutter/Secrets.xcconfig (файл в .gitignore) для своих локальных xcconfig-переменных.
// Ключ MapKit задаётся в конце Debug/Release/Profile.xcconfig — не подставляйте туда тестовый плейсхолдер вместо реального ключа.
// https://developer.tech.yandex.ru/

48
ios/Podfile Normal file
View File

@@ -0,0 +1,48 @@
# image_picker_ios и др. требуют минимум iOS 13
platform :ios, '13.0'
# CocoaPods analytics sends network stats synchronously affecting flutter build latency.
ENV['COCOAPODS_DISABLE_STATS'] = 'true'
project 'Runner', {
'Debug' => :debug,
'Profile' => :release,
'Release' => :release,
}
def flutter_root
generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__)
unless File.exist?(generated_xcode_build_settings_path)
raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first"
end
File.foreach(generated_xcode_build_settings_path) do |line|
matches = line.match(/FLUTTER_ROOT\=(.*)/)
return matches[1].strip if matches
end
raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get"
end
require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root)
flutter_ios_podfile_setup
target 'Runner' do
use_frameworks!
flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__))
target 'RunnerTests' do
inherit! :search_paths
end
end
post_install do |installer|
installer.pods_project.targets.each do |target|
flutter_additional_ios_build_settings(target)
target.build_configurations.each do |config|
# ML Kit исключает arm64 для симулятора → сборка только x86_64.
# На M1 это ещё могло работать через Rosetta; на M4 симулятор только arm64.
config.build_settings['EXCLUDED_ARCHS[sdk=iphonesimulator*]'] = 'i386'
end
end
end

81
ios/Podfile.lock Normal file
View File

@@ -0,0 +1,81 @@
PODS:
- device_info_plus (0.0.1):
- Flutter
- Flutter (1.0.0)
- flutter_secure_storage (6.0.0):
- Flutter
- geolocator_apple (1.2.0):
- Flutter
- FlutterMacOS
- image_picker_ios (0.0.1):
- Flutter
- mobile_scanner (7.0.0):
- Flutter
- FlutterMacOS
- path_provider_foundation (0.0.1):
- Flutter
- FlutterMacOS
- shared_preferences_foundation (0.0.1):
- Flutter
- FlutterMacOS
- url_launcher_ios (0.0.1):
- Flutter
- yandex_mapkit (0.0.1):
- Flutter
- YandexMapsMobile (= 4.22.0-lite)
- YandexMapsMobile (4.22.0-lite)
DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- Flutter (from `Flutter`)
- flutter_secure_storage (from `.symlinks/plugins/flutter_secure_storage/ios`)
- geolocator_apple (from `.symlinks/plugins/geolocator_apple/darwin`)
- image_picker_ios (from `.symlinks/plugins/image_picker_ios/ios`)
- mobile_scanner (from `.symlinks/plugins/mobile_scanner/darwin`)
- path_provider_foundation (from `.symlinks/plugins/path_provider_foundation/darwin`)
- shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`)
- url_launcher_ios (from `.symlinks/plugins/url_launcher_ios/ios`)
- yandex_mapkit (from `.symlinks/plugins/yandex_mapkit/ios`)
SPEC REPOS:
trunk:
- YandexMapsMobile
EXTERNAL SOURCES:
device_info_plus:
:path: ".symlinks/plugins/device_info_plus/ios"
Flutter:
:path: Flutter
flutter_secure_storage:
:path: ".symlinks/plugins/flutter_secure_storage/ios"
geolocator_apple:
:path: ".symlinks/plugins/geolocator_apple/darwin"
image_picker_ios:
:path: ".symlinks/plugins/image_picker_ios/ios"
mobile_scanner:
:path: ".symlinks/plugins/mobile_scanner/darwin"
path_provider_foundation:
:path: ".symlinks/plugins/path_provider_foundation/darwin"
shared_preferences_foundation:
:path: ".symlinks/plugins/shared_preferences_foundation/darwin"
url_launcher_ios:
:path: ".symlinks/plugins/url_launcher_ios/ios"
yandex_mapkit:
:path: ".symlinks/plugins/yandex_mapkit/ios"
SPEC CHECKSUMS:
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
flutter_secure_storage: 1ed9476fba7e7a782b22888f956cce43e2c62f13
geolocator_apple: ab36aa0e8b7d7a2d7639b3b4e48308394e8cef5e
image_picker_ios: e0ece4aa2a75771a7de3fa735d26d90817041326
mobile_scanner: 9157936403f5a0644ca3779a38ff8404c5434a93
path_provider_foundation: bb55f6dbba17d0dccd6737fe6f7f34fbd0376880
shared_preferences_foundation: 7036424c3d8ec98dfe75ff1667cb0cd531ec82bb
url_launcher_ios: 7a95fa5b60cc718a708b8f2966718e93db0cef1b
yandex_mapkit: bac34ca1bdf97e87252a8c6d09a95b1fe39ba103
YandexMapsMobile: c73844c6096bbb240d1491adc511c2ef4a88d88d
PODFILE CHECKSUM: 715b1068ab8815ca4115a24e305e836709652b69
COCOAPODS: 1.16.2

23
ios/README.md Normal file
View File

@@ -0,0 +1,23 @@
# Запуск на симуляторе (Mac)
**Терминал** (⌘ + `):
```bash
cd /Users/commercesparkit.by/Desktop/projekt/be_happy_public
export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
open -a Simulator
flutter run
```
Первый раз или после обновления кода:
```bash
cd /Users/commercesparkit.by/Desktop/projekt/be_happy_public
export LANG=en_US.UTF-8 LC_ALL=en_US.UTF-8
flutter pub get
cd ios && pod install && cd ..
open -a Simulator
flutter run
```
Важно: команды только из папки `be_happy_public`, не из `~`.

View File

@@ -10,10 +10,12 @@
1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; };
331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; };
3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; };
6D33173F8CCB65895D564988 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4BB22D9E6003D7254B27B141 /* Pods_RunnerTests.framework */; };
74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; };
97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; };
97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; };
97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; };
FB3C1019114E649A3758FC7D /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 151F40E0A8A0B88D7F9715D8 /* Pods_Runner.framework */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
@@ -40,14 +42,20 @@
/* End PBXCopyFilesBuildPhase section */ /* End PBXCopyFilesBuildPhase section */
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
0EB91AA8815BC4C23E9C769C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = "<group>"; };
1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = "<group>"; };
1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = "<group>"; };
151F40E0A8A0B88D7F9715D8 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; };
331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; }; 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = "<group>"; };
331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = "<group>"; };
4BB22D9E6003D7254B27B141 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; };
4D0302F803C0949E93DDCE44 /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = "<group>"; };
525F1415727A8E2A6F5CA828 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = "<group>"; };
74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = "<group>"; };
74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = "<group>"; };
9740EEB41CF90195004384FC /* Profile.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Profile.xcconfig; path = Flutter/Profile.xcconfig; sourceTree = "<group>"; };
9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = "<group>"; };
9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = "<group>"; };
97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; };
@@ -55,13 +63,25 @@
97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; }; 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
BAF1C86965EF66AE2F0839BD /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = "<group>"; };
BE9A8C77364E79EA4C6EE36B /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = "<group>"; };
D24CB1920C4748F67C854B22 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */ /* Begin PBXFrameworksBuildPhase section */
83F4E53737A5B4793588BFEB /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
6D33173F8CCB65895D564988 /* Pods_RunnerTests.framework in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
97C146EB1CF9000F007C117D /* Frameworks */ = { 97C146EB1CF9000F007C117D /* Frameworks */ = {
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
FB3C1019114E649A3758FC7D /* Pods_Runner.framework in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -82,6 +102,7 @@
3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */,
9740EEB21CF90195004384FC /* Debug.xcconfig */, 9740EEB21CF90195004384FC /* Debug.xcconfig */,
7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 7AFA3C8E1D35360C0083082E /* Release.xcconfig */,
9740EEB41CF90195004384FC /* Profile.xcconfig */,
9740EEB31CF90195004384FC /* Generated.xcconfig */, 9740EEB31CF90195004384FC /* Generated.xcconfig */,
); );
name = Flutter; name = Flutter;
@@ -94,6 +115,8 @@
97C146F01CF9000F007C117D /* Runner */, 97C146F01CF9000F007C117D /* Runner */,
97C146EF1CF9000F007C117D /* Products */, 97C146EF1CF9000F007C117D /* Products */,
331C8082294A63A400263BE5 /* RunnerTests */, 331C8082294A63A400263BE5 /* RunnerTests */,
991E98147265C7C44D711731 /* Pods */,
E94B50B8FA90842D98EF918A /* Frameworks */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
}; };
@@ -121,6 +144,29 @@
path = Runner; path = Runner;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
991E98147265C7C44D711731 /* Pods */ = {
isa = PBXGroup;
children = (
4D0302F803C0949E93DDCE44 /* Pods-Runner.debug.xcconfig */,
0EB91AA8815BC4C23E9C769C /* Pods-Runner.release.xcconfig */,
525F1415727A8E2A6F5CA828 /* Pods-Runner.profile.xcconfig */,
BAF1C86965EF66AE2F0839BD /* Pods-RunnerTests.debug.xcconfig */,
D24CB1920C4748F67C854B22 /* Pods-RunnerTests.release.xcconfig */,
BE9A8C77364E79EA4C6EE36B /* Pods-RunnerTests.profile.xcconfig */,
);
name = Pods;
path = Pods;
sourceTree = "<group>";
};
E94B50B8FA90842D98EF918A /* Frameworks */ = {
isa = PBXGroup;
children = (
151F40E0A8A0B88D7F9715D8 /* Pods_Runner.framework */,
4BB22D9E6003D7254B27B141 /* Pods_RunnerTests.framework */,
);
name = Frameworks;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
@@ -128,8 +174,10 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */;
buildPhases = ( buildPhases = (
1D742F73AAFC432DF3836D7E /* [CP] Check Pods Manifest.lock */,
331C807D294A63A400263BE5 /* Sources */, 331C807D294A63A400263BE5 /* Sources */,
331C807F294A63A400263BE5 /* Resources */, 331C807F294A63A400263BE5 /* Resources */,
83F4E53737A5B4793588BFEB /* Frameworks */,
); );
buildRules = ( buildRules = (
); );
@@ -145,12 +193,15 @@
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */;
buildPhases = ( buildPhases = (
8734FCBECF65F25CB259DF82 /* [CP] Check Pods Manifest.lock */,
9740EEB61CF901F6004384FC /* Run Script */, 9740EEB61CF901F6004384FC /* Run Script */,
97C146EA1CF9000F007C117D /* Sources */, 97C146EA1CF9000F007C117D /* Sources */,
97C146EB1CF9000F007C117D /* Frameworks */, 97C146EB1CF9000F007C117D /* Frameworks */,
97C146EC1CF9000F007C117D /* Resources */, 97C146EC1CF9000F007C117D /* Resources */,
9705A1C41CF9048500538489 /* Embed Frameworks */, 9705A1C41CF9048500538489 /* Embed Frameworks */,
3B06AD1E1E4923F5004D2608 /* Thin Binary */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */,
25F6FF3856E913FFAF963081 /* [CP] Embed Pods Frameworks */,
FF12327738E91E5AA87207B8 /* [CP] Copy Pods Resources */,
); );
buildRules = ( buildRules = (
); );
@@ -222,6 +273,45 @@
/* End PBXResourcesBuildPhase section */ /* End PBXResourcesBuildPhase section */
/* Begin PBXShellScriptBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */
1D742F73AAFC432DF3836D7E /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
25F6FF3856E913FFAF963081 /* [CP] Embed Pods Frameworks */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Embed Pods Frameworks";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n";
showEnvVarsInLog = 0;
};
3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@@ -238,6 +328,28 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin";
}; };
8734FCBECF65F25CB259DF82 /* [CP] Check Pods Manifest.lock */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
);
inputPaths = (
"${PODS_PODFILE_DIR_PATH}/Podfile.lock",
"${PODS_ROOT}/Manifest.lock",
);
name = "[CP] Check Pods Manifest.lock";
outputFileListPaths = (
);
outputPaths = (
"$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n";
showEnvVarsInLog = 0;
};
9740EEB61CF901F6004384FC /* Run Script */ = { 9740EEB61CF901F6004384FC /* Run Script */ = {
isa = PBXShellScriptBuildPhase; isa = PBXShellScriptBuildPhase;
alwaysOutOfDate = 1; alwaysOutOfDate = 1;
@@ -253,6 +365,23 @@
shellPath = /bin/sh; shellPath = /bin/sh;
shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build";
}; };
FF12327738E91E5AA87207B8 /* [CP] Copy Pods Resources */ = {
isa = PBXShellScriptBuildPhase;
buildActionMask = 2147483647;
files = (
);
inputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-input-files.xcfilelist",
);
name = "[CP] Copy Pods Resources";
outputFileListPaths = (
"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources-${CONFIGURATION}-output-files.xcfilelist",
);
runOnlyForDeploymentPostprocessing = 0;
shellPath = /bin/sh;
shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-resources.sh\"\n";
showEnvVarsInLog = 0;
};
/* End PBXShellScriptBuildPhase section */ /* End PBXShellScriptBuildPhase section */
/* Begin PBXSourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */
@@ -346,7 +475,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;
@@ -357,7 +486,7 @@
}; };
249021D4217E4FDB00AE95B9 /* Profile */ = { 249021D4217E4FDB00AE95B9 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; baseConfigurationReference = 9740EEB41CF90195004384FC /* Profile.xcconfig */;
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_MODULES = YES;
@@ -378,6 +507,7 @@
}; };
331C8088294A63A400263BE5 /* Debug */ = { 331C8088294A63A400263BE5 /* Debug */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = BAF1C86965EF66AE2F0839BD /* Pods-RunnerTests.debug.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -395,6 +525,7 @@
}; };
331C8089294A63A400263BE5 /* Release */ = { 331C8089294A63A400263BE5 /* Release */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = D24CB1920C4748F67C854B22 /* Pods-RunnerTests.release.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -410,6 +541,7 @@
}; };
331C808A294A63A400263BE5 /* Profile */ = { 331C808A294A63A400263BE5 /* Profile */ = {
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
baseConfigurationReference = BE9A8C77364E79EA4C6EE36B /* Pods-RunnerTests.profile.xcconfig */;
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
@@ -427,7 +559,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@@ -472,7 +604,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = iphoneos; SDKROOT = iphoneos;
@@ -484,7 +616,7 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
ALWAYS_SEARCH_USER_PATHS = NO; ALWAYS_SEARCH_USER_PATHS = NO;
ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES;
CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NONNULL = YES;
CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x";
CLANG_CXX_LIBRARY = "libc++"; CLANG_CXX_LIBRARY = "libc++";
@@ -523,7 +655,7 @@
GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE;
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 12.0; IPHONEOS_DEPLOYMENT_TARGET = 13.0;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = iphoneos; SDKROOT = iphoneos;
SUPPORTED_PLATFORMS = iphoneos; SUPPORTED_PLATFORMS = iphoneos;

View File

@@ -4,4 +4,7 @@
<FileRef <FileRef
location = "group:Runner.xcodeproj"> location = "group:Runner.xcodeproj">
</FileRef> </FileRef>
<FileRef
location = "group:Pods/Pods.xcodeproj">
</FileRef>
</Workspace> </Workspace>

View File

@@ -1,13 +1,29 @@
import Flutter import Flutter
import UIKit import UIKit
import YandexMapsMobile
@main @main
@objc class AppDelegate: FlutterAppDelegate { @objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate {
override func application( override func application(
_ application: UIApplication, _ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool { ) -> Bool {
GeneratedPluginRegistrant.register(with: self) let apiKey =
(Bundle.main.object(forInfoDictionaryKey: "YandexMapKitApiKey") as? String ?? "")
.trimmingCharacters(in: .whitespacesAndNewlines)
#if DEBUG
if apiKey.isEmpty || apiKey.contains("$(") {
assertionFailure(
"YandexMapKitApiKey пустой или не подставился из xcconfig — проверь YANDEX_MAPKIT_API_KEY в ios/Flutter/*.xcconfig"
)
}
#endif
YMKMapKit.setLocale(Locale.current.identifier)
YMKMapKit.setApiKey(apiKey)
return super.application(application, didFinishLaunchingWithOptions: launchOptions) return super.application(application, didFinishLaunchingWithOptions: launchOptions)
} }
func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) {
GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry)
}
} }

View File

@@ -1 +1,122 @@
{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} {
"images" : [
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "20x20",
"idiom" : "iphone",
"filename" : "Icon-App-20x20@3x.png",
"scale" : "3x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "iphone",
"filename" : "Icon-App-29x29@3x.png",
"scale" : "3x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "iphone",
"filename" : "Icon-App-40x40@3x.png",
"scale" : "3x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@2x.png",
"scale" : "2x"
},
{
"size" : "60x60",
"idiom" : "iphone",
"filename" : "Icon-App-60x60@3x.png",
"scale" : "3x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@1x.png",
"scale" : "1x"
},
{
"size" : "20x20",
"idiom" : "ipad",
"filename" : "Icon-App-20x20@2x.png",
"scale" : "2x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@1x.png",
"scale" : "1x"
},
{
"size" : "29x29",
"idiom" : "ipad",
"filename" : "Icon-App-29x29@2x.png",
"scale" : "2x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@1x.png",
"scale" : "1x"
},
{
"size" : "40x40",
"idiom" : "ipad",
"filename" : "Icon-App-40x40@2x.png",
"scale" : "2x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@1x.png",
"scale" : "1x"
},
{
"size" : "76x76",
"idiom" : "ipad",
"filename" : "Icon-App-76x76@2x.png",
"scale" : "2x"
},
{
"size" : "83.5x83.5",
"idiom" : "ipad",
"filename" : "Icon-App-83.5x83.5@2x.png",
"scale" : "2x"
},
{
"size" : "1024x1024",
"idiom" : "ios-marketing",
"filename" : "Icon-App-1024x1024@1x.png",
"scale" : "1x"
}
],
"info" : {
"version" : 1,
"author" : "xcode"
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 351 KiB

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 295 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 450 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 282 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 462 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 704 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 406 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.5 KiB

After

Width:  |  Height:  |  Size: 586 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 9.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 862 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 762 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 21 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@@ -2,6 +2,8 @@
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"> <plist version="1.0">
<dict> <dict>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
<key>CFBundleDevelopmentRegion</key> <key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string> <string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key> <key>CFBundleDisplayName</key>
@@ -24,6 +26,29 @@
<string>$(FLUTTER_BUILD_NUMBER)</string> <string>$(FLUTTER_BUILD_NUMBER)</string>
<key>LSRequiresIPhoneOS</key> <key>LSRequiresIPhoneOS</key>
<true/> <true/>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneClassName</key>
<string>UIWindowScene</string>
<key>UISceneConfigurationName</key>
<string>flutter</string>
<key>UISceneDelegateClassName</key>
<string>FlutterSceneDelegate</string>
<key>UISceneStoryboardFile</key>
<string>Main</string>
</dict>
</array>
</dict>
</dict>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>UILaunchStoryboardName</key> <key>UILaunchStoryboardName</key>
<string>LaunchScreen</string> <string>LaunchScreen</string>
<key>UIMainStoryboardFile</key> <key>UIMainStoryboardFile</key>
@@ -41,9 +66,7 @@
<string>UIInterfaceOrientationLandscapeLeft</string> <string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string> <string>UIInterfaceOrientationLandscapeRight</string>
</array> </array>
<key>CADisableMinimumFrameDurationOnPhone</key> <key>YandexMapKitApiKey</key>
<true/> <string>$(YANDEX_MAPKIT_API_KEY)</string>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
</dict> </dict>
</plist> </plist>

BIN
lib.rar Normal file

Binary file not shown.

View File

@@ -1,25 +0,0 @@
import '../../domain/entities/promo_code_result.dart';
class PromoCodeResponseDto {
final bool success;
final double balance;
PromoCodeResponseDto({
required this.success,
required this.balance,
});
factory PromoCodeResponseDto.fromJson(Map<String, dynamic> json) {
return PromoCodeResponseDto(
success: json['success'] as bool,
balance: (json['balance'] as num).toDouble(),
);
}
PromoCodeResult toEntity() {
return PromoCodeResult(
success: success,
balance: balance,
);
}
}

View File

@@ -20,7 +20,6 @@ import 'package:path/path.dart';
import 'package:flutter_client_sse/constants/sse_request_type_enum.dart'; import 'package:flutter_client_sse/constants/sse_request_type_enum.dart';
import 'package:flutter_client_sse/flutter_client_sse.dart'; import 'package:flutter_client_sse/flutter_client_sse.dart';
import '../../domain/entities/client_subscription.dart';
import '../../domain/entities/point.dart'; import '../../domain/entities/point.dart';
import '../../domain/entities/user_profile.dart'; import '../../domain/entities/user_profile.dart';
import '../../domain/entities/payment_card.dart'; import '../../domain/entities/payment_card.dart';
@@ -138,8 +137,7 @@ class ApiService {
if (avatarId != null && profileData["avatar"] != null) { if (avatarId != null && profileData["avatar"] != null) {
final String? avatarPath = profileData["avatar"]["path"]; final String? avatarPath = profileData["avatar"]["path"];
if (avatarPath != null && avatarPath.isNotEmpty) { if (avatarPath != null && avatarPath.isNotEmpty) {
avatarUrl = Uri.parse(fileBaseUrl).resolve(avatarPath).toString(); avatarUrl = Uri.parse(fileBaseUrl).resolve(avatarPath).toString(); }
}
} }
dynamic balanceRaw = profileData["balance"]; dynamic balanceRaw = profileData["balance"];
@@ -396,7 +394,10 @@ class ApiService {
final url = "$baseUrl/scooter/$title/code"; final url = "$baseUrl/scooter/$title/code";
try { try {
final response = await _dio.get(url, options: await _getAuthOptions()); final response = await _dio.get(
url,
options: await _getAuthOptions(),
);
if (response.statusCode == 200 || response.statusCode == 201) { if (response.statusCode == 200 || response.statusCode == 201) {
return Scooter.fromJson(response.data); return Scooter.fromJson(response.data);
@@ -462,7 +463,7 @@ class ApiService {
} }
} }
Future<List<ClientSubscription>> getClientSubscriptions() async { Future<List<Subscription>> getClientSubscriptions() async {
const url = "$baseUrl/scootersubscription/client"; const url = "$baseUrl/scootersubscription/client";
try { try {
@@ -473,7 +474,14 @@ class ApiService {
final List<dynamic> items = responseData['data'] ?? []; final List<dynamic> items = responseData['data'] ?? [];
return items.map((item) { return items.map((item) {
return ClientSubscription.fromJson(item); final Map<String, dynamic> subscriptionMap =
Map<String, dynamic>.from(item['subscription'] ?? {});
if (item['expiredAt'] != null) {
subscriptionMap['activeTo'] = item['expiredAt'];
}
return Subscription.fromJson(subscriptionMap);
}).toList(); }).toList();
} }
return []; return [];
@@ -539,7 +547,7 @@ class ApiService {
} }
} }
Future<void> addPaymentCard({ Future<int> addPaymentCard({
required String cardNumber, required String cardNumber,
required String cardHolder, required String cardHolder,
required int expirationMonth, required int expirationMonth,
@@ -563,9 +571,8 @@ class ApiService {
); );
if (response.statusCode == 200 || response.statusCode == 201) { if (response.statusCode == 200 || response.statusCode == 201) {
return; return response.data['id'] as int;
} }
throw AuthException('Непредвиденный статус: ${response.statusCode}', 0); throw AuthException('Непредвиденный статус: ${response.statusCode}', 0);
} on DioException catch (e) { } on DioException catch (e) {
final data = e.response?.data; final data = e.response?.data;
@@ -667,9 +674,7 @@ class ApiService {
final firstError = data['message'][0]['message'].toString(); final firstError = data['message'][0]['message'].toString();
if (firstError.contains("Wrong start zone")) { if (firstError.contains("Wrong start zone")) {
throw WrongZoneException( throw WrongZoneException(message: "Некорректная зона для начала поездки.");
message: "Некорректная зона для начала поездки.",
);
} }
} }
@@ -700,9 +705,7 @@ class ApiService {
final firstError = data['message'][0]['message'].toString(); final firstError = data['message'][0]['message'].toString();
if (firstError.contains("Wrong start zone")) { if (firstError.contains("Wrong start zone")) {
throw WrongZoneException( throw WrongZoneException(message: "Некорректная зона для начала поездки.");
message: "Некорректная зона для начала поездки.",
);
} }
} }
@@ -788,9 +791,7 @@ class ApiService {
final firstError = data['message'][0]['message'].toString(); final firstError = data['message'][0]['message'].toString();
if (firstError.contains("Wrong start zone")) { if (firstError.contains("Wrong start zone")) {
throw WrongZoneException( throw WrongZoneException(message: "Некорректная зона для завершения поездки.");
message: "Некорректная зона для завершения поездки.",
);
} }
} }
@@ -802,7 +803,7 @@ class ApiService {
} }
} }
Future<void> payRide(int orderId) async { Future<ScooterOrder?> payRide(int orderId) async {
try { try {
final response = await _dio.put( final response = await _dio.put(
"$baseUrl/scooterorder/$orderId/pay", "$baseUrl/scooterorder/$orderId/pay",
@@ -810,12 +811,12 @@ class ApiService {
); );
if (response.statusCode == 200 || response.statusCode == 201) { if (response.statusCode == 200 || response.statusCode == 201) {
// return ScooterOrder.fromJson(response.data); return ScooterOrder.fromJson(response.data);
return;
} }
return null;
} on DioException catch (e) { } on DioException catch (e) {
_handleDioError(e); _handleDioError(e);
return; return null;
} }
} }
@@ -877,7 +878,7 @@ class ApiService {
} }
} }
Future<void> payScooterOrderWithPhotos({ Future<ScooterOrder?> payScooterOrderWithPhotos({
required int orderId, required int orderId,
required int? cardId, required int? cardId,
required bool isBalance, required bool isBalance,
@@ -890,10 +891,12 @@ class ApiService {
); );
if (response.statusCode == 200 || response.statusCode == 201) { if (response.statusCode == 200 || response.statusCode == 201) {
return; return ScooterOrder.fromJson(response.data);
} }
return null;
} on DioException catch (e) { } on DioException catch (e) {
_handleDioError(e); _handleDioError(e);
return null;
} }
} }
@@ -915,6 +918,7 @@ class ApiService {
} }
} }
Future<List<Point>> getScooterOrderRouteHistory({required int id}) async { Future<List<Point>> getScooterOrderRouteHistory({required int id}) async {
try { try {
final response = await _dio.get( final response = await _dio.get(
@@ -926,19 +930,13 @@ class ApiService {
final String routeString = response.data['route'] ?? '[]'; final String routeString = response.data['route'] ?? '[]';
final List<dynamic> routeList = json.decode(routeString); final List<dynamic> routeList = json.decode(routeString);
return routeList return routeList.map((item) => Point(
.map( (item[1] as num).toDouble(),
(item) => Point( (item[0] as num).toDouble(),
(item[1] as num).toDouble(), )).toList();
(item[0] as num).toDouble(),
),
)
.toList();
} }
throw RouteHistoryNotFoundException( throw RouteHistoryNotFoundException(message: "История маршрута не найдена");
message: "История маршрута не найдена",
);
} on DioException catch (e) { } on DioException catch (e) {
if (e.response?.statusCode == 401) throw UnauthorizedException(); if (e.response?.statusCode == 401) throw UnauthorizedException();
if (e.response?.statusCode == 403) throw AuthBlockException(); if (e.response?.statusCode == 403) throw AuthBlockException();
@@ -1040,57 +1038,6 @@ class ApiService {
return controller.stream; return controller.stream;
} }
Future<List<Map<String, dynamic>>> getNotifications() async {
final url = Uri.parse('$baseUrl/notification/client');
final accessToken = await _securityService.getAccessToken();
if (accessToken == null) {
print("APISERVICE Error: Access token is null.");
throw UnauthorizedException();
}
print("GET NOTIFICATIONS REQUEST:");
print("URL: $url");
final response = await http.get(
url,
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer $accessToken",
},
);
print("GET NOTIFICATIONS RESPONSE:");
print("STATUS: ${response.statusCode}");
print("BODY: ${response.body}");
if (response.statusCode == 200) {
final data = jsonDecode(utf8.decode(response.bodyBytes));
// ✅ Проверяем, является ли ответ массивом или объектом с 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}');
}
} else if (response.statusCode == 401) {
throw UnauthorizedException();
} else if (response.statusCode == 403) {
throw AuthBlockException();
}
throw Exception('Ошибка сервера: ${response.statusCode}');
}
Future<List<Certificate>> getCertificates() async { Future<List<Certificate>> getCertificates() async {
try { try {
final response = await _dio.get( final response = await _dio.get(
@@ -1150,31 +1097,4 @@ class ApiService {
} }
return null; return null;
} }
Future<Map<String, dynamic>> applyPromoCode(String code) async {
final token = await _securityService.getAccessToken();
if (token == null) throw Exception('No access token');
final response = await _dio.post(
'$baseUrl/promocode/apply',
data: {'code': code},
options: Options(
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer $token',
},
),
);
if (response.statusCode == 201 || response.statusCode == 200) {
return response.data;
} else if (response.statusCode == 404) {
throw PromoCodeNotFoundException();
} else {
throw Exception('API Error: ${response.statusCode}');
}
}
} }
// ✅ Добавь исключение (можно в отдельный файл)
class PromoCodeNotFoundException implements Exception {}

View File

@@ -33,22 +33,4 @@ class NotificationRepositoryImpl implements NotificationRepository {
void closeStream() { void closeStream() {
// соединение закрывается автоматически при отписке от stream // соединение закрывается автоматически при отписке от stream
} }
@override
Future<List<ClientNotification>> getNotifications() async {
try {
final List<Map<String, dynamic>> data = await _apiService.getNotifications();
final notifications = data.map((json) {
final dto = ClientNotificationDto.fromJson(json);
return dto.toEntity();
}).toList();
// dev.log('NotificationRepository: Загружено ${notifications.length} уведомлений');
return notifications;
} catch (e, stackTrace) {
// dev.log('NotificationRepository: Ошибка: $e', stackTrace: stackTrace);
throw Exception('Не удалось загрузить уведомления: $e');
}
}
} }

View File

@@ -40,7 +40,7 @@ class PaymentRepositoryImpl implements PaymentRepository {
required String cvv, required String cvv,
}) async { }) async {
try { try {
await apiService.addPaymentCard( final cardId = await apiService.addPaymentCard(
cardNumber: cardNumber, cardNumber: cardNumber,
cardHolder: cardHolder, cardHolder: cardHolder,
expirationMonth: int.parse(expiryMonth), expirationMonth: int.parse(expiryMonth),
@@ -48,7 +48,8 @@ class PaymentRepositoryImpl implements PaymentRepository {
cvv: cvv, cvv: cvv,
); );
// await securityService.saveCardFullNumber(cardId, cardNumber); // Сохраняем полный номер карты локально
await securityService.saveCardFullNumber(cardId, cardNumber);
return Success(null); return Success(null);
} on AuthException catch (e) { } on AuthException catch (e) {

View File

@@ -1,23 +0,0 @@
import '../../domain/entities/promo_code_result.dart';
import '../../domain/repositories/promo_code_repository.dart';
import '../models/promo_code_response_dto.dart';
import '../network/api_service.dart';
class PromoCodeRepositoryImpl implements PromoCodeRepository {
final ApiService apiService;
PromoCodeRepositoryImpl(this.apiService);
@override
Future<PromoCodeResult> applyPromoCode(String code) async {
try {
final data = await apiService.applyPromoCode(code);
final dto = PromoCodeResponseDto.fromJson(data);
return dto.toEntity();
} on PromoCodeNotFoundException {
throw Exception('Промокод не найден');
} catch (e) {
throw Exception('Ошибка активации промокода: $e');
}
}
}

View File

@@ -10,7 +10,6 @@ import 'package:be_happy/domain/repositories/scooter_repository.dart';
import '../../core/failures.dart'; import '../../core/failures.dart';
import '../../core/result.dart'; import '../../core/result.dart';
import '../../domain/entities/active_scooter_order.dart'; import '../../domain/entities/active_scooter_order.dart';
import '../../domain/entities/client_subscription.dart';
import '../../domain/entities/scooter.dart'; import '../../domain/entities/scooter.dart';
import '../../domain/entities/tariff.dart'; import '../../domain/entities/tariff.dart';
import '../../domain/entities/subscription.dart'; import '../../domain/entities/subscription.dart';
@@ -128,8 +127,8 @@ class ScooterRepositoryImpl extends ScooterRepository {
} }
@override @override
Future<Result<List<ClientSubscription>>> getClientSubscriptions() async { Future<Result<List<Subscription>>> getClientSubscriptions() async {
late final Result<List<ClientSubscription>> result; late final Result<List<Subscription>> result;
try { try {
final subscriptions = await _apiService.getClientSubscriptions(); final subscriptions = await _apiService.getClientSubscriptions();
result = Success(subscriptions); result = Success(subscriptions);
@@ -269,12 +268,15 @@ class ScooterRepositoryImpl extends ScooterRepository {
} }
@override @override
Future<Result<void>> payRide(int orderId) async { Future<Result<ScooterOrder>> payRide(int orderId) async {
late final Result<void> result; late final Result<ScooterOrder> result;
try { try {
await _apiService.payRide(orderId); final order = await _apiService.payRide(orderId);
result = Success(null); 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));
} catch (e) { } catch (e) {
@@ -333,19 +335,23 @@ class ScooterRepositoryImpl extends ScooterRepository {
} }
@override @override
Future<Result<void>> payScooterOrderWithPhotos({ Future<Result<ScooterOrder>> payScooterOrderWithPhotos({
required int orderId, required int orderId,
required int? cardId, required int? cardId,
required bool isBalance, required bool isBalance,
}) async { }) async {
late final Result<void> result; late final Result<ScooterOrder> result;
try { try {
final order = await _apiService.payScooterOrderWithPhotos( final order = await _apiService.payScooterOrderWithPhotos(
orderId: orderId, orderId: orderId,
cardId: cardId, cardId: cardId,
isBalance: isBalance, isBalance: isBalance,
); );
result = Success(null); 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));
} catch (e) { } catch (e) {

View File

@@ -85,17 +85,13 @@ import '../data/repositories/auth_repository_impl.dart';
import '../data/repositories/news_repository_impl.dart'; import '../data/repositories/news_repository_impl.dart';
import '../data/repositories/pin_repository_impl.dart'; import '../data/repositories/pin_repository_impl.dart';
import '../data/repositories/profile_repository_impl.dart'; import '../data/repositories/profile_repository_impl.dart';
import '../data/repositories/promo_code_repository_impl.dart';
import '../data/service/news_api_service.dart'; import '../data/service/news_api_service.dart';
import '../domain/repositories/auth_repository.dart'; import '../domain/repositories/auth_repository.dart';
import '../domain/repositories/news_repository.dart'; import '../domain/repositories/news_repository.dart';
import '../domain/repositories/promo_code_repository.dart';
import '../domain/service/device_info_service.dart'; import '../domain/service/device_info_service.dart';
import '../domain/usecase/activate_subscription_usecase.dart'; import '../domain/usecase/activate_subscription_usecase.dart';
import '../domain/usecase/apply_promo_code_usecase.dart';
import '../domain/usecase/get_client_subscriptions_usecase.dart'; import '../domain/usecase/get_client_subscriptions_usecase.dart';
import '../domain/usecase/get_news_by_id_usecase.dart'; import '../domain/usecase/get_news_by_id_usecase.dart';
import '../domain/usecase/get_notifications_usecase.dart';
import '../domain/usecase/get_scooter_by_title_usecase.dart'; import '../domain/usecase/get_scooter_by_title_usecase.dart';
import '../domain/usecase/get_scooter_order_history_usecase.dart'; import '../domain/usecase/get_scooter_order_history_usecase.dart';
import '../domain/usecase/remove_payment_card_usecase.dart'; import '../domain/usecase/remove_payment_card_usecase.dart';
@@ -104,9 +100,7 @@ import '../presentation/viewmodel/auth_bloc.dart';
import '../presentation/viewmodel/edit_profile_bloc.dart'; import '../presentation/viewmodel/edit_profile_bloc.dart';
import '../presentation/viewmodel/map_bloc.dart'; import '../presentation/viewmodel/map_bloc.dart';
import '../presentation/viewmodel/news_bloc.dart'; import '../presentation/viewmodel/news_bloc.dart';
import '../presentation/viewmodel/notifications_bloc.dart';
import '../presentation/viewmodel/order_history_bloc.dart'; import '../presentation/viewmodel/order_history_bloc.dart';
import '../presentation/viewmodel/promo_code_bloc.dart';
import '../presentation/viewmodel/scooter_detail_modal_bloc.dart'; import '../presentation/viewmodel/scooter_detail_modal_bloc.dart';
import '../presentation/viewmodel/subscription_list_bloc.dart'; import '../presentation/viewmodel/subscription_list_bloc.dart';
import '../presentation/viewmodel/verify_code_bloc.dart'; import '../presentation/viewmodel/verify_code_bloc.dart';
@@ -116,7 +110,7 @@ final getIt = GetIt.instance;
Future<void> setupDependencies() async { Future<void> setupDependencies() async {
final sharedPreferences = await SharedPreferences.getInstance(); final sharedPreferences = await SharedPreferences.getInstance();
final dio = Dio(); final dio = Dio();
dio.interceptors.add(LogInterceptor(/*requestHeader: false, responseHeader:false, */responseBody: true, requestBody: true)); dio.interceptors.add(LogInterceptor(responseBody: true, requestBody: true));
dio.interceptors.add(AuthInterceptor()); dio.interceptors.add(AuthInterceptor());
// HTTP // HTTP
getIt.registerSingleton<http.Client>(http.Client()); getIt.registerSingleton<http.Client>(http.Client());
@@ -292,13 +286,6 @@ Future<void> setupDependencies() async {
getIt.registerSingleton<GetScooterByTitleUsecase>( getIt.registerSingleton<GetScooterByTitleUsecase>(
GetScooterByTitleUsecase(getIt()), GetScooterByTitleUsecase(getIt()),
); );
getIt.registerSingleton<GetNotificationsUsecase>(
GetNotificationsUsecase(getIt<NotificationRepository>()),
);
getIt.registerFactory<NotificationsBloc>(
() => NotificationsBloc(getIt<GetNotificationsUsecase>()),
);
// Blocs // Blocs
getIt.registerLazySingleton<SplashBloc>(() => SplashBloc(getIt())); getIt.registerLazySingleton<SplashBloc>(() => SplashBloc(getIt()));
@@ -323,7 +310,6 @@ Future<void> setupDependencies() async {
getIt(), getIt(),
getIt(), getIt(),
getIt(), getIt(),
getIt(),
), ),
); );
@@ -387,20 +373,4 @@ Future<void> setupDependencies() async {
getIt.registerFactory<ScooterCodeBloc>( getIt.registerFactory<ScooterCodeBloc>(
() => ScooterCodeBloc(getScooterByTitleUsecase: getIt<GetScooterByTitleUsecase>()), () => ScooterCodeBloc(getScooterByTitleUsecase: getIt<GetScooterByTitleUsecase>()),
); );
// Repository
getIt.registerSingleton<PromoCodeRepository>(
PromoCodeRepositoryImpl(getIt<ApiService>()),
);
// UseCase
getIt.registerSingleton<ApplyPromoCodeUsecase>(
ApplyPromoCodeUsecase(getIt<PromoCodeRepository>()),
);
// Bloc (factory, т.к. экран создаёт новый экземпляр)
getIt.registerFactory<PromoCodeBloc>(
() => PromoCodeBloc(getIt<ApplyPromoCodeUsecase>()),
);
} }

View File

@@ -1,24 +0,0 @@
import 'package:be_happy/domain/entities/subscription.dart';
class ClientSubscription {
final int id;
final int subscriptionId;
final Subscription subscription;
final DateTime? expiredAt;
ClientSubscription({
required this.id,
required this.subscriptionId,
required this.subscription,
this.expiredAt,
});
factory ClientSubscription.fromJson(Map<String, dynamic> json) {
return ClientSubscription(
id: json['id'] ?? 0,
subscriptionId: json['subscriptionId'] ?? 0,
subscription: Subscription.fromJson(json['subscription'] as Map<String, dynamic>),
expiredAt: json['expiredAt'] != null ? DateTime.parse(json['expiredAt']) : null,
);
}
}

View File

@@ -1,9 +0,0 @@
class PromoCodeResult {
final bool success;
final double balance;
PromoCodeResult({
required this.success,
required this.balance,
});
}

View File

@@ -43,9 +43,8 @@ class Scooter {
); );
} }
@override @override
String toString() { String toString() {
return 'Scooter{id: $id, title: $title, status: $status, latitude: $latitude, longitude: $longitude, batteryLevel: $batteryLevel, isOnline: $isOnline}'; return 'Scooter{id: $id, title: $title}';
} }
} }

View File

@@ -3,7 +3,7 @@ import 'scooter.dart';
class ScooterOrder { class ScooterOrder {
final int id; final int id;
final int scooterId; final int scooterId;
final Scooter scooter; final Scooter? scooter;
final int? planId; final int? planId;
final ScooterPlan? plan; final ScooterPlan? plan;
final int clientId; final int clientId;
@@ -33,7 +33,7 @@ class ScooterOrder {
ScooterOrder({ ScooterOrder({
required this.id, required this.id,
required this.scooterId, required this.scooterId,
required this.scooter, this.scooter,
this.planId, this.planId,
this.plan, this.plan,
required this.clientId, required this.clientId,
@@ -65,7 +65,7 @@ class ScooterOrder {
return ScooterOrder( return ScooterOrder(
id: json['id'] ?? 0, id: json['id'] ?? 0,
scooterId: json['scooterId'] ?? 0, scooterId: json['scooterId'] ?? 0,
scooter: Scooter.fromJson(json['scooter']), scooter: json['scooter'] != null ? Scooter.fromJson(json['scooter']) : null,
planId: json['planId'], planId: json['planId'],
plan: json['plan'] != null ? ScooterPlan.fromJson(json['plan']) : null, plan: json['plan'] != null ? ScooterPlan.fromJson(json['plan']) : null,
clientId: json['clientId'] ?? 0, clientId: json['clientId'] ?? 0,

View File

@@ -8,7 +8,6 @@ class Subscription {
final String fullDescription; final String fullDescription;
final int planId; final int planId;
final bool isActive; final bool isActive;
final bool isCurrent;
final String currency; final String currency;
final DateTime? activeFrom; final DateTime? activeFrom;
final DateTime? activeTo; final DateTime? activeTo;
@@ -29,7 +28,6 @@ class Subscription {
this.activeTo, this.activeTo,
required this.createdAt, required this.createdAt,
required this.updatedAt, required this.updatedAt,
required this.isCurrent,
required this.options, required this.options,
}); });
@@ -50,7 +48,6 @@ class Subscription {
activeTo: json['activeTo'] != null ? DateTime.parse(json['activeTo']) : null, activeTo: json['activeTo'] != null ? DateTime.parse(json['activeTo']) : null,
createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : DateTime.now(), createdAt: json['createdAt'] != null ? DateTime.parse(json['createdAt']) : DateTime.now(),
updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : DateTime.now(), updatedAt: json['updatedAt'] != null ? DateTime.parse(json['updatedAt']) : DateTime.now(),
isCurrent: json['isCurrent'] ?? false,
options: optionsData.map((e) => SubscriptionPeriod.fromJson(e as Map<String, dynamic>)).toList(), options: optionsData.map((e) => SubscriptionPeriod.fromJson(e as Map<String, dynamic>)).toList(),
); );
} }

View File

@@ -9,7 +9,4 @@ abstract class NotificationRepository {
/// Закрывает SSE-соединение /// Закрывает SSE-соединение
void closeStream(); void closeStream();
/// получить список уведомлений
Future<List<ClientNotification>> getNotifications();
} }

View File

@@ -1,5 +0,0 @@
import '../entities/promo_code_result.dart';
abstract class PromoCodeRepository {
Future<PromoCodeResult> applyPromoCode(String code);
}

View File

@@ -4,7 +4,6 @@ import 'dart:io';
import 'package:be_happy/domain/entities/active_scooter_order.dart'; import 'package:be_happy/domain/entities/active_scooter_order.dart';
import '../../core/result.dart'; import '../../core/result.dart';
import '../entities/client_subscription.dart';
import '../entities/point.dart'; import '../entities/point.dart';
import '../entities/scooter.dart'; import '../entities/scooter.dart';
import '../entities/subscription.dart'; import '../entities/subscription.dart';
@@ -17,7 +16,7 @@ abstract class ScooterRepository {
Future<Result<List<Tariff>>> getAvailableTariffs(int scooterId); Future<Result<List<Tariff>>> getAvailableTariffs(int scooterId);
Future<Result<List<Subscription>>> getAvailableSubscriptions(); Future<Result<List<Subscription>>> getAvailableSubscriptions();
Future<Result<Subscription>> getSubscriptionById(int id); Future<Result<Subscription>> getSubscriptionById(int id);
Future<Result<List<ClientSubscription>>> getClientSubscriptions(); Future<Result<List<Subscription>>> getClientSubscriptions();
Future<Result<ScooterOrder>> bookScooter({ Future<Result<ScooterOrder>> bookScooter({
required int scooterId, required int scooterId,
required int planId, required int planId,
@@ -31,13 +30,13 @@ abstract class ScooterRepository {
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<ScooterOrder>> finishRide(int orderId, List<int> files);
Future<Result<void>> payRide(int orderId); Future<Result<ScooterOrder>> 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);
Future<Result<ActiveScooterOrder>> updateScooterOrderData({ Future<Result<ActiveScooterOrder>> updateScooterOrderData({
required int orderId, required int orderId,
}); });
Future<Result<void>> payScooterOrderWithPhotos({ Future<Result<ScooterOrder>> payScooterOrderWithPhotos({
required int orderId, required int orderId,
required int? cardId, required int? cardId,
required bool isBalance, required bool isBalance,

View File

@@ -1,12 +0,0 @@
import '../entities/promo_code_result.dart';
import '../repositories/promo_code_repository.dart';
class ApplyPromoCodeUsecase {
final PromoCodeRepository repository;
ApplyPromoCodeUsecase(this.repository);
Future<PromoCodeResult> call(String code) {
return repository.applyPromoCode(code);
}
}

View File

@@ -1,7 +1,6 @@
import 'package:be_happy/core/result.dart'; import 'package:be_happy/core/result.dart';
import 'package:be_happy/domain/entities/scooter_order.dart'; import 'package:be_happy/domain/entities/scooter_order.dart';
import '../entities/client_subscription.dart';
import '../repositories/scooter_repository.dart'; import '../repositories/scooter_repository.dart';
@@ -15,7 +14,7 @@ class GetClientSubscriptionsUsecase {
GetClientSubscriptionsUsecase(this.repository); GetClientSubscriptionsUsecase(this.repository);
Future<Result<List<ClientSubscription>>> call() { Future<Result<List<Subscription>>> call() {
return repository.getClientSubscriptions(); return repository.getClientSubscriptions();
} }
} }

View File

@@ -1,12 +0,0 @@
import '../entities/client_notification.dart';
import '../repositories/notification_repository.dart';
class GetNotificationsUsecase {
final NotificationRepository repository;
GetNotificationsUsecase(this.repository);
Future<List<ClientNotification>> call() {
return repository.getNotifications();
}
}

View File

@@ -7,7 +7,7 @@ class PayRideUsecase {
PayRideUsecase(this.repository); PayRideUsecase(this.repository);
Future<Result<void>> call(int orderId, int? cardId, Future<Result<ScooterOrder>> call(int orderId, int? cardId,
bool isBalance) { bool isBalance) {
return repository.payScooterOrderWithPhotos(orderId: orderId, cardId: cardId, isBalance: isBalance); return repository.payScooterOrderWithPhotos(orderId: orderId, cardId: cardId, isBalance: isBalance);
} }

View File

@@ -7,7 +7,7 @@ class PayScooterOrderWithPhotosUsecase {
PayScooterOrderWithPhotosUsecase(this.repository); PayScooterOrderWithPhotosUsecase(this.repository);
Future<Result<void>> call({ Future<Result<ScooterOrder>> call({
required int orderId, required int orderId,
required int cardId, required int cardId,
required bool isBalance, required bool isBalance,

View File

@@ -23,21 +23,9 @@ import 'di/service_locator.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
SystemChrome.setEnabledSystemUIMode( SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
SystemUiMode.manual,
overlays: [SystemUiOverlay.top],
);
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
statusBarBrightness: Brightness.dark,
),
);
await setupDependencies(); await setupDependencies();
runApp(const MyApp()); runApp(const MyApp());
} }

View File

@@ -3,8 +3,11 @@ import 'package:flutter/material.dart';
class CancelBookingDialog extends StatelessWidget { class CancelBookingDialog extends StatelessWidget {
const CancelBookingDialog({super.key}); const CancelBookingDialog({super.key});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
bool result = false;
return Dialog( return Dialog(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 40), insetPadding: const EdgeInsets.symmetric(horizontal: 40),
@@ -39,7 +42,6 @@ class CancelBookingDialog extends StatelessWidget {
), ),
const SizedBox(height: 24), const SizedBox(height: 24),
// 🔹 Кнопка "Отменить" — ПЕРВАЯ, зелёная (градиент), возвращает true
Container( Container(
width: double.infinity, width: double.infinity,
height: 52, height: 52,
@@ -50,7 +52,10 @@ class CancelBookingDialog extends StatelessWidget {
), ),
), ),
child: ElevatedButton( child: ElevatedButton(
onPressed: () => Navigator.pop(context, true), // ✅ true = отменить onPressed: () => {
result = false,
Navigator.pop(context, result)
},
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent, backgroundColor: Colors.transparent,
shadowColor: Colors.transparent, shadowColor: Colors.transparent,
@@ -59,9 +64,9 @@ class CancelBookingDialog extends StatelessWidget {
), ),
), ),
child: const Text( child: const Text(
"Отменить", "Оставить",
style: TextStyle( style: TextStyle(
color: Color(0xFF1D273A), // тёмный текст на светлом градиенте color: Color(0xFF1D273A),
fontSize: 16, fontSize: 16,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
@@ -69,13 +74,14 @@ class CancelBookingDialog extends StatelessWidget {
), ),
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
// 🔹 Кнопка "Оставить" — ВТОРАЯ, тёмная, возвращает false
SizedBox( SizedBox(
width: double.infinity, width: double.infinity,
height: 52, height: 52,
child: ElevatedButton( child: ElevatedButton(
onPressed: () => Navigator.pop(context, false), // ❌ false = оставить onPressed: () {
result = true;
Navigator.pop(context, result);
},
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0D1024), backgroundColor: const Color(0xFF0D1024),
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
@@ -83,7 +89,7 @@ class CancelBookingDialog extends StatelessWidget {
), ),
), ),
child: const Text( child: const Text(
"Оставить", "Отменить",
style: TextStyle(color: Colors.white, fontSize: 16), style: TextStyle(color: Colors.white, fontSize: 16),
), ),
), ),

View File

@@ -1,96 +0,0 @@
import 'package:flutter/material.dart';
class FinishRideConfirmationDialog extends StatelessWidget {
const FinishRideConfirmationDialog({super.key});
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent,
insetPadding: const EdgeInsets.symmetric(horizontal: 40),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
color: const Color(0xFF2E3253).withOpacity(0.95),
borderRadius: BorderRadius.circular(28),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"Завершить поездку?",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white,
fontSize: 22,
fontWeight: FontWeight.w600,
height: 1.2,
),
),
const SizedBox(height: 16),
const Text(
"Вы действительно хотите завершить поездку?",
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white70,
fontSize: 14,
height: 1.4,
),
),
const SizedBox(height: 24),
// 🔹 Кнопка "Завершить" — ПЕРВАЯ, зелёная (градиент), возвращает true
Container(
width: double.infinity,
height: 52,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(26),
gradient: const LinearGradient(
colors: [Color(0xFF8EFEB5), Color(0xFF86FEF1)],
),
),
child: ElevatedButton(
onPressed: () => Navigator.pop(context, true), // ✅ true = завершить
style: ElevatedButton.styleFrom(
backgroundColor: Colors.transparent,
shadowColor: Colors.transparent,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26),
),
),
child: const Text(
"Завершить",
style: TextStyle(
color: Color(0xFF1D273A), // тёмный текст на светлом градиенте
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
),
),
const SizedBox(height: 12),
// 🔹 Кнопка "Отмена" — ВТОРАЯ, тёмная, возвращает false
SizedBox(
width: double.infinity,
height: 52,
child: ElevatedButton(
onPressed: () => Navigator.pop(context, false), // ❌ false = отмена
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF0D1024),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(26),
),
),
child: const Text(
"Отмена",
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
),
],
),
),
);
}
}

View File

@@ -0,0 +1,141 @@
import 'dart:ui';
import 'package:be_happy/presentation/event/map_event.dart';
import 'package:be_happy/presentation/event/map_settings_modal_event.dart';
import 'package:be_happy/presentation/state/map_settings_modal_state.dart';
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../viewmodel/map_bloc.dart';
class MapSettingsSheet extends StatelessWidget {
final VoidCallback? onClose;
const MapSettingsSheet({super.key, this.onClose});
@override
Widget build(BuildContext context) {
return BlocBuilder<MapSettingsModalBloc, MapSettingsModalState>(
builder: (context, state) {
final List<_SettingItemData> items = [
_SettingItemData(
label: 'Геоточки',
icon: Icons.location_on_outlined,
color: const Color(0xFF66E3C4),
isActive: state.isAllGeomarksActive,
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeomarksToggled(val)),
),
_SettingItemData(
label: 'Геозоны',
icon: Icons.gps_fixed_outlined,
color: const Color(0xFF86EFAC),
isActive: state.isAllGeozonesActive,
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeozonesToggled(val)),
),
_SettingItemData(
label: 'Парковка',
icon: Icons.home_outlined,
color: const Color(0xFFA78BFA),
isActive: state.isParkingZoneActive,
onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)),
),
_SettingItemData(
label: 'Парковка запрещена',
icon: Icons.block_outlined,
color: const Color(0xFFF59E0B),
isActive: state.isRestrictedParkingZoneActive,
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)),
),
_SettingItemData(
label: 'Запрещено кататься',
icon: Icons.warning_amber_outlined,
color: const Color(0xFFEF4444),
isActive: state.isRestrictedDrivingZoneActive,
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedDrivingZonesToggled(val)),
),
];
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: 365,
decoration: BoxDecoration(
color: const Color(0xFF000032).withOpacity(0.88),
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
),
child: Column(
children: [
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Параметры карты',
style: TextStyle(color: Colors.white, fontSize: 16, fontWeight: FontWeight.w600),
),
TextButton(
onPressed: () {
context.read<MapSettingsModalBloc>().add(ApllyButtonClick());
context.read<MapBloc>().add(UpdateMap());
Navigator.of(context).pop();
},
child: const Text(
'Готово',
style: TextStyle(color: Color(0xFF66E3C4), fontSize: 16, fontWeight: FontWeight.w600),
),
),
],
),
),
Expanded(
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 10),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
return ListTile(
leading: Icon(item.icon, color: item.color),
title: Text(item.label, style: const TextStyle(color: Colors.white)),
trailing: Switch.adaptive(
value: item.isActive,
onChanged: item.onChanged,
activeTrackColor: const Color(0xFF66E3C4),
inactiveThumbColor: Colors.white,
),
);
},
),
),
],
),
),
),
),
);
},
);
}
}
// Вспомогательный класс для описания строк
class _SettingItemData {
final String label;
final IconData icon;
final Color color;
final bool isActive;
final ValueChanged<bool> onChanged;
_SettingItemData({
required this.label,
required this.icon,
required this.color,
required this.isActive,
required this.onChanged,
});
}

View File

@@ -1,17 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui'; import 'dart:ui';
import 'package:be_happy/domain/entities/scooter.dart';
import 'package:bot_toast/bot_toast.dart'; import 'package:bot_toast/bot_toast.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';
import '../../../di/service_locator.dart'; import '../../../di/service_locator.dart';
import '../../event/active_ride_event.dart'; import '../../event/active_ride_event.dart';
import '../../event/map_event.dart';
import '../../state/active_ride_state.dart'; import '../../state/active_ride_state.dart';
import '../../viewmodel/active_ride_bloc.dart'; import '../../viewmodel/active_ride_bloc.dart';
import '../../viewmodel/map_bloc.dart';
import '../dialog/finish_ride_confirmation_dialog.dart';
import '../notification_toast.dart'; import '../notification_toast.dart';
class ActiveRideSheet extends StatefulWidget { class ActiveRideSheet extends StatefulWidget {
@@ -59,7 +55,7 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
return BlocProvider.value( return BlocProvider.value(
value: _bloc, value: _bloc,
child: BlocConsumer<ActiveRideBloc, ActiveRideState>( child: BlocConsumer<ActiveRideBloc, ActiveRideState>(
listenWhen: (previous, current) => previous.inZone != current.inZone || previous.status != current.status, listenWhen: (previous, current) => previous.inZone != current.inZone,
listener: (context, state) { listener: (context, state) {
if (!state.inZone) { if (!state.inZone) {
BotToast.showCustomNotification( BotToast.showCustomNotification(
@@ -74,15 +70,6 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
}, },
); );
} }
if (state.status == ActiveRideStatus.success && state.order != null) {
final scooter = state.order!.scooter;
context.read<MapBloc>().add(FocusOnScooter(Scooter(id: scooter.id,
title: scooter.title, status: scooter.status,
latitude: state.longitude, longitude: state.latitude,
batteryLevel: scooter.batteryLevel, isOnline: scooter.isOnline,
maxSpeed: scooter.maxSpeed, number: scooter.number)));
}
}, },
builder: (context, state) { builder: (context, state) {
// Логика отображения загрузки и ошибок остается прежней // Логика отображения загрузки и ошибок остается прежней
@@ -215,8 +202,8 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
color: Colors.white, color: Colors.white,
fontSize: 32, fontSize: 32,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontFeatures: const [FontFeature.tabularFigures()], fontFeatures: [FontFeature.tabularFigures()],
fontFamily: 'DigitalNumbers', fontFamily: 'Digital Numbers',
), ),
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
@@ -303,23 +290,13 @@ class _ActiveRideSheetState extends State<ActiveRideSheet> {
child: InkWell( child: InkWell(
onTap: state.status == ActiveRideStatus.loading onTap: state.status == ActiveRideStatus.loading
? null ? null
: () async { : () {
// 🔹 Показываем диалог подтверждения _bloc.add(FinishRide(widget.orderId));
final result = await showDialog<bool>( Navigator.pop(context);
context: context, context.go("/home/order-photos/${widget.orderId}");
builder: (context) => const FinishRideConfirmationDialog(), },
);
// 🔹 Если пользователь подтвердил — завершаем и переходим
if (result == true) {
_bloc.add(FinishRide(widget.orderId));
Navigator.pop(context); // закрываем ActiveRideSheet
context.go("/home/order-photos/${widget.orderId}");
}
// 🔹 Если отменил — ничего не делаем, диалог уже закрылся
},
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
child: Column( // ✅ Вернули Column с иконкой и текстом child: Column(
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
children: [ children: [
const Icon( const Icon(

View File

@@ -219,11 +219,11 @@ class _RideCardState extends State<_RideCard> {
displayTime = _elapsedTime; displayTime = _elapsedTime;
} }
final timeString = _formatDuration(displayTime); final timeString = _formatDuration(displayTime);
final statusText = widget.order.status == 'Booking' ? 'Забронирован' : "Активный"; final statusText = _getStatusText(widget.order.status);
final statusColor = widget.order.status == 'Booking' ? Color(0xFFFFCC00) : Color(0xFF8bffaa); final statusColor = _getStatusColor(widget.order.status);
final scooterNumber = final scooterNumber =
widget.order.scooter.number ?? widget.order.scooterId.toString(); widget.order.scooter?.number ?? widget.order.scooterId.toString();
return Container( return Container(
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
@@ -261,7 +261,7 @@ class _RideCardState extends State<_RideCard> {
Text( Text(
statusText, statusText,
style: TextStyle( style: TextStyle(
color: statusColor, color: Colors.white,
fontSize: 14, fontSize: 14,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
), ),
@@ -269,51 +269,22 @@ class _RideCardState extends State<_RideCard> {
], ],
), ),
const SizedBox(height: 4), const SizedBox(height: 4),
Row( Text(
children: [ scooterNumber,
Image.asset("assets/icons/qr_icon_order.png"), style: const TextStyle(
const SizedBox(width: 6), color: Colors.white,
Text( fontSize: 16,
scooterNumber, fontWeight: FontWeight.w600,
style: const TextStyle( ),
color: Colors.white, ),
fontSize: 16, const SizedBox(height: 4),
fontWeight: FontWeight.w600, Text(
), _getLocationText(),
), style: TextStyle(
], color: Colors.white.withOpacity(0.6),
fontSize: 13,
),
), ),
if (isReserved)
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const SizedBox(height: 8),
Text(
"Тариф",
style: TextStyle(
color: Color(0xFF8bffaa),
fontSize: 11,
),
),
const SizedBox(height: 4),
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset("assets/icons/timer.png", width: 14,),
const SizedBox(width: 4),
Text(
widget.order.plan?.title ?? "Название тарифа",
style: TextStyle(
color: Color(0xFF8bffaa),
fontWeight: FontWeight.bold,
fontSize: 16,
),
),
],
)
],
)
], ],
), ),
), ),
@@ -330,10 +301,10 @@ class _RideCardState extends State<_RideCard> {
timeString, timeString,
style: TextStyle( style: TextStyle(
color: statusColor, color: statusColor,
fontSize: 20, fontSize: 26,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
fontFeatures: const [FontFeature.tabularFigures()], fontFeatures: const [FontFeature.tabularFigures()],
fontFamily: 'DigitalNumbers', fontFamily: 'Digital Numbers',
), ),
), ),
], ],
@@ -389,7 +360,7 @@ class _RideCardState extends State<_RideCard> {
switch (status.toLowerCase()) { switch (status.toLowerCase()) {
case 'reserved': case 'reserved':
case 'holding': case 'holding':
return 'Забронирован'; return 'Забронировано';
case 'active': case 'active':
case 'in_progress': case 'in_progress':
return 'Активно'; return 'Активно';

View File

@@ -5,6 +5,8 @@ import 'package:be_happy/presentation/state/map_settings_modal_state.dart';
import 'package:be_happy/presentation/viewmodel/map_settings_modal_bloc.dart'; import 'package:be_happy/presentation/viewmodel/map_settings_modal_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 '../../viewmodel/map_bloc.dart'; import '../../viewmodel/map_bloc.dart';
class MapSettingsSheet extends StatelessWidget { class MapSettingsSheet extends StatelessWidget {
@@ -17,6 +19,13 @@ class MapSettingsSheet extends StatelessWidget {
return BlocBuilder<MapSettingsModalBloc, MapSettingsModalState>( return BlocBuilder<MapSettingsModalBloc, MapSettingsModalState>(
builder: (context, state) { builder: (context, state) {
final List<_SettingItemData> items = [ final List<_SettingItemData> items = [
_SettingItemData(
label: 'Геоточки',
icon: Icons.location_on_outlined,
color: const Color(0xFF66E3C4),
isActive: state.isAllGeomarksActive,
onChanged: (val) => context.read<MapSettingsModalBloc>().add(AllGeomarksToggled(val)),
),
_SettingItemData( _SettingItemData(
label: 'Геозоны', label: 'Геозоны',
icon: Icons.gps_fixed_outlined, icon: Icons.gps_fixed_outlined,
@@ -32,9 +41,9 @@ class MapSettingsSheet extends StatelessWidget {
onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)), onChanged: (val) => context.read<MapSettingsModalBloc>().add(ParkingZonesToggled(val)),
), ),
_SettingItemData( _SettingItemData(
label: 'Парковка запрещена', label: 'Разрешено кататься',
icon: Icons.block_outlined, icon: Icons.block_outlined,
color: const Color(0xFFF59E0B), color: const Color(0xFF5ECD4C),
isActive: state.isRestrictedParkingZoneActive, isActive: state.isRestrictedParkingZoneActive,
onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)), onChanged: (val) => context.read<MapSettingsModalBloc>().add(RestrictedParkingZonesToggled(val)),
), ),

View File

@@ -12,13 +12,11 @@ import '../../state/payment_method_sheet_state.dart';
import '../../viewmodel/payment_method_sheet_bloc.dart'; import '../../viewmodel/payment_method_sheet_bloc.dart';
class PaymentMethodSheet extends StatefulWidget { class PaymentMethodSheet extends StatefulWidget {
final PaymentCard? initialSelectedCard; final PaymentCard? initialSelectedCard; // Добавляем это поле
final bool showBalance;
const PaymentMethodSheet({ const PaymentMethodSheet({
super.key, super.key,
this.initialSelectedCard, this.initialSelectedCard, // Инициализируем в конструкторе
this.showBalance = true,
}); });
@override @override
@@ -94,7 +92,7 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
_selectedPaymentMethod = initialIndex != -1 ? initialIndex : -1; _selectedPaymentMethod = initialIndex != -1 ? initialIndex : -1;
} else { } else {
final mainCardIndex = state.cards.indexWhere((card) => card.isMain); final mainCardIndex = state.cards.indexWhere((card) => card.isMain);
_selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : (widget.showBalance ? -1 : 0); _selectedPaymentMethod = mainCardIndex != -1 ? mainCardIndex : -1;
} }
} }
@@ -171,20 +169,19 @@ class _PaymentMethodSheetState extends State<PaymentMethodSheet> {
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column( child: Column(
children: [ children: [
if (widget.showBalance) ...[ PaymentOption(
PaymentOption( title: 'Баланс',
title: 'Баланс', subtitle: '${state.balance.toStringAsFixed(2)} BYN',
subtitle: '${state.balance.toStringAsFixed(2)} BYN', isSelected: _selectedPaymentMethod == -1,
isSelected: _selectedPaymentMethod == -1, onTap: () {
onTap: () { setState(() {
setState(() { _selectedPaymentMethod = -1;
_selectedPaymentMethod = -1; });
}); Navigator.pop(context, 'balance');
Navigator.pop(context, 'balance'); },
}, ),
),
const SizedBox(height: 12), const SizedBox(height: 12),
],
...state.cards.asMap().entries.map((entry) { ...state.cards.asMap().entries.map((entry) {
final index = entry.key; final index = entry.key;

View File

@@ -60,286 +60,264 @@ class _ReservedRideSheetState extends State<ReservedRideSheet> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider.value( return BlocProvider.value(
value: _bloc, value: _bloc,
child: Scaffold( child: Align(
backgroundColor: Colors.transparent, alignment: Alignment.bottomCenter,
resizeToAvoidBottomInset: false, child: ClipRRect(
body: Align( borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
alignment: Alignment.bottomCenter, child: BackdropFilter(
child: ClipRRect( filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20),
borderRadius: const BorderRadius.vertical(top: Radius.circular(30)), child: Container(
child: BackdropFilter( padding: const EdgeInsets.only(top: 20, bottom: 10),
filter: ImageFilter.blur(sigmaX: 20, sigmaY: 20), decoration: BoxDecoration(
child: Container( color: const Color(0xFF000032).withOpacity(0.5),
padding: const EdgeInsets.only(top: 20, bottom: 10), borderRadius: const BorderRadius.vertical(top: Radius.circular(30)),
decoration: BoxDecoration( ),
color: const Color(0xFF000032).withOpacity(0.5), child: Column(
borderRadius: const BorderRadius.vertical( mainAxisSize: MainAxisSize.min,
top: Radius.circular(30), children: [
// HEADER
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,
),
),
],
),
), ),
),
child: Column( const SizedBox(height: 20),
mainAxisSize: MainAxisSize.min,
children: [ // ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ)
// HEADER Padding(
Padding( padding: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(horizontal: 20), child: Row(
child: Row( children: [
children: [ // Таймер
GestureDetector( Expanded(
onTap: () => Navigator.pop(context), flex: 2,
child: Text(
_formatDuration(_reservationTime),
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
fontFeatures: [FontFeature.tabularFigures()],
fontFamily: 'Digital Numbers',
),
),
),
// Иконка и информация (ВЫСОКИЙ БЛОК)
Expanded(
flex: 3,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row( child: Row(
children: [ children: [
Icon( // Иконка самоката (ВЫШЕ)
Icons.arrow_back_ios_sharp, SizedBox(
color: const Color(0x99FFFFFF), width: 44,
size: 20, height: 56,
child: Image.asset(
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
fit: BoxFit.contain,
),
), ),
Icon( const SizedBox(width: 12),
Icons.arrow_back_ios_sharp, // Инфо
color: const Color(0x66FFFFFF), Expanded(
size: 20, child: Column(
), crossAxisAlignment: CrossAxisAlignment.start,
Icon( mainAxisSize: MainAxisSize.min,
Icons.arrow_back_ios_sharp, children: [
color: const Color(0x22FFFFFF), Row(
size: 20, mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFFFFB800),
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
const Text(
'Забронирован',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
],
),
const SizedBox(height: 8),
Text(
'${widget.scooterNumber}',
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
],
),
), ),
], ],
), ),
), ),
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),
// ТАЙМЕР + ИНФО О САМОКАТЕ (КОМПАКТНЫЙ)
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
children: [
// Таймер
Expanded(
flex: 2,
child: Text(
_formatDuration(_reservationTime),
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
fontFeatures: [FontFeature.tabularFigures()],
fontFamily: 'Digital Numbers',
),
),
),
// Иконка и информация (ВЫСОКИЙ БЛОК)
Expanded(
flex: 3,
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 16,
),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
// Иконка самоката (ВЫШЕ)
SizedBox(
width: 44,
height: 56,
child: Image.asset(
'assets/icons/e6a5dcb6a3e2ec2362c25ea49509ab10d2312b19-reverse.png',
fit: BoxFit.contain,
),
),
const SizedBox(width: 12),
// Инфо
Expanded(
child: Column(
crossAxisAlignment:
CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: Color(0xFFFFB800),
shape: BoxShape.circle,
),
),
const SizedBox(width: 6),
const Text(
'Забронирован',
style: TextStyle(
color: Colors.white,
fontSize: 12,
fontWeight: FontWeight.w600,
),
overflow: TextOverflow.ellipsis,
),
],
),
const SizedBox(height: 8),
Text(
'${widget.scooterNumber}',
style: const TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
],
),
),
],
),
),
),
],
),
),
const SizedBox(height: 16),
// КНОПКА "НАЧАТЬ ПОЕЗДКУ"
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: BlocListener<ReservedRideBloc, ReservedRideState>(
listener: (context, state) {
if (state.rideStarted) {
Navigator.pop(context);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => ActiveRideSheet(
scooterNumber: widget.scooterNumber,
initialElapsedTime: Duration.zero,
orderId: widget.orderId,
),
);
} else if (state.status ==
ReservedRideStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(state.errorMessage ?? 'Ошибка'),
),
);
}
},
child: GradientButton(
text: 'Начать поездку',
showArrows: true,
height: 48,
width: double.infinity,
fontSize: 15,
onTap: () {
_bloc.add(StartRide(widget.orderId));
},
), ),
],
),
),
const SizedBox(height: 16),
// КНОПКА "НАЧАТЬ ПОЕЗДКУ"
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: BlocListener<ReservedRideBloc, ReservedRideState>(
listener: (context, state) {
if (state.rideStarted) {
Navigator.pop(context);
showModalBottomSheet(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
builder: (context) => ActiveRideSheet(
scooterNumber: widget.scooterNumber,
initialElapsedTime: Duration.zero,
orderId: widget.orderId,
),
);
} else if (state.status == ReservedRideStatus.failure) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
);
}
},
child: GradientButton(
text: 'Начать поездку',
showArrows: true,
height: 48,
width: double.infinity,
fontSize: 15,
onTap: () {
_bloc.add(StartRide(widget.orderId));
},
), ),
), ),
),
const SizedBox(height: 12), const SizedBox(height: 12),
// КНОПКА "ОТМЕНИТЬ БРОНИРОВАНИЕ" // КНОПКА "ОТМЕНИТЬ БРОНИРОВАНИЕ"
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: BlocListener<ReservedRideBloc, ReservedRideState>( child: BlocListener<ReservedRideBloc, ReservedRideState>(
listener: (context, state) { listener: (context, state) {
if (state.rideCancelled) { if (state.rideCancelled) {
Navigator.pop(context); Navigator.pop(context);
} else if (state.status == } else if (state.status == ReservedRideStatus.failure) {
ReservedRideStatus.failure) { ScaffoldMessenger.of(context).showSnackBar(
ScaffoldMessenger.of(context).showSnackBar( SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
SnackBar( );
content: Text(state.errorMessage ?? 'Ошибка'), }
), },
); child: Container(
} height: 48,
}, decoration: BoxDecoration(
child: Container( borderRadius: BorderRadius.circular(24),
height: 48, border: Border.all(
decoration: BoxDecoration( color: Colors.white.withOpacity(0.4),
width: 1,
),
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () async {
final result = await showDialog<bool>(
context: context,
builder: (context) => const CancelBookingDialog(),
);
if (result != null && result) {
_bloc.add(CancelRide(widget.orderId));
}
},
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
border: Border.all( child: BlocBuilder<ReservedRideBloc, ReservedRideState>(
color: Colors.white.withOpacity(0.4), builder: (context, state) {
width: 1, if (state.status == ReservedRideStatus.loading) {
), return const Center(
), child: SizedBox(
child: Material( width: 20,
color: Colors.transparent, height: 20,
child: InkWell( child: CircularProgressIndicator(
onTap: () async { color: Colors.white,
final result = await showDialog<bool>( strokeWidth: 2,
context: context, ),
builder: (context) => ),
const CancelBookingDialog(), );
);
if (result != null && result) {
_bloc.add(CancelRide(widget.orderId));
} }
}, return const Center(
borderRadius: BorderRadius.circular(24), child: Text(
child: 'Отменить бронирование',
BlocBuilder< style: TextStyle(
ReservedRideBloc, color: Colors.white,
ReservedRideState fontSize: 15,
>( fontWeight: FontWeight.w600,
builder: (context, state) { ),
if (state.status ==
ReservedRideStatus.loading) {
return const Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
color: Colors.white,
strokeWidth: 2,
),
),
);
}
return const Center(
child: Text(
'Отменить бронирование',
style: TextStyle(
color: Colors.white,
fontSize: 15,
fontWeight: FontWeight.w600,
),
),
);
},
), ),
);
},
), ),
), ),
), ),
), ),
), ),
),
const SizedBox(height: 20), const SizedBox(height: 20),
], ],
),
), ),
), ),
), ),

View File

@@ -1,18 +1,13 @@
import 'package:be_happy/presentation/components/gradient_button.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import '../../domain/entities/subscription.dart'; import '../../domain/entities/subscription.dart';
import '../event/subscription_list_event.dart';
class SubscriptionCard extends StatelessWidget { class SubscriptionCard extends StatelessWidget {
final Subscription subscription; final Subscription subscription;
final bool isActive; final bool isActive;
final DateTime? expiredAt;
final VoidCallback? onRefresh;
const SubscriptionCard({super.key, required this.subscription, required this.isActive, this.expiredAt, this.onRefresh}); const SubscriptionCard({super.key, required this.subscription, required this.isActive});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@@ -20,9 +15,9 @@ class SubscriptionCard extends StatelessWidget {
? subscription.options.reduce((a, b) => a.price < b.price ? a : b) ? subscription.options.reduce((a, b) => a.price < b.price ? a : b)
: null; : null;
/*final maxDaysOption = subscription.options.isNotEmpty final maxDaysOption = subscription.options.isNotEmpty
? subscription.options.reduce((a, b) => a.days > b.days ? a : b) ? subscription.options.reduce((a, b) => a.days > b.days ? a : b)
: null;*/ : null;
return Container( return Container(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
@@ -48,7 +43,7 @@ class SubscriptionCard extends StatelessWidget {
padding: EdgeInsets.all(4), padding: EdgeInsets.all(4),
decoration: BoxDecoration( decoration: BoxDecoration(
color: Colors.white.withOpacity(0.3), color: Colors.white.withOpacity(0.3),
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12), // Опционально: скругление углов
), ),
child: Text( child: Text(
"АКТИВНА", "АКТИВНА",
@@ -69,19 +64,12 @@ class SubscriptionCard extends StatelessWidget {
subscription.shortDescription, subscription.shortDescription,
style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14), style: TextStyle(color: Colors.white.withOpacity(0.7), fontSize: 14),
), ),
if (isActive && expiredAt != null) ...[ const SizedBox(height: 16),
if (maxDaysOption != null) ...[
const SizedBox(height: 16), const SizedBox(height: 16),
Builder( Text(
builder: (context) { "Период действия: до ${maxDaysOption.days} дней",
final day = expiredAt!.day.toString().padLeft(2, '0'); style: const TextStyle(color: Colors.white, fontSize: 14),
final month = expiredAt!.month.toString().padLeft(2, '0');
final year = expiredAt!.year;
return Text(
"Период действия: до $day.$month.$year",
style: const TextStyle(color: Colors.white, fontSize: 14),
);
},
), ),
], ],
const SizedBox(height: 20), const SizedBox(height: 20),
@@ -95,17 +83,15 @@ class SubscriptionCard extends StatelessWidget {
) )
else else
const SizedBox.shrink(), const SizedBox.shrink(),
GradientButton( ElevatedButton(
onTap: () async { onPressed: () => context.push("/home/subscriptions/${subscription.id}"),
final isSubscribed = await context.push<bool>("/home/subscriptions/${subscription.id}"); style: ElevatedButton.styleFrom(
if (isSubscribed == true && onRefresh != null) { backgroundColor: const Color(0xFF80FFD1),
onRefresh!(); foregroundColor: Colors.black,
} shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
}, padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
text: "Подробнее", ),
enabled: true, child: const Text("Подробнее", style: TextStyle(fontWeight: FontWeight.bold)),
width: 120,
height: 40,
), ),
], ],
), ),

View File

@@ -30,14 +30,4 @@ class NotificationReceived extends ScooterEvent {
NotificationReceived(this.notification); NotificationReceived(this.notification);
} }
class FocusOnScooter extends ScooterEvent {
final Scooter scooter;
FocusOnScooter(this.scooter);
}
class ClearMapPlacemarks extends ScooterEvent {}
class ClearMapFocus extends ScooterEvent {}

View File

@@ -1,3 +0,0 @@
sealed class NotificationsEvent {}
class NotificationsFetchRequested extends NotificationsEvent {}

View File

@@ -1,8 +0,0 @@
sealed class PromoCodeEvent {}
class PromoCodeApplyRequested extends PromoCodeEvent {
final String code;
PromoCodeApplyRequested(this.code);
}
class PromoCodeReset extends PromoCodeEvent {}

View File

@@ -58,9 +58,9 @@ import '../../domain/usecase/remove_payment_card_usecase.dart';
import '../../domain/usecase/save_map_settings_usecase.dart'; import '../../domain/usecase/save_map_settings_usecase.dart';
import '../../domain/usecase/set_main_payment_card_usecase.dart'; import '../../domain/usecase/set_main_payment_card_usecase.dart';
import '../../domain/usecase/verify_pin_usecase.dart'; import '../../domain/usecase/verify_pin_usecase.dart';
import '../components/map_settings_sheet.dart';
import '../components/scooter_bottom_sheet.dart'; import '../components/scooter_bottom_sheet.dart';
import '../components/sheet/current_rides_sheet.dart'; import '../components/sheet/current_rides_sheet.dart';
import '../components/sheet/map_settings_sheet.dart';
import '../components/sheet/payment_method_sheet.dart'; import '../components/sheet/payment_method_sheet.dart';
import '../components/sheet/reserved_ride_sheet.dart'; import '../components/sheet/reserved_ride_sheet.dart';
import '../components/sheet/tariff_sheet.dart'; import '../components/sheet/tariff_sheet.dart';
@@ -75,7 +75,6 @@ import '../event/tariff_sheet_event.dart';
import '../event/top_up_event.dart'; import '../event/top_up_event.dart';
import '../screens/add_card_screen.dart'; // ← новый импорт import '../screens/add_card_screen.dart'; // ← новый импорт
import '../screens/license_agreement_screen.dart'; import '../screens/license_agreement_screen.dart';
import '../screens/notifications_screen.dart';
import '../screens/order_history_screen.dart'; import '../screens/order_history_screen.dart';
import '../screens/payment_methods_screen.dart'; import '../screens/payment_methods_screen.dart';
import '../screens/phone_login_screen.dart'; import '../screens/phone_login_screen.dart';
@@ -372,7 +371,6 @@ class AppRouter {
SubscriptionDetailsBloc( SubscriptionDetailsBloc(
getIt<GetSubscriptionByIdUsecase>(), getIt<GetSubscriptionByIdUsecase>(),
getIt<ActivateSubscriptionUsecase>(), getIt<ActivateSubscriptionUsecase>(),
getIt<GetClientSubscriptionsUsecase>(),
) )
..add( ..add(
LoadDetailsEvent( LoadDetailsEvent(
@@ -458,10 +456,6 @@ class AppRouter {
builder: (context, state) => const OrderHistoryScreen(), builder: (context, state) => const OrderHistoryScreen(),
routes: [] routes: []
), ),
GoRoute(
path: 'notifications',
builder: (context, state) => const NotificationsScreen(),
),
], ],
), ),
], ],

View File

@@ -19,14 +19,11 @@ class AddCardScreen extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: BlocListener<AddCardBloc, AddCardState>( body: BlocListener<AddCardBloc, AddCardState>(
listenWhen: (previous, current) { listenWhen: (previous, current) =>
print( previous.status != current.status &&
'Смена статуса: ${previous.status} -> ${current.status} ${current.errorMessage}'); current.status == AddCardStatus.success,
return previous.status != current.status &&
current.status == AddCardStatus.success;
},
listener: (context, state) { listener: (context, state) {
context.pop(true); context.pop();
}, },
child: Container( child: Container(
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
@@ -38,7 +35,6 @@ class AddCardScreen extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 16),
const CustomAppBar(title: 'Добавление карты'), const CustomAppBar(title: 'Добавление карты'),
const SizedBox(height: 24), const SizedBox(height: 24),
@@ -152,7 +148,10 @@ class AddCardScreen extends StatelessWidget {
child: InkWell( child: InkWell(
onTap: state.isFormValid onTap: state.isFormValid
? () => { ? () => {
context.read<AddCardBloc>().add(AddCardSubmitted()), context.read<AddCardBloc>().add(
AddCardSubmitted()),
context.read<PaymentMethodsBloc>()..add(PaymentMethodsStarted()),
context.pop()
} }
: null, : null,
borderRadius: BorderRadius.circular( borderRadius: BorderRadius.circular(

View File

@@ -1,5 +1,4 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import '../components/custom_app_bar.dart'; import '../components/custom_app_bar.dart';
@@ -18,10 +17,12 @@ class DocumentsScreen extends StatelessWidget {
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column( child: Column(
children: [ children: [
// ✅ Используем общий AppBar
const SizedBox(height: 16), const SizedBox(height: 16),
CustomAppBar(title: 'Документы'), CustomAppBar(title: 'Документы'),
const SizedBox(height: 32), const SizedBox(height: 32),
// Список ссылок
LinkRow( LinkRow(
icon: 'assets/icons/doc.png', icon: 'assets/icons/doc.png',
title: 'Договор аренды', title: 'Договор аренды',
@@ -32,14 +33,14 @@ class DocumentsScreen extends StatelessWidget {
LinkRow( LinkRow(
icon: 'assets/icons/doc.png', icon: 'assets/icons/doc.png',
title: 'Политика конфиденциальности', title: 'Политика конфиденциальности',
onTap: () => context.push('/privacy-policy') onTap: () => openLink('https://...'),
), ),
const Divider(height: 1, color: Colors.white24), const Divider(height: 1, color: Colors.white24),
const SizedBox(height: 12), const SizedBox(height: 12),
LinkRow( LinkRow(
icon: 'assets/icons/doc.png', icon: 'assets/icons/doc.png',
title: 'Правила вождения', title: 'Правила вождения',
onTap: () => openLink('https://behappybel.by/#rule'), onTap: () => openLink('https://...'),
), ),
const Divider(height: 1, color: Colors.white24), const Divider(height: 1, color: Colors.white24),
const SizedBox(height: 12), const SizedBox(height: 12),

View File

@@ -14,7 +14,7 @@ class LicenseAgreementScreen extends StatelessWidget {
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 16), // 🔹 APPBAR С КНОПКОЙ НАЗАД
const Padding( const Padding(
padding: EdgeInsets.symmetric(horizontal: 20), padding: EdgeInsets.symmetric(horizontal: 20),
child: CustomAppBar(title: ' '), child: CustomAppBar(title: ' '),

View File

@@ -93,9 +93,7 @@ class _MapScreenState extends State<MapScreen> {
listenWhen: (previous, current) { listenWhen: (previous, current) {
return current.lastNotification != return current.lastNotification !=
previous.lastNotification || previous.lastNotification ||
current.flags != previous.flags || current.flags != previous.flags;
previous.selectedScooterForFocus?.id
!= current.selectedScooterForFocus?.id;
}, },
listener: (context, state) { listener: (context, state) {
@@ -166,39 +164,14 @@ class _MapScreenState extends State<MapScreen> {
); );
} }
} }
if (state.selectedScooterForFocus != null) {
final targetScooter = state.selectedScooterForFocus!;
print("RESERVED SCOOTER: $targetScooter");
_moveCameraToPoint(
targetScooter.longitude,
targetScooter.latitude,
zoom: 17,
);
context.read<MapBloc>().add(ClearMapFocus());
}
}, },
buildWhen: (previous, current) { buildWhen: (previous, current) =>
return previous.scooters != current.scooters || previous.scooters != current.scooters ||
previous.reservedScooters != current.reservedScooters || previous.zones != current.zones,
previous.zones != current.zones ||
previous.status != current.status;
},
builder: (context, state) { builder: (context, state) {
final freeScooters = _buildScooterPlacemarks( final scooters = _buildScooterPlacemarks(
scooters: state.scooters, state.scooters,
iconAsset: 'assets/icons/scooter_placemark_fill.png', state.address ?? "Unknown address",
isClickable: true,
);
final reservedScooters = _buildScooterPlacemarks(
scooters: state.reservedScooters ?? [],
iconAsset: 'assets/icons/scooter_reserved_placemark_fill.png',
isClickable: false,
); );
final zonePolygons = _buildZonePolygons(state.zones); final zonePolygons = _buildZonePolygons(state.zones);
@@ -220,10 +193,9 @@ class _MapScreenState extends State<MapScreen> {
}, },
mapObjects: [ mapObjects: [
...zonePolygons, ...zonePolygons,
...reservedScooters,
ClusterizedPlacemarkCollection( ClusterizedPlacemarkCollection(
mapId: const MapObjectId('scooters_cluster'), mapId: const MapObjectId('scooters_cluster'),
placemarks: freeScooters, placemarks: scooters,
radius: 30, radius: 30,
minZoom: 15, minZoom: 15,
consumeTapEvents: true, consumeTapEvents: true,
@@ -260,6 +232,7 @@ class _MapScreenState extends State<MapScreen> {
}, },
), ),
// Индикатор загрузки (отдельный строитель для статуса)
BlocBuilder<MapBloc, ScooterState>( BlocBuilder<MapBloc, ScooterState>(
buildWhen: (previous, current) => buildWhen: (previous, current) =>
previous.status != current.status, previous.status != current.status,
@@ -405,42 +378,70 @@ class _MapScreenState extends State<MapScreen> {
} }
void _onMarkerTap(List<Scooter> scooters) async { void _onMarkerTap(List<Scooter> scooters) async {
context.read<MapBloc>().add(CheckUser());
final flags = context.read<MapBloc>().state.flags;
if (!flags.hasCard) {
_showExistingNotification(
child: PaymentNotificationCard(
onBindCard: () {
BotToast.cleanAll();
context.push("/home/payment-methods");
},
onClose: () => BotToast.cleanAll(),
),
);
return;
}
if (flags.hasUnpaidOrder) {
_showExistingNotification(
child: UnpaidOrderNotificationCard(
onClose: () => BotToast.cleanAll(),
),
);
return;
}
if (flags.hasFine) {
_showExistingNotification(
child: FineNotificationCard(
onClose: () => BotToast.cleanAll(),
),
);
return;
}
context.push( context.push(
"/home/scooter-sheet", "/home/scooter-sheet",
extra: {'scooters': scooters, 'currentLocation': _currentPosition}, extra: {'scooters': scooters, 'currentLocation': _currentPosition},
); );
/*final scoot = await showModalBottomSheet<Scooter>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
isDismissible: true,
builder: (context) {
return BlocProvider(
create: (context) =>
ScooterDetailModalBloc(
getIt<GetAddressByPointUsecase>(),
getIt<GetScooterUsecase>(),
getIt<GetPedestrianRoutesUsecase>(),
)..add(
ScooterDetailModalStarted(
scooters,
_currentPosition!.latitude,
_currentPosition!.longitude,
),
),
child: ScooterBottomSheet(),
);
},
);*/
/*bool? isBooking = false;
if (scoot != null) {
final result = await context.push('/home/scooter/${scoot.id}');
if (result == true) {
// Даем небольшую задержку, чтобы навигация завершилась корректно
await Future.delayed(Duration(milliseconds: 300), () async {
isBooking = await showModalBottomSheet<bool>(
context: context,
isScrollControlled: true,
backgroundColor: Colors.transparent,
isDismissible: true,
builder: (context) => BlocProvider(
create: (context) => TariffSheetBloc(
getIt<GetAvailableTariffsUsecase>(),
getIt<GetPaymentCardsUsecase>(),
getIt<BookScooterUsecase>(),
),
child: TariffSheet(scooter: scoot),
),
);
});
}
}
if (isBooking ?? false) {
showModalBottomSheet(
context: context,
builder: (context) => BlocProvider(
create: (context) =>
CurrentRidesBloc(getIt<GetClientOrdersUsecase>())
..add(LoadClientOrders(1)),
child: CurrentRidesSheet(clientId: 1),
),
);
}*/
} }
void _onMapSettingsTap() { void _onMapSettingsTap() {
@@ -581,26 +582,26 @@ class _MapScreenState extends State<MapScreen> {
await ClusterIconPainter.initImage('assets/icons/scooter_placemark.png'); await ClusterIconPainter.initImage('assets/icons/scooter_placemark.png');
} }
List<PlacemarkMapObject> _buildScooterPlacemarks({ List<PlacemarkMapObject> _buildScooterPlacemarks(
required List<Scooter> scooters, List<Scooter> scooters,
required String iconAsset, String address,
required bool isClickable, ) {
}) {
return scooters.map((scooter) { return scooters.map((scooter) {
return PlacemarkMapObject( return PlacemarkMapObject(
mapId: MapObjectId('${isClickable ? "" : "reserved_"}${scooter.id}'), // уникальный ID для карты mapId: MapObjectId('${scooter.id}'),
point: Point(latitude: scooter.longitude, longitude: scooter.latitude), point: Point(latitude: scooter.longitude, longitude: scooter.latitude),
icon: PlacemarkIcon.single( icon: PlacemarkIcon.single(
PlacemarkIconStyle( PlacemarkIconStyle(
image: BitmapDescriptor.fromAssetImage(iconAsset), image: BitmapDescriptor.fromAssetImage(
'assets/icons/scooter_placemark_fill.png',
),
scale: 0.2, scale: 0.2,
), ),
), ),
opacity: 1.0, opacity: 1.0,
consumeTapEvents: isClickable, onTap: (object, point) async => {
onTap: isClickable _onMarkerTap([scooter]),
? (object, point) async => _onMarkerTap([scooter]) },
: null,
); );
}).toList(); }).toList();
} }
@@ -695,13 +696,13 @@ class _MapScreenState extends State<MapScreen> {
children: [ children: [
_RoundIconButton( _RoundIconButton(
icon: Icons.notifications_sharp, icon: Icons.notifications_sharp,
onPressed: () => context.push("/home/notifications"), onPressed: _onNotificationTap,
), ),
const SizedBox(height: 12), const SizedBox(height: 12),
_RoundButton( _RoundIconButton(
imagePath: 'assets/icons/scooter_placemark.png', icon: Icons.directions_run,
onPressed: () => context.push("/home/current-rides-sheet"), onPressed: () => context.push("/home/current-rides-sheet"),
) ),
], ],
), ),
), ),
@@ -716,42 +717,7 @@ class _MapScreenState extends State<MapScreen> {
right: 0, right: 0,
child: Center( child: Center(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: () => context.push("/home/qr-info"),
final flags = context.read<MapBloc>().state.flags;
// 🔹 Проверка флагов — показываем те же карточки, что и в listener
if (!flags.hasCard) {
_showExistingNotification(
child: PaymentNotificationCard(
onBindCard: () {
BotToast.cleanAll();
context.push("/home/payment-methods");
},
onClose: () => BotToast.cleanAll(),
),
);
return;
}
if (flags.hasUnpaidOrder) {
_showExistingNotification(
child: UnpaidOrderNotificationCard(
onClose: () => BotToast.cleanAll(),
),
);
return;
}
if (flags.hasFine) {
_showExistingNotification(
child: FineNotificationCard(
onClose: () => BotToast.cleanAll(),
),
);
return;
}
// ✅ Все проверки пройдены — переход на сканирование
context.push("/home/qr-info");
},
child: Container( child: Container(
width: 64, width: 64,
height: 64, height: 64,
@@ -844,40 +810,6 @@ class _RoundIconButton extends StatelessWidget {
} }
} }
class _RoundButton extends StatelessWidget {
final String imagePath;
final VoidCallback onPressed;
const _RoundButton({
required this.imagePath,
required this.onPressed,
});
@override
Widget build(BuildContext context) {
return Container(
width: 40,
height: 40,
decoration: BoxDecoration(
color: AppColors.darkBlue,
borderRadius: BorderRadius.circular(12),
),
child: GestureDetector(
onTap: onPressed,
child: Center(
child: Image.asset(
imagePath,
width: 20,
height: 20,
fit: BoxFit.contain,
color: Colors.white,
),
),
),
);
}
}
class _CircleIconButton extends StatelessWidget { class _CircleIconButton extends StatelessWidget {
final IconData icon; final IconData icon;
final VoidCallback onPressed; final VoidCallback onPressed;
@@ -900,18 +832,3 @@ class _CircleIconButton extends StatelessWidget {
); );
} }
} }
void _showExistingNotification({required Widget child}) {
BotToast.showCustomNotification(
duration: null, // не исчезает, пока пользователь не закроет
toastBuilder: (_) {
return Container(
margin: const EdgeInsets.only(top: 120), // тот же отступ, что в listener
child: Material(
color: Colors.transparent,
child: child, // PaymentNotificationCard / UnpaidOrderNotificationCard / FineNotificationCard
),
);
},
);
}

View File

@@ -61,7 +61,7 @@ class _NewsDetailScreenState extends State<NewsDetailScreen> {
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 16), // 🔹 Заголовок в AppBar
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomAppBar(title: widget.title), child: CustomAppBar(title: widget.title),

View File

@@ -4,7 +4,6 @@ import 'dart:developer' as dev;
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import '../../di/service_locator.dart'; import '../../di/service_locator.dart';
import '../components/custom_app_bar.dart'; import '../components/custom_app_bar.dart';
import '../components/gradient_button.dart';
import '../event/news_event.dart'; import '../event/news_event.dart';
import '../state/news_state.dart'; import '../state/news_state.dart';
import '../viewmodel/news_bloc.dart'; import '../viewmodel/news_bloc.dart';
@@ -15,8 +14,11 @@ class NewsScreen extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
dev.log('🔍 NewsScreen: Создание экрана новостей');
return BlocProvider( return BlocProvider(
create: (context) { create: (context) {
dev.log('🔍 NewsScreen: Создание NewsBloc');
return getIt<NewsBloc>()..add(const NewsFetchRequested()); return getIt<NewsBloc>()..add(const NewsFetchRequested());
}, },
child: const NewsView(), child: const NewsView(),
@@ -29,6 +31,7 @@ class NewsView extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
dev.log('🔍 NewsView: Построение UI');
return Scaffold( return Scaffold(
body: Container( body: Container(
@@ -45,6 +48,7 @@ class NewsView extends StatelessWidget {
Expanded( Expanded(
child: BlocBuilder<NewsBloc, NewsState>( child: BlocBuilder<NewsBloc, NewsState>(
builder: (context, state) { builder: (context, state) {
dev.log('🔍 NewsView: Состояние ${state.status}, новостей: ${state.news.length}');
if (state.status == NewsStatus.initial || state.status == NewsStatus.loading) { if (state.status == NewsStatus.initial || state.status == NewsStatus.loading) {
return const Center( return const Center(
@@ -80,6 +84,7 @@ class NewsView extends StatelessWidget {
const SizedBox(height: 24), const SizedBox(height: 24),
ElevatedButton( ElevatedButton(
onPressed: () { onPressed: () {
dev.log('🔍 NewsView: Повторная загрузка');
context.read<NewsBloc>().add(const NewsFetchRequested()); context.read<NewsBloc>().add(const NewsFetchRequested());
}, },
style: ElevatedButton.styleFrom( style: ElevatedButton.styleFrom(
@@ -168,7 +173,7 @@ class _NewsCard extends StatelessWidget {
margin: const EdgeInsets.only(bottom: 16), margin: const EdgeInsets.only(bottom: 16),
padding: const EdgeInsets.all(16), padding: const EdgeInsets.all(16),
decoration: BoxDecoration( decoration: BoxDecoration(
color: const Color(0xFF0A0F2E).withOpacity(0.7), color: const Color(0xFF141530),
borderRadius: BorderRadius.circular(16), borderRadius: BorderRadius.circular(16),
), ),
child: Column( child: Column(
@@ -191,18 +196,6 @@ class _NewsCard extends StatelessWidget {
), ),
), ),
const SizedBox(height: 8), const SizedBox(height: 8),
Container(
width: double.infinity,
height: 80,
decoration: BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/news_def.png'),
fit: BoxFit.cover,
),
borderRadius: BorderRadius.circular(8),
),
),
const SizedBox(height: 16),
Text( Text(
news.previewText, news.previewText,
style: const TextStyle( style: const TextStyle(
@@ -212,28 +205,51 @@ class _NewsCard extends StatelessWidget {
), ),
), ),
const SizedBox(height: 16), const SizedBox(height: 16),
SizedBox(
Align( height: 40,
alignment: Alignment.centerRight, child: OutlinedButton(
child: ConstrainedBox( onPressed: () {
constraints: const BoxConstraints(maxWidth: 150), Navigator.push(
child: GradientButton( context,
text: 'Подробнее', MaterialPageRoute(
onTap: () { builder: (context) => NewsDetailScreen(
Navigator.push( newsId: news.id,
context, title: news.title,
MaterialPageRoute(
builder: (context) => NewsDetailScreen(
newsId: news.id,
title: news.title,
),
), ),
); ),
}, );
showArrows: true, },
fontSize: 14, style: OutlinedButton.styleFrom(
height: 40, shape: RoundedRectangleBorder(
width: double.infinity, borderRadius: BorderRadius.circular(24),
),
side: BorderSide(color: AppColors.smsDigit.withOpacity(0.3)),
padding: const EdgeInsets.symmetric(horizontal: 20),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Подробнее',
style: TextStyle(color: AppColors.smsDigit),
),
const SizedBox(width: 8),
Icon(
Icons.arrow_forward_ios_sharp,
size: 12,
color: AppColors.smsDigit,
),
Icon(
Icons.arrow_forward_ios_sharp,
size: 12,
color: AppColors.smsDigit.withOpacity(0.6),
),
Icon(
Icons.arrow_forward_ios_sharp,
size: 12,
color: AppColors.smsDigit.withOpacity(0.3),
),
],
), ),
), ),
), ),

View File

@@ -1,350 +0,0 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import 'dart:developer' as dev;
import '../../core/app_colors.dart';
import '../../di/service_locator.dart';
import '../../domain/entities/client_notification.dart';
import '../components/custom_app_bar.dart';
import '../event/notifications_event.dart';
import '../state/notifications_state.dart';
import '../viewmodel/notifications_bloc.dart';
enum NotificationFilter {
all,
auth,
payment,
order,
}
class NotificationsScreen extends StatelessWidget {
const NotificationsScreen({super.key});
@override
Widget build(BuildContext context) {
return _NotificationsScreenContent();
}
}
class _NotificationsScreenContent extends StatefulWidget {
const _NotificationsScreenContent();
@override
State<_NotificationsScreenContent> createState() => _NotificationsScreenContentState();
}
class _NotificationsScreenContentState extends State<_NotificationsScreenContent> {
NotificationFilter _filter = NotificationFilter.all;
void _setFilter(NotificationFilter filter) {
setState(() {
_filter = filter;
});
}
@override
Widget build(BuildContext context) {
return BlocProvider(
create: (context) => getIt<NotificationsBloc>()..add(NotificationsFetchRequested()),
child: NotificationsView(
filter: _filter,
onFilterChanged: _setFilter,
),
);
}
}
class NotificationsView extends StatelessWidget {
final NotificationFilter filter;
final ValueChanged<NotificationFilter> onFilterChanged;
const NotificationsView({
super.key,
this.filter = NotificationFilter.all,
required this.onFilterChanged,
});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
child: SafeArea(
child: Column(
children: [
const SizedBox(height: 16),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 20),
child: CustomAppBar(title: 'Уведомления'),
),
const SizedBox(height: 16),
_buildFilterBar(),
const SizedBox(height: 16),
Expanded(
child: BlocBuilder<NotificationsBloc, NotificationsState>(
builder: (context, state) {
if (state.status == NotificationsStatus.loading) {
return const Center(child: CircularProgressIndicator(color: Colors.white));
}
if (state.status == NotificationsStatus.failure) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'Ошибка загрузки уведомлений',
style: TextStyle(
color: Colors.white,
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 16),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Text(
state.errorMessage ?? '',
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 14,
),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 24),
ElevatedButton(
onPressed: () {
context.read<NotificationsBloc>().add(NotificationsFetchRequested());
},
child: const Text('Повторить'),
),
],
),
);
}
final filtered = state.notifications.where((n) {
switch (filter) {
case NotificationFilter.all:
return true;
case NotificationFilter.auth:
return n.category == NotificationCategory.auth;
case NotificationFilter.payment:
return n.category == NotificationCategory.payment;
case NotificationFilter.order:
return n.category == NotificationCategory.scooter;
// || n.category == NotificationCategory.adminInfo
// || n.category == NotificationCategory.companyInfo;
default:
return true;
}
}).toList();
if (filtered.isEmpty) {
return const _EmptyState();
}
return RefreshIndicator(
onRefresh: () async {
context.read<NotificationsBloc>().add(NotificationsFetchRequested());
},
child: ListView.builder(
padding: const EdgeInsets.symmetric(horizontal: 20),
itemCount: filtered.length,
itemBuilder: (context, index) {
return _NotificationCard(notification: filtered[index]);
},
),
);
},
),
),
],
),
),
),
);
}
Widget _buildFilterBar() {
final items = [
{'label': 'Все', 'value': NotificationFilter.all},
{'label': 'Авторизация', 'value': NotificationFilter.auth},
{'label': 'Оплата', 'value': NotificationFilter.payment},
{'label': 'Поездка', 'value': NotificationFilter.order},
];
return Container(
height: 40,
padding: const EdgeInsets.symmetric(horizontal: 20),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: items.map((item) {
final isActive = item['value'] == filter;
return Padding(
padding: const EdgeInsets.only(right: 12),
child: GestureDetector(
onTap: () => onFilterChanged(item['value'] as NotificationFilter),
child: Container(
height: 32,
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.center,
decoration: BoxDecoration(
gradient: isActive ? AppColors.activeButtonGradient : null,
color: isActive ? null : Colors.transparent,
borderRadius: BorderRadius.circular(20),
border: Border.all(
color: isActive
? Colors.transparent
: Colors.white.withOpacity(0.4),
width: 1,
),
),
child: Text(
item['label'] as String,
textAlign: TextAlign.center,
style: TextStyle(
color: isActive
? AppColors.activeButtonText
: Colors.white,
fontSize: 14,
fontWeight: isActive ? FontWeight.bold : FontWeight.normal,
),
),
),
),
);
}).toList(),
),
),
);
}
}
class _EmptyState extends StatelessWidget {
const _EmptyState();
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Image.asset(
'assets/notification_empty.png',
width: 280,
height: 280,
fit: BoxFit.contain,
errorBuilder: (context, error, stackTrace) {
return const Icon(
Icons.notifications_none_outlined,
size: 120,
color: Colors.white38,
);
},
),
const SizedBox(height: 32),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 40),
child: Text(
'У вас пока нет уведомлений.',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.white70,
fontSize: 16,
height: 1.5,
),
),
),
],
);
}
}
class _NotificationCard extends StatelessWidget {
final ClientNotification notification;
const _NotificationCard({required this.notification});
@override
Widget build(BuildContext context) {
final date = _formatDate(notification.createdAt);
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: const Color(0xFF141530).withOpacity(0.7),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
date,
style: TextStyle(
color: Colors.white.withOpacity(0.6),
fontSize: 12,
),
),
const SizedBox(height: 8),
Text(
notification.content,
style: const TextStyle(
color: Colors.white,
fontSize: 14,
),
),
],
),
);
}
String _formatDate(DateTime date) {
final now = DateTime.now();
final difference = now.difference(date);
if (difference.inDays == 0) {
return 'Сегодня, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
} else if (difference.inDays == 1) {
return 'Вчера, ${date.hour.toString().padLeft(2, '0')}:${date.minute.toString().padLeft(2, '0')}';
} else {
return '${date.day.toString().padLeft(2, '0')}.${date.month.toString().padLeft(2, '0')}.${date.year}';
}
}
String _getTypeLabel(NotificationType type) {
switch (type) {
case NotificationType.info:
return 'Информация';
case NotificationType.attention:
return 'Внимание';
case NotificationType.warning:
return 'Предупреждение';
}
}
String _getCategoryLabel(NotificationCategory category) {
switch (category) {
case NotificationCategory.auth:
return 'Авторизация';
case NotificationCategory.zone:
return 'Зоны';
case NotificationCategory.payment:
return 'Оплата';
case NotificationCategory.companyInfo:
return 'Акции';
case NotificationCategory.adminInfo:
return 'Админ';
case NotificationCategory.scooter:
return 'Самокат';
}
}
}

View File

@@ -32,7 +32,7 @@ class OrderHistoryDetailScreen extends StatelessWidget {
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 16), // 🔹 HEADER
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomAppBar(title: 'Поездка $date'), child: CustomAppBar(title: 'Поездка $date'),

View File

@@ -32,7 +32,6 @@ class OrderHistoryView extends StatelessWidget {
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 16),
const Padding( const Padding(
padding: EdgeInsets.symmetric(horizontal: 20), padding: EdgeInsets.symmetric(horizontal: 20),
child: CustomAppBar(title: 'История поездок'), child: CustomAppBar(title: 'История поездок'),

View File

@@ -9,11 +9,9 @@ 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/map_event.dart';
import '../event/payment_confirm_event.dart'; import '../event/payment_confirm_event.dart';
import '../event/payment_method_sheet_event.dart'; import '../event/payment_method_sheet_event.dart';
import '../state/payment_confirm_state.dart'; import '../state/payment_confirm_state.dart';
import '../viewmodel/map_bloc.dart';
import '../viewmodel/payment_confirm_bloc.dart'; import '../viewmodel/payment_confirm_bloc.dart';
import '../viewmodel/payment_method_sheet_bloc.dart'; import '../viewmodel/payment_method_sheet_bloc.dart';
@@ -64,7 +62,6 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
Column( Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
const SizedBox(height: 16),
const Padding( const Padding(
padding: EdgeInsets.symmetric(horizontal: 20), padding: EdgeInsets.symmetric(horizontal: 20),
child: CustomAppBar(title: 'Завершение поездки'), child: CustomAppBar(title: 'Завершение поездки'),
@@ -80,7 +77,6 @@ class _PaymentConfirmScreenContent extends StatelessWidget {
listener: (context, state) { listener: (context, state) {
if (state.status == PaymentConfirmStatus.success && state.paymentCompleted) { if (state.status == PaymentConfirmStatus.success && state.paymentCompleted) {
context.read<MapBloc>().add(ClearMapPlacemarks());
context.go('/home'); context.go('/home');
} else if (state.status == PaymentConfirmStatus.failure) { } else if (state.status == PaymentConfirmStatus.failure) {
ScaffoldMessenger.of(context).hideCurrentSnackBar(); ScaffoldMessenger.of(context).hideCurrentSnackBar();

View File

@@ -17,63 +17,42 @@ class PaymentMethodsScreen extends StatelessWidget {
body: Container( body: Container(
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
child: SafeArea( child: SafeArea(
child: BlocConsumer<PaymentMethodsBloc, PaymentMethodsState>( child: Column(
listener: (context, state) { children: [
if (state.status == PaymentMethodsStatus.failure) { const Padding(
ScaffoldMessenger.of(context).showSnackBar( padding: EdgeInsets.symmetric(horizontal: 20),
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')), child: CustomAppBar(title: 'Способы оплаты'),
); ),
} const SizedBox(height: 24),
}, Expanded(
builder: (context, state) { child: BlocConsumer<PaymentMethodsBloc, PaymentMethodsState>(
final isNetworkProcessing = state.status == PaymentMethodsStatus.loading || listener: (context, state) {
(state.isDeleting ?? false) || if (state.status == PaymentMethodsStatus.failure) {
(state.isSettingMain ?? false); ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.errorMessage ?? 'Ошибка')),
);
}
},
builder: (context, state) {
if (state.status == PaymentMethodsStatus.loading && state.cards.isEmpty) {
return const Center(child: CircularProgressIndicator(color: Color(0xFF00D4AA)));
}
return Stack( return SingleChildScrollView(
children: [ padding: const EdgeInsets.symmetric(horizontal: 20),
Column( child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.stretch,
const SizedBox(height: 16), children: [
const Padding( _buildBalanceCard(context, state.balance),
padding: EdgeInsets.symmetric(horizontal: 20), const SizedBox(height: 20),
child: CustomAppBar(title: 'Способы оплаты'), _buildCardsList(context, state),
],
), ),
const SizedBox(height: 24), );
Expanded( },
child: state.cards.isEmpty && state.status == PaymentMethodsStatus.loading ),
? const Center( ),
child: CircularProgressIndicator(color: Color(0xFF00D4AA)), ],
)
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildBalanceCard(context, state.balance),
const SizedBox(height: 20),
_buildCardsList(context, state),
],
),
),
),
],
),
if (isNetworkProcessing && state.cards.isNotEmpty)
Positioned.fill(
child: Container(
color: Colors.black.withOpacity(0.4),
child: const Center(
child: CircularProgressIndicator(
color: Color(0xFF00D4AA),
),
),
),
),
],
);
},
), ),
), ),
), ),
@@ -183,13 +162,7 @@ class PaymentMethodsScreen extends StatelessWidget {
child: Material( child: Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
onTap: () async { onTap: () => context.go('/home/payment-methods/add-card'),
final isCardAdded = await context.push<bool>('/home/payment-methods/add-card');
if (isCardAdded == true && context.mounted) {
context.read<PaymentMethodsBloc>().add(PaymentMethodsStarted());
}
},
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
child: const Padding( child: const Padding(
padding: EdgeInsets.symmetric(horizontal: 16), padding: EdgeInsets.symmetric(horizontal: 16),

View File

@@ -14,7 +14,6 @@ class PrivacyPolicyScreen extends StatelessWidget {
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 16),
const Padding( const Padding(
padding: EdgeInsets.symmetric(horizontal: 20), padding: EdgeInsets.symmetric(horizontal: 20),
child: CustomAppBar(title: ''), child: CustomAppBar(title: ''),

View File

@@ -50,9 +50,7 @@ class _ProfileScreenState extends State<ProfileScreen> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
body: Container( body: Container(
decoration: const BoxDecoration( decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
gradient: AppColors.phoneScreenBg,
),
child: SafeArea( child: SafeArea(
child: BlocBuilder<ProfileBloc, ProfileState>( child: BlocBuilder<ProfileBloc, ProfileState>(
builder: (context, state) { builder: (context, state) {
@@ -72,82 +70,56 @@ class _ProfileScreenState extends State<ProfileScreen> {
} }
final profile = state.profile!; final profile = state.profile!;
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column(
children: [
const SizedBox(height: 16),
CustomAppBar(title: 'Профиль'),
const SizedBox(height: 32),
Stack(
alignment: Alignment.topRight,
children: [
return LayoutBuilder( CircleAvatar(
builder: (context, constraints) { radius: 60,
return SingleChildScrollView( backgroundColor: AppColors.checkboxFill,
child: ConstrainedBox( backgroundImage: (profile.avatarUrl != null && profile.avatarUrl!.isNotEmpty)
constraints: BoxConstraints( ? NetworkImage("${profile.avatarUrl!}?v=${DateTime.now().minute}")
minHeight: constraints.maxHeight, : null,
), child: (profile.avatarUrl == null || profile.avatarUrl!.isEmpty)
child: Padding( ? Text(
padding: const EdgeInsets.symmetric(horizontal: 20), profile.name.isNotEmpty ? profile.name[0].toUpperCase() : '',
child: Column( style: const TextStyle(fontSize: 50, color: AppColors.darkBlue),
children: [ )
const SizedBox(height: 16), : null,
CustomAppBar(title: 'Профиль'), ), GestureDetector(
const SizedBox(height: 32), onTap: _pickImage,
child: Container(
Stack( margin: const EdgeInsets.only(top: 0, right: 0),
alignment: Alignment.topRight, child: Image.asset(
children: [ 'assets/icons/edit.png',
CircleAvatar( width: 24,
radius: 60, height: 24,
backgroundColor: AppColors.checkboxFill,
backgroundImage:
(profile.avatarUrl != null &&
profile.avatarUrl!.isNotEmpty)
? NetworkImage(
"${profile.avatarUrl!}?v=${DateTime.now().minute}",
)
: null,
child:
(profile.avatarUrl == null ||
profile.avatarUrl!.isEmpty)
? Text(
profile.name.isNotEmpty
? profile.name[0].toUpperCase()
: '',
style: const TextStyle(
fontSize: 50,
color: AppColors.darkBlue,
),
)
: null,
),
GestureDetector(
onTap: _pickImage,
child: Container(
child: Image.asset(
'assets/icons/edit.png',
width: 24,
height: 24,
),
),
),
],
), ),
),
const SizedBox(height: 32),
_ProfileInfoBlock(
profile: profile,
onEditTap: () => context.go("/home/profile/edit"),
),
// const SizedBox(height: 24),
// _SettingsBlock(
// notificationsEnabled: notificationsEnabled,
// onNotificationsChanged: (v) =>
// setState(() => notificationsEnabled = v),
// ),
],
), ),
), ],
), ),
); const SizedBox(height: 32),
}, _ProfileInfoBlock(
profile: profile,
onEditTap: () => context.go("/home/profile/edit"),
),
const SizedBox(height: 24),
_SettingsBlock(
notificationsEnabled: notificationsEnabled,
onNotificationsChanged: (v) =>
setState(() => notificationsEnabled = v),
),
const SizedBox(height: 24),
],
),
); );
}, },
), ),

View File

@@ -1,45 +1,40 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/app_colors.dart'; import '../../core/app_colors.dart';
import '../../di/service_locator.dart';
import '../../domain/usecase/apply_promo_code_usecase.dart';
import '../components/custom_app_bar.dart'; import '../components/custom_app_bar.dart';
import '../event/promo_code_event.dart';
import '../state/promo_code_state.dart';
import '../viewmodel/promo_code_bloc.dart';
class PromoCodeScreen extends StatelessWidget { class PromoCodeScreen extends StatefulWidget {
const PromoCodeScreen({super.key}); const PromoCodeScreen({super.key});
@override @override
Widget build(BuildContext context) { State<PromoCodeScreen> createState() => _PromoCodeScreenState();
return BlocProvider(
create: (context) => PromoCodeBloc(getIt<ApplyPromoCodeUsecase>()),
child: const _PromoCodeScreenContent(),
);
}
} }
class _PromoCodeScreenContent extends StatefulWidget { class _PromoCodeScreenState extends State<PromoCodeScreen> {
const _PromoCodeScreenContent();
@override
State<_PromoCodeScreenContent> createState() => _PromoCodeScreenContentState();
}
class _PromoCodeScreenContentState extends State<_PromoCodeScreenContent> {
final TextEditingController promoController = TextEditingController(); final TextEditingController promoController = TextEditingController();
bool isError = false;
@override void _activatePromo() {
void dispose() { if (promoController.text == 'G17N160') {
promoController.dispose(); ScaffoldMessenger.of(context).showSnackBar(
super.dispose(); const SnackBar(content: Text('Промокод активирован!')),
);
} else {
setState(() {
isError = true;
});
}
}
void _retry() {
setState(() {
isError = false;
promoController.clear();
});
} }
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
resizeToAvoidBottomInset: false,
body: Container( body: Container(
decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg), decoration: const BoxDecoration(gradient: AppColors.phoneScreenBg),
child: SafeArea( child: SafeArea(
@@ -50,139 +45,109 @@ class _PromoCodeScreenContentState extends State<_PromoCodeScreenContent> {
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 16),
CustomAppBar(title: 'Промокоды'), CustomAppBar(title: 'Промокоды'),
const SizedBox(height: 32), const SizedBox(height: 32),
// 🔹 LISTENER: Только сайд-эффекты (SnackBar, навигация) Container(
BlocListener<PromoCodeBloc, PromoCodeState>( padding: const EdgeInsets.all(25),
listener: (context, state) { decoration: BoxDecoration(
if (state.status == PromoCodeStatus.success) { color: Color(0xFF141530),
ScaffoldMessenger.of(context).showSnackBar( borderRadius: BorderRadius.circular(16),
SnackBar( ),
content: Text( child: Column(
'Промокод активирован! Баланс: ${state.newBalance?.toStringAsFixed(2)} BYN', crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'У вас есть промокод?',
style: TextStyle(
color: Colors.white,
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height:24),
const Text(
'Введите промокод и получите скидку на поездку',
style: TextStyle(
color: AppColors.white70,
fontSize: 16,
),
),
const SizedBox(height: 20),
TextField(
controller: promoController,
style: TextStyle(
color: isError ? Colors.red : Colors.white,
),
decoration: InputDecoration(
hintText: 'Введите промокод',
hintStyle: const TextStyle(color: AppColors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(24),
borderSide: BorderSide(color: AppColors.smsDigit, width: 1.5),
), ),
backgroundColor: Colors.green,
), ),
); ),
// Очищаем поле визуально const SizedBox(height: 20),
promoController.clear(); Row(
// Ждем чуть меньше, чтобы пользователь увидел успех children: [
Future.delayed(const Duration(milliseconds: 1200), () { Expanded(
if (mounted) Navigator.pop(context); child: OutlinedButton(
}); onPressed: () => Navigator.pop(context),
} else if (state.status == PromoCodeStatus.failure) { style: OutlinedButton.styleFrom(
ScaffoldMessenger.of(context).showSnackBar( shape: RoundedRectangleBorder(
SnackBar(
content: Text(state.errorMessage ?? 'Ошибка активации'),
backgroundColor: Colors.red,
),
);
}
},
child: BlocBuilder<PromoCodeBloc, PromoCodeState>(
builder: (context, state) {
final bool hasError = state.status == PromoCodeStatus.failure;
final bool isLoading = state.status == PromoCodeStatus.loading;
return Container(
padding: const EdgeInsets.all(25),
decoration: BoxDecoration(
color: const Color(0xFF141530),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'У вас есть промокод?',
style: TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600),
),
const SizedBox(height: 24),
const Text(
'Введите промокод и получите скидку на поездку',
style: TextStyle(color: AppColors.white70, fontSize: 16),
),
const SizedBox(height: 20),
TextField(
controller: promoController,
enabled: !isLoading,
style: TextStyle(color: hasError ? Colors.red : Colors.white),
decoration: InputDecoration(
hintText: 'Введите промокод',
hintStyle: const TextStyle(color: AppColors.white70),
filled: true,
fillColor: Colors.white.withOpacity(0.1),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(24), borderRadius: BorderRadius.circular(24),
borderSide: BorderSide.none,
), ),
enabledBorder: OutlineInputBorder( side: BorderSide(color: AppColors.white70.withOpacity(0.4)),
borderRadius: BorderRadius.circular(24), padding: const EdgeInsets.symmetric(vertical: 12),
borderSide: BorderSide.none, ),
), child: const Text(
focusedBorder: OutlineInputBorder( 'Отмена',
borderRadius: BorderRadius.circular(24), style: TextStyle(color: AppColors.white70),
borderSide: BorderSide(
color: hasError ? Colors.red : AppColors.smsDigit,
width: 1.5,
),
),
// ✅ Явно убираем ошибку при успехе
errorText: hasError ? 'Неверный промокод' : null,
), ),
onSubmitted: (_) => _submit(context),
), ),
const SizedBox(height: 20), ),
const SizedBox(width: 22),
Row( Expanded(
children: [ child: ElevatedButton(
Expanded( onPressed: _activatePromo,
child: OutlinedButton( style: ElevatedButton.styleFrom(
onPressed: isLoading ? null : () => Navigator.pop(context), shape: RoundedRectangleBorder(
style: OutlinedButton.styleFrom( borderRadius: BorderRadius.circular(24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)),
side: BorderSide(color: AppColors.white70.withOpacity(0.4)),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text('Отмена', style: TextStyle(color: AppColors.white70)),
),
), ),
const SizedBox(width: 22), backgroundColor: AppColors.activeButtonGradient.colors[0],
Expanded( padding: const EdgeInsets.symmetric(vertical: 12),
child: ElevatedButton( ),
onPressed: isLoading ? null : () => _submit(context), child: const Text(
style: ElevatedButton.styleFrom( 'Активировать',
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(24)), style: TextStyle(color: AppColors.activeButtonText),
backgroundColor: AppColors.activeButtonGradient.colors[0], ),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: isLoading
? const SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(color: AppColors.activeButtonText, strokeWidth: 2),
)
: const Text('Активировать', style: TextStyle(color: AppColors.activeButtonText)),
),
),
],
), ),
], ),
), ],
); ),
}, ],
), ),
), ),
const Spacer(), const Spacer(),
], ],
), ),
), ),
), ),
Image.asset(
'assets/promo_bottom.png',
Image.asset('assets/promo_bottom.png',
width: double.infinity, width: double.infinity,
fit: BoxFit.contain, fit: BoxFit.contain,
alignment: Alignment.center, alignment: Alignment.center,
@@ -194,10 +159,4 @@ class _PromoCodeScreenContentState extends State<_PromoCodeScreenContent> {
), ),
); );
} }
void _submit(BuildContext context) {
final code = promoController.text.trim();
if (code.isEmpty) return;
context.read<PromoCodeBloc>().add(PromoCodeApplyRequested(code));
}
} }

View File

@@ -17,7 +17,6 @@ class QRScanInfoScreen extends StatelessWidget {
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 16),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomAppBar(title: "Сканирование QR-кода"), child: CustomAppBar(title: "Сканирование QR-кода"),

View File

@@ -6,7 +6,6 @@ import 'package:mobile_scanner/mobile_scanner.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:be_happy/di/service_locator.dart'; import 'package:be_happy/di/service_locator.dart';
import 'package:be_happy/domain/usecase/get_scooter_by_title_usecase.dart'; import 'package:be_happy/domain/usecase/get_scooter_by_title_usecase.dart';
import '../components/custom_app_bar.dart';
import '../components/gradient_button.dart'; import '../components/gradient_button.dart';
class QrScanScreen extends StatefulWidget { class QrScanScreen extends StatefulWidget {
@@ -150,13 +149,11 @@ class _QrScanScreenState extends State<QrScanScreen> {
), ),
), ),
// ✅ ИЗМЕНЕНО: прижимаем аппбар к левому краю
SafeArea( SafeArea(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 40),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, // ✅ Выравнивание по левому краю
children: [ children: [
const SizedBox(height: 16),
CustomAppBar(title: "Сканирование QR-кода"),
const SizedBox(height: 60), const SizedBox(height: 60),
const Text( const Text(
'Наведите рамку на QR-код — номер будет распознан автоматически', 'Наведите рамку на QR-код — номер будет распознан автоматически',
@@ -173,6 +170,7 @@ class _QrScanScreenState extends State<QrScanScreen> {
], ],
), ),
), ),
),
SafeArea( SafeArea(
child: Align( child: Align(

View File

@@ -58,7 +58,6 @@ class _ScooterCodeInputScreenState extends State<ScooterCodeInputScreen> {
child: SafeArea( child: SafeArea(
child: Column( child: Column(
children: [ children: [
const SizedBox(height: 16),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: CustomAppBar(title: "Ввод QR-кода"), child: CustomAppBar(title: "Ввод QR-кода"),

Some files were not shown because too many files have changed in this diff Show More