feat(apps): تسجيل السائق والدردشة والكوبون — إكمال التغطية
فحص آليّ لنقاط docs/38 مقابل الكود كشف ثغرات لم تظهر بالقراءة. ## تسجيل السائق — أخطر ثغرة لم يكن موجوداً أصلاً: سائق جديد يسجّل دخوله ثم يقف. بُنيت features/onboarding كاملة — تقديم ← ملف ← مركبة ← وثائق ← انتظار الاعتماد. - الخطوة تُشتقّ من حالة الخادم لا من تقدّم محلّي: سائق يعيد تثبيت التطبيق يعود إلى حيث وقف لا إلى البداية - الاعتماد يقع على الخادم تلقائياً حين تكتمل الوثائق؛ التطبيق لا يعتمد أحداً - عند الاعتماد خروج إجباري: الدور يتغيّر على الخادم والتوكن القديم يحمل القديم، فتفشل نقاط السائق بـ403 (مصيدة docs/38 §5) - بوابة توجيه: مستخدم دوره ليس driver يُحجز في /onboarding - الوثائق تُصوَّر بالكاميرا لا من المعرض: أصعب تزويراً ## الدردشة والكوبون - Features.chat كان مفعّلاً بلا ميزة. features/chat باستطلاع كل خمس ثوان — قائمة أحداث الخادم لا تتضمّن الرسائل (docs/38 §9)، فالاستطلاع قيد خادم لا اختيار تصميمي - الكوبون: حقل في ورقة التأكيد خلف طبقتَي الميزات، يُقيَّم على الخادم ## Features.calls أُطفئ صراحةً الخادم يدعم WebRTC والتطبيق لا. عَلَم مفعّل بلا ميزة كذبٌ على القارئ التالي. ## بنية - ApiClient.upload للرفع متعدّد الأجزاء - AuthFailure → ApiFailure في core/api: يخدم المصادقة والملف والتسجيل - tool/sync_from_rider.sh: المزامنة اليدوية بين التوأمين انكسرت أربع مرات، فصارت سكربتاً واحداً يعرّف ما يملكه كل تطبيق flutter analyze نظيف · الراكب 101 ملف/7,809 سطر · السائق 110 ملف/8,116 سطر. فحوص آلية: صفر استيراد بين ميزتين · صفر عَلَم مفعّل بلا ميزة · كل نقاط العقد المخصّصة للتطبيقين مستهلكة. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
096acae74e
commit
bb2c0de478
@@ -0,0 +1,114 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
|
||||
import '../../../core/design/tokens.dart';
|
||||
import '../../../core/design/tripz_colors.dart';
|
||||
import '../../../core/di.dart';
|
||||
import '../../../core/l10n/l10n.dart';
|
||||
import '../../../core/session/session_cubit.dart';
|
||||
import '../../../core/ui/empty_view.dart';
|
||||
import '../../../core/ui/tripz_scaffold.dart';
|
||||
import '../../../core/ui/tripz_text_field.dart';
|
||||
import '../cubit/chat_cubit.dart';
|
||||
import '../data/chat_repository.dart';
|
||||
|
||||
class ChatPage extends StatefulWidget {
|
||||
const ChatPage({super.key, required this.tripId});
|
||||
|
||||
final String tripId;
|
||||
|
||||
@override
|
||||
State<ChatPage> createState() => _ChatPageState();
|
||||
}
|
||||
|
||||
class _ChatPageState extends State<ChatPage> {
|
||||
late final ChatCubit _cubit = sl<ChatCubit>()..open(widget.tripId);
|
||||
final _input = TextEditingController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_input.dispose();
|
||||
_cubit.close();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final l10n = context.l10n;
|
||||
final myId = sl<SessionCubit>().state.user?.id;
|
||||
|
||||
return BlocProvider.value(
|
||||
value: _cubit,
|
||||
child: BlocBuilder<ChatCubit, ChatState>(
|
||||
builder: (context, state) => TripzScaffold(
|
||||
title: l10n.chatTitle,
|
||||
status: state.status,
|
||||
error: state.error,
|
||||
bottomAction: TripzTextField(
|
||||
controller: _input,
|
||||
hint: l10n.chatHint,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (v) {
|
||||
_cubit.send(v);
|
||||
_input.clear();
|
||||
},
|
||||
suffix: IconButton(
|
||||
icon: const Icon(Icons.send_rounded),
|
||||
onPressed: () {
|
||||
_cubit.send(_input.text);
|
||||
_input.clear();
|
||||
},
|
||||
),
|
||||
),
|
||||
child: state.messages.isEmpty
|
||||
? EmptyView(
|
||||
icon: Icons.forum_outlined,
|
||||
message: l10n.chatEmpty,
|
||||
)
|
||||
: ListView.builder(
|
||||
// الأحدث أسفل، والقائمة مقلوبة كي تبقى عند آخر رسالة.
|
||||
reverse: true,
|
||||
itemCount: state.messages.length,
|
||||
itemBuilder: (context, i) {
|
||||
final msg = state.messages[state.messages.length - 1 - i];
|
||||
return _Bubble(msg: msg, mine: msg.senderId == myId);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _Bubble extends StatelessWidget {
|
||||
const _Bubble({required this.msg, required this.mine});
|
||||
|
||||
final ChatMessage msg;
|
||||
final bool mine;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Align(
|
||||
alignment:
|
||||
mine ? AlignmentDirectional.centerEnd : AlignmentDirectional.centerStart,
|
||||
child: Container(
|
||||
margin: const EdgeInsets.symmetric(vertical: Space.xxs),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: Space.sm,
|
||||
vertical: Space.xs,
|
||||
),
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.sizeOf(context).width * 0.75,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: mine
|
||||
? context.colors.primaryContainer
|
||||
: context.tripzColors.surfaceRaised,
|
||||
borderRadius: Radii.card_,
|
||||
border: mine ? null : Border.all(color: context.colors.outlineVariant),
|
||||
),
|
||||
child: Text(msg.body, style: context.texts.bodyMedium),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user