QR Payment Flow
Flow: QR Payment
Document: LLD-002 Version: 1.0 Date: 2026-02-21 Author: Frontend Architect (AI Agent) Status: Draft Scope: End-to-end QR payment flow covering camera handling, merchant resolution, payment confirmation, SCA, and error states for both web and mobile
1. Overview
QR payments allow Drop users to pay in-store by scanning a merchant's QR code. The payment is initiated via PISP (Payment Initiation Service Provider) directly from the user's bank account under the PSD2 pass-through model. Drop never holds customer funds.
QR Code Format: drop://pay/{merchantId}
Payment Fee: 1% of transaction amount (fee is deducted from user's balance: totalOre = amountOre + feeOre. Settlement to merchant is separate.)
Note: Database stores amounts in ore (minor units). API accepts NOK and converts internally using
nokToOre(). Example: 129 NOK = 12900 ore in DB.
Flow summary: Camera permission → QR scan → decode merchant ID → fetch merchant details → amount entry → SCA trigger → payment confirmation → receipt display
2. QR Payment Sequence Diagram
sequenceDiagram
actor User
participant App as Drop App<br/>(Web/Mobile)
participant Camera as Camera API<br/>(Browser/Native)
participant API as Drop API<br/>(/api/transactions)
participant Bank as User's Bank<br/>(via PISP)
participant DB as Database
User->>App: Navigate to /scan
App->>App: Check auth (useAuth / Bearer token)
alt Camera available
App->>Camera: Request camera permission
Camera-->>App: Permission granted
App->>App: Show camera viewfinder<br/>with scan frame brackets
else Camera denied / unavailable
App->>App: Show "Simuler skanning" button<br/>(demo mode fallback)
end
User->>Camera: Point at merchant QR code
Camera->>App: Decode QR: "drop://pay/{merchantId}"
App->>App: Parse merchantId from QR URI
App->>API: GET /api/merchants/{merchantId}
API->>DB: SELECT merchant WHERE id = ?
API-->>App: { merchantId, businessName, category }
App->>App: Show merchant info + amount input
User->>App: Enter amount (e.g., 129 NOK)
App->>App: Calculate fee (1% = 1.29 NOK)
App->>App: Show payment summary<br/>(amount, fee, total, source account)
User->>App: Tap "Betal nå" (confirm)
App->>API: POST /api/transactions/qr-payment<br/>{ merchantId, amount }
API->>API: Rate limit check (10/min)
API->>DB: Verify merchant exists
API->>DB: Get user's primary bank account
API->>API: Calculate fee (1% of amount)
Note over API,Bank: Production: PISP initiates<br/>payment from user's bank.<br/>Demo: Direct DB debit.
API->>DB: BEGIN TRANSACTION
API->>DB: UPDATE bank_accounts SET balance = balance - (amount + fee)
API->>DB: INSERT transaction (type=qr_payment, status=completed)
API->>DB: COMMIT
API-->>App: 201 { transaction }
App->>App: Show success screen<br/>(checkmark, merchant, amount, fee)
User->>App: Tap "Tilbake til hjem"
App->>App: Navigate to /dashboard
3. Payment Flow State Diagram
stateDiagram-v2
[*] --> Idle: Navigate to /scan
Idle --> RequestingPermission: Camera API available
Idle --> SimulationMode: No camera / demo mode
RequestingPermission --> Scanning: Permission granted
RequestingPermission --> PermissionDenied: Permission denied
PermissionDenied --> Scanning: User grants in settings
PermissionDenied --> SimulationMode: Use simulation
SimulationMode --> MerchantResolved: Click "Simuler skanning"
Scanning --> Decoding: QR code detected
Decoding --> MerchantResolved: Valid drop:// URI
Decoding --> InvalidQR: Not a Drop QR code
InvalidQR --> Scanning: Dismiss error, retry scan
MerchantResolved --> AmountEntry: Merchant details loaded
MerchantResolved --> MerchantNotFound: Merchant lookup failed
MerchantNotFound --> Scanning: Go back, scan again
AmountEntry --> PaymentReview: Amount entered + confirmed
AmountEntry --> Scanning: Cancel / go back
PaymentReview --> Processing: Tap "Betal nå"
PaymentReview --> AmountEntry: Edit amount
Processing --> Success: Payment completed (201)
Processing --> InsufficientFunds: Balance too low
Processing --> PaymentFailed: API error
InsufficientFunds --> AmountEntry: Adjust amount
PaymentFailed --> PaymentReview: Retry
Success --> [*]: Navigate to dashboard
4. Camera Permission Handling
4.1 Camera Permission Table (iOS / Android)
| Platform | Permission API | First Request | After Denial | Settings Redirect |
|---|---|---|---|---|
| iOS (Safari) | navigator.mediaDevices.getUserMedia() |
System prompt: "getdrop.no would like to access the camera" | Blocked silently; must reset in Safari Settings → getdrop.no → Camera | Link to Settings not programmatically available |
| iOS (Expo) | expo-camera Camera.requestCameraPermissionsAsync() |
System prompt: "Drop would like to access the camera" | Returns { status: 'denied' }; use Linking.openSettings() |
Linking.openSettings() → iOS Settings → Drop → Camera |
| Android (Chrome) | navigator.mediaDevices.getUserMedia() |
System prompt: "Allow getdrop.no to use your camera?" | Blocked; user must tap lock icon → Site settings → Camera → Allow | Site settings accessible via address bar |
| Android (Expo) | expo-camera Camera.requestCameraPermissionsAsync() |
System prompt: "Allow Drop to take pictures and record video?" | Returns { status: 'denied' }; { canAskAgain: false } after permanent deny |
Linking.openSettings() → App Info → Permissions → Camera |
4.2 Fallback Behavior
- Web: Shows "Simuler skanning" button that triggers a demo merchant scan (Ahmetov Kebab, merchant_001)
- Mobile: Shows camera placeholder (gray box with QR icon) + "Simuler skanning" button + nearby merchants list (hardcoded: Ahmetov Kebab, Kafe Oslo, Narvesen)
5. QR Code Format and Validation
| Field | Value | Validation |
|---|---|---|
| URI scheme | drop:// |
Must match exactly |
| Path | pay/{merchantId} |
Must start with pay/ |
| Merchant ID format | mer_ prefix + flexible identifier |
No strict regex enforced (e.g., mer_demo1 is valid) |
| Example | drop://pay/mer_demo1 |
Validated by DB lookup |
HMAC verification: QR codes may optionally include timestamp and signature parameters for HMAC-SHA256 verification using the merchant's qr_hmac_key. Verification is performed only when both qrTimestamp and qrSignature are present in the request. If omitted, the payment proceeds without cryptographic QR verification.
Invalid QR handling:
- Non-Drop QR codes: Show "Ugyldig QR-kode. Vennligst skann en Drop-butikks QR-kode."
- Malformed merchant ID: Show "Ugyldig betalingskode."
- Empty scan result: Continue scanning (do not trigger error)
6. Error States
| Error | HTTP Status | Cause | User-Facing Message (Norwegian) | Recovery |
|---|---|---|---|---|
| Invalid QR | N/A (client) | Not a drop://pay/ URI |
"Ugyldig QR-kode. Skann en Drop-butikks QR-kode." | Retry scan |
| Merchant Not Found | 404 | Merchant ID not in database | "Butikken ble ikke funnet. QR-koden kan være utdatert." | Scan different QR |
| Insufficient Funds | 402 | Bank balance < amount + fee | "Ikke nok penger på kontoen. Saldo: {balance} NOK." | Reduce amount or top up bank |
| No Bank Account | 400 | No linked bank account | "Ingen bankkonto koblet. Koble en konto først." | Navigate to /accounts |
| Rate Limited | 429 | >10 payments/min | "For mange betalinger. Vent litt." | Wait and retry |
| Network Error | N/A | No connectivity | "Ingen nettverkstilkobling." | Retry when online |
| Server Error | 500 | Internal error | "Noe gikk galt. Prøv igjen." | Retry |
| Camera Error | N/A | Camera hardware failure | "Kameraet fungerer ikke. Bruk 'Simuler skanning'." | Use simulation mode |
7. UI Components
7.1 Web — Scan Page (/scan)
| State | UI Elements | Components Used |
|---|---|---|
| Scanning | Dark background (#0F172A), camera viewfinder with gold corner brackets (#D4A017), instruction text, "Simuler skanning" button, BankID/Vipps badges | BottomNav, Button, ArrowLeft/Camera (lucide) |
| Payment | White background, merchant icon (gold gradient circle), merchant name (Fraunces font), amount display (4xl), source account info, "Betal nå" button (green), "Avbryt" button | BottomNav, Button, ChevronLeft (lucide) |
| Paying | Loading spinner overlay | Spinner component |
| Success | Checkmark icon (green), transaction details, merchant name, amount, fee | BottomNav, Check/Store (lucide) |
7.2 Mobile — Scan Screen ((tabs)/scan.js)
| State | UI Elements |
|---|---|
| Scanning | Gray camera placeholder, QR icon, "Skann QR-kode" text, "Simuler skanning" button, nearby merchants list |
| Payment | Merchant info, amount input, confirm button |
| Success | Confirmation with "Tilbake til hjem" button |
7.3 Figma Reference
Source of truth: mockups/figma-make-export/src/app/screens/ScanQR.tsx
- Dark scanning mode with gold bracket viewfinder
- Payment confirmation with merchant details and amount
- Green "Betal nå" CTA button
8. Data Flow
8.1 Request: POST /api/transactions/qr-payment
{
"merchantId": "mer_a1b2c3d4e5f6g7h8",
"amount": 129
}
8.2 Response: 201 Created
{
"data": {
"id": "tx_qr_a1b2c3d4e5f6g7h8",
"type": "qr_payment",
"status": "completed",
"amount": 129,
"currency": "NOK",
"fee": 1.29,
"feePercent": 1,
"merchantName": "Ahmetov Kebab",
"merchantId": "mer_1",
"fromAccount": "DNB",
"createdAt": "2026-02-21T14:30:00.000Z"
}
}
8.3 Database Operations (Atomic Transaction)
BEGIN;
UPDATE bank_accounts SET balance = balance - 130.29 WHERE id = ? AND user_id = ?;
INSERT INTO transactions (id, user_id, type, status, amount, currency, fee, merchant_id, created_at, completed_at)
VALUES (?, ?, 'qr_payment', 'completed', 129, 'NOK', 1.29, ?, datetime('now'), datetime('now'));
COMMIT;
9. Production vs Demo Differences
| Aspect | Demo (Current) | Production (Phase 2+) |
|---|---|---|
| Camera | Simulated scan button | Real camera scanning via expo-camera or getUserMedia() |
| Payment execution | Direct DB balance debit | PISP initiation via Open Banking API |
| SCA | Not implemented | BankID SCA required for each payment |
| Merchant verification | Static seed data (Ahmetov Kebab) | Live Brønnøysund org number verification |
| Fee handling | Fee deducted from user's balance (totalOre = amountOre + feeOre) |
Merchant settlement is separate from user debit |
| Settlement | Instant (DB update) | T+1 or T+2 settlement to merchant bank account |
10. Accessibility Considerations (WCAG 2.1 AA)
| Requirement | Implementation |
|---|---|
| Camera alternative | "Simuler skanning" button provides non-camera path |
| Amount input | Labeled with "Beløp" and suffixed with "NOK" |
| Confirmation | "Betal nå" button clearly labeled; "Avbryt" provides escape |
| Success feedback | Visual checkmark + text confirmation of payment |
| Color contrast | Gold (#D4A017) on dark (#0F172A) = 5.2:1 ratio (passes AA) |
| Screen reader | Merchant name and amount announced on payment confirmation |
11. Cross-References
- QR payment API:
POST /api/transactions/qr-payment— See API Reference - Merchant registration:
POST /api/merchants/register— See API Reference - Transaction schema:
transactionstable — See Database Schema - Merchant schema:
merchantstable — See Database Schema - Component overview: See component-overview.md
- Figma scan screen:
mockups/figma-make-export/src/app/screens/ScanQR.tsx - Web scan page:
src/drop-app/src/app/scan/page.tsx— See PAGES.md - Mobile scan screen:
src/drop-mobile/app/(tabs)/scan.js— See MOBILE-APP.md - Merchant onboarding flow: See flow-merchant-onboarding.md
- PSD2 PISP details: See open-banking-aisp-pisp.md