Skip to main content

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:

  1. Real-time balance verification via AISP (Open Banking)
  2. Live payment initiation via PISP (Open Banking)
  3. 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:

  1. Enable airplane mode on device
  2. Attempt to send money → verify error message appears (not crash)
  3. Attempt QR payment → verify error message appears
  4. Navigate to transaction history → verify error or empty state (Phase 1)
  5. 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