QR Scanning Process¶
QR Code Detection and Equipment Lookup Flow¶
graph TD
Start["đ User Initiates<br/>Scan Equipment"] --> OpenScanner["đą Open Mobile Scanner<br/>- Camera permission required<br/>- QR scanner UI loads"]
OpenScanner --> PermCheck{Camera<br/>Permission?}
PermCheck -->|Denied| PermError["â Permission Error<br/>Cannot access camera<br/>Request in settings"]
PermCheck -->|Granted| CameraReady["â
Camera Ready<br/>Show live preview<br/>Auto-focus enabled"]
CameraReady --> Scanning["đ Scanning Active<br/>Using jsQR or similar<br/>- Decode pixels<br/>- Detect patterns"]
Scanning --> ProcessFrames["⥠Process Video Frames<br/>- Continuous stream<br/>- 30fps processing<br/>- Look for QR marker"]
ProcessFrames --> QRFound{QR Code<br/>Found?}
QRFound -->|No| ProcessFrames
QRFound -->|Yes| DecodeQR["đ Decode QR Data<br/>Extract: walletItemId<br/>Format: UUID string"]
DecodeQR --> ValidateQR["â Validate QR<br/>- Is valid UUID?<br/>- Contains metadata?<br/>- Not corrupted?"]
ValidateQR --> QRValid{QR Valid?}
QRValid -->|Invalid| QRError["â Invalid QR<br/>Show: 'Invalid QR code'<br/>Return to scanner"]
QRError --> Scanning
QRValid -->|Valid| HapticFeedback["đŗ Haptic Feedback<br/>Vibrate device<br/>Signal: Code found"]
HapticFeedback --> CallFunction["âī¸ Call Backend Function<br/>lookupQRCode()<br/>Data: {walletItemId, jwt}"]
CallFunction --> FunctionReceive["âī¸ Function Receives<br/>- Verify JWT token<br/>- Validate walletItemId format"]
FunctionReceive --> AuthCheck["đ Auth Check<br/>- User authenticated?<br/>- Valid claims?"]
AuthCheck --> AuthFail{Auth<br/>Valid?}
AuthFail -->|No| AuthError["â Auth Failed<br/>HTTP 401"]
AuthError --> UIError1["Show: 'Session expired<br/>Please login again'"]
AuthFail -->|Yes| FetchWallet["đĻ Fetch Wallet Item<br/>Query: wallet_items<br/>Where: walletItemId==value"]
FetchWallet --> WalletFound{Item<br/>Found?}
WalletFound -->|No| WalletError["â Not Found<br/>HTTP 404<br/>Return error"]
WalletError --> UIError2["Show: 'Equipment not found<br/>Invalid QR code'"]
WalletFound -->|Yes| CheckStatus["â ī¸ Check Item Status"]
CheckStatus --> Status{Status<br/>Check}
Status -->|BLOCKED| StatusError1["â Item Blocked<br/>Cannot redeem"]
Status -->|EXPIRED| StatusError2["â° Item Expired<br/>Cannot redeem"]
Status -->|REDEEMED| StatusError3["â
Already Redeemed<br/>Show remaining: 0"]
Status -->|ACTIVE| StatusOK["â
Item Active<br/>Ready to redeem"]
StatusError1 --> UIError3["Display Status<br/>Show reason"]
StatusError2 --> UIError3
StatusError3 --> UIError3
StatusOK --> FetchProduct["đĻ Fetch Product Info<br/>Query: products<br/>Where: productId==wallet.productId"]
FetchProduct --> ProductFound{Product<br/>Found?}
ProductFound -->|No| ProductError["â Product metadata lost<br/>Return with wallet only"]
ProductFound -->|Yes| GatherData["đ Gather Item Data<br/>{<br/> walletItemId,<br/> type,<br/> value,<br/> status,<br/> remainingBalance,<br/> productName,<br/> productDescription,<br/> issuedAt,<br/> expiresAt<br/>}"]
GatherData --> ReturnData["âŠī¸ Return to Function<br/>Combined wallet + product"]
ReturnData --> FunctionResponse["đ¤ Send HTTPS Response<br/>Status: 200 OK<br/>Body: {<br/> success: true,<br/> item: {...},<br/> product: {...},<br/> timestamp: now<br/>}"]
FunctionResponse --> UIReceive["đģ UI Receives Data<br/>JSON parse<br/>Validate response"]
UIReceive --> ShowDetails["đ Display Equipment Details<br/>âââââââââââââââââââââââââ<br/>Product: [name]<br/>Type: GIFTCARD / TOKEN / TICKET<br/>Value: [amount] NOK<br/>Status: ACTIVE â
<br/>Remaining: [balance]<br/>Expires: [date]<br/>âââââââââââââââââââââââââ"]
ShowDetails --> UserAction["đ¤ User Choice"]
UserAction -->|Redeem| Redeem["Proceed to Redemption<br/>â redeemValue() function"]
UserAction -->|View More| Details["Show Details<br/>- Owner info<br/>- Purchase date<br/>- Transaction history"]
UserAction -->|Share| Share["Share Equipment<br/>- Send QR link<br/>- Copy code<br/>- Print QR"]
UserAction -->|Cancel| Scanning
Redeem --> RedeemStart["đ Start Redemption Flow<br/>See: GPS & Photo Capture Flow"]
Details --> HistoryFetch["Fetch Transaction History<br/>Query: ledger_transactions<br/>Where: walletItemId==value"]
HistoryFetch --> ShowHistory["Display:<br/>- ISSUE transaction<br/>- REDEEM transactions<br/>- Remaining balance"]
Share --> GenerateQR["Generate Share Link<br/>Deep link format:<br/>?qr=[walletItemId]"]
GenerateQR --> ShareUI["Copy to clipboard<br/>or Share via social"]
PermError --> ErrorUI["â Error UI"]
UIError1 --> ErrorUI
UIError2 --> ErrorUI
UIError3 --> ErrorUI
ProductError --> ShowDetails
style Start fill:#e3f2fd
style CameraReady fill:#c8e6c9
style DecodeQR fill:#f3e5f5
style ValidateQR fill:#fff3e0
style HapticFeedback fill:#e0f2f1
style AuthCheck fill:#fce4ec
style StatusOK fill:#c8e6c9
style FunctionResponse fill:#e8f5e9
style ShowDetails fill:#fff9c4
style RedeemStart fill:#c8e6c9
style PermError fill:#ffcdd2
style QRError fill:#ffcdd2
style AuthError fill:#ffcdd2
style WalletError fill:#ffcdd2
style StatusError1 fill:#ffcdd2
style StatusError2 fill:#ffcdd2
style StatusError3 fill:#ffcdd2
style ErrorUI fill:#ffcdd2
Detailed Process Flow¶
Phase 1: Camera & QR Detection¶
// Frontend: QR Scanner Component
const [permission, setPermission] = useState<'pending' | 'granted' | 'denied'>();
// Request camera permission
const requestCameraAccess = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
video: { facingMode: 'environment' } // Rear camera on mobile
});
setPermission('granted');
// Process video stream...
} catch (e) {
setPermission('denied');
}
};
QR Detection Library:
- Library: jsQR or html5-qrcode
- Processing: Continuous frame decode at ~30fps
- Detection: Looks for QR code patterns in pixels
- Output: Decoded text (walletItemId UUID)
Phase 2: QR Code Format¶
Generated QR contains:
Data: 550e8400-e29b-41d4-a716-446655440000
Type: UUID (v4)
Size: 36 characters
Encoding: UTF-8
Error Correction: Level M (15%)
QR Code Details: - Module Size: 10x10 pixels minimum - Quiet Zone: 4 modules border - Capacity: ~4296 bytes (this data is ~36 bytes) - Scannable: Works from 10cm to 1m distance
Phase 3: Backend Lookup Function¶
// functions/src/handlers/qr/generateQRCode.ts
export async function lookupQRCode(
data: { walletItemId: string },
context: functions.https.CallableContext,
db: FirebaseFirestore.Firestore
): Promise<LookupQRResponse> {
// 1. Verify authentication
if (!context.auth) throw new HttpsError('unauthenticated', '...');
const userId = context.auth.uid;
// 2. Validate walletItemId format
if (!isValidUUID(data.walletItemId)) {
throw new HttpsError('invalid-argument', 'Invalid wallet item ID');
}
// 3. Fetch wallet item
const walletDoc = await db
.collection('wallet_items')
.doc(data.walletItemId)
.get();
if (!walletDoc.exists) {
throw new HttpsError('not-found', 'Wallet item not found');
}
const walletItem = walletDoc.data() as WalletItem;
// 4. Check status
if (!['ACTIVE', 'CLAIMED'].includes(walletItem.status)) {
throw new HttpsError(
'failed-precondition',
`Cannot scan: item is ${walletItem.status}`
);
}
// 5. Fetch product
const productDoc = await db
.collection('products')
.doc(walletItem.productId)
.get();
if (!productDoc.exists) {
return {
success: true,
item: walletItem,
product: null, // Graceful degradation
timestamp: Timestamp.now()
};
}
// 6. Return combined data
return {
success: true,
item: walletItem,
product: productDoc.data(),
timestamp: Timestamp.now()
};
}
Phase 4: Data Validation¶
Client-side Validation (Before API call):
const validateQRData = (rawData: string) => {
// Must be valid UUID v4 format
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(rawData);
};
Server-side Validation (In function):
const isValidUUID = (id: string): boolean => {
const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
return uuidRegex.test(id);
};
Phase 5: Status Checks¶
| Status | Action | Reason |
|---|---|---|
| ACTIVE | â Allow scan & redeem | Item ready to use |
| CLAIMED | â Allow scan & redeem | Already claimed but not used |
| REDEEMED | â Show "Already used" | No value remaining |
| BLOCKED | â Show "Blocked" | Admin revoked |
| EXPIRED | â Show "Expired" | Past expiry date |
Phase 6: UI Display After Scan¶
Information Card:
ââââââââââââââââââââââââââââââââââââââââââ
â đ EQUIPMENT FOUND â
ââââââââââââââââââââââââââââââââââââââââââĸ
â Product: Golfklubb Gift Card â
â Type: GIFTCARD â
â Value: 500 NOK â
â Balance: 500 NOK â
â
â Status: ACTIVE â
â Expires: 2026-12-31 â
â Issued: 2024-01-15 â
ââââââââââââââââââââââââââââââââââââââââââĸ
â [đ° Redeem Now] [đ View Details] â
â [đ Scan Again] [â Cancel] â
ââââââââââââââââââââââââââââââââââââââââââ
Phase 7: Actions After Scan¶
Option 1: Redeem¶
â Transition to redeemValue() function
â Include GPS & photo capture
â Update balance in real-time
Option 2: View Details¶
// Fetch transaction history
const transactions = await db
.collection('ledger_transactions')
.where('walletItemId', '==', walletItemId)
.orderBy('createdAt', 'desc')
.limit(50)
.get();
// Display:
// - ISSUE: +500 NOK (2024-01-15)
// - REDEEM: -150 NOK (2024-01-20)
// - REDEEM: -100 NOK (2024-01-25)
// Remaining: 250 NOK
Option 3: Share¶
// Generate shareable link
const shareUrl = new URL('https://gavekort.example.com/redeem');
shareUrl.searchParams.set('qr', walletItemId);
shareUrl.searchParams.set('claim_code', claimCode);
// Share via:
// - Copy to clipboard
// - Email link
// - WhatsApp/SMS
// - Social media
Error Handling & Recovery¶
Scanning Errors¶
| Error | Cause | Recovery |
|---|---|---|
| Camera timeout | Phone slow | Retry, show loading |
| Invalid QR | Damaged/fake code | Return to scanner |
| Invalid UUID | Malformed data | Show error message |
| Item not found | Deleted item | Suggest scanning different item |
| Network timeout | No connection | Offline mode / Retry |
| Auth expired | Token invalid | Redirect to login |
Status Errors¶
| Status | Message | Suggestion |
|---|---|---|
| REDEEMED | "Item already used" | Check transaction history |
| EXPIRED | "Item has expired" | Contact admin |
| BLOCKED | "Item is blocked" | Contact support |
Security Considerations¶
â QR contains only UUID - No sensitive data â JWT token required - Prevents unauthorized lookup â Wallet item ownership - Optional check (open or user-specific) â Rate limiting - Prevent brute-force scanning â HTTPS only - Encrypted transmission â Firestore rules - Enforce read access
Performance Optimization¶
| Optimization | Implementation |
|---|---|
| Caching | Store item in local state |
| Offline mode | Queue scan for later if offline |
| Haptic feedback | Immediate UX confirmation |
| Single function call | Combine wallet + product fetch |
| Lazy loading | Load details on-demand |