Skip to main content

Architecture Overview

DPM Delivery Mobile is built using Expo Router with a file-based routing system, TanStack Query for server state management, and MMKV for local storage. The app follows a modular architecture with clear separation of concerns.

Technology Stack

  • Framework: React Native with Expo
  • Routing: Expo Router (file-based routing)
  • State Management: TanStack Query for server state
  • Storage: MMKV (native) and SecureStore (tokens)
  • HTTP Client: Axios with custom interceptors
  • UI Components: HeroUI Native
  • Styling: NativeWind (Tailwind CSS for React Native)

Folder Structure

src/
├── app/                          # File-based routing (Expo Router)
│   ├── _layout.tsx              # Root layout with providers
│   ├── (public)/                # Public routes (auth)
│   │   ├── _layout.tsx
│   │   └── (auth)/
│   │       └── sign-in.tsx
│   └── (parcel)/                # Protected routes
│       ├── _layout.tsx          # Auth guard
│       ├── (tabs)/              # Bottom tab navigation
│       │   ├── _layout.tsx
│       │   ├── index.tsx        # Home/Dashboard
│       │   ├── history.tsx
│       │   ├── transactions.tsx
│       │   └── profile.tsx
│       └── (stack)/             # Stack navigation
│           ├── _layout.tsx
│           ├── request-payment.tsx
│           └── shipments/[reference]/index.tsx
├── components/                   # Reusable UI components
│   ├── providers/               # Context providers
│   ├── ui/                      # Base UI components
│   ├── form-field.tsx
│   ├── select-field.tsx
│   ├── shipment-card.tsx
│   └── ...
├── modules/                      # Feature modules
│   ├── auth/                    # Authentication module
│   │   ├── login-form.tsx
│   │   └── validations.ts
│   └── dashboard/               # Dashboard features
│       └── parcels/
│           └── riders/
├── services/                     # API and business logic
│   ├── api/                     # API service aggregation
│   │   ├── index.ts            # Main API service
│   │   └── end-points.ts       # API endpoints
│   ├── http.service.ts         # HTTP client with interceptors
│   ├── auth/                    # Auth service
│   ├── users/                   # User service
│   ├── shipments/               # Shipment service
│   └── payment/                 # Payment service
├── lib/                          # Third-party integrations
│   ├── tanstack-query/          # TanStack Query setup
│   │   ├── query-keys.ts       # Centralized query keys
│   │   └── query-options/      # Query configurations
│   ├── logger/                  # Logging utilities
│   └── payment-gateways/        # Payment integrations
├── hooks/                        # Custom React hooks
│   ├── api/                     # API-related hooks
│   ├── use-color-scheme.ts
│   ├── use-theme-color.ts
│   └── use-error-handler.ts
├── utils/                        # Utility functions
│   ├── storage.ts              # Storage abstraction
│   ├── web-crypto.ts           # Web encryption
│   └── errors.ts               # Error handling
├── types/                        # TypeScript types
│   ├── auth.types.ts
│   ├── enums/
│   └── ...
├── constants/                    # App constants
│   ├── config.ts
│   ├── theme.ts
│   └── data.ts
└── assets/                       # Static assets
    ├── images/
    └── animations/

Key Design Patterns

1. Service Layer Pattern

All API calls are abstracted into service classes:
// src/services/api/index.ts
class ApiService {
  private readonly http: HttpClient;

  auth: AuthService;
  users: IUserService;
  shipments: ShipmentService;
  payment: IPaymentService;

  constructor(onTokenRefreshFailed?: () => void) {
    this.http = createHttpClient({
      baseURL: apiEndPoints.baseUrl,
      onTokenRefreshFailed,
    });

    this.auth = createAuthService(this.http);
    this.users = createUserService(this.http);
    this.shipments = createShipmentService(this.http);
    this.payment = createPaymentService(this.http);
  }
}

export const api = new ApiService();

2. Repository Pattern with TanStack Query

Query options are defined separately and reused:
// src/lib/tanstack-query/query-options/shipment.ts
export const getShipmentsQueryOptions = (riderId: string) => ({
  queryKey: queryKeys.shipments.getRiderLatestOrders(riderId),
  queryFn: () => api.shipments.getRiderLatestOrders(riderId),
});

3. Provider Pattern

All global providers are composed in a single component:
// src/components/providers/index.tsx
export function Providers({ children }: { children: React.ReactNode }) {
  return (
    <QueryClientProvider client={queryClient}>
      <GestureHandlerRootView style={{ flex: 1 }}>
        <KeyboardProvider>
          <HeroUINativeProvider config={config}>
            {children}
          </HeroUINativeProvider>
        </KeyboardProvider>
      </GestureHandlerRootView>
    </QueryClientProvider>
  );
}

4. Storage Abstraction Pattern

Platform-specific storage implementations are unified:
// src/utils/storage.ts
export const Storage = {
  setItem,      // MMKV for general data
  getString,
  setObject,
  getObject,
  removeItem,
  setToken,     // SecureStore/Web Crypto for sensitive data
  getToken,
  deleteToken,
};

Module Organization

Feature Modules

Features are organized into self-contained modules under src/modules/:
  • auth/: Login forms, validation schemas
  • dashboard/parcels/riders/: Rider-specific features
    • home/: Dashboard components
    • request-payout/: Payout flow
    • shipment/: Shipment management

Shared Components

Reusable components live in src/components/:
  • UI components: Basic building blocks (ui/)
  • Form components: form-field.tsx, select-field.tsx
  • Business components: shipment-card.tsx

Services

Each domain has its own service:
  • auth.service.ts: Authentication operations
  • users.service.ts: User management
  • shipments.service.ts: Shipment operations
  • payment.service.ts: Payment processing
All services use the centralized HttpClient for consistent error handling and token management.

Cross-Cutting Concerns

Error Handling

Centralized error handling in the HTTP client:
// src/services/http.service.ts:89
private handleError = async (error: AxiosError): Promise<never> => {
  const apiError: ApiError = {
    message: "Une erreur inattendue s'est produite",
    isApiError: true,
  };

  if (error.response) {
    switch (error.response.status) {
      case 401:
        if (await this.attemptTokenRefresh()) {
          return this.instance.request(error.config!);
        }
        break;
      // ... other cases
    }
  }

  return Promise.reject(apiError);
};

Logging

Centralized logging with the Logger utility:
import { Logger } from "@/lib/logger";

const logger = new Logger("ComponentName");
logger.info("Operation", { metadata });
logger.error("Error", error, { component, operation });

Authentication

Token management with automatic refresh:
  1. Tokens stored in SecureStore (native) or Web Crypto (web)
  2. Auth token attached to requests via interceptor
  3. Automatic token refresh on 401 responses
  4. Logout on refresh failure

Platform-Specific Code

The app handles platform differences using:
  1. Platform checks: Platform.OS === "web"
  2. Platform-specific files: use-color-scheme.web.ts, icon-symbol.ios.tsx
  3. Conditional implementations: Different storage strategies per platform

Next Steps

  • Routing - Learn about file-based routing and route groups
  • State Management - Understand TanStack Query setup
  • Storage - Explore storage strategies for different platforms

Build docs developers (and LLMs) love