chore: remove in-app review

This commit is contained in:
edde746
2026-05-09 11:47:37 +02:00
parent afda61119e
commit 3a612df3d3
10 changed files with 3 additions and 278 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ platform :android do
git_commit = `git rev-parse --short HEAD`.strip
# Build the Flutter app
sh("cd #{ENV['PWD']}/.. && flutter build appbundle --dart-define=ENABLE_IN_APP_REVIEW=true --dart-define=ENABLE_SENTRY=true --dart-define=GIT_COMMIT=#{git_commit} --dart-define=SENTRY_ENVIRONMENT=play-store --obfuscate --split-debug-info=debug-info/android-aab")
sh("cd #{ENV['PWD']}/.. && flutter build appbundle --dart-define=ENABLE_SENTRY=true --dart-define=GIT_COMMIT=#{git_commit} --dart-define=SENTRY_ENVIRONMENT=play-store --obfuscate --split-debug-info=debug-info/android-aab")
sh("cd #{ENV['PWD']}/.. && ./scripts/upload-symbols.sh android-aab")
upload_to_play_store(
-6
View File
@@ -11,8 +11,6 @@ PODS:
- file_picker (0.0.1):
- Flutter
- Flutter (1.0.0)
- in_app_review (2.0.0):
- Flutter
- os_media_controls (0.0.1):
- Flutter
- package_info_plus (0.4.5):
@@ -64,7 +62,6 @@ DEPENDENCIES:
- device_info_plus (from `.symlinks/plugins/device_info_plus/ios`)
- file_picker (from `.symlinks/plugins/file_picker/ios`)
- Flutter (from `Flutter`)
- in_app_review (from `.symlinks/plugins/in_app_review/ios`)
- os_media_controls (from `.symlinks/plugins/os_media_controls/ios`)
- package_info_plus (from `.symlinks/plugins/package_info_plus/ios`)
- sentry_flutter (from `.symlinks/plugins/sentry_flutter/ios`)
@@ -92,8 +89,6 @@ EXTERNAL SOURCES:
:path: ".symlinks/plugins/file_picker/ios"
Flutter:
:path: Flutter
in_app_review:
:path: ".symlinks/plugins/in_app_review/ios"
os_media_controls:
:path: ".symlinks/plugins/os_media_controls/ios"
package_info_plus:
@@ -118,7 +113,6 @@ SPEC CHECKSUMS:
device_info_plus: 21fcca2080fbcd348be798aa36c3e5ed849eefbe
file_picker: 8fc6fe5e42585a217d44d22f79ec046cb8d81140
Flutter: cabc95a1d2626b1b06e7179b784ebcf0c0cde467
in_app_review: 7dd1ea365263f834b8464673f9df72c80c17c937
os_media_controls: 94cc278f5802b82b2d6373003aeb511f96718b27
package_info_plus: af8e2ca6888548050f16fa2f1938db7b5a5df499
Sentry: d587a8fe91ca13503ecd69a1905f3e8a0fcf61be
+1 -1
View File
@@ -9,7 +9,7 @@ platform :ios do
git_commit = `git rev-parse --short HEAD`.strip
# Build the Flutter app
sh("cd #{ENV['PWD']}/.. && flutter build ipa --dart-define=ENABLE_IN_APP_REVIEW=true --dart-define=ENABLE_SENTRY=true --dart-define=GIT_COMMIT=#{git_commit} --dart-define=SENTRY_ENVIRONMENT=app-store --split-debug-info=debug-info/ios")
sh("cd #{ENV['PWD']}/.. && flutter build ipa --dart-define=ENABLE_SENTRY=true --dart-define=GIT_COMMIT=#{git_commit} --dart-define=SENTRY_ENVIRONMENT=app-store --split-debug-info=debug-info/ios")
# Upload debug symbols
sh("cd #{ENV['PWD']}/.. && ./scripts/upload-symbols.sh ios")
+1 -7
View File
@@ -56,7 +56,6 @@ import 'watch_together/providers/watch_together_provider.dart';
import 'services/multi_server_manager.dart';
import 'services/offline_watch_sync_service.dart';
import 'services/data_aggregation_service.dart';
import 'services/in_app_review_service.dart';
import 'services/server_registry.dart';
import 'services/download_manager_service.dart';
import 'services/pip_service.dart';
@@ -469,9 +468,6 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
return AppExitResponse.exit;
},
);
// Start in-app review session tracking
InAppReviewService.instance.startSession();
}
@override
@@ -584,10 +580,9 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
void didChangeAppLifecycleState(AppLifecycleState state) {
switch (state) {
case AppLifecycleState.resumed:
// App came back to foreground - trigger sync check and start new session
// App came back to foreground - trigger sync check
_offlineWatchSyncService.onAppResumed();
TraktSyncService.instance.flushQueue();
InAppReviewService.instance.startSession();
// Re-probe servers — mobile OS may have dropped TCP connections during doze/sleep.
// On desktop, resumed fires on every window focus (alt-tab), so apply a cooldown
// to avoid piling up network probes from rapid alt-tabbing.
@@ -610,7 +605,6 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
// Closing here would kill the Drift isolate channel while services
// (sync, downloads, cache) still hold references to the executor.
// SQLite WAL mode handles process death; desktop uses onExitRequested.
InAppReviewService.instance.endSession();
if (PlatformDetector.isDesktopOS()) {
if (ProcessInfo.currentRss > 1024 * 1024 * 1024) {
// 1GB
-124
View File
@@ -1,124 +0,0 @@
import 'dart:async';
import 'dart:io' show Platform;
import 'package:in_app_review/in_app_review.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/app_logger.dart';
import 'base_shared_preferences_service.dart';
/// Service to manage in-app review prompts
/// Only enabled when ENABLE_IN_APP_REVIEW build flag is set
class InAppReviewService {
static final InAppReviewService _instance = InAppReviewService._();
static InAppReviewService get instance => _instance;
InAppReviewService._();
final InAppReview _inAppReview = InAppReview.instance;
Future<SharedPreferencesWithCache> _getPrefs() => BaseSharedPreferencesService.sharedCache();
static const String _keyQualifyingSessionsCount = 'review_qualifying_sessions_count';
static const String _keyLastPromptTime = 'review_last_prompt_time';
// Configuration
static const int _requiredSessions = 6;
static const Duration _minimumSessionDuration = Duration(minutes: 5);
static const Duration _promptCooldown = Duration(days: 60);
DateTime? _sessionStartTime;
static bool get isEnabled {
if (!Platform.isIOS && !Platform.isAndroid) {
return false;
}
return const bool.fromEnvironment('ENABLE_IN_APP_REVIEW', defaultValue: false);
}
void startSession() {
if (!isEnabled) return;
_sessionStartTime = DateTime.now();
appLogger.d('In-app review: Session started');
// Prompt checks should run while the app is in the foreground.
unawaited(maybeRequestReview());
}
/// End the current session and check if it qualifies
/// Call this when app goes to background or is closed
Future<void> endSession() async {
if (!isEnabled || _sessionStartTime == null) return;
final sessionDuration = DateTime.now().difference(_sessionStartTime!);
_sessionStartTime = null;
if (sessionDuration >= _minimumSessionDuration) {
await _incrementQualifyingSessions();
appLogger.d('In-app review: Qualifying session ended (${sessionDuration.inMinutes} minutes)');
} else {
appLogger.d('In-app review: Session too short (${sessionDuration.inMinutes} minutes)');
}
}
Future<void> _incrementQualifyingSessions() async {
final prefs = await _getPrefs();
final currentCount = prefs.getInt(_keyQualifyingSessionsCount) ?? 0;
await prefs.setInt(_keyQualifyingSessionsCount, currentCount + 1);
}
Future<int> _getQualifyingSessionsCount() async {
final prefs = await _getPrefs();
return prefs.getInt(_keyQualifyingSessionsCount) ?? 0;
}
/// Check if we should request a review based on session count and cooldown
Future<bool> _shouldRequestReview() async {
final prefs = await _getPrefs();
final sessionCount = await _getQualifyingSessionsCount();
if (sessionCount < _requiredSessions) {
appLogger.d('In-app review: Not enough sessions ($sessionCount/$_requiredSessions)');
return false;
}
final lastPromptString = prefs.getString(_keyLastPromptTime);
if (lastPromptString != null) {
final lastPrompt = DateTime.parse(lastPromptString);
final timeSinceLastPrompt = DateTime.now().difference(lastPrompt);
if (timeSinceLastPrompt < _promptCooldown) {
final daysRemaining = (_promptCooldown - timeSinceLastPrompt).inDays;
appLogger.d('In-app review: Cooldown active ($daysRemaining days remaining)');
return false;
}
}
return true;
}
Future<void> maybeRequestReview() async {
if (!isEnabled) return;
final shouldRequest = await _shouldRequestReview();
if (!shouldRequest) return;
try {
final isAvailable = await _inAppReview.isAvailable();
if (!isAvailable) {
appLogger.d('In-app review: Not available on this device');
return;
}
await _inAppReview.requestReview();
appLogger.i('In-app review: Review prompt shown');
await _recordPromptShown();
} catch (e) {
appLogger.e('In-app review: Error requesting review', error: e);
}
}
Future<void> _recordPromptShown() async {
final prefs = await _getPrefs();
await prefs.setString(_keyLastPromptTime, DateTime.now().toIso8601String());
// Reset session count so user needs to use app more before next prompt
await prefs.setInt(_keyQualifyingSessionsCount, 0);
}
}
@@ -9,7 +9,6 @@ import auto_updater_macos
import connectivity_plus
import device_info_plus
import file_picker
import in_app_review
import os_media_controls
import package_info_plus
import screen_retriever_macos
@@ -26,7 +25,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
ConnectivityPlusPlugin.register(with: registry.registrar(forPlugin: "ConnectivityPlusPlugin"))
DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin"))
FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin"))
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
OsMediaControlsPlugin.register(with: registry.registrar(forPlugin: "OsMediaControlsPlugin"))
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin"))
-6
View File
@@ -12,8 +12,6 @@ PODS:
- file_picker (0.0.1):
- FlutterMacOS
- FlutterMacOS (1.0.0)
- in_app_review (2.0.0):
- FlutterMacOS
- os_media_controls (0.0.1):
- FlutterMacOS
- package_info_plus (0.0.1):
@@ -70,7 +68,6 @@ DEPENDENCIES:
- device_info_plus (from `Flutter/ephemeral/.symlinks/plugins/device_info_plus/macos`)
- file_picker (from `Flutter/ephemeral/.symlinks/plugins/file_picker/macos`)
- FlutterMacOS (from `Flutter/ephemeral`)
- in_app_review (from `Flutter/ephemeral/.symlinks/plugins/in_app_review/macos`)
- os_media_controls (from `Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos`)
- package_info_plus (from `Flutter/ephemeral/.symlinks/plugins/package_info_plus/macos`)
- screen_retriever_macos (from `Flutter/ephemeral/.symlinks/plugins/screen_retriever_macos/macos`)
@@ -101,8 +98,6 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/file_picker/macos
FlutterMacOS:
:path: Flutter/ephemeral
in_app_review:
:path: Flutter/ephemeral/.symlinks/plugins/in_app_review/macos
os_media_controls:
:path: Flutter/ephemeral/.symlinks/plugins/os_media_controls/macos
package_info_plus:
@@ -131,7 +126,6 @@ SPEC CHECKSUMS:
device_info_plus: 4fb280989f669696856f8b129e4a5e3cd6c48f76
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
in_app_review: 66e7680752b632d83f4f0e88b34d52ed303fbff4
os_media_controls: c07c04c4afdf59dda0a3f398457a46823c4ce0ed
package_info_plus: f0052d280d17aa382b932f399edf32507174e870
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f
-16
View File
@@ -554,22 +554,6 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.1.0"
in_app_review:
dependency: "direct main"
description:
name: in_app_review
sha256: ab26ac54dbd802896af78c670b265eaeab7ecddd6af4d0751e9604b60574817f
url: "https://pub.dev"
source: hosted
version: "2.0.11"
in_app_review_platform_interface:
dependency: transitive
description:
name: in_app_review_platform_interface
sha256: fed2c755f2125caa9ae10495a3c163aa7fab5af3585a9c62ef4a6920c5b45f10
url: "https://pub.dev"
source: hosted
version: "2.0.5"
intl:
dependency: "direct main"
description:
-1
View File
@@ -50,7 +50,6 @@ dependencies:
saf_stream: ^2.0.0
material_symbols_icons: ^4.2906.0
web_socket_channel: ^3.0.1
in_app_review: ^2.0.11
dart_discord_presence: ^1.2.0
flutter_svg: ^2.2.3
background_downloader:
@@ -1,114 +0,0 @@
import 'dart:io' show Platform;
import 'package:flutter_test/flutter_test.dart';
import 'package:plezy/services/base_shared_preferences_service.dart';
import 'package:plezy/services/in_app_review_service.dart';
import '../test_helpers/prefs.dart';
void main() {
setUp(resetSharedPreferencesForTest);
// Keys read directly from the underlying SharedPreferences to bypass the
// platform-channel `InAppReview.requestReview()` and assert state.
const keyQualifyingSessionsCount = 'review_qualifying_sessions_count';
const keyLastPromptTime = 'review_last_prompt_time';
// ============================================================
// Singleton + isEnabled gate
// ============================================================
group('singleton & isEnabled', () {
test('instance returns the same singleton', () {
final a = InAppReviewService.instance;
final b = InAppReviewService.instance;
expect(identical(a, b), isTrue);
});
test('isEnabled is false on desktop test platforms (no ENABLE_IN_APP_REVIEW)', () {
// Test platform on macOS/Linux/Windows is desktop, so the platform gate
// alone forces isEnabled=false regardless of the build flag.
if (!Platform.isIOS && !Platform.isAndroid) {
expect(InAppReviewService.isEnabled, isFalse);
}
});
});
// ============================================================
// Session tracking — these methods are no-ops when isEnabled=false
// ============================================================
group('session tracking when disabled (test environment)', () {
test('startSession does not write any prefs when isEnabled=false', () async {
InAppReviewService.instance.startSession();
// No qualifying-session counter set yet.
final prefs = await BaseSharedPreferencesService.sharedCache();
expect(prefs.getInt(keyQualifyingSessionsCount), isNull);
});
test('endSession is a no-op when no session was started AND isEnabled=false', () async {
// Call without preceding startSession — should not throw or mutate prefs.
await InAppReviewService.instance.endSession();
final prefs = await BaseSharedPreferencesService.sharedCache();
expect(prefs.getInt(keyQualifyingSessionsCount), isNull);
});
test('maybeRequestReview is a no-op when isEnabled=false', () async {
// Pre-set what would normally trigger a prompt.
final prefs = await BaseSharedPreferencesService.sharedCache();
await prefs.setInt(keyQualifyingSessionsCount, 100);
await InAppReviewService.instance.maybeRequestReview();
// Counter is unchanged because the early-return short-circuits the path
// that would reset it after a successful prompt.
expect(prefs.getInt(keyQualifyingSessionsCount), 100);
expect(prefs.getString(keyLastPromptTime), isNull);
});
});
// ============================================================
// Pref persistence — ensures the keys/format the service reads/writes
// are the same shape its private logic expects, so we verify the
// gating math separately by writing the prefs directly.
// ============================================================
group('pref shape (sanity for gating logic)', () {
test('qualifying sessions counter is int-typed under the documented key', () async {
final prefs = await BaseSharedPreferencesService.sharedCache();
await prefs.setInt(keyQualifyingSessionsCount, 3);
expect(prefs.getInt(keyQualifyingSessionsCount), 3);
});
test('last prompt timestamp is an ISO 8601 string under the documented key', () async {
final prefs = await BaseSharedPreferencesService.sharedCache();
final now = DateTime.utc(2026, 1, 1, 12, 0, 0).toIso8601String();
await prefs.setString(keyLastPromptTime, now);
// Must round-trip through DateTime.parse (matches service's internal use).
final parsed = DateTime.parse(prefs.getString(keyLastPromptTime)!);
expect(parsed.toIso8601String(), now);
});
});
// ============================================================
// What's NOT covered (and why)
// ============================================================
// Because [InAppReviewService] is a singleton with no `@visibleForTesting`
// override hooks for either:
// - the `Platform.isIOS / isAndroid` gate, or
// - the `bool.fromEnvironment('ENABLE_IN_APP_REVIEW')` flag,
// we cannot directly exercise the gating math (`_shouldRequestReview`,
// `_incrementQualifyingSessions`) without modifying the service. Per the
// task brief, we do NOT add @visibleForTesting hooks just for tests.
//
// Behavior that requires `isEnabled == true` and is therefore unverified
// here:
// - endSession increments the counter only when sessionDuration ≥ 5 min
// - maybeRequestReview returns false when sessionCount < required (6)
// - maybeRequestReview returns false during the 60-day cooldown window
// - _recordPromptShown writes timestamp + resets the counter
//
// The pref-shape tests above pin the on-disk schema the service depends on,
// so if those keys/types change the production code will fail loudly.
}