Skip to content

GPS & Photo Capture Flow

Location and Photo Capture to Storage Pipeline

graph TD
    User["👤 User Action"]

    subgraph ClientApp["📱 Client Application"]
        Start["Start Redemption<br/>User clicks 'Redeem'"]
        CheckPerms["🔒 Request Permissions<br/>- Geolocation API<br/>- Camera/Photo API"]
        PermDenied{Permissions<br/>Granted?}
        PermError["❌ Show Error<br/>Permission Denied<br/>User must enable"]
    end

    subgraph GPSCapture["📍 GPS Capture"]
        GetGPS["Get Current Location<br/>navigator.geolocation<br/>.getCurrentPosition()"]
        GPSWait["⏳ Wait for Signal<br/>accuracy ≥ 10m"]
        GPSTimeout{Location<br/>Found?}
        GPSFail["⚠️ GPS Unavailable<br/>Show warning<br/>Allow to proceed"]
        GPSSuccess["✅ Location Obtained<br/>{latitude, longitude,<br/>accuracy, timestamp}"]
    end

    subgraph PhotoCapture["📸 Photo Capture"]
        PhotoPrompt["Open Camera/Gallery<br/>- Camera: Real-time capture<br/>- Gallery: Select existing"]
        PhotoInput{User<br/>Captured?}
        PhotoCancel["❌ Photo Cancelled<br/>Allow to proceed<br/>without photo"]
        PhotoObtained["✅ Photo Blob<br/>{File object,<br/>size, type}"]
    end

    subgraph Validation["✓ Validation"]
        ValidateGPS["Check GPS Data<br/>- Coordinates valid?<br/>- Within bounds?"]
        ValidatePhoto["Check Photo<br/>- Size < 10MB?<br/>- Format: JPG/PNG?"]
        AllValid{Both Valid?}
        ValidationFail["❌ Validation Failed<br/>Show error message"]
    end

    subgraph Compress["🗜️ Compression"]
        CompressPhoto["Compress Image<br/>- Max width: 2048px<br/>- Quality: 80%<br/>- Max size: 2MB"]
        CreateMeta["Create Metadata<br/>{<br/>  originalSize,<br/>  compressedSize,<br/>  mime: image/jpeg,<br/>  captureTime<br/>}"]
    end

    subgraph Upload["📤 Cloud Storage Upload"]
        PrepareUpload["Prepare Upload<br/>- Path: /redemptions/{redemptionId}/{photoId}.jpg<br/>- Set metadata<br/>- Add download token"]
        UploadStart["Start Upload<br/>- Chunked upload<br/>- Progress tracking<br/>- Retry on failure"]
        UploadMonitor["Monitor Upload<br/>- onProgress callback<br/>- Show progress bar<br/>- Allow cancel"]
        UploadComplete{Upload<br/>Success?}
        UploadRetry["🔄 Retry Upload<br/>Max 3 attempts<br/>Exponential backoff"]
        UploadFail["❌ Upload Failed<br/>Show error<br/>Allow retry"]
        UploadSuccess["✅ Upload Complete<br/>Get downloadURL<br/>gsutil URI"]
    end

    subgraph Database["💾 Firestore"]
        CreateRedemption["Create Redemption Doc<br/>redemptions/{redemptionId}"]
        RedemptionData["📊 Store Fields:<br/>{<br/>  redemptionId: UUID,<br/>  walletItemId: reference,<br/>  userId: current user,<br/>  amount: redeemed amount,<br/>  location: {<br/>    latitude: number,<br/>    longitude: number,<br/>    accuracy: number,<br/>    timestamp: Timestamp<br/>  },<br/>  photo: {<br/>    url: downloadURL,<br/>    storageUri: gs://...,<br/>    originalSize: bytes,<br/>    compressedSize: bytes,<br/>    captureTime: Timestamp<br/>  },<br/>  status: REDEEMED,<br/>  createdAt: Timestamp<br/>}"]
        CreateLedger["Create Ledger Entry<br/>ledger_transactions/{txId}<br/>- type: REDEEM<br/>- amount<br/>- reference: redemptionId"]
        UpdateProjection["Update Projection<br/>wallet_projections/{userId}<br/>- Recalculate balance"]
        CreateAudit["Create Audit Entry<br/>admin_audit_log/{auditId}<br/>- Action: PHOTO_REDEMPTION<br/>- Location attached<br/>- Photo URL logged"]
    end

    subgraph UIResponse["🎉 UI Response"]
        Success["✅ Success Message<br/>- Redemption complete<br/>- Remaining balance<br/>- Location + photo saved"]
        UpdateUI["Update UI State<br/>- Refresh wallet balance<br/>- Show redemption in history<br/>- Display location on map"]
        PlaySound["🔊 Play Success Sound<br/>User feedback"]
    end

    subgraph Error["❌ Error Handling"]
        ErrorLog["Log Error<br/>- Frontend: localStorage<br/>- Backend: Cloud Logging"]
        ErrorDisplay["Display Error<br/>- User-friendly message<br/>- Suggestion for fix"]
        Offline["Offline Detection<br/>- Queue for later<br/>- Sync when online"]
    end

    %% FLOW CONNECTIONS
    User --> Start
    Start --> CheckPerms
    CheckPerms --> PermDenied
    PermDenied -->|No| PermError
    PermError --> Error
    PermDenied -->|Yes| GetGPS

    GetGPS --> GPSWait
    GPSWait --> GPSTimeout
    GPSTimeout -->|Timeout| GPSFail
    GPSFail --> PhotoPrompt
    GPSTimeout -->|Found| GPSSuccess
    GPSSuccess --> PhotoPrompt

    PhotoPrompt --> PhotoInput
    PhotoInput -->|Cancel| PhotoCancel
    PhotoCancel --> ValidateGPS
    PhotoInput -->|Captured| PhotoObtained
    PhotoObtained --> ValidatePhoto

    ValidateGPS --> AllValid
    ValidatePhoto --> AllValid
    AllValid -->|No| ValidationFail
    ValidationFail --> Error
    AllValid -->|Yes| CompressPhoto

    CompressPhoto --> CreateMeta
    CreateMeta --> PrepareUpload
    PrepareUpload --> UploadStart
    UploadStart --> UploadMonitor
    UploadMonitor --> UploadComplete
    UploadComplete -->|No| UploadRetry
    UploadRetry -->|Retries Left| UploadStart
    UploadRetry -->|Max Retries| UploadFail
    UploadFail --> Error
    UploadComplete -->|Yes| UploadSuccess

    UploadSuccess --> CreateRedemption
    CreateRedemption --> RedemptionData
    RedemptionData --> CreateLedger
    CreateLedger --> UpdateProjection
    UpdateProjection --> CreateAudit
    CreateAudit --> Success

    Success --> UpdateUI
    UpdateUI --> PlaySound
    PlaySound --> UIResponse

    Error --> ErrorLog
    ErrorLog --> ErrorDisplay
    ErrorDisplay --> Offline

    style Start fill:#e3f2fd
    style CheckPerms fill:#fff3e0
    style GPSSuccess fill:#c8e6c9
    style PhotoObtained fill:#c8e6c9
    style CompressPhoto fill:#f3e5f5
    style UploadSuccess fill:#c8e6c9
    style RedemptionData fill:#fff3e0
    style CreateAudit fill:#f3e5f5
    style Success fill:#c8e6c9
    style PermError fill:#ffcdd2
    style ValidationFail fill:#ffcdd2
    style UploadFail fill:#ffcdd2

