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> userRelays; // User-added relays with metadata + final String? customLogStorageDirectory; // Custom directory for log exports Settings({ required this.relays, @@ -17,6 +18,7 @@ class Settings { this.defaultLightningAddress, this.blacklistedRelays = const [], this.userRelays = const [], + this.customLogStorageDirectory, }); Settings copyWith({ @@ -29,6 +31,7 @@ class Settings { bool clearDefaultLightningAddress = false, List? blacklistedRelays, List>? userRelays, + String? customLogStorageDirectory, }) { return Settings( relays: relays ?? this.relays, @@ -36,11 +39,12 @@ class Settings { mostroPublicKey: mostroPublicKey ?? this.mostroPublicKey, defaultFiatCode: defaultFiatCode ?? this.defaultFiatCode, selectedLanguage: selectedLanguage ?? this.selectedLanguage, - defaultLightningAddress: clearDefaultLightningAddress - ? null + defaultLightningAddress: clearDefaultLightningAddress + ? null : (defaultLightningAddress ?? this.defaultLightningAddress), blacklistedRelays: blacklistedRelays ?? this.blacklistedRelays, userRelays: userRelays ?? this.userRelays, + customLogStorageDirectory: customLogStorageDirectory ?? this.customLogStorageDirectory, ); } @@ -53,6 +57,7 @@ class Settings { 'defaultLightningAddress': defaultLightningAddress, 'blacklistedRelays': blacklistedRelays, 'userRelays': userRelays, + 'customLogStorageDirectory': customLogStorageDirectory, }; factory Settings.fromJson(Map json) { @@ -66,6 +71,7 @@ class Settings { blacklistedRelays: (json['blacklistedRelays'] as List?)?.cast() ?? [], userRelays: (json['userRelays'] as List?) ?.cast>() ?? [], + customLogStorageDirectory: json['customLogStorageDirectory'], ); } } diff --git a/lib/features/settings/settings_notifier.dart b/lib/features/settings/settings_notifier.dart index 206a4ac2..06bf7762 100644 --- a/lib/features/settings/settings_notifier.dart +++ b/lib/features/settings/settings_notifier.dart @@ -1,6 +1,6 @@ import 'dart:convert'; 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/models/enums/storage_keys.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; @@ -9,7 +9,7 @@ import 'package:shared_preferences/shared_preferences.dart'; class SettingsNotifier extends StateNotifier { final SharedPreferencesAsync _prefs; final Ref? ref; - final _logger = Logger(); + static final String _storageKey = SharedPreferencesKeys.appSettings.value; SettingsNotifier(this._prefs, {this.ref}) : super(_defaultSettings()); @@ -50,7 +50,7 @@ class SettingsNotifier extends StateNotifier { final oldPubkey = state.mostroPublicKey; if (oldPubkey != newValue) { - _logger.i('Mostro change detected: $oldPubkey → $newValue'); + logger.i('Mostro change detected: $oldPubkey → $newValue'); // COMPLETE RESET: Clear blacklist and user relays when changing Mostro state = state.copyWith( @@ -59,7 +59,7 @@ class SettingsNotifier extends StateNotifier { userRelays: const [], // User relays vacíos ); - _logger.i('Reset blacklist and user relays for new Mostro instance'); + logger.i('Reset blacklist and user relays for new Mostro instance'); } else { // Only update pubkey if it's the same (without reset) state = state.copyWith(mostroPublicKey: newValue); @@ -97,7 +97,7 @@ class SettingsNotifier extends StateNotifier { currentBlacklist.add(normalized); state = state.copyWith(blacklistedRelays: currentBlacklist); await _saveToPrefs(); - _logger.i('Added relay to blacklist: $normalized'); + logger.i('Added relay to blacklist: $normalized'); } } @@ -108,7 +108,7 @@ class SettingsNotifier extends StateNotifier { if (currentBlacklist.remove(normalized)) { state = state.copyWith(blacklistedRelays: currentBlacklist); await _saveToPrefs(); - _logger.i('Removed relay from blacklist: $normalized'); + logger.i('Removed relay from blacklist: $normalized'); } } @@ -133,7 +133,7 @@ class SettingsNotifier extends StateNotifier { if (state.blacklistedRelays.isNotEmpty) { state = state.copyWith(blacklistedRelays: const []); await _saveToPrefs(); - _logger.i('Cleared all blacklisted relays'); + logger.i('Cleared all blacklisted relays'); } } @@ -141,7 +141,7 @@ class SettingsNotifier extends StateNotifier { Future updateUserRelays(List> newUserRelays) async { state = state.copyWith(userRelays: newUserRelays); await _saveToPrefs(); - _logger.i('Updated user relays: ${newUserRelays.length} relays'); + logger.i('Updated user relays: ${newUserRelays.length} relays'); } Future _saveToPrefs() async { diff --git a/lib/features/settings/settings_screen.dart b/lib/features/settings/settings_screen.dart index a078a8f1..c30e0518 100644 --- a/lib/features/settings/settings_screen.dart +++ b/lib/features/settings/settings_screen.dart @@ -100,6 +100,10 @@ class _SettingsScreenState extends ConsumerState { _buildRelaysCard(context), const SizedBox(height: 16), + // Dev Tools Card + _buildDevToolsCard(context), + const SizedBox(height: 16), + // Mostro Card _buildMostroCard(context, _mostroTextController), const SizedBox(height: 16), @@ -492,6 +496,104 @@ class _SettingsScreenState extends ConsumerState { ); } + Widget _buildDevToolsCard(BuildContext context) { + return Container( + decoration: BoxDecoration( + color: AppTheme.backgroundCard, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: Colors.white.withValues(alpha: 0.1)), + ), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + const Icon( + Icons.bug_report_outlined, + color: AppTheme.textSecondary, + size: 20, + ), + const SizedBox(width: 8), + Text( + S.of(context)!.devTools, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 18, + fontWeight: FontWeight.w600, + ), + ), + ], + ), + const SizedBox(height: 8), + Text( + S.of(context)!.devToolsWarning, + style: const TextStyle( + color: AppTheme.textSecondary, + fontSize: 13, + fontWeight: FontWeight.w500, + ), + ), + const SizedBox(height: 16), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => context.push('/logs'), + borderRadius: BorderRadius.circular(8), + child: Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.backgroundInput, + borderRadius: BorderRadius.circular(8), + border: Border.all(color: Colors.white.withValues(alpha: 0.1)), + ), + child: Row( + children: [ + const Icon( + LucideIcons.fileText, + color: AppTheme.activeColor, + size: 20, + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + S.of(context)!.logsReport, + style: const TextStyle( + color: AppTheme.textPrimary, + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 4), + Text( + S.of(context)!.viewAndExportLogs, + style: const TextStyle( + color: AppTheme.textSecondary, + fontSize: 13, + ), + ), + ], + ), + ), + const Icon( + Icons.chevron_right, + color: AppTheme.textSecondary, + ), + ], + ), + ), + ), + ), + ], + ), + ), + ); + } + Widget _buildCurrencySelector(BuildContext context) { final currenciesAsync = ref.watch(currencyCodesProvider); final settings = ref.watch(settingsProvider); diff --git a/lib/features/subscriptions/subscription_manager.dart b/lib/features/subscriptions/subscription_manager.dart index 7909db27..abe336f7 100644 --- a/lib/features/subscriptions/subscription_manager.dart +++ b/lib/features/subscriptions/subscription_manager.dart @@ -2,7 +2,7 @@ import 'dart:async'; 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/core/models/relay_list_event.dart'; import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/features/subscriptions/subscription.dart'; @@ -18,7 +18,7 @@ import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; class SubscriptionManager { final Ref ref; final Map _subscriptions = {}; - final _logger = Logger(); + late final ProviderSubscription _sessionListener; final _ordersController = StreamController.broadcast(); @@ -44,7 +44,7 @@ class SubscriptionManager { }, fireImmediately: false, onError: (error, stackTrace) { - _logger.e('Error in session listener', + logger.e('Error in session listener', error: error, stackTrace: stackTrace); }, ); @@ -60,20 +60,20 @@ class SubscriptionManager { try { final existingSessions = ref.read(sessionNotifierProvider); if (existingSessions.isNotEmpty) { - _logger.i('Initializing subscriptions for ${existingSessions.length} existing sessions'); + logger.i('Initializing subscriptions for ${existingSessions.length} existing sessions'); _updateAllSubscriptions(existingSessions); } else { - _logger.i('No existing sessions found during SubscriptionManager initialization'); + logger.i('No existing sessions found during SubscriptionManager initialization'); } } catch (e, stackTrace) { - _logger.e('Error initializing existing sessions', + logger.e('Error initializing existing sessions', error: e, stackTrace: stackTrace); } } void _updateAllSubscriptions(List sessions) { if (sessions.isEmpty) { - _logger.i('No sessions available, clearing all subscriptions'); + logger.i('No sessions available, clearing all subscriptions'); _clearAllSubscriptions(); return; } @@ -91,7 +91,7 @@ class SubscriptionManager { void _updateSubscription(SubscriptionType type, List sessions) { if (sessions.isEmpty) { - _logger.i('No sessions for $type subscription'); + logger.i('No sessions for $type subscription'); unsubscribeByType(type); return; } @@ -108,10 +108,10 @@ class SubscriptionManager { filter: filter, ); - _logger + logger .i('Subscription created for $type with ${sessions.length} sessions'); } catch (e, stackTrace) { - _logger.e('Failed to create $type subscription', + logger.e('Failed to create $type subscription', error: e, stackTrace: stackTrace); } } @@ -164,7 +164,7 @@ class SubscriptionManager { break; } } catch (e, stackTrace) { - _logger.e('Error handling $type event', error: e, stackTrace: stackTrace); + logger.e('Error handling $type event', error: e, stackTrace: stackTrace); } } @@ -182,7 +182,7 @@ class SubscriptionManager { final streamSubscription = stream.listen( (event) => _handleEvent(type, event), onError: (error, stackTrace) { - _logger.e('Error in $type subscription', + logger.e('Error in $type subscription', error: error, stackTrace: stackTrace); }, cancelOnError: false, @@ -270,9 +270,9 @@ class SubscriptionManager { _subscribeToRelayList(filter); - _logger.i('Subscribed to relay list for Mostro: $mostroPubkey'); + logger.i('Subscribed to relay list for Mostro: $mostroPubkey'); } catch (e, stackTrace) { - _logger.e('Failed to subscribe to Mostro relay list', + logger.e('Failed to subscribe to Mostro relay list', error: e, stackTrace: stackTrace); } } @@ -295,7 +295,7 @@ class SubscriptionManager { } }, onError: (error, stackTrace) { - _logger.e('Error in relay list subscription', + logger.e('Error in relay list subscription', error: error, stackTrace: stackTrace); }, cancelOnError: false, @@ -320,7 +320,7 @@ class SubscriptionManager { /// Unsubscribes from Mostro relay list events void unsubscribeFromMostroRelayList() { unsubscribeByType(SubscriptionType.relayList); - _logger.i('Unsubscribed from Mostro relay list'); + logger.i('Unsubscribed from Mostro relay list'); } void dispose() { diff --git a/lib/features/trades/providers/trades_provider.dart b/lib/features/trades/providers/trades_provider.dart index 37fe2008..9e78992c 100644 --- a/lib/features/trades/providers/trades_provider.dart +++ b/lib/features/trades/providers/trades_provider.dart @@ -1,7 +1,7 @@ import 'dart:math' as math; import 'package:dart_nostr/nostr/model/event/event.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/status.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/features/order/models/order_state.dart'; @@ -9,8 +9,6 @@ import 'package:mostro_mobile/features/order/providers/order_notifier_provider.d import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; import 'package:mostro_mobile/shared/providers/session_notifier_provider.dart'; -final _logger = Logger(); - // Status filter provider - holds the currently selected status filter final statusFilterProvider = StateProvider((ref) => null); @@ -21,13 +19,13 @@ final filteredTradesWithOrderStateProvider = final sessions = ref.watch(sessionNotifierProvider); final selectedStatusFilter = ref.watch(statusFilterProvider); - _logger.d( + logger.d( 'Filtering trades with OrderState: Orders state=${allOrdersAsync.toString().substring(0, math.min(100, allOrdersAsync.toString().length))}, Sessions count=${sessions.length}, Status filter=${selectedStatusFilter?.value}'); return allOrdersAsync.when( data: (allOrders) { final orderIds = sessions.map((s) => s.orderId).toSet(); - _logger + logger .d('Got ${allOrders.length} orders and ${orderIds.length} sessions'); // Make a copy to avoid modifying the original list @@ -47,7 +45,7 @@ final filteredTradesWithOrderStateProvider = final orderState = ref.watch(orderNotifierProvider(order.orderId!)); orderStates[order.orderId!] = orderState; } catch (e) { - _logger.w('Could not watch OrderState for ${order.orderId}: $e'); + logger.w('Could not watch OrderState for ${order.orderId}: $e'); // Skip this order if we can't get its state } } @@ -69,15 +67,15 @@ final filteredTradesWithOrderStateProvider = } final result = filtered.toList(); - _logger.d('Filtered to ${result.length} trades'); + logger.d('Filtered to ${result.length} trades'); return AsyncValue.data(result); }, loading: () { - _logger.d('Orders loading'); + logger.d('Orders loading'); return const AsyncValue.loading(); }, error: (error, stackTrace) { - _logger.e('Error filtering trades: $error'); + logger.e('Error filtering trades: $error'); return AsyncValue.error(error, stackTrace); }, ); diff --git a/lib/features/trades/widgets/status_filter_widget.dart b/lib/features/trades/widgets/status_filter_widget.dart index c5fb0362..1e92db2d 100644 --- a/lib/features/trades/widgets/status_filter_widget.dart +++ b/lib/features/trades/widgets/status_filter_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logger/logger.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:lucide_icons/lucide_icons.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; @@ -9,9 +9,6 @@ import 'package:mostro_mobile/features/trades/providers/trades_provider.dart'; class StatusFilterWidget extends ConsumerWidget { const StatusFilterWidget({super.key}); - - static final _logger = Logger(); - @override Widget build(BuildContext context, WidgetRef ref) { final selectedStatusFilter = ref.watch(statusFilterProvider); @@ -173,7 +170,7 @@ class StatusFilterWidget extends ConsumerWidget { try { statusValue = Status.fromString(value); } catch (e) { - _logger.e('Error parsing status: $e'); + logger.e('Error parsing status: $e'); statusValue = null; } } diff --git a/lib/l10n/intl_en.arb b/lib/l10n/intl_en.arb index aa255c64..07571efb 100644 --- a/lib/l10n/intl_en.arb +++ b/lib/l10n/intl_en.arb @@ -1196,6 +1196,66 @@ "restoreError": "Restore error", "restoreErrorMessage": "Error restoring user data. Please check your connection and try again.", + "@_comment_logs_screen": "Logs screen and export functionality", + "logsReport": "Logs", + "viewAndExportLogs": "View and export application logs", + "clearLogs": "Clear Logs", + "totalLogs": "{count} logs", + "@totalLogs": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "maxEntries": "Max {max} entries", + "@maxEntries": { + "placeholders": { + "max": { + "type": "int" + } + } + }, + "noLogsAvailable": "No logs available", + "logsWillAppearHere": "Logs will appear here as you use the app", + "saveToDevice": "Save to Device", + "shareReport": "Share", + "logsExportTitle": "Mostro Logs Export", + "exportFailed": "Failed to export logs", + "saveFailed": "Failed to save logs", + "logsSavedTo": "Logs saved to: {path}", + "@logsSavedTo": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "clearLogsConfirmTitle": "Clear all logs?", + "clearLogsConfirmMessage": "This action cannot be undone. All logs will be permanently deleted.", + + "@_comment_dev_tools": "Developer Tools Section", + "devTools": "Dev Tools", + "devToolsWarning": "Advanced features for technical users", + "devToolsDescription": "Access diagnostic tools and logs", + "logStorageLocation": "Log Storage Location", + "storageLocationUpdated": "Storage location updated", + "defaultDownloads": "App Storage", + + "@_comment_logs_filters": "Logs Filtering and Search", + "filterByLevel": "Filter by Level", + "allLevels": "All Levels", + "errors": "Errors", + "warnings": "Warnings", + "info": "Info", + "debug": "Debug", + "searchLogs": "Search logs...", + "searchInLogs": "Search in logs", + "noLogsMatchFilter": "No logs match the current filter", + "clearFilters": "Clear Filters", + "copyLog": "Copy log", + "logCopied": "Log copied to clipboard", + "@_comment_file_messaging": "File messaging strings", "download": "Download", "openFile": "Open File", diff --git a/lib/l10n/intl_es.arb b/lib/l10n/intl_es.arb index f97ffa0b..505ab0c4 100644 --- a/lib/l10n/intl_es.arb +++ b/lib/l10n/intl_es.arb @@ -1174,6 +1174,66 @@ "restoreError": "Error de restauración", "restoreErrorMessage": "Error al restaurar datos del usuario. Verifica tu conexión e inténtalo más tarde.", + "@_comment_logs_screen": "Pantalla de logs y funcionalidad de exportación", + "logsReport": "Registros", + "viewAndExportLogs": "Ver y exportar registros de la aplicación", + "clearLogs": "Limpiar Registros", + "totalLogs": "{count} registros", + "@totalLogs": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "maxEntries": "Máx {max} entradas", + "@maxEntries": { + "placeholders": { + "max": { + "type": "int" + } + } + }, + "noLogsAvailable": "No hay registros disponibles", + "logsWillAppearHere": "Los registros aparecerán aquí mientras usas la aplicación", + "saveToDevice": "Guardar en Dispositivo", + "shareReport": "Compartir", + "logsExportTitle": "Exportar Registros de Mostro", + "exportFailed": "Error al exportar registros", + "saveFailed": "Error al guardar registros", + "logsSavedTo": "Registros guardados en: {path}", + "@logsSavedTo": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "clearLogsConfirmTitle": "¿Limpiar todos los registros?", + "clearLogsConfirmMessage": "Esta acción no se puede deshacer. Todos los registros se eliminarán permanentemente.", + + "@_comment_dev_tools": "Sección de Herramientas de Desarrollador", + "devTools": "Herramientas Dev", + "devToolsWarning": "Funciones avanzadas para usuarios técnicos", + "devToolsDescription": "Acceder a herramientas de diagnóstico y registros", + "logStorageLocation": "Ubicación de Almacenamiento de Registros", + "storageLocationUpdated": "Ubicación de almacenamiento actualizada", + "defaultDownloads": "Almacenamiento de la App", + + "@_comment_logs_filters": "Filtrado y Búsqueda de Registros", + "filterByLevel": "Filtrar por Nivel", + "allLevels": "Todos los Niveles", + "errors": "Errores", + "warnings": "Advertencias", + "info": "Info", + "debug": "Debug", + "searchLogs": "Buscar registros...", + "searchInLogs": "Buscar en registros", + "noLogsMatchFilter": "No hay registros que coincidan con el filtro actual", + "clearFilters": "Limpiar Filtros", + "copyLog": "Copiar registro", + "logCopied": "Registro copiado al portapapeles", + "@_comment_file_messaging": "File messaging strings", "download": "Descargar", "openFile": "Abrir Archivo", diff --git a/lib/l10n/intl_it.arb b/lib/l10n/intl_it.arb index 5ca41f24..2b8e05ff 100644 --- a/lib/l10n/intl_it.arb +++ b/lib/l10n/intl_it.arb @@ -1229,6 +1229,66 @@ "restoreError": "Errore di ripristino", "restoreErrorMessage": "Errore nel ripristino dei dati utente. Verifica la tua connessione e riprova più tardi.", + "@_comment_logs_screen": "Schermata logs e funzionalità di esportazione", + "logsReport": "Registri", + "viewAndExportLogs": "Visualizza ed esporta i registri dell'applicazione", + "clearLogs": "Cancella Registri", + "totalLogs": "{count} registri", + "@totalLogs": { + "placeholders": { + "count": { + "type": "int" + } + } + }, + "maxEntries": "Max {max} voci", + "@maxEntries": { + "placeholders": { + "max": { + "type": "int" + } + } + }, + "noLogsAvailable": "Nessun registro disponibile", + "logsWillAppearHere": "I registri appariranno qui mentre usi l'app", + "saveToDevice": "Salva su Dispositivo", + "shareReport": "Condividi", + "logsExportTitle": "Esportazione Registri Mostro", + "exportFailed": "Errore durante l'esportazione dei registri", + "saveFailed": "Errore durante il salvataggio dei registri", + "logsSavedTo": "Registri salvati in: {path}", + "@logsSavedTo": { + "placeholders": { + "path": { + "type": "String" + } + } + }, + "clearLogsConfirmTitle": "Cancellare tutti i registri?", + "clearLogsConfirmMessage": "Questa azione non può essere annullata. Tutti i registri verranno eliminati permanentemente.", + + "@_comment_dev_tools": "Sezione Strumenti Sviluppatore", + "devTools": "Strumenti Dev", + "devToolsWarning": "Funzionalità avanzate per utenti tecnici", + "devToolsDescription": "Accedi a strumenti diagnostici e registri", + "logStorageLocation": "Posizione Archiviazione Registri", + "storageLocationUpdated": "Posizione di archiviazione aggiornata", + "defaultDownloads": "Archiviazione App", + + "@_comment_logs_filters": "Filtro e Ricerca Registri", + "filterByLevel": "Filtra per Livello", + "allLevels": "Tutti i Livelli", + "errors": "Errori", + "warnings": "Avvisi", + "info": "Info", + "debug": "Debug", + "searchLogs": "Cerca registri...", + "searchInLogs": "Cerca nei registri", + "noLogsMatchFilter": "Nessun registro corrisponde al filtro corrente", + "clearFilters": "Cancella Filtri", + "copyLog": "Copia registro", + "logCopied": "Registro copiato negli appunti", + "@_comment_file_messaging": "File messaging strings", "download": "Scarica", "openFile": "Apri File", diff --git a/lib/main.dart b/lib/main.dart index 0c46fdcd..350eb6ab 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -12,12 +12,15 @@ import 'package:mostro_mobile/shared/providers/background_service_provider.dart' import 'package:mostro_mobile/shared/providers/providers.dart'; import 'package:mostro_mobile/shared/utils/biometrics_helper.dart'; import 'package:mostro_mobile/shared/utils/notification_permission_helper.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:timeago/timeago.dart' as timeago; Future main() async { WidgetsFlutterBinding.ensureInitialized(); + initIsolateLogReceiver(); + await requestNotificationPermissionIfNeeded(); final biometricsHelper = BiometricsHelper(); diff --git a/lib/services/deep_link_service.dart b/lib/services/deep_link_service.dart index cd0436ee..96dfca32 100644 --- a/lib/services/deep_link_service.dart +++ b/lib/services/deep_link_service.dart @@ -4,8 +4,8 @@ import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter/widgets.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; -import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; import 'package:mostro_mobile/generated/l10n.dart'; @@ -22,7 +22,6 @@ class OrderInfo { } class DeepLinkService { - final Logger _logger = Logger(); final AppLinks _appLinks = AppLinks(); // Stream controller for deep link events @@ -41,21 +40,21 @@ class DeepLinkService { // Listen for incoming deep links when app is already running _appLinks.uriLinkStream.listen( (Uri uri) { - _logger.i('Deep link received while app running: $uri'); + logger.i('DeepLink: received while app running - $uri'); _handleDeepLink(uri); }, onError: (Object err) { - _logger.e('Deep link stream error: $err'); + logger.e('DeepLink: stream error - $err'); }, ); // NOTE: We don't process the initial link here to avoid GoRouter conflicts // The initial link will be handled by the app initialization in app.dart - + _isInitialized = true; - _logger.i('DeepLinkService initialized successfully'); + logger.i('DeepLinkService: initialized successfully'); } catch (e) { - _logger.e('Failed to initialize DeepLinkService: $e'); + logger.e('DeepLinkService: initialization failed - $e'); rethrow; } } @@ -72,7 +71,7 @@ class DeepLinkService { BuildContext context, ) async { try { - _logger.i('Processing mostro link: $url'); + logger.i('DeepLink: processing mostro link $url'); // Validate URL format if (!NostrUtils.isValidMostroUrl(url)) { @@ -98,7 +97,7 @@ class DeepLinkService { return DeepLinkResult.error(S.of(context)!.deepLinkNoRelays); } - _logger.i('Parsed order ID: $orderId, relays: $relays'); + logger.i('DeepLink: parsed order ID $orderId with ${relays.length} relays'); // Fetch the order info directly using the order ID final fetchedOrderInfo = await _fetchOrderInfoById( @@ -117,7 +116,7 @@ class DeepLinkService { return DeepLinkResult.success(fetchedOrderInfo); } catch (e) { - _logger.e('Error processing mostro link: $e'); + logger.e('DeepLink: processing failed - $e'); return DeepLinkResult.error('Failed to process deep link: $e'); } } @@ -148,7 +147,7 @@ class DeepLinkService { // If no events found and we have default relays, try those if (events.isEmpty) { - _logger.i('Order not found in specified relays, trying default relays'); + logger.i('DeepLink: order not found in specified relays - trying default relays'); final defaultEvents = await nostrService.fetchEvents(filter); events.addAll(defaultEvents); } @@ -177,7 +176,7 @@ class DeepLinkService { return null; } catch (e) { - _logger.e('Error fetching order info by ID: $e'); + logger.e('DeepLink: fetch order info failed - $e'); return null; } } @@ -196,9 +195,8 @@ class DeepLinkService { /// Navigates to the appropriate screen for the given order void navigateToOrder(GoRouter router, OrderInfo orderInfo) { final route = getNavigationRoute(orderInfo); - _logger.i( - 'Navigating to: $route (Order: ${orderInfo.orderId}, Type: ${orderInfo.orderType.value})'); - + logger.i('Navigation: routing to $route - order ${orderInfo.orderId}, type ${orderInfo.orderType.value}'); + // Use post-frame callback to ensure navigation happens after the current frame // This prevents GoRouter assertion failures during app lifecycle transitions WidgetsBinding.instance.addPostFrameCallback((_) { @@ -208,15 +206,15 @@ class DeepLinkService { if (context != null && context.mounted) { router.push(route); } else { - _logger.w('Router context is not available for navigation to: $route'); + logger.w('Navigation: router context not available for $route'); } } catch (e) { - _logger.e('Error navigating to order: $e'); + logger.e('Navigation: failed to navigate to order - $e'); // Fallback: try using go instead of push if push fails try { router.go(route); } catch (fallbackError) { - _logger.e('Fallback navigation also failed: $fallbackError'); + logger.e('Navigation: fallback also failed - $fallbackError'); } } }); diff --git a/lib/services/lifecycle_manager.dart b/lib/services/lifecycle_manager.dart index fda71073..03322dca 100644 --- a/lib/services/lifecycle_manager.dart +++ b/lib/services/lifecycle_manager.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:dart_nostr/nostr/model/request/filter.dart'; import 'package:flutter/material.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/features/chat/providers/chat_room_providers.dart'; import 'package:mostro_mobile/features/subscriptions/subscription_type.dart'; import 'package:mostro_mobile/features/trades/providers/trades_provider.dart'; @@ -15,7 +15,6 @@ import 'package:mostro_mobile/features/subscriptions/subscription_manager_provid class LifecycleManager extends WidgetsBindingObserver { final Ref ref; bool _isInBackground = false; - final _logger = Logger(); LifecycleManager(this.ref) { WidgetsBinding.instance.addObserver(this); @@ -48,12 +47,12 @@ class LifecycleManager extends WidgetsBindingObserver { Future _switchToForeground() async { try { _isInBackground = false; - _logger.i("Switching to foreground"); + logger.i("Lifecycle: switching to foreground"); // Stop background service final backgroundService = ref.read(backgroundServiceProvider); await backgroundService.setForegroundStatus(true); - _logger.i("Background service foreground status set to true"); + logger.i("BackgroundService: foreground status set to true"); // Add a small delay to ensure the background service has fully transitioned await Future.delayed(const Duration(milliseconds: 500)); @@ -62,26 +61,26 @@ class LifecycleManager extends WidgetsBindingObserver { subscriptionManager.subscribeAll(); // Reinitialize the mostro service - _logger.i("Reinitializing MostroService"); + logger.i("MostroService: reinitializing"); ref.read(mostroServiceProvider).init(); // Refresh order repository by re-reading it - _logger.i("Refreshing order repository"); + logger.i("Repository: refreshing orders"); final orderRepo = ref.read(orderRepositoryProvider); orderRepo.reloadData(); // Reinitialize chat rooms - _logger.i("Reloading chat rooms"); + logger.i("Chat: reloading rooms"); final chatRooms = ref.read(chatRoomsNotifierProvider.notifier); chatRooms.reloadAllChats(); // Force UI update for trades - _logger.i("Invalidating providers to refresh UI"); + logger.i("UI: invalidating providers to refresh"); ref.invalidate(filteredTradesWithOrderStateProvider); - _logger.i("Foreground transition complete"); + logger.i("Lifecycle: foreground transition complete"); } catch (e) { - _logger.e("Error during foreground transition: $e"); + logger.e("Error during foreground transition: $e"); } } @@ -95,34 +94,33 @@ class LifecycleManager extends WidgetsBindingObserver { for (final type in SubscriptionType.values) { final filters = subscriptionManager.getActiveFilters(type); if (filters.isNotEmpty) { - _logger.d('Found ${filters.length} active filters for $type'); + logger.d('Subscription: found ${filters.length} active filters for $type'); activeFilters.addAll(filters); } } if (activeFilters.isNotEmpty) { _isInBackground = true; - _logger.i("Switching to background"); + logger.i("Lifecycle: switching to background"); subscriptionManager.unsubscribeAll(); // Transfer active subscriptions to background service final backgroundService = ref.read(backgroundServiceProvider); await backgroundService.setForegroundStatus(false); - _logger.i( - "Transferring ${activeFilters.length} active filters to background service"); + logger.i("BackgroundService: transferring ${activeFilters.length} active filters"); backgroundService.subscribe(activeFilters); } else { - _logger.w("No active subscriptions to transfer to background service"); + logger.w("BackgroundService: no active subscriptions to transfer"); } - _logger.i("Background transition complete"); + logger.i("Lifecycle: background transition complete"); } catch (e) { - _logger.e("Error during background transition: $e"); + logger.e("Lifecycle: background transition failed - $e"); } } @Deprecated('Use SubscriptionManager instead.') void addSubscription(NostrFilter filter) { - _logger.w('LifecycleManager.addSubscription is deprecated. Use SubscriptionManager instead.'); + logger.w('Lifecycle: addSubscription deprecated - use SubscriptionManager instead'); // No-op - subscriptions are now tracked by SubscriptionManager } diff --git a/lib/services/logger_service.dart b/lib/services/logger_service.dart new file mode 100644 index 00000000..fb0d11af --- /dev/null +++ b/lib/services/logger_service.dart @@ -0,0 +1,319 @@ +import 'dart:isolate'; +import 'package:logger/logger.dart'; +import 'package:mostro_mobile/core/config.dart'; + +// Isolate log communication +ReceivePort? _isolateLogReceiver; +SendPort? _isolateLogSender; + +/// Initialize receiver to collect logs from background isolates +void initIsolateLogReceiver() { + if (_isolateLogReceiver != null) return; + + _isolateLogReceiver = ReceivePort(); + _isolateLogSender = _isolateLogReceiver!.sendPort; + + _isolateLogReceiver!.listen((message) { + if (message is Map) { + try { + addLogFromIsolate(message); + } catch (e, stack) { + // ignore: avoid_print + print('ERROR: Failed to process isolate log message: $e'); + // ignore: avoid_print + print('Stack trace: $stack'); + } + } + }); +} + +SendPort? get isolateLogSenderPort => _isolateLogSender; + +String cleanMessage(String message) { + var cleaned = message; + cleaned = cleaned + .replaceAll(RegExp(r'\x1B\[[0-9;]*[a-zA-Z]'), '') + .replaceAll(RegExp(r'\[\d+m'), '') + .replaceAll(RegExp(r'\[38;5;\d+m'), '') + .replaceAll(RegExp(r'\[39m'), '') + .replaceAll(RegExp(r'\[2m'), '') + .replaceAll(RegExp(r'\[22m'), '') + .replaceAll(RegExp(r'[┌┐└┘├┤─│┬┴┼╭╮╰╯╔╗╚╝╠╣═║╦╩╬━┃┄├]'), '') + .replaceAll(RegExp(r'[\u{1F300}-\u{1F9FF}]', unicode: true), '') + .replaceAll(RegExp(r'nsec[0-9a-z]+'), '[PRIVATE_KEY]') + .replaceAll(RegExp(r'"privateKey"\s*:\s*"[^"]*"'), '"privateKey":"[REDACTED]"') + .replaceAll(RegExp(r'"mnemonic"\s*:\s*"[^"]*"'), '"mnemonic":"[REDACTED]"') + .replaceAll(RegExp(r'[^A-Za-z0-9\s.:,!?\-_/\[\]]'), ' ') + .replaceAll(RegExp(r'\s+'), ' '); + return cleaned.trim(); +} + +void addLogFromIsolate(Map logData) { + DateTime timestamp; + try { + final timestampStr = logData['timestamp']; + if (timestampStr == null) { + timestamp = DateTime.now(); + } else { + timestamp = DateTime.parse(timestampStr.toString()); + } + } catch (e) { + timestamp = DateTime.now(); + } + + final levelStr = logData['level']?.toString() ?? 'debug'; + final level = _levelFromString(levelStr); + final rawMessage = logData['message']?.toString() ?? ''; + final message = cleanMessage(rawMessage); + final service = logData['service']?.toString() ?? 'Background'; + final line = logData['line']?.toString() ?? '0'; + + MemoryLogOutput.instance._buffer.add(LogEntry( + timestamp: timestamp, + level: level, + message: message, + service: service, + line: line, + )); + + if (MemoryLogOutput.instance._buffer.length > Config.logMaxEntries) { + final deleteCount = MemoryLogOutput.instance._buffer.length < Config.logBatchDeleteSize + ? MemoryLogOutput.instance._buffer.length - Config.logMaxEntries + : Config.logBatchDeleteSize; + if (deleteCount > 0) { + MemoryLogOutput.instance._buffer.removeRange(0, deleteCount); + } + } +} + +Level _levelFromString(String level) { + switch (level) { + case 'error': return Level.error; + case 'warning': return Level.warning; + case 'info': return Level.info; + case 'debug': return Level.debug; + case 'trace': return Level.trace; + default: return Level.debug; + } +} + +class LogEntry { + final DateTime timestamp; + final Level level; + final String message; + final String service; + final String line; + + LogEntry({ + required this.timestamp, + required this.level, + required this.message, + required this.service, + required this.line, + }); + + String format() { + final time = timestamp.toString().substring(0, 19); + final levelStr = level.toString().split('.').last.toUpperCase(); + return '[$levelStr]($service:$line) $time - $message'; + } +} + +/// Custom LogOutput that captures all logs to memory buffer +class MemoryLogOutput extends LogOutput { + static final MemoryLogOutput instance = MemoryLogOutput._(); + + MemoryLogOutput._(); + + final List _buffer = []; + final SimplePrinter _printer = SimplePrinter(); + + @override + void output(OutputEvent event) { + // Use StackTrace.current as fallback to get accurate caller info + final stackTrace = event.origin.stackTrace ?? StackTrace.current; + final serviceAndLine = _printer.extractFromStackTrace(stackTrace); + + // Always add to buffer + _buffer.add(LogEntry( + timestamp: event.origin.time, + level: event.level, + message: cleanMessage(event.origin.message.toString()), + service: serviceAndLine['service'] ?? 'Unknown', + line: serviceAndLine['line'] ?? '0', + )); + + // Maintain buffer size limit + if (_buffer.length > Config.logMaxEntries) { + final deleteCount = _buffer.length < Config.logBatchDeleteSize + ? _buffer.length - Config.logMaxEntries + : Config.logBatchDeleteSize; + if (deleteCount > 0) { + _buffer.removeRange(0, deleteCount); + } + } + } + + List getAllLogs() => List.unmodifiable(_buffer); + void clear() => _buffer.clear(); + int get logCount => _buffer.length; +} + +class _ConsoleOnlyOutput extends LogOutput { + @override + void output(OutputEvent event) { + for (final line in event.lines) { + // ignore: avoid_print + print(line); + } + } +} + +class _MultiOutput extends LogOutput { + final MemoryLogOutput memoryOutput; + final LogOutput consoleOutput; + + _MultiOutput(this.memoryOutput, this.consoleOutput); + + @override + void output(OutputEvent event) { + memoryOutput.output(event); + consoleOutput.output(event); + } +} + +class SimplePrinter extends LogPrinter { + @override + List log(LogEvent event) { + final level = _formatLevel(event.level); + final message = event.message.toString(); + final timestamp = event.time.toString().substring(0, 19); + // Use StackTrace.current as fallback to get accurate caller info + final stackTrace = event.stackTrace ?? StackTrace.current; + final serviceAndLine = extractFromStackTrace(stackTrace); + final service = serviceAndLine['service'] ?? 'Unknown'; + final line = serviceAndLine['line'] ?? '0'; + + return [ + '[$level]($service:$line) $timestamp - $message', + ]; + } + + String _formatLevel(Level level) { + switch (level) { + case Level.error: + return 'ERROR'; + case Level.warning: + return 'WARN'; + case Level.info: + return 'INFO'; + case Level.debug: + return 'DEBUG'; + case Level.trace: + return 'TRACE'; + default: + return 'LOG'; + } + } + + Map extractFromStackTrace(StackTrace? stackTrace) { + if (stackTrace == null) return {'service': 'Unknown', 'line': '0'}; + + final lines = stackTrace.toString().split('\n'); + + for (final line in lines) { + if (line.contains('logger_service.dart') || + line.contains('logger.dart') || + line.contains(' (dart:') || + line.contains('') || + line.trim().isEmpty) { + continue; + } + + var match = RegExp(r'#\d+\s+\S+\s+\((?:package:[\w_]+/)?(?:.*/)(\w+)\.dart:(\d+)').firstMatch(line); + if (match != null) { + return { + 'service': match.group(1) ?? 'Unknown', + 'line': match.group(2) ?? '0' + }; + } + + match = RegExp(r'package:[\w_]+/(?:.*/)(\w+)\.dart:(\d+)').firstMatch(line); + if (match != null) { + return { + 'service': match.group(1) ?? 'Unknown', + 'line': match.group(2) ?? '0' + }; + } + } + + return {'service': 'Unknown', 'line': '0'}; + } +} + +class _AlwaysStackTraceFilter extends LogFilter { + @override + bool shouldLog(LogEvent event) => true; +} + +Logger? _cachedSimpleLogger; +Logger? _cachedFullLogger; + +Logger get logger { + if (Config.fullLogsInfo) { + _cachedFullLogger ??= Logger( + printer: PrettyPrinter( + methodCount: 2, + errorMethodCount: 8, + lineLength: 120, + colors: true, + printEmojis: true, + dateTimeFormat: DateTimeFormat.onlyTimeAndSinceStart, + ), + output: _MultiOutput(MemoryLogOutput.instance, ConsoleOutput()), + level: Level.debug, + filter: _AlwaysStackTraceFilter(), + ); + return _cachedFullLogger!; + } else { + _cachedSimpleLogger ??= Logger( + printer: SimplePrinter(), + output: _MultiOutput(MemoryLogOutput.instance, _ConsoleOnlyOutput()), + level: Level.debug, + filter: _AlwaysStackTraceFilter(), + ); + return _cachedSimpleLogger!; + } +} + +/// LogOutput that forwards logs from isolates to main thread via SendPort +class IsolateLogOutput extends LogOutput { + final SendPort? sendPort; + + IsolateLogOutput(this.sendPort); + + @override + void output(OutputEvent event) { + for (final line in event.lines) { + // ignore: avoid_print + print(line); + } + + if (sendPort != null) { + final printer = SimplePrinter(); + final serviceAndLine = printer.extractFromStackTrace(event.origin.stackTrace); + + final rawMessage = event.origin.message.toString(); + final sanitizedMessage = cleanMessage(rawMessage); + + sendPort!.send({ + 'timestamp': event.origin.time.toIso8601String(), + 'level': event.level.name, + 'message': sanitizedMessage, + 'service': serviceAndLine['service'] ?? 'Background', + 'line': serviceAndLine['line'] ?? '0', + }); + } + } +} + diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index adf53186..9584c203 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -4,7 +4,6 @@ import 'package:collection/collection.dart'; import 'package:dart_nostr/dart_nostr.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/enums.dart'; import 'package:mostro_mobile/data/models.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; @@ -13,10 +12,10 @@ import 'package:mostro_mobile/shared/providers.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/key_manager/key_manager_provider.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; class MostroService { final Ref ref; - final _logger = Logger(); Settings _settings; StreamSubscription? _ordersSubscription; @@ -29,7 +28,7 @@ class MostroService { _ordersSubscription = ref.read(subscriptionManagerProvider).orders.listen( _onData, onError: (error, stackTrace) { - _logger.e('Error in orders subscription', + logger.e('Subscription: orders subscription error - $error', error: error, stackTrace: stackTrace); }, cancelOnError: false, @@ -38,7 +37,7 @@ class MostroService { void dispose() { _ordersSubscription?.cancel(); - _logger.i('MostroService disposed'); + logger.i('MostroService: disposed successfully'); } //IMPORTANT : The app always use trade index 1 for restore-related messages @@ -112,7 +111,7 @@ class MostroService { (s) => s.tradeKey.public == event.recipient, ); if (matchingSession == null) { - _logger.w('No matching session found for recipient: ${event.recipient}'); + logger.w('Session: no match found for recipient ${event.recipient}'); return; } final privateKey = matchingSession.tradeKey.private; @@ -126,13 +125,13 @@ class MostroService { // Ensure result is a non-empty List before accessing elements if (result is! List || result.isEmpty) { - _logger.w('Received empty or invalid payload, skipping'); + logger.w('Event: received empty or invalid payload - skipping'); return; } // Skip dispute chat messages (they have "dm" key and are handled by DisputeChatNotifier) if (result[0] is Map && (result[0] as Map).containsKey('dm')) { - _logger.i('Skipping dispute chat message (handled by DisputeChatNotifier)'); + logger.i('Event: skipping dispute chat message - handled by DisputeChatNotifier'); return; } @@ -149,13 +148,11 @@ class MostroService { // This handles cases where admin messages might not have an id in the decrypted event final messageKey = decryptedEvent.id ?? event.id ?? 'msg_${DateTime.now().millisecondsSinceEpoch}'; await messageStorage.addMessage(messageKey, msg); - _logger.i( - 'Received DM, Event ID: ${decryptedEvent.id ?? event.id} with payload: ${decryptedEvent.content}', - ); + logger.i('Message: received DM event ${decryptedEvent.id ?? event.id}'); await _maybeLinkChildOrder(msg, matchingSession); } catch (e) { - _logger.e('Error processing event', error: e); + logger.e('Event: processing failed - $e', error: e); } } @@ -179,9 +176,7 @@ class MostroService { ref.read(orderNotifierProvider(message.id!).notifier).subscribe(); - _logger.i( - 'Linked child order ${message.id} to parent ${session.parentOrderId}', - ); + logger.i('Order: linked child ${message.id} to parent ${session.parentOrderId}'); } Future submitOrder(MostroMessage order) async { @@ -291,9 +286,7 @@ class MostroService { final remaining = maxAmount - selectedAmount; if (remaining < minAmount) { - _logger.i( - '[$callerLabel] Range order $orderId exhausted (remaining $remaining < min $minAmount); skipping child preparation.', - ); + logger.i('Order: [$callerLabel] range order $orderId exhausted - remaining $remaining < min $minAmount'); return null; } @@ -310,13 +303,9 @@ class MostroService { parentOrderId: orderId, role: currentSession.role!, ); - _logger.i( - '[$callerLabel] Prepared child session for $orderId using key index $nextKeyIndex', - ); + logger.i('Session: [$callerLabel] prepared child for $orderId using key index $nextKeyIndex'); } else { - _logger.w( - '[$callerLabel] Unable to prepare child session for $orderId; session or role missing.', - ); + logger.w('Session: [$callerLabel] unable to prepare child for $orderId - session or role missing'); } return NextTrade( @@ -353,7 +342,7 @@ class MostroService { masterKey: session.fullPrivacy ? null : session.masterKey, keyIndex: session.fullPrivacy ? null : session.keyIndex, ); - _logger + logger .i('Sending DM, Event ID: ${event.id} with payload: ${order.toJson()}'); await ref.read(nostrServiceProvider).publishEvent(event); } diff --git a/lib/services/nostr_service.dart b/lib/services/nostr_service.dart index d13f7058..0986463b 100644 --- a/lib/services/nostr_service.dart +++ b/lib/services/nostr_service.dart @@ -3,18 +3,17 @@ import 'package:dart_nostr/dart_nostr.dart'; import 'package:dart_nostr/nostr/model/ease.dart'; import 'package:dart_nostr/nostr/model/ok.dart'; import 'package:dart_nostr/nostr/model/relay_informations.dart'; -import 'package:logger/logger.dart'; import 'package:mostro_mobile/core/config.dart'; import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/services/deep_link_service.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; class NostrService { Settings? _settings; final Nostr _nostr = Nostr.instance; - final Logger _logger = Logger(); bool _isInitialized = false; NostrService(); @@ -32,7 +31,7 @@ class NostrService { throw Exception('Cannot initialize NostrService: No relays provided'); } - _logger.i('Initializing NostrService with relays: ${settings.relays}'); + logger.i('NostrService: initializing with ${settings.relays.length} relays'); _settings = settings; try { @@ -44,30 +43,30 @@ class NostrService { retryOnError: true, onRelayListening: (relayUrl, receivedData, channel) { if (receivedData is NostrEvent) { - _logger.d('Event from $relayUrl: ${receivedData.id}'); + logger.d('Event: received ${receivedData.id} from $relayUrl'); } else if (receivedData is NostrNotice) { - _logger.i('Notice from $relayUrl: ${receivedData.message}'); + logger.i('Relay: notice from $relayUrl - ${receivedData.message}'); } else if (receivedData is NostrEventOkCommand) { - _logger.d('OK from $relayUrl: ${receivedData.eventId} (accepted: ${receivedData.isEventAccepted})'); + logger.d('Relay: OK for ${receivedData.eventId} from $relayUrl - accepted: ${receivedData.isEventAccepted}'); } else if (receivedData is NostrRequestEoseCommand) { - _logger.d('EOSE from $relayUrl for subscription: ${receivedData.subscriptionId}'); + logger.d('Relay: EOSE from $relayUrl for subscription ${receivedData.subscriptionId}'); } else if (receivedData is NostrCountResponse) { - _logger.d('Count from $relayUrl: ${receivedData.count}'); + logger.d('Relay: count ${receivedData.count} from $relayUrl'); } }, onRelayConnectionError: (relay, error, channel) { - _logger.w('Failed to connect to relay $relay: $error'); + logger.w('Relay: connection failed to $relay - $error'); }, onRelayConnectionDone: (relay, socket) { - _logger.i('Successfully connected to relay: $relay'); + logger.i('Relay: connected successfully to $relay'); }, ); _isInitialized = true; - _logger.i('NostrService initialized successfully with ${settings.relays.length} relays'); + logger.i('NostrService: initialized successfully with ${settings.relays.length} relays'); } catch (e) { _isInitialized = false; - _logger.e('Failed to initialize NostrService: $e'); + logger.e('NostrService: initialization failed - $e'); rethrow; } } @@ -75,11 +74,11 @@ class NostrService { Future updateSettings(Settings newSettings) async { // Compare with current settings instead of relying on dart_nostr internal state if (!ListEquality().equals(settings.relays, newSettings.relays)) { - _logger.i('Updating relays from ${settings.relays} to ${newSettings.relays}'); - + logger.i('Relay: updating from ${settings.relays.length} to ${newSettings.relays.length} relays'); + // Validate that new relay list is not empty if (newSettings.relays.isEmpty) { - _logger.w('Warning: Attempting to update with empty relay list'); + logger.w('Relay: update rejected - empty relay list provided'); return; } @@ -89,24 +88,24 @@ class NostrService { // Disconnect from current relays first await _nostr.services.relays.disconnectFromRelays(); - _logger.i('Disconnected from previous relays'); - + logger.i('Relay: disconnected from previous relays'); + // Initialize with new relay list await init(newSettings); - _logger.i('Successfully updated to new relays: ${newSettings.relays}'); + logger.i('Relay: updated successfully to ${newSettings.relays.length} relays'); } catch (e) { - _logger.e('Failed to update relays: $e'); + logger.e('Relay: update failed - $e'); // Try to restore previous state if update fails try { await init(settings); - _logger.i('Restored previous relay configuration'); + logger.i('Relay: restored previous configuration'); } catch (restoreError) { - _logger.e('Failed to restore previous relay configuration: $restoreError'); + logger.e('Relay: failed to restore previous configuration - $restoreError'); rethrow; } } } else { - _logger.d('Relay list unchanged, skipping update'); + logger.d('Relay: list unchanged - skipping update'); } } @@ -127,16 +126,16 @@ class NostrService { } try { - _logger.i('Publishing event ${event.id} to relays: ${settings.relays}'); - + logger.i('Event: publishing ${event.id} to ${settings.relays.length} relays'); + await _nostr.services.relays.sendEventToRelaysAsync( event, timeout: Config.nostrConnectionTimeout, ); - - _logger.i('Successfully published event ${event.id}'); + + logger.i('Event: published successfully ${event.id}'); } catch (e) { - _logger.w('Failed to publish event ${event.id}: $e'); + logger.w('Event: publish failed ${event.id} - $e'); // If it's the empty relay list assertion error, provide better context if (e.toString().contains('relaysUrl.isNotEmpty')) { @@ -181,7 +180,7 @@ class NostrService { await _nostr.services.relays.disconnectFromRelays(); _isInitialized = false; - _logger.i('Disconnected from all relays'); + logger.i('Relay: disconnected from all relays'); } bool get isInitialized => _isInitialized; @@ -270,7 +269,7 @@ class NostrService { } try { - _logger.i('Fetching event with ID: $eventId'); + logger.i('Event: fetching by ID $eventId'); // Create filter to fetch the specific event final filter = NostrFilter( @@ -289,7 +288,7 @@ class NostrService { } if (events.isEmpty) { - _logger.w('No event found with ID: $eventId'); + logger.w('Event: not found with ID $eventId'); return null; } @@ -301,20 +300,20 @@ class NostrService { // Validate it's a proper order event if (event.kind != 38383) { - _logger.w('Event $eventId is not an order event (kind: ${event.kind})'); + logger.w('Event: invalid kind ${event.kind} for $eventId - expected 38383'); return null; } // Check if it's from a valid Mostro instance if (event.pubkey != settings.mostroPublicKey) { - _logger.w('Event $eventId is not from the configured Mostro instance'); + logger.w('Event: rejected $eventId - not from configured Mostro instance'); return null; } - _logger.i('Successfully found order event: ${event.id}'); + logger.i('Event: found order successfully ${event.id}'); return Order.fromEvent(event); } catch (e) { - _logger.e('Error fetching event by ID: $e'); + logger.e('Event: fetch by ID failed - $e'); return null; } } @@ -324,7 +323,7 @@ class NostrService { Future fetchOrderInfoByEventId(String eventId, [List? specificRelays]) async { try { - _logger.i('Fetching order ID from event: $eventId'); + logger.i('Order: fetching info from event $eventId'); final filter = NostrFilter( ids: [eventId], @@ -342,7 +341,7 @@ class NostrService { } if (events.isEmpty) { - _logger.w('No event found with ID: $eventId'); + logger.w('Order: event not found $eventId'); return null; } @@ -354,13 +353,13 @@ class NostrService { // Validate it's a proper order event if (event.kind != 38383) { - _logger.w('Event $eventId is not an order event (kind: ${event.kind})'); + logger.w('Order: invalid kind ${event.kind} for $eventId - expected 38383'); return null; } // Check if it's from a valid Mostro instance if (event.pubkey != settings.mostroPublicKey) { - _logger.w('Event $eventId is not from the configured Mostro instance'); + logger.w('Order: rejected $eventId - not from configured Mostro instance'); return null; } @@ -373,7 +372,7 @@ class NostrService { .firstOrNull; if (dTag == null || dTag.isEmpty) { - _logger.w('Event $eventId does not contain a valid d tag'); + logger.w('Order: missing d tag in event $eventId'); return null; } @@ -386,7 +385,7 @@ class NostrService { .firstOrNull; if (kTag == null || kTag.isEmpty) { - _logger.w('Event $eventId does not contain a valid k tag (order type)'); + logger.w('Order: missing k tag in event $eventId'); return null; } @@ -394,15 +393,14 @@ class NostrService { try { orderType = OrderType.fromString(kTag); } catch (e) { - _logger.w('Event $eventId contains invalid order type: $kTag'); + logger.w('Order: invalid type $kTag in event $eventId'); return null; } - _logger.i( - 'Successfully extracted order info - ID: $dTag, Type: ${orderType.value} from event: $eventId'); + logger.i('Order: extracted info successfully - ID: $dTag, Type: ${orderType.value} from $eventId'); return OrderInfo(orderId: dTag, orderType: orderType); } catch (e) { - _logger.e('Error fetching order ID from event: $e'); + logger.e('Order: fetch info failed - $e'); return null; } } @@ -420,7 +418,7 @@ class NostrService { final allRelays = {...originalRelays, ...relays}.toList(); if (!ListEquality().equals(originalRelays, allRelays)) { - _logger.i('Temporarily connecting to additional relays: $relays'); + logger.i('Relay: connecting temporarily to ${relays.length} additional relays'); // Update settings with additional relays final tempSettings = Settings( @@ -445,12 +443,12 @@ class NostrService { return await fetchEvents(filter); } } catch (e) { - _logger.e('Error fetching from specific relays: $e'); + logger.e('Relay: fetch from specific relays failed - $e'); // Ensure we restore original settings even on error try { await updateSettings(settings); } catch (restoreError) { - _logger.e('Failed to restore original relay settings: $restoreError'); + logger.e('Relay: failed to restore original settings - $restoreError'); } rethrow; } diff --git a/lib/shared/notifiers/session_notifier.dart b/lib/shared/notifiers/session_notifier.dart index 9e9e6213..4c111a1b 100644 --- a/lib/shared/notifiers/session_notifier.dart +++ b/lib/shared/notifiers/session_notifier.dart @@ -7,7 +7,7 @@ import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/data/repositories/session_storage.dart'; import 'package:mostro_mobile/features/key_manager/key_manager_provider.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; -import 'package:logger/logger.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; import 'package:dart_nostr/dart_nostr.dart'; import 'package:mostro_mobile/data/models/peer.dart'; @@ -25,7 +25,6 @@ class SessionNotifier extends StateNotifier> { final Map _pendingChildSessions = {}; Timer? _cleanupTimer; - final Logger _logger = Logger(); List get sessions => _sessions.values.toList(); @@ -204,7 +203,7 @@ class SessionNotifier extends StateNotifier> { .removeWhere((_, pending) => identical(pending, session)); _sessions.removeWhere((_, stored) => identical(stored, session)); _emitState(); - _logger.d('Cleaned up temporary session for requestId: $requestId'); + logger.d('Cleaned up temporary session for requestId: $requestId'); } } @@ -231,7 +230,7 @@ class SessionNotifier extends StateNotifier> { _pendingChildSessions[tradeKey.public] = session; _emitState(); - _logger.i( + logger.i( 'Prepared child session for parent order $parentOrderId using key index $keyIndex', ); @@ -246,7 +245,7 @@ class SessionNotifier extends StateNotifier> { ) async { final session = _pendingChildSessions.remove(tradeKeyPublic); if (session == null) { - _logger.w( + logger.w( 'No pending child session found for trade key $tradeKeyPublic; nothing to link.', ); return; @@ -257,7 +256,7 @@ class SessionNotifier extends StateNotifier> { await _storage.putSession(session); _emitState(); - _logger.i( + logger.i( 'Linked child order $childOrderId to prepared session (parent: ${session.parentOrderId})', ); } @@ -268,10 +267,10 @@ class SessionNotifier extends StateNotifier> { final sharedKey = NostrUtils.computeSharedKey(tradePrivateKey, counterpartyPublicKey); - _logger.d('Shared key calculated: ${sharedKey.public}'); + logger.d('Shared key calculated: ${sharedKey.public}'); return sharedKey; } catch (e) { - _logger.e('Error calculating shared key: $e'); + logger.e('Error calculating shared key: $e'); rethrow; } } @@ -293,7 +292,7 @@ class SessionNotifier extends StateNotifier> { _emitState(); - _logger.d('Session updated with shared key for orderId: $orderId'); + logger.d('Session updated with shared key for orderId: $orderId'); } @override diff --git a/lib/shared/widgets/notification_listener_widget.dart b/lib/shared/widgets/notification_listener_widget.dart index 722fe166..62bd9e59 100644 --- a/lib/shared/widgets/notification_listener_widget.dart +++ b/lib/shared/widgets/notification_listener_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.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/generated/l10n.dart'; import 'package:mostro_mobile/data/models/enums/action.dart' as mostro; import 'package:mostro_mobile/features/notifications/notifiers/notification_temporary_state.dart'; @@ -8,8 +8,6 @@ import 'package:mostro_mobile/features/notifications/providers/notifications_pro import 'package:mostro_mobile/features/notifications/utils/notification_message_mapper.dart'; class CantDoNotificationMapper { - static final _logger = Logger(); - static final _messageMap = { 'pending_order_exists': (context) => S.of(context)!.pendingOrderExists, 'not_allowed_by_status': (context) => S.of(context)!.notAllowedByStatus, @@ -36,7 +34,7 @@ class CantDoNotificationMapper { return messageGetter(context); } - _logger.w('Unhandled cant-do reason: $cantDoReason. Consider adding to CantDoNotificationMapper.'); + logger.w('Unhandled cant-do reason: $cantDoReason. Consider adding to CantDoNotificationMapper.'); // Fallback to generic cant-do message return NotificationMessageMapper.getLocalizedTitle(context, mostro.Action.cantDo); diff --git a/lib/shared/widgets/pay_lightning_invoice_widget.dart b/lib/shared/widgets/pay_lightning_invoice_widget.dart index dde1eda9..ae2aa32e 100644 --- a/lib/shared/widgets/pay_lightning_invoice_widget.dart +++ b/lib/shared/widgets/pay_lightning_invoice_widget.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; -import 'package:logger/logger.dart'; +import 'package:mostro_mobile/services/logger_service.dart'; import 'package:mostro_mobile/core/app_theme.dart'; import 'package:qr_flutter/qr_flutter.dart'; import 'package:share_plus/share_plus.dart'; @@ -10,14 +10,13 @@ import 'package:mostro_mobile/generated/l10n.dart'; class PayLightningInvoiceWidget extends StatefulWidget { final VoidCallback onSubmit; final VoidCallback onCancel; - final Logger logger = Logger(); final String lnInvoice; final int sats; final String fiatAmount; final String fiatCode; final String orderId; - PayLightningInvoiceWidget({ + const PayLightningInvoiceWidget({ super.key, required this.onSubmit, required this.onCancel, @@ -75,7 +74,7 @@ class _PayLightningInvoiceWidgetState extends State { ElevatedButton.icon( onPressed: () { Clipboard.setData(ClipboardData(text: widget.lnInvoice)); - widget.logger + logger .i('Copied LN Invoice to clipboard: ${widget.lnInvoice}'); ScaffoldMessenger.of(context).showSnackBar( SnackBar( @@ -100,17 +99,17 @@ class _PayLightningInvoiceWidgetState extends State { final uri = Uri.parse('lightning:${widget.lnInvoice}'); if (await canLaunchUrl(uri)) { await launchUrl(uri); - widget.logger.i( + logger.i( 'Launched Lightning wallet with invoice: ${widget.lnInvoice}'); } else { // Fallback to generic share if no Lightning apps available // lightning: URL scheme is not necessary then await Share.share(widget.lnInvoice); - widget.logger.i( + logger.i( 'Shared LN Invoice via share sheet: ${widget.lnInvoice}'); } } catch (e) { - widget.logger.e('Failed to share LN Invoice: $e'); + logger.e('Failed to share LN Invoice: $e'); if (mounted) { // ignore: use_build_context_synchronously ScaffoldMessenger.of(context).showSnackBar( diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index ebf0337d..f76360c3 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -2009,6 +2009,7 @@ class MockSettings extends _i1.Mock implements _i2.Settings { bool? clearDefaultLightningAddress = false, List? blacklistedRelays, List>? userRelays, + String? customLogStorageDirectory, }) => (super.noSuchMethod( Invocation.method( @@ -2024,6 +2025,7 @@ class MockSettings extends _i1.Mock implements _i2.Settings { #clearDefaultLightningAddress: clearDefaultLightningAddress, #blacklistedRelays: blacklistedRelays, #userRelays: userRelays, + #customLogStorageDirectory: customLogStorageDirectory, }, ), returnValue: _FakeSettings_0( @@ -2041,6 +2043,7 @@ class MockSettings extends _i1.Mock implements _i2.Settings { #clearDefaultLightningAddress: clearDefaultLightningAddress, #blacklistedRelays: blacklistedRelays, #userRelays: userRelays, + #customLogStorageDirectory: customLogStorageDirectory, }, ), ),