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:

  1. Verification System: Allows users to have their published scores verified by witnesses via shareable tokens/links
  2. 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

LevelTrust ScoreHow Achieved
SELF_REPORTED1Default when publishing
IMAGE_RECOGNITION2Camera scoring (future ML)
WITNESS_VERIFIED3Another user verifies via token
TOURNAMENT4Official 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

MilestoneThresholdIcon
First Arrow1arrow.right
Getting Started100target
Hundred Club500flame.fill
Consistent Archer1,000star.fill
Dedicated2,500medal.fill
Committed5,000trophy.fill
True Archer10,000crown.fill
Master Archer25,000sparkles
Legendary50,000bolt.fill
Elite100,000diamond.fill

Firebase Integration

Firestore Collections

CollectionPurposeKey Fields
verificationRequestsPending verification requeststoken, requesterId, status
verificationProofsImmutable verification evidencewitnessUserId, imageUrls
user_arrow_statsUser arrow aggregatestotalArrowsShot, 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

ComponentPurpose
VerificationServiceOrchestrates verification workflow
ArrowStatsServiceArrow leaderboard queries
RequestVerificationDialogGenerate/share verification links
VerifyScoreDialogWitness verification UI with photo
VerificationBadgeVerification level indicator
ArrowsLeaderboardScreenGlobal arrows leaderboard UI
LeaderboardBrowserScreenIndoor/Outdoor grouped conditions
VerificationDeepLinkScreenHandle incoming deep links

iOS Components

ComponentPurpose
VerificationRequestViewGenerate verification links
VerifyScoreViewWitness verification UI
ArrowsLeaderboardViewGlobal arrows leaderboard
LeaderboardBrowserViewCondition browser
Deep link handlerUniversal links support

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

ComponentTestsCoverage
VerificationRequest12100%
VerificationProof8100%
VerificationTokenGenerator15100%
VerificationService1895%
ArrowStatsService1092%
UserArrowStats8100%
VerificationBadge6100%
Total77-

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

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:

  • totalArrowsShot for all-time
  • weeklyArrows, monthlyArrows, seasonalArrows
  • Enables time-filtered leaderboards without recalculation

Commits

  1. feat: Add Phase 3 verification system infrastructure - Core models and services
  2. feat: Add leaderboard navigation and deep link verification - Deep link handling
  3. feat: Add GlobalLeaderboardScreen with inline verify buttons - UI integration
  4. feat: Add iOS verification UI components - iOS views
  5. feat: Add iOS deep link and Firebase Storage configuration - iOS config
  6. feat: Add iOS verification system integration - iOS service layer
  7. test: Add unit tests for Phase 3 verification system - Initial tests
  8. fix: Address Copilot review comments - Review fixes
  9. feat: Add global arrows leaderboard engagement feature - Arrows leaderboard
  10. fix: Address remaining Copilot review comments - Final fixes
  11. test: Add comprehensive coverage tests - Coverage improvement