Process Breakdown

Phase 1: Permission & Initialization

Geolocation API Permissions:
├─ Browser: "Allow location access?"
├─ User: Grant/Deny
└─ App: Continue or show error

Camera/Photo Permissions:
├─ Browser: "Allow camera/photos?"
├─ User: Grant/Deny
└─ App: Open camera or gallery picker

Phase 2: GPS Capture

// Pseudocode
const gpsData = await navigator.geolocation.getCurrentPosition({
  timeout: 10000,      // Wait up to 10 seconds
  maximumAge: 0,       // Fresh location only
  enableHighAccuracy: true
});

const {latitude, longitude, accuracy} = gpsData.coords;

Constraints: - ✅ Accuracy ≥ 10m preferred - ✅ Timeout: 10 seconds max wait - ✅ Store timestamp when captured - ⚠️ Can fail in poor signal (underground, dense forest) - ⚠️ Optional but encouraged for context

Phase 3: Photo Capture

User Options:
├─ 📷 Open Camera  Real-time capture
├─ 🖼️ Select Gallery  Use existing photo
└─ ⏭️ Skip Photo  Continue without

Result: File Blob object
├─ MIME: image/jpeg, image/png
├─ Size: actual bytes
└─ Data: binary file content

Phase 4: Validation

GPS Validation:
├─ -90  latitude  90
├─ -180  longitude  180
├─ accuracy > 0
└─ timestamp is recent

