diff --git a/app/lib/pages/action_items/action_items_page.dart b/app/lib/pages/action_items/action_items_page.dart index 08624e4ecbb..4e6ac7d77b1 100644 --- a/app/lib/pages/action_items/action_items_page.dart +++ b/app/lib/pages/action_items/action_items_page.dart @@ -10,6 +10,7 @@ import 'package:omi/providers/action_items_provider.dart'; import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/services/apple_reminders_service.dart'; import 'package:omi/utils/platform/platform_service.dart'; +import 'package:omi/services/app_review_service.dart'; class ActionItemsPage extends StatefulWidget { const ActionItemsPage({super.key}); @@ -21,6 +22,7 @@ class ActionItemsPage extends StatefulWidget { class _ActionItemsPageState extends State with AutomaticKeepAliveClientMixin { final ScrollController _scrollController = ScrollController(); Set _exportedToAppleReminders = {}; + final AppReviewService _appReviewService = AppReviewService(); @override bool get wantKeepAlive => true; @@ -63,6 +65,19 @@ class _ActionItemsPageState extends State with AutomaticKeepAli } } + // checks if it's the first action item completed + Future _onActionItemCompleted() async { + final hasCompletedFirst = await _appReviewService.hasCompletedFirstActionItem(); + + if (!hasCompletedFirst) { + await _appReviewService.markFirstActionItemCompleted(); + + if (mounted) { + await _appReviewService.showReviewPromptIfNeeded(context); + } + } + } + void _onScroll() { final provider = Provider.of(context, listen: false); if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) { @@ -465,7 +480,12 @@ class _ActionItemsPageState extends State with AutomaticKeepAli }, child: ActionItemTileWidget( actionItem: item, - onToggle: (newState) => provider.updateActionItemState(item, newState), + onToggle: (newState) { + provider.updateActionItemState(item, newState); + if (newState) { + _onActionItemCompleted(); + } + }, exportedToAppleReminders: _exportedToAppleReminders, onExportedToAppleReminders: _checkExistingAppleReminders, ), diff --git a/app/lib/pages/conversation_detail/page.dart b/app/lib/pages/conversation_detail/page.dart index e4e9681620b..66b52bc44cb 100644 --- a/app/lib/pages/conversation_detail/page.dart +++ b/app/lib/pages/conversation_detail/page.dart @@ -13,6 +13,7 @@ import 'package:omi/pages/home/page.dart'; import 'package:omi/providers/connectivity_provider.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/people_provider.dart'; +import 'package:omi/services/app_review_service.dart'; import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/widgets/conversation_bottom_bar.dart'; @@ -755,6 +756,7 @@ class ActionItemDetailWidget extends StatefulWidget { class _ActionItemDetailWidgetState extends State { static final Map _pendingStates = {}; // Track pending states by description + final AppReviewService _appReviewService = AppReviewService(); @override void dispose() { @@ -888,6 +890,11 @@ class _ActionItemDetailWidgetState extends State { if (currentIndex != -1) { if (newValue) { MixpanelManager().checkedActionItem(provider.conversation, currentIndex); + + if (!await _appReviewService.hasCompletedFirstActionItem()) { + await _appReviewService.markFirstActionItemCompleted(); + _appReviewService.showReviewPromptIfNeeded(context); + } } else { MixpanelManager().uncheckedActionItem(provider.conversation, currentIndex); } diff --git a/app/lib/services/app_review_service.dart b/app/lib/services/app_review_service.dart new file mode 100644 index 00000000000..72713b0d272 --- /dev/null +++ b/app/lib/services/app_review_service.dart @@ -0,0 +1,173 @@ +import 'dart:io'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:font_awesome_flutter/font_awesome_flutter.dart'; +import 'package:in_app_review/in_app_review.dart'; +import 'package:omi/utils/analytics/mixpanel.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class AppReviewService { + static final AppReviewService _instance = AppReviewService._internal(); + factory AppReviewService() => _instance; + AppReviewService._internal(); + + final InAppReview _inAppReview = InAppReview.instance; + static const String _hasCompletedFirstActionItemKey = 'has_completed_first_action_item'; + static const String _hasShownReviewPromptKey = 'has_shown_review_prompt'; + + // Checks if the user has completed their first action item + Future hasCompletedFirstActionItem() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_hasCompletedFirstActionItemKey) ?? false; + } + + // Marks that the user has completed their first action item + Future markFirstActionItemCompleted() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_hasCompletedFirstActionItemKey, true); + } + + // Checks if the review prompt has already been shown + Future hasShownReviewPrompt() async { + final prefs = await SharedPreferences.getInstance(); + return prefs.getBool(_hasShownReviewPromptKey) ?? false; + } + + // Marks that the review prompt has been shown + Future markReviewPromptShown() async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setBool(_hasShownReviewPromptKey, true); + } + + // Shows the review prompt if conditions are met + Future showReviewPromptIfNeeded(BuildContext context) async { + final hasCompleted = await hasCompletedFirstActionItem(); + final hasShown = await hasShownReviewPrompt(); + + if (hasCompleted && !hasShown) { + await markReviewPromptShown(); + _showReviewDialog(context); + return true; + } + return false; + } + + // Shows a dialog asking the user to review the app + Future _showReviewDialog(BuildContext context) async { + HapticFeedback.mediumImpact(); + + return showDialog( + context: context, + barrierDismissible: false, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, + elevation: 0, + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(24), + decoration: BoxDecoration( + color: Colors.black, + borderRadius: BorderRadius.circular(24), + border: Border.all(color: Colors.grey.shade800, width: 1), + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Loving Omi?', + style: TextStyle( + color: Colors.white, + fontSize: 24, + fontWeight: FontWeight.bold, + height: 1.2, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + Text( + 'Help us reach more people by leaving a review in the ${Platform.isIOS ? 'App Store' : 'Google Play Store'}. Your feedback means the world to us!', + style: const TextStyle( + color: Colors.grey, + fontSize: 16, + height: 1.4, + ), + textAlign: TextAlign.center, + ), + const SizedBox(height: 32), + Column( + children: [ + ElevatedButton( + onPressed: () async { + HapticFeedback.mediumImpact(); + Navigator.of(context).pop(); + + try { + // Check if the in-app review is available + if (await _inAppReview.isAvailable()) { + // Request the review + await _inAppReview.requestReview(); + MixpanelManager() + .track('App Review Requested', properties: {'source': 'action_item_completion'}); + } else { + await _inAppReview.openStoreListing( + appStoreId: Platform.isIOS ? '6651027111' : null, + ); + MixpanelManager() + .track('App Store Opened', properties: {'source': 'action_item_completion'}); + } + } catch (e) { + debugPrint('Error requesting review: $e'); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.deepPurple, + foregroundColor: Colors.white, + minimumSize: const Size(double.infinity, 48), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + FaIcon( + Platform.isIOS ? FontAwesomeIcons.appStoreIos : FontAwesomeIcons.googlePlay, + size: 20, + ), + const SizedBox(width: 12), + Text( + 'Rate on ${Platform.isIOS ? 'App Store' : 'Google Play'}', + style: const TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + ), + const SizedBox(height: 8), + TextButton( + onPressed: () { + HapticFeedback.lightImpact(); + MixpanelManager().track('App Review Skipped', properties: {'source': 'action_item_completion'}); + Navigator.of(context).pop(); + }, + child: const Text( + 'Maybe later', + style: TextStyle( + color: Colors.grey, + fontSize: 16, + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ); + } +}