Files
tripz-llc/apps/driver/lib/views/auth/captin/otp_page.dart
T
HamzaandClaude Opus 4.8 6234e66471 feat: Gemini AI (doc OCR + face match) + role-assignment endpoints
- GeminiService (integrations/gemini, @Global): extractDocument (OCR fields) + faceMatch; graceful if no GEMINI_API_KEY
- documents: face-match now real via Gemini; POST /admin/documents/:id/ocr extracts fields for CS auto-fill
- roles: PATCH /admin/users/:id/role (admin) + POST /platform/users/role (bootstrap via x-platform-secret)
- config: gemini + platform.secret; .env.example entries

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 20:01:26 +03:00

704 lines
27 KiB
Dart

import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
// --- افترض أن هذه المسارات موجودة في مشروعك ---
import 'package:sefer_driver/controller/auth/captin/login_captin_controller.dart';
import 'package:sefer_driver/controller/auth/captin/phone_helper_controller.dart';
import '../../../constant/box_name.dart';
import '../../../constant/colors.dart';
import '../../../controller/local/phone_intel/intl_phone_field.dart';
import '../../../controller/local/phone_intel/phone_number.dart';
// import '../../../main.dart';
/// ----------------------------------------------------------------------------
/// 1. AUTH SCREEN WRAPPER
/// (يحتوي على التصميم العام + اللوجيك الخفي للـ App Tester)
/// ----------------------------------------------------------------------------
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,
});
// --- دالة إظهار نافذة دخول الفاحصين (Hidden Tester Dialog) ---
void _showTesterLoginDialog(BuildContext context) {
final controller = Get.put(LoginDriverController());
final emailController = TextEditingController();
final passwordController = TextEditingController();
final formKey = GlobalKey<FormState>();
showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: '',
transitionDuration: const Duration(milliseconds: 300),
pageBuilder: (ctx, anim1, anim2) => Container(),
transitionBuilder: (ctx, anim1, anim2, child) {
return ScaleTransition(
scale: CurvedAnimation(parent: anim1, curve: Curves.easeOutBack),
child: AlertDialog(
shape:
RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
backgroundColor: Colors.white,
surfaceTintColor: Colors.white,
title: Column(
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColor.primaryColor.withOpacity(0.1),
shape: BoxShape.circle,
),
child: const Icon(Icons.admin_panel_settings_rounded,
color: AppColor.primaryColor, size: 30),
),
const SizedBox(height: 10),
const Text(
'Tester Access',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppColor.writeColor,
),
),
],
),
content: Form(
key: formKey,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
TextFormField(
controller: emailController,
style: const TextStyle(color: AppColor.writeColor),
decoration: InputDecoration(
labelText: 'Tester Email',
prefixIcon:
const Icon(Icons.email_outlined, color: Colors.grey),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
const BorderSide(color: AppColor.primaryColor),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
),
validator: (v) => v!.contains('@') ? null : 'Invalid Email',
),
const SizedBox(height: 16),
TextFormField(
controller: passwordController,
style: const TextStyle(color: AppColor.writeColor),
obscureText: true,
decoration: InputDecoration(
labelText: 'Password',
prefixIcon:
const Icon(Icons.lock_outline, color: Colors.grey),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12)),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide:
const BorderSide(color: AppColor.primaryColor),
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 16, vertical: 12),
),
validator: (v) => v!.isEmpty ? 'Required' : null,
),
],
),
),
actions: [
TextButton(
onPressed: () => Get.back(),
child: Text('Cancel',
style: TextStyle(color: Colors.grey.shade600)),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10)),
elevation: 0,
),
onPressed: () {
if (formKey.currentState!.validate()) {
controller.logintest(
passwordController.text.trim(),
emailController.text.trim(),
);
Get.back();
}
},
child:
const Text('Login', style: TextStyle(color: Colors.white)),
),
],
),
);
},
);
}
@override
Widget build(BuildContext context) {
final Size size = MediaQuery.of(context).size;
return Scaffold(
backgroundColor: Colors.white,
body: SingleChildScrollView(
child: SizedBox(
height: size.height,
child: Stack(
children: [
// 1. الخلفية العلوية (Blue Gradient Header)
Container(
height: size.height * 0.45,
width: double.infinity,
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topLeft,
end: Alignment.bottomRight,
colors: AppColor.gradientStartEnd,
),
borderRadius: const BorderRadius.only(
bottomRight: Radius.circular(60),
bottomLeft: Radius.circular(0),
),
),
child: Stack(
children: [
// زخارف خلفية بسيطة
Positioned(
top: -40,
left: -40,
child: Container(
width: 150,
height: 150,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
shape: BoxShape.circle,
),
),
),
Positioned(
bottom: 40,
right: -20,
child: Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: Colors.white.withOpacity(0.1),
shape: BoxShape.circle,
),
),
),
// المحتوى (اللوجو والعناوين)
SafeArea(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// --- اللوجو مع الضغط المطول (Backdoor) ---
GestureDetector(
onLongPress: () {
_showTesterLoginDialog(context);
},
child: Container(
padding: const EdgeInsets.all(4),
decoration: const BoxDecoration(
color: Colors.white,
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.black12,
blurRadius: 10,
spreadRadius: 2,
)
],
),
child: ClipOval(
child: Image.asset(
'assets/images/logo.gif',
height: 80,
width: 80,
fit: BoxFit.cover,
),
),
),
),
const SizedBox(height: 20),
Text(
title,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
letterSpacing: 1,
),
),
const SizedBox(height: 8),
Text(
subtitle,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 14,
color: Colors.white.withOpacity(0.9),
),
),
const SizedBox(
height: 40), // مساحة لرفع المحتوى عن الكارت
],
),
),
),
],
),
),
// 2. الفورم (White Floating Card)
Positioned(
top: size.height * 0.38,
left: 20,
right: 20,
child: Container(
padding:
const EdgeInsets.symmetric(horizontal: 24, vertical: 30),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(24),
boxShadow: [
BoxShadow(
color: AppColor.primaryColor.withOpacity(0.15),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: form,
),
),
// 3. التذييل
Positioned(
bottom: 20,
left: 0,
right: 0,
child: Center(
child: Text(
"Tripz Driver Egypt © 2026",
style: TextStyle(
color: Colors.grey.shade400,
fontSize: 12,
fontWeight: FontWeight.w500,
),
),
),
),
],
),
),
),
);
}
}
/// ----------------------------------------------------------------------------
/// 2. PHONE NUMBER SCREEN
/// (تحقق صارم جداً للأرقام المصرية + تنظيف الأصفار)
/// ----------------------------------------------------------------------------
class PhoneNumberScreen extends StatefulWidget {
const PhoneNumberScreen({super.key});
@override
State<PhoneNumberScreen> createState() => _PhoneNumberScreenState();
}
class _PhoneNumberScreenState extends State<PhoneNumberScreen> {
final _formKey = GlobalKey<FormState>();
// ignore: unused_field
String _completePhoneNumber = "";
String _pureNumber = ""; // الرقم بدون كود الدولة
bool _isLoading = false;
void _submit() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
// --- منطق المعالجة (Strict Processing) ---
// 1. تنظيف الرقم من أي صفر في البداية (Standardization)
String cleanNumber =
_pureNumber.startsWith('0') ? _pureNumber.substring(1) : _pureNumber;
// 2. بناء الرقم الكامل ليكون +2010xxxxxxxxx
String finalPhone = "+20$cleanNumber";
// 3. الإرسال
final success = await PhoneAuthHelper.sendOtp(finalPhone);
if (success && mounted) {
setState(() => _isLoading = false);
// ✅ [تصحيح]: الانتقال لصفحة الـ OTP بدلاً من استدعاء دالة verifyOtp
Get.to(() => OtpVerificationScreen(phoneNumber: finalPhone));
} else {
if (mounted) setState(() => _isLoading = false);
}
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return AuthScreen(
title: 'أهلاً بك كابتن'.tr,
subtitle: 'سجل دخولك وابدأ رحلاتك في مصر'.tr,
form: Form(
key: _formKey,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'رقم الهاتف'.tr,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
color: AppColor.writeColor,
),
),
const SizedBox(height: 10),
Directionality(
textDirection: TextDirection.ltr,
child: IntlPhoneField(
disableLengthCheck: true, // تفعيل التحقق اليدوي للصرامة
showCountryFlag: true,
flagsButtonPadding: const EdgeInsets.only(left: 10),
dropdownIconPosition: IconPosition.trailing,
dropdownIcon: const Icon(Icons.keyboard_arrow_down,
color: AppColor.primaryColor),
initialCountryCode: 'EG', // مصر افتراضياً
languageCode: 'ar', // الدول بالعربية
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppColor.writeColor),
decoration: InputDecoration(
hintText: '010xxxxxxxxx',
hintStyle: TextStyle(color: Colors.grey.shade300),
filled: true,
fillColor: Colors.grey.shade50,
contentPadding: const EdgeInsets.symmetric(vertical: 16),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(
color: AppColor.primaryColor, width: 2),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: Colors.redAccent),
),
),
onChanged: (phone) {
_completePhoneNumber = phone.completeNumber;
_pureNumber = phone.number;
},
// --- التحقق الصارم (Strict Validation) ---
validator: (PhoneNumber? phone) {
if (phone == null || phone.number.isEmpty) {
return 'رقم الهاتف مطلوب'.tr;
}
// 1. التأكد أن الدولة مصر
if (phone.countryISOCode != 'EG') {
return 'التطبيق متاح حالياً في مصر فقط'.tr;
}
// 2. تنظيف الرقم من الرموز والمسافات
String number = phone.number.replaceAll(RegExp(r'\D'), '');
// 3. حذف الصفر من البداية لتوحيد التحقق
String cleanNumber =
number.startsWith('0') ? number.substring(1) : number;
// 4. التحقق من الطول (يجب أن يكون 10 أرقام صافية)
if (cleanNumber.length != 10) {
return 'الرقم يجب أن يكون 11 خانة (01xxxxxxxxx)'.tr;
}
// 5. التحقق من مفاتيح الشبكات (010, 011, 012, 015)
List<String> validPrefixes = ['10', '11', '12', '15'];
bool isValidPrefix = validPrefixes
.any((prefix) => cleanNumber.startsWith(prefix));
if (!isValidPrefix) {
return 'يجب أن يبدأ الرقم بـ 010, 011, 012, أو 015'.tr;
}
return null;
},
),
),
const SizedBox(height: 30),
_isLoading
? const Center(
child:
CircularProgressIndicator(color: AppColor.primaryColor))
: SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
foregroundColor: Colors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'إرسال رمز التحقق'.tr,
style: const TextStyle(
fontSize: 16, fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}
/// ----------------------------------------------------------------------------
/// 3. OTP VERIFICATION SCREEN
/// ----------------------------------------------------------------------------
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);
if (mounted) setState(() => _isLoading = false);
}
}
@override
Widget build(BuildContext context) {
return AuthScreen(
title: 'تأكيد الرقم'.tr,
subtitle: '${'تم إرسال الرمز إلى'.tr} ${widget.phoneNumber}',
form: Column(
children: [
Text(
'أدخل الرمز المكون من 5 أرقام'.tr,
style: const TextStyle(
fontSize: 14, color: Colors.grey, fontWeight: FontWeight.w600),
),
const SizedBox(height: 20),
Form(
key: _formKey,
child: Directionality(
textDirection: TextDirection.ltr,
child: TextFormField(
controller: _otpController,
keyboardType: TextInputType.number,
maxLength: 5,
textAlign: TextAlign.center,
style: const TextStyle(
fontSize: 32,
fontWeight: FontWeight.bold,
letterSpacing: 10,
color: AppColor.primaryColor,
),
decoration: InputDecoration(
counterText: '',
hintText: '•••••',
hintStyle: TextStyle(
color: Colors.grey.shade300,
letterSpacing: 10,
),
enabledBorder: UnderlineInputBorder(
borderSide:
BorderSide(color: Colors.grey.shade300, width: 2),
),
focusedBorder: const UnderlineInputBorder(
borderSide:
BorderSide(color: AppColor.primaryColor, width: 3),
),
),
validator: (v) => (v == null || v.length < 5) ? '' : null,
onChanged: (value) {
if (value.length == 5) _submit();
},
),
),
),
const SizedBox(height: 40),
_isLoading
? const Center(
child:
CircularProgressIndicator(color: AppColor.primaryColor))
: SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: Text(
'تحقق الآن'.tr,
style: const TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold),
),
),
),
],
),
);
}
}
/// ----------------------------------------------------------------------------
/// 4. REGISTRATION SCREEN
/// ----------------------------------------------------------------------------
class RegistrationScreen extends StatefulWidget {
final String phoneNumber;
const RegistrationScreen({super.key, required this.phoneNumber});
@override
State<RegistrationScreen> createState() => _RegistrationScreenState();
}
class _RegistrationScreenState extends State<RegistrationScreen> {
final _formKey = GlobalKey<FormState>();
final _firstNameController = TextEditingController();
final _lastNameController = TextEditingController();
final _emailController = TextEditingController();
bool _isLoading = false;
void _submit() async {
if (_formKey.currentState!.validate()) {
setState(() => _isLoading = true);
// await PhoneAuthHelper.registerUser(
// phoneNumber: widget.phoneNumber,
// firstName: _firstNameController.text.trim(),
// lastName: _lastNameController.text.trim(),
// email: _emailController.text.trim(),
// );
if (mounted) setState(() => _isLoading = false);
}
}
Widget _buildField(
TextEditingController controller, String label, IconData icon,
{bool required = true}) {
return TextFormField(
controller: controller,
style: const TextStyle(color: AppColor.writeColor),
decoration: InputDecoration(
labelText: label,
labelStyle: TextStyle(color: Colors.grey.shade600),
prefixIcon: Icon(icon, color: AppColor.primaryColor),
border: OutlineInputBorder(borderRadius: BorderRadius.circular(12)),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: BorderSide(color: Colors.grey.shade200),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
borderSide: const BorderSide(color: AppColor.primaryColor, width: 2),
),
filled: true,
fillColor: Colors.grey.shade50,
),
validator: required ? (v) => v!.isEmpty ? '$label مطلوب'.tr : null : null,
);
}
@override
Widget build(BuildContext context) {
return AuthScreen(
title: 'بيانات الحساب'.tr,
subtitle: 'أكمل بياناتك لإنشاء حساب السائق'.tr,
form: Form(
key: _formKey,
child: Column(
children: [
_buildField(
_firstNameController, 'الاسم الأول'.tr, Icons.person_outline),
const SizedBox(height: 16),
_buildField(
_lastNameController, 'اسم العائلة'.tr, Icons.person_outline),
const SizedBox(height: 16),
_buildField(_emailController, 'البريد الإلكتروني (اختياري)'.tr,
Icons.email_outlined,
required: false),
const SizedBox(height: 30),
_isLoading
? const Center(
child:
CircularProgressIndicator(color: AppColor.primaryColor))
: SizedBox(
width: double.infinity,
height: 55,
child: ElevatedButton(
onPressed: _submit,
style: ElevatedButton.styleFrom(
backgroundColor: AppColor.primaryColor,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12)),
),
child: Text(
'إتمام التسجيل'.tr,
style: const TextStyle(
fontSize: 16,
color: Colors.white,
fontWeight: FontWeight.bold),
),
),
),
],
),
),
);
}
}