feat: implement splash and onboarding flows, update app assets, and add project design documentation

This commit is contained in:
Hamza-Ayed
2026-07-24 04:15:07 +03:00
parent 9f7bc31408
commit 213b459912
208 changed files with 23801 additions and 1066 deletions
@@ -1,13 +1,19 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../../core/l10n/app_localizations.dart';
import 'package:go_router/go_router.dart';
import '../../../core/build_config.dart';
import '../cubit/auth_cubit.dart';
import '../cubit/auth_state.dart';
import '../../../core/design/tokens.dart';
import '../../../core/utils/phone_normalizer.dart';
import '../../../core/ui/phone_intel/intl_phone_field.dart';
import '../../../core/ui/phone_intel/phone_number.dart';
import '../../../core/ui/tripz_scaffold.dart';
import '../../../core/ui/tripz_button.dart';
import '../../../core/ui/tripz_text_field.dart';
/// شاشة إدخال الهاتف — تطابق stitch/phone_login.html:
/// خلفية فاتحة + شعار بالمنتصف بالأعلى + sheet أبيض سفلي بحواف مدورة
/// فيه حقل الهاتف (علم + كود دولة + رقم) وزر إرسال الكود.
class PhoneScreen extends StatefulWidget {
const PhoneScreen({super.key});
@override
@@ -15,64 +21,156 @@ class PhoneScreen extends StatefulWidget {
}
class _PhoneScreenState extends State<PhoneScreen> {
final _controller = TextEditingController();
PhoneNumber? _phoneNumber;
bool _valid = false;
@override
void dispose() {
_controller.dispose();
super.dispose();
void _sendOtp(BuildContext context) {
if (_phoneNumber == null) return;
final normalized = PhoneNormalizer.normalize(
_phoneNumber!.number,
isoCode: _phoneNumber!.countryISOCode,
);
context.read<AuthCubit>().sendOtp(normalized);
}
@override
Widget build(BuildContext context) {
final l10n = AppLocalizations.of(context)!;
final theme = Theme.of(context);
final cs = theme.colorScheme;
return TripzScaffold(
safeAreaTop: true,
padding: true,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const Spacer(flex: 2),
Text(
l10n.appName,
style: Theme.of(context).textTheme.headlineLarge,
return BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
if (state.status == AuthStatus.error && state.error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.error!)),
);
}
},
builder: (context, state) {
final isLoading = state.status == AuthStatus.authenticating;
return TripzScaffold(
backgroundColor: cs.surfaceContainerLow,
padding: false,
safeAreaBottom: false,
bottomAction: TripzButton(
onPressed: (_valid && !isLoading) ? () => _sendOtp(context) : null,
label: l10n.sendCode,
loading: isLoading,
),
const SizedBox(height: 8),
Text(
l10n.phoneTitle,
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
child: Column(
children: [
// ── منطقة الشعار (النصف العلوي) ──
Expanded(
child: Center(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: T.s32),
child: Image.asset(
'assets/images/login_hero.png',
width: 200,
fit: BoxFit.contain,
errorBuilder: (_, __, ___) => Icon(
Icons.directions_car_filled_rounded,
size: 96,
color: cs.primary,
),
),
),
),
),
// ── بطاقة الإدخال السفلية (منطقة الإبهام) ──
Container(
width: double.infinity,
decoration: BoxDecoration(
color: cs.surfaceContainerLowest,
borderRadius: const BorderRadius.vertical(top: Radius.circular(T.r24)),
boxShadow: const [T.shadowLg],
),
padding: const EdgeInsets.fromLTRB(T.pagePadding, T.s16, T.pagePadding, T.s32),
child: SafeArea(
top: false,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// مقبض السحب الزخرفي
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: cs.outlineVariant,
borderRadius: BorderRadius.circular(T.rCircle),
),
),
),
const SizedBox(height: T.s24),
// العنوان
Text(
l10n.loginTitle,
style: theme.textTheme.headlineSmall?.copyWith(
fontWeight: FontWeight.w600,
color: cs.onSurface,
),
),
const SizedBox(height: T.s8),
Text(
l10n.phoneLoginSubtitle,
style: theme.textTheme.bodyMedium?.copyWith(
color: cs.onSurfaceVariant,
),
),
const SizedBox(height: T.s24),
// حقل الهاتف — علم + كود الدولة (افتراضي BuildConfig.defaultCountry) + الرقم
IntlPhoneField(
initialCountryCode: BuildConfig.defaultCountry,
languageCode: Localizations.localeOf(context).languageCode,
textAlign: TextAlign.left,
style: theme.textTheme.bodyLarge,
dropdownTextStyle: theme.textTheme.bodyLarge,
flagsButtonPadding: const EdgeInsets.symmetric(horizontal: T.s12),
dropdownDecoration: BoxDecoration(
border: BorderDirectional(
end: BorderSide(color: cs.outlineVariant),
),
),
decoration: InputDecoration(
hintText: l10n.phoneHint,
filled: true,
fillColor: cs.surfaceContainerLowest,
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(T.r8),
borderSide: BorderSide(color: cs.outlineVariant),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(T.r8),
borderSide: BorderSide(color: cs.outlineVariant),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(T.r8),
borderSide: BorderSide(color: cs.primary, width: 1.5),
),
contentPadding: const EdgeInsets.symmetric(horizontal: T.s16, vertical: T.s16),
),
onChanged: (phone) {
_phoneNumber = phone;
final num = phone.number;
_valid = num.isNotEmpty && num.length >= 7;
setState(() {});
},
),
],
),
),
),
],
),
const Spacer(),
TripzTextField(
controller: _controller,
label: l10n.phoneLabel,
hint: l10n.phoneHint,
keyboardType: TextInputType.phone,
),
const SizedBox(height: 20),
BlocConsumer<AuthCubit, AuthState>(
listener: (context, state) {
if (state.status == AuthStatus.error && state.error != null) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text(state.error!)),
);
}
},
builder: (context, state) {
return TripzButton(
label: l10n.sendCode,
loading: state.status == AuthStatus.authenticating,
onPressed: () =>
context.read<AuthCubit>().sendOtp(_controller.text),
);
},
),
const Spacer(flex: 2),
],
),
);
},
);
}
}