Skip to content

Repository files navigation

Wassalha — Mobile App

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.

Flutter Dart State DI

Table of Contents

What it does

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.

Stack

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

Architecture at a glance

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 converts dio/HTTP outcomes into a typed ApiResult<T> (SuccessApiResult / ErrorApiResult with statusCode) and maps technical errors to human-readable messages (EN/AR).
  • Routing is centralized in go_router; routes wrap pages in their BlocProviders and a global redirect guards authenticated routes.

Project layout

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

Features

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.

User roles

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.

State-management pattern

Every feature uses a consistent Cubit + Intent + State convention on top of flutter_bloc:

  • State holds Resource<T> fields — a small wrapper exposing isInitial / isLoading / isSuccess / isError plus data and error, 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-await emits are guarded with if (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)));
}

Networking

  • ApiClient is a retrofit interface over dio; endpoints are declared in app/core/values/app_endpoint_strings.dart.
  • safeApiCall<T> wraps each call and returns ApiResult<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 and Accept-Language from 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.

Authentication & token handling

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 legacy SharedPreferences token).
  • go_router has 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.

Order lifecycle (client view)

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)

Real-time (WebSocket)

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).

Payments & wallet

  • 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.

Maps, location & tracking

  • google_maps_flutter for map rendering, geolocator for device position (with timeouts + last-known fallback), geocoding for reverse-geocoding address labels.
  • A ref-counted device-location stream is shared across tracking screens; GPS fixes are noise-filtered before driving marker movement.

Notifications

  • firebase_messaging receives push; flutter_local_notifications renders foreground notifications.
  • NotificationService syncs the FCM device token after login, routes notification taps to the right screen (orders, chat, wallet, bids), and refreshes the in-app feed.
  • firebase_crashlytics captures crashes (release only); AppErrorHandler records framework/zone/bloc errors.

Localization & theming

  • easy_localization with assets/translations/ (EN/AR) and full RTL support; a per-request Accept-Language header is sent to the backend.
  • ThemeCubit toggles light/dark and persists the choice; a single source of theme/colors lives in app/core/ui_helper.

Error handling

  • AppErrorHandler (set up in main.dart inside runZonedGuarded) wires FlutterError.onError, PlatformDispatcher.onError, ErrorWidget.builder, and a BlocObserver, forwarding to Crashlytics in release.
  • User-facing errors are friendly and localized via safeApiCall; verbose network logs are gated behind kDebugMode.

Running locally

Prerequisites

  • 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

Steps

# 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 run

Other 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)

Configuration

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.

Code generation

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-outputs

Generators: injectable_generator, retrofit_generator, json_serializable.

Project conventions

  • Feature-first Clean Architecture — keep new code inside its feature's data / domain / presentation layers.
  • Cubit + Intent + State for all presentation logic; wrap async results in Resource<T> and guard post-await emits with if (isClosed) return;.
  • All HTTP through safeApiCall so 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).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages