Update: 2026-06-14 05:48:58

This commit is contained in:
Hamza-Ayed
2026-06-14 05:48:58 +03:00
parent 2645ed0cf1
commit 8e3b9eca4d
22 changed files with 789 additions and 179 deletions

View File

@@ -117,10 +117,11 @@ class PhoneAuthHelper {
String? email,
}) async {
try {
final fixedPhone = CountryLogic.formatCurrentCountryPhone(phoneNumber);
final response = await CRUD().post(
link: _registerUrl,
payload: {
'phone_number': phoneNumber,
'phone_number': fixedPhone,
'first_name': firstName,
'last_name': lastName,
'email': email ?? '', // Send empty string if null

View File

@@ -19,6 +19,7 @@ import 'package:siro_rider/controller/voice_call_controller.dart';
import '../home/map/ride_lifecycle_controller.dart';
import '../home/map/ride_state.dart';
import 'local_notification.dart';
import '../../views/widgets/mydialoug.dart';
class FirebaseMessagesController extends GetxController {
final fcmToken = FirebaseMessaging.instance;
@@ -171,14 +172,22 @@ class FirebaseMessagesController extends GetxController {
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'ding');
}
passengerDialog(body);
MyDialog().getChatDialog(
title.isNotEmpty ? title : 'message From passenger'.tr,
body,
() {},
);
update();
} else if (category == 'message From Driver') {
// <-- كان 'message From Driver'
if (Platform.isAndroid) {
notificationController.showNotification(title, body, 'ding');
}
passengerDialog(body);
MyDialog().getChatDialog(
title.isNotEmpty ? title : 'message From Driver'.tr,
body,
() {},
);
update();
} else if (category == 'Trip is Begin') {
// <-- كان 'Trip is Begin'

View File

@@ -81,17 +81,55 @@ class CountryLogic {
/// Helper to format phone using the current country in box.
static String formatCurrentCountryPhone(String phone) {
String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)]'), '').trim();
if (cleanPhone.startsWith('+963') || cleanPhone.startsWith('00963')) {
String cleanPhone = phone.replaceAll(RegExp(r'[ \-\(\)\+]'), '').trim();
// 1. Explicit International Code Detection
if (cleanPhone.startsWith('00963')) {
cleanPhone = cleanPhone.replaceFirst('00963', '963');
}
if (cleanPhone.startsWith('00962')) {
cleanPhone = cleanPhone.replaceFirst('00962', '962');
}
if (cleanPhone.startsWith('0020')) {
cleanPhone = cleanPhone.replaceFirst('0020', '20');
}
if (cleanPhone.startsWith('963')) {
return formatPhone(cleanPhone, 'Syria');
}
if (cleanPhone.startsWith('+20') || cleanPhone.startsWith('0020')) {
if (cleanPhone.startsWith('962')) {
return formatPhone(cleanPhone, 'Jordan');
}
if (cleanPhone.startsWith('20')) {
return formatPhone(cleanPhone, 'Egypt');
}
if (cleanPhone.startsWith('+962') || cleanPhone.startsWith('00962')) {
// 2. Local/National Format Detection by Country-Specific Mobile Prefixes
// Jordan: 07x / 7x (9 national digits)
if (cleanPhone.startsWith('07') && cleanPhone.length == 10) {
return formatPhone(cleanPhone, 'Jordan');
}
if (cleanPhone.startsWith('7') && cleanPhone.length == 9) {
return formatPhone(cleanPhone, 'Jordan');
}
// Syria: 09x / 9x (9 national digits)
if (cleanPhone.startsWith('09') && cleanPhone.length == 10) {
return formatPhone(cleanPhone, 'Syria');
}
if (cleanPhone.startsWith('9') && cleanPhone.length == 9) {
return formatPhone(cleanPhone, 'Syria');
}
// Egypt: 01x (10 national digits) / 1x (9 national digits)
if (cleanPhone.startsWith('01') && cleanPhone.length == 11) {
return formatPhone(cleanPhone, 'Egypt');
}
if (cleanPhone.startsWith('1') && cleanPhone.length == 10) {
return formatPhone(cleanPhone, 'Egypt');
}
// 3. Fallback: Default to current user's country code saved in box
final country = box.read(BoxName.countryCode) ?? 'Jordan';
return formatPhone(cleanPhone, country);
}

View File

@@ -9,7 +9,7 @@ void showInBrowser(String url) async {
} else {}
}
Future<void> makePhoneCall(String phoneNumber) async {
String cleanAndFormatPhoneNumber(String phoneNumber) {
String formattedNumber = phoneNumber.replaceAll(RegExp(r'\s+'), '');
if (formattedNumber.length > 6) {
@@ -18,6 +18,11 @@ Future<void> makePhoneCall(String phoneNumber) async {
formattedNumber = '+$formattedNumber';
}
}
return formattedNumber;
}
Future<void> makePhoneCall(String phoneNumber) async {
String formattedNumber = cleanAndFormatPhoneNumber(phoneNumber);
// ملاحظة: الأرقام القصيرة (مثل 112) ستتجاوز الشرط أعلاه وتبقى "112" وهو الصحيح
// 3. التنفيذ (Launch)
@@ -44,23 +49,26 @@ Future<void> makePhoneCall(String phoneNumber) async {
void launchCommunication(
String method, String contactInfo, String message) async {
String formattedContact = cleanAndFormatPhoneNumber(contactInfo);
// WhatsApp prefers the phone number without the '+' prefix
String whatsappContact = formattedContact.replaceAll('+', '');
String url;
if (Platform.isIOS) {
switch (method) {
case 'phone':
url = 'tel:$contactInfo';
url = 'tel:$formattedContact';
break;
case 'sms':
url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}';
url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}';
break;
case 'whatsapp':
url =
'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
break;
case 'email':
url =
'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}';
'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}';
break;
default:
return;
@@ -68,11 +76,11 @@ void launchCommunication(
} else if (Platform.isAndroid) {
switch (method) {
case 'phone':
url = 'tel:$contactInfo';
url = 'tel:$formattedContact';
break;
case 'sms':
url = 'sms:$contactInfo?body=${Uri.encodeComponent(message)}';
url = 'sms:$formattedContact?body=${Uri.encodeComponent(message)}';
break;
case 'whatsapp':
// Check if WhatsApp is installed
@@ -80,16 +88,16 @@ void launchCommunication(
await canLaunchUrl(Uri.parse('whatsapp://'));
if (whatsappInstalled) {
url =
'whatsapp://send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
'whatsapp://send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
} else {
// Provide an alternative action, such as opening the WhatsApp Web API
url =
'https://api.whatsapp.com/send?phone=$contactInfo&text=${Uri.encodeComponent(message)}';
'https://api.whatsapp.com/send?phone=$whatsappContact&text=${Uri.encodeComponent(message)}';
}
break;
case 'email':
url =
'mailto:$contactInfo?subject=Subject&body=${Uri.encodeComponent(message)}';
'mailto:$formattedContact?subject=Subject&body=${Uri.encodeComponent(message)}';
break;
default:
return;

View File

@@ -0,0 +1,30 @@
import 'dart:convert';
import 'package:http/http.dart' as http;
class TranslateHelper {
static Future<String> translateText(String text, String targetLang) async {
if (text.isEmpty) return text;
try {
final url = Uri.parse(
'https://translate.googleapis.com/translate_a/single?client=gtx&sl=auto&tl=$targetLang&dt=t&q=${Uri.encodeComponent(text)}'
);
final response = await http.get(url);
if (response.statusCode == 200) {
final decoded = jsonDecode(response.body);
if (decoded != null && decoded is List && decoded.isNotEmpty && decoded[0] is List) {
final List parts = decoded[0];
String translated = '';
for (var part in parts) {
if (part is List && part.isNotEmpty) {
translated += part[0].toString();
}
}
return translated;
}
}
} catch (e) {
// Fallback to original text on any exception
}
return text;
}
}

View File

@@ -293,7 +293,7 @@ class ApplyOrderWidget extends StatelessWidget {
const Icon(Icons.star_rounded,
color: Colors.amber, size: 14),
Text(
" ${controller.driverRate}${controller.model}",
" ${controller.driverRate}${controller.model}${controller.carColor.isNotEmpty ? '${controller.carColor.tr}' : ''}",
style: TextStyle(
fontSize: 12,
color: Colors.grey[700],
@@ -356,23 +356,25 @@ class ApplyOrderWidget extends StatelessWidget {
final bool isBike = vehicleText.contains('scooter') ||
vehicleText.contains('bike') ||
vehicleText.contains('دراجة');
return Container(
height: 40, // تصغير من 50
width: 40,
decoration: BoxDecoration(
color: carColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(4),
child: ColorFiltered(
colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn),
child: Image.asset(
isBike ||
box.read(BoxName.carType) == 'Scooter' ||
box.read(BoxName.carType) == 'Pink Bike'
? 'assets/images/moto.png'
: 'assets/images/car3.png',
fit: BoxFit.contain,
return AnimatedCarIcon(
child: Container(
height: 40, // تصغير من 50
width: 40,
decoration: BoxDecoration(
color: carColor.withOpacity(0.1),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(4),
child: ColorFiltered(
colorFilter: ColorFilter.mode(carColor, BlendMode.srcIn),
child: Image.asset(
isBike ||
box.read(BoxName.carType) == 'Scooter' ||
box.read(BoxName.carType) == 'Pink Bike'
? 'assets/images/moto.png'
: 'assets/images/car3.png',
fit: BoxFit.contain,
),
),
),
);
@@ -841,3 +843,48 @@ class TimeDriverToPassenger extends StatelessWidget {
});
}
}
class AnimatedCarIcon extends StatefulWidget {
final Widget child;
const AnimatedCarIcon({Key? key, required this.child}) : super(key: key);
@override
State<AnimatedCarIcon> createState() => _AnimatedCarIconState();
}
class _AnimatedCarIconState extends State<AnimatedCarIcon>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<Offset> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(seconds: 2),
)..repeat(reverse: true);
_animation = Tween<Offset>(
begin: const Offset(-0.06, 0.0),
end: const Offset(0.06, 0.0),
).animate(CurvedAnimation(
parent: _controller,
curve: Curves.easeInOut,
));
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return SlideTransition(
position: _animation,
child: widget.child,
);
}
}

View File

@@ -162,7 +162,7 @@ class RideBeginPassenger extends StatelessWidget {
children: [
Flexible(
child: Text(
'${controller.model}',
'${controller.model}${controller.carColor.isNotEmpty ? "${controller.carColor.tr}" : ""}',
style: TextStyle(fontSize: 12, color: AppColor.grayColor),
maxLines: 1,
overflow: TextOverflow.ellipsis,

View File

@@ -97,6 +97,15 @@ class VipRideBeginPassenger extends StatelessWidget {
controller.model,
style: AppStyle.title,
),
if (controller.carColor.isNotEmpty) ...[
const SizedBox(
width: 10,
),
Text(
controller.carColor.tr,
style: AppStyle.title,
),
],
],
),
),

View File

@@ -7,6 +7,7 @@ import 'package:get/get.dart';
import '../../constant/colors.dart';
import '../../constant/style.dart';
import '../../controller/functions/tts.dart';
import '../../controller/functions/translate_helper.dart';
// ─────────────────────────────────────────────────────────────────────────────
// Config
@@ -417,6 +418,167 @@ class MyDialog extends GetxController {
barrierColor: _DC.barrierColor,
);
}
void getChatDialog(
String title,
String messageContent,
VoidCallback onPressed, {
IconData? icon,
}) {
HapticFeedback.mediumImpact();
String displayedText = messageContent;
bool isTranslated = false;
bool isLoading = false;
Get.dialog(
StatefulBuilder(
builder: (dialogContext, setState) {
return _DialogShell(
child: _GlassCard(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// ── Body ──────────────────────────────────────────────
Padding(
padding: const EdgeInsets.fromLTRB(24, 28, 24, 20),
child: Column(
children: [
// Icon badge
Container(
width: 56,
height: 56,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.primaryColor.withOpacity(0.1),
border: Border.all(
color: AppColor.primaryColor.withOpacity(0.2),
),
),
child: Icon(
icon ?? Icons.chat_bubble_outline_rounded,
color: AppColor.primaryColor,
size: 26,
),
),
const SizedBox(height: 16),
// Title
Text(
title.tr,
textAlign: TextAlign.center,
style: AppStyle.title.copyWith(
fontSize: 18,
fontWeight: FontWeight.w700,
letterSpacing: -0.4,
color: AppColor.writeColor,
),
),
const SizedBox(height: 10),
if (isLoading)
const Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: Center(
child: CupertinoActivityIndicator(radius: 12),
),
)
else
Text(
displayedText,
textAlign: TextAlign.center,
style: AppStyle.subtitle.copyWith(
fontSize: 14.5,
height: 1.5,
color: Colors.grey[600],
),
),
const SizedBox(height: 16),
// TTS button
_SpeakButton(
texts: [title.tr, displayedText],
),
],
),
),
// ── Actions ───────────────────────────────────────────
Container(
decoration: BoxDecoration(
border: Border(
top: BorderSide(color: Colors.grey.withOpacity(0.15), width: 1),
),
),
child: Row(
children: [
// Translate Toggle
Expanded(
child: _ActionButton(
label: isTranslated ? 'Original'.tr : 'Translate'.tr,
color: AppColor.blueColor,
backgroundColor: AppColor.blueColor.withOpacity(0.07),
onPressed: () async {
if (isLoading) return;
HapticFeedback.lightImpact();
if (isTranslated) {
setState(() {
displayedText = messageContent;
isTranslated = false;
});
} else {
setState(() {
isLoading = true;
});
try {
final targetLang = Get.locale?.languageCode ?? 'ar';
final translated = await TranslateHelper.translateText(messageContent, targetLang);
setState(() {
displayedText = translated;
isTranslated = true;
isLoading = false;
});
} catch (e) {
setState(() {
isLoading = false;
});
}
}
},
isLeft: true,
),
),
Container(width: 1, height: 52, color: Colors.grey.withOpacity(0.15)),
// Confirm
Expanded(
child: _ActionButton(
label: 'OK'.tr,
color: AppColor.primaryColor,
backgroundColor: AppColor.primaryColor.withOpacity(0.07),
onPressed: () {
HapticFeedback.mediumImpact();
Navigator.of(dialogContext, rootNavigator: true).pop();
Future.delayed(const Duration(milliseconds: 100), () {
onPressed();
});
},
isLeft: false,
isBold: true,
),
),
],
),
),
],
),
),
);
},
),
barrierDismissible: true,
barrierColor: _DC.barrierColor,
);
}
}
// ─────────────────────────────────────────────────────────────────────────────