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
(Web/Mobile)
participant Camera as Camera API
(Browser/Native)
participant API as Drop API
(/api/transactions)
participant Bank as User's Bank
(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
with scan frame brackets
else Camera denied / unavailable
App->>App: Show "Simuler skanning" button
(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
(amount, fee, total, source account)
User->>App: Tap "Betal nå" (confirm)
App->>API: POST /api/transactions/qr-payment
{ 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
payment from user's bank.
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
(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
When camera is unavailable or denied:
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: transactions table — See Database Schema
Merchant schema: merchants table — 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