feat: prompt for reviews
This commit is contained in:
@@ -32,13 +32,13 @@ platform :android do
|
||||
desc "Build release AAB"
|
||||
lane :build do
|
||||
# Navigate to Flutter project root and build
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build appbundle")
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build appbundle --dart-define=ENABLE_IN_APP_REVIEW=true")
|
||||
end
|
||||
|
||||
desc "Build and deploy to Google Play Store"
|
||||
lane :release do
|
||||
# Build the Flutter app
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build appbundle")
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build appbundle --dart-define=ENABLE_IN_APP_REVIEW=true")
|
||||
|
||||
# Get version code from pubspec.yaml
|
||||
pubspec_path = "#{ENV['PWD']}/../pubspec.yaml"
|
||||
|
||||
@@ -19,13 +19,13 @@ platform :ios do
|
||||
desc "Build release IPA"
|
||||
lane :build do
|
||||
# Navigate to Flutter project root and build
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build ipa")
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build ipa --dart-define=ENABLE_IN_APP_REVIEW=true")
|
||||
end
|
||||
|
||||
desc "Build and deploy to TestFlight"
|
||||
lane :deploy_testflight do
|
||||
# Build the Flutter app
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build ipa")
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build ipa --dart-define=ENABLE_IN_APP_REVIEW=true")
|
||||
|
||||
# Upload to TestFlight
|
||||
upload_to_testflight(
|
||||
@@ -36,7 +36,7 @@ platform :ios do
|
||||
desc "Build and deploy to App Store"
|
||||
lane :deploy_appstore do
|
||||
# Build the Flutter app
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build ipa")
|
||||
sh("cd #{ENV['PWD']}/.. && flutter build ipa --dart-define=ENABLE_IN_APP_REVIEW=true")
|
||||
|
||||
# Upload to App Store
|
||||
upload_to_app_store(
|
||||
|
||||
+17
-3
@@ -27,6 +27,7 @@ import 'watch_together/watch_together.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/download_storage_service.dart';
|
||||
@@ -129,6 +130,9 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
_downloadManager = DownloadManagerService(database: _appDatabase, storageService: DownloadStorageService.instance);
|
||||
|
||||
_offlineWatchSyncService = OfflineWatchSyncService(database: _appDatabase, serverManager: _serverManager);
|
||||
|
||||
// Start in-app review session tracking
|
||||
InAppReviewService.instance.startSession();
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -139,9 +143,19 @@ class _MainAppState extends State<MainApp> with WidgetsBindingObserver {
|
||||
|
||||
@override
|
||||
void didChangeAppLifecycleState(AppLifecycleState state) {
|
||||
if (state == AppLifecycleState.resumed) {
|
||||
// App came back to foreground - trigger sync check
|
||||
_offlineWatchSyncService.onAppResumed();
|
||||
switch (state) {
|
||||
case AppLifecycleState.resumed:
|
||||
// App came back to foreground - trigger sync check and start new session
|
||||
_offlineWatchSyncService.onAppResumed();
|
||||
InAppReviewService.instance.startSession();
|
||||
case AppLifecycleState.paused:
|
||||
case AppLifecycleState.detached:
|
||||
// App went to background or is closing - end session
|
||||
InAppReviewService.instance.endSession();
|
||||
case AppLifecycleState.inactive:
|
||||
case AppLifecycleState.hidden:
|
||||
// Transitional states - don't trigger session events
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
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';
|
||||
|
||||
/// 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;
|
||||
|
||||
// SharedPreferences keys
|
||||
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);
|
||||
|
||||
// Session tracking
|
||||
DateTime? _sessionStartTime;
|
||||
|
||||
/// Check if in-app review is enabled via build flag
|
||||
/// Only enabled on mobile platforms (iOS and Android)
|
||||
static bool get isEnabled {
|
||||
if (!Platform.isIOS && !Platform.isAndroid) {
|
||||
return false;
|
||||
}
|
||||
const enabled = bool.fromEnvironment('ENABLE_IN_APP_REVIEW', defaultValue: false);
|
||||
return enabled;
|
||||
}
|
||||
|
||||
/// Start tracking a new session
|
||||
void startSession() {
|
||||
if (!isEnabled) return;
|
||||
_sessionStartTime = DateTime.now();
|
||||
appLogger.d('In-app review: Session started');
|
||||
}
|
||||
|
||||
/// 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)');
|
||||
await maybeRequestReview();
|
||||
} else {
|
||||
appLogger.d('In-app review: Session too short (${sessionDuration.inMinutes} minutes)');
|
||||
}
|
||||
}
|
||||
|
||||
/// Increment the qualifying sessions counter
|
||||
Future<void> _incrementQualifyingSessions() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final currentCount = prefs.getInt(_keyQualifyingSessionsCount) ?? 0;
|
||||
await prefs.setInt(_keyQualifyingSessionsCount, currentCount + 1);
|
||||
}
|
||||
|
||||
/// Get the current qualifying sessions count
|
||||
Future<int> _getQualifyingSessionsCount() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
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 SharedPreferences.getInstance();
|
||||
|
||||
// Check session count
|
||||
final sessionCount = await _getQualifyingSessionsCount();
|
||||
if (sessionCount < _requiredSessions) {
|
||||
appLogger.d('In-app review: Not enough sessions ($sessionCount/$_requiredSessions)');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check cooldown
|
||||
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;
|
||||
}
|
||||
|
||||
/// Request a review if conditions are met
|
||||
Future<void> maybeRequestReview() async {
|
||||
if (!isEnabled) return;
|
||||
|
||||
final shouldRequest = await _shouldRequestReview();
|
||||
if (!shouldRequest) return;
|
||||
|
||||
try {
|
||||
// Check if in-app review is available on this device
|
||||
final isAvailable = await _inAppReview.isAvailable();
|
||||
if (!isAvailable) {
|
||||
appLogger.d('In-app review: Not available on this device');
|
||||
return;
|
||||
}
|
||||
|
||||
// Request the review
|
||||
await _inAppReview.requestReview();
|
||||
appLogger.i('In-app review: Review prompt shown');
|
||||
|
||||
// Record that we showed the prompt and reset session count
|
||||
await _recordPromptShown();
|
||||
} catch (e) {
|
||||
appLogger.e('In-app review: Error requesting review', error: e);
|
||||
}
|
||||
}
|
||||
|
||||
/// Record that the review prompt was shown
|
||||
Future<void> _recordPromptShown() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
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);
|
||||
}
|
||||
|
||||
/// Get debug info about the current state (for development/testing)
|
||||
Future<Map<String, dynamic>> getDebugInfo() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final sessionCount = prefs.getInt(_keyQualifyingSessionsCount) ?? 0;
|
||||
final lastPromptString = prefs.getString(_keyLastPromptTime);
|
||||
final isAvailable = await _inAppReview.isAvailable();
|
||||
|
||||
return {
|
||||
'isEnabled': isEnabled,
|
||||
'isAvailable': isAvailable,
|
||||
'qualifyingSessions': sessionCount,
|
||||
'requiredSessions': _requiredSessions,
|
||||
'lastPromptTime': lastPromptString,
|
||||
'cooldownDays': _promptCooldown.inDays,
|
||||
'currentSessionStartTime': _sessionStartTime?.toIso8601String(),
|
||||
};
|
||||
}
|
||||
|
||||
/// Reset all stored data (for testing purposes)
|
||||
Future<void> reset() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
await prefs.remove(_keyQualifyingSessionsCount);
|
||||
await prefs.remove(_keyLastPromptTime);
|
||||
_sessionStartTime = null;
|
||||
appLogger.d('In-app review: State reset');
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import file_picker
|
||||
import flutter_webrtc
|
||||
import gamepads_darwin
|
||||
import hotkey_manager_macos
|
||||
import in_app_review
|
||||
import macos_window_utils
|
||||
import os_media_controls
|
||||
import package_info_plus
|
||||
@@ -31,6 +32,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
|
||||
FlutterWebRTCPlugin.register(with: registry.registrar(forPlugin: "FlutterWebRTCPlugin"))
|
||||
GamepadsDarwinPlugin.register(with: registry.registrar(forPlugin: "GamepadsDarwinPlugin"))
|
||||
HotkeyManagerMacosPlugin.register(with: registry.registrar(forPlugin: "HotkeyManagerMacosPlugin"))
|
||||
InAppReviewPlugin.register(with: registry.registrar(forPlugin: "InAppReviewPlugin"))
|
||||
MacOSWindowUtilsPlugin.register(with: registry.registrar(forPlugin: "MacOSWindowUtilsPlugin"))
|
||||
OsMediaControlsPlugin.register(with: registry.registrar(forPlugin: "OsMediaControlsPlugin"))
|
||||
FPPPackageInfoPlusPlugin.register(with: registry.registrar(forPlugin: "FPPPackageInfoPlusPlugin"))
|
||||
|
||||
@@ -47,6 +47,7 @@ dependencies:
|
||||
material_symbols_icons: ^4.2892.0
|
||||
peerdart: ^0.5.6
|
||||
share_plus: ^10.0.0
|
||||
in_app_review: ^2.0.11
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user