feat: استيراد كود سيرو إلى تريبز (سيرو @ecfe7568) — بلا تعديل

قرار المالك 2026-07-27: باك إند سيرو PHP هو المعتمد، وتطبيقاته المجرّبة
ميدانياً تحل محل إعادة البناء المؤرشفة. سيرو نفسه لم يُمسّ.

الخريطة:
  backend · payment_server · loction_server · ride_server ·
  passenger_server · docker · dashboard · stress_test  → الجذر
  siro_rider  → apps/rider          siro_driver  → apps/driver
  siro_admin  → dashboards/admin    siro_service → dashboards/service
  android_bot → apps/android_bot    socialBot    → apps/socialBot

نُسخ المتعقَّب في git سيرو فقط عبر `git archive` (3,198 ملفاً / ~169 م.ب)
لا `cp -r` — فاستُثنيت مخلفات البناء تلقائياً. بلا أي تعديل محتوى عمداً:
كل ما يلي يصير فرقاً مقروءاً مقابل المصدر.

لم يُستورد وسببه: siromove.com (الموقع التسويقي يبقى marketing/ في تريبز،
سيرو فيه 8 ملفات) · docs و planning (تريبز له docs/ الخاص) · deploy.sh
(ليس نشراً على سيرفر بل `git add . && git push origin --all` — فخّ في
مستودع آخر) · transit_dashboard (بانتظار قرار مصير backend-transit و
dashboards/transit-web).

⚠️ لا يبني بعد — ثلاثة نواقص متوقعة ومقصودة:
1. `.env` و `lib/env/env.g.dart` غير متعقَّبين في سيرو (أسرار لكل مستأجر):
   كل تطبيق فلاتر يحتاج .env خاصاً ثم توليد env.g.dart بـ build_runner.
2. إعدادات Firebase (9 ملفات google-services.json و GoogleService-Info.plist)
   يستبعدها .gitignore تريبز — ولكل مستأجر مشروع Firebase خاص أصلاً.
3. apps/driver في سيرو يشير إلى `../../Intaleq/packages/get` خارج المستودع →
   يجب ضمّ الحزم داخله أسوة بـ apps/rider.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Hamza-Ayed
