diff --git a/app/lib/core/app_shell.dart b/app/lib/core/app_shell.dart index f8762b9f614..dbcef561b36 100644 --- a/app/lib/core/app_shell.dart +++ b/app/lib/core/app_shell.dart @@ -30,7 +30,6 @@ import 'package:omi/services/google_tasks_service.dart'; import 'package:omi/services/notifications.dart'; import 'package:omi/services/todoist_service.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/platform/platform_manager.dart'; @@ -72,7 +71,7 @@ class _AppShellState extends State { if (mounted) { var app = await context.read().getAppFromId(uri.pathSegments[1]); if (app != null) { - PlatformManager.instance.mixpanel.track('App Opened From DeepLink', properties: {'appId': app.id}); + PlatformManager.instance.analytics.track('App Opened From DeepLink', properties: {'appId': app.id}); if (mounted) { Navigator.of(context).push(MaterialPageRoute(builder: (context) => AppDetailPage(app: app))); } @@ -83,19 +82,19 @@ class _AppShellState extends State { } } else if (uri.pathSegments.first == 'wrapped') { if (mounted) { - PlatformManager.instance.mixpanel.track('Wrapped Opened From DeepLink'); + PlatformManager.instance.analytics.track('Wrapped Opened From DeepLink'); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const Wrapped2025Page())); } } else if (uri.pathSegments.first == 'tasks' && uri.pathSegments.length > 1) { if (mounted) { final token = uri.pathSegments[1]; - PlatformManager.instance.mixpanel.track('Shared Tasks Opened From DeepLink', properties: {'token': token}); + PlatformManager.instance.analytics.track('Shared Tasks Opened From DeepLink', properties: {'token': token}); _handleSharedTasksDeepLink(token); } } else if (uri.pathSegments.first == 'unlimited') { if (mounted) { if (!context.read().showSubscriptionUI) return; - PlatformManager.instance.mixpanel.track('Plans Opened From DeepLink'); + PlatformManager.instance.analytics.track('Plans Opened From DeepLink'); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const UsagePage(showUpgradeDialog: true))); } } else if (uri.host == 'todoist' && uri.pathSegments.isNotEmpty && uri.pathSegments.first == 'callback') { @@ -229,7 +228,7 @@ class _AppShellState extends State { if (!mounted) return; if (success) { - MixpanelManager().taskIntegrationEnabled(appName: 'todoist', success: true); + PlatformManager.instance.analytics.taskIntegrationEnabled(appName: 'todoist', success: true); Logger.debug('✓ Todoist authentication completed successfully'); Logger.debug('✓ Task integration enabled: Todoist - authentication complete'); AppSnackbar.showSnackbar(context.l10n.successfullyConnectedTodoist); @@ -237,7 +236,7 @@ class _AppShellState extends State { // Notify task integration provider to refresh UI from Firebase context.read().refresh(); } else { - MixpanelManager().taskIntegrationAuthFailed(appName: 'todoist'); + PlatformManager.instance.analytics.taskIntegrationAuthFailed(appName: 'todoist'); Logger.debug('Failed to complete Todoist authentication'); AppSnackbar.showSnackbarError(context.l10n.failedToConnectTodoistRetry); } @@ -250,7 +249,7 @@ class _AppShellState extends State { if (!mounted) return; if (success) { - MixpanelManager().taskIntegrationEnabled(appName: 'asana', success: true); + PlatformManager.instance.analytics.taskIntegrationEnabled(appName: 'asana', success: true); Logger.debug('✓ Asana authentication completed successfully'); Logger.debug('✓ Task integration enabled: Asana - authentication complete'); AppSnackbar.showSnackbar(context.l10n.successfullyConnectedAsana); @@ -263,7 +262,7 @@ class _AppShellState extends State { Navigator.of(context).push(MaterialPageRoute(builder: (context) => const AsanaSettingsPage())); } } else { - MixpanelManager().taskIntegrationAuthFailed(appName: 'asana'); + PlatformManager.instance.analytics.taskIntegrationAuthFailed(appName: 'asana'); Logger.debug('Failed to complete Asana authentication'); AppSnackbar.showSnackbarError(context.l10n.failedToConnectAsanaRetry); } @@ -276,7 +275,7 @@ class _AppShellState extends State { if (!mounted) return; if (success) { - MixpanelManager().taskIntegrationEnabled(appName: 'google_tasks', success: true); + PlatformManager.instance.analytics.taskIntegrationEnabled(appName: 'google_tasks', success: true); Logger.debug('✓ Google Tasks authentication completed successfully'); Logger.debug('✓ Task integration enabled: Google Tasks - authentication complete'); AppSnackbar.showSnackbar(context.l10n.successfullyConnectedGoogleTasks); @@ -284,7 +283,7 @@ class _AppShellState extends State { // Notify task integration provider to refresh UI from Firebase context.read().refresh(); } else { - MixpanelManager().taskIntegrationAuthFailed(appName: 'google_tasks'); + PlatformManager.instance.analytics.taskIntegrationAuthFailed(appName: 'google_tasks'); Logger.debug('Failed to complete Google Tasks authentication'); AppSnackbar.showSnackbarError(context.l10n.failedToConnectGoogleTasksRetry); } @@ -297,7 +296,7 @@ class _AppShellState extends State { if (!mounted) return; if (success) { - MixpanelManager().taskIntegrationEnabled(appName: 'clickup', success: true); + PlatformManager.instance.analytics.taskIntegrationEnabled(appName: 'clickup', success: true); Logger.debug('✓ ClickUp authentication completed successfully'); Logger.debug('✓ Task integration enabled: ClickUp - authentication complete'); AppSnackbar.showSnackbar(context.l10n.successfullyConnectedClickUp); @@ -310,7 +309,7 @@ class _AppShellState extends State { Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ClickUpSettingsPage())); } } else { - MixpanelManager().taskIntegrationAuthFailed(appName: 'clickup'); + PlatformManager.instance.analytics.taskIntegrationAuthFailed(appName: 'clickup'); Logger.debug('Failed to complete ClickUp authentication'); AppSnackbar.showSnackbarError(context.l10n.failedToConnectClickUpRetry); } diff --git a/app/lib/main.dart b/app/lib/main.dart index e2c44040997..e5f772a0a7a 100644 --- a/app/lib/main.dart +++ b/app/lib/main.dart @@ -172,7 +172,7 @@ Future _init() async { bool isAuth = (await AuthService.instance.getIdToken()) != null; print('DEBUG main: After getIdToken - isAuth=$isAuth, currentUser=${FirebaseAuth.instance.currentUser?.uid}'); if (isAuth) { - PlatformManager.instance.mixpanel.identify(); + PlatformManager.instance.analytics.identify(); // Restore onboarding state from server if not already set locally // This handles the case where cached credentials are used on startup if (!SharedPreferencesUtil().onboardingCompleted) { diff --git a/app/lib/mobile/mobile_app.dart b/app/lib/mobile/mobile_app.dart index 2d9ce5e28f3..611ac954dc7 100644 --- a/app/lib/mobile/mobile_app.dart +++ b/app/lib/mobile/mobile_app.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -8,7 +9,6 @@ import 'package:omi/pages/onboarding/device_selection.dart'; import 'package:omi/pages/onboarding/permissions/permissions_checker.dart'; import 'package:omi/pages/onboarding/wrapper.dart'; import 'package:omi/providers/auth_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; class MobileApp extends StatelessWidget { const MobileApp({super.key}); @@ -81,7 +81,7 @@ class _PermissionsGateState extends State<_PermissionsGate> { if (_permissionsGranted!) { return const HomePageWrapper(); } - MixpanelManager().permissionsInterstitialShown(); + PlatformManager.instance.analytics.permissionsInterstitialShown(); return const PermissionsInterstitialPage(); } } diff --git a/app/lib/pages/action_items/action_items_page.dart b/app/lib/pages/action_items/action_items_page.dart index 22740412bbb..0fe290889b3 100644 --- a/app/lib/pages/action_items/action_items_page.dart +++ b/app/lib/pages/action_items/action_items_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,7 +13,6 @@ import 'package:omi/providers/action_items_provider.dart'; import 'package:omi/providers/goals_provider.dart'; import 'package:omi/providers/task_integration_provider.dart'; import 'package:omi/services/app_review_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/debouncer.dart'; import 'widgets/action_item_form_sheet.dart'; @@ -76,7 +76,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli _loadTaskGoalLinks(); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; - MixpanelManager().actionItemsPageOpened(); + PlatformManager.instance.analytics.actionItemsPageOpened(); final provider = Provider.of(context, listen: false); if (provider.actionItems.isEmpty) { provider.fetchActionItems(showShimmer: true); @@ -152,7 +152,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli } Future _onActionItemCompleted() async { - MixpanelManager().actionItemCompleted(fromTab: 'Tasks'); + PlatformManager.instance.analytics.actionItemCompleted(fromTab: 'Tasks'); final hasCompletedFirst = await _appReviewService.hasCompletedFirstActionItem(); @@ -190,7 +190,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli currentValue: current, ); if (created != null) { - MixpanelManager().goalCreated( + PlatformManager.instance.analytics.goalCreated( goalId: created.id, titleLength: title.length, targetValue: target, @@ -247,10 +247,8 @@ class _ActionItemsPageState extends State with AutomaticKeepAli filled: true, fillColor: const Color(0xFF1F1F25), border: OutlineInputBorder(borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none), - focusedBorder: - OutlineInputBorder(borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none), - enabledBorder: - OutlineInputBorder(borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none), + focusedBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none), + enabledBorder: OutlineInputBorder(borderRadius: BorderRadius.circular(24), borderSide: BorderSide.none), prefixIcon: const Icon(Icons.search, color: Colors.white60), suffixIcon: ValueListenableBuilder( valueListenable: _searchController, @@ -325,9 +323,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli width: 36, height: 36, decoration: const BoxDecoration(color: Color(0xFF1F1F25), shape: BoxShape.circle), - child: const Center( - child: Icon(Icons.more_horiz_rounded, color: Colors.white70, size: 20), - ), + child: const Center(child: Icon(Icons.more_horiz_rounded, color: Colors.white70, size: 20)), ), ), ); @@ -611,10 +607,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli const SliverPadding(padding: EdgeInsets.only(top: 12)), SliverToBoxAdapter(child: _buildGoalsRow()), const SliverPadding(padding: EdgeInsets.only(top: 8)), - SliverFillRemaining( - hasScrollBody: false, - child: Center(child: _buildEmptyTasksContent()), - ), + SliverFillRemaining(hasScrollBody: false, child: Center(child: _buildEmptyTasksContent())), ], ); } @@ -637,10 +630,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( - colors: [ - Colors.deepPurple.withValues(alpha: 0.35), - Colors.deepPurple.withValues(alpha: 0.0), - ], + colors: [Colors.deepPurple.withValues(alpha: 0.35), Colors.deepPurple.withValues(alpha: 0.0)], stops: const [0.0, 1.0], ), ), @@ -672,12 +662,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli const SizedBox(height: 28), Text( context.l10n.noTasksYet, - style: const TextStyle( - color: Colors.white, - fontSize: 22, - fontWeight: FontWeight.w700, - letterSpacing: -0.3, - ), + style: const TextStyle(color: Colors.white, fontSize: 22, fontWeight: FontWeight.w700, letterSpacing: -0.3), ), const SizedBox(height: 10), ConstrainedBox( @@ -685,11 +670,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli child: Text( context.l10n.tasksEmptyStateMessage, textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white.withValues(alpha: 0.55), - fontSize: 15, - height: 1.5, - ), + style: TextStyle(color: Colors.white.withValues(alpha: 0.55), fontSize: 15, height: 1.5), ), ), const SizedBox(height: 28), @@ -710,11 +691,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli color: Colors.white, borderRadius: BorderRadius.circular(28), boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.35), - blurRadius: 18, - offset: const Offset(0, 6), - ), + BoxShadow(color: Colors.black.withValues(alpha: 0.35), blurRadius: 18, offset: const Offset(0, 6)), ], ), child: Row( @@ -756,27 +733,21 @@ class _ActionItemsPageState extends State with AutomaticKeepAli if (isSearching) ...[ if (filteredItems.isEmpty) - SliverFillRemaining( - hasScrollBody: false, - child: Center(child: _buildNoSearchResultsContent()), - ) + SliverFillRemaining(hasScrollBody: false, child: Center(child: _buildNoSearchResultsContent())) else SliverList( - delegate: SliverChildBuilderDelegate( - (context, index) { - final item = filteredItems[index]; - return Padding( - padding: const EdgeInsets.symmetric(horizontal: 16), - child: _buildTaskItem( - item, - provider, - category: _getCategoryForItem(item), - categoryItems: filteredItems, - ), - ); - }, - childCount: filteredItems.length, - ), + delegate: SliverChildBuilderDelegate((context, index) { + final item = filteredItems[index]; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: _buildTaskItem( + item, + provider, + category: _getCategoryForItem(item), + categoryItems: filteredItems, + ), + ); + }, childCount: filteredItems.length), ), ] else ...[ SliverToBoxAdapter(child: _buildGoalsRow()), @@ -834,7 +805,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli GestureDetector( onTap: () { HapticFeedback.lightImpact(); - MixpanelManager().track('Add Goal Clicked from Tasks Page'); + PlatformManager.instance.analytics.track('Add Goal Clicked from Tasks Page'); _showCreateGoalSheet(); }, child: Container( @@ -866,7 +837,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli onWillAcceptWithDetails: (details) => goal != null, onAcceptWithDetails: (details) { if (goal == null) return; - MixpanelManager().taskDraggedToGoal(taskId: details.data.id, goalId: goal.id); + PlatformManager.instance.analytics.taskDraggedToGoal(taskId: details.data.id, goalId: goal.id); _attachTaskToGoal(details.data.id, goal.id); }, builder: (context, candidateData, rejectedData) { @@ -1222,10 +1193,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli /// stopping at the first sibling/ancestor. The data model is flat (no /// parent_id), so the page is the right layer to compute this: it owns the /// category-grouped display order; the provider does not. - List _visibleDescendantIds( - ActionItemWithMetadata parent, - List categoryItems, - ) { + List _visibleDescendantIds(ActionItemWithMetadata parent, List categoryItems) { final idx = categoryItems.indexWhere((i) => i.id == parent.id); if (idx < 0) return const []; final ids = []; @@ -1462,11 +1430,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli await provider.updateActionItemState(item, !item.completed); if (!item.completed) _onActionItemCompleted(); }, - child: SizedBox( - width: 44, - height: 48, - child: Center(child: _buildCheckbox(item.completed)), - ), + child: SizedBox(width: 44, height: 48, child: Center(child: _buildCheckbox(item.completed))), ), // Task text Expanded( @@ -1512,10 +1476,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli // Different shape + position from the leading completion circle // so completion vs. selection cannot be confused. if (provider.isSelectionMode) - Padding( - padding: const EdgeInsets.only(left: 8, right: 8), - child: _buildSelectionSquare(isSelected), - ), + Padding(padding: const EdgeInsets.only(left: 8, right: 8), child: _buildSelectionSquare(isSelected)), ], ), ), @@ -1599,7 +1560,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli // Goals are not part of selection mode — selection only applies to // tasks (the action bar's Export action acts on tasks only). if (provider.isSelectionMode) return; - MixpanelManager().goalItemTappedForEdit(goalId: goal.id, source: 'tasks_page'); + PlatformManager.instance.analytics.goalItemTappedForEdit(goalId: goal.id, source: 'tasks_page'); _showEditGoalSheet(goal); }, child: Container( @@ -1669,7 +1630,7 @@ class _ActionItemsPageState extends State with AutomaticKeepAli false; }, onDismissed: (direction) async { - MixpanelManager().goalDeleted(goalId: goal.id, source: 'tasks_page', method: 'swipe'); + PlatformManager.instance.analytics.goalDeleted(goalId: goal.id, source: 'tasks_page', method: 'swipe'); await _deleteGoal(goal); }, background: Container( @@ -1695,10 +1656,10 @@ class _ActionItemsPageState extends State with AutomaticKeepAli // Update goal via provider final goalsProvider = Provider.of(context, listen: false); await goalsProvider.updateGoal(goal.id, title: title, currentValue: current, targetValue: target); - MixpanelManager().goalUpdated(goalId: goal.id, source: 'tasks_page'); + PlatformManager.instance.analytics.goalUpdated(goalId: goal.id, source: 'tasks_page'); }, onDelete: () { - MixpanelManager().goalDeleted(goalId: goal.id, source: 'tasks_page', method: 'button'); + PlatformManager.instance.analytics.goalDeleted(goalId: goal.id, source: 'tasks_page', method: 'button'); _deleteGoal(goal); }, ), @@ -2168,13 +2129,7 @@ class _DashedCirclePainter extends CustomPainter { for (var i = 0; i < segments; i++) { final startAngle = i * stepAngle; - canvas.drawArc( - Rect.fromCircle(center: center, radius: radius), - startAngle, - dashAngle, - false, - paint, - ); + canvas.drawArc(Rect.fromCircle(center: center, radius: radius), startAngle, dashAngle, false, paint); } } diff --git a/app/lib/pages/action_items/services/action_item_export_service.dart b/app/lib/pages/action_items/services/action_item_export_service.dart index c41d789af85..9c4b778380f 100644 --- a/app/lib/pages/action_items/services/action_item_export_service.dart +++ b/app/lib/pages/action_items/services/action_item_export_service.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:omi/backend/http/api/action_items.dart'; import 'package:omi/backend/schema/schema.dart'; import 'package:omi/pages/settings/task_integrations_page.dart'; @@ -6,7 +7,6 @@ import 'package:omi/services/asana_service.dart'; import 'package:omi/services/clickup_service.dart'; import 'package:omi/services/google_tasks_service.dart'; import 'package:omi/services/todoist_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/platform/platform_service.dart'; @@ -45,16 +45,16 @@ class ActionItemExportService { if (!service.isAuthenticated) return ExportResult.failed; try { - final ok = await service.createTask( - content: item.description, - description: 'From Omi', - dueDate: item.dueAt, - ); + final ok = await service.createTask(content: item.description, description: 'From Omi', dueDate: item.dueAt); if (!ok) return ExportResult.failed; final exportTime = DateTime.now(); await updateActionItem(item.id, exported: true, exportDate: exportTime, exportPlatform: 'todoist'); - MixpanelManager().actionItemExported(actionItemId: item.id, appName: 'Todoist', timestamp: exportTime); + PlatformManager.instance.analytics.actionItemExported( + actionItemId: item.id, + appName: 'Todoist', + timestamp: exportTime, + ); return ExportResult.success; } catch (e) { Logger.debug('Todoist bulk export failed for ${item.id}: $e'); @@ -67,16 +67,16 @@ class ActionItemExportService { if (!service.isAuthenticated) return ExportResult.failed; try { - final ok = await service.createTask( - name: item.description, - notes: 'From Omi', - dueDate: item.dueAt, - ); + final ok = await service.createTask(name: item.description, notes: 'From Omi', dueDate: item.dueAt); if (!ok) return ExportResult.failed; final exportTime = DateTime.now(); await updateActionItem(item.id, exported: true, exportDate: exportTime, exportPlatform: 'asana'); - MixpanelManager().actionItemExported(actionItemId: item.id, appName: 'Asana', timestamp: exportTime); + PlatformManager.instance.analytics.actionItemExported( + actionItemId: item.id, + appName: 'Asana', + timestamp: exportTime, + ); return ExportResult.success; } catch (e) { Logger.debug('Asana bulk export failed for ${item.id}: $e'); @@ -89,16 +89,16 @@ class ActionItemExportService { if (!service.isAuthenticated) return ExportResult.failed; try { - final ok = await service.createTask( - title: item.description, - notes: 'From Omi', - dueDate: item.dueAt, - ); + final ok = await service.createTask(title: item.description, notes: 'From Omi', dueDate: item.dueAt); if (!ok) return ExportResult.failed; final exportTime = DateTime.now(); await updateActionItem(item.id, exported: true, exportDate: exportTime, exportPlatform: 'google_tasks'); - MixpanelManager().actionItemExported(actionItemId: item.id, appName: 'Google Tasks', timestamp: exportTime); + PlatformManager.instance.analytics.actionItemExported( + actionItemId: item.id, + appName: 'Google Tasks', + timestamp: exportTime, + ); return ExportResult.success; } catch (e) { Logger.debug('Google Tasks bulk export failed for ${item.id}: $e'); @@ -111,16 +111,16 @@ class ActionItemExportService { if (!service.isAuthenticated) return ExportResult.failed; try { - final ok = await service.createTask( - name: item.description, - description: 'From Omi', - dueDate: item.dueAt, - ); + final ok = await service.createTask(name: item.description, description: 'From Omi', dueDate: item.dueAt); if (!ok) return ExportResult.failed; final exportTime = DateTime.now(); await updateActionItem(item.id, exported: true, exportDate: exportTime, exportPlatform: 'clickup'); - MixpanelManager().actionItemExported(actionItemId: item.id, appName: 'ClickUp', timestamp: exportTime); + PlatformManager.instance.analytics.actionItemExported( + actionItemId: item.id, + appName: 'ClickUp', + timestamp: exportTime, + ); return ExportResult.success; } catch (e) { Logger.debug('ClickUp bulk export failed for ${item.id}: $e'); @@ -152,7 +152,11 @@ class ActionItemExportService { exportPlatform: 'apple_reminders', appleReminderId: calendarItemId, ); - MixpanelManager().actionItemExported(actionItemId: item.id, appName: 'Apple Reminders', timestamp: exportTime); + PlatformManager.instance.analytics.actionItemExported( + actionItemId: item.id, + appName: 'Apple Reminders', + timestamp: exportTime, + ); return ExportResult.success; } catch (e) { Logger.debug('Apple Reminders bulk export failed for ${item.id}: $e'); diff --git a/app/lib/pages/action_items/widgets/action_item_form_sheet.dart b/app/lib/pages/action_items/widgets/action_item_form_sheet.dart index 4de3a465be4..a67ace06a7e 100644 --- a/app/lib/pages/action_items/widgets/action_item_form_sheet.dart +++ b/app/lib/pages/action_items/widgets/action_item_form_sheet.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -11,7 +12,6 @@ import 'package:share_plus/share_plus.dart'; import 'package:omi/backend/http/api/action_items.dart' as action_items_api; import 'package:omi/backend/schema/schema.dart'; import 'package:omi/providers/action_items_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/responsive/responsive_helper.dart'; import 'package:omi/widgets/calendar_date_picker_sheet.dart'; @@ -72,8 +72,7 @@ class _ActionItemFormSheetState extends State { String newDescription = _textController.text.trim(); bool descriptionChanged = newDescription != widget.actionItem!.description; // Compare due dates - handle null cases explicitly - bool dueDateChanged = - (_selectedDueDate == null && widget.actionItem!.dueAt != null) || + bool dueDateChanged = (_selectedDueDate == null && widget.actionItem!.dueAt != null) || (_selectedDueDate != null && widget.actionItem!.dueAt == null) || (_selectedDueDate != null && widget.actionItem!.dueAt != null && @@ -109,7 +108,7 @@ class _ActionItemFormSheetState extends State { // Track action item edit if (descriptionChanged || dueDateChanged) { - MixpanelManager().actionItemEdited( + PlatformManager.instance.analytics.actionItemEdited( actionItemId: widget.actionItem!.id, titleChanged: descriptionChanged, dateChanged: dueDateChanged, @@ -146,7 +145,10 @@ class _ActionItemFormSheetState extends State { if (createdItem != null) { // Track manually added action item - MixpanelManager().actionItemManuallyAdded(actionItemId: createdItem.id, timestamp: DateTime.now()); + PlatformManager.instance.analytics.actionItemManuallyAdded( + actionItemId: createdItem.id, + timestamp: DateTime.now(), + ); } else if (mounted) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -206,7 +208,10 @@ class _ActionItemFormSheetState extends State { final url = result['url'] as String; HapticFeedback.lightImpact(); await Share.share(url); - MixpanelManager().track('Action Item Shared', properties: {'actionItemId': widget.actionItem!.id}); + PlatformManager.instance.analytics.track( + 'Action Item Shared', + properties: {'actionItemId': widget.actionItem!.id}, + ); } else { ScaffoldMessenger.of( context, @@ -472,8 +477,8 @@ class _DateTimePickerSheetState extends State { color: isSelected == true ? ResponsiveHelper.purplePrimary : isCurrentYear == true - ? ResponsiveHelper.purplePrimary.withValues(alpha: 0.3) - : Colors.transparent, + ? ResponsiveHelper.purplePrimary.withValues(alpha: 0.3) + : Colors.transparent, borderRadius: BorderRadius.circular(8), ), child: Center( diff --git a/app/lib/pages/action_items/widgets/action_item_tile_widget.dart b/app/lib/pages/action_items/widgets/action_item_tile_widget.dart index a51a39cacf3..db999e66723 100644 --- a/app/lib/pages/action_items/widgets/action_item_tile_widget.dart +++ b/app/lib/pages/action_items/widgets/action_item_tile_widget.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -18,7 +19,6 @@ import 'package:omi/services/asana_service.dart'; import 'package:omi/services/clickup_service.dart'; import 'package:omi/services/google_tasks_service.dart'; import 'package:omi/services/todoist_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/utils/platform/platform_service.dart'; @@ -66,7 +66,7 @@ class _ActionItemTileWidgetState extends State { final newState = !widget.actionItem.completed; // Track action item checked/unchecked - MixpanelManager().actionItemChecked( + PlatformManager.instance.analytics.actionItemChecked( actionItemId: widget.actionItem.id, completed: newState, timestamp: DateTime.now(), @@ -429,7 +429,7 @@ class _ActionItemTileWidgetState extends State { await updateActionItem(widget.actionItem.id, exported: true, exportDate: exportTime, exportPlatform: 'todoist'); // Track action item export - MixpanelManager().actionItemExported( + PlatformManager.instance.analytics.actionItemExported( actionItemId: widget.actionItem.id, appName: 'Todoist', timestamp: exportTime, @@ -541,7 +541,7 @@ class _ActionItemTileWidgetState extends State { await updateActionItem(widget.actionItem.id, exported: true, exportDate: exportTime, exportPlatform: 'asana'); // Track action item export - MixpanelManager().actionItemExported( + PlatformManager.instance.analytics.actionItemExported( actionItemId: widget.actionItem.id, appName: 'Asana', timestamp: exportTime, @@ -662,7 +662,7 @@ class _ActionItemTileWidgetState extends State { ); // Track action item export - MixpanelManager().actionItemExported( + PlatformManager.instance.analytics.actionItemExported( actionItemId: widget.actionItem.id, appName: 'Google Tasks', timestamp: exportTime, @@ -754,7 +754,7 @@ class _ActionItemTileWidgetState extends State { await updateActionItem(widget.actionItem.id, exported: true, exportDate: exportTime, exportPlatform: 'clickup'); // Track action item export - MixpanelManager().actionItemExported( + PlatformManager.instance.analytics.actionItemExported( actionItemId: widget.actionItem.id, appName: 'ClickUp', timestamp: exportTime, @@ -888,7 +888,7 @@ class _ActionItemTileWidgetState extends State { ); // Track action item export - MixpanelManager().actionItemExported( + PlatformManager.instance.analytics.actionItemExported( actionItemId: widget.actionItem.id, appName: 'Apple Reminders', timestamp: exportTime, @@ -1013,7 +1013,7 @@ class _ActionItemTileWidgetState extends State { child: GestureDetector( onTap: () { if (!context.read().showSubscriptionUI) return; - MixpanelManager().paywallOpened('Action Item'); + PlatformManager.instance.analytics.paywallOpened('Action Item'); routeToPage(context, const UsagePage(showUpgradeDialog: true)); return; }, diff --git a/app/lib/pages/action_items/widgets/task_integrations_banner.dart b/app/lib/pages/action_items/widgets/task_integrations_banner.dart index 58d3fca0e24..10241ff6850 100644 --- a/app/lib/pages/action_items/widgets/task_integrations_banner.dart +++ b/app/lib/pages/action_items/widgets/task_integrations_banner.dart @@ -1,9 +1,9 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:omi/gen/assets.gen.dart'; import 'package:omi/pages/settings/task_integrations_page.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class TaskIntegrationsBanner extends StatelessWidget { @@ -16,7 +16,7 @@ class TaskIntegrationsBanner extends StatelessWidget { HapticFeedback.lightImpact(); // Track banner click - MixpanelManager().exportTasksBannerClicked(); + PlatformManager.instance.analytics.exportTasksBannerClicked(); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const TaskIntegrationsPage())); }, diff --git a/app/lib/pages/apps/add_app.dart b/app/lib/pages/apps/add_app.dart index adcf523c9ce..a9c90aee75c 100644 --- a/app/lib/pages/apps/add_app.dart +++ b/app/lib/pages/apps/add_app.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -20,7 +21,6 @@ import 'package:omi/pages/apps/widgets/notification_scopes_chips_widget.dart'; import 'package:omi/pages/payments/payment_method_provider.dart'; import 'package:omi/pages/payments/payments_page.dart'; import 'package:omi/providers/app_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/widgets/confirmation_dialog.dart'; import 'widgets/capabilities_chips_widget.dart'; @@ -69,7 +69,7 @@ class _AddAppPageState extends State { borderRadius: BorderRadius.circular(20), child: InkWell( onTap: () { - MixpanelManager().pageOpened('App Submission Help'); + PlatformManager.instance.analytics.pageOpened('App Submission Help'); launchUrl(Uri.parse('https://docs.omi.me/doc/developer/apps/Introduction')); }, borderRadius: BorderRadius.circular(20), @@ -723,14 +723,14 @@ class _AddAppPageState extends State { }, onConfirm: () async { if (provider.makeAppPublic) { - MixpanelManager().publicAppSubmitted({ + PlatformManager.instance.analytics.publicAppSubmitted({ 'app_name': provider.appNameController.text, 'app_category': provider.appCategory, 'app_capabilities': provider.capabilities.map((e) => e.id).toList(), 'is_paid': provider.isPaid, }); } else { - MixpanelManager().privateAppSubmitted({ + PlatformManager.instance.analytics.privateAppSubmitted({ 'app_name': provider.appNameController.text, 'app_category': provider.appCategory, 'app_capabilities': provider.capabilities.map((e) => e.id).toList(), diff --git a/app/lib/pages/apps/app_detail/app_detail.dart b/app/lib/pages/apps/app_detail/app_detail.dart index 772bc0932f3..6a4944e1659 100644 --- a/app/lib/pages/apps/app_detail/app_detail.dart +++ b/app/lib/pages/apps/app_detail/app_detail.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -24,7 +25,6 @@ import 'package:omi/pages/apps/widgets/full_screen_image_viewer.dart'; import 'package:omi/pages/chat/page.dart'; import 'package:omi/providers/app_provider.dart'; import 'package:omi/providers/message_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/widgets/animated_loading_button.dart'; @@ -115,7 +115,7 @@ class _AppDetailPageState extends State { if (enabled) { prefs.enableApp(app.id); - MixpanelManager().appEnabled(app.id); + PlatformManager.instance.analytics.appEnabled(app.id); context.read().filterApps(); setState(() { @@ -159,7 +159,10 @@ class _AppDetailPageState extends State { final result = await cancelAppSubscription(widget.app.id); if (result != null && result['status'] == 'success') { // Track subscription cancellation - MixpanelManager().appDetailSubscriptionCancelled(appId: widget.app.id, appName: widget.app.name); + PlatformManager.instance.analytics.appDetailSubscriptionCancelled( + appId: widget.app.id, + appName: widget.app.name, + ); await _loadSubscriptionData(); @@ -201,7 +204,7 @@ class _AppDetailPageState extends State { app = widget.app; // Track app detail page viewed - MixpanelManager().appDetailViewed( + PlatformManager.instance.analytics.appDetailViewed( appId: app.id, appName: app.name, category: app.category, @@ -289,7 +292,7 @@ class _AppDetailPageState extends State { } Future _checkPaymentStatus(String appId) async { - MixpanelManager().appPurchaseStarted(appId); + PlatformManager.instance.analytics.appPurchaseStarted(appId); _paymentCheckTimer = Timer.periodic(const Duration(seconds: 5), (timer) async { var prefs = SharedPreferencesUtil(); if (mounted) { @@ -300,9 +303,9 @@ class _AppDetailPageState extends State { if (details != null && details['is_user_paid']) { var enabled = await enableAppServer(appId); if (enabled) { - MixpanelManager().appPurchaseCompleted(appId); + PlatformManager.instance.analytics.appPurchaseCompleted(appId); prefs.enableApp(appId); - MixpanelManager().appEnabled(appId); + PlatformManager.instance.analytics.appEnabled(appId); if (!mounted) { timer.cancel(); @@ -614,7 +617,7 @@ class _AppDetailPageState extends State { } // Track chat button clicked - MixpanelManager().appDetailChatClicked(appId: app.id, appName: app.name); + PlatformManager.instance.analytics.appDetailChatClicked(appId: app.id, appName: app.name); // Navigate directly to chat page if (mounted) { @@ -672,10 +675,10 @@ class _AppDetailPageState extends State { icon: const FaIcon(FontAwesomeIcons.arrowUpFromBracket, size: 16.0, color: Colors.white), onPressed: () async { HapticFeedback.mediumImpact(); - MixpanelManager().track('App Shared', properties: {'appId': app.id}); + PlatformManager.instance.analytics.track('App Shared', properties: {'appId': app.id}); // Track share button clicked - MixpanelManager().appDetailShared(appId: app.id, appName: app.name); + PlatformManager.instance.analytics.appDetailShared(appId: app.id, appName: app.name); // Get the position of the share button for iOS final RenderBox? box = context.findRenderObject() as RenderBox?; @@ -850,7 +853,7 @@ class _AppDetailPageState extends State { text: "Subscribe", onPressed: () async { // Track subscribe button clicked - MixpanelManager().appDetailSubscribeClicked( + PlatformManager.instance.analytics.appDetailSubscribeClicked( appId: app.id, appName: app.name, ); @@ -1191,7 +1194,10 @@ class _AppDetailPageState extends State { return GestureDetector( onTap: () { // Track preview image viewed - MixpanelManager().appDetailPreviewImageViewed(appId: app.id, imageIndex: index); + PlatformManager.instance.analytics.appDetailPreviewImageViewed( + appId: app.id, + imageIndex: index, + ); Navigator.push( context, @@ -1340,7 +1346,7 @@ class _AppDetailPageState extends State { onTap: () { if (app.reviews.isNotEmpty) { // Track reviews page opened - MixpanelManager().appDetailReviewsOpened( + PlatformManager.instance.analytics.appDetailReviewsOpened( appId: app.id, reviewCount: app.reviews.length, ); @@ -1520,7 +1526,7 @@ class _AppDetailPageState extends State { } prefs.enableApp(appId); - MixpanelManager().appEnabled(appId); + PlatformManager.instance.analytics.appEnabled(appId); context.read().filterApps(); setState(() { @@ -1543,7 +1549,7 @@ class _AppDetailPageState extends State { prefs.disableApp(appId); var res = await disableAppServer(appId); print(res); - MixpanelManager().appDisabled(appId); + PlatformManager.instance.analytics.appDisabled(appId); if (!mounted) return; @@ -1712,7 +1718,7 @@ class _RecentReviewsSectionState extends State { SharedPreferencesUtil().appsList = appsList; } - MixpanelManager().appRated(widget.app.id, editRating); + PlatformManager.instance.analytics.appRated(widget.app.id, editRating); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/app/lib/pages/apps/app_detail/widgets/add_review_widget.dart b/app/lib/pages/apps/app_detail/widgets/add_review_widget.dart index c81f81c40cc..1fdd3bbf4ae 100644 --- a/app/lib/pages/apps/app_detail/widgets/add_review_widget.dart +++ b/app/lib/pages/apps/app_detail/widgets/add_review_widget.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter_rating_bar/flutter_rating_bar.dart'; @@ -7,7 +8,6 @@ import 'package:omi/backend/http/api/apps.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/backend/schema/app.dart'; import 'package:omi/providers/connectivity_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/widgets/animated_loading_button.dart'; @@ -139,10 +139,10 @@ class _AddReviewWidgetState extends State { duration: const Duration(milliseconds: 300), height: showReviewField ? (showButton - ? (MediaQuery.sizeOf(context).height < 680 - ? MediaQuery.sizeOf(context).height * 0.28 - : MediaQuery.sizeOf(context).height * 0.2) - : MediaQuery.sizeOf(context).height * 0.132) + ? (MediaQuery.sizeOf(context).height < 680 + ? MediaQuery.sizeOf(context).height * 0.28 + : MediaQuery.sizeOf(context).height * 0.2) + : MediaQuery.sizeOf(context).height * 0.132) : 0, child: !showReviewField ? null @@ -243,10 +243,10 @@ class _AddReviewWidgetState extends State { var index = appsList.indexWhere((element) => element.id == widget.app.id); appsList[index] = widget.app; SharedPreferencesUtil().appsList = appsList; - MixpanelManager().appRated(widget.app.id.toString(), rating); + PlatformManager.instance.analytics.appRated(widget.app.id.toString(), rating); // Track review added - MixpanelManager().appDetailReviewAdded( + PlatformManager.instance.analytics.appDetailReviewAdded( appId: widget.app.id, rating: rating.toInt(), hasComment: reviewController.text.trim().isNotEmpty, diff --git a/app/lib/pages/apps/explore_install_page.dart b/app/lib/pages/apps/explore_install_page.dart index eeec69f89c2..30c425578ae 100644 --- a/app/lib/pages/apps/explore_install_page.dart +++ b/app/lib/pages/apps/explore_install_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -16,7 +17,6 @@ import 'package:omi/pages/apps/widgets/filter_sheet.dart'; import 'package:omi/pages/apps/widgets/popular_apps_section.dart'; import 'package:omi/providers/app_provider.dart'; import 'package:omi/providers/home_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/app_localizations_helper.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/debouncer.dart'; @@ -702,7 +702,10 @@ class ExploreInstallPageState extends State with AutomaticKe final wasSelected = provider.isFilterSelected('My Apps', 'Apps'); provider.addOrRemoveFilter('My Apps', 'Apps'); provider.applyFilters(); - MixpanelManager().appsTypeFilter('My Apps', !wasSelected); + PlatformManager.instance.analytics.appsTypeFilter( + 'My Apps', + !wasSelected, + ); }, icon: const FaIcon( FontAwesomeIcons.solidUser, @@ -740,7 +743,10 @@ class ExploreInstallPageState extends State with AutomaticKe final wasSelected = provider.isFilterSelected('My Apps', 'Apps'); provider.addOrRemoveFilter('My Apps', 'Apps'); provider.applyFilters(); - MixpanelManager().appsTypeFilter('My Apps', !wasSelected); + PlatformManager.instance.analytics.appsTypeFilter( + 'My Apps', + !wasSelected, + ); }, icon: const FaIcon( FontAwesomeIcons.solidUser, @@ -772,7 +778,10 @@ class ExploreInstallPageState extends State with AutomaticKe final wasSelected = provider.isFilterSelected('Installed Apps', 'Apps'); provider.addOrRemoveFilter('Installed Apps', 'Apps'); provider.applyFilters(); - MixpanelManager().appsTypeFilter('Installed Apps', !wasSelected); + PlatformManager.instance.analytics.appsTypeFilter( + 'Installed Apps', + !wasSelected, + ); }, icon: const FaIcon( FontAwesomeIcons.download, @@ -812,7 +821,10 @@ class ExploreInstallPageState extends State with AutomaticKe final wasSelected = provider.isFilterSelected('Installed Apps', 'Apps'); provider.addOrRemoveFilter('Installed Apps', 'Apps'); provider.applyFilters(); - MixpanelManager().appsTypeFilter('Installed Apps', !wasSelected); + PlatformManager.instance.analytics.appsTypeFilter( + 'Installed Apps', + !wasSelected, + ); }, icon: const FaIcon( FontAwesomeIcons.download, diff --git a/app/lib/pages/apps/list_item.dart b/app/lib/pages/apps/list_item.dart index d1341447179..4748e4d8ca7 100644 --- a/app/lib/pages/apps/list_item.dart +++ b/app/lib/pages/apps/list_item.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -6,7 +7,6 @@ import 'package:omi/widgets/shimmer_with_timeout.dart'; import 'package:omi/backend/schema/app.dart'; import 'package:omi/providers/app_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/widgets/dialog.dart'; import 'package:omi/widgets/extensions/string.dart'; @@ -35,7 +35,7 @@ class AppListItem extends StatelessWidget { builder: (context, state, child) { return GestureDetector( onTap: () async { - MixpanelManager().pageOpened('App Detail'); + PlatformManager.instance.analytics.pageOpened('App Detail'); await routeToPage(context, AppDetailPage(app: app)); }, child: Container( @@ -140,7 +140,7 @@ class AppListItem extends StatelessWidget { onTap: () { if (state.enabled) { // App is enabled, open app detail - MixpanelManager().pageOpened('App Detail'); + PlatformManager.instance.analytics.pageOpened('App Detail'); routeToPage(context, AppDetailPage(app: app)); return; } diff --git a/app/lib/pages/apps/manage_create_page.dart b/app/lib/pages/apps/manage_create_page.dart index 45ab78bd4ac..e04a97fc256 100644 --- a/app/lib/pages/apps/manage_create_page.dart +++ b/app/lib/pages/apps/manage_create_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -7,7 +8,6 @@ import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/pages/apps/add_app.dart'; import 'package:omi/pages/apps/list_item.dart'; import 'package:omi/providers/app_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/other/temp.dart'; class ManageCreatePage extends StatelessWidget { @@ -86,7 +86,7 @@ class ManageCreatePage extends StatelessWidget { const SizedBox(height: 16), GestureDetector( onTap: () { - MixpanelManager().pageOpened('Submit App'); + PlatformManager.instance.analytics.pageOpened('Submit App'); routeToPage(context, const AddAppPage()); }, child: Container( diff --git a/app/lib/pages/apps/widgets/ai_app_generator_banner.dart b/app/lib/pages/apps/widgets/ai_app_generator_banner.dart index 626ff38546a..d4ea417d017 100644 --- a/app/lib/pages/apps/widgets/ai_app_generator_banner.dart +++ b/app/lib/pages/apps/widgets/ai_app_generator_banner.dart @@ -1,10 +1,10 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:omi/pages/settings/ai_app_generator_page.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; class AiAppGeneratorBanner extends StatelessWidget { const AiAppGeneratorBanner({super.key}); @@ -16,7 +16,7 @@ class AiAppGeneratorBanner extends StatelessWidget { HapticFeedback.lightImpact(); // Track banner click - MixpanelManager().track('AI App Generator Banner Clicked'); + PlatformManager.instance.analytics.track('AI App Generator Banner Clicked'); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const AiAppGeneratorPage())); }, diff --git a/app/lib/pages/apps/widgets/app_section_card.dart b/app/lib/pages/apps/widgets/app_section_card.dart index f5e2899b964..02af2c55191 100644 --- a/app/lib/pages/apps/widgets/app_section_card.dart +++ b/app/lib/pages/apps/widgets/app_section_card.dart @@ -1,5 +1,6 @@ import 'dart:math'; // Need max and min +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -8,7 +9,6 @@ import 'package:provider/provider.dart'; import 'package:omi/backend/schema/app.dart'; import 'package:omi/pages/apps/app_detail/app_detail.dart'; import 'package:omi/providers/app_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/widgets/extensions/string.dart'; @@ -85,7 +85,7 @@ class SectionAppItemCard extends StatelessWidget { builder: (context, provider, child) { return GestureDetector( onTap: () async { - MixpanelManager().pageOpened('App Detail From Popular Apps Section'); + PlatformManager.instance.analytics.pageOpened('App Detail From Popular Apps Section'); await routeToPage(context, AppDetailPage(app: app)); provider.filterApps(); }, diff --git a/app/lib/pages/apps/widgets/capability_category_section.dart b/app/lib/pages/apps/widgets/capability_category_section.dart index 9b93de9d4fa..26bb96489c2 100644 --- a/app/lib/pages/apps/widgets/capability_category_section.dart +++ b/app/lib/pages/apps/widgets/capability_category_section.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -10,7 +11,6 @@ import 'package:omi/backend/schema/app.dart'; import 'package:omi/pages/apps/app_detail/app_detail.dart'; import 'package:omi/pages/apps/providers/add_app_provider.dart'; import 'package:omi/providers/app_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/app_localizations_helper.dart'; import 'package:omi/utils/other/temp.dart'; @@ -130,7 +130,7 @@ class CapabilitySectionAppItemCard extends StatelessWidget { builder: (context, isEnabled, child) { return GestureDetector( onTap: () async { - MixpanelManager().pageOpened('App Detail'); + PlatformManager.instance.analytics.pageOpened('App Detail'); await routeToPage(context, AppDetailPage(app: app)); context.read().filterApps(); }, diff --git a/app/lib/pages/apps/widgets/category_apps_page.dart b/app/lib/pages/apps/widgets/category_apps_page.dart index 6befc1dd589..cf80c80204b 100644 --- a/app/lib/pages/apps/widgets/category_apps_page.dart +++ b/app/lib/pages/apps/widgets/category_apps_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -6,7 +7,6 @@ import 'package:omi/backend/http/api/apps.dart'; import 'package:omi/backend/schema/app.dart'; import 'package:omi/pages/apps/list_item.dart'; import 'package:omi/providers/app_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/app_localizations_helper.dart'; import 'package:omi/utils/logger.dart'; @@ -32,7 +32,10 @@ class _CategoryAppsPageState extends State { _totalCount = widget.apps.length; // Track category page opened - MixpanelManager().appsCategoryPageOpened(category: widget.category.title, appCount: widget.apps.length); + PlatformManager.instance.analytics.appsCategoryPageOpened( + category: widget.category.title, + appCount: widget.apps.length, + ); _fetchCategoryApps(); } @@ -96,36 +99,36 @@ class _CategoryAppsPageState extends State { child: _isLoading ? const Center(child: CircularProgressIndicator(color: Colors.deepPurpleAccent)) : _apps.isEmpty - ? Center( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Icon(Icons.folder_open_outlined, size: 64, color: Colors.grey.shade600), - const SizedBox(height: 16), - Text( - 'No apps in this category yet', - style: TextStyle(fontSize: 18, color: Colors.grey.shade400), - ), - const SizedBox(height: 8), - Text( - 'Check back later for new apps', - style: TextStyle(fontSize: 14, color: Colors.grey.shade500), + ? Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Icon(Icons.folder_open_outlined, size: 64, color: Colors.grey.shade600), + const SizedBox(height: 16), + Text( + 'No apps in this category yet', + style: TextStyle(fontSize: 18, color: Colors.grey.shade400), + ), + const SizedBox(height: 8), + Text( + 'Check back later for new apps', + style: TextStyle(fontSize: 14, color: Colors.grey.shade500), + ), + ], ), - ], - ), - ) - : ListView.separated( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - itemCount: _apps.length, - itemBuilder: (context, index) { - final app = _apps[index]; - final allApps = context.read().apps; - final originalIndex = allApps.indexWhere((a) => a.id == app.id); + ) + : ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + itemCount: _apps.length, + itemBuilder: (context, index) { + final app = _apps[index]; + final allApps = context.read().apps; + final originalIndex = allApps.indexWhere((a) => a.id == app.id); - return AppListItem(app: app, index: originalIndex >= 0 ? originalIndex : index); - }, - separatorBuilder: (context, index) => const SizedBox(height: 8), - ), + return AppListItem(app: app, index: originalIndex >= 0 ? originalIndex : index); + }, + separatorBuilder: (context, index) => const SizedBox(height: 8), + ), ), ], ), diff --git a/app/lib/pages/apps/widgets/category_section.dart b/app/lib/pages/apps/widgets/category_section.dart index bfec500e1ce..aec2bf2d99a 100644 --- a/app/lib/pages/apps/widgets/category_section.dart +++ b/app/lib/pages/apps/widgets/category_section.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -9,7 +10,6 @@ import 'package:omi/backend/schema/app.dart'; import 'package:omi/pages/apps/app_detail/app_detail.dart'; import 'package:omi/pages/apps/providers/add_app_provider.dart'; import 'package:omi/providers/app_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/app_localizations_helper.dart'; import 'package:omi/utils/other/temp.dart'; @@ -135,7 +135,7 @@ class SectionAppItemCard extends StatelessWidget { builder: (context, isEnabled, child) { return GestureDetector( onTap: () async { - MixpanelManager().pageOpened('App Detail'); + PlatformManager.instance.analytics.pageOpened('App Detail'); await routeToPage(context, AppDetailPage(app: app)); context.read().filterApps(); }, diff --git a/app/lib/pages/apps/widgets/create_options_sheet.dart b/app/lib/pages/apps/widgets/create_options_sheet.dart index aecf3455d1c..9676d2a3f3c 100644 --- a/app/lib/pages/apps/widgets/create_options_sheet.dart +++ b/app/lib/pages/apps/widgets/create_options_sheet.dart @@ -1,8 +1,8 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:omi/pages/apps/add_app.dart'; import 'package:omi/pages/apps/add_mcp_server_page.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; @@ -24,9 +24,9 @@ class CreateOptionsSheet extends StatelessWidget { Text( context.l10n.whatWouldYouLikeToCreate, style: Theme.of(context).textTheme.headlineSmall?.copyWith( - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurface, - ), + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurface, + ), ), const SizedBox(height: 24), Card( @@ -47,7 +47,7 @@ class CreateOptionsSheet extends StatelessWidget { ), onTap: () { Navigator.pop(context); - MixpanelManager().pageOpened('Submit App'); + PlatformManager.instance.analytics.pageOpened('Submit App'); routeToPage(context, const AddAppPage()); }, ), @@ -71,7 +71,7 @@ class CreateOptionsSheet extends StatelessWidget { ), onTap: () { Navigator.pop(context); - MixpanelManager().pageOpened('Add MCP Server'); + PlatformManager.instance.analytics.pageOpened('Add MCP Server'); routeToPage(context, const AddMcpServerPage()); }, ), diff --git a/app/lib/pages/apps/widgets/filter_sheet.dart b/app/lib/pages/apps/widgets/filter_sheet.dart index 29f0796b732..4774a468e72 100644 --- a/app/lib/pages/apps/widgets/filter_sheet.dart +++ b/app/lib/pages/apps/widgets/filter_sheet.dart @@ -1,10 +1,10 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:omi/l10n/app_localizations.dart'; import 'package:omi/providers/app_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/app_localizations_helper.dart'; class FilterBottomSheet extends StatelessWidget { @@ -106,7 +106,7 @@ class FilterBottomSheet extends StatelessWidget { child: TextButton( onPressed: () { provider.clearFilters(); - MixpanelManager().appsClearFilters(); + PlatformManager.instance.analytics.appsClearFilters(); Navigator.of(context).pop(); Future.microtask(() => provider.applyFilters()); }, @@ -170,7 +170,10 @@ class FilterBottomSheet extends StatelessWidget { child: GestureDetector( onTap: () { provider.addOrRemoveFilter(filterKey, 'Rating'); - MixpanelManager().appsRatingFilter(filterKey, provider.isFilterSelected(filterKey, 'Rating')); + PlatformManager.instance.analytics.appsRatingFilter( + filterKey, + provider.isFilterSelected(filterKey, 'Rating'), + ); }, child: Container( margin: const EdgeInsets.only(right: 8), @@ -206,7 +209,10 @@ class FilterBottomSheet extends StatelessWidget { return GestureDetector( onTap: () { provider.addOrRemoveCategoryFilter(category); - MixpanelManager().appsCategoryFilter(category.title, provider.isCategoryFilterSelected(category)); + PlatformManager.instance.analytics.appsCategoryFilter( + category.title, + provider.isCategoryFilterSelected(category), + ); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), @@ -247,7 +253,10 @@ class FilterBottomSheet extends StatelessWidget { child: GestureDetector( onTap: () { provider.addOrRemoveFilter(option['key']!, 'Sort'); - MixpanelManager().appsSortFilter(option['key']!, provider.isFilterSelected(option['key']!, 'Sort')); + PlatformManager.instance.analytics.appsSortFilter( + option['key']!, + provider.isFilterSelected(option['key']!, 'Sort'), + ); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), @@ -296,7 +305,10 @@ class FilterBottomSheet extends StatelessWidget { return GestureDetector( onTap: () { provider.addOrRemoveCapabilityFilter(capability); - MixpanelManager().appsCapabilityFilter(capability.title, provider.isCapabilityFilterSelected(capability)); + PlatformManager.instance.analytics.appsCapabilityFilter( + capability.title, + provider.isCapabilityFilterSelected(capability), + ); }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), diff --git a/app/lib/pages/apps/widgets/popular_apps_section.dart b/app/lib/pages/apps/widgets/popular_apps_section.dart index 816efc58ca9..8dbeb80dfdb 100644 --- a/app/lib/pages/apps/widgets/popular_apps_section.dart +++ b/app/lib/pages/apps/widgets/popular_apps_section.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -6,7 +7,6 @@ import 'package:omi/widgets/shimmer_with_timeout.dart'; import 'package:omi/backend/schema/app.dart'; import 'package:omi/providers/app_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; // Custom notification class to communicate with parent widgets @@ -77,7 +77,7 @@ class PopularAppsSection extends StatelessWidget { appProvider.filterApps(); - MixpanelManager().pageOpened('App Detail'); + PlatformManager.instance.analytics.pageOpened('App Detail'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/app/lib/pages/capture/connect.dart b/app/lib/pages/capture/connect.dart index 5d74ee4f28c..85c52dab2d5 100644 --- a/app/lib/pages/capture/connect.dart +++ b/app/lib/pages/capture/connect.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -7,7 +8,6 @@ import 'package:omi/pages/home/page.dart'; import 'package:omi/pages/onboarding/find_device/page.dart'; import 'package:omi/pages/settings/device_settings.dart'; import 'package:omi/providers/onboarding_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/other/temp.dart'; @@ -26,11 +26,11 @@ class _ConnectDevicePageState extends State { @override void initState() { super.initState(); - MixpanelManager().connectDevicePageOpened(); + PlatformManager.instance.analytics.connectDevicePageOpened(); } void _showConnectionGuide() { - MixpanelManager().connectionGuideOpened(); + PlatformManager.instance.analytics.connectionGuideOpened(); showModalBottomSheet( context: context, backgroundColor: Colors.transparent, diff --git a/app/lib/pages/capture/widgets/widgets.dart b/app/lib/pages/capture/widgets/widgets.dart index 738d05996a5..aa8a645e7d1 100644 --- a/app/lib/pages/capture/widgets/widgets.dart +++ b/app/lib/pages/capture/widgets/widgets.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -15,7 +16,6 @@ import 'package:omi/pages/speech_profile/page.dart'; import 'package:omi/providers/capture_provider.dart'; import 'package:omi/providers/device_provider.dart'; import 'package:omi/providers/home_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/enums.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; @@ -43,7 +43,7 @@ class SpeechProfileCardWidget extends StatelessWidget { children: [ GestureDetector( onTap: () async { - MixpanelManager().pageOpened('Speech Profile Memories'); + PlatformManager.instance.analytics.pageOpened('Speech Profile Memories'); bool hasSpeakerProfile = SharedPreferencesUtil().hasSpeakerProfile; await routeToPage(context, const SpeechProfilePage()); final newHasSpeakerProfile = SharedPreferencesUtil().hasSpeakerProfile; @@ -104,15 +104,14 @@ class UpdateFirmwareCardWidget extends StatelessWidget { builder: (context, provider, child) { if (!provider.havingNewFirmware) return const SizedBox(); - final isOmiGlass = - provider.pairedDevice?.type == DeviceType.openglass || + final isOmiGlass = provider.pairedDevice?.type == DeviceType.openglass || (provider.pairedDevice?.name.toLowerCase().contains('glass') ?? false); return Stack( children: [ GestureDetector( onTap: () { - MixpanelManager().pageOpened('Update Firmware Memories'); + PlatformManager.instance.analytics.pageOpened('Update Firmware Memories'); if (isOmiGlass) { routeToPage( context, diff --git a/app/lib/pages/chat/page.dart b/app/lib/pages/chat/page.dart index 6bf02890510..17d77c7e8ed 100644 --- a/app/lib/pages/chat/page.dart +++ b/app/lib/pages/chat/page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:flutter/services.dart'; @@ -32,7 +33,6 @@ import 'package:omi/providers/message_provider.dart'; import 'package:omi/providers/usage_provider.dart'; import 'package:omi/providers/voice_recorder_provider.dart'; import 'package:omi/services/apple_health_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/widgets/dialog.dart'; @@ -189,7 +189,7 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { void _openSettingsDrawer() { HapticFeedback.mediumImpact(); - MixpanelManager().pageOpened('Settings'); + PlatformManager.instance.analytics.pageOpened('Settings'); final previousLanguage = SharedPreferencesUtil().userPrimaryLanguage; final previousSpeech = SharedPreferencesUtil().hasSpeakerProfile; final previousModel = SharedPreferencesUtil().transcriptionModel; @@ -530,8 +530,10 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { color: const Color(0xFF1f1f25), borderRadius: BorderRadius.circular(16), ), - padding: - const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + padding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 8, + ), child: Row( mainAxisSize: MainAxisSize.min, children: [ @@ -565,8 +567,11 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { _selectedContext = null; }); }, - child: const Icon(Icons.close, - size: 14, color: Colors.blue), + child: const Icon( + Icons.close, + size: 14, + color: Colors.blue, + ), ), ], ), @@ -607,8 +612,10 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { textAlignVertical: const TextAlignVertical(y: -0.35), decoration: InputDecoration( hintText: context.l10n.askAnything, - hintStyle: - const TextStyle(fontSize: 16.0, color: Colors.grey), + hintStyle: const TextStyle( + fontSize: 16.0, + color: Colors.grey, + ), focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, contentPadding: const EdgeInsets.symmetric( @@ -777,9 +784,7 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { ), ], ), - child: const Center( - child: Icon(Icons.stop, color: Colors.white, size: 18), - ), + child: const Center(child: Icon(Icons.stop, color: Colors.white, size: 18)), ), ), ), @@ -1079,7 +1084,7 @@ class ChatPageState extends State with AutomaticKeepAliveClientMixin { Future _navigateToChatAppsPage() async { if (!mounted) return; - MixpanelManager().pageOpened('Chat Apps'); + PlatformManager.instance.analytics.pageOpened('Chat Apps'); // Navigate to chat capability apps page await routeToPage( context, @@ -1627,8 +1632,10 @@ class _PlansSheetWrapperState extends State<_PlansSheetWrapper> with TickerProvi _waveController = AnimationController(vsync: this, duration: const Duration(seconds: 2))..repeat(); _arrowController = AnimationController(vsync: this, duration: const Duration(milliseconds: 800))..repeat(); _notesController = AnimationController(vsync: this, duration: const Duration(seconds: 3))..repeat(); - _arrowAnimation = - Tween(begin: 0, end: 10).animate(CurvedAnimation(parent: _arrowController, curve: Curves.easeInOut)); + _arrowAnimation = Tween( + begin: 0, + end: 10, + ).animate(CurvedAnimation(parent: _arrowController, curve: Curves.easeInOut)); } @override diff --git a/app/lib/pages/chat/widgets/ai_message.dart b/app/lib/pages/chat/widgets/ai_message.dart index ee94c4f7ea9..4e3675ca8c9 100644 --- a/app/lib/pages/chat/widgets/ai_message.dart +++ b/app/lib/pages/chat/widgets/ai_message.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:convert'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -26,7 +27,6 @@ import 'package:omi/providers/app_provider.dart'; import 'package:omi/providers/connectivity_provider.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/message_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/widgets/extensions/string.dart'; @@ -59,8 +59,7 @@ Widget _buildAppIcon(BuildContext context, String appId, {double size = 15, doub final appProvider = Provider.of(context, listen: false); final messageProvider = Provider.of(context, listen: false); // Check both public apps and user's installed chat apps (includes private MCP apps) - final app = - appProvider.apps.firstWhereOrNull((a) => a.id == appId) ?? + final app = appProvider.apps.firstWhereOrNull((a) => a.id == appId) ?? messageProvider.chatApps.firstWhereOrNull((a) => a.id == appId); if (app != null) { @@ -752,28 +751,28 @@ class _MemoriesMessageWidgetState extends State { ), ) : widget.showTypingIndicator - ? const Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisAlignment: MainAxisAlignment.start, - children: [SizedBox(width: 4), TypingIndicator(), Spacer()], - ) - : Builder( - builder: (context) { - String? selectedText; - return SelectionArea( - onSelectionChanged: (SelectedContent? selectedContent) { - selectedText = selectedContent?.plainText; - }, - contextMenuBuilder: (context, selectableRegionState) { - return omiSelectionMenuBuilder(context, selectableRegionState, (text) { - widget.onAskOmi?.call(text); - }, selectedText: selectedText); + ? const Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, + children: [SizedBox(width: 4), TypingIndicator(), Spacer()], + ) + : Builder( + builder: (context) { + String? selectedText; + return SelectionArea( + onSelectionChanged: (SelectedContent? selectedContent) { + selectedText = selectedContent?.plainText; + }, + contextMenuBuilder: (context, selectableRegionState) { + return omiSelectionMenuBuilder(context, selectableRegionState, (text) { + widget.onAskOmi?.call(text); + }, selectedText: selectedText); + }, + child: getMarkdownWidget(context, widget.messageText, onAskOmi: widget.onAskOmi), + ); }, - child: getMarkdownWidget(context, widget.messageText, onAskOmi: widget.onAskOmi), - ); - }, - ), + ), if (widget.messageText.isNotEmpty && widget.messageText != '...' && !widget.showTypingIndicator) MessageActionBar( messageText: widget.messageText, @@ -803,7 +802,7 @@ class _MemoriesMessageWidgetState extends State { if (idx != -1) { context.read().updateConversation(data.$2.id, date); var m = memProvider.groupedConversations[date]![idx]; - MixpanelManager().chatMessageConversationClicked(m); + PlatformManager.instance.analytics.chatMessageConversationClicked(m); await Navigator.of( context, ).push(MaterialPageRoute(builder: (c) => ConversationDetailPage(conversation: m))); @@ -813,7 +812,7 @@ class _MemoriesMessageWidgetState extends State { ServerConversation? m = await getConversationById(data.$2.id); if (m == null) return; (idx, date) = memProvider.addConversationWithDateGrouped(m); - MixpanelManager().chatMessageConversationClicked(m); + PlatformManager.instance.analytics.chatMessageConversationClicked(m); setState(() => conversationDetailLoading[data.$1] = false); context.read().updateConversation(m.id, date); await Navigator.of( @@ -1151,7 +1150,10 @@ class _MessageActionBarState extends State { onTap: () async { HapticFeedback.lightImpact(); await Clipboard.setData(ClipboardData(text: widget.messageText)); - MixpanelManager().track('Chat Message Copied', properties: {'message': widget.messageText}); + PlatformManager.instance.analytics.track( + 'Chat Message Copied', + properties: {'message': widget.messageText}, + ); // Implicit positive feedback - user copied the message (silent, no UI change) if (_selectedNps == null) { @@ -1210,7 +1212,10 @@ class _MessageActionBarState extends State { onTap: () async { HapticFeedback.lightImpact(); await Share.share(widget.messageText); - MixpanelManager().track('Chat Message Shared', properties: {'message': widget.messageText}); + PlatformManager.instance.analytics.track( + 'Chat Message Shared', + properties: {'message': widget.messageText}, + ); // Implicit positive feedback - user shared the message (silent, no UI change) if (_selectedNps == null) { diff --git a/app/lib/pages/conversation_capturing/page.dart b/app/lib/pages/conversation_capturing/page.dart index 592464d819c..920be9ebe06 100644 --- a/app/lib/pages/conversation_capturing/page.dart +++ b/app/lib/pages/conversation_capturing/page.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -15,7 +16,6 @@ import 'package:omi/pages/conversation_detail/widgets/name_speaker_sheet.dart'; import 'package:omi/providers/capture_provider.dart'; import 'package:omi/providers/connectivity_provider.dart'; import 'package:omi/providers/device_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/enums.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/services/wals/wal.dart'; @@ -63,7 +63,7 @@ class _ConversationCapturingPageState extends State w } else { // Phone mic await provider.streamRecording(); - MixpanelManager().phoneMicRecordingStarted(); + PlatformManager.instance.analytics.phoneMicRecordingStarted(); } } else { // Mute - pause recording with interesting haptic @@ -80,7 +80,7 @@ class _ConversationCapturingPageState extends State w } else { // Phone mic await provider.stopStreamRecording(); - MixpanelManager().phoneMicRecordingStopped(); + PlatformManager.instance.analytics.phoneMicRecordingStopped(); } } } diff --git a/app/lib/pages/conversation_detail/conversation_detail_provider.dart b/app/lib/pages/conversation_detail/conversation_detail_provider.dart index fedae6a446c..d57303cf6be 100644 --- a/app/lib/pages/conversation_detail/conversation_detail_provider.dart +++ b/app/lib/pages/conversation_detail/conversation_detail_provider.dart @@ -16,7 +16,6 @@ import 'package:omi/backend/schema/structured.dart'; import 'package:omi/backend/schema/transcript_segment.dart'; import 'package:omi/providers/app_provider.dart'; import 'package:omi/providers/conversation_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/platform/platform_manager.dart'; @@ -318,7 +317,7 @@ class ConversationDetailProvider extends ChangeNotifier with MessageNotifierMixi try { var updatedConversation = await reProcessConversationServer(conversation.id, appId: appId); if (_isDisposed) return false; - MixpanelManager().reProcessConversation(conversation); + PlatformManager.instance.analytics.reProcessConversation(conversation); updateReprocessConversationLoadingState(false); updateReprocessConversationId(''); if (updatedConversation == null) { @@ -349,7 +348,7 @@ class ConversationDetailProvider extends ChangeNotifier with MessageNotifierMixi return true; } catch (err, stacktrace) { print(err); - var conversationReporting = MixpanelManager().getConversationEventProperties(conversation); + var conversationReporting = PlatformManager.instance.analytics.getConversationEventProperties(conversation); await PlatformManager.instance.crashReporter.reportCrash( err, stacktrace, @@ -432,9 +431,8 @@ class ConversationDetailProvider extends ChangeNotifier with MessageNotifierMixi if (_isDisposed) return; // Preserve locally added apps that aren't in the API response yet - final locallyAddedApps = _cachedEnabledConversationApps - .where((app) => _locallyAddedAppIds.contains(app.id)) - .toList(); + final locallyAddedApps = + _cachedEnabledConversationApps.where((app) => _locallyAddedAppIds.contains(app.id)).toList(); _cachedEnabledConversationApps.clear(); _cachedEnabledConversationApps.addAll(apps); diff --git a/app/lib/pages/conversation_detail/page.dart b/app/lib/pages/conversation_detail/page.dart index f95d4aebd32..760249f428a 100644 --- a/app/lib/pages/conversation_detail/page.dart +++ b/app/lib/pages/conversation_detail/page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -23,7 +24,6 @@ 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/services/audio_download_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/other/temp.dart'; @@ -177,7 +177,7 @@ class _ConversationDetailPageState extends State with Ti selectedTab = ConversationTab.summary; } if (tabName != null) { - MixpanelManager().conversationDetailTabChanged(tabName); + PlatformManager.instance.analytics.conversationDetailTabChanged(tabName); } if (_searchQuery.isNotEmpty) { _updateSearchResults(); @@ -314,7 +314,10 @@ class _ConversationDetailPageState extends State with Ti void _handleMenuSelection(BuildContext context, String value, ConversationDetailProvider provider) async { // Track the menu action selection - MixpanelManager().conversationThreeDotsMenuActionSelected(conversationId: provider.conversation.id, action: value); + PlatformManager.instance.analytics.conversationThreeDotsMenuActionSelected( + conversationId: provider.conversation.id, + action: value, + ); switch (value) { case 'copy_transcript': @@ -357,9 +360,7 @@ class _ConversationDetailPageState extends State with Ti break; case 'copy_conversation_id': Clipboard.setData(ClipboardData(text: provider.conversation.id)); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.conversationIdCopied)), - ); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.conversationIdCopied))); HapticFeedback.lightImpact(); break; case 'delete': @@ -421,7 +422,10 @@ class _ConversationDetailPageState extends State with Ti final startTime = DateTime.now(); // Track share start - MixpanelManager().audioShareStarted(conversationId: provider.conversation.id, audioFileCount: audioFileCount); + PlatformManager.instance.analytics.audioShareStarted( + conversationId: provider.conversation.id, + audioFileCount: audioFileCount, + ); AudioDownloadState currentState = AudioDownloadState.preparing; double currentProgress = 0.0; @@ -483,7 +487,7 @@ class _ConversationDetailPageState extends State with Ti // Track successful completion final durationSeconds = DateTime.now().difference(startTime).inSeconds; - MixpanelManager().audioShareCompleted( + PlatformManager.instance.analytics.audioShareCompleted( conversationId: provider.conversation.id, audioFileCount: audioFileCount, wasCombined: audioFileCount > 1, @@ -497,7 +501,7 @@ class _ConversationDetailPageState extends State with Ti } // Track failure (no audio available) - MixpanelManager().audioShareFailed( + PlatformManager.instance.analytics.audioShareFailed( conversationId: provider.conversation.id, errorMessage: 'No audio files available', ); @@ -506,7 +510,10 @@ class _ConversationDetailPageState extends State with Ti Logger.debug('Error downloading audio: $e'); // Track failure - MixpanelManager().audioShareFailed(conversationId: provider.conversation.id, errorMessage: e.toString()); + PlatformManager.instance.analytics.audioShareFailed( + conversationId: provider.conversation.id, + errorMessage: e.toString(), + ); currentState = AudioDownloadState.error; updateSheet?.call(() {}); @@ -665,7 +672,7 @@ class _ConversationDetailPageState extends State with Ti provider.conversation, ); // Track star/unstar action - MixpanelManager().conversationStarToggled( + PlatformManager.instance.analytics.conversationStarToggled( conversation: provider.conversation, starred: newStarredState, source: 'detail_page_button', @@ -731,7 +738,7 @@ class _ConversationDetailPageState extends State with Ti } provider.updateVisibilityLocally(ConversationVisibility.shared); // Track share event - MixpanelManager().conversationShared( + PlatformManager.instance.analytics.conversationShared( conversation: provider.conversation, shareMethod: 'url_share', ); @@ -790,7 +797,7 @@ class _ConversationDetailPageState extends State with Ti _searchFocusNode.unfocus(); } else { _searchFocusNode.requestFocus(); - MixpanelManager().conversationDetailSearchClicked( + PlatformManager.instance.analytics.conversationDetailSearchClicked( conversationId: provider.conversation.id, ); } @@ -855,7 +862,7 @@ class _ConversationDetailPageState extends State with Ti buttonBuilder: (context, showMenu) => GestureDetector( onTap: () { HapticFeedback.mediumImpact(); - MixpanelManager().conversationThreeDotsMenuOpened( + PlatformManager.instance.analytics.conversationThreeDotsMenuOpened( conversationId: provider.conversation.id, ); showMenu(); @@ -1251,7 +1258,7 @@ class _ConversationDetailPageState extends State with Ti if (value.isNotEmpty) { // Track search query with results final provider = Provider.of(context, listen: false); - MixpanelManager().conversationDetailSearchQueryEntered( + PlatformManager.instance.analytics.conversationDetailSearchQueryEntered( conversationId: provider.conversation.id, query: value, resultsCount: _totalSearchResults, @@ -1326,10 +1333,10 @@ class _SummaryTabState extends State with AutomaticKeepAliveClientMi } return true; }, - onEditStarted: (_) => MixpanelManager().editSummaryStarted(), - onEditCancelled: (_) => MixpanelManager().editSummaryCancelled(), + onEditStarted: (_) => PlatformManager.instance.analytics.editSummaryStarted(), + onEditCancelled: (_) => PlatformManager.instance.analytics.editSummaryCancelled(), onSaveSummary: (appId, newContent) { - MixpanelManager().editSummarySaved(); + PlatformManager.instance.analytics.editSummarySaved(); context.read().saveEditingSummary(appId, newContent); }, ), @@ -1433,7 +1440,7 @@ class _TranscriptWidgetsState extends State with AutomaticKee segment.personId != null ? SharedPreferencesUtil().getPersonById(segment.personId!) : null; final speakerName = person?.name ?? context.l10n.speakerWithId('${TranscriptSegment.getDisplaySpeakerId(segment.speakerId, segments)}'); - MixpanelManager().editSegmentTextStarted(); + PlatformManager.instance.analytics.editSegmentTextStarted(); bool saved = false; showEditSegmentBottomSheet( context, @@ -1441,11 +1448,11 @@ class _TranscriptWidgetsState extends State with AutomaticKee speakerName: speakerName, onSave: (newText) { saved = true; - MixpanelManager().editSegmentTextSaved(); + PlatformManager.instance.analytics.editSegmentTextSaved(); provider.saveEditingSegmentText(segmentIndex, newText); }, onDismissed: () { - if (!saved) MixpanelManager().editSegmentTextCancelled(); + if (!saved) PlatformManager.instance.analytics.editSegmentTextCancelled(); }, ); }, @@ -1480,7 +1487,9 @@ class _TranscriptWidgetsState extends State with AutomaticKee } } - MixpanelManager().taggedSegment(finalPersonId == 'user' ? 'User' : 'User Person'); + PlatformManager.instance.analytics.taggedSegment( + finalPersonId == 'user' ? 'User' : 'User Person', + ); for (final segmentId in segmentIds) { final segmentIndex = provider.conversation.transcriptSegments.indexWhere( @@ -1652,14 +1661,14 @@ class _ActionItemDetailWidgetState extends State { ); if (currentIndex != -1) { if (newValue) { - MixpanelManager().checkedActionItem(provider.conversation, currentIndex); + PlatformManager.instance.analytics.checkedActionItem(provider.conversation, currentIndex); if (!await _appReviewService.hasCompletedFirstActionItem()) { await _appReviewService.markFirstActionItemCompleted(); _appReviewService.showReviewPromptIfNeeded(context, isProcessingFirstConversation: false); } } else { - MixpanelManager().uncheckedActionItem(provider.conversation, currentIndex); + PlatformManager.instance.analytics.uncheckedActionItem(provider.conversation, currentIndex); } } } catch (e) { diff --git a/app/lib/pages/conversation_detail/widgets.dart b/app/lib/pages/conversation_detail/widgets.dart index cbe9c456586..56c762ea8f3 100644 --- a/app/lib/pages/conversation_detail/widgets.dart +++ b/app/lib/pages/conversation_detail/widgets.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -28,7 +29,6 @@ import 'package:omi/pages/conversation_detail/widgets/summarized_apps_sheet.dart import 'package:omi/pages/conversations/widgets/move_to_folder_sheet.dart'; import 'package:omi/pages/settings/developer.dart'; import 'package:omi/providers/folder_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/folders/folder_icon_mapper.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/utils/other/time_utils.dart'; @@ -178,7 +178,7 @@ class GetSummaryWidgets extends StatelessWidget { HapticFeedback.selectionClick(); // Track folder chip clicked - MixpanelManager().conversationDetailFolderChipClicked( + PlatformManager.instance.analytics.conversationDetailFolderChipClicked( conversationId: conversationId, currentFolderId: currentFolderId, ); @@ -197,7 +197,7 @@ class GetSummaryWidgets extends StatelessWidget { context.read().updateFolderIdLocally(newFolderId); // Track conversation moved to folder - MixpanelManager().conversationMovedToFolder( + PlatformManager.instance.analytics.conversationMovedToFolder( conversationId: conversationId, fromFolderId: currentFolderId, toFolderId: newFolderId, @@ -323,7 +323,7 @@ class GetSummaryWidgets extends StatelessWidget { provider.updateVisibilityLocally(previousVisibility); return; } - MixpanelManager().conversationVisibilityChanged( + PlatformManager.instance.analytics.conversationVisibilityChanged( conversationId: conversation.id, fromVisibility: previousVisibility.value, toVisibility: ConversationVisibility.private_.value, @@ -353,7 +353,7 @@ class GetSummaryWidgets extends StatelessWidget { provider.updateVisibilityLocally(previousVisibility); return; } - MixpanelManager().conversationVisibilityChanged( + PlatformManager.instance.analytics.conversationVisibilityChanged( conversationId: conversation.id, fromVisibility: previousVisibility.value, toVisibility: ConversationVisibility.shared.value, @@ -495,7 +495,10 @@ class ActionItemsListWidget extends StatelessWidget { duration: const Duration(seconds: 2), ), ); - MixpanelManager().copiedConversationDetails(provider.conversation, source: 'Action Items'); + PlatformManager.instance.analytics.copiedConversationDetails( + provider.conversation, + source: 'Action Items', + ); }, icon: const Icon(Icons.copy_rounded, color: Colors.white, size: 20), ), @@ -522,7 +525,7 @@ class ActionItemsListWidget extends StatelessWidget { var tempIdx = idx; provider.deleteActionItem(idx); provider.deleteActionItemPermanently(tempItem, tempIdx); - MixpanelManager().deletedActionItem(provider.conversation); + PlatformManager.instance.analytics.deletedActionItem(provider.conversation); // ScaffoldMessenger.of(context) // .showSnackBar( // SnackBar( @@ -541,7 +544,7 @@ class ActionItemsListWidget extends StatelessWidget { // .then((reason) { // if (reason != SnackBarClosedReason.action) { // provider.deleteActionItemPermanently(tempItem, tempIdx); - // MixpanelManager().deletedActionItem(provider.conversation); + // PlatformManager.instance.analytics.deletedActionItem(provider.conversation); // } // }); }, @@ -563,9 +566,9 @@ class ActionItemsListWidget extends StatelessWidget { context.read().updateActionItemState(value, idx); setConversationActionItemState(provider.conversation.id, [idx], [value]); if (value) { - MixpanelManager().checkedActionItem(provider.conversation, idx); + PlatformManager.instance.analytics.checkedActionItem(provider.conversation, idx); } else { - MixpanelManager().uncheckedActionItem(provider.conversation, idx); + PlatformManager.instance.analytics.uncheckedActionItem(provider.conversation, idx); } } }, @@ -840,7 +843,7 @@ class _AppResultDetailWidgetState extends State { GestureDetector( onTap: () async { if (widget.app != null) { - MixpanelManager().pageOpened('App Detail'); + PlatformManager.instance.analytics.pageOpened('App Detail'); await routeToPage(context, AppDetailPage(app: widget.app!)); } }, diff --git a/app/lib/pages/conversation_detail/widgets/create_template_bottom_sheet.dart b/app/lib/pages/conversation_detail/widgets/create_template_bottom_sheet.dart index 1aca2b7dbe1..04877226141 100644 --- a/app/lib/pages/conversation_detail/widgets/create_template_bottom_sheet.dart +++ b/app/lib/pages/conversation_detail/widgets/create_template_bottom_sheet.dart @@ -1,6 +1,7 @@ import 'dart:io'; import 'dart:ui' as ui; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -15,7 +16,6 @@ import 'package:omi/pages/conversation_detail/conversation_detail_provider.dart' import 'package:omi/pages/conversation_detail/widgets/summarized_apps_sheet.dart'; import 'package:omi/providers/app_provider.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; class CreateTemplateBottomSheet extends StatefulWidget { @@ -137,7 +137,7 @@ class _CreateTemplateBottomSheetState extends State { if (submitResult.$1) { // Success - MixpanelManager().quickTemplateCreated( + PlatformManager.instance.analytics.quickTemplateCreated( conversationId: widget.conversationId ?? '', appName: name, isPublic: _isPublic, diff --git a/app/lib/pages/conversation_detail/widgets/share_to_contacts_sheet.dart b/app/lib/pages/conversation_detail/widgets/share_to_contacts_sheet.dart index e15780fa52e..a425601f759 100644 --- a/app/lib/pages/conversation_detail/widgets/share_to_contacts_sheet.dart +++ b/app/lib/pages/conversation_detail/widgets/share_to_contacts_sheet.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -12,7 +13,6 @@ import 'package:omi/backend/http/api/conversations.dart'; import 'package:omi/backend/schema/conversation.dart'; import 'package:omi/pages/conversation_detail/conversation_detail_provider.dart'; import 'package:omi/widgets/extensions/string.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; /// Contact with phone number for sharing @@ -58,7 +58,7 @@ class _ShareToContactsBottomSheetState extends State void initState() { super.initState(); // Track sheet opened - MixpanelManager().shareToContactsSheetOpened(widget.conversation.id); + PlatformManager.instance.analytics.shareToContactsSheetOpened(widget.conversation.id); _loadContacts(); } @@ -153,7 +153,7 @@ class _ShareToContactsBottomSheetState extends State // Track selection changes final selectedCount = _selectedContacts.length; if (selectedCount > 0) { - MixpanelManager().shareToContactsSelected(widget.conversation.id, selectedCount); + PlatformManager.instance.analytics.shareToContactsSelected(widget.conversation.id, selectedCount); } } @@ -204,7 +204,7 @@ class _ShareToContactsBottomSheetState extends State // Launch native SMS app if (await canLaunchUrl(smsUri)) { // Track SMS opened - MixpanelManager().shareToContactsSmsOpened(widget.conversation.id, selected.length); + PlatformManager.instance.analytics.shareToContactsSmsOpened(widget.conversation.id, selected.length); HapticFeedback.mediumImpact(); Navigator.of(context).pop(); await launchUrl(smsUri); @@ -372,8 +372,8 @@ class _ShareToContactsBottomSheetState extends State _selectedContacts.isEmpty ? context.l10n.selectContactsToShare : _selectedContacts.length > 1 - ? context.l10n.shareWithContactsCount(_selectedContacts.length) - : context.l10n.shareWithContactCount(_selectedContacts.length), + ? context.l10n.shareWithContactsCount(_selectedContacts.length) + : context.l10n.shareWithContactCount(_selectedContacts.length), style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600, color: Colors.white), ), ), diff --git a/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart b/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart index f1e380828dd..726b7e837d3 100644 --- a/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart +++ b/app/lib/pages/conversation_detail/widgets/summarized_apps_sheet.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -14,7 +15,6 @@ import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/pages/conversation_detail/conversation_detail_provider.dart'; import 'package:omi/pages/conversation_detail/widgets/create_template_bottom_sheet.dart'; import 'package:omi/providers/app_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/widgets/extensions/string.dart'; @@ -36,7 +36,7 @@ class SummarizedAppsBottomSheet extends StatelessWidget { final currentAppId = summarizedApp?.appId; final conversationId = provider.conversation.id; - MixpanelManager().summarizedAppSheetViewed( + PlatformManager.instance.analytics.summarizedAppSheetViewed( conversationId: conversationId, currentSummarizedAppId: currentAppId, ); @@ -355,7 +355,7 @@ class _AppsListState extends State<_AppsList> { final previousAppId = provider.getSummarizedApp()?.appId; final conversationId = provider.conversation.id; - MixpanelManager().summarizedAppSelected( + PlatformManager.instance.analytics.summarizedAppSelected( conversationId: conversationId, selectedAppId: app.id, previousAppId: previousAppId, @@ -401,7 +401,7 @@ class _AppsListState extends State<_AppsList> { } // Track analytics - MixpanelManager().summarizedAppSelected( + PlatformManager.instance.analytics.summarizedAppSelected( conversationId: conversationId, selectedAppId: app.id, previousAppId: conversationProvider.getSummarizedApp()?.appId, @@ -647,8 +647,7 @@ class _AppListItemState extends State<_AppListItem> { Widget _buildTrailingWidget() { // Check if this app is currently being processed - final isProcessing = - widget.provider != null && + final isProcessing = widget.provider != null && widget.provider!.loadingReprocessConversation && widget.provider!.selectedAppForReprocessing?.id == widget.app.id; @@ -719,7 +718,7 @@ class _CreateTemplateListItem extends StatelessWidget { trailing: const Icon(Icons.arrow_forward_ios, color: Colors.white, size: 16), onTap: () { final conversationId = context.read().conversation.id; - MixpanelManager().summarizedAppCreateTemplateClicked(conversationId: conversationId); + PlatformManager.instance.analytics.summarizedAppCreateTemplateClicked(conversationId: conversationId); // Close the current bottom sheet first Navigator.pop(context); @@ -757,7 +756,7 @@ class _EnableAppsListItem extends StatelessWidget { onTap: () { Navigator.pop(context); final conversationId = context.read().conversation.id; - MixpanelManager().summarizedAppEnableAppsClicked(conversationId: conversationId); + PlatformManager.instance.analytics.summarizedAppEnableAppsClicked(conversationId: conversationId); // Navigate to Summary (memories) capability apps page final appProvider = context.read(); @@ -770,7 +769,7 @@ class _EnableAppsListItem extends StatelessWidget { apps: memoriesApps, ), ); - MixpanelManager().pageOpened('Summary Apps'); + PlatformManager.instance.analytics.pageOpened('Summary Apps'); }, ), Divider(height: 1, thickness: 0.5, color: Colors.grey.withValues(alpha: 0.2), indent: 56, endIndent: 16), diff --git a/app/lib/pages/conversations/widgets/conversation_list_item.dart b/app/lib/pages/conversations/widgets/conversation_list_item.dart index f77a9c630cb..610d481b771 100644 --- a/app/lib/pages/conversations/widgets/conversation_list_item.dart +++ b/app/lib/pages/conversations/widgets/conversation_list_item.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:ui'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -16,7 +17,6 @@ import 'package:omi/pages/settings/usage_page.dart'; import 'package:omi/providers/connectivity_provider.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/usage_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/utils/other/time_utils.dart'; @@ -99,7 +99,7 @@ class _ConversationListItemState extends State { if (widget.conversation.isLocked) { if (!context.read().showSubscriptionUI) return; - MixpanelManager().paywallOpened('Conversation List Item'); + PlatformManager.instance.analytics.paywallOpened('Conversation List Item'); routeToPage(context, const UsagePage(showUpgradeDialog: true)); return; } @@ -110,14 +110,14 @@ class _ConversationListItemState extends State { String searchQuery = provider.previousQuery; if (searchQuery.isNotEmpty) { // Track conversation opened from search - MixpanelManager().conversationOpenedFromSearch( + PlatformManager.instance.analytics.conversationOpenedFromSearch( conversation: widget.conversation, searchQuery: searchQuery, conversationIndexInResults: widget.conversationIdx, ); } else { // Track normal conversation list item click with time difference - MixpanelManager().conversationListItemClickedWithTimeDifference( + PlatformManager.instance.analytics.conversationListItemClickedWithTimeDifference( conversation: widget.conversation, conversationIndex: widget.conversationIdx, hoursSinceConversation: hoursSinceConversation, @@ -292,7 +292,7 @@ class _ConversationListItemState extends State { onDismissed: (direction) async { var conversation = widget.conversation; var conversationIdx = widget.conversationIdx; - MixpanelManager().conversationSwipedToDelete(conversation); + PlatformManager.instance.analytics.conversationSwipedToDelete(conversation); provider.deleteConversationLocally(conversation, conversationIdx, widget.date); }, child: Padding( diff --git a/app/lib/pages/conversations/widgets/create_folder_sheet.dart b/app/lib/pages/conversations/widgets/create_folder_sheet.dart index 0addea93d68..7a000d078f9 100644 --- a/app/lib/pages/conversations/widgets/create_folder_sheet.dart +++ b/app/lib/pages/conversations/widgets/create_folder_sheet.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -6,7 +7,6 @@ import 'package:provider/provider.dart'; import 'package:omi/backend/schema/folder.dart'; import 'package:omi/providers/folder_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/folders/folder_icon_mapper.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/responsive/responsive_helper.dart'; @@ -93,7 +93,7 @@ class _CreateFolderBottomSheetState extends State { success = result != null; if (success) { - MixpanelManager().folderUpdated(folderId: widget.folderToEdit!.id, folderName: name); + PlatformManager.instance.analytics.folderUpdated(folderId: widget.folderToEdit!.id, folderName: name); } } else { final result = await folderProvider.createFolder( @@ -105,7 +105,12 @@ class _CreateFolderBottomSheetState extends State { success = result != null; if (result != null) { - MixpanelManager().folderCreated(folderId: result.id, folderName: name, icon: _selectedIcon, color: colorHex); + PlatformManager.instance.analytics.folderCreated( + folderId: result.id, + folderName: name, + icon: _selectedIcon, + color: colorHex, + ); } } diff --git a/app/lib/pages/conversations/widgets/daily_score_widget.dart b/app/lib/pages/conversations/widgets/daily_score_widget.dart index aed98eaeca5..b2bf9ecf890 100644 --- a/app/lib/pages/conversations/widgets/daily_score_widget.dart +++ b/app/lib/pages/conversations/widgets/daily_score_widget.dart @@ -1,12 +1,12 @@ import 'dart:math' as math; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:omi/pages/action_items/widgets/action_item_form_sheet.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/pages/conversations/widgets/goals_widget.dart'; import 'package:omi/providers/action_items_provider.dart'; import 'package:omi/providers/goals_provider.dart'; @@ -113,7 +113,9 @@ class DailyScoreWidgetState extends State { GestureDetector( onTap: () { HapticFeedback.lightImpact(); - MixpanelManager().dailyScoreCtaTapped(ctaType: goals.isEmpty ? 'add_goal' : 'new_task'); + PlatformManager.instance.analytics.dailyScoreCtaTapped( + ctaType: goals.isEmpty ? 'add_goal' : 'new_task', + ); if (goals.isEmpty) { _addGoal(); } else { @@ -186,7 +188,7 @@ class DailyScoreWidgetState extends State { child: GestureDetector( onTap: () { HapticFeedback.lightImpact(); - MixpanelManager().dailyScoreHelpTapped(); + PlatformManager.instance.analytics.dailyScoreHelpTapped(); _showScoreDetails(context, score, completedTasks, totalTasks); }, child: Container( diff --git a/app/lib/pages/conversations/widgets/daily_summaries_list.dart b/app/lib/pages/conversations/widgets/daily_summaries_list.dart index 4f987b521c0..71219c22ae6 100644 --- a/app/lib/pages/conversations/widgets/daily_summaries_list.dart +++ b/app/lib/pages/conversations/widgets/daily_summaries_list.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -6,7 +7,6 @@ import 'package:omi/widgets/shimmer_with_timeout.dart'; import 'package:omi/backend/http/api/users.dart'; import 'package:omi/backend/schema/daily_summary.dart'; import 'package:omi/pages/settings/daily_summary_detail_page.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/ui_guidelines.dart'; @@ -61,7 +61,11 @@ class _DailySummariesListState extends State { void _openSummary(DailySummary summary) { // Track recap card click final cardIndex = _summaries.indexOf(summary); - MixpanelManager().recapSummaryCardClicked(summaryId: summary.id, date: summary.date, cardIndex: cardIndex); + PlatformManager.instance.analytics.recapSummaryCardClicked( + summaryId: summary.id, + date: summary.date, + cardIndex: cardIndex, + ); Navigator.push( context, diff --git a/app/lib/pages/conversations/widgets/folder_tabs.dart b/app/lib/pages/conversations/widgets/folder_tabs.dart index 78a12fb5370..ecd72229cd1 100644 --- a/app/lib/pages/conversations/widgets/folder_tabs.dart +++ b/app/lib/pages/conversations/widgets/folder_tabs.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -9,7 +10,6 @@ import 'package:omi/backend/schema/folder.dart'; import 'package:omi/pages/conversations/widgets/create_folder_sheet.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/folder_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/folders/folder_icon_mapper.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/responsive/responsive_helper.dart'; @@ -91,7 +91,7 @@ class _FolderTabsState extends State { skipFolderTracking: true, onTap: () { // Track starred filter toggle with the NEW state (opposite of current) - MixpanelManager().starredFilterToggled( + PlatformManager.instance.analytics.starredFilterToggled( enabled: !widget.showStarredOnly, selectedFolderId: widget.selectedFolderId, ); @@ -215,7 +215,7 @@ class _FolderTab extends StatelessWidget { HapticFeedback.mediumImpact(); // Track context menu opened - MixpanelManager().folderContextMenuOpened(folderId: folder!.id, folderName: folder!.name); + PlatformManager.instance.analytics.folderContextMenuOpened(folderId: folder!.id, folderName: folder!.name); showModalBottomSheet( context: context, @@ -234,7 +234,7 @@ class _FolderTab extends StatelessWidget { onTap: () { // Track folder selection (skip for Starred tab which has its own tracking) if (!skipFolderTracking) { - MixpanelManager().folderSelected(folderId: folder?.id, folderName: label); + PlatformManager.instance.analytics.folderSelected(folderId: folder?.id, folderName: label); } onTap(); }, @@ -280,7 +280,7 @@ class _AddFolderButton extends StatelessWidget { child: GestureDetector( onTap: () async { HapticFeedback.mediumImpact(); - MixpanelManager().createFolderButtonClicked(); + PlatformManager.instance.analytics.createFolderButtonClicked(); await showCreateFolderBottomSheet(context); }, child: Container( @@ -324,7 +324,7 @@ class _FolderContextMenu extends StatelessWidget { Navigator.pop(ctx); // Track folder deletion - MixpanelManager().folderDeleted( + PlatformManager.instance.analytics.folderDeleted( folderId: folder.id, folderName: folder.name, conversationCount: folder.conversationCount, diff --git a/app/lib/pages/conversations/widgets/goals_widget.dart b/app/lib/pages/conversations/widgets/goals_widget.dart index ccd0bc408b7..233f376afd2 100644 --- a/app/lib/pages/conversations/widgets/goals_widget.dart +++ b/app/lib/pages/conversations/widgets/goals_widget.dart @@ -1,5 +1,6 @@ import 'dart:convert'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,7 +9,6 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'package:omi/backend/http/api/goals.dart'; import 'package:omi/providers/goals_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; /// Multi-goal widget supporting up to 3 goals with minimalistic UI @@ -202,7 +202,7 @@ class GoalsWidgetState extends State with WidgetsBindingObserver { return; } - MixpanelManager().goalAddButtonTapped(source: 'home'); + PlatformManager.instance.analytics.goalAddButtonTapped(source: 'home'); HapticFeedback.lightImpact(); _titleController.clear(); _currentController.text = '0'; @@ -277,7 +277,7 @@ class GoalsWidgetState extends State with WidgetsBindingObserver { return GestureDetector( onTap: () { HapticFeedback.selectionClick(); - MixpanelManager().goalEmojiSelected(emoji: emoji); + PlatformManager.instance.analytics.goalEmojiSelected(emoji: emoji); setSheetState(() => _selectedEmoji = emoji); }, child: Container( @@ -287,9 +287,8 @@ class GoalsWidgetState extends State with WidgetsBindingObserver { decoration: BoxDecoration( color: isSelected ? Colors.white.withOpacity(0.15) : Colors.white.withOpacity(0.05), borderRadius: BorderRadius.circular(12), - border: isSelected - ? Border.all(color: Colors.white.withOpacity(0.3), width: 2) - : null, + border: + isSelected ? Border.all(color: Colors.white.withOpacity(0.3), width: 2) : null, ), child: Center(child: Text(emoji, style: const TextStyle(fontSize: 22))), ), @@ -391,7 +390,11 @@ class GoalsWidgetState extends State with WidgetsBindingObserver { Expanded( child: TextButton( onPressed: () async { - MixpanelManager().goalDeleted(goalId: existingGoal.id, source: 'home', method: 'button'); + PlatformManager.instance.analytics.goalDeleted( + goalId: existingGoal.id, + source: 'home', + method: 'button', + ); Navigator.pop(context); await _deleteGoal(existingGoal); }, @@ -442,7 +445,7 @@ class GoalsWidgetState extends State with WidgetsBindingObserver { // Update existing goal via provider await goalsProvider.updateGoal(existingGoal.id, title: title, currentValue: current, targetValue: target); - MixpanelManager().goalUpdated(goalId: existingGoal.id, source: 'home'); + PlatformManager.instance.analytics.goalUpdated(goalId: existingGoal.id, source: 'home'); // Save emoji setState(() { _goalEmojis[existingGoal.id] = _selectedEmoji; @@ -458,7 +461,7 @@ class GoalsWidgetState extends State with WidgetsBindingObserver { ); if (created != null) { - MixpanelManager().goalCreated( + PlatformManager.instance.analytics.goalCreated( goalId: created.id, titleLength: title.length, targetValue: target, @@ -568,12 +571,12 @@ class GoalsWidgetState extends State with WidgetsBindingObserver { child: const Icon(Icons.delete, color: Colors.white), ), onDismissed: (direction) async { - MixpanelManager().goalDeleted(goalId: goal.id, source: 'home', method: 'swipe'); + PlatformManager.instance.analytics.goalDeleted(goalId: goal.id, source: 'home', method: 'swipe'); await _deleteGoal(goal); }, child: GestureDetector( onTap: () { - MixpanelManager().goalItemTappedForEdit(goalId: goal.id, source: 'home'); + PlatformManager.instance.analytics.goalItemTappedForEdit(goalId: goal.id, source: 'home'); _editGoal(goal); }, child: Container( @@ -629,7 +632,7 @@ class GoalsWidgetState extends State with WidgetsBindingObserver { divisions: goal.targetValue >= 1 ? goal.targetValue.toInt() : null, onChanged: (value) => _updateGoalProgressUI(goal, value), onChangeEnd: (value) { - MixpanelManager().goalProgressChanged( + PlatformManager.instance.analytics.goalProgressChanged( goalId: goal.id, oldValue: goal.currentValue, newValue: value, diff --git a/app/lib/pages/conversations/widgets/processing_capture.dart b/app/lib/pages/conversations/widgets/processing_capture.dart index 083b01aa272..9200fdbd648 100644 --- a/app/lib/pages/conversations/widgets/processing_capture.dart +++ b/app/lib/pages/conversations/widgets/processing_capture.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -13,7 +14,6 @@ import 'package:omi/pages/capture/widgets/widgets.dart'; import 'package:omi/pages/conversations/widgets/capture.dart'; import 'package:omi/pages/processing_conversations/page.dart'; import 'package:omi/providers/capture_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/enums.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/other/temp.dart'; @@ -54,7 +54,7 @@ class _ConversationCaptureWidgetState extends State { return GestureDetector( onTap: () async { if (provider.segments.isEmpty && provider.photos.isEmpty) return; - MixpanelManager().liveTranscriptCardClicked( + PlatformManager.instance.analytics.liveTranscriptCardClicked( hasSegments: provider.segments.isNotEmpty, hasPhotos: provider.photos.isNotEmpty, segmentCount: provider.segments.length, @@ -108,14 +108,14 @@ class _ConversationCaptureWidgetState extends State { _isPhoneMicPaused = true; }); await provider.stopStreamRecording(); - MixpanelManager().phoneMicRecordingStopped(); + PlatformManager.instance.analytics.phoneMicRecordingStopped(); } else if (_isPhoneMicPaused) { // Resume recording setState(() { _isPhoneMicPaused = false; }); await provider.streamRecording(); - MixpanelManager().phoneMicRecordingStarted(); + PlatformManager.instance.analytics.phoneMicRecordingStarted(); } else if (recordingState == RecordingState.initialising) { Logger.debug('initialising, have to wait'); } else { @@ -123,7 +123,7 @@ class _ConversationCaptureWidgetState extends State { _isPhoneMicPaused = false; }); await provider.streamRecording(); - MixpanelManager().phoneMicRecordingStarted(); + PlatformManager.instance.analytics.phoneMicRecordingStarted(); } } } @@ -395,13 +395,13 @@ class _ConversationCaptureWidgetState extends State { HapticFeedback.heavyImpact(); await Future.delayed(const Duration(milliseconds: 80)); HapticFeedback.lightImpact(); - MixpanelManager().recordingMuteToggled( + PlatformManager.instance.analytics.recordingMuteToggled( isMuted: true, recordingType: isDeviceRecording ? 'device' : 'phone_mic', ); } else { HapticFeedback.mediumImpact(); - MixpanelManager().recordingMuteToggled( + PlatformManager.instance.analytics.recordingMuteToggled( isMuted: false, recordingType: isDeviceRecording ? 'device' : 'phone_mic', ); diff --git a/app/lib/pages/conversations/widgets/search_widget.dart b/app/lib/pages/conversations/widgets/search_widget.dart index 43840aa669c..6926e508599 100644 --- a/app/lib/pages/conversations/widgets/search_widget.dart +++ b/app/lib/pages/conversations/widgets/search_widget.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,7 +9,6 @@ import 'package:provider/provider.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/home_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/debouncer.dart'; import 'package:omi/utils/responsive/responsive_helper.dart'; @@ -105,7 +105,7 @@ class _SearchWidgetState extends State { final provider = Provider.of(context, listen: false); Navigator.of(context).pop(); await provider.clearDateFilter(); - MixpanelManager().calendarFilterCleared(); + PlatformManager.instance.analytics.calendarFilterCleared(); } else { Navigator.of(context).pop(); } @@ -122,7 +122,7 @@ class _SearchWidgetState extends State { final provider = Provider.of(context, listen: false); Navigator.of(context).pop(); await provider.filterConversationsByDate(selectedDate); - MixpanelManager().calendarFilterApplied(selectedDate); + PlatformManager.instance.analytics.calendarFilterApplied(selectedDate); }, child: Text( context.l10n.done, @@ -171,7 +171,7 @@ class _SearchWidgetState extends State { controller: searchController, focusNode: context.read().convoSearchFieldFocusNode, onTap: () { - MixpanelManager().searchBarFocused(); + PlatformManager.instance.analytics.searchBarFocused(); }, onChanged: (value) { var provider = Provider.of(context, listen: false); @@ -179,7 +179,7 @@ class _SearchWidgetState extends State { await provider.searchConversations(value); if (value.isNotEmpty) { // Track search query with results count - MixpanelManager().searchQueryEntered(value, provider.searchedConversations.length); + PlatformManager.instance.analytics.searchQueryEntered(value, provider.searchedConversations.length); } }); setShowClearButton(); @@ -203,7 +203,7 @@ class _SearchWidgetState extends State { setShowClearButton(); // Hide search bar when search is cleared homeProvider.hideConvoSearchBar(); - MixpanelManager().searchQueryCleared(); + PlatformManager.instance.analytics.searchQueryCleared(); }, child: const Icon(Icons.close, color: Colors.white), ) diff --git a/app/lib/pages/home/device.dart b/app/lib/pages/home/device.dart index 1b3ae48b3a3..36e2f3d6872 100644 --- a/app/lib/pages/home/device.dart +++ b/app/lib/pages/home/device.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -13,7 +14,6 @@ import 'package:omi/providers/device_provider.dart'; import 'package:omi/providers/sync_provider.dart'; import 'package:omi/services/services.dart'; import 'package:omi/utils/analytics/intercom.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/time_utils.dart'; import 'package:omi/utils/platform/platform_service.dart'; @@ -174,11 +174,7 @@ class _ConnectedDeviceState extends State { child: Padding( padding: const EdgeInsets.only(left: 2, top: 1), child: charging - ? const FaIcon( - FontAwesomeIcons.chargingStation, - color: Color.fromARGB(255, 0, 255, 8), - size: 20, - ) + ? const FaIcon(FontAwesomeIcons.chargingStation, color: Color.fromARGB(255, 0, 255, 8), size: 20) : FaIcon( _getBatteryIcon(provider.batteryLevel), color: _getBatteryColor(provider.batteryLevel), @@ -376,7 +372,7 @@ class _ConnectedDeviceState extends State { if (mounted && Navigator.of(context).canPop()) { Navigator.of(context).pop(); } - MixpanelManager().disconnectFriendClicked(); + PlatformManager.instance.analytics.disconnectFriendClicked(); }, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18), diff --git a/app/lib/pages/home/home_content.dart b/app/lib/pages/home/home_content.dart index b2aea309874..df8eb656298 100644 --- a/app/lib/pages/home/home_content.dart +++ b/app/lib/pages/home/home_content.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -18,7 +19,6 @@ import 'package:omi/pages/phone_calls/phone_calls_page.dart'; import 'package:omi/pages/settings/daily_summary_detail_page.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/home_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/ui_guidelines.dart'; import 'package:omi/widgets/shimmer_with_timeout.dart'; @@ -173,11 +173,7 @@ class HomeContentPageState extends State with AutomaticKeepAliv } Widget _buildGetStartedOptions(BuildContext context) { - Widget option({ - required IconData icon, - required String label, - required VoidCallback onTap, - }) { + Widget option({required IconData icon, required String label, required VoidCallback onTap}) { return GestureDetector( onTap: () { HapticFeedback.lightImpact(); @@ -214,12 +210,7 @@ class HomeContentPageState extends State with AutomaticKeepAliv child: Text( label, textAlign: TextAlign.center, - style: const TextStyle( - color: Colors.white, - fontSize: 13, - fontWeight: FontWeight.w500, - height: 1.2, - ), + style: const TextStyle(color: Colors.white, fontSize: 13, fontWeight: FontWeight.w500, height: 1.2), ), ), ], @@ -231,30 +222,21 @@ class HomeContentPageState extends State with AutomaticKeepAliv icon: Icons.mic_rounded, label: 'Record with Phone', onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const ConversationCapturingPage()), - ); + Navigator.push(context, MaterialPageRoute(builder: (_) => const ConversationCapturingPage())); }, ); final callOption = option( icon: Icons.phone_in_talk_rounded, label: 'Record Call', onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const PhoneCallsPage()), - ); + Navigator.push(context, MaterialPageRoute(builder: (_) => const PhoneCallsPage())); }, ); final deviceOption = option( icon: Icons.bluetooth_searching_rounded, label: 'Connect Device', onTap: () { - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const DeviceSelectionPage()), - ); + Navigator.push(context, MaterialPageRoute(builder: (_) => const DeviceSelectionPage())); }, ); @@ -266,13 +248,7 @@ class HomeContentPageState extends State with AutomaticKeepAliv phoneOption, const SizedBox(height: 22), // Bottom of the triangle: the other two side by side. - Row( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - callOption, - deviceOption, - ], - ), + Row(mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [callOption, deviceOption]), ], ), ); @@ -284,7 +260,10 @@ class HomeContentPageState extends State with AutomaticKeepAliv child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ - Text(title, style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600)), + Text( + title, + style: const TextStyle(color: Colors.white, fontSize: 18, fontWeight: FontWeight.w600), + ), if (onViewAll != null) GestureDetector( onTap: onViewAll, @@ -359,10 +338,12 @@ class HomeContentPageState extends State with AutomaticKeepAliv return GestureDetector( onTap: () { - MixpanelManager().dailySummaryDetailViewed(summaryId: summary.id, date: summary.date); + PlatformManager.instance.analytics.dailySummaryDetailViewed(summaryId: summary.id, date: summary.date); Navigator.push( context, - MaterialPageRoute(builder: (context) => DailySummaryDetailPage(summaryId: summary.id, summary: summary)), + MaterialPageRoute( + builder: (context) => DailySummaryDetailPage(summaryId: summary.id, summary: summary), + ), ); }, child: Container( @@ -375,14 +356,7 @@ class HomeContentPageState extends State with AutomaticKeepAliv child: Stack( children: [ // Map at bottom - if (hasMap) - Positioned( - bottom: 0, - left: 0, - right: 0, - height: _mapHeight, - child: _buildCardMap(summary), - ), + if (hasMap) Positioned(bottom: 0, left: 0, right: 0, height: _mapHeight, child: _buildCardMap(summary)), // Text content at top Positioned( top: 0, @@ -427,15 +401,17 @@ class HomeContentPageState extends State with AutomaticKeepAliv final centerLng = summary.locations.map((l) => l.longitude).reduce((a, b) => a + b) / summary.locations.length; final markers = summary.locations - .map((loc) => Marker( - point: LatLng(loc.latitude, loc.longitude), - width: 22, - height: 22, - child: Container( - decoration: const BoxDecoration(color: Colors.deepPurple, shape: BoxShape.circle), - child: const Icon(Icons.location_on, color: Colors.white, size: 13), - ), - )) + .map( + (loc) => Marker( + point: LatLng(loc.latitude, loc.longitude), + width: 22, + height: 22, + child: Container( + decoration: const BoxDecoration(color: Colors.deepPurple, shape: BoxShape.circle), + child: const Icon(Icons.location_on, color: Colors.white, size: 13), + ), + ), + ) .toList(); return SizedBox( @@ -555,19 +531,11 @@ class HomeContentPageState extends State with AutomaticKeepAliv if (recent.isEmpty) return const SliverToBoxAdapter(child: SizedBox.shrink()); return SliverList( - delegate: SliverChildBuilderDelegate( - childCount: recent.length, - (context, index) { - final c = recent[index]; - final dateKey = DateTime(c.createdAt.year, c.createdAt.month, c.createdAt.day); - return ConversationListItem( - key: ValueKey(c.id), - conversation: c, - date: dateKey, - conversationIdx: index, - ); - }, - ), + delegate: SliverChildBuilderDelegate(childCount: recent.length, (context, index) { + final c = recent[index]; + final dateKey = DateTime(c.createdAt.year, c.createdAt.month, c.createdAt.day); + return ConversationListItem(key: ValueKey(c.id), conversation: c, date: dateKey, conversationIdx: index); + }), ); } } diff --git a/app/lib/pages/home/page.dart b/app/lib/pages/home/page.dart index 6b1f962c1fd..eba5e26bb8c 100644 --- a/app/lib/pages/home/page.dart +++ b/app/lib/pages/home/page.dart @@ -59,7 +59,6 @@ import 'package:omi/services/quick_actions_service.dart'; import 'package:omi/utils/platform/platform_service.dart'; import 'package:omi/services/announcement_service.dart'; import 'package:omi/services/notifications.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/utils/audio/foreground.dart'; import 'package:omi/utils/l10n_extensions.dart'; @@ -354,10 +353,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker // All async setup (streamDeviceRecording, refreshMessages) is already awaited above, // so the widget tree is fully settled — push directly. if (mounted) { - Navigator.push( - context, - MaterialPageRoute(builder: (context) => const ChatPage(isPivotBottom: false)), - ); + Navigator.push(context, MaterialPageRoute(builder: (context) => const ChatPage(isPivotBottom: false))); } break; case "settings": @@ -404,7 +400,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker case "daily-summary": if (detailPageId != null && detailPageId.isNotEmpty) { // Track notification opened - MixpanelManager().dailySummaryNotificationOpened( + PlatformManager.instance.analytics.dailySummaryNotificationOpened( summaryId: detailPageId, date: '', // Date not available in navigate_to, will be fetched when detail page loads ); @@ -684,12 +680,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker }, ), if (home.selectedIndex == 0) - Positioned( - left: 16, - right: 16, - bottom: 78, - child: _buildChatBar(context), - ), + Positioned(left: 16, right: 16, bottom: 78, child: _buildChatBar(context)), ], ); }, @@ -715,7 +706,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker return GestureDetector( onTap: () { HapticFeedback.lightImpact(); - MixpanelManager().bottomNavigationTabClicked('Chat'); + PlatformManager.instance.analytics.bottomNavigationTabClicked('Chat'); Navigator.push(context, MaterialPageRoute(builder: (context) => const ChatPage(isPivotBottom: false))); }, child: Container( @@ -726,15 +717,17 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker border: Border.all(color: const Color(0xFF35343B), width: 1), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.65), - blurRadius: 60, - spreadRadius: 14, - offset: const Offset(0, -16)), + color: Colors.black.withValues(alpha: 0.65), + blurRadius: 60, + spreadRadius: 14, + offset: const Offset(0, -16), + ), BoxShadow( - color: Colors.black.withValues(alpha: 0.45), - blurRadius: 32, - spreadRadius: 6, - offset: const Offset(0, -8)), + color: Colors.black.withValues(alpha: 0.45), + blurRadius: 32, + spreadRadius: 6, + offset: const Offset(0, -8), + ), BoxShadow(color: Colors.black.withValues(alpha: 0.25), blurRadius: 10, offset: const Offset(0, 2)), ], ), @@ -751,11 +744,11 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker GestureDetector( onTap: () { HapticFeedback.lightImpact(); - MixpanelManager().bottomNavigationTabClicked('Chat Voice'); + PlatformManager.instance.analytics.bottomNavigationTabClicked('Chat Voice'); Navigator.push( - context, - MaterialPageRoute( - builder: (context) => const ChatPage(isPivotBottom: false, autoStartVoice: true))); + context, + MaterialPageRoute(builder: (context) => const ChatPage(isPivotBottom: false, autoStartVoice: true)), + ); }, child: Container( width: 42, @@ -907,7 +900,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ); Navigator.of(context).pop(); await provider.clearDateFilter(); - MixpanelManager().calendarFilterCleared(); + PlatformManager.instance.analytics.calendarFilterCleared(); }, child: Text( context.l10n.removeFilter, @@ -924,7 +917,9 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker ); Navigator.of(context).pop(); await provider.filterConversationsByDate(selectedDate); - MixpanelManager().calendarFilterApplied(selectedDate); + PlatformManager.instance.analytics.calendarFilterApplied( + selectedDate, + ); }, child: Text( context.l10n.done, @@ -990,7 +985,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker icon: const Icon(FontAwesomeIcons.arrowUpFromBracket, size: 16, color: Colors.white70), onPressed: () { HapticFeedback.mediumImpact(); - MixpanelManager().exportTasksBannerClicked(); + PlatformManager.instance.analytics.exportTasksBannerClicked(); Navigator.of( context, ).push(MaterialPageRoute(builder: (context) => const TaskIntegrationsPage())); @@ -1037,7 +1032,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker subtitle: context.l10n.createAndShareYourApp, iconWidget: const Icon(Icons.apps, size: 18), onTap: () { - MixpanelManager().pageOpened('Submit App'); + PlatformManager.instance.analytics.pageOpened('Submit App'); routeToPage(context, const AddAppPage()); }, ), @@ -1046,7 +1041,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker subtitle: context.l10n.connectExternalAiTools, iconWidget: const Icon(Icons.cable, size: 18), onTap: () { - MixpanelManager().pageOpened('Add MCP Server'); + PlatformManager.instance.analytics.pageOpened('Add MCP Server'); routeToPage(context, const AddMcpServerPage()); }, ), @@ -1077,7 +1072,7 @@ class _HomePageState extends State with WidgetsBindingObserver, Ticker icon: const Icon(FontAwesomeIcons.gear, size: 16, color: Colors.white70), onPressed: () { HapticFeedback.mediumImpact(); - MixpanelManager().pageOpened('Settings'); + PlatformManager.instance.analytics.pageOpened('Settings'); String language = SharedPreferencesUtil().userPrimaryLanguage; bool hasSpeech = SharedPreferencesUtil().hasSpeakerProfile; String transcriptModel = SharedPreferencesUtil().transcriptionModel; diff --git a/app/lib/pages/home/widgets/battery_info_widget.dart b/app/lib/pages/home/widgets/battery_info_widget.dart index 836d8ce6720..8a62deb5965 100644 --- a/app/lib/pages/home/widgets/battery_info_widget.dart +++ b/app/lib/pages/home/widgets/battery_info_widget.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -14,7 +15,6 @@ import 'package:omi/pages/phone_calls/phone_calls_page.dart'; import 'package:omi/providers/capture_provider.dart'; import 'package:omi/providers/device_provider.dart'; import 'package:omi/providers/home_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/device.dart'; import 'package:omi/utils/enums.dart'; import 'package:omi/utils/l10n_extensions.dart'; @@ -42,10 +42,7 @@ class _BatteryInfoWidgetState extends State { onPickPhoneCall: () { Navigator.pop(sheetContext); if (!context.mounted) return; - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const PhoneCallsPage()), - ); + Navigator.push(context, MaterialPageRoute(builder: (_) => const PhoneCallsPage())); }, ), ); @@ -58,11 +55,11 @@ class _BatteryInfoWidgetState extends State { if (captureProvider.recordingState == RecordingState.record) { await captureProvider.stopStreamRecording(); captureProvider.forceProcessingCurrentConversation(); - MixpanelManager().phoneMicRecordingStopped(); + PlatformManager.instance.analytics.phoneMicRecordingStopped(); return; } await captureProvider.streamRecording(); - MixpanelManager().phoneMicRecordingStarted(); + PlatformManager.instance.analytics.phoneMicRecordingStarted(); if (context.mounted) { final topConvoId = (captureProvider.conversationProvider?.conversations ?? []).isNotEmpty ? captureProvider.conversationProvider!.conversations.first.id @@ -87,7 +84,7 @@ class _BatteryInfoWidgetState extends State { provider.connectedDevice, provider.pairedDevice, provider.isConnecting, - provider.isCharging + provider.isCharging, ), builder: (context, data, child) { final (batteryLevel, connectedDevice, pairedDevice, isConnecting, isCharging) = data; @@ -95,7 +92,7 @@ class _BatteryInfoWidgetState extends State { final batteryPill = GestureDetector( onTap: () { routeToPage(context, const ConnectedDevice()); - MixpanelManager().batteryIndicatorClicked(); + PlatformManager.instance.analytics.batteryIndicatorClicked(); }, child: Container( height: 36, @@ -155,10 +152,7 @@ class _BatteryInfoWidgetState extends State { GestureDetector( onTap: () { HapticFeedback.lightImpact(); - Navigator.push( - context, - MaterialPageRoute(builder: (_) => const PhoneCallsPage()), - ); + Navigator.push(context, MaterialPageRoute(builder: (_) => const PhoneCallsPage())); }, child: Container( height: 36, @@ -167,11 +161,7 @@ class _BatteryInfoWidgetState extends State { color: const Color(0xFF1F1F25), borderRadius: BorderRadius.circular(18), ), - child: const Icon( - Icons.phone_in_talk_rounded, - color: Colors.white, - size: 16, - ), + child: const Icon(Icons.phone_in_talk_rounded, color: Colors.white, size: 16), ), ), ], @@ -196,10 +186,7 @@ class _BatteryInfoWidgetState extends State { height: 16, child: Stack( children: [ - Image.asset( - DeviceUtils.getDeviceImageFromBtDevice(pairedDevice), - fit: BoxFit.contain, - ), + Image.asset(DeviceUtils.getDeviceImageFromBtDevice(pairedDevice), fit: BoxFit.contain), // Slash line across the image Positioned.fill(child: CustomPaint(painter: SlashLinePainter())), ], @@ -222,7 +209,7 @@ class _BatteryInfoWidgetState extends State { onTap: () async { if (SharedPreferencesUtil().btDevice.id.isEmpty) { routeToPage(context, const ConnectDevicePage()); - MixpanelManager().connectFriendClicked(); + PlatformManager.instance.analytics.connectFriendClicked(); } else { await routeToPage(context, const ConnectedDevice()); } @@ -243,16 +230,13 @@ class _BatteryInfoWidgetState extends State { isConnecting && isMemoriesPage ? Text( context.l10n.searching, - style: Theme.of(context) - .textTheme - .bodyMedium! - .copyWith(color: Colors.white, fontSize: 12), + style: Theme.of( + context, + ).textTheme.bodyMedium!.copyWith(color: Colors.white, fontSize: 12), ) : isMemoriesPage - ? Text( - context.l10n.connect, - style: const TextStyle(color: Colors.white, fontSize: 12), - ) + ? Text(context.l10n.connect, + style: const TextStyle(color: Colors.white, fontSize: 12)) : const SizedBox.shrink(), ], ), @@ -306,18 +290,17 @@ class _BatteryInfoWidgetState extends State { ? '...' : context.l10n.record, style: const TextStyle( - color: Colors.white, fontSize: 12, fontWeight: FontWeight.w600), + color: Colors.white, + fontSize: 12, + fontWeight: FontWeight.w600, + ), ), ], ), ), ), if (showChevron) ...[ - Container( - width: 1, - height: 18, - color: Colors.white.withValues(alpha: 0.25), - ), + Container(width: 1, height: 18, color: Colors.white.withValues(alpha: 0.25)), GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _showRecordOptions(context), @@ -386,10 +369,7 @@ class _RecordOptionsSheet extends StatelessWidget { final VoidCallback onPickPhoneMic; final VoidCallback onPickPhoneCall; - const _RecordOptionsSheet({ - required this.onPickPhoneMic, - required this.onPickPhoneCall, - }); + const _RecordOptionsSheet({required this.onPickPhoneMic, required this.onPickPhoneCall}); @override Widget build(BuildContext context) { @@ -407,10 +387,7 @@ class _RecordOptionsSheet extends StatelessWidget { child: Container( width: 36, height: 4, - decoration: BoxDecoration( - color: Colors.white24, - borderRadius: BorderRadius.circular(2), - ), + decoration: BoxDecoration(color: Colors.white24, borderRadius: BorderRadius.circular(2)), ), ), const SizedBox(height: 18), @@ -439,12 +416,7 @@ class _RecordOption extends StatelessWidget { final String subtitle; final VoidCallback onTap; - const _RecordOption({ - required this.icon, - required this.title, - required this.subtitle, - required this.onTap, - }); + const _RecordOption({required this.icon, required this.title, required this.subtitle, required this.onTap}); @override Widget build(BuildContext context) { @@ -455,10 +427,7 @@ class _RecordOption extends StatelessWidget { }, child: Container( padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12), - decoration: BoxDecoration( - color: const Color(0xFF2A2A33), - borderRadius: BorderRadius.circular(16), - ), + decoration: BoxDecoration(color: const Color(0xFF2A2A33), borderRadius: BorderRadius.circular(16)), child: Row( children: [ Container( @@ -492,10 +461,7 @@ class _RecordOption extends StatelessWidget { style: const TextStyle(color: Colors.white, fontSize: 15, fontWeight: FontWeight.w600), ), const SizedBox(height: 2), - Text( - subtitle, - style: TextStyle(color: Colors.grey[400], fontSize: 12), - ), + Text(subtitle, style: TextStyle(color: Colors.grey[400], fontSize: 12)), ], ), ), diff --git a/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart b/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart index 685f5a29fb2..f805fa167bc 100644 --- a/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart +++ b/app/lib/pages/home/widgets/chat_apps_dropdown_widget.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:cached_network_image/cached_network_image.dart'; @@ -9,7 +10,6 @@ import 'package:omi/gen/assets.gen.dart'; import 'package:omi/providers/app_provider.dart'; import 'package:omi/providers/home_provider.dart'; import 'package:omi/providers/message_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/widgets/dialog.dart'; @@ -93,7 +93,7 @@ class ChatAppsDropdownWidget extends StatelessWidget { // enable apps if (val == 'enable') { - MixpanelManager().pageOpened('Chat Apps'); + PlatformManager.instance.analytics.pageOpened('Chat Apps'); context.read().setIndex(4); controller?.animateToPage(4, duration: const Duration(milliseconds: 200), curve: Curves.easeInOut); return; diff --git a/app/lib/pages/home/widgets/out_of_credits_widget.dart b/app/lib/pages/home/widgets/out_of_credits_widget.dart index e306731be2d..1022d7b5468 100644 --- a/app/lib/pages/home/widgets/out_of_credits_widget.dart +++ b/app/lib/pages/home/widgets/out_of_credits_widget.dart @@ -1,10 +1,10 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:omi/pages/settings/usage_page.dart'; import 'package:omi/providers/usage_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; @@ -39,7 +39,7 @@ class OutOfCreditsWidget extends StatelessWidget { const SizedBox(width: 12), TextButton( onPressed: () { - MixpanelManager().paywallOpened('Out of Credits Banner'); + PlatformManager.instance.analytics.paywallOpened('Out of Credits Banner'); routeToPage(context, const UsagePage()); }, child: Text( diff --git a/app/lib/pages/memories/category_memories_page.dart b/app/lib/pages/memories/category_memories_page.dart index eccf4a363cc..2ed7f7431f7 100644 --- a/app/lib/pages/memories/category_memories_page.dart +++ b/app/lib/pages/memories/category_memories_page.dart @@ -1,10 +1,10 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:omi/backend/schema/memory.dart'; import 'package:omi/providers/memories_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'widgets/memory_dialog.dart'; import 'widgets/memory_edit_sheet.dart'; @@ -41,7 +41,7 @@ class CategoryMemoriesPage extends StatelessWidget { icon: const Icon(Icons.add), onPressed: () { showMemoryDialog(context, provider); - MixpanelManager().memoriesPageCreateMemoryBtn(); + PlatformManager.instance.analytics.memoriesPageCreateMemoryBtn(); }, ), ], diff --git a/app/lib/pages/memories/page.dart b/app/lib/pages/memories/page.dart index 3730f0b59bc..37a3c34ad2d 100644 --- a/app/lib/pages/memories/page.dart +++ b/app/lib/pages/memories/page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,7 +9,6 @@ import 'package:omi/widgets/shimmer_with_timeout.dart'; import 'package:omi/backend/schema/memory.dart'; import 'package:omi/providers/home_provider.dart'; import 'package:omi/providers/memories_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/ui_guidelines.dart'; import 'package:omi/widgets/extensions/functions.dart'; @@ -258,7 +258,7 @@ class MemoriesPageState extends State with AutomaticKeepAliveClien onPressed: () { _searchController.clear(); provider.setSearchQuery(''); - MixpanelManager().memorySearchCleared( + PlatformManager.instance.analytics.memorySearchCleared( provider.memories.length, ); }, @@ -279,7 +279,7 @@ class MemoriesPageState extends State with AutomaticKeepAliveClien onChanged: (value) => provider.setSearchQuery(value), onSubmitted: (value) { if (value.isNotEmpty) { - MixpanelManager().memorySearched( + PlatformManager.instance.analytics.memorySearched( value, provider.filteredMemories.length, ); @@ -371,7 +371,7 @@ class MemoriesPageState extends State with AutomaticKeepAliveClien provider: provider, onTap: (BuildContext context, Memory tappedMemory, MemoriesProvider tappedProvider) { - MixpanelManager().memoryListItemClicked(tappedMemory); + PlatformManager.instance.analytics.memoryListItemClicked(tappedMemory); _showQuickEditSheet(context, tappedMemory, tappedProvider); }, ); @@ -388,7 +388,7 @@ class MemoriesPageState extends State with AutomaticKeepAliveClien heroTag: 'memories_fab', onPressed: () { showMemoryDialog(context, provider); - MixpanelManager().memoriesPageCreateMemoryBtn(); + PlatformManager.instance.analytics.memoriesPageCreateMemoryBtn(); }, backgroundColor: Colors.deepPurple, tooltip: context.l10n.createMemoryTooltip, @@ -490,7 +490,7 @@ class MemoriesPageState extends State with AutomaticKeepAliveClien } void _showMemoryManagementSheet(BuildContext context, MemoriesProvider provider) { - MixpanelManager().memoriesManagementSheetOpened(); + PlatformManager.instance.analytics.memoriesManagementSheetOpened(); showModalBottomSheet( context: context, backgroundColor: Colors.transparent, diff --git a/app/lib/pages/memories/widgets/memory_dialog.dart b/app/lib/pages/memories/widgets/memory_dialog.dart index 8f7e29624d2..a8bb7772893 100644 --- a/app/lib/pages/memories/widgets/memory_dialog.dart +++ b/app/lib/pages/memories/widgets/memory_dialog.dart @@ -1,8 +1,8 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:omi/backend/schema/memory.dart'; import 'package:omi/providers/memories_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'delete_confirmation.dart'; @@ -68,10 +68,10 @@ class _MemoryDialogState extends State { Text( isEditing ? (widget.memory!.category == MemoryCategory.manual - ? context.l10n.filterManual - : widget.memory!.category == MemoryCategory.interesting - ? context.l10n.filterInteresting - : context.l10n.filterSystem) + ? context.l10n.filterManual + : widget.memory!.category == MemoryCategory.interesting + ? context.l10n.filterInteresting + : context.l10n.filterSystem) : context.l10n.newMemory, style: const TextStyle(color: Colors.white, fontSize: 14), ), @@ -169,7 +169,7 @@ class _MemoryDialogState extends State { if (isEditing) { success = await widget.provider.editMemory(widget.memory!, contentController.text); if (success) { - MixpanelManager().memoriesPageEditedMemory(); + PlatformManager.instance.analytics.memoriesPageEditedMemory(); } } else { success = await widget.provider.createMemory( @@ -178,7 +178,7 @@ class _MemoryDialogState extends State { MemoryCategory.manual, ); if (success) { - MixpanelManager().memoriesPageCreatedMemory(MemoryCategory.manual); + PlatformManager.instance.analytics.memoriesPageCreatedMemory(MemoryCategory.manual); } } } catch (e) { diff --git a/app/lib/pages/memories/widgets/memory_graph_page.dart b/app/lib/pages/memories/widgets/memory_graph_page.dart index cc0848dcd59..b7402f77fbe 100644 --- a/app/lib/pages/memories/widgets/memory_graph_page.dart +++ b/app/lib/pages/memories/widgets/memory_graph_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:math'; import 'dart:ui' as ui; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:flutter/material.dart'; @@ -15,7 +16,6 @@ import 'package:vector_math/vector_math_64.dart' as v; import 'package:omi/backend/http/api/knowledge_graph_api.dart'; import 'package:omi/backend/preferences.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; @@ -285,7 +285,7 @@ class _MemoryGraphPageState extends State with SingleTickerProv }); if (widget.trackOpenEvent) { - MixpanelManager().brainMapOpened(); + PlatformManager.instance.analytics.brainMapOpened(); } _loadGraph(); } @@ -374,7 +374,7 @@ class _MemoryGraphPageState extends State with SingleTickerProv }); try { - MixpanelManager().brainMapRebuilt(); + PlatformManager.instance.analytics.brainMapRebuilt(); await KnowledgeGraphApi.rebuildKnowledgeGraph(); if (!mounted) return; @@ -524,7 +524,7 @@ class _MemoryGraphPageState extends State with SingleTickerProv } Future _shareGraph() async { - MixpanelManager().brainMapShareClicked(); + PlatformManager.instance.analytics.brainMapShareClicked(); try { final boundary = _graphKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) return; @@ -821,7 +821,7 @@ class _MemoryGraphPageState extends State with SingleTickerProv final node = simulation.nodeMap[hitNodeId]; if (node != null) { - MixpanelManager().brainMapNodeClicked(node.id, node.label, node.nodeType); + PlatformManager.instance.analytics.brainMapNodeClicked(node.id, node.label, node.nodeType); } // Find neighbors diff --git a/app/lib/pages/memories/widgets/memory_item.dart b/app/lib/pages/memories/widgets/memory_item.dart index bdba3ee993d..cd70e5e80fc 100644 --- a/app/lib/pages/memories/widgets/memory_item.dart +++ b/app/lib/pages/memories/widgets/memory_item.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -16,7 +17,6 @@ import 'package:omi/providers/app_provider.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/memories_provider.dart'; import 'package:omi/providers/usage_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/utils/ui_guidelines.dart'; @@ -83,7 +83,7 @@ class MemoryItem extends StatelessWidget { child: GestureDetector( onTap: () { if (!context.read().showSubscriptionUI) return; - MixpanelManager().paywallOpened('Action Item'); + PlatformManager.instance.analytics.paywallOpened('Action Item'); routeToPage(context, const UsagePage(showUpgradeDialog: true)); return; }, @@ -125,7 +125,7 @@ class MemoryItem extends StatelessWidget { final memoryContent = memory.content.decodeString; provider.deleteMemory(memory); - MixpanelManager().memoriesPageDeletedMemory(memory); + PlatformManager.instance.analytics.memoriesPageDeletedMemory(memory); if (context.findAncestorStateOfType() != null) { context.findAncestorStateOfType()!.showDeleteNotification(memoryContent, memory); @@ -259,7 +259,7 @@ class MemoryItem extends StatelessWidget { // ], // onSelected: (visibility) { // provider.updateMemoryVisibility(memory, visibility); - // MixpanelManager().memoryVisibilityChanged(memory, visibility); + // PlatformManager.instance.analytics.memoryVisibilityChanged(memory, visibility); // }, // ); // } diff --git a/app/lib/pages/onboarding/conversation_created_widget.dart b/app/lib/pages/onboarding/conversation_created_widget.dart index 058c6485b28..ceb88ce24eb 100644 --- a/app/lib/pages/onboarding/conversation_created_widget.dart +++ b/app/lib/pages/onboarding/conversation_created_widget.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:gradient_borders/box_borders/gradient_box_border.dart'; @@ -9,7 +10,6 @@ import 'package:omi/pages/conversation_detail/page.dart'; import 'package:omi/pages/conversations/widgets/conversation_list_item.dart'; import 'package:omi/providers/conversation_provider.dart'; import 'package:omi/providers/speech_profile_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/other/temp.dart'; Future updateConvoDetailProvider(BuildContext context, ServerConversation conversation) { @@ -88,7 +88,7 @@ class _ConversationCreatedWidgetState extends State { padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 16), onPressed: () { // updateMemoryDetailProvider(context, provider.memory!); - MixpanelManager().conversationListItemClicked(provider.conversation!, 0); + PlatformManager.instance.analytics.conversationListItemClicked(provider.conversation!, 0); routeToPage( context, ConversationDetailPage(conversation: provider.conversation!, isFromOnboarding: true), diff --git a/app/lib/pages/onboarding/device_onboarding/device_onboarding_wrapper.dart b/app/lib/pages/onboarding/device_onboarding/device_onboarding_wrapper.dart index 85020634ed0..e9e9811745b 100644 --- a/app/lib/pages/onboarding/device_onboarding/device_onboarding_wrapper.dart +++ b/app/lib/pages/onboarding/device_onboarding/device_onboarding_wrapper.dart @@ -1,9 +1,9 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:omi/backend/http/api/users.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/pages/home/page.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/other/temp.dart'; import 'device_onboarding_page.dart'; @@ -53,7 +53,7 @@ class _DeviceOnboardingWrapperState extends State with SharedPreferencesUtil().onboardingCompleted = true; SharedPreferencesUtil().permissionsCompleted = true; updateUserOnboardingState(completed: true); - MixpanelManager().onboardingStepCompleted('Device Onboarding Completed'); + PlatformManager.instance.analytics.onboardingStepCompleted('Device Onboarding Completed'); PaintingBinding.instance.imageCache.clear(); routeToPage(context, const HomePageWrapper(), replace: true); } @@ -73,7 +73,7 @@ class _DeviceOnboardingWrapperState extends State with isFirstSlide: i == 0, isLastSlide: i == _totalSlides - 1, onNext: () { - MixpanelManager().onboardingStepCompleted('Device Onboarding Slide ${i + 1}'); + PlatformManager.instance.analytics.onboardingStepCompleted('Device Onboarding Slide ${i + 1}'); if (i == _totalSlides - 1) { // Last slide, complete onboarding _completeOnboarding(); diff --git a/app/lib/pages/onboarding/find_device/page.dart b/app/lib/pages/onboarding/find_device/page.dart index 09bb007317a..71c71fae1ec 100644 --- a/app/lib/pages/onboarding/find_device/page.dart +++ b/app/lib/pages/onboarding/find_device/page.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; @@ -9,7 +10,6 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:omi/providers/home_provider.dart'; import 'package:omi/providers/onboarding_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/widgets/dialog.dart'; import 'found_devices.dart'; @@ -131,7 +131,7 @@ class _FindDevicesPageState extends State { } else { widget.goNext(); } - MixpanelManager().useWithoutDeviceOnboardingFindDevices(); + PlatformManager.instance.analytics.useWithoutDeviceOnboardingFindDevices(); }, child: Container( width: double.infinity, diff --git a/app/lib/pages/onboarding/found_omi/found_omi_widget.dart b/app/lib/pages/onboarding/found_omi/found_omi_widget.dart index 71c551ce07b..48c0e587700 100644 --- a/app/lib/pages/onboarding/found_omi/found_omi_widget.dart +++ b/app/lib/pages/onboarding/found_omi/found_omi_widget.dart @@ -1,10 +1,10 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:omi/backend/http/api/users.dart'; import 'package:omi/backend/preferences.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class _SourceOption { @@ -174,7 +174,7 @@ class _FoundOmiWidgetState extends State { : _selectedSource!; SharedPreferencesUtil().foundOmiSource = source; updateUserOnboardingState(acquisitionSource: source); - MixpanelManager().onboardingUserAcquisitionSource(source); + PlatformManager.instance.analytics.onboardingUserAcquisitionSource(source); widget.goNext(); } : null, diff --git a/app/lib/pages/onboarding/permissions/permissions_checker.dart b/app/lib/pages/onboarding/permissions/permissions_checker.dart index 7e1fd71e5af..8c8df779e30 100644 --- a/app/lib/pages/onboarding/permissions/permissions_checker.dart +++ b/app/lib/pages/onboarding/permissions/permissions_checker.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -9,7 +10,6 @@ import 'package:omi/backend/preferences.dart'; import 'package:omi/gen/assets.gen.dart'; import 'package:omi/pages/home/page.dart'; import 'package:omi/providers/onboarding_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/widgets/dialog.dart'; @@ -185,7 +185,7 @@ class PermissionsInterstitialPage extends StatelessWidget { } } }); - MixpanelManager().permissionsInterstitialCompleted(); + PlatformManager.instance.analytics.permissionsInterstitialCompleted(); provider.setLoading(false); if (context.mounted) { _goHome(context); diff --git a/app/lib/pages/onboarding/setup/setup_questions.dart b/app/lib/pages/onboarding/setup/setup_questions.dart index d03655057bc..cde3a711d89 100644 --- a/app/lib/pages/onboarding/setup/setup_questions.dart +++ b/app/lib/pages/onboarding/setup/setup_questions.dart @@ -1,7 +1,7 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:omi/pages/speech_profile/page.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class SetupQuestionsPage extends StatefulWidget { @@ -95,7 +95,11 @@ class _SetupQuestionsPageState extends State { child: MaterialButton( onPressed: () { if (selectedProfession != null && selectedUsage != null && selectedAge != null) { - MixpanelManager().setUserProperties(selectedProfession!, selectedUsage!, selectedAge!); + PlatformManager.instance.analytics.setUserProperties( + selectedProfession!, + selectedUsage!, + selectedAge!, + ); Navigator.of( context, ).pushReplacement(MaterialPageRoute(builder: (c) => const SpeechProfilePage(onbording: true))); diff --git a/app/lib/pages/onboarding/user_review_page.dart b/app/lib/pages/onboarding/user_review_page.dart index 0d5cebc92a5..327447c20cd 100644 --- a/app/lib/pages/onboarding/user_review_page.dart +++ b/app/lib/pages/onboarding/user_review_page.dart @@ -1,12 +1,12 @@ import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; @@ -35,7 +35,7 @@ class _UserReviewPageState extends State { if (await canLaunchUrl(reviewUrl)) { await launchUrl(reviewUrl, mode: LaunchMode.externalApplication); - MixpanelManager().track('App Review Opened', properties: {'source': 'onboarding'}); + PlatformManager.instance.analytics.track('App Review Opened', properties: {'source': 'onboarding'}); await Future.delayed(const Duration(milliseconds: 500)); } else { Logger.debug('Could not launch review URL'); @@ -51,7 +51,7 @@ class _UserReviewPageState extends State { Future _skipReview() async { HapticFeedback.lightImpact(); - MixpanelManager().track('App Review Skipped', properties: {'source': 'onboarding'}); + PlatformManager.instance.analytics.track('App Review Skipped', properties: {'source': 'onboarding'}); widget.goNext(); } diff --git a/app/lib/pages/onboarding/wrapper.dart b/app/lib/pages/onboarding/wrapper.dart index ac8c984105b..fcd0f15407c 100644 --- a/app/lib/pages/onboarding/wrapper.dart +++ b/app/lib/pages/onboarding/wrapper.dart @@ -1,5 +1,6 @@ import 'dart:math'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:firebase_auth/firebase_auth.dart'; @@ -27,7 +28,6 @@ import 'package:omi/providers/onboarding_provider.dart'; import 'package:omi/providers/speech_profile_provider.dart'; import 'package:omi/services/auth_service.dart'; import 'package:omi/utils/analytics/intercom.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/widgets/device_widget.dart'; @@ -267,7 +267,7 @@ class _OnboardingWrapperState extends State with TickerProvid List pages = [ AuthComponent( onSignIn: () async { - MixpanelManager().onboardingStepCompleted('Auth'); + PlatformManager.instance.analytics.onboardingStepCompleted('Auth'); context.read().setupHasSpeakerProfile(); IntercomManager.instance.loginIdentifiedUser(SharedPreferencesUtil().uid); // Consent is checked first regardless of server-side onboarding @@ -285,7 +285,7 @@ class _OnboardingWrapperState extends State with TickerProvid AiConsentWidget( onAgree: () async { SharedPreferencesUtil().aiConsentGiven = true; - MixpanelManager().onboardingStepCompleted('AI Consent'); + PlatformManager.instance.analytics.onboardingStepCompleted('AI Consent'); // If the server says this user already completed onboarding, jump // straight to home — their first-time onboarding ran in a previous // session and we don't want to re-run it. @@ -304,32 +304,32 @@ class _OnboardingWrapperState extends State with TickerProvid FirebaseAuth.instance.currentUser!.displayName, FirebaseAuth.instance.currentUser!.uid, ); - MixpanelManager().onboardingStepCompleted('Name'); + PlatformManager.instance.analytics.onboardingStepCompleted('Name'); }, ), PrimaryLanguageWidget( goNext: () { _goNext(); // Go to Found Omi page - MixpanelManager().onboardingStepCompleted('Primary Language'); + PlatformManager.instance.analytics.onboardingStepCompleted('Primary Language'); }, ), FoundOmiWidget( goNext: () { _goNext(); // Go to Permissions page - MixpanelManager().onboardingStepCompleted('Acquisition Source'); + PlatformManager.instance.analytics.onboardingStepCompleted('Acquisition Source'); }, ), PermissionsWidget( goNext: () { _goNext(); // Go to User Review page - MixpanelManager().onboardingStepCompleted('Permissions'); + PlatformManager.instance.analytics.onboardingStepCompleted('Permissions'); }, ), UserReviewPage( goNext: () { // Go directly to Speech Profile (skip device steps - we use phone mic now) _controller!.animateTo(kSpeechProfilePage); - MixpanelManager().onboardingStepCompleted('User Review'); + PlatformManager.instance.analytics.onboardingStepCompleted('User Review'); }, ), // Placeholder pages - not used in new flow but kept for index consistency @@ -339,18 +339,18 @@ class _OnboardingWrapperState extends State with TickerProvid value: _speechProfileProvider!, child: SpeechProfileWidget( goNext: () { - MixpanelManager().onboardingStepCompleted('Speech Profile'); + PlatformManager.instance.analytics.onboardingStepCompleted('Speech Profile'); _controller!.animateTo(kKnowledgeGraphPage); }, onSkip: () { - MixpanelManager().onboardingStepCompleted('Speech Profile Skipped'); + PlatformManager.instance.analytics.onboardingStepCompleted('Speech Profile Skipped'); _controller!.animateTo(kKnowledgeGraphPage); }, ), ), OnboardingKnowledgeGraphStep( onContinue: () { - MixpanelManager().onboardingStepCompleted('Knowledge Graph'); + PlatformManager.instance.analytics.onboardingStepCompleted('Knowledge Graph'); _controller!.animateTo(kCompletePage); }, ), @@ -359,7 +359,7 @@ class _OnboardingWrapperState extends State with TickerProvid SharedPreferencesUtil().onboardingCompleted = true; SharedPreferencesUtil().permissionsCompleted = true; updateUserOnboardingState(completed: true); - MixpanelManager().onboardingCompleted(); + PlatformManager.instance.analytics.onboardingCompleted(); PaintingBinding.instance.imageCache.clear(); routeToPage(context, const HomePageWrapper(), replace: true); }, diff --git a/app/lib/pages/payments/payments_page.dart b/app/lib/pages/payments/payments_page.dart index 5f58bba5cfd..456a90828d8 100644 --- a/app/lib/pages/payments/payments_page.dart +++ b/app/lib/pages/payments/payments_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -7,7 +8,6 @@ import 'package:omi/pages/payments/payment_method_provider.dart'; import 'package:omi/pages/payments/paypal_setup_page.dart'; import 'package:omi/pages/payments/stripe_connect_setup.dart'; import 'package:omi/pages/payments/widgets/payment_method_card.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; import 'models/payment_method_config.dart'; @@ -23,7 +23,7 @@ class _PaymentsPageState extends State { @override void initState() { super.initState(); - MixpanelManager().paymentsPageOpened(); + PlatformManager.instance.analytics.paymentsPageOpened(); WidgetsBinding.instance.addPostFrameCallback((_) async { await context.read().getPaymentMethodsStatus(); }); @@ -124,7 +124,7 @@ class _PaymentsPageState extends State { title: context.l10n.paymentMethodStripe, subtitle: _getPaymentSubtitle(isActive: true, isConnected: true), onManageTap: () { - MixpanelManager().paymentMethodSelected(methodName: 'Stripe'); + PlatformManager.instance.analytics.paymentMethodSelected(methodName: 'Stripe'); routeToPage(context, const StripeConnectSetup()); }, isActive: true, @@ -134,7 +134,7 @@ class _PaymentsPageState extends State { title: context.l10n.paymentMethodPayPal, subtitle: _getPaymentSubtitle(isActive: true, isConnected: true), onManageTap: () { - MixpanelManager().paymentMethodSelected(methodName: 'PayPal'); + PlatformManager.instance.analytics.paymentMethodSelected(methodName: 'PayPal'); routeToPage(context, const PaypalSetupPage()); }, isActive: true, @@ -161,12 +161,12 @@ class _PaymentsPageState extends State { title: context.l10n.paymentMethodStripe, subtitle: _getPaymentSubtitle(isActive: false, isConnected: true), onManageTap: () { - MixpanelManager().track('Manage Stripe'); + PlatformManager.instance.analytics.track('Manage Stripe'); routeToPage(context, const StripeConnectSetup()); }, onSetActiveTap: () { provider.setActiveMethod(PaymentMethodType.stripe); - MixpanelManager().track('Set Stripe as active'); + PlatformManager.instance.analytics.track('Set Stripe as active'); }, isConnected: true, isActive: false, @@ -179,12 +179,12 @@ class _PaymentsPageState extends State { title: context.l10n.paymentMethodPayPal, subtitle: _getPaymentSubtitle(isActive: false, isConnected: true), onManageTap: () { - MixpanelManager().track('Manage PayPal'); + PlatformManager.instance.analytics.track('Manage PayPal'); routeToPage(context, const PaypalSetupPage()); }, onSetActiveTap: () { provider.setActiveMethod(PaymentMethodType.paypal); - MixpanelManager().track('Set PayPal as active'); + PlatformManager.instance.analytics.track('Set PayPal as active'); }, isConnected: true, isActive: false, @@ -197,7 +197,7 @@ class _PaymentsPageState extends State { title: context.l10n.paymentMethodStripe, subtitle: _getPaymentSubtitle(isActive: false, isConnected: false), onManageTap: () { - MixpanelManager().track('Manage Stripe'); + PlatformManager.instance.analytics.track('Manage Stripe'); routeToPage(context, const StripeConnectSetup()); }, isConnected: false, @@ -210,7 +210,7 @@ class _PaymentsPageState extends State { title: context.l10n.paymentMethodPayPal, subtitle: _getPaymentSubtitle(isActive: false, isConnected: false), onManageTap: () { - MixpanelManager().track('Manage PayPal'); + PlatformManager.instance.analytics.track('Manage PayPal'); routeToPage(context, const PaypalSetupPage()); }, isConnected: false, diff --git a/app/lib/pages/payments/paypal_setup_page.dart b/app/lib/pages/payments/paypal_setup_page.dart index 2c28cedcd71..d641d40faa2 100644 --- a/app/lib/pages/payments/paypal_setup_page.dart +++ b/app/lib/pages/payments/paypal_setup_page.dart @@ -1,10 +1,10 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:omi/gen/assets.gen.dart'; import 'package:omi/pages/payments/payment_method_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/widgets/animated_loading_button.dart'; import 'package:omi/utils/other/validators.dart'; @@ -243,11 +243,11 @@ class _PaypalSetupPageState extends State { onPressed: () async { if (_formKey.currentState!.validate()) { setState(() => _isLoading = true); - MixpanelManager().track(_isComplete ? 'Update PayPal Details' : 'Save PayPal Details'); + PlatformManager.instance.analytics.track(_isComplete ? 'Update PayPal Details' : 'Save PayPal Details'); await context.read().connectPayPal( - _emailController.text.trim(), - _paypalMeLinkController.text.trim(), - ); + _emailController.text.trim(), + _paypalMeLinkController.text.trim(), + ); setState(() { _isLoading = false; diff --git a/app/lib/pages/payments/stripe_connect_setup.dart b/app/lib/pages/payments/stripe_connect_setup.dart index 299e0ef41a0..59217a890d2 100644 --- a/app/lib/pages/payments/stripe_connect_setup.dart +++ b/app/lib/pages/payments/stripe_connect_setup.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:collection/collection.dart'; @@ -8,7 +9,6 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:omi/gen/assets.gen.dart'; import 'package:omi/pages/payments/widgets/country_bottom_sheet.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/widgets/animated_loading_button.dart'; import 'package:omi/widgets/extensions/string.dart'; @@ -151,12 +151,10 @@ class _StripeConnectSetupState extends State with SingleTick provider.selectedCountryId?.isEmpty ?? true ? context.l10n.selectYourCountry : ((provider.filteredCountries.firstWhereOrNull( - (country) => - country['id'] == provider.selectedCountryId, - )?['name'] - as String?) - ?.decodeString ?? - context.l10n.selectYourCountry), + (country) => country['id'] == provider.selectedCountryId, + )?['name'] as String?) + ?.decodeString ?? + context.l10n.selectYourCountry), style: const TextStyle(color: Colors.white), ), ], @@ -209,11 +207,10 @@ class _StripeConnectSetupState extends State with SingleTick AnimatedLoadingButton( text: context.l10n.connectNow, loaderColor: Colors.black, - onPressed: - provider.stripeConnectionState == PaymentConnectionState.inComplete || + onPressed: provider.stripeConnectionState == PaymentConnectionState.inComplete || provider.selectedCountryId != null ? () async { - MixpanelManager().track('Stripe Connect Started'); + PlatformManager.instance.analytics.track('Stripe Connect Started'); var url = await provider.connectStripe(); if (url != null) { provider.startStripePolling(); @@ -223,15 +220,13 @@ class _StripeConnectSetupState extends State with SingleTick } } : () async {}, - color: - provider.stripeConnectionState == PaymentConnectionState.inComplete || + color: provider.stripeConnectionState == PaymentConnectionState.inComplete || provider.selectedCountryId != null ? Colors.white : Colors.grey, textStyle: TextStyle( fontSize: 16, - color: - provider.stripeConnectionState == PaymentConnectionState.inComplete || + color: provider.stripeConnectionState == PaymentConnectionState.inComplete || provider.selectedCountryId != null ? Colors.black : Colors.grey[600], @@ -278,7 +273,7 @@ class _StripeConnectSetupState extends State with SingleTick AnimatedLoadingButton( text: context.l10n.failedTryAgain, onPressed: () async { - MixpanelManager().track('Stripe Connect Retry'); + PlatformManager.instance.analytics.track('Stripe Connect Retry'); var res = await provider.connectStripe(); if (res != null) { provider.startStripePolling(); @@ -294,7 +289,7 @@ class _StripeConnectSetupState extends State with SingleTick ), TextButton( onPressed: () { - MixpanelManager().track('Stripe Connect Later'); + PlatformManager.instance.analytics.track('Stripe Connect Later'); provider.stopStripePolling(); Navigator.pop(context); }, @@ -354,7 +349,7 @@ class _StripeConnectSetupState extends State with SingleTick AnimatedLoadingButton( text: context.l10n.updateStripeDetails, onPressed: () async { - MixpanelManager().track('Stripe Connect Update'); + PlatformManager.instance.analytics.track('Stripe Connect Update'); var url = await provider.connectStripe(); if (url != null) { provider.startStripePolling(); diff --git a/app/lib/pages/phone_calls/active_call_banner.dart b/app/lib/pages/phone_calls/active_call_banner.dart index 4714ccd72e1..1189ca191f4 100644 --- a/app/lib/pages/phone_calls/active_call_banner.dart +++ b/app/lib/pages/phone_calls/active_call_banner.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -6,7 +7,6 @@ import 'package:provider/provider.dart'; import 'package:omi/backend/schema/phone_call.dart'; import 'package:omi/pages/phone_calls/active_call_page.dart'; import 'package:omi/providers/phone_call_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; /// Compact call banner shown on the home screen when a phone call is active. @@ -27,15 +27,12 @@ class ActiveCallBanner extends StatelessWidget { return GestureDetector( onTap: () { - MixpanelManager().track('Phone Call Banner Tapped'); + PlatformManager.instance.analytics.track('Phone Call Banner Tapped'); Navigator.of(context).push(MaterialPageRoute(builder: (_) => const ActiveCallPage())); }, child: Container( margin: const EdgeInsets.fromLTRB(16, 12, 16, 4), - decoration: BoxDecoration( - color: const Color(0xFF1F1F25), - borderRadius: BorderRadius.circular(24), - ), + decoration: BoxDecoration(color: const Color(0xFF1F1F25), borderRadius: BorderRadius.circular(24)), child: Padding( padding: const EdgeInsets.fromLTRB(16, 14, 12, 14), child: Column( @@ -120,26 +117,28 @@ class _CallInfoRow extends StatelessWidget { return Row( children: [ // Phone icon — green when transcribing, orange when reconnecting, red when failed - Builder(builder: (context) { - final provider = context.watch(); - Color iconColor; - switch (provider.transcriptionStatus) { - case TranscriptionStatus.reconnecting: - iconColor = Colors.orange; - break; - case TranscriptionStatus.failed: - iconColor = Colors.red; - break; - default: - iconColor = const Color(0xFF34C759); - } - return Container( - width: 32, - height: 32, - decoration: BoxDecoration(color: iconColor, shape: BoxShape.circle), - child: const Icon(Icons.phone_in_talk, color: Colors.white, size: 16), - ); - }), + Builder( + builder: (context) { + final provider = context.watch(); + Color iconColor; + switch (provider.transcriptionStatus) { + case TranscriptionStatus.reconnecting: + iconColor = Colors.orange; + break; + case TranscriptionStatus.failed: + iconColor = Colors.red; + break; + default: + iconColor = const Color(0xFF34C759); + } + return Container( + width: 32, + height: 32, + decoration: BoxDecoration(color: iconColor, shape: BoxShape.circle), + child: const Icon(Icons.phone_in_talk, color: Colors.white, size: 16), + ); + }, + ), const SizedBox(width: 10), // Contact name / number Expanded( @@ -335,7 +334,7 @@ class ActiveCallTopBar extends StatelessWidget { return GestureDetector( onTap: () { - MixpanelManager().track('Phone Call Top Bar Tapped'); + PlatformManager.instance.analytics.track('Phone Call Top Bar Tapped'); Navigator.of(context).push(MaterialPageRoute(builder: (_) => const ActiveCallPage())); }, child: Container( diff --git a/app/lib/pages/phone_calls/active_call_page.dart b/app/lib/pages/phone_calls/active_call_page.dart index 8d926b11490..52bb68e82d3 100644 --- a/app/lib/pages/phone_calls/active_call_page.dart +++ b/app/lib/pages/phone_calls/active_call_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; @@ -6,7 +7,6 @@ import 'package:omi/backend/schema/phone_call.dart'; import 'package:omi/backend/schema/transcript_segment.dart'; import 'package:omi/models/audio_route.dart'; import 'package:omi/providers/phone_call_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class ActiveCallPage extends StatefulWidget { @@ -46,13 +46,13 @@ class _ActiveCallPageState extends State { } void _showDtmfDialpad(BuildContext context, PhoneCallProvider provider) { - MixpanelManager().phoneCallDialpadOpened(); + PlatformManager.instance.analytics.phoneCallDialpadOpened(); showModalBottomSheet( context: context, backgroundColor: Colors.transparent, builder: (_) => _DtmfDialpadSheet( onDigitPressed: (digit) { - MixpanelManager().phoneCallDialpadDigitPressed(digit); + PlatformManager.instance.analytics.phoneCallDialpadDigitPressed(digit); provider.sendDtmf(digit); }, ), @@ -97,7 +97,7 @@ class _ActiveCallPageState extends State { if (isCallInProgress) IconButton( onPressed: () { - MixpanelManager().track('Phone Call Minimized'); + PlatformManager.instance.analytics.track('Phone Call Minimized'); Navigator.of(context).popUntil((route) => route.isFirst); }, icon: const Icon(Icons.arrow_back_ios_new, color: Colors.white, size: 22), @@ -369,8 +369,10 @@ class _CallControls extends StatelessWidget { Container( width: 56, height: 56, - decoration: - BoxDecoration(shape: BoxShape.circle, color: isSpeakerOn ? Colors.white : Colors.grey[800]), + decoration: BoxDecoration( + shape: BoxShape.circle, + color: isSpeakerOn ? Colors.white : Colors.grey[800], + ), child: Icon( isSpeakerOn ? Icons.volume_up : Icons.volume_down, color: isSpeakerOn ? Colors.black : (isActive ? Colors.white : Colors.grey[600]), @@ -378,8 +380,10 @@ class _CallControls extends StatelessWidget { ), ), const SizedBox(height: 8), - Text(context.l10n.phoneSpeaker, - style: TextStyle(fontSize: 12, color: isActive ? Colors.white : Colors.grey[600])), + Text( + context.l10n.phoneSpeaker, + style: TextStyle(fontSize: 12, color: isActive ? Colors.white : Colors.grey[600]), + ), ], ), ), @@ -635,11 +639,7 @@ class _AudioRouteSheet extends StatelessWidget { final AudioRoute? selectedRoute; final void Function(AudioRoute route) onRouteSelected; - const _AudioRouteSheet({ - required this.routes, - required this.selectedRoute, - required this.onRouteSelected, - }); + const _AudioRouteSheet({required this.routes, required this.selectedRoute, required this.onRouteSelected}); IconData _iconForType(AudioRouteType type) { switch (type) { @@ -677,8 +677,10 @@ class _AudioRouteSheet extends StatelessWidget { decoration: BoxDecoration(color: Colors.grey[700], borderRadius: BorderRadius.circular(2)), ), const SizedBox(height: 16), - Text(context.l10n.audioOutput, - style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white)), + Text( + context.l10n.audioOutput, + style: const TextStyle(fontSize: 18, fontWeight: FontWeight.w600, color: Colors.white), + ), const SizedBox(height: 12), ...routes.map((route) { bool isSelected = selectedRoute?.id == route.id; diff --git a/app/lib/pages/phone_calls/phone_calls_page.dart b/app/lib/pages/phone_calls/phone_calls_page.dart index 0b78d64a833..825007cbe87 100644 --- a/app/lib/pages/phone_calls/phone_calls_page.dart +++ b/app/lib/pages/phone_calls/phone_calls_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; @@ -11,7 +12,6 @@ import 'package:omi/pages/phone_calls/phone_setup_intro_page.dart'; import 'package:omi/pages/settings/phone_call_settings_page.dart'; import 'package:omi/providers/phone_call_provider.dart'; import 'package:omi/providers/usage_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class PhoneCallsPage extends StatefulWidget { @@ -36,7 +36,7 @@ class _PhoneCallsPageState extends State with SingleTickerProvid super.initState(); _tabController = TabController(length: 2, vsync: this); _loadContacts(); - MixpanelManager().phoneCallPageOpened(); + PlatformManager.instance.analytics.phoneCallPageOpened(); WidgetsBinding.instance.addPostFrameCallback((_) { context.read().loadVerifiedNumbers(); }); @@ -99,9 +99,7 @@ class _PhoneCallsPageState extends State with SingleTickerProvid // Block if already on a call if (provider.callState != PhoneCallState.idle && provider.callState != PhoneCallState.ended) { if (!mounted) return; - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(context.l10n.callAlreadyInProgress)), - ); + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(context.l10n.callAlreadyInProgress))); return; } @@ -166,9 +164,8 @@ class _PhoneCallsPageState extends State with SingleTickerProvid actions: [ IconButton( icon: const Icon(Icons.settings_outlined, color: Colors.white), - onPressed: () => Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const PhoneCallSettingsPage()), - ), + onPressed: () => + Navigator.of(context).push(MaterialPageRoute(builder: (_) => const PhoneCallSettingsPage())), ), ], bottom: TabBar( diff --git a/app/lib/pages/phone_calls/phone_calls_upsell_sheet.dart b/app/lib/pages/phone_calls/phone_calls_upsell_sheet.dart index 9b166f7612f..496755376bb 100644 --- a/app/lib/pages/phone_calls/phone_calls_upsell_sheet.dart +++ b/app/lib/pages/phone_calls/phone_calls_upsell_sheet.dart @@ -1,8 +1,8 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:omi/pages/settings/widgets/plans_sheet.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; /// Shows the phone calls upsell bottom sheet. @@ -136,7 +136,7 @@ class _PhoneCallsUpsellSheet extends StatelessWidget { GestureDetector( onTap: () { HapticFeedback.mediumImpact(); - MixpanelManager().phoneCallUpsellUpgradeTapped(); + PlatformManager.instance.analytics.phoneCallUpsellUpgradeTapped(); final navigator = Navigator.of(context); navigator.pop(); showModalBottomSheet( @@ -162,7 +162,7 @@ class _PhoneCallsUpsellSheet extends StatelessWidget { // Dismiss GestureDetector( onTap: () { - MixpanelManager().phoneCallUpsellDismissed(); + PlatformManager.instance.analytics.phoneCallUpsellDismissed(); Navigator.of(context).pop(); }, child: Padding( diff --git a/app/lib/pages/referral/referral_page.dart b/app/lib/pages/referral/referral_page.dart index 07ded1d100a..4491e94bee2 100644 --- a/app/lib/pages/referral/referral_page.dart +++ b/app/lib/pages/referral/referral_page.dart @@ -1,8 +1,8 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:webview_flutter/webview_flutter.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class ReferralPage extends StatefulWidget { @@ -20,7 +20,7 @@ class _ReferralPageState extends State { void initState() { super.initState(); - MixpanelManager().pageOpened('Referral Program'); + PlatformManager.instance.analytics.pageOpened('Referral Program'); WidgetsBinding.instance.addPostFrameCallback((_) { if (!mounted) return; diff --git a/app/lib/pages/settings/about.dart b/app/lib/pages/settings/about.dart index ec980aa5f91..3ff7cf4bb4c 100644 --- a/app/lib/pages/settings/about.dart +++ b/app/lib/pages/settings/about.dart @@ -1,10 +1,10 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:omi/pages/settings/webview.dart'; import 'package:omi/utils/analytics/intercom.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; @@ -30,7 +30,7 @@ class _AboutOmiPageState extends State { title: Text(context.l10n.privacyPolicy, style: const TextStyle(color: Colors.white)), trailing: const Icon(Icons.privacy_tip_outlined, size: 20), onTap: () { - MixpanelManager().pageOpened('About Privacy Policy'); + PlatformManager.instance.analytics.pageOpened('About Privacy Policy'); routeToPage( context, PageWebView(url: 'https://www.omi.me/pages/privacy', title: context.l10n.privacyPolicyTitle), @@ -43,7 +43,7 @@ class _AboutOmiPageState extends State { subtitle: const Text('https://omi.me'), trailing: const Icon(Icons.language_outlined, size: 20), onTap: () { - MixpanelManager().pageOpened('About Visit Website'); + PlatformManager.instance.analytics.pageOpened('About Visit Website'); // routeToPage(context, const PageWebView(url: 'https://www.omi.me/', title: 'omi')); launchUrl(Uri.parse('https://www.omi.me/')); }, @@ -63,7 +63,7 @@ class _AboutOmiPageState extends State { subtitle: Text(context.l10n.membersAndCounting), trailing: const Icon(Icons.discord, color: Colors.purple, size: 20), onTap: () { - MixpanelManager().pageOpened('About Join Discord'); + PlatformManager.instance.analytics.pageOpened('About Join Discord'); launchUrl(Uri.parse('http://discord.omi.me')); }, ), diff --git a/app/lib/pages/settings/ai_app_generator_page.dart b/app/lib/pages/settings/ai_app_generator_page.dart index 6999a84c5ae..c54135fb60b 100644 --- a/app/lib/pages/settings/ai_app_generator_page.dart +++ b/app/lib/pages/settings/ai_app_generator_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,7 +9,6 @@ import 'package:omi/widgets/shimmer_with_timeout.dart'; import 'package:omi/backend/schema/app.dart'; import 'package:omi/pages/apps/app_detail/app_detail.dart'; import 'package:omi/pages/settings/ai_app_generator_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/providers/app_provider.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; @@ -18,10 +18,7 @@ class AiAppGeneratorPage extends StatelessWidget { @override Widget build(BuildContext context) { - return ChangeNotifierProvider( - create: (_) => AiAppGeneratorProvider(), - child: const _AiAppGeneratorPageView(), - ); + return ChangeNotifierProvider(create: (_) => AiAppGeneratorProvider(), child: const _AiAppGeneratorPageView()); } } @@ -40,7 +37,7 @@ class _AiAppGeneratorPageState extends State<_AiAppGeneratorPageView> { @override void initState() { super.initState(); - MixpanelManager().aiAppGeneratorPageOpened(); + PlatformManager.instance.analytics.aiAppGeneratorPageOpened(); WidgetsBinding.instance.addPostFrameCallback((_) { final provider = context.read(); provider.setAppProvider(context.read()); @@ -1106,9 +1103,9 @@ class _AiAppGeneratorPageState extends State<_AiAppGeneratorPageView> { Future _generateApp(AiAppGeneratorProvider provider) async { FocusScope.of(context).unfocus(); - MixpanelManager().aiAppGeneratorPromptSubmitted(promptLength: _promptController.text.length); + PlatformManager.instance.analytics.aiAppGeneratorPromptSubmitted(promptLength: _promptController.text.length); await provider.generateApp(_promptController.text); - MixpanelManager().aiAppGeneratorAppGenerated(success: provider.hasGeneratedApp); + PlatformManager.instance.analytics.aiAppGeneratorAppGenerated(success: provider.hasGeneratedApp); } Future _submitApp(AiAppGeneratorProvider provider) async { diff --git a/app/lib/pages/settings/apple_health_detail_page.dart b/app/lib/pages/settings/apple_health_detail_page.dart index 02d257fa83e..b3b6d93c541 100644 --- a/app/lib/pages/settings/apple_health_detail_page.dart +++ b/app/lib/pages/settings/apple_health_detail_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -5,7 +6,6 @@ import 'package:omi/gen/assets.gen.dart'; import 'package:omi/pages/settings/integrations_page.dart'; import 'package:omi/providers/integration_provider.dart'; import 'package:omi/services/apple_health_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/widgets/animated_loading_button.dart'; @@ -37,13 +37,13 @@ class _AppleHealthDetailPageState extends State { final scaffoldMessenger = ScaffoldMessenger.of(context); final integrationProvider = context.read(); - MixpanelManager().integrationConnectAttempted(integrationName: 'Apple Health'); + PlatformManager.instance.analytics.integrationConnectAttempted(integrationName: 'Apple Health'); final result = await service.connect(); if (!mounted) return; if (result.isSuccess) { - MixpanelManager().integrationConnectSucceeded(integrationName: 'Apple Health'); + PlatformManager.instance.analytics.integrationConnectSucceeded(integrationName: 'Apple Health'); final synced = await service.syncHealthDataToBackend(days: 7); if (synced) { Logger.debug('✓ Apple Health data synced to backend'); @@ -52,20 +52,14 @@ class _AppleHealthDetailPageState extends State { } await integrationProvider.saveConnection(IntegrationApp.appleHealth.key, {}); if (!mounted) return; - scaffoldMessenger.showSnackBar( - SnackBar(content: Text(result.message), duration: const Duration(seconds: 2)), - ); + scaffoldMessenger.showSnackBar(SnackBar(content: Text(result.message), duration: const Duration(seconds: 2))); } else { - MixpanelManager().integrationConnectFailed(integrationName: 'Apple Health'); + PlatformManager.instance.analytics.integrationConnectFailed(integrationName: 'Apple Health'); if (result == AppleHealthResult.permissionDenied) { _showDeniedDialog(); } else { scaffoldMessenger.showSnackBar( - SnackBar( - content: Text(result.message), - backgroundColor: Colors.red, - duration: const Duration(seconds: 3), - ), + SnackBar(content: Text(result.message), backgroundColor: Colors.red, duration: const Duration(seconds: 3)), ); } } @@ -80,10 +74,7 @@ class _AppleHealthDetailPageState extends State { return AlertDialog( backgroundColor: const Color(0xFF1C1C1E), shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), - title: Text( - context.l10n.appleHealthDeniedTitle, - style: const TextStyle(color: Colors.white), - ), + title: Text(context.l10n.appleHealthDeniedTitle, style: const TextStyle(color: Colors.white)), content: Text( context.l10n.appleHealthDeniedBody, style: const TextStyle(color: Color(0xFF8E8E93), height: 1.4), @@ -135,7 +126,7 @@ class _AppleHealthDetailPageState extends State { final success = await integrationProvider.deleteConnection(IntegrationApp.appleHealth.key); if (!mounted) return; if (success) { - MixpanelManager().integrationDisconnected(integrationName: 'Apple Health'); + PlatformManager.instance.analytics.integrationDisconnected(integrationName: 'Apple Health'); scaffoldMessenger.showSnackBar( SnackBar( content: Text(context.l10n.disconnectedFrom(IntegrationApp.appleHealth.displayName)), diff --git a/app/lib/pages/settings/conversation_display_settings.dart b/app/lib/pages/settings/conversation_display_settings.dart index c2a45ae7598..2637952cde7 100644 --- a/app/lib/pages/settings/conversation_display_settings.dart +++ b/app/lib/pages/settings/conversation_display_settings.dart @@ -1,10 +1,10 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:omi/providers/conversation_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class ConversationDisplaySettings extends StatefulWidget { @@ -18,7 +18,7 @@ class _ConversationDisplaySettingsState extends State children}) { @@ -146,7 +146,7 @@ class _ConversationDisplaySettingsState extends State { @override Widget build(BuildContext context) { - MixpanelManager().pageOpened('Custom Vocabulary'); + PlatformManager.instance.analytics.pageOpened('Custom Vocabulary'); return GestureDetector( onTap: () => FocusScope.of(context).unfocus(), diff --git a/app/lib/pages/settings/daily_summary_detail_page.dart b/app/lib/pages/settings/daily_summary_detail_page.dart index 5e5c0086174..1d0acb6b2c5 100644 --- a/app/lib/pages/settings/daily_summary_detail_page.dart +++ b/app/lib/pages/settings/daily_summary_detail_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -9,7 +10,6 @@ import 'package:omi/backend/http/api/users.dart'; import 'package:omi/backend/schema/daily_summary.dart'; import 'package:omi/pages/conversation_detail/maps_util.dart'; import 'package:omi/pages/conversation_detail/page.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class DailySummaryDetailPage extends StatefulWidget { @@ -50,7 +50,7 @@ class _DailySummaryDetailPageState extends State with Si }); _animationController.forward(); // Track page view - MixpanelManager().dailySummaryDetailViewed( + PlatformManager.instance.analytics.dailySummaryDetailViewed( summaryId: widget.summaryId, date: widget.summary!.date, source: 'direct', @@ -67,7 +67,7 @@ class _DailySummaryDetailPageState extends State with Si _animationController.forward(); // Track page view if (summary != null) { - MixpanelManager().dailySummaryDetailViewed( + PlatformManager.instance.analytics.dailySummaryDetailViewed( summaryId: widget.summaryId, date: summary.date, source: 'api_fetch', @@ -83,8 +83,8 @@ class _DailySummaryDetailPageState extends State with Si body: _isLoading ? const Center(child: CircularProgressIndicator(color: Colors.white)) : _summary == null - ? _buildNotFound() - : _buildContent(), + ? _buildNotFound() + : _buildContent(), ); } @@ -93,7 +93,7 @@ class _DailySummaryDetailPageState extends State with Si // Track conversation click if (_summary != null) { - MixpanelManager().dailySummaryConversationClicked( + PlatformManager.instance.analytics.dailySummaryConversationClicked( summaryId: widget.summaryId, conversationId: conversationId, source: 'daily_summary_detail', @@ -412,9 +412,8 @@ class _DailySummaryDetailPageState extends State with Si initialCenter: singleLocation ? points.first : LatLng(centerLat, centerLng), initialZoom: singleLocation ? 14 : 12, // Use bounds fitting for multiple locations - initialCameraFit: singleLocation - ? null - : CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(50)), + initialCameraFit: + singleLocation ? null : CameraFit.bounds(bounds: bounds, padding: const EdgeInsets.all(50)), interactionOptions: const InteractionOptions(flags: InteractiveFlag.none), ), children: [ @@ -668,8 +667,8 @@ class _DailySummaryDetailPageState extends State with Si final endFormatted = _formatTimeTo12Hour(location.endTime); final timeText = startFormatted.isNotEmpty ? (endFormatted.isNotEmpty && startFormatted != endFormatted - ? '$startFormatted - $endFormatted' - : startFormatted) + ? '$startFormatted - $endFormatted' + : startFormatted) : ''; return GestureDetector( diff --git a/app/lib/pages/settings/daily_summary_settings_page.dart b/app/lib/pages/settings/daily_summary_settings_page.dart index 00dc507b0a4..a15804785a8 100644 --- a/app/lib/pages/settings/daily_summary_settings_page.dart +++ b/app/lib/pages/settings/daily_summary_settings_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; @@ -6,7 +7,6 @@ import 'package:provider/provider.dart'; import 'package:omi/backend/http/api/users.dart'; import 'package:omi/providers/conversation_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class DailySummarySettingsPage extends StatefulWidget { @@ -25,7 +25,7 @@ class _DailySummarySettingsPageState extends State { void initState() { super.initState(); _loadSettings(); - MixpanelManager().dailySummarySettingsOpened(); + PlatformManager.instance.analytics.dailySummarySettingsOpened(); } Future _loadSettings() async { @@ -52,13 +52,13 @@ class _DailySummarySettingsPageState extends State { Future _updateEnabled(bool value) async { setState(() => _enabled = value); await setDailySummarySettings(enabled: value); - MixpanelManager().dailySummaryToggled(enabled: value); + PlatformManager.instance.analytics.dailySummaryToggled(enabled: value); } Future _updateHour(int hour) async { setState(() => _selectedHour = hour); await setDailySummarySettings(hour: hour); - MixpanelManager().dailySummaryTimeChanged(hour: hour); + PlatformManager.instance.analytics.dailySummaryTimeChanged(hour: hour); } Future _showHourPicker() async { @@ -172,7 +172,7 @@ class _DailySummarySettingsPageState extends State { Navigator.pop(context); // Dismiss loading if (summaryId != null) { - MixpanelManager().dailySummaryTestGenerated(date: dateStr); + PlatformManager.instance.analytics.dailySummaryTestGenerated(date: dateStr); // Refresh the hasDailySummaries flag so the Recap tab shows Provider.of(context, listen: false).checkHasDailySummaries(); @@ -184,7 +184,7 @@ class _DailySummarySettingsPageState extends State { ), ); } else { - MixpanelManager().dailySummaryTestGenerationFailed(date: dateStr); + PlatformManager.instance.analytics.dailySummaryTestGenerationFailed(date: dateStr); ScaffoldMessenger.of(context).showSnackBar( SnackBar( diff --git a/app/lib/pages/settings/data_privacy_page.dart b/app/lib/pages/settings/data_privacy_page.dart index 7200cd776c2..be70da7ebcb 100644 --- a/app/lib/pages/settings/data_privacy_page.dart +++ b/app/lib/pages/settings/data_privacy_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; @@ -6,7 +7,6 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:omi/backend/schema/app.dart'; import 'package:omi/pages/apps/app_detail/app_detail.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/pages/settings/widgets/data_protection_section.dart'; import 'package:omi/providers/app_provider.dart'; import 'package:omi/providers/user_provider.dart'; @@ -24,7 +24,7 @@ class _DataPrivacyPageState extends State { @override void initState() { super.initState(); - MixpanelManager().dataPrivacyPageOpened(); + PlatformManager.instance.analytics.dataPrivacyPageOpened(); } Widget _buildIntroSection(BuildContext context) { @@ -143,9 +143,8 @@ class _DataPrivacyPageState extends State { const SizedBox(height: 24), Consumer( builder: (context, appProvider, child) { - final appsWithDataAccess = appProvider.apps - .where((app) => app.enabled && app.worksExternally()) - .toList(); + final appsWithDataAccess = + appProvider.apps.where((app) => app.enabled && app.worksExternally()).toList(); return Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/app/lib/pages/settings/delete_account.dart b/app/lib/pages/settings/delete_account.dart index 029c5755d3f..1629d7830d3 100644 --- a/app/lib/pages/settings/delete_account.dart +++ b/app/lib/pages/settings/delete_account.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -8,7 +9,6 @@ import 'package:omi/backend/http/api/users.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/core/app_shell.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; import 'package:omi/utils/wal_file_manager.dart'; @@ -33,7 +33,7 @@ class _DeleteAccountState extends State { @override void initState() { super.initState(); - MixpanelManager().deleteAccountFlowStarted(); + PlatformManager.instance.analytics.deleteAccountFlowStarted(); _confirmController.addListener(() => setState(() {})); } @@ -98,9 +98,12 @@ class _DeleteAccountState extends State { setState(() => _isDeleting = false); return; } - MixpanelManager().deleteAccountConfirmed(); - MixpanelManager().deleteAccountFeedbackSubmitted(reason: _selectedReason ?? 'unspecified', details: details); - MixpanelManager().deleteUser(); + PlatformManager.instance.analytics.deleteAccountConfirmed(); + PlatformManager.instance.analytics.deleteAccountFeedbackSubmitted( + reason: _selectedReason ?? 'unspecified', + details: details, + ); + PlatformManager.instance.analytics.deleteUser(); await WalFileManager.clearAll(); await SharedPreferencesUtil().clear(); await FirebaseAuth.instance.signOut(); @@ -120,7 +123,7 @@ class _DeleteAccountState extends State { onPopInvokedWithResult: (didPop, _) { if (didPop) { // Native swipe/back on page 0 still counts as an abandon. - MixpanelManager().deleteAccountAbandoned(step: _page + 1, reason: _selectedReason); + PlatformManager.instance.analytics.deleteAccountAbandoned(step: _page + 1, reason: _selectedReason); return; } if (!_isDeleting) _back(); @@ -207,7 +210,7 @@ class _DeleteAccountState extends State { child: ElevatedButton( onPressed: canContinue ? () { - MixpanelManager().deleteAccountReasonSelected(reason: _selectedReason!); + PlatformManager.instance.analytics.deleteAccountReasonSelected(reason: _selectedReason!); _next(); } : null, @@ -480,7 +483,10 @@ class _DeleteAccountState extends State { onPressed: _isDeleting ? null : () { - MixpanelManager().deleteAccountKeptAccount(step: 3, reason: _selectedReason); + PlatformManager.instance.analytics.deleteAccountKeptAccount( + step: 3, + reason: _selectedReason, + ); Navigator.of(context).pop(); }, style: ElevatedButton.styleFrom( diff --git a/app/lib/pages/settings/developer.dart b/app/lib/pages/settings/developer.dart index 55bf7595bef..e676b93a7c2 100644 --- a/app/lib/pages/settings/developer.dart +++ b/app/lib/pages/settings/developer.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -27,7 +28,6 @@ import 'package:omi/providers/device_provider.dart'; import 'package:omi/providers/developer_mode_provider.dart'; import 'package:omi/providers/mcp_provider.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/debug_log_manager.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; @@ -307,7 +307,7 @@ class _DeveloperSettingsPageState extends State<_DeveloperSettingsPageView> { child: InkWell( onTap: () { launchUrl(Uri.parse(url)); - MixpanelManager().pageOpened('$label Docs'); + PlatformManager.instance.analytics.pageOpened('$label Docs'); }, borderRadius: BorderRadius.circular(20), child: Padding( @@ -849,7 +849,7 @@ class _DeveloperSettingsPageState extends State<_DeveloperSettingsPageView> { if (result.status == ShareResultStatus.success) { Logger.debug('Export shared'); } - MixpanelManager().exportMemories(); + PlatformManager.instance.analytics.exportMemories(); setState(() => provider.loadingExportMemories = false); }, child: Container( diff --git a/app/lib/pages/settings/device_diagnostics.dart b/app/lib/pages/settings/device_diagnostics.dart index 775a2f7452a..f0228c64d48 100644 --- a/app/lib/pages/settings/device_diagnostics.dart +++ b/app/lib/pages/settings/device_diagnostics.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:fl_chart/fl_chart.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -12,7 +13,6 @@ import 'package:share_plus/share_plus.dart'; import 'package:omi/gen/pigeon_communicator.g.dart'; import 'package:omi/providers/device_provider.dart'; import 'package:omi/services/bridges/ble_bridge.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class DeviceDiagnostics extends StatefulWidget { @@ -38,7 +38,7 @@ class _DeviceDiagnosticsState extends State { @override void initState() { super.initState(); - MixpanelManager().track('Diagnostics Opened'); + PlatformManager.instance.analytics.track('Diagnostics Opened'); _loadAll(); _startRssiStreaming(); } @@ -121,7 +121,7 @@ class _DeviceDiagnosticsState extends State { final file = File('${dir.path}/omi_diagnostics_${DateTime.now().millisecondsSinceEpoch}.json'); await file.writeAsString(json); await SharePlus.instance.share(ShareParams(files: [XFile(file.path)], subject: 'Omi Device Diagnostics')); - MixpanelManager().track( + PlatformManager.instance.analytics.track( 'Diagnostics Exported', properties: { 'disconnect_count': (_diagnostics?.disconnectHistory ?? []).length, @@ -479,8 +479,11 @@ class _DeviceDiagnosticsState extends State { ), child: Text( label, - style: - TextStyle(color: active ? Colors.white : Colors.grey.shade400, fontSize: 13, fontWeight: FontWeight.w500), + style: TextStyle( + color: active ? Colors.white : Colors.grey.shade400, + fontSize: 13, + fontWeight: FontWeight.w500, + ), ), ), ); diff --git a/app/lib/pages/settings/device_settings.dart b/app/lib/pages/settings/device_settings.dart index c0949bfbb76..7b4171b450b 100644 --- a/app/lib/pages/settings/device_settings.dart +++ b/app/lib/pages/settings/device_settings.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -18,7 +19,6 @@ import 'package:omi/providers/device_provider.dart'; import 'package:omi/services/devices.dart'; import 'package:omi/services/services.dart'; import 'package:omi/utils/analytics/intercom.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/other/temp.dart'; @@ -746,7 +746,7 @@ class _DeviceSettingsState extends State { provider.setIsConnected(false); await provider.setConnectedDevice(null); provider.updateConnectingStatus(false); - MixpanelManager().disconnectFriendClicked(); + PlatformManager.instance.analytics.disconnectFriendClicked(); if (context.mounted) { Navigator.of(context).pop(); ScaffoldMessenger.of( diff --git a/app/lib/pages/settings/import_history_page.dart b/app/lib/pages/settings/import_history_page.dart index 8ef2cfecb33..4122b9bc51c 100644 --- a/app/lib/pages/settings/import_history_page.dart +++ b/app/lib/pages/settings/import_history_page.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -10,7 +11,6 @@ import 'package:pull_down_button/pull_down_button.dart'; import 'package:omi/widgets/shimmer_with_timeout.dart'; import 'package:omi/backend/http/api/imports.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; @@ -30,7 +30,7 @@ class _ImportHistoryPageState extends State { @override void initState() { super.initState(); - MixpanelManager().importHistoryPageOpened(); + PlatformManager.instance.analytics.importHistoryPageOpened(); _loadJobs(); } @@ -92,7 +92,7 @@ class _ImportHistoryPageState extends State { Future _startLimitlessImport() async { try { if (!mounted) return; - MixpanelManager().importStarted(source: 'limitless'); + PlatformManager.instance.analytics.importStarted(source: 'limitless'); setState(() => _isUploading = true); // Pick ZIP file diff --git a/app/lib/pages/settings/integrations_page.dart b/app/lib/pages/settings/integrations_page.dart index 981b4eaf722..a6dc19e3a81 100644 --- a/app/lib/pages/settings/integrations_page.dart +++ b/app/lib/pages/settings/integrations_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -8,7 +9,6 @@ import 'package:omi/pages/settings/apple_health_detail_page.dart'; import 'package:omi/providers/integration_provider.dart'; import 'package:omi/services/apple_health_service.dart'; import 'package:omi/services/google_calendar_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/other/temp.dart'; @@ -99,7 +99,7 @@ class _IntegrationsPageState extends State with WidgetsBinding @override void initState() { super.initState(); - MixpanelManager().integrationsPageOpened(); + PlatformManager.instance.analytics.integrationsPageOpened(); WidgetsBinding.instance.addObserver(this); // Schedule loading for after the first frame to avoid setState during build WidgetsBinding.instance.addPostFrameCallback((_) { @@ -131,7 +131,7 @@ class _IntegrationsPageState extends State with WidgetsBinding if (!app.isAvailable) { return; } - MixpanelManager().integrationConnectAttempted(integrationName: app.displayName); + PlatformManager.instance.analytics.integrationConnectAttempted(integrationName: app.displayName); if (app == IntegrationApp.googleCalendar) { final service = GoogleCalendarService(); @@ -158,9 +158,7 @@ class _IntegrationsPageState extends State with WidgetsBinding } return; } - await Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const AppleHealthDetailPage()), - ); + await Navigator.of(context).push(MaterialPageRoute(builder: (_) => const AppleHealthDetailPage())); if (mounted) await _loadFromBackend(); } @@ -175,7 +173,7 @@ class _IntegrationsPageState extends State with WidgetsBinding final success = await authenticate(); if (success) { - MixpanelManager().integrationConnectSucceeded(integrationName: app.displayName); + PlatformManager.instance.analytics.integrationConnectSucceeded(integrationName: app.displayName); if (mounted) { scaffoldMessenger.showSnackBar( SnackBar(content: Text(context.l10n.completeAuthInBrowser), duration: const Duration(seconds: 5)), @@ -184,7 +182,7 @@ class _IntegrationsPageState extends State with WidgetsBinding await _loadFromBackend(); Logger.debug('✓ Integration enabled: ${app.displayName} (${app.key}) - authentication in progress'); } else { - MixpanelManager().integrationConnectFailed(integrationName: app.displayName); + PlatformManager.instance.analytics.integrationConnectFailed(integrationName: app.displayName); if (mounted) { scaffoldMessenger.showSnackBar( SnackBar( @@ -237,7 +235,7 @@ class _IntegrationsPageState extends State with WidgetsBinding final success = await integrationProvider.deleteConnection(IntegrationApp.appleHealth.key); if (success) { - MixpanelManager().integrationDisconnected(integrationName: 'Apple Health'); + PlatformManager.instance.analytics.integrationDisconnected(integrationName: 'Apple Health'); if (mounted) { scaffoldMessenger.showSnackBar( SnackBar( @@ -268,7 +266,7 @@ class _IntegrationsPageState extends State with WidgetsBinding final success = await disconnect(); if (success) { - MixpanelManager().integrationDisconnected(integrationName: app.displayName); + PlatformManager.instance.analytics.integrationDisconnected(integrationName: app.displayName); if (mounted) { await integrationProvider.deleteConnection(app.key); } diff --git a/app/lib/pages/settings/language_settings_page.dart b/app/lib/pages/settings/language_settings_page.dart index 33bc7708a23..7454bbca10b 100644 --- a/app/lib/pages/settings/language_settings_page.dart +++ b/app/lib/pages/settings/language_settings_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -7,7 +8,6 @@ import 'package:omi/providers/capture_provider.dart'; import 'package:omi/providers/home_provider.dart'; import 'package:omi/providers/locale_provider.dart'; import 'package:omi/providers/user_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class LanguageSettingsPage extends StatefulWidget { @@ -78,11 +78,11 @@ class _LanguageSettingsPageState extends State { ) { final languageName = homeProvider.userPrimaryLanguage.isNotEmpty ? homeProvider.availableLanguages.entries - .firstWhere( - (element) => element.value == homeProvider.userPrimaryLanguage, - orElse: () => MapEntry(context.l10n.notSet, ''), - ) - .key + .firstWhere( + (element) => element.value == homeProvider.userPrimaryLanguage, + orElse: () => MapEntry(context.l10n.notSet, ''), + ) + .key : context.l10n.notSet; final isUpdatingTranslation = userProvider.isUpdatingSingleLanguageMode; @@ -322,7 +322,7 @@ class _LanguageSettingsPageState extends State { ); if (success) { captureProvider.onRecordProfileSettingChanged(); - MixpanelManager().languageChanged(entry.value); + PlatformManager.instance.analytics.languageChanged(entry.value); } } finally { if (mounted) { @@ -348,7 +348,7 @@ class _LanguageSettingsPageState extends State { @override Widget build(BuildContext context) { - MixpanelManager().pageOpened('Language Settings'); + PlatformManager.instance.analytics.pageOpened('Language Settings'); return Scaffold( backgroundColor: const Color(0xFF0D0D0D), diff --git a/app/lib/pages/settings/notifications_settings_page.dart b/app/lib/pages/settings/notifications_settings_page.dart index f6285e97f1f..52172eb1367 100644 --- a/app/lib/pages/settings/notifications_settings_page.dart +++ b/app/lib/pages/settings/notifications_settings_page.dart @@ -1,9 +1,9 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:omi/backend/http/api/users.dart'; import 'package:omi/backend/preferences.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class NotificationsSettingsPage extends StatefulWidget { @@ -27,7 +27,7 @@ class _NotificationsSettingsPageState extends State { void initState() { super.initState(); _loadSettings(); - MixpanelManager().dailySummarySettingsOpened(); + PlatformManager.instance.analytics.dailySummarySettingsOpened(); } Future _loadSettings() async { @@ -58,7 +58,10 @@ class _NotificationsSettingsPageState extends State { } Future _updateNotificationFrequency(int value) async { - MixpanelManager().notificationFrequencyChanged(oldFrequency: _notificationFrequency, newFrequency: value); + PlatformManager.instance.analytics.notificationFrequencyChanged( + oldFrequency: _notificationFrequency, + newFrequency: value, + ); setState(() => _notificationFrequency = value); SharedPreferencesUtil().notificationFrequency = value; await setMentorNotificationSettings(value); @@ -111,13 +114,13 @@ class _NotificationsSettingsPageState extends State { Future _updateDailySummaryEnabled(bool value) async { setState(() => _dailySummaryEnabled = value); await setDailySummarySettings(enabled: value); - MixpanelManager().dailySummaryToggled(enabled: value); + PlatformManager.instance.analytics.dailySummaryToggled(enabled: value); } Future _updateDailySummaryHour(int hour) async { setState(() => _dailySummaryHour = hour); await setDailySummarySettings(hour: hour); - MixpanelManager().dailySummaryTimeChanged(hour: hour); + PlatformManager.instance.analytics.dailySummaryTimeChanged(hour: hour); } Future _showHourPicker() async { diff --git a/app/lib/pages/settings/permissions_page.dart b/app/lib/pages/settings/permissions_page.dart index 45b3e0d7435..7391d57ae55 100644 --- a/app/lib/pages/settings/permissions_page.dart +++ b/app/lib/pages/settings/permissions_page.dart @@ -1,12 +1,12 @@ import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter_foreground_task/flutter_foreground_task.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:permission_handler/permission_handler.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class PermissionsPage extends StatefulWidget { @@ -83,7 +83,7 @@ class _PermissionsPageState extends State with WidgetsBindingOb await openAppSettings(); } await _checkPermissions(); - MixpanelManager().permissionChanged(permission: name, granted: status.isGranted); + PlatformManager.instance.analytics.permissionChanged(permission: name, granted: status.isGranted); } } @@ -104,7 +104,7 @@ class _PermissionsPageState extends State with WidgetsBindingOb } } await _checkPermissions(); - MixpanelManager().permissionChanged(permission: 'bluetooth', granted: _bluetoothGranted); + PlatformManager.instance.analytics.permissionChanged(permission: 'bluetooth', granted: _bluetoothGranted); } } @@ -114,7 +114,7 @@ class _PermissionsPageState extends State with WidgetsBindingOb } else { await FlutterForegroundTask.requestIgnoreBatteryOptimization(); await _checkPermissions(); - MixpanelManager().permissionChanged(permission: 'background', granted: _backgroundGranted); + PlatformManager.instance.analytics.permissionChanged(permission: 'background', granted: _backgroundGranted); } } @@ -125,7 +125,7 @@ class _PermissionsPageState extends State with WidgetsBindingOb if (await Permission.location.serviceStatus.isDisabled) { await openAppSettings(); await _checkPermissions(); - MixpanelManager().permissionChanged(permission: 'location', granted: _locationGranted); + PlatformManager.instance.analytics.permissionChanged(permission: 'location', granted: _locationGranted); return; } final status = await Permission.locationWhenInUse.request(); @@ -136,7 +136,7 @@ class _PermissionsPageState extends State with WidgetsBindingOb await openAppSettings(); } await _checkPermissions(); - MixpanelManager().permissionChanged(permission: 'location', granted: _locationGranted); + PlatformManager.instance.analytics.permissionChanged(permission: 'location', granted: _locationGranted); } } diff --git a/app/lib/pages/settings/profile.dart b/app/lib/pages/settings/profile.dart index a54d711fb39..7404796dc78 100644 --- a/app/lib/pages/settings/profile.dart +++ b/app/lib/pages/settings/profile.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -10,7 +11,6 @@ import 'package:omi/pages/settings/people.dart'; import 'package:omi/pages/settings/data_privacy_page.dart'; import 'package:omi/pages/speech_profile/page.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; @@ -141,7 +141,7 @@ class _ProfilePageState extends State { builder: (context, setSheetState) { void pick(int value) { setState(() => SharedPreferencesUtil().voiceResponseMode = value); - MixpanelManager().voiceResponseModeChanged(value); + PlatformManager.instance.analytics.voiceResponseModeChanged(value); Navigator.pop(sheetContext); } @@ -262,7 +262,7 @@ class _ProfilePageState extends State { : SharedPreferencesUtil().givenName, icon: const FaIcon(FontAwesomeIcons.solidUser, color: Color(0xFF8E8E93), size: 20), onTap: () async { - MixpanelManager().pageOpened('Profile Change Name'); + PlatformManager.instance.analytics.pageOpened('Profile Change Name'); await showDialog( context: context, builder: (BuildContext context) { @@ -308,7 +308,7 @@ class _ProfilePageState extends State { icon: const FaIcon(FontAwesomeIcons.microphone, color: Color(0xFF8E8E93), size: 20), onTap: () { routeToPage(context, const SpeechProfilePage()); - MixpanelManager().pageOpened('Profile Speech Profile'); + PlatformManager.instance.analytics.pageOpened('Profile Speech Profile'); }, ), const Divider(height: 1, color: Color(0xFF3C3C43)), @@ -384,7 +384,7 @@ class _ProfilePageState extends State { title: context.l10n.deleteAccountTitle, icon: const FaIcon(FontAwesomeIcons.exclamationTriangle, color: Colors.red, size: 20), onTap: () { - MixpanelManager().pageOpened('Profile Delete Account Dialog'); + PlatformManager.instance.analytics.pageOpened('Profile Delete Account Dialog'); Navigator.push(context, MaterialPageRoute(builder: (context) => const DeleteAccount())); }, ), diff --git a/app/lib/pages/settings/settings_drawer.dart b/app/lib/pages/settings/settings_drawer.dart index 5d43e51f1f8..643e7119ae5 100644 --- a/app/lib/pages/settings/settings_drawer.dart +++ b/app/lib/pages/settings/settings_drawer.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -28,7 +29,6 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:device_info_plus/device_info_plus.dart'; import 'package:omi/backend/http/api/announcements.dart'; import 'package:omi/pages/announcements/changelog_sheet.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'device_settings.dart'; import '../conversations/auto_sync_page.dart'; import '../conversations/sync_page.dart'; @@ -295,7 +295,7 @@ class _SettingsDrawerState extends State { void goToIntegrations() => Navigator.of(context).push(MaterialPageRoute(builder: (context) => const IntegrationsPage())); void goToPermissions() { - MixpanelManager().permissionsSettingsOpened(); + PlatformManager.instance.analytics.permissionsSettingsOpened(); routeToPage(context, const PermissionsPage()); } @@ -397,7 +397,7 @@ class _SettingsDrawerState extends State { title: context.l10n.whatsNew, icon: const FaIcon(FontAwesomeIcons.solidStar, color: Color(0xFF8E8E93), size: 20), onTap: () { - MixpanelManager().whatsNewOpened(); + PlatformManager.instance.analytics.whatsNewOpened(); ChangelogSheet.showWithLoading(context, () => getAppChangelogs(limit: 5)); }, ), @@ -595,7 +595,7 @@ class _SettingsDrawerState extends State { title: context.l10n.permissions, icon: const FaIcon(FontAwesomeIcons.shieldHalved, color: Color(0xFF8E8E93), size: 20), onTap: () { - MixpanelManager().permissionsSettingsOpened(); + PlatformManager.instance.analytics.permissionsSettingsOpened(); routeToPage(context, const PermissionsPage()); }, ), @@ -654,7 +654,7 @@ class _SettingsDrawerState extends State { title: context.l10n.whatsNew, icon: const FaIcon(FontAwesomeIcons.solidStar, color: Color(0xFF8E8E93), size: 20), onTap: () { - MixpanelManager().whatsNewOpened(); + PlatformManager.instance.analytics.whatsNewOpened(); ChangelogSheet.showWithLoading(context, () => getAppChangelogs(limit: 5)); }, ), diff --git a/app/lib/pages/settings/task_integrations_page.dart b/app/lib/pages/settings/task_integrations_page.dart index 6dec1909f8f..248f6148d66 100644 --- a/app/lib/pages/settings/task_integrations_page.dart +++ b/app/lib/pages/settings/task_integrations_page.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -15,7 +16,6 @@ import 'package:omi/services/asana_service.dart'; import 'package:omi/services/clickup_service.dart'; import 'package:omi/services/google_tasks_service.dart'; import 'package:omi/services/todoist_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/platform/platform_service.dart'; @@ -180,16 +180,16 @@ class _TaskIntegrationsPageState extends State with Widget void _openSelectedAppSettings() { final selected = context.read().selectedApp; if (selected == TaskIntegrationApp.asana && AsanaService().isAuthenticated) { - MixpanelManager().taskIntegrationSettingsOpened(appName: 'asana'); + PlatformManager.instance.analytics.taskIntegrationSettingsOpened(appName: 'asana'); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const AsanaSettingsPage())); } else if (selected == TaskIntegrationApp.clickup && ClickUpService().isAuthenticated) { - MixpanelManager().taskIntegrationSettingsOpened(appName: 'clickup'); + PlatformManager.instance.analytics.taskIntegrationSettingsOpened(appName: 'clickup'); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const ClickUpSettingsPage())); } else if (selected == TaskIntegrationApp.todoist && TodoistService().isAuthenticated) { - MixpanelManager().taskIntegrationSettingsOpened(appName: 'todoist'); + PlatformManager.instance.analytics.taskIntegrationSettingsOpened(appName: 'todoist'); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const TodoistSettingsPage())); } else if (selected == TaskIntegrationApp.googleTasks && GoogleTasksService().isAuthenticated) { - MixpanelManager().taskIntegrationSettingsOpened(appName: 'google_tasks'); + PlatformManager.instance.analytics.taskIntegrationSettingsOpened(appName: 'google_tasks'); Navigator.of(context).push(MaterialPageRoute(builder: (context) => const GoogleTasksSettingsPage())); } } @@ -214,7 +214,7 @@ class _TaskIntegrationsPageState extends State with Widget // Save connected status to backend so auto-sync works await provider.saveConnectionDetails(app.key, {'connected': true}); await provider.setSelectedApp(app); - MixpanelManager().taskIntegrationEnabled(appName: app.key, success: true); + PlatformManager.instance.analytics.taskIntegrationEnabled(appName: app.key, success: true); Logger.debug('✓ Task integration enabled: ${app.displayName} (${app.key})'); } else { if (mounted) { @@ -254,7 +254,7 @@ class _TaskIntegrationsPageState extends State with Widget Logger.debug('✓ Task integration enabled: ${app.displayName} (${app.key}) - authentication in progress'); } else { // Track authentication failure - MixpanelManager().taskIntegrationAuthFailed(appName: 'todoist'); + PlatformManager.instance.analytics.taskIntegrationAuthFailed(appName: 'todoist'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -288,7 +288,7 @@ class _TaskIntegrationsPageState extends State with Widget Logger.debug('✓ Task integration enabled: ${app.displayName} (${app.key}) - authentication in progress'); } else { // Track authentication failure - MixpanelManager().taskIntegrationAuthFailed(appName: 'asana'); + PlatformManager.instance.analytics.taskIntegrationAuthFailed(appName: 'asana'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -322,7 +322,7 @@ class _TaskIntegrationsPageState extends State with Widget Logger.debug('✓ Task integration enabled: ${app.displayName} (${app.key}) - authentication in progress'); } else { // Track authentication failure - MixpanelManager().taskIntegrationAuthFailed(appName: 'google_tasks'); + PlatformManager.instance.analytics.taskIntegrationAuthFailed(appName: 'google_tasks'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( @@ -356,7 +356,7 @@ class _TaskIntegrationsPageState extends State with Widget Logger.debug('✓ Task integration enabled: ${app.displayName} (${app.key}) - authentication in progress'); } else { // Track authentication failure - MixpanelManager().taskIntegrationAuthFailed(appName: 'clickup'); + PlatformManager.instance.analytics.taskIntegrationAuthFailed(appName: 'clickup'); if (mounted) { ScaffoldMessenger.of(context).showSnackBar( diff --git a/app/lib/pages/settings/transcription_settings_page.dart b/app/lib/pages/settings/transcription_settings_page.dart index 35743a40089..59787e43396 100644 --- a/app/lib/pages/settings/transcription_settings_page.dart +++ b/app/lib/pages/settings/transcription_settings_page.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -23,7 +24,6 @@ import 'package:omi/providers/usage_provider.dart'; import 'package:omi/services/custom_stt_log_service.dart'; import 'package:omi/services/services.dart'; import 'package:omi/services/sockets/transcription_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; @@ -1005,7 +1005,9 @@ class _TranscriptionSettingsPageState extends State { if (!isIOS) { _checkLocalModel(); } - MixpanelManager().transcriptionSourceSelected(source: isIOS ? 'custom_on_device_ios' : 'custom_on_device'); + PlatformManager.instance.analytics.transcriptionSourceSelected( + source: isIOS ? 'custom_on_device_ios' : 'custom_on_device', + ); }); } @@ -1036,7 +1038,7 @@ class _TranscriptionSettingsPageState extends State { onTap: () { setState(() { _useCustomStt = false; - MixpanelManager().transcriptionSourceSelected(source: 'omi'); + PlatformManager.instance.analytics.transcriptionSourceSelected(source: 'omi'); }); }, ), @@ -1063,7 +1065,7 @@ class _TranscriptionSettingsPageState extends State { } // Track source selection - MixpanelManager().transcriptionSourceSelected(source: 'custom_cloud'); + PlatformManager.instance.analytics.transcriptionSourceSelected(source: 'custom_cloud'); }); }, ), @@ -1233,7 +1235,7 @@ class _TranscriptionSettingsPageState extends State { }); // Track which provider was selected (name only, no keys/URLs) - MixpanelManager().transcriptionProviderSelected(provider: provider.name); + PlatformManager.instance.analytics.transcriptionProviderSelected(provider: provider.name); _validateAndSetError(); } diff --git a/app/lib/pages/settings/widgets/cancel_subscription_sheet.dart b/app/lib/pages/settings/widgets/cancel_subscription_sheet.dart index 379dcd5228d..229ba465416 100644 --- a/app/lib/pages/settings/widgets/cancel_subscription_sheet.dart +++ b/app/lib/pages/settings/widgets/cancel_subscription_sheet.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -6,7 +7,6 @@ import 'package:provider/provider.dart'; import 'package:omi/providers/usage_provider.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; /// Full-screen 3-step cancellation flow shown as a page (not a sheet). @@ -14,9 +14,7 @@ class CancelSubscriptionFlow extends StatefulWidget { const CancelSubscriptionFlow({super.key}); static Future show(BuildContext context) { - return Navigator.of(context).push( - MaterialPageRoute(builder: (_) => const CancelSubscriptionFlow()), - ); + return Navigator.of(context).push(MaterialPageRoute(builder: (_) => const CancelSubscriptionFlow())); } @override @@ -33,7 +31,7 @@ class _CancelSubscriptionFlowState extends State { @override void initState() { super.initState(); - MixpanelManager().subscriptionCancelFlowStarted(); + PlatformManager.instance.analytics.subscriptionCancelFlowStarted(); } static const _reasons = [ @@ -76,7 +74,7 @@ class _CancelSubscriptionFlowState extends State { _pageController.previousPage(duration: const Duration(milliseconds: 250), curve: Curves.easeOut); setState(() => _page--); } else { - MixpanelManager().subscriptionCancelAbandoned(step: _page + 1, reason: _selectedReason); + PlatformManager.instance.analytics.subscriptionCancelAbandoned(step: _page + 1, reason: _selectedReason); Navigator.of(context).pop(false); } } @@ -85,7 +83,7 @@ class _CancelSubscriptionFlowState extends State { setState(() => _isCancelling = true); final provider = context.read(); final details = _detailsController.text.trim().isNotEmpty ? _detailsController.text.trim() : null; - MixpanelManager().subscriptionCancelConfirmed(reason: _selectedReason!, details: details); + PlatformManager.instance.analytics.subscriptionCancelConfirmed(reason: _selectedReason!, details: details); try { final success = await provider.cancelUserSubscription(reason: _selectedReason, reasonDetails: details); @@ -167,8 +165,10 @@ class _CancelSubscriptionFlowState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(context.l10n.whyAreYouCanceling, - style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w700)), + Text( + context.l10n.whyAreYouCanceling, + style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w700), + ), const SizedBox(height: 6), Text(context.l10n.cancelReasonSubtitle, style: TextStyle(color: Colors.grey.shade500, fontSize: 15)), ], @@ -193,7 +193,7 @@ class _CancelSubscriptionFlowState extends State { child: ElevatedButton( onPressed: canContinue ? () { - MixpanelManager().subscriptionCancelReasonSelected(reason: _selectedReason!); + PlatformManager.instance.analytics.subscriptionCancelReasonSelected(reason: _selectedReason!); _next(); } : null, @@ -205,8 +205,10 @@ class _CancelSubscriptionFlowState extends State { elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), - child: Text(context.l10n.continueButton, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + child: Text( + context.l10n.continueButton, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), ), ), ), @@ -241,8 +243,10 @@ class _CancelSubscriptionFlowState extends State { ), const SizedBox(width: 14), Expanded( - child: Text(_label(reason.key), - style: TextStyle(color: selected ? Colors.white : Colors.grey.shade400, fontSize: 15)), + child: Text( + _label(reason.key), + style: TextStyle(color: selected ? Colors.white : Colors.grey.shade400, fontSize: 15), + ), ), Icon( selected ? Icons.check_circle_rounded : Icons.circle_outlined, @@ -286,8 +290,10 @@ class _CancelSubscriptionFlowState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(_feedbackTitle(), - style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w700)), + Text( + _feedbackTitle(), + style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w700), + ), const SizedBox(height: 6), Text(_feedbackSubtitle(), style: TextStyle(color: Colors.grey.shade500, fontSize: 15)), ], @@ -305,11 +311,17 @@ class _CancelSubscriptionFlowState extends State { filled: true, fillColor: Colors.grey.shade900.withValues(alpha: 0.5), border: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), borderSide: BorderSide(color: Colors.grey.shade800)), + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: Colors.grey.shade800), + ), enabledBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), borderSide: BorderSide(color: Colors.grey.shade800)), + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: Colors.grey.shade800), + ), focusedBorder: OutlineInputBorder( - borderRadius: BorderRadius.circular(14), borderSide: BorderSide(color: Colors.grey.shade600)), + borderRadius: BorderRadius.circular(14), + borderSide: BorderSide(color: Colors.grey.shade600), + ), counterStyle: TextStyle(color: Colors.grey.shade700), contentPadding: const EdgeInsets.all(16), ), @@ -333,8 +345,10 @@ class _CancelSubscriptionFlowState extends State { elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), - child: Text(context.l10n.continueButton, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + child: Text( + context.l10n.continueButton, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), ), ), const SizedBox(height: 8), @@ -372,11 +386,15 @@ class _CancelSubscriptionFlowState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(context.l10n.justAMoment, - style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w700)), + Text( + context.l10n.justAMoment, + style: const TextStyle(color: Colors.white, fontSize: 24, fontWeight: FontWeight.w700), + ), const SizedBox(height: 6), - Text(context.l10n.cancelConsequencesSubtitle, - style: TextStyle(color: Colors.grey.shade500, fontSize: 15)), + Text( + context.l10n.cancelConsequencesSubtitle, + style: TextStyle(color: Colors.grey.shade500, fontSize: 15), + ), ], ), ), @@ -437,7 +455,10 @@ class _CancelSubscriptionFlowState extends State { onPressed: _isCancelling ? null : () { - MixpanelManager().subscriptionCancelKeptPlan(step: 3, reason: _selectedReason); + PlatformManager.instance.analytics.subscriptionCancelKeptPlan( + step: 3, + reason: _selectedReason, + ); Navigator.of(context).pop(false); }, style: ElevatedButton.styleFrom( @@ -446,8 +467,10 @@ class _CancelSubscriptionFlowState extends State { elevation: 0, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)), ), - child: Text(context.l10n.keepMyPlan, - style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600)), + child: Text( + context.l10n.keepMyPlan, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w600), + ), ), ), const SizedBox(height: 8), @@ -462,8 +485,10 @@ class _CancelSubscriptionFlowState extends State { height: 20, child: CircularProgressIndicator(strokeWidth: 2, color: Colors.grey), ) - : Text(context.l10n.confirmAndCancel, - style: TextStyle(color: Colors.grey.shade600, fontSize: 15)), + : Text( + context.l10n.confirmAndCancel, + style: TextStyle(color: Colors.grey.shade600, fontSize: 15), + ), ), ), ], @@ -490,11 +515,15 @@ class _CancelSubscriptionFlowState extends State { width: 36, height: 36, decoration: BoxDecoration( - color: Colors.grey.shade800.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(10)), + color: Colors.grey.shade800.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(10), + ), child: Center(child: FaIcon(icon, size: 14, color: Colors.grey.shade500)), ), const SizedBox(width: 14), - Expanded(child: Text(text, style: TextStyle(color: Colors.grey.shade400, fontSize: 14, height: 1.3))), + Expanded( + child: Text(text, style: TextStyle(color: Colors.grey.shade400, fontSize: 14, height: 1.3)), + ), ], ), ), diff --git a/app/lib/pages/settings/widgets/developer_api_keys_section.dart b/app/lib/pages/settings/widgets/developer_api_keys_section.dart index c8693c04b90..e7677424cb8 100644 --- a/app/lib/pages/settings/widgets/developer_api_keys_section.dart +++ b/app/lib/pages/settings/widgets/developer_api_keys_section.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:font_awesome_flutter/font_awesome_flutter.dart'; @@ -7,7 +8,6 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:omi/pages/settings/widgets/create_dev_api_key_sheet.dart'; import 'package:omi/pages/settings/widgets/dev_api_key_list_item.dart'; import 'package:omi/providers/dev_api_key_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class DeveloperApiKeysSection extends StatelessWidget { @@ -20,7 +20,7 @@ class DeveloperApiKeysSection extends StatelessWidget { child: InkWell( onTap: () { launchUrl(Uri.parse(url)); - MixpanelManager().pageOpened('$label Docs'); + PlatformManager.instance.analytics.pageOpened('$label Docs'); }, borderRadius: BorderRadius.circular(20), child: Padding( diff --git a/app/lib/pages/settings/widgets/plans_sheet.dart b/app/lib/pages/settings/widgets/plans_sheet.dart index 2d3c7ecea6a..4e070e99346 100644 --- a/app/lib/pages/settings/widgets/plans_sheet.dart +++ b/app/lib/pages/settings/widgets/plans_sheet.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -18,7 +19,6 @@ import 'package:omi/providers/usage_provider.dart'; import 'package:omi/providers/user_provider.dart'; import 'package:omi/services/freemium_transcription_service.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/widgets/confirmation_dialog.dart'; @@ -68,7 +68,7 @@ class _PlansSheetState extends State { await userProvider.optInForTrainingData(); // Track the opt-in submission - MixpanelManager().trainingDataOptInSubmitted(); + PlatformManager.instance.analytics.trainingDataOptInSubmitted(); if (mounted) { AppSnackbar.showSnackbar(context.l10n.thankYouRequestUnderReview); @@ -260,7 +260,7 @@ class _PlansSheetState extends State { setState(() => _isSwitchingToFree = true); try { - MixpanelManager().track('Free Plan Selected', properties: {'source': 'plans_sheet'}); + PlatformManager.instance.analytics.track('Free Plan Selected', properties: {'source': 'plans_sheet'}); final freemiumService = FreemiumTranscriptionService(); final readiness = await freemiumService.checkReadiness(); @@ -478,7 +478,7 @@ class _PlansSheetState extends State { } } - MixpanelManager().upgradePlanSelected(plan: selectedPlan, source: 'Usage Page Plan Sheet'); + PlatformManager.instance.analytics.upgradePlanSelected(plan: selectedPlan, source: 'Usage Page Plan Sheet'); await _handleUpgrade(priceId); } @@ -552,7 +552,7 @@ class _PlansSheetState extends State { // Quick reactivation - no charge now final message = sessionData['message'] as String? ?? context.l10n.subscriptionReactivatedDefault; AppSnackbar.showSnackbar(message); - MixpanelManager().upgradeSucceeded(); + PlatformManager.instance.analytics.upgradeSucceeded(); await provider.fetchSubscription(); } // Otherwise, this is a new subscription requiring checkout @@ -563,9 +563,9 @@ class _PlansSheetState extends State { if (checkoutResult == true) { AppSnackbar.showSnackbar(context.l10n.subscriptionSuccessfulCharged); - MixpanelManager().upgradeSucceeded(); + PlatformManager.instance.analytics.upgradeSucceeded(); } else { - MixpanelManager().upgradeCancelled(); + PlatformManager.instance.analytics.upgradeCancelled(); } } else { AppSnackbar.showSnackbarError(context.l10n.couldNotProcessSubscription); @@ -1466,9 +1466,7 @@ class _PlansSheetState extends State { // Auto-select current plan's tier, or first tier if none selected if (selectedTierId == null) { - final activeTier = sortedTierIds.firstWhereOrNull( - (tid) => grouped[tid]!.any((p) => p['is_active'] == true), - ); + final activeTier = sortedTierIds.firstWhereOrNull((tid) => grouped[tid]!.any((p) => p['is_active'] == true)); selectedTierId = activeTier ?? sortedTierIds.first; } @@ -1506,10 +1504,7 @@ class _PlansSheetState extends State { const SizedBox(width: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: BoxDecoration( - color: Colors.green.shade800, - borderRadius: BorderRadius.circular(8), - ), + decoration: BoxDecoration(color: Colors.green.shade800, borderRadius: BorderRadius.circular(8)), child: Text( context.l10n.savePercent, style: const TextStyle( @@ -1557,9 +1552,7 @@ class _PlansSheetState extends State { // Tier cards — reuse the existing _buildDynamicPlanOption card style ...sortedTierIds.map((tierId) { final tierPlans = grouped[tierId]!; - final planForPeriod = tierPlans.firstWhereOrNull( - (p) => p['interval'] == (isYearly ? 'year' : 'month'), - ); + final planForPeriod = tierPlans.firstWhereOrNull((p) => p['interval'] == (isYearly ? 'year' : 'month')); if (planForPeriod == null) return const SizedBox.shrink(); final isSelected = selectedTierId == tierId; @@ -1761,8 +1754,10 @@ class _PlansSheetState extends State { const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: - BoxDecoration(color: Colors.green.shade800, borderRadius: BorderRadius.circular(8)), + decoration: BoxDecoration( + color: Colors.green.shade800, + borderRadius: BorderRadius.circular(8), + ), child: Text( saveTag, style: const TextStyle( @@ -1778,8 +1773,10 @@ class _PlansSheetState extends State { const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: - BoxDecoration(color: Colors.red.shade800, borderRadius: BorderRadius.circular(8)), + decoration: BoxDecoration( + color: Colors.red.shade800, + borderRadius: BorderRadius.circular(8), + ), child: Text( 'Ends on $endsOnDate', style: const TextStyle( @@ -1794,8 +1791,10 @@ class _PlansSheetState extends State { const SizedBox(height: 8), Container( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), - decoration: - BoxDecoration(color: Colors.red.shade800, borderRadius: BorderRadius.circular(8)), + decoration: BoxDecoration( + color: Colors.red.shade800, + borderRadius: BorderRadius.circular(8), + ), child: const Text( 'Active', style: TextStyle( @@ -1844,22 +1843,21 @@ class _PlansSheetState extends State { padding: const EdgeInsets.only(top: 8), child: Column( children: features - .map((f) => Padding( - padding: const EdgeInsets.only(bottom: 4), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Icon(Icons.check, color: Colors.green[400], size: 14), - const SizedBox(width: 6), - Expanded( - child: Text( - f, - style: TextStyle(color: Colors.grey[300], fontSize: 12, height: 1.3), - ), - ), - ], - ), - )) + .map( + (f) => Padding( + padding: const EdgeInsets.only(bottom: 4), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Icon(Icons.check, color: Colors.green[400], size: 14), + const SizedBox(width: 6), + Expanded( + child: Text(f, style: TextStyle(color: Colors.grey[300], fontSize: 12, height: 1.3)), + ), + ], + ), + ), + ) .toList(), ), ), diff --git a/app/lib/pages/settings/wrapped_2025_page.dart b/app/lib/pages/settings/wrapped_2025_page.dart index cbeddd006ee..b298b4ca5d0 100644 --- a/app/lib/pages/settings/wrapped_2025_page.dart +++ b/app/lib/pages/settings/wrapped_2025_page.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'dart:math' as math; import 'dart:ui' as ui; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; import 'package:flutter/services.dart'; @@ -14,7 +15,6 @@ import 'package:share_plus/share_plus.dart'; import 'package:omi/backend/http/api/wrapped.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/pages/settings/wrapped_2025_share_templates.dart' as templates; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; @@ -59,7 +59,7 @@ class _Wrapped2025PageState extends State { @override void initState() { super.initState(); - MixpanelManager().wrappedPageOpened(); + PlatformManager.instance.analytics.wrappedPageOpened(); SharedPreferencesUtil().hasViewedWrapped2025 = true; _loadWrappedStatus(); _pageController.addListener(_onPageChanged); @@ -100,7 +100,7 @@ class _Wrapped2025PageState extends State { 'Summary Collage', ]; if (page >= 0 && page < cardNames.length) { - MixpanelManager().wrappedCardViewed(cardName: cardNames[page], cardIndex: page); + PlatformManager.instance.analytics.wrappedCardViewed(cardName: cardNames[page], cardIndex: page); } } @@ -148,13 +148,13 @@ class _Wrapped2025PageState extends State { final totalMinutes = (totalHours * 60).toInt(); final totalConvs = _result?['total_conversations'] ?? 0; final daysActive = _result?['days_active'] ?? 0; - MixpanelManager().wrappedGenerationCompleted( + PlatformManager.instance.analytics.wrappedGenerationCompleted( totalConversations: totalConvs, totalMinutes: totalMinutes, daysActive: daysActive, ); } else if (_status == WrappedStatus.error) { - MixpanelManager().wrappedGenerationFailed(error: _error); + PlatformManager.instance.analytics.wrappedGenerationFailed(error: _error); } } } @@ -162,7 +162,7 @@ class _Wrapped2025PageState extends State { } Future _generateWrapped() async { - MixpanelManager().wrappedGenerationStarted(); + PlatformManager.instance.analytics.wrappedGenerationStarted(); setState(() { _status = WrappedStatus.processing; @@ -180,7 +180,7 @@ class _Wrapped2025PageState extends State { _startPolling(); } } else { - MixpanelManager().wrappedGenerationFailed(error: 'Failed to start generation'); + PlatformManager.instance.analytics.wrappedGenerationFailed(error: 'Failed to start generation'); setState(() { _status = WrappedStatus.error; _error = context.l10n.wrappedFailedToStartGeneration; @@ -210,7 +210,7 @@ class _Wrapped2025PageState extends State { final cardName = cardInfo?['name'] ?? filename; final cardIndex = cardInfo?['index'] ?? -1; - MixpanelManager().wrappedShareButtonClicked(cardName: cardName, cardIndex: cardIndex); + PlatformManager.instance.analytics.wrappedShareButtonClicked(cardName: cardName, cardIndex: cardIndex); try { HapticFeedback.mediumImpact(); @@ -226,7 +226,11 @@ class _Wrapped2025PageState extends State { final boundary = _shareTemplateKey.currentContext?.findRenderObject() as RenderRepaintBoundary?; if (boundary == null) { Logger.debug('Share template boundary is null for $filename'); - MixpanelManager().wrappedShareFailed(cardName: cardName, cardIndex: cardIndex, error: 'Boundary is null'); + PlatformManager.instance.analytics.wrappedShareFailed( + cardName: cardName, + cardIndex: cardIndex, + error: 'Boundary is null', + ); return; } @@ -235,7 +239,11 @@ class _Wrapped2025PageState extends State { final image = await boundary.toImage(pixelRatio: 1.5); final byteData = await image.toByteData(format: ui.ImageByteFormat.png); if (byteData == null) { - MixpanelManager().wrappedShareFailed(cardName: cardName, cardIndex: cardIndex, error: 'Byte data is null'); + PlatformManager.instance.analytics.wrappedShareFailed( + cardName: cardName, + cardIndex: cardIndex, + error: 'Byte data is null', + ); return; } @@ -254,7 +262,7 @@ class _Wrapped2025PageState extends State { sharePositionOrigin: sharePositionOrigin, ); - MixpanelManager().wrappedSharedSuccessfully( + PlatformManager.instance.analytics.wrappedSharedSuccessfully( cardName: cardName, cardIndex: cardIndex, fileSizeBytes: bytes.length, @@ -268,7 +276,11 @@ class _Wrapped2025PageState extends State { } } catch (e) { Logger.debug('Error sharing $filename: $e'); - MixpanelManager().wrappedShareFailed(cardName: cardName, cardIndex: cardIndex, error: e.toString()); + PlatformManager.instance.analytics.wrappedShareFailed( + cardName: cardName, + cardIndex: cardIndex, + error: e.toString(), + ); if (mounted) { setState(() { _currentShareTemplate = null; @@ -1988,9 +2000,8 @@ class _CategoryChartAnimatedState extends State<_CategoryChartAnimated> with Tic final isFirst = index == 0; // Label appears when its slice starts animating - final labelOpacity = index < _sliceAnimations.length - ? _sliceAnimations[index].value.clamp(0.0, 1.0) - : 0.0; + final labelOpacity = + index < _sliceAnimations.length ? _sliceAnimations[index].value.clamp(0.0, 1.0) : 0.0; return Opacity( opacity: labelOpacity, @@ -2247,9 +2258,8 @@ class _ActionsAnimatedState extends State<_ActionsAnimated> with TickerProviderS color: WrappedColors.indigo, fontSize: 16, fontWeight: FontWeight.w700, - decoration: strikethroughProgress > 0.9 - ? TextDecoration.lineThrough - : TextDecoration.none, + decoration: + strikethroughProgress > 0.9 ? TextDecoration.lineThrough : TextDecoration.none, decorationColor: WrappedColors.indigo, decorationThickness: 2, ), @@ -2446,8 +2456,8 @@ class _MemorableDayData { required this.title, required this.description, required this.dateStr, - }) : month = _parseMonth(dateStr), - day = _parseDay(dateStr); + }) : month = _parseMonth(dateStr), + day = _parseDay(dateStr); static int _parseMonth(String dateStr) { final months = { @@ -2728,9 +2738,8 @@ class _MemorableDaysAnimatedState extends State<_MemorableDaysAnimated> with Tic return AnimatedBuilder( animation: _introAnimation, builder: (context, child) { - final currentDay = widget.days.isNotEmpty && _currentDayIndex < widget.days.length - ? widget.days[_currentDayIndex] - : null; + final currentDay = + widget.days.isNotEmpty && _currentDayIndex < widget.days.length ? widget.days[_currentDayIndex] : null; return Column( children: [ diff --git a/app/lib/providers/action_items_provider.dart b/app/lib/providers/action_items_provider.dart index 4244847f954..7d6826b24ca 100644 --- a/app/lib/providers/action_items_provider.dart +++ b/app/lib/providers/action_items_provider.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:omi/backend/http/api/action_items.dart' as api; @@ -9,7 +10,6 @@ import 'package:omi/pages/action_items/services/action_item_export_service.dart' import 'package:omi/pages/settings/task_integrations_page.dart'; import 'package:omi/services/apple_reminders_service.dart'; import 'package:omi/services/notifications/action_item_notification_handler.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/platform/platform_service.dart'; @@ -490,7 +490,7 @@ class ActionItemsProvider extends ChangeNotifier { exportPlatform: 'apple_reminders', appleReminderId: calendarItemId, ); - MixpanelManager().appleReminderDirectSync(actionItemId: item.id); + PlatformManager.instance.analytics.appleReminderDirectSync(actionItemId: item.id); } } catch (e) { Logger.debug('Direct Apple Reminders sync failed: $e'); @@ -517,7 +517,7 @@ class ActionItemsProvider extends ChangeNotifier { if (item.appleReminderId == null || item.appleReminderId!.isEmpty) return; AppleRemindersService().deleteReminderById(item.appleReminderId!); - MixpanelManager().appleReminderDeleted(actionItemId: item.id); + PlatformManager.instance.analytics.appleReminderDeleted(actionItemId: item.id); } // Sort order and indent level persistence @@ -735,9 +735,7 @@ class ActionItemsProvider extends ChangeNotifier { final ids = itemsToDelete.map((item) => item.id).toList(growable: false); // Snapshot positions so a failed bulk delete can re-insert the rows // back where the user expected them, instead of dumping them at the end. - final snapshot = { - for (final item in itemsToDelete) _actionItems.indexOf(item): item, - }; + final snapshot = {for (final item in itemsToDelete) _actionItems.indexOf(item): item}; final wasInSelection = _isSelectionMode; // Dismiss UI immediately — don't wait for API diff --git a/app/lib/providers/app_provider.dart b/app/lib/providers/app_provider.dart index 998aff45f24..f1546d13615 100644 --- a/app/lib/providers/app_provider.dart +++ b/app/lib/providers/app_provider.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:collection/collection.dart'; import 'package:omi/backend/http/api/apps.dart'; @@ -9,7 +10,6 @@ import 'package:omi/app_globals.dart'; import 'package:omi/providers/base_provider.dart'; import 'package:omi/utils/alerts/app_dialog.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; @@ -97,20 +97,20 @@ class AppProvider extends BaseProvider { // Track filter changes if (filterGroup == 'Apps') { if (filter == 'My Apps') { - MixpanelManager().appsFilterMyApps(enabled: isAdding); + PlatformManager.instance.analytics.appsFilterMyApps(enabled: isAdding); } else if (filter == 'Installed Apps') { - MixpanelManager().appsFilterInstalled(enabled: isAdding); + PlatformManager.instance.analytics.appsFilterInstalled(enabled: isAdding); } } else if (filterGroup == 'Rating') { if (isAdding) { String ratingStr = filter.replaceAll('+ Stars', '').trim(); int? rating = int.tryParse(ratingStr); if (rating != null) { - MixpanelManager().appsFilterRating(rating: rating); + PlatformManager.instance.analytics.appsFilterRating(rating: rating); } } } else if (filterGroup == 'Sort' && isAdding) { - MixpanelManager().appsSortChanged(sortOption: filter); + PlatformManager.instance.analytics.appsSortChanged(sortOption: filter); } notifyListeners(); @@ -131,7 +131,7 @@ class AppProvider extends BaseProvider { // Track category filter if (isAdding) { - MixpanelManager().appsFilterCategory(category: category.title); + PlatformManager.instance.analytics.appsFilterCategory(category: category.title); } notifyListeners(); @@ -152,7 +152,7 @@ class AppProvider extends BaseProvider { // Track capability filter if (isAdding) { - MixpanelManager().appsFilterCapability(capability: capability.title); + PlatformManager.instance.analytics.appsFilterCapability(capability: capability.title); } notifyListeners(); @@ -284,7 +284,10 @@ class AppProvider extends BaseProvider { // Track search if there was a query if (queryBeingSearched.isNotEmpty) { - MixpanelManager().appsSearched(searchTerm: queryBeingSearched, resultCount: result.apps.length); + PlatformManager.instance.analytics.appsSearched( + searchTerm: queryBeingSearched, + resultCount: result.apps.length, + ); } } } catch (e) { @@ -803,12 +806,12 @@ class AppProvider extends BaseProvider { ? context.l10n.errorActivatingAppIntegration : 'Error activating the app. If this is an integration app, make sure the setup is completed.'; } else { - MixpanelManager().appEnabled(appId); + PlatformManager.instance.analytics.appEnabled(appId); } } else { await disableAppServer(appId); success = true; - MixpanelManager().appDisabled(appId); + PlatformManager.instance.analytics.appDisabled(appId); } } catch (e) { print('Error toggling app $appId: $e'); diff --git a/app/lib/providers/auth_provider.dart b/app/lib/providers/auth_provider.dart index 22b83e07416..0b668251442 100644 --- a/app/lib/providers/auth_provider.dart +++ b/app/lib/providers/auth_provider.dart @@ -13,7 +13,6 @@ import 'package:omi/providers/base_provider.dart'; import 'package:omi/services/auth_service.dart'; import 'package:omi/services/notifications.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/platform/platform_manager.dart'; @@ -180,7 +179,7 @@ class AuthenticationProvider extends BaseProvider { } String newUid = user.uid; SharedPreferencesUtil().uid = newUid; - MixpanelManager().identify(); + PlatformManager.instance.analytics.identify(); onSignIn(); } else { AppSnackbar.showSnackbarError( diff --git a/app/lib/providers/capture_provider.dart b/app/lib/providers/capture_provider.dart index ac5a807825e..6304bf98b2c 100644 --- a/app/lib/providers/capture_provider.dart +++ b/app/lib/providers/capture_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -39,7 +40,6 @@ import 'package:omi/services/audio_sources/ble_device_source.dart'; import 'package:omi/services/audio_sources/phone_mic_source.dart'; import 'package:omi/services/wals.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/enums.dart'; import 'package:omi/utils/image/image_utils.dart'; import 'package:omi/utils/l10n_extensions.dart'; @@ -632,7 +632,7 @@ class CaptureProvider extends ChangeNotifier Logger.debug("Double tap: toggling pause/mute"); _isProcessingButtonEvent = true; if (_isPaused) { - MixpanelManager().omiDoubleTap(feature: 'unmute'); + PlatformManager.instance.analytics.omiDoubleTap(feature: 'unmute'); resumeDeviceRecording().then((_) { _isProcessingButtonEvent = false; }).catchError((e) { @@ -640,7 +640,7 @@ class CaptureProvider extends ChangeNotifier _isProcessingButtonEvent = false; }); } else { - MixpanelManager().omiDoubleTap(feature: 'mute'); + PlatformManager.instance.analytics.omiDoubleTap(feature: 'mute'); pauseDeviceRecording().then((_) { _isProcessingButtonEvent = false; }).catchError((e) { @@ -653,19 +653,19 @@ class CaptureProvider extends ChangeNotifier Logger.debug("Double tap: marking conversation for starring"); if (!_starOngoingConversation) { markConversationForStarring(); - MixpanelManager().omiDoubleTap(feature: 'star_conversation'); + PlatformManager.instance.analytics.omiDoubleTap(feature: 'star_conversation'); // Haptic feedback to confirm HapticFeedback.mediumImpact(); } else { // Toggle off if already marked unmarkConversationForStarring(); - MixpanelManager().omiDoubleTap(feature: 'unstar_conversation'); + PlatformManager.instance.analytics.omiDoubleTap(feature: 'unstar_conversation'); HapticFeedback.lightImpact(); } } else { // End conversation and process (default) Logger.debug("Double tap: processing conversation"); - MixpanelManager().omiDoubleTap(feature: 'process_conversation'); + PlatformManager.instance.analytics.omiDoubleTap(feature: 'process_conversation'); forceProcessingCurrentConversation(); } return; @@ -1551,7 +1551,7 @@ class CaptureProvider extends ChangeNotifier } conversationProvider?.upsertConversation(conversation); - MixpanelManager().conversationCreated(conversation); + PlatformManager.instance.analytics.conversationCreated(conversation); } Future _handleLastConvoEvent(String memoryId) async { diff --git a/app/lib/providers/conversation_provider.dart b/app/lib/providers/conversation_provider.dart index 4eed0d61d93..708d4ecbcda 100644 --- a/app/lib/providers/conversation_provider.dart +++ b/app/lib/providers/conversation_provider.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:omi/backend/http/api/conversations.dart'; @@ -9,7 +10,6 @@ import 'package:omi/backend/schema/conversation.dart'; import 'package:omi/backend/schema/structured.dart'; import 'package:omi/services/app_review_service.dart'; import 'package:omi/services/notifications/merge_notification_handler.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; class ConversationProvider extends ChangeNotifier { @@ -206,7 +206,7 @@ class ConversationProvider extends ChangeNotifier { fetchConversations(); } - MixpanelManager().showDiscardedMemoriesToggled(showDiscardedConversations); + PlatformManager.instance.analytics.showDiscardedMemoriesToggled(showDiscardedConversations); } void toggleShortConversations() { @@ -961,7 +961,7 @@ class ConversationProvider extends ChangeNotifier { void enterSelectionMode() { isSelectionModeActive = true; selectedConversationIds.clear(); - MixpanelManager().conversationMergeSelectionModeEntered(); + PlatformManager.instance.analytics.conversationMergeSelectionModeEntered(); notifyListeners(); } @@ -969,7 +969,7 @@ class ConversationProvider extends ChangeNotifier { void exitSelectionMode() { isSelectionModeActive = false; selectedConversationIds.clear(); - MixpanelManager().conversationMergeSelectionModeExited(); + PlatformManager.instance.analytics.conversationMergeSelectionModeExited(); notifyListeners(); } @@ -996,7 +996,7 @@ class ConversationProvider extends ChangeNotifier { } } else { selectedConversationIds.add(conversationId); - MixpanelManager().conversationSelectedForMerge(conversationId, selectedConversationIds.length); + PlatformManager.instance.analytics.conversationSelectedForMerge(conversationId, selectedConversationIds.length); } notifyListeners(); } @@ -1042,10 +1042,10 @@ class ConversationProvider extends ChangeNotifier { // Call merge API final response = await mergeConversations(idsToMerge); - MixpanelManager().conversationMergeInitiated(idsToMerge); + PlatformManager.instance.analytics.conversationMergeInitiated(idsToMerge); if (response == null) { - MixpanelManager().conversationMergeFailed(idsToMerge); + PlatformManager.instance.analytics.conversationMergeFailed(idsToMerge); if (conversationIds != null) { for (final id in conversationIds) { mergingConversationIds.remove(id); @@ -1069,7 +1069,7 @@ class ConversationProvider extends ChangeNotifier { mergingConversationIds.remove(id); } - MixpanelManager().conversationMergeCompleted(mergedConversationId, removedConversationIds); + PlatformManager.instance.analytics.conversationMergeCompleted(mergedConversationId, removedConversationIds); // Remove deleted conversations from local state for (final id in removedConversationIds) { diff --git a/app/lib/providers/developer_mode_provider.dart b/app/lib/providers/developer_mode_provider.dart index ae3b2324980..619284ce64f 100644 --- a/app/lib/providers/developer_mode_provider.dart +++ b/app/lib/providers/developer_mode_provider.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:omi/backend/http/api/agents.dart'; @@ -7,7 +8,6 @@ import 'package:omi/app_globals.dart'; import 'package:omi/providers/base_provider.dart'; import 'package:omi/services/agent_chat_service.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/other/validators.dart'; @@ -229,7 +229,7 @@ class DeveloperModeProvider extends BaseProvider { prefs.showDailyScoreEnabled = showDailyScoreEnabled; prefs.showTasksEnabled = showTasksEnabled; - MixpanelManager().settingsSaved( + PlatformManager.instance.analytics.settingsSaved( hasWebhookConversationCreated: conversationEventsToggled, hasWebhookTranscriptReceived: transcriptsToggled, ); diff --git a/app/lib/providers/device_provider.dart b/app/lib/providers/device_provider.dart index a10a03c047f..986dd778b7c 100644 --- a/app/lib/providers/device_provider.dart +++ b/app/lib/providers/device_provider.dart @@ -18,7 +18,6 @@ import 'package:omi/services/devices/omi_connection.dart'; import 'package:omi/services/notifications.dart'; import 'package:omi/services/services.dart'; import 'package:omi/services/battery_widget_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/device.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/utils/other/debouncer.dart'; @@ -316,7 +315,7 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption await setConnectedDevice(connection.device); setisDeviceStorageSupport(); SharedPreferencesUtil().deviceName = connection.device.name; - MixpanelManager().deviceConnected(); + PlatformManager.instance.analytics.deviceConnected(); setIsConnected(true); } } catch (e) { @@ -370,7 +369,7 @@ class DeviceProvider extends ChangeNotifier implements IDeviceServiceSubsciption PlatformManager.instance.crashReporter.logInfo('Omi Device Disconnected'); - MixpanelManager().deviceDisconnected(); + PlatformManager.instance.analytics.deviceDisconnected(); BatteryWidgetService().updateBatteryInfo( deviceName: SharedPreferencesUtil().deviceName, batteryLevel: -1, diff --git a/app/lib/providers/home_provider.dart b/app/lib/providers/home_provider.dart index d351d46e521..12c9da1611e 100644 --- a/app/lib/providers/home_provider.dart +++ b/app/lib/providers/home_provider.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:omi/backend/http/api/speech_profile.dart'; @@ -6,7 +7,6 @@ import 'package:omi/backend/preferences.dart'; import 'package:omi/app_globals.dart'; import 'package:omi/pages/settings/language_selection_dialog.dart'; import 'package:omi/providers/user_provider.dart'; -import 'package:omi/utils/analytics/analytics_manager.dart'; import 'package:omi/utils/logger.dart'; /// Languages supported by Deepgram Nova-3 multi-language auto-detection. @@ -191,7 +191,7 @@ class HomeProvider extends ChangeNotifier { setSpeakerProfile(res); SharedPreferencesUtil().hasSpeakerProfile = res; Logger.debug('_setupHasSpeakerProfile: ${SharedPreferencesUtil().hasSpeakerProfile}'); - AnalyticsManager().setUserAttribute('Speaker Profile', SharedPreferencesUtil().hasSpeakerProfile); + PlatformManager.instance.analytics.setUserAttribute('Speaker Profile', SharedPreferencesUtil().hasSpeakerProfile); setIsLoading(false); notifyListeners(); @@ -220,7 +220,7 @@ class HomeProvider extends ChangeNotifier { hasSetPrimaryLanguage = true; SharedPreferencesUtil().userPrimaryLanguage = language; SharedPreferencesUtil().hasSetPrimaryLanguage = true; - AnalyticsManager().setUserAttribute('Primary Language', language); + PlatformManager.instance.analytics.setUserAttribute('Primary Language', language); } Logger.debug('setupUserPrimaryLanguage: $language, hasSet: $hasSetPrimaryLanguage'); } catch (e) { @@ -246,7 +246,7 @@ class HomeProvider extends ChangeNotifier { hasSetPrimaryLanguage = true; SharedPreferencesUtil().userPrimaryLanguage = languageCode; SharedPreferencesUtil().hasSetPrimaryLanguage = true; - AnalyticsManager().setUserAttribute('Primary Language', languageCode); + PlatformManager.instance.analytics.setUserAttribute('Primary Language', languageCode); // Backend auto-sets single_language_mode — sync local state to match final singleLanguageMode = !multiLanguageSupported.contains(languageCode); diff --git a/app/lib/providers/memories_provider.dart b/app/lib/providers/memories_provider.dart index 6a94163b43b..0752aa7dd73 100644 --- a/app/lib/providers/memories_provider.dart +++ b/app/lib/providers/memories_provider.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; @@ -11,7 +12,6 @@ import 'package:omi/backend/http/api/memories.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/backend/schema/memory.dart'; import 'package:omi/providers/connectivity_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; import 'package:omi/widgets/extensions/string.dart'; @@ -288,7 +288,7 @@ class MemoriesProvider extends ChangeNotifier { await deleteAllMemoriesServer(); _memories.clear(); if (countBeforeDeletion > 0) { - MixpanelManager().memoriesAllDeleted(countBeforeDeletion); + PlatformManager.instance.analytics.memoriesAllDeleted(countBeforeDeletion); } _setCategories(); } @@ -346,7 +346,7 @@ class MemoriesProvider extends ChangeNotifier { memoryToUpdate.visibility = visibility; _memories[idx] = memoryToUpdate; - MixpanelManager().memoryVisibilityChanged(memoryToUpdate, visibility); + PlatformManager.instance.analytics.memoryVisibilityChanged(memoryToUpdate, visibility); _setCategories(); } } @@ -394,7 +394,7 @@ class MemoriesProvider extends ChangeNotifier { } if (updatedCount > 0) { - MixpanelManager().memoriesAllVisibilityChanged(visibility, updatedCount); + PlatformManager.instance.analytics.memoriesAllVisibilityChanged(visibility, updatedCount); } _setCategories(); diff --git a/app/lib/providers/message_provider.dart b/app/lib/providers/message_provider.dart index 3fa12af89b3..57b45064bc2 100644 --- a/app/lib/providers/message_provider.dart +++ b/app/lib/providers/message_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -27,7 +28,6 @@ import 'package:omi/app_globals.dart'; import 'package:omi/services/agent_chat_service.dart'; import 'package:omi/utils/alerts/app_snackbar.dart'; import 'package:omi/utils/l10n_extensions.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/file.dart'; import 'package:omi/utils/logger.dart'; @@ -501,7 +501,7 @@ class MessageProvider extends ChangeNotifier { App? targetApp = currentAppId != null ? appProvider?.apps.firstWhereOrNull((app) => app.id == currentAppId) : null; bool isPersonaChat = false; - MixpanelManager().chatVoiceInputUsed(chatTargetId: chatTargetId, isPersonaChat: isPersonaChat); + PlatformManager.instance.analytics.chatVoiceInputUsed(chatTargetId: chatTargetId, isPersonaChat: isPersonaChat); setShowTypingIndicator(true); var message = ServerMessage.empty(); @@ -621,7 +621,7 @@ class MessageProvider extends ChangeNotifier { App? targetApp = currentAppId != null ? appProvider?.apps.firstWhereOrNull((app) => app.id == currentAppId) : null; bool isPersonaChat = false; - MixpanelManager().chatMessageSent( + PlatformManager.instance.analytics.chatMessageSent( message: text, includesFiles: uploadedFiles.isNotEmpty, numberOfFiles: uploadedFiles.length, diff --git a/app/lib/providers/onboarding_provider.dart b/app/lib/providers/onboarding_provider.dart index 20e2a136b97..09297e66864 100644 --- a/app/lib/providers/onboarding_provider.dart +++ b/app/lib/providers/onboarding_provider.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/widgets.dart'; @@ -19,7 +20,6 @@ import 'package:omi/providers/device_provider.dart'; import 'package:omi/services/devices.dart'; import 'package:omi/services/notifications.dart'; import 'package:omi/services/services.dart'; -import 'package:omi/utils/analytics/analytics_manager.dart'; import 'package:omi/utils/audio/foreground.dart'; import 'package:omi/utils/bluetooth/bluetooth_adapter.dart'; import 'package:omi/utils/logger.dart'; @@ -71,20 +71,23 @@ class OnboardingProvider extends BaseProvider with MessageNotifierMixin implemen void updateLocationPermission(bool value) { hasLocationPermission = value; SharedPreferencesUtil().locationEnabled = value; - AnalyticsManager().setUserAttribute('Location Enabled', SharedPreferencesUtil().locationEnabled); + PlatformManager.instance.analytics.setUserAttribute('Location Enabled', SharedPreferencesUtil().locationEnabled); notifyListeners(); } void updateNotificationPermission(bool value) { hasNotificationPermission = value; SharedPreferencesUtil().notificationsEnabled = value; - AnalyticsManager().setUserAttribute('Notifications Enabled', SharedPreferencesUtil().notificationsEnabled); + PlatformManager.instance.analytics.setUserAttribute( + 'Notifications Enabled', + SharedPreferencesUtil().notificationsEnabled, + ); notifyListeners(); } void updateBackgroundPermission(bool value) { hasBackgroundPermission = value; - AnalyticsManager().setUserAttribute('Background Permission Enabled', hasBackgroundPermission); + PlatformManager.instance.analytics.setUserAttribute('Background Permission Enabled', hasBackgroundPermission); notifyListeners(); } diff --git a/app/lib/providers/phone_call_provider.dart b/app/lib/providers/phone_call_provider.dart index 0e895e8a1bf..1e0ff8fd46c 100644 --- a/app/lib/providers/phone_call_provider.dart +++ b/app/lib/providers/phone_call_provider.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'dart:typed_data'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter_contacts/flutter_contacts.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -15,7 +16,6 @@ import 'package:omi/backend/schema/phone_call.dart'; import 'package:omi/backend/schema/transcript_segment.dart'; import 'package:omi/models/audio_route.dart'; import 'package:omi/services/phone_call_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; enum TranscriptionStatus { idle, connecting, active, reconnecting, failed } @@ -134,7 +134,7 @@ class PhoneCallProvider extends ChangeNotifier { _verificationStatus = null; notifyListeners(); - MixpanelManager().phoneCallVerificationStarted(); + PlatformManager.instance.analytics.phoneCallVerificationStarted(); var result = await api.verifyPhoneNumber(phoneNumber); _isLoading = false; @@ -163,7 +163,7 @@ class PhoneCallProvider extends ChangeNotifier { bool verified = result['verified'] == true; if (verified) { - MixpanelManager().phoneCallVerificationCompleted(); + PlatformManager.instance.analytics.phoneCallVerificationCompleted(); await loadVerifiedNumbers(); } return verified; @@ -243,13 +243,13 @@ class PhoneCallProvider extends ChangeNotifier { if (!callStarted) { _callState = PhoneCallState.idle; _error = 'Failed to start call'; - MixpanelManager().phoneCallFailed(error: 'Failed to start call'); + PlatformManager.instance.analytics.phoneCallFailed(error: 'Failed to start call'); _disconnectTranscriptionSocket(); notifyListeners(); return false; } - MixpanelManager().phoneCallStarted(contactName: _contactName); + PlatformManager.instance.analytics.phoneCallStarted(contactName: _contactName); return true; } @@ -307,7 +307,7 @@ class PhoneCallProvider extends ChangeNotifier { _callStartTime = DateTime.now(); _startDurationTimer(); _connectTranscriptionSocket(); - MixpanelManager().phoneCallConnected(); + PlatformManager.instance.analytics.phoneCallConnected(); } else if (state == PhoneCallState.ended || state == PhoneCallState.failed) { _onCallEnded(); } @@ -347,7 +347,7 @@ class PhoneCallProvider extends ChangeNotifier { } void _onCallEnded() { - MixpanelManager().phoneCallEnded(durationSeconds: _callDuration.inSeconds); + PlatformManager.instance.analytics.phoneCallEnded(durationSeconds: _callDuration.inSeconds); _callState = PhoneCallState.ended; _stopDurationTimer(); _disconnectTranscriptionSocket(); diff --git a/app/lib/providers/usage_provider.dart b/app/lib/providers/usage_provider.dart index 322c10c3c77..ed6ca12c8a5 100644 --- a/app/lib/providers/usage_provider.dart +++ b/app/lib/providers/usage_provider.dart @@ -1,10 +1,10 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:omi/backend/http/api/payment.dart'; import 'package:omi/backend/http/api/users.dart'; import 'package:omi/models/subscription.dart'; import 'package:omi/models/user_usage.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; class UsageProvider with ChangeNotifier { @@ -119,7 +119,7 @@ class UsageProvider with ChangeNotifier { try { _subscription = await getUserSubscription(); if (_subscription != null) { - MixpanelManager().setSubscriptionTier(_subscription!.subscription.plan.name); + PlatformManager.instance.analytics.setSubscriptionTier(_subscription!.subscription.plan.name); } } catch (e) { _error = 'Failed to load subscription data. Please try again later.'; diff --git a/app/lib/services/announcement_service.dart b/app/lib/services/announcement_service.dart index 03c6433ce49..1a5c3aa78eb 100644 --- a/app/lib/services/announcement_service.dart +++ b/app/lib/services/announcement_service.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:package_info_plus/package_info_plus.dart'; @@ -9,7 +10,6 @@ import 'package:omi/pages/announcements/announcement_dialog.dart'; import 'package:omi/pages/announcements/changelog_sheet.dart'; import 'package:omi/pages/announcements/feature_screen.dart'; import 'package:omi/providers/announcement_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; /// Service that handles announcement detection and display. /// Call this on app startup and after firmware updates. @@ -54,13 +54,13 @@ class AnnouncementService { if (changelogs.isNotEmpty && context.mounted) { _isShowingAnnouncement = true; try { - MixpanelManager().changelogShown( + PlatformManager.instance.analytics.changelogShown( changelogCount: changelogs.length, fromVersion: lastKnownVersion, toVersion: currentVersion, ); await ChangelogSheet.show(context, changelogs); - MixpanelManager().changelogDismissed(changelogCount: changelogs.length); + PlatformManager.instance.analytics.changelogDismissed(changelogCount: changelogs.length); } finally { _isShowingAnnouncement = false; } @@ -125,7 +125,7 @@ class AnnouncementService { final typeName = announcement.type.toString().split('.').last; // Track announcement shown - MixpanelManager().announcementShown( + PlatformManager.instance.analytics.announcementShown( announcementId: announcement.id, type: typeName, priority: announcement.display?.priority, @@ -145,7 +145,7 @@ class AnnouncementService { } // Track dismissal and mark as dismissed - MixpanelManager().announcementDismissed( + PlatformManager.instance.analytics.announcementDismissed( announcementId: announcement.id, type: typeName, ctaClicked: ctaClicked, diff --git a/app/lib/services/app_review_service.dart b/app/lib/services/app_review_service.dart index f35f54652b8..835376997d8 100644 --- a/app/lib/services/app_review_service.dart +++ b/app/lib/services/app_review_service.dart @@ -1,5 +1,6 @@ import 'dart:io'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -7,7 +8,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; class AppReviewService { @@ -158,7 +158,7 @@ class AppReviewService { if (await canLaunchUrl(reviewUrl)) { await launchUrl(reviewUrl, mode: LaunchMode.externalApplication); - MixpanelManager().track('App Review Opened'); + PlatformManager.instance.analytics.track('App Review Opened'); await Future.delayed(const Duration(milliseconds: 500)); } else { Logger.debug('Could not launch review URL'); @@ -186,7 +186,7 @@ class AppReviewService { TextButton( onPressed: () { HapticFeedback.lightImpact(); - MixpanelManager().track('App Review Skipped'); + PlatformManager.instance.analytics.track('App Review Skipped'); Navigator.of(context).pop(); }, child: const Text('Maybe later', style: TextStyle(color: Colors.grey, fontSize: 16)), diff --git a/app/lib/services/apple_reminders_sync_service.dart b/app/lib/services/apple_reminders_sync_service.dart index 6cadc84958c..0fe82e5a971 100644 --- a/app/lib/services/apple_reminders_sync_service.dart +++ b/app/lib/services/apple_reminders_sync_service.dart @@ -1,7 +1,7 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:omi/backend/http/api/action_items.dart'; import 'package:omi/backend/schema/action_item.dart'; import 'package:omi/services/apple_reminders_service.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; /// Orchestrates bidirectional Apple Reminders sync on foreground resume. @@ -31,7 +31,7 @@ class AppleRemindersSyncService { final stats = await _performBidirectionalSync(syncData.syncedItems); _lastSyncTime = DateTime.now(); - MixpanelManager().appleRemindersSyncCompleted( + PlatformManager.instance.analytics.appleRemindersSyncCompleted( pendingExported: exported, syncedChecked: stats['checked'] ?? 0, completionsPulled: stats['completionsPulled'] ?? 0, diff --git a/app/lib/services/notifications/important_conversation_notification_handler.dart b/app/lib/services/notifications/important_conversation_notification_handler.dart index 6320226f73f..95ba0e3bd8d 100644 --- a/app/lib/services/notifications/important_conversation_notification_handler.dart +++ b/app/lib/services/notifications/important_conversation_notification_handler.dart @@ -1,9 +1,9 @@ import 'dart:async'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:awesome_notifications/awesome_notifications.dart'; import 'package:omi/app_globals.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/logger.dart'; @@ -49,7 +49,7 @@ class ImportantConversationNotificationHandler { Logger.debug('[ImportantConversationNotification] Navigate to: $navigateTo'); // Track notification received - MixpanelManager().importantConversationNotificationReceived(conversationId); + PlatformManager.instance.analytics.importantConversationNotificationReceived(conversationId); // Broadcast the event so providers can update their state _importantConversationController.add( diff --git a/app/lib/utils/analytics/adapters/posthog_adapter.dart b/app/lib/utils/analytics/adapters/posthog_adapter.dart new file mode 100644 index 00000000000..52dcc26f1f3 --- /dev/null +++ b/app/lib/utils/analytics/adapters/posthog_adapter.dart @@ -0,0 +1,73 @@ +import 'package:posthog_flutter/posthog_flutter.dart'; + +import 'package:omi/utils/analytics/analytics_adapter.dart'; + +class PostHogAnalyticsAdapter implements AnalyticsAdapter { + PostHogAnalyticsAdapter({ + required this.apiKey, + this.host = 'https://us.i.posthog.com', + this.captureLifecycleEvents = true, + this.debug = false, + }); + + final String apiKey; + final String host; + final bool captureLifecycleEvents; + final bool debug; + + bool _initialized = false; + + @override + bool get isInitialized => _initialized; + + @override + Future init() async { + if (_initialized) return; + final config = PostHogConfig(apiKey); + config.host = host; + config.captureApplicationLifecycleEvents = captureLifecycleEvents; + config.debug = debug; + await Posthog().setup(config); + _initialized = true; + } + + @override + void identify({required String userId, Map? userProperties}) { + if (!_initialized) return; + if (userProperties == null) { + Posthog().identify(userId: userId); + } else { + Posthog().identify(userId: userId, userProperties: userProperties); + } + } + + @override + void alias({required String newUserId}) { + if (!_initialized) return; + Posthog().alias(alias: newUserId); + } + + @override + void track({required String eventName, Map? properties}) { + if (!_initialized) return; + Posthog().capture(eventName: eventName, properties: properties); + } + + @override + void enable() { + if (!_initialized) return; + Posthog().enable(); + } + + @override + void disable() { + if (!_initialized) return; + Posthog().disable(); + } + + @override + void reset() { + if (!_initialized) return; + Posthog().reset(); + } +} diff --git a/app/lib/utils/analytics/analytics_adapter.dart b/app/lib/utils/analytics/analytics_adapter.dart new file mode 100644 index 00000000000..b5083b17ac7 --- /dev/null +++ b/app/lib/utils/analytics/analytics_adapter.dart @@ -0,0 +1,35 @@ +/// Provider-agnostic primitives every analytics SDK wrapper must satisfy. +/// +/// Adding a new analytics SDK is one file under `analytics/adapters/` plus +/// a single `AnalyticsManager.configure(...)` call at boot. No call site in +/// the rest of the app needs to know which adapter is active. +abstract class AnalyticsAdapter { + /// Initialize the underlying SDK. Calls before this resolves should be no-ops + /// inside the implementation, so the manager can fan calls through without + /// branching on init state. + Future init(); + + /// True once `init()` has resolved successfully. + bool get isInitialized; + + /// Identify the current user. Pass `userProperties` to set them in the same + /// call (most SDKs accept this fused shape and it's cheaper than a separate + /// set-property call). + void identify({required String userId, Map? userProperties}); + + /// Link an existing anonymous identity to a known user id. + void alias({required String newUserId}); + + /// Capture a single event with optional properties. + void track({required String eventName, Map? properties}); + + /// Resume capture after a previous `disable()`. + void enable(); + + /// Stop capture. Calls between `disable()` and the next `enable()` are + /// dropped by the underlying SDK. + void disable(); + + /// Forget the current identity (e.g. on logout). Does not disable capture. + void reset(); +} diff --git a/app/lib/utils/analytics/analytics_manager.dart b/app/lib/utils/analytics/analytics_manager.dart index 897e8aa2d4f..5258677fd33 100644 --- a/app/lib/utils/analytics/analytics_manager.dart +++ b/app/lib/utils/analytics/analytics_manager.dart @@ -1,9 +1,38 @@ -import 'dart:io'; - -import 'package:omi/utils/platform/platform_manager.dart'; +import 'package:omi/backend/preferences.dart'; +import 'package:omi/backend/schema/conversation.dart'; +import 'package:omi/backend/schema/memory.dart'; +import 'package:omi/env/env.dart'; +import 'package:omi/utils/analytics/adapters/posthog_adapter.dart'; +import 'package:omi/utils/analytics/analytics_adapter.dart'; +import 'package:omi/utils/analytics/intercom.dart'; +import 'package:omi/utils/platform/platform_service.dart'; class AnalyticsManager { static final AnalyticsManager _instance = AnalyticsManager._internal(); + static AnalyticsAdapter? _adapter; + static bool _initStarted = false; + static final SharedPreferencesUtil _preferences = SharedPreferencesUtil(); + static final Map _pendingTimedEvents = {}; + static const Duration _pendingTimedEventTtl = Duration(hours: 1); + + /// Inject the analytics adapter at boot. Must be called before [init]. + /// Calling without ever configuring leaves every method as a no-op, which + /// is the right behavior for environments without an analytics key (e.g. + /// local dev with no PostHog token). + static void configure(AnalyticsAdapter adapter) { + assert(!_initStarted, 'AnalyticsManager.configure() must be called before init().'); + _adapter = adapter; + } + + static Future init() async { + _initStarted = true; + if (_adapter == null && Env.posthogApiKey != null) { + _adapter = PostHogAnalyticsAdapter(apiKey: Env.posthogApiKey!); + } + final adapter = _adapter; + if (adapter == null) return; + await PlatformService.executeIfSupportedAsync(PlatformService.isAnalyticsSupported, adapter.init); + } factory AnalyticsManager() { return _instance; @@ -11,18 +40,1639 @@ class AnalyticsManager { AnalyticsManager._internal(); - void setUserAttributes() { - PlatformManager.instance.mixpanel.setPeopleValues(); - PlatformManager.instance.intercom.setUserAttributes(); + void setUserAttribute(String key, dynamic value) { + setUserProperty(key, value); + PlatformService.executeIfSupported(PlatformService.isIntercomSupported, () { + IntercomManager.instance.updateCustomAttributes({key: value}); + }); } - void setUserAttribute(String key, dynamic value) { - PlatformManager.instance.mixpanel.setUserProperty(key, value); - PlatformManager.instance.intercom.updateCustomAttributes({key: value}); + void setUserAttributes() { + setPeopleValues(); + PlatformService.executeIfSupported(PlatformService.isIntercomSupported, () { + IntercomManager.instance.setUserAttributes(); + }); } void trackEvent(String eventName, {Map? properties}) { - PlatformManager.instance.mixpanel.track(eventName, properties: properties); - PlatformManager.instance.intercom.logEvent(eventName, metaData: properties); + track(eventName, properties: properties); + } + + setPeopleValues() { + _setUserPropertiesBatch({ + 'Notifications Enabled': _preferences.notificationsEnabled, + 'Location Enabled': _preferences.locationEnabled, + 'Apps Enabled Count': _preferences.enabledAppsCount, + 'Apps Integrations Enabled Count': _preferences.enabledAppsIntegrationsCount, + 'Speaker Profile': _preferences.hasSpeakerProfile, + 'Calendar Enabled': _preferences.calendarEnabled, + 'Primary Language': _preferences.userPrimaryLanguage, + 'Authorized Storing Recordings': _preferences.permissionStoreRecordingsEnabled, + }); + } + + void setSubscriptionTier(String tier) => setUserProperty('Subscription Tier', tier); + + setUserProperty(String key, dynamic value) => _setUserPropertiesBatch({key: value}); + + void _setUserPropertiesBatch(Map properties) => + PlatformService.executeIfSupported(PlatformService.isAnalyticsSupported, () { + final adapter = _adapter; + if (adapter == null) return; + final uid = _preferences.uid; + if (uid.isEmpty) return; + final coerced = {}; + properties.forEach((k, v) { + final c = _coerceProperty(v); + if (c != null) coerced[k] = c; + }); + if (coerced.isEmpty) return; + adapter.identify(userId: uid, userProperties: coerced); + }); + + void optInTracking() { + PlatformService.executeIfSupported(PlatformService.isAnalyticsSupported, () { + final adapter = _adapter; + if (adapter == null) return; + adapter.enable(); + identify(); + }); + } + + void optOutTracking() { + PlatformService.executeIfSupported(PlatformService.isAnalyticsSupported, () { + final adapter = _adapter; + if (adapter == null) return; + adapter.disable(); + adapter.reset(); + }); + } + + void identify() { + PlatformService.executeIfSupported(PlatformService.isAnalyticsSupported, () { + final adapter = _adapter; + if (adapter == null) return; + final uid = _preferences.uid; + if (uid.isEmpty) return; + adapter.identify(userId: uid); + _instance.setPeopleValues(); + setNameAndEmail(); + }); + } + + void migrateUser(String newUid) { + PlatformService.executeIfSupported(PlatformService.isAnalyticsSupported, () { + final adapter = _adapter; + if (adapter == null) return; + adapter.alias(newUserId: newUid); + adapter.identify(userId: newUid); + setNameAndEmail(); + }); + } + + void setNameAndEmail() { + _setUserPropertiesBatch({ + '\$name': SharedPreferencesUtil().fullName, + '\$email': SharedPreferencesUtil().email, + }); + } + + void track(String eventName, {Map? properties}) => + PlatformService.executeIfSupported(PlatformService.isAnalyticsSupported, () { + final adapter = _adapter; + if (adapter == null) return; + final props = {}; + if (properties != null) { + properties.forEach((k, v) { + final coerced = _coerceProperty(v); + if (coerced != null) props[k] = coerced; + }); + } + _evictStaleTimedEvents(); + final start = _pendingTimedEvents.remove(eventName); + if (start != null) { + props['\$duration'] = DateTime.now().difference(start).inMilliseconds / 1000.0; + } + adapter.track(eventName: eventName, properties: props); + }); + + void startTimingEvent(String eventName) => + PlatformService.executeIfSupported(PlatformService.isAnalyticsSupported, () { + if (_adapter == null) return; + _evictStaleTimedEvents(); + _pendingTimedEvents[eventName] = DateTime.now(); + }); + + // Drop pending starts that exceeded the TTL — guards against unbounded growth + // when a startTimingEvent has no matching track() (navigation away, crash, short-circuit). + static void _evictStaleTimedEvents() { + if (_pendingTimedEvents.isEmpty) return; + final cutoff = DateTime.now().subtract(_pendingTimedEventTtl); + _pendingTimedEvents.removeWhere((_, started) => started.isBefore(cutoff)); + } + + void onboardingDeviceConnected() => track('Onboarding Device Connected'); + + void onboardingCompleted() => track('Onboarding Completed'); + + void onboardingStepCompleted(String step) => track('Onboarding Step $step Completed'); + + void onboardingUserAcquisitionSource(String source) => + track('User Acquisition Source', properties: {'source': source}); + + void settingsSaved({bool hasWebhookConversationCreated = false, bool hasWebhookTranscriptReceived = false}) => track( + 'Developer Settings Saved', + properties: { + 'has_webhook_memory_created': hasWebhookConversationCreated, + 'has_webhook_transcript_received': hasWebhookTranscriptReceived, + }, + ); + + void pageOpened(String name) => track('$name Opened'); + + void appEnabled(String appId) { + track('App Enabled', properties: {'app_id': appId}); + setUserProperty('Apps Enabled Count', _preferences.enabledAppsCount); + } + + void appPurchaseStarted(String appId) => track('App Purchase Started', properties: {'app_id': appId}); + + void appPurchaseCompleted(String appId) => track('App Purchase Completed', properties: {'app_id': appId}); + + void privateAppSubmitted(Map properties) => track('Private App Submitted', properties: properties); + + void publicAppSubmitted(Map properties) => track('Public App Submitted', properties: properties); + + void appDisabled(String appId) { + track('App Disabled', properties: {'app_id': appId}); + setUserProperty('Apps Enabled Count', _preferences.enabledAppsCount); + } + + void appRated(String appId, double rating) { + track('App Rated', properties: {'app_id': appId, 'rating': rating}); + } + + void phoneMicRecordingStarted() => track('Phone Mic Recording Started'); + + void phoneMicRecordingStopped() => track('Phone Mic Recording Stopped'); + + // Phone Calls (VoIP) + void phoneCallPageOpened() => track('Phone Call Page Opened'); + + void phoneCallVerificationStarted() => track('Phone Call Verification Started'); + + void phoneCallVerificationCompleted() => track('Phone Call Verification Completed'); + + void phoneCallStarted({String? contactName}) => + track('Phone Call Started', properties: {'has_contact_name': contactName != null}); + + void phoneCallConnected() => track('Phone Call Connected'); + + void phoneCallEnded({required int durationSeconds}) => + track('Phone Call Ended', properties: {'duration_seconds': durationSeconds}); + + void phoneCallFailed({String? error}) => track('Phone Call Failed', properties: {'error': error ?? 'unknown'}); + + // Phone Calls Dialpad + void phoneCallDialpadOpened() => track('Phone Call Dialpad Opened'); + + void phoneCallDialpadDigitPressed(String digit) => + track('Phone Call Dialpad Digit Pressed', properties: {'digit': digit}); + + // Phone Calls Upsell + void phoneCallUpsellShown({required String source}) => + track('Phone Call Upsell Shown', properties: {'source': source}); + + void phoneCallUpsellUpgradeTapped() => track('Phone Call Upsell Upgrade Tapped'); + + void phoneCallUpsellDismissed() => track('Phone Call Upsell Dismissed'); + + void appResultExpanded(ServerConversation conversation, String appId) { + track('App Result Expanded', properties: getConversationEventProperties(conversation)..['app_id'] = appId); + } + + void languageChanged(String language) { + track('App Language Changed', properties: {'language': language}); + setUserProperty('App Primary Language', language); + } + + void recordingLanguageChanged(String language) { + track('Recording Language Changed', properties: {'language': language}); + setUserProperty('Recordings Language', language); + } + + void calendarEnabled() { + track('Calendar Enabled'); + setUserProperty('Calendar Enabled', true); + } + + void calendarDisabled() { + track('Calendar Disabled'); + setUserProperty('Calendar Enabled', false); + } + + void calendarModePressed(String mode) => track('Calendar Mode $mode Pressed'); + + void calendarSelected() => track('Calendar Selected'); + + void bottomNavigationTabClicked(String tab) => track('Bottom Navigation Tab Clicked', properties: {'tab': tab}); + + void deviceConnected() => track('Device Connected', properties: {..._preferences.btDevice.toJson()}); + + void deviceDisconnected() => track('Device Disconnected'); + + void memoriesPageCategoryOpened(MemoryCategory category) => + track('Fact Page Category Opened', properties: {'category': category.toString().split('.').last}); + + void memoriesPageDeletedMemory(Memory memory) => + track('Fact Page Deleted Fact', properties: {'fact_category': memory.category.toString().split('.').last}); + + void memoriesPageEditedMemory() => track('Fact Page Edited Fact'); + + void memoriesPageCreateMemoryBtn() => track('Fact Page Create Fact Button Pressed'); + + void memoriesPageCreatedMemory(MemoryCategory category) => + track('Fact Page Created Fact', properties: {'fact_category': category.toString().split('.').last}); + + void memorySearched(String query, int resultsCount) { + track('Fact Searched', properties: {'search_query_length': query.length, 'results_count': resultsCount}); + } + + void memorySearchCleared(int totalFactsCount) { + track('Fact Search Cleared', properties: {'total_facts_count': totalFactsCount}); + } + + void memoryListItemClicked(Memory memory) { + track( + 'Fact List Item Clicked', + properties: {'fact_id': memory.id, 'fact_category': memory.category.toString().split('.').last}, + ); + } + + void memoryVisibilityChanged(Memory memory, MemoryVisibility newVisibility) { + track( + 'Fact Visibility Changed', + properties: { + 'fact_id': memory.id, + 'fact_category': memory.category.toString().split('.').last, + 'new_visibility': newVisibility.name, + }, + ); + } + + void memoriesAllVisibilityChanged(MemoryVisibility newVisibility, int count) { + track('All Facts Visibility Changed', properties: {'new_visibility': newVisibility.name, 'facts_count': count}); + } + + void memoriesAllDeleted(int countBeforeDeletion) { + track('All Facts Deleted', properties: {'facts_count_before_deletion': countBeforeDeletion}); + } + + void memoriesFiltered(String filter) => track('Facts Filtered', properties: {'filter': filter}); + + void memoriesManagementSheetOpened() => track('Facts Management Sheet Opened'); + + Map _getTranscriptProperties(String transcript) { + String transcriptCopy = transcript.substring(0, transcript.length); + int speakersCount = 0; + for (int i = 0; i < 5; i++) { + if (transcriptCopy.contains('Speaker $i:')) speakersCount++; + transcriptCopy = transcriptCopy.replaceAll('Speaker $i:', ''); + } + transcriptCopy = transcriptCopy.replaceAll(' ', ' ').trim(); + return { + 'transcript_length': transcriptCopy.length, + 'transcript_word_count': transcriptCopy.split(' ').length, + 'speaker_count': speakersCount, + }; + } + + Map getConversationEventProperties(ServerConversation convo) { + var properties = _getTranscriptProperties(convo.getTranscript()); + int hoursAgo = DateTime.now().difference(convo.createdAt).inHours; + properties['memory_hours_since_creation'] = hoursAgo; + properties['memory_id'] = convo.id; + properties['memory_discarded'] = convo.discarded; + return properties; + } + + void conversationCreated(ServerConversation conversation) { + var properties = getConversationEventProperties(conversation); + properties['memory_result'] = conversation.discarded ? 'discarded' : 'saved'; + properties['action_items_count'] = conversation.structured.actionItems.length; + properties['transcript_language'] = _preferences.userPrimaryLanguage; + + // Additional properties for conversation creation + properties['conversation_source'] = conversation.source?.toString().split('.').last ?? 'unknown'; + properties['duration_seconds'] = conversation.getDurationInSeconds(); + properties['timestamp'] = conversation.createdAt.toIso8601String(); + + // Get the summarized app info if available + if (conversation.appResults.isNotEmpty) { + var summarizedApp = conversation.appResults.firstOrNull; + if (summarizedApp != null && summarizedApp.appId != null) { + properties['summary_app_id'] = summarizedApp.appId!; + } + } + + track('Memory Created', properties: properties); + } + + void conversationListItemClicked(ServerConversation conversation, int idx) => + track('Memory List Item Clicked', properties: getConversationEventProperties(conversation)); + + void conversationShareButtonClick(ServerConversation conversation) => + track('Memory Share Button Clicked', properties: getConversationEventProperties(conversation)); + + void conversationDeleted(ServerConversation conversation) => + track('Memory Deleted', properties: getConversationEventProperties(conversation)); + + void chatMessageSent({ + required String message, + required bool includesFiles, + required int numberOfFiles, + required String chatTargetId, + required bool isPersonaChat, + required bool isVoiceInput, + }) => + track( + 'Chat Message Sent', + properties: { + 'message_length': message.length, + 'message_word_count': message.split(' ').length, + 'includes_files': includesFiles, + 'number_of_files': numberOfFiles, + 'chat_target_id': chatTargetId, + 'is_persona_chat': isPersonaChat, + 'is_voice_input': isVoiceInput, + }, + ); + + void chatVoiceInputUsed({required String chatTargetId, required bool isPersonaChat}) { + track('Chat Voice Input Used', properties: {'chat_target_id': chatTargetId, 'is_persona_chat': isPersonaChat}); + } + + void speechProfileCapturePageClicked() => track('Speech Profile Capture Page Clicked'); + + void showDiscardedMemoriesToggled(bool showDiscarded) => + track('Show Discarded Memories Toggled', properties: {'show_discarded': showDiscarded}); + + // Conversation Display Settings Events + void conversationDisplaySettingsOpened() => track('Conversation Display Settings Opened'); + + void showShortConversationsToggled(bool showShort) => + track('Show Short Conversations Toggled', properties: {'show_short': showShort}); + + void showDiscardedConversationsToggled(bool showDiscarded) => + track('Show Discarded Conversations Toggled', properties: {'show_discarded': showDiscarded}); + + void shortConversationThresholdChanged(int thresholdSeconds) => track( + 'Short Conversation Threshold Changed', + properties: {'threshold_seconds': thresholdSeconds, 'threshold_minutes': thresholdSeconds ~/ 60}, + ); + + void voiceResponseToggled(bool enabled) => track('Voice Response Audio Toggled', properties: {'enabled': enabled}); + + void voiceResponseModeChanged(int mode) { + const names = {0: 'off', 1: 'headphones_only', 2: 'always'}; + track('Voice Response Mode Changed', properties: {'mode': names[mode] ?? 'unknown', 'mode_int': mode}); + } + + // Conversation Merge Events + void conversationMergeSelectionModeEntered() => track('Conversation Merge Selection Mode Entered'); + + void conversationMergeSelectionModeExited() => track('Conversation Merge Selection Mode Exited'); + + void conversationSelectedForMerge(String conversationId, int totalSelected) => track( + 'Conversation Selected For Merge', + properties: {'conversation_id': conversationId, 'total_selected': totalSelected}, + ); + + void conversationMergeInitiated(List conversationIds) => track( + 'Conversation Merge Initiated', + properties: {'conversation_count': conversationIds.length, 'conversation_ids': conversationIds}, + ); + + void conversationMergeCompleted(String mergedConversationId, List removedConversationIds) => track( + 'Conversation Merge Completed', + properties: { + 'merged_conversation_id': mergedConversationId, + 'removed_count': removedConversationIds.length, + 'removed_conversation_ids': removedConversationIds, + }, + ); + + void conversationMergeFailed(List conversationIds) => track( + 'Conversation Merge Failed', + properties: {'conversation_count': conversationIds.length, 'conversation_ids': conversationIds}, + ); + + // Important Conversation Share Events + void importantConversationNotificationReceived(String conversationId) => + track('Important Conversation Notification Received', properties: {'conversation_id': conversationId}); + + void shareToContactsSheetOpened(String conversationId) => + track('Share To Contacts Sheet Opened', properties: {'conversation_id': conversationId}); + + void shareToContactsSelected(String conversationId, int contactCount) => track( + 'Share To Contacts Selected', + properties: {'conversation_id': conversationId, 'contact_count': contactCount}, + ); + + void shareToContactsSmsOpened(String conversationId, int contactCount) => track( + 'Share To Contacts SMS Opened', + properties: {'conversation_id': conversationId, 'contact_count': contactCount}, + ); + + void chatMessageConversationClicked(ServerConversation conversation) => + track('Chat Message Memory Clicked', properties: getConversationEventProperties(conversation)); + + void addManualConversationClicked() => track('Add Manual Memory Clicked'); + + void manualConversationCreated(ServerConversation conversation) => + track('Manual Memory Created', properties: getConversationEventProperties(conversation)); + + void setUserProperties(String whatDoYouDo, String whereDoYouPlanToUseYourFriend, String ageRange) { + _setUserPropertiesBatch({ + 'What the user does': whatDoYouDo, + 'Using Omi At': whereDoYouPlanToUseYourFriend, + 'Age Range': ageRange, + }); + } + + void reProcessConversation(ServerConversation conversation) => + track('Re-process Memory', properties: getConversationEventProperties(conversation)); + + void developerModeEnabled() { + track('Developer Mode Enabled'); + setUserProperty('Dev Mode Enabled', true); + } + + void developerModeDisabled() { + track('Developer Mode Disabled'); + setUserProperty('Dev Mode Enabled', false); + } + + void userIDCopied() => track('User ID Copied'); + + void exportMemories() => track('Dev Mode Export Memories'); + + void importMemories() => track('Dev Mode Import Memories'); + + void importedMemories() => track('Dev Mode Imported Memories'); + + void supportContacted() => track('Support Contacted'); + + void copiedConversationDetails(ServerConversation conversation, {String source = ''}) => + track('Copied Memory Detail $source'.trim(), properties: getConversationEventProperties(conversation)); + + void checkedActionItem(ServerConversation conversation, int idx) => + track('Checked Action Item', properties: getConversationEventProperties(conversation)); + + void uncheckedActionItem(ServerConversation conversation, int idx) => + track('Unchecked Action Item', properties: getConversationEventProperties(conversation)); + + void deletedActionItem(ServerConversation conversation) => + track('Deleted Action Item', properties: getConversationEventProperties(conversation)); + + void paywallOpened(String source) => track('Paywall Opened', properties: {'source': source}); + + void upgradePlanSelected({required String plan, required String source}) => + track('Upgrade Plan Selected', properties: {'plan': plan, 'source': source}); + + void upgradeSucceeded() => track('Upgrade Succeeded'); + + void upgradeCancelled() => track('Upgrade Cancelled'); + + void upgradeModalDismissed() => track('Upgrade Modal Dismissed'); + + void upgradeModalClicked() => track('Upgrade Modal Clicked'); + + void subscriptionCancelFlowStarted() => track('Subscription Cancel Flow Started'); + + void subscriptionCancelReasonSelected({required String reason}) => + track('Subscription Cancel Reason Selected', properties: {'reason': reason}); + + void subscriptionCancelConfirmed({required String reason, String? details}) => + track('Subscription Cancel Confirmed', properties: {'reason': reason, 'reason_details': details}); + + void subscriptionCancelKeptPlan({required int step, String? reason}) => + track('Subscription Cancel Kept Plan', properties: {'step': step, 'reason': reason}); + + void subscriptionCancelAbandoned({required int step, String? reason}) => + track('Subscription Cancel Abandoned', properties: {'step': step, 'reason': reason}); + + void getFriendClicked() => track('Get Friend Clicked'); + + void connectFriendClicked() => track('Connect Friend Clicked'); + + void disconnectFriendClicked() => track('Disconnect Friend Clicked'); + + void batteryIndicatorClicked() => track('Battery Indicator Clicked'); + + void useWithoutDeviceOnboardingWelcome() => track('Use Without Device Onboarding Welcome'); + + void useWithoutDeviceOnboardingFindDevices() => track('Use Without Device Onboarding Find Devices'); + + // void pageViewed(String pageName) => startTimingEvent('Page View $pageName'); + + void addedPerson() => track('Added Person'); + + void removedPerson() => track('Removed Person'); + + void tagSheetOpened() => track('Tag Sheet Opened'); + + void taggedSegment(String assignType) => track('Tagged Segment $assignType'); + + void untaggedSegment() => track('Untagged Segment'); + + void editSegmentTextStarted() => track('Edit Segment Text Started'); + + void editSegmentTextSaved() => track('Edit Segment Text Saved'); + + void editSegmentTextCancelled() => track('Edit Segment Text Cancelled'); + + void editSummaryStarted() => track('Edit Summary Started'); + + void editSummarySaved() => track('Edit Summary Saved'); + + void editSummaryCancelled() => track('Edit Summary Cancelled'); + + void deleteAccountClicked() => track('Delete Account Clicked'); + + void deleteAccountConfirmed() => track('Delete Account Confirmed'); + + void deleteAccountCancelled() => track('Delete Account Cancelled'); + + void deleteAccountFlowStarted() => track('Delete Account Flow Started'); + + void deleteAccountReasonSelected({required String reason}) => + track('Delete Account Reason Selected', properties: {'reason': reason}); + + void deleteAccountFeedbackSubmitted({required String reason, String? details}) => + track('Delete Account Feedback Submitted', properties: {'reason': reason, 'reason_details': details}); + + void deleteAccountAbandoned({required int step, String? reason}) => + track('Delete Account Abandoned', properties: {'step': step, 'reason': reason}); + + void deleteAccountKeptAccount({required int step, String? reason}) => + track('Delete Account Kept Account', properties: {'step': step, 'reason': reason}); + + void deleteUser() => PlatformService.executeIfSupported(PlatformService.isAnalyticsSupported, () { + final adapter = _adapter; + if (adapter == null) return; + adapter.track(eventName: 'User Deleted'); + adapter.reset(); + }); + + // Apps Filter + void appsFilterOpened() => track('Apps Filter Opened'); + void appsFilterApplied() => track('Apps Filter Applied'); + void appsCategoryFilter(String category, bool isSelected) { + track('Apps Category Filter', properties: {'category': category, 'selected': isSelected}); + } + + void appsTypeFilter(String type, bool isSelected) { + track('Apps Type Filter', properties: {'type': type, 'selected': isSelected}); + } + + void appsSortFilter(String sortBy, bool isSelected) { + track('Apps Sort Filter', properties: {'sort_by': sortBy, 'selected': isSelected}); + } + + void appsRatingFilter(String rating, bool isSelected) { + track('Apps Rating Filter', properties: {'rating': rating, 'selected': isSelected}); + } + + void appsCapabilityFilter(String capability, bool isSelected) { + track('Apps Capability Filter', properties: {'capability': capability, 'selected': isSelected}); + } + + void appsClearFilters() => track('Apps Clear Filters'); + + // Brain Map Events + void brainMapOpened() => track('Brain Map Opened'); + + void brainMapNodeClicked(String nodeId, String label, String type) { + track('Brain Map Node Clicked', properties: {'node_id': nodeId, 'label': label, 'type': type}); + } + + void brainMapShareClicked() => track('Brain Map Share Clicked'); + + void brainMapRebuilt() => track('Brain Map Rebuilt'); + + // Summarized Apps Sheet Events + void summarizedAppSheetViewed({required String conversationId, String? currentSummarizedAppId}) { + track( + 'Summarized App Sheet Viewed', + properties: {'conversation_id': conversationId, 'current_summarized_app_id': currentSummarizedAppId ?? 'auto'}, + ); + } + + void summarizedAppSelected({required String conversationId, required String selectedAppId, String? previousAppId}) { + track( + 'Summarized App Selected', + properties: { + 'conversation_id': conversationId, + 'selected_app_id': selectedAppId, + 'previous_app_id': previousAppId ?? 'auto', + }, + ); + } + + void summarizedAppEnableAppsClicked({required String conversationId}) { + track('Summarized App Enable Apps Clicked', properties: {'conversation_id': conversationId}); + } + + void summarizedAppCreateTemplateClicked({required String conversationId}) { + track('Summarized App Create Template Clicked', properties: {'conversation_id': conversationId}); + } + + void quickTemplateCreated({required String conversationId, required String appName, required bool isPublic}) { + track( + 'Quick Template Created', + properties: {'conversation_id': conversationId, 'app_name': appName, 'is_public': isPublic}, + ); + } + + // Action Items Page Events + void actionItemsPageOpened() => track('Action Items Page Opened'); + + void actionItemsViewToggled(bool isGroupedView) { + track('Action Items View Toggled', properties: {'grouped_view': isGroupedView}); + } + + void actionItemToggledCompletionOnActionItemsPage({ + required String conversationId, + required String actionItemDescription, // Using description as a pseudo-ID if no stable ID exists + required bool isCompleted, + }) { + track( + 'Action Item Completion Toggled on Action Items Page', + properties: { + 'conversation_id': conversationId, + 'action_item_description': actionItemDescription, + 'is_completed': isCompleted, + }, + ); + } + + void actionItemTappedForEditOnActionItemsPage({ + required String conversationId, + required String actionItemDescription, + }) { + track( + 'Action Item Tapped for Edit on Action Items Page', + properties: {'conversation_id': conversationId, 'action_item_description': actionItemDescription}, + ); + } + + void actionItemsDateFilterApplied(String filterType) { + track('Action Items Date Filter Applied', properties: {'filter_type': filterType}); + } + + void actionItemsDateFilterCleared() { + track('Action Items Date Filter Cleared'); + } + + void actionItemTabChanged(String tabName) { + track('Action Item Tab Changed', properties: {'tab_name': tabName}); + } + + void actionItemCompleted({required String fromTab}) { + track('Action Item Completed', properties: {'from_tab': fromTab}); + } + + void trainingDataOptInSubmitted() { + track('Training Data Opt-In Submitted'); + setUserProperty('Training Data Opted In', true); + } + + void trainingDataOptInApproved() { + track('Training Data Opt-In Approved'); + setUserProperty('Training Data Status', 'approved'); + } + + // Homepage Events + void recordingMuteToggled({required bool isMuted, required String recordingType}) { + track('Recording Mute Toggled', properties: {'is_muted': isMuted, 'recording_type': recordingType}); + } + + void deletedConversationsFilterToggled(bool showDeleted) { + track('Deleted Conversations Filter Toggled', properties: {'show_deleted': showDeleted}); + } + + void calendarFilterApplied(DateTime selectedDate) { + track( + 'Calendar Filter Applied', + properties: { + 'selected_date': selectedDate.toIso8601String(), + 'days_ago': DateTime.now().difference(selectedDate).inDays, + }, + ); + } + + void calendarFilterCleared() { + track('Calendar Filter Cleared'); + } + + void searchBarFocused() { + track('Search Bar Focused'); + } + + void searchQueryEntered(String query, int resultsCount) { + track( + 'Search Query Entered', + properties: { + 'query_length': query.length, + 'query_word_count': query.split(' ').length, + 'results_count': resultsCount, + }, + ); + } + + void searchQueryCleared() { + track('Search Query Cleared'); + } + + void conversationOpenedFromSearch({ + required ServerConversation conversation, + required String searchQuery, + required int conversationIndexInResults, + }) { + var properties = getConversationEventProperties(conversation); + properties['search_query'] = searchQuery; + properties['search_query_length'] = searchQuery.length; + properties['conversation_index_in_results'] = conversationIndexInResults; + track('Conversation Opened From Search', properties: properties); + } + + void liveTranscriptCardClicked({ + required bool hasSegments, + required bool hasPhotos, + required int segmentCount, + required int photoCount, + }) { + track( + 'Live Transcript Card Clicked', + properties: { + 'has_segments': hasSegments, + 'has_photos': hasPhotos, + 'segment_count': segmentCount, + 'photo_count': photoCount, + }, + ); + } + + void deviceInfoButtonClicked({String? deviceId, String? deviceName, int? batteryLevel}) { + track( + 'Device Info Button Clicked', + properties: { + if (deviceId != null) 'device_id': deviceId, + if (deviceName != null) 'device_name': deviceName, + if (batteryLevel != null) 'battery_level': batteryLevel, + }, + ); + } + + void conversationListItemClickedWithTimeDifference({ + required ServerConversation conversation, + required int conversationIndex, + required int hoursSinceConversation, + }) { + var properties = getConversationEventProperties(conversation); + properties['conversation_index'] = conversationIndex; + properties['hours_since_conversation'] = hoursSinceConversation; + track('Conversation List Item Clicked', properties: properties); + } + + void conversationSwipedToDelete(ServerConversation conversation) { + var properties = getConversationEventProperties(conversation); + track('Conversation Swiped To Delete', properties: properties); + } + + // Conversation Detail Page Events + void conversationDetailTabChanged(String tabName) { + track('Conversation Detail Tab Changed', properties: {'tab_name': tabName}); + } + + void speakerEdited({required String conversationId, required int oldSpeakerCount, required int newSpeakerCount}) { + track( + 'Speaker Edited', + properties: { + 'conversation_id': conversationId, + 'old_speaker_count': oldSpeakerCount, + 'new_speaker_count': newSpeakerCount, + 'speaker_count_changed': oldSpeakerCount != newSpeakerCount, + }, + ); + } + + void conversationDetailSearchClicked({required String conversationId}) { + track('Conversation Detail Search Clicked', properties: {'conversation_id': conversationId}); + } + + void conversationDetailSearchQueryEntered({ + required String conversationId, + required String query, + required int resultsCount, + required String activeTab, + }) { + track( + 'Conversation Detail Search Query Entered', + properties: { + 'conversation_id': conversationId, + 'query_length': query.length, + 'results_count': resultsCount, + 'active_tab': activeTab, + }, + ); + } + + void conversationReprocessedWithApp({ + required String conversationId, + required String appId, + required String appName, + required bool isOwnApp, + required bool wasAutoSelected, + }) { + track( + 'Conversation Reprocessed', + properties: { + 'conversation_id': conversationId, + 'app_id': appId, + 'app_name': appName, + 'is_own_app': isOwnApp, + 'was_auto_selected': wasAutoSelected, + }, + ); + } + + void conversationShared({required ServerConversation conversation, required String shareMethod}) { + var properties = getConversationEventProperties(conversation); + properties['share_method'] = shareMethod; + track('Conversation Shared', properties: properties); + } + + void conversationThreeDotsMenuOpened({required String conversationId}) { + track('Conversation Three Dots Menu Opened', properties: {'conversation_id': conversationId}); + } + + void conversationThreeDotsMenuActionSelected({required String conversationId, required String action}) { + track( + 'Conversation Three Dots Menu Action Selected', + properties: {'conversation_id': conversationId, 'action': action}, + ); + } + + // ============================================================================ + // ACTION ITEMS TRACKING + // ============================================================================ + + void actionItemChecked({required String actionItemId, required bool completed, required DateTime timestamp}) { + track( + 'Action Item Checked', + properties: {'action_item_id': actionItemId, 'completed': completed, 'timestamp': timestamp.toIso8601String()}, + ); + } + + void exportTasksBannerClicked() { + track('Export Tasks Banner Clicked'); + } + + void taskIntegrationEnabled({required String appName, required bool success}) { + track('Task Integration Enabled', properties: {'app_name': appName, 'success': success}); + } + + void taskIntegrationAuthFailed({required String appName}) { + track('Task Integration Auth Failed', properties: {'app_name': appName}); + } + + void taskIntegrationSettingsOpened({required String appName}) { + track('Task Integration Settings Opened', properties: {'app_name': appName}); + } + + // ============================================================================ + // TRANSCRIPTION / CUSTOM STT TRACKING + // ============================================================================ + + void transcriptionSourceSelected({ + required String source, // 'omi' or 'custom' + }) { + track('Transcription Source Selected', properties: {'source': source}); + } + + void transcriptionProviderSelected({ + required String provider, // e.g. 'openai', 'deepgram', 'gemini', 'local_whisper', 'custom', 'custom_live' + }) { + track('Transcription Provider Selected', properties: {'provider': provider}); + } + + // ============================================================================ + // AUDIO PLAYBACK TRACKING + // ============================================================================ + + void audioPlaybackStarted({required String conversationId, int? durationSeconds}) { + track( + 'Audio Playback Started', + properties: {'conversation_id': conversationId, if (durationSeconds != null) 'duration_seconds': durationSeconds}, + ); + } + + void audioPlaybackPaused({required String conversationId, required int positionSeconds, int? durationSeconds}) { + track( + 'Audio Playback Paused', + properties: { + 'conversation_id': conversationId, + 'position_seconds': positionSeconds, + if (durationSeconds != null) 'duration_seconds': durationSeconds, + if (durationSeconds != null && durationSeconds > 0) + 'completion_percentage': ((positionSeconds / durationSeconds) * 100).round(), + }, + ); + } + + void audioPlaybackSeeked({required String conversationId, required int toPositionSeconds}) { + track( + 'Audio Playback Seeked', + properties: {'conversation_id': conversationId, 'to_position_seconds': toPositionSeconds}, + ); + } + + void transcriptSegmentTapped({ + required String conversationId, + required double segmentStartSeconds, + required double seekPositionSeconds, + }) { + track( + 'Transcript Segment Tapped', + properties: { + 'conversation_id': conversationId, + 'segment_start_seconds': segmentStartSeconds, + 'seek_position_seconds': seekPositionSeconds, + }, + ); + } + + void audioShareStarted({required String conversationId, required int audioFileCount}) { + track('Audio Share Started', properties: {'conversation_id': conversationId, 'audio_file_count': audioFileCount}); + } + + void audioShareCompleted({ + required String conversationId, + required int audioFileCount, + required bool wasCombined, + required int durationSeconds, + }) { + track( + 'Audio Share Completed', + properties: { + 'conversation_id': conversationId, + 'audio_file_count': audioFileCount, + 'was_combined': wasCombined, + 'duration_seconds': durationSeconds, + }, + ); + } + + void audioShareFailed({required String conversationId, String? errorMessage}) { + track( + 'Audio Share Failed', + properties: {'conversation_id': conversationId, if (errorMessage != null) 'error_message': errorMessage}, + ); + } + + void audioShareCancelled({required String conversationId}) { + track('Audio Share Cancelled', properties: {'conversation_id': conversationId}); + } + + void actionItemExported({required String actionItemId, required String appName, required DateTime timestamp}) { + track( + 'Action Item Exported', + properties: {'action_item_id': actionItemId, 'app_name': appName, 'timestamp': timestamp.toIso8601String()}, + ); + } + + void actionItemManuallyAdded({required String actionItemId, required DateTime timestamp}) { + track( + 'Action Item Manually Added', + properties: {'action_item_id': actionItemId, 'timestamp': timestamp.toIso8601String()}, + ); + } + + void actionItemEdited({required String actionItemId, required bool titleChanged, required bool dateChanged}) { + track( + 'Action Item Edited', + properties: {'action_item_id': actionItemId, 'title_changed': titleChanged, 'date_changed': dateChanged}, + ); + } + + void appleRemindersSyncCompleted({ + required int pendingExported, + required int syncedChecked, + required int completionsPulled, + required int completionsPushed, + required int titleDuePulled, + required int titleDuePushed, + required int remindersUnlinked, + }) { + track( + 'Apple Reminders Sync Completed', + properties: { + 'pending_exported': pendingExported, + 'synced_checked': syncedChecked, + 'completions_pulled': completionsPulled, + 'completions_pushed': completionsPushed, + 'title_due_pulled': titleDuePulled, + 'title_due_pushed': titleDuePushed, + 'reminders_unlinked': remindersUnlinked, + }, + ); + } + + void appleReminderDirectSync({required String actionItemId}) { + track('Apple Reminder Direct Sync', properties: {'action_item_id': actionItemId}); + } + + void appleReminderDeleted({required String actionItemId}) { + track('Apple Reminder Deleted', properties: {'action_item_id': actionItemId}); + } + + // ============================================================================ + // SETTINGS PAGE TRACKING + // ============================================================================ + + void settingsPageOpened({required String pageName}) { + track('Settings Page Opened', properties: {'page_name': pageName}); + } + + void usageTabChanged({required String tabName}) { + track('Usage Tab Changed', properties: {'tab_name': tabName}); + } + + // ============================================================================ + // APPS PAGE TRACKING + // ============================================================================ + + void appsSearched({required String searchTerm, required int resultCount}) { + track('Apps Searched', properties: {'search_term': searchTerm, 'result_count': resultCount}); + } + + void appsFilterMyApps({required bool enabled}) { + track('Apps Filter My Apps', properties: {'enabled': enabled}); + } + + void appsFilterInstalled({required bool enabled}) { + track('Apps Filter Installed', properties: {'enabled': enabled}); + } + + void appsFilterRating({required int rating}) { + track('Apps Filter Rating', properties: {'rating': rating}); + } + + void appsFilterCategory({required String category}) { + track('Apps Filter Category', properties: {'category': category}); + } + + void appsSortChanged({required String sortOption}) { + track('Apps Sort Changed', properties: {'sort_option': sortOption}); + } + + void appsFilterCapability({required String capability}) { + track('Apps Filter Capability', properties: {'capability': capability}); + } + + void appsCategoryPageOpened({required String category, required int appCount}) { + track('Apps Category Page Opened', properties: {'category': category, 'app_count': appCount}); + } + + // ============================================================================ + // APP DETAIL PAGE TRACKING + // ============================================================================ + + void appDetailViewed({ + required String appId, + required String appName, + String? category, + double? rating, + int? installs, + bool? isInstalled, + }) { + track( + 'App Detail Viewed', + properties: { + 'app_id': appId, + 'app_name': appName, + if (category != null) 'category': category, + if (rating != null) 'rating': rating, + if (installs != null) 'installs': installs, + if (isInstalled != null) 'is_installed': isInstalled, + }, + ); + } + + void appDetailSectionViewed({required String appId, required String sectionName}) { + track('App Detail Section Viewed', properties: {'app_id': appId, 'section_name': sectionName}); + } + + void appDetailShared({required String appId, required String appName}) { + track('App Detail Shared', properties: {'app_id': appId, 'app_name': appName}); + } + + void appDetailReviewsOpened({required String appId, required int reviewCount}) { + track('App Detail Reviews Opened', properties: {'app_id': appId, 'review_count': reviewCount}); + } + + void appDetailReviewAdded({required String appId, required int rating, required bool hasComment}) { + track('App Detail Review Added', properties: {'app_id': appId, 'rating': rating, 'has_comment': hasComment}); + } + + void appDetailSettingsOpened({required String appId}) { + track('App Detail Settings Opened', properties: {'app_id': appId}); + } + + void appDetailSubscribeClicked({required String appId, required String appName, double? price}) { + track( + 'App Detail Subscribe Clicked', + properties: {'app_id': appId, 'app_name': appName, if (price != null) 'price': price}, + ); + } + + void appDetailSubscriptionCancelled({required String appId, required String appName}) { + track('App Detail Subscription Cancelled', properties: {'app_id': appId, 'app_name': appName}); + } + + void appDetailPreviewImageViewed({required String appId, required int imageIndex}) { + track('App Detail Preview Image Viewed', properties: {'app_id': appId, 'image_index': imageIndex}); + } + + void appDetailChatClicked({required String appId, required String appName}) { + track('App Detail Chat Clicked', properties: {'app_id': appId, 'app_name': appName}); + } + + // ============================================================================ + // FOLDER TRACKING + // ============================================================================ + + void folderCreated({ + required String folderId, + required String folderName, + required String icon, + required String color, + }) { + track( + 'Folder Created', + properties: {'folder_id': folderId, 'folder_name': folderName, 'icon': icon, 'color': color}, + ); + } + + void folderUpdated({required String folderId, required String folderName}) { + track('Folder Updated', properties: {'folder_id': folderId, 'folder_name': folderName}); + } + + void folderDeleted({ + required String folderId, + required String folderName, + required int conversationCount, + String? moveToFolderId, + }) { + track( + 'Folder Deleted', + properties: { + 'folder_id': folderId, + 'folder_name': folderName, + 'conversation_count': conversationCount, + if (moveToFolderId != null) 'move_to_folder_id': moveToFolderId, + 'moved_conversations': moveToFolderId != null, + }, + ); + } + + void folderSelected({String? folderId, String? folderName}) { + track( + 'Folder Selected', + properties: { + if (folderId != null) 'folder_id': folderId, + if (folderName != null) 'folder_name': folderName, + 'is_all_tab': folderId == null, + }, + ); + } + + void folderContextMenuOpened({required String folderId, required String folderName}) { + track('Folder Context Menu Opened', properties: {'folder_id': folderId, 'folder_name': folderName}); + } + + void createFolderButtonClicked() { + track('Create Folder Button Clicked'); + } + + void conversationDetailFolderChipClicked({required String conversationId, String? currentFolderId}) { + track( + 'Conversation Detail Folder Chip Clicked', + properties: { + 'conversation_id': conversationId, + if (currentFolderId != null) 'current_folder_id': currentFolderId, + 'has_folder': currentFolderId != null, + }, + ); + } + + void conversationMovedToFolder({ + required String conversationId, + String? fromFolderId, + String? toFolderId, + required String source, + }) { + track( + 'Conversation Moved To Folder', + properties: { + 'conversation_id': conversationId, + if (fromFolderId != null) 'from_folder_id': fromFolderId, + if (toFolderId != null) 'to_folder_id': toFolderId, + 'source': source, + 'was_in_folder': fromFolderId != null, + }, + ); + } + + void conversationVisibilityChanged({ + required String conversationId, + required String fromVisibility, + required String toVisibility, + }) { + track( + 'Conversation Visibility Changed', + properties: {'conversation_id': conversationId, 'from_visibility': fromVisibility, 'to_visibility': toVisibility}, + ); + } + + void starredFilterToggled({required bool enabled, String? selectedFolderId}) { + track( + 'Starred Filter Toggled', + properties: { + 'enabled': enabled, + if (selectedFolderId != null) 'selected_folder_id': selectedFolderId, + 'has_folder_filter': selectedFolderId != null, + }, + ); + } + + void conversationStarToggled({ + required ServerConversation conversation, + required bool starred, + required String source, + }) { + var properties = getConversationEventProperties(conversation); + properties['starred'] = starred; + properties['source'] = source; + properties['duration_seconds'] = conversation.getDurationInSeconds(); + + // Get the summarized app id if available + if (conversation.appResults.isNotEmpty) { + var summarizedApp = conversation.appResults.firstOrNull; + if (summarizedApp != null && summarizedApp.appId != null) { + properties['summary_app_id'] = summarizedApp.appId!; + } + } + + track('Conversation Star Toggled', properties: properties); + } + + void omiDoubleTap({required String feature, Map? additionalProperties}) { + track( + 'Omi Double Tap', + properties: {'feature': feature, if (additionalProperties != null) ...additionalProperties}, + ); + } + + // ============================================================================ + // WRAPPED 2025 TRACKING + // ============================================================================ + + void wrappedPageOpened() { + track('Wrapped Page Opened'); + } + + void wrappedBannerClicked() { + track('Wrapped Banner Clicked'); + } + + void wrappedGenerationStarted() { + track('Wrapped Generation Started'); + startTimingEvent('Wrapped Generation Completed'); + } + + void wrappedGenerationCompleted({ + required int totalConversations, + required int totalMinutes, + required int daysActive, + }) { + track( + 'Wrapped Generation Completed', + properties: {'total_conversations': totalConversations, 'total_minutes': totalMinutes, 'days_active': daysActive}, + ); + } + + void wrappedGenerationFailed({String? error}) { + track('Wrapped Generation Failed', properties: {if (error != null) 'error': error}); + } + + void wrappedCardViewed({required String cardName, required int cardIndex}) { + track('Wrapped Card Viewed', properties: {'card_name': cardName, 'card_index': cardIndex}); + } + + void wrappedShareButtonClicked({required String cardName, required int cardIndex}) { + track('Wrapped Share Button Clicked', properties: {'card_name': cardName, 'card_index': cardIndex}); + startTimingEvent('Wrapped Shared Successfully'); + } + + void wrappedSharedSuccessfully({required String cardName, required int cardIndex, int? fileSizeBytes}) { + track( + 'Wrapped Shared Successfully', + properties: { + 'card_name': cardName, + 'card_index': cardIndex, + if (fileSizeBytes != null) 'file_size_bytes': fileSizeBytes, + if (fileSizeBytes != null) 'file_size_kb': (fileSizeBytes / 1024).round(), + if (fileSizeBytes != null) 'file_size_mb': (fileSizeBytes / (1024 * 1024)).toStringAsFixed(2), + }, + ); + } + + void wrappedShareFailed({required String cardName, required int cardIndex, String? error}) { + track( + 'Wrapped Share Failed', + properties: {'card_name': cardName, 'card_index': cardIndex, if (error != null) 'error': error}, + ); + } + + // ============================================================================ + // DAILY SUMMARY / RECAP TRACKING + // ============================================================================ + + void dailySummarySettingsOpened() => track('Daily Summary Settings Opened'); + + // ============================================================================ + // Permissions + // ============================================================================ + + void permissionsSettingsOpened() => track('Permissions Settings Opened'); + + void permissionChanged({required String permission, required bool granted}) { + track('Permission Changed', properties: {'permission': permission, 'granted': granted}); + } + + void permissionsInterstitialShown() => track('Permissions Interstitial Shown'); + + void permissionsInterstitialCompleted() => track('Permissions Interstitial Completed'); + + void permissionsInterstitialSkipped() => track('Permissions Interstitial Skipped'); + + void dailySummaryToggled({required bool enabled}) { + track('Daily Summary Toggled', properties: {'enabled': enabled}); + setUserProperty('Daily Summary Enabled', enabled); + } + + void dailySummaryTimeChanged({required int hour}) { + final hour12 = hour == 0 ? 12 : (hour > 12 ? hour - 12 : hour); + final period = hour >= 12 ? 'PM' : 'AM'; + track( + 'Daily Summary Time Changed', + properties: {'hour_24': hour, 'hour_12': hour12, 'period': period, 'display_time': '$hour12:00 $period'}, + ); + setUserProperty('Daily Summary Hour', hour); + } + + void dailySummaryDetailViewed({required String summaryId, required String date, String? source}) { + track( + 'Daily Summary Detail Viewed', + properties: {'summary_id': summaryId, 'date': date, if (source != null) 'source': source}, + ); + } + + void dailySummaryTestGenerated({required String date}) { + track('Daily Summary Test Generated', properties: {'date': date}); + } + + void dailySummaryTestGenerationFailed({required String date, String? error}) { + track('Daily Summary Test Generation Failed', properties: {'date': date, if (error != null) 'error': error}); + } + + void recapTabOpened() => track('Recap Tab Opened'); + + void recapSummaryCardClicked({required String summaryId, required String date, required int cardIndex}) { + track('Recap Summary Card Clicked', properties: {'summary_id': summaryId, 'date': date, 'card_index': cardIndex}); + } + + void dailySummaryNotificationReceived({required String summaryId, required String date}) { + track('Daily Summary Notification Received', properties: {'summary_id': summaryId, 'date': date}); + } + + void dailySummaryNotificationOpened({required String summaryId, required String date}) { + track('Daily Summary Notification Opened', properties: {'summary_id': summaryId, 'date': date}); + } + + void dailySummaryConversationClicked({ + required String summaryId, + required String conversationId, + required String source, + }) { + track( + 'Daily Summary Conversation Clicked', + properties: {'summary_id': summaryId, 'conversation_id': conversationId, 'source': source}, + ); + } + + void dailySummarySectionViewed({required String summaryId, required String sectionName}) { + track('Daily Summary Section Viewed', properties: {'summary_id': summaryId, 'section_name': sectionName}); + } + + // ============================================================================ + // ANNOUNCEMENT TRACKING + // ============================================================================ + + void announcementShown({required String announcementId, required String type, String? trigger, int? priority}) { + track( + 'Announcement Shown', + properties: { + 'announcement_id': announcementId, + 'type': type, + if (trigger != null) 'trigger': trigger, + if (priority != null) 'priority': priority, + }, + ); + } + + void announcementDismissed({required String announcementId, required String type, required bool ctaClicked}) { + track( + 'Announcement Dismissed', + properties: {'announcement_id': announcementId, 'type': type, 'cta_clicked': ctaClicked}, + ); + } + + void changelogShown({required int changelogCount, required String fromVersion, required String toVersion}) { + track( + 'Changelog Shown', + properties: {'changelog_count': changelogCount, 'from_version': fromVersion, 'to_version': toVersion}, + ); + } + + void changelogDismissed({required int changelogCount}) { + track('Changelog Dismissed', properties: {'changelog_count': changelogCount}); + } + + void whatsNewOpened() => track('Whats New Opened'); + + // ============================================================================ + // GOALS TRACKING + // ============================================================================ + + void goalAddButtonTapped({required String source}) { + track('Goal Add Button Tapped', properties: {'source': source}); + } + + void goalCreated({ + required String goalId, + required int titleLength, + required double targetValue, + required String source, + }) { + track( + 'Goal Created', + properties: {'goal_id': goalId, 'title_length': titleLength, 'target_value': targetValue, 'source': source}, + ); + } + + void goalUpdated({required String goalId, required String source}) { + track('Goal Updated', properties: {'goal_id': goalId, 'source': source}); + } + + void goalDeleted({required String goalId, required String source, required String method}) { + track('Goal Deleted', properties: {'goal_id': goalId, 'source': source, 'method': method}); + } + + void goalItemTappedForEdit({required String goalId, required String source}) { + track('Goal Item Tapped For Edit', properties: {'goal_id': goalId, 'source': source}); + } + + void goalEmojiSelected({required String emoji}) { + track('Goal Emoji Selected', properties: {'emoji': emoji}); + } + + void goalProgressChanged({ + required String goalId, + required double oldValue, + required double newValue, + required double targetValue, + }) { + track( + 'Goal Progress Changed', + properties: { + 'goal_id': goalId, + 'old_value': oldValue, + 'new_value': newValue, + 'target_value': targetValue, + 'progress_percentage': targetValue > 0 ? (newValue / targetValue * 100).round() : 0, + }, + ); + } + + void taskDraggedToGoal({required String taskId, required String goalId}) { + track('Task Dragged To Goal', properties: {'task_id': taskId, 'goal_id': goalId}); + } + + void dailyScoreCtaTapped({required String ctaType}) { + track('Daily Score CTA Tapped', properties: {'cta_type': ctaType}); + } + + void dailyScoreHelpTapped() => track('Daily Score Help Tapped'); + + // ============================================================================ + // INTEGRATIONS PAGE TRACKING + // ============================================================================ + + void integrationsPageOpened() => track('Integrations Page Opened'); + + void integrationConnectAttempted({required String integrationName}) { + track('Integration Connect Attempted', properties: {'integration_name': integrationName}); + } + + void integrationConnectSucceeded({required String integrationName}) { + track('Integration Connect Succeeded', properties: {'integration_name': integrationName}); + } + + void integrationConnectFailed({required String integrationName}) { + track('Integration Connect Failed', properties: {'integration_name': integrationName}); + } + + void integrationDisconnected({required String integrationName}) { + track('Integration Disconnected', properties: {'integration_name': integrationName}); + } + + // ============================================================================ + // PAYMENTS PAGE TRACKING + // ============================================================================ + + void paymentsPageOpened() => track('Payments Page Opened'); + + void paymentMethodSelected({required String methodName}) { + track('Payment Method Selected', properties: {'method_name': methodName}); + } + + // ============================================================================ + // OTHER PAGES TRACKING + // ============================================================================ + + void connectDevicePageOpened() => track('Connect Device Page Opened'); + + void connectionGuideOpened() => track('Connection Guide Opened'); + + void connectionGuideDeviceTapped(String deviceId) => + track('Connection Guide Device Tapped', properties: {'device_id': deviceId}); + + void connectionGuideDismissed(String deviceId) => + track('Connection Guide Dismissed', properties: {'device_id': deviceId}); + + void connectionGuideReportIssue(String deviceId) => + track('Connection Guide Report Issue', properties: {'device_id': deviceId}); + + void dataPrivacyPageOpened() => track('Data Privacy Page Opened'); + + void aiAppGeneratorPageOpened() => track('AI App Generator Page Opened'); + + void aiAppGeneratorPromptSubmitted({required int promptLength}) { + track('AI App Generator Prompt Submitted', properties: {'prompt_length': promptLength}); + } + + void aiAppGeneratorAppGenerated({required bool success}) { + track('AI App Generator App Generated', properties: {'success': success}); + } + + void importHistoryPageOpened() => track('Import History Page Opened'); + + void importStarted({required String source}) { + track('Import Started', properties: {'source': source}); + } + + void notificationFrequencyChanged({required int oldFrequency, required int newFrequency}) { + track('Notification Frequency Changed', properties: {'old_frequency': oldFrequency, 'new_frequency': newFrequency}); + } + + static Object? _coerceProperty(dynamic value) { + if (value == null) return null; + if (value is bool || value is num || value is String) return value; + if (value is List) { + return value.map(_coerceProperty).where((e) => e != null).cast().toList(); + } + if (value is Map) { + final out = {}; + value.forEach((k, v) { + final coerced = _coerceProperty(v); + if (coerced != null) out[k.toString()] = coerced; + }); + return out; + } + return value.toString(); } } diff --git a/app/lib/utils/analytics/mixpanel.dart b/app/lib/utils/analytics/mixpanel.dart deleted file mode 100644 index 5dd1b77fc36..00000000000 --- a/app/lib/utils/analytics/mixpanel.dart +++ /dev/null @@ -1,1635 +0,0 @@ -import 'package:posthog_flutter/posthog_flutter.dart'; - -import 'package:omi/backend/preferences.dart'; -import 'package:omi/backend/schema/conversation.dart'; -import 'package:omi/backend/schema/memory.dart'; -import 'package:omi/env/env.dart'; -import 'package:omi/utils/platform/platform_service.dart'; - -class MixpanelManager { - static final MixpanelManager _instance = MixpanelManager._internal(); - static const String _posthogHost = 'https://us.i.posthog.com'; - static bool _initialized = false; - static final SharedPreferencesUtil _preferences = SharedPreferencesUtil(); - static final Map _pendingTimedEvents = {}; - static const Duration _pendingTimedEventTtl = Duration(hours: 1); - - static Future init() async { - if (Env.posthogApiKey == null) return; - return PlatformService.executeIfSupportedAsync(PlatformService.isMixpanelSupported, () async { - if (_initialized) return; - final config = PostHogConfig(Env.posthogApiKey!); - config.host = _posthogHost; - config.captureApplicationLifecycleEvents = true; - config.debug = false; - await Posthog().setup(config); - _initialized = true; - }); - } - - factory MixpanelManager() { - return _instance; - } - - MixpanelManager._internal(); - - setPeopleValues() { - PlatformService.executeIfSupported(PlatformService.isMixpanelSupported, () { - setUserProperty('Notifications Enabled', _preferences.notificationsEnabled); - setUserProperty('Location Enabled', _preferences.locationEnabled); - setUserProperty('Apps Enabled Count', _preferences.enabledAppsCount); - setUserProperty('Apps Integrations Enabled Count', _preferences.enabledAppsIntegrationsCount); - setUserProperty('Speaker Profile', _preferences.hasSpeakerProfile); - setUserProperty('Calendar Enabled', _preferences.calendarEnabled); - setUserProperty('Primary Language', _preferences.userPrimaryLanguage); - setUserProperty('Authorized Storing Recordings', _preferences.permissionStoreRecordingsEnabled); - }); - } - - void setSubscriptionTier(String tier) => setUserProperty('Subscription Tier', tier); - - setUserProperty(String key, dynamic value) => - PlatformService.executeIfSupported(PlatformService.isMixpanelSupported, () { - if (!_initialized) return; - final uid = _preferences.uid; - if (uid.isEmpty) return; - final coerced = _coerceProperty(value); - if (coerced == null) return; - Posthog().identify(userId: uid, userProperties: {key: coerced}); - }); - - void optInTracking() { - PlatformService.executeIfSupported(PlatformService.isMixpanelSupported, () { - if (!_initialized) return; - Posthog().enable(); - identify(); - }); - } - - void optOutTracking() { - PlatformService.executeIfSupported(PlatformService.isMixpanelSupported, () { - if (!_initialized) return; - Posthog().disable(); - Posthog().reset(); - }); - } - - void identify() { - PlatformService.executeIfSupported(PlatformService.isMixpanelSupported, () { - if (!_initialized) return; - final uid = _preferences.uid; - if (uid.isEmpty) return; - Posthog().identify(userId: uid); - _instance.setPeopleValues(); - setNameAndEmail(); - }); - } - - void migrateUser(String newUid) { - PlatformService.executeIfSupported(PlatformService.isMixpanelSupported, () { - if (!_initialized) return; - Posthog().alias(alias: newUid); - Posthog().identify(userId: newUid); - setNameAndEmail(); - }); - } - - void setNameAndEmail() { - setUserProperty('\$name', SharedPreferencesUtil().fullName); - setUserProperty('\$email', SharedPreferencesUtil().email); - } - - void track(String eventName, {Map? properties}) => - PlatformService.executeIfSupported(PlatformService.isMixpanelSupported, () { - if (!_initialized) return; - final props = {}; - if (properties != null) { - properties.forEach((k, v) { - final coerced = _coerceProperty(v); - if (coerced != null) props[k] = coerced; - }); - } - _evictStaleTimedEvents(); - final start = _pendingTimedEvents.remove(eventName); - if (start != null) { - props['\$duration'] = DateTime.now().difference(start).inMilliseconds / 1000.0; - } - Posthog().capture(eventName: eventName, properties: props); - }); - - void startTimingEvent(String eventName) => - PlatformService.executeIfSupported(PlatformService.isMixpanelSupported, () { - if (!_initialized) return; - _evictStaleTimedEvents(); - _pendingTimedEvents[eventName] = DateTime.now(); - }); - - // Drop pending starts that exceeded the TTL — guards against unbounded growth - // when a startTimingEvent has no matching track() (navigation away, crash, short-circuit). - static void _evictStaleTimedEvents() { - if (_pendingTimedEvents.isEmpty) return; - final cutoff = DateTime.now().subtract(_pendingTimedEventTtl); - _pendingTimedEvents.removeWhere((_, started) => started.isBefore(cutoff)); - } - - void onboardingDeviceConnected() => track('Onboarding Device Connected'); - - void onboardingCompleted() => track('Onboarding Completed'); - - void onboardingStepCompleted(String step) => track('Onboarding Step $step Completed'); - - void onboardingUserAcquisitionSource(String source) => - track('User Acquisition Source', properties: {'source': source}); - - void settingsSaved({bool hasWebhookConversationCreated = false, bool hasWebhookTranscriptReceived = false}) => track( - 'Developer Settings Saved', - properties: { - 'has_webhook_memory_created': hasWebhookConversationCreated, - 'has_webhook_transcript_received': hasWebhookTranscriptReceived, - }, - ); - - void pageOpened(String name) => track('$name Opened'); - - void appEnabled(String appId) { - track('App Enabled', properties: {'app_id': appId}); - setUserProperty('Apps Enabled Count', _preferences.enabledAppsCount); - } - - void appPurchaseStarted(String appId) => track('App Purchase Started', properties: {'app_id': appId}); - - void appPurchaseCompleted(String appId) => track('App Purchase Completed', properties: {'app_id': appId}); - - void privateAppSubmitted(Map properties) => track('Private App Submitted', properties: properties); - - void publicAppSubmitted(Map properties) => track('Public App Submitted', properties: properties); - - void appDisabled(String appId) { - track('App Disabled', properties: {'app_id': appId}); - setUserProperty('Apps Enabled Count', _preferences.enabledAppsCount); - } - - void appRated(String appId, double rating) { - track('App Rated', properties: {'app_id': appId, 'rating': rating}); - } - - void phoneMicRecordingStarted() => track('Phone Mic Recording Started'); - - void phoneMicRecordingStopped() => track('Phone Mic Recording Stopped'); - - // Phone Calls (VoIP) - void phoneCallPageOpened() => track('Phone Call Page Opened'); - - void phoneCallVerificationStarted() => track('Phone Call Verification Started'); - - void phoneCallVerificationCompleted() => track('Phone Call Verification Completed'); - - void phoneCallStarted({String? contactName}) => - track('Phone Call Started', properties: {'has_contact_name': contactName != null}); - - void phoneCallConnected() => track('Phone Call Connected'); - - void phoneCallEnded({required int durationSeconds}) => - track('Phone Call Ended', properties: {'duration_seconds': durationSeconds}); - - void phoneCallFailed({String? error}) => track('Phone Call Failed', properties: {'error': error ?? 'unknown'}); - - // Phone Calls Dialpad - void phoneCallDialpadOpened() => track('Phone Call Dialpad Opened'); - - void phoneCallDialpadDigitPressed(String digit) => - track('Phone Call Dialpad Digit Pressed', properties: {'digit': digit}); - - // Phone Calls Upsell - void phoneCallUpsellShown({required String source}) => - track('Phone Call Upsell Shown', properties: {'source': source}); - - void phoneCallUpsellUpgradeTapped() => track('Phone Call Upsell Upgrade Tapped'); - - void phoneCallUpsellDismissed() => track('Phone Call Upsell Dismissed'); - - void appResultExpanded(ServerConversation conversation, String appId) { - track('App Result Expanded', properties: getConversationEventProperties(conversation)..['app_id'] = appId); - } - - void languageChanged(String language) { - track('App Language Changed', properties: {'language': language}); - setUserProperty('App Primary Language', language); - } - - void recordingLanguageChanged(String language) { - track('Recording Language Changed', properties: {'language': language}); - setUserProperty('Recordings Language', language); - } - - void calendarEnabled() { - track('Calendar Enabled'); - setUserProperty('Calendar Enabled', true); - } - - void calendarDisabled() { - track('Calendar Disabled'); - setUserProperty('Calendar Enabled', false); - } - - void calendarModePressed(String mode) => track('Calendar Mode $mode Pressed'); - - void calendarSelected() => track('Calendar Selected'); - - void bottomNavigationTabClicked(String tab) => track('Bottom Navigation Tab Clicked', properties: {'tab': tab}); - - void deviceConnected() => track('Device Connected', properties: {..._preferences.btDevice.toJson()}); - - void deviceDisconnected() => track('Device Disconnected'); - - void memoriesPageCategoryOpened(MemoryCategory category) => - track('Fact Page Category Opened', properties: {'category': category.toString().split('.').last}); - - void memoriesPageDeletedMemory(Memory memory) => - track('Fact Page Deleted Fact', properties: {'fact_category': memory.category.toString().split('.').last}); - - void memoriesPageEditedMemory() => track('Fact Page Edited Fact'); - - void memoriesPageCreateMemoryBtn() => track('Fact Page Create Fact Button Pressed'); - - void memoriesPageCreatedMemory(MemoryCategory category) => - track('Fact Page Created Fact', properties: {'fact_category': category.toString().split('.').last}); - - void memorySearched(String query, int resultsCount) { - track('Fact Searched', properties: {'search_query_length': query.length, 'results_count': resultsCount}); - } - - void memorySearchCleared(int totalFactsCount) { - track('Fact Search Cleared', properties: {'total_facts_count': totalFactsCount}); - } - - void memoryListItemClicked(Memory memory) { - track( - 'Fact List Item Clicked', - properties: {'fact_id': memory.id, 'fact_category': memory.category.toString().split('.').last}, - ); - } - - void memoryVisibilityChanged(Memory memory, MemoryVisibility newVisibility) { - track( - 'Fact Visibility Changed', - properties: { - 'fact_id': memory.id, - 'fact_category': memory.category.toString().split('.').last, - 'new_visibility': newVisibility.name, - }, - ); - } - - void memoriesAllVisibilityChanged(MemoryVisibility newVisibility, int count) { - track('All Facts Visibility Changed', properties: {'new_visibility': newVisibility.name, 'facts_count': count}); - } - - void memoriesAllDeleted(int countBeforeDeletion) { - track('All Facts Deleted', properties: {'facts_count_before_deletion': countBeforeDeletion}); - } - - void memoriesFiltered(String filter) => track('Facts Filtered', properties: {'filter': filter}); - - void memoriesManagementSheetOpened() => track('Facts Management Sheet Opened'); - - Map _getTranscriptProperties(String transcript) { - String transcriptCopy = transcript.substring(0, transcript.length); - int speakersCount = 0; - for (int i = 0; i < 5; i++) { - if (transcriptCopy.contains('Speaker $i:')) speakersCount++; - transcriptCopy = transcriptCopy.replaceAll('Speaker $i:', ''); - } - transcriptCopy = transcriptCopy.replaceAll(' ', ' ').trim(); - return { - 'transcript_length': transcriptCopy.length, - 'transcript_word_count': transcriptCopy.split(' ').length, - 'speaker_count': speakersCount, - }; - } - - Map getConversationEventProperties(ServerConversation convo) { - var properties = _getTranscriptProperties(convo.getTranscript()); - int hoursAgo = DateTime.now().difference(convo.createdAt).inHours; - properties['memory_hours_since_creation'] = hoursAgo; - properties['memory_id'] = convo.id; - properties['memory_discarded'] = convo.discarded; - return properties; - } - - void conversationCreated(ServerConversation conversation) { - var properties = getConversationEventProperties(conversation); - properties['memory_result'] = conversation.discarded ? 'discarded' : 'saved'; - properties['action_items_count'] = conversation.structured.actionItems.length; - properties['transcript_language'] = _preferences.userPrimaryLanguage; - - // Additional properties for conversation creation - properties['conversation_source'] = conversation.source?.toString().split('.').last ?? 'unknown'; - properties['duration_seconds'] = conversation.getDurationInSeconds(); - properties['timestamp'] = conversation.createdAt.toIso8601String(); - - // Get the summarized app info if available - if (conversation.appResults.isNotEmpty) { - var summarizedApp = conversation.appResults.firstOrNull; - if (summarizedApp != null && summarizedApp.appId != null) { - properties['summary_app_id'] = summarizedApp.appId!; - } - } - - track('Memory Created', properties: properties); - } - - void conversationListItemClicked(ServerConversation conversation, int idx) => - track('Memory List Item Clicked', properties: getConversationEventProperties(conversation)); - - void conversationShareButtonClick(ServerConversation conversation) => - track('Memory Share Button Clicked', properties: getConversationEventProperties(conversation)); - - void conversationDeleted(ServerConversation conversation) => - track('Memory Deleted', properties: getConversationEventProperties(conversation)); - - void chatMessageSent({ - required String message, - required bool includesFiles, - required int numberOfFiles, - required String chatTargetId, - required bool isPersonaChat, - required bool isVoiceInput, - }) => track( - 'Chat Message Sent', - properties: { - 'message_length': message.length, - 'message_word_count': message.split(' ').length, - 'includes_files': includesFiles, - 'number_of_files': numberOfFiles, - 'chat_target_id': chatTargetId, - 'is_persona_chat': isPersonaChat, - 'is_voice_input': isVoiceInput, - }, - ); - - void chatVoiceInputUsed({required String chatTargetId, required bool isPersonaChat}) { - track('Chat Voice Input Used', properties: {'chat_target_id': chatTargetId, 'is_persona_chat': isPersonaChat}); - } - - void speechProfileCapturePageClicked() => track('Speech Profile Capture Page Clicked'); - - void showDiscardedMemoriesToggled(bool showDiscarded) => - track('Show Discarded Memories Toggled', properties: {'show_discarded': showDiscarded}); - - // Conversation Display Settings Events - void conversationDisplaySettingsOpened() => track('Conversation Display Settings Opened'); - - void showShortConversationsToggled(bool showShort) => - track('Show Short Conversations Toggled', properties: {'show_short': showShort}); - - void showDiscardedConversationsToggled(bool showDiscarded) => - track('Show Discarded Conversations Toggled', properties: {'show_discarded': showDiscarded}); - - void shortConversationThresholdChanged(int thresholdSeconds) => track( - 'Short Conversation Threshold Changed', - properties: {'threshold_seconds': thresholdSeconds, 'threshold_minutes': thresholdSeconds ~/ 60}, - ); - - void voiceResponseToggled(bool enabled) => track('Voice Response Audio Toggled', properties: {'enabled': enabled}); - - void voiceResponseModeChanged(int mode) { - const names = {0: 'off', 1: 'headphones_only', 2: 'always'}; - track('Voice Response Mode Changed', properties: {'mode': names[mode] ?? 'unknown', 'mode_int': mode}); - } - - // Conversation Merge Events - void conversationMergeSelectionModeEntered() => track('Conversation Merge Selection Mode Entered'); - - void conversationMergeSelectionModeExited() => track('Conversation Merge Selection Mode Exited'); - - void conversationSelectedForMerge(String conversationId, int totalSelected) => track( - 'Conversation Selected For Merge', - properties: {'conversation_id': conversationId, 'total_selected': totalSelected}, - ); - - void conversationMergeInitiated(List conversationIds) => track( - 'Conversation Merge Initiated', - properties: {'conversation_count': conversationIds.length, 'conversation_ids': conversationIds}, - ); - - void conversationMergeCompleted(String mergedConversationId, List removedConversationIds) => track( - 'Conversation Merge Completed', - properties: { - 'merged_conversation_id': mergedConversationId, - 'removed_count': removedConversationIds.length, - 'removed_conversation_ids': removedConversationIds, - }, - ); - - void conversationMergeFailed(List conversationIds) => track( - 'Conversation Merge Failed', - properties: {'conversation_count': conversationIds.length, 'conversation_ids': conversationIds}, - ); - - // Important Conversation Share Events - void importantConversationNotificationReceived(String conversationId) => - track('Important Conversation Notification Received', properties: {'conversation_id': conversationId}); - - void shareToContactsSheetOpened(String conversationId) => - track('Share To Contacts Sheet Opened', properties: {'conversation_id': conversationId}); - - void shareToContactsSelected(String conversationId, int contactCount) => track( - 'Share To Contacts Selected', - properties: {'conversation_id': conversationId, 'contact_count': contactCount}, - ); - - void shareToContactsSmsOpened(String conversationId, int contactCount) => track( - 'Share To Contacts SMS Opened', - properties: {'conversation_id': conversationId, 'contact_count': contactCount}, - ); - - void chatMessageConversationClicked(ServerConversation conversation) => - track('Chat Message Memory Clicked', properties: getConversationEventProperties(conversation)); - - void addManualConversationClicked() => track('Add Manual Memory Clicked'); - - void manualConversationCreated(ServerConversation conversation) => - track('Manual Memory Created', properties: getConversationEventProperties(conversation)); - - void setUserProperties(String whatDoYouDo, String whereDoYouPlanToUseYourFriend, String ageRange) { - setUserProperty('What the user does', whatDoYouDo); - setUserProperty('Using Omi At', whereDoYouPlanToUseYourFriend); - setUserProperty('Age Range', ageRange); - } - - void reProcessConversation(ServerConversation conversation) => - track('Re-process Memory', properties: getConversationEventProperties(conversation)); - - void developerModeEnabled() { - track('Developer Mode Enabled'); - setUserProperty('Dev Mode Enabled', true); - } - - void developerModeDisabled() { - track('Developer Mode Disabled'); - setUserProperty('Dev Mode Enabled', false); - } - - void userIDCopied() => track('User ID Copied'); - - void exportMemories() => track('Dev Mode Export Memories'); - - void importMemories() => track('Dev Mode Import Memories'); - - void importedMemories() => track('Dev Mode Imported Memories'); - - void supportContacted() => track('Support Contacted'); - - void copiedConversationDetails(ServerConversation conversation, {String source = ''}) => - track('Copied Memory Detail $source'.trim(), properties: getConversationEventProperties(conversation)); - - void checkedActionItem(ServerConversation conversation, int idx) => - track('Checked Action Item', properties: getConversationEventProperties(conversation)); - - void uncheckedActionItem(ServerConversation conversation, int idx) => - track('Unchecked Action Item', properties: getConversationEventProperties(conversation)); - - void deletedActionItem(ServerConversation conversation) => - track('Deleted Action Item', properties: getConversationEventProperties(conversation)); - - void paywallOpened(String source) => track('Paywall Opened', properties: {'source': source}); - - void upgradePlanSelected({required String plan, required String source}) => - track('Upgrade Plan Selected', properties: {'plan': plan, 'source': source}); - - void upgradeSucceeded() => track('Upgrade Succeeded'); - - void upgradeCancelled() => track('Upgrade Cancelled'); - - void upgradeModalDismissed() => track('Upgrade Modal Dismissed'); - - void upgradeModalClicked() => track('Upgrade Modal Clicked'); - - void subscriptionCancelFlowStarted() => track('Subscription Cancel Flow Started'); - - void subscriptionCancelReasonSelected({required String reason}) => - track('Subscription Cancel Reason Selected', properties: {'reason': reason}); - - void subscriptionCancelConfirmed({required String reason, String? details}) => - track('Subscription Cancel Confirmed', properties: {'reason': reason, 'reason_details': details}); - - void subscriptionCancelKeptPlan({required int step, String? reason}) => - track('Subscription Cancel Kept Plan', properties: {'step': step, 'reason': reason}); - - void subscriptionCancelAbandoned({required int step, String? reason}) => - track('Subscription Cancel Abandoned', properties: {'step': step, 'reason': reason}); - - void getFriendClicked() => track('Get Friend Clicked'); - - void connectFriendClicked() => track('Connect Friend Clicked'); - - void disconnectFriendClicked() => track('Disconnect Friend Clicked'); - - void batteryIndicatorClicked() => track('Battery Indicator Clicked'); - - void useWithoutDeviceOnboardingWelcome() => track('Use Without Device Onboarding Welcome'); - - void useWithoutDeviceOnboardingFindDevices() => track('Use Without Device Onboarding Find Devices'); - - // void pageViewed(String pageName) => startTimingEvent('Page View $pageName'); - - void addedPerson() => track('Added Person'); - - void removedPerson() => track('Removed Person'); - - void tagSheetOpened() => track('Tag Sheet Opened'); - - void taggedSegment(String assignType) => track('Tagged Segment $assignType'); - - void untaggedSegment() => track('Untagged Segment'); - - void editSegmentTextStarted() => track('Edit Segment Text Started'); - - void editSegmentTextSaved() => track('Edit Segment Text Saved'); - - void editSegmentTextCancelled() => track('Edit Segment Text Cancelled'); - - void editSummaryStarted() => track('Edit Summary Started'); - - void editSummarySaved() => track('Edit Summary Saved'); - - void editSummaryCancelled() => track('Edit Summary Cancelled'); - - void deleteAccountClicked() => track('Delete Account Clicked'); - - void deleteAccountConfirmed() => track('Delete Account Confirmed'); - - void deleteAccountCancelled() => track('Delete Account Cancelled'); - - void deleteAccountFlowStarted() => track('Delete Account Flow Started'); - - void deleteAccountReasonSelected({required String reason}) => - track('Delete Account Reason Selected', properties: {'reason': reason}); - - void deleteAccountFeedbackSubmitted({required String reason, String? details}) => - track('Delete Account Feedback Submitted', properties: {'reason': reason, 'reason_details': details}); - - void deleteAccountAbandoned({required int step, String? reason}) => - track('Delete Account Abandoned', properties: {'step': step, 'reason': reason}); - - void deleteAccountKeptAccount({required int step, String? reason}) => - track('Delete Account Kept Account', properties: {'step': step, 'reason': reason}); - - void deleteUser() => PlatformService.executeIfSupported(PlatformService.isMixpanelSupported, () { - if (!_initialized) return; - Posthog().capture(eventName: 'User Deleted'); - Posthog().reset(); - }); - - // Apps Filter - void appsFilterOpened() => track('Apps Filter Opened'); - void appsFilterApplied() => track('Apps Filter Applied'); - void appsCategoryFilter(String category, bool isSelected) { - track('Apps Category Filter', properties: {'category': category, 'selected': isSelected}); - } - - void appsTypeFilter(String type, bool isSelected) { - track('Apps Type Filter', properties: {'type': type, 'selected': isSelected}); - } - - void appsSortFilter(String sortBy, bool isSelected) { - track('Apps Sort Filter', properties: {'sort_by': sortBy, 'selected': isSelected}); - } - - void appsRatingFilter(String rating, bool isSelected) { - track('Apps Rating Filter', properties: {'rating': rating, 'selected': isSelected}); - } - - void appsCapabilityFilter(String capability, bool isSelected) { - track('Apps Capability Filter', properties: {'capability': capability, 'selected': isSelected}); - } - - void appsClearFilters() => track('Apps Clear Filters'); - - // Brain Map Events - void brainMapOpened() => track('Brain Map Opened'); - - void brainMapNodeClicked(String nodeId, String label, String type) { - track('Brain Map Node Clicked', properties: {'node_id': nodeId, 'label': label, 'type': type}); - } - - void brainMapShareClicked() => track('Brain Map Share Clicked'); - - void brainMapRebuilt() => track('Brain Map Rebuilt'); - - // Summarized Apps Sheet Events - void summarizedAppSheetViewed({required String conversationId, String? currentSummarizedAppId}) { - track( - 'Summarized App Sheet Viewed', - properties: {'conversation_id': conversationId, 'current_summarized_app_id': currentSummarizedAppId ?? 'auto'}, - ); - } - - void summarizedAppSelected({required String conversationId, required String selectedAppId, String? previousAppId}) { - track( - 'Summarized App Selected', - properties: { - 'conversation_id': conversationId, - 'selected_app_id': selectedAppId, - 'previous_app_id': previousAppId ?? 'auto', - }, - ); - } - - void summarizedAppEnableAppsClicked({required String conversationId}) { - track('Summarized App Enable Apps Clicked', properties: {'conversation_id': conversationId}); - } - - void summarizedAppCreateTemplateClicked({required String conversationId}) { - track('Summarized App Create Template Clicked', properties: {'conversation_id': conversationId}); - } - - void quickTemplateCreated({required String conversationId, required String appName, required bool isPublic}) { - track( - 'Quick Template Created', - properties: {'conversation_id': conversationId, 'app_name': appName, 'is_public': isPublic}, - ); - } - - // Action Items Page Events - void actionItemsPageOpened() => track('Action Items Page Opened'); - - void actionItemsViewToggled(bool isGroupedView) { - track('Action Items View Toggled', properties: {'grouped_view': isGroupedView}); - } - - void actionItemToggledCompletionOnActionItemsPage({ - required String conversationId, - required String actionItemDescription, // Using description as a pseudo-ID if no stable ID exists - required bool isCompleted, - }) { - track( - 'Action Item Completion Toggled on Action Items Page', - properties: { - 'conversation_id': conversationId, - 'action_item_description': actionItemDescription, - 'is_completed': isCompleted, - }, - ); - } - - void actionItemTappedForEditOnActionItemsPage({ - required String conversationId, - required String actionItemDescription, - }) { - track( - 'Action Item Tapped for Edit on Action Items Page', - properties: {'conversation_id': conversationId, 'action_item_description': actionItemDescription}, - ); - } - - void actionItemsDateFilterApplied(String filterType) { - track('Action Items Date Filter Applied', properties: {'filter_type': filterType}); - } - - void actionItemsDateFilterCleared() { - track('Action Items Date Filter Cleared'); - } - - void actionItemTabChanged(String tabName) { - track('Action Item Tab Changed', properties: {'tab_name': tabName}); - } - - void actionItemCompleted({required String fromTab}) { - track('Action Item Completed', properties: {'from_tab': fromTab}); - } - - void trainingDataOptInSubmitted() { - track('Training Data Opt-In Submitted'); - setUserProperty('Training Data Opted In', true); - } - - void trainingDataOptInApproved() { - track('Training Data Opt-In Approved'); - setUserProperty('Training Data Status', 'approved'); - } - - // Homepage Events - void recordingMuteToggled({required bool isMuted, required String recordingType}) { - track('Recording Mute Toggled', properties: {'is_muted': isMuted, 'recording_type': recordingType}); - } - - void deletedConversationsFilterToggled(bool showDeleted) { - track('Deleted Conversations Filter Toggled', properties: {'show_deleted': showDeleted}); - } - - void calendarFilterApplied(DateTime selectedDate) { - track( - 'Calendar Filter Applied', - properties: { - 'selected_date': selectedDate.toIso8601String(), - 'days_ago': DateTime.now().difference(selectedDate).inDays, - }, - ); - } - - void calendarFilterCleared() { - track('Calendar Filter Cleared'); - } - - void searchBarFocused() { - track('Search Bar Focused'); - } - - void searchQueryEntered(String query, int resultsCount) { - track( - 'Search Query Entered', - properties: { - 'query_length': query.length, - 'query_word_count': query.split(' ').length, - 'results_count': resultsCount, - }, - ); - } - - void searchQueryCleared() { - track('Search Query Cleared'); - } - - void conversationOpenedFromSearch({ - required ServerConversation conversation, - required String searchQuery, - required int conversationIndexInResults, - }) { - var properties = getConversationEventProperties(conversation); - properties['search_query'] = searchQuery; - properties['search_query_length'] = searchQuery.length; - properties['conversation_index_in_results'] = conversationIndexInResults; - track('Conversation Opened From Search', properties: properties); - } - - void liveTranscriptCardClicked({ - required bool hasSegments, - required bool hasPhotos, - required int segmentCount, - required int photoCount, - }) { - track( - 'Live Transcript Card Clicked', - properties: { - 'has_segments': hasSegments, - 'has_photos': hasPhotos, - 'segment_count': segmentCount, - 'photo_count': photoCount, - }, - ); - } - - void deviceInfoButtonClicked({String? deviceId, String? deviceName, int? batteryLevel}) { - track( - 'Device Info Button Clicked', - properties: { - if (deviceId != null) 'device_id': deviceId, - if (deviceName != null) 'device_name': deviceName, - if (batteryLevel != null) 'battery_level': batteryLevel, - }, - ); - } - - void conversationListItemClickedWithTimeDifference({ - required ServerConversation conversation, - required int conversationIndex, - required int hoursSinceConversation, - }) { - var properties = getConversationEventProperties(conversation); - properties['conversation_index'] = conversationIndex; - properties['hours_since_conversation'] = hoursSinceConversation; - track('Conversation List Item Clicked', properties: properties); - } - - void conversationSwipedToDelete(ServerConversation conversation) { - var properties = getConversationEventProperties(conversation); - track('Conversation Swiped To Delete', properties: properties); - } - - // Conversation Detail Page Events - void conversationDetailTabChanged(String tabName) { - track('Conversation Detail Tab Changed', properties: {'tab_name': tabName}); - } - - void speakerEdited({required String conversationId, required int oldSpeakerCount, required int newSpeakerCount}) { - track( - 'Speaker Edited', - properties: { - 'conversation_id': conversationId, - 'old_speaker_count': oldSpeakerCount, - 'new_speaker_count': newSpeakerCount, - 'speaker_count_changed': oldSpeakerCount != newSpeakerCount, - }, - ); - } - - void conversationDetailSearchClicked({required String conversationId}) { - track('Conversation Detail Search Clicked', properties: {'conversation_id': conversationId}); - } - - void conversationDetailSearchQueryEntered({ - required String conversationId, - required String query, - required int resultsCount, - required String activeTab, - }) { - track( - 'Conversation Detail Search Query Entered', - properties: { - 'conversation_id': conversationId, - 'query_length': query.length, - 'results_count': resultsCount, - 'active_tab': activeTab, - }, - ); - } - - void conversationReprocessedWithApp({ - required String conversationId, - required String appId, - required String appName, - required bool isOwnApp, - required bool wasAutoSelected, - }) { - track( - 'Conversation Reprocessed', - properties: { - 'conversation_id': conversationId, - 'app_id': appId, - 'app_name': appName, - 'is_own_app': isOwnApp, - 'was_auto_selected': wasAutoSelected, - }, - ); - } - - void conversationShared({required ServerConversation conversation, required String shareMethod}) { - var properties = getConversationEventProperties(conversation); - properties['share_method'] = shareMethod; - track('Conversation Shared', properties: properties); - } - - void conversationThreeDotsMenuOpened({required String conversationId}) { - track('Conversation Three Dots Menu Opened', properties: {'conversation_id': conversationId}); - } - - void conversationThreeDotsMenuActionSelected({required String conversationId, required String action}) { - track( - 'Conversation Three Dots Menu Action Selected', - properties: {'conversation_id': conversationId, 'action': action}, - ); - } - - // ============================================================================ - // ACTION ITEMS TRACKING - // ============================================================================ - - void actionItemChecked({required String actionItemId, required bool completed, required DateTime timestamp}) { - track( - 'Action Item Checked', - properties: {'action_item_id': actionItemId, 'completed': completed, 'timestamp': timestamp.toIso8601String()}, - ); - } - - void exportTasksBannerClicked() { - track('Export Tasks Banner Clicked'); - } - - void taskIntegrationEnabled({required String appName, required bool success}) { - track('Task Integration Enabled', properties: {'app_name': appName, 'success': success}); - } - - void taskIntegrationAuthFailed({required String appName}) { - track('Task Integration Auth Failed', properties: {'app_name': appName}); - } - - void taskIntegrationSettingsOpened({required String appName}) { - track('Task Integration Settings Opened', properties: {'app_name': appName}); - } - - // ============================================================================ - // TRANSCRIPTION / CUSTOM STT TRACKING - // ============================================================================ - - void transcriptionSourceSelected({ - required String source, // 'omi' or 'custom' - }) { - track('Transcription Source Selected', properties: {'source': source}); - } - - void transcriptionProviderSelected({ - required String provider, // e.g. 'openai', 'deepgram', 'gemini', 'local_whisper', 'custom', 'custom_live' - }) { - track('Transcription Provider Selected', properties: {'provider': provider}); - } - - // ============================================================================ - // AUDIO PLAYBACK TRACKING - // ============================================================================ - - void audioPlaybackStarted({required String conversationId, int? durationSeconds}) { - track( - 'Audio Playback Started', - properties: {'conversation_id': conversationId, if (durationSeconds != null) 'duration_seconds': durationSeconds}, - ); - } - - void audioPlaybackPaused({required String conversationId, required int positionSeconds, int? durationSeconds}) { - track( - 'Audio Playback Paused', - properties: { - 'conversation_id': conversationId, - 'position_seconds': positionSeconds, - if (durationSeconds != null) 'duration_seconds': durationSeconds, - if (durationSeconds != null && durationSeconds > 0) - 'completion_percentage': ((positionSeconds / durationSeconds) * 100).round(), - }, - ); - } - - void audioPlaybackSeeked({required String conversationId, required int toPositionSeconds}) { - track( - 'Audio Playback Seeked', - properties: {'conversation_id': conversationId, 'to_position_seconds': toPositionSeconds}, - ); - } - - void transcriptSegmentTapped({ - required String conversationId, - required double segmentStartSeconds, - required double seekPositionSeconds, - }) { - track( - 'Transcript Segment Tapped', - properties: { - 'conversation_id': conversationId, - 'segment_start_seconds': segmentStartSeconds, - 'seek_position_seconds': seekPositionSeconds, - }, - ); - } - - void audioShareStarted({required String conversationId, required int audioFileCount}) { - track('Audio Share Started', properties: {'conversation_id': conversationId, 'audio_file_count': audioFileCount}); - } - - void audioShareCompleted({ - required String conversationId, - required int audioFileCount, - required bool wasCombined, - required int durationSeconds, - }) { - track( - 'Audio Share Completed', - properties: { - 'conversation_id': conversationId, - 'audio_file_count': audioFileCount, - 'was_combined': wasCombined, - 'duration_seconds': durationSeconds, - }, - ); - } - - void audioShareFailed({required String conversationId, String? errorMessage}) { - track( - 'Audio Share Failed', - properties: {'conversation_id': conversationId, if (errorMessage != null) 'error_message': errorMessage}, - ); - } - - void audioShareCancelled({required String conversationId}) { - track('Audio Share Cancelled', properties: {'conversation_id': conversationId}); - } - - void actionItemExported({required String actionItemId, required String appName, required DateTime timestamp}) { - track( - 'Action Item Exported', - properties: {'action_item_id': actionItemId, 'app_name': appName, 'timestamp': timestamp.toIso8601String()}, - ); - } - - void actionItemManuallyAdded({required String actionItemId, required DateTime timestamp}) { - track( - 'Action Item Manually Added', - properties: {'action_item_id': actionItemId, 'timestamp': timestamp.toIso8601String()}, - ); - } - - void actionItemEdited({required String actionItemId, required bool titleChanged, required bool dateChanged}) { - track( - 'Action Item Edited', - properties: {'action_item_id': actionItemId, 'title_changed': titleChanged, 'date_changed': dateChanged}, - ); - } - - void appleRemindersSyncCompleted({ - required int pendingExported, - required int syncedChecked, - required int completionsPulled, - required int completionsPushed, - required int titleDuePulled, - required int titleDuePushed, - required int remindersUnlinked, - }) { - track( - 'Apple Reminders Sync Completed', - properties: { - 'pending_exported': pendingExported, - 'synced_checked': syncedChecked, - 'completions_pulled': completionsPulled, - 'completions_pushed': completionsPushed, - 'title_due_pulled': titleDuePulled, - 'title_due_pushed': titleDuePushed, - 'reminders_unlinked': remindersUnlinked, - }, - ); - } - - void appleReminderDirectSync({required String actionItemId}) { - track('Apple Reminder Direct Sync', properties: {'action_item_id': actionItemId}); - } - - void appleReminderDeleted({required String actionItemId}) { - track('Apple Reminder Deleted', properties: {'action_item_id': actionItemId}); - } - - // ============================================================================ - // SETTINGS PAGE TRACKING - // ============================================================================ - - void settingsPageOpened({required String pageName}) { - track('Settings Page Opened', properties: {'page_name': pageName}); - } - - void usageTabChanged({required String tabName}) { - track('Usage Tab Changed', properties: {'tab_name': tabName}); - } - - // ============================================================================ - // APPS PAGE TRACKING - // ============================================================================ - - void appsSearched({required String searchTerm, required int resultCount}) { - track('Apps Searched', properties: {'search_term': searchTerm, 'result_count': resultCount}); - } - - void appsFilterMyApps({required bool enabled}) { - track('Apps Filter My Apps', properties: {'enabled': enabled}); - } - - void appsFilterInstalled({required bool enabled}) { - track('Apps Filter Installed', properties: {'enabled': enabled}); - } - - void appsFilterRating({required int rating}) { - track('Apps Filter Rating', properties: {'rating': rating}); - } - - void appsFilterCategory({required String category}) { - track('Apps Filter Category', properties: {'category': category}); - } - - void appsSortChanged({required String sortOption}) { - track('Apps Sort Changed', properties: {'sort_option': sortOption}); - } - - void appsFilterCapability({required String capability}) { - track('Apps Filter Capability', properties: {'capability': capability}); - } - - void appsCategoryPageOpened({required String category, required int appCount}) { - track('Apps Category Page Opened', properties: {'category': category, 'app_count': appCount}); - } - - // ============================================================================ - // APP DETAIL PAGE TRACKING - // ============================================================================ - - void appDetailViewed({ - required String appId, - required String appName, - String? category, - double? rating, - int? installs, - bool? isInstalled, - }) { - track( - 'App Detail Viewed', - properties: { - 'app_id': appId, - 'app_name': appName, - if (category != null) 'category': category, - if (rating != null) 'rating': rating, - if (installs != null) 'installs': installs, - if (isInstalled != null) 'is_installed': isInstalled, - }, - ); - } - - void appDetailSectionViewed({required String appId, required String sectionName}) { - track('App Detail Section Viewed', properties: {'app_id': appId, 'section_name': sectionName}); - } - - void appDetailShared({required String appId, required String appName}) { - track('App Detail Shared', properties: {'app_id': appId, 'app_name': appName}); - } - - void appDetailReviewsOpened({required String appId, required int reviewCount}) { - track('App Detail Reviews Opened', properties: {'app_id': appId, 'review_count': reviewCount}); - } - - void appDetailReviewAdded({required String appId, required int rating, required bool hasComment}) { - track('App Detail Review Added', properties: {'app_id': appId, 'rating': rating, 'has_comment': hasComment}); - } - - void appDetailSettingsOpened({required String appId}) { - track('App Detail Settings Opened', properties: {'app_id': appId}); - } - - void appDetailSubscribeClicked({required String appId, required String appName, double? price}) { - track( - 'App Detail Subscribe Clicked', - properties: {'app_id': appId, 'app_name': appName, if (price != null) 'price': price}, - ); - } - - void appDetailSubscriptionCancelled({required String appId, required String appName}) { - track('App Detail Subscription Cancelled', properties: {'app_id': appId, 'app_name': appName}); - } - - void appDetailPreviewImageViewed({required String appId, required int imageIndex}) { - track('App Detail Preview Image Viewed', properties: {'app_id': appId, 'image_index': imageIndex}); - } - - void appDetailChatClicked({required String appId, required String appName}) { - track('App Detail Chat Clicked', properties: {'app_id': appId, 'app_name': appName}); - } - - // ============================================================================ - // FOLDER TRACKING - // ============================================================================ - - void folderCreated({ - required String folderId, - required String folderName, - required String icon, - required String color, - }) { - track( - 'Folder Created', - properties: {'folder_id': folderId, 'folder_name': folderName, 'icon': icon, 'color': color}, - ); - } - - void folderUpdated({required String folderId, required String folderName}) { - track('Folder Updated', properties: {'folder_id': folderId, 'folder_name': folderName}); - } - - void folderDeleted({ - required String folderId, - required String folderName, - required int conversationCount, - String? moveToFolderId, - }) { - track( - 'Folder Deleted', - properties: { - 'folder_id': folderId, - 'folder_name': folderName, - 'conversation_count': conversationCount, - if (moveToFolderId != null) 'move_to_folder_id': moveToFolderId, - 'moved_conversations': moveToFolderId != null, - }, - ); - } - - void folderSelected({String? folderId, String? folderName}) { - track( - 'Folder Selected', - properties: { - if (folderId != null) 'folder_id': folderId, - if (folderName != null) 'folder_name': folderName, - 'is_all_tab': folderId == null, - }, - ); - } - - void folderContextMenuOpened({required String folderId, required String folderName}) { - track('Folder Context Menu Opened', properties: {'folder_id': folderId, 'folder_name': folderName}); - } - - void createFolderButtonClicked() { - track('Create Folder Button Clicked'); - } - - void conversationDetailFolderChipClicked({required String conversationId, String? currentFolderId}) { - track( - 'Conversation Detail Folder Chip Clicked', - properties: { - 'conversation_id': conversationId, - if (currentFolderId != null) 'current_folder_id': currentFolderId, - 'has_folder': currentFolderId != null, - }, - ); - } - - void conversationMovedToFolder({ - required String conversationId, - String? fromFolderId, - String? toFolderId, - required String source, - }) { - track( - 'Conversation Moved To Folder', - properties: { - 'conversation_id': conversationId, - if (fromFolderId != null) 'from_folder_id': fromFolderId, - if (toFolderId != null) 'to_folder_id': toFolderId, - 'source': source, - 'was_in_folder': fromFolderId != null, - }, - ); - } - - void conversationVisibilityChanged({ - required String conversationId, - required String fromVisibility, - required String toVisibility, - }) { - track( - 'Conversation Visibility Changed', - properties: {'conversation_id': conversationId, 'from_visibility': fromVisibility, 'to_visibility': toVisibility}, - ); - } - - void starredFilterToggled({required bool enabled, String? selectedFolderId}) { - track( - 'Starred Filter Toggled', - properties: { - 'enabled': enabled, - if (selectedFolderId != null) 'selected_folder_id': selectedFolderId, - 'has_folder_filter': selectedFolderId != null, - }, - ); - } - - void conversationStarToggled({ - required ServerConversation conversation, - required bool starred, - required String source, - }) { - var properties = getConversationEventProperties(conversation); - properties['starred'] = starred; - properties['source'] = source; - properties['duration_seconds'] = conversation.getDurationInSeconds(); - - // Get the summarized app id if available - if (conversation.appResults.isNotEmpty) { - var summarizedApp = conversation.appResults.firstOrNull; - if (summarizedApp != null && summarizedApp.appId != null) { - properties['summary_app_id'] = summarizedApp.appId!; - } - } - - track('Conversation Star Toggled', properties: properties); - } - - void omiDoubleTap({required String feature, Map? additionalProperties}) { - track( - 'Omi Double Tap', - properties: {'feature': feature, if (additionalProperties != null) ...additionalProperties}, - ); - } - - // ============================================================================ - // WRAPPED 2025 TRACKING - // ============================================================================ - - void wrappedPageOpened() { - track('Wrapped Page Opened'); - } - - void wrappedBannerClicked() { - track('Wrapped Banner Clicked'); - } - - void wrappedGenerationStarted() { - track('Wrapped Generation Started'); - startTimingEvent('Wrapped Generation Completed'); - } - - void wrappedGenerationCompleted({ - required int totalConversations, - required int totalMinutes, - required int daysActive, - }) { - track( - 'Wrapped Generation Completed', - properties: {'total_conversations': totalConversations, 'total_minutes': totalMinutes, 'days_active': daysActive}, - ); - } - - void wrappedGenerationFailed({String? error}) { - track('Wrapped Generation Failed', properties: {if (error != null) 'error': error}); - } - - void wrappedCardViewed({required String cardName, required int cardIndex}) { - track('Wrapped Card Viewed', properties: {'card_name': cardName, 'card_index': cardIndex}); - } - - void wrappedShareButtonClicked({required String cardName, required int cardIndex}) { - track('Wrapped Share Button Clicked', properties: {'card_name': cardName, 'card_index': cardIndex}); - startTimingEvent('Wrapped Shared Successfully'); - } - - void wrappedSharedSuccessfully({required String cardName, required int cardIndex, int? fileSizeBytes}) { - track( - 'Wrapped Shared Successfully', - properties: { - 'card_name': cardName, - 'card_index': cardIndex, - if (fileSizeBytes != null) 'file_size_bytes': fileSizeBytes, - if (fileSizeBytes != null) 'file_size_kb': (fileSizeBytes / 1024).round(), - if (fileSizeBytes != null) 'file_size_mb': (fileSizeBytes / (1024 * 1024)).toStringAsFixed(2), - }, - ); - } - - void wrappedShareFailed({required String cardName, required int cardIndex, String? error}) { - track( - 'Wrapped Share Failed', - properties: {'card_name': cardName, 'card_index': cardIndex, if (error != null) 'error': error}, - ); - } - - // ============================================================================ - // DAILY SUMMARY / RECAP TRACKING - // ============================================================================ - - void dailySummarySettingsOpened() => track('Daily Summary Settings Opened'); - - // ============================================================================ - // Permissions - // ============================================================================ - - void permissionsSettingsOpened() => track('Permissions Settings Opened'); - - void permissionChanged({required String permission, required bool granted}) { - track('Permission Changed', properties: {'permission': permission, 'granted': granted}); - } - - void permissionsInterstitialShown() => track('Permissions Interstitial Shown'); - - void permissionsInterstitialCompleted() => track('Permissions Interstitial Completed'); - - void permissionsInterstitialSkipped() => track('Permissions Interstitial Skipped'); - - void dailySummaryToggled({required bool enabled}) { - track('Daily Summary Toggled', properties: {'enabled': enabled}); - setUserProperty('Daily Summary Enabled', enabled); - } - - void dailySummaryTimeChanged({required int hour}) { - final hour12 = hour == 0 ? 12 : (hour > 12 ? hour - 12 : hour); - final period = hour >= 12 ? 'PM' : 'AM'; - track( - 'Daily Summary Time Changed', - properties: {'hour_24': hour, 'hour_12': hour12, 'period': period, 'display_time': '$hour12:00 $period'}, - ); - setUserProperty('Daily Summary Hour', hour); - } - - void dailySummaryDetailViewed({required String summaryId, required String date, String? source}) { - track( - 'Daily Summary Detail Viewed', - properties: {'summary_id': summaryId, 'date': date, if (source != null) 'source': source}, - ); - } - - void dailySummaryTestGenerated({required String date}) { - track('Daily Summary Test Generated', properties: {'date': date}); - } - - void dailySummaryTestGenerationFailed({required String date, String? error}) { - track('Daily Summary Test Generation Failed', properties: {'date': date, if (error != null) 'error': error}); - } - - void recapTabOpened() => track('Recap Tab Opened'); - - void recapSummaryCardClicked({required String summaryId, required String date, required int cardIndex}) { - track('Recap Summary Card Clicked', properties: {'summary_id': summaryId, 'date': date, 'card_index': cardIndex}); - } - - void dailySummaryNotificationReceived({required String summaryId, required String date}) { - track('Daily Summary Notification Received', properties: {'summary_id': summaryId, 'date': date}); - } - - void dailySummaryNotificationOpened({required String summaryId, required String date}) { - track('Daily Summary Notification Opened', properties: {'summary_id': summaryId, 'date': date}); - } - - void dailySummaryConversationClicked({ - required String summaryId, - required String conversationId, - required String source, - }) { - track( - 'Daily Summary Conversation Clicked', - properties: {'summary_id': summaryId, 'conversation_id': conversationId, 'source': source}, - ); - } - - void dailySummarySectionViewed({required String summaryId, required String sectionName}) { - track('Daily Summary Section Viewed', properties: {'summary_id': summaryId, 'section_name': sectionName}); - } - - // ============================================================================ - // ANNOUNCEMENT TRACKING - // ============================================================================ - - void announcementShown({required String announcementId, required String type, String? trigger, int? priority}) { - track( - 'Announcement Shown', - properties: { - 'announcement_id': announcementId, - 'type': type, - if (trigger != null) 'trigger': trigger, - if (priority != null) 'priority': priority, - }, - ); - } - - void announcementDismissed({required String announcementId, required String type, required bool ctaClicked}) { - track( - 'Announcement Dismissed', - properties: {'announcement_id': announcementId, 'type': type, 'cta_clicked': ctaClicked}, - ); - } - - void changelogShown({required int changelogCount, required String fromVersion, required String toVersion}) { - track( - 'Changelog Shown', - properties: {'changelog_count': changelogCount, 'from_version': fromVersion, 'to_version': toVersion}, - ); - } - - void changelogDismissed({required int changelogCount}) { - track('Changelog Dismissed', properties: {'changelog_count': changelogCount}); - } - - void whatsNewOpened() => track('Whats New Opened'); - - // ============================================================================ - // GOALS TRACKING - // ============================================================================ - - void goalAddButtonTapped({required String source}) { - track('Goal Add Button Tapped', properties: {'source': source}); - } - - void goalCreated({ - required String goalId, - required int titleLength, - required double targetValue, - required String source, - }) { - track( - 'Goal Created', - properties: {'goal_id': goalId, 'title_length': titleLength, 'target_value': targetValue, 'source': source}, - ); - } - - void goalUpdated({required String goalId, required String source}) { - track('Goal Updated', properties: {'goal_id': goalId, 'source': source}); - } - - void goalDeleted({required String goalId, required String source, required String method}) { - track('Goal Deleted', properties: {'goal_id': goalId, 'source': source, 'method': method}); - } - - void goalItemTappedForEdit({required String goalId, required String source}) { - track('Goal Item Tapped For Edit', properties: {'goal_id': goalId, 'source': source}); - } - - void goalEmojiSelected({required String emoji}) { - track('Goal Emoji Selected', properties: {'emoji': emoji}); - } - - void goalProgressChanged({ - required String goalId, - required double oldValue, - required double newValue, - required double targetValue, - }) { - track( - 'Goal Progress Changed', - properties: { - 'goal_id': goalId, - 'old_value': oldValue, - 'new_value': newValue, - 'target_value': targetValue, - 'progress_percentage': targetValue > 0 ? (newValue / targetValue * 100).round() : 0, - }, - ); - } - - void taskDraggedToGoal({required String taskId, required String goalId}) { - track('Task Dragged To Goal', properties: {'task_id': taskId, 'goal_id': goalId}); - } - - void dailyScoreCtaTapped({required String ctaType}) { - track('Daily Score CTA Tapped', properties: {'cta_type': ctaType}); - } - - void dailyScoreHelpTapped() => track('Daily Score Help Tapped'); - - // ============================================================================ - // INTEGRATIONS PAGE TRACKING - // ============================================================================ - - void integrationsPageOpened() => track('Integrations Page Opened'); - - void integrationConnectAttempted({required String integrationName}) { - track('Integration Connect Attempted', properties: {'integration_name': integrationName}); - } - - void integrationConnectSucceeded({required String integrationName}) { - track('Integration Connect Succeeded', properties: {'integration_name': integrationName}); - } - - void integrationConnectFailed({required String integrationName}) { - track('Integration Connect Failed', properties: {'integration_name': integrationName}); - } - - void integrationDisconnected({required String integrationName}) { - track('Integration Disconnected', properties: {'integration_name': integrationName}); - } - - // ============================================================================ - // PAYMENTS PAGE TRACKING - // ============================================================================ - - void paymentsPageOpened() => track('Payments Page Opened'); - - void paymentMethodSelected({required String methodName}) { - track('Payment Method Selected', properties: {'method_name': methodName}); - } - - // ============================================================================ - // OTHER PAGES TRACKING - // ============================================================================ - - void connectDevicePageOpened() => track('Connect Device Page Opened'); - - void connectionGuideOpened() => track('Connection Guide Opened'); - - void connectionGuideDeviceTapped(String deviceId) => - track('Connection Guide Device Tapped', properties: {'device_id': deviceId}); - - void connectionGuideDismissed(String deviceId) => - track('Connection Guide Dismissed', properties: {'device_id': deviceId}); - - void connectionGuideReportIssue(String deviceId) => - track('Connection Guide Report Issue', properties: {'device_id': deviceId}); - - void dataPrivacyPageOpened() => track('Data Privacy Page Opened'); - - void aiAppGeneratorPageOpened() => track('AI App Generator Page Opened'); - - void aiAppGeneratorPromptSubmitted({required int promptLength}) { - track('AI App Generator Prompt Submitted', properties: {'prompt_length': promptLength}); - } - - void aiAppGeneratorAppGenerated({required bool success}) { - track('AI App Generator App Generated', properties: {'success': success}); - } - - void importHistoryPageOpened() => track('Import History Page Opened'); - - void importStarted({required String source}) { - track('Import Started', properties: {'source': source}); - } - - void notificationFrequencyChanged({required int oldFrequency, required int newFrequency}) { - track('Notification Frequency Changed', properties: {'old_frequency': oldFrequency, 'new_frequency': newFrequency}); - } - - static Object? _coerceProperty(dynamic value) { - if (value == null) return null; - if (value is bool || value is num || value is String) return value; - if (value is List) { - return value.map(_coerceProperty).where((e) => e != null).cast().toList(); - } - if (value is Map) { - final out = {}; - value.forEach((k, v) { - final coerced = _coerceProperty(v); - if (coerced != null) out[k.toString()] = coerced; - }); - return out; - } - return value.toString(); - } -} diff --git a/app/lib/utils/platform/platform_manager.dart b/app/lib/utils/platform/platform_manager.dart index eddcbcece95..6d020418348 100644 --- a/app/lib/utils/platform/platform_manager.dart +++ b/app/lib/utils/platform/platform_manager.dart @@ -7,7 +7,7 @@ import 'package:package_info_plus/package_info_plus.dart'; import 'package:omi/backend/preferences.dart'; import 'package:omi/utils/analytics/intercom.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; +import 'package:omi/utils/analytics/analytics_manager.dart'; import 'package:omi/utils/debugging/crash_reporter.dart'; import 'package:omi/utils/debugging/crashlytics_manager.dart'; import 'package:omi/utils/platform/platform_service.dart'; @@ -25,14 +25,14 @@ class PlatformManager { static PlatformManager get instance => _instance; // Service instances - MixpanelManager get mixpanel => MixpanelManager(); + AnalyticsManager get analytics => AnalyticsManager(); IntercomManager get intercom => IntercomManager.instance; CrashReporter get crashReporter => CrashlyticsManager.instance; static Future initializeServices() async { _instance._packageInfo = await PackageInfo.fromPlatform(); _instance._deviceIdHash = await _instance._getDeviceIdHash(); - await MixpanelManager.init(); + await AnalyticsManager.init(); await IntercomManager.instance.initIntercom(); } diff --git a/app/lib/utils/platform/platform_service.dart b/app/lib/utils/platform/platform_service.dart index c2b95d36eac..217ddc178f9 100644 --- a/app/lib/utils/platform/platform_service.dart +++ b/app/lib/utils/platform/platform_service.dart @@ -9,11 +9,9 @@ class PlatformService { static bool get isIOS => Platform.isIOS; static bool get isMobile => isAndroid || isIOS; static bool get isApple => isIOS; - static bool get isAnalyticsSupported => true; + static bool get isAnalyticsSupported => !(kIsWeb); static bool get isNotificationSupported => true; static bool get isIntercomSupported => true; - static bool get isMixpanelSupported => !(kIsWeb); - static bool get isMixpanelNativelySupported => isAndroid || isIOS; static bool get isCrashlyticsSupported => true; /// Execute a function only if the platform supports it diff --git a/app/lib/widgets/bottom_nav_bar.dart b/app/lib/widgets/bottom_nav_bar.dart index ef09bdfc6d9..b43211c95ad 100644 --- a/app/lib/widgets/bottom_nav_bar.dart +++ b/app/lib/widgets/bottom_nav_bar.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -5,7 +6,6 @@ import 'package:font_awesome_flutter/font_awesome_flutter.dart'; import 'package:provider/provider.dart'; import 'package:omi/providers/home_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; class BottomNavBar extends StatelessWidget { const BottomNavBar({super.key, required this.onTabTap}); @@ -49,19 +49,13 @@ class BottomNavBar extends StatelessWidget { child: InkWell( onTap: () { HapticFeedback.mediumImpact(); - MixpanelManager().bottomNavigationTabClicked(label); + PlatformManager.instance.analytics.bottomNavigationTabClicked(label); primaryFocus?.unfocus(); onTabTap(index, home.selectedIndex == index); }, child: SizedBox( height: 90, - child: Center( - child: Icon( - icon, - color: home.selectedIndex == index ? Colors.white : Colors.grey, - size: 26, - ), - ), + child: Center(child: Icon(icon, color: home.selectedIndex == index ? Colors.white : Colors.grey, size: 26)), ), ), ); diff --git a/app/lib/widgets/connection_guide_sheet.dart b/app/lib/widgets/connection_guide_sheet.dart index 69fa6ca3fad..081813f3c12 100644 --- a/app/lib/widgets/connection_guide_sheet.dart +++ b/app/lib/widgets/connection_guide_sheet.dart @@ -1,8 +1,8 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:omi/backend/schema/device_guide.dart'; import 'package:omi/gen/assets.gen.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/responsive/responsive_helper.dart'; import 'package:omi/widgets/device_pairing_sheet.dart'; @@ -87,7 +87,7 @@ class ConnectionGuideSheet extends StatelessWidget { } void _onDeviceTapped(BuildContext context, DeviceGuideProduct product) { - MixpanelManager().connectionGuideDeviceTapped(product.id); + PlatformManager.instance.analytics.connectionGuideDeviceTapped(product.id); showModalBottomSheet( context: context, backgroundColor: Colors.transparent, @@ -95,7 +95,7 @@ class ConnectionGuideSheet extends StatelessWidget { builder: (sheetContext) => DevicePairingSheet( product: product, onDismissAll: () { - MixpanelManager().connectionGuideDismissed(product.id); + PlatformManager.instance.analytics.connectionGuideDismissed(product.id); Navigator.of(sheetContext).pop(); Navigator.of(context).pop(); }, diff --git a/app/lib/widgets/conversation_bottom_bar.dart b/app/lib/widgets/conversation_bottom_bar.dart index 723cf1e8ad6..9087e5088fb 100644 --- a/app/lib/widgets/conversation_bottom_bar.dart +++ b/app/lib/widgets/conversation_bottom_bar.dart @@ -1,3 +1,4 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -14,7 +15,6 @@ import 'package:omi/backend/schema/conversation.dart'; import 'package:omi/gen/assets.gen.dart'; import 'package:omi/pages/conversation_detail/conversation_detail_provider.dart'; import 'package:omi/pages/conversation_detail/widgets/summarized_apps_sheet.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/logger.dart'; enum ConversationBottomBarMode { @@ -185,7 +185,7 @@ class _ConversationBottomBarState extends State { // Track transcript segment tap final conversationId = widget.conversation?.id ?? ''; - MixpanelManager().transcriptSegmentTapped( + PlatformManager.instance.analytics.transcriptSegmentTapped( conversationId: conversationId, segmentStartSeconds: segmentStartSeconds, seekPositionSeconds: filePosition, @@ -195,7 +195,7 @@ class _ConversationBottomBarState extends State { // Auto-play after seeking to segment if (_audioPlayer != null && !_audioPlayer!.playing) { - MixpanelManager().audioPlaybackStarted( + PlatformManager.instance.analytics.audioPlaybackStarted( conversationId: conversationId, durationSeconds: _totalDuration.inSeconds > 0 ? _totalDuration.inSeconds : null, ); @@ -274,7 +274,7 @@ class _ConversationBottomBarState extends State { final currentIndex = _audioPlayer!.currentIndex ?? 0; final combinedPosition = _getCombinedPosition(currentIndex, position); - MixpanelManager().audioPlaybackPaused( + PlatformManager.instance.analytics.audioPlaybackPaused( conversationId: conversationId, positionSeconds: combinedPosition.inSeconds, durationSeconds: _totalDuration.inSeconds > 0 ? _totalDuration.inSeconds : null, @@ -283,7 +283,7 @@ class _ConversationBottomBarState extends State { await _audioPlayer!.pause(); } else { // Track play - MixpanelManager().audioPlaybackStarted( + PlatformManager.instance.analytics.audioPlaybackStarted( conversationId: conversationId, durationSeconds: _totalDuration.inSeconds > 0 ? _totalDuration.inSeconds : null, ); @@ -727,7 +727,10 @@ class _ConversationBottomBarState extends State { // Track seek final conversationId = widget.conversation?.id ?? ''; - MixpanelManager().audioPlaybackSeeked(conversationId: conversationId, toPositionSeconds: targetPosition.inSeconds); + PlatformManager.instance.analytics.audioPlaybackSeeked( + conversationId: conversationId, + toPositionSeconds: targetPosition.inSeconds, + ); await _audioPlayer!.seek(positionInTrack, index: targetIndex); } diff --git a/app/lib/widgets/device_pairing_sheet.dart b/app/lib/widgets/device_pairing_sheet.dart index c51530457a0..54bc84d14d1 100644 --- a/app/lib/widgets/device_pairing_sheet.dart +++ b/app/lib/widgets/device_pairing_sheet.dart @@ -1,8 +1,8 @@ +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:omi/backend/schema/device_guide.dart'; import 'package:omi/utils/analytics/intercom.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/responsive/responsive_helper.dart'; @@ -92,7 +92,7 @@ class DevicePairingSheet extends StatelessWidget { // Report an issue GestureDetector( onTap: () async { - MixpanelManager().connectionGuideReportIssue(product.id); + PlatformManager.instance.analytics.connectionGuideReportIssue(product.id); onDismissAll(); await IntercomManager.instance.intercom.displayMessenger(); }, diff --git a/app/lib/widgets/transcript.dart b/app/lib/widgets/transcript.dart index 5779d06847b..cea056f70da 100644 --- a/app/lib/widgets/transcript.dart +++ b/app/lib/widgets/transcript.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'dart:math'; +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -10,7 +11,6 @@ import 'package:omi/backend/schema/person.dart'; import 'package:omi/backend/schema/transcript_segment.dart'; import 'package:omi/gen/assets.gen.dart'; import 'package:omi/models/stt_provider.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/constants.dart'; import 'package:omi/utils/l10n_extensions.dart'; import 'package:omi/utils/other/temp.dart'; @@ -111,9 +111,8 @@ class _TranscriptWidgetState extends State { return Image.asset(Assets.images.speaker0Icon.path, width: 24, height: 24); } // Always modulo by speakerImagePath.length to prevent index out of bounds - final imageIndex = person != null - ? person.colorIdx! % speakerImagePath.length - : speakerId % speakerImagePath.length; + final imageIndex = + person != null ? person.colorIdx! % speakerImagePath.length : speakerId % speakerImagePath.length; return Image.asset(speakerImagePath[imageIndex], width: 24, height: 24); } @@ -322,13 +321,13 @@ class _TranscriptWidgetState extends State { _isAutoScrolling = true; _scrollController .animateTo( - targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent), - duration: const Duration(milliseconds: 400), - curve: Curves.easeInOutCubic, - ) + targetOffset.clamp(0.0, _scrollController.position.maxScrollExtent), + duration: const Duration(milliseconds: 400), + curve: Curves.easeInOutCubic, + ) .then((_) { - _isAutoScrolling = false; - }); + _isAutoScrolling = false; + }); } } @@ -470,7 +469,7 @@ class _TranscriptWidgetState extends State { ? null : () { widget.editSegment?.call(data.id, data.speakerId); - MixpanelManager().tagSheetOpened(); + PlatformManager.instance.analytics.tagSheetOpened(); }, child: Column( children: [ @@ -502,15 +501,15 @@ class _TranscriptWidgetState extends State { ? null : () { widget.editSegment?.call(data.id, data.speakerId); - MixpanelManager().tagSheetOpened(); + PlatformManager.instance.analytics.tagSheetOpened(); }, child: Text( data.speakerId == omiSpeakerId ? 'omi' : (person?.name ?? - context.l10n.speakerWithId( - '${TranscriptSegment.getDisplaySpeakerId(data.speakerId, widget.segments)}', - )), + context.l10n.speakerWithId( + '${TranscriptSegment.getDisplaySpeakerId(data.speakerId, widget.segments)}', + )), style: TextStyle( color: data.speakerId == omiSpeakerId || person != null ? Colors.grey.shade300 @@ -551,8 +550,8 @@ class _TranscriptWidgetState extends State { isUser ? 18 : (segmentIdx > 0 && !widget.segments[segmentIdx - 1].isUser) - ? 6 - : 18, + ? 6 + : 18, ), topRight: Radius.circular(isUser ? 18 : 18), bottomLeft: Radius.circular(18), @@ -615,9 +614,8 @@ class _TranscriptWidgetState extends State { Text( SttProviderConfig.getDisplayName(data.sttProvider), style: TextStyle( - color: isUser - ? Colors.white.withValues(alpha: 0.5) - : Colors.grey.shade500, + color: + isUser ? Colors.white.withValues(alpha: 0.5) : Colors.grey.shade500, fontSize: 10, fontStyle: FontStyle.italic, ), @@ -626,9 +624,8 @@ class _TranscriptWidgetState extends State { Text( ' · ', style: TextStyle( - color: isUser - ? Colors.white.withValues(alpha: 0.5) - : Colors.grey.shade500, + color: + isUser ? Colors.white.withValues(alpha: 0.5) : Colors.grey.shade500, fontSize: 10, ), ), @@ -644,9 +641,8 @@ class _TranscriptWidgetState extends State { child: Icon( Icons.play_circle_outline, size: 16, - color: isUser - ? Colors.white.withValues(alpha: 0.7) - : Colors.grey.shade400, + color: + isUser ? Colors.white.withValues(alpha: 0.7) : Colors.grey.shade400, ), ), const SizedBox(width: 6), @@ -655,9 +651,8 @@ class _TranscriptWidgetState extends State { Text( data.getTimestampString(), style: TextStyle( - color: isUser - ? Colors.white.withValues(alpha: 0.7) - : Colors.grey.shade400, + color: + isUser ? Colors.white.withValues(alpha: 0.7) : Colors.grey.shade400, fontSize: 11, ), ), @@ -682,7 +677,7 @@ class _TranscriptWidgetState extends State { GestureDetector( onTap: () { widget.editSegment?.call(data.id, data.speakerId); - MixpanelManager().tagSheetOpened(); + PlatformManager.instance.analytics.tagSheetOpened(); }, child: Column( children: [ diff --git a/app/lib/widgets/upgrade_alert.dart b/app/lib/widgets/upgrade_alert.dart index 65172cb3788..df241996220 100644 --- a/app/lib/widgets/upgrade_alert.dart +++ b/app/lib/widgets/upgrade_alert.dart @@ -1,11 +1,11 @@ // Copyright (c) 2023 Larry Aasen. All rights reserved. +import 'package:omi/utils/platform/platform_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:upgrader/upgrader.dart'; -import 'package:omi/utils/analytics/mixpanel.dart'; import 'package:omi/utils/l10n_extensions.dart'; class MyUpgrader extends Upgrader { @@ -48,14 +48,14 @@ class MyUpgradeAlertState extends UpgradeAlertState { child: Text(context.l10n.no, style: TextStyle(color: Colors.grey.shade200, fontSize: 16)), onPressed: () { onUserIgnored(context, true); - MixpanelManager().upgradeModalDismissed(); + PlatformManager.instance.analytics.upgradeModalDismissed(); }, ), TextButton( child: Text(context.l10n.upgrade, style: const TextStyle(color: Colors.white, fontSize: 16)), onPressed: () { onUserUpdated(context, !widget.upgrader.blocked()); - MixpanelManager().upgradeModalClicked(); + PlatformManager.instance.analytics.upgradeModalClicked(); }, ), ],