cec4c3e427
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>
50 lines
1.8 KiB
Dart
50 lines
1.8 KiB
Dart
// lib/services/review_service.dart
|
|
import '../data/save_repository.dart';
|
|
import 'review_prompt_policy.dart';
|
|
|
|
/// A thin seam over the in_app_review plugin so unit tests never reach platform
|
|
/// channels. The release wiring uses [StoreReviewer] (see store_reviewer.dart),
|
|
/// which forwards to the real InAppReview instance.
|
|
abstract class Reviewer {
|
|
Future<bool> isAvailable();
|
|
Future<void> requestReview();
|
|
}
|
|
|
|
/// Decides and triggers the native "rate this app" sheet at most once, on a
|
|
/// genuine high point. Holds no plugin dependency — that lives behind
|
|
/// [Reviewer] — so the decision path is fully unit-testable.
|
|
class ReviewService {
|
|
ReviewService({
|
|
required SaveRepository save,
|
|
required Reviewer reviewer,
|
|
ReviewPromptPolicy policy = const ReviewPromptPolicy(),
|
|
}) : _save = save,
|
|
_reviewer = reviewer,
|
|
_policy = policy;
|
|
|
|
final SaveRepository _save;
|
|
final Reviewer _reviewer;
|
|
final ReviewPromptPolicy _policy;
|
|
|
|
/// Call right after a stage win has been recorded to progress. Asks for a
|
|
/// review if the policy allows and the store can show the sheet, then never
|
|
/// again. The one-shot flag is only burned once the sheet is actually
|
|
/// requested, so an unavailable store retries on a later win. Every failure
|
|
/// is swallowed — a review prompt must never break gameplay.
|
|
Future<void> maybeRequestAfterWin({required int stars}) async {
|
|
final allowed = _policy.shouldRequest(
|
|
alreadyRequested: _save.reviewRequested,
|
|
won: true,
|
|
stars: stars,
|
|
totalStagesWon: _save.stagesClearedCount,
|
|
);
|
|
if (!allowed) return;
|
|
try {
|
|
if (await _reviewer.isAvailable()) {
|
|
await _reviewer.requestReview();
|
|
await _save.markReviewRequested();
|
|
}
|
|
} catch (_) {/* never break gameplay over a review prompt */}
|
|
}
|
|
}
|