Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion app/lib/pages/action_items/action_items_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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});
Expand All @@ -21,6 +22,7 @@ class ActionItemsPage extends StatefulWidget {
class _ActionItemsPageState extends State<ActionItemsPage> with AutomaticKeepAliveClientMixin {
final ScrollController _scrollController = ScrollController();
Set<String> _exportedToAppleReminders = <String>{};
final AppReviewService _appReviewService = AppReviewService();

@override
bool get wantKeepAlive => true;
Expand Down Expand Up @@ -63,6 +65,19 @@ class _ActionItemsPageState extends State<ActionItemsPage> with AutomaticKeepAli
}
}

// checks if it's the first action item completed
Future<void> _onActionItemCompleted() async {
final hasCompletedFirst = await _appReviewService.hasCompletedFirstActionItem();

if (!hasCompletedFirst) {
await _appReviewService.markFirstActionItemCompleted();

if (mounted) {
await _appReviewService.showReviewPromptIfNeeded(context);
}
}
}

void _onScroll() {
final provider = Provider.of<ActionItemsProvider>(context, listen: false);
if (_scrollController.position.pixels >= _scrollController.position.maxScrollExtent - 200) {
Expand Down Expand Up @@ -465,7 +480,12 @@ class _ActionItemsPageState extends State<ActionItemsPage> 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,
),
Expand Down
7 changes: 7 additions & 0 deletions app/lib/pages/conversation_detail/page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -755,6 +756,7 @@ class ActionItemDetailWidget extends StatefulWidget {

class _ActionItemDetailWidgetState extends State<ActionItemDetailWidget> {
static final Map<String, bool> _pendingStates = {}; // Track pending states by description
final AppReviewService _appReviewService = AppReviewService();

@override
void dispose() {
Expand Down Expand Up @@ -888,6 +890,11 @@ class _ActionItemDetailWidgetState extends State<ActionItemDetailWidget> {
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);
}
Expand Down
173 changes: 173 additions & 0 deletions app/lib/services/app_review_service.dart
Original file line number Diff line number Diff line change
@@ -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<bool> hasCompletedFirstActionItem() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_hasCompletedFirstActionItemKey) ?? false;
}

// Marks that the user has completed their first action item
Future<void> markFirstActionItemCompleted() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_hasCompletedFirstActionItemKey, true);
}

// Checks if the review prompt has already been shown
Future<bool> hasShownReviewPrompt() async {
final prefs = await SharedPreferences.getInstance();
return prefs.getBool(_hasShownReviewPromptKey) ?? false;
}

// Marks that the review prompt has been shown
Future<void> markReviewPromptShown() async {
final prefs = await SharedPreferences.getInstance();
await prefs.setBool(_hasShownReviewPromptKey, true);
}

// Shows the review prompt if conditions are met
Future<bool> 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<void> _showReviewDialog(BuildContext context) async {
HapticFeedback.mediumImpact();

return showDialog<void>(
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,
),
),
),
],
),
],
),
),
);
},
);
}
}