2026-07-27 05:14:13 +03:00
co-authored by Claude Opus 5
parent 9909d9b4c1
commit 4d8414c96b
3198 changed files with 766859 additions and 0 deletions
@@ -0,0 +1,83 @@
// import 'package:camera/camera.dart';
// import 'package:flutter/material.dart';
// import 'package:get/get.dart';
// import 'package:siro_driver/views/widgets/elevated_btn.dart';
// import '../../../../constant/colors.dart';
// import '../../../../constant/style.dart';
// import '../../../../controller/functions/camer_controller.dart';
// import '../../../../controller/functions/ocr_controller.dart';
// import '../../../widgets/my_scafold.dart';
// class CameraLisencePage extends StatelessWidget {
// CameraLisencePage.CameraLicensePage({super.key});
// final CameraClassController cameraClassController =
// Get.put(CameraClassController());
// @override
// Widget build(BuildContext context) {
// return MyScafolld(
// title: 'Scan Driver License'.tr,
// body: [
// Column(children: [
// Text(
// 'Please put your licence in these border'.tr,
// style: AppStyle.title.copyWith(color: AppColor.greenColor),
// ),
// Padding(
// padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
// child: GetBuilder<CameraClassController>(
// builder: (cameraClassController) =>
// cameraClassController.isCameraInitialized
// ? Stack(
// children: [
// Container(
// decoration: AppStyle.boxDecoration,
// child: CameraPreview(
// cameraClassController.cameraController,
// ),
// ),
// Positioned(
// top: Get.height * .1,
// right: 5,
// left: 5,
// child: Container(
// height: Get.width * 3 / 4,
// width: Get.width * .9,
// decoration: BoxDecoration(
// // color: AppColor.blueColor,
// border: Border.all(
// color: AppColor.yellowColor, width: 2),
// ),
// ),
// ),
// ],
// )
// : Container(
// decoration: AppStyle.boxDecoration,
// height: Get.width * 3 / 4,
// width: Get.width,
// child: Center(
// child: Text(
// 'Camera not initialized yet',
// style: AppStyle.title,
// ),
// ),
// ),
// ),
// ),
// const SizedBox(
// height: 20,
// ),
// MyElevatedButton(
// title: 'Take Image'.tr,
// onPressed: () {
// ScanDocumentsByApi().scanDocumentsByApi();
// },
// )
// ]),
// ],
// isleading: true,
// );
// }
// }
+80
View File
@@ -0,0 +1,80 @@
import 'dart:convert';
import 'package:get/get.dart';
import 'package:http/http.dart' as http;
class GeminiEgypt extends GetxController {
Map<String, dynamic> responseIdCardDriverEgypt = {};
String? responseIdCardDriverEgypt1;
Future geminiAiExtraction(String prompt, payload) async {
var requestBody = jsonEncode({
'contents': [
{
'parts': [
// {
// 'inlineData': {
// 'mimeType': 'image/jpeg',
// 'data': imageData,
// },
// },
{
'text': """
$payload
$prompt ,and make dates format like year-month-day"""
},
],
},
],
'generationConfig': {
'temperature': 0.4,
'topK': 32,
'topP': 1,
'maxOutputTokens': 4096,
'stopSequences': [],
},
'safety_settings': [
{"category": "HARM_CATEGORY_HARASSMENT", "threshold": "BLOCK_NONE"},
{"category": "HARM_CATEGORY_HATE_SPEECH", "threshold": "BLOCK_NONE"},
{
"category": "HARM_CATEGORY_SEXUALLY_EXPLICIT",
"threshold": "BLOCK_NONE"
},
{
"category": "HARM_CATEGORY_DANGEROUS_CONTENT",
"threshold": "BLOCK_NONE"
},
]
});
final response = await http.post(
Uri.parse(
// 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro-vision:generateContent?key=${AK.geminiApi}'),
'https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-pro-latest:generateContent?key=AIzaSyCyoLcSkDzK5_SMe00nhut56SSXWPR074w'),
headers: {'Content-Type': 'application/json'},
body: requestBody,
);
if (response.statusCode == 200) {
var responseData = jsonDecode(response.body);
// Process the responseData as needed
var result = responseData['candidates'][0]['content']['parts'][0]['text'];
RegExp regex = RegExp(r"```json([^`]*)```");
String? jsonString =
regex.firstMatch(responseData.toString())?.group(1)?.trim();
if (jsonString != null) {
// Convert the JSON object to a String
jsonString = jsonEncode(json.decode(jsonString));
responseIdCardDriverEgypt1 = jsonString;
responseIdCardDriverEgypt = jsonDecode(responseIdCardDriverEgypt1!);
update();
return responseIdCardDriverEgypt;
} else {}
// Rest of your code...
} else {}
}
}
+100
View File
@@ -0,0 +1,100 @@
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 '../../../constant/colors.dart';
import '../../../constant/style.dart';
import '../../../controller/functions/tts.dart';
import '../../../controller/home/captin/contact_us_controller.dart';
import '../../widgets/my_scafold.dart';
class ContactUsPage extends StatelessWidget {
ContactUsPage({super.key});
@override
Widget build(BuildContext context) {
Get.put(ContactUsController());
return GetBuilder<ContactUsController>(builder: (controller) {
return MyScafolld(
title: "Contact Us".tr,
body: [
Padding(
padding: const EdgeInsets.all(8.0),
child: ListView(
// crossAxisAlignment: CrossAxisAlignment.center,
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
decoration: AppStyle.boxDecoration1,
child: Column(
children: [
ClipRRect(
borderRadius: BorderRadius.circular(15),
child: Image.asset('assets/images/logo.gif')),
IconButton(
onPressed: () async {
Get.put(TextToSpeechController()).speakText(
'Siro is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
.tr);
},
icon: const Icon(Icons.headphones),
),
Padding(
padding: const EdgeInsets.all(8.0),
child: Text(
'Siro is the safest ride-sharing app that introduces many features for both captains and passengers. We offer the lowest commission rate of just 8%, ensuring you get the best value for your rides. Our app includes insurance for the best captains, regular maintenance of cars with top engineers, and on-road services to ensure a respectful and high-quality experience for all users.'
.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 - 17:00."
.tr,
style: AppStyle.title,
textAlign: TextAlign.center,
),
),
),
InkWell(
onTap: () => controller.showContactDialog(context),
child: const Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Icon(
Icons.phone,
color: AppColor.blueColor,
),
Icon(
FontAwesome.whatsapp,
color: AppColor.greenColor,
),
Icon(
Icons.email,
color: AppColor.redColor,
),
],
),
),
const SizedBox(
height: 30,
)
],
),
)
],
isleading: true);
});
}
}
@@ -0,0 +1,110 @@
import 'dart:convert';
import 'package:get/get.dart';
import '../../../constant/box_name.dart';
import '../../../constant/links.dart';
import '../../../controller/functions/crud.dart';
import '../../../main.dart';
import '../../widgets/error_snakbar.dart';
class DriverCarController extends GetxController {
bool isLoading = false;
List cars = [];
// int? carId;
fetchCatrsForDrivers() async {
isLoading = true;
update();
var res = await CRUD().get(link: AppLink.getNewCarsDrivers, payload: {
"driverID": box.read(BoxName.driverID).toString(),
});
if (res != 'failure') {
var d = jsonDecode(res)['message'];
cars = d;
// carId = cars.isEmpty ? 1 : cars.length + 1;
}
isLoading = false;
update();
}
addCarsForDrivers(
String vin,
String carPlate,
String make,
String model,
String year,
String expirationDate,
String color,
String colorHex,
String address,
String owner,
String registrationDate,
String displacement,
String fuel) async {
var res = await CRUD().post(
link: AppLink.addRegisrationCar,
payload: {
"driverID": box.read(BoxName.driverID).toString(),
"vin": vin ?? 'unknown',
"car_plate": carPlate.toString(),
"make": make ?? 'unknown',
"model": model ?? 'unknown',
"year": year ?? 'unknown',
"expiration_date": expirationDate ?? 'unknown',
"color": color ?? 'unknown',
"owner": owner ?? 'unknown',
"color_hex": colorHex ?? '#000000',
"address": address ?? 'unknown',
"displacement": displacement ?? 'unknown',
"fuel": fuel ?? 'unknown',
"registration_date": registrationDate ?? 'unknown',
},
);
if (res != 'failure') {
mySnackbarSuccess('');
fetchCatrsForDrivers();
} else {
mySnackbarError('');
}
}
// update carRegistration only and insert on it without tow column
Future<void> updateCarRegistration(String id, String driverID) async {
final body = {
'id': id,
'driverID': driverID,
};
// remove default before update
var response = await CRUD().post(
link: AppLink.makeDefaultCar,
payload: body,
);
if (response != 'failure') {
mySnackbarSuccess('Updated'.tr);
} else {
mySnackbarError('Not updated'.tr);
}
}
//todo need review
removeCar(String carId) async {
isLoading = true;
update();
var res = await CRUD().post(link: AppLink.deleteNewCarsDrivers, payload: {
"id": carId.toString(),
});
if (res != 'failure') {
mySnackbarSuccess('deleted'.tr);
}
isLoading = false;
update();
}
@override
void onInit() {
fetchCatrsForDrivers();
super.onInit();
}
}
+900
View File
@@ -0,0 +1,900 @@
import 'package:flutter/material.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/services.dart';
import 'package:get/get.dart';
import 'package:share_plus/share_plus.dart';
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../constant/links.dart';
import '../../../controller/auth/captin/invit_controller.dart';
import '../../../controller/functions/encrypt_decrypt.dart';
import '../../../main.dart';
import '../../../controller/home/invites_rewards_controller.dart';
import '../../widgets/error_snakbar.dart';
class InviteScreen extends StatelessWidget {
final InviteController controller = Get.put(InviteController());
final InvitesRewardsController rewardsController = Get.put(InvitesRewardsController());
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: CupertinoColors.systemBackground,
appBar: AppBar(
backgroundColor: CupertinoColors.systemBackground,
elevation: 0,
title: Text(
'Invite'.tr,
style: const TextStyle(color: CupertinoColors.label),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: AppColor.blueColor),
onPressed: () => Get.back(),
),
),
body: SafeArea(
child: GetBuilder<InviteController>(
builder: (controller) {
return Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Container(
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(8),
),
child: SegmentedButton<int>(
style: ButtonStyle(
backgroundColor: WidgetStateProperty.resolveWith<Color>(
(states) => states.contains(WidgetState.selected)
? CupertinoColors.white
: Colors.transparent,
),
foregroundColor: WidgetStateProperty.resolveWith<Color>(
(states) => states.contains(WidgetState.selected)
? AppColor.blueColor
: CupertinoColors.label,
),
),
segments: [
ButtonSegment(
value: 0,
label: Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Drivers'.tr),
),
),
ButtonSegment(
value: 1,
label: Padding(
padding: const EdgeInsets.all(8.0),
child: Text('Passengers'.tr),
),
),
],
selected: {controller.selectedTab},
onSelectionChanged: (Set<int> newSelection) {
controller.updateSelectedTab(newSelection.first);
},
),
),
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: controller.selectedTab == 0
? _buildDriverTab(context)
: _buildPassengerTab(context),
),
),
],
);
},
),
),
);
}
Widget _buildDriverTab(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildPermanentCodeCard(context),
const SizedBox(height: 10),
_buildManualCodeEntry(context),
const SizedBox(height: 20),
Text(
"Invite another driver and both get a gift after he completes 100 trips!"
.tr,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: CupertinoColors.label,
),
),
const SizedBox(height: 20),
_buildPhoneInput(),
const SizedBox(height: 20),
_buildActionButtons(),
const SizedBox(height: 20),
_buildInvitationsList(context),
const SizedBox(height: 20),
_buildUnifiedRewardsList(isDriver: true),
],
);
}
Widget _buildPassengerTab(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildPermanentCodeCard(context),
const SizedBox(height: 10),
_buildManualCodeEntry(context),
const SizedBox(height: 20),
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(12),
),
child: Column(
children: [
Text(
"Share this code with passengers and earn rewards when they use it!"
.tr,
textAlign: TextAlign.center,
style: const TextStyle(
color: CupertinoColors.secondaryLabel,
fontSize: 13,
),
),
],
),
),
const SizedBox(height: 20),
_buildPhoneInput(),
const SizedBox(height: 20),
_buildActionButtonsPassengers(),
const SizedBox(height: 20),
const SizedBox(height: 20),
_buildInvitationsListPassengers(context),
const SizedBox(height: 20),
_buildUnifiedRewardsList(isDriver: false),
],
);
}
Widget _buildPermanentCodeCard(BuildContext context) {
return GetBuilder<InvitesRewardsController>(
builder: (rc) {
String code = rc.referralCode ?? 'Loading...'.tr;
return Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
margin: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColor.blueColor.withOpacity(0.2)),
),
child: Column(
children: [
Text(
"Your Permanent Referral Code".tr,
style: const TextStyle(
fontSize: 14,
color: CupertinoColors.secondaryLabel,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
code,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: AppColor.blueColor,
letterSpacing: 2,
),
),
if (rc.referralCode != null) ...[
const SizedBox(width: 16),
GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: rc.referralCode!));
mySnackbarSuccess('Code copied!'.tr);
},
child: const Icon(
CupertinoIcons.doc_on_doc,
color: AppColor.blueColor,
size: 20,
),
),
],
],
),
if (rc.referralCode != null) ...[
const SizedBox(height: 16),
CupertinoButton(
color: AppColor.blueColor.withOpacity(0.1),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 8),
borderRadius: BorderRadius.circular(8),
onPressed: () {
final appName = 'Siro';
final isDriver = controller.selectedTab == 0;
final shareText = isDriver
? 'Join $appName as a driver! Use my referral code: ${rc.referralCode}\nDownload: ${AppLink.inviteRedirectUrl}?code=${rc.referralCode}&app=driver\n\n💡 Note: If the link is not clickable, save this number or reply to activate links!'
: 'Get a ride with $appName! Use my referral code: ${rc.referralCode} for a discount.\nDownload: ${AppLink.inviteRedirectUrl}?code=${rc.referralCode}&app=rider\n\n💡 Note: If the link is not clickable, save this number or reply to activate links!';
Share.share(shareText);
},
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(CupertinoIcons.share, color: AppColor.blueColor, size: 18),
const SizedBox(width: 8),
Text(
'Share via WhatsApp Groups'.tr,
style: const TextStyle(
color: AppColor.blueColor,
fontSize: 14,
fontWeight: FontWeight.bold,
),
),
],
),
),
],
],
),
);
},
);
}
Widget _buildManualCodeEntry(BuildContext context) {
final TextEditingController manualCodeController = TextEditingController();
return Container(
padding: const EdgeInsets.all(16),
margin: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Enter Inviter's Code".tr,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: CupertinoColors.label,
),
),
const SizedBox(height: 8),
Text(
"If someone referred you, enter their code below to link accounts and receive rewards.".tr,
style: const TextStyle(
fontSize: 12,
color: CupertinoColors.secondaryLabel,
),
),
const SizedBox(height: 12),
Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: CupertinoColors.white,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: CupertinoColors.systemGrey4),
),
child: CupertinoTextField.borderless(
controller: manualCodeController,
placeholder: 'Enter Code (e.g. AB1234)'.tr,
padding: const EdgeInsets.all(10),
textCapitalization: TextCapitalization.characters,
),
),
),
const SizedBox(width: 12),
CupertinoButton(
color: AppColor.blueColor,
padding: const EdgeInsets.symmetric(horizontal: 16),
borderRadius: BorderRadius.circular(8),
onPressed: () {
if (manualCodeController.text.trim().isNotEmpty) {
rewardsController.linkInviteCode(manualCodeController.text.trim());
manualCodeController.clear();
} else {
mySnackbarError('Please enter a referral code'.tr);
}
},
child: Text('Link'.tr, style: const TextStyle(color: Colors.white, fontSize: 14)),
),
],
),
],
),
);
}
Widget _buildPhoneInput() {
return Container(
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
Expanded(
child: CupertinoTextField.borderless(
controller: controller.invitePhoneController,
placeholder: 'Enter phone'.tr,
padding: const EdgeInsets.all(12),
keyboardType: TextInputType.phone,
),
),
CupertinoButton(
child: const Icon(CupertinoIcons.person_badge_plus,
color: AppColor.blueColor),
onPressed: () async {
await controller.pickContactFromNativeApp();
if (controller.contacts.isNotEmpty) {
if (box.read(BoxName.isSavedPhones) == null) {
// controller.savePhoneToServer();
box.write(BoxName.isSavedPhones, true);
}
_showContactsDialog(Get.context!);
}
},
),
],
),
);
}
Widget _buildActionButtons() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 16.0),
child: Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: CupertinoButton(
color: AppColor.blueColor,
borderRadius: BorderRadius.circular(10),
padding: const EdgeInsets.symmetric(vertical: 14),
onPressed: controller.sendInvite,
child: Text(
'Send Invite'.tr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CupertinoColors.white,
),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: CupertinoButton(
color: AppColor.blueColor,
borderRadius: BorderRadius.circular(10),
padding: const EdgeInsets.symmetric(vertical: 14),
child: Text(
'Show Invitations'.tr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CupertinoColors.white,
),
),
onPressed: () async {
controller.fetchDriverStats();
},
),
),
),
],
),
);
}
Widget _buildActionButtonsPassengers() {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 20.0, horizontal: 16.0),
child: Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: CupertinoButton(
color: AppColor.blueColor,
borderRadius: BorderRadius.circular(10),
padding: const EdgeInsets.symmetric(vertical: 14),
onPressed: controller.sendInviteToPassenger,
child: Text(
'Send Invite'.tr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CupertinoColors.white,
),
),
),
),
),
const SizedBox(width: 16),
Expanded(
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(10),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 6,
offset: const Offset(0, 3),
),
],
),
child: CupertinoButton(
color: AppColor.blueColor,
borderRadius: BorderRadius.circular(10),
padding: const EdgeInsets.symmetric(vertical: 14),
child: Text(
'Show Invitations'.tr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: CupertinoColors.white,
),
),
onPressed: () async {
controller.fetchDriverStatsPassengers();
},
),
),
),
],
),
);
}
Widget _buildInvitationsList(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Invitations Sent".tr, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
controller.driverInvitationData.isEmpty
? Center(
child: Text(
"No invitation found yet!".tr,
style: const TextStyle(
color: CupertinoColors.secondaryLabel,
fontSize: 17,
),
),
)
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.driverInvitationData.length,
itemBuilder: (context, index) {
return _buildInvitationItem(context, index);
},
),
],
);
}
Widget _buildInvitationsListPassengers(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Invitations Sent".tr, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
controller.driverInvitationDataToPassengers.isEmpty
? Center(
child: Text(
"No invitation found yet!".tr,
style: const TextStyle(
color: CupertinoColors.secondaryLabel,
fontSize: 17,
),
),
)
: ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: controller.driverInvitationDataToPassengers.length,
itemBuilder: (context, index) {
return _buildInvitationItemPassengers(context, index);
},
),
],
);
}
Widget _buildInvitationItem(BuildContext context, int index) {
int countOfInvitDriver = int.tryParse(
(controller.driverInvitationData[index]['countOfInvitDriver'])
?.toString() ??
'0') ??
0;
double progressValue = (countOfInvitDriver / 100.0).clamp(0.0, 1.0);
return GestureDetector(
onTap: () {
controller.onSelectDriverInvitation(index);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 8.0),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
(controller.driverInvitationData[index]['invitorName']),
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: CupertinoColors.label,
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: progressValue,
backgroundColor: CupertinoColors.systemGrey4,
valueColor:
const AlwaysStoppedAnimation<Color>(AppColor.blueColor),
minHeight: 6,
),
),
const SizedBox(height: 4),
Text(
'$countOfInvitDriver / 100 ${'Trip'.tr}',
style: const TextStyle(
fontSize: 13,
color: CupertinoColors.secondaryLabel,
),
),
],
),
),
);
}
Widget _buildInvitationItemPassengers(BuildContext context, int index) {
// Extracting the data from the sample JSON-like structure
var invitation = controller.driverInvitationDataToPassengers[index];
int countOfInvitDriver =
int.tryParse((invitation['countOfInvitDriver'])?.toString() ?? '0') ??
0;
double progressValue = (countOfInvitDriver / 10.0).clamp(0.0, 1.0);
return GestureDetector(
onTap: () {
controller.onSelectPassengerInvitation(index);
},
child: Container(
margin: const EdgeInsets.symmetric(vertical: 8.0),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
(invitation['passengerName'])
.toString(), // Handle null or missing data
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: CupertinoColors.label,
),
),
const SizedBox(height: 8),
ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: progressValue,
backgroundColor: CupertinoColors.systemGrey4,
valueColor:
const AlwaysStoppedAnimation<Color>(AppColor.blueColor),
minHeight: 6,
),
),
const SizedBox(height: 4),
Text(
'$countOfInvitDriver / 3 ${'Trip'.tr}', // Show trips completed
style: const TextStyle(
fontSize: 13,
color: CupertinoColors.secondaryLabel,
),
),
],
),
),
);
}
/*
Widget _buildPassengerStats(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"Your Rewards".tr,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
color: CupertinoColors.label,
),
),
const SizedBox(height: 16),
_buildStatItem(
context,
"Total Invites".tr,
(controller.driverInvitationDataToPassengers[0]
['countOfInvitDriver']
.toString()),
),
_buildStatItem(
context,
"Active Users".tr,
(controller.driverInvitationDataToPassengers[0]['passengerName'])
.toString(),
),
],
),
);
}
Widget _buildStatItem(BuildContext context, String label, String value) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: const TextStyle(
color: CupertinoColors.label,
fontSize: 15,
),
),
Text(
value,
style: const TextStyle(
fontWeight: FontWeight.w600,
color: AppColor.blueColor,
fontSize: 15,
),
),
],
),
);
}
*/
void _showContactsDialog(BuildContext context) {
showCupertinoModalPopup(
context: context,
builder: (BuildContext context) => Container(
height: 400,
color: CupertinoColors.systemBackground,
child: Column(
children: [
Container(
padding: const EdgeInsets.all(16.0),
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: CupertinoColors.separator.withOpacity(0.5)),
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
CupertinoButton(
padding: EdgeInsets.zero,
child: Text('Cancel'.tr),
onPressed: () => Navigator.pop(context),
),
Text(
'Choose from contact'.tr,
style: const TextStyle(
fontSize: 17,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 60), // For balance
],
),
),
Expanded(
child: ListView.builder(
itemCount: controller.contactMaps.length,
itemBuilder: (context, index) {
final contact = controller.contactMaps[index];
return Container(
decoration: BoxDecoration(
border: Border(
bottom: BorderSide(
color: CupertinoColors.separator.withOpacity(0.5),
),
),
),
child: CupertinoButton(
padding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
contact['name'],
style: const TextStyle(
color: CupertinoColors.label,
fontSize: 17,
),
),
Text(
(contact['phones'][0].toString()),
style: const TextStyle(
color: CupertinoColors.secondaryLabel,
fontSize: 15,
),
),
],
),
),
const Icon(
CupertinoIcons.chevron_right,
color: CupertinoColors.secondaryLabel,
),
],
),
onPressed: () {
controller.selectPhone(contact['phones'].toString());
// Navigator.pop(context);
},
),
);
},
),
),
],
),
),
);
}
Widget _buildUnifiedRewardsList({required bool isDriver}) {
return GetBuilder<InvitesRewardsController>(
builder: (rewardsController) {
if (rewardsController.isLoading) {
return const Center(child: CupertinoActivityIndicator());
}
var filteredList = rewardsController.referrals.where((ref) =>
isDriver ? ref['invited_user_type'] == 'driver' : ref['invited_user_type'] == 'passenger'
).toList();
if (filteredList.isEmpty) {
return const SizedBox(); // Hide if no valid rewards
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text("Reward Status".tr, style: const TextStyle(fontSize: 17, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: filteredList.length,
itemBuilder: (context, index) {
var ref = filteredList[index];
int trips = ref['trip_count'] ?? 0;
int target = ref['target_trips'] ?? 1;
bool canClaim = ref['can_claim'] ?? false;
bool isClaimed = ref['is_reward_claimed'] == 1;
return Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: CupertinoColors.systemGrey6,
borderRadius: BorderRadius.circular(12),
),
child: Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(isDriver ? "Driver Referral".tr : "Passenger Referral".tr, style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text("Trips: $trips / $target".tr, style: const TextStyle(color: CupertinoColors.secondaryLabel, fontSize: 13)),
if (isClaimed)
Text("Reward Claimed".tr, style: const TextStyle(color: CupertinoColors.activeGreen, fontSize: 12, fontWeight: FontWeight.bold))
else if (!canClaim)
Text("Waiting for trips".tr, style: const TextStyle(color: CupertinoColors.systemOrange, fontSize: 12))
],
),
),
if (canClaim && !isClaimed)
CupertinoButton(
color: AppColor.blueColor,
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
onPressed: () => rewardsController.claimUnifiedReward(ref['id']),
child: Text("Claim".tr, style: const TextStyle(color: Colors.white, fontSize: 14)),
)
],
),
);
},
),
],
);
},
);
}
}
+235
View File
@@ -0,0 +1,235 @@
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:get/get.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../constant/info.dart';
import '../../../constant/links.dart';
import '../../../constant/style.dart';
import '../../../controller/auth/captin/login_captin_controller.dart';
import '../../../main.dart';
import '../../widgets/elevated_btn.dart';
import 'otp_page.dart'; // تأكد من وجود هذا الملف لديك
class LoginCaptin extends StatefulWidget {
const LoginCaptin({super.key});
@override
State<LoginCaptin> createState() => _LoginCaptinState();
}
class _LoginCaptinState extends State<LoginCaptin> with WidgetsBindingObserver {
final LoginDriverController controller = Get.put(LoginDriverController());
@override
void initState() {
super.initState();
WidgetsBinding.instance.addObserver(this);
// فحص الإذن عند فتح الصفحة مباشرة
controller.getLocationPermission();
}
@override
void dispose() {
WidgetsBinding.instance.removeObserver(this);
super.dispose();
}
@override
void didChangeAppLifecycleState(AppLifecycleState state) {
// التحقق عند العودة من الإعدادات
if (state == AppLifecycleState.resumed) {
controller.getLocationPermission();
}
}
@override
Widget build(BuildContext context) {
return GetBuilder<LoginDriverController>(
builder: (controller) {
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
body: SafeArea(
child: Center(
child: _buildBodyContent(context, controller),
),
),
);
},
);
}
Widget _buildBodyContent(
BuildContext context, LoginDriverController controller) {
// 1. صفحة الموافقة على الشروط
if (box.read(BoxName.agreeTerms) != 'agreed') {
return _buildAgreementPage(context, controller);
}
// 2. صفحة إذن الموقع
if (box.read(BoxName.locationPermission) != 'true') {
return _buildLocationPermissionPage(context, controller);
}
// 3. صفحة تسجيل الدخول (رقم الهاتف)
return PhoneNumberScreen();
}
// --- صفحة الشروط والأحكام ---
Widget _buildAgreementPage(
BuildContext context, LoginDriverController controller) {
return Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.policy_outlined,
size: 80, color: AppColor.primaryColor),
const SizedBox(height: 20),
Text('Driver Agreement'.tr,
textAlign: TextAlign.center, style: AppStyle.headTitle2),
const SizedBox(height: 30),
RichText(
textAlign: TextAlign.center,
text: TextSpan(
style: AppStyle.title.copyWith(height: 1.5),
children: [
TextSpan(
text:
"To become a driver, you must review and agree to the "
.tr),
TextSpan(
text: 'Terms of Use'.tr,
style: const TextStyle(
decoration: TextDecoration.underline,
color: AppColor.blueColor,
fontWeight: FontWeight.bold),
recognizer: TapGestureRecognizer()
..onTap = () {
launchUrl(
Uri.parse('${AppLink.server}/privacy_policy.php'),
mode: LaunchMode.externalApplication);
}),
TextSpan(text: " and acknowledge our Privacy Policy.".tr),
],
),
),
Expanded(
child: Container(
decoration: BoxDecoration(
border: Border.all(color: Theme.of(context).dividerColor),
borderRadius: BorderRadius.circular(8),
color: Theme.of(context).cardColor.withOpacity(0.5),
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(12),
child: HtmlWidget(
box.read(BoxName.lang).toString() == 'ar'
? AppInformation.privacyPolicyArabic
: AppInformation.privacyPolicy,
textStyle: TextStyle(
color: Theme.of(context).textTheme.bodyLarge?.color),
),
),
),
),
CheckboxListTile(
title: Text('I Agree'.tr, style: AppStyle.title),
value: controller.isAgreeTerms,
onChanged: (value) => controller.changeAgreeTerm(),
activeColor: AppColor.primaryColor,
controlAffinity: ListTileControlAffinity.leading,
),
const SizedBox(height: 16),
SizedBox(
width: double.infinity,
child: MyElevatedButton(
title: 'Continue'.tr,
onPressed: controller.isAgreeTerms
? () => controller.saveAgreementTerms()
: () {},
),
),
],
),
);
}
// --- صفحة إذن الموقع ---
Widget _buildLocationPermissionPage(
BuildContext context, LoginDriverController controller) {
return Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Icon(Icons.location_on_outlined,
size: 80, color: AppColor.primaryColor),
const SizedBox(height: 20),
Text(
'Location Access Required'.tr,
textAlign: TextAlign.center,
style: AppStyle.headTitle2,
),
const SizedBox(height: 16),
Text(
'We need access to your location to match you with nearby passengers and provide accurate navigation.'
.tr,
textAlign: TextAlign.center,
style: AppStyle.title,
),
const SizedBox(height: 24),
Text(
'Please allow location access "all the time" to receive ride requests even when the app is in the background.'
.tr,
textAlign: TextAlign.center,
style: AppStyle.title.copyWith(
color: AppColor.greenColor, fontWeight: FontWeight.bold),
),
const SizedBox(height: 40),
MyElevatedButton(
title: "Allow Location Access".tr,
kolor: AppColor.greenColor,
onPressed: () async {
box.write(BoxName.locationPermission, 'true');
controller.update();
// 1. طلب إذن الموقع العادي (أثناء الاستخدام) أولاً
var status = await Permission.location.request();
if (status.isGranted) {
// 2. إذا كنت تحتاج "طوال الوقت" (Background)، اطلبه الآن بشكل منفصل
// ملاحظة: في أندرويد 11+ سينقلك هذا لصفحة إعدادات خاصة
var backgroundStatus =
await Permission.locationAlways.request();
if (backgroundStatus.isGranted) {
box.write(BoxName.locationPermission, 'true');
controller.update();
} else {
// المستخدم وافق على "أثناء الاستخدام" فقط، يمكنك المشي في الحال
// أو إجباره، حسب منطق تطبيقك (تطبيق سائق يفضل Always)
box.write(BoxName.locationPermission, 'true');
controller.update();
}
} else if (status.isPermanentlyDenied) {
// إذا كانت الحالة مرفوضة نهائياً، يجب فتح الإعدادات
openAppSettings();
}
},
),
const SizedBox(height: 16),
TextButton(
onPressed: () => openAppSettings(),
child: Text("Open Settings".tr,
style: const TextStyle(color: AppColor.blueColor)),
),
],
),
);
}
}
+42
View File
@@ -0,0 +1,42 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_driver/constant/colors.dart';
import 'package:siro_driver/controller/functions/log_out.dart';
import 'package:siro_driver/views/widgets/elevated_btn.dart';
import 'package:siro_driver/views/widgets/my_scafold.dart';
class LogoutCaptain extends StatelessWidget {
const LogoutCaptain({super.key});
@override
Widget build(BuildContext context) {
return MyScafolld(
title: 'Log Out Page'.tr,
body: [
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
MyElevatedButton(
title: 'Log Off'.tr,
onPressed: () {
LogOutController().logOutCaptain();
}),
const SizedBox(
height: 30,
),
MyElevatedButton(
title: 'Delete My Account'.tr,
onPressed: () {
LogOutController().deletecaptainAccount();
},
kolor: AppColor.redColor,
),
],
),
)
],
isleading: true,
);
}
}
@@ -0,0 +1,495 @@
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:siro_driver/controller/auth/captin/login_captin_controller.dart';
// --- Placeholder Imports ---
// Assuming these files exist in your project structure.
import '../../../constant/colors.dart';
import '../../../controller/auth/captin/phone_helper_controller.dart';
import '../../../controller/local/phone_intel/intl_phone_field.dart';
import '../../../print.dart';
import '../../../constant/box_name.dart';
import '../../../main.dart';
import '../../../controller/functions/country_logic.dart';
// Assuming you have an AppColor class defined in your project.
// import 'path/to/your/app_color.dart';
/// A visually revamped authentication screen with a light, glassy effect,
/// themed for the driver application using a green primary color.
class AuthScreen extends StatelessWidget {
final String title;
final String subtitle;
final Widget form;
const AuthScreen({
super.key,
required this.title,
required this.subtitle,
required this.form,
});
/// Shows a dialog for testers to log in using email and password.
void _showTesterLoginDialog(
BuildContext context, LoginDriverController controller) {
final testerEmailController = TextEditingController();
final testerPasswordController = TextEditingController();
final testerFormKey = GlobalKey<FormState>();
showDialog(
context: context,
barrierDismissible: true,
builder: (BuildContext dialogContext) {
return BackdropFilter(
filter: ImageFilter.blur(sigmaX: 5, sigmaY: 5),
child: AlertDialog(
backgroundColor: Colors.white.withOpacity(0.85),
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
title: const Text(
'App Tester Login',
textAlign: TextAlign.center,
style:
TextStyle(fontWeight: FontWeight.bold, color: Colors.black87),
),
content: Form(
key: testerFormKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: testerEmailController,
keyboardType: TextInputType.emailAddress,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
labelText: 'Email',
prefixIcon: const Icon(Icons.email_outlined),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: AppColor.greenColor, width: 2),
),
),
validator: (value) => value == null || !value.contains('@')
? 'Enter a valid email'.tr
: null,
),
const SizedBox(height: 16),
TextFormField(
controller: testerPasswordController,
obscureText: true,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
labelText: 'Password',
prefixIcon: const Icon(Icons.lock_outline),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: AppColor.greenColor, width: 2),
),
),
validator: (value) => value == null || value.isEmpty
? 'Enter a password'
: null,
),
],
),
),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.of(dialogContext).pop(),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.greenColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child:
const Text('Login', style: TextStyle(color: Colors.white)),
onPressed: () {
if (testerFormKey.currentState!.validate()) {
controller.loginUsingCredentialsWithoutGoogle(
testerPasswordController.text.trim(),
testerEmailController.text.trim(),
);
Navigator.of(dialogContext).pop();
}
},
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
// Controller for the driver's login logic
final loginController = Get.put(LoginDriverController());
return Scaffold(
body: Container(
// NEW: Light and airy gradient with green accents
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
Colors.white,
Colors.green.shade50,
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Stack(
children: [
// Subtle background shapes with the new primary color
Positioned(
top: -80,
left: -100,
child: Container(
width: 250,
height: 250,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.greenColor.withOpacity(0.1),
),
),
),
Positioned(
bottom: -120,
right: -150,
child: Container(
width: 350,
height: 350,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: AppColor.greenColor.withOpacity(0.15),
),
),
),
Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// GestureDetector for tester login
GestureDetector(
onLongPress: () {
_showTesterLoginDialog(context, loginController);
},
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.white.withOpacity(0.5),
border: Border.all(
color: Colors.white.withOpacity(0.8), width: 2),
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.05),
blurRadius: 10,
spreadRadius: 5)
]),
child: ClipRRect(
borderRadius: BorderRadius.circular(50),
child: Image.asset('assets/images/logo.gif',
height: 100)),
),
),
const SizedBox(height: 20),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87),
),
const SizedBox(height: 10),
Text(
subtitle,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 16,
color: Colors.black54,
),
),
const SizedBox(height: 30),
// Glassmorphism Container for the form with a whiter look
ClipRRect(
borderRadius: BorderRadius.circular(25.0),
child: BackdropFilter(
filter: ImageFilter.blur(sigmaX: 15, sigmaY: 15),
child: Container(
padding: const EdgeInsets.all(24.0),
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.6),
borderRadius: BorderRadius.circular(25.0),
border: Border.all(
color: Colors.white.withOpacity(0.8),
width: 1.5,
),
),
child: form,
),
),
),
const SizedBox(height: 20),
// Button for app testers, adapted to the light theme
Material(
color: Colors.black.withOpacity(0.05),
borderRadius: BorderRadius.circular(12),
child: InkWell(
onTap: () =>
_showTesterLoginDialog(context, loginController),
borderRadius: BorderRadius.circular(12),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 10, horizontal: 16),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(Icons.admin_panel_settings_outlined,
color: Colors.black.withOpacity(0.6)),
const SizedBox(width: 8),
Text(
'For App Reviewers / Testers',
style: TextStyle(
color: Colors.black.withOpacity(0.6),
fontWeight: FontWeight.w600,
),
),
],
),
),
),
),
],
),
),
),
],
),
),
);
}
}
// --- UI Screens ---
class PhoneNumberScreen extends StatefulWidget {
const PhoneNumberScreen({super.key});
@override
State<PhoneNumberScreen> createState() => _PhoneNumberScreenState();
}
class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
final _formKey = GlobalKey<FormState>();
bool _isLoading = false;
String _completePhone = '';
void _submit() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
final rawPhone = _completePhone.replaceFirst('+', '');
Log.print(
'📱 _submit rawPhone: "$rawPhone" (from _completePhone: "$_completePhone")');
final success = await PhoneAuthHelper.sendOtp(rawPhone);
if (success && mounted) {
Get.to(() => OtpVerificationScreen(phoneNumber: rawPhone));
}
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return AuthScreen(
title: 'welcome to siro'.tr,
subtitle: 'login or register subtitle'.tr,
form: Form(
key: _formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Enter your phone number'.tr,
style: TextStyle(color: Colors.black87, fontSize: 16),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
IntlPhoneField(
showCountryFlag: false,
searchText: 'Search country'.tr,
languageCode: 'ar',
style: const TextStyle(color: Colors.black),
dropdownTextStyle: const TextStyle(color: Colors.black87),
decoration: InputDecoration(
labelText: 'Phone Number'.tr,
hintText: 'witout zero'.tr,
labelStyle: const TextStyle(color: Colors.black54),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.black.withOpacity(0.1)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
const BorderSide(color: AppColor.greenColor, width: 2),
),
),
initialCountryCode: CountryLogic.getCountryPrefix(box.read(BoxName.countryCode) ?? 'Syria'),
onChanged: (phone) {
_completePhone = phone.completeNumber;
},
validator: (phone) {
if (phone == null || phone.number.isEmpty) {
return 'Please enter your phone number'.tr;
} // Check if the number is a Syrian number
if (phone.countryISOCode != 'SY') {
return 'Only Syrian phone numbers are allowed'.tr;
}
// Check if the national number part starts with '0'
if (phone.completeNumber.startsWith('96309') ||
phone.completeNumber.startsWith('+9630') ||
phone.completeNumber.startsWith('09')) {
return 'Please enter the number without the leading 0'.tr;
}
if (phone.completeNumber.length < 10) {
return 'Phone number seems too short'.tr;
}
return null;
},
),
const SizedBox(height: 24),
_isLoading
? const CircularProgressIndicator(color: AppColor.greenColor)
: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.greenColor,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: Text(
'send otp button'.tr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white),
),
),
),
],
),
),
);
}
}
class OtpVerificationScreen extends StatefulWidget {
final String phoneNumber;
const OtpVerificationScreen({super.key, required this.phoneNumber});
@override
State<OtpVerificationScreen> createState() => _OtpVerificationScreenState();
}
class _OtpVerificationScreenState extends State<OtpVerificationScreen> {
final _formKey = GlobalKey<FormState>();
final _otpController = TextEditingController();
bool _isLoading = false;
void _submit() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
await PhoneAuthHelper.verifyOtp(
widget.phoneNumber, _otpController.text.trim());
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return AuthScreen(
title: 'verify your number title'.tr,
subtitle:
'otp sent subtitle'.trParams({'phoneNumber': widget.phoneNumber}),
form: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Enter the 3-digit code',
style: TextStyle(color: Colors.black87, fontSize: 16),
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
Form(
key: _formKey,
child: TextFormField(
controller: _otpController,
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 3,
style: const TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.black87,
letterSpacing: 18,
),
decoration: InputDecoration(
counterText: "",
hintText: '---',
hintStyle: TextStyle(
color: Colors.black.withOpacity(0.2),
letterSpacing: 18,
fontSize: 28),
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 10),
),
validator: (v) => v == null || v.length < 3 ? '' : null,
),
),
const SizedBox(height: 30),
_isLoading
? const CircularProgressIndicator(color: AppColor.greenColor)
: SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.greenColor,
padding: const EdgeInsets.symmetric(vertical: 16),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: Text(
'verify and continue button'.tr,
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: Colors.white),
),
),
),
],
),
);
}
}
@@ -0,0 +1,176 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../../../controller/auth/captin/opt_token_controller.dart';
class OtpVerificationPage extends StatefulWidget {
final String phone;
final String deviceToken;
final String token;
final String ptoken;
const OtpVerificationPage({
super.key,
required this.phone,
required this.deviceToken,
required this.token,
required this.ptoken,
});
@override
State<OtpVerificationPage> createState() => _OtpVerificationPageState();
}
class _OtpVerificationPageState extends State<OtpVerificationPage> {
late final OtpVerificationController controller;
final List<FocusNode> _focusNodes = List.generate(3, (index) => FocusNode());
final List<TextEditingController> _textControllers =
List.generate(3, (index) => TextEditingController());
@override
void initState() {
super.initState();
controller = Get.put(OtpVerificationController(
phone: widget.phone,
deviceToken: widget.deviceToken,
token: widget.token,
));
}
@override
void dispose() {
for (var node in _focusNodes) {
node.dispose();
}
for (var controller in _textControllers) {
controller.dispose();
}
super.dispose();
}
void _onOtpChanged(String value, int index) {
if (value.isNotEmpty) {
if (index < 2) {
_focusNodes[index + 1].requestFocus();
} else {
_focusNodes[index].unfocus(); // إلغاء التركيز بعد آخر حقل
}
} else if (index > 0) {
_focusNodes[index - 1].requestFocus();
}
// تجميع نصوص كل الحقول لتكوين الرمز النهائي
controller.otpCode.value = _textControllers.map((c) => c.text).join();
}
Widget _buildOtpInputFields() {
return Directionality(
textDirection: TextDirection.ltr, // لضمان ترتيب الحقول من اليسار لليمين
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: List.generate(3, (index) {
return SizedBox(
width: 45,
height: 55,
child: TextFormField(
controller: _textControllers[index],
focusNode: _focusNodes[index],
textAlign: TextAlign.center,
keyboardType: TextInputType.number,
maxLength: 1,
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
decoration: InputDecoration(
counterText: "",
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade300),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(
color: Theme.of(context).primaryColor, width: 2),
),
),
onChanged: (value) => _onOtpChanged(value, index),
),
);
}),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Verify OTP'.tr),
backgroundColor: Colors.transparent,
elevation: 0,
centerTitle: true,
),
backgroundColor: Colors.grey[50],
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(24.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const SizedBox(height: 20),
Icon(Icons.phonelink_lock_rounded,
size: 80, color: Theme.of(context).primaryColor),
const SizedBox(height: 24),
Text(
'Verification Code'.tr,
style:
const TextStyle(fontSize: 24, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20.0),
child: Text(
'${'We have sent a verification code to your mobile number:'.tr} ${widget.phone}',
textAlign: TextAlign.center,
style: TextStyle(
color: Colors.grey.shade600, fontSize: 16, height: 1.5),
),
),
const SizedBox(height: 40),
_buildOtpInputFields(),
const SizedBox(height: 40),
Obx(() => SizedBox(
width: double.infinity,
height: 50,
child: controller.isVerifying.value
? const Center(child: CircularProgressIndicator())
: ElevatedButton(
style: ElevatedButton.styleFrom(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12))),
onPressed: () =>
controller.verifyOtp(widget.ptoken),
child: Text('Verify'.tr,
style: const TextStyle(
fontSize: 18, fontWeight: FontWeight.w600)),
),
)),
const SizedBox(height: 24),
Obx(
() => controller.canResend.value
? TextButton(
onPressed: controller.sendOtp,
child: Text('Resend Code'.tr),
)
: Text(
'${'You can resend in'.tr} ${controller.countdown.value} ${'seconds'.tr}',
style: const TextStyle(color: Colors.grey),
),
)
],
),
),
),
);
}
}