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 default ✅ Firestore Rules: User can only access own redemptions ✅ Photos are immutable audit evidence ✅ Timestamps prevent tampering ✅ Location data sanitized before logging