Files
saqel/apps/student_app/lib/presentation/screens/auth/auth_screen.dart
T

552 lines
20 KiB
Dart

import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/config/app_config.dart';
import '../../../core/localization/app_strings.dart';
import '../../../core/theme/app_colors.dart';
import '../../../core/theme/app_theme.dart';
import '../../../core/utils/saqel_toast.dart';
import '../../../logic/cubits/auth_cubit.dart';
import '../../widgets/luxury_widgets.dart';
class AuthScreen extends StatefulWidget {
const AuthScreen({super.key});
@override
State<AuthScreen> createState() => _AuthScreenState();
}
class _AuthScreenState extends State<AuthScreen> {
final TextEditingController _phoneController = TextEditingController();
final TextEditingController _otpController = TextEditingController();
final TextEditingController _nationalIdController = TextEditingController();
final TextEditingController _fullNameController = TextEditingController();
String _selectedRole = 'student';
String _selectedGrade = 'grade_10';
String _selectedStream = 'scientific';
@override
void dispose() {
_phoneController.dispose();
_otpController.dispose();
_nationalIdController.dispose();
_fullNameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
body: Container(
decoration: const BoxDecoration(
gradient: RadialGradient(
center: Alignment(0.8, -0.6),
radius: 1.3,
colors: [
Color(0xFF0A2540),
AppColors.darkBackground,
],
),
),
child: SafeArea(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 32),
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 480),
child: BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
if (state is AuthError) {
SaqelToast.showError(context, state.message, title: 'تنبيه المصادقة');
} else if (state is AuthOtpSent) {
SaqelToast.showWhatsApp(
context,
'تم إرسال كود التحقق بنجاح إلى الرقم ${state.phoneNumber}',
title: 'رسالة واتساب من نبيه 📲',
);
} else if (state is AuthNeedsOnboarding) {
SaqelToast.showInfo(
context,
'يرجى استكمال الاسم والفرع لفتح ملفك الأكاديمي',
title: 'طالب جديد في صَقِل 🎓',
);
} else if (state is Authenticated) {
SaqelToast.showSuccess(
context,
'أهلاً بك يا ${state.user.name}',
title: 'تم تسجيل الدخول بنجاح ✨',
);
}
},
builder: (context, state) {
final isLoading = state is AuthLoading;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
// Server Connection Health Badge
Center(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(20),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: AppColors.saqelCyan,
shape: BoxShape.circle,
),
),
const SizedBox(width: 8),
Text(
AppConfig.baseUrl.replaceAll('https://', '').replaceAll('http://', ''),
style: const TextStyle(
fontSize: 11,
fontWeight: FontWeight.w600,
color: AppColors.textSecondaryDark,
),
),
],
),
),
),
const SizedBox(height: 24),
if (state is AuthNeedsOnboarding)
_buildOnboardingStep(context, state, isLoading)
else if (state is AuthNeedsNationalId)
_buildNationalIdStep(context, state, isLoading)
else if (state is AuthOtpSent)
_buildOtpStep(context, state, isLoading)
else
_buildPhoneStep(context, isLoading),
],
);
},
),
),
),
),
),
),
);
}
/// Step 1: Phone Input & Role Selector
Widget _buildPhoneStep(BuildContext context, bool isLoading) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 76,
height: 76,
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [AppColors.appleBlue, AppColors.saqelCyan],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(22),
boxShadow: const [
BoxShadow(
color: Color(0x4D00F5D4),
blurRadius: 25,
offset: Offset(0, 10),
),
],
),
child: const Center(
child: Text(
'ص',
style: TextStyle(
fontSize: 38,
fontWeight: FontWeight.w900,
color: Colors.black,
),
),
),
),
),
const SizedBox(height: 24),
Text(
AppStrings.enterPhoneTitle,
textAlign: TextAlign.center,
style: AppTypography.displayLarge.copyWith(color: Colors.white),
),
const SizedBox(height: 8),
Text(
AppStrings.enterPhoneSubtitle,
textAlign: TextAlign.center,
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondaryDark),
),
const SizedBox(height: 28),
// Role Segmented Pill
Container(
padding: const EdgeInsets.all(4),
decoration: BoxDecoration(
color: AppColors.darkSurface,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: AppColors.darkCardBorder),
),
child: Row(
children: [
Expanded(
child: GestureDetector(
onTap: isLoading ? null : () => setState(() => _selectedRole = 'student'),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: _selectedRole == 'student' ? AppColors.appleBlue : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
AppStrings.studentRole,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: _selectedRole == 'student' ? Colors.white : AppColors.textSecondaryDark,
),
),
),
),
),
),
Expanded(
child: GestureDetector(
onTap: isLoading ? null : () => setState(() => _selectedRole = 'guardian'),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 10),
decoration: BoxDecoration(
color: _selectedRole == 'guardian' ? AppColors.guardianAmber : Colors.transparent,
borderRadius: BorderRadius.circular(12),
),
child: Center(
child: Text(
AppStrings.guardianRole,
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: _selectedRole == 'guardian' ? Colors.black : AppColors.textSecondaryDark,
),
),
),
),
),
),
],
),
),
const SizedBox(height: 20),
LuxuryTextField(
controller: _phoneController,
label: AppStrings.phoneNumber,
hint: '0791234567',
keyboardType: TextInputType.phone,
enabled: !isLoading,
prefixIcon: const Icon(CupertinoIcons.phone_fill, color: AppColors.saqelCyan, size: 20),
),
const SizedBox(height: 24),
LuxuryButton(
text: AppStrings.requestOtp,
isLoading: isLoading,
onPressed: () {
final phone = _phoneController.text.trim();
if (phone.isEmpty) {
SaqelToast.showError(context, 'يرجى إدخال رقم الهاتف المسجل في منصة صَقِل');
return;
}
context.read<AuthCubit>().sendOtp(phone, role: _selectedRole);
},
),
],
);
}
/// Step 2: OTP Verification
Widget _buildOtpStep(BuildContext context, AuthOtpSent state, bool isLoading) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.whatsappGreen.withAlpha(35),
shape: BoxShape.circle,
),
child: const Icon(CupertinoIcons.chat_bubble_2_fill, color: AppColors.whatsappGreen, size: 30),
),
),
const SizedBox(height: 20),
Text(
AppStrings.enterOtpTitle,
textAlign: TextAlign.center,
style: AppTypography.headlineMedium.copyWith(color: Colors.white),
),
const SizedBox(height: 8),
Text(
'${AppStrings.enterOtpSubtitle} (${state.phoneNumber})',
textAlign: TextAlign.center,
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondaryDark),
),
const SizedBox(height: 24),
LuxuryTextField(
controller: _otpController,
label: 'رمز التحقق (6 أرقام)',
hint: '123456',
keyboardType: TextInputType.number,
enabled: !isLoading,
prefixIcon: const Icon(CupertinoIcons.lock_shield_fill, color: AppColors.appleBlue, size: 20),
),
const SizedBox(height: 24),
LuxuryButton(
text: AppStrings.verifyOtp,
isLoading: isLoading,
onPressed: () {
final otp = _otpController.text.trim();
if (otp.isEmpty) {
SaqelToast.showError(context, 'يرجى إدخال رمز التحقق المستلم عبر الواتساب');
return;
}
context.read<AuthCubit>().verifyOtp(state.phoneNumber, otp, role: state.role);
},
),
const SizedBox(height: 14),
TextButton(
onPressed: isLoading ? null : () => context.read<AuthCubit>().checkSession(),
child: const Text(
'تغيير رقم الهاتف ↺',
style: TextStyle(
color: AppColors.textSecondaryDark,
fontWeight: FontWeight.w600,
fontSize: 13,
),
),
),
],
);
}
/// Step 3: National ID Verification (For Students)
Widget _buildNationalIdStep(BuildContext context, AuthNeedsNationalId state, bool isLoading) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.saqelCyan.withAlpha(35),
shape: BoxShape.circle,
),
child: const Icon(CupertinoIcons.person_crop_square_fill, color: AppColors.saqelCyan, size: 30),
),
),
const SizedBox(height: 20),
Text(
AppStrings.enterNationalIdTitle,
textAlign: TextAlign.center,
style: AppTypography.headlineMedium.copyWith(color: Colors.white),
),
const SizedBox(height: 8),
Text(
AppStrings.enterNationalIdSubtitle,
textAlign: TextAlign.center,
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondaryDark),
),
const SizedBox(height: 24),
LuxuryTextField(
controller: _nationalIdController,
label: AppStrings.nationalId,
hint: '2008XXXXXX',
keyboardType: TextInputType.number,
enabled: !isLoading,
prefixIcon: const Icon(CupertinoIcons.number, color: AppColors.saqelCyan, size: 20),
),
const SizedBox(height: 24),
LuxuryButton(
text: AppStrings.continueBtn,
isLoading: isLoading,
onPressed: () {
final nationalId = _nationalIdController.text.trim();
if (nationalId.isEmpty) {
SaqelToast.showError(context, 'يرجى إدخال الرقم الوطني الخاص بالطالب');
return;
}
context.read<AuthCubit>().verifyNationalId(state.identityToken, nationalId);
},
),
],
);
}
/// Step 4: Onboarding (For New Students not registered in school roster)
Widget _buildOnboardingStep(BuildContext context, AuthNeedsOnboarding state, bool isLoading) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Center(
child: Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.appleBlue.withAlpha(35),
shape: BoxShape.circle,
),
child: const Icon(CupertinoIcons.sparkles, color: AppColors.appleBlue, size: 30),
),
),
const SizedBox(height: 20),
Text(
'استكمال بيانات الطالب 🎓',
textAlign: TextAlign.center,
style: AppTypography.headlineMedium.copyWith(color: Colors.white),
),
const SizedBox(height: 8),
Text(
'يرجى إدخال اسمك الكامل واختيار الفرع الأكاديمي لضبط المنهاج',
textAlign: TextAlign.center,
style: AppTypography.bodyMedium.copyWith(color: AppColors.textSecondaryDark),
),
const SizedBox(height: 24),
LuxuryTextField(
controller: _fullNameController,
label: 'الاسم الكامل للطالب',
hint: 'حمزة أحمد الغويريين',
enabled: !isLoading,
prefixIcon: const Icon(CupertinoIcons.person_fill, color: AppColors.appleBlue, size: 20),
),
const SizedBox(height: 18),
Text(
'الصف الدراسي',
style: AppTypography.titleMedium.copyWith(color: AppColors.textSecondaryDark, fontSize: 13),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
children: [
('grade_10', 'الصف العاشر'),
('grade_11', 'الأول ثانوي'),
('grade_12', 'الثاني ثانوي'),
].map((grade) => ChoiceChip(
label: Text(grade.$2),
selected: _selectedGrade == grade.$1,
onSelected: isLoading ? null : (_) => setState(() {
_selectedGrade = grade.$1;
if (_selectedGrade == 'grade_10') _selectedStream = 'general';
}),
)).toList(),
),
const SizedBox(height: 18),
// Stream Selector (only secondary grades have a stream)
if (_selectedGrade != 'grade_10') ...[
Text(
'الفرع الأكاديمي',
style: AppTypography.titleMedium.copyWith(color: AppColors.textSecondaryDark, fontSize: 13),
),
const SizedBox(height: 8),
Row(
children: [
Expanded(
child: GestureDetector(
onTap: isLoading ? null : () => setState(() => _selectedStream = 'scientific'),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _selectedStream == 'scientific' ? AppColors.saqelCyan.withAlpha(30) : AppColors.darkSurface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _selectedStream == 'scientific' ? AppColors.saqelCyan : AppColors.darkCardBorder,
),
),
child: Center(
child: Text(
'الفرع العلمي 🔬',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: _selectedStream == 'scientific' ? AppColors.saqelCyan : Colors.white,
),
),
),
),
),
),
const SizedBox(width: 10),
Expanded(
child: GestureDetector(
onTap: isLoading ? null : () => setState(() => _selectedStream = 'literary'),
child: Container(
padding: const EdgeInsets.symmetric(vertical: 12),
decoration: BoxDecoration(
color: _selectedStream == 'literary' ? AppColors.guardianAmber.withAlpha(30) : AppColors.darkSurface,
borderRadius: BorderRadius.circular(12),
border: Border.all(
color: _selectedStream == 'literary' ? AppColors.guardianAmber : AppColors.darkCardBorder,
),
),
child: Center(
child: Text(
'الفرع الأدبي 📚',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w700,
color: _selectedStream == 'literary' ? AppColors.guardianAmber : Colors.white,
),
),
),
),
),
),
],
),
],
const SizedBox(height: 24),
LuxuryButton(
text: 'فتح الملف والدخول للمنصة 🚀',
isLoading: isLoading,
onPressed: () {
final name = _fullNameController.text.trim();
if (name.isEmpty) {
SaqelToast.showError(context, 'يرجى إدخال الاسم الكامل للطالب');
return;
}
context.read<AuthCubit>().setupProfile(
identityToken: state.identityToken,
fullName: name,
nationalId: state.nationalId,
gradeLevel: _selectedGrade,
stream: _selectedStream,
);
},
),
],
);
}
}