Files
tripz-llc/apps/driver_new/lib/features/chat/view/chat_page.dart
T
Hamza-AyedandClaude Opus 5 bb2c0de478 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>
2026-08-05 00:04:28 +03:00

115 lines
3.4 KiB
Dart

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),
),
);
}
}