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
+11
View File
@@ -10,6 +10,8 @@ import '../services/audio_service.dart';
import '../services/consent_service.dart';
import '../services/iap_service.dart';
import '../services/music_service.dart';
import '../services/review_service.dart';
import '../services/store_reviewer.dart';
import 'ads_notifier.dart';
import 'endless_best_notifier.dart';
import 'music_notifier.dart';
@@ -126,6 +128,15 @@ final analyticsProvider = Provider<AnalyticsService>(
(ref) => AnalyticsService(DebugAnalyticsBackend()),
);
/// Asks for a store review at most once, after a genuine high point. Reads the
/// one-time flag and cleared-stage count live from [saveRepositoryProvider].
final reviewServiceProvider = Provider<ReviewService>(
(ref) => ReviewService(
save: ref.read(saveRepositoryProvider),
reviewer: StoreReviewer(),
),
);
/// The visual theme of whatever season is in play; fallback outside seasons
/// (home, endless). Pure model — UI converts via ThemeColors.
final activeThemeProvider = Provider<SeasonTheme>((ref) {