flutter(rider): rebuild lib on Cubit/Bloc + Tripz backend (core + auth slice)

- keep store identity (com.mobileapp.store.ride, shorebird, firebase, fonts); old lib -> lib_old_getx
- core: config/di/router(go_router)/theme/api_client(dio+tenant+bearer)/token_store(secure)
- auth: repo + AuthCubit + OTP phone/otp screens; home: flutter_map (OSM placeholder, انطلق TODO)
- android usesCleartextTraffic=true for http API; maps=flutter_map (no Google), state=Cubit

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-16 20:47:55 +03:00
co-authored by Claude Opus 4.8
parent 6234e66471
commit 6116d23d0c
203 changed files with 573 additions and 83094 deletions
@@ -0,0 +1,46 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import '../data/auth_repository.dart';
import 'auth_state.dart';
class AuthCubit extends Cubit<AuthState> {
final AuthRepository _repo;
AuthCubit(this._repo) : super(const AuthState());
Future<void> checkAuth() async {
final user = await _repo.me();
emit(state.copyWith(
status: user != null ? AuthStatus.authenticated : AuthStatus.unauthenticated,
user: user,
));
}
Future<void> sendOtp(String phone) async {
if (phone.trim().isEmpty) {
emit(state.copyWith(status: AuthStatus.error, error: 'أدخل رقم الهاتف'));
return;
}
try {
emit(state.copyWith(status: AuthStatus.authenticating, phone: phone.trim()));
await _repo.sendOtp(phone.trim());
emit(state.copyWith(status: AuthStatus.codeSent, phone: phone.trim()));
} catch (_) {
emit(state.copyWith(status: AuthStatus.error, error: 'تعذّر إرسال الرمز'));
}
}
Future<void> verify(String code) async {
try {
emit(state.copyWith(status: AuthStatus.authenticating));
final user = await _repo.verify(state.phone, code.trim());
emit(state.copyWith(status: AuthStatus.authenticated, user: user));
} catch (_) {
emit(state.copyWith(status: AuthStatus.error, error: 'رمز غير صحيح'));
}
}
Future<void> logout() async {
await _repo.logout();
emit(const AuthState(status: AuthStatus.unauthenticated));
}
}
@@ -0,0 +1,42 @@
import 'package:equatable/equatable.dart';
import '../data/auth_repository.dart';
enum AuthStatus {
unknown,
unauthenticated,
codeSent,
authenticating,
authenticated,
error,
}
class AuthState extends Equatable {
final AuthStatus status;
final String phone;
final AppUser? user;
final String? error;
const AuthState({
this.status = AuthStatus.unknown,
this.phone = '',
this.user,
this.error,
});
AuthState copyWith({
AuthStatus? status,
String? phone,
AppUser? user,
String? error,
}) {
return AuthState(
status: status ?? this.status,
phone: phone ?? this.phone,
user: user ?? this.user,
error: error,
);
}
@override
List<Object?> get props => [status, phone, user, error];
}
@@ -0,0 +1,53 @@
import 'package:dio/dio.dart';
import '../../../core/api/api_client.dart';
import '../../../core/storage/token_store.dart';
class AppUser {
final String id;
final String phone;
final String? name;
final String role;
AppUser({required this.id, required this.phone, this.name, required this.role});
factory AppUser.fromJson(Map<String, dynamic> j) => AppUser(
id: j['id'] as String,
phone: (j['phone'] ?? '') as String,
name: j['name'] as String?,
role: (j['role'] ?? 'rider') as String,
);
}
class AuthRepository {
final ApiClient _api;
final TokenStore _store;
AuthRepository(this._api, this._store);
Future<void> sendOtp(String phone) async {
await _api.dio.post('/auth/send-otp', data: {'phone': phone});
}
Future<AppUser> verify(String phone, String code) async {
final r = await _api.dio.post(
'/auth/verify-otp',
data: {'phone': phone, 'code': code},
);
await _store.save(
r.data['access_token'] as String,
r.data['refresh_token'] as String,
);
return AppUser.fromJson(Map<String, dynamic>.from(r.data['user'] as Map));
}
Future<AppUser?> me() async {
try {
final r = await _api.dio.get('/users/me');
return AppUser.fromJson(Map<String, dynamic>.from(r.data as Map));
} on DioException {
return null;
}
}
Future<void> logout() => _store.clear();
}
@@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../cubit/auth_cubit.dart';
import '../cubit/auth_state.dart';
class OtpScreen extends StatefulWidget {
const OtpScreen({super.key});
@override
State<OtpScreen> createState() => _OtpScreenState();
}
class _OtpScreenState extends State<OtpScreen> {
final _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
leading: IconButton(
icon: const Icon(Icons.arrow_forward),
onPressed: () => context.go('/phone'),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: 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 loading = state.status == AuthStatus.authenticating;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 24),
Text('أدخل رمز التحقق',
style: Theme.of(context).textTheme.titleLarge),
const SizedBox(height: 8),
Text('أُرسل إلى ${state.phone}'),
const SizedBox(height: 28),
TextField(
controller: _controller,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
decoration: const InputDecoration(labelText: 'الرمز'),
),
const SizedBox(height: 20),
FilledButton(
onPressed: loading
? null
: () => context.read<AuthCubit>().verify(_controller.text),
child: loading
? const SizedBox(
height: 22, width: 22,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('تأكيد'),
),
],
);
},
),
),
),
);
}
}
@@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../cubit/auth_cubit.dart';
import '../cubit/auth_state.dart';
class PhoneScreen extends StatefulWidget {
const PhoneScreen({super.key});
@override
State<PhoneScreen> createState() => _PhoneScreenState();
}
class _PhoneScreenState extends State<PhoneScreen> {
final _controller = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
const SizedBox(height: 40),
Text('Tripz', style: Theme.of(context).textTheme.headlineMedium),
const SizedBox(height: 8),
const Text('أدخل رقم هاتفك للمتابعة'),
const SizedBox(height: 32),
TextField(
controller: _controller,
keyboardType: TextInputType.phone,
decoration: const InputDecoration(
labelText: 'رقم الهاتف',
hintText: '07XXXXXXXX',
),
),
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) {
final loading = state.status == AuthStatus.authenticating;
return FilledButton(
onPressed: loading
? null
: () => context.read<AuthCubit>().sendOtp(_controller.text),
child: loading
? const SizedBox(
height: 22, width: 22,
child: CircularProgressIndicator(strokeWidth: 2),
)
: const Text('إرسال الرمز'),
);
},
),
],
),
),
),
);
}
}
@@ -0,0 +1,77 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_map/flutter_map.dart';
import 'package:latlong2/latlong.dart';
import '../../auth/cubit/auth_cubit.dart';
import '../../auth/cubit/auth_state.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
final center = const LatLng(31.9539, 35.9106); // عمّان
return Scaffold(
appBar: AppBar(
title: const Text('Tripz'),
actions: [
IconButton(
icon: const Icon(Icons.logout),
onPressed: () => context.read<AuthCubit>().logout(),
),
],
),
body: Stack(
children: [
FlutterMap(
options: MapOptions(initialCenter: center, initialZoom: 13),
children: [
// TODO: استبدال بلاطات انطلق (Martin) بدل OSM
TileLayer(
urlTemplate: 'https://tile.openstreetmap.org/{z}/{x}/{y}.png',
userAgentPackageName: 'com.mobileapp.store.ride',
),
MarkerLayer(markers: [
Marker(
point: center,
width: 40,
height: 40,
child: const Icon(Icons.location_pin, color: Colors.red, size: 40),
),
]),
],
),
Positioned(
left: 16,
right: 16,
bottom: 24,
child: Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: BlocBuilder<AuthCubit, AuthState>(
builder: (context, state) {
final name = state.user?.name ?? state.user?.phone ?? '';
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text('مرحباً $name',
style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
FilledButton.icon(
onPressed: () {},
icon: const Icon(Icons.local_taxi),
label: const Text('اطلب رحلة (قريباً)'),
),
],
);
},
),
),
),
),
],
),
);
}
}