change to map-saas api key and env - 2026-04-12
This commit is contained in:
2
.env
2
.env
@@ -113,4 +113,4 @@ W=T
|
||||
X=D
|
||||
Y=S
|
||||
Z=M
|
||||
mapSaasKey=intaleq_secret_2026
|
||||
mapSaasKey=zP9vL5mK2nQ8xR7jT4wS1yB6hG3fV0cX
|
||||
|
||||
@@ -48,8 +48,8 @@ android {
|
||||
// For more information, see: https://flutter.dev/to/review-gradle-config.
|
||||
minSdkVersion = 24
|
||||
targetSdk = 36
|
||||
versionCode = 63
|
||||
versionName = '1.1.63'
|
||||
versionCode = 65
|
||||
versionName = '1.1.65'
|
||||
multiDexEnabled = true
|
||||
ndk {
|
||||
abiFilters "armeabi-v7a", "arm64-v8a"
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<uses-permission android:name="android.permission.INTERNET" />
|
||||
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
|
||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
||||
<uses-permission android:name="android.permission.USE_BIOMETRIC" />
|
||||
<uses-permission android:name="android.permission.VIBRATE" />
|
||||
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
|
||||
|
||||
@@ -42,6 +42,11 @@ post_install do |installer|
|
||||
config.build_settings['IPHONEOS_DEPLOYMENT_TARGET'] = '16.1'
|
||||
# Fix for "non-modular header" issues with some pods.
|
||||
config.build_settings['CLANG_ALLOW_NON_MODULAR_INCLUDES_IN_FRAMEWORK_MODULES'] = 'YES'
|
||||
|
||||
config.build_settings['GCC_PREPROCESSOR_DEFINITIONS'] ||= [
|
||||
'$(inherited)',
|
||||
'PERMISSION_CONTACTS=1',
|
||||
]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -486,6 +486,6 @@ SPEC CHECKSUMS:
|
||||
wakelock_plus: e29112ab3ef0b318e58cfa5c32326458be66b556
|
||||
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d
|
||||
|
||||
PODFILE CHECKSUM: 095099247b81b4f879b2736241d2072ab1875fad
|
||||
PODFILE CHECKSUM: 8a0b04ec79a0d49122ae6c10242e7cb023122802
|
||||
|
||||
COCOAPODS: 1.16.2
|
||||
|
||||
@@ -12,7 +12,8 @@ class ContactUsController extends GetxController {
|
||||
final TimeOfDay workStartTime = const TimeOfDay(hour: 10, minute: 0);
|
||||
final TimeOfDay workEndTime = const TimeOfDay(hour: 16, minute: 0);
|
||||
|
||||
bool _isWithinWorkTime(TimeOfDay now) {
|
||||
bool get isWorkTime {
|
||||
final now = TimeOfDay.now();
|
||||
return (now.hour > workStartTime.hour ||
|
||||
(now.hour == workStartTime.hour &&
|
||||
now.minute >= workStartTime.minute)) &&
|
||||
@@ -20,6 +21,11 @@ class ContactUsController extends GetxController {
|
||||
(now.hour == workEndTime.hour && now.minute <= workEndTime.minute));
|
||||
}
|
||||
|
||||
/// Helper to format working hours for UI
|
||||
String get workHoursString =>
|
||||
'${workStartTime.hour.toString().padLeft(2, '0')}:${workStartTime.minute.toString().padLeft(2, '0')} - '
|
||||
'${workEndTime.hour.toString().padLeft(2, '0')}:${workEndTime.minute.toString().padLeft(2, '0')}';
|
||||
|
||||
/// PHONE LIST (USED FOR CALLS + WHATSAPP)
|
||||
final List<String> phoneNumbers = [
|
||||
'+963952475734',
|
||||
@@ -33,10 +39,24 @@ class ContactUsController extends GetxController {
|
||||
return phoneNumbers[random.nextInt(phoneNumbers.length)];
|
||||
}
|
||||
|
||||
/// SHOW DIALOG
|
||||
/// DIRECT ACTIONS
|
||||
void makeCall() {
|
||||
if (isWorkTime) {
|
||||
makePhoneCall(getRandomPhone());
|
||||
}
|
||||
}
|
||||
|
||||
void sendWhatsApp() {
|
||||
launchCommunication('whatsapp', getRandomPhone(), 'Hello'.tr);
|
||||
}
|
||||
|
||||
void sendEmail() {
|
||||
launchCommunication('email', 'support@intaleqapp.com', 'Hello'.tr);
|
||||
}
|
||||
|
||||
/// SHOW DIALOG (Optional legacy support)
|
||||
void showContactDialog(BuildContext context) {
|
||||
TimeOfDay now = TimeOfDay.now();
|
||||
bool withinHours = _isWithinWorkTime(now);
|
||||
bool withinHours = isWorkTime;
|
||||
|
||||
showCupertinoModalPopup(
|
||||
context: context,
|
||||
@@ -44,7 +64,6 @@ class ContactUsController extends GetxController {
|
||||
title: Text('Contact Us'.tr),
|
||||
message: Text('Choose a contact option'.tr),
|
||||
actions: <Widget>[
|
||||
/// 📞 CALL (RANDOM) — ONLY DURING WORK HOURS
|
||||
if (withinHours)
|
||||
CupertinoActionSheetAction(
|
||||
child: Row(
|
||||
@@ -55,12 +74,10 @@ class ContactUsController extends GetxController {
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
final phone = getRandomPhone();
|
||||
makePhoneCall(phone);
|
||||
Navigator.pop(context);
|
||||
makeCall();
|
||||
},
|
||||
),
|
||||
|
||||
/// ⛔ OUTSIDE WORK HOURS — SHOW INFO
|
||||
if (!withinHours)
|
||||
CupertinoActionSheetAction(
|
||||
child: Text(
|
||||
@@ -70,8 +87,6 @@ class ContactUsController extends GetxController {
|
||||
),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
|
||||
/// 💬 WHATSAPP (RANDOM)
|
||||
CupertinoActionSheetAction(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
||||
@@ -84,20 +99,18 @@ class ContactUsController extends GetxController {
|
||||
],
|
||||
),
|
||||
onPressed: () {
|
||||
final phone = getRandomPhone();
|
||||
launchCommunication('whatsapp', phone, 'Hello'.tr);
|
||||
Navigator.pop(context);
|
||||
sendWhatsApp();
|
||||
},
|
||||
),
|
||||
|
||||
/// 📧 EMAIL
|
||||
CupertinoActionSheetAction(
|
||||
child: Text('Send Email'.tr),
|
||||
onPressed: () => launchCommunication(
|
||||
'email', 'support@intaleqapp.com', 'Hello'.tr),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
sendEmail();
|
||||
},
|
||||
),
|
||||
],
|
||||
|
||||
/// ❌ CANCEL BUTTON
|
||||
cancelButton: CupertinoActionSheetAction(
|
||||
child: Text('Cancel'.tr),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
|
||||
@@ -611,8 +611,10 @@ class MapPassengerController extends GetxController {
|
||||
if (response.statusCode == 200) {
|
||||
final responseData = json.decode(response.body);
|
||||
|
||||
if (responseData['code'] == 'Ok' || responseData['routes'] != null) {
|
||||
var routeData = responseData['routes'][0];
|
||||
// Support both old format (routes[0]) and new SaaS format (top-level)
|
||||
var routeData = responseData['routes'] != null
|
||||
? responseData['routes'][0]
|
||||
: responseData;
|
||||
|
||||
// 2. تحديث المتغيرات (المسافة والوقت)
|
||||
double durationSecondsRaw = (routeData['duration'] as num).toDouble();
|
||||
@@ -634,7 +636,9 @@ class MapPassengerController extends GetxController {
|
||||
'✅ Driver Route Info: $minutes min, ${distanceMeters.toInt()} m');
|
||||
|
||||
// 3. معالجة الرسم (Polyline)
|
||||
String pointsString = routeData['geometry'] ?? "";
|
||||
// SaaS uses 'points', OSRM uses 'geometry'
|
||||
String pointsString =
|
||||
routeData['points'] ?? routeData['geometry'] ?? "";
|
||||
if (pointsString.isNotEmpty) {
|
||||
List<LatLng> decodedPoints =
|
||||
await compute(decodePolylineIsolate, pointsString);
|
||||
@@ -658,7 +662,6 @@ class MapPassengerController extends GetxController {
|
||||
|
||||
update(); // تحديث واحد للكل
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
Log.print('❌ Error calculating driver route: $e');
|
||||
}
|
||||
@@ -6478,12 +6481,10 @@ Intaleq Team''';
|
||||
double lngDest = double.parse(coordDestination[1]);
|
||||
myDestination = LatLng(latDest, lngDest);
|
||||
|
||||
// ── 2. Routing Decision: Hybrid Strategy ───────────────────────────
|
||||
final bool isSaaSRequest = activeMenuWaypointCount == 0;
|
||||
// ── 2. Unified SaaS Routing Strategy ──────────────────────────
|
||||
final bool isSaaSRequest = true;
|
||||
Uri uri;
|
||||
|
||||
if (isSaaSRequest) {
|
||||
// Mapping SaaS format: Query Parameters
|
||||
var originCoords = origin.split(',');
|
||||
final Map<String, String> queryParams = {
|
||||
'fromLat': originCoords[0].trim(),
|
||||
@@ -6491,16 +6492,17 @@ Intaleq Team''';
|
||||
'toLat': latDest.toString(),
|
||||
'toLng': lngDest.toString(),
|
||||
};
|
||||
uri =
|
||||
Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams);
|
||||
} else {
|
||||
// Legacy OSRM format for multi-waypoint support
|
||||
var originCoords = origin.split(',');
|
||||
String waypointCoords = _buildOsrmWaypointCoords();
|
||||
String osrmUrl =
|
||||
'${AppLink.routesOsm}/route/v1/driving/${originCoords[1]},${originCoords[0]}$waypointCoords;$lngDest,$latDest';
|
||||
uri = Uri.parse('$osrmUrl?steps=false&overview=full');
|
||||
|
||||
// Add multi-stop waypoints to the query parameters
|
||||
for (int i = 0; i < activeMenuWaypointCount; i++) {
|
||||
final wp = menuWaypoints[i];
|
||||
if (wp != null) {
|
||||
queryParams['stop${i + 1}Lat'] = wp.latitude.toString();
|
||||
queryParams['stop${i + 1}Lng'] = wp.longitude.toString();
|
||||
}
|
||||
}
|
||||
|
||||
uri = Uri.parse(AppLink.mapSaasRoute).replace(queryParameters: queryParams);
|
||||
|
||||
Log.print(
|
||||
'Requesting Route URI (${isSaaSRequest ? "SaaS" : "OSRM"}, Attempt: ${attemptCount + 1}): $uri');
|
||||
|
||||
@@ -7,6 +7,7 @@ import 'package:Intaleq/controller/functions/crud.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:get/get.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:share_plus/share_plus.dart';
|
||||
|
||||
import '../../../main.dart';
|
||||
@@ -107,7 +108,37 @@ ${'Download the Intaleq app now and enjoy your ride!'.tr}
|
||||
/// tried to access the first phone number of a contact that had none.
|
||||
Future<void> pickContacts() async {
|
||||
try {
|
||||
if (await FlutterContacts.requestPermission(readonly: true)) {
|
||||
// 1. Check current permission status using permission_handler for better control
|
||||
PermissionStatus status = await Permission.contacts.status;
|
||||
|
||||
// 2. If status is permanently denied, direct user to settings
|
||||
if (status.isPermanentlyDenied) {
|
||||
Get.defaultDialog(
|
||||
title: 'Permission Required'.tr,
|
||||
middleText:
|
||||
'Contact permission is permanently denied. Please enable it in settings to continue.'
|
||||
.tr,
|
||||
textConfirm: 'Settings'.tr,
|
||||
textCancel: 'Cancel'.tr,
|
||||
confirmTextColor: Colors.white,
|
||||
onConfirm: () {
|
||||
openAppSettings();
|
||||
Get.back();
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 3. Request permission if not already granted
|
||||
if (!status.isGranted) {
|
||||
status = await Permission.contacts.request();
|
||||
}
|
||||
|
||||
// 4. Proceed if granted
|
||||
if (status.isGranted) {
|
||||
// Also call flutter_contacts requestPermission to ensure it's synced (internal state)
|
||||
await FlutterContacts.requestPermission(readonly: true);
|
||||
|
||||
final List<Contact> allContacts =
|
||||
await FlutterContacts.getContacts(withProperties: true);
|
||||
final int totalContactsOnDevice = allContacts.length;
|
||||
|
||||
@@ -4,6 +4,18 @@ class MyTranslation extends Translations {
|
||||
@override
|
||||
Map<String, Map<String, String>> get keys => {
|
||||
"ar": {
|
||||
"About Intaleq": "حول انطلق",
|
||||
"Chat with us anytime": "دردش معنا في أي وقت",
|
||||
"Direct talk with our team": "تحدث مباشرة مع فريقنا",
|
||||
"Email Support": "الدعم عبر البريد الإلكتروني",
|
||||
"For official inquiries": "للاستفسارات الرسمية",
|
||||
"Intaleq Support": "دعم انطلق",
|
||||
"Reach out to us via": "تواصل معنا عبر",
|
||||
"Support is Away": "الدعم غير متاح حالياً",
|
||||
"Support is currently Online": "الدعم متاح حالياً",
|
||||
"Voice Call": "اتصال صوتي",
|
||||
"We're here to help you 24/7": "نحن هنا لمساعدتكم على مدار الساعة",
|
||||
"Working Hours:": "ساعات العمل:",
|
||||
"1 Passenger": "راكب واحد",
|
||||
"2 Passengers": "راكبين",
|
||||
"3 Passengers": "٣ ركاب",
|
||||
@@ -7263,6 +7275,18 @@ class MyTranslation extends Translations {
|
||||
"Change Country": "تبديل الدولة",
|
||||
},
|
||||
"tr": {
|
||||
"About Intaleq": "Intaleq Hakkında",
|
||||
"Chat with us anytime": "İstediğiniz zaman bizimle sohbet edin",
|
||||
"Direct talk with our team": "Ekibimizle doğrudan görüşün",
|
||||
"Email Support": "E-posta Desteği",
|
||||
"For official inquiries": "Resmi sorular için",
|
||||
"Intaleq Support": "Intaleq Destek",
|
||||
"Reach out to us via": "Bize şuradan ulaşın",
|
||||
"Support is Away": "Destek şu an uzakta",
|
||||
"Support is currently Online": "Destek şu an çevrimiçi",
|
||||
"Voice Call": "Sesli Arama",
|
||||
"We're here to help you 24/7": "7/24 size yardımcı olmaya hazırız",
|
||||
"Working Hours:": "Çalışma Saatleri:",
|
||||
"1 Passenger": "1 Passenger",
|
||||
"2 Passengers": "2 Passengers",
|
||||
"3 Passengers": "3 Passengers",
|
||||
@@ -13840,6 +13864,18 @@ class MyTranslation extends Translations {
|
||||
"Change Country": "Cambiar país",
|
||||
},
|
||||
"fa": {
|
||||
"About Intaleq": "درباره انطلق",
|
||||
"Chat with us anytime": "هر زمان با ما چت کنید",
|
||||
"Direct talk with our team": "صحبت مستقیم با تیم ما",
|
||||
"Email Support": "پشتیبانی ایمیلی",
|
||||
"For official inquiries": "برای استعلامهای رسمی",
|
||||
"Intaleq Support": "پشتیبانی انطلق",
|
||||
"Reach out to us via": "با ما در تماس باشید از طریق",
|
||||
"Support is Away": "پشتیبانی در حال حاضر در دسترس نیست",
|
||||
"Support is currently Online": "پشتیبانی در حال حاضر آنلاین است",
|
||||
"Voice Call": "تماس صوتی",
|
||||
"We're here to help you 24/7": "ما ۷/۲۴ برای کمک به شما آمادهایم",
|
||||
"Working Hours:": "ساعات کاری:",
|
||||
"1 Passenger": "1 Passenger",
|
||||
"2 Passengers": "2 Passengers",
|
||||
"3 Passengers": "3 Passengers",
|
||||
@@ -16826,6 +16862,18 @@ class MyTranslation extends Translations {
|
||||
"Change Country": "Αλλαγή Χώρας",
|
||||
},
|
||||
"ur": {
|
||||
"About Intaleq": "انطلق کے بارے میں",
|
||||
"Chat with us anytime": "کسی भी समय हमसे चैट करें",
|
||||
"Direct talk with our team": "ہماری ٹیم سے براہ راست بات کریں",
|
||||
"Email Support": "ای میل سپورٹ",
|
||||
"For official inquiries": "سرکاری استفسارات کے لیے",
|
||||
"Intaleq Support": "انطلق سپورٹ",
|
||||
"Reach out to us via": "ہم سے رابطہ کریں بذریعہ",
|
||||
"Support is Away": "سپورٹ اب دستیاب نہیں ہے",
|
||||
"Support is currently Online": "سپورٹ اب آن لائن ہے",
|
||||
"Voice Call": "صوتی کال",
|
||||
"We're here to help you 24/7": "ہم چوبیس گھنٹے آپ کی مدد کے لیے حاضر ہیں",
|
||||
"Working Hours:": "کام کے اوقات:",
|
||||
"1 Passenger": "1 Passenger",
|
||||
"2 Passengers": "2 Passengers",
|
||||
"3 Passengers": "3 Passengers",
|
||||
@@ -18375,6 +18423,18 @@ class MyTranslation extends Translations {
|
||||
"Change Country": "ملک تبدیل کریں",
|
||||
},
|
||||
"hi": {
|
||||
"About Intaleq": "Intaleq के बारे में",
|
||||
"Chat with us anytime": "हमसे कभी भी चैट करें",
|
||||
"Direct talk with our team": "हमारी टीम से सीधे बात करें",
|
||||
"Email Support": "ईमेल सहायता",
|
||||
"For official inquiries": "आधिकारिक पूछताछ के लिए",
|
||||
"Intaleq Support": "Intaleq सहायता",
|
||||
"Reach out to us via": "हमसे संपर्क करें",
|
||||
"Support is Away": "सहायता अभी उपलब्ध नहीं है",
|
||||
"Support is currently Online": "सहायता अभी ऑनलाइन है",
|
||||
"Voice Call": "वॉइस कॉल",
|
||||
"We're here to help you 24/7": "हम आपकी सहायता के लिए 24/7 उपलब्ध हैं",
|
||||
"Working Hours:": "कार्य समय:",
|
||||
"1 Passenger": "1 Passenger",
|
||||
"2 Passengers": "2 Passengers",
|
||||
"3 Passengers": "3 Passengers",
|
||||
@@ -19923,6 +19983,18 @@ class MyTranslation extends Translations {
|
||||
"Change Country": "देश बदलें",
|
||||
},
|
||||
"ru": {
|
||||
"About Intaleq": "Об Intaleq",
|
||||
"Chat with us anytime": "Пишите нам в любое время",
|
||||
"Direct talk with our team": "Прямой разговор с нашей командой",
|
||||
"Email Support": "Поддержка по электронной почте",
|
||||
"For official inquiries": "Для официальных запросов",
|
||||
"Intaleq Support": "Поддержка Intaleq",
|
||||
"Reach out to us via": "Свяжитесь с нами через",
|
||||
"Support is Away": "Поддержка сейчас недоступна",
|
||||
"Support is currently Online": "Поддержка сейчас онлайн",
|
||||
"Voice Call": "Голосовой звонок",
|
||||
"We're here to help you 24/7": "Мы готовы помочь вам 24/7",
|
||||
"Working Hours:": "Рабочие часы:",
|
||||
"1 Passenger": "1 Passenger",
|
||||
"2 Passengers": "2 Passengers",
|
||||
"3 Passengers": "3 Passengers",
|
||||
@@ -21375,6 +21447,18 @@ class MyTranslation extends Translations {
|
||||
"Change Country": "Сменить страну",
|
||||
},
|
||||
"it": {
|
||||
"About Intaleq": "Informazioni su Intaleq",
|
||||
"Chat with us anytime": "Chatta con noi in qualsiasi momento",
|
||||
"Direct talk with our team": "Parla direttamente con il nostro team",
|
||||
"Email Support": "Supporto via email",
|
||||
"For official inquiries": "Per richieste ufficiali",
|
||||
"Intaleq Support": "Supporto Intaleq",
|
||||
"Reach out to us via": "Contattaci tramite",
|
||||
"Support is Away": "Il supporto è attualmente assente",
|
||||
"Support is currently Online": "Il supporto è attualmente online",
|
||||
"Voice Call": "Chiamata vocale",
|
||||
"We're here to help you 24/7": "Siamo qui per aiutarti 24/7",
|
||||
"Working Hours:": "Orario di lavoro:",
|
||||
"1 Passenger": "1 Passenger",
|
||||
"2 Passengers": "2 Passengers",
|
||||
"3 Passengers": "3 Passengers",
|
||||
|
||||
1
lib/env/env.dart
vendored
1
lib/env/env.dart
vendored
@@ -1,3 +1,4 @@
|
||||
// Force rebuild to pick up late coordinates from .env
|
||||
import 'package:envied/envied.dart';
|
||||
|
||||
part 'env.g.dart';
|
||||
|
||||
26290
lib/env/env.g.dart
vendored
26290
lib/env/env.g.dart
vendored
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@ class Log {
|
||||
Log._();
|
||||
|
||||
static void print(String value, {StackTrace? stackTrace}) {
|
||||
developer.log(value, name: 'LOG', stackTrace: stackTrace);
|
||||
// developer.log(value, name: 'LOG', stackTrace: stackTrace);
|
||||
}
|
||||
|
||||
static Object? inspect(Object? object) {
|
||||
|
||||
@@ -35,118 +35,350 @@ class LoginPage extends StatelessWidget {
|
||||
if (box.read(BoxName.agreeTerms) != 'agreed')
|
||||
_buildAgreementPage(context, controller)
|
||||
else if (box.read(BoxName.locationPermission) != 'true')
|
||||
_buildLocationPermissionDialog(controller)
|
||||
// else if (box.read(BoxName.isTest).toString() == '0')
|
||||
// buildEmailPasswordForm(controller)
|
||||
_buildLocationPermissionDialog(context, controller)
|
||||
else
|
||||
// _buildLoginContent(controller, authController),
|
||||
PhoneNumberScreen()
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAgreementPage(BuildContext context, LoginController controller) {
|
||||
// This UI can be identical to the one in LoginPage for consistency.
|
||||
// I am reusing the improved design from the previous request.
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// SHARED HELPERS
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Subtle geometric background — two soft circles, no heavy blur needed.
|
||||
Widget _buildBackground(BuildContext context) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
return Stack(
|
||||
children: [
|
||||
Icon(Icons.policy_outlined, size: 80, color: AppColor.primaryColor),
|
||||
// Base gradient
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: isDark
|
||||
? [const Color(0xFF0D0D14), const Color(0xFF161622)]
|
||||
: [const Color(0xFFF8F9FF), const Color(0xFFEFF1FB)],
|
||||
),
|
||||
),
|
||||
),
|
||||
// Top-right accent circle
|
||||
Positioned(
|
||||
top: -80,
|
||||
right: -60,
|
||||
child: Container(
|
||||
width: 260,
|
||||
height: 260,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
AppColor.primaryColor.withOpacity(isDark ? 0.18 : 0.12),
|
||||
AppColor.primaryColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
// Bottom-left accent circle
|
||||
Positioned(
|
||||
bottom: -100,
|
||||
left: -80,
|
||||
child: Container(
|
||||
width: 320,
|
||||
height: 320,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: RadialGradient(
|
||||
colors: [
|
||||
AppColor.primaryColor.withOpacity(isDark ? 0.12 : 0.08),
|
||||
AppColor.primaryColor.withOpacity(0.0),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// Glassy card container used across screens.
|
||||
Widget _glassCard({
|
||||
required Widget child,
|
||||
required bool isDark,
|
||||
EdgeInsets padding = const EdgeInsets.all(20),
|
||||
double radius = 20,
|
||||
}) {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(radius),
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(0.05)
|
||||
: Colors.white.withOpacity(0.75),
|
||||
border: Border.all(
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(0.08)
|
||||
: Colors.white.withOpacity(0.9),
|
||||
width: 1,
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDark
|
||||
? Colors.black.withOpacity(0.3)
|
||||
: Colors.black.withOpacity(0.06),
|
||||
blurRadius: 24,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
padding: padding,
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
|
||||
/// Pill-shaped icon badge with gradient background.
|
||||
Widget _iconBadge(IconData icon, bool isDark) {
|
||||
return Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
AppColor.primaryColor,
|
||||
AppColor.primaryColor.withOpacity(0.7),
|
||||
],
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: AppColor.primaryColor.withOpacity(0.35),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 8),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Icon(icon, color: Colors.white, size: 36),
|
||||
);
|
||||
}
|
||||
|
||||
/// Section divider line.
|
||||
Widget _divider(bool isDark) => Container(
|
||||
height: 1,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
Colors.transparent,
|
||||
isDark
|
||||
? Colors.white.withOpacity(0.1)
|
||||
: Colors.black.withOpacity(0.08),
|
||||
Colors.transparent,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// AGREEMENT PAGE
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
Widget _buildAgreementPage(BuildContext context, LoginController controller) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textMain = isDark ? Colors.white : const Color(0xFF1A1A2E);
|
||||
final textSub = isDark ? Colors.white60 : const Color(0xFF6B7280);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
_buildBackground(context),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
// ── Header ──────────────────────────────────────────────
|
||||
const SizedBox(height: 20),
|
||||
Text("passenger agreement".tr,
|
||||
textAlign: TextAlign.center, style: AppStyle.headTitle2),
|
||||
const SizedBox(height: 30),
|
||||
_iconBadge(Icons.policy_outlined, isDark),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"passenger agreement".tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle2.copyWith(
|
||||
color: textMain,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.3,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Subtitle with link
|
||||
RichText(
|
||||
textAlign: TextAlign.center,
|
||||
text: TextSpan(
|
||||
style: AppStyle.title.copyWith(height: 1.5),
|
||||
style: AppStyle.title.copyWith(
|
||||
height: 1.6,
|
||||
color: textSub,
|
||||
fontSize: 13.5,
|
||||
),
|
||||
children: [
|
||||
TextSpan(
|
||||
text:
|
||||
"To become a passenger, you must review and agree to the "
|
||||
.tr),
|
||||
.tr,
|
||||
),
|
||||
TextSpan(
|
||||
text: 'Terms of Use'.tr,
|
||||
style: TextStyle(
|
||||
decoration: TextDecoration.underline,
|
||||
color: AppColor.cyanBlue,
|
||||
fontWeight: FontWeight.bold),
|
||||
decorationColor: AppColor.primaryColor,
|
||||
color: AppColor.primaryColor,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
recognizer: TapGestureRecognizer()
|
||||
..onTap = () {
|
||||
launchUrl(Uri.parse(
|
||||
'https://intaleq.xyz/intaleq/privacy_policy.php'));
|
||||
}),
|
||||
},
|
||||
),
|
||||
TextSpan(text: " and acknowledge our Privacy Policy.".tr),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_divider(isDark),
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── Policy scroll area ──────────────────────────────────
|
||||
Expanded(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Colors.grey.shade300),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: _glassCard(
|
||||
isDark: isDark,
|
||||
padding: EdgeInsets.zero,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: HtmlWidget(box.read(BoxName.lang).toString() == 'ar'
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.all(18),
|
||||
child: HtmlWidget(
|
||||
box.read(BoxName.lang).toString() == 'ar'
|
||||
? AppInformation.privacyPolicyArabic
|
||||
: AppInformation.privacyPolicy),
|
||||
: AppInformation.privacyPolicy,
|
||||
textStyle: TextStyle(
|
||||
color: textSub,
|
||||
fontSize: 13,
|
||||
height: 1.7,
|
||||
),
|
||||
),
|
||||
),
|
||||
CheckboxListTile(
|
||||
title: Text('I Agree'.tr, style: AppStyle.title),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
_divider(isDark),
|
||||
|
||||
// ── Checkbox row ────────────────────────────────────────
|
||||
_glassCard(
|
||||
isDark: isDark,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
child: CheckboxListTile(
|
||||
title: Text(
|
||||
'I Agree'.tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: textMain,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
value: controller.isAgreeTerms,
|
||||
onChanged: (value) => controller.changeAgreeTerm(),
|
||||
activeColor: AppColor.primaryColor,
|
||||
checkColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(6)),
|
||||
controlAffinity: ListTileControlAffinity.leading,
|
||||
contentPadding: EdgeInsets.zero,
|
||||
dense: true,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: MyElevatedButton(
|
||||
title: 'Continue'.tr,
|
||||
),
|
||||
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// ── CTA Button ──────────────────────────────────────────
|
||||
_buildPrimaryButton(
|
||||
label: 'Continue'.tr,
|
||||
enabled: controller.isAgreeTerms,
|
||||
onPressed: controller.isAgreeTerms
|
||||
? () => controller.saveAgreementTerms()
|
||||
: () {},
|
||||
isDark: isDark,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget buildEmailPasswordForm(LoginController controller) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.2),
|
||||
spreadRadius: 2,
|
||||
blurRadius: 5,
|
||||
offset: const Offset(0, 3),
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// EMAIL / PASSWORD FORM
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
Widget buildEmailPasswordForm(
|
||||
BuildContext context, LoginController controller) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textMain = isDark ? Colors.white : const Color(0xFF1A1A2E);
|
||||
final textSub = isDark ? Colors.white60 : const Color(0xFF6B7280);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
_buildBackground(context),
|
||||
SafeArea(
|
||||
child: Center(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
// Logo / badge
|
||||
Center(child: _iconBadge(Icons.lock_outline_rounded, isDark)),
|
||||
const SizedBox(height: 20),
|
||||
Text(
|
||||
'Welcome Back'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.headTitle2.copyWith(
|
||||
color: textMain,
|
||||
fontSize: 26,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
Text(
|
||||
'Sign in to continue'.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style:
|
||||
AppStyle.title.copyWith(color: textSub, fontSize: 14),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
_glassCard(
|
||||
isDark: isDark,
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Form(
|
||||
key: controller.formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
TextFormField(
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
// Email field
|
||||
_buildTextField(
|
||||
controller: controller.emailController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Email'.tr,
|
||||
hintText: 'Your email address'.tr,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
label: 'Email'.tr,
|
||||
hint: 'Your email address'.tr,
|
||||
icon: Icons.email_outlined,
|
||||
isDark: isDark,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
validator: (value) => value == null ||
|
||||
value.isEmpty ||
|
||||
!value.contains('@') ||
|
||||
@@ -155,67 +387,323 @@ class LoginPage extends StatelessWidget {
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextFormField(
|
||||
obscureText: true,
|
||||
|
||||
// Password field
|
||||
_buildTextField(
|
||||
controller: controller.passwordController,
|
||||
decoration: InputDecoration(
|
||||
labelText: 'Password'.tr,
|
||||
hintText: 'Your password'.tr,
|
||||
border: const OutlineInputBorder(),
|
||||
),
|
||||
label: 'Password'.tr,
|
||||
hint: 'Your password'.tr,
|
||||
icon: Icons.lock_outline,
|
||||
isDark: isDark,
|
||||
obscureText: true,
|
||||
validator: (value) => value == null || value.isEmpty
|
||||
? 'Enter your password'.tr
|
||||
: null,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
const SizedBox(height: 28),
|
||||
|
||||
GetBuilder<LoginController>(
|
||||
builder: (controller) => controller.isloading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: ElevatedButton(
|
||||
? Center(
|
||||
child: SizedBox(
|
||||
width: 28,
|
||||
height: 28,
|
||||
child: CircularProgressIndicator(
|
||||
color: AppColor.primaryColor,
|
||||
strokeWidth: 2.5,
|
||||
),
|
||||
),
|
||||
)
|
||||
: _buildPrimaryButton(
|
||||
label: 'Submit'.tr,
|
||||
enabled: true,
|
||||
isDark: isDark,
|
||||
onPressed: () {
|
||||
if (controller.formKey.currentState!.validate()) {
|
||||
if (controller.formKey.currentState!
|
||||
.validate()) {
|
||||
controller.login();
|
||||
}
|
||||
},
|
||||
child: Text('Submit'.tr),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// LOCATION PERMISSION PAGE
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
Widget _buildLocationPermissionDialog(
|
||||
BuildContext context, LoginController controller) {
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
final textMain = isDark ? Colors.white : const Color(0xFF1A1A2E);
|
||||
final textSub = isDark ? Colors.white60 : const Color(0xFF6B7280);
|
||||
|
||||
return Stack(
|
||||
children: [
|
||||
_buildBackground(context),
|
||||
SafeArea(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
const Spacer(flex: 2),
|
||||
|
||||
// Animated-look stacked circles around icon
|
||||
Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
// Outer glow ring
|
||||
Container(
|
||||
width: 140,
|
||||
height: 140,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColor.primaryColor.withOpacity(0.08),
|
||||
border: Border.all(
|
||||
color: AppColor.primaryColor.withOpacity(0.2),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Mid ring
|
||||
Container(
|
||||
width: 108,
|
||||
height: 108,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColor.primaryColor.withOpacity(0.12),
|
||||
border: Border.all(
|
||||
color: AppColor.primaryColor.withOpacity(0.3),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Core badge
|
||||
_iconBadge(Icons.location_on_outlined, isDark),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: 36),
|
||||
|
||||
Text(
|
||||
'Enable Location Access'.tr,
|
||||
style: AppStyle.headTitle2.copyWith(
|
||||
color: textMain,
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w700,
|
||||
letterSpacing: 0.2,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'We need your location to find nearby drivers for pickups and drop-offs.'
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title.copyWith(
|
||||
color: textSub,
|
||||
fontSize: 14.5,
|
||||
height: 1.6,
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// Feature chips row
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
_featureChip(
|
||||
Icons.speed_outlined, 'Fast matching'.tr, isDark),
|
||||
const SizedBox(width: 10),
|
||||
_featureChip(Icons.shield_outlined, 'Secure'.tr, isDark),
|
||||
const SizedBox(width: 10),
|
||||
_featureChip(Icons.near_me_outlined, 'Nearby'.tr, isDark),
|
||||
],
|
||||
),
|
||||
|
||||
const Spacer(flex: 3),
|
||||
|
||||
_buildPrimaryButton(
|
||||
label: 'Next'.tr,
|
||||
enabled: true,
|
||||
isDark: isDark,
|
||||
onPressed: () async =>
|
||||
await controller.getLocationPermission(),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
// SHARED SMALL WIDGETS
|
||||
// ─────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Reusable styled text field.
|
||||
Widget _buildTextField({
|
||||
required TextEditingController controller,
|
||||
required String label,
|
||||
required String hint,
|
||||
required IconData icon,
|
||||
required bool isDark,
|
||||
bool obscureText = false,
|
||||
TextInputType? keyboardType,
|
||||
String? Function(String?)? validator,
|
||||
}) {
|
||||
final fill = isDark ? Colors.white.withOpacity(0.05) : Colors.white;
|
||||
final border =
|
||||
isDark ? Colors.white.withOpacity(0.1) : const Color(0xFFE5E7EB);
|
||||
|
||||
return TextFormField(
|
||||
controller: controller,
|
||||
obscureText: obscureText,
|
||||
keyboardType: keyboardType,
|
||||
style: TextStyle(
|
||||
color: isDark ? Colors.white : const Color(0xFF1A1A2E),
|
||||
fontSize: 14.5,
|
||||
),
|
||||
decoration: InputDecoration(
|
||||
labelText: label,
|
||||
hintText: hint,
|
||||
prefixIcon: Icon(icon,
|
||||
size: 20, color: isDark ? Colors.white38 : const Color(0xFF9CA3AF)),
|
||||
labelStyle: TextStyle(
|
||||
color: isDark ? Colors.white38 : const Color(0xFF9CA3AF),
|
||||
fontSize: 13.5,
|
||||
),
|
||||
hintStyle: TextStyle(
|
||||
color: isDark ? Colors.white24 : const Color(0xFFD1D5DB),
|
||||
),
|
||||
filled: true,
|
||||
fillColor: fill,
|
||||
contentPadding:
|
||||
const EdgeInsets.symmetric(horizontal: 16, vertical: 16),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide(color: border, width: 1),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: BorderSide(color: AppColor.primaryColor, width: 1.5),
|
||||
),
|
||||
errorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: const BorderSide(color: Color(0xFFEF4444), width: 1),
|
||||
),
|
||||
focusedErrorBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
borderSide: const BorderSide(color: Color(0xFFEF4444), width: 1.5),
|
||||
),
|
||||
),
|
||||
validator: validator,
|
||||
);
|
||||
}
|
||||
|
||||
/// Full-width gradient primary button.
|
||||
Widget _buildPrimaryButton({
|
||||
required String label,
|
||||
required bool enabled,
|
||||
required VoidCallback onPressed,
|
||||
required bool isDark,
|
||||
}) {
|
||||
return SizedBox(
|
||||
width: double.infinity,
|
||||
height: 54,
|
||||
child: DecoratedBox(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
gradient: enabled
|
||||
? LinearGradient(
|
||||
begin: Alignment.centerLeft,
|
||||
end: Alignment.centerRight,
|
||||
colors: [
|
||||
AppColor.primaryColor,
|
||||
AppColor.primaryColor.withOpacity(0.8),
|
||||
],
|
||||
)
|
||||
: null,
|
||||
color: enabled
|
||||
? null
|
||||
: (isDark ? Colors.white12 : const Color(0xFFE5E7EB)),
|
||||
boxShadow: enabled
|
||||
? [
|
||||
BoxShadow(
|
||||
color: AppColor.primaryColor.withOpacity(0.32),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6),
|
||||
)
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: ElevatedButton(
|
||||
onPressed: onPressed,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.transparent,
|
||||
shadowColor: Colors.transparent,
|
||||
shape:
|
||||
RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
||||
),
|
||||
child: Text(
|
||||
label,
|
||||
style: TextStyle(
|
||||
color: enabled
|
||||
? Colors.white
|
||||
: (isDark ? Colors.white38 : const Color(0xFF9CA3AF)),
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 15,
|
||||
letterSpacing: 0.4,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLocationPermissionDialog(LoginController controller) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(32),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
/// Small chip used on location page.
|
||||
Widget _featureChip(IconData icon, String text, bool isDark) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(0.06)
|
||||
: Colors.white.withOpacity(0.8),
|
||||
border: Border.all(
|
||||
color: isDark
|
||||
? Colors.white.withOpacity(0.1)
|
||||
: Colors.black.withOpacity(0.07),
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(Icons.location_on, size: 60, color: AppColor.primaryColor),
|
||||
const SizedBox(height: 20),
|
||||
Icon(icon, size: 14, color: AppColor.primaryColor),
|
||||
const SizedBox(width: 5),
|
||||
Text(
|
||||
'Enable Location Access'.tr,
|
||||
style: AppStyle.headTitle2,
|
||||
textAlign: TextAlign.center,
|
||||
text,
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isDark ? Colors.white70 : const Color(0xFF374151),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'We need your location to find nearby drivers for pickups and drop-offs.'
|
||||
.tr,
|
||||
textAlign: TextAlign.center,
|
||||
style: AppStyle.title,
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
ElevatedButton(
|
||||
onPressed: () async => await controller.getLocationPermission(),
|
||||
child: Text('Next'.tr),
|
||||
// child: Text('Allow Location Access'.tr),
|
||||
),
|
||||
// TextButton(
|
||||
// onPressed: () => openAppSettings(),
|
||||
// child: Text('Open Settings'.tr),
|
||||
// ),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,102 +1,360 @@
|
||||
import 'package:Intaleq/constant/colors.dart';
|
||||
import 'package:Intaleq/constant/style.dart';
|
||||
// ignore: unused_import
|
||||
import 'package:Intaleq/controller/functions/launch.dart';
|
||||
import 'package:Intaleq/views/widgets/my_scafold.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_font_icons/flutter_font_icons.dart';
|
||||
import 'package:get/get.dart';
|
||||
|
||||
import '../../../controller/functions/tts.dart';
|
||||
import '../../../controller/home/contact_us_controller.dart';
|
||||
import '../../widgets/elevated_btn.dart';
|
||||
|
||||
class ContactUsPage extends StatelessWidget {
|
||||
ContactUsPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Get.put(ContactUsController());
|
||||
return GetBuilder<ContactUsController>(builder: (controller) {
|
||||
final controller = Get.put(ContactUsController());
|
||||
final isDark = Theme.of(context).brightness == Brightness.dark;
|
||||
|
||||
return MyScafolld(
|
||||
title: "Contact Us".tr,
|
||||
isleading: true,
|
||||
body: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: ListView(
|
||||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
// Background subtle gradient/shape
|
||||
Positioned.fill(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: isDark
|
||||
? [const Color(0xFF0D0D14), const Color(0xFF161622)]
|
||||
: [const Color(0xFFF8F9FF), const Color(0xFFEFF1FB)],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
physics: const BouncingScrollPhysics(),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
// ── Hero Section ──────────────────────────────────────────
|
||||
_buildHeroSection(isDark),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Availability Status ────────────────────────────────────
|
||||
_buildAvailabilityStatus(controller, isDark),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// ── Support Actions ────────────────────────────────────────
|
||||
Text(
|
||||
"Reach out to us via".tr,
|
||||
style: AppStyle.title.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildContactCards(controller, isDark),
|
||||
const SizedBox(height: 32),
|
||||
|
||||
// ── About Section ──────────────────────────────────────────
|
||||
_buildAboutSection(isDark),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildHeroSection(bool isDark) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.white.withOpacity(0.05) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(
|
||||
color: isDark ? Colors.white12 : Colors.black.withOpacity(0.05),
|
||||
),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: isDark ? Colors.black26 : Colors.black.withOpacity(0.03),
|
||||
blurRadius: 20,
|
||||
offset: const Offset(0, 10),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
ClipRRect(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
child: Image.asset('assets/images/logo.gif')),
|
||||
IconButton(
|
||||
onPressed: () async {
|
||||
Get.find<TextToSpeechController>().speakText(
|
||||
'Intaleq is the safest and most reliable ride-sharing app designed especially for passengers in Syria. We provide a comfortable, respectful, and affordable riding experience with features that prioritize your safety and convenience. Our trusted captains are verified, insured, and supported by regular car maintenance carried out by top engineers. We also offer on-road support services to make sure every trip is smooth and worry-free. With Intaleq, you enjoy quality, safety, and peace of mind—every time you ride.'
|
||||
.tr);
|
||||
},
|
||||
icon: const Icon(Icons.headphones),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
'Intaleq is the safest and most reliable ride-sharing app designed especially for passengers in Syria. We provide a comfortable, respectful, and affordable riding experience with features that prioritize your safety and convenience. Our trusted captains are verified, insured, and supported by regular car maintenance carried out by top engineers. We also offer on-road support services to make sure every trip is smooth and worry-free. With Intaleq, you enjoy quality, safety, and peace of mind—every time you ride.'
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
),
|
||||
Container(
|
||||
decoration: AppStyle.boxDecoration1,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
"You can contact us during working hours from 10:00 - 16:00."
|
||||
.tr,
|
||||
style: AppStyle.title,
|
||||
textAlign: TextAlign.center,
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isDark ? Colors.white10 : Colors.white,
|
||||
border: Border.all(color: AppColor.primaryColor.withOpacity(0.2)),
|
||||
),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: Image.asset('assets/images/logo.gif', height: 80),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
"Intaleq Support".tr,
|
||||
style: AppStyle.headTitle2.copyWith(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isDark ? Colors.white : AppColor.primaryColor,
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => controller.showContactDialog(context),
|
||||
child: const Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
"We're here to help you 24/7".tr,
|
||||
style: TextStyle(
|
||||
color: isDark ? Colors.white54 : Colors.black45,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAvailabilityStatus(ContactUsController controller, bool isDark) {
|
||||
final isOpen = controller.isWorkTime;
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: (isOpen ? AppColor.greenColor : Colors.orange).withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: (isOpen ? AppColor.greenColor : Colors.orange).withOpacity(0.2),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.phone,
|
||||
color: AppColor.blueColor,
|
||||
Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isOpen ? AppColor.greenColor : Colors.orange,
|
||||
),
|
||||
Icon(
|
||||
FontAwesome.whatsapp,
|
||||
child: Icon(
|
||||
isOpen ? Icons.check_circle_outline : Icons.access_time,
|
||||
color: Colors.white,
|
||||
size: 18,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
isOpen ? "Support is currently Online".tr : "Support is Away".tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isOpen ? AppColor.greenColor : Colors.orange,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"${"Working Hours:".tr} ${controller.workHoursString}",
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
color: isDark ? Colors.white60 : Colors.black54,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContactCards(ContactUsController controller, bool isDark) {
|
||||
return Column(
|
||||
children: [
|
||||
_ContactCard(
|
||||
title: "Voice Call".tr,
|
||||
subtitle: "Direct talk with our team".tr,
|
||||
icon: Icons.phone_in_talk_outlined,
|
||||
color: AppColor.primaryColor,
|
||||
isDark: isDark,
|
||||
enabled: controller.isWorkTime,
|
||||
onTap: controller.makeCall,
|
||||
trailing: controller.isWorkTime ? null : Icon(Icons.lock_clock_outlined, size: 20, color: isDark ? Colors.white24 : Colors.black26),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_ContactCard(
|
||||
title: "WhatsApp".tr,
|
||||
subtitle: "Chat with us anytime".tr,
|
||||
icon: FontAwesome.whatsapp,
|
||||
color: AppColor.greenColor,
|
||||
isDark: isDark,
|
||||
onTap: controller.sendWhatsApp,
|
||||
),
|
||||
Icon(
|
||||
Icons.email,
|
||||
const SizedBox(height: 12),
|
||||
_ContactCard(
|
||||
title: "Email Support".tr,
|
||||
subtitle: "For official inquiries".tr,
|
||||
icon: Icons.alternate_email_outlined,
|
||||
color: AppColor.redColor,
|
||||
isDark: isDark,
|
||||
onTap: controller.sendEmail,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildAboutSection(bool isDark) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: isDark ? Colors.white.withOpacity(0.02) : Colors.black.withOpacity(0.02),
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: isDark ? Colors.white10 : Colors.black12),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
"About Intaleq".tr,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 18,
|
||||
color: isDark ? Colors.white : Colors.black87,
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
Get.find<TextToSpeechController>().speakText(
|
||||
'Intaleq is the safest and most reliable ride-sharing app designed especially for passengers in Syria. We provide a comfortable, respectful, and affordable riding experience with features that prioritize your safety and convenience.'
|
||||
.tr,
|
||||
);
|
||||
},
|
||||
icon: Icon(Icons.volume_up_outlined, color: AppColor.primaryColor),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Text(
|
||||
'Intaleq is the safest and most reliable ride-sharing app designed especially for passengers in Syria. We provide a comfortable, respectful, and affordable riding experience with features that prioritize your safety and convenience. Our trusted captains are verified, insured, and supported by regular car maintenance carried out by top engineers. We also offer on-road support services to make sure every trip is smooth and worry-free. With Intaleq, you enjoy quality, safety, and peace of mind—every time you ride.'
|
||||
.tr,
|
||||
style: TextStyle(
|
||||
color: isDark ? Colors.white70 : Colors.black54,
|
||||
fontSize: 14,
|
||||
height: 1.6,
|
||||
),
|
||||
),
|
||||
const SizedBox(
|
||||
height: 30,
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
isleading: true);
|
||||
});
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ContactCard extends StatelessWidget {
|
||||
final String title;
|
||||
final String subtitle;
|
||||
final IconData icon;
|
||||
final Color color;
|
||||
final bool isDark;
|
||||
final bool enabled;
|
||||
final VoidCallback onTap;
|
||||
final Widget? trailing;
|
||||
|
||||
const _ContactCard({
|
||||
required this.title,
|
||||
required this.subtitle,
|
||||
required this.icon,
|
||||
required this.color,
|
||||
required this.isDark,
|
||||
this.enabled = true,
|
||||
required this.onTap,
|
||||
this.trailing,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: enabled ? onTap : null,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: enabled
|
||||
? (isDark ? Colors.white.withOpacity(0.05) : Colors.white)
|
||||
: (isDark ? Colors.white.withOpacity(0.02) : Colors.grey.withOpacity(0.05)),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: enabled
|
||||
? (isDark ? Colors.white12 : Colors.black.withOpacity(0.05))
|
||||
: Colors.transparent,
|
||||
),
|
||||
boxShadow: enabled ? [
|
||||
BoxShadow(
|
||||
color: isDark ? Colors.black12 : Colors.black.withOpacity(0.02),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
)
|
||||
] : null,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: (enabled ? color : Colors.grey).withOpacity(0.12),
|
||||
borderRadius: BorderRadius.circular(14),
|
||||
),
|
||||
child: Icon(icon, color: enabled ? color : Colors.grey, size: 24),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 16,
|
||||
color: enabled
|
||||
? (isDark ? Colors.white : Colors.black87)
|
||||
: Colors.grey,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
subtitle,
|
||||
style: TextStyle(
|
||||
fontSize: 13,
|
||||
color: isDark ? Colors.white38 : Colors.black38,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing ?? Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 14,
|
||||
color: isDark ? Colors.white24 : Colors.black12,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import '../../../constant/links.dart';
|
||||
import '../../../controller/functions/crud.dart';
|
||||
import '../../../controller/functions/tts.dart';
|
||||
import '../../../controller/home/decode_polyline_isolate.dart';
|
||||
import '../../../env/env.dart';
|
||||
import '../../../main.dart';
|
||||
import '../../../print.dart';
|
||||
import 'dart:ui';
|
||||
@@ -719,7 +720,7 @@ class NavigationController extends GetxController
|
||||
try {
|
||||
// 1. Try SaaS first
|
||||
http.Response response = await http.get(saasUri, headers: {
|
||||
'x-api-key': 'intaleq_secret_2026',
|
||||
'x-api-key': Env.mapSaasKey,
|
||||
});
|
||||
bool useSaaS = response.statusCode == 200;
|
||||
|
||||
@@ -947,7 +948,8 @@ class NavigationController extends GetxController
|
||||
final destBytes = await rootBundle.load('assets/images/b.png');
|
||||
|
||||
await mapController!.addImage('car_icon', carBytes.buffer.asUint8List());
|
||||
await mapController!.addImage('start_icon', startBytes.buffer.asUint8List());
|
||||
await mapController!
|
||||
.addImage('start_icon', startBytes.buffer.asUint8List());
|
||||
await mapController!.addImage('dest_icon', destBytes.buffer.asUint8List());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user