diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 0cf9a990..78f405ec 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -4,9 +4,6 @@ on: pull_request: branches: - main - push: - branches: - - wip jobs: build: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..2690150e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,120 @@ +# Contributing to Mostro Mobile + +Welcome! Thank you for your interest in contributing to the Mostro Mobile project. This guide will help you get started, understand the project architecture, and contribute effectively. + +--- + +## Table of Contents +- [How to Contribute](#how-to-contribute) +- [Project Architecture Overview](#project-architecture-overview) +- [Getting Started](#getting-started) +- [Best Practices & Tips](#best-practices--tips) +- [Contact & Further Resources](#contact--further-resources) + +--- + +## How to Contribute + +We follow the standard GitHub workflow: + +1. **Fork the repository** +2. **Create a new branch** for your feature or bugfix +3. **Write clear, descriptive commit messages** +4. **Push your branch** and open a Pull Request (PR) +5. **Describe your changes** thoroughly in the PR +6. **Participate in code review** and address feedback + +For bug reports and feature requests, please [open an issue](https://github.com/MostroP2P/mobile/issues) with: +- Clear title and description +- Steps to reproduce (for bugs) +- Screenshots/logs if relevant + +--- + +## Project Architecture Overview + +Mostro Mobile is a Flutter app designed with modularity and maintainability in mind. Here’s a high-level overview of the architecture and key technologies: + +### 1. State Management: Riverpod & Notifier Pattern +- **Riverpod** is used for dependency injection and state management. +- Providers are organized by feature, e.g., `features/order/providers/order_notifier_provider.dart`. +- The **Notifier pattern** is used for complex state logic (e.g., order submission, authentication). Notifiers encapsulate business logic and expose state via providers. +- Dedicated state providers are often used to track transient UI states, such as submission progress or error messages. + +### 2. Data Persistence: Sembast +- **Sembast** is a NoSQL database used for local data persistence. +- Database initialization is handled in `shared/providers/mostro_database_provider.dart`. +- Data repositories (e.g., `data/repositories/base_storage.dart`) abstract CRUD operations and encode/decode models. +- All local data access should go through repository classes, not direct database calls. + +### 3. Connectivity: NostrService +- Connectivity with the Nostr protocol is handled by `services/nostr_service.dart`. +- The `NostrService` manages relay connections, event subscriptions, and message handling. +- All Nostr-related logic should be routed through this service or its providers. + +### 4. Routing: GoRouter +- Navigation is managed using **GoRouter**. +- The main router configuration is set up in `core/app.dart` (see the `goRouter` instance). +- Route definitions are centralized for maintainability and deep linking. + +### 5. Internationalization (i18n) +- Internationalization is handled using the `intl` package and Flutter’s localization tools. +- Localization files are in `lib/l10n/` and generated code in `lib/generated/`. +- Use `S.of(context).yourKey` for all user-facing strings. + +### 6. Other Key Patterns & Utilities +- **Background Services:** Found in `lib/background/` for handling background tasks (e.g., notifications, data sync). +- **Notifications:** Managed via `notifications/` and corresponding providers. +- **Shared Utilities:** Common widgets, helpers, and providers live in `shared/`. +- **Testing:** Tests are in the `test/` and `integration_test/` directories. + +--- + +## Getting Started + +1. **Clone the repo:** + ```sh + git clone https://github.com/chebizarro/mobile.git + cd mobile + ``` +2. **Install dependencies:** + ```sh + flutter pub get + ``` +3. **Run the app:** + ```sh + flutter run + ``` +4. **Configure environment:** + - Localization: Run `flutter gen-l10n` if you add new strings. + - Platform-specific setup: See `README.md` for details. + +5. **Testing:** + - Unit tests: `flutter test` + - Integration tests: `flutter test integration_test/` + +6. **Linting & Formatting:** + - Check code style: `flutter analyze` + - Format code: `flutter format .` + +--- + +## Best Practices & Tips + +- **Follow the existing folder and provider structure.** +- **Use Riverpod for all state management.** Avoid mixing with other state solutions. +- **Encapsulate business logic in Notifiers** and expose state via providers. +- **Use repositories for all data access** (Sembast or Nostr). +- **Keep UI code declarative and side-effect free.** +- **For SnackBars, dialogs, or overlays,** always use a post-frame callback to avoid build-phase errors (see `NostrResponsiveButton` pattern). +- **Refer to existing features** (e.g., order submission, chat) for implementation examples. + +--- + +## Contact & Further Resources + +- **Main repo:** [https://github.com/MostroP2P/mobile](https://github.com/MostroP2P/mobile) +- **Questions/Help:** Open a GitHub issue or discussion +- **Docs:** See `README.md` and code comments for more details + +Happy contributing! diff --git a/README.md b/README.md index 4755a3f0..cd79f2bb 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,8 @@ flutter run cargo run ``` +See the README.md in the mostro repository for more details. + ## Setting up Polar for Testing 1. Launch Polar and create a new Lightning Network. @@ -106,18 +108,17 @@ This project is licensed under the MIT License. See the `LICENSE` file for detai - [x] Displays order list - [x] Take orders (Buy & Sell) - [x] Posts Orders (Buy & Sell) -- [ ] Direct message with peers (use nip-17) -- [ ] Fiat sent -- [ ] Release -- [ ] Maker cancel pending order -- [ ] Cooperative cancellation +- [x] Direct message with peers +- [x] Fiat sent +- [x] Release +- [x] Maker cancel pending order +- [x] Cooperative cancellation - [ ] Buyer: add new invoice if payment fails -- [ ] Rate users -- [ ] List own orders +- [x] Rate users +- [x] List own orders - [ ] Dispute flow (users) - [ ] Dispute management (for admins) - [ ] Conversation key management -- [ ] Create buy orders with LN address -- [ ] Nip-06 support (identity management) -- [ ] Settings tab -- [ ] Notifications +- [x] Create buy orders with LN address +- [x] Settings tab +- [x] Notifications diff --git a/android/app/build.gradle b/android/app/build.gradle index 9ba0ff47..65a0b31f 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -11,12 +11,14 @@ android { ndkVersion = "26.3.11579264" //flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + coreLibraryDesugaringEnabled true + + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = "11" } defaultConfig { @@ -42,3 +44,7 @@ android { flutter { source = "../.." } + +dependencies { + coreLibraryDesugaring 'com.android.tools:desugar_jdk_libs:2.1.4' +} diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index b9ec7241..b871ba6e 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,10 +1,13 @@ - + + android:fullBackupContent="false" + android:enableOnBackInvokedCallback="true"> + - + android:resource="@style/NormalTheme" /> - + + + + + + - + + + + + + - \ No newline at end of file diff --git a/android/app/src/main/res/drawable-hdpi/ic_bg_service_small.png b/android/app/src/main/res/drawable-hdpi/ic_bg_service_small.png new file mode 100644 index 00000000..0fc6e393 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_bg_service_small.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_bg_service_small.png b/android/app/src/main/res/drawable-mdpi/ic_bg_service_small.png new file mode 100644 index 00000000..3b2affdd Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_bg_service_small.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_bg_service_small.png b/android/app/src/main/res/drawable-xhdpi/ic_bg_service_small.png new file mode 100644 index 00000000..76d14641 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_bg_service_small.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_bg_service_small.png b/android/app/src/main/res/drawable-xxhdpi/ic_bg_service_small.png new file mode 100644 index 00000000..1b297154 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_bg_service_small.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_bg_service_small.png b/android/app/src/main/res/drawable-xxxhdpi/ic_bg_service_small.png new file mode 100644 index 00000000..33f2a559 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_bg_service_small.png differ diff --git a/ios/Flutter/Debug.xcconfig b/ios/Flutter/Debug.xcconfig index 592ceee8..ec97fc6f 100644 --- a/ios/Flutter/Debug.xcconfig +++ b/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Flutter/Release.xcconfig b/ios/Flutter/Release.xcconfig index 592ceee8..c4855bfe 100644 --- a/ios/Flutter/Release.xcconfig +++ b/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/ios/Podfile b/ios/Podfile new file mode 100644 index 00000000..e549ee22 --- /dev/null +++ b/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '12.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/ios/Runner/Info.plist b/ios/Runner/Info.plist index 83c757fd..33bd7a65 100644 --- a/ios/Runner/Info.plist +++ b/ios/Runner/Info.plist @@ -5,7 +5,7 @@ CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) CFBundleDisplayName - Mostro P2P + Mostro P2P CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -13,7 +13,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - Mostro P2P + Mostro P2P CFBundlePackageType APPL CFBundleShortVersionString @@ -47,5 +47,11 @@ NSCameraUsageDescription This app needs camera access to scan QR codes + UIBackgroundModes + + fetch + processing + remote-notification + \ No newline at end of file diff --git a/lib/background/abstract_background_service.dart b/lib/background/abstract_background_service.dart new file mode 100644 index 00000000..2dc114af --- /dev/null +++ b/lib/background/abstract_background_service.dart @@ -0,0 +1,13 @@ +import 'package:dart_nostr/dart_nostr.dart'; +import 'package:mostro_mobile/features/settings/settings.dart'; + +abstract class BackgroundService { + Future init(); + void subscribe(List filters); + void updateSettings(Settings settings); + Future setForegroundStatus(bool isForeground); + Future unsubscribe(String subscriptionId); + Future unsubscribeAll(); + Future getActiveSubscriptionCount(); + bool get isRunning; +} diff --git a/lib/background/background.dart b/lib/background/background.dart new file mode 100644 index 00000000..cd86c25f --- /dev/null +++ b/lib/background/background.dart @@ -0,0 +1,107 @@ +import 'dart:async'; +import 'dart:ui'; +import 'package:flutter/material.dart'; +import 'package:flutter_background_service/flutter_background_service.dart'; +import 'package:mostro_mobile/data/models/nostr_filter.dart'; +import 'package:mostro_mobile/data/repositories/event_storage.dart'; +import 'package:mostro_mobile/features/settings/settings.dart'; +import 'package:mostro_mobile/notifications/notification_service.dart'; +import 'package:mostro_mobile/services/nostr_service.dart'; +import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; + +bool isAppForeground = true; + +@pragma('vm:entry-point') +Future serviceMain(ServiceInstance service) async { + // If on Android, set up a permanent notification so the OS won't kill it. + if (service is AndroidServiceInstance) { + service.setAsForegroundService(); + service.setForegroundNotificationInfo( + title: "Mostro P2P", + content: "Connected to Mostro service", + ); + } + + final Map> activeSubscriptions = {}; + final nostrService = NostrService(); + final db = await openMostroDatabase('events.db'); + final eventStore = EventStorage(db: db); + + service.on('app-foreground-status').listen((data) { + isAppForeground = data?['is-foreground'] ?? isAppForeground; + }); + + service.on('start').listen((data) async { + if (data == null) return; + + final settingsMap = data['settings']; + if (settingsMap == null) return; + + final settings = Settings.fromJson(settingsMap); + await nostrService.init(settings); + + service.invoke('service-ready', {}); + }); + + service.on('update-settings').listen((data) async { + if (data == null) return; + + final settingsMap = data['settings']; + if (settingsMap == null) return; + + final settings = Settings.fromJson(settingsMap); + await nostrService.updateSettings(settings); + + service.invoke('service-ready', {}); + }); + + service.on('create-subscription').listen((data) { + if (data == null || data['filters'] == null) return; + + final filterMap = data['filters']; + + final filters = filterMap.toList(); + + final request = NostrRequestX.fromJson(filters); + + final subscription = nostrService.subscribeToEvents(request); + + activeSubscriptions[request.subscriptionId!] = { + 'filters': filters, + 'subscription': subscription, + }; + + subscription.listen((event) async { + if (await eventStore.hasItem(event.id!)) return; + await retryNotification(event); + }); + }); + + service.on('cancel-subscription').listen((event) { + if (event == null) return; + + final id = event['id'] as String?; + if (id != null && activeSubscriptions.containsKey(id)) { + activeSubscriptions.remove(id); + nostrService.unsubscribe(id); + } + }); + + service.on("stop").listen((event) async { + nostrService.disconnectFromRelays(); + await db.close(); + service.stopSelf(); + }); + + service.invoke('on-start', { + 'isRunning': true, + }); +} + +@pragma('vm:entry-point') +Future onIosBackground(ServiceInstance service) async { + WidgetsFlutterBinding.ensureInitialized(); + DartPluginRegistrant.ensureInitialized(); + + return true; +} diff --git a/lib/background/background_service.dart b/lib/background/background_service.dart new file mode 100644 index 00000000..9398a94b --- /dev/null +++ b/lib/background/background_service.dart @@ -0,0 +1,17 @@ +import 'dart:io'; +import 'package:flutter/foundation.dart'; +import 'package:mostro_mobile/background/abstract_background_service.dart'; +import 'package:mostro_mobile/background/desktop_background_service.dart'; +import 'package:mostro_mobile/background/mobile_background_service.dart'; +import 'package:mostro_mobile/features/settings/settings.dart'; + +BackgroundService createBackgroundService(Settings settings) { + if (kIsWeb) { + throw UnsupportedError('Background services are not supported on web'); + } + if (Platform.isAndroid || Platform.isIOS) { + return MobileBackgroundService(settings); + } else { + return DesktopBackgroundService(settings); + } +} diff --git a/lib/background/desktop_background_service.dart b/lib/background/desktop_background_service.dart new file mode 100644 index 00000000..1277d045 --- /dev/null +++ b/lib/background/desktop_background_service.dart @@ -0,0 +1,165 @@ +import 'dart:async'; +import 'dart:isolate'; +import 'package:dart_nostr/dart_nostr.dart'; +import 'package:flutter/services.dart'; +import 'package:logger/logger.dart'; +import 'package:mostro_mobile/data/models/nostr_filter.dart'; +import 'package:mostro_mobile/data/repositories.dart'; +import 'package:mostro_mobile/features/settings/settings.dart'; +import 'package:mostro_mobile/services/nostr_service.dart'; +import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; +import 'abstract_background_service.dart'; + +class DesktopBackgroundService implements BackgroundService { + final _subscriptions = >{}; + bool _isRunning = false; + late SendPort _sendPort; + Settings _settings; + + DesktopBackgroundService(this._settings); + + @override + Future init() async {} + + static void isolateEntry(List args) async { + final isolateReceivePort = ReceivePort(); + final mainSendPort = args[0] as SendPort; + final token = args[1] as RootIsolateToken; + + mainSendPort.send(isolateReceivePort.sendPort); + + BackgroundIsolateBinaryMessenger.ensureInitialized(token); + + final nostrService = NostrService(); + final db = await openMostroDatabase('events.db'); + final backgroundStorage = EventStorage(db: db); + final logger = Logger(); + bool isAppForeground = true; + + isolateReceivePort.listen((message) async { + if (message is! Map || message['command'] == null) return; + + final command = message['command']; + + switch (command) { + case 'app-foreground-status': + isAppForeground = message['is-foreground'] ?? isAppForeground; + break; + case 'settings-change': + if (message['settings'] == null) return; + + await nostrService.updateSettings( + Settings.fromJson( + message['settings'], + ), + ); + break; + case 'create-subscription': + if (message['filters'] == null) return; + + final filterMap = message['filters'] as List>; + + final filters = + filterMap.map((e) => NostrFilterX.fromJsonSafe(e)).toList(); + + final request = NostrRequest( + filters: filters, + ); + + final subscription = nostrService.subscribeToEvents(request); + subscription.listen((event) async { + await backgroundStorage.putItem( + event.id!, + event, + ); + mainSendPort.send({ + 'event': event.toMap(), + }); + if (!isAppForeground) { + //await showLocalNotification(event); + } + }); + break; + default: + logger.i('Unknown command: $command'); + break; + } + }); + + mainSendPort.send({ + 'is-running': true, + }); + } + + @override + void subscribe(List filter) { + if (!_isRunning) return; + + _sendPort.send( + { + 'command': 'create-subscription', + 'filter': filter, + }, + ); + } + + @override + Future setForegroundStatus(bool isForeground) async { + if (!_isRunning) return; + _sendPort.send( + { + 'command': 'app-foreground-status', + 'is-foreground': isForeground, + }, + ); + } + + @override + Future getActiveSubscriptionCount() async { + return _subscriptions.length; + } + + @override + Future unsubscribe(String subscriptionId) async { + if (!_isRunning) return false; + + if (!_subscriptions.containsKey(subscriptionId)) { + return false; + } + + _subscriptions.remove(subscriptionId); + _sendPort.send( + { + 'command': 'cancel-subscription', + 'id': subscriptionId, + }, + ); + // If no more subscriptions, stop the service + if (_subscriptions.isEmpty && _isRunning) { + //await _stopService(); + } + return true; + } + + @override + Future unsubscribeAll() async { + if (!_isRunning) return; + for (final id in _subscriptions.keys.toList()) { + await unsubscribe(id); + } + } + + @override + void updateSettings(Settings settings) { + if (!_isRunning) return; + _sendPort.send( + { + 'command': 'settings-change', + 'settings': settings.toJson(), + }, + ); + } + + @override + bool get isRunning => _isRunning; +} diff --git a/lib/background/mobile_background_service.dart b/lib/background/mobile_background_service.dart new file mode 100644 index 00000000..5f290646 --- /dev/null +++ b/lib/background/mobile_background_service.dart @@ -0,0 +1,197 @@ +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/background/background.dart'; +import 'package:mostro_mobile/features/settings/settings.dart'; +import 'abstract_background_service.dart'; + +class MobileBackgroundService implements BackgroundService { + Settings _settings; + + MobileBackgroundService(this._settings); + + final service = FlutterBackgroundService(); + + final _subscriptions = >{}; + bool _isRunning = false; + final _logger = Logger(); + bool _serviceReady = false; + final List _pendingOperations = []; + + @override + Future init() async { + await service.configure( + iosConfiguration: IosConfiguration( + autoStart: true, + onForeground: serviceMain, + onBackground: onIosBackground, + ), + androidConfiguration: AndroidConfiguration( + autoStart: false, + onStart: serviceMain, + isForegroundMode: true, + autoStartOnBoot: true, + initialNotificationTitle: "Mostro P2P", + initialNotificationContent: "Connected to Mostro service", + foregroundServiceTypes: [ + AndroidForegroundType.dataSync, + ]), + ); + + service.on('on-start').listen((data) { + _isRunning = true; + service.invoke('start', { + 'settings': _settings.toJson(), + }); + _logger.d( + 'Service started with settings: ${_settings.toJson()}', + ); + }); + + service.on('on-stop').listen((event) { + _isRunning = false; + _logger.i('Service stopped'); + }); + + service.on('service-ready').listen((data) { + _logger.i("Service confirmed it's ready"); + _serviceReady = true; + _processPendingOperations(); + }); + } + + @override + void subscribe(List filters) { + final subId = DateTime.now().millisecondsSinceEpoch.toString(); + _subscriptions[subId] = {'filters': filters}; + + _executeWhenReady(() { + _logger.i("Sending subscription to service"); + service.invoke('create-subscription', { + 'id': subId, + 'filters': filters.map((f) => f.toMap()).toList(), + }); + }); + } + + @override + Future unsubscribe(String subscriptionId) async { + if (!_subscriptions.containsKey(subscriptionId)) { + return false; + } + + _subscriptions.remove(subscriptionId); + service.invoke('cancel-subscription', { + 'id': subscriptionId, + }); + + if (_subscriptions.isEmpty && _isRunning) { + await _stopService(); + } + + return true; + } + + @override + Future unsubscribeAll() async { + for (final id in _subscriptions.keys.toList()) { + await unsubscribe(id); + } + } + + @override + Future getActiveSubscriptionCount() async { + return _subscriptions.length; + } + + @override + Future setForegroundStatus(bool isForeground) async { + // Always inform the service about status change + service.invoke('app-foreground-status', { + 'is-foreground': isForeground, + }); + + // Check current running state first + final isCurrentlyRunning = await service.isRunning(); + + if (isForeground) { + // Only stop if actually running + if (isCurrentlyRunning) { + await _stopService(); + } + } else { + // Only start if not already running + if (!isCurrentlyRunning) { + try { + await _startService(); + } catch (e) { + _logger.e('Error starting service: $e'); + // Retry with a delay if needed + await Future.delayed(Duration(seconds: 1)); + await _startService(); + } + } + } + } + + Future _startService() async { + _logger.i("Starting service"); + await service.startService(); + _serviceReady = false; // Reset ready state when starting + + // Wait for the service to be running + const maxWait = Duration(seconds: 5); + final deadline = DateTime.now().add(maxWait); + + while (!(await service.isRunning())) { + if (DateTime.now().isAfter(deadline)) { + throw StateError('Background service failed to start within $maxWait'); + } + await Future.delayed(const Duration(milliseconds: 50)); + } + + _logger.i("Service running, sending settings"); + service.invoke('start', { + 'settings': _settings.toJson(), + }); + } + + Future _stopService() async { + service.invoke('stop'); + _isRunning = false; + } + + @override + void updateSettings(Settings settings) { + _settings = settings; + _executeWhenReady(() { + service.invoke('update-settings', { + 'settings': settings.toJson(), + }); + }); + } + + @override + bool get isRunning => _isRunning; + + // Method to execute operations when service is ready + void _executeWhenReady(Function operation) { + if (_serviceReady) { + operation(); + } else { + _pendingOperations.add(operation); + } + } + +// Method to process pending operations + void _processPendingOperations() { + if (_serviceReady) { + for (final operation in _pendingOperations) { + operation(); + } + _pendingOperations.clear(); + } + } +} diff --git a/lib/core/app.dart b/lib/core/app.dart index 69ff51d1..12e44d4b 100644 --- a/lib/core/app.dart +++ b/lib/core/app.dart @@ -7,16 +7,28 @@ import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/features/auth/providers/auth_notifier_provider.dart'; import 'package:mostro_mobile/generated/l10n.dart'; import 'package:mostro_mobile/features/auth/notifiers/auth_state.dart'; +import 'package:mostro_mobile/services/lifecycle_manager.dart'; import 'package:mostro_mobile/shared/providers/app_init_provider.dart'; -class MostroApp extends ConsumerWidget { +class MostroApp extends ConsumerStatefulWidget { const MostroApp({super.key}); static final GlobalKey navigatorKey = GlobalKey(); @override - Widget build(BuildContext context, WidgetRef ref) { + ConsumerState createState() => _MostroAppState(); +} + +class _MostroAppState extends ConsumerState { + @override + void initState() { + super.initState(); + ref.read(lifecycleManagerProvider); + } + + @override + Widget build(BuildContext context) { final initAsyncValue = ref.watch(appInitializerProvider); return initAsyncValue.when( @@ -24,9 +36,11 @@ class MostroApp extends ConsumerWidget { ref.listen(authNotifierProvider, (previous, state) { WidgetsBinding.instance.addPostFrameCallback((_) { if (!context.mounted) return; - if (state is AuthAuthenticated || state is AuthRegistrationSuccess) { + if (state is AuthAuthenticated || + state is AuthRegistrationSuccess) { context.go('/'); - } else if (state is AuthUnregistered || state is AuthUnauthenticated) { + } else if (state is AuthUnregistered || + state is AuthUnauthenticated) { context.go('/'); } }); @@ -51,7 +65,7 @@ class MostroApp extends ConsumerWidget { darkTheme: AppTheme.theme, home: Scaffold( backgroundColor: AppTheme.dark1, - body: Center(child: CircularProgressIndicator()), + body: const Center(child: CircularProgressIndicator()), ), ), error: (err, stack) => MaterialApp( diff --git a/lib/core/app_routes.dart b/lib/core/app_routes.dart index 5bebe4a1..f9dee2b7 100644 --- a/lib/core/app_routes.dart +++ b/lib/core/app_routes.dart @@ -36,65 +36,118 @@ final goRouter = GoRouter( routes: [ GoRoute( path: '/', - builder: (context, state) => const HomeScreen(), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: const HomeScreen(), + ), ), GoRoute( path: '/welcome', - builder: (context, state) => const WelcomeScreen(), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: const WelcomeScreen(), + ), ), GoRoute( path: '/order_book', - builder: (context, state) => const TradesScreen(), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: const TradesScreen(), + ), ), GoRoute( path: '/trade_detail/:orderId', - builder: (context, state) => TradeDetailScreen( - orderId: state.pathParameters['orderId']!, - ), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: TradeDetailScreen( + orderId: state.pathParameters['orderId']!, + )), ), GoRoute( path: '/chat_list', - builder: (context, state) => const ChatRoomsScreen(), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: const ChatRoomsScreen(), + ), ), GoRoute( path: '/chat_room/:orderId', - builder: (context, state) => ChatRoomScreen( - orderId: state.pathParameters['orderId']!, - ), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: ChatRoomScreen( + orderId: state.pathParameters['orderId']!, + )), ), GoRoute( path: '/register', - builder: (context, state) => const RegisterScreen(), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: const RegisterScreen(), + ), ), GoRoute( path: '/relays', - builder: (context, state) => const RelaysScreen(), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: const RelaysScreen(), + ), ), GoRoute( path: '/key_management', - builder: (context, state) => const KeyManagementScreen(), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: const KeyManagementScreen(), + ), ), GoRoute( path: '/settings', - builder: (context, state) => const SettingsScreen(), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: const SettingsScreen(), + ), ), GoRoute( path: '/about', - builder: (context, state) => const AboutScreen(), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: const AboutScreen(), + ), ), GoRoute( path: '/walkthrough', - builder: (context, state) => WalkthroughScreen(), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: WalkthroughScreen(), + ), ), GoRoute( path: '/add_order', - builder: (context, state) => AddOrderScreen(), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: AddOrderScreen(), + ), ), GoRoute( path: '/rate_user/:orderId', - builder: (context, state) => RateCounterpartScreen( - orderId: state.pathParameters['orderId']!, - ), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: RateCounterpartScreen( + orderId: state.pathParameters['orderId']!, + )), ), GoRoute( path: '/take_sell/:orderId', @@ -112,19 +165,30 @@ final goRouter = GoRouter( ), GoRoute( path: '/order_confirmed/:orderId', - builder: (context, state) => OrderConfirmationScreen( - orderId: state.pathParameters['orderId']!, - ), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: OrderConfirmationScreen( + orderId: state.pathParameters['orderId']!, + )), ), GoRoute( path: '/pay_invoice/:orderId', - builder: (context, state) => PayLightningInvoiceScreen( - orderId: state.pathParameters['orderId']!), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: PayLightningInvoiceScreen( + orderId: state.pathParameters['orderId']!, + )), ), GoRoute( path: '/add_invoice/:orderId', - builder: (context, state) => AddLightningInvoiceScreen( - orderId: state.pathParameters['orderId']!), + pageBuilder: (context, state) => buildPageWithDefaultTransition( + context: context, + state: state, + child: AddLightningInvoiceScreen( + orderId: state.pathParameters['orderId']!, + )), ), ], ), @@ -134,3 +198,21 @@ final goRouter = GoRouter( body: Center(child: Text(state.error.toString())), ), ); + +CustomTransitionPage buildPageWithDefaultTransition({ + required BuildContext context, + required GoRouterState state, + required Widget child, +}) { + return CustomTransitionPage( + key: state.pageKey, + child: child, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + return FadeTransition( + opacity: CurveTween(curve: Curves.easeInOut).animate(animation), + child: child, + ); + }, + transitionDuration: const Duration(milliseconds: 150), + ); +} diff --git a/lib/core/app_theme.dart b/lib/core/app_theme.dart index 168aef68..6c48b055 100644 --- a/lib/core/app_theme.dart +++ b/lib/core/app_theme.dart @@ -57,7 +57,7 @@ class AppTheme { backgroundColor: mostroGreen, textStyle: GoogleFonts.robotoCondensed( fontWeight: FontWeight.w500, - fontSize: 16.0, + fontSize: 14.0, ), padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 30), ), diff --git a/lib/core/config.dart b/lib/core/config.dart index bb105062..7ffd74e8 100644 --- a/lib/core/config.dart +++ b/lib/core/config.dart @@ -30,4 +30,9 @@ class Config { static int expirationSeconds = 900; static int expirationHours = 24; + + // Configuración de notificaciones + static String notificationChannelId = 'mostro_mobile'; + static int notificationId = 38383; + } diff --git a/lib/core/mostro_fsm.dart b/lib/core/mostro_fsm.dart new file mode 100644 index 00000000..d213daa8 --- /dev/null +++ b/lib/core/mostro_fsm.dart @@ -0,0 +1,93 @@ +import 'package:mostro_mobile/data/models/enums/action.dart'; +import 'package:mostro_mobile/data/models/enums/status.dart'; + +/// Finite-State-Machine helper for Mostro order lifecycles. +/// +/// This table was generated directly from the authoritative +/// specification sent by the Mostro team. Only *state–transition → +/// next-state* information is encoded here. All auxiliary / neutral +/// notifications intentionally map to the **same** state so that +/// `nextStatus` always returns a non-null value. +class MostroFSM { + MostroFSM._(); + + /// Nested map: *currentStatus → { action → nextStatus }*. + static final Map> _transitions = { + // ───────────────────────── MATCHING / TAKING ──────────────────────── + Status.pending: { + Action.takeSell: Status.waitingBuyerInvoice, + Action.takeBuy: Status.waitingBuyerInvoice, // invoice presence handled elsewhere + Action.cancel: Status.canceled, + Action.disputeInitiatedByYou: Status.dispute, + Action.disputeInitiatedByPeer: Status.dispute, + }, + + // ───────────────────────── INVOICING ──────────────────────────────── + Status.waitingBuyerInvoice: { + Action.addInvoice: Status.waitingPayment, + Action.cancel: Status.canceled, + Action.disputeInitiatedByYou: Status.dispute, + Action.disputeInitiatedByPeer: Status.dispute, + }, + + // ───────────────────────── HOLD INVOICE PAYMENT ──────────────────── + Status.waitingPayment: { + Action.payInvoice: Status.active, + Action.holdInvoicePaymentAccepted: Status.active, + Action.holdInvoicePaymentCanceled: Status.canceled, + Action.cancel: Status.canceled, + Action.disputeInitiatedByYou: Status.dispute, + Action.disputeInitiatedByPeer: Status.dispute, + }, + + // ───────────────────────── ACTIVE TRADE ──────────────────────────── + Status.active: { + Action.fiatSent: Status.fiatSent, + Action.cooperativeCancelInitiatedByYou: Status.cooperativelyCanceled, + Action.cooperativeCancelInitiatedByPeer: Status.cooperativelyCanceled, + Action.cancel: Status.canceled, + Action.disputeInitiatedByYou: Status.dispute, + Action.disputeInitiatedByPeer: Status.dispute, + }, + + // ───────────────────────── AFTER FIAT SENT ───────────────────────── + Status.fiatSent: { + Action.release: Status.settledHoldInvoice, + Action.holdInvoicePaymentSettled: Status.settledHoldInvoice, + Action.cooperativeCancelInitiatedByYou: Status.cooperativelyCanceled, + Action.cooperativeCancelInitiatedByPeer: Status.cooperativelyCanceled, + Action.cancel: Status.canceled, + Action.disputeInitiatedByYou: Status.dispute, + Action.disputeInitiatedByPeer: Status.dispute, + }, + + // ───────────────────────── AFTER HOLD INVOICE SETTLED ────────────── + Status.settledHoldInvoice: { + Action.purchaseCompleted: Status.success, + Action.disputeInitiatedByYou: Status.dispute, + Action.disputeInitiatedByPeer: Status.dispute, + }, + + // ───────────────────────── DISPUTE BRANCH ────────────────────────── + Status.dispute: { + Action.adminSettle: Status.settledByAdmin, + Action.adminSettled: Status.settledByAdmin, + Action.adminCancel: Status.canceledByAdmin, + Action.adminCanceled: Status.canceledByAdmin, + }, + }; + + /// Returns the next `Status` after applying [action] to [current]. + /// If the action does **not** cause a state change, the same status + /// is returned. This makes it easier to call without additional + /// null-checking. + static Status nextStatus(Status? current, Action action) { + // Note: Initial state handled externally — we start from `pending` when the + // very first `newOrder` message arrives, so there is no `Status.start` + // entry here. + // If current is null (unknown), treat as pending so that first transition + // works for historical messages. + final safeCurrent = current ?? Status.pending; + return _transitions[safeCurrent]?[action] ?? safeCurrent; + } +} diff --git a/lib/data/models.dart b/lib/data/models.dart new file mode 100644 index 00000000..338f713d --- /dev/null +++ b/lib/data/models.dart @@ -0,0 +1,16 @@ +export 'package:mostro_mobile/data/models/amount.dart'; +export 'package:mostro_mobile/data/models/cant_do.dart'; +export 'package:mostro_mobile/data/models/dispute.dart'; +export 'package:mostro_mobile/data/models/mostro_message.dart'; +export 'package:mostro_mobile/data/models/nostr_event.dart'; +export 'package:mostro_mobile/data/models/order.dart'; +export 'package:mostro_mobile/data/models/payload.dart'; +export 'package:mostro_mobile/data/models/payment_request.dart'; +export 'package:mostro_mobile/data/models/rating_user.dart'; +export 'package:mostro_mobile/data/models/rating.dart'; +export 'package:mostro_mobile/data/models/session.dart'; + +export 'package:mostro_mobile/data/models/enums/order_type.dart'; +export 'package:mostro_mobile/data/models/enums/role.dart'; +export 'package:mostro_mobile/data/models/enums/action.dart'; +export 'package:mostro_mobile/data/models/enums/status.dart'; \ No newline at end of file diff --git a/lib/data/models/enums/storage_keys.dart b/lib/data/models/enums/storage_keys.dart index 6ed42b84..de24fcc8 100644 --- a/lib/data/models/enums/storage_keys.dart +++ b/lib/data/models/enums/storage_keys.dart @@ -27,9 +27,7 @@ enum SharedPreferencesKeys { enum SecureStorageKeys { masterKey('master_key'), - mnemonic('mnemonic'), - message('message-'), - sessionKey('session-'); + mnemonic('mnemonic'); final String value; diff --git a/lib/data/models/mostro_message.dart b/lib/data/models/mostro_message.dart index 94e88202..ae600346 100644 --- a/lib/data/models/mostro_message.dart +++ b/lib/data/models/mostro_message.dart @@ -14,14 +14,16 @@ class MostroMessage { final Action action; int? tradeIndex; T? _payload; + int? timestamp; - MostroMessage({ - required this.action, - this.requestId, - this.id, - T? payload, - this.tradeIndex, - }) : _payload = payload; + MostroMessage( + {required this.action, + this.requestId, + this.id, + T? payload, + this.tradeIndex, + this.timestamp}) + : _payload = payload; Map toJson() { Map json = { @@ -38,9 +40,10 @@ class MostroMessage { } factory MostroMessage.fromJson(Map json) { + final timestamp = json['timestamp']; json = json['order'] ?? json['cant-do'] ?? json; - + return MostroMessage( action: Action.fromString(json['action']), requestId: json['request_id'], @@ -49,6 +52,7 @@ class MostroMessage { payload: json['payload'] != null ? Payload.fromJson(json['payload']) as T? : null, + timestamp: timestamp, ); } diff --git a/lib/data/models/nostr_filter.dart b/lib/data/models/nostr_filter.dart new file mode 100644 index 00000000..e1165912 --- /dev/null +++ b/lib/data/models/nostr_filter.dart @@ -0,0 +1,71 @@ +import 'package:dart_nostr/dart_nostr.dart'; + +extension NostrRequestX on NostrRequest { + static NostrRequest fromJson(List json) { + + final filters = json + .map( + (e) => NostrFilterX.fromJsonSafe(e), + ) + .toList(); + + return NostrRequest( + filters: filters, + ); + } +} + +extension NostrFilterX on NostrFilter { + static NostrFilter fromJsonSafe(Map json) { + final additional = {}; + + for (final entry in json.entries) { + if (![ + 'ids', + 'authors', + 'kinds', + '#e', + '#p', + '#t', + '#a', + 'since', + 'until', + 'limit', + 'search', + ].contains(entry.key)) { + additional[entry.key] = entry.value; + } + } + + return NostrFilter( + ids: castList(json['ids']), + authors: castList(json['authors']), + kinds: castList(json['kinds']), + e: castList(json['#e']), + p: castList(json['#p']), + t: castList(json['#t']), + a: castList(json['#a']), + since: safeCast(json['since']) != null + ? DateTime.fromMillisecondsSinceEpoch( + safeCast(json['since'])! * 1000) + : null, + until: safeCast(json['until']) != null + ? DateTime.fromMillisecondsSinceEpoch( + safeCast(json['until'])! * 1000) + : null, + limit: safeCast(json['limit']), + search: safeCast(json['search']), + additionalFilters: additional.isEmpty ? null : additional, + ); + } + + static T? safeCast(dynamic value) { + if (value is T) return value; + return null; + } + + static List? castList(dynamic value) { + if (value is List) return value.cast(); + return null; + } +} diff --git a/lib/data/models/order.dart b/lib/data/models/order.dart index f2f544af..5f221fec 100644 --- a/lib/data/models/order.dart +++ b/lib/data/models/order.dart @@ -72,11 +72,11 @@ class Order implements Payload { data[type]['buyer_token'] = buyerToken; data[type]['seller_token'] = sellerToken; - if (masterBuyerPubkey != null) { - data[type]['buyer_trade_pubkey'] = masterBuyerPubkey; + if (buyerTradePubkey != null) { + data[type]['buyer_trade_pubkey'] = buyerTradePubkey; } - if (masterSellerPubkey != null) { - data[type]['seller_trade_pubkey'] = masterSellerPubkey; + if (sellerTradePubkey != null) { + data[type]['seller_trade_pubkey'] = sellerTradePubkey; } return data; } diff --git a/lib/data/models/session.dart b/lib/data/models/session.dart index c0165155..feba2707 100644 --- a/lib/data/models/session.dart +++ b/lib/data/models/session.dart @@ -64,10 +64,15 @@ class Session { Peer? get peer => _peer; set peer(Peer? newPeer) { + if (newPeer == null) { + _peer = null; + _sharedKey = null; + return; + } _peer = newPeer; _sharedKey = NostrUtils.computeSharedKey( tradeKey.private, - newPeer!.publicKey, + newPeer.publicKey, ); } } diff --git a/lib/data/repositories.dart b/lib/data/repositories.dart new file mode 100644 index 00000000..167fcb53 --- /dev/null +++ b/lib/data/repositories.dart @@ -0,0 +1,2 @@ +export 'package:mostro_mobile/data/repositories/event_storage.dart'; +export 'package:mostro_mobile/data/repositories/mostro_storage.dart'; diff --git a/lib/data/repositories/base_storage.dart b/lib/data/repositories/base_storage.dart index f242a133..e5490494 100644 --- a/lib/data/repositories/base_storage.dart +++ b/lib/data/repositories/base_storage.dart @@ -1,11 +1,12 @@ import 'package:sembast/sembast.dart'; +/// Base repository /// A base interface for a Sembast-backed storage of items of type [T]. +/// Sub-class must implement: +/// • `toDbMap` → encode T → Map +/// • `fromDbMap` → decode Map → T abstract class BaseStorage { - /// Reference to the Sembast database. final Database db; - - /// The Sembast store reference (string key, Map<String, dynamic> value). final StoreRef> store; BaseStorage(this.db, this.store); @@ -24,32 +25,12 @@ abstract class BaseStorage { }); } - /// Retrieve an item by [id]. Future getItem(String id) async { - final record = await store.record(id).get(db); - if (record == null) return null; - return fromDbMap(id, record); + final json = await store.record(id).get(db); + return json == null ? null : fromDbMap(id, json); } - /// Check if an item exists in the data store by [id]. - Future hasItem(String id) async { - return await store.record(id).exists(db); - } - - /// Return all items in the store. - Future> getAllItems() async { - final records = await store.find(db); - final result = []; - for (final record in records) { - try { - final item = fromDbMap(record.key, record.value); - result.add(item); - } catch (e) { - // Optionally handle or log parse errors - } - } - return result; - } + Future hasItem(String id) => store.record(id).exists(db); /// Delete an item by [id]. Future deleteItem(String id) async { @@ -59,44 +40,63 @@ abstract class BaseStorage { } /// Delete all items in the store. - Future deleteAllItems() async { + Future deleteAll() async { await db.transaction((txn) async { await store.delete(txn); }); } - /// Delete items that match a predicate [filter]. - /// Return the list of deleted IDs. - Future> deleteWhere(bool Function(T) filter, - {int? maxBatchSize}) async { - final toDelete = []; - final records = await store.find(db); + /// Delete by arbitrary Sembast [Filter]. + Future deleteWhere(Filter filter) async { + return await db.transaction((txn) async { + return await store.delete( + txn, + finder: Finder(filter: filter), + ); + }); + } - for (final record in records) { - if (maxBatchSize != null && toDelete.length >= maxBatchSize) { - break; - } - try { - final item = fromDbMap(record.key, record.value); - if (filter(item)) { - toDelete.add(record.key); - } - } catch (_) { - // Could not parse => also consider removing or ignoring - toDelete.add(record.key); - } - } + Future> find({ + Filter? filter, + List? sort, + int? limit, + int? offset, + }) async { + final records = await store.find( + db, + finder: Finder( + filter: filter, + sortOrders: sort, + limit: limit, + offset: offset, + ), + ); + return records + .map((rec) => fromDbMap(rec.key, rec.value)) + .toList(growable: false); + } - // Remove the matched records in a transaction - await db.transaction((txn) async { - for (final key in toDelete) { - await store.record(key).delete(txn); - } - }); + Future> getAll() => find(); + + Stream> watch({ + Filter? filter, + List? sort, + }) { + final query = store.query( + finder: Finder(filter: filter, sortOrders: sort), + ); + return query.onSnapshots(db).map((snaps) => + snaps.map((s) => fromDbMap(s.key, s.value)).toList(growable: false)); + } - return toDelete; + /// Watch a single record by its [id] – emits *null* when deleted. + Stream watchById(String id) { + return store + .record(id) + .onSnapshot(db) + .map((snap) => snap == null ? null : fromDbMap(id, snap.value)); } - /// If needed, close or clean up resources here. - void dispose() {} + /// Equality filter on a given [field] (`x == value`) + Filter eq(String field, Object? value) => Filter.equals(field, value); } diff --git a/lib/data/repositories/chat_repository.dart b/lib/data/repositories/chat_repository.dart deleted file mode 100644 index 198f9364..00000000 --- a/lib/data/repositories/chat_repository.dart +++ /dev/null @@ -1,4 +0,0 @@ -class ChatRepository { - - -} \ No newline at end of file diff --git a/lib/data/repositories/event_storage.dart b/lib/data/repositories/event_storage.dart index a345452f..956423b9 100644 --- a/lib/data/repositories/event_storage.dart +++ b/lib/data/repositories/event_storage.dart @@ -39,4 +39,47 @@ class EventStorage extends BaseStorage { Map toDbMap(NostrEvent event) { return event.toMap(); } + + /// Stream of all events for a query + Stream> watchAll({Filter? filter}) { + final finder = filter != null ? Finder(filter: filter) : null; + + return store + .query(finder: finder) + .onSnapshots(db) + .map((snapshots) => snapshots + .map((snapshot) => fromDbMap(snapshot.key, snapshot.value)) + .toList()); + } + + /// Stream of the latest event matching a query + Stream watchLatest({Filter? filter, List? sortOrders}) { + final finder = Finder( + filter: filter, + sortOrders: sortOrders ?? [SortOrder('created_at', false)], + limit: 1 + ); + + return store + .query(finder: finder) + .onSnapshots(db) + .map((snapshots) => snapshots.isNotEmpty + ? fromDbMap(snapshots.first.key, snapshots.first.value) + : null); + } + + /// Stream of events filtered by event ID + Stream watchById(String eventId) { + final finder = Finder( + filter: Filter.equals('id', eventId), + limit: 1 + ); + + return store + .query(finder: finder) + .onSnapshots(db) + .map((snapshots) => snapshots.isNotEmpty + ? fromDbMap(snapshots.first.key, snapshots.first.value) + : null); + } } diff --git a/lib/data/repositories/mostro_storage.dart b/lib/data/repositories/mostro_storage.dart index 4c552de5..6de134c7 100644 --- a/lib/data/repositories/mostro_storage.dart +++ b/lib/data/repositories/mostro_storage.dart @@ -11,15 +11,23 @@ class MostroStorage extends BaseStorage { : super(db, stringMapStoreFactory.store('orders')); /// Save or update any MostroMessage - Future addMessage(MostroMessage message) async { - final id = messageKey(message); + Future addMessage(String key, MostroMessage message) async { + final id = key; try { - await putItem(id, message); + // Add metadata for easier querying + final Map dbMap = message.toJson(); + if (message.timestamp == null) { + message.timestamp = DateTime.now().millisecondsSinceEpoch; + } + dbMap['timestamp'] = message.timestamp; + + await store.record(id).put(db, dbMap); _logger.i( - 'Saved message of type \${message.payload.runtimeType} with id \$id'); + 'Saved message of type ${message.payload?.runtimeType} with id $id', + ); } catch (e, stack) { _logger.e( - 'addMessage failed for \$id', + 'addMessage failed for $id', error: e, stackTrace: stack, ); @@ -27,13 +35,6 @@ class MostroStorage extends BaseStorage { } } - /// Save or update a list of MostroMessages - Future addMessages(List messages) async { - for (final message in messages) { - await addMessage(message); - } - } - /// Retrieve a MostroMessage by ID Future getMessageById( String orderId, @@ -43,8 +44,7 @@ class MostroStorage extends BaseStorage { try { return await getItem(id); } catch (e, stack) { - _logger.e('Error deserializing message \$id', - error: e, stackTrace: stack); + _logger.e('Error deserializing message $id', error: e, stackTrace: stack); return null; } } @@ -52,29 +52,17 @@ class MostroStorage extends BaseStorage { /// Get all messages Future> getAllMessages() async { try { - return await getAllItems(); + return await getAll(); } catch (e, stack) { _logger.e('getAllMessages failed', error: e, stackTrace: stack); return []; } } - /// Delete a message by ID - Future deleteMessage(String orderId) async { - final id = '${T.runtimeType}:$orderId'; - try { - await deleteItem(id); - _logger.i('Message \$id deleted from DB'); - } catch (e, stack) { - _logger.e('deleteMessage failed for \$id', error: e, stackTrace: stack); - rethrow; - } - } - /// Delete all messages Future deleteAllMessages() async { try { - await deleteAllItems(); + await deleteAll(); _logger.i('All messages deleted'); } catch (e, stack) { _logger.e('deleteAllMessages failed', error: e, stackTrace: stack); @@ -83,29 +71,14 @@ class MostroStorage extends BaseStorage { } /// Delete all messages by Id regardless of type - Future deleteAllMessagesById(String orderId) async { - try { - final messages = await getMessagesForId(orderId); - for (var m in messages) { - final id = messageKey(m); - try { - await deleteItem(id); - _logger.i('Message \$id deleted from DB'); - } catch (e, stack) { - _logger.e('deleteMessage failed for \$id', - error: e, stackTrace: stack); - rethrow; - } - } - _logger.i('All messages for order: $orderId deleted'); - } catch (e, stack) { - _logger.e('deleteAllMessagesForId failed', error: e, stackTrace: stack); - rethrow; - } + Future deleteAllMessagesByOrderId(String orderId) async { + await deleteWhere( + Filter.equals('id', orderId), + ); } /// Filter messages by payload type - Future>> getMessagesOfType() async { + Future> getMessagesOfType() async { final messages = await getAllMessages(); return messages .where((m) => m.payload is T) @@ -113,6 +86,19 @@ class MostroStorage extends BaseStorage { .toList(); } + /// Filter messages by payload type + Future getLatestMessageOfTypeById( + String orderId, + ) async { + final messages = await getMessagesForId(orderId); + for (final message in messages.reversed) { + if (message.payload is T) { + return message; + } + } + return null; + } + /// Filter messages by tradeKeyPublic Future> getMessagesForId(String orderId) async { final messages = await getAllMessages(); @@ -129,10 +115,90 @@ class MostroStorage extends BaseStorage { return item.toJson(); } - String messageKey(MostroMessage msg) { - final type = - msg.payload != null ? msg.payload.runtimeType.toString() : 'Order'; - final id = msg.id ?? msg.requestId.toString(); - return '$type:$id'; + Future hasMessageByKey(String key) async { + return hasItem(key); + } + + /// Get the latest message for an order, regardless of type + Future getLatestMessageById(String orderId) async { + final finder = Finder( + filter: Filter.equals('id', orderId), + sortOrders: _getDefaultSort(), + limit: 1, + ); + + final snapshot = await store.findFirst(db, finder: finder); + if (snapshot != null) { + return MostroMessage.fromJson(snapshot.value); + } + return null; + } + + /// Stream of the latest message for an order + Stream watchLatestMessage(String orderId) { + // We want to watch ALL messages for this orderId, not just a specific key + final query = store.query( + finder: Finder( + filter: Filter.equals('id', orderId), + sortOrders: _getDefaultSort(), + limit: 1, + ), + ); + + return query + .onSnapshots(db) + .map((snaps) => snaps.isEmpty ? null : MostroMessage.fromJson(snaps.first.value)); + } + + /// Stream of the latest message for an order whose payload is of type T + Stream watchLatestMessageOfType(String orderId) { + // Watch all messages for the orderId, sorted by timestamp descending + final query = store.query( + finder: Finder( + filter: Filter.equals('id', orderId), + sortOrders: _getDefaultSort(), + ), + ); + return query.onSnapshots(db).map((snaps) { + for (final snap in snaps) { + final msg = MostroMessage.fromJson(snap.value); + if (msg.payload is T) { + return msg; + } + } + return null; + }); + } + + // Use the same sorting across all methods that return lists of messages + List _getDefaultSort() => [SortOrder('timestamp', false, true)]; + + /// Stream of all messages for an order + Stream> watchAllMessages(String orderId) { + return watch( + filter: Filter.equals('id', orderId), + sort: _getDefaultSort(), + ); + } + + /// Stream of all messages for an order + Stream watchByRequestId(int requestId) { + final query = store.query( + finder: Finder(filter: Filter.equals('request_id', requestId)), + ); + return query + .onSnapshot(db) + .map((snap) => snap == null ? null : fromDbMap('', snap.value)); + } + + Future> getAllMessagesForOrderId(String orderId) async { + final finder = Finder( + filter: Filter.equals('id', orderId), + sortOrders: [SortOrder('timestamp', false)]); + + final snapshots = await store.find(db, finder: finder); + return snapshots + .map((snapshot) => MostroMessage.fromJson(snapshot.value)) + .toList(); } } diff --git a/lib/data/repositories/open_orders_repository.dart b/lib/data/repositories/open_orders_repository.dart index e120b9eb..325049d0 100644 --- a/lib/data/repositories/open_orders_repository.dart +++ b/lib/data/repositories/open_orders_repository.dart @@ -1,6 +1,7 @@ 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'; @@ -24,7 +25,10 @@ class OpenOrdersRepository implements OrderRepository { NostrEvent? get mostroInstance => _mostroInstance; OpenOrdersRepository(this._nostrService, this._settings) { + // Subscribe to orders and initialize data _subscribeToOrders(); + // Immediately emit current (possibly empty) cache so UI doesn't remain in loading state + _emitEvents(); } /// Subscribes to events matching the given filter. @@ -33,13 +37,18 @@ class OpenOrdersRepository implements OrderRepository { final filterTime = DateTime.now().subtract(Duration(hours: orderFilterDurationHours)); - var filter = NostrFilter( - kinds: const [orderEventKind], - authors: [_settings.mostroPublicKey], + + final filter = NostrFilter( + kinds: [orderEventKind], since: filterTime, + authors: [_settings.mostroPublicKey], ); - _subscription = _nostrService.subscribeToEvents(filter).listen((event) { + final request = NostrRequest( + filters: [filter], + ); + + _subscription = _nostrService.subscribeToEvents(request).listen((event) { if (event.type == 'order') { _events[event.orderId!] = event; _eventStreamController.add(_events.values.toList()); @@ -50,7 +59,17 @@ class OpenOrdersRepository implements OrderRepository { } }, onError: (error) { _logger.e('Error in order subscription: $error'); + // Optionally, you could auto-resubscribe here if desired }); + + // Ensure listeners receive at least one snapshot right after (re)subscription + _emitEvents(); + } + + void _emitEvents() { + if (!_eventStreamController.isClosed) { + _eventStreamController.add(_events.values.toList()); + } } @override @@ -65,30 +84,41 @@ class OpenOrdersRepository implements OrderRepository { return Future.value(_events[orderId]); } - Stream> get eventsStream => _eventStreamController.stream; + // Stream that immediately emits current cache to every new listener before + // forwarding live updates. + Stream> get eventsStream async* { + // Emit cached events synchronously. + yield _events.values.toList(); + // Forward subsequent updates. + yield* _eventStreamController.stream; + } @override Future addOrder(NostrEvent order) { - // TODO: implement addOrder - throw UnimplementedError(); + _events[order.id!] = order; + _emitEvents(); + return Future.value(); } @override Future deleteOrder(String orderId) { - // TODO: implement deleteOrder - throw UnimplementedError(); + _events.remove(orderId); + _emitEvents(); + return Future.value(); } @override Future> getAllOrders() { - // TODO: implement getAllOrders - throw UnimplementedError(); + return Future.value(_events.values.toList()); } @override Future updateOrder(NostrEvent order) { - // TODO: implement updateOrder - throw UnimplementedError(); + if (order.id != null && _events.containsKey(order.id)) { + _events[order.id!] = order; + _emitEvents(); + } + return Future.value(); } void updateSettings(Settings settings) { @@ -101,4 +131,10 @@ class OpenOrdersRepository implements OrderRepository { _settings = settings.copyWith(); } } + + void reloadData() { + _logger.i('Reloading repository data'); + _subscribeToOrders(); + _emitEvents(); + } } diff --git a/lib/data/repositories/order_storage.dart b/lib/data/repositories/order_storage.dart deleted file mode 100644 index 337856ac..00000000 --- a/lib/data/repositories/order_storage.dart +++ /dev/null @@ -1,95 +0,0 @@ -import 'dart:async'; -import 'package:logger/logger.dart'; -import 'package:mostro_mobile/data/models/order.dart'; -import 'package:mostro_mobile/data/repositories/base_storage.dart'; -import 'package:sembast/sembast.dart'; - -class OrderStorage extends BaseStorage { - final Logger _logger = Logger(); - - OrderStorage({ - required Database db, - }) : super( - db, - stringMapStoreFactory.store('orders'), - ); - - Future init() async { - await getAllOrders(); - } - - /// Save or update an Order - Future addOrder(Order order) async { - final orderId = order.id; - if (orderId == null) { - throw ArgumentError('Cannot save an order with a null order.id'); - } - - try { - await putItem(orderId, order); - _logger.i('Order $orderId saved'); - } catch (e, stack) { - _logger.e('addOrder failed for $orderId', error: e, stackTrace: stack); - rethrow; - } - } - - Future addOrders(List orders) async { - for (final order in orders) { - addOrder(order); - } - } - - /// Retrieve an order by ID - Future getOrderById(String orderId) async { - try { - return await getItem(orderId); - } catch (e, stack) { - _logger.e('Error deserializing order $orderId', - error: e, stackTrace: stack); - return null; - } - } - - /// Return all orders - Future> getAllOrders() async { - try { - return await getAllItems(); - } catch (e, stack) { - _logger.e('getAllOrders failed', error: e, stackTrace: stack); - return []; - } - } - - /// Delete an order from DB - Future deleteOrder(String orderId) async { - try { - await deleteItem(orderId); - _logger.i('Order $orderId deleted from DB'); - } catch (e, stack) { - _logger.e('deleteOrder failed for $orderId', error: e, stackTrace: stack); - rethrow; - } - } - - /// Delete all orders - Future deleteAllOrders() async { - try { - await deleteAllItems(); - _logger.i('All orders deleted'); - } catch (e, stack) { - _logger.e('deleteAllOrders failed', error: e, stackTrace: stack); - rethrow; - } - } - - @override - Order fromDbMap(String key, Map jsonMap) { - return Order.fromJson(jsonMap); - } - - @override - Map toDbMap(Order item) { - return item.toJson(); - } -} diff --git a/lib/data/repositories/session_storage.dart b/lib/data/repositories/session_storage.dart index 59a34dac..df722c29 100644 --- a/lib/data/repositories/session_storage.dart +++ b/lib/data/repositories/session_storage.dart @@ -56,17 +56,15 @@ class SessionStorage extends BaseStorage { Future getSession(String sessionId) => getItem(sessionId); /// Shortcut to get all sessions (direct pass-through). - Future> getAllSessions() => getAllItems(); + Future> getAllSessions() => getAll(); /// Shortcut to remove a specific session by its ID. Future deleteSession(String sessionId) => deleteItem(sessionId); - Future> deleteExpiredSessions( - int sessionExpirationHours, int maxBatchSize) { - final now = DateTime.now(); - return deleteWhere((session) { - final startTime = session.startTime; - return now.difference(startTime).inHours >= sessionExpirationHours; - }, maxBatchSize: maxBatchSize); - } + /// Watch a single session by ID with immediate value emission + Stream watchSession(String sessionId) => watchById(sessionId); + + /// Watch all sessions with immediate value emission + Stream> watchAllSessions() => watch(); + } diff --git a/lib/features/chat/notifiers/chat_room_notifier.dart b/lib/features/chat/notifiers/chat_room_notifier.dart index 50b171a4..2e2305fb 100644 --- a/lib/features/chat/notifiers/chat_room_notifier.dart +++ b/lib/features/chat/notifiers/chat_room_notifier.dart @@ -5,36 +5,70 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/chat_room.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; +import 'package:mostro_mobile/services/lifecycle_manager.dart'; +import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; class ChatRoomNotifier extends StateNotifier { + /// Reload the chat room by re-subscribing to events. + void reload() { + subscription.cancel(); + subscribe(); + } + final _logger = Logger(); final String orderId; final Ref ref; late StreamSubscription subscription; - ChatRoomNotifier(super.state, this.orderId, this.ref); + ChatRoomNotifier( + super.state, + this.orderId, + this.ref, + ); void subscribe() { final session = ref.read(sessionProvider(orderId)); + if (session == null) { + _logger.e('Session is null'); + return; + } + if (session.sharedKey == null) { + _logger.e('Shared key is null'); + return; + } final filter = NostrFilter( kinds: [1059], - p: [session!.sharedKey!.public], + p: [session.sharedKey!.public], + ); + final request = NostrRequest( + filters: [filter], ); + + ref.read(lifecycleManagerProvider).addSubscription(filter); + subscription = - ref.read(nostrServiceProvider).subscribeToEvents(filter).listen( + ref.read(nostrServiceProvider).subscribeToEvents(request).listen( (event) async { try { + final eventStore = ref.read(eventStorageProvider); + + await eventStore.putItem( + event.id!, + event, + ); + final chat = await event.mostroUnWrap(session.sharedKey!); - if (!state.messages.contains(chat)) { - state = state.copy( - messages: [ - ...state.messages, - chat, - ], - ); - } + // Deduplicate by message ID and always sort by createdAt + final allMessages = [ + ...state.messages, + chat, + ]; + // Use a map to deduplicate by event id + final deduped = {for (var m in allMessages) m.id: m}.values.toList(); + deduped.sort((a, b) => a.createdAt!.compareTo(b.createdAt!)); + state = state.copy(messages: deduped); } catch (e) { _logger.e(e); } @@ -44,8 +78,16 @@ class ChatRoomNotifier extends StateNotifier { Future sendMessage(String text) async { final session = ref.read(sessionProvider(orderId)); + if (session == null) { + _logger.e('Session is null'); + return; + } + if (session.sharedKey == null) { + _logger.e('Shared key is null'); + return; + } final event = NostrEvent.fromPartialData( - keyPairs: session!.tradeKey, + keyPairs: session.tradeKey, content: text, kind: 1, ); diff --git a/lib/features/chat/notifiers/chat_rooms_notifier.dart b/lib/features/chat/notifiers/chat_rooms_notifier.dart index d69e88dc..26de1aff 100644 --- a/lib/features/chat/notifiers/chat_rooms_notifier.dart +++ b/lib/features/chat/notifiers/chat_rooms_notifier.dart @@ -14,13 +14,43 @@ class ChatRoomsNotifier extends StateNotifier> { loadChats(); } + /// Reload all chat rooms by triggering their notifiers to resubscribe to events. + void reloadAllChats() { + for (final chat in state) { + try { + final notifier = ref.read(chatRoomsProvider(chat.orderId).notifier); + if (notifier.mounted) { + notifier.reload(); + } + } catch (e) { + _logger.e('Failed to reload chat for orderId ${chat.orderId}: $e'); + } + } + } + Future loadChats() async { - final sessions = ref.watch(sessionNotifierProvider.notifier).sessions; + final sessions = ref.read(sessionNotifierProvider.notifier).sessions; + if (sessions.isEmpty) { + _logger.i("No sessions yet, skipping chat load."); + return; + } + final now = DateTime.now(); + final cutoff = now.subtract(const Duration(hours: 36)); + try { - state = sessions.where((s) => s.peer != null).map((s) { + final chats = sessions + .where( + (s) => s.peer != null && s.startTime.isAfter(cutoff), + ) + .map((s) { final chat = ref.read(chatRoomsProvider(s.orderId!)); return chat; }).toList(); + if (chats.isNotEmpty) { + state = chats; + } else { + _logger.i("No chats found for sessions, keeping previous state."); + } } catch (e) { _logger.e(e); } diff --git a/lib/features/chat/screens/chat_room_screen.dart b/lib/features/chat/screens/chat_room_screen.dart index 97481fa7..2f96decb 100644 --- a/lib/features/chat/screens/chat_room_screen.dart +++ b/lib/features/chat/screens/chat_room_screen.dart @@ -9,7 +9,7 @@ import 'package:mostro_mobile/data/models/chat_room.dart'; import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; import 'package:mostro_mobile/shared/providers/avatar_provider.dart'; -import 'package:mostro_mobile/shared/providers/legible_hande_provider.dart'; +import 'package:mostro_mobile/shared/providers/legible_handle_provider.dart'; import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; import 'package:mostro_mobile/shared/widgets/clickable_text_widget.dart'; diff --git a/lib/features/chat/screens/chat_rooms_list.dart b/lib/features/chat/screens/chat_rooms_list.dart index fc4705ea..8a869605 100644 --- a/lib/features/chat/screens/chat_rooms_list.dart +++ b/lib/features/chat/screens/chat_rooms_list.dart @@ -6,7 +6,7 @@ import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/data/models/chat_room.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; import 'package:mostro_mobile/shared/providers/avatar_provider.dart'; -import 'package:mostro_mobile/shared/providers/legible_hande_provider.dart'; +import 'package:mostro_mobile/shared/providers/legible_handle_provider.dart'; import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; import 'package:mostro_mobile/shared/widgets/bottom_nav_bar.dart'; import 'package:mostro_mobile/shared/widgets/mostro_app_bar.dart'; diff --git a/lib/features/home/providers/home_order_providers.dart b/lib/features/home/providers/home_order_providers.dart index 89cff259..d9aa1c30 100644 --- a/lib/features/home/providers/home_order_providers.dart +++ b/lib/features/home/providers/home_order_providers.dart @@ -6,7 +6,6 @@ import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; - final homeOrderTypeProvider = StateProvider((ref) => OrderType.sell); final filteredOrdersProvider = Provider>((ref) { diff --git a/lib/features/key_manager/key_storage.dart b/lib/features/key_manager/key_storage.dart index a478bf53..2df86888 100644 --- a/lib/features/key_manager/key_storage.dart +++ b/lib/features/key_manager/key_storage.dart @@ -6,28 +6,48 @@ class KeyStorage { final FlutterSecureStorage secureStorage; final SharedPreferencesAsync sharedPrefs; - KeyStorage({required this.secureStorage, required this.sharedPrefs}); + KeyStorage({ + required this.secureStorage, + required this.sharedPrefs, + }); + Future storeMasterKey(String masterKey) async { - await secureStorage.write(key: SecureStorageKeys.masterKey.value, value: masterKey); + await secureStorage.write( + key: SecureStorageKeys.masterKey.value, + value: masterKey, + ); } Future readMasterKey() async { - return secureStorage.read(key: SecureStorageKeys.masterKey.value); + return secureStorage.read( + key: SecureStorageKeys.masterKey.value, + ); } Future storeMnemonic(String mnemonic) async { - await secureStorage.write(key: SecureStorageKeys.mnemonic.value, value: mnemonic); + await secureStorage.write( + key: SecureStorageKeys.mnemonic.value, + value: mnemonic, + ); } Future readMnemonic() async { - return secureStorage.read(key: SecureStorageKeys.mnemonic.value); + return secureStorage.read( + key: SecureStorageKeys.mnemonic.value, + ); } Future storeTradeKeyIndex(int index) async { - await sharedPrefs.setInt(SharedPreferencesKeys.keyIndex.value, index); + await sharedPrefs.setInt( + SharedPreferencesKeys.keyIndex.value, + index, + ); } Future readTradeKeyIndex() async { - return await sharedPrefs.getInt(SharedPreferencesKeys.keyIndex.value) ?? 1; + return await sharedPrefs.getInt( + SharedPreferencesKeys.keyIndex.value, + ) ?? + 1; } } diff --git a/lib/features/notifications/foreground_service_controller.dart b/lib/features/notifications/foreground_service_controller.dart deleted file mode 100644 index 8827163b..00000000 --- a/lib/features/notifications/foreground_service_controller.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/services.dart'; -import 'package:logger/logger.dart'; - -class ForegroundServiceController { - static const _channel = MethodChannel('com.example.myapp/foreground_service'); - static final _logger = Logger(); - - static Future startService() async { - try { - await _channel.invokeMethod('startService'); - } on PlatformException catch (e) { - _logger.e("Failed to start service: ${e.message}"); - } - } - - static Future stopService() async { - try { - await _channel.invokeMethod('stopService'); - } on PlatformException catch (e) { - _logger.e("Failed to stop service: ${e.message}"); - } - } -} diff --git a/lib/features/order/notfiers/abstract_mostro_notifier.dart b/lib/features/order/notfiers/abstract_mostro_notifier.dart index b6be9bea..b765c49f 100644 --- a/lib/features/order/notfiers/abstract_mostro_notifier.dart +++ b/lib/features/order/notfiers/abstract_mostro_notifier.dart @@ -4,27 +4,31 @@ import 'package:mostro_mobile/core/config.dart'; import 'package:mostro_mobile/data/models/cant_do.dart'; import 'package:mostro_mobile/data/models/dispute.dart'; import 'package:mostro_mobile/data/models/enums/action.dart'; +import 'package:mostro_mobile/data/models/enums/status.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/data/models/order.dart'; -import 'package:mostro_mobile/data/models/payload.dart'; import 'package:mostro_mobile/data/models/peer.dart'; +import 'package:mostro_mobile/core/mostro_fsm.dart'; import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; +import 'package:mostro_mobile/features/mostro/mostro_instance.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; import 'package:mostro_mobile/shared/providers/navigation_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; -import 'package:mostro_mobile/features/mostro/mostro_instance.dart'; import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; -import 'package:mostro_mobile/shared/providers/session_providers.dart'; -class AbstractMostroNotifier - extends StateNotifier { +class AbstractMostroNotifier extends StateNotifier { final String orderId; final Ref ref; - ProviderSubscription>? subscription; + ProviderSubscription>? subscription; final logger = Logger(); + // Keep local FSM state in sync with every incoming MostroMessage. + Status _currentStatus = Status.pending; + + Status get currentStatus => _currentStatus; + AbstractMostroNotifier( this.orderId, this.ref, @@ -32,19 +36,31 @@ class AbstractMostroNotifier Future sync() async { final storage = ref.read(mostroStorageProvider); - state = await storage.getMessageById(orderId) ?? state; + final latestMessage = await storage.getMessageById(orderId); + if (latestMessage != null) { + state = latestMessage; + // Bootstrap FSM status from the order payload if present. + final orderPayload = latestMessage.getPayload(); + _currentStatus = orderPayload?.status ?? _currentStatus; + } } void subscribe() { - subscription = ref.listen(sessionMessagesProvider(orderId), (_, next) { - next.when( - data: (msg) { - handleEvent(msg); - }, - error: (error, stack) => handleError(error, stack), - loading: () {}, - ); - }); + // Use the mostroMessageStream provider that directly watches Sembast storage changes + subscription = ref.listen( + mostroMessageStreamProvider(orderId), + (_, AsyncValue next) { + next.when( + data: (MostroMessage? msg) { + if (msg != null) { + handleEvent(msg); + } + }, + error: (error, stack) => handleError(error, stack), + loading: () {}, + ); + }, + ); } void handleError(Object err, StackTrace stack) { @@ -52,7 +68,12 @@ class AbstractMostroNotifier } void handleEvent(MostroMessage event) { + // Update FSM first so UI can react to new `Status` if needed. + _currentStatus = MostroFSM.nextStatus(_currentStatus, event.action); + + // Persist the message as the latest state for the order. state = event; + handleOrderUpdate(); } @@ -99,18 +120,26 @@ class AbstractMostroNotifier break; case Action.buyerTookOrder: final order = state.getPayload(); + if (order == null) { + logger.e('Buyer took order, but order is null'); + break; + } notifProvider.showInformation(state.action, values: { - 'buyer_npub': order?.buyerTradePubkey ?? 'Unknown', - 'fiat_code': order?.fiatCode, - 'fiat_amount': order?.fiatAmount, - 'payment_method': order?.paymentMethod, + 'buyer_npub': order.buyerTradePubkey ?? 'Unknown', + 'fiat_code': order.fiatCode, + 'fiat_amount': order.fiatAmount, + 'payment_method': order.paymentMethod, }); // add seller tradekey to session // open chat final sessionProvider = ref.read(sessionNotifierProvider.notifier); - final session = sessionProvider.getSessionByOrderId(orderId); - session?.peer = Peer(publicKey: order!.buyerTradePubkey!); - sessionProvider.saveSession(session!); + final peer = order.buyerTradePubkey != null + ? Peer(publicKey: order.buyerTradePubkey!) + : null; + sessionProvider.updateSession( + orderId, + (s) => s.peer = peer, + ); final chat = ref.read(chatRoomsProvider(orderId).notifier); chat.subscribe(); break; @@ -130,9 +159,11 @@ class AbstractMostroNotifier // add seller tradekey to session // open chat final sessionProvider = ref.read(sessionNotifierProvider.notifier); - final session = sessionProvider.getSessionByOrderId(orderId); - session?.peer = Peer(publicKey: order!.sellerTradePubkey!); - sessionProvider.saveSession(session!); + final peer = Peer(publicKey: order!.sellerTradePubkey!); + sessionProvider.updateSession( + orderId, + (s) => s.peer = peer, + ); final chat = ref.read(chatRoomsProvider(orderId).notifier); chat.subscribe(); break; diff --git a/lib/features/order/notfiers/add_order_notifier.dart b/lib/features/order/notfiers/add_order_notifier.dart index b4a80074..7a1d235a 100644 --- a/lib/features/order/notfiers/add_order_notifier.dart +++ b/lib/features/order/notfiers/add_order_notifier.dart @@ -10,32 +10,48 @@ import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; -class AddOrderNotifier extends AbstractMostroNotifier { +class AddOrderNotifier extends AbstractMostroNotifier { late final MostroService mostroService; - int? requestId; + late int requestId; AddOrderNotifier(super.orderId, super.ref) { mostroService = ref.read(mostroServiceProvider); + + // Generate a unique requestId from the orderId but with better uniqueness + // Take a portion of the UUID and combine with current timestamp to ensure uniqueness + final uuid = orderId.replaceAll('-', ''); + final timestamp = DateTime.now().microsecondsSinceEpoch; + + // Use only the first 8 chars of UUID combined with current timestamp for uniqueness + // This avoids potential collisions from truncation while keeping values in int range + requestId = (int.parse(uuid.substring(0, 8), radix: 16) ^ timestamp) & 0x7FFFFFFF; + + subscribe(); } @override void subscribe() { - subscription = ref.listen(addOrderEventsProvider(requestId!), (_, next) { - next.when( - data: (msg) { - if (msg.payload is Order) { - state = msg; - if (msg.action == Action.newOrder) { - confirmOrder(msg); + subscription = ref.listen( + addOrderEventsProvider(requestId), + (_, next) { + next.when( + data: (msg) { + if (msg != null) { + if (msg.payload is Order) { + state = msg; + if (msg.action == Action.newOrder) { + confirmOrder(msg); + } + } else if (msg.payload is CantDo) { + _handleCantDo(msg); + } } - } else if (msg.payload is CantDo) { - _handleCantDo(msg); - } - }, - error: (error, stack) => handleError(error, stack), - loading: () {}, - ); - }); + }, + error: (error, stack) => handleError(error, stack), + loading: () {}, + ); + }, + ); } void _handleCantDo(MostroMessage message) { @@ -49,29 +65,22 @@ class AddOrderNotifier extends AbstractMostroNotifier { ); } - // This method is called when the order is confirmed. Future confirmOrder(MostroMessage confirmedOrder) async { - final orderNotifier = - ref.watch(orderNotifierProvider(confirmedOrder.id!).notifier); + final orderNotifier = ref.watch( + orderNotifierProvider(confirmedOrder.id!).notifier, + ); handleOrderUpdate(); orderNotifier.subscribe(); dispose(); } Future submitOrder(Order order) async { - requestId = requestId ?? - BigInt.parse( - orderId.replaceAll('-', ''), - radix: 16, - ).toUnsigned(64).toInt(); - final message = MostroMessage( action: Action.newOrder, id: null, requestId: requestId, payload: order, ); - if (subscription == null) subscribe(); await mostroService.submitOrder(message); } } diff --git a/lib/features/order/notfiers/cant_do_notifier.dart b/lib/features/order/notfiers/cant_do_notifier.dart index 48c0129f..79d3b376 100644 --- a/lib/features/order/notfiers/cant_do_notifier.dart +++ b/lib/features/order/notfiers/cant_do_notifier.dart @@ -4,7 +4,7 @@ import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; -class CantDoNotifier extends AbstractMostroNotifier { +class CantDoNotifier extends AbstractMostroNotifier { CantDoNotifier(super.orderId, super.ref) { sync(); subscribe(); diff --git a/lib/features/order/notfiers/dispute_notifier.dart b/lib/features/order/notfiers/dispute_notifier.dart index fa3618f0..59b3442b 100644 --- a/lib/features/order/notfiers/dispute_notifier.dart +++ b/lib/features/order/notfiers/dispute_notifier.dart @@ -2,7 +2,7 @@ import 'package:mostro_mobile/data/models/dispute.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; -class DisputeNotifier extends AbstractMostroNotifier { +class DisputeNotifier extends AbstractMostroNotifier { DisputeNotifier(super.orderId, super.ref) { sync(); subscribe(); diff --git a/lib/features/order/notfiers/order_notifier.dart b/lib/features/order/notfiers/order_notifier.dart index 9c9a363d..9138d3e1 100644 --- a/lib/features/order/notfiers/order_notifier.dart +++ b/lib/features/order/notfiers/order_notifier.dart @@ -3,10 +3,11 @@ import 'package:mostro_mobile/data/models/enums/action.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; +import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; -class OrderNotifier extends AbstractMostroNotifier { +class OrderNotifier extends AbstractMostroNotifier { late final MostroService mostroService; OrderNotifier(super.orderId, super.ref) { @@ -18,10 +19,9 @@ class OrderNotifier extends AbstractMostroNotifier { @override void handleEvent(MostroMessage event) { - if (event.payload is Order || event.payload == null) { - state = event; - handleOrderUpdate(); - } + // Forward all messages so UI reacts to CantDo, Peer, PaymentRequest, etc. + state = event; + handleOrderUpdate(); } Future submitOrder(Order order) async { @@ -79,4 +79,12 @@ class OrderNotifier extends AbstractMostroNotifier { rating, ); } + + @override + void dispose() { + ref.invalidate(cantDoNotifierProvider(orderId)); + ref.invalidate(paymentNotifierProvider(orderId)); + ref.invalidate(disputeNotifierProvider(orderId)); + super.dispose(); + } } diff --git a/lib/features/order/notfiers/payment_request_notifier.dart b/lib/features/order/notfiers/payment_request_notifier.dart index 487f5d74..31c67675 100644 --- a/lib/features/order/notfiers/payment_request_notifier.dart +++ b/lib/features/order/notfiers/payment_request_notifier.dart @@ -2,7 +2,7 @@ import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/data/models/payment_request.dart'; import 'package:mostro_mobile/features/order/notfiers/abstract_mostro_notifier.dart'; -class PaymentRequestNotifier extends AbstractMostroNotifier { +class PaymentRequestNotifier extends AbstractMostroNotifier { PaymentRequestNotifier(super.orderId, super.ref) { sync(); subscribe(); @@ -10,9 +10,11 @@ class PaymentRequestNotifier extends AbstractMostroNotifier { @override void handleEvent(MostroMessage event) { + // Only react to PaymentRequest payloads; delegate full handling to the + // base notifier so that the Finite-State Machine and generic side-effects + // (navigation, notifications, etc.) remain consistent. if (event.payload is PaymentRequest) { - state = event; - handleOrderUpdate(); + super.handleEvent(event); } } } diff --git a/lib/features/order/providers/order_notifier_provider.dart b/lib/features/order/providers/order_notifier_provider.dart index 91dc5282..69fa4848 100644 --- a/lib/features/order/providers/order_notifier_provider.dart +++ b/lib/features/order/providers/order_notifier_provider.dart @@ -5,7 +5,7 @@ import 'package:mostro_mobile/features/order/notfiers/add_order_notifier.dart'; import 'package:mostro_mobile/features/order/notfiers/dispute_notifier.dart'; import 'package:mostro_mobile/features/order/notfiers/order_notifier.dart'; import 'package:mostro_mobile/features/order/notfiers/payment_request_notifier.dart'; -import 'package:mostro_mobile/services/event_bus.dart'; +import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; import 'package:mostro_mobile/features/order/notfiers/cant_do_notifier.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'order_notifier_provider.g.dart'; @@ -13,9 +13,11 @@ part 'order_notifier_provider.g.dart'; final orderNotifierProvider = StateNotifierProvider.family( (ref, orderId) { + // Initialize all related notifiers ref.read(cantDoNotifierProvider(orderId)); ref.read(paymentNotifierProvider(orderId)); ref.read(disputeNotifierProvider(orderId)); + return OrderNotifier( orderId, ref, @@ -36,21 +38,30 @@ final addOrderNotifierProvider = final cantDoNotifierProvider = StateNotifierProvider.family( (ref, orderId) { - return CantDoNotifier(orderId, ref); + return CantDoNotifier( + orderId, + ref, + ); }, ); final paymentNotifierProvider = StateNotifierProvider.family( (ref, orderId) { - return PaymentRequestNotifier(orderId, ref); + return PaymentRequestNotifier( + orderId, + ref, + ); }, ); final disputeNotifierProvider = StateNotifierProvider.family( (ref, orderId) { - return DisputeNotifier(orderId, ref); + return DisputeNotifier( + orderId, + ref, + ); }, ); @@ -63,9 +74,16 @@ class OrderTypeNotifier extends _$OrderTypeNotifier { void set(OrderType value) => state = value; } -final addOrderEventsProvider = StreamProvider.family( +final addOrderEventsProvider = StreamProvider.family( (ref, requestId) { - final bus = ref.watch(eventBusProvider); - return bus.stream.where((msg) => msg.requestId == requestId); + final storage = ref.watch(mostroStorageProvider); + return storage.watchByRequestId(requestId); + }, +); + +final orderMessagesStreamProvider = StreamProvider.family, String>( + (ref, orderId) { + final storage = ref.watch(mostroStorageProvider); + return storage.watchAllMessages(orderId); }, ); diff --git a/lib/features/order/providers/order_status_provider.dart b/lib/features/order/providers/order_status_provider.dart new file mode 100644 index 00000000..33cab9aa --- /dev/null +++ b/lib/features/order/providers/order_status_provider.dart @@ -0,0 +1,25 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/core/mostro_fsm.dart'; +import 'package:mostro_mobile/data/models/enums/status.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; + +/// Exposes a live [Status] stream for a given order based on the full history +/// of stored `MostroMessage`s. Any new message automatically recomputes the +/// status using the canonical [MostroFSM]. +final orderStatusProvider = StreamProvider.family((ref, orderId) { + final storage = ref.watch(mostroStorageProvider); + + Status computeStatus(Iterable messages) { + var status = Status.pending; // default starting point + for (final m in messages) { + status = MostroFSM.nextStatus(status, m.action); + } + return status; + } + + return storage + .watchAllMessages(orderId) // emits list whenever new message saved + .map((messages) => computeStatus(messages)) + .distinct(); +}); diff --git a/lib/features/order/screens/add_lightning_invoice_screen.dart b/lib/features/order/screens/add_lightning_invoice_screen.dart index bec80646..634a68f1 100644 --- a/lib/features/order/screens/add_lightning_invoice_screen.dart +++ b/lib/features/order/screens/add_lightning_invoice_screen.dart @@ -2,9 +2,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:mostro_mobile/core/app_theme.dart'; -import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; +import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; +import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/shared/widgets/add_lightning_invoice_widget.dart'; class AddLightningInvoiceScreen extends ConsumerStatefulWidget { @@ -23,65 +24,73 @@ class _AddLightningInvoiceScreenState @override Widget build(BuildContext context) { - final order = ref.read(orderNotifierProvider(widget.orderId)); + final orderId = widget.orderId; + final mostroOrderAsync = ref.watch(mostroOrderStreamProvider(orderId)); - final amount = order.getPayload()?.amount; + return mostroOrderAsync.when( + data: (mostroMessage) { + final orderPayload = mostroMessage?.getPayload(); + final amount = orderPayload?.amount; - return Scaffold( - backgroundColor: AppTheme.dark1, - appBar: OrderAppBar(title: 'Add Lightning Invoice'), - body: Column( - children: [ - Expanded( - child: Container( - margin: const EdgeInsets.all(16), - padding: EdgeInsets.all(20), - decoration: BoxDecoration( - color: AppTheme.dark2, - borderRadius: BorderRadius.circular(20), - ), - child: AddLightningInvoiceWidget( - controller: invoiceController, - onSubmit: () async { - final invoice = invoiceController.text.trim(); - if (invoice.isNotEmpty) { - final orderNotifier = ref - .read(orderNotifierProvider(widget.orderId).notifier); - try { - await orderNotifier.sendInvoice( - widget.orderId, invoice, amount); - context.go('/'); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text('Failed to update invoice: ${e.toString()}'), - ), - ); - } - } - }, - onCancel: () async { - final orderNotifier = - ref.read(orderNotifierProvider(widget.orderId).notifier); - try { - await orderNotifier.cancelOrder(); - context.go('/'); - } catch (e) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: - Text('Failed to cancel order: ${e.toString()}'), - ), - ); - } - }, - amount: amount!, + return Scaffold( + backgroundColor: AppTheme.dark1, + appBar: OrderAppBar(title: 'Add Lightning Invoice'), + body: Column( + children: [ + Expanded( + child: Container( + margin: const EdgeInsets.all(16), + padding: EdgeInsets.all(20), + decoration: BoxDecoration( + color: AppTheme.dark2, + borderRadius: BorderRadius.circular(20), + ), + child: AddLightningInvoiceWidget( + controller: invoiceController, + onSubmit: () async { + final invoice = invoiceController.text.trim(); + if (invoice.isNotEmpty) { + final orderNotifier = ref + .read(orderNotifierProvider(widget.orderId).notifier); + try { + await orderNotifier.sendInvoice( + widget.orderId, invoice, amount); + context.go('/'); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Failed to update invoice: ${e.toString()}'), + ), + ); + } + } + }, + onCancel: () async { + final orderNotifier = + ref.read(orderNotifierProvider(widget.orderId).notifier); + try { + await orderNotifier.cancelOrder(); + context.go('/'); + } catch (e) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: + Text('Failed to cancel order: ${e.toString()}'), + ), + ); + } + }, + amount: amount!, + ), + ), ), - ), + ], ), - ], - ), + ); + }, + loading: () => const Center(child: CircularProgressIndicator()), + error: (e, st) => Center(child: Text('Error: $e')), ); } } diff --git a/lib/features/order/screens/add_order_screen.dart b/lib/features/order/screens/add_order_screen.dart index 38571f42..ca33d536 100644 --- a/lib/features/order/screens/add_order_screen.dart +++ b/lib/features/order/screens/add_order_screen.dart @@ -5,15 +5,23 @@ import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:heroicons/heroicons.dart'; import 'package:mostro_mobile/core/app_theme.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart' as nostr_action; import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/features/order/widgets/fixed_switch_widget.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/shared/widgets/currency_combo_box.dart'; import 'package:mostro_mobile/shared/widgets/currency_text_field.dart'; import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart'; +import 'package:mostro_mobile/shared/widgets/mostro_reactive_button.dart'; import 'package:uuid/uuid.dart'; +// Create a direct state provider tied to the order action/status +final orderActionStatusProvider = + Provider.family, int>( + (ref, requestId) => ref.watch(addOrderEventsProvider(requestId))); + class AddOrderScreen extends ConsumerStatefulWidget { const AddOrderScreen({super.key}); @@ -435,26 +443,33 @@ class _AddOrderScreenState extends ConsumerState { /// /// ACTION BUTTONS /// + // Track the current request ID for the button state providers + int? _currentRequestId; + Widget _buildActionButtons( BuildContext context, WidgetRef ref, OrderType orderType) { return Row( - mainAxisAlignment: MainAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ TextButton( - onPressed: () => context.go('/'), - child: const Text('CANCEL', style: TextStyle(color: AppTheme.red2)), + onPressed: () { + context.pop(); + }, + child: const Text('CANCEL'), ), - const SizedBox(width: 16), - ElevatedButton( + const SizedBox(width: 8.0), + MostroReactiveButton( + label: 'SUBMIT', + buttonStyle: ButtonStyleType.raised, + orderId: _currentRequestId?.toString() ?? '', + action: nostr_action.Action.newOrder, onPressed: () { if (_formKey.currentState?.validate() ?? false) { _submitOrder(context, ref, orderType); } }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, - ), - child: const Text('SUBMIT'), + timeout: const Duration(seconds: 5), // Short timeout for better UX + showSuccessIndicator: true, ), ], ); @@ -464,13 +479,25 @@ class _AddOrderScreenState extends ConsumerState { /// SUBMIT ORDER /// void _submitOrder(BuildContext context, WidgetRef ref, OrderType orderType) { + // No need to reset state providers as they're now derived from the order state + final selectedFiatCode = ref.read(selectedFiatCodeProvider); - if (_formKey.currentState?.validate() ?? false) { + try { // Generate a unique temporary ID for this new order final uuid = Uuid(); final tempOrderId = uuid.v4(); - final notifier = ref.read(addOrderNotifierProvider(tempOrderId).notifier); + final notifier = ref.read( + addOrderNotifierProvider(tempOrderId).notifier, + ); + + // Calculate the request ID the same way AddOrderNotifier does + final requestId = notifier.requestId; + + // Store the current request ID for the button state providers + setState(() { + _currentRequestId = requestId; + }); final fiatAmount = _maxFiatAmount != null ? 0 : _minFiatAmount; final minAmount = _maxFiatAmount != null ? _minFiatAmount : null; @@ -495,7 +522,27 @@ class _AddOrderScreenState extends ConsumerState { buyerInvoice: buyerInvoice, ); + // Submit the order first notifier.submitOrder(order); + + // The timeout is now handled by the NostrResponsiveButton + } catch (e) { + // If there's an exception, show an error dialog instead of using providers + if (context.mounted) { + showDialog( + context: context, + builder: (context) => AlertDialog( + title: const Text('Error'), + content: Text(e.toString()), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('OK'), + ), + ], + ), + ); + } } } } diff --git a/lib/features/order/screens/error_screen.dart b/lib/features/order/screens/error_screen.dart deleted file mode 100644 index 279b4a92..00000000 --- a/lib/features/order/screens/error_screen.dart +++ /dev/null @@ -1,45 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:mostro_mobile/core/app_theme.dart'; -import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; - -class ErrorScreen extends StatelessWidget { - final String errorMessage; - - const ErrorScreen({super.key, required this.errorMessage}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppTheme.dark1, - appBar: OrderAppBar(title: 'Error'), - body: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - errorMessage, - style: const TextStyle( - color: Colors.red, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: () => context.go('/'), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.cream1, - ), - child: const Text('Return to Main Screen'), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/features/order/widgets/completion_message.dart b/lib/features/order/widgets/completion_message.dart deleted file mode 100644 index ce35b2e9..00000000 --- a/lib/features/order/widgets/completion_message.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:go_router/go_router.dart'; -import 'package:mostro_mobile/core/app_theme.dart'; -import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; - -class CompletionMessage extends StatelessWidget { - final String message; - - const CompletionMessage({super.key, required this.message}); - - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: AppTheme.dark1, - appBar: OrderAppBar(title: 'Completion'), - body: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - message, - style: const TextStyle( - color: AppTheme.cream1, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - const Text( - 'Thank you for using our service!', - style: TextStyle(color: AppTheme.grey2), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: () => context.go('/'), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, - ), - child: const Text('Return to Main Screen'), - ), - ], - ), - ), - ), - ); - } -} diff --git a/lib/features/order/widgets/seller_info.dart b/lib/features/order/widgets/seller_info.dart deleted file mode 100644 index e9a05e2b..00000000 --- a/lib/features/order/widgets/seller_info.dart +++ /dev/null @@ -1,37 +0,0 @@ -import 'package:dart_nostr/nostr/model/event/event.dart'; -import 'package:flutter/material.dart'; -import 'package:mostro_mobile/core/app_theme.dart'; -import 'package:mostro_mobile/data/models/nostr_event.dart'; - -class SellerInfo extends StatelessWidget { - final NostrEvent order; - - const SellerInfo({super.key, required this.order}); - - @override - Widget build(BuildContext context) { - return Row( - children: [ - const CircleAvatar( - backgroundColor: AppTheme.grey2, - foregroundImage: AssetImage('assets/images/launcher-icon.png'), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(order.name!, - style: const TextStyle( - color: AppTheme.cream1, fontWeight: FontWeight.bold)), - Text( - '${order.rating?.totalRating}/${order.rating?.maxRate} (${order.rating?.totalReviews})', - style: const TextStyle(color: AppTheme.mostroGreen), - ), - ], - ), - ), - ], - ); - } -} diff --git a/lib/features/settings/about_screen.dart b/lib/features/settings/about_screen.dart index 45bded11..92f45386 100644 --- a/lib/features/settings/about_screen.dart +++ b/lib/features/settings/about_screen.dart @@ -26,57 +26,92 @@ class AboutScreen extends ConsumerWidget { onPressed: () => context.pop(), ), title: Text( - 'ABOUT', + 'About', style: TextStyle( color: AppTheme.cream1, ), ), ), backgroundColor: AppTheme.dark1, - body: nostrEvent == null - ? const Center(child: CircularProgressIndicator()) - : Padding( - padding: const EdgeInsets.all(16.0), - child: Container( - decoration: BoxDecoration( - color: AppTheme.dark2, - borderRadius: BorderRadius.circular(20), - ), - child: SingleChildScrollView( - child: Column( - children: [ - const SizedBox(height: 24), - Center( - child: CircleAvatar( - radius: 36, - backgroundColor: Colors.grey, - foregroundImage: - AssetImage('assets/images/launcher-icon.png'), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + padding: AppTheme.largePadding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: 24, + children: [ + CustomCard( + padding: const EdgeInsets.all(24), + child: Column( + spacing: 16, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8, + children: [ + const Icon( + Icons.content_paste, + color: AppTheme.mostroGreen, + ), + Text('App Information', + style: textTheme.titleLarge), + ], ), - ), - const SizedBox(height: 16), - Text( - 'Mostro Mobile Client', - style: textTheme.displayMedium, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: _buildClientDetails(), - ), - Text( - 'Mostro Daemon', - style: textTheme.displayMedium, - ), - Padding( - padding: const EdgeInsets.all(16.0), - child: _buildInstanceDetails( - MostroInstance.fromEvent(nostrEvent)), - ), - ], + Row( + spacing: 8, + children: [ + Expanded( + child: _buildClientDetails(), + ), + ], + ), + ], + ), + ), + CustomCard( + color: AppTheme.dark2, + padding: const EdgeInsets.all(16), + child: Column( + spacing: 16, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + spacing: 8, + children: [ + const Icon( + Icons.content_paste, + color: AppTheme.mostroGreen, + ), + Text('About Mostro Instance', + style: textTheme.titleLarge), + ], + ), + Text('General Info', + style: textTheme.titleMedium?.copyWith( + color: AppTheme.mostroGreen, + )), + nostrEvent == null + ? const Center(child: CircularProgressIndicator()) + : Row( + spacing: 8, + children: [ + Expanded( + child: _buildInstanceDetails( + MostroInstance.fromEvent(nostrEvent)), + ), + ], + ), + ], + ), ), - ), + ], ), ), + ) + ], + ), ); } @@ -88,20 +123,32 @@ class AboutScreen extends ConsumerWidget { String.fromEnvironment('GIT_COMMIT', defaultValue: 'N/A'); return CustomCard( - color: AppTheme.dark1, - padding: EdgeInsets.all(16), child: Column( - crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Version', + ), + Text( + appVersion, + ), + const SizedBox(height: 8), + Row( children: [ Text( - 'Version: $appVersion', - ), - const SizedBox(height: 3), - Text( - 'Commit Hash: $gitCommit', + 'GitHub Repository', ), ], - )); + ), + const SizedBox(height: 8), + Text( + 'Commit Hash', + ), + Text( + gitCommit, + ), + ], + )); } /// Builds the header displaying details from the MostroInstance. @@ -109,7 +156,6 @@ class AboutScreen extends ConsumerWidget { final formatter = NumberFormat.decimalPattern(Intl.getCurrentLocale()); return CustomCard( - color: AppTheme.dark1, padding: EdgeInsets.all(16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, diff --git a/lib/features/settings/settings_notifier.dart b/lib/features/settings/settings_notifier.dart index a12fa5b7..55107834 100644 --- a/lib/features/settings/settings_notifier.dart +++ b/lib/features/settings/settings_notifier.dart @@ -23,8 +23,7 @@ class SettingsNotifier extends StateNotifier { final settingsJson = await _prefs.getString(_storageKey); if (settingsJson != null) { try { - final loaded = Settings.fromJson(jsonDecode(settingsJson)); - state = loaded; + state = Settings.fromJson(jsonDecode(settingsJson)); } catch (_) { state = _defaultSettings(); } @@ -57,4 +56,6 @@ class SettingsNotifier extends StateNotifier { final jsonString = jsonEncode(state.toJson()); await _prefs.setString(_storageKey, jsonString); } + + Settings get settings => state.copyWith(); } diff --git a/lib/features/trades/models/trade_state.dart b/lib/features/trades/models/trade_state.dart new file mode 100644 index 00000000..bb682698 --- /dev/null +++ b/lib/features/trades/models/trade_state.dart @@ -0,0 +1,42 @@ +import 'package:mostro_mobile/data/models/enums/status.dart'; +import 'package:mostro_mobile/data/models/order.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart' as actions; + +class TradeState { + final Status status; + final actions.Action? lastAction; + final Order? orderPayload; + + TradeState({ + required this.status, + required this.lastAction, + required this.orderPayload, + }); + + @override + String toString() => + 'TradeState(status: $status, lastAction: $lastAction, orderPayload: $orderPayload)'; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is TradeState && + other.status == status && + other.lastAction == lastAction && + other.orderPayload == orderPayload; + + @override + int get hashCode => Object.hash(status, lastAction, orderPayload); + + TradeState copyWith({ + Status? status, + actions.Action? lastAction, + Order? orderPayload, + }) { + return TradeState( + status: status ?? this.status, + lastAction: lastAction ?? this.lastAction, + orderPayload: orderPayload ?? this.orderPayload, + ); + } +} diff --git a/lib/features/trades/providers/trade_state_provider.dart b/lib/features/trades/providers/trade_state_provider.dart new file mode 100644 index 00000000..5c0292f7 --- /dev/null +++ b/lib/features/trades/providers/trade_state_provider.dart @@ -0,0 +1,30 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/models/enums/status.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart' as actions; +import 'package:mostro_mobile/data/models/nostr_event.dart'; +import 'package:mostro_mobile/data/models/order.dart'; +import 'package:mostro_mobile/features/trades/models/trade_state.dart'; +import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; +import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; +import 'package:collection/collection.dart'; + +/// Provides a reactive TradeState for a given orderId. + +final tradeStateProvider = + Provider.family.autoDispose((ref, orderId) { + final statusAsync = ref.watch(eventProvider(orderId)); + final messagesAsync = ref.watch(mostroMessageHistoryProvider(orderId)); + final lastOrderMessageAsync = ref.watch(mostroOrderStreamProvider(orderId)); + + final status = statusAsync?.status ?? Status.pending; + final messages = messagesAsync.value ?? []; + final lastActionMessage = + messages.firstWhereOrNull((m) => m.action != actions.Action.cantDo); + final orderPayload = lastOrderMessageAsync.value?.getPayload(); + + return TradeState( + status: status, + lastAction: lastActionMessage?.action, + orderPayload: orderPayload, + ); +}); diff --git a/lib/features/trades/providers/trades_provider.dart b/lib/features/trades/providers/trades_provider.dart index 9df86346..0cb02587 100644 --- a/lib/features/trades/providers/trades_provider.dart +++ b/lib/features/trades/providers/trades_provider.dart @@ -1,28 +1,45 @@ +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/data/models/enums/status.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; -final filteredTradesProvider = Provider>((ref) { +final _logger = Logger(); + +final filteredTradesProvider = Provider>>((ref) { final allOrdersAsync = ref.watch(orderEventsProvider); final sessions = ref.watch(sessionNotifierProvider); - - return allOrdersAsync.maybeWhen( + + _logger.d('Filtering trades: Orders state=${allOrdersAsync.toString().substring(0, math.min(100, allOrdersAsync.toString().length))}, Sessions count=${sessions.length}'); + + return allOrdersAsync.when( data: (allOrders) { final orderIds = sessions.map((s) => s.orderId).toSet(); - - allOrders - .sort((o1, o2) => o1.expirationDate.compareTo(o2.expirationDate)); - - final filtered = allOrders.reversed + _logger.d('Got ${allOrders.length} orders and ${orderIds.length} sessions'); + + // Make a copy to avoid modifying the original list + final sortedOrders = List.from(allOrders); + sortedOrders.sort((o1, o2) => o1.expirationDate.compareTo(o2.expirationDate)); + + final filtered = sortedOrders.reversed .where((o) => orderIds.contains(o.orderId)) .where((o) => o.status != Status.canceled) .where((o) => o.status != Status.canceledByAdmin) .toList(); - return filtered; + + _logger.d('Filtered to ${filtered.length} trades'); + return AsyncValue.data(filtered); + }, + loading: () { + _logger.d('Orders loading'); + return const AsyncValue.loading(); + }, + error: (error, stackTrace) { + _logger.e('Error filtering trades: $error'); + return AsyncValue.error(error, stackTrace); }, - orElse: () => [], ); }); diff --git a/lib/features/trades/screens/trade_detail_screen.dart b/lib/features/trades/screens/trade_detail_screen.dart index 64e465b1..0a9561ad 100644 --- a/lib/features/trades/screens/trade_detail_screen.dart +++ b/lib/features/trades/screens/trade_detail_screen.dart @@ -1,5 +1,4 @@ import 'package:circular_countdown/circular_countdown.dart'; -import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -9,14 +8,15 @@ import 'package:mostro_mobile/core/app_theme.dart'; import 'package:mostro_mobile/data/models/enums/action.dart' as actions; import 'package:mostro_mobile/data/models/enums/role.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/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/order/widgets/order_app_bar.dart'; +import 'package:mostro_mobile/features/trades/models/trade_state.dart'; +import 'package:mostro_mobile/features/trades/providers/trade_state_provider.dart'; import 'package:mostro_mobile/features/trades/widgets/mostro_message_detail_widget.dart'; -import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; import 'package:mostro_mobile/shared/utils/currency_utils.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; +import 'package:mostro_mobile/shared/widgets/mostro_reactive_button.dart'; class TradeDetailScreen extends ConsumerWidget { final String orderId; @@ -26,10 +26,10 @@ class TradeDetailScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final order = ref.watch(eventProvider(orderId)); - - // Make sure we actually have an order from the provider: - if (order == null) { + final tradeState = ref.watch(tradeStateProvider(orderId)); + // If message is null or doesn't have an Order payload, show loading + final orderPayload = tradeState.orderPayload; + if (orderPayload == null) { return const Scaffold( backgroundColor: AppTheme.dark1, body: Center(child: CircularProgressIndicator()), @@ -39,59 +39,79 @@ class TradeDetailScreen extends ConsumerWidget { return Scaffold( backgroundColor: AppTheme.dark1, appBar: OrderAppBar(title: 'ORDER DETAILS'), - body: SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - const SizedBox(height: 16), - // Display basic info about the trade: - _buildSellerAmount(ref, order), - const SizedBox(height: 16), - _buildOrderId(context), - const SizedBox(height: 16), - // Detailed info: includes the last Mostro message action text - MostroMessageDetail(order: order), - const SizedBox(height: 24), - _buildCountDownTime(order.expirationDate), - const SizedBox(height: 36), - Wrap( - alignment: WrapAlignment.center, - spacing: 10, - runSpacing: 10, - children: _buildActionButtons(context, ref, order), + body: Builder( + builder: (context) { + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + const SizedBox(height: 16), + // Display basic info about the trade: + _buildSellerAmount(ref, tradeState), + const SizedBox(height: 16), + _buildOrderId(context), + const SizedBox(height: 16), + // Detailed info: includes the last Mostro message action text + MostroMessageDetail(orderId: orderId), + const SizedBox(height: 24), + _buildCountDownTime(orderPayload.expiresAt), + const SizedBox(height: 36), + Wrap( + alignment: WrapAlignment.center, + spacing: 10, + runSpacing: 10, + children: [ + _buildCloseButton(context), + ..._buildActionButtons( + context, + ref, + tradeState, + ), + ], + ), + ], ), - ], - ), + ); + }, ), ); } /// Builds a card showing the user is "selling/buying X sats for Y fiat" etc. - Widget _buildSellerAmount(WidgetRef ref, NostrEvent order) { - final session = ref.watch(sessionProvider(order.orderId!)); + Widget _buildSellerAmount(WidgetRef ref, TradeState tradeState) { + final session = ref.watch(sessionProvider(orderId)); final selling = session!.role == Role.seller ? 'selling' : 'buying'; + final currencyFlag = CurrencyUtils.getFlagFromCurrency( + tradeState.orderPayload!.fiatCode, + ); final amountString = - '${order.fiatAmount} ${order.currency} ${CurrencyUtils.getFlagFromCurrency(order.currency!)}'; + '${tradeState.orderPayload!.fiatAmount} ${tradeState.orderPayload!.fiatCode} $currencyFlag'; - // If `order.amount` is "0", the trade is "at market price" - final isZeroAmount = (order.amount == '0'); - final satText = isZeroAmount ? '' : ' ${order.amount}'; + // If `orderPayload.amount` is 0, the trade is "at market price" + final isZeroAmount = (tradeState.orderPayload!.amount == 0); + final satText = isZeroAmount ? '' : ' ${tradeState.orderPayload!.amount}'; final priceText = isZeroAmount ? 'at market price' : ''; - final premium = int.tryParse(order.premium ?? '0') ?? 0; + final premium = tradeState.orderPayload!.premium; final premiumText = premium == 0 ? '' : (premium > 0) ? 'with a +$premium% premium' : 'with a $premium% discount'; - // Payment method can be multiple, we only display the first for brevity: - final method = order.paymentMethods.isNotEmpty - ? order.paymentMethods[0] - : 'No payment method'; - + // Payment method + final method = tradeState.orderPayload!.paymentMethod; + final timestamp = formatDateTime( + tradeState.orderPayload!.createdAt != null && + tradeState.orderPayload!.createdAt! > 0 + ? DateTime.fromMillisecondsSinceEpoch( + tradeState.orderPayload!.createdAt!) + : DateTime.fromMillisecondsSinceEpoch( + tradeState.orderPayload!.createdAt ?? 0, + ), + ); return CustomCard( padding: const EdgeInsets.all(16), child: Row( @@ -108,7 +128,7 @@ class TradeDetailScreen extends ConsumerWidget { ), const SizedBox(height: 16), Text( - 'Created on: ${formatDateTime(order.createdAt!)}', + 'Created on: $timestamp', style: textTheme.bodyLarge, ), const SizedBox(height: 16), @@ -160,7 +180,12 @@ class TradeDetailScreen extends ConsumerWidget { } /// Build a circular countdown to show how many hours are left until expiration. - Widget _buildCountDownTime(DateTime expiration) { + Widget _buildCountDownTime(int? expiresAtTimestamp) { + // Convert timestamp to DateTime + final expiration = expiresAtTimestamp != null && expiresAtTimestamp > 0 + ? DateTime.fromMillisecondsSinceEpoch(expiresAtTimestamp) + : DateTime.now().add(const Duration(hours: 24)); + // If expiration has passed, the difference is negative => zero. final now = DateTime.now(); final Duration difference = @@ -180,102 +205,300 @@ class TradeDetailScreen extends ConsumerWidget { ); } - /// Main action button area, switching on `order.status`. + /// Main action button area, switching on `orderPayload.status`. /// Additional checks use `message.action` to refine which button to show. + /// Following the Mostro protocol state machine for order flow. List _buildActionButtons( - BuildContext context, WidgetRef ref, NostrEvent order) { - final message = ref.watch(orderNotifierProvider(orderId)); + BuildContext context, WidgetRef ref, TradeState tradeState) { final session = ref.watch(sessionProvider(orderId)); + final userRole = session?.role; - // The finite-state-machine approach: decide based on the order.status. - // Then refine if needed using the last action in `message.action`. - switch (order.status) { + switch (tradeState.status) { case Status.pending: - return [ - //_buildCloseButton(context), - _buildCancelButton(context, ref), - if (message.action == actions.Action.addInvoice) - _buildAddInvoiceButton(context), - ]; + // According to Mostro FSM: Pending state + final widgets = []; + + // FSM: In pending state, seller can cancel + widgets.add(_buildNostrButton( + 'CANCEL', + action: actions.Action.cancel, + backgroundColor: AppTheme.red1, + onPressed: () => + ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), + )); + + return widgets; + case Status.waitingPayment: - return [ - //_buildCloseButton(context), - _buildCancelButton(context, ref), - _buildPayInvoiceButton(context), - ]; + // According to Mostro FSM: waiting-payment state + final widgets = []; + + // FSM: Seller can pay-invoice and cancel + if (userRole == Role.seller) { + widgets.add(_buildNostrButton( + 'PAY INVOICE', + action: actions.Action.waitingBuyerInvoice, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/pay_invoice/$orderId'), + )); + } + widgets.add(_buildNostrButton( + 'CANCEL', + action: actions.Action.canceled, + backgroundColor: AppTheme.red1, + onPressed: () => + ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), + )); + return widgets; case Status.waitingBuyerInvoice: - return [ - //_buildCloseButton(context), - _buildCancelButton(context, ref), - if (message.action == actions.Action.addInvoice) - _buildAddInvoiceButton(context), - ]; + // According to Mostro FSM: waiting-buyer-invoice state + final widgets = []; + + // FSM: Buyer can add-invoice and cancel + if (userRole == Role.buyer) { + widgets.add(_buildNostrButton( + 'ADD INVOICE', + action: actions.Action.payInvoice, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/add_invoice/$orderId'), + )); + } + widgets.add(_buildNostrButton( + 'CANCEL', + action: actions.Action.canceled, + backgroundColor: AppTheme.red1, + onPressed: () => + ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), + )); + + return widgets; + case Status.settledHoldInvoice: - return [ - _buildCloseButton(context), - if (message.action == actions.Action.rate) _buildRateButton(context), - ]; + if (tradeState.lastAction == actions.Action.rate) { + return [ + // Rate button if applicable (common for both roles) + _buildNostrButton( + 'RATE', + action: actions.Action.rateReceived, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/rate_user/$orderId'), + ) + ]; + } else { + return []; + } + case Status.active: - return [ - //_buildCloseButton(context), - _buildCancelButton(context, ref), + // According to Mostro FSM: active state + final widgets = []; + + // Role-specific actions according to FSM + if (userRole == Role.buyer) { + // FSM: Buyer can fiat-sent + if (tradeState.lastAction != actions.Action.fiatSentOk && + tradeState.lastAction != actions.Action.fiatSent) { + widgets.add(_buildNostrButton( + 'FIAT SENT', + action: actions.Action.fiatSentOk, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .sendFiatSent(), + )); + } + + // FSM: Buyer can cancel + widgets.add(_buildNostrButton( + 'CANCEL', + action: actions.Action.canceled, + backgroundColor: AppTheme.red1, + onPressed: () => + ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), + )); + + // FSM: Buyer can dispute + if (tradeState.lastAction != actions.Action.disputeInitiatedByYou && + tradeState.lastAction != actions.Action.disputeInitiatedByPeer && + tradeState.lastAction != actions.Action.dispute) { + widgets.add(_buildNostrButton( + 'DISPUTE', + action: actions.Action.disputeInitiatedByYou, + backgroundColor: AppTheme.red1, + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .disputeOrder(), + )); + } + } else if (userRole == Role.seller) { + // FSM: Seller can cancel + widgets.add(_buildNostrButton( + 'CANCEL', + action: actions.Action.canceled, + backgroundColor: AppTheme.red1, + onPressed: () => + ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), + )); + + // FSM: Seller can dispute + if (tradeState.lastAction != actions.Action.disputeInitiatedByYou && + tradeState.lastAction != actions.Action.disputeInitiatedByPeer && + tradeState.lastAction != actions.Action.dispute) { + widgets.add(_buildNostrButton( + 'DISPUTE', + action: actions.Action.disputeInitiatedByYou, + backgroundColor: AppTheme.red1, + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .disputeOrder(), + )); + } + } + + // Rate button if applicable (common for both roles) + if (tradeState.lastAction == actions.Action.rate) { + widgets.add(_buildNostrButton( + 'RATE', + action: actions.Action.rateReceived, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/rate_user/$orderId'), + )); + } + + widgets.add( _buildContactButton(context), - // If user has not opened a dispute already - if (message.action != actions.Action.disputeInitiatedByYou && - message.action != actions.Action.disputeInitiatedByPeer && - message.action != actions.Action.rate) - _buildDisputeButton(ref), - // If the action is "addInvoice" => show a button for the invoice screen. - if (message.action == actions.Action.addInvoice) - _buildAddInvoiceButton(context), - // If the order is waiting for buyer to confirm fiat was sent - if (session!.role == Role.buyer) _buildFiatSentButton(ref), - // If the user is the seller & the buyer is done => show release button - if (session.role == Role.seller) _buildReleaseButton(ref), - // If the user is ready to rate - if (message.action == actions.Action.rate) _buildRateButton(context), - ]; + ); + + return widgets; case Status.fiatSent: - // Usually the user can open dispute if the other side doesn't confirm, - // or just close the screen and wait. - return [ - //_buildCloseButton(context), - if (session!.role == Role.seller) _buildReleaseButton(ref), - _buildDisputeButton(ref), - ]; + // According to Mostro FSM: fiat-sent state + final widgets = []; + + if (userRole == Role.seller) { + // FSM: Seller can release + widgets.add(_buildNostrButton( + 'RELEASE SATS', + action: actions.Action.released, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .releaseOrder(), + )); + + // FSM: Seller can cancel + widgets.add(_buildNostrButton( + 'CANCEL', + action: actions.Action.canceled, + backgroundColor: AppTheme.red1, + onPressed: () => + ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), + )); + + // FSM: Seller can dispute + widgets.add(_buildNostrButton( + 'DISPUTE', + action: actions.Action.disputeInitiatedByYou, + backgroundColor: AppTheme.red1, + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .disputeOrder(), + )); + } else if (userRole == Role.buyer) { + // FSM: Buyer can only dispute in fiat-sent state + widgets.add(_buildNostrButton( + 'DISPUTE', + action: actions.Action.disputeInitiatedByYou, + backgroundColor: AppTheme.red1, + onPressed: () => ref + .read(orderNotifierProvider(orderId).notifier) + .disputeOrder(), + )); + } + + return widgets; case Status.cooperativelyCanceled: - return [ - //_buildCloseButton(context), - if (message.action == actions.Action.cooperativeCancelInitiatedByPeer) - _buildCancelButton(context, ref), - ]; + // According to Mostro FSM: cooperatively-canceled state + final widgets = []; + + // Add confirm cancel if cooperative cancel was initiated by peer + if (tradeState.lastAction == + actions.Action.cooperativeCancelInitiatedByPeer) { + widgets.add(_buildNostrButton( + 'CONFIRM CANCEL', + action: actions.Action.cooperativeCancelAccepted, + backgroundColor: AppTheme.red1, + onPressed: () => + ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), + )); + } + + return widgets; case Status.success: - return [ - //_buildCloseButton(context), - if (message.action != actions.Action.rateReceived) - _buildRateButton(context), - ]; + // According to Mostro FSM: success state + // Both buyer and seller can only rate + final widgets = []; + + // FSM: Both roles can rate counterparty if not already rated + if (tradeState.lastAction != actions.Action.rateReceived) { + widgets.add(_buildNostrButton( + 'RATE', + action: actions.Action.rateReceived, + backgroundColor: AppTheme.mostroGreen, + onPressed: () => context.push('/rate_user/$orderId'), + )); + } + + return widgets; + case Status.inProgress: - return [ - _buildCancelButton(context, ref), - ]; + // According to Mostro FSM: in-progress is a transitional state + // This is not explicitly in the FSM but we follow cancel rules as active state + final widgets = []; + + // Both roles can cancel during in-progress state, similar to active + widgets.add(_buildNostrButton( + 'CANCEL', + action: actions.Action.canceled, + backgroundColor: AppTheme.red1, + onPressed: () => + ref.read(orderNotifierProvider(orderId).notifier).cancelOrder(), + )); + + return widgets; + + // Terminal states according to Mostro FSM case Status.expired: case Status.dispute: case Status.completedByAdmin: case Status.canceledByAdmin: case Status.settledByAdmin: case Status.canceled: - return [ - _buildCloseButton(context), - ]; + // No actions allowed in these terminal states + return []; } } - /// CONTACT + /// Helper method to build a NostrResponsiveButton with common properties + Widget _buildNostrButton( + String label, { + required actions.Action action, + required VoidCallback onPressed, + Color? backgroundColor, + }) { + return MostroReactiveButton( + label: label, + buttonStyle: ButtonStyleType.raised, + orderId: orderId, + action: action, + backgroundColor: backgroundColor, + onPressed: onPressed, + showSuccessIndicator: true, + timeout: const Duration(seconds: 30), + ); + } Widget _buildContactButton(BuildContext context) { return ElevatedButton( onPressed: () { @@ -288,92 +511,6 @@ class TradeDetailScreen extends ConsumerWidget { ); } - /// RELEASE - Widget _buildReleaseButton(WidgetRef ref) { - final orderDetailsNotifier = - ref.read(orderNotifierProvider(orderId).notifier); - - return ElevatedButton( - onPressed: () async { - await orderDetailsNotifier.releaseOrder(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, - ), - child: const Text('RELEASE SATS'), - ); - } - - /// FIAT_SENT - Widget _buildFiatSentButton(WidgetRef ref) { - final orderDetailsNotifier = - ref.read(orderNotifierProvider(orderId).notifier); - - return ElevatedButton( - onPressed: () async { - await orderDetailsNotifier.sendFiatSent(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, - ), - child: const Text('FIAT SENT'), - ); - } - - /// ADD INVOICE - Widget _buildAddInvoiceButton(BuildContext context) { - return ElevatedButton( - onPressed: () async { - context.push('/add_invoice/$orderId'); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, - ), - child: const Text('ADD INVOICE'), - ); - } - - /// ADD INVOICE - Widget _buildPayInvoiceButton(BuildContext context) { - return ElevatedButton( - onPressed: () async { - context.push('/pay_invoice/$orderId'); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, - ), - child: const Text('ADD INVOICE'), - ); - } - - /// CANCEL - Widget _buildCancelButton(BuildContext context, WidgetRef ref) { - final notifier = ref.read(orderNotifierProvider(orderId).notifier); - return ElevatedButton( - onPressed: () async { - await notifier.cancelOrder(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.red1, - ), - child: const Text('CANCEL'), - ); - } - - /// DISPUTE - Widget _buildDisputeButton(WidgetRef ref) { - final notifier = ref.read(orderNotifierProvider(orderId).notifier); - return ElevatedButton( - onPressed: () async { - await notifier.disputeOrder(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.red1, - ), - child: const Text('DISPUTE'), - ); - } - /// CLOSE Widget _buildCloseButton(BuildContext context) { return OutlinedButton( @@ -383,17 +520,6 @@ class TradeDetailScreen extends ConsumerWidget { ); } - /// RATE - Widget _buildRateButton(BuildContext context) { - return OutlinedButton( - onPressed: () { - context.push('/rate_user/$orderId'); - }, - style: AppTheme.theme.outlinedButtonTheme.style, - child: const Text('RATE'), - ); - } - /// Format the date time to a user-friendly string with UTC offset String formatDateTime(DateTime dt) { final dateFormatter = DateFormat('EEE MMM dd yyyy HH:mm:ss'); diff --git a/lib/features/trades/screens/trades_screen.dart b/lib/features/trades/screens/trades_screen.dart index 8474ab4f..afc56b2f 100644 --- a/lib/features/trades/screens/trades_screen.dart +++ b/lib/features/trades/screens/trades_screen.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:heroicons/heroicons.dart'; import 'package:mostro_mobile/core/app_theme.dart'; +import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; import 'package:mostro_mobile/shared/widgets/order_filter.dart'; import 'package:mostro_mobile/features/trades/providers/trades_provider.dart'; import 'package:mostro_mobile/features/trades/widgets/trades_list.dart'; @@ -15,15 +16,19 @@ class TradesScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(filteredTradesProvider); - + // Watch the async trades data + final tradesAsync = ref.watch(filteredTradesProvider); + return Scaffold( backgroundColor: AppTheme.dark1, appBar: const MostroAppBar(), drawer: const MostroAppDrawer(), body: RefreshIndicator( onRefresh: () async { - return await ref.refresh(filteredTradesProvider); + // Force reload the orders repository first + ref.read(orderRepositoryProvider).reloadData(); + // Then refresh the filtered trades provider + ref.invalidate(filteredTradesProvider); }, child: Container( margin: const EdgeInsets.fromLTRB(16, 16, 16, 16), @@ -40,10 +45,50 @@ class TradesScreen extends ConsumerWidget { style: TextStyle(color: AppTheme.mostroGreen), ), ), - _buildFilterButton(context, state), + // Use the async value pattern to handle different states + tradesAsync.when( + data: (trades) => _buildFilterButton(context, ref, trades), + loading: () => _buildFilterButton(context, ref, []), + error: (error, _) => _buildFilterButton(context, ref, []), + ), const SizedBox(height: 6.0), Expanded( - child: _buildOrderList(state), + child: tradesAsync.when( + data: (trades) => _buildOrderList(trades), + loading: () => const Center( + child: CircularProgressIndicator(), + ), + error: (error, _) => Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Icon( + Icons.error_outline, + color: Colors.red, + size: 60, + ), + const SizedBox(height: 16), + Text( + 'Error loading trades', + style: TextStyle(color: AppTheme.cream1), + ), + Text( + error.toString(), + style: TextStyle(color: AppTheme.cream1, fontSize: 12), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () { + ref.invalidate(orderEventsProvider); + ref.invalidate(filteredTradesProvider); + }, + child: const Text('Retry'), + ), + ], + ), + ), + ), ), const BottomNavBar(), ], @@ -53,32 +98,33 @@ class TradesScreen extends ConsumerWidget { ); } - Widget _buildFilterButton(BuildContext context, List trades) { + Widget _buildFilterButton(BuildContext context, WidgetRef ref, List trades) { return Container( padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ OutlinedButton.icon( onPressed: () { showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return const Padding( - padding: EdgeInsets.all(16.0), - child: OrderFilter(), - ); - }, + context: context, + backgroundColor: Colors.transparent, + builder: (BuildContext context) { + return const Padding( + padding: EdgeInsets.all(16.0), + child: OrderFilter(), + ); + }, ); }, icon: const HeroIcon(HeroIcons.funnel, - style: HeroIconStyle.outline, color: AppTheme.cream1), + style: HeroIconStyle.outline, color: AppTheme.cream1), label: - const Text("FILTER", style: TextStyle(color: AppTheme.cream1)), + const Text("FILTER", style: TextStyle(color: AppTheme.cream1)), style: OutlinedButton.styleFrom( side: const BorderSide(color: AppTheme.cream1), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), + borderRadius: BorderRadius.circular(20), ), ), ), @@ -87,6 +133,15 @@ class TradesScreen extends ConsumerWidget { "${trades.length} trades", style: const TextStyle(color: AppTheme.cream1), ), + // Add a manual refresh button + IconButton( + icon: const Icon(Icons.refresh, color: AppTheme.cream1), + onPressed: () { + // Use the ref from the build method + ref.invalidate(orderEventsProvider); + ref.invalidate(filteredTradesProvider); + }, + ), ], ), ); diff --git a/lib/features/trades/widgets/mostro_message_detail_widget.dart b/lib/features/trades/widgets/mostro_message_detail_widget.dart index d2fdefab..101e0b64 100644 --- a/lib/features/trades/widgets/mostro_message_detail_widget.dart +++ b/lib/features/trades/widgets/mostro_message_detail_widget.dart @@ -1,279 +1,226 @@ -import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/core/app_theme.dart'; -import 'package:mostro_mobile/data/models/cant_do.dart'; import 'package:mostro_mobile/data/models/dispute.dart'; -import 'package:mostro_mobile/data/models/enums/cant_do_reason.dart'; import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; -import 'package:mostro_mobile/data/models/order.dart'; +import 'package:mostro_mobile/features/trades/models/trade_state.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart' as actions; import 'package:mostro_mobile/features/mostro/mostro_instance.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart' as actions; import 'package:mostro_mobile/generated/l10n.dart'; -import 'package:mostro_mobile/shared/notifiers/order_action_notifier.dart'; import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; import 'package:mostro_mobile/shared/widgets/custom_card.dart'; +import 'package:mostro_mobile/features/trades/providers/trade_state_provider.dart'; +import 'package:mostro_mobile/data/models/enums/cant_do_reason.dart'; class MostroMessageDetail extends ConsumerWidget { - final NostrEvent order; - - const MostroMessageDetail({super.key, required this.order}); + final String orderId; + const MostroMessageDetail({super.key, required this.orderId}); @override Widget build(BuildContext context, WidgetRef ref) { - // Retrieve the MostroMessage using the order's orderId - final mostroMessage = ref.watch(orderNotifierProvider(order.orderId!)); - final session = ref.watch(sessionProvider(order.orderId!)); - final action = ref.watch(orderActionNotifierProvider(order.orderId!)); - // Map the action enum to the corresponding i10n string. - String actionText; + final tradeState = ref.watch(tradeStateProvider(orderId)); + + if (tradeState.lastAction == null || tradeState.orderPayload == null) { + return const CustomCard( + padding: EdgeInsets.all(16), + child: Center(child: CircularProgressIndicator()), + ); + } + + final actionText = _getActionText( + context, + ref, + tradeState, + ); + return CustomCard( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + const CircleAvatar( + backgroundColor: Colors.grey, + foregroundImage: AssetImage('assets/images/launcher-icon.png'), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + actionText, + style: Theme.of(context).textTheme.bodyLarge, + ), + const SizedBox(height: 16), + Text('${tradeState.status} - ${tradeState.lastAction}'), + ], + ), + ), + ], + ), + ); + } + + String _getActionText( + BuildContext context, + WidgetRef ref, + TradeState tradeState, + ) { + final action = tradeState.lastAction; + final orderPayload = tradeState.orderPayload; switch (action) { case actions.Action.newOrder: final expHrs = ref.read(orderRepositoryProvider).mostroInstance?.expiration ?? '24'; - actionText = S.of(context)!.newOrder(int.tryParse(expHrs) ?? 24); - break; + return S.of(context)!.newOrder(int.tryParse(expHrs) ?? 24); case actions.Action.canceled: - actionText = S.of(context)!.canceled(order.orderId!); - break; + return S.of(context)!.canceled(orderPayload?.id ?? ''); case actions.Action.payInvoice: final expSecs = ref .read(orderRepositoryProvider) .mostroInstance ?.expirationSeconds ?? 900; - actionText = S.of(context)!.payInvoice( - order.amount!, order.currency!, order.fiatAmount.minimum, expSecs); - break; + return S.of(context)!.payInvoice( + orderPayload?.amount.toString() ?? '', + orderPayload?.fiatCode ?? '', + orderPayload?.fiatAmount.toString() ?? '', + expSecs, + ); case actions.Action.addInvoice: final expSecs = ref .read(orderRepositoryProvider) .mostroInstance ?.expirationSeconds ?? 900; - actionText = S.of(context)!.addInvoice( - order.amount!, order.currency!, order.fiatAmount.minimum, expSecs); - break; + return S.of(context)!.addInvoice( + orderPayload?.amount.toString() ?? '', + orderPayload?.fiatCode ?? '', + orderPayload?.fiatAmount.toString() ?? '', + expSecs, + ); case actions.Action.waitingSellerToPay: final expSecs = ref .read(orderRepositoryProvider) .mostroInstance ?.expirationSeconds ?? 900; - actionText = S.of(context)!.waitingSellerToPay(order.orderId!, expSecs); - break; + return S + .of(context)! + .waitingSellerToPay(orderPayload?.id ?? '', expSecs); case actions.Action.waitingBuyerInvoice: final expSecs = ref .read(orderRepositoryProvider) .mostroInstance ?.expirationSeconds ?? 900; - actionText = S.of(context)!.waitingBuyerInvoice(expSecs); - break; + return S.of(context)!.waitingBuyerInvoice(expSecs); case actions.Action.buyerInvoiceAccepted: - actionText = S.of(context)!.buyerInvoiceAccepted; - break; + return S.of(context)!.buyerInvoiceAccepted; case actions.Action.holdInvoicePaymentAccepted: - final payload = mostroMessage.getPayload(); - actionText = S.of(context)!.holdInvoicePaymentAccepted( - payload!.fiatAmount, - payload.fiatCode, - payload.paymentMethod, - payload.sellerTradePubkey ?? session!.peer!.publicKey, + final session = ref.watch(sessionProvider(orderPayload?.id ?? '')); + return S.of(context)!.holdInvoicePaymentAccepted( + orderPayload?.fiatAmount.toString() ?? '', + orderPayload?.fiatCode ?? '', + orderPayload?.paymentMethod ?? '', + session?.peer?.publicKey ?? '', ); - break; case actions.Action.buyerTookOrder: - final payload = mostroMessage.getPayload(); - actionText = S.of(context)!.buyerTookOrder( - payload!.buyerTradePubkey ?? session!.peer!.publicKey, - payload.fiatCode, - payload.fiatAmount, - payload.paymentMethod, + final session = ref.watch(sessionProvider(orderPayload?.id ?? '')); + return S.of(context)!.buyerTookOrder( + session?.peer?.publicKey ?? '', + orderPayload?.fiatCode ?? '', + orderPayload?.fiatAmount.toString() ?? '', + orderPayload?.paymentMethod ?? '', ); - break; case actions.Action.fiatSentOk: - actionText = session!.role == Role.buyer + final session = ref.watch(sessionProvider(orderPayload?.id ?? '')); + return session!.role == Role.buyer ? S.of(context)!.fiatSentOkBuyer(session.peer!.publicKey) : S.of(context)!.fiatSentOkSeller(session.peer!.publicKey); - break; case actions.Action.released: - actionText = S.of(context)!.released('{seller_npub}'); - break; + return S.of(context)!.released('{seller_npub}'); case actions.Action.purchaseCompleted: - actionText = S.of(context)!.purchaseCompleted; - break; + return S.of(context)!.purchaseCompleted; case actions.Action.holdInvoicePaymentSettled: - actionText = S.of(context)!.holdInvoicePaymentSettled('{buyer_npub}'); - break; + return S.of(context)!.holdInvoicePaymentSettled('{buyer_npub}'); case actions.Action.rate: - actionText = S.of(context)!.rate; - break; + return S.of(context)!.rate; case actions.Action.rateReceived: - actionText = S.of(context)!.rateReceived; - break; + return S.of(context)!.rateReceived; case actions.Action.cooperativeCancelInitiatedByYou: - actionText = - S.of(context)!.cooperativeCancelInitiatedByYou(order.orderId!); - break; + return S + .of(context)! + .cooperativeCancelInitiatedByYou(orderPayload?.id ?? ''); case actions.Action.cooperativeCancelInitiatedByPeer: - actionText = - S.of(context)!.cooperativeCancelInitiatedByPeer(order.orderId!); - break; + return S + .of(context)! + .cooperativeCancelInitiatedByPeer(orderPayload?.id ?? ''); case actions.Action.cooperativeCancelAccepted: - actionText = S.of(context)!.cooperativeCancelAccepted(order.orderId!); - break; + return S.of(context)!.cooperativeCancelAccepted(orderPayload?.id ?? ''); case actions.Action.disputeInitiatedByYou: - final payload = ref.read(disputeNotifierProvider(order.orderId!)).getPayload(); - actionText = S + final payload = ref + .read(disputeNotifierProvider(orderPayload?.id ?? '')) + .getPayload(); + return S .of(context)! - .disputeInitiatedByYou(order.orderId!, payload!.disputeId); - break; + .disputeInitiatedByYou(orderPayload?.id ?? '', payload!.disputeId); case actions.Action.disputeInitiatedByPeer: - - final payload = ref.read(disputeNotifierProvider(order.orderId!)).getPayload(); - actionText = S + final payload = ref + .read(disputeNotifierProvider(orderPayload?.id ?? '')) + .getPayload(); + return S .of(context)! - .disputeInitiatedByPeer(order.orderId!, payload!.disputeId); - break; + .disputeInitiatedByPeer(orderPayload?.id ?? '', payload!.disputeId); case actions.Action.adminTookDispute: - //actionText = S.of(context)!.adminTookDisputeAdmin(''); - actionText = S.of(context)!.adminTookDisputeUsers('{admin token}'); - break; + return S.of(context)!.adminTookDisputeUsers('{admin token}'); case actions.Action.adminCanceled: - //actionText = S.of(context)!.adminCanceledAdmin(''); - actionText = S.of(context)!.adminCanceledUsers(order.orderId!); - break; + return S.of(context)!.adminCanceledUsers(orderPayload?.id ?? ''); case actions.Action.adminSettled: - //actionText = S.of(context)!.adminSettledAdmin; - actionText = S.of(context)!.adminSettledUsers(order.orderId!); - break; + return S.of(context)!.adminSettledUsers(orderPayload?.id ?? ''); case actions.Action.paymentFailed: - actionText = S.of(context)!.paymentFailed('{attempts}', '{retries}'); - break; + return S.of(context)!.paymentFailed('{attempts}', '{retries}'); case actions.Action.invoiceUpdated: - actionText = S.of(context)!.invoiceUpdated; - break; + return S.of(context)!.invoiceUpdated; case actions.Action.holdInvoicePaymentCanceled: - actionText = S.of(context)!.holdInvoicePaymentCanceled; - break; + return S.of(context)!.holdInvoicePaymentCanceled; case actions.Action.cantDo: - final msg = ref.read(cantDoNotifierProvider(order.orderId!)); - final cantDo = msg.getPayload(); - switch (cantDo!.cantDoReason) { - case CantDoReason.invalidSignature: - actionText = S.of(context)!.invalidSignature; - break; - case CantDoReason.invalidTradeIndex: - actionText = S.of(context)!.invalidTradeIndex; - break; - case CantDoReason.invalidAmount: - actionText = S.of(context)!.invalidAmount; - break; - case CantDoReason.invalidInvoice: - actionText = S.of(context)!.invalidInvoice; - break; - case CantDoReason.invalidPaymentRequest: - actionText = S.of(context)!.invalidPaymentRequest; - break; - case CantDoReason.invalidPeer: - actionText = S.of(context)!.invalidPeer; - break; - case CantDoReason.invalidRating: - actionText = S.of(context)!.invalidRating; - break; - case CantDoReason.invalidTextMessage: - actionText = S.of(context)!.invalidTextMessage; - break; - case CantDoReason.invalidOrderKind: - actionText = S.of(context)!.invalidOrderKind; - break; - case CantDoReason.invalidOrderStatus: - actionText = S.of(context)!.invalidOrderStatus; - break; - case CantDoReason.invalidPubkey: - actionText = S.of(context)!.invalidPubkey; - break; - case CantDoReason.invalidParameters: - actionText = S.of(context)!.invalidParameters; - break; - case CantDoReason.orderAlreadyCanceled: - actionText = S.of(context)!.orderAlreadyCanceled; - break; - case CantDoReason.cantCreateUser: - actionText = S.of(context)!.cantCreateUser; - break; - case CantDoReason.isNotYourOrder: - actionText = S.of(context)!.isNotYourOrder; - break; - case CantDoReason.notAllowedByStatus: - actionText = - S.of(context)!.notAllowedByStatus(order.orderId!, order.status); - break; - case CantDoReason.outOfRangeFiatAmount: - actionText = - S.of(context)!.outOfRangeFiatAmount('{fiat_min}', '{fiat_max}'); - break; - case CantDoReason.outOfRangeSatsAmount: - final mostroInstance = - ref.read(orderRepositoryProvider).mostroInstance; - actionText = S.of(context)!.outOfRangeSatsAmount( - mostroInstance!.maxOrderAmount, mostroInstance.minOrderAmount); - break; - case CantDoReason.isNotYourDispute: - actionText = S.of(context)!.isNotYourDispute; - break; - case CantDoReason.disputeCreationError: - actionText = S.of(context)!.disputeCreationError; - break; - case CantDoReason.notFound: - actionText = S.of(context)!.notFound; - break; - case CantDoReason.invalidDisputeStatus: - actionText = S.of(context)!.invalidDisputeStatus; - break; - case CantDoReason.invalidAction: - actionText = S.of(context)!.invalidAction; - break; - case CantDoReason.pendingOrderExists: - actionText = S.of(context)!.pendingOrderExists; - break; - } - break; - case actions.Action.adminAddSolver: - actionText = S.of(context)!.adminAddSolver('{admin_solver}'); - break; + return _getCantDoMessage(context, ref, tradeState); default: - actionText = ''; + return 'No message found for action ${tradeState.lastAction}'; } + } - return CustomCard( - padding: const EdgeInsets.all(16), - child: Row( - children: [ - const CircleAvatar( - backgroundColor: AppTheme.grey2, - foregroundImage: AssetImage('assets/images/launcher-icon.png'), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - actionText, - style: AppTheme.theme.textTheme.bodyLarge, - ), - const SizedBox(height: 16), - Text('${order.status} - $action'), - ], - ), - ), - ], - ), - ); + String _getCantDoMessage(BuildContext context, WidgetRef ref, TradeState tradeState) { + final orderPayload = tradeState.orderPayload; + final status = tradeState.status; + final cantDoReason = ref + .read(cantDoNotifierProvider(orderPayload?.id ?? '')) + .getPayload(); + switch (cantDoReason) { + case CantDoReason.invalidSignature: + return S.of(context)!.invalidSignature; + case CantDoReason.notAllowedByStatus: + return S.of(context)!.notAllowedByStatus(orderPayload?.id ?? '', status); + case CantDoReason.outOfRangeFiatAmount: + return S.of(context)!.outOfRangeFiatAmount('{fiat_min}', '{fiat_max}'); + case CantDoReason.outOfRangeSatsAmount: + final mostroInstance = ref.read(orderRepositoryProvider).mostroInstance; + return S.of(context)!.outOfRangeSatsAmount( + mostroInstance!.maxOrderAmount, mostroInstance.minOrderAmount); + case CantDoReason.isNotYourDispute: + return S.of(context)!.isNotYourDispute; + case CantDoReason.disputeCreationError: + return S.of(context)!.disputeCreationError; + case CantDoReason.invalidDisputeStatus: + return S.of(context)!.invalidDisputeStatus; + case CantDoReason.invalidAction: + return S.of(context)!.invalidAction; + case CantDoReason.pendingOrderExists: + return S.of(context)!.pendingOrderExists; + default: + return '${status.toString()} - ${tradeState.lastAction}'; + } } } diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 0ed88b5e..92039df8 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -6,6 +6,7 @@ import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:intl/intl.dart' as intl; import 'l10n_en.dart'; +import 'l10n_it.dart'; // ignore_for_file: type=lint @@ -90,7 +91,8 @@ abstract class S { /// A list of this localizations delegate's supported locales. static const List supportedLocales = [ - Locale('en') + Locale('en'), + Locale('it') ]; /// No description provided for @newOrder. @@ -439,7 +441,7 @@ class _SDelegate extends LocalizationsDelegate { } @override - bool isSupported(Locale locale) => ['en'].contains(locale.languageCode); + bool isSupported(Locale locale) => ['en', 'it'].contains(locale.languageCode); @override bool shouldReload(_SDelegate old) => false; @@ -451,6 +453,7 @@ S lookupS(Locale locale) { // Lookup logic when only language code is specified. switch (locale.languageCode) { case 'en': return SEn(); + case 'it': return SIt(); } throw FlutterError( diff --git a/lib/generated/l10n_it.dart b/lib/generated/l10n_it.dart new file mode 100644 index 00000000..8cf01ec3 --- /dev/null +++ b/lib/generated/l10n_it.dart @@ -0,0 +1,236 @@ +// ignore: unused_import +import 'package:intl/intl.dart' as intl; +import 'l10n.dart'; + +// ignore_for_file: type=lint + +/// The translations for Italian (`it`). +class SIt extends S { + SIt([String locale = 'it']) : super(locale); + + @override + String newOrder(Object expiration_hours) { + return 'La tua offerta è stata pubblicata! Attendi fino a quando un altro utente accetta il tuo ordine. Sarà disponibile per $expiration_hours ore. Puoi annullare questo ordine prima che un altro utente lo prenda premendo: Cancella.'; + } + + @override + String canceled(Object id) { + return 'Hai annullato l\'ordine ID: $id!'; + } + + @override + String payInvoice(Object amount, Object expiration_seconds, Object fiat_amount, Object fiat_code) { + return 'Paga questa Hodl Invoice di $amount Sats per $fiat_amount $fiat_code per iniziare l\'operazione. Se non la paghi entro $expiration_seconds lo scambio sarà annullato.'; + } + + @override + String addInvoice(Object amount, Object expiration_seconds, Object fiat_amount, Object fiat_code) { + return 'Inserisci una Invoice bolt11 di $amount satoshi equivalente a $fiat_amount $fiat_code. Questa invoice riceverà i fondi una volta completato lo scambio. Se non fornisci una invoice entro $expiration_seconds questo scambio sarà annullato.'; + } + + @override + String waitingSellerToPay(Object expiration_seconds, Object id) { + return 'Attendi un momento per favore. Ho inviato una richiesta di pagamento al venditore per rilasciare i Sats per l\'ordine ID $id. Una volta effettuato il pagamento, vi connetterò entrambi. Se il venditore non completa il pagamento entro $expiration_seconds minuti lo scambio sarà annullato.'; + } + + @override + String waitingBuyerInvoice(Object expiration_seconds) { + return 'Pagamento ricevuto! I tuoi Sats sono ora \'custoditi\' nel tuo portafoglio. Attendi un momento per favore. Ho richiesto all\'acquirente di fornire una invoice lightning. Una volta ricevuta, vi connetterò entrambi, altrimenti se non la riceviamo entro $expiration_seconds i tuoi Sats saranno nuovamente disponibili nel tuo portafoglio e lo scambio sarà annullato.'; + } + + @override + String get buyerInvoiceAccepted => 'Fattura salvata con successo!'; + + @override + String holdInvoicePaymentAccepted(Object fiat_amount, Object fiat_code, Object payment_method, Object seller_npub) { + return 'Contatta il venditore tramite la sua npub $seller_npub ed ottieni i dettagli per inviare il pagamento di $fiat_amount $fiat_code utilizzando $payment_method. Una volta inviato il pagamento, clicca: Pagamento inviato'; + } + + @override + String buyerTookOrder(Object buyer_npub, Object fiat_amount, Object fiat_code, Object payment_method) { + return 'Contatta l\'acquirente, ecco il suo npub $buyer_npub, per informarlo su come inviarti $fiat_amount $fiat_code tramite $payment_method. Riceverai una notifica una volta che l\'acquirente indicherà che il pagamento fiat è stato inviato. Successivamente, dovresti verificare se sono arrivati i fondi. Se l\'acquirente non risponde, puoi iniziare l\'annullamento dell\'ordine o una disputa. Ricorda, un amministratore NON ti contatterà per risolvere il tuo ordine a meno che tu non apra prima una disputa.'; + } + + @override + String fiatSentOkBuyer(Object seller_npub) { + return 'Ho informato $seller_npub che hai inviato il pagamento. Quando il venditore confermerà di aver ricevuto il tuo pagamento, dovrebbe rilasciare i fondi. Se rifiuta, puoi aprire una disputa.'; + } + + @override + String fiatSentOkSeller(Object buyer_npub) { + return '$buyer_npub ha confermato di aver inviato il pagamento. Una volta confermato la ricezione dei fondi fiat, puoi rilasciare i Sats. Dopo il rilascio, i Sats andranno all\'acquirente, l\'azione è irreversibile, quindi procedi solo se sei sicuro. Se vuoi rilasciare i Sats all\'acquirente, premi: Rilascia Fondi.'; + } + + @override + String released(Object seller_npub) { + return '$seller_npub ha già rilasciato i Sats! Aspetta solo che la tua invoice venga pagata. Ricorda, il tuo portafoglio deve essere online per ricevere i fondi tramite Lightning Network.'; + } + + @override + String get purchaseCompleted => 'L\'acquisto di nuovi satoshi è stato completato con successo. Goditi questi dolci Sats!'; + + @override + String holdInvoicePaymentSettled(Object buyer_npub) { + return 'Il tuo scambio di vendita di Sats è stato completato dopo aver confermato il pagamento da $buyer_npub.'; + } + + @override + String get rate => 'Dai una valutazione alla controparte'; + + @override + String get rateReceived => 'Valutazione salvata con successo!'; + + @override + String cooperativeCancelInitiatedByYou(Object id) { + return 'Hai iniziato l\'annullamento dell\'ordine ID: $id. La tua controparte deve anche concordare l\'annullamento. Se non risponde, puoi aprire una disputa. Nota che nessun amministratore ti contatterà MAI riguardo questo annullamento a meno che tu non apra prima una disputa.'; + } + + @override + String cooperativeCancelInitiatedByPeer(Object id) { + return 'La tua controparte vuole annullare l\'ordine ID: $id. Nota che nessun amministratore ti contatterà MAI riguardo questo annullamento a meno che tu non apra prima una disputa. Se concordi su tale annullamento, premi: Annulla Ordine.'; + } + + @override + String cooperativeCancelAccepted(Object id) { + return 'L\'ordine $id è stato annullato con successo!'; + } + + @override + String disputeInitiatedByYou(Object id, Object user_token) { + return 'Hai iniziato una disputa per l\'ordine ID: $id. Un amministratore sarà assegnato presto alla tua disputa. Una volta assegnato, riceverai il suo npub e solo questo account potrà assisterti. Devi contattare l\'amministratore direttamente, ma se qualcuno ti contatta prima, assicurati di chiedergli di fornirti il token per la tua disputa. Il token di questa disputa è: $user_token.'; + } + + @override + String disputeInitiatedByPeer(Object id, Object user_token) { + return 'La tua controparte ha iniziato una disputa per l\'ordine ID: $id. Un amministratore sarà assegnato presto alla tua disputa. Una volta assegnato, ti condividerò il loro npub e solo loro potranno assisterti. Devi contattare l\'amministratore direttamente, ma se qualcuno ti contatta prima, assicurati di chiedergli di fornirti il token per la tua disputa. Il token di questa disputa è: $user_token.'; + } + + @override + String adminTookDisputeAdmin(Object details) { + return 'Ecco i dettagli dell\'ordine della disputa che hai preso: $details. Devi determinare quale utente ha ragione e decidere se annullare o completare l\'ordine. Nota che la tua decisione sarà finale e non può essere annullata.'; + } + + @override + String adminTookDisputeUsers(Object admin_npub) { + return 'L\'amministratore $admin_npub gestirà la tua disputa. Devi contattare l\'amministratore direttamente, ma se qualcuno ti contatta prima, assicurati di chiedergli di fornirti il token per la tua disputa..'; + } + + @override + String adminCanceledAdmin(Object id) { + return 'Hai annullato l\'ordine ID: $id!'; + } + + @override + String adminCanceledUsers(Object id) { + return 'L\'amministratore ha annullato l\'ordine ID: $id!'; + } + + @override + String adminSettledAdmin(Object id) { + return 'Hai completato l\'ordine ID: $id!'; + } + + @override + String adminSettledUsers(Object id) { + return 'L\'amministratore ha completato l\'ordine ID: $id!'; + } + + @override + String paymentFailed(Object payment_attempts, Object payment_retries_interval) { + return 'Ho provato a inviarti i Sats ma il pagamento della tua invoice è fallito. Tenterò $payment_attempts volte ancora ogni $payment_retries_interval minuti. Per favore assicurati che il tuo nodo/portafoglio lightning sia online.'; + } + + @override + String get invoiceUpdated => 'Invoice aggiornata con successo!'; + + @override + String get holdInvoicePaymentCanceled => 'Invoice annullata; i tuoi Sats saranno nuovamente disponibili nel tuo portafoglio.'; + + @override + String cantDo(Object action) { + return 'Non sei autorizzato a $action per questo ordine!'; + } + + @override + String adminAddSolver(Object npub) { + return 'Hai aggiunto con successo l\'amministratore $npub.'; + } + + @override + String get invalidSignature => 'L\'azione non può essere completata perché la firma non è valida.'; + + @override + String get invalidTradeIndex => 'L\'indice di scambio fornito non è valido. Assicurati che il tuo client sia sincronizzato e riprova.'; + + @override + String get invalidAmount => 'L\'importo fornito non è valido. Verificalo e riprova.'; + + @override + String get invalidInvoice => 'La fattura Lightning fornita non è valida. Controlla i dettagli della fattura e riprova.'; + + @override + String get invalidPaymentRequest => 'La richiesta di pagamento non è valida o non può essere elaborata.'; + + @override + String get invalidPeer => 'Non sei autorizzato ad eseguire questa azione.'; + + @override + String get invalidRating => 'Il valore della valutazione è non valido o fuori dal range consentito.'; + + @override + String get invalidTextMessage => 'Il messaggio di testo non è valido o contiene contenuti proibiti.'; + + @override + String get invalidOrderKind => 'Il tipo di ordine non è valido.'; + + @override + String get invalidOrderStatus => 'L\'azione non può essere completata a causa dello stato attuale dell\'ordine.'; + + @override + String get invalidPubkey => 'L\'azione non può essere completata perché la chiave pubblica non è valida.'; + + @override + String get invalidParameters => 'L\'azione non può essere completata a causa di parametri non validi. Rivedi i valori forniti e riprova.'; + + @override + String get orderAlreadyCanceled => 'L\'azione non può essere completata perché l\'ordine è già stato annullato.'; + + @override + String get cantCreateUser => 'L\'azione non può essere completata perché l\'utente non può essere creato.'; + + @override + String get isNotYourOrder => 'Questo ordine non appartiene a te.'; + + @override + String notAllowedByStatus(Object id, Object order_status) { + return 'Non sei autorizzato ad eseguire questa azione perché lo stato dell\'ordine ID $id è $order_status.'; + } + + @override + String outOfRangeFiatAmount(Object max_amount, Object min_amount) { + return 'L\'importo richiesto è errato e potrebbe essere fuori dai limiti accettabili. Il limite minimo è $min_amount e il limite massimo è $max_amount.'; + } + + @override + String outOfRangeSatsAmount(Object max_order_amount, Object min_order_amount) { + return 'L\'importo consentito per gli ordini di questo Mostro è compreso tra min $min_order_amount e max $max_order_amount Sats. Inserisci un importo all\'interno di questo range.'; + } + + @override + String get isNotYourDispute => 'Questa disputa non è assegnata a te!'; + + @override + String get disputeCreationError => 'Non è possibile avviare una disputa per questo ordine.'; + + @override + String get notFound => 'Disputa non trovata.'; + + @override + String get invalidDisputeStatus => 'Lo stato della disputa è invalido.'; + + @override + String get invalidAction => 'L\'azione richiesta è invalida'; + + @override + String get pendingOrderExists => 'Esiste già un ordine in attesa.'; +} diff --git a/lib/main.dart b/lib/main.dart index 8ee75534..65330b9e 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; @@ -5,30 +7,46 @@ import 'package:mostro_mobile/core/app.dart'; import 'package:mostro_mobile/features/auth/providers/auth_notifier_provider.dart'; import 'package:mostro_mobile/features/settings/settings_notifier.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; -import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; -import 'package:mostro_mobile/shared/providers/storage_providers.dart'; +import 'package:mostro_mobile/background/background_service.dart'; +import 'package:mostro_mobile/notifications/notification_service.dart'; +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:shared_preferences/shared_preferences.dart'; -void main() async { +Future main() async { WidgetsFlutterBinding.ensureInitialized(); - + + if (Platform.isAndroid) { + await requestNotificationPermissionIfNeeded(); + } + final biometricsHelper = BiometricsHelper(); final sharedPreferences = SharedPreferencesAsync(); final secureStorage = const FlutterSecureStorage(); - final database = await openMostroDatabase(); + + final mostroDatabase = await openMostroDatabase('mostro.db'); + final eventsDatabase = await openMostroDatabase('events.db'); final settings = SettingsNotifier(sharedPreferences); await settings.init(); + await initializeNotifications(); + + final backgroundService = createBackgroundService(settings.settings); + await backgroundService.init(); + runApp( ProviderScope( overrides: [ settingsProvider.overrideWith((b) => settings), + backgroundServiceProvider.overrideWithValue(backgroundService), biometricsHelperProvider.overrideWithValue(biometricsHelper), sharedPreferencesProvider.overrideWithValue(sharedPreferences), secureStorageProvider.overrideWithValue(secureStorage), - mostroDatabaseProvider.overrideWithValue(database), + mostroDatabaseProvider.overrideWithValue(mostroDatabase), + eventDatabaseProvider.overrideWithValue(eventsDatabase), ], child: const MostroApp(), ), diff --git a/lib/notifications/notification_service.dart b/lib/notifications/notification_service.dart new file mode 100644 index 00000000..a133838c --- /dev/null +++ b/lib/notifications/notification_service.dart @@ -0,0 +1,87 @@ +import 'dart:math'; + +import 'package:dart_nostr/nostr/model/event/event.dart'; +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:logger/logger.dart'; + +final FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin = + FlutterLocalNotificationsPlugin(); + +Future initializeNotifications() async { + const android = AndroidInitializationSettings( + '@drawable/ic_bg_service_small', + ); + const ios = DarwinInitializationSettings(); + + const linux = LinuxInitializationSettings( + defaultActionName: 'Open', + ); + + const initSettings = InitializationSettings( + android: android, + iOS: ios, + linux: linux, + macOS: ios + ); + await flutterLocalNotificationsPlugin.initialize(initSettings); +} + +Future showLocalNotification(NostrEvent event) async { + const details = NotificationDetails( + android: AndroidNotificationDetails( + 'mostro_channel', + 'Mostro Notifications', + channelDescription: 'Notifications for Mostro trades and messages', + importance: Importance.max, + priority: Priority.high, + visibility: NotificationVisibility.public, + playSound: true, + enableVibration: true, + ticker: 'ticker', + // Uncomment for heads-up notification, use with care: + // fullScreenIntent: true, + ), + iOS: DarwinNotificationDetails( + presentAlert: true, + presentBadge: true, + presentSound: true, + // Optionally set interruption level for iOS 15+: + interruptionLevel: InterruptionLevel.critical, + ), + ); + await flutterLocalNotificationsPlugin.show( + event.id.hashCode, // Use unique ID for each event + 'New Mostro Event', + 'You have received a new message from Mostro', + details, + ); +} + +Future retryNotification(NostrEvent event, {int maxAttempts = 3}) async { + int attempt = 0; + bool success = false; + + while (!success && attempt < maxAttempts) { + try { + await showLocalNotification(event); + success = true; + } catch (e) { + attempt++; + if (attempt >= maxAttempts) { + 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'); + await Future.delayed(Duration(seconds: backoffSeconds)); + } + } + + // Optionally store failed notifications for later retry when app returns to foreground + if (!success) { + // Store the event ID in a persistent queue for later retry + // await failedNotificationsQueue.add(event.id!); + } +} diff --git a/lib/services/currency_input_formatter.dart b/lib/services/currency_input_formatter.dart deleted file mode 100644 index 699e508b..00000000 --- a/lib/services/currency_input_formatter.dart +++ /dev/null @@ -1,18 +0,0 @@ -import 'package:flutter/services.dart'; - -class CurrencyInputFormatter extends TextInputFormatter { - @override - TextEditingValue formatEditUpdate( - TextEditingValue oldValue, - TextEditingValue newValue, - ) { - final text = newValue.text; - final regex = RegExp(r'^\d*\.?\d{0,2}$'); - - if (regex.hasMatch(text)) { - return newValue; - } else { - return oldValue; - } - } -} diff --git a/lib/services/lifecycle_manager.dart b/lib/services/lifecycle_manager.dart new file mode 100644 index 00000000..82f91bff --- /dev/null +++ b/lib/services/lifecycle_manager.dart @@ -0,0 +1,120 @@ +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/features/chat/providers/chat_room_providers.dart'; +import 'package:mostro_mobile/features/trades/providers/trades_provider.dart'; +import 'package:mostro_mobile/shared/providers/background_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; + +class LifecycleManager extends WidgetsBindingObserver { + final Ref ref; + bool _isInBackground = false; + final List _activeSubscriptions = []; + final _logger = Logger(); + + LifecycleManager(this.ref) { + WidgetsBinding.instance.addObserver(this); + } + + @override + void didChangeAppLifecycleState(AppLifecycleState state) async { + if (Platform.isAndroid || Platform.isIOS) { + switch (state) { + case AppLifecycleState.resumed: + // App is in foreground + if (_isInBackground) { + await _switchToForeground(); + } + break; + case AppLifecycleState.paused: + case AppLifecycleState.inactive: + case AppLifecycleState.detached: + // App is in background + if (!_isInBackground) { + await _switchToBackground(); + } + break; + default: + break; + } + } + } + + Future _switchToForeground() async { + try { + _isInBackground = false; + _logger.i("Switching to foreground"); + + // Clear active subscriptions + _activeSubscriptions.clear(); + + // Stop background service + final backgroundService = ref.read(backgroundServiceProvider); + await backgroundService.setForegroundStatus(true); + _logger.i("Background service foreground status set to true"); + + // Add a small delay to ensure the background service has fully transitioned + await Future.delayed(const Duration(milliseconds: 500)); + + // Reinitialize the mostro service + _logger.i("Reinitializing MostroService"); + ref.read(mostroServiceProvider).init(); + + // Refresh order repository by re-reading it + _logger.i("Refreshing order repository"); + final orderRepo = ref.read(orderRepositoryProvider); + orderRepo.reloadData(); + + // Reinitialize chat rooms + _logger.i("Reloading chat rooms"); + final chatRooms = ref.read(chatRoomsNotifierProvider.notifier); + chatRooms.reloadAllChats(); + + // Force UI update for trades + _logger.i("Invalidating providers to refresh UI"); + ref.invalidate(filteredTradesProvider); + + _logger.i("Foreground transition complete"); + } catch (e) { + _logger.e("Error during foreground transition: $e"); + } + } + + Future _switchToBackground() async { + try { + _isInBackground = true; + _logger.i("Switching to background"); + + // Transfer active subscriptions to background service + final backgroundService = ref.read(backgroundServiceProvider); + await backgroundService.setForegroundStatus(false); + + if (_activeSubscriptions.isNotEmpty) { + _logger.i( + "Transferring ${_activeSubscriptions.length} active subscriptions to background service"); + backgroundService.subscribe(_activeSubscriptions); + } else { + _logger.w("No active subscriptions to transfer to background service"); + } + + _logger.i("Background transition complete"); + } catch (e) { + _logger.e("Error during background transition: $e"); + } + } + + void addSubscription(NostrFilter filter) { + _activeSubscriptions.add(filter); + } + + void dispose() { + WidgetsBinding.instance.removeObserver(this); + } +} + +// Provider for the lifecycle manager +final lifecycleManagerProvider = Provider((ref) => LifecycleManager(ref)); diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 549e37fd..98a55cf0 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -1,46 +1,61 @@ import 'dart:convert'; -import 'package:dart_nostr/dart_nostr.dart'; +import 'package:dart_nostr/nostr/model/export.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/data/models/amount.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart' as actions; -import 'package:mostro_mobile/data/models/enums/order_type.dart'; -import 'package:mostro_mobile/data/models/enums/role.dart'; -import 'package:mostro_mobile/data/models/mostro_message.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart'; -import 'package:mostro_mobile/data/models/nostr_event.dart'; -import 'package:mostro_mobile/data/models/order.dart'; -import 'package:mostro_mobile/data/models/payment_request.dart'; -import 'package:mostro_mobile/data/models/rating_user.dart'; -import 'package:mostro_mobile/data/models/session.dart'; -import 'package:mostro_mobile/data/repositories/event_storage.dart'; -import 'package:mostro_mobile/data/repositories/mostro_storage.dart'; +import 'package:mostro_mobile/data/models.dart'; +import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; -import 'package:mostro_mobile/services/event_bus.dart'; -import 'package:mostro_mobile/services/nostr_service.dart'; +import 'package:mostro_mobile/services/lifecycle_manager.dart'; import 'package:mostro_mobile/shared/notifiers/order_action_notifier.dart'; import 'package:mostro_mobile/shared/notifiers/session_notifier.dart'; +import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; class MostroService { final Ref ref; - final NostrService _nostrService; final SessionNotifier _sessionNotifier; - final EventStorage _eventStorage; - final MostroStorage _messageStorage; - - final EventBus _bus; Settings _settings; MostroService( this._sessionNotifier, - this._eventStorage, - this._bus, - this._messageStorage, this.ref, - ) : _nostrService = ref.read(nostrServiceProvider), - _settings = ref.read(settingsProvider); + ) : _settings = ref.read(settingsProvider).copyWith() { + init(); + } + + void init() async { + final now = DateTime.now(); + final cutoff = now.subtract(const Duration(hours: 24)); + final sessions = _sessionNotifier.sessions; + final messageStorage = ref.read(mostroStorageProvider); + // Set of terminal statuses + const terminalStatuses = { + Status.canceled, + Status.cooperativelyCanceled, + Status.success, + Status.expired, + Status.canceledByAdmin, + Status.settledByAdmin, + Status.completedByAdmin, + }; + for (final session in sessions) { + if (session.startTime.isAfter(cutoff)) { + if (session.orderId != null) { + final latestOrderMsg = await messageStorage + .getLatestMessageOfTypeById(session.orderId!); + final status = latestOrderMsg?.payload is Order + ? (latestOrderMsg!.payload as Order).status + : null; + if (status != null && terminalStatuses.contains(status)) { + continue; + } + } + subscribe(session); + } + } + } void subscribe(Session session) { final filter = NostrFilter( @@ -48,11 +63,17 @@ class MostroService { p: [session.tradeKey.public], ); - _nostrService.subscribeToEvents(filter).listen((event) async { - // The item has already beeen processed - if (await _eventStorage.hasItem(event.id!)) return; - // Store the event - await _eventStorage.putItem( + final request = NostrRequest(filters: [filter]); + + ref.read(lifecycleManagerProvider).addSubscription(filter); + + final nostrService = ref.read(nostrServiceProvider); + + nostrService.subscribeToEvents(request).listen((event) async { + final eventStore = ref.read(eventStorageProvider); + + if (await eventStore.hasItem(event.id!)) return; + await eventStore.putItem( event.id!, event, ); @@ -65,34 +86,25 @@ class MostroService { final result = jsonDecode(decryptedEvent.content!); if (result is! List) return; - final msgMap = result[0]; - - final msg = MostroMessage.fromJson(msgMap); + result[0]['timestamp'] = decryptedEvent.createdAt?.millisecondsSinceEpoch; + final msg = MostroMessage.fromJson(result[0]); + final messageStorage = ref.read(mostroStorageProvider); if (msg.id != null) { - ref - .read( - orderActionNotifierProvider(msg.id!).notifier, - ) - .set( - msg.action, - ); + if (await messageStorage.hasMessageByKey(decryptedEvent.id!)) return; + ref.read(orderActionNotifierProvider(msg.id!).notifier).set(msg.action); } - - if (msg.action == actions.Action.canceled) { - await _messageStorage.deleteAllMessagesById(session.orderId!); + if (msg.action == Action.canceled) { + ref.read(orderNotifierProvider(session.orderId!).notifier).dispose(); + await messageStorage.deleteAllMessagesByOrderId(session.orderId!); await _sessionNotifier.deleteSession(session.orderId!); return; } - - await _messageStorage.addMessage(msg); - + await messageStorage.addMessage(decryptedEvent.id!, msg); if (session.orderId == null && msg.id != null) { session.orderId = msg.id; await _sessionNotifier.saveSession(session); } - - _bus.emit(msg); }); } @@ -207,7 +219,8 @@ class MostroService { masterKey: session.fullPrivacy ? null : session.masterKey, keyIndex: session.fullPrivacy ? null : session.keyIndex, ); - await _nostrService.publishEvent(event); + + await ref.read(nostrServiceProvider).publishEvent(event); return session; } diff --git a/lib/services/nostr_service.dart b/lib/services/nostr_service.dart index 742a57e9..5dd88796 100644 --- a/lib/services/nostr_service.dart +++ b/lib/services/nostr_service.dart @@ -1,5 +1,7 @@ import 'package:collection/collection.dart'; 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'; @@ -7,22 +9,37 @@ import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; class NostrService { - Settings settings; + late Settings settings; final Nostr _nostr = Nostr.instance; - NostrService(this.settings); + NostrService(); final Logger _logger = Logger(); bool _isInitialized = false; - Future init() async { + Future init(Settings settings) async { + this.settings = settings; try { await _nostr.services.relays.init( relaysUrl: settings.relays, connectionTimeout: Config.nostrConnectionTimeout, shouldReconnectToRelayOnNotice: true, - onRelayListening: (relay, url, channel) { - _logger.i('Connected to relay: $relay'); + retryOnClose: true, + retryOnError: true, + onRelayListening: (relayUrl, receivedData, channel) { + if (receivedData is NostrEvent) { + _logger.i('Event from $relayUrl: ${receivedData.content}'); + } else if (receivedData is NostrNotice) { + _logger.i('Notice from $relayUrl: ${receivedData.message}'); + } else if (receivedData is NostrEventOkCommand) { + _logger.i( + 'OK from $relayUrl: ${receivedData.eventId} (accepted: ${receivedData.isEventAccepted})'); + } else if (receivedData is NostrRequestEoseCommand) { + _logger.i( + 'EOSE from $relayUrl for subscription: ${receivedData.subscriptionId}'); + } else if (receivedData is NostrCountResponse) { + _logger.i('Count from $relayUrl: ${receivedData.count}'); + } }, onRelayConnectionError: (relay, error, channel) { _logger.w('Failed to connect to relay $relay: $error'); @@ -30,8 +47,6 @@ class NostrService { onRelayConnectionDone: (relay, socket) { _logger.i('Connection to relay: $relay via $socket is done'); }, - retryOnClose: true, - retryOnError: true, ); _isInitialized = true; _logger.i('Nostr initialized successfully'); @@ -42,11 +57,10 @@ class NostrService { } Future updateSettings(Settings newSettings) async { - settings = newSettings.copyWith(); final relays = Nostr.instance.services.relays.relaysList; - if (!ListEquality().equals(relays, settings.relays)) { + if (!ListEquality().equals(relays, newSettings.relays)) { _logger.i('Updating relays...'); - await init(); + await init(newSettings); } } @@ -79,20 +93,17 @@ class NostrService { } final request = NostrRequest(filters: [filter]); - return - await _nostr.services.relays.startEventsSubscriptionAsync( + return await _nostr.services.relays.startEventsSubscriptionAsync( request: request, timeout: Config.nostrConnectionTimeout, ); - } - Stream subscribeToEvents(NostrFilter filter) { + Stream subscribeToEvents(NostrRequest request) { if (!_isInitialized) { throw Exception('Nostr is not initialized. Call init() first.'); } - final request = NostrRequest(filters: [filter]); final subscription = _nostr.services.relays.startEventsSubscription(request: request); @@ -119,7 +130,7 @@ class NostrService { } String getMostroPubKey() { - return Config.mostroPubKey; + return settings.mostroPublicKey; } Future createNIP59Event( @@ -175,4 +186,12 @@ class NostrService { recipientPubKey, ); } + + void unsubscribe(String id) { + if (!_isInitialized) { + throw Exception('Nostr is not initialized. Call init() first.'); + } + + _nostr.services.relays.closeEventsSubscription(id); + } } diff --git a/lib/shared/notifiers/session_notifier.dart b/lib/shared/notifiers/session_notifier.dart index 99ac7a68..6b6a2e85 100644 --- a/lib/shared/notifiers/session_notifier.dart +++ b/lib/shared/notifiers/session_notifier.dart @@ -1,6 +1,5 @@ import 'dart:async'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:logger/logger.dart'; import 'package:mostro_mobile/data/models/enums/role.dart'; import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/data/repositories/session_storage.dart'; @@ -8,52 +7,42 @@ import 'package:mostro_mobile/features/key_manager/key_manager.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; class SessionNotifier extends StateNotifier> { - // Dependencies - final Logger _logger = Logger(); final KeyManager _keyManager; final SessionStorage _storage; - // Current settings Settings _settings; - // In-memory session cache, keyed by `session.keyIndex`. final Map _sessions = {}; - // Cleanup / expiration logic Timer? _cleanupTimer; static const int sessionExpirationHours = 36; static const int cleanupIntervalMinutes = 30; static const int maxBatchSize = 100; - /// Public getter to expose sessions (if needed) List get sessions => _sessions.values.toList(); SessionNotifier( this._keyManager, this._storage, this._settings, - ) : super([]) { - //_init(); - //_initializeCleanup(); - } + ) : super([]); - /// Initialize by loading all sessions from DB into memory, then updating state. Future init() async { final allSessions = await _storage.getAllSessions(); + final now = DateTime.now(); + final cutoff = now.subtract(const Duration(hours: 48)); for (final session in allSessions) { - _sessions[session.orderId!] = session; + if (session.startTime.isAfter(cutoff)) { + _sessions[session.orderId!] = session; + } } - // Update the notifier state with fresh data. state = sessions; } - /// Update the application settings if needed. void updateSettings(Settings settings) { _settings = settings.copyWith(); - // You might want to refresh or do something else if settings impact sessions. } - /// Creates a new session, storing it both in memory and in the database. Future newSession({String? orderId, Role? role}) async { final masterKey = _keyManager.masterKeyPair!; final keyIndex = await _keyManager.getCurrentKeyIndex(); @@ -69,11 +58,8 @@ class SessionNotifier extends StateNotifier> { role: role, ); - // Cache it in memory - if (orderId != null) { _sessions[orderId] = session; - // Persist it to DB await _storage.putSession(session); state = sessions; } else { @@ -82,14 +68,23 @@ class SessionNotifier extends StateNotifier> { return session; } - /// Updates a session in both memory and database. Future saveSession(Session session) async { _sessions[session.orderId!] = session; await _storage.putSession(session); state = sessions; } - /// Retrieve the first session whose `orderId` matches [orderId]. + /// Generic session update and persist method + Future updateSession( + String orderId, void Function(Session) update) async { + final session = _sessions[orderId]; + if (session != null) { + update(session); + await _storage.putSession(session); + state = sessions; + } + } + Session? getSessionByOrderId(String orderId) { try { return _sessions[orderId]; @@ -98,57 +93,35 @@ class SessionNotifier extends StateNotifier> { } } - /// Retrieve a session by its keyIndex (checks memory first, then DB). + Session? getSessionByTradeKey(String tradeKey) { + try { + return state.firstWhere( + (s) => s.tradeKey.public == tradeKey, + ); + } on StateError { + return null; + } + } + Future loadSession(int keyIndex) async { final sessions = await _storage.getAllSessions(); - return sessions.firstWhere((s) => s.keyIndex == keyIndex); + return sessions.firstWhere( + (s) => s.keyIndex == keyIndex, + ); } - /// Resets all stored sessions by clearing DB and memory. Future reset() async { - await _storage.deleteAllItems(); + await _storage.deleteAll(); _sessions.clear(); state = []; } - /// Deletes a session from memory and DB. Future deleteSession(String sessionId) async { _sessions.remove(sessionId); await _storage.deleteSession(sessionId); state = sessions; } - /// Removes sessions older than [sessionExpirationHours] from both DB and memory. - Future clearExpiredSessions() async { - try { - final removedIds = await _storage.deleteExpiredSessions( - sessionExpirationHours, - maxBatchSize, - ); - for (final id in removedIds) { - // If your underlying session keys are strings, - // you may have to parse them to int or store them as-is. - _sessions.remove(id); - } - state = sessions; - } catch (e) { - _logger.e('Error during session cleanup: $e'); - } - } - - /// Set up an initial cleanup run and a periodic timer. - void _initializeCleanup() { - _cleanupTimer?.cancel(); - // Immediately do a cleanup pass - clearExpiredSessions(); - // Schedule periodic cleanup - _cleanupTimer = Timer.periodic( - const Duration(minutes: cleanupIntervalMinutes), - (_) => clearExpiredSessions(), - ); - } - - /// Dispose resources (like timers) when no longer needed. @override void dispose() { _cleanupTimer?.cancel(); diff --git a/lib/shared/providers/app_init_provider.dart b/lib/shared/providers/app_init_provider.dart index 7fea1a94..bcf9404a 100644 --- a/lib/shared/providers/app_init_provider.dart +++ b/lib/shared/providers/app_init_provider.dart @@ -7,7 +7,10 @@ import 'package:mostro_mobile/features/chat/providers/chat_room_providers.dart'; import 'package:mostro_mobile/features/order/providers/order_notifier_provider.dart'; import 'package:mostro_mobile/features/settings/settings.dart'; import 'package:mostro_mobile/features/settings/settings_provider.dart'; +import 'package:mostro_mobile/data/models/enums/status.dart'; +import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/shared/notifiers/order_action_notifier.dart'; +import 'package:mostro_mobile/shared/providers/background_service_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; @@ -16,7 +19,7 @@ import 'package:shared_preferences/shared_preferences.dart'; final appInitializerProvider = FutureProvider((ref) async { final nostrService = ref.read(nostrServiceProvider); - await nostrService.init(); + await nostrService.init(ref.read(settingsProvider)); final keyManager = ref.read(keyManagerProvider); await keyManager.init(); @@ -24,31 +27,75 @@ final appInitializerProvider = FutureProvider((ref) async { final sessionManager = ref.read(sessionNotifierProvider.notifier); await sessionManager.init(); + // --- Custom logic for initializing notifiers and chats --- + final now = DateTime.now(); + final cutoff = now.subtract(const Duration(hours: 24)); + final sessions = sessionManager.sessions; + final messageStorage = ref.read(mostroStorageProvider); + final terminalStatuses = { + Status.canceled, + Status.cooperativelyCanceled, + Status.success, + Status.expired, + Status.canceledByAdmin, + Status.settledByAdmin, + Status.completedByAdmin, + }; + for (final session in sessions) { + if (session.startTime.isAfter(cutoff)) { + bool isActive = true; + if (session.orderId != null) { + final latestOrderMsg = await messageStorage.getLatestMessageOfTypeById(session.orderId!); + final status = latestOrderMsg?.payload is Order + ? (latestOrderMsg!.payload as Order).status + : null; + if (status != null && terminalStatuses.contains(status)) { + isActive = false; + } + } + if (isActive) { + // Initialize order notifier if needed + ref.read(orderNotifierProvider(session.orderId!).notifier); + // Initialize chat notifier if needed + if (session.peer != null) { + ref.read(chatRoomsProvider(session.orderId!).notifier); + } + } + } + } + final mostroService = ref.read(mostroServiceProvider); ref.listen(settingsProvider, (previous, next) { sessionManager.updateSettings(next); mostroService.updateSettings(next); + ref.read(backgroundServiceProvider).updateSettings(next); }); final mostroStorage = ref.read(mostroStorageProvider); for (final session in sessionManager.sessions) { if (session.orderId != null) { - final orderList = await mostroStorage.getMessagesForId(session.orderId!); - if (orderList.isNotEmpty) { + final order = await mostroStorage.getLatestMessageById(session.orderId!); + if (order != null) { + // Set the order action ref.read(orderActionNotifierProvider(session.orderId!).notifier).set( - orderList.last.action, + order.action, ); + + // Explicitly initialize EACH notifier in the family + // to ensure they're all properly set up for this orderId + ref.read(paymentNotifierProvider(session.orderId!).notifier).sync(); + ref.read(cantDoNotifierProvider(session.orderId!).notifier).sync(); + ref.read(disputeNotifierProvider(session.orderId!).notifier).sync(); } - ref.read( - orderNotifierProvider(session.orderId!), - ); - mostroService.subscribe(session); + + // Read the order notifier provider last, which will watch all the above + ref.read(orderNotifierProvider(session.orderId!)); } if (session.peer != null) { - final chat = ref.watch( + final chat = ref.read( chatRoomsProvider(session.orderId!).notifier, ); chat.subscribe(); diff --git a/lib/shared/providers/background_service_provider.dart b/lib/shared/providers/background_service_provider.dart new file mode 100644 index 00000000..d493a59d --- /dev/null +++ b/lib/shared/providers/background_service_provider.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/background/abstract_background_service.dart'; + +final backgroundServiceProvider = Provider((ref) { + throw UnimplementedError(); +}); diff --git a/lib/shared/providers/legible_handle_provider.dart b/lib/shared/providers/legible_handle_provider.dart new file mode 100644 index 00000000..f3965915 --- /dev/null +++ b/lib/shared/providers/legible_handle_provider.dart @@ -0,0 +1,139 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +/// A small Dart "library" that generates a memorable, on-theme nickname +/// from a 32-byte (64-hex-char) public key string. Ideal for ephemeral +/// or No-KYC usage. Collisions are possible with many users; expand +/// lists for a bigger namespace if needed. + +/// Some playful and thematic adjectives (Bitcoin/Nostr/privacy vibes + more). +const List kAdjectives = [ + 'shadowy', + 'orange', + 'lightning', + 'p2p', + 'noncustodial', + 'trustless', + 'unbanked', + 'atomic', + 'magic', + 'tor', + 'hidden', + 'incognito', + 'anonymous', + 'encrypted', + 'ghostly', + 'silent', + 'masked', + 'stealthy', + 'free', + 'nostalgic', + 'ephemeral', + 'sovereign', + 'unstoppable', + 'private', + 'censorshipresistant', + 'hush', + 'defiant', + 'subversive', + 'fiery', + 'subzero', + 'burning', + 'cosmic', + 'mighty', + 'whispering', + 'cyber', + 'rusty', + 'nihilistic', + 'mempool', + 'dark', + 'wicked', + 'spicy', + 'noKYC', + 'discreet', + 'loose', + 'boosted', + 'starving', + 'hungry', + 'orwellian', + 'bullish', + 'bearish', +]; + +/// Some nouns mixing animals, Bitcoin legends, Nostr references, & places. +const List kNouns = [ + 'wizard', + 'pirate', + 'zap', + 'node', + 'invoice', + 'nipster', + 'nomad', + 'sats', + 'bull', + 'bear', + 'whale', + 'frog', + 'gorilla', + 'nostrich', + 'halfinney', + 'hodlonaut', + 'satoshi', + 'nakamoto', + 'gigi', + 'samurai', + 'crusader', + 'tinkerer', + 'nostr', + 'pleb', + 'warrior', + 'ecdsa', + 'monkey', + 'wolf', + 'renegade', + 'minotaur', + 'phoenix', + 'dragon', + 'fiatjaf', + 'jackmallers', + 'roasbeef', + 'berlin', + 'tokyo', + 'buenosaires', + 'miami', + 'prague', + 'amsterdam', + 'lugano', + 'seoul', + 'bitcoinbeach', + 'odell', + 'bitcoinkid', + 'marty', + 'finney', + 'carnivore', + 'ape', + 'honeybadger', +]; + +/// Convert a 32-byte hex string (64 hex chars) into a fun, deterministic handle. +/// Example result: "shadowy-wizard", "noKYC-satoshi", etc. +String deterministicHandleFromHexKey(String hexKey) { + // 1) Parse the 64-char hex into a BigInt. + // Because it's 32 bytes, there's up to 256 bits of data here. + final pubKeyBigInt = BigInt.parse(hexKey, radix: 16); + + // 2) Use modulo arithmetic to pick an adjective and a noun. + final adjectivesCount = kAdjectives.length; + final nounsCount = kNouns.length; + + final indexAdjective = pubKeyBigInt % BigInt.from(adjectivesCount); + final indexNoun = (pubKeyBigInt ~/ BigInt.from(adjectivesCount)) % + BigInt.from(nounsCount); + + final adjective = kAdjectives[indexAdjective.toInt()]; + final noun = kNouns[indexNoun.toInt()]; + + return '$adjective-$noun'; +} + +final nickNameProvider = Provider.family( + (ref, pubkey) => deterministicHandleFromHexKey(pubkey)); diff --git a/lib/shared/providers/local_notifications_providers.dart b/lib/shared/providers/local_notifications_providers.dart new file mode 100644 index 00000000..caa0d9f3 --- /dev/null +++ b/lib/shared/providers/local_notifications_providers.dart @@ -0,0 +1,7 @@ +import 'package:flutter_local_notifications/flutter_local_notifications.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +final localNotificationsProvider = Provider((ref) { + return FlutterLocalNotificationsPlugin(); +}); + diff --git a/lib/shared/providers/mostro_database_provider.dart b/lib/shared/providers/mostro_database_provider.dart index 866661d4..8d5b5019 100644 --- a/lib/shared/providers/mostro_database_provider.dart +++ b/lib/shared/providers/mostro_database_provider.dart @@ -1,13 +1,24 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:tekartik_app_flutter_sembast/sembast.dart'; +import 'package:path/path.dart' as p; +import 'package:path_provider/path_provider.dart'; +import 'package:sembast/sembast_io.dart'; -Future openMostroDatabase() async { +Future openMostroDatabase(String dbName) async { + //var factory = getDatabaseFactory(packageName: dbName); + //final db = await factory.openDatabase(dbName); - var factory = getDatabaseFactory(); - final db = await factory.openDatabase('mostro.db'); + final dir = await getApplicationSupportDirectory(); + final path = p.join(dir.path, 'mostro', 'databases', dbName); + + final db = await databaseFactoryIo.openDatabase(path); return db; } final mostroDatabaseProvider = Provider((ref) { throw UnimplementedError(); }); + + +final eventDatabaseProvider = Provider((ref) { + throw UnimplementedError(); +}); diff --git a/lib/shared/providers/mostro_service_provider.dart b/lib/shared/providers/mostro_service_provider.dart index 4724f5c0..1129784a 100644 --- a/lib/shared/providers/mostro_service_provider.dart +++ b/lib/shared/providers/mostro_service_provider.dart @@ -1,31 +1,23 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:mostro_mobile/data/repositories/event_storage.dart'; -import 'package:mostro_mobile/services/event_bus.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; -import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; import 'package:mostro_mobile/shared/providers/session_manager_provider.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; part 'mostro_service_provider.g.dart'; -@riverpod +@Riverpod(keepAlive: true) EventStorage eventStorage(Ref ref) { - final db = ref.watch(mostroDatabaseProvider); + final db = ref.watch(eventDatabaseProvider); return EventStorage(db: db); } -@riverpod +@Riverpod(keepAlive: true) MostroService mostroService(Ref ref) { - final sessionStorage = ref.read(sessionNotifierProvider.notifier); - final eventStore = ref.read(eventStorageProvider); - final eventBus = ref.read(eventBusProvider); - final mostroDatabase = ref.read(mostroStorageProvider); + final sessionNotifier = ref.read(sessionNotifierProvider.notifier); final mostroService = MostroService( - sessionStorage, - eventStore, - eventBus, - mostroDatabase, - ref + sessionNotifier, + ref, ); return mostroService; } diff --git a/lib/shared/providers/mostro_service_provider.g.dart b/lib/shared/providers/mostro_service_provider.g.dart index 34196ff7..cd970ea4 100644 --- a/lib/shared/providers/mostro_service_provider.g.dart +++ b/lib/shared/providers/mostro_service_provider.g.dart @@ -6,11 +6,11 @@ part of 'mostro_service_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$eventStorageHash() => r'671aa58f4248e9d508ea22bcc85da2d19d36b0b6'; +String _$eventStorageHash() => r'ba0996d381cefc0cc74a999ed3b83baf77446117'; /// See also [eventStorage]. @ProviderFor(eventStorage) -final eventStorageProvider = AutoDisposeProvider.internal( +final eventStorageProvider = Provider.internal( eventStorage, name: r'eventStorageProvider', debugGetCreateSourceHash: @@ -21,12 +21,12 @@ final eventStorageProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef EventStorageRef = AutoDisposeProviderRef; -String _$mostroServiceHash() => r'6e27d58dbe7170c180dcf3729f0f5f27a29f06c6'; +typedef EventStorageRef = ProviderRef; +String _$mostroServiceHash() => r'41bba48eb8dcfb160c783c1f1bde78928c3df1cb'; /// See also [mostroService]. @ProviderFor(mostroService) -final mostroServiceProvider = AutoDisposeProvider.internal( +final mostroServiceProvider = Provider.internal( mostroService, name: r'mostroServiceProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -38,6 +38,6 @@ final mostroServiceProvider = AutoDisposeProvider.internal( @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element -typedef MostroServiceRef = AutoDisposeProviderRef; +typedef MostroServiceRef = ProviderRef; // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/shared/providers/mostro_storage_provider.dart b/lib/shared/providers/mostro_storage_provider.dart index 1ccdfab0..b251e7d7 100644 --- a/lib/shared/providers/mostro_storage_provider.dart +++ b/lib/shared/providers/mostro_storage_provider.dart @@ -1,4 +1,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/data/repositories/mostro_storage.dart'; import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; @@ -6,3 +8,20 @@ final mostroStorageProvider = Provider((ref) { final mostroDatabase = ref.watch(mostroDatabaseProvider); return MostroStorage(db: mostroDatabase); }); + +final mostroMessageStreamProvider = StreamProvider.family((ref, orderId) { + final storage = ref.read(mostroStorageProvider); + return storage.watchLatestMessage(orderId); +}); + +final mostroMessageHistoryProvider = StreamProvider.family, String>( + (ref, orderId) { + final storage = ref.read(mostroStorageProvider); + return storage.watchAllMessages(orderId); + }, +); + +final mostroOrderStreamProvider = StreamProvider.family((ref, orderId) { + final storage = ref.read(mostroStorageProvider); + return storage.watchLatestMessageOfType(orderId); +}); diff --git a/lib/shared/providers/nostr_service_provider.dart b/lib/shared/providers/nostr_service_provider.dart index f44fb50a..a481a407 100644 --- a/lib/shared/providers/nostr_service_provider.dart +++ b/lib/shared/providers/nostr_service_provider.dart @@ -4,8 +4,7 @@ import 'package:mostro_mobile/features/settings/settings_provider.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; final nostrServiceProvider = Provider((ref) { - final settings = ref.read(settingsProvider); - final nostrService = NostrService(settings); + final nostrService = NostrService(); ref.listen(settingsProvider, (previous, next) { nostrService.updateSettings(next); diff --git a/lib/shared/providers/order_repository_provider.dart b/lib/shared/providers/order_repository_provider.dart index e772ceb8..7e600755 100644 --- a/lib/shared/providers/order_repository_provider.dart +++ b/lib/shared/providers/order_repository_provider.dart @@ -25,12 +25,12 @@ final orderEventsProvider = StreamProvider>((ref) { }); final eventProvider = Provider.family((ref, orderId) { - final allEventsAsync = ref.watch(orderEventsProvider); final allEvents = allEventsAsync.maybeWhen( data: (data) => data, orElse: () => [], ); - // firstWhereOrNull returns null if no match is found - return allEvents.firstWhereOrNull((evt) => (evt as NostrEvent).orderId == orderId); + // lastWhereOrNull returns null if no match is found + return allEvents + .lastWhereOrNull((evt) => (evt as NostrEvent).orderId == orderId); }); diff --git a/lib/shared/providers/providers.dart b/lib/shared/providers/providers.dart new file mode 100644 index 00000000..851ff56b --- /dev/null +++ b/lib/shared/providers/providers.dart @@ -0,0 +1,2 @@ +export 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; +export 'package:mostro_mobile/shared/providers/storage_providers.dart'; diff --git a/lib/shared/providers/session_providers.dart b/lib/shared/providers/session_providers.dart index 322c76f2..ea4bba57 100644 --- a/lib/shared/providers/session_providers.dart +++ b/lib/shared/providers/session_providers.dart @@ -1,11 +1,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/data/models/mostro_message.dart'; import 'package:mostro_mobile/features/order/notfiers/order_notifier.dart'; -import 'package:mostro_mobile/services/event_bus.dart'; import 'package:mostro_mobile/features/order/notfiers/cant_do_notifier.dart'; import 'package:mostro_mobile/features/order/notfiers/payment_request_notifier.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; -part 'session_providers.g.dart'; class SessionProviders { final String orderId; @@ -27,15 +24,6 @@ class SessionProviders { } } -@riverpod -class SessionMessages extends _$SessionMessages { - @override - Stream build(String orderId) { - final bus = ref.watch(eventBusProvider); - return bus.stream.where((msg) => msg.id == orderId); - } -} - @riverpod SessionProviders sessionProviders(Ref ref, String orderId) { final providers = SessionProviders(orderId: orderId, ref: ref); diff --git a/lib/shared/providers/session_providers.g.dart b/lib/shared/providers/session_providers.g.dart deleted file mode 100644 index e0724638..00000000 --- a/lib/shared/providers/session_providers.g.dart +++ /dev/null @@ -1,308 +0,0 @@ -// GENERATED CODE - DO NOT MODIFY BY HAND - -part of 'session_providers.dart'; - -// ************************************************************************** -// RiverpodGenerator -// ************************************************************************** - -String _$sessionProvidersHash() => r'7756994a4a75d695f0ea3377c0a3158155a46e50'; - -/// Copied from Dart SDK -class _SystemHash { - _SystemHash._(); - - static int combine(int hash, int value) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + value); - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10)); - return hash ^ (hash >> 6); - } - - static int finish(int hash) { - // ignore: parameter_assignments - hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3)); - // ignore: parameter_assignments - hash = hash ^ (hash >> 11); - return 0x1fffffff & (hash + ((0x00003fff & hash) << 15)); - } -} - -/// See also [sessionProviders]. -@ProviderFor(sessionProviders) -const sessionProvidersProvider = SessionProvidersFamily(); - -/// See also [sessionProviders]. -class SessionProvidersFamily extends Family { - /// See also [sessionProviders]. - const SessionProvidersFamily(); - - /// See also [sessionProviders]. - SessionProvidersProvider call( - String orderId, - ) { - return SessionProvidersProvider( - orderId, - ); - } - - @override - SessionProvidersProvider getProviderOverride( - covariant SessionProvidersProvider provider, - ) { - return call( - provider.orderId, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'sessionProvidersProvider'; -} - -/// See also [sessionProviders]. -class SessionProvidersProvider extends AutoDisposeProvider { - /// See also [sessionProviders]. - SessionProvidersProvider( - String orderId, - ) : this._internal( - (ref) => sessionProviders( - ref as SessionProvidersRef, - orderId, - ), - from: sessionProvidersProvider, - name: r'sessionProvidersProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$sessionProvidersHash, - dependencies: SessionProvidersFamily._dependencies, - allTransitiveDependencies: - SessionProvidersFamily._allTransitiveDependencies, - orderId: orderId, - ); - - SessionProvidersProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.orderId, - }) : super.internal(); - - final String orderId; - - @override - Override overrideWith( - SessionProviders Function(SessionProvidersRef provider) create, - ) { - return ProviderOverride( - origin: this, - override: SessionProvidersProvider._internal( - (ref) => create(ref as SessionProvidersRef), - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - orderId: orderId, - ), - ); - } - - @override - AutoDisposeProviderElement createElement() { - return _SessionProvidersProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is SessionProvidersProvider && other.orderId == orderId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, orderId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin SessionProvidersRef on AutoDisposeProviderRef { - /// The parameter `orderId` of this provider. - String get orderId; -} - -class _SessionProvidersProviderElement - extends AutoDisposeProviderElement - with SessionProvidersRef { - _SessionProvidersProviderElement(super.provider); - - @override - String get orderId => (origin as SessionProvidersProvider).orderId; -} - -String _$sessionMessagesHash() => r'a8f6f4e0f8ec58f745b1e1acd68651436706d948'; - -abstract class _$SessionMessages - extends BuildlessAutoDisposeStreamNotifier { - late final String orderId; - - Stream build( - String orderId, - ); -} - -/// See also [SessionMessages]. -@ProviderFor(SessionMessages) -const sessionMessagesProvider = SessionMessagesFamily(); - -/// See also [SessionMessages]. -class SessionMessagesFamily extends Family> { - /// See also [SessionMessages]. - const SessionMessagesFamily(); - - /// See also [SessionMessages]. - SessionMessagesProvider call( - String orderId, - ) { - return SessionMessagesProvider( - orderId, - ); - } - - @override - SessionMessagesProvider getProviderOverride( - covariant SessionMessagesProvider provider, - ) { - return call( - provider.orderId, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'sessionMessagesProvider'; -} - -/// See also [SessionMessages]. -class SessionMessagesProvider extends AutoDisposeStreamNotifierProviderImpl< - SessionMessages, MostroMessage> { - /// See also [SessionMessages]. - SessionMessagesProvider( - String orderId, - ) : this._internal( - () => SessionMessages()..orderId = orderId, - from: sessionMessagesProvider, - name: r'sessionMessagesProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$sessionMessagesHash, - dependencies: SessionMessagesFamily._dependencies, - allTransitiveDependencies: - SessionMessagesFamily._allTransitiveDependencies, - orderId: orderId, - ); - - SessionMessagesProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.orderId, - }) : super.internal(); - - final String orderId; - - @override - Stream runNotifierBuild( - covariant SessionMessages notifier, - ) { - return notifier.build( - orderId, - ); - } - - @override - Override overrideWith(SessionMessages Function() create) { - return ProviderOverride( - origin: this, - override: SessionMessagesProvider._internal( - () => create()..orderId = orderId, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - orderId: orderId, - ), - ); - } - - @override - AutoDisposeStreamNotifierProviderElement - createElement() { - return _SessionMessagesProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is SessionMessagesProvider && other.orderId == orderId; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, orderId.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin SessionMessagesRef - on AutoDisposeStreamNotifierProviderRef { - /// The parameter `orderId` of this provider. - String get orderId; -} - -class _SessionMessagesProviderElement - extends AutoDisposeStreamNotifierProviderElement with SessionMessagesRef { - _SessionMessagesProviderElement(super.provider); - - @override - String get orderId => (origin as SessionMessagesProvider).orderId; -} -// ignore_for_file: type=lint -// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/shared/providers/session_storage_provider.dart b/lib/shared/providers/session_storage_provider.dart index 0b3c26dd..04fa321b 100644 --- a/lib/shared/providers/session_storage_provider.dart +++ b/lib/shared/providers/session_storage_provider.dart @@ -6,5 +6,5 @@ import 'package:mostro_mobile/shared/providers/mostro_database_provider.dart'; final sessionStorageProvider = Provider((ref) { final keyManager = ref.read(keyManagerProvider); final database = ref.read(mostroDatabaseProvider); - return SessionStorage(db: database, keyManager); + return SessionStorage(keyManager, db: database); }); diff --git a/lib/shared/utils/notification_permission_helper.dart b/lib/shared/utils/notification_permission_helper.dart new file mode 100644 index 00000000..867d494a --- /dev/null +++ b/lib/shared/utils/notification_permission_helper.dart @@ -0,0 +1,9 @@ +import 'package:permission_handler/permission_handler.dart'; + +/// Requests notification permission at runtime (Android 13+/API 33+). +Future requestNotificationPermissionIfNeeded() async { + final status = await Permission.notification.status; + if (status.isDenied || status.isRestricted) { + await Permission.notification.request(); + } +} diff --git a/lib/shared/utils/tray_manager.dart b/lib/shared/utils/tray_manager.dart new file mode 100644 index 00000000..b7151d8b --- /dev/null +++ b/lib/shared/utils/tray_manager.dart @@ -0,0 +1,70 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:system_tray/system_tray.dart'; + +class TrayManager { + static final TrayManager _instance = TrayManager._internal(); + factory TrayManager() => _instance; + + final SystemTray _tray = SystemTray(); + + TrayManager._internal(); + + Future init( + GlobalKey navigatorKey, { + String iconPath = 'assets/images/launcher-icon.png', + }) async { + try { + await _tray.initSystemTray( + iconPath: iconPath, + toolTip: "Mostro is running", + title: '', + ); + + final menu = Menu(); + + menu.buildFrom([ + MenuItemLabel( + label: 'Open Mostro', + onClicked: (menuItem) { + navigatorKey.currentState?.pushNamed('/'); + }, + ), + MenuItemLabel( + label: 'Quit', + onClicked: (menuItem) async { + await dispose(); + Future.delayed(const Duration(milliseconds: 300), () { + if (Platform.isAndroid || Platform.isIOS) { + SystemNavigator.pop(); + } else { + exit(0); // Only as a last resort on desktop + } + }); + }, + ), + ]); + + await _tray.setContextMenu(menu); + + // Handle tray icon click (e.g., double click = open) + _tray.registerSystemTrayEventHandler((eventName) { + if (eventName == kSystemTrayEventClick) { + navigatorKey.currentState?.pushNamed('/'); + } + }); + } catch (e) { + debugPrint('Failed to initialize system tray: $e'); + } + } + + Future dispose() async { + try { + await _tray.destroy(); + } catch (e) { + debugPrint('Failed to destroy system tray: $e'); + } + } +} diff --git a/lib/shared/widgets/mostro_reactive_button.dart b/lib/shared/widgets/mostro_reactive_button.dart new file mode 100644 index 00000000..5dd7a76d --- /dev/null +++ b/lib/shared/widgets/mostro_reactive_button.dart @@ -0,0 +1,149 @@ +import 'dart:async'; +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart' as actions; +import 'package:mostro_mobile/shared/providers/mostro_storage_provider.dart'; +import 'package:mostro_mobile/core/app_theme.dart'; + +enum ButtonStyleType { raised, outlined, text } + +/// A button specially designed for reactive operations that shows loading state +/// and handles the unique event-based nature of the mostro protocol. +class MostroReactiveButton extends ConsumerStatefulWidget { + final String label; + final ButtonStyleType buttonStyle; + final VoidCallback onPressed; + final String orderId; + final actions.Action action; + final Duration timeout; + + final bool showSuccessIndicator; + + final Color? backgroundColor; + + const MostroReactiveButton({ + super.key, + required this.label, + required this.buttonStyle, + required this.onPressed, + required this.orderId, + required this.action, + this.timeout = const Duration(seconds: 30), + this.showSuccessIndicator = false, + this.backgroundColor, + }); + + @override + ConsumerState createState() => + _MostroReactiveButtonState(); +} + +class _MostroReactiveButtonState extends ConsumerState { + bool _loading = false; + bool _showSuccess = false; + Timer? _timeoutTimer; + dynamic _lastSeenAction; + + @override + void dispose() { + _timeoutTimer?.cancel(); + super.dispose(); + } + + void _startOperation() { + setState(() { + _loading = true; + _showSuccess = false; + }); + widget.onPressed(); + _timeoutTimer?.cancel(); + _timeoutTimer = Timer(widget.timeout, _handleTimeout); + } + + void _handleTimeout() { + if (_loading) { + setState(() { + _loading = false; + _showSuccess = false; + }); + } + } + + @override + Widget build(BuildContext context) { + ref.listen>(mostroMessageStreamProvider(widget.orderId), + (prev, next) { + next.whenData((msg) { + if (msg == null || msg.action == _lastSeenAction) return; + _lastSeenAction = msg.action; + if (!_loading) return; + if (msg.action == actions.Action.cantDo || + msg.action == widget.action) { + setState(() { + _loading = false; + _showSuccess = + widget.showSuccessIndicator && msg.action == widget.action; + }); + } + }); + }); + + Widget childWidget; + if (_loading) { + childWidget = const SizedBox( + width: 24, + height: 24, + child: CircularProgressIndicator(strokeWidth: 2), + ); + } else if (_showSuccess) { + childWidget = const Icon(Icons.check_circle, color: Colors.green); + } else { + childWidget = Text(widget.label); + } + + Widget button; + switch (widget.buttonStyle) { + case ButtonStyleType.raised: + button = ElevatedButton( + onPressed: _loading ? null : _startOperation, + style: widget.backgroundColor != null + ? AppTheme.theme.elevatedButtonTheme.style?.copyWith( + backgroundColor: WidgetStateProperty.resolveWith( + (_) => widget.backgroundColor!, + ), + ) + : AppTheme.theme.elevatedButtonTheme.style, + child: childWidget, + ); + break; + case ButtonStyleType.outlined: + button = OutlinedButton( + onPressed: _loading ? null : _startOperation, + style: widget.backgroundColor != null + ? AppTheme.theme.outlinedButtonTheme.style?.copyWith( + backgroundColor: WidgetStateProperty.resolveWith( + (_) => widget.backgroundColor!, + ), + ) + : AppTheme.theme.outlinedButtonTheme.style, + child: childWidget, + ); + break; + case ButtonStyleType.text: + button = TextButton( + onPressed: _loading ? null : _startOperation, + style: widget.backgroundColor != null + ? AppTheme.theme.textButtonTheme.style?.copyWith( + backgroundColor: WidgetStateProperty.resolveWith( + (_) => widget.backgroundColor!, + ), + ) + : AppTheme.theme.textButtonTheme.style, + child: childWidget, + ); + break; + } + + return button; + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index d0e7f797..54a1276e 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -7,9 +7,13 @@ #include "generated_plugin_registrant.h" #include +#include void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) flutter_secure_storage_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FlutterSecureStorageLinuxPlugin"); flutter_secure_storage_linux_plugin_register_with_registrar(flutter_secure_storage_linux_registrar); + g_autoptr(FlPluginRegistrar) system_tray_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "SystemTrayPlugin"); + system_tray_plugin_register_with_registrar(system_tray_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index b29e9ba0..72b5a632 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -4,6 +4,7 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_linux + system_tray ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/Flutter-Debug.xcconfig b/macos/Flutter/Flutter-Debug.xcconfig index c2efd0b6..4b81f9b2 100644 --- a/macos/Flutter/Flutter-Debug.xcconfig +++ b/macos/Flutter/Flutter-Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/Flutter-Release.xcconfig b/macos/Flutter/Flutter-Release.xcconfig index c2efd0b6..5caa9d15 100644 --- a/macos/Flutter/Flutter-Release.xcconfig +++ b/macos/Flutter/Flutter-Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "ephemeral/Flutter-Generated.xcconfig" diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index ea00013e..30dc8f04 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,16 +5,18 @@ import FlutterMacOS import Foundation +import flutter_local_notifications import flutter_secure_storage_darwin import local_auth_darwin import path_provider_foundation import shared_preferences_foundation -import sqflite_darwin +import system_tray func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterLocalNotificationsPlugin.register(with: registry.registrar(forPlugin: "FlutterLocalNotificationsPlugin")) FlutterSecureStorageDarwinPlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStorageDarwinPlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) - SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + SystemTrayPlugin.register(with: registry.registrar(forPlugin: "SystemTrayPlugin")) } diff --git a/macos/Podfile b/macos/Podfile new file mode 100644 index 00000000..a46f7f23 --- /dev/null +++ b/macos/Podfile @@ -0,0 +1,42 @@ +platform :osx, '11.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_macos_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_macos_build_settings(target) + end +end diff --git a/macos/Runner.xcodeproj/project.pbxproj b/macos/Runner.xcodeproj/project.pbxproj index 206e9899..c5bc05ea 100644 --- a/macos/Runner.xcodeproj/project.pbxproj +++ b/macos/Runner.xcodeproj/project.pbxproj @@ -27,6 +27,8 @@ 33CC10F32044A3C60003C045 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F22044A3C60003C045 /* Assets.xcassets */; }; 33CC10F62044A3C60003C045 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 33CC10F42044A3C60003C045 /* MainMenu.xib */; }; 33CC11132044BFA00003C045 /* MainFlutterWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 33CC11122044BFA00003C045 /* MainFlutterWindow.swift */; }; + 4E01751B6286E29D961D56F7 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 442022593F44B487B7E0B505 /* Pods_Runner.framework */; }; + 551447E97D99C7A2C18B92A5 /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 95B1FDB795AD5DAF83CCA2A0 /* Pods_RunnerTests.framework */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -60,11 +62,14 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 1D476F71501BA37E89973936 /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; + 21952D613B510B4E199DA9B1 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; + 25A77DE7EB5393707DACD088 /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; 331C80D5294CF71000263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 331C80D7294CF71000263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; 333000ED22D3DE5D00554162 /* Warnings.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Warnings.xcconfig; sourceTree = ""; }; 335BBD1A22A9A15E00E9071D /* GeneratedPluginRegistrant.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = GeneratedPluginRegistrant.swift; sourceTree = ""; }; - 33CC10ED2044A3C60003C045 /* mostro_mobile.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "mostro_mobile.app"; sourceTree = BUILT_PRODUCTS_DIR; }; + 33CC10ED2044A3C60003C045 /* Mostro P2P.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "Mostro P2P.app"; sourceTree = BUILT_PRODUCTS_DIR; }; 33CC10F02044A3C60003C045 /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 33CC10F22044A3C60003C045 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Assets.xcassets; path = Runner/Assets.xcassets; sourceTree = ""; }; 33CC10F52044A3C60003C045 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/MainMenu.xib; sourceTree = ""; }; @@ -76,8 +81,13 @@ 33E51913231747F40026EE4D /* DebugProfile.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = DebugProfile.entitlements; sourceTree = ""; }; 33E51914231749380026EE4D /* Release.entitlements */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.entitlements; path = Release.entitlements; sourceTree = ""; }; 33E5194F232828860026EE4D /* AppInfo.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = AppInfo.xcconfig; sourceTree = ""; }; + 442022593F44B487B7E0B505 /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 48C70AF5E50E9F2A680D5F9C /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 7407E578B4E4227E6E0D791B /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Release.xcconfig; sourceTree = ""; }; + 95B1FDB795AD5DAF83CCA2A0 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; path = Debug.xcconfig; sourceTree = ""; }; + CFD8F3A93E2F4D5FCCA3A41C /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -85,6 +95,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 551447E97D99C7A2C18B92A5 /* Pods_RunnerTests.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -92,6 +103,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 4E01751B6286E29D961D56F7 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -125,13 +137,14 @@ 331C80D6294CF71000263BE5 /* RunnerTests */, 33CC10EE2044A3C60003C045 /* Products */, D73912EC22F37F3D000D13A0 /* Frameworks */, + B1527F813E7C064EC3243A6F /* Pods */, ); sourceTree = ""; }; 33CC10EE2044A3C60003C045 /* Products */ = { isa = PBXGroup; children = ( - 33CC10ED2044A3C60003C045 /* mostro_mobile.app */, + 33CC10ED2044A3C60003C045 /* Mostro P2P.app */, 331C80D5294CF71000263BE5 /* RunnerTests.xctest */, ); name = Products; @@ -172,9 +185,24 @@ path = Runner; sourceTree = ""; }; + B1527F813E7C064EC3243A6F /* Pods */ = { + isa = PBXGroup; + children = ( + 7407E578B4E4227E6E0D791B /* Pods-Runner.debug.xcconfig */, + CFD8F3A93E2F4D5FCCA3A41C /* Pods-Runner.release.xcconfig */, + 1D476F71501BA37E89973936 /* Pods-Runner.profile.xcconfig */, + 21952D613B510B4E199DA9B1 /* Pods-RunnerTests.debug.xcconfig */, + 25A77DE7EB5393707DACD088 /* Pods-RunnerTests.release.xcconfig */, + 48C70AF5E50E9F2A680D5F9C /* Pods-RunnerTests.profile.xcconfig */, + ); + path = Pods; + sourceTree = ""; + }; D73912EC22F37F3D000D13A0 /* Frameworks */ = { isa = PBXGroup; children = ( + 442022593F44B487B7E0B505 /* Pods_Runner.framework */, + 95B1FDB795AD5DAF83CCA2A0 /* Pods_RunnerTests.framework */, ); name = Frameworks; sourceTree = ""; @@ -186,6 +214,7 @@ isa = PBXNativeTarget; buildConfigurationList = 331C80DE294CF71000263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; buildPhases = ( + 1AEF54D95208AD587A1182D5 /* [CP] Check Pods Manifest.lock */, 331C80D1294CF70F00263BE5 /* Sources */, 331C80D2294CF70F00263BE5 /* Frameworks */, 331C80D3294CF70F00263BE5 /* Resources */, @@ -204,11 +233,13 @@ isa = PBXNativeTarget; buildConfigurationList = 33CC10FB2044A3C60003C045 /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + DC918E7C8C0DE61585524B37 /* [CP] Check Pods Manifest.lock */, 33CC10E92044A3C60003C045 /* Sources */, 33CC10EA2044A3C60003C045 /* Frameworks */, 33CC10EB2044A3C60003C045 /* Resources */, 33CC110E2044A8840003C045 /* Bundle Framework */, 3399D490228B24CF009A79C7 /* ShellScript */, + F51200762EC816BECF6382A7 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -217,7 +248,7 @@ ); name = Runner; productName = Runner; - productReference = 33CC10ED2044A3C60003C045 /* mostro_mobile.app */; + productReference = 33CC10ED2044A3C60003C045 /* Mostro P2P.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ @@ -291,6 +322,28 @@ /* End PBXResourcesBuildPhase section */ /* Begin PBXShellScriptBuildPhase section */ + 1AEF54D95208AD587A1182D5 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 3399D490228B24CF009A79C7 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; alwaysOutOfDate = 1; @@ -311,6 +364,7 @@ }; 33CC111E2044C6BF0003C045 /* ShellScript */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -327,7 +381,46 @@ ); runOnlyForDeploymentPostprocessing = 0; shellPath = /bin/sh; - shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire"; + shellScript = "\"$FLUTTER_ROOT\"/packages/flutter_tools/bin/macos_assemble.sh && touch Flutter/ephemeral/tripwire\n"; + }; + DC918E7C8C0DE61585524B37 /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F51200762EC816BECF6382A7 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; }; /* End PBXShellScriptBuildPhase section */ @@ -380,10 +473,12 @@ /* Begin XCBuildConfiguration section */ 331C80DB294CF71000263BE5 /* Debug */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 21952D613B510B4E199DA9B1 /* Pods-RunnerTests.debug.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.mostroMobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -394,10 +489,12 @@ }; 331C80DC294CF71000263BE5 /* Release */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 25A77DE7EB5393707DACD088 /* Pods-RunnerTests.release.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.mostroMobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -408,10 +505,12 @@ }; 331C80DD294CF71000263BE5 /* Profile */ = { isa = XCBuildConfiguration; + baseConfigurationReference = 48C70AF5E50E9F2A680D5F9C /* Pods-RunnerTests.profile.xcconfig */; buildSettings = { BUNDLE_LOADER = "$(TEST_HOST)"; CURRENT_PROJECT_VERSION = 1; GENERATE_INFOPLIST_FILE = YES; + MACOSX_DEPLOYMENT_TARGET = 11.0; MARKETING_VERSION = 1.0; PRODUCT_BUNDLE_IDENTIFIER = com.example.mostroMobile.RunnerTests; PRODUCT_NAME = "$(TARGET_NAME)"; @@ -476,13 +575,16 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 11.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -492,6 +594,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; + MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Profile; @@ -608,13 +711,16 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/DebugProfile.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 11.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; @@ -628,13 +734,16 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CODE_SIGN_ENTITLEMENTS = Runner/Release.entitlements; - CODE_SIGN_STYLE = Automatic; + CODE_SIGN_IDENTITY = ""; + CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; + CODE_SIGN_STYLE = Manual; COMBINE_HIDPI_IMAGES = YES; INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); + MACOSX_DEPLOYMENT_TARGET = 11.0; PROVISIONING_PROFILE_SPECIFIER = ""; SWIFT_VERSION = 5.0; }; @@ -644,6 +753,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Manual; + MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Debug; @@ -652,6 +762,7 @@ isa = XCBuildConfiguration; buildSettings = { CODE_SIGN_STYLE = Automatic; + MACOSX_DEPLOYMENT_TARGET = 11.0; PRODUCT_NAME = "$(TARGET_NAME)"; }; name = Release; diff --git a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 662c6707..c2f0ba2b 100644 --- a/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -15,7 +15,7 @@ @@ -31,7 +31,7 @@ @@ -59,13 +59,14 @@ ignoresPersistentStateOnLaunch = "NO" debugDocumentVersioning = "YES" debugServiceExtension = "internal" + enableGPUValidationMode = "1" allowLocationSimulation = "YES"> @@ -82,7 +83,7 @@ diff --git a/macos/Runner.xcworkspace/contents.xcworkspacedata b/macos/Runner.xcworkspace/contents.xcworkspacedata index 1d526a16..21a3cc14 100644 --- a/macos/Runner.xcworkspace/contents.xcworkspacedata +++ b/macos/Runner.xcworkspace/contents.xcworkspacedata @@ -4,4 +4,7 @@ + + diff --git a/macos/Runner/AppDelegate.swift b/macos/Runner/AppDelegate.swift index 8e02df28..b3c17614 100644 --- a/macos/Runner/AppDelegate.swift +++ b/macos/Runner/AppDelegate.swift @@ -6,4 +6,8 @@ class AppDelegate: FlutterAppDelegate { override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { return true } + + override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { + return true + } } diff --git a/pubspec.lock b/pubspec.lock index dd62c244..22061f77 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -5,34 +5,39 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: dc27559385e905ad30838356c5f5d574014ba39872d732111cd07ac0beff4c57 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "80.0.0" + version: "76.0.0" + _macros: + dependency: transitive + description: dart + source: sdk + version: "0.3.3" analyzer: dependency: transitive description: name: analyzer - sha256: "192d1c5b944e7e53b24b5586db760db934b177d4147c42fbca8c8c5f1eb8d11e" + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "7.3.0" + version: "6.11.0" analyzer_plugin: dependency: transitive description: name: analyzer_plugin - sha256: b3075265c5ab222f8b3188342dcb50b476286394a40323e85d1fa725035d40a4 + sha256: "9661b30b13a685efaee9f02e5d01ed9f2b423bd889d28a304d02d704aee69161" url: "https://pub.dev" source: hosted - version: "0.13.0" + version: "0.11.3" archive: dependency: transitive description: name: archive - sha256: "7dcbd0f87fe5f61cb28da39a1a8b70dbc106e2fe0516f7836eb7bb2948481a12" + sha256: "2fde1607386ab523f7a36bb3e7edb43bd58e6edaf2ffb29d8a6d578b297fdbbd" url: "https://pub.dev" source: hosted - version: "4.0.5" + version: "4.0.7" args: dependency: transitive description: @@ -157,10 +162,10 @@ packages: dependency: "direct dev" description: name: build_runner - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + sha256: "74691599a5bc750dc96a6b4bfd48f7d9d66453eab04c7f4063134800d6a5c573" url: "https://pub.dev" source: hosted - version: "2.4.15" + version: "2.4.14" build_runner_core: dependency: transitive description: @@ -209,6 +214,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.0" + cli_config: + dependency: transitive + description: + name: cli_config + sha256: ac20a183a07002b700f0c25e61b7ee46b23c309d76ab7b7640a028f18e4d99ec + url: "https://pub.dev" + source: hosted + version: "0.2.0" cli_util: dependency: transitive description: @@ -253,10 +266,10 @@ packages: dependency: transitive description: name: coverage - sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 + sha256: "802bd084fb82e55df091ec8ad1553a7331b61c08251eef19a508b6f3f3a9858d" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.13.1" crypto: dependency: "direct main" description: @@ -285,50 +298,34 @@ packages: dependency: transitive description: name: custom_lint_core - sha256: "31110af3dde9d29fb10828ca33f1dce24d2798477b167675543ce3d208dee8be" - url: "https://pub.dev" - source: hosted - version: "0.7.5" - custom_lint_visitor: - dependency: transitive - description: - name: custom_lint_visitor - sha256: "36282d85714af494ee2d7da8c8913630aa6694da99f104fb2ed4afcf8fc857d8" + sha256: "76a4046cc71d976222a078a8fd4a65e198b70545a8d690a75196dd14f08510f6" url: "https://pub.dev" source: hosted - version: "1.0.0+7.3.0" - dart_flutter_team_lints: - dependency: transitive - description: - name: dart_flutter_team_lints - sha256: "4c8f38142598339cd28c0b48a66b6b04434ee0499b6e40baf7c62c76daa1fcad" - url: "https://pub.dev" - source: hosted - version: "3.5.1" + version: "0.6.10" dart_nostr: dependency: "direct main" description: name: dart_nostr - sha256: "13ef2c7b45ad3bb52448098c7ef517ba8f70e563e17fba17d3c26e6104bdf0c1" + sha256: d7ffb5159be6ab174c8af4457d5112c925eb2c2294ce1251707ae680c90e15db url: "https://pub.dev" source: hosted - version: "9.1.0" + version: "9.1.1" dart_style: dependency: transitive description: name: dart_style - sha256: "27eb0ae77836989a3bc541ce55595e8ceee0992807f14511552a898ddd0d88ac" + sha256: "7306ab8a2359a48d22310ad823521d723acfed60ee1f7e37388e8986853b6820" url: "https://pub.dev" source: hosted - version: "3.0.1" - dev_build: + version: "2.3.8" + dbus: dependency: transitive description: - name: dev_build - sha256: "87abaa4f6cfd50320b229e29a7ef5532d86bc804f3124328c7e9db238ba45b33" + name: dbus + sha256: "79e0c23480ff85dc68de79e2cd6334add97e48f7f4865d17686dd6ea81a47e8c" url: "https://pub.dev" source: hosted - version: "1.1.2+8" + version: "0.7.11" dots_indicator: dependency: transitive description: @@ -390,6 +387,38 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_background_service: + dependency: "direct main" + description: + name: flutter_background_service + sha256: "70a1c185b1fa1a44f8f14ecd6c86f6e50366e3562f00b2fa5a54df39b3324d3d" + url: "https://pub.dev" + source: hosted + version: "5.1.0" + flutter_background_service_android: + dependency: transitive + description: + name: flutter_background_service_android + sha256: b73d903056240e23a5c56d9e52d3a5d02ae41cb18b2988a97304ae37b2bae4bf + url: "https://pub.dev" + source: hosted + version: "6.3.0" + flutter_background_service_ios: + dependency: transitive + description: + name: flutter_background_service_ios + sha256: "6037ffd45c4d019dab0975c7feb1d31012dd697e25edc05505a4a9b0c7dc9fba" + url: "https://pub.dev" + source: hosted + version: "5.0.3" + flutter_background_service_platform_interface: + dependency: transitive + description: + name: flutter_background_service_platform_interface + sha256: ca74aa95789a8304f4d3f57f07ba404faa86bed6e415f83e8edea6ad8b904a41 + url: "https://pub.dev" + source: hosted + version: "5.1.2" flutter_driver: dependency: transitive description: flutter @@ -475,6 +504,38 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_local_notifications: + dependency: "direct main" + description: + name: flutter_local_notifications + sha256: "33b3e0269ae9d51669957a923f2376bee96299b09915d856395af8c4238aebfa" + url: "https://pub.dev" + source: hosted + version: "19.1.0" + flutter_local_notifications_linux: + dependency: transitive + description: + name: flutter_local_notifications_linux + sha256: e3c277b2daab8e36ac5a6820536668d07e83851aeeb79c446e525a70710770a5 + url: "https://pub.dev" + source: hosted + version: "6.0.0" + flutter_local_notifications_platform_interface: + dependency: transitive + description: + name: flutter_local_notifications_platform_interface + sha256: "2569b973fc9d1f63a37410a9f7c1c552081226c597190cb359ef5d5762d1631c" + url: "https://pub.dev" + source: hosted + version: "9.0.0" + flutter_local_notifications_windows: + dependency: transitive + description: + name: flutter_local_notifications_windows + sha256: f8fc0652a601f83419d623c85723a3e82ad81f92b33eaa9bcc21ea1b94773e6e + url: "https://pub.dev" + source: hosted + version: "1.0.0" flutter_localizations: dependency: "direct main" description: flutter @@ -484,10 +545,10 @@ packages: dependency: transitive description: name: flutter_plugin_android_lifecycle - sha256: "5a1e6fb2c0561958d7e4c33574674bda7b77caaca7a33b758876956f2902eea3" + sha256: f948e346c12f8d5480d2825e03de228d0eb8c3a737e4cdaa122267b89c022b5e url: "https://pub.dev" source: hosted - version: "2.0.27" + version: "2.0.28" flutter_riverpod: dependency: "direct main" description: @@ -548,10 +609,10 @@ packages: dependency: transitive description: name: flutter_svg - sha256: c200fd79c918a40c5cd50ea0877fa13f81bdaf6f0a5d3dbcc2a13e3285d6aa1b + sha256: d44bf546b13025ec7353091516f6881f1d4c633993cb109c3916c3a0159dadf1 url: "https://pub.dev" source: hosted - version: "2.0.17" + version: "2.1.0" flutter_test: dependency: "direct dev" description: flutter @@ -566,10 +627,10 @@ packages: dependency: transitive description: name: freezed_annotation - sha256: c87ff004c8aa6af2d531668b46a4ea379f7191dc6dfa066acd53d506da6e044b + sha256: c2e2d632dd9b8a2b7751117abcfc2b4888ecfe181bd9fca7170d9ef02e595fe2 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.4.4" frontend_server_client: dependency: transitive description: @@ -595,10 +656,10 @@ packages: dependency: "direct main" description: name: go_router - sha256: f02fd7d2a4dc512fec615529824fdd217fecb3a3d3de68360293a551f21634b3 + sha256: "2b9ba6d4c247457c35a6622f1dee6aab6694a4e15237ff7c32320345044112b6" url: "https://pub.dev" source: hosted - version: "14.8.1" + version: "15.1.1" google_fonts: dependency: "direct main" description: @@ -776,10 +837,10 @@ packages: dependency: transitive description: name: local_auth_android - sha256: "0abe4e72f55c785b28900de52a2522c86baba0988838b5dc22241b072ecccd74" + sha256: "63ad7ca6396290626dc0cb34725a939e4cfe965d80d36112f08d49cf13a8136e" url: "https://pub.dev" source: hosted - version: "1.0.48" + version: "1.0.49" local_auth_darwin: dependency: transitive description: @@ -820,6 +881,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + macros: + dependency: transitive + description: + name: macros + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" + url: "https://pub.dev" + source: hosted + version: "0.1.3-main.0" matcher: dependency: transitive description: @@ -902,7 +971,7 @@ packages: source: hosted version: "1.1.0" path_provider: - dependency: transitive + dependency: "direct main" description: name: path_provider sha256: "50c5dd5b6e1aaf6fb3a78b33f6aa3afca52bf903a8a5298f53101fdaee55bbcd" @@ -913,10 +982,10 @@ packages: dependency: transitive description: name: path_provider_android - sha256: "0ca7359dad67fd7063cb2892ab0c0737b2daafd807cf1acecd62374c8fae6c12" + sha256: d0d310befe2c8ab9e7f393288ccbb11b60c019c6b5afc21973eeee4dda2b35e9 url: "https://pub.dev" source: hosted - version: "2.2.16" + version: "2.2.17" path_provider_foundation: dependency: transitive description: @@ -949,6 +1018,54 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.0" + permission_handler: + dependency: "direct main" + description: + name: permission_handler + sha256: "2d070d8684b68efb580a5997eb62f675e8a885ef0be6e754fb9ef489c177470f" + url: "https://pub.dev" + source: hosted + version: "12.0.0+1" + permission_handler_android: + dependency: transitive + description: + name: permission_handler_android + sha256: "1e3bc410ca1bf84662104b100eb126e066cb55791b7451307f9708d4007350e6" + url: "https://pub.dev" + source: hosted + version: "13.0.1" + permission_handler_apple: + dependency: transitive + description: + name: permission_handler_apple + sha256: f000131e755c54cf4d84a5d8bd6e4149e262cc31c5a8b1d698de1ac85fa41023 + url: "https://pub.dev" + source: hosted + version: "9.4.7" + permission_handler_html: + dependency: transitive + description: + name: permission_handler_html + sha256: "38f000e83355abb3392140f6bc3030660cfaef189e1f87824facb76300b4ff24" + url: "https://pub.dev" + source: hosted + version: "0.1.3+5" + permission_handler_platform_interface: + dependency: transitive + description: + name: permission_handler_platform_interface + sha256: eb99b295153abce5d683cac8c02e22faab63e50679b937fa1bf67d58bb282878 + url: "https://pub.dev" + source: hosted + version: "4.3.0" + permission_handler_windows: + dependency: transitive + description: + name: permission_handler_windows + sha256: "1a790728016f79a41216d88672dbc5df30e686e811ad4e698bfc51f76ad91f1e" + url: "https://pub.dev" + source: hosted + version: "0.2.1" petitparser: dependency: transitive description: @@ -993,10 +1110,10 @@ packages: dependency: transitive description: name: posix - sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + sha256: f0d7856b6ca1887cfa6d1d394056a296ae33489db914e365e2044fdada449e62 url: "https://pub.dev" source: hosted - version: "6.0.1" + version: "6.0.2" process: dependency: transitive description: @@ -1005,14 +1122,6 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.3" - process_run: - dependency: transitive - description: - name: process_run - sha256: "6ec839cdd3e6de4685318e7686cd4abb523c3d3a55af0e8d32a12ae19bc66622" - url: "https://pub.dev" - source: hosted - version: "1.2.4" pub_semver: dependency: transitive description: @@ -1057,10 +1166,10 @@ packages: dependency: transitive description: name: riverpod_analyzer_utils - sha256: "03a17170088c63aab6c54c44456f5ab78876a1ddb6032ffde1662ddab4959611" + sha256: "0dcb0af32d561f8fa000c6a6d95633c9fb08ea8a8df46e3f9daca59f11218167" url: "https://pub.dev" source: hosted - version: "0.5.10" + version: "0.5.6" riverpod_annotation: dependency: "direct main" description: @@ -1073,26 +1182,18 @@ packages: dependency: "direct dev" description: name: riverpod_generator - sha256: "44a0992d54473eb199ede00e2260bd3c262a86560e3c6f6374503d86d0580e36" + sha256: "851aedac7ad52693d12af3bf6d92b1626d516ed6b764eb61bf19e968b5e0b931" url: "https://pub.dev" source: hosted - version: "2.6.5" + version: "2.6.1" sembast: dependency: "direct main" description: name: sembast - sha256: "06b0274ca48ea92aedeab62303fc9ade3272684c842e9447be3e9b040f935559" - url: "https://pub.dev" - source: hosted - version: "3.8.4+1" - sembast_sqflite: - dependency: transitive - description: - name: sembast_sqflite - sha256: "38ffa967af36d6867d087ec82c37b70b55707b2cd533157e8a32d15aa66e40d2" + sha256: d3f0d0ba501a5f1fd7d6c8532ee01385977c8a069c334635dae390d059ae3d6d url: "https://pub.dev" source: hosted - version: "2.2.1" + version: "3.8.5" sembast_web: dependency: "direct main" description: @@ -1113,10 +1214,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "3ec7210872c4ba945e3244982918e502fa2bfb5230dff6832459ca0e1879b7ad" + sha256: "20cbd561f743a342c76c151d6ddb93a9ce6005751e7aa458baad3858bfbfb6ac" url: "https://pub.dev" source: hosted - version: "2.4.8" + version: "2.4.10" shared_preferences_foundation: dependency: transitive description: @@ -1185,10 +1286,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "3632775c8e90d6c9712f883e633716432a27758216dfb61bd86a8321c0580925" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "3.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -1198,10 +1299,10 @@ packages: dependency: transitive description: name: source_gen - sha256: "35c8150ece9e8c8d263337a265153c3329667640850b9304861faea59fc98f6b" + sha256: "14658ba5f669685cd3d63701d01b31ea748310f7ab854e471962670abcf57832" url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "1.5.0" source_map_stack_trace: dependency: transitive description: @@ -1226,78 +1327,6 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.1" - sprintf: - dependency: transitive - description: - name: sprintf - sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" - url: "https://pub.dev" - source: hosted - version: "7.0.0" - sqflite: - dependency: transitive - description: - name: sqflite - sha256: e2297b1da52f127bc7a3da11439985d9b536f75070f3325e62ada69a5c585d03 - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_android: - dependency: transitive - description: - name: sqflite_android - sha256: "2b3070c5fa881839f8b402ee4a39c1b4d561704d4ebbbcfb808a119bc2a1701b" - url: "https://pub.dev" - source: hosted - version: "2.4.1" - sqflite_common: - dependency: transitive - description: - name: sqflite_common - sha256: "84731e8bfd8303a3389903e01fb2141b6e59b5973cacbb0929021df08dddbe8b" - url: "https://pub.dev" - source: hosted - version: "2.5.5" - sqflite_common_ffi: - dependency: transitive - description: - name: sqflite_common_ffi - sha256: "1f3ef3888d3bfbb47785cc1dda0dc7dd7ebd8c1955d32a9e8e9dae1e38d1c4c1" - url: "https://pub.dev" - source: hosted - version: "2.3.5" - sqflite_common_ffi_web: - dependency: transitive - description: - name: sqflite_common_ffi_web - sha256: "983cf7b33b16e6bc086c8e09f6a1fae69d34cdb167d7acaf64cbd3515942d4e6" - url: "https://pub.dev" - source: hosted - version: "1.0.0" - sqflite_darwin: - dependency: transitive - description: - name: sqflite_darwin - sha256: "279832e5cde3fe99e8571879498c9211f3ca6391b0d818df4e17d9fff5c6ccb3" - url: "https://pub.dev" - source: hosted - version: "2.4.2" - sqflite_platform_interface: - dependency: transitive - description: - name: sqflite_platform_interface - sha256: "8dd4515c7bdcae0a785b0062859336de775e8c65db81ae33dd5445f35be61920" - url: "https://pub.dev" - source: hosted - version: "2.4.0" - sqlite3: - dependency: transitive - description: - name: sqlite3 - sha256: "310af39c40dd0bb2058538333c9d9840a2725ae0b9f77e4fd09ad6696aa8f66e" - url: "https://pub.dev" - source: hosted - version: "2.7.5" stack_trace: dependency: transitive description: @@ -1354,42 +1383,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.3.1" - tekartik_app_flutter_sembast: + system_tray: dependency: "direct main" description: - path: app_sembast - ref: dart3a - resolved-ref: "485d95cc933ccf373e7b4a53c19e8f43194c66c5" - url: "https://github.com/tekartik/app_flutter_utils.dart" - source: git - version: "0.5.1" - tekartik_app_flutter_sqflite: - dependency: transitive - description: - path: app_sqflite - ref: dart3a - resolved-ref: "485d95cc933ccf373e7b4a53c19e8f43194c66c5" - url: "https://github.com/tekartik/app_flutter_utils.dart" - source: git - version: "0.6.1" - tekartik_lints: - dependency: transitive - description: - path: "packages/lints" - ref: dart3a - resolved-ref: "6f9dded79246357567633739554ae58bde1bc9a3" - url: "https://github.com/tekartik/common.dart" - source: git - version: "0.4.2" - tekartik_lints_flutter: - dependency: transitive - description: - path: "packages/lints_flutter" - ref: dart3a - resolved-ref: "4d58d3cc559c5c92854fce73fc176c5951e78338" - url: "https://github.com/tekartik/common_flutter.dart" - source: git - version: "0.2.3" + name: system_tray + sha256: "40444e5de8ed907822a98694fd031b8accc3cb3c0baa547634ce76189cf3d9cf" + url: "https://pub.dev" + source: hosted + version: "2.0.3" term_glyph: dependency: transitive description: @@ -1430,6 +1431,14 @@ packages: url: "https://pub.dev" source: hosted version: "3.7.0" + timezone: + dependency: transitive + description: + name: timezone + sha256: dd14a3b83cfd7cb19e7888f1cbc20f258b8d71b54c06f79ac585f14093a287d1 + url: "https://pub.dev" + source: hosted + version: "0.10.1" timing: dependency: transitive description: @@ -1450,10 +1459,10 @@ packages: dependency: "direct main" description: name: uuid - sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + sha256: "648e103079f7c64a36dc7d39369cabb358d377078a051d6ae2ad3aa539519313" url: "https://pub.dev" source: hosted - version: "4.5.1" + version: "3.0.7" vector_graphics: dependency: transitive description: @@ -1486,14 +1495,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.4" - very_good_analysis: - dependency: transitive - description: - name: very_good_analysis - sha256: "62d2b86d183fb81b2edc22913d9f155d26eb5cf3855173adb1f59fac85035c63" - url: "https://pub.dev" - source: hosted - version: "7.0.0" vm_service: dependency: transitive description: @@ -1514,18 +1515,18 @@ packages: dependency: transitive description: name: web - sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + sha256: "97da13628db363c635202ad97068d47c5b8aa555808e7a9411963c533b449b27" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "0.5.1" web_socket_channel: dependency: transitive description: name: web_socket_channel - sha256: d88238e5eac9a42bb43ca4e721edba3c08c6354d4a53063afaa568516217621b + sha256: "58c6666b342a38816b2e7e50ed0f1e261959630becd4c879c4f26bfa14aa5a42" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.5" webdriver: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index e8c4f5bc..9b0cae53 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,9 +49,9 @@ dependencies: collection: ^1.18.0 elliptic: ^0.3.11 intl: ^0.19.0 - uuid: ^4.5.1 + uuid: ^3.0.7 flutter_secure_storage: ^10.0.0-beta.4 - go_router: ^14.6.2 + go_router: ^15.0.0 bip39: ^1.0.6 flutter_hooks: ^0.21.2 hooks_riverpod: ^2.6.1 @@ -73,12 +73,11 @@ dependencies: url: https://github.com/chebizarro/dart-nip44.git ref: master - tekartik_app_flutter_sembast: - git: - url: https://github.com/tekartik/app_flutter_utils.dart - ref: dart3a - path: app_sembast - version: '>=0.1.0' + flutter_local_notifications: ^19.0.0 + flutter_background_service: ^5.1.0 + system_tray: ^2.0.3 + path_provider: ^2.1.5 + permission_handler: ^12.0.0+1 dev_dependencies: flutter_test: diff --git a/test/mocks.mocks.dart b/test/mocks.mocks.dart index de227f6f..e67cf929 100644 --- a/test/mocks.mocks.dart +++ b/test/mocks.mocks.dart @@ -5,15 +5,13 @@ // ignore_for_file: no_leading_underscores_for_library_prefixes import 'dart:async' as _i5; -import 'package:dart_nostr/dart_nostr.dart' as _i10; +import 'package:dart_nostr/nostr/model/export.dart' as _i8; import 'package:flutter_riverpod/flutter_riverpod.dart' as _i2; import 'package:mockito/mockito.dart' as _i1; -import 'package:mostro_mobile/data/models/mostro_message.dart' as _i6; -import 'package:mostro_mobile/data/models/payload.dart' as _i7; -import 'package:mostro_mobile/data/models/session.dart' as _i3; +import 'package:mostro_mobile/data/models.dart' as _i3; import 'package:mostro_mobile/data/repositories/open_orders_repository.dart' - as _i9; -import 'package:mostro_mobile/features/settings/settings.dart' as _i8; + as _i7; +import 'package:mostro_mobile/features/settings/settings.dart' as _i6; import 'package:mostro_mobile/services/mostro_service.dart' as _i4; // ignore_for_file: type=lint @@ -32,13 +30,23 @@ import 'package:mostro_mobile/services/mostro_service.dart' as _i4; class _FakeRef_0 extends _i1.SmartFake implements _i2.Ref { - _FakeRef_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeRef_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeSession_1 extends _i1.SmartFake implements _i3.Session { - _FakeSession_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeSession_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } /// A class which mocks [MostroService]. @@ -50,41 +58,66 @@ class MockMostroService extends _i1.Mock implements _i4.MostroService { } @override - _i2.Ref get ref => - (super.noSuchMethod( - Invocation.getter(#ref), - returnValue: _FakeRef_0(this, Invocation.getter(#ref)), - ) - as _i2.Ref); + _i2.Ref get ref => (super.noSuchMethod( + Invocation.getter(#ref), + returnValue: _FakeRef_0( + this, + Invocation.getter(#ref), + ), + ) as _i2.Ref); + + @override + void init() => super.noSuchMethod( + Invocation.method( + #init, + [], + ), + returnValueForMissingStub: null, + ); @override void subscribe(_i3.Session? session) => super.noSuchMethod( - Invocation.method(#subscribe, [session]), - returnValueForMissingStub: null, - ); + Invocation.method( + #subscribe, + [session], + ), + returnValueForMissingStub: null, + ); @override _i3.Session? getSessionByOrderId(String? orderId) => - (super.noSuchMethod(Invocation.method(#getSessionByOrderId, [orderId])) - as _i3.Session?); + (super.noSuchMethod(Invocation.method( + #getSessionByOrderId, + [orderId], + )) as _i3.Session?); @override - _i5.Future submitOrder(_i6.MostroMessage<_i7.Payload>? order) => + _i5.Future submitOrder(_i3.MostroMessage<_i3.Payload>? order) => (super.noSuchMethod( - Invocation.method(#submitOrder, [order]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method( + #submitOrder, + [order], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future takeBuyOrder(String? orderId, int? amount) => + _i5.Future takeBuyOrder( + String? orderId, + int? amount, + ) => (super.noSuchMethod( - Invocation.method(#takeBuyOrder, [orderId, amount]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method( + #takeBuyOrder, + [ + orderId, + amount, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override _i5.Future takeSellOrder( @@ -93,154 +126,209 @@ class MockMostroService extends _i1.Mock implements _i4.MostroService { String? lnAddress, ) => (super.noSuchMethod( - Invocation.method(#takeSellOrder, [orderId, amount, lnAddress]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method( + #takeSellOrder, + [ + orderId, + amount, + lnAddress, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future sendInvoice(String? orderId, String? invoice, int? amount) => + _i5.Future sendInvoice( + String? orderId, + String? invoice, + int? amount, + ) => (super.noSuchMethod( - Invocation.method(#sendInvoice, [orderId, invoice, amount]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method( + #sendInvoice, + [ + orderId, + invoice, + amount, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future cancelOrder(String? orderId) => - (super.noSuchMethod( - Invocation.method(#cancelOrder, [orderId]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future cancelOrder(String? orderId) => (super.noSuchMethod( + Invocation.method( + #cancelOrder, + [orderId], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future sendFiatSent(String? orderId) => - (super.noSuchMethod( - Invocation.method(#sendFiatSent, [orderId]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future sendFiatSent(String? orderId) => (super.noSuchMethod( + Invocation.method( + #sendFiatSent, + [orderId], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future releaseOrder(String? orderId) => - (super.noSuchMethod( - Invocation.method(#releaseOrder, [orderId]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future releaseOrder(String? orderId) => (super.noSuchMethod( + Invocation.method( + #releaseOrder, + [orderId], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future disputeOrder(String? orderId) => - (super.noSuchMethod( - Invocation.method(#disputeOrder, [orderId]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future disputeOrder(String? orderId) => (super.noSuchMethod( + Invocation.method( + #disputeOrder, + [orderId], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future submitRating(String? orderId, int? rating) => + _i5.Future submitRating( + String? orderId, + int? rating, + ) => (super.noSuchMethod( - Invocation.method(#submitRating, [orderId, rating]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + Invocation.method( + #submitRating, + [ + orderId, + rating, + ], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future<_i3.Session> publishOrder(_i6.MostroMessage<_i7.Payload>? order) => + _i5.Future<_i3.Session> publishOrder(_i3.MostroMessage<_i3.Payload>? order) => (super.noSuchMethod( - Invocation.method(#publishOrder, [order]), - returnValue: _i5.Future<_i3.Session>.value( - _FakeSession_1(this, Invocation.method(#publishOrder, [order])), - ), - ) - as _i5.Future<_i3.Session>); - - @override - void updateSettings(_i8.Settings? settings) => super.noSuchMethod( - Invocation.method(#updateSettings, [settings]), - returnValueForMissingStub: null, - ); + Invocation.method( + #publishOrder, + [order], + ), + returnValue: _i5.Future<_i3.Session>.value(_FakeSession_1( + this, + Invocation.method( + #publishOrder, + [order], + ), + )), + ) as _i5.Future<_i3.Session>); + + @override + void updateSettings(_i6.Settings? settings) => super.noSuchMethod( + Invocation.method( + #updateSettings, + [settings], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [OpenOrdersRepository]. /// /// See the documentation for Mockito's code generation for more information. class MockOpenOrdersRepository extends _i1.Mock - implements _i9.OpenOrdersRepository { + implements _i7.OpenOrdersRepository { MockOpenOrdersRepository() { _i1.throwOnMissingStub(this); } @override - _i5.Stream> get eventsStream => - (super.noSuchMethod( - Invocation.getter(#eventsStream), - returnValue: _i5.Stream>.empty(), - ) - as _i5.Stream>); + _i5.Stream> get eventsStream => (super.noSuchMethod( + Invocation.getter(#eventsStream), + returnValue: _i5.Stream>.empty(), + ) as _i5.Stream>); @override void dispose() => super.noSuchMethod( - Invocation.method(#dispose, []), - returnValueForMissingStub: null, - ); + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); @override - _i5.Future<_i10.NostrEvent?> getOrderById(String? orderId) => + _i5.Future<_i8.NostrEvent?> getOrderById(String? orderId) => (super.noSuchMethod( - Invocation.method(#getOrderById, [orderId]), - returnValue: _i5.Future<_i10.NostrEvent?>.value(), - ) - as _i5.Future<_i10.NostrEvent?>); + Invocation.method( + #getOrderById, + [orderId], + ), + returnValue: _i5.Future<_i8.NostrEvent?>.value(), + ) as _i5.Future<_i8.NostrEvent?>); @override - _i5.Future addOrder(_i10.NostrEvent? order) => - (super.noSuchMethod( - Invocation.method(#addOrder, [order]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future addOrder(_i8.NostrEvent? order) => (super.noSuchMethod( + Invocation.method( + #addOrder, + [order], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future deleteOrder(String? orderId) => - (super.noSuchMethod( - Invocation.method(#deleteOrder, [orderId]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future deleteOrder(String? orderId) => (super.noSuchMethod( + Invocation.method( + #deleteOrder, + [orderId], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); @override - _i5.Future> getAllOrders() => - (super.noSuchMethod( - Invocation.method(#getAllOrders, []), - returnValue: _i5.Future>.value( - <_i10.NostrEvent>[], - ), - ) - as _i5.Future>); + _i5.Future> getAllOrders() => (super.noSuchMethod( + Invocation.method( + #getAllOrders, + [], + ), + returnValue: _i5.Future>.value(<_i8.NostrEvent>[]), + ) as _i5.Future>); @override - _i5.Future updateOrder(_i10.NostrEvent? order) => - (super.noSuchMethod( - Invocation.method(#updateOrder, [order]), - returnValue: _i5.Future.value(), - returnValueForMissingStub: _i5.Future.value(), - ) - as _i5.Future); + _i5.Future updateOrder(_i8.NostrEvent? order) => (super.noSuchMethod( + Invocation.method( + #updateOrder, + [order], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); + + @override + void updateSettings(_i6.Settings? settings) => super.noSuchMethod( + Invocation.method( + #updateSettings, + [settings], + ), + returnValueForMissingStub: null, + ); @override - void updateSettings(_i8.Settings? settings) => super.noSuchMethod( - Invocation.method(#updateSettings, [settings]), - returnValueForMissingStub: null, - ); + _i5.Future reloadData() => (super.noSuchMethod( + Invocation.method( + #reloadData, + [], + ), + returnValue: _i5.Future.value(), + returnValueForMissingStub: _i5.Future.value(), + ) as _i5.Future); } diff --git a/test/services/mostro_service_test.mocks.dart b/test/services/mostro_service_test.mocks.dart index 7749e35b..dc80e4cf 100644 --- a/test/services/mostro_service_test.mocks.dart +++ b/test/services/mostro_service_test.mocks.dart @@ -36,41 +36,85 @@ import 'package:state_notifier/state_notifier.dart' as _i15; // ignore_for_file: subtype_of_sealed_class class _FakeSettings_0 extends _i1.SmartFake implements _i2.Settings { - _FakeSettings_0(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeSettings_0( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeNostrKeyPairs_1 extends _i1.SmartFake implements _i3.NostrKeyPairs { - _FakeNostrKeyPairs_1(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeNostrKeyPairs_1( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeNostrEvent_2 extends _i1.SmartFake implements _i3.NostrEvent { - _FakeNostrEvent_2(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeNostrEvent_2( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeSession_3 extends _i1.SmartFake implements _i4.Session { - _FakeSession_3(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeSession_3( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeDatabase_4 extends _i1.SmartFake implements _i5.Database { - _FakeDatabase_4(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeDatabase_4( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeStoreRef_5 - extends _i1.SmartFake - implements _i5.StoreRef { - _FakeStoreRef_5(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + extends _i1.SmartFake implements _i5.StoreRef { + _FakeStoreRef_5( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } class _FakeMostroMessage_6 extends _i1.SmartFake implements _i7.MostroMessage { - _FakeMostroMessage_6(Object parent, Invocation parentInvocation) - : super(parent, parentInvocation); + _FakeMostroMessage_6( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); +} + +class _FakeFilter_7 extends _i1.SmartFake implements _i5.Filter { + _FakeFilter_7( + Object parent, + Invocation parentInvocation, + ) : super( + parent, + parentInvocation, + ); } /// A class which mocks [NostrService]. @@ -82,120 +126,145 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { } @override - _i2.Settings get settings => - (super.noSuchMethod( - Invocation.getter(#settings), - returnValue: _FakeSettings_0(this, Invocation.getter(#settings)), - ) - as _i2.Settings); + _i2.Settings get settings => (super.noSuchMethod( + Invocation.getter(#settings), + returnValue: _FakeSettings_0( + this, + Invocation.getter(#settings), + ), + ) as _i2.Settings); @override set settings(_i2.Settings? _settings) => super.noSuchMethod( - Invocation.setter(#settings, _settings), - returnValueForMissingStub: null, - ); + Invocation.setter( + #settings, + _settings, + ), + returnValueForMissingStub: null, + ); @override - bool get isInitialized => - (super.noSuchMethod(Invocation.getter(#isInitialized), returnValue: false) - as bool); + bool get isInitialized => (super.noSuchMethod( + Invocation.getter(#isInitialized), + returnValue: false, + ) as bool); @override - _i9.Future init() => - (super.noSuchMethod( - Invocation.method(#init, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future init(_i2.Settings? settings) => (super.noSuchMethod( + Invocation.method( + #init, + [settings], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i9.Future updateSettings(_i2.Settings? newSettings) => (super.noSuchMethod( - Invocation.method(#updateSettings, [newSettings]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + Invocation.method( + #updateSettings, + [newSettings], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i9.Future<_i10.RelayInformations?> getRelayInfo(String? relayUrl) => (super.noSuchMethod( - Invocation.method(#getRelayInfo, [relayUrl]), - returnValue: _i9.Future<_i10.RelayInformations?>.value(), - ) - as _i9.Future<_i10.RelayInformations?>); + Invocation.method( + #getRelayInfo, + [relayUrl], + ), + returnValue: _i9.Future<_i10.RelayInformations?>.value(), + ) as _i9.Future<_i10.RelayInformations?>); @override - _i9.Future publishEvent(_i3.NostrEvent? event) => - (super.noSuchMethod( - Invocation.method(#publishEvent, [event]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future publishEvent(_i3.NostrEvent? event) => (super.noSuchMethod( + Invocation.method( + #publishEvent, + [event], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i9.Future> fecthEvents(_i3.NostrFilter? filter) => (super.noSuchMethod( - Invocation.method(#fecthEvents, [filter]), - returnValue: _i9.Future>.value( - <_i3.NostrEvent>[], - ), - ) - as _i9.Future>); - - @override - _i9.Stream<_i3.NostrEvent> subscribeToEvents(_i3.NostrFilter? filter) => - (super.noSuchMethod( - Invocation.method(#subscribeToEvents, [filter]), - returnValue: _i9.Stream<_i3.NostrEvent>.empty(), - ) - as _i9.Stream<_i3.NostrEvent>); - - @override - _i9.Future disconnectFromRelays() => - (super.noSuchMethod( - Invocation.method(#disconnectFromRelays, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); - - @override - _i9.Future<_i3.NostrKeyPairs> generateKeyPair() => - (super.noSuchMethod( - Invocation.method(#generateKeyPair, []), - returnValue: _i9.Future<_i3.NostrKeyPairs>.value( - _FakeNostrKeyPairs_1( - this, - Invocation.method(#generateKeyPair, []), - ), - ), - ) - as _i9.Future<_i3.NostrKeyPairs>); + Invocation.method( + #fecthEvents, + [filter], + ), + returnValue: _i9.Future>.value(<_i3.NostrEvent>[]), + ) as _i9.Future>); + + @override + _i9.Stream<_i3.NostrEvent> subscribeToEvents(_i3.NostrRequest? request) => + (super.noSuchMethod( + Invocation.method( + #subscribeToEvents, + [request], + ), + returnValue: _i9.Stream<_i3.NostrEvent>.empty(), + ) as _i9.Stream<_i3.NostrEvent>); + + @override + _i9.Future disconnectFromRelays() => (super.noSuchMethod( + Invocation.method( + #disconnectFromRelays, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future<_i3.NostrKeyPairs> generateKeyPair() => (super.noSuchMethod( + Invocation.method( + #generateKeyPair, + [], + ), + returnValue: _i9.Future<_i3.NostrKeyPairs>.value(_FakeNostrKeyPairs_1( + this, + Invocation.method( + #generateKeyPair, + [], + ), + )), + ) as _i9.Future<_i3.NostrKeyPairs>); @override _i3.NostrKeyPairs generateKeyPairFromPrivateKey(String? privateKey) => (super.noSuchMethod( - Invocation.method(#generateKeyPairFromPrivateKey, [privateKey]), - returnValue: _FakeNostrKeyPairs_1( - this, - Invocation.method(#generateKeyPairFromPrivateKey, [privateKey]), - ), - ) - as _i3.NostrKeyPairs); - - @override - String getMostroPubKey() => - (super.noSuchMethod( - Invocation.method(#getMostroPubKey, []), - returnValue: _i11.dummyValue( - this, - Invocation.method(#getMostroPubKey, []), - ), - ) - as String); + Invocation.method( + #generateKeyPairFromPrivateKey, + [privateKey], + ), + returnValue: _FakeNostrKeyPairs_1( + this, + Invocation.method( + #generateKeyPairFromPrivateKey, + [privateKey], + ), + ), + ) as _i3.NostrKeyPairs); + + @override + String getMostroPubKey() => (super.noSuchMethod( + Invocation.method( + #getMostroPubKey, + [], + ), + returnValue: _i11.dummyValue( + this, + Invocation.method( + #getMostroPubKey, + [], + ), + ), + ) as String); @override _i9.Future<_i3.NostrEvent> createNIP59Event( @@ -204,23 +273,26 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { String? senderPrivateKey, ) => (super.noSuchMethod( - Invocation.method(#createNIP59Event, [ + Invocation.method( + #createNIP59Event, + [ + content, + recipientPubKey, + senderPrivateKey, + ], + ), + returnValue: _i9.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( + this, + Invocation.method( + #createNIP59Event, + [ content, recipientPubKey, senderPrivateKey, - ]), - returnValue: _i9.Future<_i3.NostrEvent>.value( - _FakeNostrEvent_2( - this, - Invocation.method(#createNIP59Event, [ - content, - recipientPubKey, - senderPrivateKey, - ]), - ), - ), - ) - as _i9.Future<_i3.NostrEvent>); + ], + ), + )), + ) as _i9.Future<_i3.NostrEvent>); @override _i9.Future<_i3.NostrEvent> decryptNIP59Event( @@ -228,15 +300,24 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { String? privateKey, ) => (super.noSuchMethod( - Invocation.method(#decryptNIP59Event, [event, privateKey]), - returnValue: _i9.Future<_i3.NostrEvent>.value( - _FakeNostrEvent_2( - this, - Invocation.method(#decryptNIP59Event, [event, privateKey]), - ), - ), - ) - as _i9.Future<_i3.NostrEvent>); + Invocation.method( + #decryptNIP59Event, + [ + event, + privateKey, + ], + ), + returnValue: _i9.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( + this, + Invocation.method( + #decryptNIP59Event, + [ + event, + privateKey, + ], + ), + )), + ) as _i9.Future<_i3.NostrEvent>); @override _i9.Future createRumor( @@ -246,25 +327,28 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { String? content, ) => (super.noSuchMethod( - Invocation.method(#createRumor, [ + Invocation.method( + #createRumor, + [ + senderKeyPair, + wrapperKey, + recipientPubKey, + content, + ], + ), + returnValue: _i9.Future.value(_i11.dummyValue( + this, + Invocation.method( + #createRumor, + [ senderKeyPair, wrapperKey, recipientPubKey, content, - ]), - returnValue: _i9.Future.value( - _i11.dummyValue( - this, - Invocation.method(#createRumor, [ - senderKeyPair, - wrapperKey, - recipientPubKey, - content, - ]), - ), - ), - ) - as _i9.Future); + ], + ), + )), + ) as _i9.Future); @override _i9.Future createSeal( @@ -274,25 +358,28 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { String? encryptedContent, ) => (super.noSuchMethod( - Invocation.method(#createSeal, [ + Invocation.method( + #createSeal, + [ + senderKeyPair, + wrapperKey, + recipientPubKey, + encryptedContent, + ], + ), + returnValue: _i9.Future.value(_i11.dummyValue( + this, + Invocation.method( + #createSeal, + [ senderKeyPair, wrapperKey, recipientPubKey, encryptedContent, - ]), - returnValue: _i9.Future.value( - _i11.dummyValue( - this, - Invocation.method(#createSeal, [ - senderKeyPair, - wrapperKey, - recipientPubKey, - encryptedContent, - ]), - ), - ), - ) - as _i9.Future); + ], + ), + )), + ) as _i9.Future); @override _i9.Future<_i3.NostrEvent> createWrap( @@ -301,23 +388,35 @@ class MockNostrService extends _i1.Mock implements _i8.NostrService { String? recipientPubKey, ) => (super.noSuchMethod( - Invocation.method(#createWrap, [ + Invocation.method( + #createWrap, + [ + wrapperKeyPair, + sealedContent, + recipientPubKey, + ], + ), + returnValue: _i9.Future<_i3.NostrEvent>.value(_FakeNostrEvent_2( + this, + Invocation.method( + #createWrap, + [ wrapperKeyPair, sealedContent, recipientPubKey, - ]), - returnValue: _i9.Future<_i3.NostrEvent>.value( - _FakeNostrEvent_2( - this, - Invocation.method(#createWrap, [ - wrapperKeyPair, - sealedContent, - recipientPubKey, - ]), - ), - ), - ) - as _i9.Future<_i3.NostrEvent>); + ], + ), + )), + ) as _i9.Future<_i3.NostrEvent>); + + @override + void unsubscribe(String? id) => super.noSuchMethod( + Invocation.method( + #unsubscribe, + [id], + ), + returnValueForMissingStub: null, + ); } /// A class which mocks [SessionNotifier]. @@ -329,155 +428,182 @@ class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { } @override - List<_i4.Session> get sessions => - (super.noSuchMethod( - Invocation.getter(#sessions), - returnValue: <_i4.Session>[], - ) - as List<_i4.Session>); + List<_i4.Session> get sessions => (super.noSuchMethod( + Invocation.getter(#sessions), + returnValue: <_i4.Session>[], + ) as List<_i4.Session>); @override set onError(_i13.ErrorListener? _onError) => super.noSuchMethod( - Invocation.setter(#onError, _onError), - returnValueForMissingStub: null, - ); + Invocation.setter( + #onError, + _onError, + ), + returnValueForMissingStub: null, + ); @override - bool get mounted => - (super.noSuchMethod(Invocation.getter(#mounted), returnValue: false) - as bool); + bool get mounted => (super.noSuchMethod( + Invocation.getter(#mounted), + returnValue: false, + ) as bool); @override - _i9.Stream> get stream => - (super.noSuchMethod( - Invocation.getter(#stream), - returnValue: _i9.Stream>.empty(), - ) - as _i9.Stream>); + _i9.Stream> get stream => (super.noSuchMethod( + Invocation.getter(#stream), + returnValue: _i9.Stream>.empty(), + ) as _i9.Stream>); @override - List<_i4.Session> get state => - (super.noSuchMethod( - Invocation.getter(#state), - returnValue: <_i4.Session>[], - ) - as List<_i4.Session>); + List<_i4.Session> get state => (super.noSuchMethod( + Invocation.getter(#state), + returnValue: <_i4.Session>[], + ) as List<_i4.Session>); @override set state(List<_i4.Session>? value) => super.noSuchMethod( - Invocation.setter(#state, value), - returnValueForMissingStub: null, - ); + Invocation.setter( + #state, + value, + ), + returnValueForMissingStub: null, + ); @override - List<_i4.Session> get debugState => - (super.noSuchMethod( - Invocation.getter(#debugState), - returnValue: <_i4.Session>[], - ) - as List<_i4.Session>); + List<_i4.Session> get debugState => (super.noSuchMethod( + Invocation.getter(#debugState), + returnValue: <_i4.Session>[], + ) as List<_i4.Session>); @override - bool get hasListeners => - (super.noSuchMethod(Invocation.getter(#hasListeners), returnValue: false) - as bool); + bool get hasListeners => (super.noSuchMethod( + Invocation.getter(#hasListeners), + returnValue: false, + ) as bool); @override - _i9.Future init() => - (super.noSuchMethod( - Invocation.method(#init, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future init() => (super.noSuchMethod( + Invocation.method( + #init, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override void updateSettings(_i2.Settings? settings) => super.noSuchMethod( - Invocation.method(#updateSettings, [settings]), - returnValueForMissingStub: null, - ); + Invocation.method( + #updateSettings, + [settings], + ), + returnValueForMissingStub: null, + ); @override - _i9.Future<_i4.Session> newSession({String? orderId, _i14.Role? role}) => + _i9.Future<_i4.Session> newSession({ + String? orderId, + _i14.Role? role, + }) => (super.noSuchMethod( - Invocation.method(#newSession, [], { + Invocation.method( + #newSession, + [], + { + #orderId: orderId, + #role: role, + }, + ), + returnValue: _i9.Future<_i4.Session>.value(_FakeSession_3( + this, + Invocation.method( + #newSession, + [], + { #orderId: orderId, #role: role, - }), - returnValue: _i9.Future<_i4.Session>.value( - _FakeSession_3( - this, - Invocation.method(#newSession, [], { - #orderId: orderId, - #role: role, - }), - ), - ), - ) - as _i9.Future<_i4.Session>); + }, + ), + )), + ) as _i9.Future<_i4.Session>); @override - _i9.Future saveSession(_i4.Session? session) => - (super.noSuchMethod( - Invocation.method(#saveSession, [session]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future saveSession(_i4.Session? session) => (super.noSuchMethod( + Invocation.method( + #saveSession, + [session], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i4.Session? getSessionByOrderId(String? orderId) => - (super.noSuchMethod(Invocation.method(#getSessionByOrderId, [orderId])) - as _i4.Session?); + (super.noSuchMethod(Invocation.method( + #getSessionByOrderId, + [orderId], + )) as _i4.Session?); @override - _i9.Future<_i4.Session?> loadSession(int? keyIndex) => - (super.noSuchMethod( - Invocation.method(#loadSession, [keyIndex]), - returnValue: _i9.Future<_i4.Session?>.value(), - ) - as _i9.Future<_i4.Session?>); + _i4.Session? getSessionByTradeKey(String? tradeKey) => + (super.noSuchMethod(Invocation.method( + #getSessionByTradeKey, + [tradeKey], + )) as _i4.Session?); @override - _i9.Future reset() => - (super.noSuchMethod( - Invocation.method(#reset, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future<_i4.Session?> loadSession(int? keyIndex) => (super.noSuchMethod( + Invocation.method( + #loadSession, + [keyIndex], + ), + returnValue: _i9.Future<_i4.Session?>.value(), + ) as _i9.Future<_i4.Session?>); @override - _i9.Future deleteSession(String? sessionId) => - (super.noSuchMethod( - Invocation.method(#deleteSession, [sessionId]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future reset() => (super.noSuchMethod( + Invocation.method( + #reset, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i9.Future clearExpiredSessions() => - (super.noSuchMethod( - Invocation.method(#clearExpiredSessions, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future deleteSession(String? sessionId) => (super.noSuchMethod( + Invocation.method( + #deleteSession, + [sessionId], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override void dispose() => super.noSuchMethod( - Invocation.method(#dispose, []), - returnValueForMissingStub: null, - ); + Invocation.method( + #dispose, + [], + ), + returnValueForMissingStub: null, + ); @override - bool updateShouldNotify(List<_i4.Session>? old, List<_i4.Session>? current) => + bool updateShouldNotify( + List<_i4.Session>? old, + List<_i4.Session>? current, + ) => (super.noSuchMethod( - Invocation.method(#updateShouldNotify, [old, current]), - returnValue: false, - ) - as bool); + Invocation.method( + #updateShouldNotify, + [ + old, + current, + ], + ), + returnValue: false, + ) as bool); @override _i13.RemoveListener addListener( @@ -485,14 +611,13 @@ class MockSessionNotifier extends _i1.Mock implements _i12.SessionNotifier { bool? fireImmediately = true, }) => (super.noSuchMethod( - Invocation.method( - #addListener, - [listener], - {#fireImmediately: fireImmediately}, - ), - returnValue: () {}, - ) - as _i13.RemoveListener); + Invocation.method( + #addListener, + [listener], + {#fireImmediately: fireImmediately}, + ), + returnValue: () {}, + ) as _i13.RemoveListener); } /// A class which mocks [MostroStorage]. @@ -504,112 +629,116 @@ class MockMostroStorage extends _i1.Mock implements _i16.MostroStorage { } @override - _i5.Database get db => - (super.noSuchMethod( - Invocation.getter(#db), - returnValue: _FakeDatabase_4(this, Invocation.getter(#db)), - ) - as _i5.Database); - - @override - _i5.StoreRef> get store => - (super.noSuchMethod( - Invocation.getter(#store), - returnValue: _FakeStoreRef_5>( - this, - Invocation.getter(#store), - ), - ) - as _i5.StoreRef>); + _i5.Database get db => (super.noSuchMethod( + Invocation.getter(#db), + returnValue: _FakeDatabase_4( + this, + Invocation.getter(#db), + ), + ) as _i5.Database); @override - _i9.Future addMessage(_i7.MostroMessage<_i6.Payload>? message) => - (super.noSuchMethod( - Invocation.method(#addMessage, [message]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i5.StoreRef> get store => (super.noSuchMethod( + Invocation.getter(#store), + returnValue: _FakeStoreRef_5>( + this, + Invocation.getter(#store), + ), + ) as _i5.StoreRef>); @override - _i9.Future addMessages( - List<_i7.MostroMessage<_i6.Payload>>? messages, + _i9.Future addMessage( + String? key, + _i7.MostroMessage<_i6.Payload>? message, ) => (super.noSuchMethod( - Invocation.method(#addMessages, [messages]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + Invocation.method( + #addMessage, + [ + key, + message, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override _i9.Future<_i7.MostroMessage<_i6.Payload>?> - getMessageById(String? orderId) => - (super.noSuchMethod( - Invocation.method(#getMessageById, [orderId]), + getMessageById(String? orderId) => + (super.noSuchMethod( + Invocation.method( + #getMessageById, + [orderId], + ), returnValue: _i9.Future<_i7.MostroMessage<_i6.Payload>?>.value(), - ) - as _i9.Future<_i7.MostroMessage<_i6.Payload>?>); + ) as _i9.Future<_i7.MostroMessage<_i6.Payload>?>); @override _i9.Future>> getAllMessages() => (super.noSuchMethod( - Invocation.method(#getAllMessages, []), - returnValue: _i9.Future>>.value( - <_i7.MostroMessage<_i6.Payload>>[], - ), - ) - as _i9.Future>>); + Invocation.method( + #getAllMessages, + [], + ), + returnValue: _i9.Future>>.value( + <_i7.MostroMessage<_i6.Payload>>[]), + ) as _i9.Future>>); @override - _i9.Future deleteMessage(String? orderId) => - (super.noSuchMethod( - Invocation.method(#deleteMessage, [orderId]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future deleteAllMessages() => (super.noSuchMethod( + Invocation.method( + #deleteAllMessages, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i9.Future deleteAllMessages() => + _i9.Future deleteAllMessagesByOrderId(String? orderId) => (super.noSuchMethod( - Invocation.method(#deleteAllMessages, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + Invocation.method( + #deleteAllMessagesByOrderId, + [orderId], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i9.Future deleteAllMessagesById(String? orderId) => - (super.noSuchMethod( - Invocation.method(#deleteAllMessagesById, [orderId]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + _i9.Future>> + getMessagesOfType() => (super.noSuchMethod( + Invocation.method( + #getMessagesOfType, + [], + ), + returnValue: _i9.Future>>.value( + <_i7.MostroMessage<_i6.Payload>>[]), + ) as _i9.Future>>); @override - _i9.Future>> - getMessagesOfType() => - (super.noSuchMethod( - Invocation.method(#getMessagesOfType, []), - returnValue: _i9.Future>>.value( - <_i7.MostroMessage>[], + _i9.Future<_i7.MostroMessage<_i6.Payload>?> + getLatestMessageOfTypeById(String? orderId) => + (super.noSuchMethod( + Invocation.method( + #getLatestMessageOfTypeById, + [orderId], ), - ) - as _i9.Future>>); + returnValue: _i9.Future<_i7.MostroMessage<_i6.Payload>?>.value(), + ) as _i9.Future<_i7.MostroMessage<_i6.Payload>?>); @override _i9.Future>> getMessagesForId( - String? orderId, - ) => + String? orderId) => (super.noSuchMethod( - Invocation.method(#getMessagesForId, [orderId]), - returnValue: _i9.Future>>.value( - <_i7.MostroMessage<_i6.Payload>>[], - ), - ) - as _i9.Future>>); + Invocation.method( + #getMessagesForId, + [orderId], + ), + returnValue: _i9.Future>>.value( + <_i7.MostroMessage<_i6.Payload>>[]), + ) as _i9.Future>>); @override _i7.MostroMessage<_i6.Payload> fromDbMap( @@ -617,104 +746,258 @@ class MockMostroStorage extends _i1.Mock implements _i16.MostroStorage { Map? jsonMap, ) => (super.noSuchMethod( - Invocation.method(#fromDbMap, [key, jsonMap]), - returnValue: _FakeMostroMessage_6<_i6.Payload>( - this, - Invocation.method(#fromDbMap, [key, jsonMap]), - ), - ) - as _i7.MostroMessage<_i6.Payload>); + Invocation.method( + #fromDbMap, + [ + key, + jsonMap, + ], + ), + returnValue: _FakeMostroMessage_6<_i6.Payload>( + this, + Invocation.method( + #fromDbMap, + [ + key, + jsonMap, + ], + ), + ), + ) as _i7.MostroMessage<_i6.Payload>); @override Map toDbMap(_i7.MostroMessage<_i6.Payload>? item) => (super.noSuchMethod( - Invocation.method(#toDbMap, [item]), - returnValue: {}, - ) - as Map); + Invocation.method( + #toDbMap, + [item], + ), + returnValue: {}, + ) as Map); + + @override + _i9.Future hasMessageByKey(String? key) => (super.noSuchMethod( + Invocation.method( + #hasMessageByKey, + [key], + ), + returnValue: _i9.Future.value(false), + ) as _i9.Future); @override - String messageKey(_i7.MostroMessage<_i6.Payload>? msg) => + _i9.Future<_i7.MostroMessage<_i6.Payload>?> getLatestMessageById( + String? orderId) => (super.noSuchMethod( - Invocation.method(#messageKey, [msg]), - returnValue: _i11.dummyValue( - this, - Invocation.method(#messageKey, [msg]), - ), - ) - as String); + Invocation.method( + #getLatestMessageById, + [orderId], + ), + returnValue: _i9.Future<_i7.MostroMessage<_i6.Payload>?>.value(), + ) as _i9.Future<_i7.MostroMessage<_i6.Payload>?>); @override - _i9.Future putItem(String? id, _i7.MostroMessage<_i6.Payload>? item) => + _i9.Stream<_i7.MostroMessage<_i6.Payload>?> watchLatestMessage( + String? orderId) => (super.noSuchMethod( - Invocation.method(#putItem, [id, item]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + Invocation.method( + #watchLatestMessage, + [orderId], + ), + returnValue: _i9.Stream<_i7.MostroMessage<_i6.Payload>?>.empty(), + ) as _i9.Stream<_i7.MostroMessage<_i6.Payload>?>); @override - _i9.Future<_i7.MostroMessage<_i6.Payload>?> getItem(String? id) => + _i9.Stream<_i7.MostroMessage<_i6.Payload>?> watchLatestMessageOfType( + String? orderId) => (super.noSuchMethod( - Invocation.method(#getItem, [id]), - returnValue: _i9.Future<_i7.MostroMessage<_i6.Payload>?>.value(), - ) - as _i9.Future<_i7.MostroMessage<_i6.Payload>?>); + Invocation.method( + #watchLatestMessageOfType, + [orderId], + ), + returnValue: _i9.Stream<_i7.MostroMessage<_i6.Payload>?>.empty(), + ) as _i9.Stream<_i7.MostroMessage<_i6.Payload>?>); @override - _i9.Future hasItem(String? id) => + _i9.Stream>> watchAllMessages( + String? orderId) => (super.noSuchMethod( - Invocation.method(#hasItem, [id]), - returnValue: _i9.Future.value(false), - ) - as _i9.Future); + Invocation.method( + #watchAllMessages, + [orderId], + ), + returnValue: _i9.Stream>>.empty(), + ) as _i9.Stream>>); @override - _i9.Future>> getAllItems() => + _i9.Stream<_i7.MostroMessage<_i6.Payload>?> watchByRequestId( + int? requestId) => (super.noSuchMethod( - Invocation.method(#getAllItems, []), - returnValue: _i9.Future>>.value( - <_i7.MostroMessage<_i6.Payload>>[], - ), - ) - as _i9.Future>>); + Invocation.method( + #watchByRequestId, + [requestId], + ), + returnValue: _i9.Stream<_i7.MostroMessage<_i6.Payload>?>.empty(), + ) as _i9.Stream<_i7.MostroMessage<_i6.Payload>?>); @override - _i9.Future deleteItem(String? id) => + _i9.Future>> getAllMessagesForOrderId( + String? orderId) => (super.noSuchMethod( - Invocation.method(#deleteItem, [id]), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + Invocation.method( + #getAllMessagesForOrderId, + [orderId], + ), + returnValue: _i9.Future>>.value( + <_i7.MostroMessage<_i6.Payload>>[]), + ) as _i9.Future>>); @override - _i9.Future deleteAllItems() => + _i9.Future putItem( + String? id, + _i7.MostroMessage<_i6.Payload>? item, + ) => (super.noSuchMethod( - Invocation.method(#deleteAllItems, []), - returnValue: _i9.Future.value(), - returnValueForMissingStub: _i9.Future.value(), - ) - as _i9.Future); + Invocation.method( + #putItem, + [ + id, + item, + ], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); @override - _i9.Future> deleteWhere( - bool Function(_i7.MostroMessage<_i6.Payload>)? filter, { - int? maxBatchSize, + _i9.Future<_i7.MostroMessage<_i6.Payload>?> getItem(String? id) => + (super.noSuchMethod( + Invocation.method( + #getItem, + [id], + ), + returnValue: _i9.Future<_i7.MostroMessage<_i6.Payload>?>.value(), + ) as _i9.Future<_i7.MostroMessage<_i6.Payload>?>); + + @override + _i9.Future hasItem(String? id) => (super.noSuchMethod( + Invocation.method( + #hasItem, + [id], + ), + returnValue: _i9.Future.value(false), + ) as _i9.Future); + + @override + _i9.Future deleteItem(String? id) => (super.noSuchMethod( + Invocation.method( + #deleteItem, + [id], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future deleteAll() => (super.noSuchMethod( + Invocation.method( + #deleteAll, + [], + ), + returnValue: _i9.Future.value(), + returnValueForMissingStub: _i9.Future.value(), + ) as _i9.Future); + + @override + _i9.Future deleteWhere(_i5.Filter? filter) => (super.noSuchMethod( + Invocation.method( + #deleteWhere, + [filter], + ), + returnValue: _i9.Future.value(0), + ) as _i9.Future); + + @override + _i9.Future>> find({ + _i5.Filter? filter, + List<_i5.SortOrder>? sort, + int? limit, + int? offset, }) => (super.noSuchMethod( - Invocation.method( - #deleteWhere, - [filter], - {#maxBatchSize: maxBatchSize}, - ), - returnValue: _i9.Future>.value([]), - ) - as _i9.Future>); + Invocation.method( + #find, + [], + { + #filter: filter, + #sort: sort, + #limit: limit, + #offset: offset, + }, + ), + returnValue: _i9.Future>>.value( + <_i7.MostroMessage<_i6.Payload>>[]), + ) as _i9.Future>>); + + @override + _i9.Future>> getAll() => + (super.noSuchMethod( + Invocation.method( + #getAll, + [], + ), + returnValue: _i9.Future>>.value( + <_i7.MostroMessage<_i6.Payload>>[]), + ) as _i9.Future>>); + + @override + _i9.Stream>> watch({ + _i5.Filter? filter, + List<_i5.SortOrder>? sort, + }) => + (super.noSuchMethod( + Invocation.method( + #watch, + [], + { + #filter: filter, + #sort: sort, + }, + ), + returnValue: _i9.Stream>>.empty(), + ) as _i9.Stream>>); @override - void dispose() => super.noSuchMethod( - Invocation.method(#dispose, []), - returnValueForMissingStub: null, - ); + _i9.Stream<_i7.MostroMessage<_i6.Payload>?> watchById(String? id) => + (super.noSuchMethod( + Invocation.method( + #watchById, + [id], + ), + returnValue: _i9.Stream<_i7.MostroMessage<_i6.Payload>?>.empty(), + ) as _i9.Stream<_i7.MostroMessage<_i6.Payload>?>); + + @override + _i5.Filter eq( + String? field, + Object? value, + ) => + (super.noSuchMethod( + Invocation.method( + #eq, + [ + field, + value, + ], + ), + returnValue: _FakeFilter_7( + this, + Invocation.method( + #eq, + [ + field, + value, + ], + ), + ), + ) as _i5.Filter); } diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 011734da..94cd0408 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -8,10 +8,16 @@ #include #include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); + PermissionHandlerWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("PermissionHandlerWindowsPlugin")); + SystemTrayPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("SystemTrayPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 11485fce..52df19df 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -5,9 +5,12 @@ list(APPEND FLUTTER_PLUGIN_LIST flutter_secure_storage_windows local_auth_windows + permission_handler_windows + system_tray ) list(APPEND FLUTTER_FFI_PLUGIN_LIST + flutter_local_notifications_windows ) set(PLUGIN_BUNDLED_LIBRARIES)