Mobile App Architecture
Drop Mobile App
React Native / Expo Router app in
src/drop-mobile/.
Tech Stack
| Technology | Version/Details |
|---|---|
| Framework | React Native (Expo) |
| Routing | Expo Router (file-based) |
| Fonts | DM Sans (expo-google-fonts), Fraunces (expo-google-fonts) |
| Navigation | Stack (root) + Tabs (main app) |
| State | React useState (no global state library) |
| API | Custom fetch wrapper with Bearer token auth |
| Storage | AsyncStorage (for auth token) |
Directory Structure
src/drop-mobile/
├── App.js # Expo entry (unused — Expo Router takes over)
├── app/
│ ├── _layout.js # Root Stack layout + font loading
│ ├── index.js # Welcome screen
│ ├── login.js # Login screen
│ ├── register.js # Registration (2-step)
│ ├── history.js # Transaction history
│ └── (tabs)/
│ ├── _layout.js # Tab navigator (4 tabs)
│ ├── index.js # Dashboard / Home
│ ├── send.js # Send money
│ ├── scan.js # QR scanner
│ └── profile.js # Profile & settings
├── lib/
│ ├── api.js # API client
│ └── theme.js # Theme constants
└── assets/ # Static assets
Navigation
Root Stack (app/_layout.js)
Stack.Screen: index (Welcome — no header)
Stack.Screen: login (Login — no header)
Stack.Screen: register (Register — no header)
Stack.Screen: (tabs) (Main app — no header)
Stack.Screen: send (Send money — modal presentation)
Stack.Screen: scan (QR scan — modal presentation)
Font loading happens here: Fraunces_600SemiBold, Fraunces_700Bold, DMSans_400Regular, DMSans_500Medium, DMSans_700Bold. App shows nothing until fonts are loaded (SplashScreen.preventAutoHideAsync).
Tab Navigator (app/(tabs)/_layout.js)
| Tab | Label | Icon | Screen |
|---|---|---|---|
| index | Hjem | Unicode house | Dashboard |
| send | Send | Unicode arrow | Send money |
| scan | QR | Unicode QR | QR scanner |
| profile | Profil | Unicode person | Profile |
Tab bar style: backgroundColor: colors.white, borderTopColor: colors.border, height: 60.
Active tint: colors.green (#0B6E35), inactive: colors.textLight (#9CA3AF).
Screens
Welcome (app/index.js)
- Green background (
colors.green) - "drop." wordmark in white Fraunces font
- Headline: "Enklere betalinger. Lavere gebyrer."
- Two buttons: "Logg inn" (outline) → /login, "Opprett konto" (solid white) → /register
Login (app/login.js)
- White background
- Email + password fields
- "Logg inn" green button → calls
api.login(email, password) - On success: stores token, navigates to
/(tabs) - "Opprett konto" link → /register
- Pre-filled demo credentials in dev
Register (app/register.js)
- 2-step flow with progress dots indicator
- Step 1: firstName, lastName, email, phone
- Step 2: password, confirmPassword
- Calls
api.register(data), on success navigates to/(tabs)
Dashboard (app/(tabs)/index.js)
- Greeting: "Hei, {firstName}" with hand wave emoji
- Green balance card: "Total saldo" with formatted NOK amount
- Quick stats row: Sendt, Mottatt, Ventende (sent, received, pending)
- "Siste transaksjoner" section with FlatList
- Each transaction: icon (arrow up=sent, arrow down=received), name, date, amount with color coding
- "Se alle" link → /history
- Pull-to-refresh via
RefreshControl
Send Money (app/(tabs)/send.js)
- 2-step flow:
- Step 1: Recipient name input + currency picker (5 currencies with flags)
- Currencies: BAM (Bosnia), RSD (Serbia), PKR (Pakistan), TRY (Turkey), PLN (Poland)
- Step 2: Amount input (NOK) + conversion card showing:
- Exchange rate (fetched from
api.getRate) - Fee (0.5%)
- "Mottaker far" (recipient gets) calculated amount
- Exchange rate (fetched from
- "Bekreft og send" button →
api.sendRemittance(data) - Success: shows confirmation, "Tilbake til hjem" button
QR Scanner (app/(tabs)/scan.js)
- Camera placeholder (gray box with QR icon)
- "Skann QR-kode" instruction text
- "Simuler skanning" button for demo
- Nearby merchants list (hardcoded: Ahmetov Kebab, Kafe Oslo, Narvesen)
- Payment flow: merchant info → amount input → confirm → success
- Calls
api.payQR({ merchantId, amount })
Transaction History (app/history.js)
- Filter tabs: Alle, Sendinger (remittance), QR (qr_payment)
- FlatList with transaction items
- Each item: direction icon, recipient/merchant name, date, amount
- Pull-to-refresh
- Filter changes trigger re-fetch via
api.getTransactions({ type })
Profile (app/(tabs)/profile.js)
- User info section: initials avatar, name, email
- "Mine mottakere" section with recipients list (fetched from
api.getRecipients()) - Each recipient: name, country, account number
- Settings menu: Sprak, Varsler, Personvern, Vilkar
- Logout button → clears token, navigates to /index
API Client
File: lib/api.js
Configuration
const API_URL = __DEV__
? "http://localhost:3000/api"
: "https://drop-app.vercel.app/api";
Auth Pattern
- Token stored in module-level variable
let token = null - All requests include
Authorization: Bearer ${token}header setToken(t)/getToken()exported for auth management
Available Methods
| Method | HTTP | Endpoint | Returns |
|---|---|---|---|
api.login(email, password) |
POST | /auth/login |
{ token, user } |
api.register(data) |
POST | /auth/register |
{ token, user } |
api.logout() |
POST | /auth/logout |
— |
api.getMe() |
GET | /auth/me |
{ user } |
api.getTransactions(params) |
GET | /transactions |
{ transactions } |
api.sendRemittance(data) |
POST | /transactions/remittance |
{ transaction } |
api.payQR(data) |
POST | /transactions/qr-payment |
{ transaction } |
api.getRecipients() |
GET | /recipients |
{ recipients } |
api.addRecipient(data) |
POST | /recipients |
{ recipient } |
api.getMerchants() |
GET | /merchants |
{ merchants } |
api.getRates() |
GET | /rates |
{ rates } |
api.getRate(currency) |
GET | /rates/{currency} |
{ rate } |
api.health() |
GET | /health |
{ status } |
Error Handling
All methods use a shared request(endpoint, options) function that:
- Adds auth header if token exists
- Parses JSON response
- Throws on non-OK status with
error.statusanderror.data
Theme
File: lib/theme.js
Colors
colors: {
green: '#0B6E35', // Primary — matches web app
greenDark: '#095C2C', // Hover/pressed state
greenLight: '#E8F5E9', // Light green bg
gold: '#D4A017', // Accent
white: '#FFFFFF',
bg: '#FAFCF8', // Off-white background
card: '#FFFFFF',
text: '#1A1A1A', // Primary text
textSecondary: '#6B7280', // Secondary text
textLight: '#9CA3AF', // Muted text
border: '#E5E7EB', // Borders
error: '#EF4444', // Errors
success: '#10B981', // Success/positive
}
Fonts
fonts: {
display: 'Fraunces_700Bold',
heading: 'Fraunces_600SemiBold',
body: 'DMSans_400Regular',
bodyMedium: 'DMSans_500Medium',
bodyBold: 'DMSans_700Bold',
}
Spacing
spacing: { xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48 }
Border Radius
radius: { sm: 8, md: 12, lg: 16, full: 9999 }
Web App vs Mobile App Comparison
| Feature | Web (drop-app) |
Mobile (drop-mobile) |
|---|---|---|
| Auth storage | httpOnly cookie | Bearer token (in-memory) |
| Navigation | Next.js App Router | Expo Router (Stack + Tabs) |
| Send flow | 4 steps | 2 steps |
| Onboarding | 4 steps (info, OTP, PIN, success) | 2 steps (info, password) |
| QR scanner | Simulated camera UI | Simulated camera UI |
| Bottom nav | 5 tabs (Hjem, Aktivitet, Skann, Kontoer, Profil) | 4 tabs (Hjem, Send, QR, Profil) |
| Cards page | Yes (feature-flagged) | No |
| Merchant page | Yes (feature-flagged) | No |
| Accounts page | Yes (dedicated) | No (balance on dashboard) |
| History | Dedicated page with filters | Dedicated screen with filters |
| Feature flags | Environment variables | Not implemented |
| UI components | shadcn/ui (Radix) | React Native StyleSheet |
| State | useState + useAuth hook | useState + api module |
Development Notes
- API compatibility: Mobile app calls the same API endpoints as the web app (same backend)
- Dev API URL:
http://localhost:3000/api— requires the Next.js dev server running - Prod API URL:
https://drop-app.vercel.app/api - No deep linking configured yet
- No push notifications implemented yet
- No offline support — requires network for all operations
- No biometric auth — login is email/password only