Offline-First Strategy
Offline-First Strategy
Project: Drop — Fintech Payment App Version: 0.1.0 Date: 2026-02-23 Author: John (AI Director, ALAI) Status: Draft Reviewers: Alem Bašić (CEO)
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-02-23 | John | Initial draft — current state (no offline support) + Phase 2 requirements |
1. Offline Capability Requirements
Critical context: Drop is a pass-through payment app (PSD2 PISP model). Drop never holds customer money. All payment transactions require:
- Real-time balance verification via AISP (Open Banking)
- Live payment initiation via PISP (Open Banking)
- BankID authentication for sensitive operations
This fundamentally limits offline capability — payment initiation cannot work offline by design.
| Feature | Offline Support | Priority | Notes |
|---|---|---|---|
| Send money (remittance) | Not supported | — | Requires live PISP + BankID. Cannot be queued. |
| QR payment | Not supported | — | Requires live PISP + real-time merchant verification |
| View balance | Not supported | — | AISP reads live balance from bank — no cached balance |
| View transaction history | Partial (Phase 2) | P2 | Last 50 transactions cached locally |
| View recipient list | Partial (Phase 2) | P2 | Cached recipients for reference |
| Notification history | Partial (Phase 2) | P2 | Cached notification list |
| Login | Not supported | — | BankID requires network |
| Exchange rates | Partial (Phase 2) | P3 | Last known rate shown as estimate |
| App navigation | Supported | P1 | App shell loads without network |
Phase 1 (current) offline minimum viable experience:
When completely offline, the app shows a "Ingen nettverkstilkobling" error banner and prevents all payment operations. Previously loaded screens remain visible but all data shows as stale. No payment data is cached.
Phase 2 offline minimum viable experience:
User can view cached transaction history (last 50), recipient list, and notification history without network. A banner indicates "Viser lagret data". All payment and balance operations require network.
2. Local Storage Architecture
2.1 Current State (Phase 1)
| Data | Storage | Notes |
|---|---|---|
| Auth token | AsyncStorage (in-memory during session) |
Module-level let token in lib/api.js |
| User preferences | None — fetched from server on each launch | |
| Transaction history | None — fetched on each page load | |
| Recipients | None — fetched on send page load |
Phase 1 conclusion: Drop has no offline storage beyond the auth token. All data is fetched fresh on each mount.
2.2 Phase 2 — Planned Local Storage
Selected database: expo-sqlite (SQLite) — lightweight, Expo-managed, no native module ejection required.
Rationale: SQLite provides structured query capability for transaction filtering and recipient lookup. WatermelonDB adds sync complexity not needed for Drop's read-only cache use case.
Schema overview (Phase 2):
CREATE TABLE transactions (
id TEXT PRIMARY KEY,
type TEXT NOT NULL, -- 'remittance' | 'qr_payment'
status TEXT NOT NULL,
amount REAL NOT NULL,
currency TEXT NOT NULL DEFAULT 'NOK',
recipient_name TEXT,
created_at INTEGER NOT NULL, -- Unix timestamp
synced_at INTEGER NOT NULL -- When cached
);
CREATE TABLE recipients (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
country TEXT NOT NULL,
account_number TEXT NOT NULL,
synced_at INTEGER NOT NULL
);
CREATE TABLE notifications (
id TEXT PRIMARY KEY,
type TEXT NOT NULL,
title TEXT NOT NULL,
body TEXT NOT NULL,
is_read INTEGER DEFAULT 0,
created_at INTEGER NOT NULL,
synced_at INTEGER NOT NULL
);
-- No sync_queue table — Drop does NOT queue payment operations offline
-- Payments require real-time network by design
2.3 Secure Storage
| Data | Storage | Reason |
|---|---|---|
| Auth token | AsyncStorage (current) → expo-secure-store (Phase 2) |
Phase 2: encrypted, Keychain/Keystore |
| Refresh token | expo-secure-store (Phase 2) |
Encrypted |
| Biometric keys | expo-secure-store (Phase 2) |
Never in plain storage |
| Non-sensitive prefs | AsyncStorage |
Language setting, theme preference |
Rule: Payment credentials (BankID tokens) are never stored locally — they are transient session tokens managed by expo-web-browser.
3. Sync Protocol Design
Critical constraint: Drop does NOT have a sync queue for payment operations. Payments cannot be created offline and synced later — this would create unacceptable financial risk and violate PSD2 requirements.
Sync scope: Read-only data (transactions, recipients, notifications) — Phase 2 only.
sequenceDiagram
participant App
participant LocalDB
participant API
Note over App,API: Online — foreground sync (Phase 2)
App->>API: GET /v1/transactions?limit=50
API-->>App: { transactions: [...] }
App->>LocalDB: Upsert transactions (replace all)
App->>API: GET /v1/recipients
API-->>App: { recipients: [...] }
App->>LocalDB: Upsert recipients
Note over App,API: Offline scenario
App->>LocalDB: Read cached transactions
LocalDB-->>App: Cached data (may be stale)
App->>App: Show "Viser lagret data" banner
Note over App,API: Reconnect
App->>API: GET /v1/transactions (fresh)
API-->>App: Latest data
App->>LocalDB: Overwrite cache
3.1 Sync Strategy
Approach: Pull-only cache refresh (no push, no bidirectional sync)
| Property | Value |
|---|---|
| Protocol | REST |
| Pull endpoint | GET /v1/transactions?limit=50, GET /v1/recipients, GET /v1/notifications |
| Sync trigger | App foreground, after successful payment, pull-to-refresh |
| Cache expiry | 5 minutes (data older than 5 min triggers background refresh) |
| Conflict resolution | Server always wins — local cache is read-only, overwritten on sync |
3.2 Conflict Resolution
Not applicable. Drop's local cache is read-only. Users cannot modify cached data offline. Server data always overwrites cache on next sync.
3.3 Sync Frequency & Triggers
| Trigger | Action | Conditions |
|---|---|---|
| App foreground (from background) | Pull sync for all cached entities | Network available, > 5 min since last sync |
| Pull-to-refresh | Full data refresh | Network available |
| After successful transaction | Refresh transactions list | Network available |
| App launch (authenticated) | Full data refresh | Network available |
| Network restored (from offline) | Full data refresh | Re-connect detected |
4. Sync Queue Management
No sync queue for payments. Drop explicitly does NOT queue payment operations.
Rationale:
- PSD2 PISP requirements: payment must be authorized and initiated in real-time with user present
- BankID consent is single-use and time-limited — cannot be stored
- Financial risk: queuing payments creates potential for duplicate or delayed transactions
- Regulatory: Finanstilsynet requires real-time payment authorization
If user tries to pay while offline: Show error: "Du trenger nettverkstilkobling for å sende penger eller betale med QR."
5. Network State Detection & Handling
Library: @react-native-community/netinfo (Phase 2) or expo-network (Phase 1 alternative)
// Phase 2 — network state hook
import NetInfo from '@react-native-community/netinfo';
export function useNetworkState() {
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
return NetInfo.addEventListener((state) => {
setIsOnline(state.isConnected && state.isInternetReachable);
});
}, []);
return { isOnline };
}
UI behavior per state:
| State | UI Response |
|---|---|
| Offline | Banner: "Ingen nettverkstilkobling — betalinger er ikke tilgjengelig" |
| Offline (with cache) | Banner: "Viser lagret data" (Phase 2 only) |
| Reconnected | Banner: "Tilkoblet — oppdaterer..." (auto-dismiss 3s) |
| Payment attempted offline | Error modal: "Du trenger nettverkstilkobling for å sende penger" |
Current Phase 1 behavior: API calls fail with network error → fetch() throws → .catch() shows error state. No proactive network detection.
6. Data Flow: Online vs Offline
flowchart TD
UserAction["User Action"] --> CheckFeature{Payment or\nview feature?}
CheckFeature -->|"Payment (Send/QR)"| CheckNetwork{Network\nAvailable?}
CheckFeature -->|"View data"| CheckNetworkView{Network\nAvailable?}
CheckNetwork -->|"Yes"| InitiatePayment["Initiate payment via API\n(BankID + PISP)"]
CheckNetwork -->|"No"| ShowError["Error: Ingen tilkobling\nBetalinger krever nettverk"]
CheckNetworkView -->|"Yes"| FetchFresh["Fetch fresh data from API\nUpdate local cache (Phase 2)"]
CheckNetworkView -->|"No"| ShowCached["Show cached data\n(Phase 2) or empty state (Phase 1)"]
InitiatePayment -->|"Success"| ShowConfirmation["Show payment confirmation"]
InitiatePayment -->|"Failure"| ShowPaymentError["Show payment error\n(retry option)"]
style ShowError fill:#f8d7da
style ShowPaymentError fill:#f8d7da
style ShowCached fill:#fff3cd
style ShowConfirmation fill:#d4edda
7. Testing Strategy for Offline Scenarios
| Test Type | Scope | Tool |
|---|---|---|
| Unit | API error handling (network failure) | Jest |
| Integration | Cache read when offline | Jest + mock SQLite (Phase 2) |
| Manual | Airplane mode → attempt payment → verify error message | iOS/Android Simulator |
| Manual | Airplane mode → view cached history → verify banner (Phase 2) | iOS/Android Simulator |
| E2E | Full offline → reconnect flow (Phase 2) | Maestro |
Manual test scenarios:
- Enable airplane mode on device
- Attempt to send money → verify error message appears (not crash)
- Attempt QR payment → verify error message appears
- Navigate to transaction history → verify error or empty state (Phase 1)
- Disable airplane mode → verify app refreshes data automatically
8. Storage Limits & Data Eviction Policy
Phase 1: No local storage to manage.
Phase 2 targets:
| Storage Type | Limit | Eviction Strategy |
|---|---|---|
| SQLite cache (transactions) | 5 MB (50 transactions max) | Always overwrite with latest from server |
| SQLite cache (recipients) | 1 MB | Always overwrite |
| SQLite cache (notifications) | 2 MB | Always overwrite |
| Auth token (AsyncStorage) | Negligible | On logout / token expiry |
| Total app storage | < 30 MB | Alert user if device storage < 200 MB |
9. Error Handling & User Feedback
| Error | User Feedback | Recovery Action |
|---|---|---|
| Network failure on payment | "Du trenger nettverkstilkobling for å sende penger eller betale med QR" | Retry button, check connection |
| Network failure on data load | Empty state with retry button | Pull-to-refresh |
| Network failure on login | "Sjekk nettverkstilkoblingen din" | Retry login |
| Stale cache (Phase 2) | "Viser lagret data fra {time}" | Auto-refresh on reconnect |
| Sync failure (Phase 2) | Silent — app shows cached data | Auto-retry on next foreground |
Approval
| Role | Name | Date | Signature |
|---|---|---|---|
| Author | John (AI Director) | 2026-02-23 | |
| Mobile Lead | |||
| Backend Lead | |||
| Product Owner |