feat(review): request a store review after a 3-star win, once

Adds an in-app review prompt gated by ReviewPromptPolicy: only after a
3-star stage win, once the player has cleared >=5 stages, at most once
ever (persisted reviewRequested flag). ReviewService swallows all
failures and only burns the one-shot when the store actually shows the
sheet, so an unavailable store retries on a later win. StoreReviewer
wraps in_app_review behind a Reviewer seam so unit tests skip platform
channels. 13 new tests; full suite 194 green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-18 11:13:55 +09:00
parent 395e4a189b
commit cec4c3e427
11 changed files with 422 additions and 0 deletions
+18
View File
@@ -48,6 +48,9 @@ class SaveRepository {
_musicEnabled =
(json['flags'] as Map<String, dynamic>?)?['musicEnabled'] as bool? ??
true;
_reviewRequested = (json['flags']
as Map<String, dynamic>?)?['reviewRequested'] as bool? ??
false;
}
}
@@ -64,6 +67,7 @@ class SaveRepository {
bool _adsRemoved = false;
bool _soundEnabled = true;
bool _musicEnabled = true;
bool _reviewRequested = false;
StreakState get streak => _streak;
bool get tutorialDone => _tutorialDone;
@@ -72,6 +76,14 @@ class SaveRepository {
bool get soundEnabled => _soundEnabled;
bool get musicEnabled => _musicEnabled;
/// Whether we've already asked the player for a store review (one-time).
bool get reviewRequested => _reviewRequested;
/// Distinct stages the player has cleared (any season, >=1 star). Used to
/// gate the review prompt behind a player who is clearly invested.
int get stagesClearedCount =>
_progress.values.where((p) => p.stars > 0).length;
Future<void> markTutorialDone() {
_tutorialDone = true;
return _flush();
@@ -92,6 +104,11 @@ class SaveRepository {
return _flush();
}
Future<void> markReviewRequested() {
_reviewRequested = true;
return _flush();
}
Future<void> recordEndlessScore(int score) {
if (score > _endlessBest) _endlessBest = score;
return _flush();
@@ -165,6 +182,7 @@ class SaveRepository {
'adsRemoved': _adsRemoved,
'soundEnabled': _soundEnabled,
'musicEnabled': _musicEnabled,
'reviewRequested': _reviewRequested,
},
'endless': {'best': _endlessBest},
}),