chore: remove in-app review
This commit is contained in:
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
}
|
||||
Reference in New Issue
Block a user