feat(leaderboard): opt-out support — server endpoint, client method, UI button

- Server: DELETE /api/leaderboard/opt-in sets leaderboard_opt_in=0,
  hiding the player without deleting their row (scores preserved for re-opt-in)
- SyncProvider trait: opt_out_leaderboard() default no-op method + blanket impl
- SolitaireServerClient: implements opt_out_leaderboard via DELETE request with JWT refresh
- Leaderboard UI: "Opt Out" button (dark red) alongside existing "Opt In" button
- Server integration test: opt-out hides, opt-in restores (round-trip verified)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
root
2026-04-27 02:01:20 +00:00
parent 15b9b5477b
commit 648c5c18d9
6 changed files with 205 additions and 21 deletions
+8
View File
@@ -46,6 +46,11 @@ pub trait SyncProvider: Send + Sync {
async fn opt_in_leaderboard(&self, _display_name: &str) -> Result<(), SyncError> {
Ok(())
}
/// Remove the authenticated player from the leaderboard.
/// No-op for backends that don't support leaderboards.
async fn opt_out_leaderboard(&self) -> Result<(), SyncError> {
Ok(())
}
}
/// Blanket impl so `Box<dyn SyncProvider + Send + Sync>` (returned by
@@ -76,6 +81,9 @@ impl SyncProvider for Box<dyn SyncProvider + Send + Sync> {
async fn opt_in_leaderboard(&self, display_name: &str) -> Result<(), SyncError> {
(**self).opt_in_leaderboard(display_name).await
}
async fn opt_out_leaderboard(&self) -> Result<(), SyncError> {
(**self).opt_out_leaderboard().await
}
}
pub mod stats;