Phase 2: Published Practice Rounds

Date: December 28, 2025 PRs: #410 (main implementation), #422 (deferred cleanup) Status: Complete

Overview

Phase 2 enables users to publish their completed practice rounds to the global leaderboard. This extends the leaderboard system (established in Phase 1 for tournaments) to include individual practice sessions.

Key Features

Round Publishing

  • Users can publish completed (non-tournament) rounds to the global leaderboard
  • Eligibility checks prevent publishing incomplete or tournament rounds
  • “Published” indicator displays on rounds after publishing

Visibility Options

Three visibility levels give users control over their data:

  • Public - Visible to all users, appears on global leaderboard
  • Authenticated Only - Visible only to signed-in users
  • Private - Visible only to the owner

My Published Rounds

  • Dedicated screen accessible from Settings
  • View all previously published rounds
  • Unpublish rounds to remove from leaderboard

Technical Implementation

Data Layer

New Domain Models:

  • PublishedRoundStatus enum - Tracks publishing state
  • PublishedRound - Domain model for published round data

New Room Entity:

  • PublishedRoundReference - Local tracking of published rounds
  • Database version bumped to 39

Firebase Integration

FirebasePublishingDataSource:

  • Atomic batch writes to ensure consistency
  • Writes to both publishedRounds and leaderboards collections simultaneously
  • Composite index for efficient user queries

Firestore Collections:

publishedRounds/
  {publishedRoundId}/
    userId: string
    localRoundId: number
    score: number
    arrowCount: number
    visibility: "PUBLIC" | "AUTHENTICATED_ONLY" | "PRIVATE"
    status: "PENDING" | "PUBLISHED" | "REJECTED" | "WITHDRAWN"
    verificationLevel: "SELF_REPORTED" | "IMAGE_RECOGNITION" | "WITNESS_VERIFIED" | "TOURNAMENT"
    ...

leaderboards/
  {entryId}/
    userId: string
    score: number
    conditionKey: string
    sourceType: "TOURNAMENT" | "PRACTICE_PUBLISHED"
    visibility: string
    ...

Android Implementation

Services:

  • RoundPublishingService - Handles eligibility checks and publishing logic

UI Components:

  • PublishRoundDialog - Visibility selector dialog
  • MyPublishedRoundsScreen - List of published rounds with unpublish action
  • Publish button in RoundDetailsScreen

Navigation:

  • Settings → My Published Rounds

iOS Implementation

Services:

  • RoundPublishingRepositoryBridge - Swift bridge to Firebase

UI Components:

  • PublishRoundSheet - SwiftUI sheet for visibility selection
  • MyPublishedRoundsView - List of published rounds
  • Publish button in RoundDetailView

Navigation:

  • Settings → My Published Rounds

Phase 2 Deferred Cleanup (PR #422)

Firestore Security Rules

Added create validation rules:

  • Type validation: score is int, arrowCount is int
  • Enum validation for visibility, status, verificationLevel, sourceType

New Helper Functions:

function isValidVisibility(visibility) {
  return visibility in ['PUBLIC', 'AUTHENTICATED_ONLY', 'PRIVATE'];
}
 
function isValidPublishedRoundStatus(status) {
  return status in ['PENDING', 'PUBLISHED', 'REJECTED', 'WITHDRAWN'];
}
 
function isValidVerificationLevel(level) {
  return level in ['SELF_REPORTED', 'IMAGE_RECOGNITION', 'WITNESS_VERIFIED', 'TOURNAMENT'];
}
 
function isValidScoreSourceType(sourceType) {
  return sourceType in ['TOURNAMENT', 'PRACTICE_PUBLISHED'];
}

Batch Operation Cleanup

Refactored FirebasePublishingDataSource.publishRound():

  • Create document refs first to get IDs upfront
  • Build complete documents with all IDs included
  • Single batch.set per document (removed redundant batch.update calls)

Security Rules Test Suite

  • 26 test cases for Firestore security rules
  • Uses @firebase/rules-unit-testing package
  • Covers create validation, read access, admin operations
  • See Firestore Rules Testing Guide

Testing Performed

  • Complete a practice round on Android
  • Publish with Public visibility → verify in Firebase Console
  • Check “Published” indicator shows after publishing
  • Navigate to Settings → My Published Rounds
  • Unpublish → verify removed from leaderboards collection
  • Repeat on iOS
  • Verify tournament rounds do NOT show publish button
  • Verify incomplete rounds do NOT show publish button

Lessons Learned

  1. Atomic Batch Writes: Using Firebase batch operations ensures both publishedRounds and leaderboards documents are created atomically, preventing orphaned records.

  2. Enum Validation at Rules Level: Moving enum validation to Firestore security rules provides server-side enforcement, not just client-side validation.

  3. Visibility Model: The three-tier visibility model (Public, Authenticated Only, Private) provides good granularity for user privacy preferences.