Skip to main content

Overview

DPM Delivery Mobile implements a secure authentication system using phone number and password credentials. The app stores authentication tokens using platform-specific secure storage solutions, with a web crypto fallback for browser environments.

Login Flow

Login Form Component

The login form is built with React Hook Form and Zod validation, providing a clean and type-safe user experience.
src/modules/auth/login-form.tsx
import { FormField } from "@/components/form-field";
import { useErrorHandler } from "@/hooks/use-error-handler";
import { api } from "@/services/api";
import { Storage, StorageKeys } from "@/utils/storage";
import { zodResolver } from "@hookform/resolvers/zod";
import { useMutation } from "@tanstack/react-query";
import { useRouter } from "expo-router";
import { Button, Spinner } from "heroui-native";
import { useForm } from "react-hook-form";
import { loginSchema, type LoginSchemaInput } from "./validations";

export function LoginForm() {
  const form = useForm<LoginSchemaInput>({
    resolver: zodResolver(loginSchema),
  });
  const { handleError } = useErrorHandler("LoginForm");
  const router = useRouter();

  const loginMutation = useMutation({
    mutationFn: api.auth.login,
    retry: 1,
  });

  const onSubmit = async (data: LoginSchemaInput) => {
    try {
      const response = await loginMutation.mutateAsync(data);
      Storage.setToken(StorageKeys.AUTH_TOKEN, response.data.accessToken);
      Storage.setObject(StorageKeys.USER, response.data.user);
      router.replace("/");
    } catch (error) {
      handleError(error);
    }
  };

  return (
    <View>
      <FormField
        control={form.control}
        name="phone"
        label="Phone Number"
        placeholder="Enter your phone number"
        inputMode="tel"
        keyboardType="phone-pad"
      />
      <FormField
        control={form.control}
        name="password"
        label="Password"
        placeholder="Enter your password"
        secureTextEntry
      />
      <Button
        onPress={form.handleSubmit(onSubmit)}
        isDisabled={loginMutation.isPending}
      >
        {loginMutation.isPending ? <Spinner /> : null}
        Log In
      </Button>
    </View>
  );
}

Key Features

Phone Authentication

Users log in with their phone number and password for quick access.

Secure Storage

Tokens are stored using SecureStore on native platforms and encrypted localStorage on web.

Error Handling

Comprehensive error handling with user-friendly error messages.

Type Safety

Full TypeScript support with Zod schema validation.

Secure Token Storage

Storage Implementation

The app uses a unified storage API that automatically selects the appropriate storage mechanism based on the platform.
src/utils/storage.ts
import * as SecureStore from "expo-secure-store";
import { Platform } from "react-native";
import { createMMKV } from "react-native-mmkv";
import { encryptForWeb, decryptForWeb } from "./web-crypto";

export enum StorageKeys {
  AUTH_TOKEN = "auth_token",
  REFRESH_TOKEN = "refresh_token",
  USER = "user",
  DEVICE_KEY = "__device_key",
  WEB_CRYPTO_KEY = "__web_crypto_key",
}

// Secure token storage with platform-specific implementations
async function setToken(key: string, value: string): Promise<void> {
  if (Platform.OS === "web") {
    const encrypted = await encryptForWeb(value);
    localStorage.setItem(key, encrypted);
  } else {
    await SecureStore.setItemAsync(key, value);
  }
}

async function getToken(key: string): Promise<string | null> {
  if (Platform.OS === "web") {
    const encrypted = localStorage.getItem(key);
    if (!encrypted) return null;
    return await decryptForWeb(encrypted);
  } else {
    return await SecureStore.getItemAsync(key);
  }
}

export const Storage = {
  setToken,
  getToken,
  deleteToken,
  // ... other methods
};

Platform-Specific Storage

On native platforms, the app uses:
  • SecureStore for sensitive data (auth tokens)
  • MMKV for encrypted general storage
  • Device-specific encryption keys

Web Crypto Fallback

AES-GCM Encryption

For web environments, the app implements AES-GCM encryption to secure authentication tokens in localStorage.
src/utils/web-crypto.ts
const IV_LENGTH = 12;
const KEY_LENGTH = 256;

// Generate or retrieve a device-specific encryption key
async function getWebEncryptionKey(): Promise<CryptoKey> {
  const storedKeyData = localStorage.getItem(WEB_CRYPTO_KEY);

  if (storedKeyData) {
    const keyData = JSON.parse(storedKeyData);
    return await crypto.subtle.importKey(
      "jwk",
      keyData,
      { name: "AES-GCM", length: KEY_LENGTH },
      true,
      ["encrypt", "decrypt"]
    );
  }

  const key = await crypto.subtle.generateKey(
    { name: "AES-GCM", length: KEY_LENGTH },
    true,
    ["encrypt", "decrypt"]
  );

  const exportedKey = await crypto.subtle.exportKey("jwk", key);
  localStorage.setItem(WEB_CRYPTO_KEY, JSON.stringify(exportedKey));

  return key;
}

// Encrypt a string value using AES-GCM
export async function encryptForWeb(plaintext: string): Promise<string> {
  const key = await getWebEncryptionKey();
  const iv = crypto.getRandomValues(new Uint8Array(IV_LENGTH));
  const encoder = new TextEncoder();
  const data = encoder.encode(plaintext);

  const encryptedData = await crypto.subtle.encrypt(
    { name: "AES-GCM", iv: iv },
    key,
    data
  );

  // Combine IV + encrypted data and convert to base64
  const combined = new Uint8Array(iv.length + encryptedData.byteLength);
  combined.set(iv, 0);
  combined.set(new Uint8Array(encryptedData), iv.length);

  return btoa(String.fromCharCode(...combined));
}

Security Features

Uses industry-standard AES-GCM encryption with 256-bit keys for maximum security.
Each encryption operation uses a unique randomly generated IV for enhanced security.
Encryption keys are generated per device and stored securely in the browser.
Encrypted data is base64-encoded for safe storage in localStorage.

Authentication Service

The authentication service provides a clean API for login operations.
src/services/auth/auth.service.ts
import { apiEndPoints } from "../api/end-points";
import type { HttpClient } from "../http.service";
import type { AuthService } from "./interface";

export function createAuthService(httpClient: HttpClient): AuthService {
  return {
    login: (data) => httpClient.post(apiEndPoints.auth.login(), data),
  };
}

Best Practices

1

Validate Input

Always validate phone numbers and passwords using Zod schemas before submission.
2

Handle Errors Gracefully

Use the error handler hook to display user-friendly error messages.
3

Secure Token Storage

Never store tokens in plain text. Always use the Storage utility functions.
4

Clear Tokens on Logout

Remove all tokens and user data when users log out.
The authentication system is designed to work seamlessly across iOS, Android, and web platforms with appropriate security measures for each environment.

Profile Management

Learn how users manage their profile and logout.

API Services

Understand how API calls are authenticated.

Build docs developers (and LLMs) love