diff --git a/apps/driver/lib/core/api/api_client.dart b/apps/driver/lib/core/api/api_client.dart index 33423b8..c79274f 100644 --- a/apps/driver/lib/core/api/api_client.dart +++ b/apps/driver/lib/core/api/api_client.dart @@ -28,6 +28,8 @@ class ApiClient { dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) async { options.headers['x-tenant-id'] = AppConfig.tenantSlug; + // راكب أم سائق — هويتان منفصلتان لنفس الرقم (قرار المالك 2026-07-20 — ب). + options.headers['x-app-role'] = AppConfig.appRole; final device = await _resolveDeviceId(); if (device != null) options.headers['x-device-id'] = device; diff --git a/apps/driver/pubspec.lock b/apps/driver/pubspec.lock index 056995f..f52588a 100644 --- a/apps/driver/pubspec.lock +++ b/apps/driver/pubspec.lock @@ -180,10 +180,10 @@ packages: dependency: transitive description: name: dbus - sha256: "792974a4007974fbc5c1b5433eb2330a9db3e368c3f906253af4c007d0f49a91" + sha256: "0ce9b0a839e6dee59a37a623d2fc26a35bbbe6404213e419b0d6411023d62645" url: "https://pub.dev" source: hosted - version: "0.7.13" + version: "0.7.14" device_info_plus: dependency: "direct main" description: @@ -438,10 +438,10 @@ packages: dependency: "direct dev" description: name: flutter_native_splash - sha256: "9db4b80b044e9af17cc4b1272137fc7ace0054d879ef8210a76adc34aaf4cdff" + sha256: "4fb9f4113350d3a80841ce05ebf1976a36de622af7d19aca0ca9a9911c7ff002" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.7" flutter_overlay_window: dependency: "direct main" description: @@ -640,10 +640,10 @@ packages: dependency: transitive description: name: image - sha256: "6300175e00616bbc832e2fc91bfa4d776af5402c81c7151bee6905bb08473c52" + sha256: f9881ff4998044947ec38d098bc7c8316ae1186fa786eddffdb867b9bc94dfce url: "https://pub.dev" source: hosted - version: "4.9.1" + version: "4.8.0" image_picker: dependency: "direct main" description: @@ -656,10 +656,10 @@ packages: dependency: transitive description: name: image_picker_android - sha256: "6f3a1995eafb000333174fae92202622033b0ee7fd917a6cd3730295264df84a" + sha256: d5b3e1774af29c9ab00103afb0d4614070f924d2e0057ac867ec98800114793f url: "https://pub.dev" source: hosted - version: "0.8.13+19" + version: "0.8.13+17" image_picker_for_web: dependency: transitive description: @@ -872,10 +872,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 + sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" url: "https://pub.dev" source: hosted - version: "0.12.19" + version: "0.12.18" material_color_utilities: dependency: transitive description: @@ -888,10 +888,10 @@ packages: dependency: transitive description: name: meta - sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" + sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.17.0" mime: dependency: transitive description: @@ -1127,10 +1127,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "0634e64bd719f89c012f392938e173521f535d3ecaf66558fa94a056d22b5cc7" + sha256: e8d4762b1e2e8578fc4d0fd548cebf24afd24f49719c08974df92834565e2c53 url: "https://pub.dev" source: hosted - version: "2.4.27" + version: "2.4.23" shared_preferences_foundation: dependency: transitive description: @@ -1236,10 +1236,10 @@ packages: dependency: transitive description: name: test_api - sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" + sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" url: "https://pub.dev" source: hosted - version: "0.7.11" + version: "0.7.9" timezone: dependency: transitive description: @@ -1291,10 +1291,10 @@ packages: dependency: transitive description: name: url_launcher_android - sha256: b413d49b73867ac08dd2f9890efd3cc11f2a0e577618d50843440a1fb3776c32 + sha256: "17bc677f0b301615530dd1d67e0a9828cafa2d0b6b6eae4cd3679b7eac4a273c" url: "https://pub.dev" source: hosted - version: "6.3.32" + version: "6.3.30" url_launcher_ios: dependency: transitive description: @@ -1443,10 +1443,10 @@ packages: dependency: transitive description: name: xml - sha256: "67f0aff7be013d107995e9b75bf4e7f2c3ef2dfdb2c8e68024bba0a7fd5756a4" + sha256: "971043b3a0d3da28727e40ed3e0b5d18b742fa5a68665cca88e74b7876d5e025" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "6.6.1" yaml: dependency: transitive description: @@ -1456,5 +1456,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.12.0 <4.0.0" - flutter: ">=3.44.0" + dart: ">=3.11.0 <4.0.0" + flutter: ">=3.41.0" diff --git a/apps/driver/trip_overlay_plugin/lib/trip_overlay_plugin_method_channel.dart b/apps/driver/trip_overlay_plugin/lib/trip_overlay_plugin_method_channel.dart index ec6fd65..4db3a40 100644 --- a/apps/driver/trip_overlay_plugin/lib/trip_overlay_plugin_method_channel.dart +++ b/apps/driver/trip_overlay_plugin/lib/trip_overlay_plugin_method_channel.dart @@ -9,9 +9,4 @@ class MethodChannelTripOverlayPlugin extends TripOverlayPluginPlatform { @visibleForTesting final methodChannel = const MethodChannel('trip_overlay_plugin'); - @override - Future getPlatformVersion() async { - final version = await methodChannel.invokeMethod('getPlatformVersion'); - return version; - } } diff --git a/apps/driver/trip_overlay_plugin/lib/trip_overlay_plugin_platform_interface.dart b/apps/driver/trip_overlay_plugin/lib/trip_overlay_plugin_platform_interface.dart index 00ad8c5..4473bfa 100644 --- a/apps/driver/trip_overlay_plugin/lib/trip_overlay_plugin_platform_interface.dart +++ b/apps/driver/trip_overlay_plugin/lib/trip_overlay_plugin_platform_interface.dart @@ -23,7 +23,4 @@ abstract class TripOverlayPluginPlatform extends PlatformInterface { _instance = instance; } - Future getPlatformVersion() { - throw UnimplementedError('platformVersion() has not been implemented.'); - } } diff --git a/apps/driver/trip_overlay_plugin/test/trip_overlay_plugin_method_channel_test.dart b/apps/driver/trip_overlay_plugin/test/trip_overlay_plugin_method_channel_test.dart index 64ea860..1f50843 100644 --- a/apps/driver/trip_overlay_plugin/test/trip_overlay_plugin_method_channel_test.dart +++ b/apps/driver/trip_overlay_plugin/test/trip_overlay_plugin_method_channel_test.dart @@ -1,27 +1,5 @@ -import 'package:flutter/services.dart'; import 'package:flutter_test/flutter_test.dart'; -import 'package:trip_overlay_plugin/trip_overlay_plugin_method_channel.dart'; void main() { TestWidgetsFlutterBinding.ensureInitialized(); - - MethodChannelTripOverlayPlugin platform = MethodChannelTripOverlayPlugin(); - const MethodChannel channel = MethodChannel('trip_overlay_plugin'); - - setUp(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( - channel, - (MethodCall methodCall) async { - return '42'; - }, - ); - }); - - tearDown(() { - TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); - }); - - test('getPlatformVersion', () async { - expect(await platform.getPlatformVersion(), '42'); - }); } diff --git a/apps/driver/trip_overlay_plugin/test/trip_overlay_plugin_test.dart b/apps/driver/trip_overlay_plugin/test/trip_overlay_plugin_test.dart index ff62cbf..282305a 100644 --- a/apps/driver/trip_overlay_plugin/test/trip_overlay_plugin_test.dart +++ b/apps/driver/trip_overlay_plugin/test/trip_overlay_plugin_test.dart @@ -7,9 +7,6 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; class MockTripOverlayPluginPlatform with MockPlatformInterfaceMixin implements TripOverlayPluginPlatform { - - @override - Future getPlatformVersion() => Future.value('42'); } void main() { @@ -19,11 +16,5 @@ void main() { expect(initialPlatform, isInstanceOf()); }); - test('getPlatformVersion', () async { - TripOverlayPlugin tripOverlayPlugin = TripOverlayPlugin(); - MockTripOverlayPluginPlatform fakePlatform = MockTripOverlayPluginPlatform(); - TripOverlayPluginPlatform.instance = fakePlatform; - expect(await tripOverlayPlugin.getPlatformVersion(), '42'); - }); } diff --git a/apps/rider/android/app/src/main/res/drawable-hdpi/android12splash.png b/apps/rider/android/app/src/main/res/drawable-hdpi/android12splash.png index 9e1385c..05b0cb9 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-hdpi/android12splash.png and b/apps/rider/android/app/src/main/res/drawable-hdpi/android12splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-hdpi/splash.png b/apps/rider/android/app/src/main/res/drawable-hdpi/splash.png index 9e1385c..05b0cb9 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-hdpi/splash.png and b/apps/rider/android/app/src/main/res/drawable-hdpi/splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-mdpi/android12splash.png b/apps/rider/android/app/src/main/res/drawable-mdpi/android12splash.png index 22a6568..6694368 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-mdpi/android12splash.png and b/apps/rider/android/app/src/main/res/drawable-mdpi/android12splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-mdpi/splash.png b/apps/rider/android/app/src/main/res/drawable-mdpi/splash.png index 22a6568..6694368 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-mdpi/splash.png and b/apps/rider/android/app/src/main/res/drawable-mdpi/splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-night-hdpi/android12splash.png b/apps/rider/android/app/src/main/res/drawable-night-hdpi/android12splash.png index 9e1385c..05b0cb9 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-night-hdpi/android12splash.png and b/apps/rider/android/app/src/main/res/drawable-night-hdpi/android12splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-night-mdpi/android12splash.png b/apps/rider/android/app/src/main/res/drawable-night-mdpi/android12splash.png index 22a6568..6694368 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-night-mdpi/android12splash.png and b/apps/rider/android/app/src/main/res/drawable-night-mdpi/android12splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-night-xhdpi/android12splash.png b/apps/rider/android/app/src/main/res/drawable-night-xhdpi/android12splash.png index 10b4fa8..769f7c4 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-night-xhdpi/android12splash.png and b/apps/rider/android/app/src/main/res/drawable-night-xhdpi/android12splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png b/apps/rider/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png index 5932617..5fd14db 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png and b/apps/rider/android/app/src/main/res/drawable-night-xxhdpi/android12splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png b/apps/rider/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png index 55d4af5..1c91252 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png and b/apps/rider/android/app/src/main/res/drawable-night-xxxhdpi/android12splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-xhdpi/android12splash.png b/apps/rider/android/app/src/main/res/drawable-xhdpi/android12splash.png index 10b4fa8..769f7c4 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-xhdpi/android12splash.png and b/apps/rider/android/app/src/main/res/drawable-xhdpi/android12splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-xhdpi/splash.png b/apps/rider/android/app/src/main/res/drawable-xhdpi/splash.png index 10b4fa8..769f7c4 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-xhdpi/splash.png and b/apps/rider/android/app/src/main/res/drawable-xhdpi/splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-xxhdpi/android12splash.png b/apps/rider/android/app/src/main/res/drawable-xxhdpi/android12splash.png index 5932617..5fd14db 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-xxhdpi/android12splash.png and b/apps/rider/android/app/src/main/res/drawable-xxhdpi/android12splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-xxhdpi/splash.png b/apps/rider/android/app/src/main/res/drawable-xxhdpi/splash.png index 5932617..5fd14db 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-xxhdpi/splash.png and b/apps/rider/android/app/src/main/res/drawable-xxhdpi/splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-xxxhdpi/android12splash.png b/apps/rider/android/app/src/main/res/drawable-xxxhdpi/android12splash.png index 55d4af5..1c91252 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-xxxhdpi/android12splash.png and b/apps/rider/android/app/src/main/res/drawable-xxxhdpi/android12splash.png differ diff --git a/apps/rider/android/app/src/main/res/drawable-xxxhdpi/splash.png b/apps/rider/android/app/src/main/res/drawable-xxxhdpi/splash.png index 55d4af5..1c91252 100644 Binary files a/apps/rider/android/app/src/main/res/drawable-xxxhdpi/splash.png and b/apps/rider/android/app/src/main/res/drawable-xxxhdpi/splash.png differ diff --git a/apps/rider/android/app/src/main/res/mipmap-hdpi/launcher_icon.png b/apps/rider/android/app/src/main/res/mipmap-hdpi/launcher_icon.png index 69cf9e7..5f35220 100644 Binary files a/apps/rider/android/app/src/main/res/mipmap-hdpi/launcher_icon.png and b/apps/rider/android/app/src/main/res/mipmap-hdpi/launcher_icon.png differ diff --git a/apps/rider/android/app/src/main/res/mipmap-mdpi/launcher_icon.png b/apps/rider/android/app/src/main/res/mipmap-mdpi/launcher_icon.png index a00e563..3a2be81 100644 Binary files a/apps/rider/android/app/src/main/res/mipmap-mdpi/launcher_icon.png and b/apps/rider/android/app/src/main/res/mipmap-mdpi/launcher_icon.png differ diff --git a/apps/rider/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png b/apps/rider/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png index 3873177..2165361 100644 Binary files a/apps/rider/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png and b/apps/rider/android/app/src/main/res/mipmap-xhdpi/launcher_icon.png differ diff --git a/apps/rider/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png b/apps/rider/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png index 19320e3..7514dfe 100644 Binary files a/apps/rider/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png and b/apps/rider/android/app/src/main/res/mipmap-xxhdpi/launcher_icon.png differ diff --git a/apps/rider/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png b/apps/rider/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png index 2e3238e..8df33f2 100644 Binary files a/apps/rider/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png and b/apps/rider/android/app/src/main/res/mipmap-xxxhdpi/launcher_icon.png differ diff --git a/apps/rider/assets/images/login_hero.png b/apps/rider/assets/images/login_hero.png new file mode 100644 index 0000000..34bd669 Binary files /dev/null and b/apps/rider/assets/images/login_hero.png differ diff --git a/apps/rider/assets/images/logo.png b/apps/rider/assets/images/logo.png index e9e88cf..34bd669 100644 Binary files a/apps/rider/assets/images/logo.png and b/apps/rider/assets/images/logo.png differ diff --git a/apps/rider/assets/images/name_hero.png b/apps/rider/assets/images/name_hero.png new file mode 100644 index 0000000..34bd669 Binary files /dev/null and b/apps/rider/assets/images/name_hero.png differ diff --git a/apps/rider/assets/images/onboarding_1.png b/apps/rider/assets/images/onboarding_1.png new file mode 100644 index 0000000..7f04ae5 Binary files /dev/null and b/apps/rider/assets/images/onboarding_1.png differ diff --git a/apps/rider/assets/images/onboarding_2.png b/apps/rider/assets/images/onboarding_2.png new file mode 100644 index 0000000..2b08791 Binary files /dev/null and b/apps/rider/assets/images/onboarding_2.png differ diff --git a/apps/rider/assets/images/onboarding_3.png b/apps/rider/assets/images/onboarding_3.png new file mode 100644 index 0000000..cdcb027 Binary files /dev/null and b/apps/rider/assets/images/onboarding_3.png differ diff --git a/apps/rider/assets/images/splash_logo.png b/apps/rider/assets/images/splash_logo.png new file mode 100644 index 0000000..34bd669 Binary files /dev/null and b/apps/rider/assets/images/splash_logo.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png deleted file mode 100644 index 87f76ce..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/100.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/102.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/102.png deleted file mode 100644 index 8233691..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/102.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png deleted file mode 100644 index 84825cb..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/1024.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/108.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/108.png deleted file mode 100644 index 39bbcfe..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/108.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png deleted file mode 100644 index 7208564..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/114.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png deleted file mode 100644 index 0272925..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/120.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png deleted file mode 100644 index b73bd9b..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/128.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png deleted file mode 100644 index 187bbca..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/144.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png deleted file mode 100644 index e0ad3eb..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/152.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png deleted file mode 100644 index 8d2ccb5..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/16.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png deleted file mode 100644 index 5825b02..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/167.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png deleted file mode 100644 index 141027c..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/172.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png deleted file mode 100644 index 1d97d7d..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/180.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png deleted file mode 100644 index c45114b..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/196.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png deleted file mode 100644 index dde36f4..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/20.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png deleted file mode 100644 index 72b4c56..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/216.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/234.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/234.png deleted file mode 100644 index be9f126..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/234.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png deleted file mode 100644 index fd4fb3c..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/256.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/258.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/258.png deleted file mode 100644 index 447c980..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/258.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png deleted file mode 100644 index 97bd402..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/29.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png deleted file mode 100644 index 15629ff..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/32.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png deleted file mode 100644 index d0278f4..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/40.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png deleted file mode 100644 index 4b1bbb3..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/48.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png deleted file mode 100644 index 741db10..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/50.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png deleted file mode 100644 index 1a39df9..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/512.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png deleted file mode 100644 index d8818d8..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/55.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png deleted file mode 100644 index 05db8db..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/57.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png deleted file mode 100644 index 92f1c4c..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/58.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png deleted file mode 100644 index 2bd420b..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/60.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png deleted file mode 100644 index 09a8aae..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/64.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/66.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/66.png deleted file mode 100644 index e691e2c..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/66.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png deleted file mode 100644 index 269d2e8..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/72.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png deleted file mode 100644 index 47848bd..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/76.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png deleted file mode 100644 index 11ab9a2..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/80.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png deleted file mode 100644 index 0e36ee9..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/87.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png deleted file mode 100644 index 82fdd03..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/88.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/92.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/92.png deleted file mode 100644 index 19ee2a5..0000000 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/92.png and /dev/null differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index 1319290..be71788 100644 --- a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1 +1,158 @@ -{"images":[{"size":"60x60","expected-size":"180","filename":"180.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"40x40","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"60x60","expected-size":"120","filename":"120.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"57x57","expected-size":"57","filename":"57.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"1x"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"57x57","expected-size":"114","filename":"114.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"2x"},{"size":"20x20","expected-size":"60","filename":"60.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"iphone","scale":"3x"},{"size":"1024x1024","filename":"1024.png","expected-size":"1024","idiom":"ios-marketing","folder":"Assets.xcassets/AppIcon.appiconset/","scale":"1x"},{"size":"40x40","expected-size":"80","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"72x72","expected-size":"72","filename":"72.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"76x76","expected-size":"152","filename":"152.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"50x50","expected-size":"100","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"76x76","expected-size":"76","filename":"76.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"29x29","expected-size":"29","filename":"29.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"50x50","expected-size":"50","filename":"50.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"72x72","expected-size":"144","filename":"144.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"40x40","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"83.5x83.5","expected-size":"167","filename":"167.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"size":"20x20","expected-size":"20","filename":"20.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"1x"},{"size":"20x20","expected-size":"40","filename":"40.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"ipad","scale":"2x"},{"idiom":"watch","filename":"172.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"86x86","expected-size":"172","role":"quickLook"},{"idiom":"watch","filename":"80.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"40x40","expected-size":"80","role":"appLauncher"},{"idiom":"watch","filename":"88.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"40mm","scale":"2x","size":"44x44","expected-size":"88","role":"appLauncher"},{"idiom":"watch","filename":"102.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"45mm","scale":"2x","size":"51x51","expected-size":"102","role":"appLauncher"},{"idiom":"watch","filename":"108.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"49mm","scale":"2x","size":"54x54","expected-size":"108","role":"appLauncher"},{"idiom":"watch","filename":"92.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"41mm","scale":"2x","size":"46x46","expected-size":"92","role":"appLauncher"},{"idiom":"watch","filename":"100.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"50x50","expected-size":"100","role":"appLauncher"},{"idiom":"watch","filename":"196.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"98x98","expected-size":"196","role":"quickLook"},{"idiom":"watch","filename":"216.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"44mm","scale":"2x","size":"108x108","expected-size":"216","role":"quickLook"},{"idiom":"watch","filename":"234.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"45mm","scale":"2x","size":"117x117","expected-size":"234","role":"quickLook"},{"idiom":"watch","filename":"258.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"49mm","scale":"2x","size":"129x129","expected-size":"258","role":"quickLook"},{"idiom":"watch","filename":"48.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"38mm","scale":"2x","size":"24x24","expected-size":"48","role":"notificationCenter"},{"idiom":"watch","filename":"55.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"42mm","scale":"2x","size":"27.5x27.5","expected-size":"55","role":"notificationCenter"},{"idiom":"watch","filename":"66.png","folder":"Assets.xcassets/AppIcon.appiconset/","subtype":"45mm","scale":"2x","size":"33x33","expected-size":"66","role":"notificationCenter"},{"size":"29x29","expected-size":"87","filename":"87.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"3x"},{"size":"29x29","expected-size":"58","filename":"58.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch","role":"companionSettings","scale":"2x"},{"size":"1024x1024","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"watch-marketing","scale":"1x"},{"size":"128x128","expected-size":"128","filename":"128.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"256x256","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"128x128","expected-size":"256","filename":"256.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"256x256","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"512x512","expected-size":"512","filename":"512.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"16","filename":"16.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"1x"},{"size":"16x16","expected-size":"32","filename":"32.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"32x32","expected-size":"64","filename":"64.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"},{"size":"512x512","expected-size":"1024","filename":"1024.png","folder":"Assets.xcassets/AppIcon.appiconset/","idiom":"mac","scale":"2x"}]} \ No newline at end of file +{ + "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" + } +} \ No newline at end of file diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index 86b5963..cb46088 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 1be652b..36f3434 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index e5f0eb3..d34f885 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 35f37cb..3f24811 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 49079e5..844093e 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index b2a8bcc..5620f77 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 86cb446..6654589 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index e5f0eb3..d34f885 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 5cc2302..2cde9d8 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index d1e35c1..2f0d4d8 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png index 423942b..83c8425 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png index 3791695..cfc89be 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png index 745b96a..0e71812 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png index f2ef586..7310a1f 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index d1e35c1..2f0d4d8 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 45b8005..c18828b 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png index 69cf9e7..5f35220 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png index 19320e3..7514dfe 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index bb3976b..8dbb617 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 9a2b7d0..4f8bef7 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 9c91d90..184dc96 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/apps/rider/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png index 22a6568..6694368 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png and b/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png index 10b4fa8..769f7c4 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png and b/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png differ diff --git a/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png index 5932617..5fd14db 100644 Binary files a/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png and b/apps/rider/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png differ diff --git a/apps/rider/ios/Runner/Base.lproj/LaunchScreen.storyboard b/apps/rider/ios/Runner/Base.lproj/LaunchScreen.storyboard index 2876738..6f0f33c 100644 --- a/apps/rider/ios/Runner/Base.lproj/LaunchScreen.storyboard +++ b/apps/rider/ios/Runner/Base.lproj/LaunchScreen.storyboard @@ -38,7 +38,7 @@ - + diff --git a/apps/rider/lib/core/api/antlaq_api.dart b/apps/rider/lib/core/api/antlaq_api.dart new file mode 100644 index 0000000..a1e01af --- /dev/null +++ b/apps/rider/lib/core/api/antlaq_api.dart @@ -0,0 +1,65 @@ +import 'package:dio/dio.dart'; +import '../config.dart'; + +/// واجهة مباشرة مع انطلق (map-saas) — لا مرور عبر الباك إند. +/// +/// كل نداء يمر عبر هذه النافذة: البحث، التفكيك العكسي، المسار. +/// الباك إند يبقى مسؤولاً عن المصادقة والرحلات والمدفوعات فقط. +class AntlaqApi { + final String apiKey; + final Dio _dio; + + AntlaqApi({String? apiKey}) + : apiKey = apiKey ?? AppConfig.mapApiKey, + _dio = Dio(BaseOptions( + baseUrl: 'https://map-saas.intaleqapp.com', + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 15), + // المفتاح يمر كترويسة `x-api-key` — map-saas يرفض أي مفتاح في + // query (تحقّق صارم: `property api_key should not exist`). + // المفتاح مقيّد ببصمة التطبيق (bundle id) على map-saas نفسه. + headers: {'x-api-key': apiKey ?? AppConfig.mapApiKey}, + )); + + /// بحث أماكن — GET /api/geocoding/search. + Future> search(String query) async { + final r = await _dio.get('/api/geocoding/search', queryParameters: { + 'q': query, + }); + return (r.data['results'] ?? r.data) as List; + } + + /// تفكيك عنوان عكسي — GET /api/geocoding/reverse. + /// يرجّع map-saas **مصفوفة** مرتّبة بالأقرب؛ نأخذ العنصر الأول. + Future> reverse(double lat, double lng) async { + final r = await _dio.get('/api/geocoding/reverse', queryParameters: { + 'lat': lat, + 'lng': lng, + }); + final data = r.data; + if (data is List) { + if (data.isEmpty) return {'latitude': lat, 'longitude': lng}; + return Map.from(data.first as Map); + } + return Map.from(data as Map); + } + + /// حساب مسار بين نقطتين — GET /api/maps/route. + Future> route({ + required double fromLat, + required double fromLng, + required double toLat, + required double toLng, + }) async { + final r = await _dio.get('/api/maps/route', queryParameters: { + 'fromLat': fromLat, + 'fromLng': fromLng, + 'toLat': toLat, + 'toLng': toLng, + 'alternatives': true, + 'steps': true, + 'locale': 'ar', + }); + return r.data as Map; + } +} diff --git a/apps/rider/lib/core/api/api_client.dart b/apps/rider/lib/core/api/api_client.dart index 33423b8..42ef9a6 100644 --- a/apps/rider/lib/core/api/api_client.dart +++ b/apps/rider/lib/core/api/api_client.dart @@ -3,6 +3,7 @@ import 'package:dio/dio.dart'; import 'package:device_info_plus/device_info_plus.dart'; import '../config.dart'; import '../storage/token_store.dart'; +import 'logging_interceptor.dart'; /// عميل HTTP لكل نداءات الـAPI (docs/23 §4). /// @@ -21,15 +22,22 @@ class ApiClient { ApiClient(this._store) : dio = Dio(BaseOptions( baseUrl: AppConfig.baseUrl, - connectTimeout: const Duration(seconds: 15), - receiveTimeout: const Duration(seconds: 20), + connectTimeout: const Duration(seconds: 10), + receiveTimeout: const Duration(seconds: 15), headers: {'Content-Type': 'application/json'}, )) { + // Resolve device ID once in background — don't block requests + _resolveDeviceId(); + + dio.interceptors.add(AppLoggingInterceptor()); dio.interceptors.add(InterceptorsWrapper( onRequest: (options, handler) async { options.headers['x-tenant-id'] = AppConfig.tenantSlug; + // راكب أم سائق — هويتان منفصلتان لنفس الرقم (قرار المالك 2026-07-20 — ب). + options.headers['x-app-role'] = AppConfig.appRole; - final device = await _resolveDeviceId(); + // Device ID is cached after first call — non-blocking + final device = _deviceId; if (device != null) options.headers['x-device-id'] = device; final token = await _store.access(); diff --git a/apps/rider/lib/core/api/logging_interceptor.dart b/apps/rider/lib/core/api/logging_interceptor.dart new file mode 100644 index 0000000..fda5243 --- /dev/null +++ b/apps/rider/lib/core/api/logging_interceptor.dart @@ -0,0 +1,66 @@ +import 'dart:convert'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; + +/// مساطر طباعة كل طلب HTTP — يظهر فقط في وضع التطوير. +class AppLoggingInterceptor extends Interceptor { + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + if (kDebugMode) { + final method = options.method; + final uri = options.uri; + final payload = options.data; + + debugPrint('┌─────── REQUEST ───────'); + debugPrint('│ $method $uri'); + if (payload != null) { + debugPrint('└────── Payload ───────'); + try { + final json = jsonDecode(payload.toString()); + debugPrint('${const JsonEncoder.withIndent(' ').convert(json)}'); + } catch (_) { + debugPrint('$payload'); + } + } + } + handler.next(options); + } + + @override + void onResponse(Response response, ResponseInterceptorHandler handler) { + if (kDebugMode) { + final status = response.statusCode; + final uri = response.requestOptions.uri; + final body = response.data; + + debugPrint('┌─────── RESPONSE [$status] ───────'); + debugPrint('│ $uri'); + if (body != null) { + try { + final json = jsonDecode(body.toString()); + debugPrint('${const JsonEncoder.withIndent(' ').convert(json)}'); + } catch (_) { + debugPrint('$body'); + } + } + } + handler.next(response); + } + + @override + void onError(DioException err, ErrorInterceptorHandler handler) { + if (kDebugMode) { + final status = err.response?.statusCode; + final uri = err.requestOptions.uri; + final message = err.message; + + debugPrint('┌─────── ERROR [$status] ───────'); + debugPrint('│ $uri'); + debugPrint('│ $message'); + if (err.response?.data != null) { + debugPrint('│ ${err.response?.data}'); + } + } + handler.next(err); + } +} diff --git a/apps/rider/lib/core/app_info.dart b/apps/rider/lib/core/app_info.dart new file mode 100644 index 0000000..de49174 --- /dev/null +++ b/apps/rider/lib/core/app_info.dart @@ -0,0 +1,9 @@ +/// مرجع معلومات التطبيق ورقم الإصدار الموحد. +abstract final class AppInfo { + static const String name = 'Tripz'; + static const String version = '1.0.0'; + static const int buildNumber = 1; + + /// الصيغة المنسقة لرقم الإصدار (مثل v1.0.0). + static String get formattedVersion => 'v$version'; +} diff --git a/apps/rider/lib/core/build_config.dart b/apps/rider/lib/core/build_config.dart index f1321e9..eae85b1 100644 --- a/apps/rider/lib/core/build_config.dart +++ b/apps/rider/lib/core/build_config.dart @@ -10,6 +10,7 @@ class BuildConfig { // ألوان المستأجر (البذرة) static const int primaryColorValue = 0xFF0E7C66; static const int secondaryColorValue = 0xFF0E7C66; + static const String defaultCountry = 'JO'; // أعلام الميزات — const: tree-shaking يحذف الكود المطفأ في release // docs/22 §1.5, docs/23 §5 diff --git a/apps/rider/lib/core/config.dart b/apps/rider/lib/core/config.dart index 1c993c3..9c74442 100644 --- a/apps/rider/lib/core/config.dart +++ b/apps/rider/lib/core/config.dart @@ -22,9 +22,12 @@ class AppConfig { /// دور هذا التطبيق — يميّز تطبيق الراكب عن السائق عند التسجيل. static const String appRole = 'rider'; - /// مفتاح الخريطة — يُمرَّر من البيئة. لا يُكتب في الكود (docs/23 §6). + /// مفتاح الخريطة — يُضمَّن في التطبيق ويُقيَّد ببصمة التطبيق (bundle id + /// للأندرويد/iOS) على map-saas نفسه، فلا يعمل خارج هذا التطبيق (قرار المالك + /// 2026-07-20: خرائط مباشرة إلى map-saas بلا مرور عبر الباك إند). يمكن + /// تجاوزه ببيئة البناء `--dart-define=MAP_API_KEY=...` لكل مستأجر. static const String mapApiKey = String.fromEnvironment( 'MAP_API_KEY', - defaultValue: '', + defaultValue: 'in_9478b32836d19cff73db3063', ); } diff --git a/apps/rider/lib/core/design/theme.dart b/apps/rider/lib/core/design/theme.dart index 451dbc0..aeb76a1 100644 --- a/apps/rider/lib/core/design/theme.dart +++ b/apps/rider/lib/core/design/theme.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import '../build_config.dart'; import 'tripz_colors.dart'; import 'typography.dart'; @@ -6,7 +7,7 @@ import 'typography.dart'; /// docs/26 §2, §4 ThemeData buildTheme({required Brightness brightness, required Locale locale}) { final bool isDark = brightness == Brightness.dark; - final seed = const Color(0xFF0E7C66); // أخضر تركوازي — بذرة المستأجر + final seed = Color(BuildConfig.primaryColorValue); // أخضر تركوازي — بذرة المستأجر final colorScheme = ColorScheme.fromSeed( seedColor: seed, diff --git a/apps/rider/lib/core/design/tokens.dart b/apps/rider/lib/core/design/tokens.dart index 194fb44..29cdfd5 100644 --- a/apps/rider/lib/core/design/tokens.dart +++ b/apps/rider/lib/core/design/tokens.dart @@ -11,6 +11,7 @@ abstract final class T { static const double s24 = 24; static const double s32 = 32; static const double s48 = 48; + static const double s64 = 64; /// حشوة الصفحة الافتراضية static const double pagePadding = s16; diff --git a/apps/rider/lib/core/di.dart b/apps/rider/lib/core/di.dart index e258fdd..79d4536 100644 --- a/apps/rider/lib/core/di.dart +++ b/apps/rider/lib/core/di.dart @@ -1,6 +1,8 @@ import 'package:get_it/get_it.dart'; import 'api/api_client.dart'; +import 'api/antlaq_api.dart'; import 'storage/token_store.dart'; +import 'storage/saved_places_store.dart'; import '../features/auth/data/auth_repository.dart'; import '../features/home/data/maps_repository.dart'; import '../features/home/data/tariff_repository.dart'; @@ -21,8 +23,9 @@ Future setupDi() async { ); // ميزات Phase 1: الخرائط + الأسعار + getIt.registerLazySingleton(() => AntlaqApi()); getIt.registerLazySingleton( - () => MapsRepository(getIt()), + () => MapsRepository(getIt(), getIt()), ); getIt.registerLazySingleton( () => TariffRepository(getIt()), @@ -44,4 +47,6 @@ Future setupDi() async { getIt.registerLazySingleton( () => FcmService(getIt()), ); + + getIt.registerLazySingleton(() => SavedPlacesStore()); } diff --git a/apps/rider/lib/core/l10n/app_ar.arb b/apps/rider/lib/core/l10n/app_ar.arb index db9542c..04905e3 100644 --- a/apps/rider/lib/core/l10n/app_ar.arb +++ b/apps/rider/lib/core/l10n/app_ar.arb @@ -172,6 +172,58 @@ "distance": {"type": "String"} } }, + "edit": "تعديل", + "resendIn": "إعادة إرسال خلال {seconds}ث", + "@resendIn": { + "placeholders": { + "seconds": {"type": "int"} + } + }, + "resendCode": "إعادة إرسال الرمز", + "nameTitle": "ما اسمك؟", + "nameSubtitle": "ساعدنا في تخصيص تجربتك", + "nameHint": "اسمك", + "firstNameHint": "الاسم الأول", + "lastNameHint": "اسم العائلة", + "nameButton": "متابعة", "reconnect": "جارٍ إعادة الاتصال...", - "tripExpired": "انتهت صلاحية الرحلة" + "tripExpired": "انتهت صلاحية الرحلة", + "loginTitle": "مرحباً بك في Tripz", + "phoneLoginSubtitle": "أدخل رقم هاتفك لتسجيل الدخول", + "sendOtpButton": "إرسال رمز التحقق", + "nameEntryTitle": "ما اسمك؟", + "homeTitle": "الرئيسية", + "destinationTitle": "الوجهة", + "routeTitle": "المسار", + "whereToHint": "إلى أين تريد الذهاب؟", + "currentLocation": "الموقع الحالي", + "savedPlaces": "الأماكن المحفوظة", + "home": "المنزل", + "work": "العمل", + "pickFromMap": "اختيار من الخريطة", + "searchHint": "ابحث عن مكان...", + "noResultsFound": "لا توجد نتائج", + "connecting": "جارٍ الاتصال...", + "tripSummary": "ملخص الرحلة", + "totalDistance": "المسافة الإجمالية", + "estimatedTime": "الوقت المقدر", + "totalFare": "الأجرة الإجمالية", + "chooseRide": "اختر نوع الرحلة", + "promoCode": "كود الترويج", + "errorTitle": "حدث خطأ", + "errorSubtitle": "يرجى المحاولة مرة أخرى", + "emptyTitle": "لا توجد بيانات", + "emptySubtitle": "لم يتم العثور على محتوى حالياً", + "signIn": "تسجيل الدخول", + "confirmPhoneNumber": "تأكيد رقم الهاتف", + "driverRatingValue": "التقييم", + "missingDataError": "بيانات مفقودة", + "onboardingTitle1": "اطلب بلمسة واحدة", + "onboardingSubtitle1": "أدخل وجهتك واحصل على سائق في ثوانٍ", + "onboardingTitle2": "تتبع رحلتك مباشرة", + "onboardingSubtitle2": "شاهد سائقك وهو يقترب في الوقت الفعلي", + "onboardingTitle3": "ادفع كما يناسبك", + "onboardingSubtitle3": "اختر الدفع نقدًا أو بالبطاقة أو عبر المحفظة", + "getStarted": "ابدأ الآن", + "skip": "تخطي" } \ No newline at end of file diff --git a/apps/rider/lib/core/l10n/app_en.arb b/apps/rider/lib/core/l10n/app_en.arb index 9ca179d..2ac84a2 100644 --- a/apps/rider/lib/core/l10n/app_en.arb +++ b/apps/rider/lib/core/l10n/app_en.arb @@ -172,6 +172,58 @@ "distance": {"type": "String"} } }, + "edit": "Edit", + "resendIn": "Resend in {seconds}s", + "@resendIn": { + "placeholders": { + "seconds": {"type": "int"} + } + }, + "resendCode": "Resend code", + "nameTitle": "What's your name?", + "nameSubtitle": "Help us personalize your experience", + "nameHint": "Your name", + "firstNameHint": "First name", + "lastNameHint": "Last name", + "nameButton": "Continue", "reconnect": "Reconnecting...", - "tripExpired": "Trip expired" + "tripExpired": "Trip expired", + "loginTitle": "Welcome to Tripz", + "phoneLoginSubtitle": "Enter your phone number to log in", + "sendOtpButton": "Send OTP", + "nameEntryTitle": "What's your name?", + "homeTitle": "Home", + "destinationTitle": "Destination", + "routeTitle": "Route", + "whereToHint": "Where are you going?", + "currentLocation": "Current location", + "savedPlaces": "Saved places", + "home": "Home", + "work": "Work", + "pickFromMap": "Pick from map", + "searchHint": "Search for a place...", + "noResultsFound": "No results found", + "connecting": "Connecting...", + "tripSummary": "Trip summary", + "totalDistance": "Total distance", + "estimatedTime": "Estimated time", + "totalFare": "Total fare", + "chooseRide": "Choose a ride", + "promoCode": "Promo code", + "errorTitle": "Something went wrong", + "errorSubtitle": "Please try again", + "emptyTitle": "No data", + "emptySubtitle": "No content found yet", + "signIn": "Sign in", + "confirmPhoneNumber": "Confirm phone number", + "driverRatingValue": "Rating", + "missingDataError": "Missing data", + "onboardingTitle1": "Order with one tap", + "onboardingSubtitle1": "Enter your destination and get a driver in seconds", + "onboardingTitle2": "Track your trip live", + "onboardingSubtitle2": "Watch your driver approach in real-time", + "onboardingTitle3": "Pay your way", + "onboardingSubtitle3": "Choose to pay cash, card, or wallet", + "getStarted": "Get started", + "skip": "Skip" } \ No newline at end of file diff --git a/apps/rider/lib/core/l10n/app_localizations.dart b/apps/rider/lib/core/l10n/app_localizations.dart index b77c9e5..3b7ff62 100644 --- a/apps/rider/lib/core/l10n/app_localizations.dart +++ b/apps/rider/lib/core/l10n/app_localizations.dart @@ -812,6 +812,60 @@ abstract class AppLocalizations { /// **'مسافة الرحلة: {distance}'** String tripDistance(String distance); + /// No description provided for @edit. + /// + /// In ar, this message translates to: + /// **'تعديل'** + String get edit; + + /// No description provided for @resendIn. + /// + /// In ar, this message translates to: + /// **'إعادة إرسال خلال {seconds}ث'** + String resendIn(int seconds); + + /// No description provided for @resendCode. + /// + /// In ar, this message translates to: + /// **'إعادة إرسال الرمز'** + String get resendCode; + + /// No description provided for @nameTitle. + /// + /// In ar, this message translates to: + /// **'ما اسمك؟'** + String get nameTitle; + + /// No description provided for @nameSubtitle. + /// + /// In ar, this message translates to: + /// **'ساعدنا في تخصيص تجربتك'** + String get nameSubtitle; + + /// No description provided for @nameHint. + /// + /// In ar, this message translates to: + /// **'اسمك'** + String get nameHint; + + /// No description provided for @firstNameHint. + /// + /// In ar, this message translates to: + /// **'الاسم الأول'** + String get firstNameHint; + + /// No description provided for @lastNameHint. + /// + /// In ar, this message translates to: + /// **'اسم العائلة'** + String get lastNameHint; + + /// No description provided for @nameButton. + /// + /// In ar, this message translates to: + /// **'متابعة'** + String get nameButton; + /// No description provided for @reconnect. /// /// In ar, this message translates to: @@ -823,6 +877,234 @@ abstract class AppLocalizations { /// In ar, this message translates to: /// **'انتهت صلاحية الرحلة'** String get tripExpired; + + /// No description provided for @loginTitle. + /// + /// In ar, this message translates to: + /// **'مرحباً بك في Tripz'** + String get loginTitle; + + /// No description provided for @phoneLoginSubtitle. + /// + /// In ar, this message translates to: + /// **'أدخل رقم هاتفك لتسجيل الدخول'** + String get phoneLoginSubtitle; + + /// No description provided for @sendOtpButton. + /// + /// In ar, this message translates to: + /// **'إرسال رمز التحقق'** + String get sendOtpButton; + + /// No description provided for @nameEntryTitle. + /// + /// In ar, this message translates to: + /// **'ما اسمك؟'** + String get nameEntryTitle; + + /// No description provided for @homeTitle. + /// + /// In ar, this message translates to: + /// **'الرئيسية'** + String get homeTitle; + + /// No description provided for @destinationTitle. + /// + /// In ar, this message translates to: + /// **'الوجهة'** + String get destinationTitle; + + /// No description provided for @routeTitle. + /// + /// In ar, this message translates to: + /// **'المسار'** + String get routeTitle; + + /// No description provided for @whereToHint. + /// + /// In ar, this message translates to: + /// **'إلى أين تريد الذهاب؟'** + String get whereToHint; + + /// No description provided for @currentLocation. + /// + /// In ar, this message translates to: + /// **'الموقع الحالي'** + String get currentLocation; + + /// No description provided for @savedPlaces. + /// + /// In ar, this message translates to: + /// **'الأماكن المحفوظة'** + String get savedPlaces; + + /// No description provided for @home. + /// + /// In ar, this message translates to: + /// **'المنزل'** + String get home; + + /// No description provided for @work. + /// + /// In ar, this message translates to: + /// **'العمل'** + String get work; + + /// No description provided for @pickFromMap. + /// + /// In ar, this message translates to: + /// **'اختيار من الخريطة'** + String get pickFromMap; + + /// No description provided for @searchHint. + /// + /// In ar, this message translates to: + /// **'ابحث عن مكان...'** + String get searchHint; + + /// No description provided for @noResultsFound. + /// + /// In ar, this message translates to: + /// **'لا توجد نتائج'** + String get noResultsFound; + + /// No description provided for @connecting. + /// + /// In ar, this message translates to: + /// **'جارٍ الاتصال...'** + String get connecting; + + /// No description provided for @tripSummary. + /// + /// In ar, this message translates to: + /// **'ملخص الرحلة'** + String get tripSummary; + + /// No description provided for @totalDistance. + /// + /// In ar, this message translates to: + /// **'المسافة الإجمالية'** + String get totalDistance; + + /// No description provided for @estimatedTime. + /// + /// In ar, this message translates to: + /// **'الوقت المقدر'** + String get estimatedTime; + + /// No description provided for @totalFare. + /// + /// In ar, this message translates to: + /// **'الأجرة الإجمالية'** + String get totalFare; + + /// No description provided for @chooseRide. + /// + /// In ar, this message translates to: + /// **'اختر نوع الرحلة'** + String get chooseRide; + + /// No description provided for @promoCode. + /// + /// In ar, this message translates to: + /// **'كود الترويج'** + String get promoCode; + + /// No description provided for @errorTitle. + /// + /// In ar, this message translates to: + /// **'حدث خطأ'** + String get errorTitle; + + /// No description provided for @errorSubtitle. + /// + /// In ar, this message translates to: + /// **'يرجى المحاولة مرة أخرى'** + String get errorSubtitle; + + /// No description provided for @emptyTitle. + /// + /// In ar, this message translates to: + /// **'لا توجد بيانات'** + String get emptyTitle; + + /// No description provided for @emptySubtitle. + /// + /// In ar, this message translates to: + /// **'لم يتم العثور على محتوى حالياً'** + String get emptySubtitle; + + /// No description provided for @signIn. + /// + /// In ar, this message translates to: + /// **'تسجيل الدخول'** + String get signIn; + + /// No description provided for @confirmPhoneNumber. + /// + /// In ar, this message translates to: + /// **'تأكيد رقم الهاتف'** + String get confirmPhoneNumber; + + /// No description provided for @driverRatingValue. + /// + /// In ar, this message translates to: + /// **'التقييم'** + String get driverRatingValue; + + /// No description provided for @missingDataError. + /// + /// In ar, this message translates to: + /// **'بيانات مفقودة'** + String get missingDataError; + + /// No description provided for @onboardingTitle1. + /// + /// In ar, this message translates to: + /// **'اطلب بلمسة واحدة'** + String get onboardingTitle1; + + /// No description provided for @onboardingSubtitle1. + /// + /// In ar, this message translates to: + /// **'أدخل وجهتك واحصل على سائق في ثوانٍ'** + String get onboardingSubtitle1; + + /// No description provided for @onboardingTitle2. + /// + /// In ar, this message translates to: + /// **'تتبع رحلتك مباشرة'** + String get onboardingTitle2; + + /// No description provided for @onboardingSubtitle2. + /// + /// In ar, this message translates to: + /// **'شاهد سائقك وهو يقترب في الوقت الفعلي'** + String get onboardingSubtitle2; + + /// No description provided for @onboardingTitle3. + /// + /// In ar, this message translates to: + /// **'ادفع كما يناسبك'** + String get onboardingTitle3; + + /// No description provided for @onboardingSubtitle3. + /// + /// In ar, this message translates to: + /// **'اختر الدفع نقدًا أو بالبطاقة أو عبر المحفظة'** + String get onboardingSubtitle3; + + /// No description provided for @getStarted. + /// + /// In ar, this message translates to: + /// **'ابدأ الآن'** + String get getStarted; + + /// No description provided for @skip. + /// + /// In ar, this message translates to: + /// **'تخطي'** + String get skip; } class _AppLocalizationsDelegate diff --git a/apps/rider/lib/core/l10n/app_localizations_ar.dart b/apps/rider/lib/core/l10n/app_localizations_ar.dart index 8c4c015..15fa0fd 100644 --- a/apps/rider/lib/core/l10n/app_localizations_ar.dart +++ b/apps/rider/lib/core/l10n/app_localizations_ar.dart @@ -385,9 +385,153 @@ class AppLocalizationsAr extends AppLocalizations { return 'مسافة الرحلة: $distance'; } + @override + String get edit => 'تعديل'; + + @override + String resendIn(int seconds) { + return 'إعادة إرسال خلال $secondsث'; + } + + @override + String get resendCode => 'إعادة إرسال الرمز'; + + @override + String get nameTitle => 'ما اسمك؟'; + + @override + String get nameSubtitle => 'ساعدنا في تخصيص تجربتك'; + + @override + String get nameHint => 'اسمك'; + + @override + String get firstNameHint => 'الاسم الأول'; + + @override + String get lastNameHint => 'اسم العائلة'; + + @override + String get nameButton => 'متابعة'; + @override String get reconnect => 'جارٍ إعادة الاتصال...'; @override String get tripExpired => 'انتهت صلاحية الرحلة'; + + @override + String get loginTitle => 'مرحباً بك في Tripz'; + + @override + String get phoneLoginSubtitle => 'أدخل رقم هاتفك لتسجيل الدخول'; + + @override + String get sendOtpButton => 'إرسال رمز التحقق'; + + @override + String get nameEntryTitle => 'ما اسمك؟'; + + @override + String get homeTitle => 'الرئيسية'; + + @override + String get destinationTitle => 'الوجهة'; + + @override + String get routeTitle => 'المسار'; + + @override + String get whereToHint => 'إلى أين تريد الذهاب؟'; + + @override + String get currentLocation => 'الموقع الحالي'; + + @override + String get savedPlaces => 'الأماكن المحفوظة'; + + @override + String get home => 'المنزل'; + + @override + String get work => 'العمل'; + + @override + String get pickFromMap => 'اختيار من الخريطة'; + + @override + String get searchHint => 'ابحث عن مكان...'; + + @override + String get noResultsFound => 'لا توجد نتائج'; + + @override + String get connecting => 'جارٍ الاتصال...'; + + @override + String get tripSummary => 'ملخص الرحلة'; + + @override + String get totalDistance => 'المسافة الإجمالية'; + + @override + String get estimatedTime => 'الوقت المقدر'; + + @override + String get totalFare => 'الأجرة الإجمالية'; + + @override + String get chooseRide => 'اختر نوع الرحلة'; + + @override + String get promoCode => 'كود الترويج'; + + @override + String get errorTitle => 'حدث خطأ'; + + @override + String get errorSubtitle => 'يرجى المحاولة مرة أخرى'; + + @override + String get emptyTitle => 'لا توجد بيانات'; + + @override + String get emptySubtitle => 'لم يتم العثور على محتوى حالياً'; + + @override + String get signIn => 'تسجيل الدخول'; + + @override + String get confirmPhoneNumber => 'تأكيد رقم الهاتف'; + + @override + String get driverRatingValue => 'التقييم'; + + @override + String get missingDataError => 'بيانات مفقودة'; + + @override + String get onboardingTitle1 => 'اطلب بلمسة واحدة'; + + @override + String get onboardingSubtitle1 => 'أدخل وجهتك واحصل على سائق في ثوانٍ'; + + @override + String get onboardingTitle2 => 'تتبع رحلتك مباشرة'; + + @override + String get onboardingSubtitle2 => 'شاهد سائقك وهو يقترب في الوقت الفعلي'; + + @override + String get onboardingTitle3 => 'ادفع كما يناسبك'; + + @override + String get onboardingSubtitle3 => + 'اختر الدفع نقدًا أو بالبطاقة أو عبر المحفظة'; + + @override + String get getStarted => 'ابدأ الآن'; + + @override + String get skip => 'تخطي'; } diff --git a/apps/rider/lib/core/l10n/app_localizations_en.dart b/apps/rider/lib/core/l10n/app_localizations_en.dart index 18993d9..f4c7e44 100644 --- a/apps/rider/lib/core/l10n/app_localizations_en.dart +++ b/apps/rider/lib/core/l10n/app_localizations_en.dart @@ -386,9 +386,153 @@ class AppLocalizationsEn extends AppLocalizations { return 'Trip distance: $distance'; } + @override + String get edit => 'Edit'; + + @override + String resendIn(int seconds) { + return 'Resend in ${seconds}s'; + } + + @override + String get resendCode => 'Resend code'; + + @override + String get nameTitle => 'What\'s your name?'; + + @override + String get nameSubtitle => 'Help us personalize your experience'; + + @override + String get nameHint => 'Your name'; + + @override + String get firstNameHint => 'First name'; + + @override + String get lastNameHint => 'Last name'; + + @override + String get nameButton => 'Continue'; + @override String get reconnect => 'Reconnecting...'; @override String get tripExpired => 'Trip expired'; + + @override + String get loginTitle => 'Welcome to Tripz'; + + @override + String get phoneLoginSubtitle => 'Enter your phone number to log in'; + + @override + String get sendOtpButton => 'Send OTP'; + + @override + String get nameEntryTitle => 'What\'s your name?'; + + @override + String get homeTitle => 'Home'; + + @override + String get destinationTitle => 'Destination'; + + @override + String get routeTitle => 'Route'; + + @override + String get whereToHint => 'Where are you going?'; + + @override + String get currentLocation => 'Current location'; + + @override + String get savedPlaces => 'Saved places'; + + @override + String get home => 'Home'; + + @override + String get work => 'Work'; + + @override + String get pickFromMap => 'Pick from map'; + + @override + String get searchHint => 'Search for a place...'; + + @override + String get noResultsFound => 'No results found'; + + @override + String get connecting => 'Connecting...'; + + @override + String get tripSummary => 'Trip summary'; + + @override + String get totalDistance => 'Total distance'; + + @override + String get estimatedTime => 'Estimated time'; + + @override + String get totalFare => 'Total fare'; + + @override + String get chooseRide => 'Choose a ride'; + + @override + String get promoCode => 'Promo code'; + + @override + String get errorTitle => 'Something went wrong'; + + @override + String get errorSubtitle => 'Please try again'; + + @override + String get emptyTitle => 'No data'; + + @override + String get emptySubtitle => 'No content found yet'; + + @override + String get signIn => 'Sign in'; + + @override + String get confirmPhoneNumber => 'Confirm phone number'; + + @override + String get driverRatingValue => 'Rating'; + + @override + String get missingDataError => 'Missing data'; + + @override + String get onboardingTitle1 => 'Order with one tap'; + + @override + String get onboardingSubtitle1 => + 'Enter your destination and get a driver in seconds'; + + @override + String get onboardingTitle2 => 'Track your trip live'; + + @override + String get onboardingSubtitle2 => 'Watch your driver approach in real-time'; + + @override + String get onboardingTitle3 => 'Pay your way'; + + @override + String get onboardingSubtitle3 => 'Choose to pay cash, card, or wallet'; + + @override + String get getStarted => 'Get started'; + + @override + String get skip => 'Skip'; } diff --git a/apps/rider/lib/core/router.dart b/apps/rider/lib/core/router.dart index 0b364c0..e7843d1 100644 --- a/apps/rider/lib/core/router.dart +++ b/apps/rider/lib/core/router.dart @@ -2,10 +2,14 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; import '../features/auth/cubit/auth_cubit.dart'; import '../features/auth/cubit/auth_state.dart'; +import '../features/splash/view/splash_screen.dart'; +import '../features/onboarding/view/onboarding_screen.dart'; import '../features/auth/view/phone_screen.dart'; import '../features/auth/view/otp_screen.dart'; +import '../features/auth/view/name_screen.dart'; import '../features/home/view/home_screen.dart'; import '../features/home/view/destination_screen.dart'; import '../features/home/view/route_preview_screen.dart'; @@ -15,45 +19,67 @@ import '../features/trip/view/rating_screen.dart'; import '../features/home/cubit/home_cubit.dart'; import '../features/home/cubit/destination_cubit.dart'; import '../features/home/cubit/quote_cubit.dart'; +import '../features/home/cubit/saved_places_cubit.dart'; import '../features/home/data/models/place_model.dart'; import '../features/home/data/maps_repository.dart'; import '../features/home/data/tariff_repository.dart'; +import '../core/storage/saved_places_store.dart'; +import '../core/l10n/app_localizations.dart'; import 'di.dart'; GoRouter buildRouter(AuthCubit auth) { return GoRouter( - initialLocation: '/phone', + initialLocation: '/splash', refreshListenable: _StreamRefresh(auth.stream), redirect: (context, state) { final status = auth.state.status; final loc = state.matchedLocation; - final onAuthPages = loc == '/phone' || loc == '/otp'; + final onSplash = loc == '/splash'; + final onOnboarding = loc == '/onboarding'; + final onAuthPages = loc == '/phone' || loc == '/otp' || loc == '/name'; + final onNamePage = loc == '/name'; if (status == AuthStatus.authenticated) { - return onAuthPages ? '/home' : null; + final hasName = auth.state.user?.name != null && + auth.state.user!.name!.trim().isNotEmpty; + if (!hasName && !onNamePage) return '/name'; + if (hasName && onNamePage) return '/home'; + if (onAuthPages || onSplash || onOnboarding) return hasName ? '/home' : '/name'; + return null; } + + if (onSplash || onOnboarding) return null; + if (status == AuthStatus.codeSent && loc == '/phone') return '/otp'; if (status != AuthStatus.authenticated && !onAuthPages) return '/phone'; return null; }, routes: [ + GoRoute(path: '/splash', builder: (_, __) => const SplashScreen()), + GoRoute(path: '/onboarding', builder: (_, __) => const OnboardingScreen()), GoRoute(path: '/phone', builder: (_, __) => const PhoneScreen()), GoRoute(path: '/otp', builder: (_, __) => const OtpScreen()), + GoRoute(path: '/name', builder: (_, __) => const NameScreen()), GoRoute( path: '/home', builder: (_, __) => MultiBlocProvider( providers: [ BlocProvider(create: (_) => HomeCubit(getIt())), + BlocProvider(create: (_) => SavedPlacesCubit(getIt())), ], child: const HomeScreen(), ), ), GoRoute( path: '/destination', - builder: (_, __) => BlocProvider( - create: (_) => DestinationCubit(getIt()), - child: const DestinationScreen(), - ), + builder: (_, state) { + final extra = state.extra as Map?; + final currentLocation = extra?['currentLocation'] as LatLng?; + return BlocProvider( + create: (_) => DestinationCubit(getIt()), + child: DestinationScreen(currentLocation: currentLocation), + ); + }, ), GoRoute( path: '/route-preview', @@ -62,7 +88,13 @@ GoRouter buildRouter(AuthCubit auth) { final pickup = extra?['pickup'] as PlaceModel?; final destination = extra?['destination'] as PlaceModel?; if (pickup == null || destination == null) { - return const Scaffold(body: Center(child: Text('Missing data'))); + return Builder( + builder: (ctx) => Scaffold( + body: Center( + child: Text(AppLocalizations.of(ctx)?.missingDataError ?? ''), + ), + ), + ); } return BlocProvider( create: (_) => QuoteCubit(getIt()), diff --git a/apps/rider/lib/core/storage/saved_places_store.dart b/apps/rider/lib/core/storage/saved_places_store.dart new file mode 100644 index 0000000..b16cacb --- /dev/null +++ b/apps/rider/lib/core/storage/saved_places_store.dart @@ -0,0 +1,173 @@ +import 'dart:convert'; + +import 'package:shared_preferences/shared_preferences.dart'; + +enum FavoriteType { home, work } + +class SavedPlace { + final String id; + final String name; + final String? address; + final double lat; + final double lng; + int useCount; + DateTime lastUsedAt; + FavoriteType? favoriteType; + + SavedPlace({ + required this.id, + required this.name, + this.address, + required this.lat, + required this.lng, + this.useCount = 0, + DateTime? lastUsedAt, + this.favoriteType, + }) : lastUsedAt = lastUsedAt ?? DateTime.now(); + + /// Rounding to 4 decimal places (~11m precision) for deduplication. + static double _roundCoord(double value) => + (value * 10000).roundToDouble() / 10000; + + String get dedupeKey => + '${_roundCoord(lat)},${_roundCoord(lng)}'; + + Map toJson() => { + 'id': id, + 'name': name, + 'address': address, + 'lat': lat, + 'lng': lng, + 'useCount': useCount, + 'lastUsedAt': lastUsedAt.toIso8601String(), + 'favoriteType': favoriteType?.name, + }; + + factory SavedPlace.fromJson(Map json) => SavedPlace( + id: json['id'] as String, + name: json['name'] as String, + address: json['address'] as String?, + lat: (json['lat'] as num).toDouble(), + lng: (json['lng'] as num).toDouble(), + useCount: json['useCount'] as int? ?? 0, + lastUsedAt: DateTime.parse(json['lastUsedAt'] as String), + favoriteType: json['favoriteType'] != null + ? FavoriteType.values.firstWhere( + (e) => e.name == json['favoriteType'], + ) + : null, + ); +} + +class SavedPlacesStore { + static const _key = 'saved_places'; + static const _maxItems = 50; + + Future> getAll() async { + final prefs = await SharedPreferences.getInstance(); + final raw = prefs.getString(_key); + if (raw == null) return []; + final list = (jsonDecode(raw) as List) + .map((e) => SavedPlace.fromJson(e as Map)) + .toList(); + return _sort(list); + } + + Future add(SavedPlace place) async { + final prefs = await SharedPreferences.getInstance(); + final list = await getAll(); + + final existingIndex = list.indexWhere( + (p) => p.dedupeKey == place.dedupeKey && p.favoriteType == null, + ); + + if (existingIndex != -1) { + list[existingIndex].useCount++; + list[existingIndex].lastUsedAt = DateTime.now(); + } else { + place.useCount = 1; + list.add(place); + } + + await _enforceCap(list); + await _save(prefs, list); + } + + Future remove(String placeId) async { + final prefs = await SharedPreferences.getInstance(); + final list = await getAll(); + list.removeWhere((p) => p.id == placeId); + await _save(prefs, list); + } + + Future setFavorite(String placeId, FavoriteType type) async { + final prefs = await SharedPreferences.getInstance(); + final list = await getAll(); + + // Clear any existing favorite of this type across all items. + for (final p in list) { + if (p.favoriteType == type) { + p.favoriteType = null; + } + } + + final idx = list.indexWhere((p) => p.id == placeId); + if (idx != -1) { + list[idx].favoriteType = type; + } + + await _save(prefs, list); + } + + Future clearFavorite(FavoriteType type) async { + final prefs = await SharedPreferences.getInstance(); + final list = await getAll(); + for (final p in list) { + if (p.favoriteType == type) { + p.favoriteType = null; + } + } + await _save(prefs, list); + } + + /// Removes the lowest-scoring non-favorite item when the list exceeds the cap. + /// Score = useCount (higher is better), ties broken by lastUsedAt (newer is better). + Future _enforceCap(List list) async { + if (list.length <= _maxItems) return; + + final nonFavorites = list.where((p) => p.favoriteType == null).toList() + ..sort((a, b) { + if (a.useCount != b.useCount) { + return a.useCount.compareTo(b.useCount); // ascending — lowest first + } + return a.lastUsedAt.compareTo(b.lastUsedAt); // oldest first + }); + + final toRemove = list.length - _maxItems; + final idsToRemove = + nonFavorites.take(toRemove).map((p) => p.id).toSet(); + list.removeWhere((p) => idsToRemove.contains(p.id)); + } + + List _sort(List list) { + final favoriteOrder = {FavoriteType.home: 0, FavoriteType.work: 1}; + list.sort((a, b) { + if (a.favoriteType != null && b.favoriteType == null) return -1; + if (a.favoriteType == null && b.favoriteType != null) return 1; + if (a.favoriteType != null && b.favoriteType != null) { + return favoriteOrder[a.favoriteType]! + .compareTo(favoriteOrder[b.favoriteType]!); + } + if (a.useCount != b.useCount) { + return b.useCount.compareTo(a.useCount); // descending + } + return b.lastUsedAt.compareTo(a.lastUsedAt); // newer first + }); + return list; + } + + Future _save(SharedPreferences prefs, List list) async { + final encoded = jsonEncode(list.map((p) => p.toJson()).toList()); + await prefs.setString(_key, encoded); + } +} diff --git a/apps/rider/lib/core/storage/token_store.dart b/apps/rider/lib/core/storage/token_store.dart index e091f41..9044c66 100644 --- a/apps/rider/lib/core/storage/token_store.dart +++ b/apps/rider/lib/core/storage/token_store.dart @@ -9,7 +9,17 @@ class TokenStore { await _s.write(key: 'refresh_token', value: refresh); } + Future saveUser(String userJson) async { + await _s.write(key: 'cached_user', value: userJson); + } + + Future readUser() => _s.read(key: 'cached_user'); Future access() => _s.read(key: 'access_token'); Future refresh() => _s.read(key: 'refresh_token'); + Future hasToken() async { + final token = await access(); + return token != null && token.isNotEmpty; + } Future clear() => _s.deleteAll(); } + diff --git a/apps/rider/lib/core/ui/empty_view.dart b/apps/rider/lib/core/ui/empty_view.dart deleted file mode 100644 index c79efc8..0000000 --- a/apps/rider/lib/core/ui/empty_view.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:flutter/material.dart'; -import '../design/tokens.dart'; - -/// حالة اللاشيء — رسالة + أيقونة. -/// docs/26 §7 -class EmptyView extends StatelessWidget { - const EmptyView({ - super.key, - this.icon = Icons.inbox_rounded, - this.message, - }); - - final IconData icon; - final String? message; - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - return Center( - child: Padding( - padding: const EdgeInsets.all(T.s32), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(icon, size: 64, color: cs.onSurfaceVariant.withValues(alpha: 0.4)), - const SizedBox(height: T.s16), - Text( - message ?? 'لا توجد عناصر بعد', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: cs.onSurfaceVariant, - ), - textAlign: TextAlign.center, - ), - ], - ), - ), - ); - } -} diff --git a/apps/rider/lib/core/ui/error_view.dart b/apps/rider/lib/core/ui/error_view.dart deleted file mode 100644 index 8c69850..0000000 --- a/apps/rider/lib/core/ui/error_view.dart +++ /dev/null @@ -1,48 +0,0 @@ -import 'package:flutter/material.dart'; -import '../design/tokens.dart'; - -/// حالة الخطأ — رسالة بالعربية + زر إعادة المحاولة. -/// docs/26 §7 -class ErrorView extends StatelessWidget { - const ErrorView({ - super.key, - this.message, - this.onRetry, - }); - - final String? message; - final VoidCallback? onRetry; - - @override - Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; - return Center( - child: Padding( - padding: const EdgeInsets.all(T.s32), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Icon(Icons.error_outline_rounded, - size: 64, color: cs.error.withValues(alpha: 0.6)), - const SizedBox(height: T.s16), - Text( - message ?? 'حدث خطأ ما', - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: cs.onSurface, - ), - textAlign: TextAlign.center, - ), - if (onRetry != null) ...[ - const SizedBox(height: T.s16), - FilledButton.icon( - onPressed: onRetry, - icon: const Icon(Icons.refresh_rounded, size: 20), - label: const Text('أعد المحاولة'), - ), - ], - ], - ), - ), - ); - } -} diff --git a/apps/rider/lib/core/ui/phone_intel/countries.dart b/apps/rider/lib/core/ui/phone_intel/countries.dart new file mode 100644 index 0000000..528d4fa --- /dev/null +++ b/apps/rider/lib/core/ui/phone_intel/countries.dart @@ -0,0 +1,7574 @@ +// see: https://en.wikipedia.org/wiki/List_of_country_calling_codes +// for list of country/calling codes + +const List countries = [ + Country( + name: "Afghanistan", + nameTranslations: { + "sk": "Afganistan", + "se": "Afghanistan", + "pl": "Afganistan", + "no": "Afghanistan", + "ja": "アフガニスタン", + "it": "Afghanistan", + "zh": "阿富汗", + "nl": "Afghanistan", + "de": "Afghanistan", + "fr": "Afghanistan", + "es": "Afganistán", + "en": "Afghanistan", + "pt_BR": "Afeganistão", + "sr-Cyrl": "Авганистан", + "sr-Latn": "Avganistan", + "zh_TW": "阿富汗", + "tr": "Afganistan", + "ro": "Afganistan", + "ar": "أفغانستان", + "fa": "افغانستان", + "yue": "阿富汗" + }, + flag: "🇦🇫", + code: "AF", + dialCode: "93", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Åland Islands", + nameTranslations: { + "sk": "Alandy", + "se": "Ålánda", + "pl": "Wyspy Alandzkie", + "no": "Åland", + "ja": "オーランド諸島", + "it": "Isole Åland", + "zh": "奥兰群岛", + "nl": "Åland", + "de": "Ålandinseln", + "fr": "Îles Åland", + "es": "Islas Åland", + "en": "Åland Islands", + "pt_BR": "Ilhas Aland", + "sr-Cyrl": "Аландска Острва", + "sr-Latn": "Alandska Ostrva", + "zh_TW": "奧蘭群島", + "tr": "Åland", + "ro": "Insulele Åland", + "ar": "جزر أولاند", + "fa": "جزیره اولاند", + "yue": "奧蘭群島" + }, + flag: "🇦🇽", + code: "AX", + dialCode: "358", + minLength: 15, + maxLength: 15, + ), + Country( + name: "Albania", + nameTranslations: { + "sk": "Albánsko", + "se": "Albánia", + "pl": "Albania", + "no": "Albania", + "ja": "アルバニア", + "it": "Albania", + "zh": "阿尔巴尼亚", + "nl": "Albanië", + "de": "Albanien", + "fr": "Albanie", + "es": "Albania", + "en": "Albania", + "pt_BR": "Albânia", + "sr-Cyrl": "Албанија", + "sr-Latn": "Albanija", + "zh_TW": "阿爾巴尼亞", + "tr": "Arnavutluk", + "ro": "Albania", + "ar": "ألبانيا", + "fa": "آلبانی", + "yue": "阿爾巴尼亞" + }, + flag: "🇦🇱", + code: "AL", + dialCode: "355", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Algeria", + nameTranslations: { + "sk": "Alžírsko", + "se": "Algeria", + "pl": "Algieria", + "no": "Algerie", + "ja": "アルジェリア", + "it": "Algeria", + "zh": "阿尔及利亚", + "nl": "Algerije", + "de": "Algerien", + "fr": "Algérie", + "es": "Argelia", + "en": "Algeria", + "pt_BR": "Argélia", + "sr-Cyrl": "Аргентина", + "sr-Latn": "Argentina", + "zh_TW": "阿爾及利亞", + "tr": "Cezayir", + "ro": "Algeria", + "ar": "الجزائر", + "fa": "الجزیره", + "yue": "阿爾及利亞" + }, + flag: "🇩🇿", + code: "DZ", + dialCode: "213", + minLength: 9, + maxLength: 9, + ), + Country( + name: "American Samoa", + nameTranslations: { + "sk": "Americká Samoa", + "se": "Amerihká Samoa", + "pl": "Samoa Amerykańskie", + "no": "Amerikansk Samoa", + "ja": "米領サモア", + "it": "Samoa americane", + "zh": "美属萨摩亚", + "nl": "Amerikaans-Samoa", + "de": "Amerikanisch-Samoa", + "fr": "Samoa américaines", + "es": "Samoa Americana", + "en": "American Samoa", + "pt_BR": "Samoa Americana", + "sr-Cyrl": "Америчка Самоа", + "sr-Latn": "Američka Samoa", + "zh_TW": "美屬薩摩亞", + "tr": "Amerikan Samoası", + "ro": "Samoa Americană", + "ar": "ساموا الأمريكية", + "fa": "ساموا آمریکا", + "yue": "美屬薩摩亞" + }, + flag: "🇦🇸", + code: "AS", + dialCode: "1684", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Andorra", + nameTranslations: { + "sk": "Andorra", + "se": "Andorra", + "pl": "Andora", + "no": "Andorra", + "ja": "アンドラ", + "it": "Andorra", + "zh": "安道尔", + "nl": "Andorra", + "de": "Andorra", + "fr": "Andorre", + "es": "Andorra", + "en": "Andorra", + "pt_BR": "Andorra", + "sr-Cyrl": "Андора", + "sr-Latn": "Andora", + "zh_TW": "安道爾", + "tr": "Andora", + "ro": "Andorra", + "ar": "أندورا", + "fa": "آندورا", + "yue": "安道爾" + }, + flag: "🇦🇩", + code: "AD", + dialCode: "376", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Angola", + nameTranslations: { + "sk": "Angola", + "se": "Angola", + "pl": "Angola", + "no": "Angola", + "ja": "アンゴラ", + "it": "Angola", + "zh": "安哥拉", + "nl": "Angola", + "de": "Angola", + "fr": "Angola", + "es": "Angola", + "en": "Angola", + "pt_BR": "Angola", + "sr-Cyrl": "Ангола", + "sr-Latn": "Angola", + "zh_TW": "安哥拉", + "tr": "Angola", + "ro": "Angola", + "ar": "أنغولا", + "fa": "آنگولا", + "yue": "安哥拉" + }, + flag: "🇦🇴", + code: "AO", + dialCode: "244", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Anguilla", + nameTranslations: { + "sk": "Anguilla", + "se": "Anguilla", + "pl": "Anguilla", + "no": "Anguilla", + "ja": "アンギラ", + "it": "Anguilla", + "zh": "安圭拉", + "nl": "Anguilla", + "de": "Anguilla", + "fr": "Anguilla", + "es": "Anguila", + "en": "Anguilla", + "pt_BR": "Anguilla", + "sr-Cyrl": "Ангвила", + "sr-Latn": "Angvila", + "zh_TW": "安圭拉", + "tr": "Anguilla", + "ro": "Anguilla", + "ar": "أنغويلا", + "fa": "آنگولیا", + "yue": "安圭拉" + }, + flag: "🇦🇮", + code: "AI", + dialCode: "1264", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Antarctica", + nameTranslations: { + "sk": "Antarktída", + "se": "Antárktis", + "pl": "Antarktyda", + "no": "Antarktis", + "ja": "南極", + "it": "Antartide", + "zh": "南极洲", + "nl": "Antarctica", + "de": "Antarktis", + "fr": "Antarctique", + "es": "Antártida", + "en": "Antarctica", + "pt_BR": "Antártica", + "sr-Cyrl": "Антарктик", + "sr-Latn": "Antarktik", + "zh_TW": "南極", + "tr": "Antarktika", + "ro": "Antarctica", + "ar": "القارة القطبية الجنوبية", + "fa": "قطب جنوب", + "yue": "南极洲" + }, + flag: "🇦🇶", + code: "AQ", + dialCode: "672", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Antigua and Barbuda", + nameTranslations: { + "sk": "Antigua a Barbuda", + "se": "Antigua ja Barbuda", + "pl": "Antigua i Barbuda", + "no": "Antigua og Barbuda", + "ja": "アンティグア・バーブーダ", + "it": "Antigua e Barbuda", + "zh": "安提瓜和巴布达", + "nl": "Antigua en Barbuda", + "de": "Antigua und Barbuda", + "fr": "Antigua-et-Barbuda", + "es": "Antigua y Barbuda", + "en": "Antigua & Barbuda", + "pt_BR": "Antigua e Barbuda", + "sr-Cyrl": "Антигва и Барбуда", + "sr-Latn": "Antigva i Barbuda", + "zh_TW": "安提瓜和巴布達", + "tr": "Antigua ve Barbuda", + "ro": "Antigua şi Barbuda", + "ar": "أنتيغوا وباربودا", + "fa": "آنتیگوآ و باربودا", + "yue": "安提瓜同巴布达" + }, + flag: "🇦🇬", + code: "AG", + dialCode: "1268", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Argentina", + nameTranslations: { + "sk": "Argentína", + "se": "Argentina", + "pl": "Argentyna", + "no": "Argentina", + "ja": "アルゼンチン", + "it": "Argentina", + "zh": "阿根廷", + "nl": "Argentinië", + "de": "Argentinien", + "fr": "Argentine", + "es": "Argentina", + "en": "Argentina", + "pt_BR": "Argentina", + "sr-Cyrl": "Аргентина", + "sr-Latn": "Argentina", + "zh_TW": "阿根廷", + "tr": "Arjantin", + "ro": "Argentina", + "ar": "الأرجنتين", + "fa": "آرژانتین", + "yue": "阿根廷" + }, + flag: "🇦🇷", + code: "AR", + dialCode: "54", + minLength: 12, + maxLength: 12, + ), + Country( + name: "Armenia", + nameTranslations: { + "sk": "Arménsko", + "se": "Armenia", + "pl": "Armenia", + "no": "Armenia", + "ja": "アルメニア", + "it": "Armenia", + "zh": "亚美尼亚", + "nl": "Armenië", + "de": "Armenien", + "fr": "Arménie", + "es": "Armenia", + "en": "Armenia", + "pt_BR": "Armênia", + "sr-Cyrl": "Јерменија", + "sr-Latn": "Jermenija", + "zh_TW": "亞美尼亞", + "tr": "Ermenistan", + "ro": "Armenia", + "ar": "أرمينيا", + "fa": "ارمنستان", + "yue": "亞美尼亞" + }, + flag: "🇦🇲", + code: "AM", + dialCode: "374", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Aruba", + nameTranslations: { + "sk": "Aruba", + "se": "Aruba", + "pl": "Aruba", + "no": "Aruba", + "ja": "アルバ", + "it": "Aruba", + "zh": "阿鲁巴", + "nl": "Aruba", + "de": "Aruba", + "fr": "Aruba", + "es": "Aruba", + "en": "Aruba", + "pt_BR": "Aruba", + "sr-Cyrl": "Аруба", + "sr-Latn": "Aruba", + "zh_TW": "阿魯巴", + "tr": "Aruba", + "ro": "Aruba", + "ar": "أروبا", + "fa": "آروبا", + "yue": "阿魯巴島" + }, + flag: "🇦🇼", + code: "AW", + dialCode: "297", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Australia", + nameTranslations: { + "sk": "Austrália", + "se": "Austrália", + "pl": "Australia", + "no": "Australia", + "ja": "オーストラリア", + "it": "Australia", + "zh": "澳大利亚", + "nl": "Australië", + "de": "Australien", + "fr": "Australie", + "es": "Australia", + "en": "Australia", + "pt_BR": "Austrália", + "sr-Cyrl": "Аустралија", + "sr-Latn": "Australija", + "zh_TW": "澳州", + "tr": "Avustralya", + "ro": "Australia", + "ar": "أستراليا", + "fa": "استرالیا", + "yue": "澳洲" + }, + flag: "🇦🇺", + code: "AU", + dialCode: "61", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Austria", + nameTranslations: { + "sk": "Rakúsko", + "se": "Nuortariika", + "pl": "Austria", + "no": "Østerrike", + "ja": "オーストリア", + "it": "Austria", + "zh": "奥地利", + "nl": "Oostenrijk", + "de": "Österreich", + "fr": "Autriche", + "es": "Austria", + "en": "Austria", + "pt_BR": "Áustria", + "sr-Cyrl": "Аустрија", + "sr-Latn": "Austrija", + "zh_TW": "奥地利", + "tr": "Avusturya", + "ro": "Austria", + "ar": "النمسا", + "fa": "اتریش", + "yue": "奧地利" + }, + flag: "🇦🇹", + code: "AT", + dialCode: "43", + minLength: 13, + maxLength: 13, + ), + Country( + name: "Azerbaijan", + nameTranslations: { + "sk": "Azerbajdžan", + "se": "Aserbaižan", + "pl": "Azerbejdżan", + "no": "Aserbajdsjan", + "ja": "アゼルバイジャン", + "it": "Azerbaigian", + "zh": "阿塞拜疆", + "nl": "Azerbeidzjan", + "de": "Aserbaidschan", + "fr": "Azerbaïdjan", + "es": "Azerbaiyán", + "en": "Azerbaijan", + "pt_BR": "Azerbaijão", + "sr-Cyrl": "Азербејџан", + "sr-Latn": "Azerbejdžan", + "zh_TW": "亞塞拜然", + "tr": "Azerbaycan", + "ro": "Azerbaidjan", + "ar": "أذربيجان", + "fa": "آذربایجان", + "yue": "阿塞拜疆" + }, + flag: "🇦🇿", + code: "AZ", + dialCode: "994", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Bahamas", + nameTranslations: { + "sk": "Bahamy", + "se": "Bahamas", + "pl": "Bahamy", + "no": "Bahamas", + "ja": "バハマ", + "it": "Bahamas", + "zh": "巴哈马", + "nl": "Bahama's", + "de": "Bahamas", + "fr": "Bahamas", + "es": "Bahamas", + "en": "Bahamas", + "pt_BR": "Bahamas", + "sr-Cyrl": "Бахаме", + "sr-Latn": "Bahame", + "zh_TW": "巴哈馬", + "tr": "Bahama", + "ro": "Bahamas", + "ar": "باهاماس", + "fa": "باهاماس", + "yue": "巴哈馬" + }, + flag: "🇧🇸", + code: "BS", + dialCode: "1242", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Bahrain", + nameTranslations: { + "sk": "Bahrajn", + "se": "Bahrain", + "pl": "Bahrajn", + "no": "Bahrain", + "ja": "バーレーン", + "it": "Bahrein", + "zh": "巴林", + "nl": "Bahrein", + "de": "Bahrain", + "fr": "Bahreïn", + "es": "Baréin", + "en": "Bahrain", + "pt_BR": "Bahrain", + "sr-Cyrl": "Бахреин", + "sr-Latn": "Bahrein", + "zh_TW": "巴林", + "tr": "Bahreyn", + "ro": "Bahrein", + "ar": "البحرين", + "fa": "بحرین", + "yue": "巴林" + }, + flag: "🇧🇭", + code: "BH", + dialCode: "973", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Bangladesh", + nameTranslations: { + "sk": "Bangladéš", + "se": "Bangladesh", + "pl": "Bangladesz", + "no": "Bangladesh", + "ja": "バングラデシュ", + "it": "Bangladesh", + "zh": "孟加拉国", + "nl": "Bangladesh", + "de": "Bangladesch", + "fr": "Bangladesh", + "es": "Bangladés", + "en": "Bangladesh", + "pt_BR": "Bangladesh", + "sr-Cyrl": "Бангладеш", + "sr-Latn": "Bangladeš", + "zh_TW": "孟加拉", + "tr": "Bangladeş", + "ro": "Bangladesh", + "ar": "بنغلاديش", + "fa": "بنگلادش", + "yue": "孟加拉囯" + }, + flag: "🇧🇩", + code: "BD", + dialCode: "880", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Barbados", + nameTranslations: { + "sk": "Barbados", + "se": "Barbados", + "pl": "Barbados", + "no": "Barbados", + "ja": "バルバドス", + "it": "Barbados", + "zh": "巴巴多斯", + "nl": "Barbados", + "de": "Barbados", + "fr": "Barbade", + "es": "Barbados", + "en": "Barbados", + "pt_BR": "Barbados", + "sr-Cyrl": "Барбадос", + "sr-Latn": "Barbados", + "zh_TW": "巴巴多斯", + "tr": "Barbados", + "ro": "Barbados", + "ar": "باربادوس", + "fa": "باربادوس", + "yue": "巴巴多斯" + }, + flag: "🇧🇧", + code: "BB", + dialCode: "1246", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Belarus", + nameTranslations: { + "sk": "Bielorusko", + "se": "Vilges-Ruošša", + "pl": "Białoruś", + "no": "Hviterussland", + "ja": "ベラルーシ", + "it": "Bielorussia", + "zh": "白俄罗斯", + "nl": "Belarus", + "de": "Belarus", + "fr": "Biélorussie", + "es": "Bielorrusia", + "en": "Belarus", + "pt_BR": "Bielo-Rússia", + "sr-Cyrl": "Белорусија", + "sr-Latn": "Belorusija", + "zh_TW": "白俄羅斯", + "tr": "Belarus", + "ro": "Belarus", + "ar": "بيلاروس", + "fa": "بلاروس", + "yue": "白俄羅斯" + }, + flag: "🇧🇾", + code: "BY", + dialCode: "375", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Belgium", + nameTranslations: { + "sk": "Belgicko", + "se": "Belgia", + "pl": "Belgia", + "no": "Belgia", + "ja": "ベルギー", + "it": "Belgio", + "zh": "比利时", + "nl": "België", + "de": "Belgien", + "fr": "Belgique", + "es": "Bélgica", + "en": "Belgium", + "pt_BR": "Bélgica", + "sr-Cyrl": "Белгија", + "sr-Latn": "Belgija", + "zh_TW": "比利時", + "tr": "Belçika", + "ro": "Belgia", + "ar": "بلجيكا", + "fa": "بلژیک", + "yue": "比利時" + }, + flag: "🇧🇪", + code: "BE", + dialCode: "32", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Belize", + nameTranslations: { + "sk": "Belize", + "se": "Belize", + "pl": "Belize", + "no": "Belize", + "ja": "ベリーズ", + "it": "Belize", + "zh": "伯利兹", + "nl": "Belize", + "de": "Belize", + "fr": "Belize", + "es": "Belice", + "en": "Belize", + "pt_BR": "Belize", + "sr-Cyrl": "Белизе", + "sr-Latn": "Belize", + "zh_TW": "伯利茲", + "tr": "Belize", + "ro": "Belize", + "ar": "بليز", + "fa": "بليز", + "yue": "伯利茲" + }, + flag: "🇧🇿", + code: "BZ", + dialCode: "501", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Benin", + nameTranslations: { + "sk": "Benin", + "se": "Benin", + "pl": "Benin", + "no": "Benin", + "ja": "ベナン", + "it": "Benin", + "zh": "贝宁", + "nl": "Benin", + "de": "Benin", + "fr": "Bénin", + "es": "Benín", + "en": "Benin", + "pt_BR": "Benin", + "sr-Cyrl": "Бенин", + "sr-Latn": "Benin", + "zh_TW": "貝南", + "tr": "Benin", + "ro": "Benin", + "ar": "بنين", + "fa": "بنين", + "yue": "貝寧" + }, + flag: "🇧🇯", + code: "BJ", + dialCode: "229", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Bermuda", + nameTranslations: { + "sk": "Bermudy", + "se": "Bermuda", + "pl": "Bermudy", + "no": "Bermuda", + "ja": "バミューダ", + "it": "Bermuda", + "zh": "百慕大", + "nl": "Bermuda", + "de": "Bermuda", + "fr": "Bermudes", + "es": "Bermudas", + "en": "Bermuda", + "pt_BR": "Bermudas", + "sr-Cyrl": "Бермуда", + "sr-Latn": "Bermuda", + "zh_TW": "百慕達", + "tr": "Bermuda", + "ro": "Insulele Bermude", + "ar": "برمودا", + "fa": "برمودا", + "yue": "百慕大" + }, + flag: "🇧🇲", + code: "BM", + dialCode: "1441", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Bhutan", + nameTranslations: { + "sk": "Bhután", + "se": "Bhutan", + "pl": "Bhutan", + "no": "Bhutan", + "ja": "ブータン", + "it": "Bhutan", + "zh": "不丹", + "nl": "Bhutan", + "de": "Bhutan", + "fr": "Bhoutan", + "es": "Bután", + "en": "Bhutan", + "pt_BR": "Butão", + "sr-Cyrl": "Бутан", + "sr-Latn": "Butan", + "zh_TW": "不丹", + "tr": "Bhutan", + "ro": "Bhutan", + "ar": "بوتان", + "fa": "بوتان", + "yue": "不丹" + }, + flag: "🇧🇹", + code: "BT", + dialCode: "975", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Bolivia, Plurinational State of bolivia", + nameTranslations: { + "sk": "Bolívia", + "se": "Bolivia", + "pl": "Boliwia", + "no": "Bolivia", + "ja": "ボリビア", + "it": "Bolivia", + "zh": "玻利维亚", + "nl": "Bolivia", + "de": "Bolivien", + "fr": "Bolivie", + "es": "Bolivia", + "en": "Bolivia", + "pt_BR": "Bolívia", + "sr-Cyrl": "Боливија", + "sr-Latn": "Bolivija", + "zh_TW": "玻利維亞", + "tr": "Bolivya", + "ro": "Bolivia", + "ar": "بوليفيا", + "fa": "بولیوی", + "yue": "玻利維亞(多民族國家)" + }, + flag: "🇧🇴", + code: "BO", + dialCode: "591", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Bosnia and Herzegovina", + nameTranslations: { + "sk": "Bosna a Hercegovina", + "se": "Bosnia-Hercegovina", + "pl": "Bośnia i Hercegowina", + "no": "Bosnia-Hercegovina", + "ja": "ボスニア・ヘルツェゴビナ", + "it": "Bosnia ed Erzegovina", + "zh": "波斯尼亚和黑塞哥维那", + "nl": "Bosnië en Herzegovina", + "de": "Bosnien und Herzegowina", + "fr": "Bosnie-Herzégovine", + "es": "Bosnia y Herzegovina", + "en": "Bosnia & Herzegovina", + "pt_BR": "Bósnia e Herzegovina", + "sr-Cyrl": "Босна и Херцеговина", + "sr-Latn": "Bosna i Hercegovina", + "zh_TW": "波士尼亞和黑塞哥維那", + "tr": "Bosna Hersek", + "ro": "Bosnia și Herțegovina", + "ar": "البوسنة والهرسك", + "fa": "بوسنی و هرزگوین", + "yue": "波斯尼亞黑塞哥維那" + }, + flag: "🇧🇦", + code: "BA", + dialCode: "387", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Botswana", + nameTranslations: { + "sk": "Botswana", + "se": "Botswana", + "pl": "Botswana", + "no": "Botswana", + "ja": "ボツワナ", + "it": "Botswana", + "zh": "博茨瓦纳", + "nl": "Botswana", + "de": "Botsuana", + "fr": "Botswana", + "es": "Botsuana", + "en": "Botswana", + "pt_BR": "Botswana", + "sr-Cyrl": "Боцвана", + "sr-Latn": "Bocvana", + "zh_TW": "博茨瓦納", + "tr": "Botsvana", + "ro": "Botswana", + "ar": "بوتسوانا", + "fa": "بوتسوانا", + "yue": "博茨瓦納" + }, + flag: "🇧🇼", + code: "BW", + dialCode: "267", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Bouvet Island", + nameTranslations: { + "sk": "Bouvetov ostrov", + "se": "Bouvet-sullot", + "pl": "Wyspa Bouveta", + "no": "Bouvetøya", + "ja": "ブーベ島", + "it": "Isola Bouvet", + "zh": "布韦岛", + "nl": "Bouveteiland", + "de": "Bouvetinsel", + "fr": "Île Bouvet", + "es": "Isla Bouvet", + "en": "Bouvet Island", + "pt_BR": "Ilha Bouvet", + "sr-Cyrl": "Острво Буве", + "sr-Latn": "Ostrvo Buve", + "zh_TW": "布維特島", + "tr": "Bouvet Adası", + "ro": "Insula Bouvet", + "ar": "جزيرة بوفيه", + "fa": "جزیره بووه", + "yue": "布维特岛" + }, + flag: "🇧🇻", + code: "BV", + dialCode: "47", + minLength: 15, + maxLength: 15, + ), + Country( + name: "Brazil", + nameTranslations: { + "sk": "Brazília", + "se": "Brasil", + "pl": "Brazylia", + "no": "Brasil", + "ja": "ブラジル", + "it": "Brasile", + "zh": "巴西", + "nl": "Brazilië", + "de": "Brasilien", + "fr": "Brésil", + "es": "Brasil", + "en": "Brazil", + "pt_BR": "Brasil", + "sr-Cyrl": "Бразил", + "sr-Latn": "Brazil", + "zh_TW": "巴西", + "tr": "Brezilya", + "ro": "Brazilia", + "ar": "البرازيل", + "fa": "برزیل", + "yue": "巴西" + }, + flag: "🇧🇷", + code: "BR", + dialCode: "55", + minLength: 11, + maxLength: 11, + ), + Country( + name: "British Indian Ocean Territory", + nameTranslations: { + "sk": "Britské indickooceánske územie", + "se": "British Indian Ocean Territory", + "pl": "Brytyjskie Terytorium Oceanu Indyjskiego", + "no": "Det britiske territoriet i Indiahavet", + "ja": "英領インド洋地域", + "it": "Territorio britannico dell'Oceano Indiano", + "zh": "英属印度洋领地", + "nl": "Brits Indische Oceaanterritorium", + "de": "Britisches Territorium im Indischen Ozean", + "fr": "Territoire britannique de l'océan Indien", + "es": "Territorio Británico del Océano Índico", + "en": "British Indian Ocean Territory", + "pt_BR": "Território Britânico do Oceano Índico", + "sr-Cyrl": "Британска територија Индијског океана", + "sr-Latn": "Britanska teritorija Indijskog okeana", + "zh_TW": "英屬印度洋領地", + "tr": "Britanya Hint Okyanusu Toprakları", + "ro": "Teritoriul Britanic din Oceanul Indian", + "ar": "إقليم المحيط الهندي البريطاني", + "fa": "سرزمین دریایی هند - بریتانیا", + "yue": "英屬印度洋領土" + }, + flag: "🇮🇴", + code: "IO", + dialCode: "246", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Brunei Darussalam", + nameTranslations: { + "sk": "Brunej", + "se": "Brunei", + "pl": "Brunei", + "no": "Brunei", + "ja": "ブルネイ", + "it": "Brunei", + "zh": "文莱", + "nl": "Brunei", + "de": "Brunei Darussalam", + "fr": "Brunéi Darussalam", + "es": "Brunéi", + "en": "Brunei", + "pt_BR": "Brunei", + "sr-Cyrl": "Брунеј", + "sr-Latn": "Brunej", + "zh_TW": "汶萊", + "tr": "Bruney", + "ro": "Brunei", + "ar": "بروناي", + "fa": "برونئی", + "yue": "文萊達魯薩蘭國" + }, + flag: "🇧🇳", + code: "BN", + dialCode: "673", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Bulgaria", + nameTranslations: { + "sk": "Bulharsko", + "se": "Bulgária", + "pl": "Bułgaria", + "no": "Bulgaria", + "ja": "ブルガリア", + "it": "Bulgaria", + "zh": "保加利亚", + "nl": "Bulgarije", + "de": "Bulgarien", + "fr": "Bulgarie", + "es": "Bulgaria", + "en": "Bulgaria", + "pt_BR": "Bulgária", + "sr-Cyrl": "Бугарска", + "sr-Latn": "Bugarska", + "zh_TW": "保加利亞", + "tr": "Bulgaristan", + "ro": "Bulgaria", + "ar": "بلغاريا", + "fa": "بلغارستان", + "yue": "保加利亞" + }, + flag: "🇧🇬", + code: "BG", + dialCode: "359", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Burkina Faso", + nameTranslations: { + "sk": "Burkina Faso", + "se": "Burkina Faso", + "pl": "Burkina Faso", + "no": "Burkina Faso", + "ja": "ブルキナファソ", + "it": "Burkina Faso", + "zh": "布基纳法索", + "nl": "Burkina Faso", + "de": "Burkina Faso", + "fr": "Burkina Faso", + "es": "Burkina Faso", + "en": "Burkina Faso", + "pt_BR": "Burkina Faso", + "sr-Cyrl": "Буркина Фасо", + "sr-Latn": "Burkina Faso", + "zh_TW": "布吉納法索", + "tr": "Burkina Faso", + "ro": "Burkina Faso", + "ar": "بوركينا فاسو", + "fa": "بورکینافاسو", + "yue": "布基納法索" + }, + flag: "🇧🇫", + code: "BF", + dialCode: "226", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Burundi", + nameTranslations: { + "sk": "Burundi", + "se": "Burundi", + "pl": "Burundi", + "no": "Burundi", + "ja": "ブルンジ", + "it": "Burundi", + "zh": "布隆迪", + "nl": "Burundi", + "de": "Burundi", + "fr": "Burundi", + "es": "Burundi", + "en": "Burundi", + "pt_BR": "Burundi", + "sr-Cyrl": "Бурунди", + "sr-Latn": "Burundi", + "zh_TW": "蒲隆地", + "tr": "Burundi", + "ro": "Burundi", + "ar": "بوروندي", + "fa": "بوروندی", + "yue": "蒲隆地" + }, + flag: "🇧🇮", + code: "BI", + dialCode: "257", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Cambodia", + nameTranslations: { + "sk": "Kambodža", + "se": "Kambodža", + "pl": "Kambodża", + "no": "Kambodsja", + "ja": "カンボジア", + "it": "Cambogia", + "zh": "柬埔寨", + "nl": "Cambodja", + "de": "Kambodscha", + "fr": "Cambodge", + "es": "Camboya", + "en": "Cambodia", + "pt_BR": "Camboja", + "sr-Cyrl": "Камбоџа", + "sr-Latn": "Kambodža", + "zh_TW": "柬埔寨", + "tr": "Kamboçya", + "ro": "Cambogia", + "ar": "كمبوديا", + "fa": "کامبوج", + "yue": "柬埔寨" + }, + flag: "🇰🇭", + code: "KH", + dialCode: "855", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Cameroon", + nameTranslations: { + "sk": "Kamerun", + "se": "Kamerun", + "pl": "Kamerun", + "no": "Kamerun", + "ja": "カメルーン", + "it": "Camerun", + "zh": "喀麦隆", + "nl": "Kameroen", + "de": "Kamerun", + "fr": "Cameroun", + "es": "Camerún", + "en": "Cameroon", + "pt_BR": "Camarões", + "sr-Cyrl": "Камерун", + "sr-Latn": "Kamerun", + "zh_TW": "喀麥隆", + "tr": "Kamerun", + "ro": "Camerun", + "ar": "الكاميرون", + "fa": "کامرون", + "yue": "喀 麥 隆" + }, + flag: "🇨🇲", + code: "CM", + dialCode: "237", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Canada", + nameTranslations: { + "sk": "Kanada", + "se": "Kanáda", + "pl": "Kanada", + "no": "Canada", + "ja": "カナダ", + "it": "Canada", + "zh": "加拿大", + "nl": "Canada", + "de": "Kanada", + "fr": "Canada", + "es": "Canadá", + "en": "Canada", + "pt_BR": "Canadá", + "sr-Cyrl": "Канада", + "sr-Latn": "Kanada", + "zh_TW": "加拿大", + "tr": "Kanada", + "ro": "Canada", + "ar": "كندا", + "fa": "کانادا", + "yue": "加拿大" + }, + flag: "🇨🇦", + code: "CA", + dialCode: "1", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Cayman Islands", + nameTranslations: { + "sk": "Kajmanie ostrovy", + "se": "Cayman-sullot", + "pl": "Kajmany", + "no": "Caymanøyene", + "ja": "ケイマン諸島", + "it": "Isole Cayman", + "zh": "开曼群岛", + "nl": "Kaaimaneilanden", + "de": "Kaimaninseln", + "fr": "Îles Caïmans", + "es": "Islas Caimán", + "en": "Cayman Islands", + "pt_BR": "Ilhas Cayman", + "sr-Cyrl": "Кајманска Острва", + "sr-Latn": "Kajmanska Ostrva", + "zh_TW": "開曼群島", + "tr": "Cayman Adaları", + "ro": "Insulele Cayman", + "ar": "جزر كايمان", + "fa": "جزایر کیمن", + "yue": "開曼群島" + }, + flag: "🇰🇾", + code: "KY", + dialCode: "345", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Central African Republic", + nameTranslations: { + "sk": "Stredoafrická republika", + "se": "Gaska-Afrihká dásseváldi", + "pl": "Republika Środkowoafrykańska", + "no": "Den sentralafrikanske republikk", + "ja": "中央アフリカ共和国", + "it": "Repubblica Centrafricana", + "zh": "中非共和国", + "nl": "Centraal-Afrikaanse Republiek", + "de": "Zentralafrikanische Republik", + "fr": "République centrafricaine", + "es": "República Centroafricana", + "en": "Central African Republic", + "pt_BR": "República Centro-Africana", + "sr-Cyrl": "Централноафричка Република", + "sr-Latn": "Centralnoafrička Republika", + "zh_TW": "中非共和國", + "tr": "Orta Afrika Cumhuriyeti", + "ro": "Republica Centrafricană", + "ar": "جمهورية أفريقيا الوسطى", + "fa": "جمهوری افریقای مرکزی", + "yue": "中非共和國" + }, + flag: "🇨🇫", + code: "CF", + dialCode: "236", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Chad", + nameTranslations: { + "sk": "Čad", + "se": "Tčad", + "pl": "Czad", + "no": "Tsjad", + "ja": "チャド", + "it": "Ciad", + "zh": "乍得", + "nl": "Tsjaad", + "de": "Tschad", + "fr": "Tchad", + "es": "Chad", + "en": "Chad", + "pt_BR": "Chade", + "sr-Cyrl": "Чад", + "sr-Latn": "Čad", + "zh_TW": "查德", + "tr": "Çad", + "ro": "Ciad", + "ar": "تشاد", + "fa": "چاد", + "yue": "乍得" + }, + flag: "🇹🇩", + code: "TD", + dialCode: "235", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Chile", + nameTranslations: { + "sk": "Čile", + "se": "Čiile", + "pl": "Chile", + "no": "Chile", + "ja": "チリ", + "it": "Cile", + "zh": "智利", + "nl": "Chili", + "de": "Chile", + "fr": "Chili", + "es": "Chile", + "en": "Chile", + "pt_BR": "Chile", + "sr-Cyrl": "Чиле", + "sr-Latn": "Čile", + "zh_TW": "智利", + "tr": "Şili", + "ro": "Chile", + "ar": "تشيلي", + "fa": "شیلی", + "yue": "智利" + }, + flag: "🇨🇱", + code: "CL", + dialCode: "56", + minLength: 9, + maxLength: 9, + ), + Country( + name: "China", + nameTranslations: { + "sk": "Čína", + "se": "Kiinná", + "pl": "Chiny", + "no": "Kina", + "ja": "中国", + "it": "Cina", + "zh": "中国", + "nl": "China", + "de": "China", + "fr": "Chine", + "es": "China", + "en": "China", + "pt_BR": "China", + "sr-Cyrl": "Кина", + "sr-Latn": "Kina", + "zh_TW": "中國", + "tr": "Çin", + "ro": "China", + "ar": "الصين", + "fa": "چین", + "yue": "中國" + }, + flag: "🇨🇳", + code: "CN", + dialCode: "86", + minLength: 11, + maxLength: 12, + ), + Country( + name: "Christmas Island", + nameTranslations: { + "sk": "Vianočný ostrov", + "se": "Juovllat-sullot", + "pl": "Wyspa Bożego Narodzenia", + "no": "Christmasøya", + "ja": "クリスマス島", + "it": "Isola Christmas", + "zh": "圣诞岛", + "nl": "Christmaseiland", + "de": "Weihnachtsinsel", + "fr": "Île Christmas", + "es": "Isla de Navidad", + "en": "Christmas Island", + "pt_BR": "Ilha do Natal", + "sr-Cyrl": "Ускршња Острва", + "sr-Latn": "Uskršnja Ostrva", + "zh_TW": "聖誕島", + "tr": "Christmas Adası", + "ro": "Insula Crăciunului", + "ar": "جزيرة عيد الميلاد", + "fa": "جزیره کریسمس", + "yue": "聖誕島" + }, + flag: "🇨🇽", + code: "CX", + dialCode: "61", + minLength: 15, + maxLength: 15, + ), + Country( + name: "Cocos (Keeling) Islands", + nameTranslations: { + "sk": "Kokosové ostrovy", + "se": "Cocos-sullot", + "pl": "Wyspy Kokosowe", + "no": "Kokosøyene", + "ja": "ココス(キーリング)諸島", + "it": "Isole Cocos (Keeling)", + "zh": "科科斯(基林)群岛", + "nl": "Cocoseilanden", + "de": "Kokosinseln", + "fr": "Îles Cocos", + "es": "Islas Cocos", + "en": "Cocos (Keeling) Islands", + "pt_BR": "Ilhas Cocos (Keeling)", + "sr-Cyrl": "Кокосова Острва", + "sr-Latn": "Kokosova Ostrva", + "zh_TW": "科科斯(基林)群島", + "tr": "Cocos (Keyling) Adaları", + "ro": "Insulele Cocos", + "ar": "جزر كوكوس", + "fa": "جزایر کوکوس", + "yue": "可可島(基林)群島" + }, + flag: "🇨🇨", + code: "CC", + dialCode: "61", + minLength: 15, + maxLength: 15, + ), + Country( + name: "Colombia", + nameTranslations: { + "sk": "Kolumbia", + "se": "Kolombia", + "pl": "Kolumbia", + "no": "Colombia", + "ja": "コロンビア", + "it": "Colombia", + "zh": "哥伦比亚", + "nl": "Colombia", + "de": "Kolumbien", + "fr": "Colombie", + "es": "Colombia", + "en": "Colombia", + "pt_BR": "Colômbia", + "sr-Cyrl": "Колумбија", + "sr-Latn": "Kolumbija", + "zh_TW": "哥倫比亞", + "tr": "Kolombiya", + "ro": "Columbia", + "ar": "كولومبيا", + "fa": "کلمبیا", + "yue": "哥倫比亞" + }, + flag: "🇨🇴", + code: "CO", + dialCode: "57", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Comoros", + nameTranslations: { + "sk": "Komory", + "se": "Komoros", + "pl": "Komory", + "no": "Komorene", + "ja": "コモロ", + "it": "Comore", + "zh": "科摩罗", + "nl": "Comoren", + "de": "Komoren", + "fr": "Comores", + "es": "Comoras", + "en": "Comoros", + "pt_BR": "Comores", + "sr-Cyrl": "Комори", + "sr-Latn": "Komori", + "zh_TW": "科摩羅", + "tr": "Komor Adaları", + "ro": "Comore", + "ar": "جزر القمر", + "fa": "جزیره کومور", + "yue": "科摩羅" + }, + flag: "🇰🇲", + code: "KM", + dialCode: "269", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Congo", + nameTranslations: { + "sk": "Konžská republika", + "se": "Kongo-Brazzaville", + "pl": "Kongo", + "no": "Kongo-Brazzaville", + "ja": "コンゴ共和国(ブラザビル)", + "it": "Congo-Brazzaville", + "zh": "刚果(布)", + "nl": "Congo-Brazzaville", + "de": "Kongo-Brazzaville", + "fr": "Congo-Brazzaville", + "es": "Congo", + "en": "Congo - Brazzaville", + "pt_BR": "República do Congo", + "sr-Cyrl": "Република Конго", + "sr-Latn": "Republika Kongo", + "zh_TW": "剛果共和國(布拉柴維爾)", + "tr": "Kongo Cumhuriyeti", + "ro": "Republica Congo", + "ar": "جمهورية الكونغو", + "fa": "جمهوری کنگو", + "yue": "剛果(共和國)" + }, + flag: "🇨🇬", + code: "CG", + dialCode: "242", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Congo, The Democratic Republic of the Congo", + nameTranslations: { + "sk": "Konžská demokratická republika", + "se": "Kongo-Kinshasa", + "pl": "Demokratyczna Republika Konga", + "no": "Kongo-Kinshasa", + "ja": "コンゴ民主共和国(キンシャサ)", + "it": "Congo - Kinshasa", + "zh": "刚果(金)", + "nl": "Congo-Kinshasa", + "de": "Kongo-Kinshasa", + "fr": "Congo-Kinshasa", + "es": "República Democrática del Congo", + "en": "Congo - Kinshasa", + "pt_BR": "República Democrática do Congo", + "sr-Cyrl": "Демократска Република Конго", + "sr-Latn": "Demokratska Republika Kongo", + "zh_TW": "剛果民主共和國(金沙薩)", + "tr": "Kongo Demokratik Cumhuriyeti", + "ro": "Republica Democrată Congo", + "ar": "جمهورية الكونغو الديمقراطية", + "fa": "جمهوری دموکراتیک کنگو", + "yue": "剛果(金)" + }, + flag: "🇨🇩", + code: "CD", + dialCode: "243", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Cook Islands", + nameTranslations: { + "sk": "Cookove ostrovy", + "se": "Cook-sullot", + "pl": "Wyspy Cooka", + "no": "Cookøyene", + "ja": "クック諸島", + "it": "Isole Cook", + "zh": "库克群岛", + "nl": "Cookeilanden", + "de": "Cookinseln", + "fr": "Îles Cook", + "es": "Islas Cook", + "en": "Cook Islands", + "pt_BR": "Ilhas Cook", + "sr-Cyrl": "Кукова Острва", + "sr-Latn": "Kukova Ostrva", + "zh_TW": "庫克群島", + "tr": "Cook Adaları", + "ro": "Insulele Cook", + "ar": "جزر كوك", + "fa": "جزایر کوک", + "yue": "庫克群島" + }, + flag: "🇨🇰", + code: "CK", + dialCode: "682", + minLength: 5, + maxLength: 5, + ), + Country( + name: "Costa Rica", + nameTranslations: { + "sk": "Kostarika", + "se": "Costa Rica", + "pl": "Kostaryka", + "no": "Costa Rica", + "ja": "コスタリカ", + "it": "Costa Rica", + "zh": "哥斯达黎加", + "nl": "Costa Rica", + "de": "Costa Rica", + "fr": "Costa Rica", + "es": "Costa Rica", + "en": "Costa Rica", + "pt_BR": "Costa Rica", + "sr-Cyrl": "Коста Рика", + "sr-Latn": "Kosta Rika", + "zh_TW": "哥斯大黎加", + "tr": "Kosta Rika", + "ro": "Costa Rica", + "ar": "كوستاريكا", + "fa": "کاستاریکا", + "yue": "哥斯達黎加" + }, + flag: "🇨🇷", + code: "CR", + dialCode: "506", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Côte d'Ivoire", + nameTranslations: { + "sk": "Pobrežie Slonoviny", + "se": "Elfenbenariddu", + "pl": "Côte d'Ivoire", + "no": "Elfenbenskysten", + "ja": "コートジボワール", + "it": "Costa d'Avorio", + "zh": "科特迪瓦", + "nl": "Ivoorkust", + "de": "Côte d'Ivoire", + "fr": "Côte d'Ivoire", + "es": "Côte d'Ivoire", + "en": "Côte d'Ivoire", + "pt_BR": "Côte d'Ivoire", + "sr-Cyrl": "Обала Слоноваче", + "sr-Latn": "Obala Slonovače", + "zh_TW": "象牙海岸", + "tr": "Fildişi Kıyısı", + "ro": "Coasta de fildeș", + "ar": "ساحل العاج", + "fa": "ساحل عاج", + "yue": "科特迪瓦" + }, + flag: "🇨🇮", + code: "CI", + dialCode: "225", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Croatia", + nameTranslations: { + "sk": "Chorvátsko", + "se": "Kroátia", + "pl": "Chorwacja", + "no": "Kroatia", + "ja": "クロアチア", + "it": "Croazia", + "zh": "克罗地亚", + "nl": "Kroatië", + "de": "Kroatien", + "fr": "Croatie", + "es": "Croacia", + "en": "Croatia", + "pt_BR": "Croácia", + "sr-Cyrl": "Хрватска", + "sr-Latn": "Hrvatska", + "zh_TW": "克羅埃西亞", + "tr": "Hırvatistan", + "ro": "Croația", + "ar": "كرواتيا", + "fa": "کرواسی", + "yue": "克羅地亞" + }, + flag: "🇭🇷", + code: "HR", + dialCode: "385", + minLength: 12, + maxLength: 12, + ), + Country( + name: "Cuba", + nameTranslations: { + "sk": "Kuba", + "se": "Kuba", + "pl": "Kuba", + "no": "Cuba", + "ja": "キューバ", + "it": "Cuba", + "zh": "古巴", + "nl": "Cuba", + "de": "Kuba", + "fr": "Cuba", + "es": "Cuba", + "en": "Cuba", + "pt_BR": "Cuba", + "sr-Cyrl": "Куба", + "sr-Latn": "Kuba", + "zh_TW": "古巴", + "tr": "Küba", + "ro": "Cuba", + "ar": "كوبا", + "fa": "كوبا", + "yue": "古巴" + }, + flag: "🇨🇺", + code: "CU", + dialCode: "53", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Cyprus", + nameTranslations: { + "sk": "Cyprus", + "se": "Kypros", + "pl": "Cypr", + "no": "Kypros", + "ja": "キプロス", + "it": "Cipro", + "zh": "塞浦路斯", + "nl": "Cyprus", + "de": "Zypern", + "fr": "Chypre", + "es": "Chipre", + "en": "Cyprus", + "pt_BR": "Chipre", + "sr-Cyrl": "Кипар", + "sr-Latn": "Kipar", + "zh_TW": "塞普勒斯", + "tr": "Kıbrıs", + "ro": "Cipru", + "ar": "قبرص", + "fa": "قبرس", + "yue": "塞浦路斯" + }, + flag: "🇨🇾", + code: "CY", + dialCode: "357", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Czech Republic", + nameTranslations: { + "sk": "Česko", + "se": "Čeahkka", + "pl": "Czechy", + "no": "Tsjekkia", + "ja": "チェコ", + "it": "Cechia", + "zh": "捷克", + "nl": "Tsjechië", + "de": "Tschechien", + "fr": "Tchéquie", + "es": "Chequia", + "en": "Czechia", + "pt_BR": "Czechia", + "sr-Cyrl": "Чешка", + "sr-Latn": "Češka", + "zh_TW": "捷克", + "tr": "Çek Cumhuriyeti", + "ro": "Cehia", + "ar": "جمهورية التشيك", + "fa": "جمهوری چک", + "yue": "捷克共和國" + }, + flag: "🇨🇿", + code: "CZ", + dialCode: "420", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Denmark", + nameTranslations: { + "sk": "Dánsko", + "se": "Dánmárku", + "pl": "Dania", + "no": "Danmark", + "ja": "デンマーク", + "it": "Danimarca", + "zh": "丹麦", + "nl": "Denemarken", + "de": "Dänemark", + "fr": "Danemark", + "es": "Dinamarca", + "en": "Denmark", + "pt_BR": "Dinamarca", + "sr-Cyrl": "Данска", + "sr-Latn": "Danska", + "zh_TW": "丹麥", + "tr": "Danimarka", + "ro": "Danemarca", + "ar": "الدنمارك", + "fa": "دانمارک", + "yue": "丹麥" + }, + flag: "🇩🇰", + code: "DK", + dialCode: "45", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Djibouti", + nameTranslations: { + "sk": "Džibutsko", + "se": "Djibouti", + "pl": "Dżibuti", + "no": "Djibouti", + "ja": "ジブチ", + "it": "Gibuti", + "zh": "吉布提", + "nl": "Djibouti", + "de": "Dschibuti", + "fr": "Djibouti", + "es": "Yibuti", + "en": "Djibouti", + "pt_BR": "Djibouti", + "sr-Cyrl": "Џибути", + "sr-Latn": "Džibuti", + "zh_TW": "吉布地", + "tr": "Cibuti", + "ro": "Djibouti", + "ar": "جيبوتي", + "fa": "جیبوتی", + "yue": "吉布提" + }, + flag: "🇩🇯", + code: "DJ", + dialCode: "253", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Dominica", + nameTranslations: { + "sk": "Dominika", + "se": "Dominica", + "pl": "Dominika", + "no": "Dominica", + "ja": "ドミニカ国", + "it": "Dominica", + "zh": "多米尼克", + "nl": "Dominica", + "de": "Dominica", + "fr": "Dominique", + "es": "Dominica", + "en": "Dominica", + "pt_BR": "Dominica", + "sr-Cyrl": "Доминика", + "sr-Latn": "Dominika", + "zh_TW": "多明尼加", + "tr": "Dominika", + "ro": "Dominica", + "ar": "دومينيكا", + "fa": "دومينيكا", + "yue": "多米尼加" + }, + flag: "🇩🇲", + code: "DM", + dialCode: "1767", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Dominican Republic", + nameTranslations: { + "sk": "Dominikánska republika", + "se": "Dominikána dásseváldi", + "pl": "Dominikana", + "no": "Den dominikanske republikk", + "ja": "ドミニカ共和国", + "it": "Repubblica Dominicana", + "zh": "多米尼加共和国", + "nl": "Dominicaanse Republiek", + "de": "Dominikanische Republik", + "fr": "République dominicaine", + "es": "República Dominicana", + "en": "Dominican Republic", + "pt_BR": "República Dominicana", + "sr-Cyrl": "Доминиканска Република", + "sr-Latn": "Dominikanska Republika", + "zh_TW": "多明尼加共和國", + "tr": "Dominik Cumhuriyeti", + "ro": "Republica Dominicană", + "ar": "جمهورية الدومينيكان", + "fa": "جمهوری دومنیکن", + "yue": "多明尼加共和國" + }, + flag: "🇩🇴", + code: "DO", + dialCode: "1", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Ecuador", + nameTranslations: { + "sk": "Ekvádor", + "se": "Ecuador", + "pl": "Ekwador", + "no": "Ecuador", + "ja": "エクアドル", + "it": "Ecuador", + "zh": "厄瓜多尔", + "nl": "Ecuador", + "de": "Ecuador", + "fr": "Équateur", + "es": "Ecuador", + "en": "Ecuador", + "pt_BR": "Equador", + "sr-Cyrl": "Еквадор", + "sr-Latn": "Ekvador", + "zh_TW": "厄瓜多", + "tr": "Ekvador", + "ro": "Ecuador", + "ar": "الإكوادور", + "fa": "اكوادور", + "yue": "厄瓜多爾" + }, + flag: "🇪🇨", + code: "EC", + dialCode: "593", + minLength: 8, + maxLength: 9, + ), + Country( + name: "Egypt", + nameTranslations: { + "sk": "Egypt", + "se": "Egypt", + "pl": "Egipt", + "no": "Egypt", + "ja": "エジプト", + "it": "Egitto", + "zh": "埃及", + "nl": "Egypt", + "de": "Ägypt", + "fr": "Égypte", + "es": "Egipt", + "en": "Egypt", + "pt_BR": "Egito", + "sr-Cyrl": "Египат", + "sr-Latn": "Egipat", + "zh_TW": "埃及", + "tr": "Mısır", + "ro": "Egipt", + "ar": "مصر", + "fa": "مصر", + "yue": "埃及" + }, + flag: "🇪🇬", + code: "EG", + dialCode: "2", + minLength: 11, + maxLength: 11, + ), + Country( + name: "El Salvador", + nameTranslations: { + "sk": "Salvádor", + "se": "El Salvador", + "pl": "Salwador", + "no": "El Salvador", + "ja": "エルサルバドル", + "it": "El Salvador", + "zh": "萨尔瓦多", + "nl": "El Salvador", + "de": "El Salvador", + "fr": "Salvador", + "es": "El Salvador", + "en": "El Salvador", + "pt_BR": "El Salvador", + "sr-Cyrl": "Салвадор", + "sr-Latn": "Salvador", + "zh_TW": "薩爾瓦多", + "tr": "El Salvador", + "ro": "Salvador", + "ar": "السلفادور", + "fa": "ال سالوادور", + "yue": "薩爾瓦多" + }, + flag: "🇸🇻", + code: "SV", + dialCode: "503", + minLength: 11, + maxLength: 11, + ), + Country( + name: "Equatorial Guinea", + nameTranslations: { + "sk": "Rovníková Guinea", + "se": "Ekvatoriála Guinea", + "pl": "Gwinea Równikowa", + "no": "Ekvatorial-Guinea", + "ja": "赤道ギニア", + "it": "Guinea Equatoriale", + "zh": "赤道几内亚", + "nl": "Equatoriaal-Guinea", + "de": "Äquatorialguinea", + "fr": "Guinée équatoriale", + "es": "Guinea Ecuatorial", + "en": "Equatorial Guinea", + "pt_BR": "Guiné Equatorial", + "sr-Cyrl": "Екваторијална Гвинеја", + "sr-Latn": "Ekvatorijalna Gvineja", + "zh_TW": "赤道幾內亞", + "tr": "Ekvator Ginesi", + "ro": "Guineea Ecuatorială", + "ar": "غينيا الاستوائية", + "fa": "گینه استوایی", + "yue": "赤道幾內亞" + }, + flag: "🇬🇶", + code: "GQ", + dialCode: "240", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Eritrea", + nameTranslations: { + "sk": "Eritrea", + "se": "Eritrea", + "pl": "Erytrea", + "no": "Eritrea", + "ja": "エリトリア", + "it": "Eritrea", + "zh": "厄立特里亚", + "nl": "Eritrea", + "de": "Eritrea", + "fr": "Érythrée", + "es": "Eritrea", + "en": "Eritrea", + "pt_BR": "Eritreia", + "sr-Cyrl": "Еритреја", + "sr-Latn": "Eritreja", + "zh_TW": "厄立特裡亞", + "tr": "Eritre", + "ro": "Eritreea", + "ar": "إريتريا", + "fa": "اریتره", + "yue": "厄立特里亞" + }, + flag: "🇪🇷", + code: "ER", + dialCode: "291", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Estonia", + nameTranslations: { + "sk": "Estónsko", + "se": "Estlánda", + "pl": "Estonia", + "no": "Estland", + "ja": "エストニア", + "it": "Estonia", + "zh": "爱沙尼亚", + "nl": "Estland", + "de": "Estland", + "fr": "Estonie", + "es": "Estonia", + "en": "Estonia", + "pt_BR": "Estônia", + "sr-Cyrl": "Естонија", + "sr-Latn": "Estonija", + "zh_TW": "愛沙尼亞", + "tr": "Estonya", + "ro": "Estonia", + "ar": "إستونيا", + "fa": "استونی", + "yue": "愛沙尼亞" + }, + flag: "🇪🇪", + code: "EE", + dialCode: "372", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Ethiopia", + nameTranslations: { + "sk": "Etiópia", + "se": "Etiopia", + "pl": "Etiopia", + "no": "Etiopia", + "ja": "エチオピア", + "it": "Etiopia", + "zh": "埃塞俄比亚", + "nl": "Ethiopië", + "de": "Äthiopien", + "fr": "Éthiopie", + "es": "Etiopía", + "en": "Ethiopia", + "pt_BR": "Etiópia", + "sr-Cyrl": "Етиопија", + "sr-Latn": "Etiopija", + "zh_TW": "伊索比亞", + "tr": "Etiyopya", + "ro": "Etiopia", + "ar": "إثيوبيا", + "fa": "اتیوپی", + "yue": "埃塞俄比亞" + }, + flag: "🇪🇹", + code: "ET", + dialCode: "251", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Falkland Islands (Malvinas)", + nameTranslations: { + "sk": "Falklandy", + "se": "Falklandsullot", + "pl": "Falklandy", + "no": "Falklandsøyene", + "ja": "フォークランド諸島", + "it": "Isole Falkland", + "zh": "福克兰群岛", + "nl": "Falklandeilanden", + "de": "Falklandinseln", + "fr": "Îles Malouines", + "es": "Islas Malvinas", + "en": "Falkland Islands", + "pt_BR": "Ilhas Falkland", + "sr-Cyrl": "Фокландска Острва", + "sr-Latn": "Foklandska Ostrva", + "zh_TW": "福克蘭群島", + "tr": "Falkland Adaları", + "ro": "Insulele Falklands", + "ar": "جزر فوكلاند", + "fa": "جزایر فالکلند", + "yue": "福克蘭群島(馬爾維納斯群島)" + }, + flag: "🇫🇰", + code: "FK", + dialCode: "500", + minLength: 5, + maxLength: 5, + ), + Country( + name: "Faroe Islands", + nameTranslations: { + "sk": "Faerské ostrovy", + "se": "Fearsullot", + "pl": "Wyspy Owcze", + "no": "Færøyene", + "ja": "フェロー諸島", + "it": "Isole Fær Øer", + "zh": "法罗群岛", + "nl": "Faeröer", + "de": "Färöer", + "fr": "Îles Féroé", + "es": "Islas Feroe", + "en": "Faroe Islands", + "pt_BR": "ilhas Faroe", + "sr-Cyrl": "Фарска Острва", + "sr-Latn": "Farska Ostrva", + "zh_TW": "法羅群島", + "tr": "Faroe Adaları", + "ro": "Insulele Feroe", + "ar": "جزر فارو", + "fa": "جزایر فارو", + "yue": "法羅群島" + }, + flag: "🇫🇴", + code: "FO", + dialCode: "298", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Fiji", + nameTranslations: { + "sk": "Fidži", + "se": "Fijisullot", + "pl": "Fidżi", + "no": "Fiji", + "ja": "フィジー", + "it": "Figi", + "zh": "斐济", + "nl": "Fiji", + "de": "Fidschi", + "fr": "Fidji", + "es": "Fiyi", + "en": "Fiji", + "pt_BR": "Fiji", + "sr-Cyrl": "Фиџи", + "sr-Latn": "Fidži", + "zh_TW": "斐濟", + "tr": "Fiji", + "ro": "Fiji", + "ar": "فيجي", + "fa": "فيجي", + "yue": "斐濟" + }, + flag: "🇫🇯", + code: "FJ", + dialCode: "679", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Finland", + nameTranslations: { + "sk": "Fínsko", + "se": "Suopma", + "pl": "Finlandia", + "no": "Finland", + "ja": "フィンランド", + "it": "Finlandia", + "zh": "芬兰", + "nl": "Finland", + "de": "Finnland", + "fr": "Finlande", + "es": "Finlandia", + "en": "Finland", + "pt_BR": "Finlândia", + "sr-Cyrl": "Финска", + "sr-Latn": "Finska", + "zh_TW": "芬蘭", + "tr": "Finlandiya", + "ro": "Finlanda", + "ar": "فنلندا", + "fa": "فنلاند", + "yue": "芬蘭" + }, + flag: "🇫🇮", + code: "FI", + dialCode: "358", + minLength: 12, + maxLength: 12, + ), + Country( + name: "France", + nameTranslations: { + "sk": "Francúzsko", + "se": "Frankriika", + "pl": "Francja", + "no": "Frankrike", + "ja": "フランス", + "it": "Francia", + "zh": "法国", + "nl": "Frankrijk", + "de": "Frankreich", + "fr": "France", + "es": "Francia", + "en": "France", + "pt_BR": "França", + "sr-Cyrl": "Француска", + "sr-Latn": "Francuska", + "zh_TW": "法國", + "tr": "Fransa", + "ro": "Franța", + "ar": "فرنسا", + "fa": "فرانسه", + "yue": "法國" + }, + flag: "🇫🇷", + code: "FR", + dialCode: "33", + minLength: 9, + maxLength: 9, + ), + Country( + name: "French Guiana", + nameTranslations: { + "sk": "Francúzska Guyana", + "se": "Frankriikka Guayana", + "pl": "Gujana Francuska", + "no": "Fransk Guyana", + "ja": "仏領ギアナ", + "it": "Guyana francese", + "zh": "法属圭亚那", + "nl": "Frans-Guyana", + "de": "Französisch-Guayana", + "fr": "Guyane française", + "es": "Guayana Francesa", + "en": "French Guiana", + "pt_BR": "Guiana Francesa", + "sr-Cyrl": "Француска Гвајана", + "sr-Latn": "Francuska Gvajana", + "zh_TW": "法屬蓋亞那", + "tr": "Fransız Guyanası", + "ro": "Guiana Franceză", + "ar": "غويانا الفرنسية", + "fa": "گویان فرانسه", + "yue": "法屬圭亞那" + }, + flag: "🇬🇫", + code: "GF", + dialCode: "594", + minLength: 15, + maxLength: 15, + ), + Country( + name: "French Polynesia", + nameTranslations: { + "sk": "Francúzska Polynézia", + "se": "Frankriikka Polynesia", + "pl": "Polinezja Francuska", + "no": "Fransk Polynesia", + "ja": "仏領ポリネシア", + "it": "Polinesia francese", + "zh": "法属波利尼西亚", + "nl": "Frans-Polynesië", + "de": "Französisch-Polynesien", + "fr": "Polynésie française", + "es": "Polinesia Francesa", + "en": "French Polynesia", + "pt_BR": "Polinésia Francesa", + "sr-Cyrl": "Француска Полинезија", + "sr-Latn": "Francuska Polinezija", + "zh_TW": "法屬玻里尼西亞", + "tr": "Fransız Polinezyası", + "ro": "Polinezia Franceză", + "ar": "بولينزيا الفرنسية", + "fa": "پلی‌نزی فرانسه", + "yue": "法屬波利尼西亞" + }, + flag: "🇵🇫", + code: "PF", + dialCode: "689", + minLength: 6, + maxLength: 6, + ), + Country( + name: "French Southern Territories", + nameTranslations: { + "sk": "Francúzske južné a antarktické územia", + "se": "French Southern Territories", + "pl": "Francuskie Terytoria Południowe i Antarktyczne", + "no": "De franske sørterritorier", + "ja": "仏領極南諸島", + "it": "Terre australi francesi", + "zh": "法属南部领地", + "nl": "Franse Gebieden in de zuidelijke Indische Oceaan", + "de": "Französische Süd- und Antarktisgebiete", + "fr": "Terres australes françaises", + "es": "Territorios Australes Franceses", + "en": "French Southern Territories", + "pt_BR": "Territórios Franceses do Sul", + "sr-Cyrl": "Француске јужне и антарктичке земље", + "sr-Latn": "Francuske južne i antarktičke zemlje", + "zh_TW": "法屬南部屬地", + "tr": "Fransız Güney ve Antarktika Toprakları", + "ro": "Teritoriile australe și antarctice franceze", + "ar": "أراض فرنسية جنوبية وأنتارتيكية", + "fa": "سرزمین‌های جنوبی فرانسه", + "yue": "法國南部領土" + }, + flag: "🇹🇫", + code: "TF", + dialCode: "262", + minLength: 15, + maxLength: 15, + ), + Country( + name: "Gabon", + nameTranslations: { + "sk": "Gabon", + "se": "Gabon", + "pl": "Gabon", + "no": "Gabon", + "ja": "ガボン", + "it": "Gabon", + "zh": "加蓬", + "nl": "Gabon", + "de": "Gabun", + "fr": "Gabon", + "es": "Gabón", + "en": "Gabon", + "pt_BR": "Gabão", + "sr-Cyrl": "Габон", + "sr-Latn": "Gabon", + "zh_TW": "加彭", + "tr": "Gabon", + "ro": "Gabon", + "ar": "الغابون", + "fa": "گابن", + "yue": "加蓬" + }, + flag: "🇬🇦", + code: "GA", + dialCode: "241", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Gambia", + nameTranslations: { + "sk": "Gambia", + "se": "Gámbia", + "pl": "Gambia", + "no": "Gambia", + "ja": "ガンビア", + "it": "Gambia", + "zh": "冈比亚", + "nl": "Gambia", + "de": "Gambia", + "fr": "Gambie", + "es": "Gambia", + "en": "Gambia", + "pt_BR": "Gâmbia", + "sr-Cyrl": "Гамбија", + "sr-Latn": "Gambija", + "zh_TW": "岡比亞", + "tr": "Gambiya", + "ro": "Gambia", + "ar": "غامبيا", + "fa": "گامبیا", + "yue": "岡比亞" + }, + flag: "🇬🇲", + code: "GM", + dialCode: "220", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Georgia", + nameTranslations: { + "sk": "Gruzínsko", + "se": "Georgia", + "pl": "Gruzja", + "no": "Georgia", + "ja": "ジョージア", + "it": "Georgia", + "zh": "格鲁吉亚", + "nl": "Georgië", + "de": "Georgien", + "fr": "Géorgie", + "es": "Georgia", + "en": "Georgia", + "pt_BR": "Georgia", + "sr-Cyrl": "Грузија", + "sr-Latn": "Gruzija", + "zh_TW": "喬治亞", + "tr": "Gürcistan", + "ro": "Georgia", + "ar": "جورجيا", + "fa": "گرجستان", + "yue": "格魯吉亞" + }, + flag: "🇬🇪", + code: "GE", + dialCode: "995", + minLength: 8, + maxLength: 9, + ), + Country( + name: "Germany", + nameTranslations: { + "sk": "Nemecko", + "se": "Duiska", + "pl": "Niemcy", + "no": "Tyskland", + "ja": "ドイツ", + "it": "Germania", + "zh": "德国", + "nl": "Duitsland", + "de": "Deutschland", + "fr": "Allemagne", + "es": "Alemania", + "en": "Germany", + "pt_BR": "Alemanha", + "sr-Cyrl": "Немачка", + "sr-Latn": "Nemačka", + "zh_TW": "德國", + "tr": "Almanya", + "ro": "Germania", + "ar": "ألمانيا", + "fa": "آلمان", + "yue": "德國" + }, + flag: "🇩🇪", + code: "DE", + dialCode: "49", + minLength: 9, + maxLength: 13, + ), + Country( + name: "Ghana", + nameTranslations: { + "sk": "Ghana", + "se": "Ghana", + "pl": "Ghana", + "no": "Ghana", + "ja": "ガーナ", + "it": "Ghana", + "zh": "加纳", + "nl": "Ghana", + "de": "Ghana", + "fr": "Ghana", + "es": "Ghana", + "en": "Ghana", + "pt_BR": "Gana", + "sr-Cyrl": "Гана", + "sr-Latn": "Gana", + "zh_TW": "迦納", + "tr": "Gana", + "ro": "Ghana", + "ar": "غانا", + "fa": "غنا", + "yue": "加納" + }, + flag: "🇬🇭", + code: "GH", + dialCode: "233", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Gibraltar", + nameTranslations: { + "sk": "Gibraltár", + "se": "Gibraltar", + "pl": "Gibraltar", + "no": "Gibraltar", + "ja": "ジブラルタル", + "it": "Gibilterra", + "zh": "直布罗陀", + "nl": "Gibraltar", + "de": "Gibraltar", + "fr": "Gibraltar", + "es": "Gibraltar", + "en": "Gibraltar", + "pt_BR": "Gibraltar", + "sr-Cyrl": "Гибралтар", + "sr-Latn": "Gibraltar", + "zh_TW": "直布羅陀", + "tr": "Cebelitarık", + "ro": "Gibraltar", + "ar": "جبل طارق", + "fa": "جبل الطارق", + "yue": "直布羅陀" + }, + flag: "🇬🇮", + code: "GI", + dialCode: "350", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Greece", + nameTranslations: { + "sk": "Grécko", + "se": "Greika", + "pl": "Grecja", + "no": "Hellas", + "ja": "ギリシャ", + "it": "Grecia", + "zh": "希腊", + "nl": "Griekenland", + "de": "Griechenland", + "fr": "Grèce", + "es": "Grecia", + "en": "Greece", + "pt_BR": "Grécia", + "sr-Cyrl": "Грчка", + "sr-Latn": "Grčka", + "zh_TW": "希臘", + "tr": "Yunanistan", + "ro": "Grecia", + "ar": "اليونان", + "fa": "یونان", + "yue": "希臘" + }, + flag: "🇬🇷", + code: "GR", + dialCode: "30", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Greenland", + nameTranslations: { + "sk": "Grónsko", + "se": "Kalaallit Nunaat", + "pl": "Grenlandia", + "no": "Grønland", + "ja": "グリーンランド", + "it": "Groenlandia", + "zh": "格陵兰", + "nl": "Groenland", + "de": "Grönland", + "fr": "Groenland", + "es": "Groenlandia", + "en": "Greenland", + "pt_BR": "Groenlândia", + "sr-Cyrl": "Гренланд", + "sr-Latn": "Grenland", + "zh_TW": "格陵蘭", + "tr": "Grönland", + "ro": "Groenlanda", + "ar": "جرينلاند", + "fa": "گرینلند", + "yue": "格陵蘭" + }, + flag: "🇬🇱", + code: "GL", + dialCode: "299", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Grenada", + nameTranslations: { + "sk": "Grenada", + "se": "Grenada", + "pl": "Grenada", + "no": "Grenada", + "ja": "グレナダ", + "it": "Grenada", + "zh": "格林纳达", + "nl": "Grenada", + "de": "Grenada", + "fr": "Grenade", + "es": "Granada", + "en": "Grenada", + "pt_BR": "Grenada", + "sr-Cyrl": "Гренада", + "sr-Latn": "Grenada", + "zh_TW": "格林納達", + "tr": "Grenada", + "ro": "Grenada", + "ar": "غرينادا", + "fa": "گرنادا", + "yue": "格林納達" + }, + flag: "🇬🇩", + code: "GD", + dialCode: "1473", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Guadeloupe", + nameTranslations: { + "sk": "Guadeloupe", + "se": "Guadeloupe", + "pl": "Gwadelupa", + "no": "Guadeloupe", + "ja": "グアドループ", + "it": "Guadalupa", + "zh": "瓜德罗普", + "nl": "Guadeloupe", + "de": "Guadeloupe", + "fr": "Guadeloupe", + "es": "Guadalupe", + "en": "Guadeloupe", + "pt_BR": "Guadalupe", + "sr-Cyrl": "Гваделуп", + "sr-Latn": "Gvadelup", + "zh_TW": "瓜地洛普", + "tr": "Guadeloupe", + "ro": "Guadelupa", + "ar": "غوادلوب", + "fa": "گوادلوپ", + "yue": "瓜德罗普" + }, + flag: "🇬🇵", + code: "GP", + dialCode: "590", + minLength: 15, + maxLength: 15, + ), + Country( + name: "Guam", + nameTranslations: { + "sk": "Guam", + "se": "Guam", + "pl": "Guam", + "no": "Guam", + "ja": "グアム", + "it": "Guam", + "zh": "关岛", + "nl": "Guam", + "de": "Guam", + "fr": "Guam", + "es": "Guam", + "en": "Guam", + "pt_BR": "Guam", + "sr-Cyrl": "Гвам", + "sr-Latn": "Gvam", + "zh_TW": "關島", + "tr": "Guam", + "ro": "Guam", + "ar": "غوام", + "fa": "گوام", + "yue": "關島" + }, + flag: "🇬🇺", + code: "GU", + dialCode: "1671", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Guatemala", + nameTranslations: { + "sk": "Guatemala", + "se": "Guatemala", + "pl": "Gwatemala", + "no": "Guatemala", + "ja": "グアテマラ", + "it": "Guatemala", + "zh": "危地马拉", + "nl": "Guatemala", + "de": "Guatemala", + "fr": "Guatemala", + "es": "Guatemala", + "en": "Guatemala", + "pt_BR": "Guatemala", + "sr-Cyrl": "Гватемала", + "sr-Latn": "Gvatemala", + "zh_TW": "瓜地馬拉", + "tr": "Guatemala", + "ro": "Guatemala", + "ar": "غواتيمالا", + "fa": "گواتمالا", + "yue": "危地馬拉" + }, + flag: "🇬🇹", + code: "GT", + dialCode: "502", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Guernsey", + nameTranslations: { + "sk": "Guernsey", + "se": "Guernsey", + "pl": "Guernsey", + "no": "Guernsey", + "ja": "ガーンジー", + "it": "Guernsey", + "zh": "根西岛", + "nl": "Guernsey", + "de": "Guernsey", + "fr": "Guernesey", + "es": "Guernsey", + "en": "Guernsey", + "pt_BR": "Guernsey", + "sr-Cyrl": "Гернзи", + "sr-Latn": "Gernzi", + "zh_TW": "根息島", + "tr": "Guernsey", + "ro": "Guernsey", + "ar": "غيرنزي", + "fa": "گرنزی", + "yue": "格恩西島" + }, + flag: "🇬🇬", + code: "GG", + dialCode: "44", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Guinea", + nameTranslations: { + "sk": "Guinea", + "se": "Guinea", + "pl": "Gwinea", + "no": "Guinea", + "ja": "ギニア", + "it": "Guinea", + "zh": "几内亚", + "nl": "Guinee", + "de": "Guinea", + "fr": "Guinée", + "es": "Guinea", + "en": "Guinea", + "pt_BR": "Guiné", + "sr-Cyrl": "Гвинеја", + "sr-Latn": "Gvineja", + "zh_TW": "幾內亞", + "tr": "Gine", + "ro": "Guinea", + "ar": "غينيا", + "fa": "گینه", + "yue": "幾內亞" + }, + flag: "🇬🇳", + code: "GN", + dialCode: "224", + minLength: 8, + maxLength: 9, + ), + Country( + name: "Guinea-Bissau", + nameTranslations: { + "sk": "Guinea-Bissau", + "se": "Guinea-Bissau", + "pl": "Gwinea Bissau", + "no": "Guinea-Bissau", + "ja": "ギニアビサウ", + "it": "Guinea-Bissau", + "zh": "几内亚比绍", + "nl": "Guinee-Bissau", + "de": "Guinea-Bissau", + "fr": "Guinée-Bissau", + "es": "Guinea-Bisáu", + "en": "Guinea-Bissau", + "pt_BR": "Guiné-bissau", + "sr-Cyrl": "Гвинеја Бисао", + "sr-Latn": "Gvineja Bisao", + "zh_TW": "幾內亞比索", + "tr": "Gine-Bissau", + "ro": "Guineea-Bissau", + "ar": "غينيا بيساو", + "fa": "گینه بیسائو", + "yue": "幾內亞比紹" + }, + flag: "🇬🇼", + code: "GW", + dialCode: "245", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Guyana", + nameTranslations: { + "sk": "Guyana", + "se": "Guyana", + "pl": "Gujana", + "no": "Guyana", + "ja": "ガイアナ", + "it": "Guyana", + "zh": "圭亚那", + "nl": "Guyana", + "de": "Guyana", + "fr": "Guyana", + "es": "Guyana", + "en": "Guyana", + "pt_BR": "Guiana", + "sr-Cyrl": "Гвајана", + "sr-Latn": "Gvajana", + "zh_TW": "蓋亞那", + "tr": "Guyana", + "ro": "Guyana", + "ar": "غيانا", + "fa": "گویان", + "yue": "圭亞那" + }, + flag: "🇬🇾", + code: "GY", + dialCode: "592", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Haiti", + nameTranslations: { + "sk": "Haiti", + "se": "Haiti", + "pl": "Haiti", + "no": "Haiti", + "ja": "ハイチ", + "it": "Haiti", + "zh": "海地", + "nl": "Haïti", + "de": "Haiti", + "fr": "Haïti", + "es": "Haití", + "en": "Haiti", + "pt_BR": "Haiti", + "sr-Cyrl": "Хаити", + "sr-Latn": "Haiti", + "zh_TW": "海地", + "tr": "Haiti", + "ro": "Haiti", + "ar": "هايتي", + "fa": "هائیتی", + "yue": "海地" + }, + flag: "🇭🇹", + code: "HT", + dialCode: "509", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Heard Island and Mcdonald Islands", + nameTranslations: { + "sk": "Heardov ostrov a Macdonaldove ostrovy", + "se": "Heard- ja McDonald-sullot", + "pl": "Wyspy Heard i McDonalda", + "no": "Heard- og McDonaldøyene", + "ja": "ハード島・マクドナルド諸島", + "it": "Isole Heard e McDonald", + "zh": "赫德岛和麦克唐纳群岛", + "nl": "Heard en McDonaldeilanden", + "de": "Heard und McDonaldinseln", + "fr": "Îles Heard et McDonald", + "es": "Islas Heard y McDonald", + "en": "Heard & McDonald Islands", + "pt_BR": "Ilhas Heard e McDonald", + "sr-Cyrl": "Острва Херд и Макдоналд", + "sr-Latn": "Ostrva Herd i Makdonald", + "zh_TW": "赫德暨麥當勞群島", + "tr": "Heard Adası ve McDonald Adaları", + "ro": "Insula Heard și Insulele McDonald", + "ar": "جزيرة هيرد وجزر ماكدونالد", + "fa": "جزیره هرد و جزایر مک‌دونالد", + "yue": "赫德岛同麦克唐纳群岛" + }, + flag: "🇭🇲", + code: "HM", + dialCode: "672", + minLength: 15, + maxLength: 15, + ), + Country( + name: "Holy See (Vatican City State)", + nameTranslations: { + "sk": "Vatikán", + "se": "Vatikána", + "pl": "Watykan", + "no": "Vatikanstaten", + "ja": "バチカン市国", + "it": "Città del Vaticano", + "zh": "梵蒂冈", + "nl": "Vaticaanstad", + "de": "Vatikanstadt", + "fr": "État de la Cité du Vatican", + "es": "Ciudad del Vaticano", + "en": "Vatican City", + "pt_BR": "Cidade do Vaticano", + "sr-Cyrl": "Ватикан", + "sr-Latn": "Vatikan", + "zh_TW": "梵蒂岡", + "tr": "Vatikan", + "ro": "Vatican", + "ar": "الفاتيكان", + "fa": "واتیکان", + "yue": "梵蒂岡城國" + }, + flag: "🇻🇦", + code: "VA", + dialCode: "379", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Honduras", + nameTranslations: { + "sk": "Honduras", + "se": "Honduras", + "pl": "Honduras", + "no": "Honduras", + "ja": "ホンジュラス", + "it": "Honduras", + "zh": "洪都拉斯", + "nl": "Honduras", + "de": "Honduras", + "fr": "Honduras", + "es": "Honduras", + "en": "Honduras", + "pt_BR": "Honduras", + "sr-Cyrl": "Хондурас", + "sr-Latn": "Honduras", + "zh_TW": "宏都拉斯", + "tr": "Honduras", + "ro": "Honduras", + "ar": "هندوراس", + "fa": "هندوراس", + "yue": "洪都拉斯" + }, + flag: "🇭🇳", + code: "HN", + dialCode: "504", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Hong Kong", + nameTranslations: { + "sk": "Hongkong – OAO Číny", + "se": "Hongkong", + "pl": "SRA Hongkong (Chiny)", + "no": "Hongkong S.A.R. Kina", + "ja": "中華人民共和国香港特別行政区", + "it": "RAS di Hong Kong", + "zh": "中国香港特别行政区", + "nl": "Hongkong SAR van China", + "de": "Sonderverwaltungsregion Hongkong", + "fr": "R.A.S. chinoise de Hong Kong", + "es": "RAE de Hong Kong (China)", + "en": "Hong Kong SAR China", + "pt_BR": "RAE de Hong Kong China", + "sr-Cyrl": "Хонг Конг", + "sr-Latn": "Hong Kong", + "zh_TW": "香港", + "tr": "Hong Kong", + "ro": "Hong Kong", + "ar": "هونغ كونغ", + "fa": "هنگ کنگ", + "yue": "香港" + }, + flag: "🇭🇰", + code: "HK", + dialCode: "852", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Hungary", + nameTranslations: { + "sk": "Maďarsko", + "se": "Ungár", + "pl": "Węgry", + "no": "Ungarn", + "ja": "ハンガリー", + "it": "Ungheria", + "zh": "匈牙利", + "nl": "Hongarije", + "de": "Ungarn", + "fr": "Hongrie", + "es": "Hungría", + "en": "Hungary", + "pt_BR": "Hungria", + "sr-Cyrl": "Мађарска", + "sr-Latn": "Mađarska", + "zh_TW": "匈牙利", + "tr": "Macaristan", + "ro": "Ungaria", + "ar": "المجر", + "fa": "مجارستان", + "yue": "匈牙利" + }, + flag: "🇭🇺", + code: "HU", + dialCode: "36", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Iceland", + nameTranslations: { + "sk": "Island", + "se": "Islánda", + "pl": "Islandia", + "no": "Island", + "ja": "アイスランド", + "it": "Islanda", + "zh": "冰岛", + "nl": "IJsland", + "de": "Island", + "fr": "Islande", + "es": "Islandia", + "en": "Iceland", + "pt_BR": "Islândia", + "sr-Cyrl": "Исланд", + "sr-Latn": "Island", + "zh_TW": "冰島", + "tr": "İzlanda", + "ro": "Islanda", + "ar": "آيسلندا", + "fa": "ایسلند", + "yue": "冰島" + }, + flag: "🇮🇸", + code: "IS", + dialCode: "354", + minLength: 7, + maxLength: 9, + ), + Country( + name: "India", + nameTranslations: { + "sk": "India", + "se": "India", + "pl": "Indie", + "no": "India", + "ja": "インド", + "it": "India", + "zh": "印度", + "nl": "India", + "de": "Indien", + "fr": "Inde", + "es": "India", + "en": "India", + "pt_BR": "Índia", + "sr-Cyrl": "Индија", + "sr-Latn": "Indija", + "zh_TW": "印度", + "tr": "Hindistan", + "ro": "India", + "ar": "الهند", + "fa": "هند", + "yue": "印度" + }, + flag: "🇮🇳", + code: "IN", + dialCode: "91", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Indonesia", + nameTranslations: { + "sk": "Indonézia", + "se": "Indonesia", + "pl": "Indonezja", + "no": "Indonesia", + "ja": "インドネシア", + "it": "Indonesia", + "zh": "印度尼西亚", + "nl": "Indonesië", + "de": "Indonesien", + "fr": "Indonésie", + "es": "Indonesia", + "en": "Indonesia", + "pt_BR": "Indonésia", + "sr-Cyrl": "Индонезија", + "sr-Latn": "Indonezija", + "zh_TW": "印尼", + "tr": "Endonezya", + "ro": "Indonezia", + "ar": "إندونيسيا", + "fa": "اندونزی", + "yue": "印尼" + }, + flag: "🇮🇩", + code: "ID", + dialCode: "62", + minLength: 10, + maxLength: 13, + ), + Country( + name: "Iran, Islamic Republic of Persian Gulf", + nameTranslations: { + "sk": "Irán", + "se": "Iran", + "pl": "Iran", + "no": "Iran", + "ja": "イラン", + "it": "Iran", + "zh": "伊朗", + "nl": "Iran", + "de": "Iran", + "fr": "Iran", + "es": "Irán", + "en": "Iran", + "pt_BR": "Irã", + "sr-Cyrl": "Иран", + "sr-Latn": "Iran", + "zh_TW": "伊朗", + "tr": "İran", + "ro": "Iran", + "ar": "إيران", + "fa": "ایران", + "yue": "伊朗" + }, + flag: "🇮🇷", + code: "IR", + dialCode: "98", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Iraq", + nameTranslations: { + "sk": "Irak", + "se": "Irak", + "pl": "Irak", + "no": "Irak", + "ja": "イラク", + "it": "Iraq", + "zh": "伊拉克", + "nl": "Irak", + "de": "Irak", + "fr": "Irak", + "es": "Irak", + "en": "Iraq", + "pt_BR": "Iraque", + "sr-Cyrl": "Ирак", + "sr-Latn": "Irak", + "zh_TW": "伊拉克", + "tr": "Irak", + "ro": "Irak", + "ar": "العراق", + "fa": "عراق", + "yue": "伊拉克" + }, + flag: "🇮🇶", + code: "IQ", + dialCode: "964", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Ireland", + nameTranslations: { + "sk": "Írsko", + "se": "Irlánda", + "pl": "Irlandia", + "no": "Irland", + "ja": "アイルランド", + "it": "Irlanda", + "zh": "爱尔兰", + "nl": "Ierland", + "de": "Irland", + "fr": "Irlande", + "es": "Irlanda", + "en": "Ireland", + "pt_BR": "Irlanda", + "sr-Cyrl": "Ирска", + "sr-Latn": "Irska", + "zh_TW": "愛爾蘭", + "tr": "İrlanda", + "ro": "Irlanda", + "ar": "أيرلندا", + "fa": "ایرلند", + "yue": "愛爾蘭" + }, + flag: "🇮🇪", + code: "IE", + dialCode: "353", + minLength: 7, + maxLength: 9, + ), + Country( + name: "Isle of Man", + nameTranslations: { + "sk": "Ostrov Man", + "se": "Mann-sullot", + "pl": "Wyspa Man", + "no": "Man", + "ja": "マン島", + "it": "Isola di Man", + "zh": "马恩岛", + "nl": "Isle of Man", + "de": "Isle of Man", + "fr": "Île de Man", + "es": "Isla de Man", + "en": "Isle of Man", + "pt_BR": "Ilha de Man", + "sr-Cyrl": "Острво Мен", + "sr-Latn": "Ostrvo Men", + "zh_TW": "曼島", + "tr": "Man Adası", + "ro": "Insula Man", + "ar": "جزيرة مان", + "fa": "جزیره مان", + "yue": "马伊岛" + }, + flag: "🇮🇲", + code: "IM", + dialCode: "44", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Israel", + nameTranslations: { + "sk": "Izrael", + "se": "Israel", + "pl": "Izrael", + "no": "Israel", + "ja": "イスラエル", + "it": "Israele", + "zh": "以色列", + "nl": "Israël", + "de": "Israel", + "fr": "Israël", + "es": "Israel", + "en": "Israel", + "pt_BR": "Israel", + "sr-Cyrl": "Израел", + "sr-Latn": "Izrael", + "zh_TW": "以色列", + "tr": "İsrail", + "ro": "Israel", + "ar": "إسرائيل", + "fa": "إسرائيل", + "yue": "以色列" + }, + flag: "🇮🇱", + code: "IL", + dialCode: "972", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Campione d'Italia", + nameTranslations: { + "sk": "Taliansko", + "se": "Itália", + "pl": "Włochy", + "no": "Italia", + "ja": "イタリア", + "it": "Italia", + "zh": "意大利", + "nl": "Italië", + "de": "Italien", + "fr": "Italie", + "es": "Italia", + "en": "Italy", + "pt_BR": "Itália", + "sr-Cyrl": "Италија", + "sr-Latn": "Italija", + "zh_TW": "義大利", + "tr": "İtalya", + "ro": "Italia", + "ar": "إيطاليا", + "fa": "ایتالیا", + "yue": "意大利" + }, + flag: "🇮🇹", + code: "IT", + dialCode: "39", + minLength: 9, + maxLength: 10, + ), + Country( + name: "Jamaica", + nameTranslations: { + "sk": "Jamajka", + "se": "Jamaica", + "pl": "Jamajka", + "no": "Jamaica", + "ja": "ジャマイカ", + "it": "Giamaica", + "zh": "牙买加", + "nl": "Jamaica", + "de": "Jamaika", + "fr": "Jamaïque", + "es": "Jamaica", + "en": "Jamaica", + "pt_BR": "Jamaica", + "sr-Cyrl": "Јамајка", + "sr-Latn": "Jamajka", + "zh_TW": "牙買加", + "tr": "Jamaika", + "ro": "Jamaica", + "ar": "جامايكا", + "fa": "جامائیکا", + "yue": "牙買加" + }, + flag: "🇯🇲", + code: "JM", + dialCode: "1876", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Japan", + nameTranslations: { + "sk": "Japonsko", + "se": "Japána", + "pl": "Japonia", + "no": "Japan", + "ja": "日本", + "it": "Giappone", + "zh": "日本", + "nl": "Japan", + "de": "Japan", + "fr": "Japon", + "es": "Japón", + "en": "Japan", + "pt_BR": "Japão", + "sr-Cyrl": "Јапан", + "sr-Latn": "Japan", + "zh_TW": "日本", + "tr": "Japonya", + "ro": "Japonia", + "ar": "اليابان", + "fa": "ژاپن", + "yue": "日本" + }, + flag: "🇯🇵", + code: "JP", + dialCode: "81", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Jersey", + nameTranslations: { + "sk": "Jersey", + "se": "Jersey", + "pl": "Jersey", + "no": "Jersey", + "ja": "ジャージー", + "it": "Jersey", + "zh": "泽西岛", + "nl": "Jersey", + "de": "Jersey", + "fr": "Jersey", + "es": "Jersey", + "en": "Jersey", + "pt_BR": "Jersey", + "sr-Cyrl": "Џерзи", + "sr-Latn": "Džerzi", + "zh_TW": "澤西", + "tr": "Jersey", + "ro": "Jersey", + "ar": "جيرزي", + "fa": "جرزی", + "yue": "澤西" + }, + flag: "🇯🇪", + code: "JE", + dialCode: "44", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Jordan", + nameTranslations: { + "sk": "Jordánsko", + "se": "Jordánia", + "pl": "Jordania", + "no": "Jordan", + "ja": "ヨルダン", + "it": "Giordania", + "zh": "约旦", + "nl": "Jordanië", + "de": "Jordanien", + "fr": "Jordanie", + "es": "Jordania", + "en": "Jordan", + "pt_BR": "Jordânia", + "sr-Cyrl": "Јордан", + "sr-Latn": "Jordan", + "zh_TW": "約旦", + "tr": "Mavera-i Ürdün", + "ro": "Iordania", + "ar": "الأردن", + "fa": "اردن", + "yue": "約旦" + }, + flag: "🇯🇴", + code: "JO", + dialCode: "962", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Kazakhstan", + nameTranslations: { + "sk": "Kazachstan", + "se": "Kasakstan", + "pl": "Kazachstan", + "no": "Kasakhstan", + "ja": "カザフスタン", + "it": "Kazakistan", + "zh": "哈萨克斯坦", + "nl": "Kazachstan", + "de": "Kasachstan", + "fr": "Kazakhstan", + "es": "Kazajistán", + "en": "Kazakhstan", + "pt_BR": "Cazaquistão", + "sr-Cyrl": "Казахстан", + "sr-Latn": "Kazahstan", + "zh_TW": "哈薩克", + "tr": "Kazakistan", + "ro": "Kazahstan", + "ar": "كازاخستان", + "fa": "قزاقستان", + "yue": "哈薩克斯坦" + }, + flag: "🇰🇿", + code: "KZ", + dialCode: "7", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Kenya", + nameTranslations: { + "sk": "Keňa", + "se": "Kenia", + "pl": "Kenia", + "no": "Kenya", + "ja": "ケニア", + "it": "Kenya", + "zh": "肯尼亚", + "nl": "Kenia", + "de": "Kenia", + "fr": "Kenya", + "es": "Kenia", + "en": "Kenya", + "pt_BR": "Quênia", + "sr-Cyrl": "Кенија", + "sr-Latn": "Kenija", + "zh_TW": "肯亞", + "tr": "Kenya", + "ro": "Kenya", + "ar": "كينيا", + "fa": "كنيا", + "yue": "肯雅" + }, + flag: "🇰🇪", + code: "KE", + dialCode: "254", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Kiribati", + nameTranslations: { + "sk": "Kiribati", + "se": "Kiribati", + "pl": "Kiribati", + "no": "Kiribati", + "ja": "キリバス", + "it": "Kiribati", + "zh": "基里巴斯", + "nl": "Kiribati", + "de": "Kiribati", + "fr": "Kiribati", + "es": "Kiribati", + "en": "Kiribati", + "pt_BR": "Kiribati", + "sr-Cyrl": "Кирибати", + "sr-Latn": "Kiribati", + "zh_TW": "吉里巴斯", + "tr": "Kiribati", + "ro": "Kiribati", + "ar": "كيريباتي", + "fa": "کیریباتی", + "yue": "基里巴斯" + }, + flag: "🇰🇮", + code: "KI", + dialCode: "686", + minLength: 5, + maxLength: 5, + ), + Country( + name: "Korea, Democratic People's Republic of Korea", + nameTranslations: { + "sk": "Severná Kórea", + "se": "Davvi-Korea", + "pl": "Korea Północna", + "no": "Nord-Korea", + "ja": "北朝鮮", + "it": "Corea del Nord", + "zh": "朝鲜", + "nl": "Noord-Korea", + "de": "Nordkorea", + "fr": "Corée du Nord", + "es": "Corea del Norte", + "en": "North Korea", + "pt_BR": "Coreia do Norte", + "sr-Cyrl": "Северна Кореја", + "sr-Latn": "Severna Koreja", + "zh_TW": "北韓", + "tr": "Kuzey Kore", + "ro": "Coreea de Nord", + "ar": "كوريا الشمالية", + "fa": "کره شمالی", + "yue": "朝鮮(朝鮮民主主義人民共咊囯)" + }, + flag: "🇰🇵", + code: "KP", + dialCode: "850", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Korea, Republic of South Korea", + nameTranslations: { + "sk": "Južná Kórea", + "se": "Mátta-Korea", + "pl": "Korea Południowa", + "no": "Sør-Korea", + "ja": "韓国", + "it": "Corea del Sud", + "zh": "韩国", + "nl": "Zuid-Korea", + "de": "Südkorea", + "fr": "Corée du Sud", + "es": "Corea del Sur", + "en": "South Korea", + "pt_BR": "Coreia do Sul", + "sr-Cyrl": "Јужна Кореја", + "sr-Latn": "Južna Koreja", + "zh_TW": "南韓", + "tr": "Güney Kore", + "ro": "Coreea de Sud", + "ar": "كوريا الجنوبية", + "fa": "کره جنوبی", + "yue": "韓國(大韓民國)" + }, + flag: "🇰🇷", + code: "KR", + dialCode: "82", + minLength: 11, + maxLength: 11, + ), + Country( + name: "Kuwait", + nameTranslations: { + "sk": "Kuvajt", + "se": "Kuwait", + "pl": "Kuwejt", + "no": "Kuwait", + "ja": "クウェート", + "it": "Kuwait", + "zh": "科威特", + "nl": "Koeweit", + "de": "Kuwait", + "fr": "Koweït", + "es": "Kuwait", + "en": "Kuwait", + "pt_BR": "Kuwait", + "sr-Cyrl": "Кувајт", + "sr-Latn": "Kuvajt", + "zh_TW": "科威特", + "tr": "Kuveyt", + "ro": "Kuweit", + "ar": "الكويت", + "fa": "کویت", + "yue": "科威特" + }, + flag: "🇰🇼", + code: "KW", + dialCode: "965", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Kyrgyzstan", + nameTranslations: { + "sk": "Kirgizsko", + "se": "Kirgisistan", + "pl": "Kirgistan", + "no": "Kirgisistan", + "ja": "キルギス", + "it": "Kirghizistan", + "zh": "吉尔吉斯斯坦", + "nl": "Kirgizië", + "de": "Kirgisistan", + "fr": "Kirghizistan", + "es": "Kirguistán", + "en": "Kyrgyzstan", + "pt_BR": "Quirguistão", + "sr-Cyrl": "Киргистан", + "sr-Latn": "Kirgistan", + "zh_TW": "吉爾吉斯", + "tr": "Kırgızistan", + "ro": "Kîrgîzstan", + "ar": "قيرغيزستان", + "fa": "قرقیزستان", + "yue": "吉爾吉斯斯坦" + }, + flag: "🇰🇬", + code: "KG", + dialCode: "996", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Laos", + nameTranslations: { + "sk": "Laos", + "se": "Laos", + "pl": "Laos", + "no": "Laos", + "ja": "ラオス", + "it": "Laos", + "zh": "老挝", + "nl": "Laos", + "de": "Laos", + "fr": "Laos", + "es": "Laos", + "en": "Laos", + "pt_BR": "Laos", + "sr-Cyrl": "Лаос", + "sr-Latn": "Laos", + "zh_TW": "寮國", + "tr": "Laos", + "ro": "Laos", + "ar": "لاوس", + "fa": "لائوس", + "yue": "老撾人民民主共和國" + }, + flag: "🇱🇦", + code: "LA", + dialCode: "856", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Latvia", + nameTranslations: { + "sk": "Lotyšsko", + "se": "Látvia", + "pl": "Łotwa", + "no": "Latvia", + "ja": "ラトビア", + "it": "Lettonia", + "zh": "拉脱维亚", + "nl": "Letland", + "de": "Lettland", + "fr": "Lettonie", + "es": "Letonia", + "en": "Latvia", + "pt_BR": "Letônia", + "sr-Cyrl": "Летонија", + "sr-Latn": "Letonija", + "zh_TW": "拉托維亞", + "tr": "Letonya", + "ro": "Letonia", + "ar": "لاتفيا", + "fa": "لتونی", + "yue": "拉脫維亞" + }, + flag: "🇱🇻", + code: "LV", + dialCode: "371", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Lebanon", + nameTranslations: { + "sk": "Libanon", + "se": "Libanon", + "pl": "Liban", + "no": "Libanon", + "ja": "レバノン", + "it": "Libano", + "zh": "黎巴嫩", + "nl": "Libanon", + "de": "Libanon", + "fr": "Liban", + "es": "Líbano", + "en": "Lebanon", + "pt_BR": "Líbano", + "sr-Cyrl": "Либан", + "sr-Latn": "Liban", + "zh_TW": "黎巴嫩", + "tr": "Lübnan", + "ro": "Liban", + "ar": "لبنان", + "fa": "لبنان", + "yue": "黎巴嫩" + }, + flag: "🇱🇧", + code: "LB", + dialCode: "961", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Lesotho", + nameTranslations: { + "sk": "Lesotho", + "se": "Lesotho", + "pl": "Lesotho", + "no": "Lesotho", + "ja": "レソト", + "it": "Lesotho", + "zh": "莱索托", + "nl": "Lesotho", + "de": "Lesotho", + "fr": "Lesotho", + "es": "Lesoto", + "en": "Lesotho", + "pt_BR": "Lesoto", + "sr-Cyrl": "Лесото", + "sr-Latn": "Lesoto", + "zh_TW": "賴索托", + "tr": "Lesotho", + "ro": "Lesotho", + "ar": "ليسوتو", + "fa": "لسوتو", + "yue": "萊索托" + }, + flag: "🇱🇸", + code: "LS", + dialCode: "266", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Liberia", + nameTranslations: { + "sk": "Libéria", + "se": "Liberia", + "pl": "Liberia", + "no": "Liberia", + "ja": "リベリア", + "it": "Liberia", + "zh": "利比里亚", + "nl": "Liberia", + "de": "Liberia", + "fr": "Libéria", + "es": "Liberia", + "en": "Liberia", + "pt_BR": "Libéria", + "sr-Cyrl": "Либерија", + "sr-Latn": "Liberija", + "zh_TW": "賴比瑞亞", + "tr": "Liberya", + "ro": "Liberia", + "ar": "ليبيريا", + "fa": "لیبریا", + "yue": "利比里亞" + }, + flag: "🇱🇷", + code: "LR", + dialCode: "231", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Libyan Arab Jamahiriya", + nameTranslations: { + "sk": "Líbya", + "se": "Libya", + "pl": "Libia", + "no": "Libya", + "ja": "リビア", + "it": "Libia", + "zh": "利比亚", + "nl": "Libië", + "de": "Libyen", + "fr": "Libye", + "es": "Libia", + "en": "Libya", + "pt_BR": "Líbia", + "sr-Cyrl": "Либија", + "sr-Latn": "Libija", + "zh_TW": "利比亞", + "tr": "Libya", + "ro": "Libia", + "ar": "ليبيا", + "fa": "لیبی", + "yue": "利比亞" + }, + flag: "🇱🇾", + code: "LY", + dialCode: "218", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Liechtenstein", + nameTranslations: { + "sk": "Lichtenštajnsko", + "se": "Liechtenstein", + "pl": "Liechtenstein", + "no": "Liechtenstein", + "ja": "リヒテンシュタイン", + "it": "Liechtenstein", + "zh": "列支敦士登", + "nl": "Liechtenstein", + "de": "Liechtenstein", + "fr": "Liechtenstein", + "es": "Liechtenstein", + "en": "Liechtenstein", + "pt_BR": "Liechtenstein", + "sr-Cyrl": "Лихтенштајн", + "sr-Latn": "Lihtenštajn", + "zh_TW": "列支敦斯登", + "tr": "Lihtenştayn", + "ro": "Liechtenstein", + "ar": "ليختنشتاين", + "fa": "لیختن‌اشتاین", + "yue": "列支敦士登" + }, + flag: "🇱🇮", + code: "LI", + dialCode: "423", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Lithuania", + nameTranslations: { + "sk": "Litva", + "se": "Lietuva", + "pl": "Litwa", + "no": "Litauen", + "ja": "リトアニア", + "it": "Lituania", + "zh": "立陶宛", + "nl": "Litouwen", + "de": "Litauen", + "fr": "Lituanie", + "es": "Lituania", + "en": "Lithuania", + "pt_BR": "Lituânia", + "sr-Cyrl": "Литванија", + "sr-Latn": "Litvanija", + "zh_TW": "立陶宛", + "tr": "Litvanya", + "ro": "Lituania", + "ar": "ليتوانيا", + "fa": "لیتوانی", + "yue": "立陶宛" + }, + flag: "🇱🇹", + code: "LT", + dialCode: "370", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Luxembourg", + nameTranslations: { + "sk": "Luxembursko", + "se": "Luxembourg", + "pl": "Luksemburg", + "no": "Luxemburg", + "ja": "ルクセンブルク", + "it": "Lussemburgo", + "zh": "卢森堡", + "nl": "Luxemburg", + "de": "Luxemburg", + "fr": "Luxembourg", + "es": "Luxemburgo", + "en": "Luxembourg", + "pt_BR": "Luxemburgo", + "sr-Cyrl": "Луксенбург", + "sr-Latn": "Luksenburg", + "zh_TW": "盧森堡", + "tr": "Lüksemburg", + "ro": "Luxemburg", + "ar": "لوكسمبورغ", + "fa": "لوکزامبورگ", + "yue": "盧森堡" + }, + flag: "🇱🇺", + code: "LU", + dialCode: "352", + minLength: 11, + maxLength: 11, + ), + Country( + name: "Macao", + nameTranslations: { + "sk": "Macao – OAO Číny", + "se": "Makáo", + "pl": "SRA Makau (Chiny)", + "no": "Macao S.A.R. Kina", + "ja": "中華人民共和国マカオ特別行政区", + "it": "RAS di Macao", + "zh": "中国澳门特别行政区", + "nl": "Macau SAR van China", + "de": "Sonderverwaltungsregion Macau", + "fr": "R.A.S. chinoise de Macao", + "es": "RAE de Macao (China)", + "en": "Macao SAR China", + "pt_BR": "RAE de Macau China", + "sr-Cyrl": "Макао", + "sr-Latn": "Makao", + "zh_TW": "澳門", + "tr": "Makao", + "ro": "Macao", + "ar": "ماكاو", + "fa": "ماكائو", + "yue": "澳門" + }, + flag: "🇲🇴", + code: "MO", + dialCode: "853", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Macedonia", + nameTranslations: { + "sk": "Severné Macedónsko", + "se": "North Macedonia", + "pl": "Macedonia Północna", + "no": "Nord-Makedonia", + "ja": "北マケドニア", + "it": "Macedonia del Nord", + "zh": "北马其顿", + "nl": "Noord-Macedonië", + "de": "Nordmazedonien", + "fr": "Macédoine du Nord", + "es": "Macedonia del Norte", + "en": "North Macedonia", + "pt_BR": "Macedônia do Norte", + "sr-Cyrl": "Северна Македонија", + "sr-Latn": "Severna Makedonija", + "zh_TW": "北馬其頓", + "tr": "Kuzey Makedonya", + "ro": "Macedonia de Nord", + "ar": "مقدونيا", + "fa": "مقدونیه", + "yue": "馬其頓(前南斯拉夫共和國)" + }, + flag: "🇲🇰", + code: "MK", + dialCode: "389", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Madagascar", + nameTranslations: { + "sk": "Madagaskar", + "se": "Madagaskar", + "pl": "Madagaskar", + "no": "Madagaskar", + "ja": "マダガスカル", + "it": "Madagascar", + "zh": "马达加斯加", + "nl": "Madagaskar", + "de": "Madagaskar", + "fr": "Madagascar", + "es": "Madagascar", + "en": "Madagascar", + "pt_BR": "Madagáscar", + "sr-Cyrl": "Мадагаскар", + "sr-Latn": "Madagaskar", + "zh_TW": "馬達加斯加", + "tr": "Madagaskar", + "ro": "Madagascar", + "ar": "مدغشقر", + "fa": "ماداگاسکار", + "yue": "馬達加斯加" + }, + flag: "🇲🇬", + code: "MG", + dialCode: "261", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Malawi", + nameTranslations: { + "sk": "Malawi", + "se": "Malawi", + "pl": "Malawi", + "no": "Malawi", + "ja": "マラウイ", + "it": "Malawi", + "zh": "马拉维", + "nl": "Malawi", + "de": "Malawi", + "fr": "Malawi", + "es": "Malaui", + "en": "Malawi", + "pt_BR": "Malawi", + "sr-Cyrl": "Малави", + "sr-Latn": "Malavi", + "zh_TW": "馬拉威", + "tr": "Malavi", + "ro": "Malawi", + "ar": "مالاوي", + "fa": "مالاوی", + "yue": "馬拉維" + }, + flag: "🇲🇼", + code: "MW", + dialCode: "265", + minLength: 7, + maxLength: 9, + ), + Country( + name: "Malaysia", + nameTranslations: { + "sk": "Malajzia", + "se": "Malesia", + "pl": "Malezja", + "no": "Malaysia", + "ja": "マレーシア", + "it": "Malaysia", + "zh": "马来西亚", + "nl": "Maleisië", + "de": "Malaysia", + "fr": "Malaisie", + "es": "Malasia", + "en": "Malaysia", + "pt_BR": "Malásia", + "sr-Cyrl": "Малезија", + "sr-Latn": "Malezija", + "zh_TW": "馬來西亞", + "tr": "Malezya", + "ro": "Malaezia", + "ar": "ماليزيا", + "fa": "مالزی", + "yue": "馬來西亞" + }, + flag: "🇲🇾", + code: "MY", + dialCode: "60", + minLength: 9, + maxLength: 10, + ), + Country( + name: "Maldives", + nameTranslations: { + "sk": "Maldivy", + "se": "Malediivvat", + "pl": "Malediwy", + "no": "Maldivene", + "ja": "モルディブ", + "it": "Maldive", + "zh": "马尔代夫", + "nl": "Maldiven", + "de": "Malediven", + "fr": "Maldives", + "es": "Maldivas", + "en": "Maldives", + "pt_BR": "Maldivas", + "sr-Cyrl": "Малдиви", + "sr-Latn": "Maldivi", + "zh_TW": "馬爾地夫", + "tr": "Maldivler", + "ro": "Maldive", + "ar": "جزر المالديف", + "fa": "مالدیو", + "yue": "馬爾代夫" + }, + flag: "🇲🇻", + code: "MV", + dialCode: "960", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Mali", + nameTranslations: { + "sk": "Mali", + "se": "Mali", + "pl": "Mali", + "no": "Mali", + "ja": "マリ", + "it": "Mali", + "zh": "马里", + "nl": "Mali", + "de": "Mali", + "fr": "Mali", + "es": "Mali", + "en": "Mali", + "pt_BR": "Mali", + "sr-Cyrl": "Мали", + "sr-Latn": "Mali", + "zh_TW": "馬里", + "tr": "Mali", + "ro": "Mali", + "ar": "مالي", + "fa": "مالی", + "yue": "馬里" + }, + flag: "🇲🇱", + code: "ML", + dialCode: "223", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Malta", + nameTranslations: { + "sk": "Malta", + "se": "Málta", + "pl": "Malta", + "no": "Malta", + "ja": "マルタ", + "it": "Malta", + "zh": "马耳他", + "nl": "Malta", + "de": "Malta", + "fr": "Malte", + "es": "Malta", + "en": "Malta", + "pt_BR": "Malta", + "sr-Cyrl": "Малта", + "sr-Latn": "Malta", + "zh_TW": "馬爾他", + "tr": "Malta", + "ro": "Malta", + "ar": "مالطا", + "fa": "مالت", + "yue": "馬耳他" + }, + flag: "🇲🇹", + code: "MT", + dialCode: "356", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Marshall Islands", + nameTranslations: { + "sk": "Marshallove ostrovy", + "se": "Marshallsullot", + "pl": "Wyspy Marshalla", + "no": "Marshalløyene", + "ja": "マーシャル諸島", + "it": "Isole Marshall", + "zh": "马绍尔群岛", + "nl": "Marshalleilanden", + "de": "Marshallinseln", + "fr": "Îles Marshall", + "es": "Islas Marshall", + "en": "Marshall Islands", + "pt_BR": "Ilhas Marshall", + "sr-Cyrl": "Маршалска Острва", + "sr-Latn": "Maršalska Ostrva", + "zh_TW": "馬紹爾群島", + "tr": "Marshall Adaları", + "ro": "Insulele Marshall", + "ar": "جزر مارشال", + "fa": "جزایر مارشال", + "yue": "馬紹爾群島" + }, + flag: "🇲🇭", + code: "MH", + dialCode: "692", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Martinique", + nameTranslations: { + "sk": "Martinik", + "se": "Martinique", + "pl": "Martynika", + "no": "Martinique", + "ja": "マルティニーク", + "it": "Martinica", + "zh": "马提尼克", + "nl": "Martinique", + "de": "Martinique", + "fr": "Martinique", + "es": "Martinica", + "en": "Martinique", + "pt_BR": "Martinica", + "sr-Cyrl": "Мартиник", + "sr-Latn": "Martinik", + "zh_TW": "馬丁尼克", + "tr": "Martinique", + "ro": "Martinica", + "ar": "مارتينيك", + "fa": "مارتینیک", + "yue": "马提尼克" + }, + flag: "🇲🇶", + code: "MQ", + dialCode: "596", + minLength: 15, + maxLength: 15, + ), + Country( + name: "Mauritania", + nameTranslations: { + "sk": "Mauritánia", + "se": "Mauretánia", + "pl": "Mauretania", + "no": "Mauritania", + "ja": "モーリタニア", + "it": "Mauritania", + "zh": "毛里塔尼亚", + "nl": "Mauritanië", + "de": "Mauretanien", + "fr": "Mauritanie", + "es": "Mauritania", + "en": "Mauritania", + "pt_BR": "Mauritânia", + "sr-Cyrl": "Мауританија", + "sr-Latn": "Mauritanija", + "zh_TW": "茅利塔尼亞", + "tr": "Moritanya", + "ro": "Mauritania", + "ar": "موريتانيا", + "fa": "موریتانی", + "yue": "毛里塔尼亞" + }, + flag: "🇲🇷", + code: "MR", + dialCode: "222", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Mauritius", + nameTranslations: { + "sk": "Maurícius", + "se": "Mauritius", + "pl": "Mauritius", + "no": "Mauritius", + "ja": "モーリシャス", + "it": "Mauritius", + "zh": "毛里求斯", + "nl": "Mauritius", + "de": "Mauritius", + "fr": "Maurice", + "es": "Mauricio", + "en": "Mauritius", + "pt_BR": "Maurício", + "sr-Cyrl": "Маурицијус", + "sr-Latn": "Mauricijus", + "zh_TW": "模里西斯", + "tr": "Mauritius", + "ro": "Mauritius", + "ar": "موريشيوس", + "fa": "موریس", + "yue": "毛里求斯" + }, + flag: "🇲🇺", + code: "MU", + dialCode: "230", + minLength: 7, + maxLength: 8, + ), + Country( + name: "Mayotte", + nameTranslations: { + "sk": "Mayotte", + "se": "Mayotte", + "pl": "Majotta", + "no": "Mayotte", + "ja": "マヨット", + "it": "Mayotte", + "zh": "马约特", + "nl": "Mayotte", + "de": "Mayotte", + "fr": "Mayotte", + "es": "Mayotte", + "en": "Mayotte", + "pt_BR": "Mayotte", + "sr-Cyrl": "Мајота", + "sr-Latn": "Majota", + "zh_TW": "馬約特", + "tr": "Mayotte", + "ro": "Mayotte", + "ar": "مايوت", + "fa": "مایوت", + "yue": "馬約特" + }, + flag: "🇾🇹", + code: "YT", + dialCode: "262", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Mexico", + nameTranslations: { + "sk": "Mexiko", + "se": "Meksiko", + "pl": "Meksyk", + "no": "Mexico", + "ja": "メキシコ", + "it": "Messico", + "zh": "墨西哥", + "nl": "Mexico", + "de": "Mexiko", + "fr": "Mexique", + "es": "México", + "en": "Mexico", + "pt_BR": "México", + "sr-Cyrl": "Мексико", + "sr-Latn": "Meksiko", + "zh_TW": "墨西哥", + "tr": "Meksika", + "ro": "Mexic", + "ar": "المكسيك", + "fa": "مکزیک", + "yue": "墨西哥" + }, + flag: "🇲🇽", + code: "MX", + dialCode: "52", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Micronesia, Federated States of Micronesia", + nameTranslations: { + "sk": "Mikronézia", + "se": "Mikronesia", + "pl": "Mikronezja", + "no": "Mikronesiaføderasjonen", + "ja": "ミクロネシア連邦", + "it": "Micronesia", + "zh": "密克罗尼西亚", + "nl": "Micronesia", + "de": "Mikronesien", + "fr": "États fédérés de Micronésie", + "es": "Micronesia", + "en": "Micronesia", + "pt_BR": "Micronésia", + "sr-Cyrl": "Микронезија", + "sr-Latn": "Mikronezija", + "zh_TW": "密克羅尼西亞", + "tr": "Mikronezya", + "ro": "Micronezia", + "ar": "ولايات ميكرونيسيا المتحدة", + "fa": "ایالات فدرال میکرونزی", + "yue": "密克罗尼西亚(聯邦)" + }, + flag: "🇫🇲", + code: "FM", + dialCode: "691", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Moldova", + nameTranslations: { + "sk": "Moldavsko", + "se": "Moldávia", + "pl": "Mołdawia", + "no": "Moldova", + "ja": "モルドバ", + "it": "Moldavia", + "zh": "摩尔多瓦", + "nl": "Moldavië", + "de": "Republik Moldau", + "fr": "Moldavie", + "es": "Moldavia", + "en": "Moldova", + "pt_BR": "Moldova", + "sr-Cyrl": "Молдавија", + "sr-Latn": "Moldavija", + "zh_TW": "摩爾多瓦", + "tr": "Moldova", + "ro": "Moldova", + "ar": "مولدوفا", + "fa": "مولداوی", + "yue": "摩爾多瓦(共和國)" + }, + flag: "🇲🇩", + code: "MD", + dialCode: "373", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Monaco", + nameTranslations: { + "sk": "Monako", + "se": "Monaco", + "pl": "Monako", + "no": "Monaco", + "ja": "モナコ", + "it": "Monaco", + "zh": "摩纳哥", + "nl": "Monaco", + "de": "Monaco", + "fr": "Monaco", + "es": "Mónaco", + "en": "Monaco", + "pt_BR": "Mônaco", + "sr-Cyrl": "Монако", + "sr-Latn": "Monako", + "zh_TW": "摩納哥", + "tr": "Monako", + "ro": "Monaco", + "ar": "موناكو", + "fa": "موناكو", + "yue": "摩納哥" + }, + flag: "🇲🇨", + code: "MC", + dialCode: "377", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Mongolia", + nameTranslations: { + "sk": "Mongolsko", + "se": "Mongolia", + "pl": "Mongolia", + "no": "Mongolia", + "ja": "モンゴル", + "it": "Mongolia", + "zh": "蒙古", + "nl": "Mongolië", + "de": "Mongolei", + "fr": "Mongolie", + "es": "Mongolia", + "en": "Mongolia", + "pt_BR": "Mongólia", + "sr-Cyrl": "Монголија", + "sr-Latn": "Mongolija", + "zh_TW": "蒙古", + "tr": "Moğolistan", + "ro": "Mongolia", + "ar": "منغوليا", + "fa": "مغولستان", + "yue": "蒙古" + }, + flag: "🇲🇳", + code: "MN", + dialCode: "976", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Montenegro", + nameTranslations: { + "sk": "Čierna Hora", + "se": "Montenegro", + "pl": "Czarnogóra", + "no": "Montenegro", + "ja": "モンテネグロ", + "it": "Montenegro", + "zh": "黑山", + "nl": "Montenegro", + "de": "Montenegro", + "fr": "Monténégro", + "es": "Montenegro", + "en": "Montenegro", + "pt_BR": "Montenegro", + "sr-Cyrl": "Црна Гора", + "sr-Latn": "Crna Gora", + "zh_TW": "蒙特內哥羅", + "tr": "Karadağ", + "ro": "Muntenegru", + "ar": "الجبل الأسود", + "fa": "مونته‌نگرو", + "yue": "黑山" + }, + flag: "🇲🇪", + code: "ME", + dialCode: "382", + minLength: 12, + maxLength: 12, + ), + Country( + name: "Montserrat", + nameTranslations: { + "sk": "Montserrat", + "se": "Montserrat", + "pl": "Montserrat", + "no": "Montserrat", + "ja": "モントセラト", + "it": "Montserrat", + "zh": "蒙特塞拉特", + "nl": "Montserrat", + "de": "Montserrat", + "fr": "Montserrat", + "es": "Montserrat", + "en": "Montserrat", + "pt_BR": "Montserrat", + "sr-Cyrl": "Монтсерат", + "sr-Latn": "Montserat", + "zh_TW": "蒙哲臘", + "tr": "Montserrat", + "ro": "Montserrat", + "ar": "مونتسرات", + "fa": "مونتسرات", + "yue": "蒙特塞拉特" + }, + flag: "🇲🇸", + code: "MS", + dialCode: "1664", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Morocco", + nameTranslations: { + "sk": "Maroko", + "se": "Marokko", + "pl": "Maroko", + "no": "Marokko", + "ja": "モロッコ", + "it": "Marocco", + "zh": "摩洛哥", + "nl": "Marokko", + "de": "Marokko", + "fr": "Maroc", + "es": "Marruecos", + "en": "Morocco", + "pt_BR": "Marrocos", + "sr-Cyrl": "Мароко", + "sr-Latn": "Maroko", + "zh_TW": "摩洛哥", + "tr": "Fas", + "ro": "Maroc", + "ar": "المغرب", + "fa": "مراکش", + "yue": "摩洛哥" + }, + flag: "🇲🇦", + code: "MA", + dialCode: "212", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Mozambique", + nameTranslations: { + "sk": "Mozambik", + "se": "Mosambik", + "pl": "Mozambik", + "no": "Mosambik", + "ja": "モザンビーク", + "it": "Mozambico", + "zh": "莫桑比克", + "nl": "Mozambique", + "de": "Mosambik", + "fr": "Mozambique", + "es": "Mozambique", + "en": "Mozambique", + "pt_BR": "Moçambique", + "sr-Cyrl": "Мозамбик", + "sr-Latn": "Mozambik", + "zh_TW": "莫三比克", + "tr": "Mozambik", + "ro": "Mozambic", + "ar": "موزمبيق", + "fa": "موزامبیک", + "yue": "莫桑比克" + }, + flag: "🇲🇿", + code: "MZ", + dialCode: "258", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Myanmar", + nameTranslations: { + "sk": "Mjanmarsko", + "se": "Burma", + "pl": "Mjanma (Birma)", + "no": "Myanmar (Burma)", + "ja": "ミャンマー (ビルマ)", + "it": "Myanmar (Birmania)", + "zh": "缅甸", + "nl": "Myanmar (Birma)", + "de": "Myanmar", + "fr": "Myanmar (Birmanie)", + "es": "Myanmar (Birmania)", + "en": "Myanmar (Burma)", + "pt_BR": "Mianmar (Birmânia)", + "sr-Cyrl": "Мјанмар (Бурма)", + "sr-Latn": "Mjanmar (Burma)", + "zh_TW": "緬甸", + "tr": "Myanmar", + "ro": "Myanmar", + "ar": "ميانمار", + "fa": "میانمار", + "yue": "緬甸" + }, + flag: "🇲🇲", + code: "MM", + dialCode: "95", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Namibia", + nameTranslations: { + "sk": "Namíbia", + "se": "Namibia", + "pl": "Namibia", + "no": "Namibia", + "ja": "ナミビア", + "it": "Namibia", + "zh": "纳米比亚", + "nl": "Namibië", + "de": "Namibia", + "fr": "Namibie", + "es": "Namibia", + "en": "Namibia", + "pt_BR": "Namibia", + "sr-Cyrl": "Намибија", + "sr-Latn": "Namibija", + "zh_TW": "納米比亞", + "tr": "Namibya", + "ro": "Namibia", + "ar": "ناميبيا", + "fa": "نامیبیا", + "yue": "納米比亞" + }, + flag: "🇳🇦", + code: "NA", + dialCode: "264", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Nauru", + nameTranslations: { + "sk": "Nauru", + "se": "Nauru", + "pl": "Nauru", + "no": "Nauru", + "ja": "ナウル", + "it": "Nauru", + "zh": "瑙鲁", + "nl": "Nauru", + "de": "Nauru", + "fr": "Nauru", + "es": "Nauru", + "en": "Nauru", + "pt_BR": "Nauru", + "sr-Cyrl": "Науру", + "sr-Latn": "Nauru", + "zh_TW": "諾魯", + "tr": "Nauru", + "ro": "Nauru", + "ar": "ناورو", + "fa": "نائورو", + "yue": "瑙魯" + }, + flag: "🇳🇷", + code: "NR", + dialCode: "674", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Nepal", + nameTranslations: { + "sk": "Nepál", + "se": "Nepal", + "pl": "Nepal", + "no": "Nepal", + "ja": "ネパール", + "it": "Nepal", + "zh": "尼泊尔", + "nl": "Nepal", + "de": "Nepal", + "fr": "Népal", + "es": "Nepal", + "en": "Nepal", + "pt_BR": "Nepal", + "sr-Cyrl": "Непал", + "sr-Latn": "Nepal", + "zh_TW": "尼泊爾", + "tr": "Nepal", + "ro": "Nepal", + "ar": "نيبال", + "fa": "نپال", + "yue": "尼泊爾" + }, + flag: "🇳🇵", + code: "NP", + dialCode: "977", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Netherlands", + nameTranslations: { + "sk": "Holandsko", + "se": "Vuolleeatnamat", + "pl": "Holandia", + "no": "Nederland", + "ja": "オランダ", + "it": "Paesi Bassi", + "zh": "荷兰", + "nl": "Nederland", + "de": "Niederlande", + "fr": "Pays-Bas", + "es": "Países Bajos", + "en": "Netherlands", + "pt_BR": "Países Baixos", + "sr-Cyrl": "Холандија", + "sr-Latn": "Holandija", + "zh_TW": "荷蘭", + "tr": "Hollanda", + "ro": "Olanda", + "ar": "هولندا", + "fa": "هلند", + "yue": "荷蘭" + }, + flag: "🇳🇱", + code: "NL", + dialCode: "31", + minLength: 9, + maxLength: 9, + ), + Country( + name: "New Caledonia", + nameTranslations: { + "sk": "Nová Kaledónia", + "se": "Ođđa-Kaledonia", + "pl": "Nowa Kaledonia", + "no": "Ny-Caledonia", + "ja": "ニューカレドニア", + "it": "Nuova Caledonia", + "zh": "新喀里多尼亚", + "nl": "Nieuw-Caledonië", + "de": "Neukaledonien", + "fr": "Nouvelle-Calédonie", + "es": "Nueva Caledonia", + "en": "New Caledonia", + "pt_BR": "Nova Caledônia", + "sr-Cyrl": "Нова Каледонија", + "sr-Latn": "Nova Kaledonija", + "zh_TW": "新喀里多尼亞", + "tr": "Yeni Kaledonya", + "ro": "Noua Caledonie", + "ar": "كاليدونيا الجديدة", + "fa": "کالدونیای جدید", + "yue": "新喀里多尼亚" + }, + flag: "🇳🇨", + code: "NC", + dialCode: "687", + minLength: 6, + maxLength: 6, + ), + Country( + name: "New Zealand", + nameTranslations: { + "sk": "Nový Zéland", + "se": "Ođđa-Selánda", + "pl": "Nowa Zelandia", + "no": "New Zealand", + "ja": "ニュージーランド", + "it": "Nuova Zelanda", + "zh": "新西兰", + "nl": "Nieuw-Zeeland", + "de": "Neuseeland", + "fr": "Nouvelle-Zélande", + "es": "Nueva Zelanda", + "en": "New Zealand", + "pt_BR": "Nova Zelândia", + "sr-Cyrl": "Нови Зеланд", + "sr-Latn": "Novi Zeland", + "zh_TW": "紐西蘭", + "tr": "Yeni Zelanda", + "ro": "Noua Zeelandă", + "ar": "نيوزيلندا", + "fa": "نیوزلند", + "yue": "紐西蘭" + }, + flag: "🇳🇿", + code: "NZ", + dialCode: "64", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Nicaragua", + nameTranslations: { + "sk": "Nikaragua", + "se": "Nicaragua", + "pl": "Nikaragua", + "no": "Nicaragua", + "ja": "ニカラグア", + "it": "Nicaragua", + "zh": "尼加拉瓜", + "nl": "Nicaragua", + "de": "Nicaragua", + "fr": "Nicaragua", + "es": "Nicaragua", + "en": "Nicaragua", + "pt_BR": "Nicarágua", + "sr-Cyrl": "Никарагва", + "sr-Latn": "Nikaragva", + "zh_TW": "尼加拉瓜", + "tr": "Nikaragua", + "ro": "Nicaragua", + "ar": "نيكاراغوا", + "fa": "نیکاراگوئه", + "yue": "尼加拉瓜" + }, + flag: "🇳🇮", + code: "NI", + dialCode: "505", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Niger", + nameTranslations: { + "sk": "Niger", + "se": "Niger", + "pl": "Niger", + "no": "Niger", + "ja": "ニジェール", + "it": "Niger", + "zh": "尼日尔", + "nl": "Niger", + "de": "Niger", + "fr": "Niger", + "es": "Níger", + "en": "Niger", + "pt_BR": "Níger", + "sr-Cyrl": "Нигер", + "sr-Latn": "Niger", + "zh_TW": "尼日爾", + "tr": "Nijer", + "ro": "Niger", + "ar": "النيجر", + "fa": "نیجر", + "yue": "尼日爾" + }, + flag: "🇳🇪", + code: "NE", + dialCode: "227", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Nigeria", + nameTranslations: { + "sk": "Nigéria", + "se": "Nigeria", + "pl": "Nigeria", + "no": "Nigeria", + "ja": "ナイジェリア", + "it": "Nigeria", + "zh": "尼日利亚", + "nl": "Nigeria", + "de": "Nigeria", + "fr": "Nigéria", + "es": "Nigeria", + "en": "Nigeria", + "pt_BR": "Nigéria", + "sr-Cyrl": "Нигерија", + "sr-Latn": "Nigerija", + "zh_TW": "奈及利亞", + "tr": "Nijerya", + "ro": "Nigeria", + "ar": "نيجيريا", + "fa": "نیجریه", + "yue": "尼日利亞" + }, + flag: "🇳🇬", + code: "NG", + dialCode: "234", + minLength: 10, + maxLength: 11, + ), + Country( + name: "Niue", + nameTranslations: { + "sk": "Niue", + "se": "Niue", + "pl": "Niue", + "no": "Niue", + "ja": "ニウエ", + "it": "Niue", + "zh": "纽埃", + "nl": "Niue", + "de": "Niue", + "fr": "Niue", + "es": "Niue", + "en": "Niue", + "pt_BR": "Niue", + "sr-Cyrl": "Нијуе", + "sr-Latn": "Nijue", + "zh_TW": "紐埃", + "tr": "Niue", + "ro": "Niue", + "ar": "نييوي", + "fa": "نیووی", + "yue": "紐埃" + }, + flag: "🇳🇺", + code: "NU", + dialCode: "683", + minLength: 4, + maxLength: 4, + ), + Country( + name: "Norfolk Island", + nameTranslations: { + "sk": "Norfolk", + "se": "Norfolksullot", + "pl": "Norfolk", + "no": "Norfolkøya", + "ja": "ノーフォーク島", + "it": "Isola Norfolk", + "zh": "诺福克岛", + "nl": "Norfolk", + "de": "Norfolkinsel", + "fr": "Île Norfolk", + "es": "Isla Norfolk", + "en": "Norfolk Island", + "pt_BR": "Ilha Norfolk", + "sr-Cyrl": "Острво Норфок", + "sr-Latn": "Ostrvo Norfok", + "zh_TW": "諾福克島", + "tr": "Norfolk Adası", + "ro": "Insulele Norfolk", + "ar": "جزيرة نورفولك", + "fa": "جزیره نورفک", + "yue": "诺福克岛" + }, + flag: "🇳🇫", + code: "NF", + dialCode: "672", + minLength: 15, + maxLength: 15, + ), + Country( + name: "Northern Mariana Islands", + nameTranslations: { + "sk": "Severné Mariány", + "se": "Davvi-Mariánat", + "pl": "Mariany Północne", + "no": "Nord-Marianene", + "ja": "北マリアナ諸島", + "it": "Isole Marianne settentrionali", + "zh": "北马里亚纳群岛", + "nl": "Noordelijke Marianen", + "de": "Nördliche Marianen", + "fr": "Îles Mariannes du Nord", + "es": "Islas Marianas del Norte", + "en": "Northern Mariana Islands", + "pt_BR": "Ilhas Marianas do Norte", + "sr-Cyrl": "Северна Маријанска Острва", + "sr-Latn": "Severna Marijanska Ostrva", + "zh_TW": "北馬利安納群島", + "tr": "Kuzey Mariana Adaları", + "ro": "Insulelor Mariane de Nord", + "ar": "جزر ماريانا الشمالية", + "fa": "جزایر ماریانای شمالی", + "yue": "北馬里亞納群島" + }, + flag: "🇲🇵", + code: "MP", + dialCode: "1670", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Norway", + nameTranslations: { + "sk": "Nórsko", + "se": "Norga", + "pl": "Norwegia", + "no": "Norge", + "ja": "ノルウェー", + "it": "Norvegia", + "zh": "挪威", + "nl": "Noorwegen", + "de": "Norwegen", + "fr": "Norvège", + "es": "Noruega", + "en": "Norway", + "pt_BR": "Noruega", + "sr-Cyrl": "Норвешка", + "sr-Latn": "Norveška", + "zh_TW": "挪威", + "tr": "Norveç", + "ro": "Norvegia", + "ar": "النرويج", + "fa": "نروژ", + "yue": "挪威" + }, + flag: "🇳🇴", + code: "NO", + dialCode: "47", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Oman", + nameTranslations: { + "sk": "Omán", + "se": "Oman", + "pl": "Oman", + "no": "Oman", + "ja": "オマーン", + "it": "Oman", + "zh": "阿曼", + "nl": "Oman", + "de": "Oman", + "fr": "Oman", + "es": "Omán", + "en": "Oman", + "pt_BR": "Omã", + "sr-Cyrl": "Оман", + "sr-Latn": "Oman", + "zh_TW": "阿曼", + "tr": "Umman", + "ro": "Oman", + "ar": "عمان", + "fa": "عمان", + "yue": "阿曼" + }, + flag: "🇴🇲", + code: "OM", + dialCode: "968", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Pakistan", + nameTranslations: { + "sk": "Pakistan", + "se": "Pakistan", + "pl": "Pakistan", + "no": "Pakistan", + "ja": "パキスタン", + "it": "Pakistan", + "zh": "巴基斯坦", + "nl": "Pakistan", + "de": "Pakistan", + "fr": "Pakistan", + "es": "Pakistán", + "en": "Pakistan", + "pt_BR": "Paquistão", + "sr-Cyrl": "Пакистан", + "sr-Latn": "Pakistan", + "zh_TW": "巴基斯坦", + "tr": "Pakistan", + "ro": "Pakistan", + "ar": "باكستان", + "fa": "پاکستان", + "yue": "巴基斯坦" + }, + flag: "🇵🇰", + code: "PK", + dialCode: "92", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Palau", + nameTranslations: { + "sk": "Palau", + "se": "Palau", + "pl": "Palau", + "no": "Palau", + "ja": "パラオ", + "it": "Palau", + "zh": "帕劳", + "nl": "Palau", + "de": "Palau", + "fr": "Palaos", + "es": "Palaos", + "en": "Palau", + "pt_BR": "Palau", + "sr-Cyrl": "Палау", + "sr-Latn": "Palau", + "zh_TW": "帛琉", + "tr": "Palau", + "ro": "Palau", + "ar": "بالاو", + "fa": "پالائو", + "yue": "帕劳" + }, + flag: "🇵🇼", + code: "PW", + dialCode: "680", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Palestinian Territory, Occupied", + nameTranslations: { + "sk": "Palestínske územia", + "se": "Palestina", + "pl": "Terytoria Palestyńskie", + "no": "Det palestinske området", + "ja": "パレスチナ自治区", + "it": "Territori palestinesi", + "zh": "巴勒斯坦领土", + "nl": "Palestijnse gebieden", + "de": "Palästinensische Autonomiegebiete", + "fr": "Territoires palestiniens", + "es": "Territorios Palestinos", + "en": "Palestinian Territories", + "pt_BR": "Territórios Palestinos", + "sr-Cyrl": "Палестина", + "sr-Latn": "Palestina", + "zh_TW": "巴勒斯坦", + "tr": "Filistin", + "ro": "Palestina", + "ar": "فلسطين", + "fa": "فلسطین", + "yue": "巴勒斯坦,国" + }, + flag: "🇵🇸", + code: "PS", + dialCode: "970", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Panama", + nameTranslations: { + "sk": "Panama", + "se": "Panama", + "pl": "Panama", + "no": "Panama", + "ja": "パナマ", + "it": "Panamá", + "zh": "巴拿马", + "nl": "Panama", + "de": "Panama", + "fr": "Panama", + "es": "Panamá", + "en": "Panama", + "pt_BR": "Panamá", + "sr-Cyrl": "Панама", + "sr-Latn": "Panama", + "zh_TW": "巴拿馬", + "tr": "Panama", + "ro": "Panama", + "ar": "بنما", + "fa": "پاناما", + "yue": "巴拿馬" + }, + flag: "🇵🇦", + code: "PA", + dialCode: "507", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Papua New Guinea", + nameTranslations: { + "sk": "Papua-Nová Guinea", + "se": "Papua-Ođđa-Guinea", + "pl": "Papua-Nowa Gwinea", + "no": "Papua Ny-Guinea", + "ja": "パプアニューギニア", + "it": "Papua Nuova Guinea", + "zh": "巴布亚新几内亚", + "nl": "Papoea-Nieuw-Guinea", + "de": "Papua-Neuguinea", + "fr": "Papouasie-Nouvelle-Guinée", + "es": "Papúa Nueva Guinea", + "en": "Papua New Guinea", + "pt_BR": "Papua Nova Guiné", + "sr-Cyrl": "Папуа Нова Гвинеја", + "sr-Latn": "Papua Nova Gvineja", + "zh_TW": "巴布亞新幾內亞", + "tr": "Papua Yeni Gine", + "ro": "Papua Noua Guinee", + "ar": "بابوا غينيا الجديدة", + "fa": "پاپوآ گینه نو", + "yue": "巴布亚新几内亚" + }, + flag: "🇵🇬", + code: "PG", + dialCode: "675", + minLength: 11, + maxLength: 11, + ), + Country( + name: "Paraguay", + nameTranslations: { + "sk": "Paraguaj", + "se": "Paraguay", + "pl": "Paragwaj", + "no": "Paraguay", + "ja": "パラグアイ", + "it": "Paraguay", + "zh": "巴拉圭", + "nl": "Paraguay", + "de": "Paraguay", + "fr": "Paraguay", + "es": "Paraguay", + "en": "Paraguay", + "pt_BR": "Paraguai", + "sr-Cyrl": "Парагвај", + "sr-Latn": "Paragvaj", + "zh_TW": "巴拉圭", + "tr": "Paraguay", + "ro": "Paraguay", + "ar": "باراغواي", + "fa": "پاراگوئه", + "yue": "巴拉圭" + }, + flag: "🇵🇾", + code: "PY", + dialCode: "595", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Peru", + nameTranslations: { + "sk": "Peru", + "se": "Peru", + "pl": "Peru", + "no": "Peru", + "ja": "ペルー", + "it": "Perù", + "zh": "秘鲁", + "nl": "Peru", + "de": "Peru", + "fr": "Pérou", + "es": "Perú", + "en": "Peru", + "pt_BR": "Peru", + "sr-Cyrl": "Перу", + "sr-Latn": "Peru", + "zh_TW": "秘鲁", + "tr": "Peru", + "ro": "Peru", + "ar": "بيرو", + "fa": "پرو", + "yue": "秘魯" + }, + flag: "🇵🇪", + code: "PE", + dialCode: "51", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Philippines", + nameTranslations: { + "sk": "Filipíny", + "se": "Filippiinnat", + "pl": "Filipiny", + "no": "Filippinene", + "ja": "フィリピン", + "it": "Filippine", + "zh": "菲律宾", + "nl": "Filipijnen", + "de": "Philippinen", + "fr": "Philippines", + "es": "Filipinas", + "en": "Philippines", + "pt_BR": "Filipinas", + "sr-Cyrl": "Филипини", + "sr-Latn": "Filipini", + "zh_TW": "菲律賓", + "tr": "Filipinler", + "ro": "Filipine", + "ar": "الفلبين", + "fa": "فیلیپین", + "yue": "菲律賓" + }, + flag: "🇵🇭", + code: "PH", + dialCode: "63", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Pitcairn", + nameTranslations: { + "sk": "Pitcairnove ostrovy", + "se": "Pitcairn", + "pl": "Pitcairn", + "no": "Pitcairnøyene", + "ja": "ピトケアン諸島", + "it": "Isole Pitcairn", + "zh": "皮特凯恩群岛", + "nl": "Pitcairneilanden", + "de": "Pitcairninseln", + "fr": "Îles Pitcairn", + "es": "Islas Pitcairn", + "en": "Pitcairn Islands", + "pt_BR": "Ilhas Pitcairn", + "sr-Cyrl": "Острва Питкерн", + "sr-Latn": "Ostrva Pitkern", + "zh_TW": "皮特肯群島", + "tr": "Pitcairn Adaları", + "ro": "Insulele Pitcairn", + "ar": "جزر بيتكيرن", + "fa": "جزایر پیت‌کرن", + "yue": "皮特凱恩" + }, + flag: "🇵🇳", + code: "PN", + dialCode: "64", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Poland", + nameTranslations: { + "sk": "Poľsko", + "se": "Polen", + "pl": "Polska", + "no": "Polen", + "ja": "ポーランド", + "it": "Polonia", + "zh": "波兰", + "nl": "Polen", + "de": "Polen", + "fr": "Pologne", + "es": "Polonia", + "en": "Poland", + "pt_BR": "Polônia", + "sr-Cyrl": "Пољска", + "sr-Latn": "Poljska", + "zh_TW": "波蘭", + "tr": "Polonya", + "ro": "Polonia", + "ar": "بولندا", + "fa": "لهستان", + "yue": "波蘭" + }, + flag: "🇵🇱", + code: "PL", + dialCode: "48", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Portugal", + nameTranslations: { + "sk": "Portugalsko", + "se": "Portugála", + "pl": "Portugalia", + "no": "Portugal", + "ja": "ポルトガル", + "it": "Portogallo", + "zh": "葡萄牙", + "nl": "Portugal", + "de": "Portugal", + "fr": "Portugal", + "es": "Portugal", + "en": "Portugal", + "pt_BR": "Portugal", + "sr-Cyrl": "Португалија", + "sr-Latn": "Portugalija", + "zh_TW": "葡萄牙", + "tr": "Portekiz", + "ro": "Portugalia", + "ar": "البرتغال", + "fa": "پرتغال", + "yue": "葡萄牙" + }, + flag: "🇵🇹", + code: "PT", + dialCode: "351", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Puerto Rico", + nameTranslations: { + "sk": "Portoriko", + "se": "Puerto Rico", + "pl": "Portoryko", + "no": "Puerto Rico", + "ja": "プエルトリコ", + "it": "Portorico", + "zh": "波多黎各", + "nl": "Puerto Rico", + "de": "Puerto Rico", + "fr": "Porto Rico", + "es": "Puerto Rico", + "en": "Puerto Rico", + "pt_BR": "Porto Rico", + "sr-Cyrl": "Порто Рико", + "sr-Latn": "Porto Riko", + "zh_TW": "波多黎各", + "tr": "Porto Riko", + "ro": "Puerto Rico", + "ar": "بورتوريكو", + "fa": "پورتوریکو", + "yue": "波多黎各" + }, + flag: "🇵🇷", + code: "PR", + dialCode: "1939", + minLength: 15, + maxLength: 15, + ), + Country( + name: "Qatar", + nameTranslations: { + "sk": "Katar", + "se": "Qatar", + "pl": "Katar", + "no": "Qatar", + "ja": "カタール", + "it": "Qatar", + "zh": "卡塔尔", + "nl": "Qatar", + "de": "Katar", + "fr": "Qatar", + "es": "Catar", + "en": "Qatar", + "pt_BR": "Catar", + "sr-Cyrl": "Катар", + "sr-Latn": "Katar", + "zh_TW": "卡達", + "tr": "Katar", + "ro": "Qatar", + "ar": "قطر", + "fa": "قطر", + "yue": "卡塔爾" + }, + flag: "🇶🇦", + code: "QA", + dialCode: "974", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Romania", + nameTranslations: { + "sk": "Rumunsko", + "se": "Románia", + "pl": "Rumunia", + "no": "Romania", + "ja": "ルーマニア", + "it": "Romania", + "zh": "罗马尼亚", + "nl": "Roemenië", + "de": "Rumänien", + "fr": "Roumanie", + "es": "Rumanía", + "en": "Romania", + "pt_BR": "Romênia", + "sr-Cyrl": "Румунија", + "sr-Latn": "Rumunija", + "zh_TW": "羅馬尼亞", + "tr": "Romanya", + "ro": "România", + "ar": "رومانيا", + "fa": "رومانی", + "yue": "羅馬尼亞" + }, + flag: "🇷🇴", + code: "RO", + dialCode: "40", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Russia", + nameTranslations: { + "sk": "Rusko", + "se": "Ruošša", + "pl": "Rosja", + "no": "Russland", + "ja": "ロシア", + "it": "Russia", + "zh": "俄罗斯", + "nl": "Rusland", + "de": "Russland", + "fr": "Russie", + "es": "Rusia", + "en": "Russia", + "pt_BR": "Rússia", + "sr-Cyrl": "Русија", + "sr-Latn": "Rusija", + "zh_TW": "俄羅斯", + "tr": "Rusya", + "ro": "Rusia", + "ar": "روسيا", + "fa": "روسیه", + "yue": "俄儸斯聯邦" + }, + flag: "🇷🇺", + code: "RU", + dialCode: "7", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Rwanda", + nameTranslations: { + "sk": "Rwanda", + "se": "Rwanda", + "pl": "Rwanda", + "no": "Rwanda", + "ja": "ルワンダ", + "it": "Ruanda", + "zh": "卢旺达", + "nl": "Rwanda", + "de": "Ruanda", + "fr": "Rwanda", + "es": "Ruanda", + "en": "Rwanda", + "pt_BR": "Ruanda", + "sr-Cyrl": "Руанда", + "sr-Latn": "Ruanda", + "zh_TW": "盧安達", + "tr": "Ruanda", + "ro": "Rwanda", + "ar": "رواندا", + "fa": "رواندا", + "yue": "盧旺達" + }, + flag: "🇷🇼", + code: "RW", + dialCode: "250", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Reunion", + nameTranslations: { + "sk": "Réunion", + "se": "Réunion", + "pl": "Reunion", + "no": "Réunion", + "ja": "レユニオン", + "it": "Riunione", + "zh": "留尼汪", + "nl": "Réunion", + "de": "Réunion", + "fr": "La Réunion", + "es": "Reunión", + "en": "Réunion", + "pt_BR": "Reunião", + "sr-Cyrl": "Реинион", + "sr-Latn": "Reinion", + "zh_TW": "留尼旺", + "tr": "La Réunion", + "ro": "La Réunion", + "ar": "لا ريونيون", + "fa": "رئونیون", + "yue": "留尼汪" + }, + flag: "🇷🇪", + code: "RE", + dialCode: "262", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Saint Barthelemy", + nameTranslations: { + "sk": "Svätý Bartolomej", + "se": "Saint Barthélemy", + "pl": "Saint-Barthélemy", + "no": "Saint-Barthélemy", + "ja": "サン・バルテルミー", + "it": "Saint-Barthélemy", + "zh": "圣巴泰勒米", + "nl": "Saint-Barthélemy", + "de": "St. Barthélemy", + "fr": "Saint-Barthélemy", + "es": "San Bartolomé", + "en": "St. Barthélemy", + "pt_BR": "São Bartolomeu", + "sr-Cyrl": "Сент Бартелеми", + "sr-Latn": "Sent Bartelemi", + "zh_TW": "聖巴瑟米", + "tr": "Saint Barthélemy", + "ro": "Saint Barthélemy", + "ar": "سان بارتيلمي", + "fa": "سن بارتلمی", + "yue": "聖巴泰勒米" + }, + flag: "🇧🇱", + code: "BL", + dialCode: "590", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Saint Helena, Ascension and Tristan Da Cunha", + nameTranslations: { + "sk": "Svätá Helena", + "se": "Saint Helena", + "pl": "Wyspa Świętej Heleny", + "no": "St. Helena", + "ja": "セントヘレナ", + "it": "Sant'Elena", + "zh": "圣赫勒拿", + "nl": "Sint-Helena", + "de": "St. Helena", + "fr": "Sainte-Hélène", + "es": "Santa Elena", + "en": "St. Helena", + "pt_BR": "Santa Helena", + "sr-Cyrl": "Света Јелена, Асенсион и Тристан да Куња", + "sr-Latn": "Sveta Jelena, Asension i Tristan de Kunja", + "zh_TW": "聖凱倫拿島", + "tr": "Saint Helena", + "ro": "Sfânta Elena", + "ar": "سانت هيلانة وأسينشين وتريستان دا كونا", + "fa": "سنت هلن", + "yue": "圣赫勒拿、阿森松同特里斯坦·达库尼亚" + }, + flag: "🇸🇭", + code: "SH", + dialCode: "290", + minLength: 4, + maxLength: 4, + ), + Country( + name: "Saint Kitts and Nevis", + nameTranslations: { + "sk": "Svätý Krištof a Nevis", + "se": "Saint Kitts ja Nevis", + "pl": "Saint Kitts i Nevis", + "no": "Saint Kitts og Nevis", + "ja": "セントクリストファー・ネーヴィス", + "it": "Saint Kitts e Nevis", + "zh": "圣基茨和尼维斯", + "nl": "Saint Kitts en Nevis", + "de": "St. Kitts und Nevis", + "fr": "Saint-Christophe-et-Niévès", + "es": "San Cristóbal y Nieves", + "en": "St. Kitts & Nevis", + "pt_BR": "São Cristóvão e Nevis", + "sr-Cyrl": "Сент Китс и Невис", + "sr-Latn": "Sent Kits i Nevis", + "zh_TW": "聖克里斯多福及尼維斯", + "tr": "Saint Kitts ve Nevis", + "ro": "Sfântul Kitts și Nevis", + "ar": "سانت كيتس ونيفيس", + "fa": "سنت کیتس و نویس", + "yue": "圣基茨同尼维斯" + }, + flag: "🇰🇳", + code: "KN", + dialCode: "1869", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Saint Lucia", + nameTranslations: { + "sk": "Svätá Lucia", + "se": "Saint Lucia", + "pl": "Saint Lucia", + "no": "St. Lucia", + "ja": "セントルシア", + "it": "Saint Lucia", + "zh": "圣卢西亚", + "nl": "Saint Lucia", + "de": "St. Lucia", + "fr": "Sainte-Lucie", + "es": "Santa Lucía", + "en": "St. Lucia", + "pt_BR": "Santa Lúcia", + "sr-Cyrl": "Света Луција", + "sr-Latn": "Sveta Lucija", + "zh_TW": "聖露西亞", + "tr": "Saint Lucia", + "ro": "Sfânta Elena", + "ar": "سانت لوسيا", + "fa": "سنت لوسیا", + "yue": "聖盧西亞" + }, + flag: "🇱🇨", + code: "LC", + dialCode: "1758", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Saint Martin", + nameTranslations: { + "sk": "Svätý Martin (fr.)", + "se": "Frankriikka Saint Martin", + "pl": "Saint-Martin", + "no": "Saint-Martin", + "ja": "サン・マルタン", + "it": "Saint Martin", + "zh": "法属圣马丁", + "nl": "Saint-Martin", + "de": "St. Martin", + "fr": "Saint-Martin", + "es": "San Martín", + "en": "St. Martin", + "pt_BR": "São Martinho", + "sr-Cyrl": "Свети Мартин", + "sr-Latn": "Sveti Martin", + "zh_TW": "聖馬丁", + "tr": "Saint Martin", + "ro": "Sfântul Martin", + "ar": "تجمع سان مارتين", + "fa": "سن مارتن", + "yue": "聖馬丁(法國部分)" + }, + flag: "🇲🇫", + code: "MF", + dialCode: "590", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Saint Pierre and Miquelon", + nameTranslations: { + "sk": "Saint Pierre a Miquelon", + "se": "Saint Pierre ja Miquelon", + "pl": "Saint-Pierre i Miquelon", + "no": "Saint-Pierre-et-Miquelon", + "ja": "サンピエール島・ミクロン島", + "it": "Saint-Pierre e Miquelon", + "zh": "圣皮埃尔和密克隆群岛", + "nl": "Saint-Pierre en Miquelon", + "de": "St. Pierre und Miquelon", + "fr": "Saint-Pierre-et-Miquelon", + "es": "San Pedro y Miquelón", + "en": "St. Pierre & Miquelon", + "pt_BR": "São Pedro e Miquelon", + "sr-Cyrl": "Сен Пјер и Микелон", + "sr-Latn": "Sen Pjer i Mikelon", + "zh_TW": "聖皮埃與密克隆群島", + "tr": "Saint Pierre ve Miquelon", + "ro": "Saint Pierre și Miquelon", + "ar": "سان بيير وميكلون", + "fa": "سن-پیر و میکلون", + "yue": "聖皮埃尔同米克隆" + }, + flag: "🇵🇲", + code: "PM", + dialCode: "508", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Saint Vincent and the Grenadines", + nameTranslations: { + "sk": "Svätý Vincent a Grenadíny", + "se": "Saint Vincent ja Grenadine", + "pl": "Saint Vincent i Grenadyny", + "no": "St. Vincent og Grenadinene", + "ja": "セントビンセント及びグレナディーン諸島", + "it": "Saint Vincent e Grenadine", + "zh": "圣文森特和格林纳丁斯", + "nl": "Saint Vincent en de Grenadines", + "de": "St. Vincent und die Grenadinen", + "fr": "Saint-Vincent-et-les-Grenadines", + "es": "San Vicente y las Granadinas", + "en": "St. Vincent & Grenadines", + "pt_BR": "São Vicente e Granadinas", + "sr-Cyrl": "Свети Винсент и Гренадини", + "sr-Latn": "Sveti Vinsent i Grenadini", + "zh_TW": "聖文森及格瑞那丁", + "tr": "Saint Vincent ve Grenadinler", + "ro": "Sfântul Vincențiu și Grenadinele", + "ar": "سانت فينسنت والغرينادين", + "fa": "سنت وینسنت و گرنادین‌ها", + "yue": "聖文森特同格林纳丁斯" + }, + flag: "🇻🇨", + code: "VC", + dialCode: "1784", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Samoa", + nameTranslations: { + "sk": "Samoa", + "se": "Samoa", + "pl": "Samoa", + "no": "Samoa", + "ja": "サモア", + "it": "Samoa", + "zh": "萨摩亚", + "nl": "Samoa", + "de": "Samoa", + "fr": "Samoa", + "es": "Samoa", + "en": "Samoa", + "pt_BR": "Samoa", + "sr-Cyrl": "Самоа", + "sr-Latn": "Samoa", + "zh_TW": "薩摩亞", + "tr": "Samoa", + "ro": "Samoa", + "ar": "ساموا", + "fa": "ساموآ", + "yue": "薩摩亞" + }, + flag: "🇼🇸", + code: "WS", + dialCode: "685", + minLength: 7, + maxLength: 7, + ), + Country( + name: "San Marino", + nameTranslations: { + "sk": "San Maríno", + "se": "San Marino", + "pl": "San Marino", + "no": "San Marino", + "ja": "サンマリノ", + "it": "San Marino", + "zh": "圣马力诺", + "nl": "San Marino", + "de": "San Marino", + "fr": "Saint-Marin", + "es": "San Marino", + "en": "San Marino", + "pt_BR": "San Marino", + "sr-Cyrl": "Сан Марино", + "sr-Latn": "San Marino", + "zh_TW": "聖馬利諾", + "tr": "San Marino", + "ro": "San Marino", + "ar": "سان مارينو", + "fa": "سان مارینو", + "yue": "聖馬力諾" + }, + flag: "🇸🇲", + code: "SM", + dialCode: "378", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Sao Tome and Principe", + nameTranslations: { + "sk": "Svätý Tomáš a Princov ostrov", + "se": "São Tomé ja Príncipe", + "pl": "Wyspy Świętego Tomasza i Książęca", + "no": "São Tomé og Príncipe", + "ja": "サントメ・プリンシペ", + "it": "São Tomé e Príncipe", + "zh": "圣多美和普林西比", + "nl": "Sao Tomé en Principe", + "de": "São Tomé und Príncipe", + "fr": "Sao Tomé-et-Principe", + "es": "Santo Tomé y Príncipe", + "en": "São Tomé & Príncipe", + "pt_BR": "São Tomé e Príncipe", + "sr-Cyrl": "Сао Томе и Принсипе", + "sr-Latn": "Sao Tome i Prinsipe", + "zh_TW": "聖多美普林西比", + "tr": "São Tomé ve Príncipe", + "ro": "Sao Tome şi Principe", + "ar": "ساو تومي وبرينسيب", + "fa": "سائوتومه و پرنسیپ", + "yue": "聖多美和普林西比" + }, + flag: "🇸🇹", + code: "ST", + dialCode: "239", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Saudi Arabia", + nameTranslations: { + "sk": "Saudská Arábia", + "se": "Saudi-Arábia", + "pl": "Arabia Saudyjska", + "no": "Saudi-Arabia", + "ja": "サウジアラビア", + "it": "Arabia Saudita", + "zh": "沙特阿拉伯", + "nl": "Saoedi-Arabië", + "de": "Saudi-Arabien", + "fr": "Arabie saoudite", + "es": "Arabia Saudí", + "en": "Saudi Arabia", + "pt_BR": "Arábia Saudita", + "sr-Cyrl": "Саудијска Арабија", + "sr-Latn": "Saudijska Arabija", + "zh_TW": "沙烏地阿拉", + "tr": "Suudi Arabistan", + "ro": "Arabia Saudită", + "ar": "السعودية", + "fa": "عربستان سعودی", + "yue": "沙地阿拉伯" + }, + flag: "🇸🇦", + code: "SA", + dialCode: "966", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Senegal", + nameTranslations: { + "sk": "Senegal", + "se": "Senegal", + "pl": "Senegal", + "no": "Senegal", + "ja": "セネガル", + "it": "Senegal", + "zh": "塞内加尔", + "nl": "Senegal", + "de": "Senegal", + "fr": "Sénégal", + "es": "Senegal", + "en": "Senegal", + "pt_BR": "Senegal", + "sr-Cyrl": "Сенегал", + "sr-Latn": "Senegal", + "zh_TW": "塞內加爾", + "tr": "Senegal", + "ro": "Senegal", + "ar": "السنغال", + "fa": "سنگال", + "yue": "塞內加爾" + }, + flag: "🇸🇳", + code: "SN", + dialCode: "221", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Serbia", + nameTranslations: { + "sk": "Srbsko", + "se": "Serbia", + "pl": "Serbia", + "no": "Serbia", + "ja": "セルビア", + "it": "Serbia", + "zh": "塞尔维亚", + "nl": "Servië", + "de": "Serbien", + "fr": "Serbie", + "es": "Serbia", + "en": "Serbia", + "pt_BR": "Sérvia", + "sr-Cyrl": "Србија", + "sr-Latn": "Srbija", + "zh_TW": "塞爾維亞", + "tr": "Sırbistan", + "ro": "Serbia", + "ar": "صربيا", + "fa": "صربستان", + "yue": "塞爾維亞" + }, + flag: "🇷🇸", + code: "RS", + dialCode: "381", + minLength: 12, + maxLength: 12, + ), + Country( + name: "Seychelles", + nameTranslations: { + "sk": "Seychely", + "se": "Seychellsullot", + "pl": "Seszele", + "no": "Seychellene", + "ja": "セーシェル", + "it": "Seychelles", + "zh": "塞舌尔", + "nl": "Seychellen", + "de": "Seychellen", + "fr": "Seychelles", + "es": "Seychelles", + "en": "Seychelles", + "pt_BR": "Seychelles", + "sr-Cyrl": "Сејшели", + "sr-Latn": "Sejšeli", + "zh_TW": "塞席爾", + "tr": "Seyşeller", + "ro": "Seychelles", + "ar": "سيشل", + "fa": "سیشل", + "yue": "塞舌爾" + }, + flag: "🇸🇨", + code: "SC", + dialCode: "248", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Sierra Leone", + nameTranslations: { + "sk": "Sierra Leone", + "se": "Sierra Leone", + "pl": "Sierra Leone", + "no": "Sierra Leone", + "ja": "シエラレオネ", + "it": "Sierra Leone", + "zh": "塞拉利昂", + "nl": "Sierra Leone", + "de": "Sierra Leone", + "fr": "Sierra Leone", + "es": "Sierra Leona", + "en": "Sierra Leone", + "pt_BR": "Serra Leoa", + "sr-Cyrl": "Сијера Леоне", + "sr-Latn": "Sijera Leone", + "zh_TW": "獅子山", + "tr": "Sierra Leone", + "ro": "Sierra Leone", + "ar": "سيراليون", + "fa": "سیرالئون", + "yue": "塞拉利昂" + }, + flag: "🇸🇱", + code: "SL", + dialCode: "232", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Singapore", + nameTranslations: { + "sk": "Singapur", + "se": "Singapore", + "pl": "Singapur", + "no": "Singapore", + "ja": "シンガポール", + "it": "Singapore", + "zh": "新加坡", + "nl": "Singapore", + "de": "Singapur", + "fr": "Singapour", + "es": "Singapur", + "en": "Singapore", + "pt_BR": "Cingapura", + "sr-Cyrl": "Сингапур", + "sr-Latn": "Singapur", + "zh_TW": "新加坡", + "tr": "Singapur", + "ro": "Singapore", + "ar": "سنغافورة", + "fa": "سنگاپور", + "yue": "星架坡" + }, + flag: "🇸🇬", + code: "SG", + dialCode: "65", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Slovakia", + nameTranslations: { + "sk": "Slovensko", + "se": "Slovákia", + "pl": "Słowacja", + "no": "Slovakia", + "ja": "スロバキア", + "it": "Slovacchia", + "zh": "斯洛伐克", + "nl": "Slowakije", + "de": "Slowakei", + "fr": "Slovaquie", + "es": "Eslovaquia", + "en": "Slovakia", + "pt_BR": "Eslováquia", + "sr-Cyrl": "Словачка", + "sr-Latn": "Slovačka", + "zh_TW": "斯洛伐克", + "tr": "Slovakya", + "ro": "Slovacia", + "ar": "سلوفاكيا", + "fa": "اسلواکی", + "yue": "斯洛伐克" + }, + flag: "🇸🇰", + code: "SK", + dialCode: "421", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Slovenia", + nameTranslations: { + "sk": "Slovinsko", + "se": "Slovenia", + "pl": "Słowenia", + "no": "Slovenia", + "ja": "スロベニア", + "it": "Slovenia", + "zh": "斯洛文尼亚", + "nl": "Slovenië", + "de": "Slowenien", + "fr": "Slovénie", + "es": "Eslovenia", + "en": "Slovenia", + "pt_BR": "Eslovênia", + "sr-Cyrl": "Словеније", + "sr-Latn": "Slovenija", + "zh_TW": "斯洛維尼亞", + "tr": "Slovenya", + "ro": "Slovenia", + "ar": "سلوفينيا", + "fa": "اسلوونی", + "yue": "斯洛文尼亞" + }, + flag: "🇸🇮", + code: "SI", + dialCode: "386", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Solomon Islands", + nameTranslations: { + "sk": "Šalamúnove ostrovy", + "se": "Salomon-sullot", + "pl": "Wyspy Salomona", + "no": "Salomonøyene", + "ja": "ソロモン諸島", + "it": "Isole Salomone", + "zh": "所罗门群岛", + "nl": "Salomonseilanden", + "de": "Salomonen", + "fr": "Îles Salomon", + "es": "Islas Salomón", + "en": "Solomon Islands", + "pt_BR": "Ilhas Salomão", + "sr-Cyrl": "Соломонска Острва", + "sr-Latn": "Solomonska Ostrva", + "zh_TW": "所羅門群島", + "tr": "Solomon Adaları", + "ro": "Insulele Solomon", + "ar": "جزر سليمان", + "fa": "جزایر سلیمان", + "yue": "所羅門群島" + }, + flag: "🇸🇧", + code: "SB", + dialCode: "677", + minLength: 5, + maxLength: 5, + ), + Country( + name: "Somalia", + nameTranslations: { + "sk": "Somálsko", + "se": "Somália", + "pl": "Somalia", + "no": "Somalia", + "ja": "ソマリア", + "it": "Somalia", + "zh": "索马里", + "nl": "Somalië", + "de": "Somalia", + "fr": "Somalie", + "es": "Somalia", + "en": "Somalia", + "pt_BR": "Somália", + "sr-Cyrl": "Сомалија", + "sr-Latn": "Somalija", + "zh_TW": "索馬利亞", + "tr": "Somali", + "ro": "Somalia", + "ar": "الصومال", + "fa": "سومالی", + "yue": "索馬里" + }, + flag: "🇸🇴", + code: "SO", + dialCode: "252", + minLength: 8, + maxLength: 8, + ), + Country( + name: "South Africa", + nameTranslations: { + "sk": "Južná Afrika", + "se": "Mátta-Afrihká", + "pl": "Republika Południowej Afryki", + "no": "Sør-Afrika", + "ja": "南アフリカ", + "it": "Sudafrica", + "zh": "南非", + "nl": "Zuid-Afrika", + "de": "Südafrika", + "fr": "Afrique du Sud", + "es": "Sudáfrica", + "en": "South Africa", + "pt_BR": "África do Sul", + "sr-Cyrl": "Јужноафричка Република", + "sr-Latn": "Južnoafrička Republika", + "zh_TW": "南非", + "tr": "Güney Afrika", + "ro": "Africa de Sud", + "ar": "جنوب أفريقيا", + "fa": "آفریقای جنوبی", + "yue": "南非" + }, + flag: "🇿🇦", + code: "ZA", + dialCode: "27", + minLength: 9, + maxLength: 9, + ), + Country( + name: "South Sudan", + nameTranslations: { + "sk": "Južný Sudán", + "se": "Máttasudan", + "pl": "Sudan Południowy", + "no": "Sør-Sudan", + "ja": "南スーダン", + "it": "Sud Sudan", + "zh": "南苏丹", + "nl": "Zuid-Soedan", + "de": "Südsudan", + "fr": "Soudan du Sud", + "es": "Sudán del Sur", + "en": "South Sudan", + "pt_BR": "Sudão do Sul", + "sr-Cyrl": "Јужни Судан", + "sr-Latn": "Južni Sudan", + "zh_TW": "南蘇丹", + "tr": "Güney Sudan", + "ro": "Sudanul de Sud", + "ar": "جنوب السودان", + "fa": "سودان جنوبی", + "yue": "南蘇丹" + }, + flag: "🇸🇸", + code: "SS", + dialCode: "211", + minLength: 9, + maxLength: 9, + ), + Country( + name: "South Georgia and the South Sandwich Islands", + nameTranslations: { + "sk": "Južná Georgia a Južné Sandwichove ostrovy", + "se": "Lulli Georgia ja Lulli Sandwich-sullot", + "pl": "Georgia Południowa i Sandwich Południowy", + "no": "Sør-Georgia og Sør-Sandwichøyene", + "ja": "サウスジョージア・サウスサンドウィッチ諸島", + "it": "Georgia del Sud e Sandwich australi", + "zh": "南乔治亚和南桑威奇群岛", + "nl": "Zuid-Georgia en Zuidelijke Sandwicheilanden", + "de": "Südgeorgien und die Südlichen Sandwichinseln", + "fr": "Géorgie du Sud et îles Sandwich du Sud", + "es": "Islas Georgia del Sur y Sandwich del Sur", + "en": "South Georgia & South Sandwich Islands", + "pt_BR": "Geórgia do Sul e Ilhas Sandwich do Sul", + "sr-Cyrl": "Јужна Џорџија и Јужна Сендвичка Острва", + "sr-Latn": "Južna Džordžija i Južna Sendvička Ostrva", + "zh_TW": "南喬治亞與南三明治群島 ", + "tr": "Güney Georgia ve Güney Sandwich Adaları", + "ro": "Georgia de Sud și Insulele Sandwich de Sud", + "ar": "جورجيا الجنوبية وجزر ساندويتش الجنوبية", + "fa": "جزایر جورجیای جنوبی و ساندویچ جنوبی", + "yue": "南喬治亞州同南桑威奇群島" + }, + flag: "🇬🇸", + code: "GS", + dialCode: "500", + minLength: 15, + maxLength: 15, + ), + Country( + name: "Spain", + nameTranslations: { + "sk": "Španielsko", + "se": "Spánia", + "pl": "Hiszpania", + "no": "Spania", + "ja": "スペイン", + "it": "Spagna", + "zh": "西班牙", + "nl": "Spanje", + "de": "Spanien", + "fr": "Espagne", + "es": "España", + "en": "Spain", + "pt_BR": "Espanha", + "sr-Cyrl": "Шпанија", + "sr-Latn": "Španija", + "zh_TW": "西班牙", + "tr": "İspanya", + "ro": "Spania", + "ar": "إسبانيا", + "fa": "اسپانیا", + "yue": "西班牙" + }, + flag: "🇪🇸", + code: "ES", + dialCode: "34", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Sri Lanka", + nameTranslations: { + "sk": "Srí Lanka", + "se": "Sri Lanka", + "pl": "Sri Lanka", + "no": "Sri Lanka", + "ja": "スリランカ", + "it": "Sri Lanka", + "zh": "斯里兰卡", + "nl": "Sri Lanka", + "de": "Sri Lanka", + "fr": "Sri Lanka", + "es": "Sri Lanka", + "en": "Sri Lanka", + "pt_BR": "Sri Lanka", + "sr-Cyrl": "Шри Ланка", + "sr-Latn": "Šri Lanka", + "zh_TW": "斯里蘭卡", + "tr": "Sri Lanka", + "ro": "Sri Lanka", + "ar": "سريلانكا", + "fa": "سریلانکا", + "yue": "斯里蘭卡" + }, + flag: "🇱🇰", + code: "LK", + dialCode: "94", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Sudan", + nameTranslations: { + "sk": "Sudán", + "se": "Davvisudan", + "pl": "Sudan", + "no": "Sudan", + "ja": "スーダン", + "it": "Sudan", + "zh": "苏丹", + "nl": "Soedan", + "de": "Sudan", + "fr": "Soudan", + "es": "Sudán", + "en": "Sudan", + "pt_BR": "Sudão", + "sr-Cyrl": "Судан", + "sr-Latn": "Sudan", + "zh_TW": "蘇丹", + "tr": "Sudan", + "ro": "Sudan", + "ar": "السودان", + "fa": "سودان", + "yue": "蘇丹" + }, + flag: "🇸🇩", + code: "SD", + dialCode: "249", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Suriname", + nameTranslations: { + "sk": "Surinam", + "se": "Surinam", + "pl": "Surinam", + "no": "Surinam", + "ja": "スリナム", + "it": "Suriname", + "zh": "苏里南", + "nl": "Suriname", + "de": "Suriname", + "fr": "Suriname", + "es": "Surinam", + "en": "Suriname", + "pt_BR": "Suriname", + "sr-Cyrl": "Суринам", + "sr-Latn": "Surinam", + "zh_TW": "蘇利南", + "tr": "Surinam", + "ro": "Surinam", + "ar": "سورينام", + "fa": "سورینام", + "yue": "蘇里南" + }, + flag: "🇸🇷", + code: "SR", + dialCode: "597", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Svalbard and Jan Mayen", + nameTranslations: { + "sk": "Svalbard a Jan Mayen", + "se": "Svalbárda ja Jan Mayen", + "pl": "Svalbard i Jan Mayen", + "no": "Svalbard og Jan Mayen", + "ja": "スバールバル諸島・ヤンマイエン島", + "it": "Svalbard e Jan Mayen", + "zh": "斯瓦尔巴和扬马延", + "nl": "Spitsbergen en Jan Mayen", + "de": "Spitzbergen und Jan Mayen", + "fr": "Svalbard et Jan Mayen", + "es": "Svalbard y Jan Mayen", + "en": "Svalbard & Jan Mayen", + "pt_BR": "Svalbard e Jan Mayen", + "sr-Cyrl": "Свалбард", + "sr-Latn": "Svalbard", + "zh_TW": "斯瓦巴及尖棉", + "tr": "Svalbard ve Jan Mayen", + "ro": "Svalbard și Jan Mayen", + "ar": "سفالبارد ويان ماين", + "fa": "سوالبارد و یان ماین", + "yue": "斯瓦尔巴德同扬·马延" + }, + flag: "🇸🇯", + code: "SJ", + dialCode: "47", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Eswatini", + nameTranslations: { + "sk": "Eswatini", + "se": "Svazieana", + "pl": "Eswatini", + "no": "Eswatini", + "ja": "エスワティニ", + "it": "Swaziland", + "zh": "斯威士兰", + "nl": "eSwatini", + "de": "Eswatini", + "fr": "Eswatini", + "es": "Esuatini", + "en": "Eswatini", + "pt_BR": "Eswatini", + "sr-Cyrl": "Свазиланд", + "sr-Latn": "Svaziland", + "zh_TW": "史瓦帝尼", + "tr": "Esvatini", + "ro": "Eswatini", + "ar": "إسواتيني", + "fa": "اسواتینی", + "yue": "斯威士蘭" + }, + flag: "🇸🇿", + code: "SZ", + dialCode: "268", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Sweden", + nameTranslations: { + "sk": "Švédsko", + "se": "Ruoŧŧa", + "pl": "Szwecja", + "no": "Sverige", + "ja": "スウェーデン", + "it": "Svezia", + "zh": "瑞典", + "nl": "Zweden", + "de": "Schweden", + "fr": "Suède", + "es": "Suecia", + "en": "Sweden", + "pt_BR": "Suécia", + "sr-Cyrl": "Шведска", + "sr-Latn": "Švedska", + "zh_TW": "瑞典", + "tr": "İsveç", + "ro": "Suedia", + "ar": "السويد", + "fa": "سوئد", + "yue": "瑞典" + }, + flag: "🇸🇪", + code: "SE", + dialCode: "46", + minLength: 7, + maxLength: 13, + ), + Country( + name: "Switzerland", + nameTranslations: { + "sk": "Švajčiarsko", + "se": "Šveica", + "pl": "Szwajcaria", + "no": "Sveits", + "ja": "スイス", + "it": "Svizzera", + "zh": "瑞士", + "nl": "Zwitserland", + "de": "Schweiz", + "fr": "Suisse", + "es": "Suiza", + "en": "Switzerland", + "pt_BR": "Suíça", + "sr-Cyrl": "Швајцарска", + "sr-Latn": "Švajcarska", + "zh_TW": "瑞士", + "tr": "İsviçre", + "ro": "Elveţia", + "ar": "سويسرا", + "fa": "سوئیس", + "yue": "瑞士" + }, + flag: "🇨🇭", + code: "CH", + dialCode: "41", + minLength: 9, + maxLength: 12, + ), + Country( + name: "Syrian Arab Republic", + nameTranslations: { + "sk": "Sýria", + "se": "Syria", + "pl": "Syria", + "no": "Syria", + "ja": "シリア", + "it": "Siria", + "zh": "叙利亚", + "nl": "Syrië", + "de": "Syrien", + "fr": "Syrie", + "es": "Siria", + "en": "Syria", + "pt_BR": "Síria", + "sr-Cyrl": "Сирија", + "sr-Latn": "Sirija", + "zh_TW": "敘利亞", + "tr": "Suriye", + "ro": "Siria", + "ar": "سوريا", + "fa": "سوریه", + "yue": "阿拉伯敘利亞共和國" + }, + flag: "🇸🇾", + code: "SY", + dialCode: "963", + minLength: 9, + maxLength: 10, + ), + Country( + name: "Taiwan", + nameTranslations: { + "sk": "Taiwan", + "se": "Taiwan", + "pl": "Tajwan", + "no": "Taiwan", + "ja": "台湾", + "it": "Taiwan", + "zh": "台湾", + "nl": "Taiwan", + "de": "Taiwan", + "fr": "Taïwan", + "es": "Taiwán", + "en": "Taiwan", + "pt_BR": "Taiwan", + "sr-Cyrl": "Тајван", + "sr-Latn": "Tajvan", + "zh_TW": "台灣", + "tr": "Tayvan", + "ro": "Taiwan", + "ar": "تايوان", + "fa": "تایوان", + "yue": "台灣" + }, + flag: "🇹🇼", + code: "TW", + dialCode: "886", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Tajikistan", + nameTranslations: { + "sk": "Tadžikistan", + "se": "Tažikistan", + "pl": "Tadżykistan", + "no": "Tadsjikistan", + "ja": "タジキスタン", + "it": "Tagikistan", + "zh": "塔吉克斯坦", + "nl": "Tadzjikistan", + "de": "Tadschikistan", + "fr": "Tadjikistan", + "es": "Tayikistán", + "en": "Tajikistan", + "pt_BR": "Tajiquistão", + "sr-Cyrl": "Таџикистан", + "sr-Latn": "Tadžikistan", + "zh_TW": "塔吉克", + "tr": "Tacikistan", + "ro": "Tadiquistão", + "ar": "طاجيكستان", + "fa": "تاجیکستان", + "yue": "塔吉克斯坦" + }, + flag: "🇹🇯", + code: "TJ", + dialCode: "992", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Tanzania, United Republic of Tanzania", + nameTranslations: { + "sk": "Tanzánia", + "se": "Tanzánia", + "pl": "Tanzania", + "no": "Tanzania", + "ja": "タンザニア", + "it": "Tanzania", + "zh": "坦桑尼亚", + "nl": "Tanzania", + "de": "Tansania", + "fr": "Tanzanie", + "es": "Tanzania", + "en": "Tanzania", + "pt_BR": "Tanzânia", + "sr-Cyrl": "Танзанија", + "sr-Latn": "Tanzanija", + "zh_TW": "坦尚尼亞", + "tr": "Tanzanya", + "ro": "Tanzania", + "ar": "تنزانيا", + "fa": "تانزانیا", + "yue": "坦桑尼亞,聯合共和國" + }, + flag: "🇹🇿", + code: "TZ", + dialCode: "255", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Thailand", + nameTranslations: { + "sk": "Thajsko", + "se": "Thaieana", + "pl": "Tajlandia", + "no": "Thailand", + "ja": "タイ", + "it": "Thailandia", + "zh": "泰国", + "nl": "Thailand", + "de": "Thailand", + "fr": "Thaïlande", + "es": "Tailandia", + "en": "Thailand", + "pt_BR": "Tailândia", + "sr-Cyrl": "Тајланд", + "sr-Latn": "Tajland", + "zh_TW": "泰國", + "tr": "Tayland", + "ro": "Tailanda", + "ar": "تايلاند", + "fa": "تایلند", + "yue": "泰國" + }, + flag: "🇹🇭", + code: "TH", + dialCode: "66", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Timor-Leste", + nameTranslations: { + "sk": "Východný Timor", + "se": "Nuorta-Timor", + "pl": "Timor Wschodni", + "no": "Øst-Timor", + "ja": "東ティモール", + "it": "Timor Est", + "zh": "东帝汶", + "nl": "Oost-Timor", + "de": "Timor-Leste", + "fr": "Timor oriental", + "es": "Timor-Leste", + "en": "Timor-Leste", + "pt_BR": "Timor-Leste", + "sr-Cyrl": "Источни Тимор", + "sr-Latn": "Istočni Timor", + "zh_TW": "東帝汶", + "tr": "Doğu Timor", + "ro": "Timorul de Est", + "ar": "تيمور الشرقية", + "fa": "تیمور شرقی", + "yue": "東帝汶" + }, + flag: "🇹🇱", + code: "TL", + dialCode: "670", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Togo", + nameTranslations: { + "sk": "Togo", + "se": "Togo", + "pl": "Togo", + "no": "Togo", + "ja": "トーゴ", + "it": "Togo", + "zh": "多哥", + "nl": "Togo", + "de": "Togo", + "fr": "Togo", + "es": "Togo", + "en": "Togo", + "pt_BR": "Ir", + "sr-Cyrl": "Того", + "sr-Latn": "Togo", + "zh_TW": "多哥", + "tr": "Togo", + "ro": "Togo", + "ar": "توغو", + "fa": "توگو", + "yue": "多哥" + }, + flag: "🇹🇬", + code: "TG", + dialCode: "228", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Tokelau", + nameTranslations: { + "sk": "Tokelau", + "se": "Tokelau", + "pl": "Tokelau", + "no": "Tokelau", + "ja": "トケラウ", + "it": "Tokelau", + "zh": "托克劳", + "nl": "Tokelau", + "de": "Tokelau", + "fr": "Tokelau", + "es": "Tokelau", + "en": "Tokelau", + "pt_BR": "Tokelau", + "sr-Cyrl": "Токелау", + "sr-Latn": "Tokelau", + "zh_TW": "托克勞", + "tr": "Tokelau", + "ro": "Tokelau", + "ar": "توكيلاو", + "fa": "توکلائو", + "yue": "托克劳" + }, + flag: "🇹🇰", + code: "TK", + dialCode: "690", + minLength: 4, + maxLength: 4, + ), + Country( + name: "Tonga", + nameTranslations: { + "sk": "Tonga", + "se": "Tonga", + "pl": "Tonga", + "no": "Tonga", + "ja": "トンガ", + "it": "Tonga", + "zh": "汤加", + "nl": "Tonga", + "de": "Tonga", + "fr": "Tonga", + "es": "Tonga", + "en": "Tonga", + "pt_BR": "Tonga", + "sr-Cyrl": "Тонга", + "sr-Latn": "Tonga", + "zh_TW": "東加", + "tr": "Tonga", + "ro": "Tonga", + "ar": "تونغا", + "fa": "تونگا", + "yue": "湯加" + }, + flag: "🇹🇴", + code: "TO", + dialCode: "676", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Trinidad and Tobago", + nameTranslations: { + "sk": "Trinidad a Tobago", + "se": "Trinidad ja Tobago", + "pl": "Trynidad i Tobago", + "no": "Trinidad og Tobago", + "ja": "トリニダード・トバゴ", + "it": "Trinidad e Tobago", + "zh": "特立尼达和多巴哥", + "nl": "Trinidad en Tobago", + "de": "Trinidad und Tobago", + "fr": "Trinité-et-Tobago", + "es": "Trinidad y Tobago", + "en": "Trinidad & Tobago", + "pt_BR": "Trinidad e Tobago", + "sr-Cyrl": "Тринидад и Тобаго", + "sr-Latn": "Trinidad i Tobago", + "zh_TW": "千里達及托巴哥", + "tr": "Trinidad ve Tobago", + "ro": "Trinidad şi Tobago", + "ar": "ترينيداد وتوباغو", + "fa": "ترینیداد و توباگو", + "yue": "特立尼達和多巴哥" + }, + flag: "🇹🇹", + code: "TT", + dialCode: "1868", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Tunisia", + nameTranslations: { + "sk": "Tunisko", + "se": "Tunisia", + "pl": "Tunezja", + "no": "Tunisia", + "ja": "チュニジア", + "it": "Tunisia", + "zh": "突尼斯", + "nl": "Tunesië", + "de": "Tunesien", + "fr": "Tunisie", + "es": "Túnez", + "en": "Tunisia", + "pt_BR": "Tunísia", + "sr-Cyrl": "Тунис", + "sr-Latn": "Tunis", + "zh_TW": "突尼西亞", + "tr": "Tunus", + "ro": "Tunisia", + "ar": "تونس", + "fa": "تونس", + "yue": "突尼斯" + }, + flag: "🇹🇳", + code: "TN", + dialCode: "216", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Turkey", + nameTranslations: { + "sk": "Turecko", + "se": "Durka", + "pl": "Turcja", + "no": "Tyrkia", + "ja": "トルコ", + "it": "Turchia", + "zh": "土耳其", + "nl": "Turkije", + "de": "Türkei", + "fr": "Turquie", + "es": "Turquía", + "en": "Turkey", + "pt_BR": "Peru", + "sr-Cyrl": "Турска", + "sr-Latn": "Turska", + "zh_TW": "土耳其", + "tr": "Türkiye", + "ro": "Turcia", + "ar": "تركيا", + "fa": "ترکیه", + "yue": "土耳其" + }, + flag: "🇹🇷", + code: "TR", + dialCode: "90", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Turkmenistan", + nameTranslations: { + "sk": "Turkménsko", + "se": "Turkmenistan", + "pl": "Turkmenistan", + "no": "Turkmenistan", + "ja": "トルクメニスタン", + "it": "Turkmenistan", + "zh": "土库曼斯坦", + "nl": "Turkmenistan", + "de": "Turkmenistan", + "fr": "Turkménistan", + "es": "Turkmenistán", + "en": "Turkmenistan", + "pt_BR": "Turcomenistão", + "sr-Cyrl": "Туркменистан", + "sr-Latn": "Turkmenistan", + "zh_TW": "土庫曼", + "tr": "Türkmenistan", + "ro": "Turkmenistan", + "ar": "تركمانستان", + "fa": "ترکمنستان", + "yue": "土庫曼斯坦" + }, + flag: "🇹🇲", + code: "TM", + dialCode: "993", + minLength: 8, + maxLength: 8, + ), + Country( + name: "Turks and Caicos Islands", + nameTranslations: { + "sk": "Turks a Caicos", + "se": "Turks ja Caicos-sullot", + "pl": "Turks i Caicos", + "no": "Turks- og Caicosøyene", + "ja": "タークス・カイコス諸島", + "it": "Isole Turks e Caicos", + "zh": "特克斯和凯科斯群岛", + "nl": "Turks- en Caicoseilanden", + "de": "Turks- und Caicosinseln", + "fr": "Îles Turques-et-Caïques", + "es": "Islas Turcas y Caicos", + "en": "Turks & Caicos Islands", + "pt_BR": "Ilhas Turks e Caicos", + "sr-Cyrl": "Туркс и Кајкос", + "sr-Latn": "Turks i Kajkos", + "zh_TW": "土克斯及開科斯群島", + "tr": "Turks ve Caicos Adaları", + "ro": "Insulele Turks și Caicos", + "ar": "جزر توركس وكايكوس", + "fa": "جزایر تورکس و کایکوس", + "yue": "特克斯同凯科斯群岛" + }, + flag: "🇹🇨", + code: "TC", + dialCode: "1649", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Tuvalu", + nameTranslations: { + "sk": "Tuvalu", + "se": "Tuvalu", + "pl": "Tuvalu", + "no": "Tuvalu", + "ja": "ツバル", + "it": "Tuvalu", + "zh": "图瓦卢", + "nl": "Tuvalu", + "de": "Tuvalu", + "fr": "Tuvalu", + "es": "Tuvalu", + "en": "Tuvalu", + "pt_BR": "Tuvalu", + "sr-Cyrl": "Тувалу", + "sr-Latn": "Tuvalu", + "zh_TW": "圖瓦盧", + "tr": "Tuvalu", + "ro": "Tuvalu", + "ar": "توفالو", + "fa": "تووالو", + "yue": "圖瓦盧" + }, + flag: "🇹🇻", + code: "TV", + dialCode: "688", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Uganda", + nameTranslations: { + "sk": "Uganda", + "se": "Uganda", + "pl": "Uganda", + "no": "Uganda", + "ja": "ウガンダ", + "it": "Uganda", + "zh": "乌干达", + "nl": "Oeganda", + "de": "Uganda", + "fr": "Ouganda", + "es": "Uganda", + "en": "Uganda", + "pt_BR": "Uganda", + "sr-Cyrl": "Уганда", + "sr-Latn": "Uganda", + "zh_TW": "烏干達", + "tr": "Uganda", + "ro": "Uganda", + "ar": "أوغندا", + "fa": "اوگاندا", + "yue": "烏干達" + }, + flag: "🇺🇬", + code: "UG", + dialCode: "256", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Ukraine", + nameTranslations: { + "sk": "Ukrajina", + "se": "Ukraina", + "pl": "Ukraina", + "no": "Ukraina", + "ja": "ウクライナ", + "it": "Ucraina", + "zh": "乌克兰", + "nl": "Oekraïne", + "de": "Ukraine", + "fr": "Ukraine", + "es": "Ucrania", + "en": "Ukraine", + "pt_BR": "Ucrânia", + "sr-Cyrl": "Украјина", + "sr-Latn": "Ukrajina", + "zh_TW": "烏克蘭", + "tr": "Ukrayna", + "ro": "Ucraína", + "ar": "أوكرانيا", + "fa": "اوکراین", + "yue": "烏克蘭" + }, + flag: "🇺🇦", + code: "UA", + dialCode: "380", + minLength: 9, + maxLength: 9, + ), + Country( + name: "United Arab Emirates", + nameTranslations: { + "sk": "Spojené arabské emiráty", + "se": "Ovttastuvvan Arábaemiráhtat", + "pl": "Zjednoczone Emiraty Arabskie", + "no": "De forente arabiske emirater", + "ja": "アラブ首長国連邦", + "it": "Emirati Arabi Uniti", + "zh": "阿拉伯联合酋长国", + "nl": "Verenigde Arabische Emiraten", + "de": "Vereinigte Arabische Emirate", + "fr": "Émirats arabes unis", + "es": "Emiratos Árabes Unidos", + "en": "United Arab Emirates", + "pt_BR": "Emirados Árabes Unidos", + "sr-Cyrl": "Уједињени Арапски Емирати", + "sr-Latn": "Ujedinjeni Arapski Emirati", + "zh_TW": "阿拉伯聯合大公國", + "tr": "Birleşik Arap Emirlikleri", + "ro": "Emiratele Arabe Unite", + "ar": "الإمارات العربية المتحدة", + "fa": "امارات متحده عربی", + "yue": "阿拉伯聯合酋長國" + }, + flag: "🇦🇪", + code: "AE", + dialCode: "971", + minLength: 9, + maxLength: 9, + ), + Country( + name: "United Kingdom", + nameTranslations: { + "sk": "Spojené kráľovstvo", + "se": "Stuorra-Británnia", + "pl": "Wielka Brytania", + "no": "Storbritannia", + "ja": "イギリス", + "it": "Regno Unito", + "zh": "英国", + "nl": "Verenigd Koninkrijk", + "de": "Vereinigtes Königreich", + "fr": "Royaume-Uni", + "es": "Reino Unido", + "en": "United Kingdom", + "pt_BR": "Reino Unido", + "sr-Cyrl": "Уједињено Краљевство", + "sr-Latn": "Ujedinjeno Kraljevstvo", + "zh_TW": "英國", + "tr": "Büyük Britanya ve Kuzey İrlanda Birleşik Krallığ", + "ro": "Regatul Unit al Marii Britanii și Irlandei de Nord", + "ar": "المملكة المتحدة", + "fa": "بریتانیا", + "yue": "大不列顛及北愛爾蘭聯合王國" + }, + flag: "🇬🇧", + code: "GB", + dialCode: "44", + minLength: 10, + maxLength: 10, + ), + Country( + name: "United States", + nameTranslations: { + "sk": "Spojené štáty", + "se": "Amerihká ovttastuvvan stáhtat", + "pl": "Stany Zjednoczone", + "no": "USA", + "ja": "アメリカ合衆国", + "it": "Stati Uniti", + "zh": "美国", + "nl": "Verenigde Staten", + "de": "Vereinigte Staaten", + "fr": "États-Unis", + "es": "Estados Unidos", + "en": "United States", + "pt_BR": "Estados Unidos", + "sr-Cyrl": "Сједињене Америчке Државе", + "sr-Latn": "Sjedinjene Američke Države", + "zh_TW": "美國", + "tr": "Amerika Birleşik Devletleri", + "ro": "Statele Unite ale Americii", + "ar": "الولايات المتحدة", + "fa": "ایالات متحده آمریکا", + "yue": "美利堅郃眾囯" + }, + flag: "🇺🇸", + code: "US", + dialCode: "1", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Uruguay", + nameTranslations: { + "sk": "Uruguaj", + "se": "Uruguay", + "pl": "Urugwaj", + "no": "Uruguay", + "ja": "ウルグアイ", + "it": "Uruguay", + "zh": "乌拉圭", + "nl": "Uruguay", + "de": "Uruguay", + "fr": "Uruguay", + "es": "Uruguay", + "en": "Uruguay", + "pt_BR": "Uruguai", + "sr-Cyrl": "Уругвај", + "sr-Latn": "Urugvaj", + "zh_TW": "烏拉圭", + "tr": "Uruguay", + "ro": "Uruguay", + "ar": "الأوروغواي", + "fa": "اروگوئه", + "yue": "烏拉圭" + }, + flag: "🇺🇾", + code: "UY", + dialCode: "598", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Uzbekistan", + nameTranslations: { + "sk": "Uzbekistan", + "se": "Usbekistan", + "pl": "Uzbekistan", + "no": "Usbekistan", + "ja": "ウズベキスタン", + "it": "Uzbekistan", + "zh": "乌兹别克斯坦", + "nl": "Oezbekistan", + "de": "Usbekistan", + "fr": "Ouzbékistan", + "es": "Uzbekistán", + "en": "Uzbekistan", + "pt_BR": "Uzbequistão", + "sr-Cyrl": "Узбекистан", + "sr-Latn": "Uzbekistan", + "zh_TW": "烏玆別克", + "tr": "Özbekistan", + "ro": "Uzbekistan", + "ar": "أوزبكستان", + "fa": "ازبکستان", + "yue": "月即別" + }, + flag: "🇺🇿", + code: "UZ", + dialCode: "998", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Vanuatu", + nameTranslations: { + "sk": "Vanuatu", + "se": "Vanuatu", + "pl": "Vanuatu", + "no": "Vanuatu", + "ja": "バヌアツ", + "it": "Vanuatu", + "zh": "瓦努阿图", + "nl": "Vanuatu", + "de": "Vanuatu", + "fr": "Vanuatu", + "es": "Vanuatu", + "en": "Vanuatu", + "pt_BR": "Vanuatu", + "sr-Cyrl": "Вануату", + "sr-Latn": "Vanuatu", + "zh_TW": "瓦努阿圖", + "tr": "Vanuatu", + "ro": "Vanuatu", + "ar": "فانواتو", + "fa": "وانواتو", + "yue": "瓦努阿圖" + }, + flag: "🇻🇺", + code: "VU", + dialCode: "678", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Venezuela, Bolivarian Republic of Venezuela", + nameTranslations: { + "sk": "Venezuela", + "se": "Venezuela", + "pl": "Wenezuela", + "no": "Venezuela", + "ja": "ベネズエラ", + "it": "Venezuela", + "zh": "委内瑞拉", + "nl": "Venezuela", + "de": "Venezuela", + "fr": "Venezuela", + "es": "Venezuela", + "en": "Venezuela", + "pt_BR": "Venezuela", + "sr-Cyrl": "Венецуела", + "sr-Latn": "Venecuela", + "zh_TW": "委內瑞拉", + "tr": "Venezuela", + "ro": "Venezuela", + "ar": "فنزويلا", + "fa": "ونزوئلا", + "yue": "委內瑞拉(玻利瓦爾共和國)" + }, + flag: "🇻🇪", + code: "VE", + dialCode: "58", + minLength: 10, + maxLength: 10, + ), + Country( + name: "Vietnam", + nameTranslations: { + "sk": "Vietnam", + "se": "Vietnam", + "pl": "Wietnam", + "no": "Vietnam", + "ja": "ベトナム", + "it": "Vietnam", + "zh": "越南", + "nl": "Vietnam", + "de": "Vietnam", + "fr": "Vietnam", + "es": "Vietnam", + "en": "Vietnam", + "pt_BR": "Vietnã", + "sr-Cyrl": "Вијетнам", + "sr-Latn": "Vijetnam", + "zh_TW": "越南", + "tr": "Vietnam", + "ro": "Vietnam", + "ar": "فيتنام", + "fa": "ویتنام", + "yue": "越南" + }, + flag: "🇻🇳", + code: "VN", + dialCode: "84", + minLength: 11, + maxLength: 11, + ), + Country( + name: "Virgin Islands, British", + nameTranslations: { + "sk": "Britské Panenské ostrovy", + "se": "Brittania Virgin-sullot", + "pl": "Brytyjskie Wyspy Dziewicze", + "no": "De britiske jomfruøyene", + "ja": "英領ヴァージン諸島", + "it": "Isole Vergini Britanniche", + "zh": "英属维尔京群岛", + "nl": "Britse Maagdeneilanden", + "de": "Britische Jungferninseln", + "fr": "Îles Vierges britanniques", + "es": "Islas Vírgenes Británicas", + "en": "British Virgin Islands", + "pt_BR": "Ilhas Virgens Britânicas", + "sr-Cyrl": "Британска Девичанска Острва", + "sr-Latn": "Britanska Devičanska Ostrva", + "zh_TW": "英屬維京群島", + "tr": "Britanya Virjin Adaları", + "ro": "Insulele Virgine Britanice", + "ar": "جزر العذراء البريطانية", + "fa": "جزایر ویرجین بریتانیا", + "yue": "維爾京群島(英國)" + }, + flag: "🇻🇬", + code: "VG", + dialCode: "1284", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Virgin Islands, U.S.", + nameTranslations: { + "sk": "Americké Panenské ostrovy", + "se": "AOS Virgin-sullot", + "pl": "Wyspy Dziewicze Stanów Zjednoczonych", + "no": "De amerikanske jomfruøyene", + "ja": "米領ヴァージン諸島", + "it": "Isole Vergini Americane", + "zh": "美属维尔京群岛", + "nl": "Amerikaanse Maagdeneilanden", + "de": "Amerikanische Jungferninseln", + "fr": "Îles Vierges des États-Unis", + "es": "Islas Vírgenes de EE. UU.", + "en": "U.S. Virgin Islands", + "pt_BR": "Ilhas Virgens Americanas", + "sr-Cyrl": "Амепичка Девичанска Острва", + "sr-Latn": "Američka Devičanska Ostrva", + "zh_TW": "美屬維京群島", + "tr": "Amerika Birleşik Devletleri Virjin Adaları", + "ro": "Insulele Virgine Americane", + "ar": "جزر العذراء الأمريكية", + "fa": "جزایر ویرجین ایالات متحده آمریکا", + "yue": "維爾京群島(美國)" + }, + flag: "🇻🇮", + code: "VI", + dialCode: "1340", + minLength: 7, + maxLength: 7, + ), + Country( + name: "Wallis and Futuna", + nameTranslations: { + "sk": "Wallis a Futuna", + "se": "Wallis ja Futuna", + "pl": "Wallis i Futuna", + "no": "Wallis og Futuna", + "ja": "ウォリス・フツナ", + "it": "Wallis e Futuna", + "zh": "瓦利斯和富图纳", + "nl": "Wallis en Futuna", + "de": "Wallis und Futuna", + "fr": "Wallis-et-Futuna", + "es": "Wallis y Futuna", + "en": "Wallis & Futuna", + "pt_BR": "Wallis e Futuna", + "sr-Cyrl": "Валис и Футуна", + "sr-Latn": "Valis i Futuna", + "zh_TW": "瓦利斯和富圖那", + "tr": "Wallis ve Futuna", + "ro": "Wallis și Futuna", + "ar": "والس وفوتونا", + "fa": "والیس و فوتونا", + "yue": "瓦利斯同富图纳" + }, + flag: "🇼🇫", + code: "WF", + dialCode: "681", + minLength: 6, + maxLength: 6, + ), + Country( + name: "Yemen", + nameTranslations: { + "sk": "Jemen", + "se": "Jemen", + "pl": "Jemen", + "no": "Jemen", + "ja": "イエメン", + "it": "Yemen", + "zh": "也门", + "nl": "Jemen", + "de": "Jemen", + "fr": "Yémen", + "es": "Yemen", + "en": "Yemen", + "pt_BR": "Iémen", + "sr-Cyrl": "Јемен", + "sr-Latn": "Jemen", + "zh_TW": "葉門", + "tr": "Yemen", + "ro": "Yemen", + "ar": "اليمن", + "fa": "یمن", + "yue": "也門" + }, + flag: "🇾🇪", + code: "YE", + dialCode: "967", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Zambia", + nameTranslations: { + "sk": "Zambia", + "se": "Zambia", + "pl": "Zambia", + "no": "Zambia", + "ja": "ザンビア", + "it": "Zambia", + "zh": "赞比亚", + "nl": "Zambia", + "de": "Sambia", + "fr": "Zambie", + "es": "Zambia", + "en": "Zambia", + "pt_BR": "Zâmbia", + "sr-Cyrl": "Замбија", + "sr-Latn": "Zambija", + "zh_TW": "贊比亞", + "tr": "Zambiya", + "ro": "Zambia", + "ar": "زامبيا", + "fa": "زامبیا", + "yue": "贊比亞" + }, + flag: "🇿🇲", + code: "ZM", + dialCode: "260", + minLength: 9, + maxLength: 9, + ), + Country( + name: "Zimbabwe", + nameTranslations: { + "sk": "Zimbabwe", + "se": "Zimbabwe", + "pl": "Zimbabwe", + "no": "Zimbabwe", + "ja": "ジンバブエ", + "it": "Zimbabwe", + "zh": "津巴布韦", + "nl": "Zimbabwe", + "de": "Simbabwe", + "fr": "Zimbabwe", + "es": "Zimbabue", + "en": "Zimbabwe", + "pt_BR": "Zimbábue", + "sr-Cyrl": "Зимбабве", + "sr-Latn": "Zimbabve", + "zh_TW": "辛巴威", + "tr": "Zimbabve", + "ro": "Zimbabwe", + "ar": "زيمبابوي", + "fa": "زیمبابوه", + "yue": "津巴布韋" + }, + flag: "🇿🇼", + code: "ZW", + dialCode: "263", + minLength: 9, + maxLength: 9) +]; + +class Country { + final String name; + final Map nameTranslations; + final String flag; + final String code; + final String dialCode; + final String regionCode; + final int minLength; + final int maxLength; + + const Country({ + required this.name, + required this.flag, + required this.code, + required this.dialCode, + required this.nameTranslations, + required this.minLength, + required this.maxLength, + this.regionCode = "", + }); + + String get fullCountryCode { + return dialCode + regionCode; + } + + String get displayCC { + if (regionCode != "") { + return "$dialCode $regionCode"; + } + return dialCode; + } + + String localizedName(String languageCode) { + return nameTranslations[languageCode] ?? name; + } +} diff --git a/apps/rider/lib/core/ui/phone_intel/country_picker_dialog.dart b/apps/rider/lib/core/ui/phone_intel/country_picker_dialog.dart new file mode 100644 index 0000000..ac186c6 --- /dev/null +++ b/apps/rider/lib/core/ui/phone_intel/country_picker_dialog.dart @@ -0,0 +1,168 @@ +import 'helpers.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; + +import 'countries.dart'; + +class PickerDialogStyle { + final Color? backgroundColor; + + final TextStyle? countryCodeStyle; + + final TextStyle? countryNameStyle; + + final Widget? listTileDivider; + + final EdgeInsets? listTilePadding; + + final EdgeInsets? padding; + + final Color? searchFieldCursorColor; + + final InputDecoration? searchFieldInputDecoration; + + final EdgeInsets? searchFieldPadding; + + final double? width; + + PickerDialogStyle({ + this.backgroundColor, + this.countryCodeStyle, + this.countryNameStyle, + this.listTileDivider, + this.listTilePadding, + this.padding, + this.searchFieldCursorColor, + this.searchFieldInputDecoration, + this.searchFieldPadding, + this.width, + }); +} + +class CountryPickerDialog extends StatefulWidget { + final List countryList; + final Country selectedCountry; + final ValueChanged onCountryChanged; + final String searchText; + final List filteredCountries; + final PickerDialogStyle? style; + final String languageCode; + + const CountryPickerDialog({ + Key? key, + required this.searchText, + required this.languageCode, + required this.countryList, + required this.onCountryChanged, + required this.selectedCountry, + required this.filteredCountries, + this.style, + }) : super(key: key); + + @override + State createState() => _CountryPickerDialogState(); +} + +class _CountryPickerDialogState extends State { + late List _filteredCountries; + late Country _selectedCountry; + + @override + void initState() { + _selectedCountry = widget.selectedCountry; + _filteredCountries = widget.filteredCountries.toList() + ..sort( + (a, b) => a + .localizedName(widget.languageCode) + .compareTo(b.localizedName(widget.languageCode)), + ); + + super.initState(); + } + + @override + Widget build(BuildContext context) { + final mediaWidth = MediaQuery.of(context).size.width; + final width = widget.style?.width ?? mediaWidth; + const defaultHorizontalPadding = 40.0; + const defaultVerticalPadding = 24.0; + return Dialog( + insetPadding: EdgeInsets.symmetric( + vertical: defaultVerticalPadding, + horizontal: mediaWidth > (width + defaultHorizontalPadding * 2) + ? (mediaWidth - width) / 2 + : defaultHorizontalPadding), + backgroundColor: widget.style?.backgroundColor, + child: Container( + padding: widget.style?.padding ?? const EdgeInsets.all(10), + child: Column( + children: [ + Padding( + padding: + widget.style?.searchFieldPadding ?? const EdgeInsets.all(0), + child: TextField( + cursorColor: widget.style?.searchFieldCursorColor, + decoration: widget.style?.searchFieldInputDecoration ?? + InputDecoration( + suffixIcon: const Icon(Icons.search), + labelText: widget.searchText, + ), + onChanged: (value) { + _filteredCountries = widget.countryList.stringSearch(value) + ..sort( + (a, b) => a + .localizedName(widget.languageCode) + .compareTo(b.localizedName(widget.languageCode)), + ); + if (mounted) setState(() {}); + }, + ), + ), + const SizedBox(height: 20), + Expanded( + child: ListView.builder( + shrinkWrap: true, + itemCount: _filteredCountries.length, + itemBuilder: (ctx, index) => Column( + children: [ + ListTile( + leading: kIsWeb + ? Image.asset( + 'assets/flags/${_filteredCountries[index].code.toLowerCase()}.png', + package: 'intl_phone_field', + width: 32, + ) + : Text( + _filteredCountries[index].flag, + style: const TextStyle(fontSize: 18), + ), + contentPadding: widget.style?.listTilePadding, + title: Text( + _filteredCountries[index] + .localizedName(widget.languageCode), + style: widget.style?.countryNameStyle ?? + const TextStyle(fontWeight: FontWeight.w700), + ), + trailing: Text( + '+${_filteredCountries[index].dialCode}', + style: widget.style?.countryCodeStyle ?? + const TextStyle(fontWeight: FontWeight.w700), + ), + onTap: () { + _selectedCountry = _filteredCountries[index]; + widget.onCountryChanged(_selectedCountry); + Navigator.of(context).pop(); + }, + ), + widget.style?.listTileDivider ?? + const Divider(thickness: 1), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/apps/rider/lib/core/ui/phone_intel/helpers.dart b/apps/rider/lib/core/ui/phone_intel/helpers.dart new file mode 100644 index 0000000..c2b3957 --- /dev/null +++ b/apps/rider/lib/core/ui/phone_intel/helpers.dart @@ -0,0 +1,31 @@ +import 'countries.dart'; + +bool isNumeric(String s) => + s.isNotEmpty && int.tryParse(s.replaceAll("+", "")) != null; + +String removeDiacritics(String str) { + var withDia = + 'ÀÁÂÃÄÅàáâãäåÒÓÔÕÕÖØòóôõöøÈÉÊËèéêëðÇçÐÌÍÎÏìíîïÙÚÛÜùúûüÑñŠšŸÿýŽž'; + var withoutDia = + 'AAAAAAaaaaaaOOOOOOOooooooEEEEeeeeeCcDIIIIiiiiUUUUuuuuNnSsYyyZz'; + + for (int i = 0; i < withDia.length; i++) { + str = str.replaceAll(withDia[i], withoutDia[i]); + } + + return str; +} + +extension CountryExtensions on List { + List stringSearch(String search) { + search = removeDiacritics(search.toLowerCase()); + return where( + (country) => isNumeric(search) || search.startsWith("+") + ? country.dialCode.contains(search) + : removeDiacritics(country.name.replaceAll("+", "").toLowerCase()) + .contains(search) || + country.nameTranslations.values.any((element) => + removeDiacritics(element.toLowerCase()).contains(search)), + ).toList(); + } +} diff --git a/apps/rider/lib/core/ui/phone_intel/intl_phone_field.dart b/apps/rider/lib/core/ui/phone_intel/intl_phone_field.dart new file mode 100644 index 0000000..5e16d5d --- /dev/null +++ b/apps/rider/lib/core/ui/phone_intel/intl_phone_field.dart @@ -0,0 +1,521 @@ +library intl_phone_field; + +import 'dart:async'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +import './countries.dart'; +import './phone_number.dart'; +import 'country_picker_dialog.dart'; +import 'helpers.dart'; + +class IntlPhoneField extends StatefulWidget { + /// The TextFormField key. + final GlobalKey? formFieldKey; + + /// Whether to hide the text being edited (e.g., for passwords). + final bool obscureText; + + /// How the text should be aligned horizontally. + final TextAlign textAlign; + + /// How the text should be aligned vertically. + final TextAlignVertical? textAlignVertical; + final VoidCallback? onTap; + + /// {@macro flutter.widgets.editableText.readOnly} + final bool readOnly; + final FormFieldSetter? onSaved; + + /// {@macro flutter.widgets.editableText.onChanged} + /// + /// See also: + /// + /// * [inputFormatters], which are called before [onChanged] + /// runs and can validate and change ("format") the input value. + /// * [onEditingComplete], [onSubmitted], [onSelectionChanged]: + /// which are more specialized input change notifications. + final ValueChanged? onChanged; + + final ValueChanged? onCountryChanged; + + /// An optional method that validates an input. Returns an error string to display if the input is invalid, or null otherwise. + /// + /// A [PhoneNumber] is passed to the validator as argument. + /// The validator can handle asynchronous validation when declared as a [Future]. + /// Or run synchronously when declared as a [Function]. + /// + /// By default, the validator checks whether the input number length is between selected country's phone numbers min and max length. + /// If `disableLengthCheck` is not set to `true`, your validator returned value will be overwritten by the default validator. + /// But, if `disableLengthCheck` is set to `true`, your validator will have to check phone number length itself. + final FutureOr Function(PhoneNumber?)? validator; + + /// {@macro flutter.widgets.editableText.keyboardType} + final TextInputType keyboardType; + + /// Controls the text being edited. + /// + /// If null, this widget will create its own [TextEditingController]. + final TextEditingController? controller; + + /// Defines the keyboard focus for this widget. + /// + /// The [focusNode] is a long-lived object that's typically managed by a + /// [StatefulWidget] parent. See [FocusNode] for more information. + /// + /// To give the keyboard focus to this widget, provide a [focusNode] and then + /// use the current [FocusScope] to request the focus: + /// + /// ```dart + /// FocusScope.of(context).requestFocus(myFocusNode); + /// ``` + /// + /// This happens automatically when the widget is tapped. + /// + /// To be notified when the widget gains or loses the focus, add a listener + /// to the [focusNode]: + /// + /// ```dart + /// focusNode.addListener(() { Log.print(myFocusNode.hasFocus); }); + /// ``` + /// + /// If null, this widget will create its own [FocusNode]. + /// + /// ## Keyboard + /// + /// Requesting the focus will typically cause the keyboard to be shown + /// if it's not showing already. + /// + /// On Android, the user can hide the keyboard - without changing the focus - + /// with the system back button. They can restore the keyboard's visibility + /// by tapping on a text field. The user might hide the keyboard and + /// switch to a physical keyboard, or they might just need to get it + /// out of the way for a moment, to expose something it's + /// obscuring. In this case requesting the focus again will not + /// cause the focus to change, and will not make the keyboard visible. + /// + /// This widget builds an [EditableText] and will ensure that the keyboard is + /// showing when it is tapped by calling [EditableTextState.requestKeyboard()]. + final FocusNode? focusNode; + + /// {@macro flutter.widgets.editableText.onSubmitted} + /// + /// See also: + /// + /// * [EditableText.onSubmitted] for an example of how to handle moving to + /// the next/previous field when using [TextInputAction.next] and + /// [TextInputAction.previous] for [textInputAction]. + final void Function(String)? onSubmitted; + + /// If false the widget is "disabled": it ignores taps, the [TextFormField]'s + /// [decoration] is rendered in grey, + /// [decoration]'s [InputDecoration.counterText] is set to `""`, + /// and the drop down icon is hidden no matter [showDropdownIcon] value. + /// + /// If non-null this property overrides the [decoration]'s + /// [Decoration.enabled] property. + final bool enabled; + + /// The appearance of the keyboard. + /// + /// This setting is only honored on iOS devices. + /// + /// If unset, defaults to the brightness of [ThemeData.brightness]. + final Brightness? keyboardAppearance; + + /// Initial Value for the field. + /// This property can be used to pre-fill the field. + final String? initialValue; + + final String languageCode; + + /// 2 letter ISO Code or country dial code. + /// + /// ```dart + /// initialCountryCode: 'IN', // India + /// initialCountryCode: '+225', // Côte d'Ivoire + /// ``` + final String? initialCountryCode; + + /// List of Country to display see countries.dart for format + final List? countries; + + /// The decoration to show around the text field. + /// + /// By default, draws a horizontal line under the text field but can be + /// configured to show an icon, label, hint text, and error text. + /// + /// Specify null to remove the decoration entirely (including the + /// extra padding introduced by the decoration to save space for the labels). + final InputDecoration decoration; + + /// The style to use for the text being edited. + /// + /// This text style is also used as the base style for the [decoration]. + /// + /// If null, defaults to the `subtitle1` text style from the current [Theme]. + final TextStyle? style; + + /// Disable view Min/Max Length check + final bool disableLengthCheck; + + /// Won't work if [enabled] is set to `false`. + final bool showDropdownIcon; + + final BoxDecoration dropdownDecoration; + + /// The style use for the country dial code. + final TextStyle? dropdownTextStyle; + + /// {@macro flutter.widgets.editableText.inputFormatters} + final List? inputFormatters; + + /// The text that describes the search input field. + /// + /// When the input field is empty and unfocused, the label is displayed on top of the input field (i.e., at the same location on the screen where text may be entered in the input field). + /// When the input field receives focus (or if the field is non-empty), the label moves above (i.e., vertically adjacent to) the input field. + final String searchText; + + /// Position of an icon [leading, trailing] + final IconPosition dropdownIconPosition; + + /// Icon of the drop down button. + /// + /// Default is [Icon(Icons.arrow_drop_down)] + final Icon dropdownIcon; + + /// Whether this text field should focus itself if nothing else is already focused. + final bool autofocus; + + /// Autovalidate mode for text form field. + /// + /// If [AutovalidateMode.onUserInteraction], this FormField will only auto-validate after its content changes. + /// If [AutovalidateMode.always], it will auto-validate even without user interaction. + /// If [AutovalidateMode.disabled], auto-validation will be disabled. + /// + /// Defaults to [AutovalidateMode.onUserInteraction]. + final AutovalidateMode? autovalidateMode; + + /// Whether to show or hide country flag. + /// + /// Default value is `true`. + final bool showCountryFlag; + + /// Message to be displayed on autoValidate error + /// + /// Default value is `Invalid Mobile Number`. + final String? invalidNumberMessage; + + /// The color of the cursor. + final Color? cursorColor; + + /// How tall the cursor will be. + final double? cursorHeight; + + /// How rounded the corners of the cursor should be. + final Radius? cursorRadius; + + /// How thick the cursor will be. + final double cursorWidth; + + /// Whether to show cursor. + final bool? showCursor; + + /// The padding of the Flags Button. + /// + /// The amount of insets that are applied to the Flags Button. + /// + /// If unset, defaults to [EdgeInsets.zero]. + final EdgeInsetsGeometry flagsButtonPadding; + + /// The type of action button to use for the keyboard. + final TextInputAction? textInputAction; + + /// Optional set of styles to allow for customizing the country search + /// & pick dialog + final PickerDialogStyle? pickerDialogStyle; + + /// The margin of the country selector button. + /// + /// The amount of space to surround the country selector button. + /// + /// If unset, defaults to [EdgeInsets.zero]. + final EdgeInsets flagsButtonMargin; + + /// Enable the autofill hint for phone number. + final bool disableAutoFillHints; + + /// If null, default magnification configuration will be used. + final TextMagnifierConfiguration? magnifierConfiguration; + + const IntlPhoneField({ + Key? key, + this.formFieldKey, + this.initialCountryCode, + this.languageCode = 'en', + this.disableAutoFillHints = false, + this.obscureText = false, + this.textAlign = TextAlign.left, + this.textAlignVertical, + this.onTap, + this.readOnly = false, + this.initialValue, + this.keyboardType = TextInputType.phone, + this.controller, + this.focusNode, + this.decoration = const InputDecoration(), + this.style, + this.dropdownTextStyle, + this.onSubmitted, + this.validator, + this.onChanged, + this.countries, + this.onCountryChanged, + this.onSaved, + this.showDropdownIcon = true, + this.dropdownDecoration = const BoxDecoration(), + this.inputFormatters, + this.enabled = true, + this.keyboardAppearance, + @Deprecated('Use searchFieldInputDecoration of PickerDialogStyle instead') + this.searchText = 'Search country', + this.dropdownIconPosition = IconPosition.leading, + this.dropdownIcon = const Icon(Icons.arrow_drop_down), + this.autofocus = false, + this.textInputAction, + this.autovalidateMode = AutovalidateMode.onUserInteraction, + this.showCountryFlag = true, + this.cursorColor, + this.disableLengthCheck = false, + this.flagsButtonPadding = EdgeInsets.zero, + this.invalidNumberMessage = 'Invalid Mobile Number', + this.cursorHeight, + this.cursorRadius = Radius.zero, + this.cursorWidth = 2.0, + this.showCursor = true, + this.pickerDialogStyle, + this.flagsButtonMargin = EdgeInsets.zero, + this.magnifierConfiguration, + }) : super(key: key); + + @override + State createState() => _IntlPhoneFieldState(); +} + +class _IntlPhoneFieldState extends State { + late List _countryList; + late Country _selectedCountry; + late List filteredCountries; + late String number; + + String? validatorMessage; + + @override + void initState() { + super.initState(); + _countryList = widget.countries ?? countries; + filteredCountries = _countryList; + number = widget.initialValue ?? ''; + if (widget.initialCountryCode == null && number.startsWith('+')) { + number = number.substring(1); + // parse initial value + _selectedCountry = countries.firstWhere( + (country) => number.startsWith(country.fullCountryCode), + orElse: () => _countryList.first); + + // remove country code from the initial number value + number = number.replaceFirst( + RegExp("^${_selectedCountry.fullCountryCode}"), ""); + } else { + _selectedCountry = _countryList.firstWhere( + (item) => item.code == (widget.initialCountryCode ?? 'US'), + orElse: () => _countryList.first); + + // remove country code from the initial number value + if (number.startsWith('+')) { + number = number.replaceFirst( + RegExp("^\\+${_selectedCountry.fullCountryCode}"), ""); + } else { + number = number.replaceFirst( + RegExp("^${_selectedCountry.fullCountryCode}"), ""); + } + } + + if (widget.autovalidateMode == AutovalidateMode.always) { + final initialPhoneNumber = PhoneNumber( + countryISOCode: _selectedCountry.code, + countryCode: '+${_selectedCountry.dialCode}', + number: widget.initialValue ?? '', + ); + + final value = widget.validator?.call(initialPhoneNumber); + + if (value is String) { + validatorMessage = value; + } else { + (value as Future).then((msg) { + validatorMessage = msg; + }); + } + } + } + + Future _changeCountry() async { + filteredCountries = _countryList; + await showDialog( + context: context, + useRootNavigator: false, + builder: (context) => StatefulBuilder( + builder: (ctx, setState) => CountryPickerDialog( + languageCode: widget.languageCode.toLowerCase(), + style: widget.pickerDialogStyle, + filteredCountries: filteredCountries, + searchText: widget.searchText, + countryList: _countryList, + selectedCountry: _selectedCountry, + onCountryChanged: (Country country) { + _selectedCountry = country; + widget.onCountryChanged?.call(country); + setState(() {}); + }, + ), + ), + ); + if (mounted) setState(() {}); + } + + @override + Widget build(BuildContext context) { + return TextFormField( + key: widget.formFieldKey, + initialValue: (widget.controller == null) ? number : null, + autofillHints: widget.disableAutoFillHints + ? null + : [AutofillHints.telephoneNumberNational], + readOnly: widget.readOnly, + obscureText: widget.obscureText, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + cursorColor: widget.cursorColor, + onTap: widget.onTap, + controller: widget.controller, + focusNode: widget.focusNode, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorWidth: widget.cursorWidth, + showCursor: widget.showCursor, + onFieldSubmitted: widget.onSubmitted, + magnifierConfiguration: widget.magnifierConfiguration, + decoration: widget.decoration.copyWith( + prefixIcon: _buildFlagsButton(), + counterText: !widget.enabled ? '' : null, + ), + style: widget.style, + onSaved: (value) { + widget.onSaved?.call( + PhoneNumber( + countryISOCode: _selectedCountry.code, + countryCode: + '+${_selectedCountry.dialCode}${_selectedCountry.regionCode}', + number: value!, + ), + ); + }, + onChanged: (value) async { + final phoneNumber = PhoneNumber( + countryISOCode: _selectedCountry.code, + countryCode: '+${_selectedCountry.fullCountryCode}', + number: value, + ); + + if (widget.autovalidateMode != AutovalidateMode.disabled) { + validatorMessage = await widget.validator?.call(phoneNumber); + } + + widget.onChanged?.call(phoneNumber); + }, + validator: (value) { + if (value == null || !isNumeric(value)) return validatorMessage; + if (!widget.disableLengthCheck) { + return value.length >= _selectedCountry.minLength && + value.length <= _selectedCountry.maxLength + ? null + : widget.invalidNumberMessage; + } + + return validatorMessage; + }, + maxLength: widget.disableLengthCheck ? null : _selectedCountry.maxLength, + keyboardType: widget.keyboardType, + inputFormatters: widget.inputFormatters, + enabled: widget.enabled, + keyboardAppearance: widget.keyboardAppearance, + autofocus: widget.autofocus, + textInputAction: widget.textInputAction, + autovalidateMode: widget.autovalidateMode, + ); + } + + Container _buildFlagsButton() { + return Container( + margin: widget.flagsButtonMargin, + child: DecoratedBox( + decoration: widget.dropdownDecoration, + child: InkWell( + borderRadius: widget.dropdownDecoration.borderRadius as BorderRadius?, + onTap: widget.enabled ? _changeCountry : null, + child: Padding( + padding: widget.flagsButtonPadding, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const SizedBox( + width: 4, + ), + if (widget.enabled && + widget.showDropdownIcon && + widget.dropdownIconPosition == IconPosition.leading) ...[ + widget.dropdownIcon, + const SizedBox(width: 4), + ], + if (widget.showCountryFlag) ...[ + kIsWeb + ? Image.asset( + 'assets/flags/${_selectedCountry.code.toLowerCase()}.png', + package: 'intl_phone_field', + width: 32, + ) + : Text( + _selectedCountry.flag, + style: const TextStyle(fontSize: 18), + ), + const SizedBox(width: 8), + ], + FittedBox( + child: Text( + '+${_selectedCountry.dialCode}', + style: widget.dropdownTextStyle, + ), + ), + if (widget.enabled && + widget.showDropdownIcon && + widget.dropdownIconPosition == IconPosition.trailing) ...[ + const SizedBox(width: 4), + widget.dropdownIcon, + ], + const SizedBox(width: 8), + ], + ), + ), + ), + ), + ); + } +} + +enum IconPosition { + leading, + trailing, +} \ No newline at end of file diff --git a/apps/rider/lib/core/ui/phone_intel/phone_number.dart b/apps/rider/lib/core/ui/phone_intel/phone_number.dart new file mode 100644 index 0000000..912cb07 --- /dev/null +++ b/apps/rider/lib/core/ui/phone_intel/phone_number.dart @@ -0,0 +1,79 @@ +import 'countries.dart'; + +class NumberTooLongException implements Exception {} + +class NumberTooShortException implements Exception {} + +class InvalidCharactersException implements Exception {} + +class PhoneNumber { + String countryISOCode; + String countryCode; + String number; + + PhoneNumber({ + required this.countryISOCode, + required this.countryCode, + required this.number, + }); + + factory PhoneNumber.fromCompleteNumber({required String completeNumber}) { + if (completeNumber == "") { + return PhoneNumber(countryISOCode: "", countryCode: "", number: ""); + } + + try { + Country country = getCountry(completeNumber); + String number; + if (completeNumber.startsWith('+')) { + number = completeNumber.substring(1 + country.dialCode.length + country.regionCode.length); + } else { + number = completeNumber.substring(country.dialCode.length + country.regionCode.length); + } + return PhoneNumber( + countryISOCode: country.code, countryCode: country.dialCode + country.regionCode, number: number); + } on InvalidCharactersException { + rethrow; + // ignore: unused_catch_clause + } on Exception catch (e) { + return PhoneNumber(countryISOCode: "", countryCode: "", number: ""); + } + } + + bool isValidNumber() { + Country country = getCountry(completeNumber); + if (number.length < country.minLength) { + throw NumberTooShortException(); + } + + if (number.length > country.maxLength) { + throw NumberTooLongException(); + } + return true; + } + + String get completeNumber { + return countryCode + number; + } + + static Country getCountry(String phoneNumber) { + if (phoneNumber == "") { + throw NumberTooShortException(); + } + + final validPhoneNumber = RegExp(r'^[+0-9]*[0-9]*$'); + + if (!validPhoneNumber.hasMatch(phoneNumber)) { + throw InvalidCharactersException(); + } + + if (phoneNumber.startsWith('+')) { + return countries + .firstWhere((country) => phoneNumber.substring(1).startsWith(country.dialCode + country.regionCode)); + } + return countries.firstWhere((country) => phoneNumber.startsWith(country.dialCode + country.regionCode)); + } + + @override + String toString() => 'PhoneNumber(countryISOCode: $countryISOCode, countryCode: $countryCode, number: $number)'; +} diff --git a/apps/rider/lib/core/ui/tripz_scaffold.dart b/apps/rider/lib/core/ui/tripz_scaffold.dart index 8e35a45..a603cb8 100644 --- a/apps/rider/lib/core/ui/tripz_scaffold.dart +++ b/apps/rider/lib/core/ui/tripz_scaffold.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import '../design/tokens.dart'; +import '../l10n/app_localizations.dart'; /// السكافولد الموحّد — كل شاشة في التطبيق تُبنى داخله. /// يحوّل `status` من Cubit/Bloc إلى: skeleton / error / empty / محتوى. @@ -107,23 +108,51 @@ class ErrorView extends StatelessWidget { @override Widget build(BuildContext context) { - final cs = Theme.of(context).colorScheme; + final theme = Theme.of(context); + final cs = theme.colorScheme; + final l10n = AppLocalizations.of(context); return Center( child: Padding( padding: const EdgeInsets.all(T.pagePadding), child: Column( mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.error_outline_rounded, size: 48, color: cs.error), - const SizedBox(height: T.s16), + Container( + width: T.s64, + height: T.s64, + decoration: BoxDecoration( + color: cs.error.withValues(alpha: 0.1), + shape: BoxShape.circle, + ), + child: Icon( + Icons.error_outline_rounded, + size: T.s48, + color: cs.error, + ), + ), + const SizedBox(height: T.s24), Text( - 'حدث خطأ', - style: Theme.of(context).textTheme.titleMedium, + l10n?.errorTitle ?? 'حدث خطأ', + style: theme.textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: T.s8), + Text( + l10n?.errorSubtitle ?? 'يرجى المحاولة مرة أخرى', + style: theme.textTheme.bodyMedium?.copyWith( + color: cs.onSurfaceVariant, + ), textAlign: TextAlign.center, ), if (onRetry != null) ...[ - const SizedBox(height: T.s16), - FilledButton(onPressed: onRetry, child: const Text('إعادة المحاولة')), + const SizedBox(height: T.s24), + FilledButton.icon( + onPressed: onRetry, + icon: const Icon(Icons.refresh_rounded, size: T.iconSm), + label: Text(l10n?.retry ?? 'أعد المحاولة'), + ), ], ], ), @@ -132,13 +161,182 @@ class ErrorView extends StatelessWidget { } } -class Skeleton extends StatelessWidget { +class Skeleton extends StatefulWidget { const Skeleton({super.key}); @override - Widget build(BuildContext context) { - return const Center(child: CircularProgressIndicator()); + State createState() => _SkeletonState(); +} + +class _SkeletonState extends State with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1200), + )..repeat(); } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final baseColor = cs.surfaceContainerHighest; + final highlightColor = cs.surfaceContainerLow; + + return AnimatedBuilder( + animation: _controller, + builder: (context, child) { + return CustomPaint( + painter: _ShimmerPainter( + baseColor: baseColor, + highlightColor: highlightColor, + progress: _controller.value, + ), + child: child, + ); + }, + child: _buildSkeletonLayout(context), + ); + } + + Widget _buildSkeletonLayout(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final baseColor = cs.surfaceContainerHighest; + + return Padding( + padding: const EdgeInsets.all(T.pagePadding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // Header row skeleton + _ShimmerBox( + width: 160, + height: 24, + color: baseColor, + radius: T.r8, + ), + const SizedBox(height: T.s16), + // Subtitle row skeleton + _ShimmerBox( + width: 100, + height: 14, + color: baseColor, + radius: T.r8, + ), + const SizedBox(height: T.s24), + // Card-like skeleton block 1 + _ShimmerBox( + width: double.infinity, + height: 120, + color: baseColor, + radius: T.r12, + ), + const SizedBox(height: T.s16), + // Card-like skeleton block 2 + _ShimmerBox( + width: double.infinity, + height: 80, + color: baseColor, + radius: T.r12, + ), + const SizedBox(height: T.s16), + // Row of two small boxes + Row( + children: [ + Expanded( + child: _ShimmerBox( + height: 48, + color: baseColor, + radius: T.r12, + ), + ), + const SizedBox(width: T.s12), + Expanded( + child: _ShimmerBox( + height: 48, + color: baseColor, + radius: T.r12, + ), + ), + ], + ), + ], + ), + ); + } +} + +class _ShimmerBox extends StatelessWidget { + const _ShimmerBox({ + required this.height, + required this.color, + this.width = double.infinity, + this.radius = 0, + }); + + final double width; + final double height; + final Color color; + final double radius; + + @override + Widget build(BuildContext context) { + return Container( + width: width, + height: height, + decoration: BoxDecoration( + color: color, + borderRadius: BorderRadius.circular(radius), + ), + ); + } +} + +class _ShimmerPainter extends CustomPainter { + _ShimmerPainter({ + required this.baseColor, + required this.highlightColor, + required this.progress, + }); + + final Color baseColor; + final Color highlightColor; + final double progress; + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint(); + final dx = size.width * 1.5 * progress; + final gradient = LinearGradient( + begin: const Alignment(-0.3, 0), + end: const Alignment(0.3, 0), + colors: [ + baseColor, + highlightColor, + baseColor, + ], + stops: const [0.0, 0.5, 1.0], + ); + final rect = Rect.fromCenter( + center: Offset(dx, size.height / 2), + width: size.width * 0.6, + height: size.height, + ); + paint.shader = gradient.createShader(rect); + canvas.drawRect(rect, paint); + } + + @override + bool shouldRepaint(_ShimmerPainter old) => old.progress != progress; } class EmptyView extends StatelessWidget { @@ -146,10 +344,46 @@ class EmptyView extends StatelessWidget { @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final cs = theme.colorScheme; + final l10n = AppLocalizations.of(context); return Center( - child: Text( - 'لا توجد بيانات', - style: Theme.of(context).textTheme.bodyLarge, + child: Padding( + padding: const EdgeInsets.all(T.pagePadding), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: T.s64, + height: T.s64, + decoration: BoxDecoration( + color: cs.primaryContainer, + shape: BoxShape.circle, + ), + child: Icon( + Icons.inbox_outlined, + size: T.iconLg, + color: cs.onPrimaryContainer, + ), + ), + const SizedBox(height: T.s24), + Text( + l10n?.emptyTitle ?? 'لا توجد بيانات', + style: theme.textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: T.s8), + Text( + l10n?.emptySubtitle ?? 'لم يتم العثور على محتوى حالياً', + style: theme.textTheme.bodyMedium?.copyWith( + color: cs.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), ), ); } diff --git a/apps/rider/lib/core/ui/tripz_sheet.dart b/apps/rider/lib/core/ui/tripz_sheet.dart index f4879d4..4c895eb 100644 --- a/apps/rider/lib/core/ui/tripz_sheet.dart +++ b/apps/rider/lib/core/ui/tripz_sheet.dart @@ -18,21 +18,21 @@ class TripzSheet extends StatelessWidget { final bool isScrollControlled; /// مساعد للاستدعاء بأمر واحد - static Future show( + static Future show( BuildContext context, { String? title, Widget? child, Widget? bottomAction, bool isScrollControlled = true, }) { - return showModalBottomSheet( + return showModalBottomSheet( context: context, isScrollControlled: isScrollControlled, useSafeArea: true, builder: (_) => TripzSheet( title: title, - child: child, bottomAction: bottomAction, + child: child, ), ); } @@ -40,7 +40,6 @@ class TripzSheet extends StatelessWidget { @override Widget build(BuildContext context) { final theme = Theme.of(context); - final cs = theme.colorScheme; final bottomPadding = MediaQuery.of(context).viewInsets.bottom; return Padding( diff --git a/apps/rider/lib/core/ui/tripz_text_field.dart b/apps/rider/lib/core/ui/tripz_text_field.dart index bae0ca0..38a5527 100644 --- a/apps/rider/lib/core/ui/tripz_text_field.dart +++ b/apps/rider/lib/core/ui/tripz_text_field.dart @@ -22,6 +22,7 @@ class TripzTextField extends StatelessWidget { this.maxLength, this.contentPadding, this.focusNode, + this.textInputAction, }); final TextEditingController? controller; @@ -40,6 +41,7 @@ class TripzTextField extends StatelessWidget { final int? maxLength; final EdgeInsetsGeometry? contentPadding; final FocusNode? focusNode; + final TextInputAction? textInputAction; @override Widget build(BuildContext context) { @@ -55,6 +57,7 @@ class TripzTextField extends StatelessWidget { controller: controller, focusNode: focusNode, keyboardType: keyboardType, + textInputAction: textInputAction, obscureText: obscureText, enabled: enabled, onChanged: onChanged, diff --git a/apps/rider/lib/core/utils/phone_normalizer.dart b/apps/rider/lib/core/utils/phone_normalizer.dart new file mode 100644 index 0000000..5503741 --- /dev/null +++ b/apps/rider/lib/core/utils/phone_normalizer.dart @@ -0,0 +1,51 @@ +class DialPlan { + final String callingCode; + final int nationalLength; + + const DialPlan({required this.callingCode, required this.nationalLength}); +} + +/// أداة تطبيع رقم الهاتف لصيغة دولية موحدة (مطابقة للباك إند PhoneService). +class PhoneNormalizer { + static const Map dialPlans = { + 'JO': DialPlan(callingCode: '962', nationalLength: 9), + 'SY': DialPlan(callingCode: '963', nationalLength: 9), + 'EG': DialPlan(callingCode: '20', nationalLength: 10), + 'SA': DialPlan(callingCode: '966', nationalLength: 9), + 'AE': DialPlan(callingCode: '971', nationalLength: 9), + 'IQ': DialPlan(callingCode: '964', nationalLength: 10), + }; + + /// يرجع الرقم في صيغة موحّدة (مثل 962791234567 أو +962791234567 حسب الحاجة). + static String normalize(String rawNumber, {String isoCode = 'JO', bool withPlus = true}) { + final countryKey = isoCode.toUpperCase(); + final plan = dialPlans[countryKey] ?? const DialPlan(callingCode: '962', nationalLength: 9); + + // إزالة كل الرموز باستثناء الأرقام + var digits = rawNumber.replaceAll(RegExp(r'\D'), ''); + if (digits.isEmpty) return rawNumber; + + // إزالة البادئة الدولية "00" إن وجدت + if (digits.startsWith('00')) { + digits = digits.substring(2); + } + + String resultDigits = digits; + + // 1) الرقم مكتوب بالفعل بمفتاح الدولة كلياً + if (digits.startsWith(plan.callingCode) && + digits.length == plan.callingCode.length + plan.nationalLength) { + resultDigits = digits; + } + // 2) رقم محلي يبدأ بصفر (مثل 0791234567) + else if (digits.startsWith('0') && digits.length == plan.nationalLength + 1) { + resultDigits = plan.callingCode + digits.substring(1); + } + // 3) رقم محلي محذوف منه الصفر البادئ (مثل 791234567) + else if (digits.length == plan.nationalLength) { + resultDigits = plan.callingCode + digits; + } + + return withPlus ? '+$resultDigits' : resultDigits; + } +} diff --git a/apps/rider/lib/features/auth/cubit/auth_cubit.dart b/apps/rider/lib/features/auth/cubit/auth_cubit.dart index bf09594..5580051 100644 --- a/apps/rider/lib/features/auth/cubit/auth_cubit.dart +++ b/apps/rider/lib/features/auth/cubit/auth_cubit.dart @@ -8,13 +8,34 @@ class AuthCubit extends Cubit { AuthCubit(this._repo) : super(const AuthState()); Future checkAuth() async { + final hasToken = await _repo.hasSavedToken(); + if (hasToken) { + final cachedUser = await _repo.getCachedUser(); + if (cachedUser != null) { + emit(state.copyWith( + status: AuthStatus.authenticated, + user: cachedUser, + )); + } + } final user = await _repo.me(); - emit(state.copyWith( - status: user != null ? AuthStatus.authenticated : AuthStatus.unauthenticated, - user: user, - )); + if (user != null) { + emit(state.copyWith( + status: AuthStatus.authenticated, + user: user, + )); + } else { + final hasTokenAfterMe = await _repo.hasSavedToken(); + if (!hasTokenAfterMe) { + emit(state.copyWith( + status: AuthStatus.unauthenticated, + user: null, + )); + } + } } + Future sendOtp(String phone) async { if (phone.trim().isEmpty) { emit(state.copyWith(status: AuthStatus.error, error: 'أدخل رقم الهاتف')); @@ -43,4 +64,14 @@ class AuthCubit extends Cubit { await _repo.logout(); emit(const AuthState(status: AuthStatus.unauthenticated)); } + + Future updateName(String name) async { + try { + emit(state.copyWith(status: AuthStatus.authenticating)); + final user = await _repo.updateName(name); + emit(state.copyWith(status: AuthStatus.authenticated, user: user)); + } catch (_) { + emit(state.copyWith(status: AuthStatus.error, error: 'تعذّر حفظ الاسم')); + } + } } diff --git a/apps/rider/lib/features/auth/data/auth_repository.dart b/apps/rider/lib/features/auth/data/auth_repository.dart index 5d5b531..5ec11de 100644 --- a/apps/rider/lib/features/auth/data/auth_repository.dart +++ b/apps/rider/lib/features/auth/data/auth_repository.dart @@ -1,3 +1,4 @@ +import 'dart:convert'; import 'package:dio/dio.dart'; import '../../../core/api/api_client.dart'; import '../../../core/storage/token_store.dart'; @@ -16,6 +17,13 @@ class AppUser { name: j['name'] as String?, role: (j['role'] ?? 'rider') as String, ); + + Map toJson() => { + 'id': id, + 'phone': phone, + 'name': name, + 'role': role, + }; } class AuthRepository { @@ -37,17 +45,42 @@ class AuthRepository { r.data['access_token'] as String, r.data['refresh_token'] as String, ); - return AppUser.fromJson(Map.from(r.data['user'] as Map)); + final user = AppUser.fromJson(Map.from(r.data['user'] as Map)); + await _store.saveUser(jsonEncode(user.toJson())); + return user; } - Future me() async { + Future getCachedUser() async { + final raw = await _store.readUser(); + if (raw == null || raw.isEmpty) return null; try { - final r = await _api.dio.get('/users/me'); - return AppUser.fromJson(Map.from(r.data as Map)); - } on DioException { + final map = jsonDecode(raw) as Map; + return AppUser.fromJson(map); + } catch (_) { return null; } } + Future hasSavedToken() => _store.hasToken(); + + Future me() async { + try { + final r = await _api.dio.get('/users/me'); + final user = AppUser.fromJson(Map.from(r.data as Map)); + await _store.saveUser(jsonEncode(user.toJson())); + return user; + } on DioException { + return getCachedUser(); + } + } + Future logout() => _store.clear(); + + Future updateName(String name) async { + final r = await _api.dio.patch('/users/me', data: {'name': name}); + final user = AppUser.fromJson(Map.from(r.data as Map)); + await _store.saveUser(jsonEncode(user.toJson())); + return user; + } } + diff --git a/apps/rider/lib/features/auth/view/name_screen.dart b/apps/rider/lib/features/auth/view/name_screen.dart new file mode 100644 index 0000000..ec082ca --- /dev/null +++ b/apps/rider/lib/features/auth/view/name_screen.dart @@ -0,0 +1,180 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import '../../../core/l10n/app_localizations.dart'; +import '../../../core/design/tokens.dart'; +import '../cubit/auth_cubit.dart'; +import '../cubit/auth_state.dart'; +import '../../../core/ui/tripz_scaffold.dart'; +import '../../../core/ui/tripz_button.dart'; +import '../../../core/ui/tripz_text_field.dart'; + +/// شاشة إدخال الاسم (أول دخول فقط) — تطابق stitch/name_entry.html: +/// شعار صغير بالمنتصف أعلى الشاشة + عنوان وسطر فرعي + بطاقة بيضاء +/// مرفوعة فيها حقل الاسم بأيقونة، وزر متابعة أسفل الشاشة. +/// +/// ملاحظة: تصميم Stitch يعرض 3 حقول (الاسم الأول/الأخير/البريد)، لكن +/// AuthCubit.updateName ينادي PATCH /users/me بحقل name واحد فقط +/// (docs/29 §3.5 ينص صراحة على «حقل واحد»). الشاشة هنا تستعمل حقلاً +/// واحداً للاسم الكامل بنفس لغة Stitch البصرية (بطاقة مرفوعة + أيقونة +/// + label عائم) دون تغيير عقد الـCubit. +class NameScreen extends StatefulWidget { + const NameScreen({super.key}); + @override + State createState() => _NameScreenState(); +} + +class _NameScreenState extends State { + final _firstNameController = TextEditingController(); + final _lastNameController = TextEditingController(); + bool _valid = false; + + @override + void initState() { + super.initState(); + _firstNameController.addListener(_validate); + _lastNameController.addListener(_validate); + } + + void _validate() { + final isValid = _firstNameController.text.trim().isNotEmpty && + _lastNameController.text.trim().isNotEmpty; + if (_valid != isValid) { + setState(() => _valid = isValid); + } + } + + @override + void dispose() { + _firstNameController.dispose(); + _lastNameController.dispose(); + super.dispose(); + } + + void _submit() { + if (!_valid) return; + final fullName = + '${_firstNameController.text.trim()} ${_lastNameController.text.trim()}'; + context.read().updateName(fullName); + } + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final theme = Theme.of(context); + final cs = theme.colorScheme; + + return BlocConsumer( + listener: (context, state) { + if (state.status == AuthStatus.error && state.error != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.error!)), + ); + } + }, + builder: (context, state) { + final isLoading = state.status == AuthStatus.authenticating; + return TripzScaffold( + backgroundColor: cs.surfaceContainerLow, + padding: false, + bottomAction: TripzButton( + onPressed: (_valid && !isLoading) ? _submit : null, + label: l10n.nameButton, + loading: isLoading, + ), + child: Column( + children: [ + // ── رأس الشاشة: زر عودة (يبدأ الجلسة من جديد) + شعار مصغّر ── + SizedBox( + height: T.s48, + child: Row( + children: [ + IconButton( + icon: const BackButtonIcon(), + color: cs.onSurface, + tooltip: l10n.back, + onPressed: () => context.read().logout(), + ), + Expanded( + child: Center( + child: Image.asset( + 'assets/images/name_hero.png', + height: T.iconLg, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => Icon( + Icons.directions_car_filled_rounded, + size: T.iconLg, + color: cs.primary, + ), + ), + ), + ), + const SizedBox(width: T.s48), + ], + ), + ), + const SizedBox(height: T.s24), + + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: T.pagePadding), + child: Column( + children: [ + // العنوان والسطر الفرعي + Text( + l10n.nameTitle, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + color: cs.onSurface, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: T.s8), + Text( + l10n.nameSubtitle, + style: theme.textTheme.bodyLarge?.copyWith( + color: cs.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: T.s24), + + // بطاقة بيضاء مرفوعة تحوي حقول الاسم الأول واسم العائلة + Container( + width: double.infinity, + padding: const EdgeInsets.all(T.s24), + decoration: BoxDecoration( + color: cs.surfaceContainerLowest, + borderRadius: BorderRadius.circular(T.r16), + boxShadow: const [T.shadowSm], + ), + child: Column( + children: [ + TripzTextField( + controller: _firstNameController, + label: l10n.firstNameHint, + autofocus: true, + prefix: Icon(Icons.person_outline_rounded, color: cs.outline), + textInputAction: TextInputAction.next, + ), + const SizedBox(height: T.s16), + TripzTextField( + controller: _lastNameController, + label: l10n.lastNameHint, + prefix: Icon(Icons.badge_outlined, color: cs.outline), + onSubmitted: (_) => _submit(), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), + ); + }, + ); + } + +} diff --git a/apps/rider/lib/features/auth/view/otp_screen.dart b/apps/rider/lib/features/auth/view/otp_screen.dart index 725b760..e210e7d 100644 --- a/apps/rider/lib/features/auth/view/otp_screen.dart +++ b/apps/rider/lib/features/auth/view/otp_screen.dart @@ -1,13 +1,18 @@ +import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import '../cubit/auth_cubit.dart'; import '../cubit/auth_state.dart'; +import '../../../core/design/tokens.dart'; import '../../../core/ui/tripz_scaffold.dart'; import '../../../core/ui/tripz_button.dart'; -import '../../../core/ui/tripz_text_field.dart'; +/// شاشة تحقق OTP — تطابق stitch/otp_verification.html: +/// شريط علوي بلون primary + 4 خانات منفصلة (ترتيب LTR ثابت) + عدّاد +/// إعادة إرسال + تأكيد تلقائي عند اكتمال الخانات. class OtpScreen extends StatefulWidget { const OtpScreen({super.key}); @override @@ -15,64 +20,289 @@ class OtpScreen extends StatefulWidget { } class _OtpScreenState extends State { - final _controller = TextEditingController(); + static const _otpLength = 4; + final List _controllers = + List.generate(_otpLength, (_) => TextEditingController()); + final List _focusNodes = + List.generate(_otpLength, (_) => FocusNode()); + int _resendSeconds = 60; + Timer? _resendTimer; + + @override + void initState() { + super.initState(); + _startResendTimer(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _focusNodes[0].requestFocus(); + }); + } @override void dispose() { - _controller.dispose(); + _resendTimer?.cancel(); + for (final c in _controllers) { + c.dispose(); + } + for (final f in _focusNodes) { + f.dispose(); + } super.dispose(); } + void _startResendTimer() { + _resendSeconds = 60; + _resendTimer?.cancel(); + _resendTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + if (_resendSeconds <= 0) { + timer.cancel(); + } else { + setState(() => _resendSeconds--); + } + }); + } + + String get _otpCode => _controllers.map((c) => c.text).join(); + + bool get _isComplete => _otpCode.length == _otpLength; + + void _onDigitChanged(int index, String value) { + if (value.length > 1) { + // لصق تلقائي: يوزّع الأرقام الملصوقة على الخانات بالترتيب + final digits = value.replaceAll(RegExp(r'[^0-9]'), ''); + for (int i = 0; i < _otpLength && i < digits.length; i++) { + _controllers[i].text = digits[i]; + } + final nextIdx = digits.length.clamp(0, _otpLength - 1); + _focusNodes[nextIdx].requestFocus(); + } else if (value.isNotEmpty) { + if (index < _otpLength - 1) { + _focusNodes[index + 1].requestFocus(); + } else { + _focusNodes[index].unfocus(); + } + } + + setState(() {}); + + if (_isComplete) { + _submit(); + } + } + + void _onKeyPress(int index, KeyEvent event) { + if (event is KeyDownEvent && + event.logicalKey == LogicalKeyboardKey.backspace && + _controllers[index].text.isEmpty && + index > 0) { + _controllers[index - 1].clear(); + _focusNodes[index - 1].requestFocus(); + setState(() {}); + } + } + + void _submit() { + if (!_isComplete) return; + context.read().verify(_otpCode); + } + + void _resend() { + final phone = context.read().state.phone; + context.read().sendOtp(phone); + _startResendTimer(); + } + @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; + final theme = Theme.of(context); + final cs = theme.colorScheme; + final phone = context.watch().state.phone; - return TripzScaffold( - showBackButton: true, - onBack: () => context.go('/phone'), - child: BlocConsumer( - listener: (context, state) { - if (state.status == AuthStatus.error && state.error != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.error!)), - ); - } - }, - builder: (context, state) { - return Column( + return BlocConsumer( + listener: (context, state) { + if (state.status == AuthStatus.error && state.error != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.error!)), + ); + } + }, + builder: (context, state) { + final isLoading = state.status == AuthStatus.authenticating; + + return TripzScaffold( + padding: false, + safeAreaTop: false, + bottomAction: TripzButton( + onPressed: (_isComplete && !isLoading) ? _submit : null, + label: l10n.confirm, + loading: isLoading, + ), + child: Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const SizedBox(height: 24), - Text( - l10n.otpTitle, - style: Theme.of(context).textTheme.titleLarge, - ), - const SizedBox(height: 8), - Text( - l10n.otpSentTo(state.phone), - style: Theme.of(context).textTheme.bodyMedium?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + // ── شريط علوي بلون primary (يطابق TopAppBar في stitch) ── + SafeArea( + bottom: false, + child: SizedBox( + height: kToolbarHeight, + child: ColoredBox( + color: cs.primary, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: T.s8), + child: Row( + children: [ + IconButton( + icon: const BackButtonIcon(), + color: cs.onPrimary, + onPressed: () => context.go('/phone'), + ), + const SizedBox(width: T.s8), + Text( + l10n.confirmPhoneNumber, + style: theme.textTheme.titleLarge?.copyWith( + color: cs.onPrimary, + fontWeight: FontWeight.w700, + ), + ), + ], + ), ), + ), + ), ), - const SizedBox(height: 28), - TripzTextField( - controller: _controller, - label: l10n.otpLabel, - keyboardType: TextInputType.number, - textAlign: TextAlign.center, - maxLength: 6, - ), - const SizedBox(height: 20), - TripzButton( - label: l10n.confirm, - loading: state.status == AuthStatus.authenticating, - onPressed: () => - context.read().verify(_controller.text), + + Expanded( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: T.pagePadding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: T.s24), + + // نص الشرح + تعديل الرقم + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Text( + l10n.otpSentTo(phone), + style: theme.textTheme.bodyLarge?.copyWith( + color: cs.onSurfaceVariant, + height: 1.5, + ), + ), + ), + TextButton( + onPressed: () => context.go('/phone'), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: T.s8), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + child: Text( + l10n.edit, + style: theme.textTheme.labelLarge?.copyWith( + color: cs.primary, + fontWeight: FontWeight.w600, + ), + ), + ), + ], + ), + const SizedBox(height: T.s32), + + // ── خانات الرمز — ترتيب LTR ثابت (يطابق dir="ltr" في stitch) ── + Directionality( + textDirection: TextDirection.ltr, + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(_otpLength, (i) { + final hasFocus = _focusNodes[i].hasFocus; + final hasValue = _controllers[i].text.isNotEmpty; + return Padding( + padding: EdgeInsetsDirectional.only( + end: i < _otpLength - 1 ? T.s16 : 0, + ), + child: Container( + width: 60, + height: 64, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(T.r12), + boxShadow: (hasFocus || hasValue) ? const [T.shadowSm] : null, + ), + child: KeyboardListener( + focusNode: FocusNode(), + onKeyEvent: (event) => _onKeyPress(i, event), + child: TextField( + controller: _controllers[i], + focusNode: _focusNodes[i], + textAlign: TextAlign.center, + keyboardType: TextInputType.number, + maxLength: 1, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.bold, + color: cs.onSurface, + fontFeatures: const [FontFeature.tabularFigures()], + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + ], + decoration: InputDecoration( + counterText: '', + filled: true, + fillColor: cs.surfaceContainerLowest, + contentPadding: const EdgeInsets.symmetric(vertical: 14), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(T.r12), + borderSide: BorderSide(color: cs.outlineVariant), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(T.r12), + borderSide: BorderSide( + color: hasValue ? cs.primary : cs.outlineVariant, + width: hasValue ? 1.5 : 1, + ), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(T.r12), + borderSide: BorderSide(color: cs.primary, width: 2), + ), + ), + onChanged: (v) => _onDigitChanged(i, v), + ), + ), + ), + ); + }), + ), + ), + const SizedBox(height: T.s24), + + // عدّاد إعادة الإرسال + Center( + child: _resendSeconds > 0 + ? Text( + l10n.resendIn(_resendSeconds), + style: theme.textTheme.bodyMedium?.copyWith( + color: cs.onSurfaceVariant, + fontFeatures: const [FontFeature.tabularFigures()], + ), + ) + : TripzButton( + onPressed: _resend, + style: TripzButtonStyle.text, + label: l10n.resendCode, + fullWidth: false, + ), + ), + ], + ), + ), ), ], - ); - }, - ), + ), + ); + }, ); } } diff --git a/apps/rider/lib/features/auth/view/phone_screen.dart b/apps/rider/lib/features/auth/view/phone_screen.dart index 8f8f228..4932002 100644 --- a/apps/rider/lib/features/auth/view/phone_screen.dart +++ b/apps/rider/lib/features/auth/view/phone_screen.dart @@ -1,13 +1,19 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../core/l10n/app_localizations.dart'; -import 'package:go_router/go_router.dart'; +import '../../../core/build_config.dart'; import '../cubit/auth_cubit.dart'; import '../cubit/auth_state.dart'; +import '../../../core/design/tokens.dart'; +import '../../../core/utils/phone_normalizer.dart'; +import '../../../core/ui/phone_intel/intl_phone_field.dart'; +import '../../../core/ui/phone_intel/phone_number.dart'; import '../../../core/ui/tripz_scaffold.dart'; import '../../../core/ui/tripz_button.dart'; -import '../../../core/ui/tripz_text_field.dart'; +/// شاشة إدخال الهاتف — تطابق stitch/phone_login.html: +/// خلفية فاتحة + شعار بالمنتصف بالأعلى + sheet أبيض سفلي بحواف مدورة +/// فيه حقل الهاتف (علم + كود دولة + رقم) وزر إرسال الكود. class PhoneScreen extends StatefulWidget { const PhoneScreen({super.key}); @override @@ -15,64 +21,156 @@ class PhoneScreen extends StatefulWidget { } class _PhoneScreenState extends State { - final _controller = TextEditingController(); + PhoneNumber? _phoneNumber; + bool _valid = false; - @override - void dispose() { - _controller.dispose(); - super.dispose(); + void _sendOtp(BuildContext context) { + if (_phoneNumber == null) return; + final normalized = PhoneNormalizer.normalize( + _phoneNumber!.number, + isoCode: _phoneNumber!.countryISOCode, + ); + context.read().sendOtp(normalized); } @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; + final theme = Theme.of(context); + final cs = theme.colorScheme; - return TripzScaffold( - safeAreaTop: true, - padding: true, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Spacer(flex: 2), - Text( - l10n.appName, - style: Theme.of(context).textTheme.headlineLarge, + return BlocConsumer( + listener: (context, state) { + if (state.status == AuthStatus.error && state.error != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.error!)), + ); + } + }, + builder: (context, state) { + final isLoading = state.status == AuthStatus.authenticating; + + return TripzScaffold( + backgroundColor: cs.surfaceContainerLow, + padding: false, + safeAreaBottom: false, + bottomAction: TripzButton( + onPressed: (_valid && !isLoading) ? () => _sendOtp(context) : null, + label: l10n.sendCode, + loading: isLoading, ), - const SizedBox(height: 8), - Text( - l10n.phoneTitle, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - color: Theme.of(context).colorScheme.onSurfaceVariant, + child: Column( + children: [ + // ── منطقة الشعار (النصف العلوي) ── + Expanded( + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: T.s32), + child: Image.asset( + 'assets/images/login_hero.png', + width: 200, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => Icon( + Icons.directions_car_filled_rounded, + size: 96, + color: cs.primary, + ), + ), + ), ), + ), + + // ── بطاقة الإدخال السفلية (منطقة الإبهام) ── + Container( + width: double.infinity, + decoration: BoxDecoration( + color: cs.surfaceContainerLowest, + borderRadius: const BorderRadius.vertical(top: Radius.circular(T.r24)), + boxShadow: const [T.shadowLg], + ), + padding: const EdgeInsets.fromLTRB(T.pagePadding, T.s16, T.pagePadding, T.s32), + child: SafeArea( + top: false, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // مقبض السحب الزخرفي + Center( + child: Container( + width: 40, + height: 4, + decoration: BoxDecoration( + color: cs.outlineVariant, + borderRadius: BorderRadius.circular(T.rCircle), + ), + ), + ), + const SizedBox(height: T.s24), + + // العنوان + Text( + l10n.loginTitle, + style: theme.textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w600, + color: cs.onSurface, + ), + ), + const SizedBox(height: T.s8), + Text( + l10n.phoneLoginSubtitle, + style: theme.textTheme.bodyMedium?.copyWith( + color: cs.onSurfaceVariant, + ), + ), + const SizedBox(height: T.s24), + + // حقل الهاتف — علم + كود الدولة (افتراضي BuildConfig.defaultCountry) + الرقم + IntlPhoneField( + initialCountryCode: BuildConfig.defaultCountry, + languageCode: Localizations.localeOf(context).languageCode, + textAlign: TextAlign.left, + style: theme.textTheme.bodyLarge, + dropdownTextStyle: theme.textTheme.bodyLarge, + flagsButtonPadding: const EdgeInsets.symmetric(horizontal: T.s12), + dropdownDecoration: BoxDecoration( + border: BorderDirectional( + end: BorderSide(color: cs.outlineVariant), + ), + ), + decoration: InputDecoration( + hintText: l10n.phoneHint, + filled: true, + fillColor: cs.surfaceContainerLowest, + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(T.r8), + borderSide: BorderSide(color: cs.outlineVariant), + ), + enabledBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(T.r8), + borderSide: BorderSide(color: cs.outlineVariant), + ), + focusedBorder: OutlineInputBorder( + borderRadius: BorderRadius.circular(T.r8), + borderSide: BorderSide(color: cs.primary, width: 1.5), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: T.s16, vertical: T.s16), + ), + onChanged: (phone) { + _phoneNumber = phone; + final num = phone.number; + _valid = num.isNotEmpty && num.length >= 7; + setState(() {}); + }, + ), + ], + ), + ), + ), + ], ), - const Spacer(), - TripzTextField( - controller: _controller, - label: l10n.phoneLabel, - hint: l10n.phoneHint, - keyboardType: TextInputType.phone, - ), - const SizedBox(height: 20), - BlocConsumer( - listener: (context, state) { - if (state.status == AuthStatus.error && state.error != null) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.error!)), - ); - } - }, - builder: (context, state) { - return TripzButton( - label: l10n.sendCode, - loading: state.status == AuthStatus.authenticating, - onPressed: () => - context.read().sendOtp(_controller.text), - ); - }, - ), - const Spacer(flex: 2), - ], - ), + ); + }, ); } } diff --git a/apps/rider/lib/features/home/cubit/destination_cubit.dart b/apps/rider/lib/features/home/cubit/destination_cubit.dart index f96214e..e746c85 100644 --- a/apps/rider/lib/features/home/cubit/destination_cubit.dart +++ b/apps/rider/lib/features/home/cubit/destination_cubit.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'package:equatable/equatable.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter/foundation.dart'; import '../data/maps_repository.dart'; import '../data/models/place_model.dart'; @@ -62,10 +63,11 @@ class DestinationCubit extends Cubit { Future _search(String query) async { try { - final results = await _mapsRepo.autocomplete(query); + final results = await _mapsRepo.search(query); emit(state.copyWith(results: results, searching: false)); } catch (e) { - emit(state.copyWith(searching: false, error: e.toString())); + emit(state.copyWith( + searching: false, error: e.toString(), clearError: false)); } } @@ -74,10 +76,27 @@ class DestinationCubit extends Cubit { if (query.trim().length < 2) return; emit(state.copyWith(searching: true, clearError: true)); try { - final results = await _mapsRepo.autocomplete(query); + final results = await _mapsRepo.search(query); emit(state.copyWith(results: results, searching: false)); - } catch (e) { - emit(state.copyWith(searching: false, error: e.toString())); + } catch (e, st) { + debugPrint('Map Search Error: $e\n$st'); + emit(state.copyWith( + searching: false, error: e.toString(), clearError: false)); + } + } + + Future searchVoiceDirectly(String query, + {double? lat, double? lng}) async { + _debounce?.cancel(); + if (query.trim().isEmpty) return; + emit(state.copyWith(searching: true, clearError: true)); + try { + final results = await _mapsRepo.voiceSearch(query, lat: lat, lng: lng); + emit(state.copyWith(results: results, searching: false)); + } catch (e, st) { + debugPrint('Map Voice Search Error: $e\n$st'); + emit(state.copyWith( + searching: false, error: e.toString(), clearError: false)); } } diff --git a/apps/rider/lib/features/home/cubit/home_cubit.dart b/apps/rider/lib/features/home/cubit/home_cubit.dart index 242e5cc..b9693c5 100644 --- a/apps/rider/lib/features/home/cubit/home_cubit.dart +++ b/apps/rider/lib/features/home/cubit/home_cubit.dart @@ -82,15 +82,22 @@ class HomeCubit extends Cubit { ); final loc = LatLng(pos.latitude, pos.longitude); - // جلب العنوان العكسي للموقع الحالي. - final place = await _mapsRepo.reverseGeocode(pos.latitude, pos.longitude); - + // نوسّط الكاميرا على الموقع فوراً — لا ننتظر العنوان العكسي. emit(state.copyWith( loading: false, currentLocation: loc, cameraTarget: loc, - pickup: place, )); + + // العنوان العكسي تحسين إضافي: فشله يجب ألّا يمنع توسيط الخريطة. + try { + final place = await _mapsRepo.reverseGeocode(pos.latitude, pos.longitude); + emit(state.copyWith(pickup: place)); + } catch (_) { + emit(state.copyWith( + pickup: PlaceModel(name: '', lat: loc.latitude, lng: loc.longitude), + )); + } } catch (e) { emit(state.copyWith( loading: false, diff --git a/apps/rider/lib/features/home/cubit/saved_places_cubit.dart b/apps/rider/lib/features/home/cubit/saved_places_cubit.dart new file mode 100644 index 0000000..bab7294 --- /dev/null +++ b/apps/rider/lib/features/home/cubit/saved_places_cubit.dart @@ -0,0 +1,79 @@ +import 'package:flutter_bloc/flutter_bloc.dart'; + +import '../../../core/storage/saved_places_store.dart'; + +class SavedPlacesState { + final List places; + final bool loading; + final String? error; + + const SavedPlacesState({ + this.places = const [], + this.loading = false, + this.error, + }); + + SavedPlacesState copyWith({ + List? places, + bool? loading, + String? error, + }) { + return SavedPlacesState( + places: places ?? this.places, + loading: loading ?? this.loading, + error: error, + ); + } +} + +class SavedPlacesCubit extends Cubit { + final SavedPlacesStore _store; + + SavedPlacesCubit(this._store) : super(const SavedPlacesState()); + + Future load() async { + emit(state.copyWith(loading: true, error: null)); + try { + final places = await _store.getAll(); + emit(SavedPlacesState(places: places)); + } catch (e) { + emit(state.copyWith(loading: false, error: e.toString())); + } + } + + Future addPlace(SavedPlace place) async { + try { + await _store.add(place); + await load(); + } catch (e) { + emit(state.copyWith(error: e.toString())); + } + } + + Future removePlace(String id) async { + try { + await _store.remove(id); + await load(); + } catch (e) { + emit(state.copyWith(error: e.toString())); + } + } + + Future setFavorite(String id, FavoriteType type) async { + try { + await _store.setFavorite(id, type); + await load(); + } catch (e) { + emit(state.copyWith(error: e.toString())); + } + } + + Future clearFavorite(FavoriteType type) async { + try { + await _store.clearFavorite(type); + await load(); + } catch (e) { + emit(state.copyWith(error: e.toString())); + } + } +} diff --git a/apps/rider/lib/features/home/data/maps_repository.dart b/apps/rider/lib/features/home/data/maps_repository.dart index f2a0cad..a962244 100644 --- a/apps/rider/lib/features/home/data/maps_repository.dart +++ b/apps/rider/lib/features/home/data/maps_repository.dart @@ -1,34 +1,40 @@ +import '../../../core/api/antlaq_api.dart'; import '../../../core/api/api_client.dart'; import 'models/place_model.dart'; import 'models/route_model.dart'; -/// نقطة وصول وحيدة لبيانات الخرائط — كل نداء يمرّ عبر سيرفرنا -/// (docs/23 §4) لحماية المفتاح وتفعيل التخزين المؤقت على Redis. +/// نقطة وصول وحيدة لبيانات الخرائط والبحث الصوتي الذكي عبر الباك إند. class MapsRepository { - final ApiClient _api; - MapsRepository(this._api); + final AntlaqApi _api; + final ApiClient? _apiClient; - /// بحث مكاني — يمرّ عبر `GET /maps/geocode`. - Future> autocomplete(String query) async { - if (query.trim().length < 2) return []; - final r = await _api.dio.get( - '/maps/geocode', - queryParameters: {'q': query.trim(), 'country': 'JO'}, - ); - final items = (r.data['items'] ?? r.data ?? []) as List; - return items - .map((e) => PlaceModel.fromJson(e as Map)) - .toList(); + MapsRepository(this._api, [this._apiClient]); + + /// بحث صوتي ذكي يمر عبر الباك إند (Gemini AI). + Future> voiceSearch(String text, {double? lat, double? lng}) async { + if (text.trim().isEmpty) return []; + try { + if (_apiClient != null) { + final res = await _apiClient.dio.post('/maps/voice-search', data: { + 'text': text, + if (lat != null) 'lat': lat, + if (lng != null) 'lng': lng, + }); + final list = res.data as List?; + if (list != null) { + return list + .map((e) => PlaceModel.fromJson(e as Map)) + .toList(); + } + } + } catch (_) {} + return search(text); } - /// بحث حرّ (geocode). - Future> geocode(String query) async { + /// بحث أماكن — يمر عبر GET /api/geocoding/search. + Future> search(String query) async { if (query.trim().length < 2) return []; - final r = await _api.dio.get( - '/maps/geocode', - queryParameters: {'q': query.trim(), 'country': 'JO'}, - ); - final items = (r.data['items'] ?? r.data ?? []) as List; + final items = await _api.search(query); return items .map((e) => PlaceModel.fromJson(e as Map)) .toList(); @@ -36,48 +42,44 @@ class MapsRepository { /// تفكيك عنوان عكسي (إحداثيات → نص). Future reverseGeocode(double lat, double lng) async { - final r = await _api.dio.get( - '/maps/reverse', - queryParameters: {'lat': lat, 'lng': lng}, - ); - final j = r.data as Map; + final j = await _api.reverse(lat, lng); + final name = (j['name_ar'] ?? j['name'] ?? j['formattedAddress'] ?? '') as String; + final addr = (j['address'] ?? j['full_address'] ?? j['formattedAddress'] ?? '') as String; return PlaceModel.fromJson({ - 'name': j['formattedAddress'] ?? '', - 'address': j['formattedAddress'], - 'lat': lat, - 'lng': lng, + 'name': name, + 'full_address': addr.isNotEmpty ? addr : name, + 'latitude': lat, + 'longitude': lng, }); } - /// تفاصيل مكان بالـ placeId — يمرّ عبر geocode. - Future placeDetails(String placeId) async { - final r = await _api.dio.get( - '/maps/geocode', - queryParameters: {'q': placeId, 'country': 'JO'}, - ); - final items = (r.data['items'] ?? r.data ?? []) as List; - if (items.isEmpty) { - throw Exception('Place not found'); - } - return PlaceModel.fromJson(items.first as Map); - } - - /// حساب المسار بين نقطتين. + /// حساب المسار بين نقطتين عبر الباك إند (GET /maps/route). Future getRoute({ required double fromLat, required double fromLng, required double toLat, required double toLng, }) async { - final r = await _api.dio.get( - '/maps/route', - queryParameters: { - 'fromLat': fromLat, - 'fromLng': fromLng, - 'toLat': toLat, - 'toLng': toLng, - }, + try { + if (_apiClient != null) { + final res = await _apiClient.dio.get('/maps/route', queryParameters: { + 'fromLat': fromLat, + 'fromLng': fromLng, + 'toLat': toLat, + 'toLng': toLng, + }); + if (res.data != null && res.data is Map) { + return RouteModel.fromJson(res.data as Map); + } + } + } catch (_) {} + + final j = await _api.route( + fromLat: fromLat, + fromLng: fromLng, + toLat: toLat, + toLng: toLng, ); - return RouteModel.fromJson(r.data as Map); + return RouteModel.fromJson(j); } } diff --git a/apps/rider/lib/features/home/data/models/gate_model.dart b/apps/rider/lib/features/home/data/models/gate_model.dart new file mode 100644 index 0000000..766a2b8 --- /dev/null +++ b/apps/rider/lib/features/home/data/models/gate_model.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; + +/// باب/مدخل مكان — من استجابة انطلق. +class Gate extends Equatable { + final String nameAr; + final String nameEn; + final double lat; + final double lng; + final bool isMainGate; + + const Gate({ + required this.nameAr, + required this.nameEn, + required this.lat, + required this.lng, + this.isMainGate = false, + }); + + factory Gate.fromJson(Map j) => Gate( + nameAr: (j['name_ar'] ?? '') as String, + nameEn: (j['name_en'] ?? '') as String, + lat: (j['latitude'] as num).toDouble(), + lng: (j['longitude'] as num).toDouble(), + isMainGate: j['is_main_gate'] as bool? ?? false, + ); + + LatLng get latLng => LatLng(lat, lng); + + @override + List get props => [nameAr, nameEn, lat, lng, isMainGate]; +} diff --git a/apps/rider/lib/features/home/data/models/place_model.dart b/apps/rider/lib/features/home/data/models/place_model.dart index ecf5269..6fa0da3 100644 --- a/apps/rider/lib/features/home/data/models/place_model.dart +++ b/apps/rider/lib/features/home/data/models/place_model.dart @@ -1,12 +1,17 @@ import 'package:equatable/equatable.dart'; +import 'gate_model.dart'; -/// نموذج مكان من نتائج البحث (autocomplete / geocode). +/// نموذج مكان من نتائج بحث انطلق (geocode / search). class PlaceModel extends Equatable { final String? placeId; final String name; final String? address; final double lat; final double lng; + final String? category; + final double? totalScore; + final String? source; + final List gates; const PlaceModel({ this.placeId, @@ -14,32 +19,53 @@ class PlaceModel extends Equatable { this.address, required this.lat, required this.lng, + this.category, + this.totalScore, + this.source, + this.gates = const [], }); - factory PlaceModel.fromJson(Map j) => PlaceModel( - placeId: j['placeId'] as String?, - name: (j['description'] ?? j['name'] ?? '') as String, - address: j['address'] as String?, - lat: (j['lat'] as num).toDouble(), - lng: (j['lng'] as num).toDouble(), - ); - - factory PlaceModel.fromGeocode(Map j) => PlaceModel( - placeId: j['placeId'] as String?, - name: (j['name'] ?? j['formattedAddress'] ?? '') as String, - address: j['formattedAddress'] as String?, - lat: (j['lat'] as num).toDouble(), - lng: (j['lng'] as num).toDouble(), - ); + /// استجابة انطلق الحقيقية — الحقول الم标准. + factory PlaceModel.fromJson(Map j) { + final lat = (j['latitude'] ?? j['location']?['lat'] ?? 0) as num; + final lng = (j['longitude'] ?? j['location']?['lng'] ?? 0) as num; + return PlaceModel( + placeId: j['id'] as String?, + name: (j['name'] ?? '') as String, + address: (j['full_address'] ?? j['address']) as String?, + lat: lat.toDouble(), + lng: lng.toDouble(), + category: j['category'] as String?, + totalScore: (j['totalScore'] as num?)?.toDouble(), + source: j['source'] as String?, + gates: (j['gates'] as List?) + ?.map((g) => Gate.fromJson(g as Map)) + .toList() ?? + const [], + ); + } Map toJson() => { - if (placeId != null) 'placeId': placeId, + if (placeId != null) 'id': placeId, 'name': name, - if (address != null) 'address': address, - 'lat': lat, - 'lng': lng, + if (address != null) 'full_address': address, + 'latitude': lat, + 'longitude': lng, + if (category != null) 'category': category, + if (totalScore != null) 'totalScore': totalScore, + if (source != null) 'source': source, }; + /// هل لهذا المكان أبواب متعددة? + bool get hasMultipleGates => gates.length > 1; + + /// الباب الرئيسي إن وُجد. + Gate? get mainGate => gates.cast().firstWhere( + (g) => g!.isMainGate, + orElse: () => null, + ); + @override - List get props => [placeId, name, address, lat, lng]; + List get props => + [placeId, name, address, lat, lng, category, totalScore, source, gates]; } diff --git a/apps/rider/lib/features/home/data/models/route_model.dart b/apps/rider/lib/features/home/data/models/route_model.dart index cf532d6..17e3f23 100644 --- a/apps/rider/lib/features/home/data/models/route_model.dart +++ b/apps/rider/lib/features/home/data/models/route_model.dart @@ -1,25 +1,44 @@ import 'package:equatable/equatable.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; -/// خطاب polyline مشفر ومسافة/وقت تقريبيان. +/// استجابة مسار من انطلق — مع polyline مشفر وإطار خريطة. class RouteModel extends Equatable { final int distanceMeters; final int durationSeconds; final String? polyline; + final String? routeName; + final String? startName; + final String? endName; + final List? tags; + final List? bbox; + final double? trafficFactor; const RouteModel({ required this.distanceMeters, required this.durationSeconds, this.polyline, + this.routeName, + this.startName, + this.endName, + this.tags, + this.bbox, + this.trafficFactor, }); + /// استجابة مسار انطلق الحقيقية (تُمرر عبر الباك إند كما هي). factory RouteModel.fromJson(Map j) { - final dist = j['distanceMeters'] ?? j['distance']; - final dur = j['durationSeconds'] ?? j['duration']; + final dist = (j['distance'] ?? 0) as num; + final dur = (j['duration'] ?? 0) as num; return RouteModel( - distanceMeters: dist is int ? dist : int.tryParse('$dist') ?? 0, - durationSeconds: dur is int ? dur : int.tryParse('$dur') ?? 0, - polyline: j['polyline'] as String?, + distanceMeters: dist.toInt(), + durationSeconds: dur.toInt(), + polyline: j['points'] as String?, + routeName: j['routeName'] as String?, + startName: j['startName'] as String?, + endName: j['endName'] as String?, + tags: (j['tags'] as List?)?.cast(), + bbox: (j['bbox'] as List?)?.map((e) => (e as num).toDouble()).toList(), + trafficFactor: (j['trafficFactor'] as num?)?.toDouble(), ); } @@ -36,39 +55,49 @@ class RouteModel extends Equatable { return '$h h ${rem > 0 ? '$rem min' : ''}'; } + /// تحويل bbox إلى LatLngBounds لضبط الكاميرا مع حماية للمواقع القريبة على iOS. + LatLngBounds? getLatLngBounds() { + if (bbox == null || bbox!.length < 4) return null; + var minLat = bbox![1]; + var minLng = bbox![0]; + var maxLat = bbox![3]; + var maxLng = bbox![2]; + + // إذا كانت المسافة بين النقاط قريبة جداً (أقل من 0.002 درجة)، نضمن توسعاً أمنياً أدنى لمنع عطل وقفل كاميرا MapLibre على iOS + if ((maxLat - minLat).abs() < 0.002) { + minLat -= 0.001; + maxLat += 0.001; + } + if ((maxLng - minLng).abs() < 0.002) { + minLng -= 0.001; + maxLng += 0.001; + } + + return LatLngBounds( + southwest: LatLng(minLat, minLng), + northeast: LatLng(maxLat, maxLng), + ); + } + + /// فك الـ polyline عبر PolylineUtils من الباكيج. List decodePolyline() { if (polyline == null || polyline!.isEmpty) return []; - return _decodeEncodedPolyline(polyline!); + return PolylineUtils.decode(polyline!); } - /// فك تشفير polyline وفقاً لخوارزمية Google. - static List _decodeEncodedPolyline(String encoded) { - final List poly = []; - int index = 0, lat = 0, lng = 0; - - while (index < encoded.length) { - int b, shift = 0, result = 0; - do { - b = encoded.codeUnitAt(index++) - 63; - result |= (b & 0x1f) << shift; - shift += 5; - } while (b >= 0x20); - lat += ((result & 1) != 0) ? ~(result >> 1) : (result >> 1); - - shift = 0; - result = 0; - do { - b = encoded.codeUnitAt(index++) - 63; - result |= (b & 0x1f) << shift; - shift += 5; - } while (b >= 0x20); - lng += ((result & 1) != 0) ? ~(result >> 1) : (result >> 1); - - poly.add(LatLng(lat / 1e5, lng / 1e5)); - } - return poly; - } + /// هل المسار مباشر (لا أضلاع)? + bool get isStraightLine => tags?.contains('straight-line') ?? false; @override - List get props => [distanceMeters, durationSeconds, polyline]; + List get props => [ + distanceMeters, + durationSeconds, + polyline, + routeName, + startName, + endName, + tags, + bbox, + trafficFactor, + ]; } diff --git a/apps/rider/lib/features/home/view/destination_screen.dart b/apps/rider/lib/features/home/view/destination_screen.dart index cbb13f1..0985a7e 100644 --- a/apps/rider/lib/features/home/view/destination_screen.dart +++ b/apps/rider/lib/features/home/view/destination_screen.dart @@ -1,86 +1,723 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../core/l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; +import 'package:intaleq_maps/intaleq_maps.dart'; +import '../../../core/l10n/app_localizations.dart'; import '../../../core/design/tokens.dart'; -import '../../../core/ui/tripz_scaffold.dart'; -import '../../../core/ui/tripz_text_field.dart'; +import '../../../core/design/tripz_colors.dart'; +import '../../../core/di.dart'; import '../cubit/destination_cubit.dart'; +import '../cubit/home_cubit.dart'; +import '../data/maps_repository.dart'; import '../data/models/place_model.dart'; -import 'widgets/place_tile.dart'; -/// شاشة البحث عن وجهة — full-screen مع autocomplete. +const int _maxStops = 2; + class DestinationScreen extends StatefulWidget { - const DestinationScreen({super.key}); + final LatLng? currentLocation; + + const DestinationScreen({super.key, this.currentLocation}); @override State createState() => _DestinationScreenState(); } class _DestinationScreenState extends State { - final _controller = TextEditingController(); - final _focusNode = FocusNode(); + final _destController = TextEditingController(); + final _destFocusNode = FocusNode(); + final _stopControllers = []; + final _stopFocusNodes = []; + int _editingStopIndex = -1; + String? _pickupAddressName; + bool _forSelf = true; @override void initState() { super.initState(); - _focusNode.requestFocus(); + _destFocusNode.requestFocus(); + _loadReverseGeocodePickup(); + } + + Future _loadReverseGeocodePickup() async { + final loc = widget.currentLocation; + if (loc != null) { + try { + final repo = getIt(); + final place = await repo.reverseGeocode(loc.latitude, loc.longitude); + if (mounted && place.name.isNotEmpty) { + setState(() { + _pickupAddressName = place.name; + }); + } + } catch (_) {} + } + } + + void _swapLocations() { + setState(() { + final oldDest = _destController.text; + if (_stopControllers.isNotEmpty) { + _destController.text = _stopControllers.first.text; + _stopControllers.first.text = oldDest; + } else if (_pickupAddressName != null && _pickupAddressName!.isNotEmpty) { + _destController.text = _pickupAddressName!; + _pickupAddressName = oldDest.isNotEmpty ? oldDest : _pickupAddressName; + } + }); + } + + void _showAiVoiceSearchDialog(BuildContext context) { + final voiceController = TextEditingController(); + final l10n = AppLocalizations.of(context)!; + final cs = Theme.of(context).colorScheme; + + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (modalCtx) { + return Container( + padding: EdgeInsets.only( + bottom: MediaQuery.of(modalCtx).viewInsets.bottom + T.s24, + top: T.s24, + left: T.pagePadding, + right: T.pagePadding, + ), + decoration: BoxDecoration( + color: cs.surface, + borderRadius: + const BorderRadius.vertical(top: Radius.circular(T.r24)), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 64, + height: 64, + decoration: BoxDecoration( + color: cs.primaryContainer, + shape: BoxShape.circle, + ), + child: Icon(Icons.mic_rounded, + color: cs.onPrimaryContainer, size: 36), + ), + const SizedBox(height: T.s16), + Text( + 'البحث الصوتي الذكي (AI Voice)', + style: Theme.of(modalCtx).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: T.s8), + Text( + 'قل وجهتك بصوتك أو اكتب وصف المكان (مثل: مجمع الأبحاث، الشميساني)', + textAlign: TextAlign.center, + style: Theme.of(modalCtx).textTheme.bodyMedium?.copyWith( + color: cs.onSurfaceVariant, + ), + ), + const SizedBox(height: T.s16), + TextField( + controller: voiceController, + autofocus: true, + decoration: InputDecoration( + hintText: 'تحدث أو اكتب المكان...', + prefixIcon: const Icon(Icons.graphic_eq_rounded), + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(T.r12), + ), + ), + onSubmitted: (text) { + Navigator.pop(modalCtx); + if (text.trim().isNotEmpty) { + _destController.text = text; + final loc = widget.currentLocation; + context.read().searchVoiceDirectly( + text, + lat: loc?.latitude, + lng: loc?.longitude, + ); + } + }, + ), + const SizedBox(height: T.s16), + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => Navigator.pop(modalCtx), + child: Text(l10n.cancel), + ), + const SizedBox(width: T.s8), + ElevatedButton.icon( + onPressed: () { + final text = voiceController.text; + Navigator.pop(modalCtx); + if (text.trim().isNotEmpty) { + _destController.text = text; + final loc = widget.currentLocation; + context.read().searchVoiceDirectly( + text, + lat: loc?.latitude, + lng: loc?.longitude, + ); + } + }, + icon: const Icon(Icons.search_rounded), + label: const Text('بحث بالذكاء الاصطناعي'), + ), + ], + ), + ], + ), + ); + }, + ); } @override void dispose() { - _controller.dispose(); - _focusNode.dispose(); + _destController.dispose(); + _destFocusNode.dispose(); + for (final c in _stopControllers) { + c.dispose(); + } + for (final f in _stopFocusNodes) { + f.dispose(); + } super.dispose(); } + void _addStop() { + if (_stopControllers.length >= _maxStops) return; + setState(() { + _stopControllers.add(TextEditingController()); + _stopFocusNodes.add(FocusNode()); + _editingStopIndex = _stopControllers.length - 1; + }); + _stopFocusNodes.last.requestFocus(); + } + + void _removeStop(int index) { + setState(() { + _stopControllers[index].dispose(); + _stopFocusNodes[index].dispose(); + _stopControllers.removeAt(index); + _stopFocusNodes.removeAt(index); + if (_editingStopIndex == index) { + _editingStopIndex = -1; + } else if (_editingStopIndex > index) { + _editingStopIndex--; + } + }); + } + + void _selectPlace(dynamic result) { + context.pop(result); + } + @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; + final theme = Theme.of(context); + final cs = theme.colorScheme; + final tripz = context.tripzColors; - return TripzScaffold( - padding: false, - child: Column( - children: [ - _SearchHeader( - controller: _controller, - focusNode: _focusNode, - l10n: l10n, + return Scaffold( + backgroundColor: Colors.transparent, + extendBodyBehindAppBar: true, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + onPressed: () => context.pop(), + icon: const Icon(Icons.arrow_forward), + color: cs.onSurface, + style: IconButton.styleFrom( + backgroundColor: cs.surface, + shadowColor: Colors.black12, + elevation: 2, ), - Expanded( - child: BlocBuilder( - builder: (context, state) { - if (state.searching) { - return const Center(child: CircularProgressIndicator()); - } - if (state.results.isEmpty && state.query.length >= 2) { - return Center( - child: Text(l10n.noResults, style: Theme.of(context).textTheme.bodyLarge), - ); - } - return ListView.builder( - padding: EdgeInsets.zero, - itemCount: state.results.length, - itemBuilder: (context, index) { - final place = state.results[index]; - return PlaceTile( - place: place, - onTap: () => context.pop(place), - ); - }, + ), + title: Text( + l10n.selectDestination, + style: theme.textTheme.titleMedium, + ), + centerTitle: true, + ), + body: Stack( + children: [ + // Map background + Positioned.fill( + child: Container( + color: cs.surfaceContainerHighest, + child: Opacity( + opacity: 0.5, + child: Image.asset( + 'assets/images/destination_selection_img.png', + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => const SizedBox.shrink(), + ), + ), + ), + ), + + // Map overlay gradient + Positioned.fill( + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Colors.black12, Colors.black38], + ), + ), + ), + ), + + // Draggable sheet + Positioned.fill( + child: DraggableScrollableSheet( + initialChildSize: 0.55, + minChildSize: 0.15, + maxChildSize: 0.88, + snap: true, + snapSizes: const [0.15, 0.55, 0.88], + builder: (context, scrollController) { + return Container( + decoration: BoxDecoration( + color: cs.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(T.r24)), + boxShadow: const [T.shadowLg], + ), + child: Column( + children: [ + // Drag handle + Center( + child: Padding( + padding: const EdgeInsets.symmetric(vertical: T.s12), + child: Container( + width: 48, + height: 4, + decoration: BoxDecoration( + color: cs.outlineVariant, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + ), + + Expanded( + child: ListView( + controller: scrollController, + padding: EdgeInsets.zero, + children: [ + _buildFieldsSection( + context, l10n, theme, cs, tripz), + const SizedBox(height: T.s16), + _buildSavedPlaces(context, theme, cs), + const SizedBox(height: T.s8), + _buildAutocompleteResults(context, l10n, theme, cs), + _buildPickFromMap(context, l10n, cs), + const SizedBox(height: T.s32), + ], + ), + ), + ], + ), ); }, ), ), - SafeArea( - child: Padding( - padding: const EdgeInsets.all(T.pagePadding), - child: SizedBox( - width: double.infinity, - child: OutlinedButton.icon( - onPressed: () => context.pop(null), - icon: const Icon(Icons.map_outlined), - label: Text(l10n.chooseOnMap), + ], + ), + ); + } + + Widget _buildFieldsSection( + BuildContext context, + AppLocalizations l10n, + ThemeData theme, + ColorScheme cs, + TripzColors tripz, + ) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: T.pagePadding), + child: Column( + children: [ + // Rider selection chip — "For me / For someone else" + Row( + children: [ + FilterChip( + selected: _forSelf, + label: const Text('طلب لنفسي'), + avatar: const Icon(Icons.person_rounded, size: 16), + onSelected: (val) => setState(() => _forSelf = true), + ), + const SizedBox(width: T.s8), + FilterChip( + selected: !_forSelf, + label: const Text('طلب لشخص آخر'), + avatar: const Icon(Icons.people_rounded, size: 16), + onSelected: (val) => setState(() => _forSelf = false), + ), + const Spacer(), + IconButton( + onPressed: _swapLocations, + icon: const Icon(Icons.swap_vert_rounded), + tooltip: 'تبديل الأماكن', + color: cs.primary, + ), + ], + ), + const SizedBox(height: T.s8), + + // Origin — green dot, reverse geocoded + _OriginRow( + l10n: l10n, + theme: theme, + cs: cs, + tripz: tripz, + addressName: _pickupAddressName, + ), + + // Stops + for (var i = 0; i < _stopControllers.length; i++) + _StopRow( + index: i, + controller: _stopControllers[i], + focusNode: _stopFocusNodes[i], + isEditing: _editingStopIndex == i, + l10n: l10n, + theme: theme, + cs: cs, + tripz: tripz, + onStopTapped: () { + setState(() => _editingStopIndex = i); + context.read().onQueryChanged(''); + _stopFocusNodes[i].requestFocus(); + }, + onStopChanged: (v) => + context.read().onQueryChanged(v), + onStopRemoved: () => _removeStop(i), + ), + + // Dotted connector between last stop / origin and destination + _DottedConnector(color: cs.outlineVariant), + + // Destination — red square, active with AI Voice Search button + _DestinationRow( + controller: _destController, + focusNode: _destFocusNode, + isEditing: _editingStopIndex == -1, + l10n: l10n, + theme: theme, + cs: cs, + tripz: tripz, + onFieldTapped: () { + setState(() => _editingStopIndex = -1); + context.read().onQueryChanged(''); + _destFocusNode.requestFocus(); + }, + onChanged: (v) => + context.read().onQueryChanged(v), + onVoiceSearchPressed: () => _showAiVoiceSearchDialog(context), + ), + + const SizedBox(height: T.s8), + + // Add stop button + if (_stopControllers.length < _maxStops) + Align( + alignment: Alignment.centerRight, + child: TextButton.icon( + onPressed: _addStop, + icon: const Icon(Icons.add, size: 18), + label: Text(l10n.addStop), + style: TextButton.styleFrom( + foregroundColor: cs.primary, + padding: const EdgeInsets.symmetric(vertical: T.s8), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + ), + ), + ), + + const SizedBox(height: T.s12), + Divider(color: cs.outlineVariant.withValues(alpha: 0.3)), + const SizedBox(height: T.s12), + ], + ), + ); + } + + Widget _buildSavedPlaces( + BuildContext context, + ThemeData theme, + ColorScheme cs, + ) { + final l10n = AppLocalizations.of(context)!; + return SizedBox( + height: 52, + child: ListView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.symmetric(horizontal: T.pagePadding), + children: [ + _SavedPlaceChip( + icon: Icons.home_rounded, + label: l10n.homePlace, + onTap: () {}, + ), + const SizedBox(width: T.s12), + _SavedPlaceChip( + icon: Icons.work_rounded, + label: l10n.workPlace, + onTap: () {}, + ), + const SizedBox(width: T.s12), + _SavedPlaceChip( + icon: Icons.star_rounded, + label: l10n.favorites, + onTap: () {}, + ), + ], + ), + ); + } + + Widget _buildAutocompleteResults( + BuildContext context, + AppLocalizations l10n, + ThemeData theme, + ColorScheme cs, + ) { + return BlocBuilder( + builder: (context, state) { + if (state.searching) { + return const Padding( + padding: EdgeInsets.symmetric(vertical: T.s24), + child: Center(child: CircularProgressIndicator()), + ); + } + if (state.results.isEmpty && state.query.length >= 2) { + return Padding( + padding: const EdgeInsets.symmetric(vertical: T.s24), + child: Center( + child: Text(l10n.noResults, style: theme.textTheme.bodyLarge), + ), + ); + } + return Column( + children: [ + for (var i = 0; i < state.results.length; i++) + _SearchResultTile( + place: state.results[i], + onTap: () => _selectPlace(state.results[i]), + ), + ], + ); + }, + ); + } + + Widget _buildPickFromMap( + BuildContext context, + AppLocalizations l10n, + ColorScheme cs, + ) { + return Padding( + padding: const EdgeInsets.symmetric( + horizontal: T.pagePadding, + vertical: T.s8, + ), + child: _PickFromMapTile( + onTap: () => _selectPlace({'pickFromMap': true}), + ), + ); + } +} + +// ── Origin row ── +class _OriginRow extends StatelessWidget { + final AppLocalizations l10n; + final ThemeData theme; + final ColorScheme cs; + final TripzColors tripz; + final String? addressName; + + const _OriginRow({ + required this.l10n, + required this.theme, + required this.cs, + required this.tripz, + this.addressName, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: T.s4), + child: Row( + children: [ + SizedBox( + width: 48, + child: Center( + child: Container( + width: 12, + height: 12, + decoration: BoxDecoration( + color: tripz.success, + shape: BoxShape.circle, + border: Border.all( + color: tripz.success.withValues(alpha: 0.25), + width: 4, + ), + ), + ), + ), + ), + Expanded( + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: T.s12, + vertical: T.s12, + ), + decoration: BoxDecoration( + color: cs.surfaceContainerLow, + borderRadius: BorderRadius.circular(T.r12), + border: Border.all( + color: cs.outlineVariant.withValues(alpha: 0.3), + ), + ), + child: Text( + (addressName != null && addressName!.isNotEmpty) + ? addressName! + : l10n.pickupLocation, + style: theme.textTheme.bodyLarge?.copyWith( + color: (addressName != null && addressName!.isNotEmpty) + ? cs.onSurface + : cs.onSurfaceVariant, + fontWeight: (addressName != null && addressName!.isNotEmpty) + ? FontWeight.w600 + : FontWeight.normal, + ), + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ), + ], + ), + ); + } +} + +// ── Stop row ── +class _StopRow extends StatelessWidget { + final int index; + final TextEditingController controller; + final FocusNode focusNode; + final bool isEditing; + final AppLocalizations l10n; + final ThemeData theme; + final ColorScheme cs; + final TripzColors tripz; + final VoidCallback onStopTapped; + final ValueChanged onStopChanged; + final VoidCallback onStopRemoved; + + const _StopRow({ + required this.index, + required this.controller, + required this.focusNode, + required this.isEditing, + required this.l10n, + required this.theme, + required this.cs, + required this.tripz, + required this.onStopTapped, + required this.onStopChanged, + required this.onStopRemoved, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: T.s4), + child: Row( + children: [ + SizedBox( + width: 48, + child: Center( + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: cs.surface, + shape: BoxShape.circle, + border: Border.all(color: cs.onSurfaceVariant, width: 2), + ), + child: Center( + child: Text( + '${index + 1}', + style: theme.textTheme.labelSmall?.copyWith( + color: cs.onSurfaceVariant, + fontSize: 10, + height: 1, + ), + ), + ), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: onStopTapped, + child: Container( + decoration: BoxDecoration( + color: isEditing ? cs.surface : cs.surfaceContainerLow, + borderRadius: BorderRadius.circular(T.r12), + border: Border.all( + color: isEditing + ? cs.primary + : cs.outlineVariant.withValues(alpha: 0.3), + width: isEditing ? 2 : 1, + ), + boxShadow: isEditing ? const [T.shadowSm] : null, + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: controller, + focusNode: focusNode, + readOnly: !isEditing, + decoration: InputDecoration( + hintText: '${l10n.addStop} ${index + 1}', + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: T.s12, + vertical: T.s12, + ), + hintStyle: theme.textTheme.bodyLarge?.copyWith( + color: cs.onSurfaceVariant.withValues(alpha: 0.7), + ), + ), + style: theme.textTheme.bodyLarge?.copyWith( + color: cs.onSurface, + ), + onChanged: onStopChanged, + ), + ), + if (isEditing && controller.text.isNotEmpty) + IconButton( + onPressed: onStopRemoved, + icon: const Icon(Icons.close, size: 18), + color: cs.onSurfaceVariant, + splashRadius: 18, + ), + ], ), ), ), @@ -91,103 +728,313 @@ class _DestinationScreenState extends State { } } -class _SearchHeader extends StatelessWidget { +// ── Dotted connector line ── +class _DottedConnector extends StatelessWidget { + final Color color; + const _DottedConnector({required this.color}); + + @override + Widget build(BuildContext context) { + return SizedBox( + width: 48, + child: CustomPaint( + size: const Size(48, 20), + painter: _DottedLinePainter(color: color), + ), + ); + } +} + +class _DottedLinePainter extends CustomPainter { + final Color color; + _DottedLinePainter({required this.color}); + + @override + void paint(Canvas canvas, Size size) { + final paint = Paint() + ..color = color + ..strokeWidth = 1.5 + ..style = PaintingStyle.stroke; + + const dashHeight = 3.0; + const gapHeight = 3.0; + double y = 4; + while (y < size.height) { + canvas.drawLine( + Offset(size.width / 2, y), + Offset(size.width / 2, y + dashHeight), + paint, + ); + y += dashHeight + gapHeight; + } + } + + @override + bool shouldRepaint(covariant _DottedLinePainter old) => old.color != color; +} + +// ── Destination row ── +class _DestinationRow extends StatelessWidget { final TextEditingController controller; final FocusNode focusNode; + final bool isEditing; final AppLocalizations l10n; + final ThemeData theme; + final ColorScheme cs; + final TripzColors tripz; + final VoidCallback onFieldTapped; + final ValueChanged onChanged; + final VoidCallback? onVoiceSearchPressed; - const _SearchHeader({ + const _DestinationRow({ required this.controller, required this.focusNode, + required this.isEditing, required this.l10n, + required this.theme, + required this.cs, + required this.tripz, + required this.onFieldTapped, + required this.onChanged, + this.onVoiceSearchPressed, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.only(bottom: T.s4), + child: Row( + children: [ + SizedBox( + width: 48, + child: Center( + child: Container( + width: 16, + height: 16, + decoration: BoxDecoration( + color: cs.error, + borderRadius: BorderRadius.circular(3), + ), + child: Icon(Icons.stop_rounded, size: 12, color: cs.onError), + ), + ), + ), + Expanded( + child: GestureDetector( + onTap: onFieldTapped, + child: Container( + decoration: BoxDecoration( + color: cs.surface, + borderRadius: BorderRadius.circular(T.r12), + border: Border.all( + color: isEditing ? cs.primary : cs.outlineVariant, + width: isEditing ? 2 : 1, + ), + boxShadow: isEditing ? const [T.shadowSm] : null, + ), + child: Row( + children: [ + Expanded( + child: TextField( + controller: controller, + focusNode: focusNode, + readOnly: !isEditing, + decoration: InputDecoration( + hintText: l10n.whereTo, + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric( + horizontal: T.s16, + vertical: T.s12, + ), + hintStyle: theme.textTheme.bodyLarge?.copyWith( + color: cs.onSurfaceVariant.withValues(alpha: 0.7), + ), + ), + style: theme.textTheme.bodyLarge?.copyWith( + color: cs.onSurface, + ), + onChanged: onChanged, + ), + ), + if (onVoiceSearchPressed != null) + IconButton( + onPressed: onVoiceSearchPressed, + icon: Icon(Icons.mic_rounded, color: cs.primary), + tooltip: 'البحث الصوتي الذكي (AI Voice)', + ), + ], + ), + ), + ), + ), + ], + ), + ); + } +} + +// ── Saved place chip ── +class _SavedPlaceChip extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + + const _SavedPlaceChip({ + required this.icon, + required this.label, + required this.onTap, }); @override Widget build(BuildContext context) { final cs = Theme.of(context).colorScheme; - return Container( - color: cs.surface, - padding: const EdgeInsets.only( - left: T.pagePadding, - right: T.pagePadding, - top: T.s8, - bottom: T.s12, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Row( - children: [ - IconButton( - onPressed: () => Navigator.of(context).pop(), - icon: const Icon(Icons.arrow_back_rounded), + final theme = Theme.of(context); + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(T.r12), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: T.s12, vertical: T.s8), + decoration: BoxDecoration( + color: cs.surfaceContainerLow, + borderRadius: BorderRadius.circular(T.r12), + border: Border.all(color: cs.outlineVariant.withValues(alpha: 0.3)), + ), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 32, + height: 32, + decoration: BoxDecoration( + color: cs.surface, + shape: BoxShape.circle, + boxShadow: const [T.shadowSm], ), - Expanded( - child: TripzTextField( - controller: controller, - focusNode: focusNode, - hint: l10n.searchPlaces, - prefix: const Icon(Icons.search_rounded), - onChanged: (v) => context.read().onQueryChanged(v), - ), + child: Icon(icon, color: cs.primary, size: 20), + ), + const SizedBox(width: T.s8), + Text( + label, + style: theme.textTheme.titleSmall?.copyWith( + color: cs.onSurface, + fontSize: 16, ), - ], - ), - const SizedBox(height: T.s8), - Row( - children: [ - Column( + ), + ], + ), + ), + ); + } +} + +// ── Autocomplete search result tile ── +class _SearchResultTile extends StatelessWidget { + final PlaceModel place; + final VoidCallback onTap; + + const _SearchResultTile({required this.place, required this.onTap}); + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + final theme = Theme.of(context); + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(T.r8), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: T.s12, + horizontal: T.pagePadding, + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + shape: BoxShape.circle, + ), + child: Icon( + Icons.location_on_outlined, + color: cs.onSurfaceVariant, + size: 20, + ), + ), + const SizedBox(width: T.s12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: cs.primary, - shape: BoxShape.circle, - ), - ), - Container(width: 1.5, height: 24, color: cs.outlineVariant), - Container( - width: 10, - height: 10, - decoration: BoxDecoration( - color: cs.error, - shape: BoxShape.circle, + Text( + place.name, + style: theme.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w500, ), + maxLines: 1, + overflow: TextOverflow.ellipsis, ), - ], - ), - const SizedBox(width: T.s12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l10n.pickupLocation, - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: cs.onSurfaceVariant, - ), - ), + if (place.address != null && place.address!.isNotEmpty) ...[ const SizedBox(height: T.s4), Text( - l10n.pickupSet, - style: Theme.of(context).textTheme.bodyMedium, + place.address!, + style: theme.textTheme.bodySmall?.copyWith( + color: cs.onSurfaceVariant, + ), maxLines: 1, overflow: TextOverflow.ellipsis, - ), - const SizedBox(height: T.s16), - Text( - l10n.dropoffLocation, - style: Theme.of(context).textTheme.labelSmall?.copyWith( - color: cs.onSurfaceVariant, - ), ), ], - ), + ], + ), + ), + Icon(Icons.chevron_right_rounded, color: cs.onSurfaceVariant), + ], + ), + ), + ); + } +} + +// ── Pick from map tile ── +class _PickFromMapTile extends StatelessWidget { + final VoidCallback onTap; + + const _PickFromMapTile({required this.onTap}); + + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final cs = Theme.of(context).colorScheme; + final theme = Theme.of(context); + + return InkWell( + onTap: onTap, + borderRadius: BorderRadius.circular(T.r12), + child: Container( + padding: const EdgeInsets.symmetric( + horizontal: T.s12, + vertical: T.s12, + ), + decoration: BoxDecoration( + color: cs.surfaceContainerHigh, + borderRadius: BorderRadius.circular(T.r12), + ), + child: Row( + children: [ + Icon(Icons.map_rounded, color: cs.primary, size: T.iconMd), + const SizedBox(width: T.s12), + Text( + l10n.setLocationOnMap, + style: theme.textTheme.bodyLarge?.copyWith( + color: cs.onSurface, + fontWeight: FontWeight.w500, ), - ], - ), - ], + ), + ], + ), ), ); } diff --git a/apps/rider/lib/features/home/view/home_screen.dart b/apps/rider/lib/features/home/view/home_screen.dart index 1cae318..fa9685d 100644 --- a/apps/rider/lib/features/home/view/home_screen.dart +++ b/apps/rider/lib/features/home/view/home_screen.dart @@ -1,17 +1,44 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../core/l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; +import '../../../core/l10n/app_localizations.dart'; +import '../../../core/design/tokens.dart'; import '../../auth/cubit/auth_cubit.dart'; import '../../auth/cubit/auth_state.dart'; -import '../../../core/ui/tripz_button.dart'; -import '../../../core/design/tokens.dart'; import '../../trip/bloc/trip_bloc.dart'; import '../../trip/bloc/trip_state.dart'; import '../../trip/bloc/trip_event.dart'; import '../cubit/home_cubit.dart'; +import '../cubit/saved_places_cubit.dart'; import '../data/models/place_model.dart'; +import '../data/maps_repository.dart'; +import '../../../core/storage/saved_places_store.dart'; +import '../../../core/di.dart'; + +/// زر عائم دائري صغير — للقائمة وإعادة التوسيط فوق الخريطة. +class _MapCircleButton extends StatelessWidget { + const _MapCircleButton({required this.icon, required this.onPressed}); + + final IconData icon; + final VoidCallback onPressed; + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + return Container( + decoration: BoxDecoration( + color: cs.surface, + shape: BoxShape.circle, + boxShadow: const [T.shadowSm], + ), + child: IconButton( + icon: Icon(icon, color: cs.onSurface), + onPressed: onPressed, + ), + ); + } +} class HomeScreen extends StatefulWidget { const HomeScreen({super.key}); @@ -22,144 +49,333 @@ class HomeScreen extends StatefulWidget { class _HomeScreenState extends State { IntaleqMapController? _mapController; + bool _isSelectingOnMap = false; @override void initState() { super.initState(); - // فحص رحلة نشطة عند فتح الشاشة. WidgetsBinding.instance.addPostFrameCallback((_) { context.read().add(const TripCheckActive()); + context.read().load(); }); } + String _timeBasedGreeting(String? name, AppLocalizations l10n) { + final hour = DateTime.now().hour; + final isMorning = hour >= 5 && hour < 12; + final isAr = Localizations.localeOf(context).languageCode == 'ar'; + final prefix = isMorning + ? (isAr ? 'صباح الخير' : 'Good morning') + : (isAr ? 'مساء الخير' : 'Good evening'); + + final firstName = (name != null && name.trim().isNotEmpty) + ? name.trim().split(' ').first + : ''; + return firstName.isNotEmpty ? '$prefix، $firstName' : prefix; + } + + String _whereToPrompt(String? name, AppLocalizations l10n) { + final firstName = (name != null && name.trim().isNotEmpty) + ? name.trim().split(' ').first + : ''; + if (firstName.isNotEmpty) { + final isAr = Localizations.localeOf(context).languageCode == 'ar'; + return isAr ? 'إلى أين يا $firstName؟' : 'Where to, $firstName?'; + } + return l10n.whereTo; + } + @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final cs = Theme.of(context).colorScheme; + final isDark = Theme.of(context).brightness == Brightness.dark; return BlocListener( listenWhen: (prev, cur) => prev.trip?.status != cur.trip?.status, listener: (context, state) { if (state.trip != null && !state.isCompleted) { - // رحلة نشطة — انتقل للتتبع. context.push('/trip-tracking'); } }, child: Scaffold( + backgroundColor: cs.surface, + // الخريطة تملأ الشاشة حتى الحافة العلوية (docs/29 §3.6) — كل العناصر + // العائمة فوقها تعيش داخل SafeArea الخاصة بها لا الشاشة كلها. + extendBodyBehindAppBar: true, body: Stack( children: [ - // الخريطة - BlocBuilder( - builder: (context, state) { - final center = state.currentLocation ?? const LatLng(31.9539, 35.9106); - return IntaleqMap( - apiKey: '', - initialCameraPosition: CameraPosition(target: center, zoom: 15), - myLocationEnabled: true, - zoomControlsEnabled: false, - compassEnabled: false, - onMapCreated: (controller) => _mapController = controller, - markers: _buildMarkers(state), - onCameraMove: (position) { - context.read().updateCameraTarget(position.target); - }, - ); - }, - ), - - // زر إعادة التوسيط + // ── الخريطة (75% من الشاشة) ── Positioned( - right: T.pagePadding, - bottom: 180, - child: FloatingActionButton.small( - heroTag: 'recenter', - backgroundColor: cs.surface, - onPressed: () { - final loc = context.read().state.currentLocation; - if (loc != null) { - _mapController?.animateCamera(CameraUpdate.newLatLngZoom(loc, 15)); + top: 0, + left: 0, + right: 0, + height: MediaQuery.of(context).size.height * 0.75, + child: BlocConsumer( + listenWhen: (prev, cur) => + prev.currentLocation == null && cur.currentLocation != null, + buildWhen: (prev, cur) => + prev.currentLocation != cur.currentLocation || + prev.pickup != cur.pickup || + prev.destination != cur.destination, + listener: (context, state) { + if (state.currentLocation != null && _mapController != null) { + _mapController!.animateCamera( + CameraUpdate.newLatLngZoom(state.currentLocation!, 16), + ); } }, - child: Icon(Icons.my_location_rounded, color: cs.onSurface), + builder: (context, state) { + final center = + state.currentLocation ?? const LatLng(31.9539, 35.9106); + return IntaleqMap( + apiKey: '', + initialCameraPosition: + CameraPosition(target: center, zoom: 15), + mapType: + isDark ? IntaleqMapType.normal : IntaleqMapType.light, + myLocationEnabled: true, + myLocationButtonEnabled: false, + zoomControlsEnabled: false, + compassEnabled: false, + onMapCreated: (controller) { + _mapController = controller; + if (state.currentLocation != null) { + Future.delayed(const Duration(milliseconds: 300), () { + controller.animateCamera( + CameraUpdate.newLatLngZoom( + state.currentLocation!, 16), + ); + }); + } + }, + markers: _buildMarkers(state), + onCameraMove: (position) { + context + .read() + .updateCameraTarget(position.target); + }, + ); + }, ), ), - // الشريط السفلي - Positioned( - left: T.pagePadding, - right: T.pagePadding, - bottom: T.pagePadding, - child: Card( - elevation: 2, - child: Padding( - padding: const EdgeInsets.all(T.pagePadding), - child: BlocBuilder( - builder: (context, state) { - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - BlocBuilder( - builder: (context, authState) { - final name = authState.user?.name ?? ''; - if (name.isNotEmpty) { - return Padding( - padding: const EdgeInsets.only(bottom: T.s12), - child: Text( - l10n.welcome(name), - style: Theme.of(context).textTheme.titleMedium, - ), - ); - } - return const SizedBox.shrink(); - }, - ), - if (state.destination != null) - _DestinationPreview( - destination: state.destination!, + // زر القائمة أعلى-بداية + PositionedDirectional( + top: MediaQuery.of(context).padding.top + T.s8, + start: T.pagePadding, + child: _MapCircleButton( + icon: Icons.menu_rounded, + onPressed: () => _showLogoutDialog(context, l10n), + ), + ), + + // زر إعادة التوسيط عائم أعلى البطاقة السفلية + PositionedDirectional( + end: T.pagePadding, + bottom: (_isSelectingOnMap ? 100 : MediaQuery.of(context).size.height * 0.28) + T.s16, + child: BlocBuilder( + builder: (context, state) { + return _MapCircleButton( + icon: Icons.my_location_rounded, + onPressed: state.currentLocation == null + ? () {} + : () => _mapController?.animateCamera( + CameraUpdate.newLatLngZoom( + state.currentLocation!, 16), + ), + ); + }, + ), + ), + + if (_isSelectingOnMap) + // علامة المركز (دبوس) لاختيار الموقع + Positioned( + top: (MediaQuery.of(context).size.height * 0.75) / 2 - 20, + left: MediaQuery.of(context).size.width / 2 - 20, + child: const Icon(Icons.location_on, size: 40, color: Colors.black), + ), + + // ── البطاقة السفلية: «إلى أين؟» والأماكن المحفوظة ── + if (!_isSelectingOnMap) + Positioned( + left: 0, + right: 0, + bottom: 0, + height: MediaQuery.of(context).size.height * 0.30, + child: Container( + width: double.infinity, + padding: const EdgeInsets.fromLTRB( + T.pagePadding, T.s24, T.pagePadding, T.s16), + decoration: BoxDecoration( + color: cs.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(T.r24), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 20, + offset: const Offset(0, -5), + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // التحية الزمنية مع اسم الراكب بتصميم أنيق + BlocBuilder( + builder: (context, authState) { + final greeting = _timeBasedGreeting(authState.user?.name, l10n); + return Text( + greeting, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + fontWeight: FontWeight.w800, + color: cs.onSurface, + letterSpacing: -0.5, + ), + ); + }, + ), + + const SizedBox(height: T.s24), + + // حقل «إلى أين؟» + BlocBuilder( + builder: (context, authState) { + final searchPrompt = _whereToPrompt(authState.user?.name, l10n); + return Material( + color: cs.surfaceContainerLowest, + elevation: 2, + shadowColor: cs.shadow.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(T.r16), + child: InkWell( + borderRadius: BorderRadius.circular(T.r16), onTap: () => _openDestinationSearch(context), - onClear: () => context.read().clearTripData(), - ) - else - TripzButton( - label: l10n.whereTo, - style: TripzButtonStyle.secondary, - icon: Icons.search_rounded, - onPressed: () => _openDestinationSearch(context), + child: Container( + height: 60, + padding: const EdgeInsets.symmetric(horizontal: T.s16), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(T.r16), + border: Border.all( + color: cs.outlineVariant.withValues(alpha: 0.5), + width: 1, + ), + ), + child: Row( + children: [ + Container( + width: 40, + height: 40, + decoration: BoxDecoration( + color: cs.primaryContainer.withValues(alpha: 0.5), + shape: BoxShape.circle, + ), + child: Icon( + Icons.search_rounded, + color: cs.primary, + size: 22, + ), + ), + const SizedBox(width: T.s16), + Expanded( + child: Text( + searchPrompt, + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith( + color: cs.onSurface, + fontWeight: FontWeight.w700, + fontSize: 18, + ), + ), + ), + ], + ), + ), ), - if (state.destination != null) ...[ - const SizedBox(height: T.s12), - TripzButton( - label: l10n.requestRide, - onPressed: () => _openRoutePreview(context, state), - ), - ], - ], - ); - }, + ); + }, + ), + + const Spacer(), + Divider(color: cs.outlineVariant.withValues(alpha: 0.3), height: 1), + const SizedBox(height: T.s12), + + // الأماكن المحفوظة (سطر أفقي أنيق) + BlocBuilder( + builder: (context, savedState) { + final items = savedState.places.take(3).toList(); + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildQuickAction(context, Icons.home_rounded, l10n.homePlace, items.isNotEmpty ? items.first : null), + Container(width: 1, height: 32, color: cs.outlineVariant.withValues(alpha: 0.3)), + _buildQuickAction(context, Icons.work_rounded, l10n.workPlace, items.length > 1 ? items[1] : null), + Container(width: 1, height: 32, color: cs.outlineVariant.withValues(alpha: 0.3)), + _buildQuickAction(context, Icons.star_rounded, l10n.favorites, items.length > 2 ? items[2] : null), + ], + ); + }, + ), + const SizedBox(height: T.s8), + ], + ), + ), + ) + else + // زر تأكيد الوجهة في وضع اختيار الخريطة + Positioned( + left: 0, + right: 0, + bottom: 0, + child: Container( + padding: const EdgeInsets.fromLTRB( + T.pagePadding, T.s16, T.pagePadding, T.s32), + decoration: BoxDecoration( + color: cs.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(T.r24), + ), + boxShadow: const [T.shadowLg], + ), + child: FilledButton( + onPressed: _confirmMapDestination, + style: FilledButton.styleFrom( + padding: const EdgeInsets.symmetric(vertical: 16), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(T.r12), + ), + ), + child: Text('تأكيد الوجهة', style: Theme.of(context).textTheme.titleMedium?.copyWith(color: cs.onPrimary)), ), ), ), - ), - - // حالة التحميل - BlocBuilder( - builder: (context, state) { - if (state.loading) { - return Container( - color: Colors.black26, - child: const Center(child: CircularProgressIndicator()), - ); - } - return const SizedBox.shrink(); - }, - ), ], ), ), ); } + void _onPlaceTap(BuildContext context, SavedPlace place) { + context.read().setDestination(PlaceModel( + name: place.name, + address: place.address, + lat: place.lat, + lng: place.lng, + )); + final state = context.read().state; + if (state.pickup != null) { + _openRoutePreview(context, state); + } + } + Set _buildMarkers(HomeState state) { final markers = {}; if (state.pickup != null) { @@ -184,14 +400,65 @@ class _HomeScreenState extends State { } void _openDestinationSearch(BuildContext context) { - context.push('/destination').then((place) { - if (place != null && mounted) { - context.read().setDestination(place); - _openRoutePreview(context, context.read().state); + final homeCubit = context.read(); + final pickup = homeCubit.state.pickup; + final currentLocation = homeCubit.state.currentLocation; + + context.push('/destination', extra: { + 'currentLocation': currentLocation, + }).then((result) { + if (!mounted) return; + if (result is Map && result['pickFromMap'] == true) { + setState(() { + _isSelectingOnMap = true; + }); + return; + } + + final place = result as PlaceModel?; + if (place != null && pickup != null) { + homeCubit.setDestination(place); + // ignore: use_build_context_synchronously + context.push('/route-preview', extra: { + 'pickup': pickup, + 'destination': place, + }); } }); } + Future _confirmMapDestination() async { + final homeCubit = context.read(); + final target = homeCubit.state.cameraTarget; + if (target == null) return; + + // We reverse geocode the target to get the address name + final repo = getIt(); + try { + final place = await repo.reverseGeocode(target.latitude, target.longitude); + setState(() { + _isSelectingOnMap = false; + }); + homeCubit.setDestination(place); + if (homeCubit.state.pickup != null) { + _openRoutePreview(context, homeCubit.state); + } + } catch (_) { + // In case of error, still set it with empty name + setState(() { + _isSelectingOnMap = false; + }); + homeCubit.setDestination(PlaceModel( + name: 'موقع محدد على الخريطة', + lat: target.latitude, + lng: target.longitude, + )); + if (homeCubit.state.pickup != null) { + _openRoutePreview(context, homeCubit.state); + } + } + } + void _openRoutePreview(BuildContext context, HomeState state) { if (state.pickup == null || state.destination == null) return; context.push('/route-preview', extra: { @@ -199,59 +466,59 @@ class _HomeScreenState extends State { 'destination': state.destination, }); } -} -class _DestinationPreview extends StatelessWidget { - final PlaceModel destination; - final VoidCallback onTap; - final VoidCallback onClear; + void _showLogoutDialog(BuildContext context, AppLocalizations l10n) { + final cs = Theme.of(context).colorScheme; + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: Text(l10n.logout), + content: Text(l10n.logout), + actions: [ + TextButton( + onPressed: () => Navigator.pop(ctx), + child: Text(l10n.cancel), + ), + FilledButton( + onPressed: () async { + Navigator.pop(ctx); + await context.read().logout(); + if (context.mounted) context.go('/phone'); + }, + style: FilledButton.styleFrom(backgroundColor: cs.error), + child: Text(l10n.logout), + ), + ], + ), + ); + } - const _DestinationPreview({ - required this.destination, - required this.onTap, - required this.onClear, - }); - - @override - Widget build(BuildContext context) { + Widget _buildQuickAction( + BuildContext context, IconData icon, String label, SavedPlace? place) { final cs = Theme.of(context).colorScheme; return InkWell( - onTap: onTap, + onTap: place != null ? () => _onPlaceTap(context, place) : null, borderRadius: BorderRadius.circular(T.r12), - child: Container( - padding: const EdgeInsets.all(T.s12), - decoration: BoxDecoration( - color: cs.surfaceContainerHighest, - borderRadius: BorderRadius.circular(T.r12), - ), - child: Row( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: T.s12, vertical: T.s8), + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - Icon(Icons.location_on_outlined, color: cs.error, size: 20), - const SizedBox(width: T.s8), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - destination.name, - style: Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w500), - maxLines: 1, - overflow: TextOverflow.ellipsis, + Icon(icon, + color: place != null + ? cs.primary + : cs.onSurfaceVariant.withValues(alpha: 0.5), + size: 28), + const SizedBox(height: T.s4), + Text( + label, + style: Theme.of(context).textTheme.labelMedium?.copyWith( + color: place != null + ? cs.onSurface + : cs.onSurfaceVariant.withValues(alpha: 0.5), + fontWeight: + place != null ? FontWeight.w600 : FontWeight.normal, ), - if (destination.address != null) - Text( - destination.address!, - style: Theme.of(context).textTheme.bodySmall?.copyWith(color: cs.onSurfaceVariant), - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - ], - ), - ), - IconButton( - onPressed: onClear, - icon: Icon(Icons.close_rounded, color: cs.onSurfaceVariant, size: 20), - visualDensity: VisualDensity.compact, ), ], ), diff --git a/apps/rider/lib/features/home/view/route_preview_screen.dart b/apps/rider/lib/features/home/view/route_preview_screen.dart index c9dfe2c..caac1b0 100644 --- a/apps/rider/lib/features/home/view/route_preview_screen.dart +++ b/apps/rider/lib/features/home/view/route_preview_screen.dart @@ -1,8 +1,9 @@ import 'package:flutter/material.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../core/l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; +import '../../../core/l10n/app_localizations.dart'; import '../../../core/design/tokens.dart'; import '../../../core/ui/tripz_button.dart'; import '../../trip/bloc/trip_bloc.dart'; @@ -10,9 +11,15 @@ import '../../trip/bloc/trip_state.dart'; import '../../trip/bloc/trip_event.dart'; import '../cubit/quote_cubit.dart'; import '../data/models/place_model.dart'; +import '../data/models/route_model.dart'; +import '../data/maps_repository.dart'; +import '../../../core/di.dart'; import 'widgets/ride_class_card.dart'; -/// شاشة معاينة المسار + اختيار فئة الرحلة + تأكيد الطلب. +List _decodePolyline(String encoded) { + return PolylineUtils.decode(encoded); +} + class RoutePreviewScreen extends StatefulWidget { final PlaceModel pickup; final PlaceModel destination; @@ -29,10 +36,16 @@ class RoutePreviewScreen extends StatefulWidget { class _RoutePreviewScreenState extends State { IntaleqMapController? _mapController; + RouteModel? _route; + List _polylinePoints = []; + bool _routeLoading = true; + String? _routeError; + final _promoController = TextEditingController(); @override void initState() { super.initState(); + _fetchRoute(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().fetchQuote( pickupLat: widget.pickup.lat, @@ -45,11 +58,59 @@ class _RoutePreviewScreenState extends State { }); } + @override + void dispose() { + _promoController.dispose(); + super.dispose(); + } + + Future _fetchRoute() async { + setState(() { + _routeLoading = true; + _routeError = null; + }); + try { + final repo = getIt(); + final route = await repo.getRoute( + fromLat: widget.pickup.lat, + fromLng: widget.pickup.lng, + toLat: widget.destination.lat, + toLng: widget.destination.lng, + ); + final points = await compute(_decodePolyline, route.polyline ?? ''); + if (!mounted) return; + setState(() { + _route = route; + _polylinePoints = points; + _routeLoading = false; + }); + _fitCamera(); + } catch (e) { + if (!mounted) return; + setState(() { + _routeError = e.toString(); + _routeLoading = false; + }); + } + } + + void _fitCamera() { + if (_mapController == null) return; + final bounds = _route?.getLatLngBounds(); + if (bounds != null) { + _mapController!.moveCamera(CameraUpdate.newLatLngBounds(bounds)); + } else { + _mapController!.moveCamera(CameraUpdate.newLatLngZoom( + LatLng(widget.pickup.lat, widget.pickup.lng), + 15, + )); + } + } + @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; final cs = Theme.of(context).colorScheme; - final pickupLoc = LatLng(widget.pickup.lat, widget.pickup.lng); final destLoc = LatLng(widget.destination.lat, widget.destination.lng); @@ -58,185 +119,295 @@ class _RoutePreviewScreenState extends State { prev.requestStatus != cur.requestStatus && cur.requestStatus == TripRequestStatus.searching, listener: (context, state) { - // تم إنشاء الرحلة — انتقل للتتبع. context.pushReplacement('/trip-tracking'); }, child: Scaffold( - body: Column( + body: Stack( children: [ - // الخريطة مع المسار - Expanded( - flex: 2, - child: Stack( - children: [ - IntaleqMap( - apiKey: '', - initialCameraPosition: CameraPosition( - target: LatLng( - (pickupLoc.latitude + destLoc.latitude) / 2, - (pickupLoc.longitude + destLoc.longitude) / 2, - ), - zoom: 14, + // الخريطة بكامل الشاشة + if (_routeLoading) + const Center(child: CircularProgressIndicator()) + else if (_routeError != null) + Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l10n.errorGeneric), + const SizedBox(height: T.s12), + TripzButton( + label: l10n.retry, + onPressed: _fetchRoute, ), - myLocationEnabled: false, - zoomControlsEnabled: false, - compassEnabled: false, - onMapCreated: (c) => _mapController = c, - markers: { - Marker( - markerId: const MarkerId('pickup'), - position: pickupLoc, - icon: InlqBitmap.defaultMarkerWithHue(210), - anchor: const Offset(0.5, 1.0), - ), - Marker( - markerId: const MarkerId('destination'), - position: destLoc, - icon: InlqBitmap.defaultMarkerWithHue(0), - anchor: const Offset(0.5, 1.0), - ), - }, - polylines: { - Polyline( - polylineId: const PolylineId('route'), - points: [pickupLoc, destLoc], - color: cs.primary, - width: 4, - ), - }, + ], + ), + ) + else + IntaleqMap( + apiKey: '', + initialCameraPosition: CameraPosition( + target: LatLng( + (pickupLoc.latitude + destLoc.latitude) / 2, + (pickupLoc.longitude + destLoc.longitude) / 2, ), - Positioned( - top: MediaQuery.of(context).padding.top + T.s8, - left: T.s12, - child: FloatingActionButton.small( - heroTag: 'back', - backgroundColor: cs.surface, - onPressed: () => context.pop(), - child: const Icon(Icons.arrow_back_rounded), + zoom: 14, + ), + mapType: Theme.of(context).brightness == Brightness.dark + ? IntaleqMapType.normal + : IntaleqMapType.light, + myLocationEnabled: false, + zoomControlsEnabled: false, + compassEnabled: false, + onMapCreated: (c) { + _mapController = c; + _fitCamera(); + }, + markers: { + Marker( + markerId: const MarkerId('pickup'), + position: pickupLoc, + icon: InlqBitmap.defaultMarkerWithHue(210), + anchor: const Offset(0.5, 1.0), + ), + Marker( + markerId: const MarkerId('destination'), + position: destLoc, + icon: InlqBitmap.defaultMarkerWithHue(0), + anchor: const Offset(0.5, 1.0), + ), + }, + polylines: { + // حماية محرك C++ من الانهيار إذا كانت النقاط أقل من 2 + if (_polylinePoints.length >= 2) + Polyline( + polylineId: const PolylineId('route'), + points: _polylinePoints, + color: cs.primary, + width: T.polylineWidth, ), - ), - Positioned( - top: MediaQuery.of(context).padding.top + T.s8, - right: T.s12, - child: BlocBuilder( - builder: (context, state) { - if (state.quote == null) return const SizedBox.shrink(); - return Container( - padding: const EdgeInsets.symmetric(horizontal: T.s12, vertical: T.s8), - decoration: BoxDecoration( - color: cs.surface, - borderRadius: BorderRadius.circular(T.r12), - boxShadow: [T.shadowSm], - ), - child: Text( - l10n.routeSummary( - state.quote!.distanceText ?? '', - state.quote!.durationText ?? '', - ), - style: Theme.of(context).textTheme.labelLarge, - ), - ); - }, - ), - ), - ], + }, + ), + + // زر الرجوع العائم (Top Left) + Positioned( + top: MediaQuery.of(context).padding.top + T.s8, + left: T.s12, + child: FloatingActionButton.small( + heroTag: 'back', + backgroundColor: cs.surface.withValues(alpha: 0.9), + elevation: 4, + onPressed: () => context.pop(), + child: Icon(Icons.arrow_back_rounded, color: cs.onSurface), ), ), - // قائمة فئات الرحلة - Expanded( - flex: 3, + // ملخص المسار العائم (Top Right) + Positioned( + top: MediaQuery.of(context).padding.top + T.s12, + right: T.s12, + child: BlocBuilder( + builder: (context, state) { + if (state.quote == null) return const SizedBox.shrink(); + return Container( + padding: const EdgeInsets.symmetric( + horizontal: T.s16, vertical: T.s12), + decoration: BoxDecoration( + color: cs.surface.withValues(alpha: 0.95), + borderRadius: BorderRadius.circular(T.r24), + boxShadow: const [T.shadowSm], + ), + child: Text( + l10n.routeSummary( + state.quote!.distanceText ?? '', + state.quote!.durationText ?? '', + ), + style: Theme.of(context).textTheme.labelLarge?.copyWith( + fontWeight: FontWeight.w700, + color: cs.onSurface, + ), + ), + ); + }, + ), + ), + + // Bottom Sheet: فئات الرحلات + Align( + alignment: Alignment.bottomCenter, child: Container( - color: cs.surface, - child: BlocBuilder( - builder: (context, state) { - if (state.loading) { - return const Center(child: CircularProgressIndicator()); - } - if (state.error != null) { - return Center( - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - Text(l10n.errorGeneric), - const SizedBox(height: T.s12), - TripzButton( - label: l10n.retry, - onPressed: () => context.read().fetchQuote( - pickupLat: widget.pickup.lat, - pickupLng: widget.pickup.lng, - pickupAddress: widget.pickup.name, - dropoffLat: widget.destination.lat, - dropoffLng: widget.destination.lng, - dropoffAddress: widget.destination.name, + constraints: BoxConstraints( + maxHeight: MediaQuery.of(context).size.height * 0.55, + ), + decoration: BoxDecoration( + color: cs.surface, + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.1), + blurRadius: 20, + offset: const Offset(0, -5), + ) + ], + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // شريط السحب (Grab handle) + const SizedBox(height: T.s12), + Container( + width: 40, + height: 5, + decoration: BoxDecoration( + color: cs.outlineVariant.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(10), + ), + ), + const SizedBox(height: T.s12), + + Expanded( + child: BlocBuilder( + builder: (context, state) { + if (state.loading) { + return const Center( + child: CircularProgressIndicator()); + } + if (state.error != null) { + return Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text(l10n.errorGeneric, + style: Theme.of(context) + .textTheme + .bodyLarge + ?.copyWith(color: cs.error)), + const SizedBox(height: T.s12), + TripzButton( + label: l10n.retry, + onPressed: () => context + .read() + .fetchQuote( + pickupLat: widget.pickup.lat, + pickupLng: widget.pickup.lng, + pickupAddress: widget.pickup.name, + dropoffLat: widget.destination.lat, + dropoffLng: widget.destination.lng, + dropoffAddress: + widget.destination.name, + ), ), - ), - ], - ), - ); - } + ], + ), + ); + } - final classes = state.quote?.classes ?? []; - if (classes.isEmpty) { - return Center(child: Text(l10n.noResults)); - } + final classes = state.quote?.classes ?? []; + if (classes.isEmpty) { + return Center(child: Text(l10n.noResults)); + } - return Column( - children: [ - Padding( - padding: const EdgeInsets.fromLTRB(T.pagePadding, T.s16, T.pagePadding, T.s8), - child: Align( - alignment: AlignmentDirectional.centerStart, - child: Text(l10n.rideClasses, style: Theme.of(context).textTheme.titleMedium), - ), - ), - Expanded( - child: ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: T.pagePadding), - itemCount: classes.length, - separatorBuilder: (_, __) => const SizedBox(height: T.s8), - itemBuilder: (context, index) { - final cls = classes[index]; - return RideClassCard( - quoteClass: cls, - selected: state.selectedClass == cls.vehicleClass, - onTap: () => context.read().selectClass(cls.vehicleClass), - ); - }, - ), - ), - SafeArea( - child: Padding( - padding: const EdgeInsets.all(T.pagePadding), - child: Column( - children: [ - _PaymentRow( - method: state.paymentMethod, - onChanged: (m) => context.read().setPaymentMethod(m), - ), - const SizedBox(height: T.s12), - // زر طلب الرحلة — يستخدم TripBloc - BlocBuilder( - builder: (context, tripState) { - final isLoading = tripState.requestStatus == TripRequestStatus.loading; - return SizedBox( - width: double.infinity, - child: TripzButton( - label: isLoading ? l10n.loading : l10n.requestRide, - onPressed: (state.selectedClass != null && !isLoading) - ? () => _requestTrip(context, state) - : null, + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: T.pagePadding), + child: Text( + l10n.rideClasses, + style: Theme.of(context) + .textTheme + .titleLarge + ?.copyWith( + fontWeight: FontWeight.w800, + letterSpacing: -0.5, ), + ), + ), + const SizedBox(height: T.s12), + Expanded( + child: ListView.separated( + padding: const EdgeInsets.symmetric( + horizontal: T.pagePadding), + itemCount: classes.length, + separatorBuilder: (_, __) => + const SizedBox(height: T.s12), + itemBuilder: (context, index) { + final cls = classes[index]; + return RideClassCard( + quoteClass: cls, + selected: state.selectedClass == + cls.vehicleClass, + onTap: () => context + .read() + .selectClass(cls.vehicleClass), ); }, ), - ], - ), - ), - ), - ], - ); - }, + ), + // شريط الدفع وكود الخصم + Container( + padding: const EdgeInsets.fromLTRB( + T.pagePadding, T.s12, T.pagePadding, T.s8), + decoration: BoxDecoration( + color: cs.surface, + border: Border( + top: BorderSide( + color: cs.outlineVariant + .withValues(alpha: 0.3), + ), + ), + ), + child: Column( + children: [ + _PaymentRow( + method: state.paymentMethod, + onChanged: (m) => context + .read() + .setPaymentMethod(m), + ), + const SizedBox(height: T.s12), + _PromoRow( + controller: _promoController, + onApply: (code) => context + .read() + .setCouponCode(code), + ), + ], + ), + ), + // زر طلب الرحلة + SafeArea( + top: false, + child: Padding( + padding: const EdgeInsets.fromLTRB( + T.pagePadding, 0, T.pagePadding, T.s12), + child: BlocBuilder( + builder: (context, tripState) { + final isLoading = + tripState.requestStatus == + TripRequestStatus.loading; + return TripzButton( + label: isLoading + ? l10n.loading + : l10n.requestRide, + onPressed: + (state.selectedClass != null && + !isLoading) + ? () => + _requestTrip(context, state) + : null, + ); + }, + ), + ), + ), + ], + ); + }, + ), + ), + ], ), ), ), @@ -274,7 +445,8 @@ class _PaymentRow extends StatelessWidget { final cs = Theme.of(context).colorScheme; return Container( - padding: const EdgeInsets.symmetric(horizontal: T.pagePadding, vertical: T.s12), + padding: const EdgeInsets.symmetric( + horizontal: T.pagePadding, vertical: T.s12), decoration: BoxDecoration( color: cs.surfaceContainerHighest, borderRadius: BorderRadius.circular(T.r12), @@ -282,11 +454,14 @@ class _PaymentRow extends StatelessWidget { child: Row( children: [ Icon( - method == 'wallet' ? Icons.account_balance_wallet_outlined : Icons.money_rounded, + method == 'wallet' + ? Icons.account_balance_wallet_outlined + : Icons.money_rounded, color: cs.onSurfaceVariant, ), const SizedBox(width: T.s12), - Text(l10n.paymentMethod, style: Theme.of(context).textTheme.bodyMedium), + Text(l10n.paymentMethod, + style: Theme.of(context).textTheme.bodyMedium), const Spacer(), DropdownButton( value: method, @@ -294,7 +469,8 @@ class _PaymentRow extends StatelessWidget { isDense: true, items: [ DropdownMenuItem(value: 'cash', child: Text(l10n.cash)), - DropdownMenuItem(value: 'wallet', child: Text(l10n.walletPayment)), + DropdownMenuItem( + value: 'wallet', child: Text(l10n.walletPayment)), ], onChanged: (v) { if (v != null) onChanged(v); @@ -305,3 +481,60 @@ class _PaymentRow extends StatelessWidget { ); } } + +class _PromoRow extends StatefulWidget { + final TextEditingController controller; + final ValueChanged onApply; + + const _PromoRow({required this.controller, required this.onApply}); + + @override + State<_PromoRow> createState() => _PromoRowState(); +} + +class _PromoRowState extends State<_PromoRow> { + @override + Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; + final cs = Theme.of(context).colorScheme; + + return Container( + padding: + const EdgeInsets.symmetric(horizontal: T.pagePadding, vertical: T.s8), + decoration: BoxDecoration( + color: cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(T.r12), + ), + child: Row( + children: [ + Icon( + Icons.local_offer_outlined, + color: cs.onSurfaceVariant, + ), + const SizedBox(width: T.s12), + Expanded( + child: TextField( + controller: widget.controller, + decoration: InputDecoration( + hintText: l10n.couponCode, + border: InputBorder.none, + isDense: true, + contentPadding: EdgeInsets.zero, + ), + style: Theme.of(context).textTheme.bodyMedium, + ), + ), + TripzButton( + label: l10n.confirm, + style: TripzButtonStyle.text, + fullWidth: false, + onPressed: () { + final code = widget.controller.text.trim(); + widget.onApply(code.isEmpty ? null : code); + }, + ), + ], + ), + ); + } +} diff --git a/apps/rider/lib/features/home/view/widgets/ride_class_card.dart b/apps/rider/lib/features/home/view/widgets/ride_class_card.dart index faf6ffb..8ff062c 100644 --- a/apps/rider/lib/features/home/view/widgets/ride_class_card.dart +++ b/apps/rider/lib/features/home/view/widgets/ride_class_card.dart @@ -23,52 +23,64 @@ class RideClassCard extends StatelessWidget { return InkWell( onTap: onTap, - borderRadius: BorderRadius.circular(T.r12), + borderRadius: BorderRadius.circular(T.r16), child: Container( - padding: const EdgeInsets.all(T.pagePadding), + padding: const EdgeInsets.symmetric(horizontal: T.s16, vertical: T.s12), decoration: BoxDecoration( - color: selected ? cs.primaryContainer : cs.surfaceContainerHighest, - borderRadius: BorderRadius.circular(T.r12), + color: selected ? cs.primaryContainer.withValues(alpha: 0.2) : Colors.transparent, + borderRadius: BorderRadius.circular(T.r16), border: Border.all( - color: selected ? cs.primary : Colors.transparent, - width: 2, + color: selected ? cs.primary : cs.outlineVariant.withValues(alpha: 0.3), + width: selected ? 2 : 1, ), ), child: Row( children: [ // أيقونة المركبة Container( - width: 48, - height: 48, + width: 56, + height: 56, decoration: BoxDecoration( - color: selected ? cs.primary : cs.surface, + color: selected ? cs.primary.withValues(alpha: 0.1) : cs.surfaceContainerHighest, shape: BoxShape.circle, ), - child: Icon( - Icons.directions_car_filled_rounded, - color: selected ? cs.onPrimary : cs.onSurfaceVariant, - size: 24, + child: Center( + child: Icon( + Icons.directions_car_filled_rounded, + color: selected ? cs.primary : cs.onSurfaceVariant, + size: 32, + ), ), ), - const SizedBox(width: T.s12), + const SizedBox(width: T.s16), // اسم الفئة + السعة Expanded( child: Column( crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( quoteClass.name, - style: Theme.of(context).textTheme.bodyLarge?.copyWith( - fontWeight: FontWeight.w600, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w700, + color: cs.onSurface, ), ), const SizedBox(height: 2), - Text( - l10n.capacity(quoteClass.capacity), - style: Theme.of(context).textTheme.bodySmall?.copyWith( - color: cs.onSurfaceVariant, - ), + Row( + children: [ + Icon(Icons.person_outline_rounded, + size: 14, color: cs.onSurfaceVariant), + const SizedBox(width: 4), + Text( + l10n.capacity(quoteClass.capacity), + style: Theme.of(context).textTheme.bodySmall?.copyWith( + color: cs.onSurfaceVariant, + fontWeight: FontWeight.w500, + ), + ), + ], ), ], ), @@ -77,19 +89,21 @@ class RideClassCard extends StatelessWidget { // الوقت التقديري + السعر Column( crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.center, children: [ Text( - quoteClass.estimatedTimeText, - style: Theme.of(context).textTheme.labelMedium?.copyWith( - color: cs.onSurfaceVariant, + '${quoteClass.price} ${quoteClass.currency}', + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w800, + color: selected ? cs.primary : cs.onSurface, ), ), const SizedBox(height: 2), Text( - '${quoteClass.price} ${quoteClass.currency}', - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w700, - color: selected ? cs.primary : cs.onSurface, + quoteClass.estimatedTimeText, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: cs.onSurfaceVariant, + fontWeight: FontWeight.w600, ), ), ], diff --git a/apps/rider/lib/features/onboarding/view/onboarding_screen.dart b/apps/rider/lib/features/onboarding/view/onboarding_screen.dart new file mode 100644 index 0000000..d19584d --- /dev/null +++ b/apps/rider/lib/features/onboarding/view/onboarding_screen.dart @@ -0,0 +1,231 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../../core/design/tokens.dart'; +import '../../../core/l10n/app_localizations.dart'; +import '../../../core/ui/tripz_scaffold.dart'; +import '../../../core/ui/tripz_button.dart'; + +class OnboardingScreen extends StatefulWidget { + const OnboardingScreen({super.key}); + + @override + State createState() => _OnboardingScreenState(); +} + +class _OnboardingScreenState extends State + with SingleTickerProviderStateMixin { + final PageController _pageController = PageController(); + int _currentIndex = 0; + + late final AnimationController _pulseController; + late final Animation _pulseAnimation; + + static const _onboardingKey = 'onboarding_seen'; + + @override + void initState() { + super.initState(); + _pulseController = AnimationController( + vsync: this, + duration: const Duration(milliseconds: 1400), + )..repeat(reverse: true); + _pulseAnimation = Tween(begin: 1.0, end: 1.08).animate( + CurvedAnimation(parent: _pulseController, curve: Curves.easeInOut), + ); + } + + @override + void dispose() { + _pulseController.dispose(); + _pageController.dispose(); + super.dispose(); + } + + Future _markSeen() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_onboardingKey, true); + } + + void _nextPage(int pageCount) { + if (_currentIndex < pageCount - 1) { + _pageController.nextPage( + duration: T.slow, + curve: Curves.easeInOut, + ); + } else { + _markSeen(); + context.go('/phone'); + } + } + + void _skip() { + _markSeen(); + context.go('/phone'); + } + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final cs = theme.colorScheme; + final l10n = AppLocalizations.of(context)!; + + final pages = [ + _OnboardingPageData( + title: l10n.onboardingTitle1, + subtitle: l10n.onboardingSubtitle1, + image: 'assets/images/onboarding_1.png', + fallbackIcon: Icons.touch_app_rounded, + ), + _OnboardingPageData( + title: l10n.onboardingTitle2, + subtitle: l10n.onboardingSubtitle2, + image: 'assets/images/onboarding_2.png', + fallbackIcon: Icons.location_on_rounded, + ), + _OnboardingPageData( + title: l10n.onboardingTitle3, + subtitle: l10n.onboardingSubtitle3, + image: 'assets/images/onboarding_3.png', + fallbackIcon: Icons.payment_rounded, + ), + ]; + + return TripzScaffold( + backgroundColor: cs.surface, + padding: false, + bottomAction: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // ── Page Indicators ── + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(pages.length, (index) { + final isActive = index == _currentIndex; + return AnimatedContainer( + duration: T.medium, + margin: const EdgeInsets.symmetric(horizontal: T.s4), + height: T.s8, + width: isActive ? T.s24 : T.s8, + decoration: BoxDecoration( + color: isActive + ? cs.primary + : cs.surfaceContainerHighest, + borderRadius: BorderRadius.circular(T.rCircle), + ), + ); + }), + ), + const SizedBox(height: T.s24), + + // ── Primary Button ── + TripzButton( + onPressed: () => _nextPage(pages.length), + label: _currentIndex == pages.length - 1 ? l10n.getStarted : l10n.next, + ), + ], + ), + child: Column( + children: [ + // ── Skip Button ── + Align( + alignment: AlignmentDirectional.centerEnd, + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: T.pagePadding, + vertical: T.s8, + ), + child: TripzButton( + onPressed: _skip, + style: TripzButtonStyle.text, + label: l10n.skip, + fullWidth: false, + ), + ), + ), + + // ── PageView ── + Expanded( + child: PageView.builder( + controller: _pageController, + itemCount: pages.length, + onPageChanged: (index) { + setState(() => _currentIndex = index); + }, + itemBuilder: (context, index) { + final data = pages[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: T.s24), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // ── الرسم التوضيحي (أصل مسجّل) مع نبض خفيف ── + AnimatedScale( + scale: _pulseAnimation.value, + duration: const Duration(milliseconds: 100), + child: SizedBox( + width: 240, + height: 240, + child: Image.asset( + data.image, + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => Container( + decoration: BoxDecoration( + color: cs.primaryContainer, + shape: BoxShape.circle, + ), + child: Icon( + data.fallbackIcon, + size: 88, + color: cs.onPrimaryContainer, + ), + ), + ), + ), + ), + const SizedBox(height: T.s48), + + // ── Title ── + Text( + data.title, + style: theme.textTheme.headlineMedium?.copyWith( + color: cs.onSurface, + fontWeight: FontWeight.bold, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: T.s12), + + // ── Subtitle ── + Text( + data.subtitle, + style: theme.textTheme.bodyLarge?.copyWith( + color: cs.onSurfaceVariant, + ), + textAlign: TextAlign.center, + ), + ], + ), + ); + }, + ), + ), + ], + ), + ); + } +} + +class _OnboardingPageData { + final String title; + final String subtitle; + final String image; + final IconData fallbackIcon; + + const _OnboardingPageData({ + required this.title, + required this.subtitle, + required this.image, + required this.fallbackIcon, + }); +} diff --git a/apps/rider/lib/features/splash/view/splash_screen.dart b/apps/rider/lib/features/splash/view/splash_screen.dart new file mode 100644 index 0000000..11a082b --- /dev/null +++ b/apps/rider/lib/features/splash/view/splash_screen.dart @@ -0,0 +1,247 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import '../../../core/app_info.dart'; +import '../../../core/design/tokens.dart'; +import '../../../core/di.dart'; +import '../../../core/api/api_client.dart'; +import '../../auth/cubit/auth_cubit.dart'; +import '../../auth/cubit/auth_state.dart'; +import '../../../core/ui/tripz_scaffold.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; + +class SplashScreen extends StatefulWidget { + const SplashScreen({super.key}); + + @override + State createState() => _SplashScreenState(); +} + +class _SplashScreenState extends State + with SingleTickerProviderStateMixin { + static const _kOnboardingSeen = 'onboarding_seen'; + + late final AnimationController _revealController; + late final Animation _fade; + late final Animation _scale; + + @override + void initState() { + super.initState(); + // حركة دخول واحدة هادئة: fade + scale ضمن أزمنة tokens (docs/29 §3.1) + _revealController = AnimationController(vsync: this, duration: T.slow); + final curved = CurvedAnimation( + parent: _revealController, + curve: Curves.easeOutCubic, + ); + _fade = curved; + _scale = Tween(begin: 0.85, end: 1.0).animate(curved); + _revealController.forward(); + WidgetsBinding.instance.addPostFrameCallback((_) => _route()); + } + + @override + void dispose() { + _revealController.dispose(); + super.dispose(); + } + + Future _route() async { + FlutterNativeSplash.remove(); + // Minimum 800ms for the native splash to fade out gracefully + await Future.delayed(const Duration(milliseconds: 800)); + + if (!mounted) return; + + final authStatus = context.read().state.status; + + if (authStatus != AuthStatus.authenticated) { + final prefs = await SharedPreferences.getInstance(); + if (!mounted) return; + final seen = prefs.getBool(_kOnboardingSeen) ?? false; + if (!seen) { + await prefs.setBool(_kOnboardingSeen, true); + if (!mounted) return; + context.go('/onboarding'); + } else { + context.go('/phone'); + } + return; + } + + final api = getIt(); + + Future?> pendingRating() async { + try { + final r = await api.dio + .get('/trips/rating/pending') + .timeout(const Duration(seconds: 3)); + final data = r.data; + if (data is Map && data['pending'] != null) { + return data; + } + return null; + } catch (_) { + return null; + } + } + + Future hasActiveTrip() async { + try { + final r = await api.dio + .get('/trips/mine') + .timeout(const Duration(seconds: 3)); + final data = r.data; + if (data == null) return false; + final items = (data['items'] ?? data) as List; + if (items.isEmpty) return false; + final status = items.first['status'] as String? ?? ''; + return [ + 'searching', + 'assigned', + 'driver_arriving', + 'driver_arrived', + 'in_progress', + ].contains(status); + } catch (_) { + return false; + } + } + + final results = await Future.wait([pendingRating(), hasActiveTrip()]); + final rating = results[0] as Map?; + final activeTrip = results[1] as bool; + + if (!mounted) return; + + if (rating != null) { + context.go('/trip-rating', extra: rating); + return; + } + + if (activeTrip) { + context.go('/trip-tracking'); + return; + } + + context.go('/home'); + } + + @override + Widget build(BuildContext context) { + final cs = Theme.of(context).colorScheme; + + // Just show the logo briefly — native splash handles the branded experience + return TripzScaffold( + backgroundColor: cs.surface, + padding: false, + safeAreaTop: false, + safeAreaBottom: false, + child: Stack( + alignment: Alignment.center, + children: [ + Center( + child: AnimatedBuilder( + animation: _revealController, + builder: (context, child) => Opacity( + opacity: _fade.value, + child: Transform.scale(scale: _scale.value, child: child), + ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: T.s48), + child: Image.asset( + 'assets/images/logo.png', + width: 240, + + fit: BoxFit.contain, + errorBuilder: (_, __, ___) => Icon( + Icons.directions_car_filled_rounded, + size: 120, + color: cs.primary, + ), + ), + ), + ), + ), + // مؤشر تحميل خفيف ورقم الإصدار أسفل الشاشة — يظهر بعد اكتمال حركة الشعار + Positioned( + bottom: T.s32, + child: FadeTransition( + opacity: _revealController, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Row( + mainAxisSize: MainAxisSize.min, + children: List.generate(3, (i) { + return Padding( + padding: const EdgeInsets.symmetric(horizontal: T.s4), + child: _PulsingDot(color: cs.primary, delay: i * 150), + ); + }), + ), + const SizedBox(height: T.s16), + Text( + AppInfo.formattedVersion, + style: Theme.of(context).textTheme.labelSmall?.copyWith( + color: cs.onSurfaceVariant.withValues(alpha: 0.5), + letterSpacing: 1.2, + ), + ), + ], + ), + ), + ), + ], + ), + ); + } +} + +/// نقطة نابضة صغيرة — مؤشر تحميل زخرفي بلا شبكة، `CustomPaint`-free ويكفي هنا Container بسيط. +class _PulsingDot extends StatefulWidget { + const _PulsingDot({required this.color, required this.delay}); + + final Color color; + final int delay; + + @override + State<_PulsingDot> createState() => _PulsingDotState(); +} + +class _PulsingDotState extends State<_PulsingDot> + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController(vsync: this, duration: T.slow * 3) + ..repeat(reverse: true); + Future.delayed(Duration(milliseconds: widget.delay), () { + if (mounted) _controller.forward(); + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return AnimatedBuilder( + animation: _controller, + builder: (context, _) => Opacity( + opacity: 0.2 + (_controller.value * 0.6), + child: Container( + width: T.s8, + height: T.s8, + decoration: BoxDecoration(color: widget.color, shape: BoxShape.circle), + ), + ), + ); + } +} diff --git a/apps/rider/lib/features/trip/data/trip_repository.dart b/apps/rider/lib/features/trip/data/trip_repository.dart index 5f25af8..ab66dba 100644 --- a/apps/rider/lib/features/trip/data/trip_repository.dart +++ b/apps/rider/lib/features/trip/data/trip_repository.dart @@ -20,7 +20,14 @@ class TripRepository { if (items.isEmpty) return null; final trip = TripModel.fromJson(items.first as Map); final active = ['searching', 'assigned', 'driver_arriving', 'driver_arrived', 'in_progress']; - return active.contains(trip.status) ? trip : null; + if (!active.contains(trip.status)) return null; + + // تصفية: إذا مرت أكثر من 60 دقيقة على إنشاء الرحلة، لا تُعتبر نشطة + if (trip.createdAt != null) { + final age = DateTime.now().difference(trip.createdAt!); + if (age.inMinutes >= 60) return null; + } + return trip; } catch (_) { return null; } diff --git a/apps/rider/lib/features/trip/view/rating_screen.dart b/apps/rider/lib/features/trip/view/rating_screen.dart index b6f4dfb..9ceaecc 100644 --- a/apps/rider/lib/features/trip/view/rating_screen.dart +++ b/apps/rider/lib/features/trip/view/rating_screen.dart @@ -4,6 +4,7 @@ import '../../../core/l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import '../../../core/design/tokens.dart'; import '../../../core/ui/tripz_button.dart'; +import '../../../core/ui/tripz_scaffold.dart'; import '../bloc/trip_bloc.dart'; import '../bloc/trip_state.dart'; import '../bloc/trip_event.dart'; @@ -42,72 +43,74 @@ class _RatingScreenState extends State { context.go('/home'); } }, - child: Scaffold( - appBar: AppBar(title: Text(l10n.rateYourTrip)), - body: Padding( - padding: const EdgeInsets.all(T.pagePadding), - child: Column( - children: [ - const Spacer(flex: 2), + child: TripzScaffold( + title: l10n.rateYourTrip, + showBackButton: true, + onBack: () { + context.read().add(const TripClear()); + context.go('/home'); + }, + child: Column( + children: [ + const Spacer(flex: 2), - // نجوم التقييم - Text( - l10n.rateDriver, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w700, + // نجوم التقييم + Text( + l10n.rateDriver, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, + ), + ), + const SizedBox(height: T.s24), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: List.generate(5, (i) { + final index = i + 1; + return GestureDetector( + onTap: () => setState(() => _stars = index), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Icon( + index <= _stars ? Icons.star_rounded : Icons.star_outline_rounded, + size: 48, + color: index <= _stars ? Colors.amber : cs.onSurfaceVariant, ), - ), - const SizedBox(height: T.s24), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate(5, (i) { - final index = i + 1; - return GestureDetector( - onTap: () => setState(() => _stars = index), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6), - child: Icon( - index <= _stars ? Icons.star_rounded : Icons.star_outline_rounded, - size: 48, - color: index <= _stars ? Colors.amber : cs.onSurfaceVariant, - ), - ), - ); - }), - ), + ), + ); + }), + ), - const SizedBox(height: T.s24), + const SizedBox(height: T.s24), - // حقل التعليق - TextField( - controller: _commentController, - maxLines: 3, - decoration: InputDecoration( - hintText: l10n.ratingComment, - border: const OutlineInputBorder(), - ), + // حقل التعليق + TextField( + controller: _commentController, + maxLines: 3, + decoration: InputDecoration( + hintText: l10n.ratingComment, + border: const OutlineInputBorder(), ), + ), - const Spacer(flex: 3), + const Spacer(flex: 3), - // زر الإرسال - SizedBox( - width: double.infinity, - child: TripzButton( - label: l10n.submitRating, - onPressed: _stars > 0 - ? () { - context.read().add(TripRate( - stars: _stars, - comment: _commentController.text, - )); - } - : null, - ), + // زر الإرسال + SizedBox( + width: double.infinity, + child: TripzButton( + label: l10n.submitRating, + onPressed: _stars > 0 + ? () { + context.read().add(TripRate( + stars: _stars, + comment: _commentController.text, + )); + } + : null, ), - const SizedBox(height: T.s8), - ], - ), + ), + const SizedBox(height: T.s8), + ], ), ), ); diff --git a/apps/rider/lib/features/trip/view/trip_invoice_screen.dart b/apps/rider/lib/features/trip/view/trip_invoice_screen.dart index 75c29f1..894feaf 100644 --- a/apps/rider/lib/features/trip/view/trip_invoice_screen.dart +++ b/apps/rider/lib/features/trip/view/trip_invoice_screen.dart @@ -4,6 +4,7 @@ import '../../../core/l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import '../../../core/design/tokens.dart'; import '../../../core/ui/tripz_button.dart'; +import '../../../core/ui/tripz_scaffold.dart'; import '../bloc/trip_bloc.dart'; import '../bloc/trip_state.dart'; import '../bloc/trip_event.dart'; @@ -15,94 +16,90 @@ class TripInvoiceScreen extends StatelessWidget { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final cs = Theme.of(context).colorScheme; return BlocBuilder( builder: (context, state) { final trip = state.trip; if (trip == null) { - return Scaffold( - appBar: AppBar(title: Text(l10n.rideCompleted)), - body: const Center(child: CircularProgressIndicator()), + return TripzScaffold( + title: l10n.rideCompleted, + showBackButton: true, + onBack: () => context.go('/home'), + child: const Center(child: CircularProgressIndicator()), ); } final fare = trip.fare ?? 0; final currency = trip.currency ?? 'JOD'; - return Scaffold( - appBar: AppBar( - title: Text(l10n.rideCompleted), - automaticallyImplyLeading: false, - ), - body: Padding( - padding: const EdgeInsets.all(T.pagePadding), - child: Column( - children: [ - // أيقونة النجاح - Container( - width: 80, - height: 80, - decoration: BoxDecoration( - color: Colors.green.withValues(alpha: 0.1), - shape: BoxShape.circle, - ), - child: const Icon(Icons.check_circle_rounded, size: 48, color: Colors.green), + return TripzScaffold( + title: l10n.rideCompleted, + showBackButton: false, + child: Column( + children: [ + // أيقونة النجاح + Container( + width: 80, + height: 80, + decoration: BoxDecoration( + color: Colors.green.withValues(alpha: 0.1), + shape: BoxShape.circle, ), - const SizedBox(height: T.s16), - Text( - l10n.rideCompleted, - style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w700, - ), - ), - const SizedBox(height: T.s24), - - // تفاصيل الأجرة - Card( - child: Padding( - padding: const EdgeInsets.all(T.pagePadding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Text(l10n.fareBreakdown, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - fontWeight: FontWeight.w600, - )), - const SizedBox(height: T.s12), - _FareRow(label: l10n.tripFare, value: '$fare $currency'), - if (trip.paymentMethod == 'wallet') - _FareRow(label: l10n.walletPayment, value: l10n.youPaid), - if (trip.paymentMethod == 'cash') - _FareRow(label: l10n.cash, value: l10n.payCash), - const Divider(height: T.s16), - _FareRow( - label: l10n.total, - value: '$fare $currency', - bold: true, - ), - ], + child: const Icon(Icons.check_circle_rounded, size: 48, color: Colors.green), + ), + const SizedBox(height: T.s16), + Text( + l10n.rideCompleted, + style: Theme.of(context).textTheme.headlineSmall?.copyWith( + fontWeight: FontWeight.w700, ), + ), + const SizedBox(height: T.s24), + + // تفاصيل الأجرة + Card( + child: Padding( + padding: const EdgeInsets.all(T.pagePadding), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Text(l10n.fareBreakdown, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + )), + const SizedBox(height: T.s12), + _FareRow(label: l10n.tripFare, value: '$fare $currency'), + if (trip.paymentMethod == 'wallet') + _FareRow(label: l10n.walletPayment, value: l10n.youPaid), + if (trip.paymentMethod == 'cash') + _FareRow(label: l10n.cash, value: l10n.payCash), + const Divider(height: T.s16), + _FareRow( + label: l10n.total, + value: '$fare $currency', + bold: true, + ), + ], ), ), + ), - const Spacer(), + const Spacer(), - // أزرار الإجراءات - TripzButton( - label: l10n.rateYourTrip, - onPressed: () => context.push('/trip-rating'), - ), - const SizedBox(height: T.s8), - TextButton( - onPressed: () { - context.read().add(const TripClear()); - context.go('/home'); - }, - child: Text(l10n.skipRating), - ), - ], - ), + // أزرار الإجراءات + TripzButton( + label: l10n.rateYourTrip, + onPressed: () => context.push('/trip-rating'), + ), + const SizedBox(height: T.s8), + TextButton( + onPressed: () { + context.read().add(const TripClear()); + context.go('/home'); + }, + child: Text(l10n.skipRating), + ), + ], ), ); }, diff --git a/apps/rider/lib/features/trip/view/trip_tracking_screen.dart b/apps/rider/lib/features/trip/view/trip_tracking_screen.dart index 3766c2a..6834203 100644 --- a/apps/rider/lib/features/trip/view/trip_tracking_screen.dart +++ b/apps/rider/lib/features/trip/view/trip_tracking_screen.dart @@ -5,6 +5,7 @@ import '../../../core/l10n/app_localizations.dart'; import 'package:go_router/go_router.dart'; import 'package:intaleq_maps/intaleq_maps.dart'; import '../../../core/design/tokens.dart'; +import '../../../core/ui/tripz_scaffold.dart'; import '../bloc/trip_bloc.dart'; import '../bloc/trip_state.dart'; import '../bloc/trip_event.dart'; @@ -20,7 +21,6 @@ class TripTrackingScreen extends StatefulWidget { } class _TripTrackingScreenState extends State { - IntaleqMapController? _mapController; Timer? _cameraFollowTimer; @override @@ -39,7 +39,6 @@ class _TripTrackingScreenState extends State { @override Widget build(BuildContext context) { final l10n = AppLocalizations.of(context)!; - final cs = Theme.of(context).colorScheme; return BlocConsumer( listenWhen: (prev, cur) => prev.trip?.status != cur.trip?.status, @@ -52,7 +51,23 @@ class _TripTrackingScreenState extends State { builder: (context, state) { final trip = state.trip; if (trip == null) { - return const Scaffold(body: Center(child: CircularProgressIndicator())); + return TripzScaffold( + showBackButton: true, + onBack: () { + context.read().add(const TripClear()); + context.go('/home'); + }, + child: Center( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const CircularProgressIndicator(), + const SizedBox(height: T.s16), + Text(l10n.loading), + ], + ), + ), + ); } final pickup = LatLng(trip.pickupLat, trip.pickupLng); @@ -71,10 +86,14 @@ class _TripTrackingScreenState extends State { target: driverPos ?? pickup, zoom: 15, ), - myLocationEnabled: false, + mapType: Theme.of(context).brightness == Brightness.dark + ? IntaleqMapType.normal + : IntaleqMapType.light, + myLocationEnabled: true, + myLocationButtonEnabled: false, zoomControlsEnabled: false, compassEnabled: false, - onMapCreated: (c) => _mapController = c, + onMapCreated: (_) {}, markers: _buildMarkers(pickup, dropoff, driverPos), ), @@ -106,7 +125,9 @@ class _TripTrackingScreenState extends State { // TODO: Chat — Phase 3 }, onCancel: state.canCancel - ? () => context.read().add(const TripCancelRequested()) + ? () => context + .read() + .add(const TripCancelRequested()) : null, ), ), @@ -166,8 +187,9 @@ class _SearchingBodyState extends State<_SearchingBody> @override void initState() { super.initState(); - _pulse = AnimationController(vsync: this, duration: const Duration(seconds: 2)) - ..repeat(reverse: true); + _pulse = + AnimationController(vsync: this, duration: const Duration(seconds: 2)) + ..repeat(reverse: true); _scale = Tween(begin: 0.8, end: 1.2).animate( CurvedAnimation(parent: _pulse, curve: Curves.easeInOut), ); @@ -213,14 +235,20 @@ class _SearchingBodyState extends State<_SearchingBody> ), const SizedBox(height: T.s16), Text(l10n.searchingDriver, - style: Theme.of(context).textTheme.titleMedium?.copyWith(fontWeight: FontWeight.w600), + style: Theme.of(context) + .textTheme + .titleMedium + ?.copyWith(fontWeight: FontWeight.w600), textAlign: TextAlign.center), const SizedBox(height: T.s20), SizedBox( width: double.infinity, child: OutlinedButton( - onPressed: () => context.read().add(const TripCancelRequested()), - style: OutlinedButton.styleFrom(foregroundColor: cs.error, side: BorderSide(color: cs.error)), + onPressed: () => + context.read().add(const TripCancelRequested()), + style: OutlinedButton.styleFrom( + foregroundColor: cs.error, + side: BorderSide(color: cs.error)), child: Text(l10n.cancelSearch), ), ), diff --git a/apps/rider/lib/features/trip/view/widgets/driver_card.dart b/apps/rider/lib/features/trip/view/widgets/driver_card.dart index 026a957..f877e87 100644 --- a/apps/rider/lib/features/trip/view/widgets/driver_card.dart +++ b/apps/rider/lib/features/trip/view/widgets/driver_card.dart @@ -65,7 +65,7 @@ class DriverCard extends StatelessWidget { children: [ Icon(Icons.star_rounded, size: 16, color: Colors.amber), const SizedBox(width: 4), - Text('4.8', style: Theme.of(context).textTheme.bodySmall), + Text(l10n.driverRatingValue, style: Theme.of(context).textTheme.bodySmall), ], ), ], diff --git a/apps/rider/lib/main.dart b/apps/rider/lib/main.dart index 81a3878..baf803d 100644 --- a/apps/rider/lib/main.dart +++ b/apps/rider/lib/main.dart @@ -1,11 +1,13 @@ import 'package:flutter/material.dart'; import 'package:firebase_core/firebase_core.dart'; +import 'package:flutter_native_splash/flutter_native_splash.dart'; import 'firebase_options.dart'; import 'core/di.dart'; import 'app.dart'; Future main() async { - WidgetsFlutterBinding.ensureInitialized(); + WidgetsBinding widgetsBinding = WidgetsFlutterBinding.ensureInitialized(); + FlutterNativeSplash.preserve(widgetsBinding: widgetsBinding); try { await Firebase.initializeApp(options: DefaultFirebaseOptions.currentPlatform); } catch (_) { diff --git a/apps/rider/pubspec.lock b/apps/rider/pubspec.lock index 32a30b1..638b344 100644 --- a/apps/rider/pubspec.lock +++ b/apps/rider/pubspec.lock @@ -601,10 +601,10 @@ packages: dependency: transitive description: name: matcher - sha256: "12956d0ad8390bbcc63ca2e1469c0619946ccb52809807067a7020d57e647aa6" + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.18" + version: "0.12.19" material_color_utilities: dependency: transitive description: @@ -617,10 +617,10 @@ packages: dependency: transitive description: name: meta - sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394" + sha256: "1741988757a65eb6b36abe716829688cf01910bbf91c34354ff7ec1c3de2b349" url: "https://pub.dev" source: hosted - version: "1.17.0" + version: "1.18.0" mime: dependency: transitive description: @@ -950,10 +950,10 @@ packages: dependency: transitive description: name: test_api - sha256: "93167629bfc610f71560ab9312acdda4959de4df6fac7492c89ff0d3886f6636" + sha256: "949a932224383300f01be9221c39180316445ecb8e7547f70a41a35bf421fb9e" url: "https://pub.dev" source: hosted - version: "0.7.9" + version: "0.7.11" typed_data: dependency: transitive description: diff --git a/apps/rider/pubspec.yaml b/apps/rider/pubspec.yaml index 501789f..2b09a55 100644 --- a/apps/rider/pubspec.yaml +++ b/apps/rider/pubspec.yaml @@ -30,7 +30,7 @@ dependencies: # الخرائط: SDK انطلق حصراً (docs/23 §6) — يُصدِّر LatLng/Marker/Polyline # بنفسه. لا flutter_map ولا latlong2 (نوعا LatLng متضاربان). - intaleq_maps: ^2.2.0 + intaleq_maps: ^2.2.1 # الموقع: جلب موقع المستخدم الحالي (Phase 1 — الخرائط وحساب المسار) geolocator: ^14.0.0 @@ -55,7 +55,8 @@ dev_dependencies: flutter_launcher_icons: android: "launcher_icon" ios: true - image_path: "assets/images/logo.png" + remove_alpha_ios: true + image_path: "assets/images/splash_logo.png" min_sdk_android: 21 flutter: @@ -90,7 +91,7 @@ flutter: flutter_native_splash: color: "#FFFFFF" - image: "assets/images/logo.png" + image: "assets/images/splash_logo.png" android_12: - image: "assets/images/logo.png" + image: "assets/images/splash_logo.png" icon_background_color: "#FFFFFF" diff --git a/apps/rider/web/index.html b/apps/rider/web/index.html index 14165d0..2bc81e8 100644 --- a/apps/rider/web/index.html +++ b/apps/rider/web/index.html @@ -39,6 +39,10 @@ + + + + + + + + + +
+arrow_forward +

مسار

+
+
+ +
+ + +
+
+
+
+ + +
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+

شارع العليا، الرياض

+8 min +
+
+
+

مطار الملك خالد الدولي

+3.2 km +
+
+
+
+ +

اختر فئة الرحلة

+ +
+ + + + + + +
+ +
+ + + +
+
+local_offer + +
+ +
+ + +
+
+ +
+ +
+
+ + + diff --git a/docs/stitch_screens/category___price_selection_img.png b/docs/stitch_screens/category___price_selection_img.png new file mode 100644 index 0000000..e8580b1 Binary files /dev/null and b/docs/stitch_screens/category___price_selection_img.png differ diff --git a/docs/stitch_screens/destination_selection.html b/docs/stitch_screens/destination_selection.html new file mode 100644 index 0000000..a5afed1 --- /dev/null +++ b/docs/stitch_screens/destination_selection.html @@ -0,0 +1,341 @@ + + + + + + +Tripz - Destination Selection + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+
+ مسار +
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+stop +
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + +
+ +
+ + + + + + + + +
+
+
+ + + + + diff --git a/docs/stitch_screens/destination_selection_img.png b/docs/stitch_screens/destination_selection_img.png new file mode 100644 index 0000000..4ca7898 Binary files /dev/null and b/docs/stitch_screens/destination_selection_img.png differ diff --git a/docs/stitch_screens/during_trip.html b/docs/stitch_screens/during_trip.html new file mode 100644 index 0000000..754f687 --- /dev/null +++ b/docs/stitch_screens/during_trip.html @@ -0,0 +1,298 @@ + + + + + + +During Trip - Live Tracking + + + + + + + + + + + + + + + +
+ +
+ + + + + +
+
+directions_car +
+
+ +
+location_on +
+ +
+
+
+
+
+
+
+ +
+ + +
+ + +
+ +
+ +
+ +
+
+
+
+ +
+
+
+ +
+
+route +
+ +
+

في الطريق إلى وجهتك

+
+

الوصول المتوقع:

+12:45 م +
+
+
+ +
+12 +دقيقة +
+
+ +
+
+
+
+نقطة الانطلاق +الوجهة +
+
+ +
+
+ +
+ +
+
+4.9 +star +
+
+
+ +
+

محمد عبدالله

+

تويوتا كامري • أبيض

+ +
+أ ب ج ١٢٣٤ +
+
+
+ +
+ + +
+
+ +
+

مركز السلامة

+
+ + + + +
+
+ +
+ + +
+ + +
+
+ + + diff --git a/docs/stitch_screens/during_trip_img.png b/docs/stitch_screens/during_trip_img.png new file mode 100644 index 0000000..c23c728 Binary files /dev/null and b/docs/stitch_screens/during_trip_img.png differ diff --git a/docs/stitch_screens/name_entry.html b/docs/stitch_screens/name_entry.html new file mode 100644 index 0000000..824371e --- /dev/null +++ b/docs/stitch_screens/name_entry.html @@ -0,0 +1,280 @@ + + + + + + +ما هو اسمك؟ - Tripz + + + + + + + + + + + + + +
+ +Tripz +
+ +
+ +
+

ما هو اسمك؟

+
+ +
+

ساعدنا في تخصيص تجربتك

+
+ +
+ +
+
+person +
+ + +
+ +
+
+person +
+ + +
+ +
+
+mail +
+ + +
+
+
+ +
+ +
+ + + + + + diff --git a/docs/stitch_screens/name_entry_img.png b/docs/stitch_screens/name_entry_img.png new file mode 100644 index 0000000..34bd669 Binary files /dev/null and b/docs/stitch_screens/name_entry_img.png differ diff --git a/docs/stitch_screens/onboarding_1.html b/docs/stitch_screens/onboarding_1.html new file mode 100644 index 0000000..566376d --- /dev/null +++ b/docs/stitch_screens/onboarding_1.html @@ -0,0 +1,225 @@ + + + + + + +Tripz - Onboarding + + + + + + + + + + + + + +
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+

+ اطلب بلمسة واحدة +

+

+ أدخل وجهتك واحصل على سائق فوراً +

+
+ +
+
+
+
+
+ + +
+
+ + + diff --git a/docs/stitch_screens/onboarding_1_img.png b/docs/stitch_screens/onboarding_1_img.png new file mode 100644 index 0000000..7f04ae5 Binary files /dev/null and b/docs/stitch_screens/onboarding_1_img.png differ diff --git a/docs/stitch_screens/onboarding_2.html b/docs/stitch_screens/onboarding_2.html new file mode 100644 index 0000000..9c22008 --- /dev/null +++ b/docs/stitch_screens/onboarding_2.html @@ -0,0 +1,155 @@ + + + + + + +تتبع رحلتك مباشرة - Onboarding + + + + + + + + + +
+ +
+ +
+ +
+تتبع رحلتك مباشرة +
+ +
+

+ تتبع رحلتك مباشرة +

+

+ شاهد سائقك وهو يقترب منك في الوقت الفعلي +

+
+
+ +
+ +
+
+
+
+
+ + +
+ + + diff --git a/docs/stitch_screens/onboarding_2_img.png b/docs/stitch_screens/onboarding_2_img.png new file mode 100644 index 0000000..2b08791 Binary files /dev/null and b/docs/stitch_screens/onboarding_2_img.png differ diff --git a/docs/stitch_screens/onboarding_3.html b/docs/stitch_screens/onboarding_3.html new file mode 100644 index 0000000..7377029 --- /dev/null +++ b/docs/stitch_screens/onboarding_3.html @@ -0,0 +1,151 @@ + + + + + + +Tripz - Onboarding 3 + + + + + + + + +
+ +
+ +
+ +
+Payment methods illustration +
+ +
+

+ ادفع بطريقتك +

+

+ اختر الدفع نقدًا، بالبطاقة، أو عبر المحفظة +

+
+
+ +
+ +
+
+
+
+
+ + +
+ + + diff --git a/docs/stitch_screens/onboarding_3_img.png b/docs/stitch_screens/onboarding_3_img.png new file mode 100644 index 0000000..cdcb027 Binary files /dev/null and b/docs/stitch_screens/onboarding_3_img.png differ diff --git a/docs/stitch_screens/otp_verification.html b/docs/stitch_screens/otp_verification.html new file mode 100644 index 0000000..adb4f3f --- /dev/null +++ b/docs/stitch_screens/otp_verification.html @@ -0,0 +1,203 @@ + + + + + + +تحقق من رقمك - مسار + + + + + + + + + +
+ + + +
+تحقق من رقمك +
+
+ +
+ +
+
+

+ أدخل الكود المرسل إلى +962 7XX XXX XXXX +

+ +
+
+ +
+
+ + + + +
+
+ +
+

+ إعادة إرسال الكود خلال 0:45 +

+
+ +
+ +
+
+ + + + diff --git a/docs/stitch_screens/phone_login.html b/docs/stitch_screens/phone_login.html new file mode 100644 index 0000000..81a5fe6 --- /dev/null +++ b/docs/stitch_screens/phone_login.html @@ -0,0 +1,193 @@ + + + + + + +Tripz - Login + + + + + + + + + + + + + + +
+ +
+ +
+Tripz Logo +
+ +
+ +
+ +
+

أهلاً بك في Tripz

+

أدخل رقم هاتفك للبدء

+
+ +
+ +
+ + + + +
+ + +
+
+ + + + diff --git a/docs/stitch_screens/phone_login_img.png b/docs/stitch_screens/phone_login_img.png new file mode 100644 index 0000000..34bd669 Binary files /dev/null and b/docs/stitch_screens/phone_login_img.png differ diff --git a/docs/stitch_screens/rating_screen.html b/docs/stitch_screens/rating_screen.html new file mode 100644 index 0000000..1e9be10 --- /dev/null +++ b/docs/stitch_screens/rating_screen.html @@ -0,0 +1,276 @@ + + + + + + +Tripz - تقييم الرحلة + + + + + + + + + + +
+ +
+ +
Tripz
+
+
+ +
+ +
+
+Ahmad K. +
+
+verified +
+
+
+
+

أحمد ك.

+

كيف كانت رحلتك مع أحمد؟

+
+
+ +
+
+ +star +star +star +star +star +
+

ممتاز!

+
+ +
+

ما الذي أعجبك؟

+
+ + + + + +
+
+ +
+ + +
+
+ +
+ +
+
+ + \ No newline at end of file diff --git a/docs/stitch_screens/rating_screen_img.png b/docs/stitch_screens/rating_screen_img.png new file mode 100644 index 0000000..3eab036 Binary files /dev/null and b/docs/stitch_screens/rating_screen_img.png differ diff --git a/docs/stitch_screens/route_preview.html b/docs/stitch_screens/route_preview.html new file mode 100644 index 0000000..6158674 --- /dev/null +++ b/docs/stitch_screens/route_preview.html @@ -0,0 +1,272 @@ + + + + + + +Route Preview - مسار + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+
+ +
+
1
+
+ +
+
2
+
+ +
+location_on +
+
+
+ +
+ + +
+ +
+ +
+
+
+ +
+
+3.2 كم +المسافة الإجمالية +
+
+
+8 دقائق +وقت الوصول المتوقع +
+
+ +
+
+ + + + + + +
+
+ +
+ + +
+ +
+ +
+
+
+ + + diff --git a/docs/stitch_screens/route_preview_img.png b/docs/stitch_screens/route_preview_img.png new file mode 100644 index 0000000..e7d681c Binary files /dev/null and b/docs/stitch_screens/route_preview_img.png differ diff --git a/docs/stitch_screens/searching.html b/docs/stitch_screens/searching.html new file mode 100644 index 0000000..2413bfc --- /dev/null +++ b/docs/stitch_screens/searching.html @@ -0,0 +1,184 @@ + + + + + + +Tripz - Searching for Driver + + + + + + + + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+directions_car +
+ +

جاري البحث عن سائق...

+

يتم العثور على أقرب سائق لك

+ +
+
+
+ +
+
+
+Driver 1 +Driver 2 +Driver 3 +
++5 +
+
+

يتم مراجعة طلبك من قبل السائقين

+
+
+ + +
+ +
+
+ + + diff --git a/docs/stitch_screens/searching_img.png b/docs/stitch_screens/searching_img.png new file mode 100644 index 0000000..8881ae7 Binary files /dev/null and b/docs/stitch_screens/searching_img.png differ diff --git a/docs/stitch_screens/splash_screen.html b/docs/stitch_screens/splash_screen.html new file mode 100644 index 0000000..15cb3f8 --- /dev/null +++ b/docs/stitch_screens/splash_screen.html @@ -0,0 +1,197 @@ + + + + + + +Tripz - Splash + + + + + + + + + +
+ +
+Tripz Logo +
+ +
+
+
+
+
+
+
+
+ + + + diff --git a/docs/stitch_screens/splash_screen_img.png b/docs/stitch_screens/splash_screen_img.png new file mode 100644 index 0000000..34bd669 Binary files /dev/null and b/docs/stitch_screens/splash_screen_img.png differ diff --git a/docs/stitch_screens/stitch_screen.html b/docs/stitch_screens/stitch_screen.html new file mode 100644 index 0000000..b2ffd68 --- /dev/null +++ b/docs/stitch_screens/stitch_screen.html @@ -0,0 +1,3410 @@ + + + + + + +Tripz - Splash + + + + + + + + + +
+ +
+Tripz Logo +
+ +
+
+
+
+
+
+
+
+ + + + + + + + + +Tripz - Onboarding + + + + + + + + + + + + + +
+ +
+ +
+ +
+
+ +
+
+ +
+ +
+

+ اطلب بلمسة واحدة +

+

+ أدخل وجهتك واحصل على سائق فوراً +

+
+ +
+
+
+
+
+ + +
+
+ + + + + + + + +تتبع رحلتك مباشرة - Onboarding + + + + + + + + + +
+ +
+ +
+ +
+تتبع رحلتك مباشرة +
+ +
+

+ تتبع رحلتك مباشرة +

+

+ شاهد سائقك وهو يقترب منك في الوقت الفعلي +

+
+
+ +
+ +
+
+
+
+
+ + +
+ + + + + + + + +Tripz - Onboarding 3 + + + + + + + + +
+ +
+ +
+ +
+Payment methods illustration +
+ +
+

+ ادفع بطريقتك +

+

+ اختر الدفع نقدًا، بالبطاقة، أو عبر المحفظة +

+
+
+ +
+ +
+
+
+
+
+ + +
+ + + + + + + + +Tripz - Login + + + + + + + + + + + + + + +
+ +
+ +
+Tripz Logo +
+ +
+ +
+ +
+

أهلاً بك في Tripz

+

أدخل رقم هاتفك للبدء

+
+ +
+ +
+ + + + +
+ + +
+
+ + + + + + + + + +تحقق من رقمك - مسار + + + + + + + + + +
+ + + +
+تحقق من رقمك +
+
+ +
+ +
+
+

+ أدخل الكود المرسل إلى +962 7XX XXX XXXX +

+ +
+
+ +
+
+ + + + +
+
+ +
+

+ إعادة إرسال الكود خلال 0:45 +

+
+ +
+ +
+
+ + + + + + + + + +ما هو اسمك؟ - Tripz + + + + + + + + + + + + + +
+ +Tripz +
+ +
+ +
+

ما هو اسمك؟

+
+ +
+

ساعدنا في تخصيص تجربتك

+
+ +
+ +
+
+person +
+ + +
+ +
+
+person +
+ + +
+ +
+
+mail +
+ + +
+
+
+ +
+ +
+ + + + + + + + + + + +Tripz - Destination Selection + + + + + + + + + + + + + + + + +
+ + +
+
+ + +
+
+ مسار +
+ +
+ +
+ +
+
+
+
+ +
+ +
+
+ +
+
+
+
+
+ +
+
+ +
+
+ +
+ +
+
+
+stop +
+
+
+ +
+ +
+
+
+
+ +
+ +
+ + +
+ +
+ + + + + + + + +
+
+
+ + + + + + + + + + +Route Preview - مسار + + + + + + + + + + + + + + + +
+ +
+ + + + + + +
+
+
+ +
+
1
+
+ +
+
2
+
+ +
+location_on +
+
+
+ +
+ + +
+ +
+ +
+
+
+ +
+
+3.2 كم +المسافة الإجمالية +
+
+
+8 دقائق +وقت الوصول المتوقع +
+
+ +
+
+ + + + + + +
+
+ +
+ + +
+ +
+ +
+
+
+ + + + + + + + +Tripz - Ride Category Selection + + + + + + + + + + + + + + + + +
+arrow_forward +

مسار

+
+
+ +
+ + +
+
+
+
+ + +
+ +
+
+
+
+ +
+
+ +
+
+
+
+
+
+
+

شارع العليا، الرياض

+8 min +
+
+
+

مطار الملك خالد الدولي

+3.2 km +
+
+
+
+ +

اختر فئة الرحلة

+ +
+ + + + + + +
+ +
+ + + +
+
+local_offer + +
+ +
+ + +
+
+ +
+ +
+
+ + + + + + + + +Tripz - Searching for Driver + + + + + + + + + +
+ +
+
+ +
+
+ +
+ +
+ +
+ +
+directions_car +
+ +

جاري البحث عن سائق...

+

يتم العثور على أقرب سائق لك

+ +
+
+
+ +
+
+
+Driver 1 +Driver 2 +Driver 3 +
++5 +
+
+

يتم مراجعة طلبك من قبل السائقين

+
+
+ + +
+ +
+
+ + + + + + + + +Tripz - Trip Receipt + + + + + + + + + +
+ +
+

تمت الرحلة

+
+
+
+ +
+ +
+
+
+check +
+

الرحلة مكتملة بنجاح

+
+

شكراً لاختيارك مسار. نأمل أن تكون رحلتك مريحة.

+
+ +
+ +
+ + + + +
+ 3.2 كم +
+
+
+
+
+payments +طريقة الدفع +
+نقداً +
+
+ +
+
+
+الأجرة الأساسية +3.00 د.أ +
+
+المسافة (3.2 كم) +2.00 د.أ +
+
+الوقت (8 دقائق) +0.50 د.أ +
+
+انتظار إضافي +0.00 د.أ +
+
+خصم +-1.00 د.أ +
+
+
+
+ +
+الإجمالي +4.50 د.أ +
+ +
+verified +تم الدفع +
+
+
+
+ +
+
+ +
+
+ + + + + + + + + + +During Trip - Live Tracking + + + + + + + + + + + + + + + +
+ +
+ + + + + +
+
+directions_car +
+
+ +
+location_on +
+ +
+
+
+
+
+
+
+ +
+ + +
+ + +
+ +
+ +
+ +
+
+
+
+ +
+
+
+ +
+
+route +
+ +
+

في الطريق إلى وجهتك

+
+

الوصول المتوقع:

+12:45 م +
+
+
+ +
+12 +دقيقة +
+
+ +
+
+
+
+نقطة الانطلاق +الوجهة +
+
+ +
+
+ +
+ +
+
+4.9 +star +
+
+
+ +
+

محمد عبدالله

+

تويوتا كامري • أبيض

+ +
+أ ب ج ١٢٣٤ +
+
+
+ +
+ + +
+
+ +
+

مركز السلامة

+
+ + + + +
+
+ +
+ + +
+ + +
+
+ + + + + + + + +Tripz - تقييم الرحلة + + + + + + + + + + +
+ +
+ +
Tripz
+
+
+ +
+ +
+
+Ahmad K. +
+
+verified +
+
+
+
+

أحمد ك.

+

كيف كانت رحلتك مع أحمد؟

+
+
+ +
+
+ +star +star +star +star +star +
+

ممتاز!

+
+ +
+

ما الذي أعجبك؟

+
+ + + + + +
+
+ +
+ + +
+
+ +
+ +
+
+ + \ No newline at end of file diff --git a/docs/stitch_screens/trip_invoice.html b/docs/stitch_screens/trip_invoice.html new file mode 100644 index 0000000..307bbd6 --- /dev/null +++ b/docs/stitch_screens/trip_invoice.html @@ -0,0 +1,307 @@ + + + + + + +Tripz - Trip Receipt + + + + + + + + + +
+ +
+

تمت الرحلة

+
+
+
+ +
+ +
+
+
+check +
+

الرحلة مكتملة بنجاح

+
+

شكراً لاختيارك مسار. نأمل أن تكون رحلتك مريحة.

+
+ +
+ +
+ + + + +
+ 3.2 كم +
+
+
+
+
+payments +طريقة الدفع +
+نقداً +
+
+ +
+
+
+الأجرة الأساسية +3.00 د.أ +
+
+المسافة (3.2 كم) +2.00 د.أ +
+
+الوقت (8 دقائق) +0.50 د.أ +
+
+انتظار إضافي +0.00 د.أ +
+
+خصم +-1.00 د.أ +
+
+
+
+ +
+الإجمالي +4.50 د.أ +
+ +
+verified +تم الدفع +
+
+
+
+ +
+
+ +
+
+ + + + + diff --git a/marketing/assets/intaleq.png b/marketing/assets/intaleq.png new file mode 100644 index 0000000..11869c3 Binary files /dev/null and b/marketing/assets/intaleq.png differ diff --git a/marketing/assets/logo_siro.png b/marketing/assets/logo_siro.png new file mode 100644 index 0000000..8f06995 Binary files /dev/null and b/marketing/assets/logo_siro.png differ diff --git a/marketing/assets/logotripz.png b/marketing/assets/logotripz.png new file mode 100644 index 0000000..02d5850 Binary files /dev/null and b/marketing/assets/logotripz.png differ diff --git a/marketing/assets/mock_dispatch.png b/marketing/assets/mock_dispatch.png new file mode 100644 index 0000000..125fa77 Binary files /dev/null and b/marketing/assets/mock_dispatch.png differ diff --git a/marketing/assets/mock_driver.png b/marketing/assets/mock_driver.png new file mode 100644 index 0000000..2dab803 Binary files /dev/null and b/marketing/assets/mock_driver.png differ diff --git a/marketing/assets/mock_hub.png b/marketing/assets/mock_hub.png new file mode 100644 index 0000000..b4b72c3 Binary files /dev/null and b/marketing/assets/mock_hub.png differ diff --git a/marketing/assets/mock_rider.png b/marketing/assets/mock_rider.png new file mode 100644 index 0000000..83562ff Binary files /dev/null and b/marketing/assets/mock_rider.png differ diff --git a/marketing/index.html b/marketing/index.html new file mode 100644 index 0000000..68111f3 --- /dev/null +++ b/marketing/index.html @@ -0,0 +1,1569 @@ + + + + + +Tripz — أطلق تطبيق النقل بعلامتك التجارية + + + + + + + + + + + + + + + +
+ +
+ + +
+ + +
+ +
+ +
+
+
+ Tripz Rider App +
+
+ +
+
+
+ Tripz Driver App +
+
+
+
+
+ + +
+
+
2019
+
$0.02
+
30
+
4
+
+
+ + +
+
+
+
+

+

+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
+
+

+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + 2.5% + + +
+
+

+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + 30 + +
+
+

+
+
+ +
+
+
+ + +
+
+
+
+

+
+
+
+ Tripz + Tripz — العلامة الرئيسية +
+
+ Siro + +
+
+ Intalaq + +
+
+
+
+ +
+
+

+
+
+ + +
+
+
+
+

+
+
+ + + + +
+ +
+
+

+
    +
  • ✓
  • +
  • ✓
  • +
  • ✓
  • +
+
+
+ Tripz Rider App +
+
+ +
+
+

+
    +
  • ✓
  • +
  • ✓
  • +
  • ✓
  • +
+
+
+ Tripz Driver App +
+
+ +
+
+

+
    +
  • ✓
  • +
  • ✓
  • +
  • ✓
  • +
+
+
+ Tripz Dispatch Hub +
+
+ +
+
+

+
    +
  • ✓
  • +
  • ✓
  • +
  • ✓
  • +
+
+
+ Tripz Hub Analytics +
+
+
+
+ + +
+
+
+
تفوق تكنولوجي حقيقي
+

منظومة ذكاء اصطناعي تفوق أي منافس — مدعومة بمنصة «نبيّه»

+

لا نقدم مجرد تطبيق نقل تقليدي، بل محرك ذكاء اصطناعي متكامل يرفع مبيعاتك وأرباحك ويضمن أعلى نسبة تشغيل لأسطولك.

+
+ +
+ +
+
+ +
+

استخبارات السوق والتسعير الذكي

+

خوارزميات تتبّع أسعار المنافسين لحظياً وتضبط تسعير الرحلات ديناميكياً لزيادة حصتك السوقية وأرباح السائقين دون خسارة الركاب.

+
+ + +
+
+ +
+

الرد الآلي والدعم الفوري الذكي

+

نظام مجيب آلي مدمج عبر واتساب والتطبيق يحل شكاوى الركاب والسائقين فورياً ويوفر 80% من تكاليف موظفي الدعم والـ Dispatch.

+
+ + +
+
+ +
+
إضافة حصريّة
+

ربط منصة «نبيّه» للتسويق والمبيعات

+

منظومة المبيعات وإدارة علاقات الركاب (CRM) من منصة نبيّه لإعادة استهداف الركاب الخاملين وزيادة معدل تكرار الرحلات آلياً.

+
+
+
+
+ + +
+
+
+
+

+

+
+
+
+
+ + +
501,500 / day5,000
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+
$—
+
$—
+
$—
+
+
+
$—
+
+ +
+
+
+
+ + +
+
+
+
+

+

+
+
+
+

1
+
2

+

3
+
4

+

5
+
+
+
+ + +
+
+
+
+

+

+
+
+ + + + +
+
+ +
+

+
$169/mo
+
$2,500
+ GMV 3.5% · min $0.05 +
    +
  • ✓
  • +
  • ✓
  • +
  • ✓
  • +
+ +
+ + + +
+

+
$599/mo
+
$8,500
+ GMV 2% · min $0.03 +
    +
  • ✓
  • +
  • ✓
  • +
  • ✓
  • +
+ +
+ +
+

+
$879/mo
+
$13,500+
+ GMV 1% · min $0.02 +
    +
  • ✓
  • +
  • ✓
  • +
  • ✓
  • +
+ +
+
+

+ +
+

①

+

②

+

③

+
+
+
+ 3 / 3 +
+
+
+ + +
+
+
+
+

+
+
+ + + + + + + + + + + + + + +
Tripz
$2,500–13,500$4,500–23,000$150k–500k
$0.02–0.05$0.10—
✓✕—
✓✕✓
✕✓
✓ RTL
+
+
+
+ + +
+
+
+
+

+

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Tripz$2,500–13,500$169–879
المنصات الأجنبية المنافسة$4,500–23,000$299–1,500
التطوير المخصص من الصفركود مخصص مستقل$150,000–500,000صيانة وسيرفرات مستقلة— (تكلفة رأس مال ضخمة)
+
+

+
+
+ + +
+
+
+
+

+

+

+
+
2,500
+
6,000
+
3
+
+
+
+
+
2,500
+
6,000
+
Cairo · Alexandria · Damascus
+
+
+
+
+ + +
+
+

+
+
+
3-D Secure
+
+
+
+
+
+
+ + +
+
+
+
+

+
+
+
+
+ + +
+ +
+ + +
+
+
+
+
Tripz
+

+
+
+
+
+
+
+
+ © 2026 Tripz. + +
+
+
+ + + + + +