Photo Validation:
├─ MIME type: image/*
├─ Size: < 10 MB
├─ Not corrupted
└─ Can be compressed

Phase 5: Compression

Image Compression:
├─ Read original → Blob
├─ Resize: max 2048px width
├─ Quality: 80% JPEG
├─ Target: < 2 MB
└─ Output: Compressed blob

Metadata:
├─ Original: 5.2 MB (3840×2160)
├─ Compressed: 380 KB (2048×1152)
└─ Ratio: 92.7% smaller

Phase 6: Upload to Cloud Storage

GCS Path:
  gs://[bucket]/redemptions/[redemptionId]/[photoId].jpg

Upload Process:
├─ Chunked: 256KB per chunk
├─ Retry: 3 attempts with exponential backoff
├─ Progress: onProgress callback
├─ Cancel: User can abort
└─ Complete: Get downloadURL

URL Types:
├─ Download: https://storage.googleapis.com/bucket/...
├─ GCS URI: gs://bucket/redemptions/[redemptionId]/...
└─ Token: Time-limited access token

Phase 7: Firestore Storage

Redemptions Document:

{
  redemptionId: "uuid",           // Unique ID
  walletItemId: "uuid",           // Which item was redeemed
  userId: "firebase-uid",         // Who redeemed it
  amount: 250,                    // NOK redeemed

  location: {
    latitude: 59.9,
    longitude: 10.7,
    accuracy: 8.5,                // Meters
    timestamp: Timestamp           // When captured
  },

  photo: {
    url: "https://storage.googleapis.com/...",
    storageUri: "gs://bucket/redemptions/...",
    originalSize: 5242880,        // Bytes
    compressedSize: 389120,       // After compression
    captureTime: Timestamp
  },

  status: "REDEEMED",
  createdAt: Timestamp
}

Associated Ledger Entry:

{
  transactionId: "uuid",
  userId: "firebase-uid",
  type: "REDEEM",
  walletItemId: "uuid",
  amount: 250,
  reference: "redemptionId",     // Links to redemption doc
  createdAt: Timestamp
}

Associated Audit Log Entry:

{
  auditId: "uuid",
  action: "PHOTO_REDEMPTION",
  admin: false,                   // User initiated
  userId: "firebase-uid",
  resourceType: "REDEMPTION",
  resourceId: "redemptionId",
  details: {
    amount: 250,
    hasLocation: true,
    hasPhoto: true,
    photoUrl: "gs://..."          // For verification
  },
  createdAt: Timestamp
}

Error Handling & Recovery

Error Cause Recovery
GPS Timeout Poor signal Continue without location
GPS Denied User rejected Warn user, proceed
Photo Upload Failed Network issue Retry up to 3x
File Too Large Photo > 2MB Compress more aggressively
Invalid Coordinates GPS malfunction Reject and show error
Firestore Timeout Server busy Retry with backoff

Security Considerations

All data encrypted in transit (HTTPS)Cloud Storage: Private by defaultFirestore Rules: User can only access own redemptionsPhotos are immutable audit evidenceTimestamps prevent tamperingLocation data sanitized before logging