Flutter (Dart 3.10) client for Wassalha, a peer-to-peer shipping marketplace that connects shippers (people who need something delivered) with carriers (drivers already travelling that route). A single user can act as sender, receiver, or carrier depending on the order. The app covers the full journey: ML-priced order creation, real-time bidding, live map tracking, OTP pickup & delivery, in-app chat, wallet & Stripe payments, ratings, and a bilingual (EN/AR) UI with light/dark themes.
This repository is the Flutter front end. It talks to the Wassalha Spring Boot backend over REST + STOMP WebSocket.
- What it does
- Stack
- Architecture at a glance
- Project layout
- Features
- User roles
- State-management pattern
- Networking
- Authentication & token handling
- Order lifecycle (client view)
- Real-time (WebSocket)
- Payments & wallet
- Maps, location & tracking
- Notifications
- Localization & theming
- Error handling
- Running locally
- Configuration
- Code generation
- Project conventions
A shipper describes a package (item, dimensions, weight, pickup + drop address picked on a map), and the app requests an ML-predicted price range from the backend. After the receiver confirms, the order is published to nearby carriers; carriers place bids, the shipper accepts one, and a live tracking session opens over WebSocket showing the driver's position on a Google Map with a drawn route. Pickup and delivery are each confirmed with an OTP. Payment is settled through the in-app wallet (top-up via Stripe). When the order is delivered, the sender rates the carrier, the chat for that order auto-closes, and everyone returns home. The whole experience is available in English and Arabic (full RTL) and in light/dark themes.
| Layer | Choice |
|---|---|
| Language | Dart 3.10 (environment.sdk: ^3.10.4) |
| UI | Flutter (Material) |
| State management | flutter_bloc / bloc (Cubit) + equatable |
| Dependency injection | get_it + injectable (codegen) |
| Navigation | go_router |
| Networking | dio + retrofit (codegen), pretty_dio_logger |
| Serialization | json_serializable / json_annotation |
| Real-time | stomp_dart_client (STOMP over WebSocket) |
| Maps & location | google_maps_flutter, geolocator, geocoding |
| Payments | flutter_stripe |
| Push & crash | firebase_core, firebase_messaging, flutter_local_notifications, firebase_crashlytics |
| Storage | flutter_secure_storage (tokens), shared_preferences (prefs) |
| Localization | easy_localization (EN/AR, RTL) |
| Images / media | image_picker, flutter_svg, lottie |
| Loading UX | shimmer, skeletonizer |
| Misc | url_launcher, flutter_otp_text_field |
| Testing | flutter_test, bloc_test, mockito, mocktail, network_image_mock |
The app is feature-first Clean Architecture. Each feature is a vertical slice with three layers — data (DTOs, datasources, repositories), domain (entities, repository contracts, use cases), and presentation (Cubits, states, intents, pages, widgets). Shared infrastructure lives in lib/app.
┌───────────────────────────────────────────────────────────────────────────┐
│ Presentation Pages / Widgets ── Cubit (Intent → State) [flutter_bloc] │
├───────────────────────────────────────────────────────────────────────────┤
│ Domain Entities · Repository contracts · Use cases │
├───────────────────────────────────────────────────────────────────────────┤
│ Data Models (json) · Remote datasource (retrofit) · Repo impl │
└───────────────┬───────────────────────────────────────────────────────────┘
│ get_it + injectable wire every layer
▼
dio (ApiClient) ──► safeApiCall ──► ApiResult<T> ─── REST + JWT ──► Backend
stomp_dart_client ───────────────────────────────── WebSocket ──► Backend
- One direction of dependency: presentation → domain → data. Use cases call repository contracts; repository implementations call retrofit datasources; everything is resolved through
get_it. - Every network call funnels through
safeApiCall, which convertsdio/HTTP outcomes into a typedApiResult<T>(SuccessApiResult/ErrorApiResultwithstatusCode) and maps technical errors to human-readable messages (EN/AR). - Routing is centralized in
go_router; routes wrap pages in theirBlocProviders and a globalredirectguards authenticated routes.
lib/
├── main.dart ← bootstrap: Firebase, Stripe, EasyLocalization, DI, runZonedGuarded
├── app/
│ ├── config/
│ │ ├── di/ ── get_it + injectable (di.dart, di.config.dart)
│ │ ├── network/ ── dio module, auth interceptor (JWT + refresh)
│ │ ├── auth_storage/ ── secure token storage, sign-out cleanup
│ │ ├── theme_manger/ ── ThemeCubit (light/dark, persisted)
│ │ └── base_state/ ── Resource<T> wrapper (initial/loading/success/error)
│ └── core/
│ ├── api_manger/ ── ApiClient (retrofit), error handler
│ ├── network/ ── safeApiCall, ApiResult<T>
│ ├── router/ ── app_router.dart, route_names.dart, transitions
│ ├── errors/ ── AppErrorHandler (global crash/zone/bloc handler)
│ ├── services/ ── NotificationService (FCM + local notifications)
│ ├── values/ ── endpoints, constants, keys
│ ├── ui_helper/ ── colors, text styles, theme
│ └── widgets/ ── shared widgets (buttons, fields, snackbars, shimmers)
└── features/
├── splash · onbording · login · signup · forget_password · changePass
├── home · app_section (bottom-nav shell) · profile · editProfile · address · setting
├── createOrder · orders (+ order verification) · bids · tracking
└── wallet · ratings · customer_support (chat) · notifications
Each item is a self-contained feature module (data / domain / presentation).
| Feature | Purpose |
|---|---|
splash |
Boot gate: reads the token from secure storage and routes to home / onboarding / login (fail-safe fallback). |
onbording |
First-run intro carousel; persists a "seen" flag so returning users skip straight to login. |
login / signup |
Email/password auth; signup includes ID + selfie capture (KYC) with constrained image upload. |
forget_password / changePass |
OTP-based password reset and in-app password change. |
home |
Landing dashboard, hero card with current location, service shortcuts, prohibited-items screen. |
app_section |
Bottom-navigation shell (IndexedStack): Home, Chats, Notifications, Profile. |
createOrder |
Multi-step order builder: details + dimensions, map pickup/drop, ML price estimate, submit. |
orders |
Sender / receiver / carrier order lists & details, status timeline, order verification (pickup/delivery OTP). |
bids |
Carrier offer placement and the sender's offers list / acceptance. |
tracking |
Live driver position on Google Maps (sender/receiver/driver views) with animated marker + route. |
wallet |
Balance, top-up & withdraw (Stripe PaymentSheet), activity history, insurance holds. |
ratings |
Post-delivery rating of the carrier + a "Rating & Reviews" screen with a star breakdown. |
customer_support |
One-to-one chat (order-scoped & general), live messages, media & location sharing, archive/closed state. |
notifications |
Paginated notification feed, unread count, mark-as-read, deep-link navigation. |
profile / editProfile |
View/edit profile & avatar, addresses, delete account. |
address |
Saved addresses, map place-picker, reverse geocoding. |
setting |
Language toggle (EN/AR), theme toggle (light/dark), privacy & terms, logout. |
There is one account type; the role is contextual per order (the backend enforces that sender, receiver, and carrier are distinct users).
| Acting as | In the app |
|---|---|
| Sender (shipper) | Creates orders, gets the ML price, reviews & accepts carrier bids, tracks delivery, pays from wallet, rates the carrier. |
| Receiver | Confirms the incoming order, tracks the driver, confirms delivery via OTP. |
| Carrier (driver) | Browses nearby available orders, places bids, confirms pickup/delivery via OTP, shares live location. |
Every feature uses a consistent Cubit + Intent + State convention on top of flutter_bloc:
- State holds
Resource<T>fields — a small wrapper exposingisInitial / isLoading / isSuccess / isErrorplusdataanderror, so the UI renders shimmer/skeleton, content, or an error consistently. - Intent objects describe user actions; the page calls
cubit.doIntent(SomeIntent(...)). - Cubit handles the intent, calls a use case, and emits a new state. Post-
awaitemits are guarded withif (isClosed) return;to avoid emit-after-close on fast navigation.
// page
context.read<OrdersCubit>().doIntent(LoadOrders(status: OrderStatus.delivered));
// cubit
Future<void> _load(OrderStatus s) async {
emit(state.copyWith(ordersResource: Resource.loading()));
final result = await _getOrdersUseCase.execute(s);
if (isClosed) return;
emit(result is SuccessApiResult
? state.copyWith(ordersResource: Resource.success(result.data))
: state.copyWith(ordersResource: Resource.error(result.error)));
}ApiClientis aretrofitinterface overdio; endpoints are declared inapp/core/values/app_endpoint_strings.dart.safeApiCall<T>wraps each call and returnsApiResult<T>:SuccessApiResult<T>(data)on 2xx.ErrorApiResult<T>(error, statusCode)otherwise, with friendly EN/AR messages. Server messages that "look technical" (stack traces, SQL, exception class names, very long bodies) are replaced with clean user-facing copy; recognizable ones (bad credentials, insufficient balance, etc.) are translated.
- Auth interceptor (
app/config/network/interceptor.dart):- Attaches
Authorization: Bearer <token>to non-public requests andAccept-Languagefrom the active locale. - On 401, performs a single-flight refresh (
X-Refresh-Token) and retries the original request; if refresh fails it signs out and routes to login. Multipart uploads are not blindly replayed.
- Attaches
splash ──reads token──► appSections | onboarding | login
login/signup ──► access + refresh JWT ──► flutter_secure_storage
authed request ──401──► refresh (X-Refresh-Token) ──► retry
refresh fails ──► signOut() clears tokens + caches ──► go(login)
- Tokens are stored in
flutter_secure_storage(with one-time migration from any legacySharedPreferencestoken). go_routerhas a top-level redirect guard: unauthenticated access to a protected route bounces to login (public routes: splash, onboarding, login, signup, password-reset).signOut()clears tokens, disconnects the chat/tracking WebSockets, resets user-scoped singletons, and clears in-memory caches.
Mirrors the backend status machine; each status drives a different screen/timeline.
DRAFT
│ shipper fills details + images
▼
AWAITING_RECEIVER_CONFIRMATION
│ receiver confirms
▼
WAIT_FOR_DELIVERY ← visible to carriers as an available order
│ shipper accepts a bid
▼
MATCHED
│ carrier confirms pickup (OTP)
▼
IN_TRANSIT ← live tracking active (STOMP)
│ carrier confirms delivery (OTP)
▼
DELIVERED ──► sender rates carrier · order chat auto-archives · return home
│
└► CANCELLED (terminal)
STOMP over WebSocket (stomp_dart_client); the JWT is sent in the CONNECT frame. The scheme is derived from the API base URL (https → wss, otherwise ws).
| Channel | Direction | Used by |
|---|---|---|
/app/chat.sendMessage/{conversationId} |
client → server | Send a chat message |
/user/queue/messages |
server → client | Receive chat messages |
/app/track.updateLocation/{orderId} |
carrier → server | Push GPS position |
/topic/track/{orderId} |
server → client | Receive carrier position (sender/receiver) |
- Chat keeps a STOMP subscription for instant messages and also runs a lightweight history poll as a resilient fallback, with an equality short-circuit so an unchanged list never triggers a rebuild. The inbox list auto-refreshes (silently) and re-sorts newest-first.
- Live tracking consolidates the device GPS into a single shared stream, filters noisy fixes, and animates the driver marker; the route polyline is drawn toward the active target (pickup or drop-off).
- Top-up uses the Stripe PaymentSheet (themed to match light/dark). The backend returns a
clientSecret; the app presents the sheet and refreshes the balance on success. - Money is handled in integer cents end-to-end on the client (decimal-safe parsing of user input; formatted only at the view layer) to avoid floating-point drift.
- Withdraw flows through an OTP verification screen.
- Card PAN/CVV are never persisted on the device.
google_maps_flutterfor map rendering,geolocatorfor device position (with timeouts + last-known fallback),geocodingfor reverse-geocoding address labels.- A ref-counted device-location stream is shared across tracking screens; GPS fixes are noise-filtered before driving marker movement.
firebase_messagingreceives push;flutter_local_notificationsrenders foreground notifications.NotificationServicesyncs the FCM device token after login, routes notification taps to the right screen (orders, chat, wallet, bids), and refreshes the in-app feed.firebase_crashlyticscaptures crashes (release only);AppErrorHandlerrecords framework/zone/bloc errors.
easy_localizationwithassets/translations/(EN/AR) and full RTL support; a per-requestAccept-Languageheader is sent to the backend.ThemeCubittoggles light/dark and persists the choice; a single source of theme/colors lives inapp/core/ui_helper.
AppErrorHandler(set up inmain.dartinsiderunZonedGuarded) wiresFlutterError.onError,PlatformDispatcher.onError,ErrorWidget.builder, and aBlocObserver, forwarding to Crashlytics in release.- User-facing errors are friendly and localized via
safeApiCall; verbose network logs are gated behindkDebugMode.
- Flutter SDK (Dart
^3.10.4) - Android Studio / Xcode toolchains
- A running Wassalha backend (or point the app at one — see Configuration)
- Google Maps API key, a Firebase project, and Stripe keys for full functionality
# 1. Install dependencies
flutter pub get
# 2. Generate code (DI, retrofit, json) — see "Code generation"
dart run build_runner build --delete-conflicting-outputs
# 3. Run
flutter runOther useful commands:
flutter analyze # static analysis (project is warning-clean)
flutter test # unit / bloc / widget tests
flutter build apk # Android release
flutter build ios # iOS release (on macOS)| What | Where |
|---|---|
| Backend base URL & endpoints | lib/app/core/values/app_endpoint_strings.dart |
| Stripe publishable key | lib/app/core/app_constants.dart (used in main.dart) |
| Google Maps key (Android) | android/app/src/main/AndroidManifest.xml |
| Firebase config | android/app/google-services.json, iOS GoogleService-Info.plist |
| Translations | assets/translations/ (en, ar) |
| App icon | flutter_launcher_icons block in pubspec.yaml |
Security note: for production, serve the API over HTTPS (so the WebSocket upgrades to
wss), inject the Stripe live publishable key and a restricted/rotated Google Maps key via build config rather than committing them, and configure a real release signing keystore. Secret/payment operations belong on the backend.
The project relies on build_runner for DI, networking, and JSON. Re-run after changing any @injectable, retrofit interface, or @JsonSerializable model:
dart run build_runner build --delete-conflicting-outputs
# or, while iterating:
dart run build_runner watch --delete-conflicting-outputsGenerators: injectable_generator, retrofit_generator, json_serializable.
- Feature-first Clean Architecture — keep new code inside its feature's
data/domain/presentationlayers. - Cubit + Intent + State for all presentation logic; wrap async results in
Resource<T>and guard post-awaitemits withif (isClosed) return;. - All HTTP through
safeApiCallso errors stay typed and user-friendly. - DI everything via
get_it/injectable; resolve dependencies, don't construct them inline. - Localize all user-facing strings (EN/AR) and respect RTL.
Wassalha — Flutter client · part of a graduation project (Flutter app + Spring Boot backend + AI pricing/animal-check services).