Connect Flutter phone auth and Android platform
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user