Skip to content

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