diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 49f162e7..25379580 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -63,6 +63,7 @@
+
diff --git a/lib/background/background.dart b/lib/background/background.dart
index f9f6552e..9156c059 100644
--- a/lib/background/background.dart
+++ b/lib/background/background.dart
@@ -1,4 +1,5 @@
import 'dart:async';
+import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
@@ -8,6 +9,7 @@ import 'package:mostro_mobile/data/repositories/event_storage.dart';
import 'package:mostro_mobile/features/settings/settings.dart';
import 'package:mostro_mobile/features/notifications/services/background_notification_service.dart' as notification_service;
import 'package:mostro_mobile/services/nostr_service.dart';
+import 'package:mostro_mobile/services/logger_service.dart' as logger_service;
import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart';
bool isAppForeground = true;
@@ -15,6 +17,8 @@ String currentLanguage = 'en';
@pragma('vm:entry-point')
Future serviceMain(ServiceInstance service) async {
+ SendPort? loggerSendPort;
+ late Logger logger;
final Map> activeSubscriptions = {};
final nostrService = NostrService();
@@ -31,6 +35,15 @@ Future serviceMain(ServiceInstance service) async {
final settingsMap = data['settings'];
if (settingsMap == null) return;
+ loggerSendPort = data['loggerSendPort'] as SendPort?;
+
+ // Create logger that forwards to main thread
+ logger = Logger(
+ printer: logger_service.SimplePrinter(),
+ output: logger_service.IsolateLogOutput(loggerSendPort),
+ level: Level.debug,
+ );
+
final settings = Settings.fromJson(settingsMap);
currentLanguage = settings.selectedLanguage ?? PlatformDispatcher.instance.locale.languageCode;
await nostrService.init(settings);
@@ -74,7 +87,7 @@ Future serviceMain(ServiceInstance service) async {
}
await notification_service.retryNotification(event);
} catch (e) {
- Logger().e('Error processing event', error: e);
+ logger.e('Error processing event', error: e);
}
});
});
diff --git a/lib/background/desktop_background_service.dart b/lib/background/desktop_background_service.dart
index 6e5535aa..ca530fee 100644
--- a/lib/background/desktop_background_service.dart
+++ b/lib/background/desktop_background_service.dart
@@ -6,6 +6,7 @@ import 'package:logger/logger.dart';
import 'package:mostro_mobile/data/models/nostr_filter.dart';
import 'package:mostro_mobile/features/settings/settings.dart';
import 'package:mostro_mobile/services/nostr_service.dart';
+import 'package:mostro_mobile/services/logger_service.dart' as logger_service;
import 'abstract_background_service.dart';
class DesktopBackgroundService implements BackgroundService {
@@ -22,13 +23,20 @@ class DesktopBackgroundService implements BackgroundService {
final isolateReceivePort = ReceivePort();
final mainSendPort = args[0] as SendPort;
final token = args[1] as RootIsolateToken;
+ final loggerSendPort = args.length > 2 ? args[2] as SendPort? : null; // Optional logger SendPort
mainSendPort.send(isolateReceivePort.sendPort);
BackgroundIsolateBinaryMessenger.ensureInitialized(token);
+ final logger = Logger(
+ printer: logger_service.SimplePrinter(),
+ output: logger_service.IsolateLogOutput(loggerSendPort),
+ level: Level.debug,
+ );
+
final nostrService = NostrService();
- final logger = Logger();
+
bool isAppForeground = true;
isolateReceivePort.listen((message) async {
diff --git a/lib/background/mobile_background_service.dart b/lib/background/mobile_background_service.dart
index f42c1660..4d237b79 100644
--- a/lib/background/mobile_background_service.dart
+++ b/lib/background/mobile_background_service.dart
@@ -2,7 +2,7 @@ import 'dart:async';
import 'package:dart_nostr/nostr/model/request/filter.dart';
import 'package:flutter_background_service/flutter_background_service.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart' as logger_service;
import 'package:mostro_mobile/background/background.dart';
import 'package:mostro_mobile/features/settings/settings.dart';
import 'abstract_background_service.dart';
@@ -16,7 +16,7 @@ class MobileBackgroundService implements BackgroundService {
final _subscriptions = >{};
bool _isRunning = false;
- final _logger = Logger();
+
bool _serviceReady = false;
final List _pendingOperations = [];
@@ -45,18 +45,18 @@ class MobileBackgroundService implements BackgroundService {
service.invoke('start', {
'settings': _settings.toJson(),
});
- _logger.d(
+ logger_service.logger.d(
'Service started with settings: ${_settings.toJson()}',
);
});
service.on('on-stop').listen((event) {
_isRunning = false;
- _logger.i('Service stopped');
+ logger_service.logger.i('Service stopped');
});
service.on('service-ready').listen((data) {
- _logger.i("Service confirmed it's ready");
+ logger_service.logger.i("Service confirmed it's ready");
_serviceReady = true;
_processPendingOperations();
});
@@ -68,7 +68,7 @@ class MobileBackgroundService implements BackgroundService {
_subscriptions[subId] = {'filters': filters};
_executeWhenReady(() {
- _logger.i("Sending subscription to service");
+ logger_service.logger.i("Sending subscription to service");
service.invoke('create-subscription', {
'id': subId,
'filters': filters.map((f) => f.toMap()).toList(),
@@ -127,7 +127,7 @@ class MobileBackgroundService implements BackgroundService {
try {
await _startService();
} catch (e) {
- _logger.e('Error starting service: $e');
+ logger_service.logger.e('Error starting service: $e');
// Retry with a delay if needed
await Future.delayed(Duration(seconds: 1));
await _startService();
@@ -137,7 +137,7 @@ class MobileBackgroundService implements BackgroundService {
}
Future _startService() async {
- _logger.i("Starting service");
+ logger_service.logger.i("Starting service");
await service.startService();
_serviceReady = false; // Reset ready state when starting
@@ -152,9 +152,10 @@ class MobileBackgroundService implements BackgroundService {
await Future.delayed(const Duration(milliseconds: 50));
}
- _logger.i("Service running, sending settings");
+ logger_service.logger.i("Service running, sending settings");
service.invoke('start', {
'settings': _settings.toJson(),
+ 'loggerSendPort': logger_service.isolateLogSenderPort,
});
}
diff --git a/lib/core/app_routes.dart b/lib/core/app_routes.dart
index d9511d02..a41007f1 100644
--- a/lib/core/app_routes.dart
+++ b/lib/core/app_routes.dart
@@ -24,11 +24,12 @@ import 'package:mostro_mobile/features/walkthrough/screens/walkthrough_screen.da
import 'package:mostro_mobile/features/disputes/screens/dispute_chat_screen.dart';
import 'package:mostro_mobile/features/notifications/screens/notifications_screen.dart';
+import 'package:mostro_mobile/features/logs/screens/logs_screen.dart';
import 'package:mostro_mobile/features/walkthrough/providers/first_run_provider.dart';
import 'package:mostro_mobile/shared/widgets/navigation_listener_widget.dart';
import 'package:mostro_mobile/shared/widgets/notification_listener_widget.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/generated/l10n.dart';
GoRouter createRouter(WidgetRef ref) {
@@ -59,7 +60,7 @@ GoRouter createRouter(WidgetRef ref) {
);
},
errorBuilder: (context, state) {
- final logger = Logger();
+
logger.w('GoRouter error: ${state.error}');
// For errors, show a generic error page
@@ -284,6 +285,15 @@ GoRouter createRouter(WidgetRef ref) {
child: const NotificationsScreen(),
),
),
+ GoRoute(
+ path: '/logs',
+ pageBuilder: (context, state) =>
+ buildPageWithDefaultTransition(
+ context: context,
+ state: state,
+ child: const LogsScreen(),
+ ),
+ ),
],
),
],
diff --git a/lib/core/config.dart b/lib/core/config.dart
index b914793d..8839bf81 100644
--- a/lib/core/config.dart
+++ b/lib/core/config.dart
@@ -42,4 +42,9 @@ class Config {
// Notification configuration
static String notificationChannelId = 'mostro_mobile';
static int notificationId = 38383;
+
+ // Logger configuration
+ static const int logMaxEntries = 1000;
+ static const int logBatchDeleteSize = 100;
+ static bool fullLogsInfo = true; // false = simple logs in console, true = full Logger format in console
}
diff --git a/lib/core/deep_link_handler.dart b/lib/core/deep_link_handler.dart
index c5ba5afd..b85d3039 100644
--- a/lib/core/deep_link_handler.dart
+++ b/lib/core/deep_link_handler.dart
@@ -2,21 +2,20 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/generated/l10n.dart';
import 'package:mostro_mobile/services/deep_link_service.dart';
import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart';
class DeepLinkHandler {
final Ref _ref;
- final Logger _logger = Logger();
StreamSubscription? _subscription;
DeepLinkHandler(this._ref);
/// Initializes deep link handling for the app
void initialize(GoRouter router) {
- _logger.i('Initializing DeepLinkHandler');
+ logger.i('Initializing DeepLinkHandler');
// Get the deep link service instance
final deepLinkService = _ref.read(deepLinkServiceProvider);
@@ -27,7 +26,7 @@ class DeepLinkHandler {
// Listen for deep link events
_subscription = deepLinkService.deepLinkStream.listen(
(Uri uri) => _handleDeepLink(uri, router),
- onError: (error) => _logger.e('Deep link stream error: $error'),
+ onError: (error) => logger.e('Deep link stream error: $error'),
);
}
@@ -42,20 +41,20 @@ class DeepLinkHandler {
GoRouter router,
) async {
try {
- _logger.i('Handling deep link: $uri');
+ logger.i('Handling deep link: $uri');
// Check if it's a mostro: scheme
if (uri.scheme == 'mostro') {
await _handleMostroDeepLink(uri.toString(), router);
} else {
- _logger.w('Unsupported deep link scheme: ${uri.scheme}');
+ logger.w('Unsupported deep link scheme: ${uri.scheme}');
final context = router.routerDelegate.navigatorKey.currentContext;
if (context != null && context.mounted) {
_showErrorSnackBar(context, S.of(context)!.unsupportedLinkFormat);
}
}
} catch (e) {
- _logger.e('Error handling deep link: $e');
+ logger.e('Error handling deep link: $e');
final context = router.routerDelegate.navigatorKey.currentContext;
if (context != null && context.mounted) {
_showErrorSnackBar(context, S.of(context)!.failedToOpenLink);
@@ -83,7 +82,7 @@ class DeepLinkHandler {
// Ensure we have a valid context for processing
final processingContext = context ?? router.routerDelegate.navigatorKey.currentContext;
if (processingContext == null || !processingContext.mounted) {
- _logger.e('No valid context available for deep link processing');
+ logger.e('No valid context available for deep link processing');
return;
}
@@ -103,17 +102,17 @@ class DeepLinkHandler {
WidgetsBinding.instance.addPostFrameCallback((_) {
deepLinkService.navigateToOrder(router, result.orderInfo!);
});
- _logger.i('Successfully navigated to order: ${result.orderInfo!.orderId} (${result.orderInfo!.orderType.value})');
+ logger.i('Successfully navigated to order: ${result.orderInfo!.orderId} (${result.orderInfo!.orderType.value})');
} else {
final errorContext = router.routerDelegate.navigatorKey.currentContext;
if (errorContext != null && errorContext.mounted) {
final errorMessage = result.error ?? S.of(errorContext)!.failedToLoadOrder;
_showErrorSnackBar(errorContext, errorMessage);
}
- _logger.w('Failed to process mostro link: ${result.error}');
+ logger.w('Failed to process mostro link: ${result.error}');
}
} catch (e) {
- _logger.e('Error processing mostro deep link: $e');
+ logger.e('Error processing mostro deep link: $e');
final errorContext = router.routerDelegate.navigatorKey.currentContext;
if (errorContext != null && errorContext.mounted) {
Navigator.of(errorContext).pop(); // Hide loading if still showing
diff --git a/lib/core/deep_link_interceptor.dart b/lib/core/deep_link_interceptor.dart
index edc209dc..f81fe74b 100644
--- a/lib/core/deep_link_interceptor.dart
+++ b/lib/core/deep_link_interceptor.dart
@@ -1,12 +1,11 @@
import 'dart:async';
import 'package:flutter/widgets.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
/// A deep link interceptor that prevents custom schemes from reaching GoRouter
/// This prevents assertion failures when the system tries to parse mostro: URLs
class DeepLinkInterceptor extends WidgetsBindingObserver {
- final Logger _logger = Logger();
- final StreamController _customUrlController =
+ final StreamController _customUrlController =
StreamController.broadcast();
/// Stream for custom URLs that were intercepted
@@ -15,17 +14,17 @@ class DeepLinkInterceptor extends WidgetsBindingObserver {
/// Initialize the interceptor
void initialize() {
WidgetsBinding.instance.addObserver(this);
- _logger.i('DeepLinkInterceptor initialized');
+ logger.i('DeepLinkInterceptor initialized');
}
@override
Future didPushRouteInformation(RouteInformation routeInformation) async {
final uri = routeInformation.uri;
- _logger.i('DeepLinkInterceptor: Route information received: $uri');
+ logger.i('DeepLinkInterceptor: Route information received: $uri');
// Check if this is a custom scheme URL
if (_isCustomScheme(uri)) {
- _logger.i('DeepLinkInterceptor: Custom scheme detected: ${uri.scheme}, intercepting and preventing GoRouter processing');
+ logger.i('DeepLinkInterceptor: Custom scheme detected: ${uri.scheme}, intercepting and preventing GoRouter processing');
// Emit the custom URL for processing
_customUrlController.add(uri.toString());
@@ -35,7 +34,7 @@ class DeepLinkInterceptor extends WidgetsBindingObserver {
return true;
}
- _logger.i('DeepLinkInterceptor: Allowing normal URL to pass through: $uri');
+ logger.i('DeepLinkInterceptor: Allowing normal URL to pass through: $uri');
// Let normal URLs pass through to GoRouter
return super.didPushRouteInformation(routeInformation);
}
@@ -45,17 +44,17 @@ class DeepLinkInterceptor extends WidgetsBindingObserver {
@override
// ignore: deprecated_member_use
Future didPushRoute(String route) async {
- _logger.i('DeepLinkInterceptor: didPushRoute called with: $route');
+ logger.i('DeepLinkInterceptor: didPushRoute called with: $route');
try {
final uri = Uri.parse(route);
if (_isCustomScheme(uri)) {
- _logger.i('DeepLinkInterceptor: Custom scheme detected in didPushRoute: ${uri.scheme}, intercepting');
+ logger.i('DeepLinkInterceptor: Custom scheme detected in didPushRoute: ${uri.scheme}, intercepting');
_customUrlController.add(route);
return true;
}
} catch (e) {
- _logger.w('DeepLinkInterceptor: Error parsing route in didPushRoute: $e');
+ logger.w('DeepLinkInterceptor: Error parsing route in didPushRoute: $e');
}
// ignore: deprecated_member_use
@@ -72,6 +71,6 @@ class DeepLinkInterceptor extends WidgetsBindingObserver {
void dispose() {
WidgetsBinding.instance.removeObserver(this);
_customUrlController.close();
- _logger.i('DeepLinkInterceptor disposed');
+ logger.i('DeepLinkInterceptor disposed');
}
}
\ No newline at end of file
diff --git a/lib/data/repositories/dispute_repository.dart b/lib/data/repositories/dispute_repository.dart
index de205fe2..941eb652 100644
--- a/lib/data/repositories/dispute_repository.dart
+++ b/lib/data/repositories/dispute_repository.dart
@@ -1,6 +1,6 @@
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/data/models/dispute.dart';
import 'package:mostro_mobile/data/models/mostro_message.dart';
import 'package:mostro_mobile/data/models/enums/action.dart';
@@ -13,14 +13,13 @@ class DisputeRepository {
final NostrService _nostrService;
final String _mostroPubkey;
final Ref _ref;
- final Logger _logger = Logger();
DisputeRepository(this._nostrService, this._mostroPubkey, this._ref);
/// Create a new dispute for an order
Future createDispute(String orderId) async {
try {
- _logger.d('Creating dispute for order: $orderId');
+ logger.d('Repository: creating dispute for order $orderId');
// Get user's session for the order to get the trade key
final sessions = _ref.read(sessionNotifierProvider);
@@ -29,14 +28,14 @@ class DisputeRepository {
);
if (session == null) {
- _logger
- .e('No session found for order: $orderId, cannot create dispute');
+ logger
+ .e('Repository: no session found for order $orderId, cannot create dispute');
return false;
}
// Validate trade key is present
if (session.tradeKey.private.isEmpty) {
- _logger.e('Trade key is empty for order: $orderId, cannot create dispute');
+ logger.e('Repository: trade key is empty for order $orderId, cannot create dispute');
return false;
}
@@ -55,17 +54,17 @@ class DisputeRepository {
// Send the wrapped event to Mostro
await _nostrService.publishEvent(event);
- _logger.d('Successfully sent dispute creation for order: $orderId');
+ logger.d('Repository: successfully sent dispute creation for order $orderId');
return true;
} catch (e) {
- _logger.e('Failed to create dispute: $e');
+ logger.e('Repository: create dispute failed - $e');
return false;
}
}
Future> getUserDisputes() async {
try {
- _logger.d('Getting user disputes from sessions');
+ logger.d('Repository: getting user disputes from sessions');
// Get all user sessions and check their order states for disputes
final sessions = _ref.read(sessionNotifierProvider);
@@ -81,38 +80,38 @@ class DisputeRepository {
disputes.add(orderState.dispute!);
}
} catch (e) {
- _logger.w('Failed to get order state for order ${session.orderId}: $e');
+ logger.w('Repository: failed to get order state for order ${session.orderId} - $e');
}
}
}
- _logger.d('Found ${disputes.length} disputes from sessions');
+ logger.d('Repository: found ${disputes.length} disputes from sessions');
return disputes;
} catch (e) {
- _logger.e('Failed to get user disputes: $e');
+ logger.e('Repository: get user disputes failed - $e');
return [];
}
}
Future getDispute(String disputeId) async {
try {
- _logger.d('Getting dispute by ID: $disputeId');
-
+ logger.d('Repository: getting dispute by ID $disputeId');
+
// Get all user disputes and find the one with matching ID
final disputes = await getUserDisputes();
final dispute = disputes.firstWhereOrNull(
(d) => d.disputeId == disputeId,
);
-
+
if (dispute != null) {
- _logger.d('Found dispute with ID: $disputeId');
+ logger.d('Repository: found dispute with ID $disputeId');
} else {
- _logger.w('No dispute found with ID: $disputeId');
+ logger.w('Repository: no dispute found with ID $disputeId');
}
-
+
return dispute;
} catch (e) {
- _logger.e('Failed to get dispute by ID $disputeId: $e');
+ logger.e('Repository: get dispute by ID failed for $disputeId - $e');
return null;
}
}
diff --git a/lib/data/repositories/mostro_storage.dart b/lib/data/repositories/mostro_storage.dart
index 12b2b46a..8ebd2f2f 100644
--- a/lib/data/repositories/mostro_storage.dart
+++ b/lib/data/repositories/mostro_storage.dart
@@ -1,11 +1,10 @@
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/data/models/payload.dart';
import 'package:sembast/sembast.dart';
import 'package:mostro_mobile/data/models/mostro_message.dart';
import 'package:mostro_mobile/data/repositories/base_storage.dart';
class MostroStorage extends BaseStorage {
- final Logger _logger = Logger();
MostroStorage({required Database db})
: super(db, stringMapStoreFactory.store('orders'));
@@ -21,12 +20,12 @@ class MostroStorage extends BaseStorage {
dbMap['timestamp'] = message.timestamp;
await store.record(id).put(db, dbMap);
- _logger.i(
- 'Saved message of type ${message.action} with order id ${message.id}',
+ logger.i(
+ 'Storage: saved message of type ${message.action} with order id ${message.id}',
);
} catch (e, stack) {
- _logger.e(
- 'addMessage failed for $id',
+ logger.e(
+ 'Storage: add message failed for $id - $e',
error: e,
stackTrace: stack,
);
@@ -39,7 +38,7 @@ class MostroStorage extends BaseStorage {
try {
return await getAll();
} catch (e, stack) {
- _logger.e('getAllMessages failed', error: e, stackTrace: stack);
+ logger.e('Storage: get all messages failed - $e', error: e, stackTrace: stack);
return [];
}
}
@@ -48,9 +47,9 @@ class MostroStorage extends BaseStorage {
Future deleteAllMessages() async {
try {
await deleteAll();
- _logger.i('All messages deleted');
+ logger.i('Storage: all messages deleted');
} catch (e, stack) {
- _logger.e('deleteAllMessages failed', error: e, stackTrace: stack);
+ logger.e('Storage: delete all messages failed - $e', error: e, stackTrace: stack);
rethrow;
}
}
diff --git a/lib/data/repositories/open_orders_repository.dart b/lib/data/repositories/open_orders_repository.dart
index ea0be7da..c03dbfd2 100644
--- a/lib/data/repositories/open_orders_repository.dart
+++ b/lib/data/repositories/open_orders_repository.dart
@@ -2,10 +2,10 @@ import 'dart:async';
import 'package:dart_nostr/nostr/model/event/event.dart';
import 'package:dart_nostr/nostr/model/request/filter.dart';
import 'package:dart_nostr/nostr/model/request/request.dart';
-import 'package:logger/logger.dart';
import 'package:mostro_mobile/data/models/nostr_event.dart';
import 'package:mostro_mobile/data/repositories/order_repository_interface.dart';
import 'package:mostro_mobile/features/settings/settings.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/services/nostr_service.dart';
const orderEventKind = 38383;
@@ -19,7 +19,6 @@ class OpenOrdersRepository implements OrderRepository {
final StreamController> _eventStreamController =
StreamController.broadcast();
final Map _events = {};
- final _logger = Logger();
StreamSubscription? _subscription;
NostrEvent? get mostroInstance => _mostroInstance;
@@ -54,11 +53,11 @@ class OpenOrdersRepository implements OrderRepository {
_eventStreamController.add(_events.values.toList());
} else if (event.type == 'info' &&
event.pubkey == _settings.mostroPublicKey) {
- _logger.i('Mostro instance info loaded: $event');
+ logger.i('Repository: mostro instance info loaded - $event');
_mostroInstance = event;
}
}, onError: (error) {
- _logger.e('Error in order subscription: $error');
+ logger.e('Repository: order subscription failed - $error');
// Optionally, you could auto-resubscribe here if desired
});
@@ -123,7 +122,7 @@ class OpenOrdersRepository implements OrderRepository {
void updateSettings(Settings settings) {
if (_settings.mostroPublicKey != settings.mostroPublicKey) {
- _logger.i('Mostro instance changed, updating...');
+ logger.i('Repository: mostro instance changed, updating');
_settings = settings.copyWith();
_events.clear();
_subscribeToOrders();
@@ -133,14 +132,14 @@ class OpenOrdersRepository implements OrderRepository {
}
void reloadData() {
- _logger.i('Reloading repository data');
+ logger.i('Repository: reloading data');
_subscribeToOrders();
_emitEvents();
}
/// Clear in-memory order cache and reload from relays (used during account restore)
void clearCache() {
- _logger.i('Clearing order cache and reloading');
+ logger.i('Repository: clearing order cache and reloading');
_events.clear();
_subscribeToOrders(); // Resubscribe to reload orders from relays
}
diff --git a/lib/features/chat/chat_room_provider.dart b/lib/features/chat/chat_room_provider.dart
index 7547cc06..ebeb1c04 100644
--- a/lib/features/chat/chat_room_provider.dart
+++ b/lib/features/chat/chat_room_provider.dart
@@ -1,5 +1,5 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/data/models/chat_room.dart';
import 'package:mostro_mobile/features/chat/notifiers/chat_room_notifier.dart';
@@ -15,19 +15,16 @@ final chatRoomsProvider =
chatId,
ref,
);
-
+
// Initialize the notifier with proper error handling and safety checks
_initializeChatRoomSafely(ref, notifier, chatId);
-
+
return notifier;
});
// Provider to track initialization status of chat rooms
final chatRoomInitializedProvider = StateProvider.family((ref, chatId) => false);
-// Logger instance for the chat room provider
-final _logger = Logger();
-
/// Safely initialize a chat room with proper error handling and context safety
Future _initializeChatRoomSafely(
Ref ref,
@@ -43,18 +40,18 @@ Future _initializeChatRoomSafely(
if (ref.container.read(chatRoomsProvider(chatId).notifier).mounted) {
// Mark as initialized only if the provider is still active
ref.read(chatRoomInitializedProvider(chatId).notifier).state = true;
- _logger.d('Chat room $chatId initialized successfully');
+ logger.d('Chat room $chatId initialized successfully');
} else {
- _logger.w('Chat room $chatId provider was disposed during initialization');
+ logger.w('Chat room $chatId provider was disposed during initialization');
}
} catch (e, stackTrace) {
// Use proper logging instead of print
- _logger.e(
+ logger.e(
'Error initializing chat room $chatId: $e',
error: e,
stackTrace: stackTrace,
);
-
+
// Only update error state if provider is still mounted
if (ref.container.read(chatRoomsProvider(chatId).notifier).mounted) {
// Keep initialization status as false on error
diff --git a/lib/features/chat/notifiers/chat_room_notifier.dart b/lib/features/chat/notifiers/chat_room_notifier.dart
index 0b30243e..c0e40ea9 100644
--- a/lib/features/chat/notifiers/chat_room_notifier.dart
+++ b/lib/features/chat/notifiers/chat_room_notifier.dart
@@ -4,7 +4,7 @@ import 'dart:typed_data';
import 'package:dart_nostr/dart_nostr.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/data/models/chat_room.dart';
import 'package:mostro_mobile/data/models/nostr_event.dart';
import 'package:mostro_mobile/data/models/session.dart';
@@ -26,8 +26,6 @@ class ChatRoomNotifier extends StateNotifier {
_subscription?.cancel();
subscribe();
}
-
- final _logger = Logger();
final String orderId;
final Ref ref;
StreamSubscription? _subscription;
@@ -76,12 +74,12 @@ class ChatRoomNotifier extends StateNotifier {
// Cancel any existing listener
_sessionListener?.close();
- _logger.i('Starting to listen for session changes for orderId: $orderId');
+ logger.i('Starting to listen for session changes for orderId: $orderId');
_sessionListener = ref.listen(
sessionProvider(orderId),
(previous, next) {
- _logger.i(
+ logger.i(
'Session update received for orderId: $orderId, session is null: ${next == null}, sharedKey is null: ${next?.sharedKey == null}');
if (next != null && next.sharedKey != null) {
@@ -89,7 +87,7 @@ class ChatRoomNotifier extends StateNotifier {
_sessionListener?.close();
_sessionListener = null;
- _logger.i(
+ logger.i(
'Session with shared key is now available, subscribing to chat for orderId: $orderId');
// Use SubscriptionManager to create a subscription for this specific chat room
@@ -103,7 +101,7 @@ class ChatRoomNotifier extends StateNotifier {
void _onChatEvent(NostrEvent event) async {
try {
if (event.kind != 1059) {
- _logger.w('Ignoring non-chat event kind: ${event.kind}');
+ logger.w('Ignoring non-chat event kind: ${event.kind}');
return;
}
@@ -131,7 +129,7 @@ class ChatRoomNotifier extends StateNotifier {
final session = ref.read(sessionProvider(orderId));
if (session == null || session.sharedKey == null) {
- _logger.e('Session or shared key is null when processing chat event');
+ logger.e('Session or shared key is null when processing chat event');
return;
}
@@ -144,7 +142,7 @@ class ChatRoomNotifier extends StateNotifier {
if (pTag.isEmpty ||
pTag.length < 2 ||
pTag[1] != session.sharedKey!.public) {
- _logger.w('Event not addressed to our shared key, ignoring');
+ logger.w('Event not addressed to our shared key, ignoring');
return;
}
@@ -160,30 +158,30 @@ class ChatRoomNotifier extends StateNotifier {
final updatedMessages = [...state.messages, chat];
updatedMessages.sort((a, b) => b.createdAt!.compareTo(a.createdAt!));
state = state.copy(messages: updatedMessages);
- _logger.d('New message added from relay, total messages: ${updatedMessages.length}');
+ logger.d('New message added from relay, total messages: ${updatedMessages.length}');
} else {
- _logger.d('Message already exists in state, skipping duplicate');
+ logger.d('Message already exists in state, skipping duplicate');
}
// Notify the chat rooms list to update when new messages arrive
try {
ref.read(chatRoomsNotifierProvider.notifier).refreshChatList();
} catch (e) {
- _logger.w('Could not refresh chat list: $e');
+ logger.w('Could not refresh chat list: $e');
}
} catch (e, stackTrace) {
- _logger.e('Error processing chat event: $e', stackTrace: stackTrace);
+ logger.e('Error processing chat event: $e', stackTrace: stackTrace);
}
}
Future sendMessage(String text) async {
final session = ref.read(sessionProvider(orderId));
if (session == null) {
- _logger.w('Cannot send message: Session is null for orderId: $orderId');
+ logger.w('Cannot send message: Session is null for orderId: $orderId');
return;
}
if (session.sharedKey == null) {
- _logger
+ logger
.w('Cannot send message: Shared key is null for orderId: $orderId');
return;
}
@@ -206,8 +204,8 @@ class ChatRoomNotifier extends StateNotifier {
// Publish to network first - await to catch network/initialization errors
try {
await ref.read(nostrServiceProvider).publishEvent(wrappedEvent);
- _logger.d('Message sent successfully to network');
-
+ logger.d('Message sent successfully to network');
+
// Add the inner event to state immediately for optimistic UI
// The relay will echo it back and _onChatEvent will handle deduplication
final messageExists = state.messages.any((m) => m.id == innerEvent.id);
@@ -215,13 +213,13 @@ class ChatRoomNotifier extends StateNotifier {
final updatedMessages = [...state.messages, innerEvent];
updatedMessages.sort((a, b) => b.createdAt!.compareTo(a.createdAt!));
state = state.copy(messages: updatedMessages);
- _logger.d('Message added to state optimistically, total messages: ${updatedMessages.length}');
+ logger.d('Message added to state optimistically, total messages: ${updatedMessages.length}');
} else {
- _logger.d('Message already exists in state, skipping add');
+ logger.d('Message already exists in state, skipping add');
}
-
+
} catch (publishError, publishStack) {
- _logger.e('Failed to publish message: $publishError', stackTrace: publishStack);
+ logger.e('Failed to publish message: $publishError', stackTrace: publishStack);
rethrow; // Re-throw to be caught by outer catch
}
@@ -229,31 +227,31 @@ class ChatRoomNotifier extends StateNotifier {
try {
ref.read(chatRoomsNotifierProvider.notifier).refreshChatList();
} catch (e) {
- _logger.w('Could not refresh chat list after sending message: $e');
+ logger.w('Could not refresh chat list after sending message: $e');
}
} catch (e, stackTrace) {
- _logger.e('Failed to send message: $e', stackTrace: stackTrace);
+ logger.e('Failed to send message: $e', stackTrace: stackTrace);
}
}
/// Load historical chat messages from storage
Future _loadHistoricalMessages() async {
try {
- _logger.i('Starting to load historical messages for orderId: $orderId');
+ logger.i('Starting to load historical messages for orderId: $orderId');
final session = ref.read(sessionProvider(orderId));
if (session == null) {
- _logger.w(
+ logger.w(
'Cannot load historical messages: session is null for orderId: $orderId');
return;
}
if (session.sharedKey == null) {
- _logger.w(
+ logger.w(
'Cannot load historical messages: shared key is null for orderId: $orderId');
return;
}
- _logger.i('Session found with shared key: ${session.sharedKey?.public}');
+ logger.i('Session found with shared key: ${session.sharedKey?.public}');
final eventStore = ref.read(eventStorageProvider);
@@ -261,7 +259,7 @@ class ChatRoomNotifier extends StateNotifier {
final allChatEvents = await eventStore.find(
filter: eventStore.eq('type', 'chat'),
);
- _logger.i('Total chat events in storage: ${allChatEvents.length}');
+ logger.i('Total chat events in storage: ${allChatEvents.length}');
// Find all chat events for this specific order
var chatEvents = await eventStore.find(
@@ -272,22 +270,22 @@ class ChatRoomNotifier extends StateNotifier {
sort: [SortOrder('created_at', false)], // Most recent first
);
- _logger.i('Chat events found for orderId $orderId: ${chatEvents.length}');
+ logger.i('Chat events found for orderId $orderId: ${chatEvents.length}');
// Fallback: if no events found with order_id, try to find all chat events
// This handles events stored before the order_id field was added
if (chatEvents.isEmpty) {
- _logger.i(
+ logger.i(
'No events found with order_id, trying fallback to all chat events');
chatEvents = await eventStore.find(
filter: eventStore.eq('type', 'chat'),
sort: [SortOrder('created_at', false)], // Most recent first
);
- _logger.i('Fallback: found ${chatEvents.length} total chat events');
+ logger.i('Fallback: found ${chatEvents.length} total chat events');
}
if (chatEvents.isEmpty) {
- _logger.w('No chat events found at all');
+ logger.w('No chat events found at all');
return;
}
@@ -295,11 +293,11 @@ class ChatRoomNotifier extends StateNotifier {
for (int i = 0; i < chatEvents.length; i++) {
final eventData = chatEvents[i];
- _logger.i('Processing event $i: ${eventData['id']}');
+ logger.i('Processing event $i: ${eventData['id']}');
try {
// Log the event data structure
- _logger.i('Event data keys: ${eventData.keys.toList()}');
+ logger.i('Event data keys: ${eventData.keys.toList()}');
// Check if this is a complete event (has all required fields)
final hasCompleteData = eventData.containsKey('kind') &&
@@ -309,7 +307,7 @@ class ChatRoomNotifier extends StateNotifier {
eventData.containsKey('tags');
if (!hasCompleteData) {
- _logger.w(
+ logger.w(
'Event ${eventData['id']} is incomplete (missing required fields), skipping. This is likely from an older version of the app.');
continue;
}
@@ -325,30 +323,30 @@ class ChatRoomNotifier extends StateNotifier {
'tags': eventData['tags'],
});
- _logger.i(
+ logger.i(
'Reconstructed event: ${storedEvent.id}, recipient: ${storedEvent.recipient}');
// Check if this event belongs to our chat (shared key)
if (session.sharedKey?.public == storedEvent.recipient) {
- _logger.i('Event belongs to our chat, unwrapping...');
+ logger.i('Event belongs to our chat, unwrapping...');
// Decrypt and unwrap the message
final unwrappedMessage =
await storedEvent.p2pUnwrap(session.sharedKey!);
historicalMessages.add(unwrappedMessage);
- _logger.i(
+ logger.i(
'Successfully unwrapped message: ${unwrappedMessage.content}');
} else {
- _logger.i(
+ logger.i(
'Event does not belong to our chat. Expected: ${session.sharedKey?.public}, Got: ${storedEvent.recipient}');
}
} catch (e) {
- _logger
+ logger
.e('Failed to process historical event ${eventData['id']}: $e');
// Continue processing other events even if one fails
}
}
- _logger.i(
+ logger.i(
'Total historical messages processed: ${historicalMessages.length}');
if (historicalMessages.isNotEmpty) {
@@ -363,22 +361,22 @@ class ChatRoomNotifier extends StateNotifier {
}).toList();
deduped.sort((a, b) => b.createdAt!.compareTo(a.createdAt!));
state = state.copy(messages: deduped);
- _logger.i(
+ logger.i(
'Successfully loaded and merged ${historicalMessages.length} historical messages, total: ${deduped.length} for chat $orderId');
} else {
- _logger.w('No historical messages loaded for chat $orderId');
- _logger.i('This could be because:');
- _logger.i('1. No messages have been sent in this chat yet');
- _logger
+ logger.w('No historical messages loaded for chat $orderId');
+ logger.i('This could be because:');
+ logger.i('1. No messages have been sent in this chat yet');
+ logger
.i('2. All stored events are incomplete (from older app version)');
- _logger.i(
+ logger.i(
'3. The events belong to a different chat (shared key mismatch)');
- _logger
+ logger
.i('New messages will be stored correctly and appear immediately.');
}
} catch (e) {
- _logger.e('Error loading historical messages: $e');
- _logger.e('Stack trace: ${StackTrace.current}');
+ logger.e('Error loading historical messages: $e');
+ logger.e('Stack trace: ${StackTrace.current}');
}
}
@@ -436,16 +434,16 @@ class ChatRoomNotifier extends StateNotifier {
// Check for encrypted message types
if (jsonContent != null) {
if (MessageTypeUtils.isEncryptedImageMessage(message)) {
- _logger.i('📸 Processing encrypted image message');
+ logger.i('📸 Processing encrypted image message');
await _processEncryptedImageMessage(message, jsonContent);
} else if (MessageTypeUtils.isEncryptedFileMessage(message)) {
- _logger.i('📎 Processing encrypted file message');
+ logger.i('📎 Processing encrypted file message');
await _processEncryptedFileMessage(message, jsonContent);
}
}
-
+
} catch (e) {
- _logger.w('Error processing message content: $e');
+ logger.w('Error processing message content: $e');
// Don't rethrow - message should still be displayed as text
}
}
@@ -459,28 +457,28 @@ class ChatRoomNotifier extends StateNotifier {
// Extract image metadata
final result = EncryptedImageUploadResult.fromJson(imageData);
- _logger.i('📥 Pre-downloading encrypted image: ${result.filename}');
- _logger.d('Blossom URL: ${result.blossomUrl}');
- _logger.d('Original size: ${result.originalSize} bytes');
-
+ logger.i('📥 Pre-downloading encrypted image: ${result.filename}');
+ logger.d('Blossom URL: ${result.blossomUrl}');
+ logger.d('Original size: ${result.originalSize} bytes');
+
// Get shared key for decryption
final sharedKey = await getSharedKey();
-
+
// Download and decrypt image in background
final uploadService = EncryptedImageUploadService();
final decryptedImage = await uploadService.downloadAndDecryptImage(
blossomUrl: result.blossomUrl,
sharedKey: sharedKey,
);
-
- _logger.i('✅ Image downloaded and decrypted successfully: ${decryptedImage.length} bytes');
-
+
+ logger.i('✅ Image downloaded and decrypted successfully: ${decryptedImage.length} bytes');
+
// Cache the decrypted image for immediate display
// You could store it in a Map for quick access
cacheDecryptedImage(message.id!, decryptedImage, result);
-
+
} catch (e) {
- _logger.e('❌ Failed to process encrypted image: $e');
+ logger.e('❌ Failed to process encrypted image: $e');
// Don't rethrow - message should still be displayed (maybe with error indicator)
}
}
@@ -495,13 +493,13 @@ class ChatRoomNotifier extends StateNotifier {
/// Cache a decrypted image for quick display
void cacheDecryptedImage(
- String messageId,
- Uint8List imageData,
+ String messageId,
+ Uint8List imageData,
EncryptedImageUploadResult metadata
) {
_imageCache[messageId] = imageData;
_imageMetadata[messageId] = metadata;
- _logger.d('🗄️ Cached decrypted image for message: $messageId');
+ logger.d('🗄️ Cached decrypted image for message: $messageId');
}
/// Get cached decrypted image data
@@ -522,33 +520,33 @@ class ChatRoomNotifier extends StateNotifier {
try {
// Extract file metadata
final result = EncryptedFileUploadResult.fromJson(fileData);
-
- _logger.i('📥 File message received: ${result.filename} (${result.fileType})');
- _logger.d('Blossom URL: ${result.blossomUrl}');
- _logger.d('Original size: ${result.originalSize} bytes');
-
+
+ logger.i('📥 File message received: ${result.filename} (${result.fileType})');
+ logger.d('Blossom URL: ${result.blossomUrl}');
+ logger.d('Original size: ${result.originalSize} bytes');
+
// Auto-download images for preview, but not other files
if (result.fileType == 'image') {
- _logger.i('📸 Auto-downloading image for preview: ${result.filename}');
-
+ logger.i('📸 Auto-downloading image for preview: ${result.filename}');
+
try {
// Get shared key for decryption
final sharedKey = await getSharedKey();
-
+
// Download and decrypt image in background
final uploadService = EncryptedFileUploadService();
final decryptedFile = await uploadService.downloadAndDecryptFile(
blossomUrl: result.blossomUrl,
sharedKey: sharedKey,
);
-
- _logger.i('✅ Image downloaded and decrypted successfully: ${decryptedFile.length} bytes');
-
+
+ logger.i('✅ Image downloaded and decrypted successfully: ${decryptedFile.length} bytes');
+
// Cache the decrypted image for immediate display
cacheDecryptedFile(message.id!, decryptedFile, result);
-
+
} catch (e) {
- _logger.e('❌ Failed to auto-download image: $e');
+ logger.e('❌ Failed to auto-download image: $e');
// Store metadata without file data - user can manually download
cacheDecryptedFile(message.id!, null, result);
}
@@ -557,24 +555,24 @@ class ChatRoomNotifier extends StateNotifier {
// Just store the metadata for display
cacheDecryptedFile(message.id!, null, result);
}
-
+
} catch (e) {
- _logger.e('❌ Failed to process encrypted file: $e');
+ logger.e('❌ Failed to process encrypted file: $e');
// Don't rethrow - message should still be displayed (maybe with error indicator)
}
}
/// Cache a decrypted file for quick display
void cacheDecryptedFile(
- String messageId,
- Uint8List? fileData,
+ String messageId,
+ Uint8List? fileData,
EncryptedFileUploadResult metadata
) {
if (fileData != null) {
_fileCache[messageId] = fileData;
}
_fileMetadata[messageId] = metadata;
- _logger.d('🗄️ Cached file metadata for message: $messageId');
+ logger.d('🗄️ Cached file metadata for message: $messageId');
}
/// Get cached decrypted file data
@@ -592,7 +590,7 @@ class ChatRoomNotifier extends StateNotifier {
void dispose() {
_subscription?.cancel();
_sessionListener?.close();
- _logger.i('Disposed chat room notifier for orderId: $orderId');
+ logger.i('Disposed chat room notifier for orderId: $orderId');
super.dispose();
}
}
diff --git a/lib/features/chat/notifiers/chat_rooms_notifier.dart b/lib/features/chat/notifiers/chat_rooms_notifier.dart
index de95d9eb..9635159e 100644
--- a/lib/features/chat/notifiers/chat_rooms_notifier.dart
+++ b/lib/features/chat/notifiers/chat_rooms_notifier.dart
@@ -1,7 +1,7 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/data/models/chat_room.dart';
import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart';
@@ -9,7 +9,6 @@ import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart';
class ChatRoomsNotifier extends StateNotifier> {
final Ref ref;
- final _logger = Logger();
ChatRoomsNotifier(this.ref) : super(const []) {
loadChats();
@@ -24,7 +23,7 @@ class ChatRoomsNotifier extends StateNotifier> {
notifier.reload();
}
} catch (e) {
- _logger.e('Failed to reload chat for orderId ${chat.orderId}: $e');
+ logger.e('Failed to reload chat for orderId ${chat.orderId}: $e');
}
}
@@ -34,7 +33,7 @@ class ChatRoomsNotifier extends StateNotifier> {
Future loadChats() async {
final sessions = ref.read(sessionNotifierProvider);
if (sessions.isEmpty) {
- _logger.i("No sessions yet, skipping chat load.");
+ logger.i("No sessions yet, skipping chat load.");
return;
}
final now = DateTime.now();
@@ -53,9 +52,9 @@ class ChatRoomsNotifier extends StateNotifier> {
}).toList();
state = chats;
- _logger.i("Loaded ${chats.length} chats");
+ logger.i("Loaded ${chats.length} chats");
} catch (e) {
- _logger.e("Error loading chats: $e");
+ logger.e("Error loading chats: $e");
}
}
@@ -86,17 +85,17 @@ class ChatRoomsNotifier extends StateNotifier> {
// Force update the state to trigger UI refresh
state = [...chats];
- _logger.d("Refreshed ${chats.length} chats with updated messages");
+ logger.d("Refreshed ${chats.length} chats with updated messages");
} catch (e) {
- _logger.e("Error refreshing chats: $e");
+ logger.e("Error refreshing chats: $e");
}
}
void _refreshAllSubscriptions() {
// No need to manually refresh subscriptions
// SubscriptionManager now handles this automatically based on SessionNotifier changes
- _logger.i('Subscription management is now handled by SubscriptionManager');
-
+ logger.i('Subscription management is now handled by SubscriptionManager');
+
// Just reload the chat rooms from the current sessions
//loadChats();
}
diff --git a/lib/features/chat/providers/chat_room_providers.dart b/lib/features/chat/providers/chat_room_providers.dart
index a0e16189..7659bdde 100644
--- a/lib/features/chat/providers/chat_room_providers.dart
+++ b/lib/features/chat/providers/chat_room_providers.dart
@@ -1,5 +1,5 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/data/models/chat_room.dart';
import 'package:mostro_mobile/features/chat/notifiers/chat_rooms_notifier.dart';
import 'package:mostro_mobile/features/chat/chat_room_provider.dart';
@@ -44,9 +44,6 @@ final sortedChatRoomsProvider = Provider>((ref) {
return chatRoomsWithFreshData;
});
-// Logger instance for session start time operations
-final _sessionLogger = Logger();
-
// Helper function to get session start time for sorting with improved error handling
int _getSessionStartTime(Ref ref, ChatRoom chatRoom) {
try {
@@ -55,22 +52,22 @@ int _getSessionStartTime(Ref ref, ChatRoom chatRoom) {
if (session != null) {
// Return the session start time (when the order was taken/contacted)
final startTime = session.startTime.millisecondsSinceEpoch ~/ 1000;
- _sessionLogger.d('Retrieved session start time for chat ${chatRoom.orderId}: $startTime');
+ logger.d('Retrieved session start time for chat ${chatRoom.orderId}: $startTime');
return startTime;
} else {
- _sessionLogger.i('No session found for chat ${chatRoom.orderId}, using fallback time');
+ logger.i('No session found for chat ${chatRoom.orderId}, using fallback time');
}
} catch (e, stackTrace) {
// Enhanced error handling with proper logging for diagnostics
- _sessionLogger.e(
+ logger.e(
'Error getting session start time for chat ${chatRoom.orderId}: $e',
error: e,
stackTrace: stackTrace,
);
}
-
+
// Fallback: use current time so new chats appear at top
final fallbackTime = DateTime.now().millisecondsSinceEpoch ~/ 1000;
- _sessionLogger.d('Using fallback time for chat ${chatRoom.orderId}: $fallbackTime');
+ logger.d('Using fallback time for chat ${chatRoom.orderId}: $fallbackTime');
return fallbackTime;
}
diff --git a/lib/features/disputes/notifiers/dispute_chat_notifier.dart b/lib/features/disputes/notifiers/dispute_chat_notifier.dart
index fc33472b..d0e53eb3 100644
--- a/lib/features/disputes/notifiers/dispute_chat_notifier.dart
+++ b/lib/features/disputes/notifiers/dispute_chat_notifier.dart
@@ -2,7 +2,7 @@ import 'dart:async';
import 'dart:convert';
import 'package:dart_nostr/dart_nostr.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/data/models/dispute_chat.dart';
import 'package:mostro_mobile/data/models/enums/action.dart';
import 'package:mostro_mobile/data/models/mostro_message.dart';
@@ -45,8 +45,7 @@ class DisputeChatState {
class DisputeChatNotifier extends StateNotifier {
final String disputeId;
final Ref ref;
- final _logger = Logger();
-
+
StreamSubscription? _subscription;
ProviderSubscription? _sessionListener;
bool _isInitialized = false;
@@ -57,7 +56,7 @@ class DisputeChatNotifier extends StateNotifier {
Future initialize() async {
if (_isInitialized) return;
- _logger.i('Initializing dispute chat for disputeId: $disputeId');
+ logger.i('Initializing dispute chat for disputeId: $disputeId');
await _loadHistoricalMessages();
await _subscribe();
_isInitialized = true;
@@ -67,14 +66,14 @@ class DisputeChatNotifier extends StateNotifier {
Future _subscribe() async {
final session = _getSessionForDispute();
if (session == null) {
- _logger.w('No session found for dispute: $disputeId');
+ logger.w('No session found for dispute: $disputeId');
_listenForSession();
return;
}
// Cancel existing subscription to prevent leaks and duplicate handlers
if (_subscription != null) {
- _logger.i('Cancelling previous subscription for dispute: $disputeId');
+ logger.i('Cancelling previous subscription for dispute: $disputeId');
await _subscription!.cancel();
_subscription = null;
}
@@ -91,7 +90,7 @@ class DisputeChatNotifier extends StateNotifier {
);
_subscription = nostrService.subscribeToEvents(request).listen(_onChatEvent);
- _logger.i('Subscribed to kind 1059 (Gift Wrap) for dispute: $disputeId');
+ logger.i('Subscribed to kind 1059 (Gift Wrap) for dispute: $disputeId');
}
/// Listen for session changes and subscribe when session is ready
@@ -100,19 +99,19 @@ class DisputeChatNotifier extends StateNotifier {
_sessionListener?.close();
_sessionListener = null;
- _logger.i('Starting to listen for session list changes for dispute: $disputeId');
+ logger.i('Starting to listen for session list changes for dispute: $disputeId');
// Watch the entire session list for changes
_sessionListener = ref.listen>(
sessionNotifierProvider,
(previous, next) {
- _logger.i('Session list changed, checking for dispute $disputeId match');
+ logger.i('Session list changed, checking for dispute $disputeId match');
// Try to find a session that matches this dispute
final session = _getSessionForDispute();
if (session != null) {
// Found a matching session, cancel listener and subscribe
- _logger.i('Session found for dispute $disputeId, canceling listener and subscribing');
+ logger.i('Session found for dispute $disputeId, canceling listener and subscribing');
_sessionListener?.close();
_sessionListener = null;
unawaited(_subscribe());
@@ -189,12 +188,12 @@ class DisputeChatNotifier extends StateNotifier {
}
}
} catch (e) {
- _logger.w('Failed to parse Mostro message: $e');
+ logger.w('Failed to parse Mostro message: $e');
return;
}
if (messageText.isEmpty) {
- _logger.w('Received empty message, skipping');
+ logger.w('Received empty message, skipping');
return;
}
@@ -204,16 +203,16 @@ class DisputeChatNotifier extends StateNotifier {
// 2. The assigned admin/solver (dispute.adminPubkey)
if (isFromAdmin) {
if (dispute.adminPubkey == null) {
- _logger.w('Rejecting message: No admin assigned yet for dispute $disputeId');
+ logger.w('Rejecting message: No admin assigned yet for dispute $disputeId');
return;
}
if (senderPubkey != dispute.adminPubkey) {
- _logger.w('SECURITY: Rejecting message from unauthorized pubkey: $senderPubkey (expected admin: ${dispute.adminPubkey})');
+ logger.w('SECURITY: Rejecting message from unauthorized pubkey: $senderPubkey (expected admin: ${dispute.adminPubkey})');
return;
}
- _logger.i('Validated message from authorized admin: $senderPubkey');
+ logger.i('Validated message from authorized admin: $senderPubkey');
}
// Generate event ID if not present (can happen with admin messages)
@@ -254,16 +253,16 @@ class DisputeChatNotifier extends StateNotifier {
deduped.sort((a, b) => a.timestamp.compareTo(b.timestamp));
state = state.copyWith(messages: deduped);
- _logger.i('Added dispute chat message for dispute: $disputeId (from ${isFromAdmin ? "admin" : "user"})');
+ logger.i('Added dispute chat message for dispute: $disputeId (from ${isFromAdmin ? "admin" : "user"})');
} catch (e, stackTrace) {
- _logger.e('Error processing dispute chat event: $e', stackTrace: stackTrace);
+ logger.e('Error processing dispute chat event: $e', stackTrace: stackTrace);
}
}
/// Load historical messages from storage
Future _loadHistoricalMessages() async {
try {
- _logger.i('Loading historical messages for dispute: $disputeId');
+ logger.i('Loading historical messages for dispute: $disputeId');
state = state.copyWith(isLoading: true);
final eventStore = ref.read(eventStorageProvider);
@@ -277,7 +276,7 @@ class DisputeChatNotifier extends StateNotifier {
sort: [SortOrder('created_at', true)], // Oldest first
);
- _logger.i('Found ${chatEvents.length} historical messages for dispute: $disputeId');
+ logger.i('Found ${chatEvents.length} historical messages for dispute: $disputeId');
// Get dispute to validate admin pubkey
final dispute = await ref.read(disputeDetailsProvider(disputeId).future);
@@ -297,13 +296,13 @@ class DisputeChatNotifier extends StateNotifier {
if (!isFromUser) {
// Message is from admin, validate pubkey
if (dispute?.adminPubkey == null) {
- _logger.w('Filtering historical message: No admin assigned yet');
+ logger.w('Filtering historical message: No admin assigned yet');
filteredCount++;
continue;
}
if (messagePubkey != null && messagePubkey != dispute!.adminPubkey) {
- _logger.w('SECURITY: Filtering historical message from unauthorized pubkey: $messagePubkey (expected: ${dispute.adminPubkey})');
+ logger.w('SECURITY: Filtering historical message from unauthorized pubkey: $messagePubkey (expected: ${dispute.adminPubkey})');
filteredCount++;
continue;
}
@@ -321,17 +320,17 @@ class DisputeChatNotifier extends StateNotifier {
error: eventData['error'] as String?,
));
} catch (e) {
- _logger.w('Failed to parse dispute chat message: $e');
+ logger.w('Failed to parse dispute chat message: $e');
}
}
if (filteredCount > 0) {
- _logger.i('Filtered $filteredCount unauthorized messages from dispute $disputeId');
+ logger.i('Filtered $filteredCount unauthorized messages from dispute $disputeId');
}
state = state.copyWith(messages: messages, isLoading: false);
} catch (e, stackTrace) {
- _logger.e('Error loading historical messages: $e', stackTrace: stackTrace);
+ logger.e('Error loading historical messages: $e', stackTrace: stackTrace);
state = state.copyWith(isLoading: false, error: e.toString());
}
}
@@ -341,26 +340,26 @@ class DisputeChatNotifier extends StateNotifier {
Future sendMessage(String text) async {
final session = _getSessionForDispute();
if (session == null) {
- _logger.w('Cannot send message: Session is null for dispute: $disputeId');
+ logger.w('Cannot send message: Session is null for dispute: $disputeId');
return;
}
// Get dispute to find admin pubkey and orderId
final dispute = await ref.read(disputeDetailsProvider(disputeId).future);
if (dispute == null) {
- _logger.w('Cannot send message: Dispute not found');
+ logger.w('Cannot send message: Dispute not found');
return;
}
if (dispute.adminPubkey == null) {
- _logger.w('Cannot send message: Admin pubkey not found for dispute');
+ logger.w('Cannot send message: Admin pubkey not found for dispute');
return;
}
// Get orderId from session
final orderId = session.orderId;
if (orderId == null) {
- _logger.w('Cannot send message: Session orderId is null');
+ logger.w('Cannot send message: Session orderId is null');
return;
}
@@ -369,7 +368,7 @@ class DisputeChatNotifier extends StateNotifier {
final rumorTimestamp = DateTime.now();
try {
- _logger.i('Sending Gift Wrap DM to admin: ${dispute.adminPubkey}');
+ logger.i('Sending Gift Wrap DM to admin: ${dispute.adminPubkey}');
// Add message to state with isPending=true (optimistic UI)
final pendingMessage = DisputeChat(
@@ -415,14 +414,14 @@ class DisputeChatNotifier extends StateNotifier {
dispute.adminPubkey!,
);
- _logger.i('Sending gift wrap from ${session.tradeKey.public} to ${dispute.adminPubkey}');
+ logger.i('Sending gift wrap from ${session.tradeKey.public} to ${dispute.adminPubkey}');
// Publish to network - await to catch network/initialization errors
try {
await ref.read(nostrServiceProvider).publishEvent(wrappedEvent);
- _logger.i('Dispute message sent successfully to admin for dispute: $disputeId');
+ logger.i('Dispute message sent successfully to admin for dispute: $disputeId');
} catch (publishError, publishStack) {
- _logger.e('Failed to publish dispute message: $publishError', stackTrace: publishStack);
+ logger.e('Failed to publish dispute message: $publishError', stackTrace: publishStack);
// Mark message as failed
final failedMessage = pendingMessage.copyWith(
@@ -454,7 +453,7 @@ class DisputeChatNotifier extends StateNotifier {
},
);
} catch (storageError) {
- _logger.e('Failed to store error state: $storageError');
+ logger.e('Failed to store error state: $storageError');
}
return; // Exit early, don't mark as success
}
@@ -483,7 +482,7 @@ class DisputeChatNotifier extends StateNotifier {
},
);
} catch (e, stackTrace) {
- _logger.e('Failed to send dispute message: $e', stackTrace: stackTrace);
+ logger.e('Failed to send dispute message: $e', stackTrace: stackTrace);
// Mark message as failed in state
final failedMessage = state.messages
@@ -520,7 +519,7 @@ class DisputeChatNotifier extends StateNotifier {
},
);
} catch (storageError) {
- _logger.e('Failed to store error state: $storageError');
+ logger.e('Failed to store error state: $storageError');
}
}
}
@@ -529,7 +528,7 @@ class DisputeChatNotifier extends StateNotifier {
Session? _getSessionForDispute() {
try {
final sessions = ref.read(sessionNotifierProvider);
- _logger.i('Looking for session for dispute: $disputeId, available sessions: ${sessions.length}');
+ logger.i('Looking for session for dispute: $disputeId, available sessions: ${sessions.length}');
// Search through all sessions to find the one that has this dispute
for (final session in sessions) {
@@ -539,7 +538,7 @@ class DisputeChatNotifier extends StateNotifier {
// Check if this order state contains our dispute
if (orderState.dispute?.disputeId == disputeId) {
- _logger.i('Found session for dispute: $disputeId with orderId: ${session.orderId}');
+ logger.i('Found session for dispute: $disputeId with orderId: ${session.orderId}');
return session;
}
} catch (e) {
@@ -549,10 +548,10 @@ class DisputeChatNotifier extends StateNotifier {
}
}
- _logger.w('No session found for dispute: $disputeId');
+ logger.w('No session found for dispute: $disputeId');
return null;
} catch (e, stackTrace) {
- _logger.e('Error getting session for dispute: $e', stackTrace: stackTrace);
+ logger.e('Error getting session for dispute: $e', stackTrace: stackTrace);
return null;
}
}
diff --git a/lib/features/logs/providers/logs_provider.dart b/lib/features/logs/providers/logs_provider.dart
new file mode 100644
index 00000000..e8329d25
--- /dev/null
+++ b/lib/features/logs/providers/logs_provider.dart
@@ -0,0 +1,12 @@
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
+
+/// Provider that exposes all captured logs
+final logsProvider = Provider>((ref) {
+ return MemoryLogOutput.instance.getAllLogs();
+});
+
+/// Provider for log count
+final logCountProvider = Provider((ref) {
+ return MemoryLogOutput.instance.logCount;
+});
diff --git a/lib/features/logs/screens/logs_screen.dart b/lib/features/logs/screens/logs_screen.dart
new file mode 100644
index 00000000..e7c74a49
--- /dev/null
+++ b/lib/features/logs/screens/logs_screen.dart
@@ -0,0 +1,684 @@
+import 'dart:io';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:intl/intl.dart';
+import 'package:logger/logger.dart';
+import 'package:mostro_mobile/core/app_theme.dart';
+import 'package:mostro_mobile/core/config.dart';
+import 'package:mostro_mobile/features/settings/settings_provider.dart';
+import 'package:mostro_mobile/generated/l10n.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
+import 'package:path_provider/path_provider.dart';
+import 'package:share_plus/share_plus.dart';
+
+class LogsScreen extends ConsumerStatefulWidget {
+ const LogsScreen({super.key});
+
+ @override
+ ConsumerState createState() => _LogsScreenState();
+}
+
+class _LogsScreenState extends ConsumerState {
+ bool _isExporting = false;
+ Level? _selectedLevel;
+ String _searchQuery = '';
+ final TextEditingController _searchController = TextEditingController();
+ final ScrollController _scrollController = ScrollController();
+ bool _showScrollToTop = false;
+
+ @override
+ void initState() {
+ super.initState();
+ _scrollController.addListener(_onScroll);
+ }
+
+ void _onScroll() {
+ if (_scrollController.hasClients) {
+ final showButton = _scrollController.offset > 200;
+ if (showButton != _showScrollToTop) {
+ setState(() => _showScrollToTop = showButton);
+ }
+ }
+ }
+
+ void _scrollToTop() {
+ _scrollController.animateTo(
+ 0,
+ duration: const Duration(milliseconds: 500),
+ curve: Curves.easeInOut,
+ );
+ }
+
+ @override
+ void dispose() {
+ _scrollController.dispose();
+ _searchController.dispose();
+ super.dispose();
+ }
+
+ List _filterLogs(List logs) {
+ var filtered = logs;
+
+ if (_selectedLevel != null) {
+ filtered = filtered.where((log) => log.level == _selectedLevel).toList();
+ }
+
+ if (_searchQuery.isNotEmpty) {
+ filtered = filtered.where((log) =>
+ log.message.toLowerCase().contains(_searchQuery.toLowerCase())
+ ).toList();
+ }
+
+ return filtered;
+ }
+
+ String _getLogStorageLocation(BuildContext context) {
+ final settings = ref.watch(settingsProvider);
+ return settings.customLogStorageDirectory ?? S.of(context)!.defaultDownloads;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ final allLogs = MemoryLogOutput.instance.getAllLogs();
+ final logs = _filterLogs(allLogs);
+
+ return Stack(
+ children: [
+ Scaffold(
+ backgroundColor: AppTheme.backgroundDark,
+ appBar: AppBar(
+ backgroundColor: Colors.transparent,
+ elevation: 0,
+ title: Text(
+ S.of(context)!.logsReport,
+ style: const TextStyle(
+ color: AppTheme.textPrimary,
+ fontSize: 20,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ iconTheme: const IconThemeData(color: AppTheme.textPrimary),
+ actions: [
+ IconButton(
+ icon: const Icon(Icons.delete_outline),
+ onPressed: logs.isEmpty ? null : _showClearConfirmation,
+ tooltip: S.of(context)!.clearLogs,
+ ),
+ ],
+ ),
+ body: Column(
+ children: [
+ _buildStatsHeader(allLogs.length, logs.length),
+ _buildSearchBar(),
+ _buildFilterChips(),
+ Expanded(
+ child: logs.isEmpty
+ ? _buildEmptyState()
+ : _buildLogsList(logs),
+ ),
+ if (allLogs.isNotEmpty) _buildActionButtons(),
+ ],
+ ),
+ ),
+ if (_showScrollToTop)
+ Positioned(
+ right: 16,
+ bottom: 100,
+ child: FloatingActionButton(
+ mini: true,
+ backgroundColor: AppTheme.activeColor,
+ onPressed: _scrollToTop,
+ child: const Icon(
+ Icons.arrow_upward,
+ color: Colors.white,
+ size: 20,
+ ),
+ ),
+ ),
+ ],
+ );
+ }
+
+ Widget _buildStatsHeader(int totalCount, int filteredCount) {
+ final storageLocation = _getLogStorageLocation(context);
+
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppTheme.backgroundCard,
+ border: Border(
+ bottom: BorderSide(
+ color: Colors.white.withValues(alpha: 0.1),
+ width: 1,
+ ),
+ ),
+ ),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ filteredCount == totalCount
+ ? S.of(context)!.totalLogs(totalCount)
+ : '$filteredCount / ${S.of(context)!.totalLogs(totalCount)}',
+ style: const TextStyle(
+ color: AppTheme.textPrimary,
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ Text(
+ S.of(context)!.maxEntries(Config.logMaxEntries),
+ style: const TextStyle(
+ color: AppTheme.textSecondary,
+ fontSize: 14,
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 12),
+ Row(
+ children: [
+ Icon(
+ Icons.folder_outlined,
+ color: AppTheme.textInactive,
+ size: 16,
+ ),
+ const SizedBox(width: 8),
+ Expanded(
+ child: Text(
+ storageLocation,
+ style: const TextStyle(
+ color: AppTheme.textInactive,
+ fontSize: 12,
+ ),
+ maxLines: 1,
+ overflow: TextOverflow.ellipsis,
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildSearchBar() {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ color: AppTheme.backgroundCard,
+ child: TextField(
+ controller: _searchController,
+ style: const TextStyle(color: AppTheme.textPrimary),
+ decoration: InputDecoration(
+ hintText: S.of(context)!.searchLogs,
+ hintStyle: const TextStyle(color: AppTheme.textSecondary),
+ prefixIcon: const Icon(Icons.search, color: AppTheme.textSecondary),
+ suffixIcon: _searchQuery.isNotEmpty
+ ? IconButton(
+ icon: const Icon(Icons.clear, color: AppTheme.textSecondary),
+ onPressed: () {
+ _searchController.clear();
+ setState(() => _searchQuery = '');
+ },
+ )
+ : null,
+ filled: true,
+ fillColor: AppTheme.backgroundInput,
+ border: OutlineInputBorder(
+ borderRadius: BorderRadius.circular(8),
+ borderSide: BorderSide.none,
+ ),
+ contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ ),
+ onChanged: (value) {
+ setState(() => _searchQuery = value);
+ },
+ ),
+ );
+ }
+
+ Widget _buildFilterChips() {
+ return Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ color: AppTheme.backgroundCard,
+ child: SingleChildScrollView(
+ scrollDirection: Axis.horizontal,
+ child: Row(
+ children: [
+ _buildFilterChip(S.of(context)!.allLevels, null),
+ const SizedBox(width: 8),
+ _buildFilterChip(S.of(context)!.errors, Level.error),
+ const SizedBox(width: 8),
+ _buildFilterChip(S.of(context)!.warnings, Level.warning),
+ const SizedBox(width: 8),
+ _buildFilterChip(S.of(context)!.info, Level.info),
+ const SizedBox(width: 8),
+ _buildFilterChip(S.of(context)!.debug, Level.debug),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Widget _buildFilterChip(String label, Level? level) {
+ final isSelected = _selectedLevel == level;
+ return FilterChip(
+ label: Text(label),
+ selected: isSelected,
+ onSelected: (selected) {
+ setState(() => _selectedLevel = selected ? level : null);
+ },
+ backgroundColor: AppTheme.backgroundInput,
+ selectedColor: AppTheme.statusInfo.withValues(alpha: 0.2),
+ labelStyle: TextStyle(
+ color: isSelected ? AppTheme.statusInfo : AppTheme.textSecondary,
+ fontWeight: isSelected ? FontWeight.w600 : FontWeight.normal,
+ ),
+ side: BorderSide(
+ color: isSelected ? AppTheme.statusInfo : Colors.white.withValues(alpha: 0.1),
+ ),
+ );
+ }
+
+ Widget _buildEmptyState() {
+ return Center(
+ child: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ const Icon(
+ Icons.info_outline,
+ size: 64,
+ color: AppTheme.textInactive,
+ ),
+ const SizedBox(height: 16),
+ Text(
+ S.of(context)!.noLogsAvailable,
+ style: const TextStyle(
+ color: AppTheme.textPrimary,
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ const SizedBox(height: 8),
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 32),
+ child: Text(
+ S.of(context)!.logsWillAppearHere,
+ style: const TextStyle(
+ color: AppTheme.textSecondary,
+ fontSize: 14,
+ ),
+ textAlign: TextAlign.center,
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Widget _buildLogsList(List logs) {
+ return ListView.builder(
+ controller: _scrollController,
+ itemCount: logs.length,
+ itemBuilder: (context, index) {
+ final log = logs[logs.length - 1 - index];
+ return _buildLogItem(log);
+ },
+ );
+ }
+
+ Widget _buildLogItem(LogEntry log) {
+ final color = _getLogLevelColor(log.level);
+ final icon = _getLogLevelIcon(log.level);
+ final levelStr = log.level.toString().split('.').last.toUpperCase();
+
+ return Container(
+ decoration: BoxDecoration(
+ border: Border(
+ bottom: BorderSide(
+ color: Colors.white.withValues(alpha: 0.05),
+ width: 0.5,
+ ),
+ ),
+ ),
+ child: Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
+ child: Row(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Icon(icon, color: color, size: 20),
+ const SizedBox(width: 12),
+ Expanded(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ Row(
+ children: [
+ Container(
+ padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
+ decoration: BoxDecoration(
+ color: color.withValues(alpha: 0.2),
+ borderRadius: BorderRadius.circular(4),
+ ),
+ child: Text(
+ levelStr,
+ style: TextStyle(
+ color: color,
+ fontSize: 10,
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ const SizedBox(width: 8),
+ Text(
+ '(${log.service}:${log.line})',
+ style: const TextStyle(
+ color: AppTheme.activeColor,
+ fontSize: 12,
+ fontWeight: FontWeight.w500,
+ fontFamily: 'monospace',
+ ),
+ ),
+ ],
+ ),
+ const SizedBox(height: 6),
+ Text(
+ log.message,
+ style: const TextStyle(
+ fontFamily: 'monospace',
+ color: AppTheme.textPrimary,
+ fontSize: 13,
+ ),
+ ),
+ const SizedBox(height: 4),
+ Text(
+ _formatTimestamp(log.timestamp),
+ style: const TextStyle(
+ color: AppTheme.textSecondary,
+ fontSize: 11,
+ ),
+ ),
+ ],
+ ),
+ ),
+ IconButton(
+ icon: const Icon(Icons.copy, size: 18),
+ color: AppTheme.textSecondary,
+ onPressed: () => _copyLogToClipboard(log),
+ tooltip: S.of(context)!.copyLog,
+ padding: EdgeInsets.zero,
+ constraints: const BoxConstraints(),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+
+ Future _copyLogToClipboard(LogEntry log) async {
+ await Clipboard.setData(ClipboardData(text: log.format()));
+ if (mounted) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(S.of(context)!.logCopied),
+ duration: const Duration(seconds: 2),
+ behavior: SnackBarBehavior.floating,
+ ),
+ );
+ }
+ }
+
+ Widget _buildActionButtons() {
+ return Container(
+ padding: const EdgeInsets.all(16),
+ decoration: BoxDecoration(
+ color: AppTheme.backgroundCard,
+ border: Border(
+ top: BorderSide(
+ color: Colors.white.withValues(alpha: 0.1),
+ width: 1,
+ ),
+ ),
+ ),
+ child: Row(
+ children: [
+ Expanded(
+ child: OutlinedButton.icon(
+ onPressed: _isExporting ? null : _saveToDevice,
+ icon: const Icon(Icons.save),
+ label: Text(S.of(context)!.saveToDevice),
+ ),
+ ),
+ const SizedBox(width: 12),
+ Expanded(
+ child: ElevatedButton.icon(
+ onPressed: _isExporting ? null : _shareLogs,
+ icon: const Icon(Icons.share),
+ label: Text(S.of(context)!.shareReport),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ Color _getLogLevelColor(Level level) {
+ switch (level) {
+ case Level.error:
+ case Level.fatal:
+ return Colors.red;
+ case Level.warning:
+ return Colors.orange;
+ case Level.info:
+ return Colors.blue;
+ case Level.debug:
+ case Level.trace:
+ return Colors.grey;
+ default:
+ return Colors.grey;
+ }
+ }
+
+ IconData _getLogLevelIcon(Level level) {
+ switch (level) {
+ case Level.error:
+ case Level.fatal:
+ return Icons.error_outline;
+ case Level.warning:
+ return Icons.warning_amber_outlined;
+ case Level.info:
+ return Icons.info_outline;
+ case Level.debug:
+ case Level.trace:
+ return Icons.bug_report_outlined;
+ default:
+ return Icons.circle_outlined;
+ }
+ }
+
+ String _formatTimestamp(DateTime timestamp) {
+ return DateFormat('yyyy-MM-dd HH:mm:ss').format(timestamp);
+ }
+
+ Future _shareLogs() async {
+ setState(() => _isExporting = true);
+ final exportTitle = S.of(context)!.logsExportTitle;
+ final exportFailedMsg = S.of(context)!.exportFailed;
+
+ try {
+ final file = await _createLogFile();
+ await Share.shareXFiles(
+ [XFile(file.path)],
+ subject: exportTitle,
+ );
+ } catch (e) {
+ if (mounted) {
+ _showErrorSnackBar(exportFailedMsg);
+ }
+ } finally {
+ if (mounted) {
+ setState(() => _isExporting = false);
+ }
+ }
+ }
+
+ Future _saveToDevice() async {
+ logger.i('Button pressed, starting save process');
+ setState(() => _isExporting = true);
+ try {
+ logger.i('Creating log file');
+ final file = await _createLogFile();
+
+ logger.i('Saving to storage');
+ final savedPath = await _saveToDocuments(file);
+ logger.i('Successfully saved to: $savedPath');
+
+ if (mounted) {
+ _showSuccessSnackBar(
+ S.of(context)!.logsSavedTo(savedPath),
+ );
+ }
+ } catch (e, stack) {
+ logger.e('Failed to save logs to device', error: e, stackTrace: stack);
+ if (mounted) {
+ _showErrorSnackBar('${S.of(context)!.saveFailed}: $e');
+ }
+ } finally {
+ if (mounted) {
+ setState(() => _isExporting = false);
+ }
+ }
+ }
+
+ Future _createLogFile() async {
+ final logs = MemoryLogOutput.instance.getAllLogs();
+ final buffer = StringBuffer();
+
+ buffer.writeln('Mostro Mobile - Logs Report');
+ buffer.writeln('Generated: ${DateFormat('yyyy-MM-dd HH:mm:ss').format(DateTime.now())}');
+ buffer.writeln('Total Entries: ${logs.length}');
+ buffer.writeln('${'=' * 80}\n');
+
+ for (final log in logs) {
+ buffer.writeln(log.format());
+ }
+
+ final tempDir = await getTemporaryDirectory();
+ final timestamp = DateFormat('yyyy-MM-dd_HH-mm-ss').format(DateTime.now());
+ final file = File('${tempDir.path}/mostro_logs_$timestamp.txt');
+ await file.writeAsString(buffer.toString());
+
+ return file;
+ }
+
+ Future _saveToDocuments(File tempFile) async {
+ logger.i('Saving to app storage');
+
+ final directory = Platform.isAndroid
+ ? await getExternalStorageDirectory()
+ : await getApplicationDocumentsDirectory();
+
+ if (directory == null) {
+ throw Exception('Could not get storage directory');
+ }
+
+ final logsDir = Directory('${directory.path}/MostroLogs');
+ logger.i('Creating logs directory: ${logsDir.path}');
+ await logsDir.create(recursive: true);
+
+ final timestamp = DateFormat('yyyy-MM-dd_HH-mm-ss').format(DateTime.now());
+ final destinationFile = File('${logsDir.path}/mostro_logs_$timestamp.txt');
+
+ logger.i('Copying to: ${destinationFile.path}');
+ await tempFile.copy(destinationFile.path);
+
+ final exists = await destinationFile.exists();
+ if (!exists) {
+ throw Exception('File was not created');
+ }
+
+ final fileSize = await destinationFile.length();
+ logger.i('File saved: $fileSize bytes at ${destinationFile.path}');
+
+ return destinationFile.path;
+ }
+
+ void _showClearConfirmation() {
+ showDialog(
+ context: context,
+ builder: (context) => AlertDialog(
+ backgroundColor: AppTheme.backgroundCard,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(16),
+ side: BorderSide(color: Colors.white.withValues(alpha: 0.1)),
+ ),
+ title: Text(
+ S.of(context)!.clearLogsConfirmTitle,
+ style: const TextStyle(
+ color: AppTheme.textPrimary,
+ fontSize: 18,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ content: Text(
+ S.of(context)!.clearLogsConfirmMessage,
+ style: const TextStyle(
+ color: AppTheme.textSecondary,
+ fontSize: 14,
+ ),
+ ),
+ actions: [
+ TextButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: Text(
+ S.of(context)!.cancel,
+ style: const TextStyle(
+ color: AppTheme.textPrimary,
+ fontSize: 16,
+ ),
+ ),
+ ),
+ TextButton(
+ onPressed: () {
+ MemoryLogOutput.instance.clear();
+ Navigator.of(context).pop();
+ setState(() {});
+ },
+ child: Text(
+ S.of(context)!.clear,
+ style: const TextStyle(
+ color: AppTheme.statusError,
+ fontSize: 16,
+ fontWeight: FontWeight.w600,
+ ),
+ ),
+ ),
+ ],
+ ),
+ );
+ }
+
+ void _showSuccessSnackBar(String message) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(message),
+ backgroundColor: Colors.green,
+ behavior: SnackBarBehavior.floating,
+ ),
+ );
+ }
+
+ void _showErrorSnackBar(String message) {
+ ScaffoldMessenger.of(context).showSnackBar(
+ SnackBar(
+ content: Text(message),
+ backgroundColor: Theme.of(context).colorScheme.error,
+ behavior: SnackBarBehavior.floating,
+ ),
+ );
+ }
+}
diff --git a/lib/features/notifications/services/background_notification_service.dart b/lib/features/notifications/services/background_notification_service.dart
index a7879404..c599d891 100644
--- a/lib/features/notifications/services/background_notification_service.dart
+++ b/lib/features/notifications/services/background_notification_service.dart
@@ -5,7 +5,7 @@ import 'package:dart_nostr/nostr/model/event/event.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:go_router/go_router.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:mostro_mobile/core/app.dart';
@@ -42,10 +42,10 @@ void _onNotificationTap(NotificationResponse response) {
final context = MostroApp.navigatorKey.currentContext;
if (context != null) {
context.push('/notifications');
- Logger().i('Navigated to notifications screen');
+ logger.i('Navigated to notifications screen');
}
} catch (e) {
- Logger().e('Navigation error: $e');
+ logger.e('Navigation error: $e');
}
}
@@ -104,9 +104,9 @@ Future showLocalNotification(NostrEvent event) async {
payload: mostroMessage.id,
);
- Logger().i('Shown: ${notificationText.title} - ${notificationText.body}');
+ logger.i('Shown: ${notificationText.title} - ${notificationText.body}');
} catch (e) {
- Logger().e('Notification error: $e');
+ logger.e('Notification error: $e');
}
}
@@ -143,7 +143,7 @@ Future _decryptAndProcessEvent(NostrEvent event) async {
return mostroMessage;
} catch (e) {
- Logger().e('Decrypt error: $e');
+ logger.e('Decrypt error: $e');
return null;
}
}
@@ -161,7 +161,7 @@ Future> _loadSessionsFromDatabase() async {
final sessionStorage = SessionStorage(keyManager, db: db);
return await sessionStorage.getAll();
} catch (e) {
- Logger().e('Session load error: $e');
+ logger.e('Session load error: $e');
return [];
}
}
@@ -280,13 +280,13 @@ Future retryNotification(NostrEvent event, {int maxAttempts = 3}) async {
} catch (e) {
attempt++;
if (attempt >= maxAttempts) {
- Logger().e('Failed to show notification after $maxAttempts attempts: $e');
+ logger.e('Failed to show notification after $maxAttempts attempts: $e');
break;
}
// Exponential backoff: 1s, 2s, 4s, etc.
final backoffSeconds = pow(2, attempt - 1).toInt();
- Logger().e('Notification attempt $attempt failed: $e. Retrying in ${backoffSeconds}s');
+ logger.e('Notification attempt $attempt failed: $e. Retrying in ${backoffSeconds}s');
await Future.delayed(Duration(seconds: backoffSeconds));
}
}
diff --git a/lib/features/notifications/utils/notification_data_extractor.dart b/lib/features/notifications/utils/notification_data_extractor.dart
index 35e04964..34278eee 100644
--- a/lib/features/notifications/utils/notification_data_extractor.dart
+++ b/lib/features/notifications/utils/notification_data_extractor.dart
@@ -1,5 +1,5 @@
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/core/config.dart';
import 'package:mostro_mobile/data/enums.dart';
import 'package:mostro_mobile/data/models.dart';
@@ -101,9 +101,9 @@ class NotificationDataExtractor {
? ref.read(orderRepositoryProvider).mostroInstance?.expirationSeconds ?? Config.expirationSeconds
: Config.expirationSeconds;
values['expiration_seconds'] = expirationSeconds;
- Logger().d('waitingBuyerInvoice: extracted expiration_seconds=$expirationSeconds');
+ logger.d('waitingBuyerInvoice: extracted expiration_seconds=$expirationSeconds');
} catch (e) {
- Logger().e('waitingBuyerInvoice: Error accessing providers: $e');
+ logger.e('waitingBuyerInvoice: Error accessing providers: $e');
values['expiration_seconds'] = Config.expirationSeconds;
}
break;
diff --git a/lib/features/order/models/order_state.dart b/lib/features/order/models/order_state.dart
index 69252b1d..de5b6838 100644
--- a/lib/features/order/models/order_state.dart
+++ b/lib/features/order/models/order_state.dart
@@ -1,4 +1,4 @@
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/data/models.dart';
import 'package:mostro_mobile/data/enums.dart';
@@ -11,7 +11,6 @@ class OrderState {
final Dispute? dispute;
final Peer? peer;
final PaymentFailed? paymentFailed;
- final _logger = Logger();
OrderState({
required this.status,
@@ -89,7 +88,7 @@ class OrderState {
}
OrderState updateWith(MostroMessage message) {
- _logger.i('Updating OrderState with Action: ${message.action}');
+ logger.i('Updating OrderState with Action: ${message.action}');
// Preserve the current state entirely for cantDo messages - they are informational only
if (message.action == Action.cantDo) {
@@ -101,13 +100,13 @@ class OrderState {
message.action, message.getPayload()?.status);
// DEBUG: Log status mapping
- _logger.i('Status mapping: ${message.action} → $newStatus');
+ logger.i('Status mapping: ${message.action} → $newStatus');
// Preserve PaymentRequest correctly
PaymentRequest? newPaymentRequest;
if (message.payload is PaymentRequest) {
newPaymentRequest = message.getPayload();
- _logger.i('New PaymentRequest found in message');
+ logger.i('New PaymentRequest found in message');
} else {
newPaymentRequest = paymentRequest; // Preserve existing
}
@@ -116,7 +115,7 @@ class OrderState {
if (message.payload is Peer &&
message.getPayload()!.publicKey.isNotEmpty) {
newPeer = message.getPayload();
- _logger.i('👤 New Peer found in message');
+ logger.i('👤 New Peer found in message');
} else if (message.payload is Order) {
if (message.getPayload()!.buyerTradePubkey != null) {
newPeer =
@@ -125,7 +124,7 @@ class OrderState {
newPeer =
Peer(publicKey: message.getPayload()!.sellerTradePubkey!);
}
- _logger.i('👤 New Peer found in message');
+ logger.i('👤 New Peer found in message');
} else {
newPeer = peer; // Preserve existing
}
@@ -145,18 +144,18 @@ class OrderState {
updatedDispute = updatedDispute.copyWith(
createdAt: DateTime.fromMillisecondsSinceEpoch(tsMs),
);
- _logger.i('Updated dispute ${updatedDispute.disputeId} createdAt from message timestamp: ${updatedDispute.createdAt}');
+ logger.i('Updated dispute ${updatedDispute.disputeId} createdAt from message timestamp: ${updatedDispute.createdAt}');
}
}
}
-
+
// Add defensive null check - if both message payload and existing dispute are null,
// we cannot perform dispute updates
- if (updatedDispute == null &&
- (message.action == Action.adminTookDispute ||
- message.action == Action.adminSettled ||
+ if (updatedDispute == null &&
+ (message.action == Action.adminTookDispute ||
+ message.action == Action.adminSettled ||
message.action == Action.adminCanceled)) {
- _logger.w('Cannot update dispute for action ${message.action}: no dispute found in message payload or existing state');
+ logger.w('Cannot update dispute for action ${message.action}: no dispute found in message payload or existing state');
} else if (message.action == Action.adminTookDispute && updatedDispute != null) {
// When admin takes dispute, update status to in-progress and set admin info
// Extract admin pubkey from Peer payload if available
@@ -165,31 +164,31 @@ class OrderState {
final peerPayload = message.getPayload();
if (peerPayload != null && peerPayload.publicKey.isNotEmpty) {
adminPubkey = peerPayload.publicKey;
- _logger.i('Extracted admin pubkey from Peer payload: $adminPubkey');
+ logger.i('Extracted admin pubkey from Peer payload: $adminPubkey');
}
}
-
+
updatedDispute = updatedDispute.copyWith(
status: 'in-progress',
adminTookAt: DateTime.now(),
adminPubkey: adminPubkey,
);
- _logger.i('Updated dispute status to in-progress for adminTookDispute action');
+ logger.i('Updated dispute status to in-progress for adminTookDispute action');
} else if (message.action == Action.adminSettled && updatedDispute != null) {
// When admin settles dispute, update status to resolved with settlement info
updatedDispute = updatedDispute.copyWith(
status: 'resolved',
action: 'admin-settled', // Store the resolution type
);
- _logger.i('Updated dispute status to resolved for adminSettled action');
+ logger.i('Updated dispute status to resolved for adminSettled action');
} else if (message.action == Action.adminCanceled && updatedDispute != null) {
// When admin cancels order, update dispute status to seller-refunded
updatedDispute = updatedDispute.copyWith(
status: 'seller-refunded',
action: 'admin-canceled', // Store the resolution type
);
- _logger.i('Updated dispute status to seller-refunded for adminCanceled action');
- _logger.i('Dispute status updated to: ${updatedDispute.status}');
+ logger.i('Updated dispute status to seller-refunded for adminCanceled action');
+ logger.i('Dispute status updated to: ${updatedDispute.status}');
}
final newState = copyWith(
@@ -207,8 +206,8 @@ class OrderState {
paymentFailed: message.getPayload() ?? paymentFailed,
);
- _logger.i('New state: ${newState.status} - ${newState.action}');
- _logger
+ logger.i('New state: ${newState.status} - ${newState.action}');
+ logger
.i('PaymentRequest preserved: ${newState.paymentRequest != null}');
return newState;
diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart
index f0b8429b..70e673f3 100644
--- a/lib/features/order/notfiers/abstract_mostro_notifier.dart
+++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart
@@ -11,12 +11,11 @@ import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart';
import 'package:mostro_mobile/features/notifications/providers/notifications_provider.dart';
import 'package:mostro_mobile/features/notifications/utils/notification_data_extractor.dart';
import 'package:mostro_mobile/features/settings/settings_provider.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
class AbstractMostroNotifier extends StateNotifier {
final String orderId;
final Ref ref;
- final logger = Logger();
late Session session;
@@ -504,17 +503,17 @@ class AbstractMostroNotifier extends StateNotifier {
_sessionTimeouts[orderId] = Timer(const Duration(seconds: 10), () {
try {
ref.read(sessionNotifierProvider.notifier).deleteSession(orderId);
- Logger().i('Session cleaned up after 10s timeout: $orderId');
-
+ logger.i('Session cleaned up after 10s timeout: $orderId');
+
// Show timeout message to user and navigate to order book
_showTimeoutNotificationAndNavigate(ref);
} catch (e) {
- Logger().e('Failed to cleanup session: $orderId', error: e);
+ logger.e('Failed to cleanup session: $orderId', error: e);
}
_sessionTimeouts.remove(orderId);
});
-
- Logger().i('Started 10s timeout timer for order: $orderId');
+
+ logger.i('Started 10s timeout timer for order: $orderId');
}
/// Shows timeout notification and navigates to order book
@@ -528,7 +527,7 @@ class AbstractMostroNotifier extends StateNotifier {
final navProvider = ref.read(navigationProvider.notifier);
navProvider.go('/');
} catch (e) {
- Logger().e('Failed to show timeout notification or navigate', error: e);
+ logger.e('Failed to show timeout notification or navigate', error: e);
}
}
@@ -541,17 +540,17 @@ class AbstractMostroNotifier extends StateNotifier {
_sessionTimeouts[key] = Timer(const Duration(seconds: 10), () {
try {
ref.read(sessionNotifierProvider.notifier).deleteSessionByRequestId(requestId);
- Logger().i('Session cleaned up after 10s timeout for requestId: $requestId');
-
+ logger.i('Session cleaned up after 10s timeout for requestId: $requestId');
+
// Show timeout message to user and navigate to order book
_showTimeoutNotificationAndNavigate(ref);
} catch (e) {
- Logger().e('Failed to cleanup session for requestId: $requestId', error: e);
+ logger.e('Failed to cleanup session for requestId: $requestId', error: e);
}
_sessionTimeouts.remove(key);
});
-
- Logger().i('Started 10s timeout timer for requestId: $requestId');
+
+ logger.i('Started 10s timeout timer for requestId: $requestId');
}
/// Cancels the timeout timer for a specific orderId
@@ -560,7 +559,7 @@ class AbstractMostroNotifier extends StateNotifier {
if (timer != null) {
timer.cancel();
_sessionTimeouts.remove(orderId);
- Logger().i('Cancelled 10s timeout timer for order: $orderId - Mostro responded');
+ logger.i('Cancelled 10s timeout timer for order: $orderId - Mostro responded');
}
}
@@ -571,7 +570,7 @@ class AbstractMostroNotifier extends StateNotifier {
if (timer != null) {
timer.cancel();
_sessionTimeouts.remove(key);
- Logger().i('Cancelled 10s timeout timer for requestId: $requestId - Mostro responded');
+ logger.i('Cancelled 10s timeout timer for requestId: $requestId - Mostro responded');
}
}
diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart
index fb1594b8..883bebfc 100644
--- a/lib/features/order/notfiers/add_order_notifier.dart
+++ b/lib/features/order/notfiers/add_order_notifier.dart
@@ -2,6 +2,7 @@ import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mostro_mobile/data/enums.dart';
import 'package:mostro_mobile/data/models.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/shared/providers.dart';
import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart';
import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart';
diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart
index dc1a1446..7322c5cc 100644
--- a/lib/features/order/notfiers/order_notifier.dart
+++ b/lib/features/order/notfiers/order_notifier.dart
@@ -4,6 +4,7 @@ import 'package:mostro_mobile/data/enums.dart';
import 'package:mostro_mobile/data/models.dart';
import 'package:mostro_mobile/features/order/models/order_state.dart';
import 'package:mostro_mobile/features/notifications/providers/notifications_provider.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/shared/providers.dart';
import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart';
import 'package:mostro_mobile/services/mostro_service.dart';
@@ -22,7 +23,7 @@ class OrderNotifier extends AbstractMostroNotifier {
@override
Future handleEvent(MostroMessage event, {bool bypassTimestampGate = false}) async {
- logger.i('OrderNotifier received event: ${event.action} for order $orderId');
+ logger.i('Order: received event ${event.action} for order $orderId');
// Handle the event normally - timeout/cancellation logic is now in AbstractMostroNotifier
await super.handleEvent(event, bypassTimestampGate: bypassTimestampGate);
@@ -37,7 +38,7 @@ class OrderNotifier extends AbstractMostroNotifier {
final storage = ref.read(mostroStorageProvider);
final messages = await storage.getAllMessagesForOrderId(orderId);
if (messages.isEmpty) {
- logger.w('No messages found for order $orderId');
+ logger.w('Order: no messages found for order $orderId');
return;
}
@@ -58,10 +59,10 @@ class OrderNotifier extends AbstractMostroNotifier {
state = currentState;
logger.i(
- 'Synced order $orderId to state: ${state.status} - ${state.action}');
+ 'Order: synced order $orderId to state: ${state.status} - ${state.action}');
} catch (e, stack) {
logger.e(
- 'Error syncing order state for $orderId',
+ 'Order: syncing failed - $e',
error: e,
stackTrace: stack,
);
@@ -163,11 +164,11 @@ class OrderNotifier extends AbstractMostroNotifier {
final publicEvent = ref.read(eventProvider(orderId));
final currentSession = ref.read(sessionProvider(orderId));
- if (publicEvent?.status == Status.canceled &&
+ if (publicEvent?.status == Status.canceled &&
state.status == Status.pending &&
currentSession != null) {
-
- logger.i('AUTOMATIC EXPIRATION: Order $orderId expired, removing from My Trades');
+
+ logger.i('Order: automatic expiration - order $orderId expired, removing from My Trades');
// Delete session - order disappears from My Trades
final sessionNotifier = ref.read(sessionNotifierProvider.notifier);
@@ -181,7 +182,7 @@ class OrderNotifier extends AbstractMostroNotifier {
}
} catch (e, stack) {
logger.e(
- 'Error handling automatic cancellation for order $orderId',
+ 'Order: automatic cancellation handling failed - $e',
error: e,
stackTrace: stack,
);
diff --git a/lib/features/rate/rate_counterpart_screen.dart b/lib/features/rate/rate_counterpart_screen.dart
index 9d5d2e9a..928f3efc 100644
--- a/lib/features/rate/rate_counterpart_screen.dart
+++ b/lib/features/rate/rate_counterpart_screen.dart
@@ -1,7 +1,7 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:go_router/go_router.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/core/app_theme.dart';
import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart';
import 'package:mostro_mobile/generated/l10n.dart';
@@ -21,10 +21,9 @@ class RateCounterpartScreen extends ConsumerStatefulWidget {
class _RateCounterpartScreenState extends ConsumerState {
int _rating = 0;
- final _logger = Logger();
Future _submitRating() async {
- _logger.i('Rating submitted: $_rating');
+ logger.i('Rating submitted: $_rating');
final orderNotifer = ref.watch(
orderNotifierProvider(widget.orderId).notifier,
);
diff --git a/lib/features/relays/relays_notifier.dart b/lib/features/relays/relays_notifier.dart
index 6a52e16b..ddc43c0c 100644
--- a/lib/features/relays/relays_notifier.dart
+++ b/lib/features/relays/relays_notifier.dart
@@ -3,10 +3,10 @@ import 'dart:io';
import 'package:dart_nostr/dart_nostr.dart';
import 'package:dart_nostr/nostr/model/ease.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:logger/logger.dart';
import 'package:mostro_mobile/core/models/relay_list_event.dart';
import 'package:mostro_mobile/features/settings/settings_notifier.dart';
import 'package:mostro_mobile/features/subscriptions/subscription_manager.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart';
import 'relay.dart';
@@ -27,7 +27,6 @@ class RelayValidationResult {
class RelaysNotifier extends StateNotifier> {
final SettingsNotifier settings;
final Ref ref;
- final _logger = Logger();
SubscriptionManager? _subscriptionManager;
StreamSubscription? _relayListSubscription;
Timer? _settingsWatchTimer;
@@ -51,8 +50,8 @@ class RelaysNotifier extends StateNotifier> {
void _loadRelays() {
final saved = settings.state;
- _logger.i('Loading relays from settings: ${saved.relays}');
- _logger.i('Loading user relays from settings: ${saved.userRelays}');
+ logger.i('Loading relays from settings: ${saved.relays}');
+ logger.i('Loading user relays from settings: ${saved.userRelays}');
final loadedRelays = [];
@@ -75,7 +74,7 @@ class RelaysNotifier extends StateNotifier> {
loadedRelays.addAll(userRelaysFromSettings);
state = loadedRelays;
- _logger.i('Loaded ${state.length} relays: ${state.map((r) => '${r.url} (${r.source})').toList()}');
+ logger.i('Loaded ${state.length} relays: ${state.map((r) => '${r.url} (${r.source})').toList()}');
}
Future _saveRelays() async {
@@ -91,7 +90,7 @@ class RelaysNotifier extends StateNotifier> {
// Separate user relays for metadata preservation
final userRelays = state.where((r) => r.source == RelaySource.user).toList();
- _logger.i('Saving ${allActiveRelayUrls.length} active relays (excluding ${blacklistedUrls.length} blacklisted) and ${userRelays.length} user relays metadata');
+ logger.i('Saving ${allActiveRelayUrls.length} active relays (excluding ${blacklistedUrls.length} blacklisted) and ${userRelays.length} user relays metadata');
// Save ALL active relays to settings.relays (NostrService will use these)
await settings.updateRelays(allActiveRelayUrls);
@@ -100,7 +99,7 @@ class RelaysNotifier extends StateNotifier> {
final userRelaysJson = userRelays.map((r) => r.toJson()).toList();
await settings.updateUserRelays(userRelaysJson);
- _logger.i('Relays saved successfully');
+ logger.i('Relays saved successfully');
}
Future addRelay(Relay relay) async {
@@ -380,7 +379,7 @@ class RelaysNotifier extends StateNotifier> {
// Step 5: Remove from blacklist if present (user wants to manually add it)
if (settings.state.blacklistedRelays.contains(normalizedUrl)) {
await settings.removeFromBlacklist(normalizedUrl);
- _logger.i('Removed $normalizedUrl from blacklist - user manually added it');
+ logger.i('Removed $normalizedUrl from blacklist - user manually added it');
}
// Step 6: Add relay as user relay
@@ -424,15 +423,15 @@ class RelaysNotifier extends StateNotifier> {
_handleMostroRelayListUpdate(relayListEvent);
},
onError: (error, stackTrace) {
- _logger.e('Error handling relay list event',
+ logger.e('Error handling relay list event',
error: error, stackTrace: stackTrace);
},
);
// Don't call syncWithMostroInstance() here - it's handled by Future.microtask() in constructor
- _logger.i('Mostro relay sync initialized - sync will start after provider initialization');
+ logger.i('Mostro relay sync initialized - sync will start after provider initialization');
} catch (e, stackTrace) {
- _logger.e('Failed to initialize Mostro relay sync',
+ logger.e('Failed to initialize Mostro relay sync',
error: e, stackTrace: stackTrace);
}
}
@@ -442,11 +441,11 @@ class RelaysNotifier extends StateNotifier> {
try {
final mostroPubkey = settings.state.mostroPublicKey;
if (mostroPubkey.isEmpty) {
- _logger.w('No Mostro pubkey configured, skipping relay sync');
+ logger.w('No Mostro pubkey configured, skipping relay sync');
return;
}
- _logger.i('Syncing relays with Mostro instance: $mostroPubkey');
+ logger.i('Syncing relays with Mostro instance: $mostroPubkey');
// Cancel any existing relay list subscription before creating new one
_subscriptionManager?.unsubscribeFromMostroRelayList();
@@ -460,18 +459,18 @@ class RelaysNotifier extends StateNotifier> {
// Subscribe to the new Mostro instance
_subscriptionManager?.subscribeToMostroRelayList(mostroPubkey);
- _logger.i('Successfully subscribed to relay list events for Mostro: $mostroPubkey');
+ logger.i('Successfully subscribed to relay list events for Mostro: $mostroPubkey');
// Schedule a retry in case the subscription doesn't work immediately
_scheduleRetrySync(mostroPubkey);
} catch (e) {
- _logger.w('Failed to subscribe immediately, will retry later: $e');
+ logger.w('Failed to subscribe immediately, will retry later: $e');
// Schedule a retry even if initial subscription fails
_scheduleRetrySync(mostroPubkey);
}
} catch (e, stackTrace) {
- _logger.e('Failed to sync with Mostro instance',
+ logger.e('Failed to sync with Mostro instance',
error: e, stackTrace: stackTrace);
}
}
@@ -484,11 +483,11 @@ class RelaysNotifier extends StateNotifier> {
_retryTimer = Timer(const Duration(seconds: 10), () async {
try {
if (settings.state.mostroPublicKey == mostroPubkey) {
- _logger.i('Retrying relay sync for Mostro: $mostroPubkey');
+ logger.i('Retrying relay sync for Mostro: $mostroPubkey');
_subscriptionManager?.subscribeToMostroRelayList(mostroPubkey);
}
} catch (e) {
- _logger.w('Retry sync failed: $e');
+ logger.w('Retry sync failed: $e');
} finally {
_retryTimer = null; // Clear reference after execution
}
@@ -505,11 +504,11 @@ class RelaysNotifier extends StateNotifier> {
final nostrService = ref.read(nostrServiceProvider);
// Check if NostrService is actually initialized
if (nostrService.isInitialized) {
- _logger.i('NostrService is ready for relay subscriptions');
+ logger.i('NostrService is ready for relay subscriptions');
return;
}
} catch (e) {
- _logger.w('NostrService not accessible yet, attempt ${attempt + 1}/$maxAttempts: $e');
+ logger.w('NostrService not accessible yet, attempt ${attempt + 1}/$maxAttempts: $e');
}
if (attempt < maxAttempts - 1) {
@@ -517,7 +516,7 @@ class RelaysNotifier extends StateNotifier> {
}
}
- _logger.e('NostrService failed to initialize after $maxAttempts attempts');
+ logger.e('NostrService failed to initialize after $maxAttempts attempts');
throw Exception('NostrService not available for relay synchronization');
}
@@ -528,7 +527,7 @@ class RelaysNotifier extends StateNotifier> {
// Validate that this event is from the currently configured Mostro instance
if (event.authorPubkey != currentMostroPubkey) {
- _logger.w('Ignoring relay list event from wrong Mostro instance. '
+ logger.w('Ignoring relay list event from wrong Mostro instance. '
'Expected: $currentMostroPubkey, Got: ${event.authorPubkey}');
return;
}
@@ -536,7 +535,7 @@ class RelaysNotifier extends StateNotifier> {
// Timestamp validation: ignore events older than the last processed event
if (_lastProcessedEventTime != null &&
event.publishedAt.isBefore(_lastProcessedEventTime!)) {
- _logger.i('Ignoring older relay list event from ${event.publishedAt} '
+ logger.i('Ignoring older relay list event from ${event.publishedAt} '
'(last processed: $_lastProcessedEventTime)');
return;
}
@@ -544,11 +543,11 @@ class RelaysNotifier extends StateNotifier> {
// Hash-based deduplication: ignore identical relay lists
final relayListHash = event.validRelays.join(',');
if (_lastRelayListHash == relayListHash) {
- _logger.i('Relay list unchanged (hash match), skipping update');
+ logger.i('Relay list unchanged (hash match), skipping update');
return;
}
- _logger.i('Received relay list from Mostro ${event.authorPubkey}: ${event.relays}');
+ logger.i('Received relay list from Mostro ${event.authorPubkey}: ${event.relays}');
// Normalize relay URLs to prevent duplicates
final normalizedRelays = event.validRelays
@@ -571,13 +570,13 @@ class RelaysNotifier extends StateNotifier> {
.where((relay) => relay.source == RelaySource.defaultConfig && !blacklistedUrls.contains(_normalizeRelayUrl(relay.url)))
.toList();
- _logger.i('Kept ${updatedRelays.length} default relays and ${userRelays.length} user relays');
+ logger.i('Kept ${updatedRelays.length} default relays and ${userRelays.length} user relays');
// Process Mostro relays from 10002 event
for (final relayUrl in normalizedRelays) {
// Skip if blacklisted by user
if (blacklistedUrls.contains(relayUrl)) {
- _logger.i('Skipping blacklisted Mostro relay: $relayUrl');
+ logger.i('Skipping blacklisted Mostro relay: $relayUrl');
continue;
}
@@ -592,27 +591,27 @@ class RelaysNotifier extends StateNotifier> {
userRelays.removeWhere((r) => _normalizeRelayUrl(r.url) == relayUrl);
final promotedRelay = Relay.fromMostro(relayUrl);
updatedRelays.insert(0, promotedRelay); // Insert at beginning
- _logger.i('Promoted user relay to Mostro relay: $relayUrl');
+ logger.i('Promoted user relay to Mostro relay: $relayUrl');
continue;
}
// Skip if already in updatedRelays (avoid duplicates with default relays)
if (updatedRelays.any((r) => _normalizeRelayUrl(r.url) == relayUrl)) {
- _logger.i('Skipping duplicate relay: $relayUrl');
+ logger.i('Skipping duplicate relay: $relayUrl');
continue;
}
// Add new Mostro relay
final mostroRelay = Relay.fromMostro(relayUrl);
updatedRelays.add(mostroRelay);
- _logger.i('Added Mostro relay: $relayUrl');
+ logger.i('Added Mostro relay: $relayUrl');
}
// Remove Mostro relays that are no longer in the 10002 event (ELIMINATION case)
final currentMostroRelays = state.where((relay) => relay.source == RelaySource.mostro).toList();
for (final mostroRelay in currentMostroRelays) {
if (!normalizedRelays.contains(_normalizeRelayUrl(mostroRelay.url))) {
- _logger.i('Removing Mostro relay no longer in 10002: ${mostroRelay.url}');
+ logger.i('Removing Mostro relay no longer in 10002: ${mostroRelay.url}');
// Relay is eliminated completely - no reverting to user relay
}
}
@@ -625,14 +624,14 @@ class RelaysNotifier extends StateNotifier> {
!finalRelays.every((relay) => state.contains(relay))) {
state = finalRelays;
await _saveRelays();
- _logger.i('Updated relay list with ${finalRelays.length} relays (${blacklistedUrls.length} blacklisted)');
+ logger.i('Updated relay list with ${finalRelays.length} relays (${blacklistedUrls.length} blacklisted)');
}
// Update tracking variables after successful processing
_lastProcessedEventTime = event.publishedAt;
_lastRelayListHash = relayListHash;
} catch (e, stackTrace) {
- _logger.e('Error handling Mostro relay list update',
+ logger.e('Error handling Mostro relay list update',
error: e, stackTrace: stackTrace);
}
}
@@ -644,17 +643,17 @@ class RelaysNotifier extends StateNotifier> {
final relay = state.firstWhere((r) => r.url == url, orElse: () => Relay(url: ''));
if (relay.url.isEmpty) {
- _logger.w('Attempted to remove non-existent relay: $url');
+ logger.w('Attempted to remove non-existent relay: $url');
return;
}
// Blacklist all relays to prevent re-addition during sync
await settings.addToBlacklist(url);
- _logger.i('Blacklisted ${relay.source} relay: $url');
+ logger.i('Blacklisted ${relay.source} relay: $url');
// Remove relay from current state
await removeRelay(url);
- _logger.i('Removed relay: $url (source: ${relay.source})');
+ logger.i('Removed relay: $url (source: ${relay.source})');
}
// Removed removeRelayWithSource - no longer needed since all relays are managed via blacklist
@@ -674,14 +673,14 @@ class RelaysNotifier extends StateNotifier> {
currentPubkey != null &&
newPubkey.isNotEmpty &&
currentPubkey!.isNotEmpty) {
- _logger.i('Detected REAL Mostro pubkey change: $currentPubkey -> $newPubkey');
+ logger.i('Detected REAL Mostro pubkey change: $currentPubkey -> $newPubkey');
currentPubkey = newPubkey;
// 🔥 RESET COMPLETO: Limpiar todos los relays y hacer sync fresco
_cleanAllRelaysAndResync();
} else if (newPubkey != currentPubkey) {
// Just update the tracking variable without reset (initial load)
- _logger.i('Initial Mostro pubkey load: $newPubkey');
+ logger.i('Initial Mostro pubkey load: $newPubkey');
currentPubkey = newPubkey;
syncWithMostroInstance();
}
@@ -691,14 +690,14 @@ class RelaysNotifier extends StateNotifier> {
/// Clean all relays (except default) and perform fresh sync with new Mostro
Future _cleanAllRelaysAndResync() async {
try {
- _logger.i('Cleaning all relays and performing fresh sync...');
+ logger.i('Cleaning all relays and performing fresh sync...');
// CLEAR ALL relays (only keep default)
final defaultRelay = Relay.fromDefault('wss://relay.mostro.network');
state = [defaultRelay];
await _saveRelays();
- _logger.i('Reset to default relay only, starting fresh sync');
+ logger.i('Reset to default relay only, starting fresh sync');
// Reset hash and timestamp for completely fresh sync with new Mostro
_lastRelayListHash = null;
@@ -708,7 +707,7 @@ class RelaysNotifier extends StateNotifier> {
await syncWithMostroInstance();
} catch (e, stackTrace) {
- _logger.e('Error during relay cleanup and resync',
+ logger.e('Error during relay cleanup and resync',
error: e, stackTrace: stackTrace);
}
}
@@ -784,7 +783,7 @@ class RelaysNotifier extends StateNotifier> {
final wouldBeBlacklisted = [...currentBlacklist, urlToBlacklist];
final wouldRemainActive = currentActiveRelays.where((url) => !wouldBeBlacklisted.contains(url)).toList();
- _logger.d('Current active: ${currentActiveRelays.length}, Would remain: ${wouldRemainActive.length}');
+ logger.d('Current active: ${currentActiveRelays.length}, Would remain: ${wouldRemainActive.length}');
return wouldRemainActive.isEmpty;
}
@@ -797,7 +796,7 @@ class RelaysNotifier extends StateNotifier> {
if (isCurrentlyBlacklisted) {
// Remove from blacklist and trigger sync to add back
await settings.removeFromBlacklist(url);
- _logger.i('Removed $url from blacklist, triggering re-sync');
+ logger.i('Removed $url from blacklist, triggering re-sync');
// Reset hash to allow re-processing of the same relay list with updated blacklist context
_lastRelayListHash = null;
@@ -807,14 +806,14 @@ class RelaysNotifier extends StateNotifier> {
// Add to blacklist and remove from current state
await settings.addToBlacklist(url);
await removeRelay(url);
- _logger.i('Blacklisted and removed Mostro relay: $url');
+ logger.i('Blacklisted and removed Mostro relay: $url');
}
}
/// Clear all blacklisted relays and trigger re-sync
Future clearBlacklistAndResync() async {
await settings.clearBlacklist();
- _logger.i('Cleared blacklist, triggering relay re-sync');
+ logger.i('Cleared blacklist, triggering relay re-sync');
// Reset hash to allow re-processing of relay lists with cleared blacklist
_lastRelayListHash = null;
@@ -839,7 +838,7 @@ class RelaysNotifier extends StateNotifier> {
final removedCount = state.length - cleanedRelays.length;
state = cleanedRelays;
await _saveRelays();
- _logger.i('Cleaned $removedCount Mostro relays from state');
+ logger.i('Cleaned $removedCount Mostro relays from state');
}
}
diff --git a/lib/features/restore/restore_manager.dart b/lib/features/restore/restore_manager.dart
index ce1f7603..e8daf932 100644
--- a/lib/features/restore/restore_manager.dart
+++ b/lib/features/restore/restore_manager.dart
@@ -5,7 +5,7 @@ import 'package:dart_nostr/nostr/model/event/event.dart';
import 'package:dart_nostr/nostr/model/request/filter.dart';
import 'package:dart_nostr/nostr/model/request/request.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/data/models/enums/action.dart';
import 'package:mostro_mobile/data/models/enums/role.dart';
import 'package:mostro_mobile/data/models/enums/order_type.dart';
@@ -45,7 +45,6 @@ enum RestoreStage {
class RestoreService {
final Ref ref;
- final Logger _logger = Logger();
StreamSubscription? _tempSubscription;
Completer? _currentCompleter;
RestoreStage _currentStage = RestoreStage.gettingRestoreData;
@@ -55,12 +54,12 @@ class RestoreService {
RestoreService(this.ref);
Future importMnemonicAndRestore(String mnemonic) async {
- _logger.i('Restore: importing mnemonic');
+ logger.i('Restore: importing mnemonic');
// Import the mnemonic - this saves to storage
final keyManager = ref.read(keyManagerProvider);
await keyManager.importMnemonic(mnemonic);
- _logger.i('Restore: mnemonic imported and saved to storage');
+ logger.i('Restore: mnemonic imported and saved to storage');
// Invalidate keyManagerProvider to force re-initialization
// This ensures all providers get a fresh instance with the new key
@@ -75,7 +74,7 @@ class RestoreService {
Future _clearAll() async {
try {
- _logger.i('Restore: clearing all existing data before restore');
+ logger.i('Restore: clearing all existing data before restore');
await ref.read(sessionNotifierProvider.notifier).reset();
await ref.read(mostroStorageProvider).deleteAll();
await ref.read(eventStorageProvider).deleteAll();
@@ -83,7 +82,7 @@ class RestoreService {
ref.read(orderRepositoryProvider).clearCache();
} catch (e) {
- _logger.w('Restore: cleanup error', error: e);
+ logger.w('Restore: cleanup error', error: e);
}
}
@@ -98,10 +97,10 @@ class RestoreService {
throw TimeoutException('Stage $stage timed out after ${timeout.inSeconds}s');
},
);
- _logger.i('Restore: stage $_currentStage completed - Event: ${event.id}');
+ logger.i('Restore: stage $_currentStage completed - Event: ${event.id}');
return event;
} catch (e) {
- _logger.e('Restore: stage $_currentStage failed', error: e);
+ logger.e('Restore: stage $_currentStage failed', error: e);
rethrow;
}
}
@@ -131,17 +130,17 @@ class RestoreService {
final subscription = stream.listen(
_handleTempSubscriptionsResponse,
onError: (error, stackTrace) {
- _logger.e('Restore: subscription error', error: error, stackTrace: stackTrace);
+ logger.e('Restore: subscription error', error: error, stackTrace: stackTrace);
},
cancelOnError: false,
);
- _logger.i('Restore: temporary subscription created');
+ logger.i('Restore: temporary subscription created');
return subscription;
}
Future _sendRestoreRequest() async {
- _logger.i('Restore: sending restore data request');
+ logger.i('Restore: sending restore data request');
if (_tempTradeKey == null && _masterKey == null) {
throw Exception('Temp trade key or master key not initialized');
@@ -163,7 +162,7 @@ class RestoreService {
);
await ref.read(nostrServiceProvider).publishEvent(wrappedEvent);
- _logger.i('Restore: request sent successfully');
+ logger.i('Restore: request sent successfully');
}
//Extracts restore data, returns:
@@ -187,7 +186,7 @@ class RestoreService {
// Check if Mostro returned cant-do (not found)
if (messageData.containsKey('cant-do')) {
- _logger.w('Restore: Mostro returned cant-do for restore data (no orders found)');
+ logger.w('Restore: Mostro returned cant-do for restore data (no orders found)');
return (ordersMap: {}, disputes: []);
}
@@ -195,14 +194,14 @@ class RestoreService {
final restoreWrapper = messageData['restore'] as Map?;
if (restoreWrapper == null) {
- _logger.w('Restore: no restore wrapper found, returning empty orders');
+ logger.w('Restore: no restore wrapper found, returning empty orders');
return (ordersMap: {}, disputes: []);
}
final payload = restoreWrapper['payload'] as Map?;
if (payload == null) {
- _logger.w('Restore: no payload found in restore wrapper, returning empty orders');
+ logger.w('Restore: no payload found in restore wrapper, returning empty orders');
return (ordersMap: {}, disputes: []);
}
@@ -223,13 +222,13 @@ class RestoreService {
return (ordersMap: ordersMap, disputes: disputesList);
} catch (e, stack) {
- _logger.e('Restore: failed to extract restore data', error: e, stackTrace: stack);
+ logger.e('Restore: failed to extract restore data', error: e, stackTrace: stack);
rethrow;
}
}
Future _sendOrdersDetailsRequest(List orderIds) async {
- _logger.i('Restore: sending orders details request for ${orderIds.length} orders');
+ logger.i('Restore: sending orders details request for ${orderIds.length} orders');
if (_tempTradeKey == null && _masterKey == null) {
throw Exception('Temp trade key or master key not initialized');
@@ -251,13 +250,13 @@ class RestoreService {
);
await ref.read(nostrServiceProvider).publishEvent(wrappedEvent);
- _logger.i('Restore: orders details request sent successfully');
+ logger.i('Restore: orders details request sent successfully');
}
//Extracts orders details from gift wrap event, returns OrdersResponse
Future _extractOrdersDetails(NostrEvent event) async {
try {
- _logger.i('Restore: extracting orders details from gift wrap event ${event.id}');
+ logger.i('Restore: extracting orders details from gift wrap event ${event.id}');
if (_tempTradeKey == null) {
throw Exception('Temp trade key not initialized');
@@ -280,17 +279,17 @@ class RestoreService {
final ordersResponse = OrdersResponse.fromJson(payload);
- _logger.i('Restore: found ${ordersResponse.orders.length} order details');
+ logger.i('Restore: found ${ordersResponse.orders.length} order details');
return ordersResponse;
} catch (e, stack) {
- _logger.e('Restore: failed to extract orders details', error: e, stackTrace: stack);
+ logger.e('Restore: failed to extract orders details', error: e, stackTrace: stack);
rethrow;
}
}
Future _sendLastTradeIndexRequest() async {
- _logger.i('Restore: sending last trade index request');
+ logger.i('Restore: sending last trade index request');
if (_tempTradeKey == null && _masterKey == null) {
throw Exception('Temp trade key or master key not initialized');
@@ -312,12 +311,12 @@ class RestoreService {
);
await ref.read(nostrServiceProvider).publishEvent(wrappedEvent);
- _logger.i('Restore: last trade index request sent successfully');
+ logger.i('Restore: last trade index request sent successfully');
}
Future _extractLastTradeIndex(NostrEvent event) async {
try {
- _logger.i('Restore: extracting last trade index from gift wrap event ${event.id}');
+ logger.i('Restore: extracting last trade index from gift wrap event ${event.id}');
if (_tempTradeKey == null) {
throw Exception('Temp trade key not initialized');
@@ -334,7 +333,7 @@ class RestoreService {
// Check if Mostro returned cant-do (not found)
if (messageData.containsKey('cant-do')) {
- _logger.w('Restore: Mostro returned cant-do for last trade index, defaulting to 0');
+ logger.w('Restore: Mostro returned cant-do for last trade index, defaulting to 0');
return LastTradeIndexResponse(tradeIndex: 0);
}
@@ -342,17 +341,17 @@ class RestoreService {
final restoreWrapper = messageData['restore'] as Map?;
if (restoreWrapper == null) {
- _logger.w('Restore: no restore wrapper found, defaulting trade index to 0');
+ logger.w('Restore: no restore wrapper found, defaulting trade index to 0');
return LastTradeIndexResponse(tradeIndex: 0);
}
final response = LastTradeIndexResponse.fromJson(restoreWrapper);
- _logger.i('Restore: last trade index is ${response.tradeIndex}');
+ logger.i('Restore: last trade index is ${response.tradeIndex}');
return response;
} catch (e, stack) {
- _logger.e('Restore: failed to extract last trade index', error: e, stackTrace: stack);
+ logger.e('Restore: failed to extract last trade index', error: e, stackTrace: stack);
rethrow;
}
}
@@ -382,7 +381,7 @@ class RestoreService {
}
if (!sessionMatchesOrder) {
- _logger.w(
+ logger.w(
'Restore: session pubkey mismatch for order ${order.id} - '
'session role: $sessionRole, session pubkey: $sessionPubkey, '
'buyer pubkey: ${order.buyerTradePubkey}, seller pubkey: ${order.sellerTradePubkey}'
@@ -465,7 +464,7 @@ class RestoreService {
// Enable restore mode to block all old message processing
ref.read(isRestoringProvider.notifier).state = true;
- _logger.i('Restore: enabled restore mode - blocking all old message processing');
+ logger.i('Restore: enabled restore mode - blocking all old message processing');
// Restore each a session to get future messages
for (final entry in ordersIds.entries) {
@@ -508,13 +507,13 @@ class RestoreService {
}
// Wait for historical messages to arrive and be saved to storage
- _logger.i('Restore: waiting 8 seconds for historical messages to be saved...');
+ logger.i('Restore: waiting 8 seconds for historical messages to be saved...');
//WARNING: It is very important to wait here to ensure all historical messages arrive before rebuilding state
// Relays could send them with delay
await Future.delayed(const Duration(seconds: 8));
// Build MostroMessages from ordersResponse and update state (source of truth from Mostro)
- _logger.i('Restore: building messages for ${ordersResponse.orders.length} orders from ordersResponse');
+ logger.i('Restore: building messages for ${ordersResponse.orders.length} orders from ordersResponse');
final storage = ref.read(mostroStorageProvider);
// Process each order detail
@@ -552,7 +551,7 @@ class RestoreService {
// We need the session to compare trade indexes
bool userInitiated = false;
if (session == null) {
- _logger.w('Restore: no session found for disputed order ${orderDetail.id}, defaulting to peer-initiated');
+ logger.w('Restore: no session found for disputed order ${orderDetail.id}, defaulting to peer-initiated');
action = Action.disputeInitiatedByPeer;
} else {
// Determine if user initiated with double verification TODO : improve if protocol changes
@@ -578,7 +577,7 @@ class RestoreService {
action: userInitiated ? 'dispute-initiated-by-you' : 'dispute-initiated-by-peer',
);
- _logger.i('Restore: dispute found for order ${orderDetail.id}');
+ logger.i('Restore: dispute found for order ${orderDetail.id}');
} else {
// Regular order without dispute
final session = ref.read(sessionNotifierProvider.notifier).getSessionByOrderId(orderDetail.id);
@@ -605,23 +604,23 @@ class RestoreService {
// If dispute exists, update state with dispute object using public method
if (dispute != null) {
notifier.updateDispute(dispute);
- _logger.i('Restore: added dispute to state for order ${orderDetail.id}');
+ logger.i('Restore: added dispute to state for order ${orderDetail.id}');
}
} catch (e, stack) {
- _logger.e('Restore: failed to process order ${orderDetail.id}', error: e, stackTrace: stack);
+ logger.e('Restore: failed to process order ${orderDetail.id}', error: e, stackTrace: stack);
}
}
- _logger.i('Restore: state update completed for all orders');
+ logger.i('Restore: state update completed for all orders');
// Disable restore mode - back to normal message processing
ref.read(isRestoringProvider.notifier).state = false;
- _logger.i('Restore: disabled restore mode - re-enabling message processing');
+ logger.i('Restore: disabled restore mode - re-enabling message processing');
} catch (e, stack) {
// Ensure flag is cleared even on error
ref.read(isRestoringProvider.notifier).state = false;
- _logger.e('Restore: error during restore', error: e, stackTrace: stack);
+ logger.e('Restore: error during restore', error: e, stackTrace: stack);
rethrow;
}
}
@@ -645,22 +644,22 @@ class RestoreService {
// Validate and initialize master key
final keyManager = ref.read(keyManagerProvider);
if (keyManager.masterKeyPair == null) {
- _logger.e('Restore: master key not found after import');
+ logger.e('Restore: master key not found after import');
throw Exception('Master key not found');
}
_masterKey = keyManager.masterKeyPair;
- _logger.i('Restore: initialized master key');
+ logger.i('Restore: initialized master key');
// Validate Mostro public key
final settings = ref.read(settingsProvider);
if (settings.mostroPublicKey.isEmpty) {
- _logger.e('Restore: Mostro not configured');
+ logger.e('Restore: Mostro not configured');
throw Exception('Mostro not configured');
}
// Initialize temporary trade key (index 1) for entire restore process
_tempTradeKey = await keyManager.deriveTradeKeyFromIndex(1);
- _logger.i('Restore: initialized temp trade key with pubkey ${_tempTradeKey!.public}');
+ logger.i('Restore: initialized temp trade key with pubkey ${_tempTradeKey!.public}');
// Subscribe to temporary notifications
_tempSubscription = await _createTempSubscription();
@@ -675,7 +674,7 @@ class RestoreService {
progress.setOrdersReceived(ordersMap.length);
if (ordersMap.isEmpty) {
- _logger.w('Restore: no orders or disputes to restore');
+ logger.w('Restore: no orders or disputes to restore');
await _sendLastTradeIndexRequest();
final lastTradeIndexEvent = await _waitForEvent(RestoreStage.gettingTradeIndex);
final lastTradeIndexResponse = await _extractLastTradeIndex(lastTradeIndexEvent);
@@ -688,7 +687,7 @@ class RestoreService {
// STAGE 2: Getting Orders Details
progress.updateStep(RestoreStep.loadingDetails);
final ordersIdsList = ordersMap.keys.toList();
- _logger.i('Restore: requesting details for ${ordersIdsList.length} orders: $ordersIdsList');
+ logger.i('Restore: requesting details for ${ordersIdsList.length} orders: $ordersIdsList');
await _sendOrdersDetailsRequest(ordersIdsList);
final ordersDetailsEvent = await _waitForEvent(RestoreStage.gettingOrdersDetails);
final ordersResponse = await _extractOrdersDetails(ordersDetailsEvent);
@@ -716,11 +715,11 @@ class RestoreService {
notifProvider.clearAll();
} catch (e, stack) {
- _logger.e('Restore: error during restore process', error: e, stackTrace: stack);
+ logger.e('Restore: error during restore process', error: e, stackTrace: stack);
ref.read(restoreProgressProvider.notifier).showError('');
} finally {
// Cleanup: always cancel subscription and clear keys
- _logger.i('Restore: cleaning up subscription and keys');
+ logger.i('Restore: cleaning up subscription and keys');
await _tempSubscription?.cancel();
_tempSubscription = null;
_currentCompleter = null;
diff --git a/lib/features/restore/restore_progress_notifier.dart b/lib/features/restore/restore_progress_notifier.dart
index 0ee62279..706918fd 100644
--- a/lib/features/restore/restore_progress_notifier.dart
+++ b/lib/features/restore/restore_progress_notifier.dart
@@ -1,17 +1,17 @@
import 'dart:async';
import 'package:flutter_riverpod/flutter_riverpod.dart';
-import 'package:logger/logger.dart';
+import 'package:mostro_mobile/services/logger_service.dart';
import 'package:mostro_mobile/features/restore/restore_progress_state.dart';
class RestoreProgressNotifier extends StateNotifier {
- final _logger = Logger();
+
Timer? _timeoutTimer;
static const _maxTimeout = Duration(seconds: 30);
RestoreProgressNotifier() : super(RestoreProgressState.initial());
void startRestore() {
- _logger.i('Starting restore overlay');
+ logger.i('Starting restore overlay');
state = RestoreProgressState.initial().copyWith(
isVisible: true,
step: RestoreStep.requesting,
@@ -21,7 +21,7 @@ class RestoreProgressNotifier extends StateNotifier {
}
void updateStep(RestoreStep step, {int? current, int? total}) {
- _logger.i('Restore step: $step (${current ?? 0}/${total ?? 0})');
+ logger.i('Restore step: $step (${current ?? 0}/${total ?? 0})');
state = state.copyWith(
step: step,
currentProgress: current ?? state.currentProgress,
@@ -32,7 +32,7 @@ class RestoreProgressNotifier extends StateNotifier {
}
void setOrdersReceived(int count) {
- _logger.i('Received $count orders');
+ logger.i('Received $count orders');
state = state.copyWith(
step: RestoreStep.receivingOrders,
totalProgress: count,
@@ -51,7 +51,7 @@ class RestoreProgressNotifier extends StateNotifier {
}
void completeRestore() {
- _logger.i('Restore completed successfully');
+ logger.i('Restore completed successfully');
_cancelTimeoutTimer();
state = state.copyWith(
@@ -67,7 +67,7 @@ class RestoreProgressNotifier extends StateNotifier {
}
void showError(String message) {
- _logger.w('Restore error: $message');
+ logger.w('Restore error: $message');
_cancelTimeoutTimer();
state = state.copyWith(
@@ -84,7 +84,7 @@ class RestoreProgressNotifier extends StateNotifier {
}
void hide() {
- _logger.i('Hiding restore overlay');
+ logger.i('Hiding restore overlay');
_cancelTimeoutTimer();
state = RestoreProgressState.initial();
}
@@ -93,7 +93,7 @@ class RestoreProgressNotifier extends StateNotifier {
_cancelTimeoutTimer();
_timeoutTimer = Timer(_maxTimeout, () {
if (mounted && state.isVisible) {
- _logger.w('Restore timeout - auto-hiding overlay');
+ logger.w('Restore timeout - auto-hiding overlay');
showError('Request timeout');
}
});
diff --git a/lib/features/settings/settings.dart b/lib/features/settings/settings.dart
index bba1a8bd..cd46d8f3 100644
--- a/lib/features/settings/settings.dart
+++ b/lib/features/settings/settings.dart
@@ -7,6 +7,7 @@ class Settings {
final String? defaultLightningAddress;
final List blacklistedRelays; // Relays blocked by user from auto-sync
final List