Connect Flutter phone auth and Android platform

This commit is contained in:
Hamza-Ayed
2026-10-04 01:44:07 +03:00
parent 5b90da6b18
commit 98490a17e6
44 changed files with 1271 additions and 168 deletions
+19
View File
@@ -4,7 +4,9 @@ import '../controllers/workout_controller.dart';
import '../models/workout.dart';
import '../screens/history_screen.dart';
import '../services/sync_service.dart';
import '../services/auth_service.dart';
import '../utils/formatters.dart';
import 'phone_login_screen.dart';
const _ink = Color(0xFF17251E);
const _green = Color(0xFF456C52);
@@ -73,6 +75,23 @@ class HomeScreen extends StatelessWidget {
const TextStyle(color: _ink, fontSize: 11)),
]),
)),
PopupMenuButton<String>(
tooltip: 'الحساب',
icon: const Icon(Icons.account_circle_outlined,
color: _green, size: 25),
onSelected: (value) async {
if (value == 'logout') {
await Get.find<AuthService>().logout();
Get.offAll(() => const PhoneLoginScreen());
}
},
itemBuilder: (context) => const [
PopupMenuItem(
value: 'logout',
child: Text('تسجيل الخروج'),
),
],
),
]),
const SizedBox(height: 34),
Text(_arabicToday(),
+287
View File
@@ -0,0 +1,287 @@
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import '../services/auth_service.dart';
import '../services/sync_service.dart';
import 'home_screen.dart';
const _loginInk = Color(0xFF17251E);
const _loginGreen = Color(0xFF456C52);
class PhoneLoginScreen extends StatefulWidget {
const PhoneLoginScreen({super.key});
@override
State<PhoneLoginScreen> createState() => _PhoneLoginScreenState();
}
class _PhoneLoginScreenState extends State<PhoneLoginScreen> {
final _phone = TextEditingController();
final _code = TextEditingController();
final _name = TextEditingController();
bool _registration = false;
bool _codeSent = false;
bool _busy = false;
@override
void dispose() {
_phone.dispose();
_code.dispose();
_name.dispose();
super.dispose();
}
Future<void> _requestCode() async {
final phone = _phone.text.trim();
if (!RegExp(r'^\+[1-9][0-9]{7,14}$').hasMatch(phone)) {
_showMessage('اكتب رقمك بصيغة دولية، مثل ‎+9627XXXXXXXX.');
return;
}
setState(() => _busy = true);
try {
await Get.find<AuthService>().requestOtp(
phoneE164: phone,
isRegistration: _registration,
);
if (mounted) setState(() => _codeSent = true);
_showMessage('أرسلنا رمز التحقق إذا كانت الخدمة مفعّلة.');
} catch (error) {
_showMessage(error.toString());
} finally {
if (mounted) setState(() => _busy = false);
}
}
Future<void> _verifyCode() async {
if (!RegExp(r'^\d{6}$').hasMatch(_code.text.trim())) {
_showMessage('أدخل رمز التحقق المكوّن من ٦ أرقام.');
return;
}
if (_registration && _name.text.trim().isEmpty) {
_showMessage('اكتب الاسم الذي تود ظهوره في حسابك.');
return;
}
setState(() => _busy = true);
try {
await Get.find<AuthService>().verifyOtp(
phoneE164: _phone.text.trim(),
code: _code.text.trim(),
isRegistration: _registration,
displayName: _name.text.trim().isEmpty ? null : _name.text.trim(),
);
if (mounted) {
Get.offAll(() => const HomeScreen());
Get.find<SyncService>().syncPendingWorkouts(retryNow: true);
}
} catch (error) {
_showMessage(error.toString());
} finally {
if (mounted) setState(() => _busy = false);
}
}
void _showMessage(String message) {
if (!mounted) return;
ScaffoldMessenger.of(context)
..hideCurrentSnackBar()
..showSnackBar(SnackBar(content: Text(message)));
}
@override
Widget build(BuildContext context) {
final auth = Get.find<AuthService>();
return Scaffold(
backgroundColor: const Color(0xFFF5F7F2),
body: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(24, 32, 24, 28),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Align(
alignment: AlignmentDirectional.centerStart,
child: Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: _loginGreen,
borderRadius: BorderRadius.circular(15),
),
alignment: Alignment.center,
child: const Text('S',
style: TextStyle(
color: Colors.white,
fontFamily: 'Georgia',
fontSize: 27,
fontStyle: FontStyle.italic)),
),
),
const SizedBox(height: 40),
const Text('أهلًا بك في SportPath',
style: TextStyle(
color: _loginGreen,
fontSize: 13,
fontWeight: FontWeight.w600)),
const SizedBox(height: 9),
Text(
_codeSent ? 'تحقق من رقمك.' : 'رحلتك تبدأ من هنا.',
style: const TextStyle(
color: _loginInk,
fontSize: 30,
height: 1.25,
fontWeight: FontWeight.w600,
letterSpacing: -.7),
),
const SizedBox(height: 11),
Text(
_codeSent
? 'أدخل الرمز المكوّن من ٦ أرقام الذي وصلك برسالة.'
: 'استخدم رقم هاتفك لحفظ تقدمك ومزامنة نشاطك بأمان.',
style: const TextStyle(
color: Color(0xFF778078), fontSize: 14, height: 1.8),
),
const SizedBox(height: 24),
if (!_codeSent) ...[
SegmentedButton<bool>(
segments: const [
ButtonSegment(
value: false, label: Text('تسجيل الدخول')),
ButtonSegment(value: true, label: Text('حساب جديد')),
],
selected: {_registration},
onSelectionChanged: _busy
? null
: (value) =>
setState(() => _registration = value.first),
),
const SizedBox(height: 18),
if (_registration) ...[
_InputField(
controller: _name,
label: 'الاسم',
hint: 'كيف نناديك؟',
keyboardType: TextInputType.name),
const SizedBox(height: 12),
],
_InputField(
controller: _phone,
label: 'رقم الهاتف',
hint: '+9627XXXXXXXX',
keyboardType: TextInputType.phone,
direction: TextDirection.ltr),
] else ...[
_InputField(
controller: _phone,
label: 'رقم الهاتف',
hint: '+9627XXXXXXXX',
keyboardType: TextInputType.phone,
direction: TextDirection.ltr,
enabled: false),
const SizedBox(height: 12),
_InputField(
controller: _code,
label: 'رمز التحقق',
hint: '000000',
keyboardType: TextInputType.number,
direction: TextDirection.ltr,
maxLength: 6),
const SizedBox(height: 9),
TextButton(
onPressed: _busy
? null
: () => setState(() {
_codeSent = false;
_code.clear();
}),
child: const Text('تغيير الرقم أو طلب رمز جديد')),
],
const SizedBox(height: 13),
FilledButton(
onPressed:
_busy ? null : (_codeSent ? _verifyCode : _requestCode),
style: FilledButton.styleFrom(
backgroundColor: _loginGreen,
foregroundColor: Colors.white,
minimumSize: const Size.fromHeight(52),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(15))),
child: _busy
? const SizedBox(
width: 21,
height: 21,
child: CircularProgressIndicator(
strokeWidth: 2, color: Colors.white))
: Text(_codeSent ? 'تأكيد الرمز' : 'إرسال رمز التحقق',
style: const TextStyle(
fontSize: 14, fontWeight: FontWeight.w600)),
),
const SizedBox(height: 16),
if (!auth.isApiConfigured)
const Text(
'نسخة التطوير تحتاج عنوان API آمنًا عبر API_BASE_URL قبل تسجيل الدخول.',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF8A6254),
fontSize: 11,
height: 1.7)),
const SizedBox(height: 22),
const Text(
'نستخدم رقمك للتحقق من الحساب فقط. لا نقرأ بيانات البصمة أو الوجه؛ المصادقة الحيوية تبقى داخل نظام جهازك.',
textAlign: TextAlign.center,
style: TextStyle(
color: Color(0xFF899189), fontSize: 10, height: 1.8)),
],
),
),
),
),
),
);
}
}
class _InputField extends StatelessWidget {
const _InputField(
{required this.controller,
required this.label,
required this.hint,
required this.keyboardType,
this.direction,
this.enabled = true,
this.maxLength});
final TextEditingController controller;
final String label;
final String hint;
final TextInputType keyboardType;
final TextDirection? direction;
final bool enabled;
final int? maxLength;
@override
Widget build(BuildContext context) => TextField(
controller: controller,
enabled: enabled,
keyboardType: keyboardType,
textDirection: direction,
maxLength: maxLength,
style: const TextStyle(color: _loginInk, fontSize: 15),
decoration: InputDecoration(
labelText: label,
hintText: hint,
counterText: '',
filled: true,
fillColor: Colors.white,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: BorderSide.none),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: Color(0xFFE6EBE3))),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(15),
borderSide: const BorderSide(color: _loginGreen, width: 1.4)),
),
);
}