Files
fitness/mobile/lib/screens/phone_login_screen.dart
T

288 lines
11 KiB
Dart
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)),
),
);
}