Update: 2026-08-02 17:52:28

This commit is contained in:
Hamza-Ayed
2026-08-02 17:52:28 +03:00
parent b78a6797d5
commit 4620e84d34
96 changed files with 6250 additions and 476 deletions
@@ -45,8 +45,8 @@ class InviteController extends GetxController {
final String shareText =
'''Join Siro as a driver using my referral code!
Use code: $driverCouponCode
Download the Siro Driver app now and earn rewards:
https://siromove.com/invite.php?code=$driverCouponCode&app=driver
Download the Siro Captain app now and earn rewards:
${AppLink.driverDownloadLinks}
''';
await Share.share(shareText);
}
@@ -228,10 +228,33 @@ class LoginDriverController extends GetxController {
return '';
}
getJWT() async {
static Future<bool>? _jwtFuture;
static DateTime _jwtCooldownUntil = DateTime(2000);
static int _jwtFailures = 0;
static Duration get jwtCooldownRemaining {
final d = _jwtCooldownUntil.difference(DateTime.now());
return d.isNegative ? Duration.zero : d;
}
Future<bool> getJWT() async {
if (_jwtFuture != null) {
Log.print('⏳ getJWT: تجديد قيد التنفيذ — إعادة استخدام نفس الـ future.');
return _jwtFuture!;
}
_jwtFuture = _getJwtInternal().catchError((e) {
return _onJwtFailure('exception: $e');
});
try {
return await _jwtFuture!;
} finally {
_jwtFuture = null;
}
}
Future<bool> _getJwtInternal() async {
await EncryptionHelper.initialize();
// 1. Check secure storage first to avoid redundant API calls
String? secureJwt = await storage.read(key: BoxName.jwt);
if (secureJwt != null && secureJwt.isNotEmpty) {
bool isTokenValid = false;
@@ -250,7 +273,6 @@ class LoginDriverController extends GetxController {
final decoded = jsonDecode(utf8.decode(base64Url.decode(payload)));
final exp = decoded['exp'];
if (exp != null) {
// Check if token is valid with a 30-second buffer
isTokenValid =
DateTime.now().millisecondsSinceEpoch < (exp * 1000 - 30000);
}
@@ -259,14 +281,23 @@ class LoginDriverController extends GetxController {
if (isTokenValid) {
Log.print('🔑 Valid JWT found in secure storage. Skipping generation.');
return;
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
return true;
}
}
if (DateTime.now().isBefore(_jwtCooldownUntil)) {
Log.print(
'🛑 getJWT: بـ cooldown لمدة ${jwtCooldownRemaining.inSeconds}ث — تخطّي التجديد.');
return false;
}
dev = Platform.isAndroid ? 'android' : 'ios';
Log.print(
'box.read(BoxName.firstTimeLoadKey): ${box.read(BoxName.firstTimeLoadKey)}');
if (box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
final driverId = box.read(BoxName.driverID);
final isRegistering = driverId == null || driverId.toString().isEmpty;
if (isRegistering && box.read(BoxName.firstTimeLoadKey).toString() != 'false') {
var payload = {
'id': box.read(BoxName.driverID) ?? AK.newId,
'password': AK.passnpassenger,
@@ -274,18 +305,21 @@ class LoginDriverController extends GetxController {
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
await DeviceHelper.getDeviceFingerprint(),
};
// Log.print('payload: ${payload}');
var response0 = await http.post(
Uri.parse(AppLink.loginFirstTimeDriver),
body: payload,
);
Log.print('response0: ${response0.body}');
Log.print('request: ${response0.request}');
).timeout(const Duration(seconds: 30));
if (response0.statusCode == 429) {
final retryAfter = int.tryParse(response0.headers['retry-after'] ?? '') ?? 60;
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('🛑 getJWT(firstTime): 429 — cooldown ${retryAfter}s');
return false;
}
if (response0.statusCode == 200) {
final decodedResponse1 = jsonDecode(response0.body);
Log.print('decodedResponse1: ${decodedResponse1}');
String? jwt;
if (decodedResponse1['message'] is Map &&
decodedResponse1['message']['jwt'] != null) {
@@ -295,18 +329,20 @@ class LoginDriverController extends GetxController {
}
if (jwt != null) {
// box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
await EncryptionHelper.initialize();
return _onJwtSuccess();
}
// ✅ بعد التأكد أن كل المفاتيح موجودة
await EncryptionHelper.initialize();
// await AppInitializer().getKey();
} else {}
return _onJwtFailure('firstTime: لا يوجد jwt بالرد');
}
return _onJwtFailure('firstTime: HTTP ${response0.statusCode}');
} else {
await EncryptionHelper.initialize();
if (isRegistering) {
return _onJwtFailure('renew: لا يوجد driverID');
}
var payload = {
'id': box.read(BoxName.driverID),
'password': box.read(BoxName.emailDriver),
@@ -314,18 +350,21 @@ class LoginDriverController extends GetxController {
'fingerPrint': box.read(BoxName.deviceFingerprint) ??
await DeviceHelper.getDeviceFingerprint(),
};
// print(payload);
var response1 = await http.post(
Uri.parse(AppLink.loginJwtDriver),
body: payload,
);
Log.print('response1.request: ${response1.request}');
Log.print('response1.body: ${response1.body}');
).timeout(const Duration(seconds: 30));
if (response1.statusCode == 429) {
final retryAfter = int.tryParse(response1.headers['retry-after'] ?? '') ?? 60;
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: retryAfter));
Log.print('🛑 getJWT: 429 — cooldown ${retryAfter}ث');
return false;
}
if (response1.statusCode == 200) {
final decodedResponse1 = jsonDecode(response1.body);
// Log.print('decodedResponse1: ${decodedResponse1}');
String? jwt;
if (decodedResponse1['message'] is Map &&
decodedResponse1['message']['jwt'] != null) {
@@ -335,15 +374,30 @@ class LoginDriverController extends GetxController {
}
if (jwt != null) {
// await box.write(BoxName.jwt, c(jwt));
await storage.write(key: BoxName.jwt, value: jwt);
return _onJwtSuccess();
}
// await AppInitializer().getKey();
return _onJwtFailure('renew: لا يوجد jwt بالرد');
}
return _onJwtFailure('renew: HTTP ${response1.statusCode}');
}
}
bool _onJwtSuccess() {
_jwtFailures = 0;
_jwtCooldownUntil = DateTime(2000);
Log.print('✅ getJWT: تم توليد توكن جديد بنجاح.');
return true;
}
bool _onJwtFailure(String reason) {
_jwtFailures++;
final seconds = _jwtFailures >= 6 ? 60 : (1 << _jwtFailures);
_jwtCooldownUntil = DateTime.now().add(Duration(seconds: seconds));
Log.print('❌ getJWT فشل ($reason) — محاولة #$_jwtFailures، cooldown ${seconds}ث');
return false;
}
Future<void> getLocationPermission() async {
var status = await Permission.locationAlways.status;
if (!status.isGranted) {
@@ -0,0 +1,129 @@
// food_delivery_controller.dart — حالة تبويب التوصيل (وضع التوصيل، العروض، المهام النشطة)
import 'dart:async';
import 'package:get/get.dart';
import '../../views/widgets/error_snakbar.dart';
import 'food_delivery_models.dart';
import 'food_delivery_service.dart';
class FoodDeliveryController extends GetxController {
bool isDeliveryModeEnabled = false;
bool isTogglingMode = false;
List<FoodDeliveryOffer> pendingOffers = [];
List<FoodDeliveryTask> activeTasks = [];
bool isLoadingTasks = false;
final Set<int> _respondingOfferIds = {};
final Set<int> _busyTaskIds = {};
Timer? _pollTimer;
@override
void onInit() {
super.onInit();
_refreshAll();
_pollTimer = Timer.periodic(const Duration(seconds: 4), (_) => _refreshAll());
}
Future<void> _refreshAll() async {
if (isDeliveryModeEnabled) {
await _fetchPendingOffers();
}
await fetchActiveTasks();
}
Future<void> toggleDeliveryMode(bool enable) async {
isTogglingMode = true;
update();
final res = await FoodDeliveryService.toggleAvailability(enable);
isTogglingMode = false;
if (res.success) {
isDeliveryModeEnabled = enable;
if (!enable) pendingOffers = [];
} else {
mySnackbarWarning(res.message);
}
update();
}
Future<void> _fetchPendingOffers() async {
final res = await FoodDeliveryService.getPendingOffers();
if (res.success) {
pendingOffers = res.data ?? [];
update();
}
}
Future<void> fetchActiveTasks() async {
isLoadingTasks = true;
final res = await FoodDeliveryService.getActiveTasks();
isLoadingTasks = false;
if (res.success) activeTasks = res.data ?? [];
update();
}
bool isRespondingToOffer(int orderId) => _respondingOfferIds.contains(orderId);
bool isTaskBusy(int orderId) => _busyTaskIds.contains(orderId);
Future<void> respondToOffer(int orderId, bool accept) async {
if (_respondingOfferIds.contains(orderId)) return;
_respondingOfferIds.add(orderId);
update();
final res = await FoodDeliveryService.respondToOffer(orderId, accept);
_respondingOfferIds.remove(orderId);
pendingOffers.removeWhere((o) => o.orderId == orderId);
if (!res.success) {
mySnackbarWarning(res.message);
} else if (accept) {
mySnackbarSuccess('تم قبول طلب التوصيل');
}
update();
await fetchActiveTasks();
}
Future<void> markPickedUp(int orderId) async {
if (_busyTaskIds.contains(orderId)) return;
_busyTaskIds.add(orderId);
update();
final res = await FoodDeliveryService.markPickedUp(orderId);
_busyTaskIds.remove(orderId);
if (res.success) {
mySnackbarSuccess('تم استلام الطلب من المطعم');
await fetchActiveTasks();
} else {
mySnackbarWarning(res.message);
}
update();
}
Future<void> markDelivered(int orderId) async {
if (_busyTaskIds.contains(orderId)) return;
_busyTaskIds.add(orderId);
update();
final res = await FoodDeliveryService.markDelivered(orderId);
_busyTaskIds.remove(orderId);
if (res.success) {
mySnackbarSuccess('تم تسليم الطلب بنجاح');
await fetchActiveTasks();
} else {
mySnackbarWarning(res.message);
}
update();
}
@override
void onClose() {
_pollTimer?.cancel();
super.onClose();
}
}
@@ -0,0 +1,92 @@
// food_delivery_models.dart — نماذج بيانات وحدة التوصيل (جهة السائق)
class FoodDeliveryTask {
final int id;
final String status; // courier_assigned | picked_up
final int merchantId;
final String merchantNameAr;
final double? merchantLat;
final double? merchantLng;
final String? merchantAddress;
final int deliveryFee;
final String? deliveryAddress;
final double deliveryLat;
final double deliveryLng;
final DateTime? createdAt;
FoodDeliveryTask({
required this.id,
required this.status,
required this.merchantId,
required this.merchantNameAr,
required this.deliveryFee,
required this.deliveryLat,
required this.deliveryLng,
this.merchantLat,
this.merchantLng,
this.merchantAddress,
this.deliveryAddress,
this.createdAt,
});
factory FoodDeliveryTask.fromJson(Map<String, dynamic> j) => FoodDeliveryTask(
id: int.tryParse(j['id'].toString()) ?? 0,
status: j['status']?.toString() ?? '',
merchantId: int.tryParse(j['merchant_id'].toString()) ?? 0,
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
merchantLat: double.tryParse(j['merchant_lat']?.toString() ?? ''),
merchantLng: double.tryParse(j['merchant_lng']?.toString() ?? ''),
merchantAddress: j['merchant_address']?.toString(),
deliveryFee: int.tryParse(j['delivery_fee']?.toString() ?? '0') ?? 0,
deliveryAddress: j['delivery_address']?.toString(),
deliveryLat: double.tryParse(j['delivery_lat']?.toString() ?? '0') ?? 0,
deliveryLng: double.tryParse(j['delivery_lng']?.toString() ?? '0') ?? 0,
createdAt: DateTime.tryParse(j['created_at']?.toString() ?? ''),
);
}
// عرض توصيل وارد — من polling على food/courier/pending_offers.php
class FoodDeliveryOffer {
final int orderId;
final int merchantId;
final String merchantNameAr;
final double? merchantLat;
final double? merchantLng;
final int deliveryFee;
final DateTime? offeredAt;
FoodDeliveryOffer({
required this.orderId,
required this.merchantId,
required this.merchantNameAr,
required this.deliveryFee,
this.merchantLat,
this.merchantLng,
this.offeredAt,
});
factory FoodDeliveryOffer.fromJson(Map<String, dynamic> j) => FoodDeliveryOffer(
orderId: int.tryParse(j['order_id'].toString()) ?? 0,
merchantId: int.tryParse(j['merchant_id'].toString()) ?? 0,
merchantNameAr: j['merchant_name_ar']?.toString() ?? '',
merchantLat: double.tryParse(j['merchant_lat']?.toString() ?? ''),
merchantLng: double.tryParse(j['merchant_lng']?.toString() ?? ''),
deliveryFee: int.tryParse(j['delivery_fee']?.toString() ?? '0') ?? 0,
offeredAt: DateTime.tryParse(j['offered_at']?.toString() ?? ''),
);
// مهلة العرض 20 ثانية من الخادم (SET NX EX 20) — عدّاد تقريبي للعرض فقط،
// الخادم هو الحكم الفعلي (رفض offer_respond إن انتهت المهلة فعلاً).
int get secondsRemaining {
if (offeredAt == null) return 20;
final elapsed = DateTime.now().difference(offeredAt!).inSeconds;
return (20 - elapsed).clamp(0, 20);
}
}
const int foodCurrencyDivisor = 1000;
String foodFormatPrice(int smallestUnit, {String currencySymbol = 'د.أ'}) {
final decimal = smallestUnit / foodCurrencyDivisor;
return '${decimal.toStringAsFixed(3)} $currencySymbol';
}
@@ -0,0 +1,89 @@
// food_delivery_service.dart — طبقة الاتصال بـ backend/food (جهة السائق/الموصّل)
import '../functions/crud.dart';
import '../../constant/links.dart';
import 'food_delivery_models.dart';
class FoodDeliveryApiResult<T> {
final bool success;
final T? data;
final String message;
FoodDeliveryApiResult(this.success, this.data, this.message);
}
class FoodDeliveryService {
static String get _base => '${AppLink.server}/food';
static Future<FoodDeliveryApiResult<bool>> toggleAvailability(bool enabled) async {
final res = await CRUD().post(
link: '$_base/courier/toggle_availability.php',
payload: {'enabled': enabled.toString()},
);
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, enabled, 'ok');
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<List<FoodDeliveryTask>>> getActiveTasks() async {
final res = await CRUD().post(link: '$_base/courier/active.php');
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
final list = (msg is Map && msg['orders'] is List)
? (msg['orders'] as List)
.map((o) => FoodDeliveryTask.fromJson(Map<String, dynamic>.from(o)))
.toList()
: <FoodDeliveryTask>[];
return FoodDeliveryApiResult(true, list, 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<List<FoodDeliveryOffer>>> getPendingOffers() async {
final res = await CRUD().post(link: '$_base/courier/pending_offers.php');
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
final list = (msg is Map && msg['offers'] is List)
? (msg['offers'] as List)
.map((o) => FoodDeliveryOffer.fromJson(Map<String, dynamic>.from(o)))
.toList()
: <FoodDeliveryOffer>[];
return FoodDeliveryApiResult(true, list, 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<String>> respondToOffer(int orderId, bool accept) async {
final res = await CRUD().post(
link: '$_base/courier/offer_respond.php',
payload: {'order_id': orderId.toString(), 'response': accept ? 'accept' : 'reject'},
);
if (res is Map && res['status'] == 'success') {
final msg = res['message'];
return FoodDeliveryApiResult(true, (msg is Map ? msg['response']?.toString() : null) ?? '', 'ok');
}
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<void>> markPickedUp(int orderId) async {
final res = await CRUD().post(
link: '$_base/courier/picked_up.php',
payload: {'order_id': orderId.toString()},
);
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, null, 'ok');
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static Future<FoodDeliveryApiResult<void>> markDelivered(int orderId) async {
final res = await CRUD().post(
link: '$_base/courier/delivered.php',
payload: {'order_id': orderId.toString()},
);
if (res is Map && res['status'] == 'success') return FoodDeliveryApiResult(true, null, 'ok');
return FoodDeliveryApiResult(false, null, _errMsg(res));
}
static String _errMsg(dynamic res) {
if (res == 'no_internet') return 'تحقق من اتصالك بالإنترنت';
if (res == 'token_expired') return 'انتهت الجلسة، حاول مجدداً';
if (res is Map && res['message'] is String) return res['message'];
return 'حدث خطأ، حاول مجدداً';
}
}
@@ -67,8 +67,8 @@ class AppUpdateController extends GetxController {
void _showStoreUpdateDialog() {
final String storeUrl = Platform.isAndroid
? 'https://play.google.com/store/apps/details?id=com.siro_driver'
: 'https://apps.apple.com/jo/app/siro-driver/id6482995159';
? AppLink.driverAppStoreAndroid
: AppLink.driverAppStoreIOS;
Get.defaultDialog(
title: "تحديث جديد متوفر".tr,
+56 -40
View File
@@ -22,7 +22,6 @@ import 'ssl_pinning.dart';
class CRUD {
final NetGuard _netGuard = NetGuard();
final _client = SslPinning.createPinnedClient();
static bool _isRefreshingJWT = false;
static String _lastErrorSignature = '';
static DateTime _lastErrorTimestamp = DateTime(2000);
static const Duration _errorLogDebounceDuration = Duration(minutes: 1);
@@ -107,6 +106,18 @@ class CRUD {
}
}
// ═══════════════════════════════════════════════════════════════
// _ensureJwt — يضمن وجود توكن صالح قبل الإرسال
// ═══════════════════════════════════════════════════════════════
Future<String> _ensureJwt() async {
String token = await _getJwt();
if (_isJwtValid(token)) return token;
final ok = await Get.put(LoginDriverController()).getJWT();
if (!ok) return '';
return await _getJwt();
}
// ═══════════════════════════════════════════════════════════════
// _makeRequest — دالة مركزية لكل الطلبات
// ───────────────────────────────────────────────────────────────
@@ -119,6 +130,7 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
required Map<String, String> headers,
bool allowRefresh = true,
}) async {
// timeouts مرتفعة مناسبة للإنترنت الضعيف في سوريا
const totalTimeout = Duration(seconds: 60);
@@ -186,19 +198,33 @@ class CRUD {
}
}
// 401 → تجديد التوكن (مع حماية من الحلقة اللانهائية)
// 429 → السيرفر رافض بسبب الضغط؛ ممنوع نجدّد التوكن أو نعيد المحاولة
if (sc == 429) {
Log.print('🛑 [RES-$requestId] 429 rate limited — $link');
return 'rate_limited';
}
// 401 → تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
if (sc == 401) {
// تخطي تجديد التوكن لـ endpoints غير حرجة (مثل تسجيل الأخطاء)
final isNonCritical = link.contains('errorApp.php');
if (!_isRefreshingJWT && !isNonCritical) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired';
if (isNonCritical || !allowRefresh) return 'token_expired';
final refreshed = await Get.put(LoginDriverController()).getJWT();
if (!refreshed) return 'token_expired';
final newToken = await _getJwt();
if (newToken.isEmpty) return 'token_expired';
// إعادة الطلب بالتوكن الجديد — allowRefresh: false يمنع أي تكرار إضافي
final retryHeaders = Map<String, String>.from(headers)
..['Authorization'] = 'Bearer $newToken';
return await _makeRequest(
link: link,
payload: payload,
headers: retryHeaders,
allowRefresh: false,
);
}
// 5xx
@@ -218,16 +244,11 @@ class CRUD {
required String link,
Map<String, dynamic>? payload,
}) async {
String token = await _getJwt();
// فحص صلاحية التوكن قبل الإرسال — تجنب طلب مضمون الرفض
if (!_isJwtValid(token) && !_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
token = await _getJwt();
} finally {
_isRefreshingJWT = false;
String token = await _ensureJwt();
if (token.isEmpty) {
// إذا فشل الحصول على توكن، لا ترسل الطلب للباك إند لأنّه سيرفض حتماً.
if (!link.contains('login') && !link.contains('errorApp.php')) {
return 'token_expired';
}
}
@@ -247,19 +268,11 @@ class CRUD {
Future<dynamic> get({
required String link,
Map<String, dynamic>? payload,
bool allowRefresh = true,
}) async {
try {
// فحص صلاحية التوكن قبل الإرسال
String token = await _getJwt();
if (!_isJwtValid(token) && !_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
token = await _getJwt();
} finally {
_isRefreshingJWT = false;
}
}
final String token = await _ensureJwt();
var url = Uri.parse(link);
var response = await _client.post(
@@ -280,15 +293,18 @@ class CRUD {
if (jsonData['status'] == 'success') return response.body;
return jsonData['status'];
} else if (response.statusCode == 401) {
if (!_isRefreshingJWT) {
_isRefreshingJWT = true;
try {
await Get.put(LoginDriverController()).getJWT();
} finally {
_isRefreshingJWT = false;
}
}
return 'token_expired';
// تجديد التوكن مرة واحدة ثم إعادة الطلب مرة واحدة فقط
// (allowRefresh: false تمنع أي تكرار إضافي) — نفس سلوك _makeRequest.
if (!allowRefresh) return 'token_expired';
final refreshed = await Get.put(LoginDriverController()).getJWT();
if (!refreshed) return 'token_expired';
return await get(
link: link,
payload: payload,
allowRefresh: false,
);
} else if (response.statusCode >= 500) {
addError('Non-200: ${response.statusCode}', 'crud().get - Other',
url.toString());
@@ -48,8 +48,8 @@ Future<String> getPackageInfo() async {
void showUpdateDialog(BuildContext context) {
final String storeUrl = Platform.isAndroid
? 'https://play.google.com/store/apps/details?id=com.siro_driver'
: 'https://apps.apple.com/jo/app/siro-driver/id6482995159';
? AppLink.driverAppStoreAndroid
: AppLink.driverAppStoreIOS;
showGeneralDialog(
context: context,
@@ -137,7 +137,7 @@ class ReferralController extends GetxController {
String get shareMessage {
final appName = 'Siro';
return 'Join $appName as a driver! Use my code: $referralCode\nDownload: https://siro.app/driver?ref=$referralCode';
return 'Join $appName as a driver! Use my code: $referralCode\n${AppLink.driverDownloadLinks}';
}
String get shareMessagePassenger {
@@ -22,10 +22,8 @@ class RatingController extends GetxController {
void _redirectToAppStore() async {
// URLs for App Store and Google Play Store
const appStoreUrl =
'https://apps.apple.com/st/app/siro-driver/id6482995159';
const playStoreUrl =
'https://play.google.com/store/apps/details?id=com.siro_driver';
const appStoreUrl = AppLink.driverAppStoreIOS;
const playStoreUrl = AppLink.driverAppStoreAndroid;
final url = GetPlatform.isIOS ? appStoreUrl : playStoreUrl;
if (await launchUrl(Uri.parse(url))) {