Phase 3: Verification System & Arrows Leaderboard
Date: December 29, 2025 Session Type: Feature Development Scope: Witness verification system + global arrows leaderboard PR: #411 - feat: Add Phase 3 verification system and arrows leaderboard Stats: 61 files, +10,955 lines Review: Agent 3 - 10/10 APPROVED
Overview
Phase 3 implements two major features:
- Verification System: Allows users to have their published scores verified by witnesses via shareable tokens/links
- Arrows Leaderboard: Fun engagement feature tracking total arrows shot globally with milestone badges
Architecture
┌─────────────────────────────────────────────────────────────┐
│ UI Layer │
│ RequestVerificationDialog │ VerifyScoreDialog │ Leaderboard│
└─────────────────────────────┬───────────────────────────────┘
│
┌─────────────────────────────▼───────────────────────────────┐
│ Service Layer │
│ VerificationService │ ArrowStatsService │ Publishing │
└───────────┬─────────────────────────────────┬───────────────┘
│ │
┌───────────▼───────────┐ ┌───────────▼───────────────┐
│ FirebaseVerification │ │ FirebaseArrowStats │
│ DataSource │ │ DataSource │
└───────────┬───────────┘ └───────────┬───────────────┘
│ │
┌───────────▼─────────────────────────────────▼───────────────┐
│ Firestore + Storage │
│ verificationRequests │ verificationProofs │ user_arrow_stats│
└─────────────────────────────────────────────────────────────┘
Verification System
Verification Flow
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Score Owner │ │ Share Link │ │ Witness │
│ requests │────►│ via QR/URL │────►│ opens app │
│ verification │ │ │ │ via deep link │
└─────────────────┘ └─────────────────┘ └─────────────────┘
│
▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Leaderboard │ │ Proof created │ │ Witness │
│ entry updated │◄────│ (immutable) │◄────│ confirms │
│ to VERIFIED │ │ │ │ (+ optional │
└─────────────────┘ └─────────────────┘ │ photo) │
└─────────────────┘
Verification Levels
| Level | Trust Score | How Achieved |
|---|---|---|
| SELF_REPORTED | 1 | Default when publishing |
| IMAGE_RECOGNITION | 2 | Camera scoring (future ML) |
| WITNESS_VERIFIED | 3 | Another user verifies via token |
| TOURNAMENT | 4 | Official tournament score |
Token Security
Format: 12-character alphanumeric (uppercase + digits 2-9) Excluded characters: 0, O, 1, I, L (ambiguous) Possible combinations: 31^12 ≈ 4.7 × 10^18 Expiry: 7 days
object VerificationTokenGenerator {
private const val TOKEN_LENGTH = 12
private val CHARS = (('A'..'Z') + ('2'..'9'))
.filterNot { it in listOf('O', 'I', 'L') }
fun generate(): String // e.g., "ABC23XYZ789K"
fun generateDeepLink(token: String): String // archeryapprentice://verify/{token}
fun generateWebLink(token: String): String // https://archeryapprentice.com/verify/{token}
fun extractTokenFromUrl(url: String): String?
fun isValidFormat(token: String): Boolean
}Data Models (Shared KMP)
VerificationRequest
data class VerificationRequest(
val id: String,
val token: String, // 12-char shareable token
// Requester
val requesterId: String,
val requesterDisplayName: String,
// References
val leaderboardEntryId: String,
val publishedRoundId: String,
// Denormalized score summary (for witness display)
val score: Int,
val maxPossibleScore: Int,
val conditionKey: String,
val scoredAt: Long,
// Status
val status: VerificationRequestStatus, // PENDING, COMPLETED, EXPIRED, CANCELLED
val expiresAt: Long, // 7 days from creation
// Timestamps
val createdAt: Long,
val updatedAt: Long
) {
fun isExpired(currentTimeMs: Long? = null): Boolean
fun canBeVerified(currentTimeMs: Long? = null): Boolean
}VerificationProof
data class VerificationProof(
val id: String,
// References
val verificationRequestId: String,
val leaderboardEntryId: String,
// Witness
val witnessUserId: String,
val witnessDisplayName: String,
// Evidence
val imageUrls: List<String>, // Firebase Storage URLs
val notes: String,
// Result
val verificationLevel: VerificationLevel, // WITNESS_VERIFIED
val verifiedAt: Long,
val createdAt: Long
) {
fun hasImages(): Boolean
fun isValidWitness(scoreOwnerId: String): Boolean // Cannot self-verify
}Arrows Leaderboard
UserArrowStats
data class UserArrowStats(
val userId: String,
val displayName: String,
// Primary metrics
val totalArrowsShot: Int,
val roundsCompleted: Int,
// Cumulative scoring
val totalScore: Int,
val totalXCount: Int,
val totalTenCount: Int,
val averagePerArrow: Double,
val xPercentage: Double,
// Time period filtering
val weekKey: String,
val monthKey: String,
val seasonKey: String,
val weeklyArrows: Int,
val monthlyArrows: Int,
val seasonalArrows: Int,
// Streak tracking
val currentStreak: Int, // Consecutive days shooting
val longestStreak: Int,
val lastShotDate: String // ISO date
) {
val milestone: ArrowMilestone? // Achievement badge
val formattedTotalArrows: String // "1.2K", "10K+", "1M+"
}Milestone Badges
| Milestone | Threshold | Icon |
|---|---|---|
| First Arrow | 1 | arrow.right |
| Getting Started | 100 | target |
| Hundred Club | 500 | flame.fill |
| Consistent Archer | 1,000 | star.fill |
| Dedicated | 2,500 | medal.fill |
| Committed | 5,000 | trophy.fill |
| True Archer | 10,000 | crown.fill |
| Master Archer | 25,000 | sparkles |
| Legendary | 50,000 | bolt.fill |
| Elite | 100,000 | diamond.fill |
Firebase Integration
Firestore Collections
| Collection | Purpose | Key Fields |
|---|---|---|
verificationRequests | Pending verification requests | token, requesterId, status |
verificationProofs | Immutable verification evidence | witnessUserId, imageUrls |
user_arrow_stats | User arrow aggregates | totalArrowsShot, weeklyArrows |
Composite Indexes (8 new)
verificationRequests:
- token ASC (token lookup)
- requesterId ASC, createdAt DESC (user's requests)
- status ASC, expiresAt ASC (pending requests)
user_arrow_stats:
- totalArrowsShot DESC (all-time leaderboard)
- weeklyArrows DESC, weekKey ASC (weekly leaderboard)
- monthlyArrows DESC, monthKey ASC (monthly leaderboard)
- seasonalArrows DESC, seasonKey ASC (seasonal leaderboard)
Security Rules
// Verification Requests
match /verificationRequests/{requestId} {
// Anyone authenticated can read (for token lookup)
allow read: if request.auth != null;
// Only requester can create
allow create: if request.auth.uid == request.resource.data.requesterId;
// Owner can cancel, witness can complete
allow update: if request.auth != null &&
((request.auth.uid == resource.data.requesterId &&
request.resource.data.status == 'CANCELLED') ||
(request.auth.uid != resource.data.requesterId &&
request.resource.data.status == 'COMPLETED'));
// No deletion (audit trail)
allow delete: if false;
}
// Verification Proofs (immutable)
match /verificationProofs/{proofId} {
allow read: if request.auth != null;
allow create: if request.auth.uid == request.resource.data.witnessUserId;
allow update, delete: if false; // Immutable evidence
}
// User Arrow Stats
match /user_arrow_stats/{userId} {
allow read: if request.auth != null;
allow write: if request.auth.uid == userId;
}Storage Rules (Verification Images)
match /verification_images/{userId}/{imageId} {
// Only the witness can upload
allow write: if request.auth != null &&
request.auth.uid == userId &&
request.resource.size < 10 * 1024 * 1024 && // 10MB max
request.resource.contentType.matches('image/.*');
// Anyone authenticated can read (for display)
allow read: if request.auth != null;
}Platform Implementation
Android Components
| Component | Purpose |
|---|---|
VerificationService | Orchestrates verification workflow |
ArrowStatsService | Arrow leaderboard queries |
RequestVerificationDialog | Generate/share verification links |
VerifyScoreDialog | Witness verification UI with photo |
VerificationBadge | Verification level indicator |
ArrowsLeaderboardScreen | Global arrows leaderboard UI |
LeaderboardBrowserScreen | Indoor/Outdoor grouped conditions |
VerificationDeepLinkScreen | Handle incoming deep links |
iOS Components
| Component | Purpose |
|---|---|
VerificationRequestView | Generate verification links |
VerifyScoreView | Witness verification UI |
ArrowsLeaderboardView | Global arrows leaderboard |
LeaderboardBrowserView | Condition browser |
| Deep link handler | Universal links support |
Deep Link Handling
Android (AndroidManifest.xml):
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="archeryapprentice" android:host="verify" />
<data android:scheme="https" android:host="archeryapprentice.com"
android:pathPrefix="/verify" />
</intent-filter>iOS (Info.plist):
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLSchemes</key>
<array>
<string>archeryapprentice</string>
</array>
</dict>
</array>Round Publishing Integration
When a round is published, arrow stats are automatically updated:
class RoundPublishingService {
suspend fun publishRound(round: Round): Result<PublishedRound> {
// ... publish round logic ...
// Update user's arrow stats
arrowStatsDataSource?.let { dataSource ->
val arrowCount = round.totalArrowCount
dataSource.incrementArrowCount(userId, arrowCount, round)
}
return Result.success(publishedRound)
}
}Testing
Test Coverage
| Component | Tests | Coverage |
|---|---|---|
| VerificationRequest | 12 | 100% |
| VerificationProof | 8 | 100% |
| VerificationTokenGenerator | 15 | 100% |
| VerificationService | 18 | 95% |
| ArrowStatsService | 10 | 92% |
| UserArrowStats | 8 | 100% |
| VerificationBadge | 6 | 100% |
| Total | 77 | - |
Key Test Cases
- Token generation uniqueness and format validation
- Deep link parsing for various URL formats
- Self-verification prevention
- Request expiry logic
- Witness completion flow
- Arrow stats aggregation
- Milestone badge calculation
Design Decisions
1. QR/Link Sharing (No Friend System)
Rather than building a complex friend system, verification uses shareable tokens:
- User generates link, shares via any messaging app
- Witness opens link, app handles deep link
- Simple, low-friction verification flow
2. Basic Image Capture Now, ML Later
Phase 3 includes optional image capture for witnesses:
- Simple photo attachment for evidence
- Future phases can add ML-based image recognition
- Upgrades IMAGE_RECOGNITION verification level
3. Immutable Audit Trail
Verification proofs cannot be modified or deleted:
- Maintains integrity of verification history
- Supports dispute resolution
- Firestore rules enforce immutability
4. Time-Period Aggregates for Arrows
Arrow stats stored with multiple time keys:
totalArrowsShotfor all-timeweeklyArrows,monthlyArrows,seasonalArrows- Enables time-filtered leaderboards without recalculation
Commits
feat: Add Phase 3 verification system infrastructure- Core models and servicesfeat: Add leaderboard navigation and deep link verification- Deep link handlingfeat: Add GlobalLeaderboardScreen with inline verify buttons- UI integrationfeat: Add iOS verification UI components- iOS viewsfeat: Add iOS deep link and Firebase Storage configuration- iOS configfeat: Add iOS verification system integration- iOS service layertest: Add unit tests for Phase 3 verification system- Initial testsfix: Address Copilot review comments- Review fixesfeat: Add global arrows leaderboard engagement feature- Arrows leaderboardfix: Address remaining Copilot review comments- Final fixestest: Add comprehensive coverage tests- Coverage improvement
Related Documentation
- verification-system-architecture - Full architecture details
- arrows-leaderboard-feature - Engagement feature documentation
- leaderboard-phase1-foundation - Phase 1 leaderboard foundation