diff --git a/README.md b/README.md index 65245505..4755a3f0 100644 --- a/README.md +++ b/README.md @@ -7,26 +7,31 @@ This project is a mobile interface that facilitates peer-to-peer bitcoin trading ## Prerequisites ### For the Mobile Client + - Install [Flutter](https://flutter.dev/docs/get-started/install): Follow the official guide for your operating system. - Install [Android Studio](https://developer.android.com/studio) or [Xcode](https://developer.apple.com/xcode/) (for iOS development) - Install [VS Code](https://code.visualstudio.com/) (optional but recommended) ### For Mostro Daemon + - Install [Rust](https://www.rust-lang.org/tools/install) - Install [Docker](https://docs.docker.com/get-docker/) ### For Testing Environment + - Install [Polar](https://lightningpolar.com/): For simulating Lightning Network nodes ## Installation 1. Clone the repository: + ```bash git clone https://github.com/MostroP2P/mobile.git cd mobile ``` 2. Install Flutter dependencies: + ```bash flutter pub get ``` @@ -34,12 +39,15 @@ This project is a mobile interface that facilitates peer-to-peer bitcoin trading ## Running the App ### On Emulator/Simulator + ```bash flutter run ``` ### On Physical Device + Connect your device and run: + ```bash flutter run ``` @@ -47,23 +55,28 @@ flutter run ## Setting up Mostro Daemon 1. Clone the Mostro repository: + ```bash git clone https://github.com/MostroP2P/mostro.git cd mostro ``` 2. Set up the configuration: + ```bash cp settings.tpl.toml settings.toml ``` + Edit `settings.toml` with your specific configurations. 3. Initialize the database: + ```bash ./init_db.sh ``` 4. Run the Mostro daemon: + ```bash cargo run ``` @@ -89,9 +102,10 @@ Please take a look at our issues section for areas where you can contribute. We This project is licensed under the MIT License. See the `LICENSE` file for details. ## Progress Overview -- [ ] Displays order list -- [ ] Take orders (Buy & Sell) -- [ ] Posts Orders (Buy & Sell) + +- [x] Displays order list +- [x] Take orders (Buy & Sell) +- [x] Posts Orders (Buy & Sell) - [ ] Direct message with peers (use nip-17) - [ ] Fiat sent - [ ] Release diff --git a/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 00000000..55aebf98 --- /dev/null +++ b/android/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,3 @@ + + + diff --git a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png index db77bb4b..a12bffd7 100644 Binary files a/android/app/src/main/res/mipmap-hdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png index 17987b79..b8311066 100644 Binary files a/android/app/src/main/res/mipmap-mdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png index 09d43914..a1637808 100644 Binary files a/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png index d5f1c8d3..aaad282a 100644 Binary files a/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png index 4d6372ee..7214029f 100644 Binary files a/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png and b/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/assets/images/launcher-icon.png b/assets/images/launcher-icon.png new file mode 100644 index 00000000..c9f223d6 Binary files /dev/null and b/assets/images/launcher-icon.png differ diff --git a/ios/Runner.xcodeproj/project.pbxproj b/ios/Runner.xcodeproj/project.pbxproj index 5f421e65..71492ac5 100644 --- a/ios/Runner.xcodeproj/project.pbxproj +++ b/ios/Runner.xcodeproj/project.pbxproj @@ -427,7 +427,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -484,7 +484,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; - ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = AppIcon; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json index d36b1fab..d0d98aa1 100644 --- a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,122 +1 @@ -{ - "images" : [ - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "20x20", - "idiom" : "iphone", - "filename" : "Icon-App-20x20@3x.png", - "scale" : "3x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "iphone", - "filename" : "Icon-App-29x29@3x.png", - "scale" : "3x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "iphone", - "filename" : "Icon-App-40x40@3x.png", - "scale" : "3x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@2x.png", - "scale" : "2x" - }, - { - "size" : "60x60", - "idiom" : "iphone", - "filename" : "Icon-App-60x60@3x.png", - "scale" : "3x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@1x.png", - "scale" : "1x" - }, - { - "size" : "20x20", - "idiom" : "ipad", - "filename" : "Icon-App-20x20@2x.png", - "scale" : "2x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@1x.png", - "scale" : "1x" - }, - { - "size" : "29x29", - "idiom" : "ipad", - "filename" : "Icon-App-29x29@2x.png", - "scale" : "2x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@1x.png", - "scale" : "1x" - }, - { - "size" : "40x40", - "idiom" : "ipad", - "filename" : "Icon-App-40x40@2x.png", - "scale" : "2x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@1x.png", - "scale" : "1x" - }, - { - "size" : "76x76", - "idiom" : "ipad", - "filename" : "Icon-App-76x76@2x.png", - "scale" : "2x" - }, - { - "size" : "83.5x83.5", - "idiom" : "ipad", - "filename" : "Icon-App-83.5x83.5@2x.png", - "scale" : "2x" - }, - { - "size" : "1024x1024", - "idiom" : "ios-marketing", - "filename" : "Icon-App-1024x1024@1x.png", - "scale" : "1x" - } - ], - "info" : { - "version" : 1, - "author" : "xcode" - } -} +{"images":[{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"20x20","idiom":"iphone","filename":"Icon-App-20x20@3x.png","scale":"3x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"29x29","idiom":"iphone","filename":"Icon-App-29x29@3x.png","scale":"3x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"40x40","idiom":"iphone","filename":"Icon-App-40x40@3x.png","scale":"3x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@1x.png","scale":"1x"},{"size":"57x57","idiom":"iphone","filename":"Icon-App-57x57@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@2x.png","scale":"2x"},{"size":"60x60","idiom":"iphone","filename":"Icon-App-60x60@3x.png","scale":"3x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@1x.png","scale":"1x"},{"size":"20x20","idiom":"ipad","filename":"Icon-App-20x20@2x.png","scale":"2x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@1x.png","scale":"1x"},{"size":"29x29","idiom":"ipad","filename":"Icon-App-29x29@2x.png","scale":"2x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@1x.png","scale":"1x"},{"size":"40x40","idiom":"ipad","filename":"Icon-App-40x40@2x.png","scale":"2x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@1x.png","scale":"1x"},{"size":"50x50","idiom":"ipad","filename":"Icon-App-50x50@2x.png","scale":"2x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@1x.png","scale":"1x"},{"size":"72x72","idiom":"ipad","filename":"Icon-App-72x72@2x.png","scale":"2x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@1x.png","scale":"1x"},{"size":"76x76","idiom":"ipad","filename":"Icon-App-76x76@2x.png","scale":"2x"},{"size":"83.5x83.5","idiom":"ipad","filename":"Icon-App-83.5x83.5@2x.png","scale":"2x"},{"size":"1024x1024","idiom":"ios-marketing","filename":"Icon-App-1024x1024@1x.png","scale":"1x"}],"info":{"version":1,"author":"xcode"}} \ No newline at end of file diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png index dc9ada47..77055e31 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 7353c41e..e894f869 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 797d452e..69a582c4 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index 6ed2d933..f82849ae 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cd7b009..49fbff75 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index fe730945..7e48eb04 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index 321773cd..7133bc33 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 797d452e..69a582c4 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index 502f463a..17543482 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index 0ec30343..407aa43f 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png new file mode 100644 index 00000000..66f953cb Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png new file mode 100644 index 00000000..d6fafa38 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-50x50@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png new file mode 100644 index 00000000..07d165ba Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png new file mode 100644 index 00000000..e2355256 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-57x57@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index 0ec30343..407aa43f 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index e9f5fea2..43fddc12 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png new file mode 100644 index 00000000..1f5a79d5 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png new file mode 100644 index 00000000..02864190 Binary files /dev/null and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-72x72@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index 84ac32ae..ad9c8702 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 8953cba0..2012c178 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index 0467bf12..d2b384a5 100644 Binary files a/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/lib/app/app.dart b/lib/app/app.dart new file mode 100644 index 00000000..c8d3a41a --- /dev/null +++ b/lib/app/app.dart @@ -0,0 +1,40 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_localizations/flutter_localizations.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/app/app_routes.dart'; +import 'package:mostro_mobile/app/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'; + +class MostroApp extends ConsumerWidget { + const MostroApp({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + ref.listen(authNotifierProvider, (previous, state) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!context.mounted) return; + if (state is AuthAuthenticated || state is AuthRegistrationSuccess) { + context.go('/'); + } else if (state is AuthUnregistered || state is AuthUnauthenticated) { + context.go('/welcome'); + } + }); + }); + + return MaterialApp.router( + title: 'Mostro', + theme: AppTheme.theme, + routerConfig: goRouter, + localizationsDelegates: const [ + S.delegate, + GlobalMaterialLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + ], + supportedLocales: S.delegate.supportedLocales, + ); + } +} diff --git a/lib/app/app_routes.dart b/lib/app/app_routes.dart new file mode 100644 index 00000000..51a81ae5 --- /dev/null +++ b/lib/app/app_routes.dart @@ -0,0 +1,87 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/features/add_order/screens/add_order_screen.dart'; +import 'package:mostro_mobile/features/add_order/screens/order_confirmation_screen.dart'; +import 'package:mostro_mobile/features/auth/screens/welcome_screen.dart'; +import 'package:mostro_mobile/features/home/screens/home_screen.dart'; +import 'package:mostro_mobile/features/take_order/screens/add_lightning_invoice_screen.dart'; +import 'package:mostro_mobile/features/take_order/screens/pay_lightning_invoice_screen.dart'; +import 'package:mostro_mobile/features/take_order/screens/take_buy_order_screen.dart'; +import 'package:mostro_mobile/features/take_order/screens/take_sell_order_screen.dart'; +import 'package:mostro_mobile/presentation/chat_list/screens/chat_list_screen.dart'; +import 'package:mostro_mobile/presentation/profile/screens/profile_screen.dart'; +import 'package:mostro_mobile/features/auth/screens/register_screen.dart'; +import 'package:mostro_mobile/shared/widgets/navigation_listener_widget.dart'; +import 'package:mostro_mobile/shared/widgets/notification_listener_widget.dart'; + +final goRouter = GoRouter( + navigatorKey: GlobalKey(), + routes: [ + ShellRoute( + builder: (BuildContext context, GoRouterState state, Widget child) { + // Wrap the Navigator with your listener widgets + return NotificationListenerWidget( + child: NavigationListenerWidget( + child: child, + ), + ); + }, + routes: [ + GoRoute( + path: '/welcome', + builder: (context, state) => const WelcomeScreen(), + ), + GoRoute( + path: '/', + builder: (context, state) => const HomeScreen(), + ), + GoRoute( + path: '/chat_list', + builder: (context, state) => const ChatListScreen(), + ), + GoRoute( + path: '/profile', + builder: (context, state) => const ProfileScreen(), + ), + GoRoute( + path: '/register', + builder: (context, state) => const RegisterScreen(), + ), + GoRoute( + path: '/add_order', + builder: (context, state) => AddOrderScreen(), + ), + GoRoute( + path: '/take_sell/:orderId', + builder: (context, state) => + TakeSellOrderScreen(orderId: state.pathParameters['orderId']!), + ), + GoRoute( + path: '/take_buy/:orderId', + builder: (context, state) => + TakeBuyOrderScreen(orderId: state.pathParameters['orderId']!), + ), + GoRoute( + path: '/order_confirmed/:orderId', + builder: (context, state) => OrderConfirmationScreen( + orderId: state.pathParameters['orderId']!, + ), + ), + GoRoute( + path: '/pay_invoice/:orderId', + builder: (context, state) => PayLightningInvoiceScreen( + orderId: state.pathParameters['orderId']!), + ), + GoRoute( + path: '/add_invoice/:orderId', + builder: (context, state) => AddLightningInvoiceScreen( + orderId: state.pathParameters['orderId']!), + ), + ], + ), + ], + initialLocation: '/welcome', + errorBuilder: (context, state) => Scaffold( + body: Center(child: Text(state.error.toString())), + ), +); diff --git a/lib/app/app_settings.dart b/lib/app/app_settings.dart new file mode 100644 index 00000000..6043ab5b --- /dev/null +++ b/lib/app/app_settings.dart @@ -0,0 +1,11 @@ +class AppSettings { + final bool isFirstLaunch; + + AppSettings(this.isFirstLaunch); + + factory AppSettings.intial() => AppSettings(true); + + AppSettings copyWith({bool isFirstLaunch = false}) { + return AppSettings(isFirstLaunch); + } +} diff --git a/lib/app/app_theme.dart b/lib/app/app_theme.dart new file mode 100644 index 00000000..a34656f1 --- /dev/null +++ b/lib/app/app_theme.dart @@ -0,0 +1,134 @@ +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; + +class AppTheme { + // Color Definitions + static const Color grey = Color(0xFFCCCCCC); + static const Color mostroGreen = Color(0xFF8CC541); + static const Color dark1 = Color(0xFF1D212C); + static const Color grey2 = Color(0xFF92949A); + static const Color yellow = Color(0xFFF3CA29); + static const Color red1 = Color(0xFFCA3C3C); + static const Color dark2 = Color(0xFF303544); + static const Color cream1 = Color(0xFFF9F8F1); + static const Color red2 = Color(0xFFE45A5A); + static const Color green2 = Color(0xFF739C3D); + + // Padding and Margin Constants + static const EdgeInsets smallPadding = EdgeInsets.all(8.0); + static const EdgeInsets mediumPadding = EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0); + static const EdgeInsets largePadding = EdgeInsets.all(24.0); + + static const EdgeInsets smallMargin = EdgeInsets.all(4.0); + static const EdgeInsets mediumMargin = EdgeInsets.symmetric(horizontal: 12.0, vertical: 8.0); + static const EdgeInsets largeMargin = EdgeInsets.all(20.0); + + static ThemeData get theme { + return ThemeData( + primaryColor: mostroGreen, + scaffoldBackgroundColor: dark1, + appBarTheme: const AppBarTheme( + backgroundColor: Colors.transparent, + elevation: 0, + ), + textTheme: _buildTextTheme(), + elevatedButtonTheme: ElevatedButtonThemeData( + style: ElevatedButton.styleFrom( + foregroundColor: Colors.white, + backgroundColor: mostroGreen, + textStyle: GoogleFonts.robotoCondensed( + fontWeight: FontWeight.w500, + fontSize: 16.0, + ), + padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 30), + ), + ), + textButtonTheme: TextButtonThemeData( + style: TextButton.styleFrom( + foregroundColor: mostroGreen, + textStyle: GoogleFonts.robotoCondensed( + fontWeight: FontWeight.w500, + fontSize: 14.0, + ), + ), + ), + outlinedButtonTheme: OutlinedButtonThemeData( + style: OutlinedButton.styleFrom( + foregroundColor: mostroGreen, + side: const BorderSide(color: mostroGreen), + textStyle: GoogleFonts.robotoCondensed( + fontWeight: FontWeight.w500, + fontSize: 14.0, + ), + ), + ), + inputDecorationTheme: const InputDecorationTheme( + labelStyle: TextStyle(color: grey), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: grey), + ), + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide(color: mostroGreen), + ), + ), + cardTheme: CardTheme( + color: dark2, + margin: EdgeInsets.zero, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: const BorderSide(color: dark2), + ), + ), + iconTheme: const IconThemeData( + color: cream1, + size: 24.0, + ), + ); + } + + static TextTheme _buildTextTheme() { + return GoogleFonts.robotoCondensedTextTheme( + const TextTheme( + displayLarge: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 24.0, + ), // For larger titles + displayMedium: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 20.0, + ), // For medium titles + displaySmall: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 16.0, + ), // For smaller titles + headlineMedium: TextStyle( + fontWeight: FontWeight.w700, + fontSize: 16.0, + ), // For subtitles + headlineSmall: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14.0, + ), // For secondary text + titleLarge: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 16.0, + ), // For form labels + bodyLarge: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 16.0, + ), // For body text + bodyMedium: TextStyle( + fontWeight: FontWeight.w400, + fontSize: 14.0, + ), // For smaller body text + labelLarge: TextStyle( + fontWeight: FontWeight.w500, + fontSize: 14.0, + ), // For buttons and labels + ), + ).apply( + bodyColor: cream1, + displayColor: cream1, + ); + } +} diff --git a/lib/core/config.dart b/lib/app/config.dart similarity index 82% rename from lib/core/config.dart rename to lib/app/config.dart index 41610a77..acef260f 100644 --- a/lib/core/config.dart +++ b/lib/app/config.dart @@ -4,9 +4,9 @@ class Config { // Configuración de Nostr static const List nostrRelays = [ 'ws://127.0.0.1:7000', + 'wss://relay.mostro.network', //'ws://10.0.2.2:7000', //'wss://relay.damus.io', - //'wss://relay.mostro.network', //'wss://relay.nostr.net', // Agrega más relays aquí si es necesario ]; @@ -21,5 +21,6 @@ class Config { // Modo de depuración static bool get isDebug => !kReleaseMode; - // Puedes agregar más configuraciones específicas de Mostro aquí si las necesitas en el futuro + // Versión de Mostro + static int mostroVersion = 1; } diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart deleted file mode 100644 index 7017c81b..00000000 --- a/lib/core/routes/app_routes.dart +++ /dev/null @@ -1,47 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:mostro_mobile/presentation/auth/screens/welcome_screen.dart'; -import 'package:mostro_mobile/presentation/home/screens/home_screen.dart'; -import 'package:mostro_mobile/presentation/chat_list/screens/chat_list_screen.dart'; -import 'package:mostro_mobile/presentation/profile/screens/profile_screen.dart'; -import 'package:mostro_mobile/presentation/auth/screens/register_screen.dart'; - -class AppRoutes { - static const String welcome = '/welcome'; - static const String home = '/'; - static const String chatList = '/chat_list'; - static const String profile = '/profile'; - static const String register = '/register'; - - static Map get routes => { - welcome: (context) => const WelcomeScreen(), - home: (context) => const HomeScreen(), - chatList: (context) => const ChatListScreen(), - profile: (context) => const ProfileScreen(), - register: (context) => const RegisterScreen(), - }; - - static Route onGenerateRoute(RouteSettings settings) { - return MaterialPageRoute( - builder: (context) { - switch (settings.name) { - case welcome: - return const WelcomeScreen(); - case home: - return const HomeScreen(); - case chatList: - return const ChatListScreen(); - case profile: - return const ProfileScreen(); - case register: - return const RegisterScreen(); - default: - return const Scaffold( - body: Center( - child: Text('No route defined'), - ), - ); - } - }, - ); - } -} \ No newline at end of file diff --git a/lib/core/theme/app_theme.dart b/lib/core/theme/app_theme.dart deleted file mode 100644 index f56ccdfe..00000000 --- a/lib/core/theme/app_theme.dart +++ /dev/null @@ -1,66 +0,0 @@ -import 'package:flutter/material.dart'; - -class AppTheme { - static const Color grey = Color(0xFFCCCCCC); - static const Color mostroGreen = Color(0xFF8CC541); - static const Color dark1 = Color(0xFF1D212C); - static const Color grey2 = Color(0xFF92949A); - static const Color yellow = Color(0xFFF3CA29); - static const Color red1 = Color(0xFFCA3C3C); - static const Color dark2 = Color(0xFF303544); - static const Color cream1 = Color(0xFFF9F8F1); - static const Color red2 = Color(0xFFE45A5A); - static const Color green2 = Color(0xFF739C3D); - - static ThemeData get theme { - return ThemeData( - primaryColor: mostroGreen, - scaffoldBackgroundColor: dark1, - appBarTheme: const AppBarTheme( - backgroundColor: Colors.transparent, - elevation: 0, - ), - textTheme: const TextTheme( - displayLarge: TextStyle( - fontFamily: 'RobotoCondensed', fontWeight: FontWeight.w700), - displayMedium: TextStyle( - fontFamily: 'RobotoCondensed', fontWeight: FontWeight.w700), - displaySmall: TextStyle( - fontFamily: 'RobotoCondensed', fontWeight: FontWeight.w700), - headlineMedium: TextStyle( - fontFamily: 'RobotoCondensed', fontWeight: FontWeight.w700), - headlineSmall: TextStyle( - fontFamily: 'RobotoCondensed', fontWeight: FontWeight.w500), - titleLarge: TextStyle( - fontFamily: 'RobotoCondensed', fontWeight: FontWeight.w500), - bodyLarge: TextStyle( - fontFamily: 'RobotoCondensed', fontWeight: FontWeight.w400), - bodyMedium: TextStyle( - fontFamily: 'RobotoCondensed', fontWeight: FontWeight.w400), - labelLarge: TextStyle( - fontFamily: 'RobotoCondensed', fontWeight: FontWeight.w500), - ).apply( - bodyColor: Colors.white, - displayColor: Colors.white, - ), - elevatedButtonTheme: ElevatedButtonThemeData( - style: ElevatedButton.styleFrom( - foregroundColor: Colors.white, - backgroundColor: mostroGreen, - textStyle: const TextStyle( - fontFamily: 'RobotoCondensed', fontWeight: FontWeight.w500), - padding: const EdgeInsets.symmetric(vertical: 15, horizontal: 30), - ), - ), - inputDecorationTheme: const InputDecorationTheme( - labelStyle: TextStyle(color: grey), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: grey), - ), - focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: mostroGreen), - ), - ), - ); - } -} diff --git a/lib/data/models/amount.dart b/lib/data/models/amount.dart index 972c4cae..6f335af6 100644 --- a/lib/data/models/amount.dart +++ b/lib/data/models/amount.dart @@ -1,39 +1,17 @@ -class Amount { - final int minimum; - final int? maximum; +import 'package:mostro_mobile/data/models/payload.dart'; - Amount(this.minimum, this.maximum); +class Amount implements Payload { + final int amount; - factory Amount.fromList(List fa) { - if (fa.length < 2) { - throw ArgumentError( - 'List must have at least two elements: a label and a minimum value.'); - } + Amount({required this.amount}); - final min = int.tryParse(fa[1]); - if (min == null) { - throw ArgumentError( - 'Second element must be a valid integer representing the minimum value.'); - } - - int? max; - if (fa.length > 2) { - max = int.tryParse(fa[2]); - } - - return Amount(min, max); - } - - factory Amount.empty() { - return Amount(0, null); + @override + Map toJson() { + return { + type: amount, + }; } @override - String toString() { - if (maximum != null) { - return '$minimum - $maximum'; - } else { - return '$minimum'; - } - } + String get type => 'amount'; } diff --git a/lib/data/models/dispute.dart b/lib/data/models/dispute.dart new file mode 100644 index 00000000..f4ea867a --- /dev/null +++ b/lib/data/models/dispute.dart @@ -0,0 +1,17 @@ +import 'package:mostro_mobile/data/models/payload.dart'; + +class Dispute implements Payload { + final String orderId; + + Dispute({required this.orderId}); + + @override + Map toJson() { + return { + type: orderId, + }; + } + + @override + String get type => 'dispute'; +} diff --git a/lib/data/models/enums/action.dart b/lib/data/models/enums/action.dart index 432d33b4..30f73470 100644 --- a/lib/data/models/enums/action.dart +++ b/lib/data/models/enums/action.dart @@ -51,12 +51,12 @@ enum Action { const Action(this.value); /// Converts a string value to its corresponding Action enum value. - /// + /// /// Throws an ArgumentError if the string doesn't match any Action value. static final _valueMap = { for (var action in Action.values) action.value: action }; - + static Action fromString(String value) { final action = _valueMap[value]; if (action == null) { @@ -65,4 +65,8 @@ enum Action { return action; } + @override + String toString() { + return value; + } } diff --git a/lib/data/models/mostro_instance.dart b/lib/data/models/mostro_instance.dart new file mode 100644 index 00000000..d2e0c100 --- /dev/null +++ b/lib/data/models/mostro_instance.dart @@ -0,0 +1,61 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; + +class MostroInstance { + final String mostroVersion; + final int maxOrderAmount; + final int minOrderAmount; + final int expirationHours; + final int expirationSeconds; + final double fee; + final int pow; + final int holdInvoiceExpirationWindow; + final int holdInvoiceCltvDelta; + final int invoiceExpirationWindow; + + MostroInstance( + this.mostroVersion, + this.maxOrderAmount, + this.minOrderAmount, + this.expirationHours, + this.expirationSeconds, + this.fee, + this.pow, + this.holdInvoiceExpirationWindow, + this.holdInvoiceCltvDelta, + this.invoiceExpirationWindow); + + factory MostroInstance.fromEvent(NostrEvent event) { + return MostroInstance( + event.mostroVersion, + event.maxOrderAmount, + event.minOrderAmount, + event.expirationHours, + event.expirationSeconds, + event.fee, + event.pow, + event.holdInvoiceExpirationWindow, + event.holdInvoiceCltvDelta, + event.invoiceExpirationWindow); + } +} + +extension MostroInstanceExtensions on NostrEvent { + String? _getTagValue(String key) { + final tag = tags?.firstWhere((t) => t[0] == key, orElse: () => []); + return (tag != null && tag.length > 1) ? tag[1] : null; + } + + String get mostroVersion => _getTagValue('mostro_version')!; + int get maxOrderAmount => int.parse(_getTagValue('max_order_amount')!); + int get minOrderAmount => int.parse(_getTagValue('min_order_amount')!); + int get expirationHours => int.parse(_getTagValue('expiration_hours')!); + int get expirationSeconds => int.parse(_getTagValue('expiration_seconds')!); + double get fee => double.parse(_getTagValue('fee')!); + int get pow => int.parse(_getTagValue('pow')!); + int get holdInvoiceExpirationWindow => + int.parse(_getTagValue('hold_invoice_expiration_window')!); + int get holdInvoiceCltvDelta => + int.parse(_getTagValue('hold_invoice_cltv_delta')!); + int get invoiceExpirationWindow => + int.parse(_getTagValue('invoice_expiration_window')!); +} diff --git a/lib/data/models/mostro_message.dart b/lib/data/models/mostro_message.dart index c60781f3..470b842f 100644 --- a/lib/data/models/mostro_message.dart +++ b/lib/data/models/mostro_message.dart @@ -1,24 +1,24 @@ import 'dart:convert'; +import 'package:mostro_mobile/app/config.dart'; import 'package:mostro_mobile/data/models/enums/action.dart'; -import 'package:mostro_mobile/data/models/content.dart'; -import 'package:mostro_mobile/services/mostro_service.dart'; +import 'package:mostro_mobile/data/models/payload.dart'; -class MostroMessage { - final int version = mostroVersion; - final String? requestId; +class MostroMessage { + String? requestId; final Action action; - T? content; + T? _payload; - MostroMessage({required this.action, required this.requestId, this.content}); + MostroMessage({required this.action, this.requestId, T? payload}) + : _payload = payload; Map toJson() { return { 'order': { - 'version': mostroVersion, + 'version': Config.mostroVersion, 'id': requestId, 'action': action.value, - 'content': content?.toJson(), + 'content': _payload?.toJson(), }, }; } @@ -35,19 +35,26 @@ class MostroMessage { ? Action.fromString(order['action']) : throw FormatException('Missing action field'); - final id = order['id'] != null - ? order['id'] as String? - : throw FormatException('Missing id field'); - - final content = order['content'] != null ? Content.fromJson(event['order']['content']) as T : null; + final content = order['content'] != null + ? Payload.fromJson(event['order']['content']) as T + : null; return MostroMessage( action: action, - requestId: id, - content: content, + requestId: order['id'], + payload: content, ); } catch (e) { throw FormatException('Failed to deserialize MostroMessage: $e'); } } + + T? get payload => _payload; + + R? getPayload() { + if (payload is R) { + return payload as R; + } + return null; + } } diff --git a/lib/data/models/nostr_event.dart b/lib/data/models/nostr_event.dart index ab4cf4b1..5e88ce97 100644 --- a/lib/data/models/nostr_event.dart +++ b/lib/data/models/nostr_event.dart @@ -1,5 +1,5 @@ import 'dart:convert'; -import 'package:mostro_mobile/data/models/amount.dart'; +import 'package:mostro_mobile/data/models/range_amount.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/rating.dart'; import 'package:timeago/timeago.dart' as timeago; @@ -16,7 +16,7 @@ extension NostrEventExtensions on NostrEvent { String? get currency => _getTagValue('f'); String? get status => _getTagValue('s'); String? get amount => _getTagValue('amt'); - Amount get fiatAmount => _getAmount('fa'); + RangeAmount get fiatAmount => _getAmount('fa'); List get paymentMethods => _getTagValue('pm')?.split(',') ?? []; String? get premium => _getTagValue('premium'); String? get source => _getTagValue('source'); @@ -43,9 +43,11 @@ extension NostrEventExtensions on NostrEvent { return (tag != null && tag.length > 1) ? tag[1] : null; } - Amount _getAmount(String key) { + RangeAmount _getAmount(String key) { final tag = tags?.firstWhere((t) => t[0] == key, orElse: () => []); - return (tag != null && tag.length> 1) ? Amount.fromList(tag) : Amount.empty(); + return (tag != null && tag.length > 1) + ? RangeAmount.fromList(tag) + : RangeAmount.empty(); } String _timeAgo(String? ts) { diff --git a/lib/data/models/order.dart b/lib/data/models/order.dart index 5758db26..06c3134d 100644 --- a/lib/data/models/order.dart +++ b/lib/data/models/order.dart @@ -1,10 +1,10 @@ import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; import 'package:mostro_mobile/data/models/enums/status.dart'; -import 'package:mostro_mobile/data/models/content.dart'; +import 'package:mostro_mobile/data/models/payload.dart'; import 'package:mostro_mobile/data/models/nostr_event.dart'; -class Order implements Content { +class Order implements Payload { final String? id; final OrderType kind; final Status status; @@ -33,11 +33,11 @@ class Order implements Content { this.maxAmount, required this.fiatAmount, required this.paymentMethod, - required this.premium, + this.premium = 1, this.masterBuyerPubkey, this.masterSellerPubkey, this.buyerInvoice, - this.createdAt, + this.createdAt = 0, this.expiresAt, this.buyerToken, this.sellerToken, diff --git a/lib/data/models/order_model.dart b/lib/data/models/order_model.dart deleted file mode 100644 index 89aa32af..00000000 --- a/lib/data/models/order_model.dart +++ /dev/null @@ -1,194 +0,0 @@ -import 'dart:convert'; -import 'package:timeago/timeago.dart' as timeago; - -class OrderModel { - final String id; - final String type; - final String user; - final double rating; - final int ratingCount; - final int amount; - final String currency; - final double fiatAmount; - final String fiatCurrency; - final String paymentMethod; - final String timeAgo; - final String premium; - final String status; - final double satsAmount; - final String sellerName; - final double sellerRating; - final int sellerReviewCount; - final String sellerAvatar; - final double exchangeRate; - final double buyerSatsAmount; - final double buyerFiatAmount; - - OrderModel({ - required this.id, - required this.type, - required this.user, - required this.rating, - required this.ratingCount, - required this.amount, - required this.currency, - required this.fiatAmount, - required this.fiatCurrency, - required this.paymentMethod, - required this.timeAgo, - required this.premium, - required this.status, - required this.satsAmount, - required this.sellerName, - required this.sellerRating, - required this.sellerReviewCount, - required this.sellerAvatar, - required this.exchangeRate, - required this.buyerSatsAmount, - required this.buyerFiatAmount, - }); - - // Método para crear una instancia de OrderModel desde un JSON - factory OrderModel.fromJson(Map json) { - return OrderModel( - id: json['id'], - type: json['type'], - user: json['user'], - rating: json['rating'].toDouble(), - ratingCount: json['rating_count'], - amount: json['amount'], - currency: json['currency'], - fiatAmount: json['fiat_amount'].toDouble(), - fiatCurrency: json['fiat_currency'], - paymentMethod: json['payment_method'], - timeAgo: json['time_ago'], - premium: json['premium'], - status: json['status'], - satsAmount: json['sats_amount'].toDouble(), - sellerName: json['sellerName'], - sellerRating: json['seller_rating'].toDouble(), - sellerReviewCount: json['seller_review_count'], - sellerAvatar: json['seller_avatar'], - exchangeRate: json['exchange_rate'].toDouble(), - buyerSatsAmount: json['buyer_sats_amount'].toDouble(), - buyerFiatAmount: json['buyer_fiat_amount'].toDouble(), - ); - } - - // Método para convertir una instancia de OrderModel a JSON - Map toJson() { - return { - 'id': id, - 'type': type, - 'user': user, - 'rating': rating, - 'ratingCount': ratingCount, - 'amount': amount, - 'currency': currency, - 'fiatAmount': fiatAmount, - 'fiatCurrency': fiatCurrency, - 'paymentMethod': paymentMethod, - 'timeAgo': timeAgo, - 'premium': premium, - 'status': status, - 'satsAmount': satsAmount, - 'sellerName': sellerName, - 'sellerRating': sellerRating, - 'sellerReviewCount': sellerReviewCount, - 'sellerAvatar': sellerAvatar, - 'exchangeRate': exchangeRate, - 'buyerSatsAmount': buyerSatsAmount, - 'buyerFiatAmount': buyerFiatAmount, - }; - } - - // Método para crear una instancia de OrderModel desde las Tags de un Event - factory OrderModel.fromEventTags(List> tags) { - final Map tagMap = {}; - - for (var tag in tags) { - if (tag.length >= 2) { - final key = tag[0]; - final value = tag.sublist(1); - - if (tagMap.containsKey(key)) { - if (tagMap[key] is List) { - tagMap[key].addAll(value); - } else { - tagMap[key] = [tagMap[key], ...value]; - } - } else { - tagMap[key] = value.length == 1 ? value.first : value; - } - } - } - - String getString(String key) { - if (!tagMap.containsKey(key)) return ''; - final value = tagMap[key]; - if (value is String) return value; - if (value is List) return value.join(', '); - return ''; - } - - int getInt(String key, [int defaultValue = 0]) { - final value = getString(key); - return int.tryParse(value) ?? defaultValue; - } - - double getDouble(String key, [double defaultValue = 0.0]) { - final value = getString(key); - return double.tryParse(value) ?? defaultValue; - } - - double parseRating(String ratingJson) { - if (ratingJson.isEmpty) return 0.0; - try { - final Map ratingMap = json.decode(ratingJson); - return (ratingMap['total_rating'] as num?)?.toDouble() ?? 0.0; - } catch (e) { - print('Error parsing rating JSON: $e'); - return 0.0; - } - } - - final int expirationTimestamp = getInt('expiration'); - final String timeAgoStr = _timeAgo(expirationTimestamp); - final String name = getString('name').isEmpty ? "anon" : getString('name'); - - try { - return OrderModel( - id: getString('d'), - type: getString('k'), - user: name, - rating: 0, - ratingCount: getInt('rating_count'), - amount: getInt('amt'), - currency: getString('f'), - fiatAmount: getDouble('fa'), - fiatCurrency: getString('f'), - paymentMethod: getString('pm'), - timeAgo: timeAgoStr, - premium: getString('premium'), - status: getString('s'), - satsAmount: getDouble('sats_amount'), - sellerName: getString('seller_name'), - sellerRating: getDouble('seller_rating'), - sellerReviewCount: getInt('seller_review_count'), - sellerAvatar: getString('seller_avatar'), - exchangeRate: getDouble('exchange_rate'), - buyerSatsAmount: getDouble('buyer_sats_amount'), - buyerFiatAmount: getDouble('buyer_fiat_amount'), - ); - } catch (e) { - throw const FormatException('Invalid tags format for OrderModel'); - } - } -} - -String _timeAgo(int timestamp) { - final DateTime eventTime = - DateTime.fromMillisecondsSinceEpoch(timestamp * 1000) - .subtract(Duration(hours: 24)); - return timeago.format(eventTime, allowFromNow: true); -} diff --git a/lib/data/models/content.dart b/lib/data/models/payload.dart similarity index 84% rename from lib/data/models/content.dart rename to lib/data/models/payload.dart index c5a80a11..474c0fdd 100644 --- a/lib/data/models/content.dart +++ b/lib/data/models/payload.dart @@ -1,11 +1,11 @@ import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/data/models/payment_request.dart'; -abstract class Content { +abstract class Payload { String get type; Map toJson(); - factory Content.fromJson(Map json) { + factory Payload.fromJson(Map json) { if (json.containsKey('order')) { return Order.fromJson(json['order']); } else if (json.containsKey('payment_request')) { diff --git a/lib/data/models/payment_request.dart b/lib/data/models/payment_request.dart index b4f24b34..90d03b65 100644 --- a/lib/data/models/payment_request.dart +++ b/lib/data/models/payment_request.dart @@ -1,7 +1,7 @@ -import 'package:mostro_mobile/data/models/content.dart'; +import 'package:mostro_mobile/data/models/payload.dart'; import 'package:mostro_mobile/data/models/order.dart'; -class PaymentRequest implements Content { +class PaymentRequest implements Payload { final Order? order; final String? lnInvoice; final int? amount; diff --git a/lib/data/models/peer.dart b/lib/data/models/peer.dart index b39d01ae..7f2ddfcf 100644 --- a/lib/data/models/peer.dart +++ b/lib/data/models/peer.dart @@ -1,6 +1,6 @@ -import 'package:mostro_mobile/data/models/content.dart'; +import 'package:mostro_mobile/data/models/payload.dart'; -class Peer implements Content { +class Peer implements Payload { final String publicKey; Peer({required this.publicKey}); @@ -14,7 +14,7 @@ class Peer implements Content { publicKey: pubkey, ); } - + @override Map toJson() { return { diff --git a/lib/data/models/range_amount.dart b/lib/data/models/range_amount.dart new file mode 100644 index 00000000..7401f7bf --- /dev/null +++ b/lib/data/models/range_amount.dart @@ -0,0 +1,43 @@ +class RangeAmount { + final int minimum; + final int? maximum; + + RangeAmount(this.minimum, this.maximum); + + factory RangeAmount.fromList(List fa) { + if (fa.length < 2) { + throw ArgumentError( + 'List must have at least two elements: a label and a minimum value.'); + } + + final min = int.tryParse(fa[1]); + if (min == null) { + throw ArgumentError( + 'Second element must be a valid integer representing the minimum value.'); + } + + int? max; + if (fa.length > 2) { + max = int.tryParse(fa[2]); + } + + return RangeAmount(min, max); + } + + factory RangeAmount.empty() { + return RangeAmount(0, null); + } + + bool isRange() { + return maximum != null ? true : false; + } + + @override + String toString() { + if (maximum != null) { + return '$minimum - $maximum'; + } else { + return '$minimum'; + } + } +} diff --git a/lib/data/models/text_message.dart b/lib/data/models/text_message.dart index 8ce7ae0a..c1af504d 100644 --- a/lib/data/models/text_message.dart +++ b/lib/data/models/text_message.dart @@ -1,6 +1,6 @@ -import 'package:mostro_mobile/data/models/content.dart'; +import 'package:mostro_mobile/data/models/payload.dart'; -class TextMessage implements Content { +class TextMessage implements Payload { final String message; TextMessage({required this.message}); diff --git a/lib/data/repositories/auth_repository.dart b/lib/data/repositories/auth_repository.dart index b52aca4b..7bfe9f6a 100644 --- a/lib/data/repositories/auth_repository.dart +++ b/lib/data/repositories/auth_repository.dart @@ -1,6 +1,6 @@ -import '../../core/utils/auth_utils.dart'; -import '../../core/utils/nostr_utils.dart'; -import '../../core/utils/biometrics_helper.dart'; +import '../../shared/utils/auth_utils.dart'; +import '../../shared/utils/nostr_utils.dart'; +import '../../shared/utils/biometrics_helper.dart'; class AuthRepository { final BiometricsHelper _biometricsHelper; diff --git a/lib/data/repositories/mostro_repository.dart b/lib/data/repositories/mostro_repository.dart index b9fb4e1c..4172cb0c 100644 --- a/lib/data/repositories/mostro_repository.dart +++ b/lib/data/repositories/mostro_repository.dart @@ -1,39 +1,67 @@ import 'dart:async'; -import 'package:dart_nostr/nostr/model/request/filter.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/data/models/order.dart'; +import 'package:mostro_mobile/data/models/session.dart'; import 'package:mostro_mobile/data/repositories/order_repository_interface.dart'; +import 'package:mostro_mobile/services/mostro_service.dart'; class MostroRepository implements OrderRepository { + final MostroService _mostroService; + final Map _messages = {}; - // final NostrService _nostrService; + final Map> _subscriptions = {}; - final StreamController> _eventStreamController = - StreamController.broadcast(); + final Map _orderExpirations = {}; + final StreamController> _streamController = + StreamController>.broadcast(); - final Map _orders = {}; - //final SecureStorageManager _secureStorageManager; + MostroRepository(this._mostroService); - MostroRepository(); + Stream> get ordersStream => _streamController.stream; - void subscribe(NostrFilter filter) { - //_nostrService.subscribeToEvents(filter).listen((event) async { + MostroMessage? getOrderById(String orderId) { + return _messages[orderId]; + } - // final recipient = event.recipient; + Stream _subscribe(Session session) { + return _mostroService.subscribe(session)..listen((m) { + _messages[m.requestId!] = m; + }); + } - // final session = await _secureStorageManager.loadSession(recipient!); + Future> takeSellOrder( + String orderId, int? amount, String? lnAddress) async { + final session = + await _mostroService.takeSellOrder(orderId, amount, lnAddress); + return _subscribe(session); + } - // final form = await decryptMessage( - // event.content!, session!.privateKey, event.pubkey); + Future> takeBuyOrder( + String orderId, int? amount) async { + final session = await _mostroService.takeBuyOrder(orderId, amount); + return _subscribe(session); + } - //final message = MostroMessage.deserialized(form); - //_orders[message.requestId!] = message; - //_eventStreamController.add(_orders.values.toList()); - //}); + Future sendInvoice(String orderId, String invoice) async { + await _mostroService.sendInvoice(orderId, invoice); } - MostroMessage? getOrder(String orderId) { - return _orders[orderId]; + Future> publishOrder(MostroMessage order) async { + final session = await _mostroService.publishOrder(order); + return _subscribe(session); } + void cancelOrder(String orderId) async { + await _mostroService.cancelOrder(orderId); + } + @override + void dispose() { + for (final subscription in _subscriptions.values) { + subscription.cancel(); + } + _subscriptions.clear(); + _orderExpirations.clear(); + _streamController.close(); + } } diff --git a/lib/data/repositories/open_orders_repository.dart b/lib/data/repositories/open_orders_repository.dart index 866007d9..a20dede5 100644 --- a/lib/data/repositories/open_orders_repository.dart +++ b/lib/data/repositories/open_orders_repository.dart @@ -5,31 +5,31 @@ import 'package:mostro_mobile/data/models/nostr_event.dart'; import 'package:mostro_mobile/data/repositories/order_repository_interface.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; +const orderEventKind = 38383; +const orderFilterDurationHours = 48; + class OpenOrdersRepository implements OrderRepository { final NostrService _nostrService; final StreamController> _eventStreamController = StreamController.broadcast(); final Map _events = {}; - - Stream> get eventsStream => _eventStreamController.stream; + StreamSubscription? _subscription; OpenOrdersRepository(this._nostrService); - StreamSubscription? _subscription; - /// Subscribes to events matching the given filter. - /// - /// @param filter The filter criteria for events. - /// @throws ArgumentError if filter is null - void subscribe(NostrFilter filter) { - ArgumentError.checkNotNull(filter, 'filter'); - - // Cancel existing subscription if any + void subscribeToOrders() { _subscription?.cancel(); + final filterTime = + DateTime.now().subtract(Duration(hours: orderFilterDurationHours)); + var filter = NostrFilter( + kinds: const [orderEventKind], + since: filterTime, + ); + _subscription = _nostrService.subscribeToEvents(filter).listen((event) { - final key = '${event.kind}-${event.pubkey}-${event.orderId}'; - _events[key] = event; + _events[event.orderId!] = event; _eventStreamController.add(_events.values.toList()); }, onError: (error) { // Log error and optionally notify listeners @@ -37,9 +37,18 @@ class OpenOrdersRepository implements OrderRepository { }); } + @override void dispose() { _subscription?.cancel(); _eventStreamController.close(); _events.clear(); } + + NostrEvent? getOrderById(String orderId) { + return _events[orderId]; + } + + Stream> get eventsStream => _eventStreamController.stream; + + List get currentEvents => _events.values.toList(); } diff --git a/lib/data/repositories/order_repository_interface.dart b/lib/data/repositories/order_repository_interface.dart index cb38e3b5..b5ca6c8b 100644 --- a/lib/data/repositories/order_repository_interface.dart +++ b/lib/data/repositories/order_repository_interface.dart @@ -1,3 +1,18 @@ abstract class OrderRepository { + void dispose(); +} + +enum MessageKind { + openOrder(8383), + directMessage(1059); + + final int kind; + const MessageKind(this.kind); +} + +class OrderFilter { + final List messageKinds; + OrderFilter({required this.messageKinds}); + } diff --git a/lib/data/repositories/secure_storage_manager.dart b/lib/data/repositories/secure_storage_manager.dart index 53101118..9b36a1ff 100644 --- a/lib/data/repositories/secure_storage_manager.dart +++ b/lib/data/repositories/secure_storage_manager.dart @@ -2,7 +2,7 @@ import 'dart:async'; import 'dart:convert'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:mostro_mobile/data/models/session.dart'; -import 'package:mostro_mobile/core/utils/nostr_utils.dart'; +import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; class SecureStorageManager { Timer? _cleanupTimer; diff --git a/lib/features/add_order/notifiers/add_order_notifier.dart b/lib/features/add_order/notifiers/add_order_notifier.dart new file mode 100644 index 00000000..861ee625 --- /dev/null +++ b/lib/features/add_order/notifiers/add_order_notifier.dart @@ -0,0 +1,94 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart'; +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/data/repositories/mostro_repository.dart'; +import 'package:mostro_mobile/shared/providers/navigation_notifier_provider.dart'; +import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; + +class AddOrderNotifier extends StateNotifier { + final MostroRepository _orderRepository; + final Ref ref; + final String uuid; + StreamSubscription? _orderSubscription; + + AddOrderNotifier(this._orderRepository, this.uuid, this.ref) + : super(MostroMessage(action: Action.newOrder)); + + Future submitOrder(String fiatCode, int fiatAmount, int satsAmount, + String paymentMethod, OrderType orderType, + {String? lnAddress}) async { + final order = Order( + fiatAmount: fiatAmount, + fiatCode: fiatCode, + kind: orderType, + paymentMethod: paymentMethod, + buyerInvoice: lnAddress, + ); + final message = MostroMessage( + action: Action.newOrder, requestId: null, payload: order); + + try { + final stream = await _orderRepository.publishOrder(message); + _orderSubscription = stream.listen((order) { + state = order; + _handleOrderUpdate(); + }); + } catch (e) { + _handleError(e); + } + } + + void _handleError(Object err) { + ref.read(notificationProvider.notifier).showInformation(err.toString()); + } + + void _handleOrderUpdate() { + final navProvider = ref.read(navigationProvider.notifier); + final notifProvider = ref.read(notificationProvider.notifier); + + switch (state.action) { + case Action.newOrder: + navProvider.go('/order_confirmed/${state.requestId!}'); + break; + case Action.payInvoice: + navProvider.go('/pay_invoice/${state.requestId!}'); + break; + case Action.outOfRangeSatsAmount: + notifProvider.showInformation('Sats out of range'); + break; + case Action.outOfRangeFiatAmount: + notifProvider.showInformation('Fiant amount out of range'); + break; + case Action.waitingSellerToPay: + notifProvider.showInformation('Waiting Seller to pay'); + break; + case Action.waitingBuyerInvoice: + notifProvider.showInformation('Waiting Buy Invoice'); + break; + case Action.buyerTookOrder: + notifProvider.showInformation('Buyer took order'); + break; + case Action.fiatSentOk: + case Action.holdInvoicePaymentSettled: + case Action.rate: + case Action.rateReceived: + case Action.canceled: + case Action.cooperativeCancelInitiatedByYou: + case Action.disputeInitiatedByYou: + case Action.adminSettled: + default: + notifProvider.showInformation(state.action.toString()); + break; + } + } + + @override + void dispose() { + _orderSubscription?.cancel(); + print('Disposed!'); + super.dispose(); + } +} diff --git a/lib/features/add_order/providers/add_order_notifier_provider.dart b/lib/features/add_order/providers/add_order_notifier_provider.dart new file mode 100644 index 00000000..ed7754c9 --- /dev/null +++ b/lib/features/add_order/providers/add_order_notifier_provider.dart @@ -0,0 +1,14 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/features/add_order/notifiers/add_order_notifier.dart'; +import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; + +// This provider tracks the currently selected OrderType tab +final orderTypeProvider = StateProvider((ref) => OrderType.sell); + +final addOrderNotifierProvider = + StateNotifierProvider.family((ref, uuid) { + final mostroService = ref.watch(mostroRepositoryProvider); + return AddOrderNotifier(mostroService, uuid, ref); +}); diff --git a/lib/presentation/add_order/screens/add_order_screen.dart b/lib/features/add_order/screens/add_order_screen.dart similarity index 57% rename from lib/presentation/add_order/screens/add_order_screen.dart rename to lib/features/add_order/screens/add_order_screen.dart index c570cb96..f6c97706 100644 --- a/lib/presentation/add_order/screens/add_order_screen.dart +++ b/lib/features/add_order/screens/add_order_screen.dart @@ -1,19 +1,16 @@ import 'package:bitcoin_icons/bitcoin_icons.dart'; import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:heroicons/heroicons.dart'; -import 'package:mostro_mobile/core/theme/app_theme.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; import 'package:mostro_mobile/data/models/enums/order_type.dart'; -import 'package:mostro_mobile/generated/l10n.dart'; -import 'package:mostro_mobile/presentation/add_order/bloc/add_order_bloc.dart'; -import 'package:mostro_mobile/presentation/add_order/bloc/add_order_event.dart'; -import 'package:mostro_mobile/presentation/add_order/bloc/add_order_state.dart'; +import 'package:mostro_mobile/features/add_order/providers/add_order_notifier_provider.dart'; import 'package:mostro_mobile/presentation/widgets/currency_dropdown.dart'; import 'package:mostro_mobile/presentation/widgets/currency_text_field.dart'; -import 'package:mostro_mobile/providers/exchange_service_provider.dart'; -import 'package:mostro_mobile/providers/riverpod_providers.dart'; +import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart'; +import 'package:uuid/uuid.dart'; class AddOrderScreen extends ConsumerWidget { final _formKey = GlobalKey(); @@ -27,111 +24,59 @@ class AddOrderScreen extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final orderRepo = ref.watch(mostroServiceProvider); - - return BlocProvider( - create: (context) => AddOrderBloc(orderRepo), - child: BlocBuilder( - builder: (context, state) { - return Scaffold( - backgroundColor: AppTheme.dark1, - appBar: AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: IconButton( - icon: - const HeroIcon(HeroIcons.arrowLeft, color: AppTheme.cream1), - onPressed: () => Navigator.of(context).pop(), - ), - title: Text( - 'NEW ORDER', - style: TextStyle( - color: AppTheme.cream1, - fontFamily: GoogleFonts.robotoCondensed().fontFamily, - ), + return Scaffold( + backgroundColor: AppTheme.dark1, + appBar: AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const HeroIcon(HeroIcons.arrowLeft, color: AppTheme.cream1), + onPressed: () => context.go('/'), + ), + title: Text( + 'NEW ORDER', + style: TextStyle( + color: AppTheme.cream1, + fontFamily: GoogleFonts.robotoCondensed().fontFamily, + ), + ), + ), + body: Column( + children: [ + Expanded( + child: Container( + margin: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: AppTheme.dark2, + borderRadius: BorderRadius.circular(20), ), + child: _buildContent(context, ref), ), - body: Column( - children: [ - Expanded( - child: Container( - margin: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.dark2, - borderRadius: BorderRadius.circular(20), - ), - child: _buildContent(context, state, ref), - ), - ), - ], - ), - ); - }, + ), + ], ), ); } Widget _buildContent( - BuildContext context, AddOrderState state, WidgetRef ref) { - if (state.status == AddOrderStatus.submitting) { - return Center(child: CircularProgressIndicator()); - } else if (state.status == AddOrderStatus.submitted) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text(S.of(context).new_order('24'), - style: TextStyle(fontSize: 18, color: AppTheme.cream1), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => - Navigator.of(context).pop(), - child: const Text('Back to Home'), - ), - ], - ), - ); - } else if (state.status == AddOrderStatus.failure) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - 'Order failed: ${state.errorMessage}', - style: const TextStyle(fontSize: 18, color: Colors.redAccent), - textAlign: TextAlign.center, - ), - const SizedBox(height: 16), - ElevatedButton( - onPressed: () => - Navigator.of(context).pop(), - child: const Text('Back to Home'), - ), - ], - ), - ); - } else { - return Form( - key: _formKey, - child: Column( - children: [ - _buildTabs(context, state), - Expanded( - child: state.currentType == OrderType.sell - ? _buildSellForm(context, ref) - : _buildBuyForm(context, ref), - ), - ], - ), - ); - } + BuildContext context, WidgetRef ref) { + final orderType = ref.watch(orderTypeProvider); + return Form( + key: _formKey, + child: Column( + children: [ + _buildTabs(context, ref, orderType), + Expanded( + child: orderType == OrderType.sell + ? _buildSellForm(context, ref) + : _buildBuyForm(context, ref), + ), + ], + ), + ); } - Widget _buildTabs(BuildContext context, AddOrderState state) { + Widget _buildTabs(BuildContext context, WidgetRef ref, OrderType orderType) { return Container( decoration: const BoxDecoration( color: AppTheme.dark1, @@ -143,23 +88,24 @@ class AddOrderScreen extends ConsumerWidget { child: Row( children: [ Expanded( - child: _buildTab(context, "SELL", - state.currentType == OrderType.sell, OrderType.sell), + child: _buildTab(context, ref, "SELL", orderType == OrderType.sell, + OrderType.sell), ), Expanded( - child: _buildTab(context, "BUY", state.currentType == OrderType.buy, - OrderType.buy), + child: _buildTab( + context, ref, "BUY", orderType == OrderType.buy, OrderType.buy), ), ], ), ); } - Widget _buildTab( - BuildContext context, String text, bool isActive, OrderType type) { + Widget _buildTab(BuildContext context, WidgetRef ref, String text, + bool isActive, OrderType type) { return GestureDetector( onTap: () { - context.read().add(ChangeOrderType(type)); + // Update the local orderType state + ref.read(orderTypeProvider.notifier).state = type; }, child: Container( padding: const EdgeInsets.symmetric(vertical: 12), @@ -274,9 +220,7 @@ class AddOrderScreen extends ConsumerWidget { const SizedBox(width: 8), Switch( value: false, - onChanged: (value) { - // Update the state in the bloc if necessary - }, + onChanged: (value) {}, ), ], ); @@ -288,7 +232,7 @@ class AddOrderScreen extends ConsumerWidget { mainAxisAlignment: MainAxisAlignment.end, children: [ TextButton( - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.go('/'), child: const Text('CANCEL', style: TextStyle(color: AppTheme.red2)), ), const SizedBox(width: 16), @@ -311,13 +255,18 @@ class AddOrderScreen extends ConsumerWidget { final selectedFiatCode = ref.read(selectedFiatCodeProvider); if (_formKey.currentState?.validate() ?? false) { - context.read().add(SubmitOrder( - fiatCode: selectedFiatCode ?? '', // Use selected fiat code - fiatAmount: int.tryParse(_fiatAmountController.text) ?? 0, - satsAmount: int.tryParse(_satsAmountController.text) ?? 0, - paymentMethod: _paymentMethodController.text, - orderType: orderType, - )); + // Generate a unique temporary ID for this new order + final uuid = Uuid(); + final tempOrderId = uuid.v4(); + final notifier = ref.read(addOrderNotifierProvider(tempOrderId).notifier); + + notifier.submitOrder( + selectedFiatCode ?? '', // Use selected fiat code + int.tryParse(_fiatAmountController.text) ?? 0, + int.tryParse(_satsAmountController.text) ?? 0, + _paymentMethodController.text, + orderType, + ); } } } diff --git a/lib/features/add_order/screens/order_confirmation_screen.dart b/lib/features/add_order/screens/order_confirmation_screen.dart new file mode 100644 index 00000000..081dad9d --- /dev/null +++ b/lib/features/add_order/screens/order_confirmation_screen.dart @@ -0,0 +1,42 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/features/take_order/widgets/order_app_bar.dart'; +import 'package:mostro_mobile/generated/l10n.dart'; +import 'package:mostro_mobile/shared/widgets/custom_card.dart'; + +class OrderConfirmationScreen extends ConsumerWidget { + final String orderId; + + const OrderConfirmationScreen({super.key, required this.orderId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return Scaffold( + backgroundColor: AppTheme.dark1, + appBar: OrderAppBar( + title: + 'Order Confirmed'), + body:CustomCard( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + S.of(context).new_order('24'), + style: TextStyle(fontSize: 18, color: AppTheme.cream1), + textAlign: TextAlign.center, + ), + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => context.go('/'), + child: const Text('Back to Home'), + ), + ], + ), + ), + ),); + } +} diff --git a/lib/features/add_order/widgets/buy_form_widget.dart b/lib/features/add_order/widgets/buy_form_widget.dart new file mode 100644 index 00000000..c6687483 --- /dev/null +++ b/lib/features/add_order/widgets/buy_form_widget.dart @@ -0,0 +1,119 @@ +import 'package:bitcoin_icons/bitcoin_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/presentation/widgets/currency_dropdown.dart'; +import 'package:mostro_mobile/presentation/widgets/currency_text_field.dart'; + +class BuyFormWidget extends HookConsumerWidget { + const BuyFormWidget({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + + final fiatAmountController = useTextEditingController(); + final satsAmountController = useTextEditingController(); + final paymentMethodController = useTextEditingController(); + final lightningInvoiceController = useTextEditingController(); + + return SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Make sure your order is below 20K sats', + style: TextStyle(color: AppTheme.grey2)), + const SizedBox(height: 16), + CurrencyDropdown(label: 'Fiat code'), + const SizedBox(height: 16), + CurrencyTextField( + controller: fiatAmountController, label: 'Fiat amount'), + const SizedBox(height: 16), + _buildFixedToggle(), + const SizedBox(height: 16), + _buildTextField('Sats amount', satsAmountController, + suffix: Icon(BitcoinIcons.satoshi_v1_outline).icon), + const SizedBox(height: 16), + _buildTextField('Lightning Invoice without an amount', + lightningInvoiceController), + const SizedBox(height: 16), + _buildTextField('Payment method', paymentMethodController), + const SizedBox(height: 32), + _buildActionButtons(context, ref, formKey), + ], + ), + ); + } + + Widget _buildTextField(String label, TextEditingController controller, + {IconData? suffix}) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppTheme.dark1, + borderRadius: BorderRadius.circular(8), + ), + child: TextFormField( + controller: controller, + style: const TextStyle(color: AppTheme.cream1), + decoration: InputDecoration( + border: InputBorder.none, + labelText: label, + labelStyle: const TextStyle(color: AppTheme.grey2), + suffixIcon: + suffix != null ? Icon(suffix, color: AppTheme.grey2) : null, + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a value'; + } + return null; + }, + ), + ); + } + + + Widget _buildFixedToggle() { + return Row( + children: [ + const Text('Fixed', style: TextStyle(color: AppTheme.cream1)), + const SizedBox(width: 8), + Switch( + value: false, + onChanged: (value) {}, + ), + ], + ); + } + + Widget _buildActionButtons( + BuildContext context, WidgetRef ref, GlobalKey formKey) { + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () => context.go('/'), + child: const Text('CANCEL', style: TextStyle(color: AppTheme.red2)), + ), + const SizedBox(width: 16), + ElevatedButton( + onPressed: () { + if (formKey.currentState?.validate() ?? false) { + //_submitOrder(context, ref, OrderType.buy); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.mostroGreen, + ), + child: const Text('SUBMIT'), + ), + ], + ); + } + + +} diff --git a/lib/features/add_order/widgets/sell_form_widget.dart b/lib/features/add_order/widgets/sell_form_widget.dart new file mode 100644 index 00000000..aa14d222 --- /dev/null +++ b/lib/features/add_order/widgets/sell_form_widget.dart @@ -0,0 +1,82 @@ +import 'package:bitcoin_icons/bitcoin_icons.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/presentation/widgets/currency_dropdown.dart'; +import 'package:mostro_mobile/presentation/widgets/currency_text_field.dart'; + +class SellFormWidget extends HookConsumerWidget { + const SellFormWidget({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + + final fiatAmountController = useTextEditingController(); + final satsAmountController = useTextEditingController(); + final paymentMethodController = useTextEditingController(); + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text('Make sure your order is below 20K sats', + style: TextStyle(color: AppTheme.grey2)), + const SizedBox(height: 16), + CurrencyDropdown(label: 'Fiat code'), + const SizedBox(height: 16), + CurrencyTextField( + controller: fiatAmountController, label: 'Fiat amount'), + const SizedBox(height: 16), + _buildFixedToggle(), + const SizedBox(height: 16), + _buildTextField('Sats amount', satsAmountController, + suffix: Icon(BitcoinIcons.satoshi_v1_outline).icon), + const SizedBox(height: 16), + _buildTextField('Payment method', paymentMethodController), + const SizedBox(height: 32), + ], + ); + } + + Widget _buildTextField(String label, TextEditingController controller, + {IconData? suffix}) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppTheme.dark1, + borderRadius: BorderRadius.circular(8), + ), + child: TextFormField( + controller: controller, + style: const TextStyle(color: AppTheme.cream1), + decoration: InputDecoration( + border: InputBorder.none, + labelText: label, + labelStyle: const TextStyle(color: AppTheme.grey2), + suffixIcon: + suffix != null ? Icon(suffix, color: AppTheme.grey2) : null, + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a value'; + } + return null; + }, + ), + ); + } + + Widget _buildFixedToggle() { + return Row( + children: [ + const Text('Fixed', style: TextStyle(color: AppTheme.cream1)), + const SizedBox(width: 8), + Switch( + value: false, + onChanged: (value) {}, + ), + ], + ); + } + +} diff --git a/lib/features/auth/notifiers/auth_notifier.dart b/lib/features/auth/notifiers/auth_notifier.dart new file mode 100644 index 00000000..4736598d --- /dev/null +++ b/lib/features/auth/notifiers/auth_notifier.dart @@ -0,0 +1,86 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; +import 'package:mostro_mobile/data/repositories/auth_repository.dart'; +import 'auth_state.dart'; + +class AuthNotifier extends StateNotifier { + final AuthRepository authRepository; + + AuthNotifier(this.authRepository) : super(AuthInitial()); + + Future checkAuth() async { + state = AuthLoading(); + try { + final isRegistered = await authRepository.isRegistered(); + if (isRegistered) { + state = AuthUnauthenticated(); + } else { + state = AuthUnregistered(); + } + } catch (e) { + state = AuthFailure(e.toString()); + } + } + + Future register({ + required String privateKey, + required String password, + required bool useBiometrics, + }) async { + state = AuthLoading(); + try { + if (privateKey.startsWith('nsec')) { + privateKey = NostrUtils.decodeNsecKeyToPrivateKey(privateKey); + } + await authRepository.register(privateKey, password, useBiometrics); + state = AuthRegistrationSuccess(); + } catch (e, stackTrace) { + print('Error during registration: $e'); + print('Stack trace: $stackTrace'); + state = AuthFailure(e.toString()); + } + } + + Future login({required String password}) async { + state = AuthLoading(); + try { + final isAuthenticated = await authRepository.login(password); + if (isAuthenticated) { + state = AuthAuthenticated(); + } else { + state = const AuthFailure( + 'Invalid PIN or biometric authentication failed'); + } + } catch (e) { + state = AuthFailure(e.toString()); + } + } + + Future logout() async { + state = AuthLoading(); + try { + await authRepository.logout(); + state = AuthUnregistered(); + } catch (e) { + state = AuthFailure(e.toString()); + } + } + + Future generateKey() async { + try { + final newPrivateKey = await authRepository.generateNewIdentity(); + state = AuthKeyGenerated(NostrUtils.encodePrivateKeyToNsec(newPrivateKey)); + } catch (e) { + state = AuthFailure(e.toString()); + } + } + + Future checkBiometrics() async { + try { + final isAvailable = await authRepository.isBiometricsAvailable(); + state = AuthBiometricsAvailability(isAvailable); + } catch (e) { + state = AuthFailure(e.toString()); + } + } +} diff --git a/lib/presentation/auth/bloc/auth_state.dart b/lib/features/auth/notifiers/auth_state.dart similarity index 100% rename from lib/presentation/auth/bloc/auth_state.dart rename to lib/features/auth/notifiers/auth_state.dart diff --git a/lib/features/auth/providers/auth_notifier_provider.dart b/lib/features/auth/providers/auth_notifier_provider.dart new file mode 100644 index 00000000..55e5b6fc --- /dev/null +++ b/lib/features/auth/providers/auth_notifier_provider.dart @@ -0,0 +1,24 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/repositories/auth_repository.dart'; +import 'package:mostro_mobile/features/auth/notifiers/auth_notifier.dart'; +import 'package:mostro_mobile/features/auth/notifiers/auth_state.dart'; +import 'package:mostro_mobile/shared/utils/biometrics_helper.dart'; + +final biometricsHelperProvider = Provider((ref) { + throw UnimplementedError(); +}); + +final authRepositoryProvider = Provider( + (ref) => AuthRepository( + biometricsHelper: ref.read(biometricsHelperProvider), + ), +); + +final authNotifierProvider = StateNotifierProvider( + (ref) => AuthNotifier(ref.read(authRepositoryProvider)), +); + +final obscurePrivateKeyProvider = StateProvider((ref) => true); +final obscurePinProvider = StateProvider((ref) => true); +final obscureConfirmPinProvider = StateProvider((ref) => true); +final useBiometricsProvider = StateProvider((ref) => false); \ No newline at end of file diff --git a/lib/features/auth/screens/login_screen.dart b/lib/features/auth/screens/login_screen.dart new file mode 100644 index 00000000..1bf32176 --- /dev/null +++ b/lib/features/auth/screens/login_screen.dart @@ -0,0 +1,86 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mostro_mobile/features/auth/notifiers/auth_state.dart'; +import 'package:mostro_mobile/features/auth/providers/auth_notifier_provider.dart'; +import 'package:mostro_mobile/presentation/widgets/custom_button.dart'; + +class LoginScreen extends HookConsumerWidget { + const LoginScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final formKey = useMemoized(() => GlobalKey()); + final pinController = useTextEditingController(); + + ref.listen(authNotifierProvider, (previous, state) { + if (state is AuthAuthenticated) { + context.go('/'); + } else if (state is AuthFailure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.error)), + ); + } + }); + + return Scaffold( + appBar: AppBar( + title: const Text('Login', style: TextStyle(color: Colors.white)), + backgroundColor: Colors.transparent, + elevation: 0, + ), + backgroundColor: const Color(0xFF1D212C), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + TextFormField( + controller: pinController, + decoration: const InputDecoration( + labelText: 'PIN', + labelStyle: TextStyle(color: Colors.white70), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white70), + ), + ), + style: const TextStyle(color: Colors.white), + keyboardType: TextInputType.number, + obscureText: true, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter your PIN'; + } + return null; + }, + ), + const SizedBox(height: 24), + CustomButton( + text: 'Login', + onPressed: () => _onLogin(context, ref, formKey, pinController), + ), + ], + ), + ), + ), + ); + } + + void _onLogin( + BuildContext context, + WidgetRef ref, + GlobalKey formKey, + TextEditingController pinController, + ) { + if (formKey.currentState!.validate()) { + final pin = pinController.text; + + ref.read(authNotifierProvider.notifier).login( + password: pin, + ); + } + } +} diff --git a/lib/features/auth/screens/register_screen.dart b/lib/features/auth/screens/register_screen.dart new file mode 100644 index 00000000..0ac1bbbf --- /dev/null +++ b/lib/features/auth/screens/register_screen.dart @@ -0,0 +1,229 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:mostro_mobile/features/auth/notifiers/auth_state.dart'; +import 'package:mostro_mobile/features/auth/providers/auth_notifier_provider.dart'; +import 'package:mostro_mobile/presentation/widgets/custom_button.dart'; +import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; + +class RegisterScreen extends HookConsumerWidget { + const RegisterScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + // Initialize controllers using hooks + final formKey = useMemoized(() => GlobalKey()); + final privateKeyController = useTextEditingController(); + final pinController = useTextEditingController(); + final confirmPinController = useTextEditingController(); + + // UI-specific state managed via StateProviders + final obscurePrivateKey = ref.watch(obscurePrivateKeyProvider); + final obscurePin = ref.watch(obscurePinProvider); + final obscureConfirmPin = ref.watch(obscureConfirmPinProvider); + final useBiometrics = ref.watch(useBiometricsProvider); + + // Listen to AuthState changes for side effects + ref.listen(authNotifierProvider, (previous, state) { + if (state is AuthKeyGenerated) { + privateKeyController.text = NostrUtils.nsecToHex(state.privateKey); + } else if (state is AuthRegistrationSuccess) { + // Navigate to home after successful registration + context.go('/'); + } else if (state is AuthFailure) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(state.error)), + ); + } + }); + + // Trigger biometrics check on first build + useEffect(() { + ref.read(authNotifierProvider.notifier).checkBiometrics(); + return null; + }, []); + + return Scaffold( + appBar: AppBar( + title: const Text('Register', style: TextStyle(color: Colors.white)), + backgroundColor: Colors.transparent, + elevation: 0, + ), + backgroundColor: const Color(0xFF1D212C), + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Form( + key: formKey, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + // Private Key Field + TextFormField( + controller: privateKeyController, + decoration: InputDecoration( + labelText: 'Private Key (nsec or hex)', + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white70), + ), + suffixIcon: IconButton( + icon: Icon( + obscurePrivateKey + ? Icons.visibility_off + : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + ref.read(obscurePrivateKeyProvider.notifier).state = + !obscurePrivateKey; + }, + ), + ), + style: const TextStyle(color: Colors.white), + obscureText: obscurePrivateKey, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a private key'; + } + if (!value.startsWith('nsec') && + !RegExp(r'^[0-9a-fA-F]{64}$').hasMatch(value)) { + return 'Invalid private key format'; + } + return null; + }, + ), + const SizedBox(height: 16), + + // PIN Field + TextFormField( + controller: pinController, + decoration: InputDecoration( + labelText: 'PIN', + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white70), + ), + suffixIcon: IconButton( + icon: Icon( + obscurePin ? Icons.visibility_off : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + ref.read(obscurePinProvider.notifier).state = !obscurePin; + }, + ), + ), + style: const TextStyle(color: Colors.white), + keyboardType: TextInputType.number, + obscureText: obscurePin, + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a PIN'; + } + if (value.length < 4) { + return 'PIN must be at least 4 digits'; + } + return null; + }, + ), + const SizedBox(height: 16), + + // Confirm PIN Field + TextFormField( + controller: confirmPinController, + decoration: InputDecoration( + labelText: 'Confirm PIN', + labelStyle: const TextStyle(color: Colors.white70), + enabledBorder: const UnderlineInputBorder( + borderSide: BorderSide(color: Colors.white70), + ), + suffixIcon: IconButton( + icon: Icon( + obscureConfirmPin + ? Icons.visibility_off + : Icons.visibility, + color: Colors.white70, + ), + onPressed: () { + ref.read(obscureConfirmPinProvider.notifier).state = + !obscureConfirmPin; + }, + ), + ), + style: const TextStyle(color: Colors.white), + keyboardType: TextInputType.number, + obscureText: obscureConfirmPin, + validator: (value) { + if (value != pinController.text) { + return 'PINs do not match'; + } + return null; + }, + ), + const SizedBox(height: 24), + + // Biometrics Toggle (if available) + Consumer(builder: (context, ref, _) { + final authState = ref.watch(authNotifierProvider); + bool biometricsAvailable = false; + if (authState is AuthBiometricsAvailability) { + biometricsAvailable = authState.isAvailable; + } + return biometricsAvailable + ? SwitchListTile( + title: const Text('Use Biometrics', + style: TextStyle(color: Colors.white)), + value: useBiometrics, + onChanged: (bool value) { + ref.read(useBiometricsProvider.notifier).state = + value; + }, + activeColor: Colors.green, + ) + : const SizedBox.shrink(); + }), + const SizedBox(height: 24), + + // Register Button + CustomButton( + text: 'Register', + onPressed: () => _onRegister(context, ref, formKey, + privateKeyController, pinController, useBiometrics), + ), + const SizedBox(height: 16), + + // Generate New Key Button + CustomButton( + text: 'Generate New Key', + onPressed: () => + ref.read(authNotifierProvider.notifier).generateKey(), + ), + ], + ), + ), + ), + ); + } + + /// Handles the registration process + void _onRegister( + BuildContext context, + WidgetRef ref, + GlobalKey formKey, + TextEditingController privateKeyController, + TextEditingController pinController, + bool useBiometrics, + ) { + if (formKey.currentState!.validate()) { + final privateKey = privateKeyController.text; + final pin = pinController.text; + + ref.read(authNotifierProvider.notifier).register( + privateKey: privateKey, + password: pin, + useBiometrics: useBiometrics, + ); + } + } +} diff --git a/lib/presentation/auth/screens/welcome_screen.dart b/lib/features/auth/screens/welcome_screen.dart similarity index 89% rename from lib/presentation/auth/screens/welcome_screen.dart rename to lib/features/auth/screens/welcome_screen.dart index a35d93d1..f135d64d 100644 --- a/lib/presentation/auth/screens/welcome_screen.dart +++ b/lib/features/auth/screens/welcome_screen.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; -import '../../../core/theme/app_theme.dart'; -import '../../widgets/custom_button.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/presentation/widgets/custom_button.dart'; class WelcomeScreen extends StatelessWidget { const WelcomeScreen({super.key}); @@ -42,7 +43,7 @@ class WelcomeScreen extends StatelessWidget { CustomButton( text: 'REGISTER', onPressed: () { - Navigator.pushNamed(context, '/register'); + context.go('/register'); }, ), const SizedBox(height: 16), @@ -56,7 +57,7 @@ class WelcomeScreen extends StatelessWidget { ), ), onPressed: () { - Navigator.pushReplacementNamed(context, '/'); + context.go('/'); }, ), const Spacer(), diff --git a/lib/features/home/notifiers/home_notifier.dart b/lib/features/home/notifiers/home_notifier.dart new file mode 100644 index 00000000..13ada4eb --- /dev/null +++ b/lib/features/home/notifiers/home_notifier.dart @@ -0,0 +1,76 @@ +import 'dart:async'; +import 'package:dart_nostr/nostr/model/event/event.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/nostr_event.dart'; +import 'package:mostro_mobile/data/repositories/open_orders_repository.dart'; +import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; +import 'home_state.dart'; + +class HomeNotifier extends AsyncNotifier { + StreamSubscription>? _subscription; + OpenOrdersRepository? _repository; + + @override + Future build() async { + state = const AsyncLoading(); + + _repository = ref.watch(orderRepositoryProvider); + + _repository!.subscribeToOrders(); + + _subscription = _repository!.eventsStream.listen((orders) { + final orderType = state.value?.orderType ?? OrderType.sell; + final filteredOrders = + _filterOrders(orders, orderType); + state = AsyncData( + HomeState( + orderType: orderType, + filteredOrders: filteredOrders, + ), + ); + }, onError: (error) { + state = AsyncError(error, StackTrace.current); + }); + + ref.onDispose(() { + _subscription?.cancel(); + }); + + return HomeState( + orderType: OrderType.sell, + filteredOrders: [], + ); + } + + /// Refreshes the data by re-initializing the notifier. + Future refresh() async { + _subscription?.cancel(); + await build(); + } + + List _filterOrders(List orders, OrderType type) { + final currentState = state.value; + if (currentState == null) return []; + return orders + .where((order) => type == OrderType.buy + ? order.orderType == OrderType.buy + : order.orderType == OrderType.sell) + .where((order) => order.status == 'pending') + .toList(); + } + + void changeOrderType(OrderType type) { + final currentState = state.value; + if (currentState == null || _repository == null) return; + final allOrders = _repository!.currentEvents; + final filteredOrders = _filterOrders(allOrders, type); + + state = AsyncData( + currentState.copyWith( + orderType: type, + filteredOrders: filteredOrders, + ), + ); + } +} diff --git a/lib/features/home/notifiers/home_state.dart b/lib/features/home/notifiers/home_state.dart new file mode 100644 index 00000000..3fe08f7c --- /dev/null +++ b/lib/features/home/notifiers/home_state.dart @@ -0,0 +1,30 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; + +class HomeState { + final OrderType orderType; + final List filteredOrders; + final bool isLoading; + final String? error; + + HomeState({ + required this.orderType, + required this.filteredOrders, + this.isLoading = false, + this.error, + }); + + HomeState copyWith({ + OrderType? orderType, + List? filteredOrders, + bool? isLoading, + String? error, + }) { + return HomeState( + orderType: orderType ?? this.orderType, + filteredOrders: filteredOrders ?? this.filteredOrders, + isLoading: isLoading ?? this.isLoading, + error: error, + ); + } +} diff --git a/lib/features/home/providers/home_notifer_provider.dart b/lib/features/home/providers/home_notifer_provider.dart new file mode 100644 index 00000000..c1c53df0 --- /dev/null +++ b/lib/features/home/providers/home_notifer_provider.dart @@ -0,0 +1,10 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/features/home/notifiers/home_notifier.dart'; +import 'package:mostro_mobile/features/home/notifiers/home_state.dart'; + + +final homeNotifierProvider = + AsyncNotifierProvider( + HomeNotifier.new, +); + diff --git a/lib/features/home/screens/home_screen.dart b/lib/features/home/screens/home_screen.dart new file mode 100644 index 00000000..64d5d0bc --- /dev/null +++ b/lib/features/home/screens/home_screen.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/features/home/notifiers/home_notifier.dart'; +import 'package:mostro_mobile/features/home/providers/home_notifer_provider.dart'; +import 'package:mostro_mobile/features/home/notifiers/home_state.dart'; +import 'package:mostro_mobile/presentation/widgets/bottom_nav_bar.dart'; +import 'package:mostro_mobile/presentation/widgets/custom_app_bar.dart'; +import 'package:mostro_mobile/features/home/widgets/order_filter.dart'; +import 'package:mostro_mobile/features/home/widgets/order_list.dart'; + +class HomeScreen extends ConsumerWidget { + const HomeScreen({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final homeStateAsync = ref.watch(homeNotifierProvider); + final homeNotifier = ref.read(homeNotifierProvider.notifier); + + return homeStateAsync.when( + data: (homeState) { + return Scaffold( + backgroundColor: AppTheme.dark1, + appBar: const CustomAppBar(), + body: RefreshIndicator( + onRefresh: () async { + await homeNotifier.refresh(); + }, + child: Container( + margin: const EdgeInsets.fromLTRB(16, 16, 16, 16), + decoration: BoxDecoration( + color: AppTheme.dark2, + borderRadius: BorderRadius.circular(20), + ), + child: Column( + children: [ + _buildTabs(ref, homeState, homeNotifier), + _buildFilterButton(context, homeState), + const SizedBox(height: 6.0), + Expanded( + child: _buildOrderList(homeState), + ), + const BottomNavBar(), + ], + ), + ), + ), + ); + }, + loading: () => const Scaffold( + backgroundColor: AppTheme.dark1, + body: Center(child: CircularProgressIndicator()), + ), + error: (error, stack) => Scaffold( + backgroundColor: AppTheme.dark1, + body: Center( + child: Text( + 'Error: $error', + style: const TextStyle(color: AppTheme.cream1), + ), + ), + ), + ); + } + + Widget _buildTabs( + WidgetRef ref, HomeState homeState, HomeNotifier homeNotifier) { + return Container( + decoration: const BoxDecoration( + color: AppTheme.dark1, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20), + topRight: Radius.circular(20), + ), + ), + child: Row( + children: [ + Expanded( + child: _buildTab("BUY BTC", homeState.orderType == OrderType.sell, () { + homeNotifier.changeOrderType(OrderType.sell); + }), + ), + Expanded( + child: _buildTab("SELL BTC", homeState.orderType == OrderType.buy, () { + homeNotifier.changeOrderType(OrderType.buy); + }), + ), + ], + ), + ); + } + + Widget _buildTab(String text, bool isActive, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: isActive ? AppTheme.dark2 : AppTheme.dark1, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(isActive ? 20 : 0), + topRight: Radius.circular(isActive ? 20 : 0), + ), + ), + child: Text( + text, + textAlign: TextAlign.center, + style: TextStyle( + color: isActive ? AppTheme.mostroGreen : AppTheme.red1, + fontWeight: FontWeight.bold, + ), + ), + ), + ); + } + + Widget _buildFilterButton(BuildContext context, HomeState homeState) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), + child: Row( + children: [ + OutlinedButton.icon( + onPressed: () { + showModalBottomSheet( + 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: Colors.white), + label: const Text("FILTER", style: TextStyle(color: Colors.white)), + style: OutlinedButton.styleFrom( + side: const BorderSide(color: Colors.white), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + ), + const SizedBox(width: 8), + Text( + "${homeState.filteredOrders.length} offers", + style: const TextStyle(color: Colors.white), + ), + ], + ), + ); + } + + Widget _buildOrderList(HomeState homeState) { + if (homeState.filteredOrders.isEmpty) { + return const Center( + child: Text( + 'No orders available for this type', + style: TextStyle(color: Colors.white), + ), + ); + } + + return OrderList(orders: homeState.filteredOrders); + } +} diff --git a/lib/presentation/widgets/order_filter.dart b/lib/features/home/widgets/order_filter.dart similarity index 97% rename from lib/presentation/widgets/order_filter.dart rename to lib/features/home/widgets/order_filter.dart index 05fa1873..497eef87 100644 --- a/lib/presentation/widgets/order_filter.dart +++ b/lib/features/home/widgets/order_filter.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; import 'package:heroicons/heroicons.dart'; -import 'package:mostro_mobile/core/theme/app_theme.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; class OrderFilter extends StatelessWidget { const OrderFilter({super.key}); diff --git a/lib/presentation/widgets/order_list.dart b/lib/features/home/widgets/order_list.dart similarity index 85% rename from lib/presentation/widgets/order_list.dart rename to lib/features/home/widgets/order_list.dart index 4a165b81..24fd5602 100644 --- a/lib/presentation/widgets/order_list.dart +++ b/lib/features/home/widgets/order_list.dart @@ -1,6 +1,6 @@ import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:flutter/material.dart'; -import 'order_list_item.dart'; +import 'package:mostro_mobile/features/home/widgets/order_list_item.dart'; class OrderList extends StatelessWidget { final List orders; diff --git a/lib/features/home/widgets/order_list_item.dart b/lib/features/home/widgets/order_list_item.dart new file mode 100644 index 00000000..4b40c24e --- /dev/null +++ b/lib/features/home/widgets/order_list_item.dart @@ -0,0 +1,205 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/nostr_event.dart'; +import 'package:mostro_mobile/shared/widgets/custom_card.dart'; + +class OrderListItem extends StatelessWidget { + final NostrEvent order; + + const OrderListItem({super.key, required this.order}); + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + order.orderType == OrderType.buy + ? context.go('/take_buy/${order.orderId}') + : context.go('/take_sell/${order.orderId}'); + }, + child: CustomCard( + color: AppTheme.dark1, + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _buildHeader(context), + const SizedBox(height: 16), + _buildOrderDetails(context), + const SizedBox(height: 8), + ], + ), + ), + ); + } + + Widget _buildHeader(BuildContext context) { + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${order.name} ${order.rating?.totalRating ?? 0}/${order.rating?.maxRate ?? 5} (${order.rating?.totalReviews ?? 0})', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppTheme.cream1, + ), + ), + Text( + 'Time: ${order.expiration}', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppTheme.cream1, + ), + ), + ], + ); + } + + Widget _buildOrderDetails(BuildContext context) { + return Row( + children: [ + _getOrderOffering(context, order), + const SizedBox(width: 16), + Expanded( + flex: 4, + child: _buildPaymentMethod(context), + ), + ], + ); + } + + Widget _getOrderOffering(BuildContext context, NostrEvent order) { + String offering = order.orderType == OrderType.buy ? 'Buying' : 'Selling'; + String amountText = (order.amount != null && order.amount != '0') + ? ' ${order.amount!}' + : ''; + + return Expanded( + flex: 3, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + RichText( + text: TextSpan( + children: [ + _buildStyledTextSpan( + context, + offering, + amountText, + isValue: true, + isBold: true, + ), + TextSpan( + text: 'sats', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppTheme.cream1, + fontWeight: FontWeight.normal, + ), + ), + ], + ), + ), + const SizedBox(height: 8.0), + RichText( + text: TextSpan( + children: [ + _buildStyledTextSpan( + context, + 'for ', + '${order.fiatAmount}', + isValue: true, + isBold: true, + ), + TextSpan( + text: '${order.currency} ', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppTheme.cream1, + fontSize: 16.0, + ), + ), + TextSpan( + text: '(${order.premium}%)', + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppTheme.cream1, + fontSize: 16.0, + ), + ), + ], + ), + ), + ], + ), + ); + } + + Widget _buildPaymentMethod(BuildContext context) { + String method = order.paymentMethods.isNotEmpty + ? order.paymentMethods[0] + : 'No payment method'; + + return Row( + children: [ + HeroIcon( + _getPaymentMethodIcon(method), + style: HeroIconStyle.outline, + color: AppTheme.cream1, + size: 16, + ), + const SizedBox(width: 4), + Flexible( + child: Text( + method, + style: Theme.of(context).textTheme.bodyMedium?.copyWith( + color: AppTheme.grey2, + ), + overflow: TextOverflow.ellipsis, + softWrap: true, + ), + ), + ], + ); + } + + HeroIcons _getPaymentMethodIcon(String method) { + switch (method.toLowerCase()) { + case 'wire transfer': + case 'transferencia bancaria': + return HeroIcons.buildingLibrary; + case 'revolut': + return HeroIcons.creditCard; + default: + return HeroIcons.banknotes; + } + } + + TextSpan _buildStyledTextSpan( + BuildContext context, + String label, + String value, { + bool isValue = false, + bool isBold = false, + }) { + return TextSpan( + text: label, + style: Theme.of(context).textTheme.bodyLarge?.copyWith( + color: AppTheme.cream1, + fontWeight: FontWeight.normal, + fontSize: isValue ? 16.0 : 24.0, + ), + children: isValue + ? [ + TextSpan( + text: '$value ', + style: Theme.of(context).textTheme.displayLarge?.copyWith( + fontWeight: isBold ? FontWeight.bold : FontWeight.normal, + fontSize: 24.0, + color: AppTheme.cream1, + ), + ), + ] + : [], + ); + } +} diff --git a/lib/features/take_order/notifiers/take_buy_order_notifier.dart b/lib/features/take_order/notifiers/take_buy_order_notifier.dart new file mode 100644 index 00000000..fea9c8a3 --- /dev/null +++ b/lib/features/take_order/notifiers/take_buy_order_notifier.dart @@ -0,0 +1,62 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/data/repositories/mostro_repository.dart'; +import 'package:mostro_mobile/shared/providers/navigation_notifier_provider.dart'; +import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; + +class TakeBuyOrderNotifier extends StateNotifier { + final MostroRepository _orderRepository; + final String orderId; + final Ref ref; + StreamSubscription? _orderSubscription; + + TakeBuyOrderNotifier(this._orderRepository, this.orderId, this.ref) + : super(MostroMessage(action: Action.takeBuy)); + + void takeBuyOrder(String orderId, int? amount) async { + try { + final stream = await _orderRepository.takeBuyOrder(orderId, amount); + _orderSubscription = stream.listen((order) { + state = order; + _handleOrderUpdate(); + }); + } catch (e) { + _handleError(e); + } + } + + void _handleError(Object err) { + ref.read(notificationProvider.notifier).showInformation(err.toString()); + } + + void _handleOrderUpdate() { + final notifProvider = ref.read(notificationProvider.notifier); + + switch (state.action) { + case Action.payInvoice: + ref + .read(navigationProvider.notifier) + .go('/pay_invoice/${state.requestId!}'); + break; + case Action.waitingBuyerInvoice: + notifProvider.showInformation('Waiting Buy Invoice'); + break; + case Action.waitingSellerToPay: + notifProvider.showInformation('Waiting for Seller to pay'); + break; + case Action.rate: + case Action.rateReceived: + default: + notifProvider.showInformation(state.action.toString()); + break; + } + } + + @override + void dispose() { + _orderSubscription?.cancel(); + super.dispose(); + } +} diff --git a/lib/features/take_order/notifiers/take_sell_order_notifier.dart b/lib/features/take_order/notifiers/take_sell_order_notifier.dart new file mode 100644 index 00000000..d7539366 --- /dev/null +++ b/lib/features/take_order/notifiers/take_sell_order_notifier.dart @@ -0,0 +1,85 @@ +import 'dart:async'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +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/data/repositories/mostro_repository.dart'; +import 'package:mostro_mobile/shared/providers/navigation_notifier_provider.dart'; +import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; + +class TakeSellOrderNotifier extends StateNotifier { + final MostroRepository _orderRepository; + final String orderId; + final Ref ref; + StreamSubscription? _orderSubscription; + + TakeSellOrderNotifier(this._orderRepository, this.orderId, this.ref) + : super(MostroMessage(action: Action.takeSell)); + + void takeSellOrder(String orderId, int? amount, String? lnAddress) async { + try { + final stream = + await _orderRepository.takeSellOrder(orderId, amount, lnAddress); + _orderSubscription = stream.listen((order) { + state = order; + _handleOrderUpdate(); + }); + } catch (e) { + _handleError(e); + } + } + + void _handleError(Object err) { + ref.read(notificationProvider.notifier).showInformation(err.toString()); + } + + void sendInvoice(String orderId, String invoice, int amount) async { + await _orderRepository.sendInvoice(orderId, invoice); + } + + void _handleOrderUpdate() { + final navProvider = ref.read(navigationProvider.notifier); + final notifProvider = ref.read(notificationProvider.notifier); + switch (state.action) { + case Action.addInvoice: + navProvider.go('/add_invoice/$orderId'); + break; + case Action.waitingSellerToPay: + navProvider.go('/'); + notifProvider.showInformation('Waiting for Seller to pay'); + case Action.incorrectInvoiceAmount: + notifProvider.showInformation('Incorrect Invoice Amount'); + break; + case Action.outOfRangeFiatAmount: + case Action.outOfRangeSatsAmount: + break; + case Action.holdInvoicePaymentAccepted: + break; + case Action.fiatSentOk: + break; + case Action.released: + break; + case Action.purchaseCompleted: + break; + case Action.rate: + break; + case Action.cooperativeCancelInitiatedByPeer: + case Action.disputeInitiatedByPeer: + case Action.adminSettled: + default: + notifProvider.showInformation(state.action.toString()); + break; + } + } + + void cancelOrder() { + //state = state.copyWith(status: TakeSellOrderStatus.cancelled); + dispose(); + } + + @override + void dispose() { + _orderSubscription?.cancel(); + super.dispose(); + } +} diff --git a/lib/features/take_order/providers/order_notifier_providers.dart b/lib/features/take_order/providers/order_notifier_providers.dart new file mode 100644 index 00000000..3d807263 --- /dev/null +++ b/lib/features/take_order/providers/order_notifier_providers.dart @@ -0,0 +1,19 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/models/mostro_message.dart'; +import 'package:mostro_mobile/features/take_order/notifiers/take_buy_order_notifier.dart'; +import 'package:mostro_mobile/features/take_order/notifiers/take_sell_order_notifier.dart'; +import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; + +final takeSellOrderNotifierProvider = + StateNotifierProvider.family( + (ref, orderId) { + final repository = ref.watch(mostroRepositoryProvider); + return TakeSellOrderNotifier(repository, orderId, ref); +}); + +final takeBuyOrderNotifierProvider = + StateNotifierProvider.family( + (ref, orderId) { + final repository = ref.watch(mostroRepositoryProvider); + return TakeBuyOrderNotifier(repository, orderId, ref); +}); diff --git a/lib/features/take_order/screens/add_lightning_invoice_screen.dart b/lib/features/take_order/screens/add_lightning_invoice_screen.dart new file mode 100644 index 00000000..b4eb3fa0 --- /dev/null +++ b/lib/features/take_order/screens/add_lightning_invoice_screen.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/data/models/order.dart'; +import 'package:mostro_mobile/features/take_order/widgets/order_app_bar.dart'; +import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; +import 'package:mostro_mobile/shared/widgets/custom_card.dart'; + +class AddLightningInvoiceScreen extends ConsumerWidget { + final String orderId; + final int sats = 0; + + const AddLightningInvoiceScreen({super.key, required this.orderId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mostroRepo = ref.read(mostroRepositoryProvider); + final message = mostroRepo.getOrderById(orderId); + final order = message?.getPayload(); + + final amount = order?.amount; + + final TextEditingController invoiceController = TextEditingController(); + + return Scaffold( + backgroundColor: AppTheme.dark1, + appBar: OrderAppBar(title: 'Add Lightning Invoice'), + body: CustomCard( + padding: const EdgeInsets.all(16), + child: Material( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Please enter a Lightning Invoice for $amount sats:", + style: TextStyle(color: AppTheme.cream1, fontSize: 16), + ), + const SizedBox(height: 8), + TextFormField( + controller: invoiceController, + style: const TextStyle(color: AppTheme.cream1), + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + labelText: "Lightning Invoice", + labelStyle: const TextStyle(color: AppTheme.grey2), + hintText: "Enter invoice here", + hintStyle: const TextStyle(color: AppTheme.grey2), + filled: true, + fillColor: AppTheme.dark1, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + mostroRepo.cancelOrder(orderId); + context.go('/'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + child: const Text('CANCEL'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () { + final invoice = invoiceController.text.trim(); + if (invoice.isNotEmpty) { + mostroRepo.sendInvoice(orderId, invoice); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.mostroGreen, + ), + child: const Text('SUBMIT'), + ), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/take_order/screens/error_screen.dart b/lib/features/take_order/screens/error_screen.dart new file mode 100644 index 00000000..7d0346d7 --- /dev/null +++ b/lib/features/take_order/screens/error_screen.dart @@ -0,0 +1,45 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/features/take_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/take_order/screens/pay_lightning_invoice_screen.dart b/lib/features/take_order/screens/pay_lightning_invoice_screen.dart new file mode 100644 index 00000000..f0b11361 --- /dev/null +++ b/lib/features/take_order/screens/pay_lightning_invoice_screen.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/data/models/payment_request.dart'; +import 'package:mostro_mobile/features/take_order/widgets/order_app_bar.dart'; +import 'package:mostro_mobile/shared/providers/mostro_service_provider.dart'; +import 'package:mostro_mobile/shared/widgets/custom_card.dart'; +import 'package:qr_flutter/qr_flutter.dart'; +import 'package:flutter/services.dart'; // For Clipboard + +class PayLightningInvoiceScreen extends ConsumerWidget { + final String orderId; + + const PayLightningInvoiceScreen({super.key, required this.orderId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final mostroRepo = ref.read(mostroRepositoryProvider); + final message = mostroRepo.getOrderById(orderId); + final pr = message?.getPayload(); + + return Scaffold( + backgroundColor: AppTheme.dark1, + appBar: OrderAppBar(title: 'Pay Lightning Invoice'), + body: pr == null || pr.lnInvoice == null + ? Center( + child: Text( + 'Invalid payment request.', + style: TextStyle(color: Colors.white, fontSize: 18), + ), + ) + : SingleChildScrollView( + padding: const EdgeInsets.all(16), + child: CustomCard( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + 'Pay this invoice to continue the exchange', + style: TextStyle(color: Colors.white, fontSize: 18), + textAlign: TextAlign.center, + ), + const SizedBox(height: 20), + Container( + padding: const EdgeInsets.all(8.0), + color: Colors.white, + child: QrImageView( + data: pr.lnInvoice!, + version: QrVersions.auto, + size: 250.0, + backgroundColor: Colors.white, + errorStateBuilder: (cxt, err) { + return const Center( + child: Text( + 'Failed to generate QR code', + textAlign: TextAlign.center, + ), + ); + }, + ), + ), + const SizedBox(height: 20), + ElevatedButton.icon( + onPressed: () { + Clipboard.setData(ClipboardData(text: pr.lnInvoice!)); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Invoice copied to clipboard!'), + duration: Duration(seconds: 2), + ), + ); + }, + icon: const Icon(Icons.copy), + label: const Text('Copy Invoice'), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF8CC541), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + ), + const SizedBox(height: 20), + // Open Wallet Button + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF8CC541), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: + Text('Open wallet feature not implemented.'), + duration: Duration(seconds: 2), + ), + ); + }, + child: const Text('OPEN WALLET'), + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + mostroRepo.cancelOrder(orderId); + context.go('/'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + ), + child: const Text('CANCEL'), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/features/take_order/screens/take_buy_order_screen.dart b/lib/features/take_order/screens/take_buy_order_screen.dart new file mode 100644 index 00000000..bcf8b268 --- /dev/null +++ b/lib/features/take_order/screens/take_buy_order_screen.dart @@ -0,0 +1,169 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/nostr_event.dart'; +import 'package:mostro_mobile/features/take_order/providers/order_notifier_providers.dart'; +import 'package:mostro_mobile/features/take_order/widgets/order_app_bar.dart'; +import 'package:mostro_mobile/features/take_order/widgets/buyer_info.dart'; +import 'package:mostro_mobile/features/take_order/widgets/seller_info.dart'; +import 'package:mostro_mobile/presentation/widgets/currency_text_field.dart'; +import 'package:mostro_mobile/presentation/widgets/exchange_rate_widget.dart'; +import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; +import 'package:mostro_mobile/shared/widgets/custom_card.dart'; + +class TakeBuyOrderScreen extends ConsumerWidget { + final String orderId; + final TextEditingController _satsAmountController = TextEditingController(); + final TextEditingController _lndAdrress = TextEditingController(); + + TakeBuyOrderScreen({super.key, required this.orderId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final initialOrder = ref.read(eventProvider(orderId)); + + return Scaffold( + backgroundColor: AppTheme.dark1, + appBar: OrderAppBar( + title: + '${initialOrder?.orderType == OrderType.buy ? "SELL" : "BUY"} BITCOIN'), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + CustomCard( + padding: EdgeInsets.all(16), + child: SellerInfo(order: initialOrder!)), + const SizedBox(height: 16), + _buildSellerAmount(ref), + const SizedBox(height: 16), + ExchangeRateWidget(currency: initialOrder.currency!), + const SizedBox(height: 16), + CustomCard( + padding: const EdgeInsets.all(16), + child: BuyerInfo(order: initialOrder)), + const SizedBox(height: 16), + _buildBuyerAmount(initialOrder.amount!), + const SizedBox(height: 16), + _buildLnAddress(), + const SizedBox(height: 16), + _buildActionButtons(context, ref), + ], + ), + ), + ), + ); + } + + Widget _buildSellerAmount(WidgetRef ref) { + final initialOrder = ref.read(eventProvider(orderId)); + final exchangeRateAsyncValue = + ref.watch(exchangeRateProvider(initialOrder!.currency!)); + return exchangeRateAsyncValue.when( + loading: () => const CircularProgressIndicator(), + error: (error, _) => Text('Error: $error'), + data: (exchangeRate) { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${initialOrder.fiatAmount} ${initialOrder.currency} (${initialOrder.premium}%)', + style: const TextStyle( + color: AppTheme.cream1, + fontSize: 18, + fontWeight: FontWeight.bold)), + Text('${initialOrder.amount} sats', + style: const TextStyle(color: AppTheme.grey2)), + ], + ) + ], + ), + ); + }, + ); + } + + Widget _buildBuyerAmount(String amount) { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CurrencyTextField(controller: _satsAmountController, label: 'Sats'), + const SizedBox(height: 8), + Text('\$ $amount', style: const TextStyle(color: AppTheme.grey2)), + const SizedBox(height: 24), + ], + ), + ); + } + + Widget _buildLnAddress() { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppTheme.dark1, + borderRadius: BorderRadius.circular(8), + ), + child: TextFormField( + controller: _lndAdrress, + style: const TextStyle(color: AppTheme.cream1), + decoration: InputDecoration( + border: InputBorder.none, + labelText: "Enter a Lightning Address", + labelStyle: const TextStyle(color: AppTheme.grey2), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a value'; + } + return null; + }, + ), + ), + ], + ), + ); + } + + Widget _buildActionButtons(BuildContext context, WidgetRef ref) { + final orderDetailsNotifier = + ref.read(takeBuyOrderNotifierProvider(orderId).notifier); + + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () { + context.go('/'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.red1, + ), + child: const Text('CANCEL'), + ), + const SizedBox(width: 16), + ElevatedButton( + onPressed: () => orderDetailsNotifier.takeBuyOrder(orderId, null), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.mostroGreen, + ), + child: const Text('CONTINUE'), + ), + ], + ); + } +} diff --git a/lib/features/take_order/screens/take_sell_order_screen.dart b/lib/features/take_order/screens/take_sell_order_screen.dart new file mode 100644 index 00000000..51cc8425 --- /dev/null +++ b/lib/features/take_order/screens/take_sell_order_screen.dart @@ -0,0 +1,268 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/data/models/enums/order_type.dart'; +import 'package:mostro_mobile/data/models/nostr_event.dart'; +import 'package:mostro_mobile/data/models/order.dart'; +import 'package:mostro_mobile/features/take_order/providers/order_notifier_providers.dart'; +import 'package:mostro_mobile/features/take_order/widgets/order_app_bar.dart'; +import 'package:mostro_mobile/features/take_order/widgets/buyer_info.dart'; +import 'package:mostro_mobile/features/take_order/widgets/completion_message.dart'; +import 'package:mostro_mobile/features/take_order/widgets/seller_info.dart'; +import 'package:mostro_mobile/presentation/widgets/currency_text_field.dart'; +import 'package:mostro_mobile/presentation/widgets/exchange_rate_widget.dart'; +import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/order_repository_provider.dart'; +import 'package:mostro_mobile/shared/widgets/custom_card.dart'; +import 'package:mostro_mobile/data/models/enums/action.dart' as actions; + +class TakeSellOrderScreen extends ConsumerWidget { + final String orderId; + final TextEditingController _satsAmountController = TextEditingController(); + final TextEditingController _lndAdrress = TextEditingController(); + + TakeSellOrderScreen({super.key, required this.orderId}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final orderDetailsState = ref.watch(takeSellOrderNotifierProvider(orderId)); + switch (orderDetailsState.action) { + case actions.Action.takeSell: + return _buildContent(context, ref); + case actions.Action.addInvoice: + return _buildLightningInvoiceInput(context, ref); + case actions.Action.waitingSellerToPay: + return _buildCompletionMessage(); + default: + return const Center(child: Text('Order not found')); + } + } + + Widget _buildCompletionMessage() { + final message = 'Order has been completed successfully!'; + return CompletionMessage(message: message); + } + + Widget _buildContent(BuildContext context, WidgetRef ref) { + final initialOrder = ref.read(eventProvider(orderId)); + return Scaffold( + backgroundColor: AppTheme.dark1, + appBar: OrderAppBar( + title: + '${initialOrder?.orderType == OrderType.buy ? "SELL" : "BUY"} BITCOIN'), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + children: [ + CustomCard( + padding: EdgeInsets.all(16), + child: SellerInfo(order: initialOrder!)), + const SizedBox(height: 16), + _buildSellerAmount(ref), + const SizedBox(height: 16), + ExchangeRateWidget(currency: initialOrder.currency!), + const SizedBox(height: 16), + CustomCard( + padding: const EdgeInsets.all(16), + child: BuyerInfo(order: initialOrder)), + const SizedBox(height: 16), + _buildBuyerAmount(initialOrder.amount!), + const SizedBox(height: 16), + _buildLnAddress(), + const SizedBox(height: 16), + _buildActionButtons(context, ref), + ], + ), + ), + ), + ); + } + + Widget _buildSellerAmount(WidgetRef ref) { + final initialOrder = ref.read(eventProvider(orderId)); + final exchangeRateAsyncValue = + ref.watch(exchangeRateProvider(initialOrder!.currency!)); + return exchangeRateAsyncValue.when( + loading: () => const CircularProgressIndicator(), + error: (error, _) => Text('Error: $error'), + data: (exchangeRate) { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Row( + children: [ + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + '${initialOrder.fiatAmount} ${initialOrder.currency} (${initialOrder.premium}%)', + style: const TextStyle( + color: AppTheme.cream1, + fontSize: 18, + fontWeight: FontWeight.bold)), + Text('${initialOrder.amount} sats', + style: const TextStyle(color: AppTheme.grey2)), + ], + ) + ], + ), + ); + }, + ); + } + + Widget _buildLightningInvoiceInput(BuildContext context, WidgetRef ref) { + final orderDetailsNotifier = + ref.read(takeSellOrderNotifierProvider(orderId).notifier); + final state = ref.watch(takeSellOrderNotifierProvider(orderId)); + final order = (state.payload is Order) ? state.payload as Order : null; + + final TextEditingController invoiceController = TextEditingController(); + final int val = order!.amount; + return Scaffold( + backgroundColor: AppTheme.dark1, + appBar: OrderAppBar( + title: + 'Add a Lightning Invoice'), + body: CustomCard( + padding: const EdgeInsets.all(16), + child: Material( + color: Colors.transparent, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Please enter a Lightning Invoice for $val sats:", + style: TextStyle(color: AppTheme.cream1, fontSize: 16), + ), + const SizedBox(height: 8), + TextFormField( + controller: invoiceController, + style: const TextStyle(color: AppTheme.cream1), + decoration: InputDecoration( + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(8), + ), + labelText: "Lightning Invoice", + labelStyle: const TextStyle(color: AppTheme.grey2), + hintText: "Enter invoice here", + hintStyle: const TextStyle(color: AppTheme.grey2), + filled: true, + fillColor: AppTheme.dark1, + ), + ), + const SizedBox(height: 16), + Row( + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + context.go('/'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.red, + ), + child: const Text('CANCEL'), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () { + final invoice = invoiceController.text.trim(); + if (invoice.isNotEmpty) { + orderDetailsNotifier.sendInvoice(orderId, invoice, val); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.mostroGreen, + ), + child: const Text('SUBMIT'), + ), + ), + ], + ), + ], + ), + ), + ),); + } + + Widget _buildBuyerAmount(String amount) { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + CurrencyTextField(controller: _satsAmountController, label: 'Sats'), + const SizedBox(height: 8), + Text('\$ $amount', style: const TextStyle(color: AppTheme.grey2)), + const SizedBox(height: 24), + ], + ), + ); + } + + Widget _buildLnAddress() { + return CustomCard( + padding: const EdgeInsets.all(16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: AppTheme.dark1, + borderRadius: BorderRadius.circular(8), + ), + child: TextFormField( + controller: _lndAdrress, + style: const TextStyle(color: AppTheme.cream1), + decoration: InputDecoration( + border: InputBorder.none, + labelText: "Enter a Lightning Address", + labelStyle: const TextStyle(color: AppTheme.grey2), + ), + validator: (value) { + if (value == null || value.isEmpty) { + return 'Please enter a value'; + } + return null; + }, + ), + ), + ], + ), + ); + } + + Widget _buildActionButtons(BuildContext context, WidgetRef ref) { + final orderDetailsNotifier = + ref.read(takeSellOrderNotifierProvider(orderId).notifier); + + return Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + ElevatedButton( + onPressed: () { + context.go('/'); + }, + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.red1, + ), + child: const Text('CANCEL', style: TextStyle(color: AppTheme.red2)), + ), + const SizedBox(width: 16), + ElevatedButton( + onPressed: () => + orderDetailsNotifier.takeSellOrder(orderId, null, null), + style: ElevatedButton.styleFrom( + backgroundColor: AppTheme.mostroGreen, + ), + child: const Text('CONTINUE'), + ), + ], + ); + } +} diff --git a/lib/features/take_order/widgets/buyer_info.dart b/lib/features/take_order/widgets/buyer_info.dart new file mode 100644 index 00000000..f7af6bb5 --- /dev/null +++ b/lib/features/take_order/widgets/buyer_info.dart @@ -0,0 +1,44 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; +import 'package:flutter/material.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/data/models/nostr_event.dart'; + +class BuyerInfo extends StatelessWidget { + final NostrEvent order; + + const BuyerInfo({super.key, required this.order}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const CircleAvatar( + backgroundColor: AppTheme.grey2, + child: Text('S', style: TextStyle(color: AppTheme.cream1)), + ), + 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), + ), + ], + ), + ), + TextButton( + onPressed: () { + // Implement review logic + }, + child: const Text('Read reviews', + style: TextStyle(color: AppTheme.mostroGreen)), + ), + ], + ); + } +} diff --git a/lib/features/take_order/widgets/completion_message.dart b/lib/features/take_order/widgets/completion_message.dart new file mode 100644 index 00000000..11a235bd --- /dev/null +++ b/lib/features/take_order/widgets/completion_message.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/features/take_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/take_order/widgets/order_app_bar.dart b/lib/features/take_order/widgets/order_app_bar.dart new file mode 100644 index 00000000..0ff7f318 --- /dev/null +++ b/lib/features/take_order/widgets/order_app_bar.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:heroicons/heroicons.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; + +class OrderAppBar extends StatelessWidget implements PreferredSizeWidget { + final String title; + + const OrderAppBar({super.key, required this.title}); + + @override + Widget build(BuildContext context) { + return AppBar( + backgroundColor: Colors.transparent, + elevation: 0, + leading: IconButton( + icon: const HeroIcon(HeroIcons.arrowLeft, color: AppTheme.cream1), + onPressed: () => context.go('/'), + ), + title: Text( + title, + style: TextStyle( + color: AppTheme.cream1, + fontFamily: GoogleFonts.robotoCondensed().fontFamily, + ), + ), + ); + } + + @override + Size get preferredSize => const Size.fromHeight(kToolbarHeight); +} diff --git a/lib/features/take_order/widgets/seller_info.dart b/lib/features/take_order/widgets/seller_info.dart new file mode 100644 index 00000000..a37f5a20 --- /dev/null +++ b/lib/features/take_order/widgets/seller_info.dart @@ -0,0 +1,44 @@ +import 'package:dart_nostr/nostr/model/event/event.dart'; +import 'package:flutter/material.dart'; +import 'package:mostro_mobile/app/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, + child: Text('S', style: TextStyle(color: AppTheme.cream1)), + ), + 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), + ), + ], + ), + ), + TextButton( + onPressed: () { + // Implement review logic + }, + child: const Text('Read reviews', + style: TextStyle(color: AppTheme.mostroGreen)), + ), + ], + ); + } +} diff --git a/lib/main.dart b/lib/main.dart index 7f513f82..ae74c58b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,96 +1,25 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_localizations/flutter_localizations.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/core/theme/app_theme.dart'; -import 'package:mostro_mobile/presentation/auth/bloc/auth_state.dart'; -import 'package:mostro_mobile/providers/riverpod_providers.dart'; +import 'package:mostro_mobile/app/app.dart'; +import 'package:mostro_mobile/features/auth/providers/auth_notifier_provider.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; -import 'package:shared_preferences/shared_preferences.dart'; -import 'package:mostro_mobile/core/routes/app_routes.dart'; -import 'package:mostro_mobile/presentation/home/bloc/home_bloc.dart'; -import 'package:mostro_mobile/presentation/chat_list/bloc/chat_list_bloc.dart'; -import 'package:mostro_mobile/presentation/profile/bloc/profile_bloc.dart'; -import 'package:mostro_mobile/presentation/auth/bloc/auth_bloc.dart'; -import 'package:mostro_mobile/data/repositories/auth_repository.dart'; -import 'package:mostro_mobile/core/utils/biometrics_helper.dart'; -import 'generated/l10n.dart'; +import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; +import 'package:mostro_mobile/shared/utils/biometrics_helper.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); final nostrService = NostrService(); await nostrService.init(); - - final prefs = await SharedPreferences.getInstance(); - final isFirstLaunch = prefs.getBool('isFirstLaunch') ?? true; - final biometricsHelper = BiometricsHelper(); - runApp(ProviderScope( - child: MyApp( - isFirstLaunch: isFirstLaunch, biometricsHelper: biometricsHelper))); -} - -class MyApp extends ConsumerWidget { - final bool isFirstLaunch; - final BiometricsHelper biometricsHelper; - - const MyApp({ - super.key, - required this.isFirstLaunch, - required this.biometricsHelper, - }); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final homeBloc = ref.watch(homeBlocProvider); - return MultiBlocProvider( - providers: [ - BlocProvider( - create: (context) => AuthBloc( - authRepository: AuthRepository( - biometricsHelper: biometricsHelper, - ), - ), - ), - BlocProvider( - create: (context) => homeBloc, - ), - BlocProvider( - create: (context) => ChatListBloc(), - ), - BlocProvider( - create: (context) => ProfileBloc(), - ), + runApp( + ProviderScope( + overrides: [ + nostrServicerProvider.overrideWithValue(nostrService), + biometricsHelperProvider.overrideWithValue(biometricsHelper), ], - child: BlocListener( - listener: (context, state) { - if (state is AuthAuthenticated || state is AuthRegistrationSuccess) { - Navigator.of(context).pushReplacementNamed(AppRoutes.home); - } else if (state is AuthUnregistered || - state is AuthUnauthenticated) { - Navigator.of(context).pushReplacementNamed(AppRoutes.welcome); - } - }, - child: MaterialApp( - title: 'Mostro', - theme: ThemeData( - primarySwatch: Colors.blue, - scaffoldBackgroundColor: AppTheme.dark1, - ), - initialRoute: isFirstLaunch ? AppRoutes.welcome : AppRoutes.home, - routes: AppRoutes.routes, - onGenerateRoute: AppRoutes.onGenerateRoute, - localizationsDelegates: const [ - S.delegate, - GlobalMaterialLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - ], - supportedLocales: S.delegate.supportedLocales, - ), - ), - ); - } + child: const MostroApp(), + ), + ); } diff --git a/lib/notifiers/mostro_orders_notifier.dart b/lib/notifiers/mostro_orders_notifier.dart new file mode 100644 index 00000000..32ee9e57 --- /dev/null +++ b/lib/notifiers/mostro_orders_notifier.dart @@ -0,0 +1,18 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/models/order.dart'; +import 'package:mostro_mobile/data/repositories/mostro_repository.dart'; + +class MostroOrdersNotifier extends StateNotifier> { + final MostroRepository _repository; + + MostroOrdersNotifier(this._repository) : super([]) { + _repository.ordersStream.listen((orders) { + state = orders; + }); + } + + Future cleanupExpiredOrders(DateTime now) async { + //_repository.cleanupExpiredOrders(now); + state = await _repository.ordersStream.first; + } +} diff --git a/lib/notifiers/nostr_service_notifier.dart b/lib/notifiers/nostr_service_notifier.dart new file mode 100644 index 00000000..95e55c81 --- /dev/null +++ b/lib/notifiers/nostr_service_notifier.dart @@ -0,0 +1,15 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/services/nostr_service.dart'; + +class NostrServiceNotifier extends AsyncNotifier { + @override + Future build() async { + final service = NostrService(); + await service.init(); + return service; + } +} + +final nostrServiceProvider = Provider((ref) { + return NostrService()..init(); +}); \ No newline at end of file diff --git a/lib/notifiers/open_orders_repository_notifier.dart b/lib/notifiers/open_orders_repository_notifier.dart new file mode 100644 index 00000000..a5657027 --- /dev/null +++ b/lib/notifiers/open_orders_repository_notifier.dart @@ -0,0 +1,12 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/repositories/open_orders_repository.dart'; +import 'package:mostro_mobile/notifiers/nostr_service_notifier.dart'; + +class OpenOrdersRepositoryNotifier extends AsyncNotifier { + @override + Future build() async { + final nostrService = ref.watch(nostrServiceProvider); + return OpenOrdersRepository(nostrService); + } +} + diff --git a/lib/presentation/add_order/bloc/add_order_bloc.dart b/lib/presentation/add_order/bloc/add_order_bloc.dart deleted file mode 100644 index 9a511fae..00000000 --- a/lib/presentation/add_order/bloc/add_order_bloc.dart +++ /dev/null @@ -1,50 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart'; -import 'package:mostro_mobile/services/mostro_service.dart'; -import 'add_order_event.dart'; -import 'add_order_state.dart'; - -class AddOrderBloc extends Bloc { - final MostroService mostroService; - - AddOrderBloc(this.mostroService) : super(const AddOrderState()) { - on(_onChangeOrderType); - on(_onSubmitOrder); - on(_onOrderUpdateReceived); - } - - void _onChangeOrderType(ChangeOrderType event, Emitter emit) { - emit(state.copyWith(currentType: event.orderType)); - } - - Future _onSubmitOrder( - SubmitOrder event, Emitter emit) async { - emit(state.copyWith(status: AddOrderStatus.submitting)); - - try { - final order = await mostroService.publishOrder(event.order); - add(OrderUpdateReceived(order)); - } catch (e) { - emit(state.copyWith( - status: AddOrderStatus.failure, - errorMessage: e.toString(), - )); - } - } - - void _onOrderUpdateReceived( - OrderUpdateReceived event, Emitter emit) { - switch (event.order.action) { - case Action.newOrder: - emit(state.copyWith(status: AddOrderStatus.submitted)); - break; - case Action.outOfRangeSatsAmount: - case Action.outOfRangeFiatAmount: - emit(state.copyWith( - status: AddOrderStatus.failure, errorMessage: "Invalid amount")); - break; - default: - break; - } - } -} diff --git a/lib/presentation/add_order/bloc/add_order_event.dart b/lib/presentation/add_order/bloc/add_order_event.dart deleted file mode 100644 index f2aa0074..00000000 --- a/lib/presentation/add_order/bloc/add_order_event.dart +++ /dev/null @@ -1,54 +0,0 @@ -import 'package:equatable/equatable.dart'; -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'; - -abstract class AddOrderEvent extends Equatable { - const AddOrderEvent(); - - @override - List get props => []; -} - -class ChangeOrderType extends AddOrderEvent { - final OrderType orderType; - - const ChangeOrderType(this.orderType); - - @override - List get props => [orderType]; -} - -class SubmitOrder extends AddOrderEvent { - final String fiatCode; - final int fiatAmount; - final int satsAmount; - final String paymentMethod; - final OrderType orderType; - - const SubmitOrder({ - required this.fiatCode, - required this.fiatAmount, - required this.satsAmount, - required this.paymentMethod, - required this.orderType, - }); - - Order get order => Order( - kind: orderType, - fiatCode: fiatCode, - fiatAmount: fiatAmount, - paymentMethod: paymentMethod, - premium: 0); - - @override - List get props => - [fiatCode, fiatAmount, satsAmount, paymentMethod, orderType]; -} - -class OrderUpdateReceived extends AddOrderEvent { - final MostroMessage order; - - const OrderUpdateReceived(this.order); -} - diff --git a/lib/presentation/add_order/bloc/add_order_state.dart b/lib/presentation/add_order/bloc/add_order_state.dart deleted file mode 100644 index c761042a..00000000 --- a/lib/presentation/add_order/bloc/add_order_state.dart +++ /dev/null @@ -1,33 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:mostro_mobile/data/models/enums/order_type.dart'; - -enum AddOrderStatus { initial, loading, success, submitting, submitted, failure } - -class AddOrderState extends Equatable { - final OrderType currentType; - final AddOrderStatus status; - final String? errorMessage; - final String? currency; - - const AddOrderState({ - this.currentType = OrderType.sell, - this.status = AddOrderStatus.initial, - this.errorMessage, - this.currency, - }); - - AddOrderState copyWith({ - OrderType? currentType, - AddOrderStatus? status, - String? errorMessage, - }) { - return AddOrderState( - currentType: currentType ?? this.currentType, - status: status ?? this.status, - errorMessage: errorMessage ?? this.errorMessage, - ); - } - - @override - List get props => [currentType, status, errorMessage, currency]; -} diff --git a/lib/presentation/auth/bloc/auth_bloc.dart b/lib/presentation/auth/bloc/auth_bloc.dart deleted file mode 100644 index 2c420804..00000000 --- a/lib/presentation/auth/bloc/auth_bloc.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import '../../../data/repositories/auth_repository.dart'; -import 'auth_event.dart'; -import 'auth_state.dart'; -import '../../../core/utils/nostr_utils.dart'; - -class AuthBloc extends Bloc { - final AuthRepository authRepository; - - AuthBloc({required this.authRepository}) : super(AuthInitial()) { - on(_onAuthCheckRequested); - on(_onAuthRegisterRequested); - on(_onAuthLoginRequested); - on(_onAuthLogoutRequested); - on(_onAuthGenerateKeyRequested); - on(_onAuthCheckBiometricsRequested); - } - - Future _onAuthCheckRequested( - AuthCheckRequested event, Emitter emit) async { - emit(AuthLoading()); - final isRegistered = await authRepository.isRegistered(); - if (isRegistered) { - emit(AuthUnauthenticated()); - } else { - emit(AuthUnregistered()); - } - } - - Future _onAuthRegisterRequested( - AuthRegisterRequested event, Emitter emit) async { - emit(AuthLoading()); - try { - String privateKey = event.privateKey; - if (privateKey.startsWith('nsec')) { - privateKey = NostrUtils.decodeNsecKeyToPrivateKey(privateKey); - } - await authRepository.register( - privateKey, event.password, event.useBiometrics); - emit(AuthRegistrationSuccess()); - } catch (e, stackTrace) { - print('Error during registration: $e'); - print('Stack trace: $stackTrace'); - emit(AuthFailure(e.toString())); - } - } - - Future _onAuthLoginRequested( - AuthLoginRequested event, Emitter emit) async { - emit(AuthLoading()); - try { - final isAuthenticated = await authRepository.login(event.password); - if (isAuthenticated) { - emit(AuthAuthenticated()); - } else { - emit(const AuthFailure( - 'Invalid PIN or biometric authentication failed')); - } - } catch (e) { - emit(AuthFailure(e.toString())); - } - } - - Future _onAuthLogoutRequested( - AuthLogoutRequested event, Emitter emit) async { - emit(AuthLoading()); - await authRepository.logout(); - emit(AuthUnregistered()); - } - - Future _onAuthGenerateKeyRequested( - AuthGenerateKeyRequested event, Emitter emit) async { - final newPrivateKey = await authRepository.generateNewIdentity(); - emit(AuthKeyGenerated(NostrUtils.encodePrivateKeyToNsec(newPrivateKey))); - } - - Future _onAuthCheckBiometricsRequested( - AuthCheckBiometricsRequested event, Emitter emit) async { - final isAvailable = await authRepository.isBiometricsAvailable(); - emit(AuthBiometricsAvailability(isAvailable)); - } -} diff --git a/lib/presentation/auth/bloc/auth_event.dart b/lib/presentation/auth/bloc/auth_event.dart deleted file mode 100644 index ab54b2d2..00000000 --- a/lib/presentation/auth/bloc/auth_event.dart +++ /dev/null @@ -1,36 +0,0 @@ -import 'package:equatable/equatable.dart'; - -abstract class AuthEvent extends Equatable { - const AuthEvent(); - - @override - List get props => []; -} - -class AuthCheckRequested extends AuthEvent {} - -class AuthRegisterRequested extends AuthEvent { - final String privateKey; - final String password; - final bool useBiometrics; - - const AuthRegisterRequested(this.privateKey, this.password, this.useBiometrics); - - @override - List get props => [privateKey, password, useBiometrics]; -} - -class AuthLoginRequested extends AuthEvent { - final String password; - - const AuthLoginRequested(this.password); - - @override - List get props => [password]; -} - -class AuthLogoutRequested extends AuthEvent {} - -class AuthGenerateKeyRequested extends AuthEvent {} - -class AuthCheckBiometricsRequested extends AuthEvent {} \ No newline at end of file diff --git a/lib/presentation/auth/screens/login_screen.dart b/lib/presentation/auth/screens/login_screen.dart deleted file mode 100644 index 32173136..00000000 --- a/lib/presentation/auth/screens/login_screen.dart +++ /dev/null @@ -1,82 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import '../bloc/auth_bloc.dart'; -import '../bloc/auth_event.dart'; -import '../bloc/auth_state.dart'; -import '../../widgets/custom_button.dart'; - -class LoginScreen extends StatefulWidget { - const LoginScreen({super.key}); - - @override - _LoginScreenState createState() => _LoginScreenState(); -} - -class _LoginScreenState extends State { - final _formKey = GlobalKey(); - final _pinController = TextEditingController(); - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Login', style: TextStyle(color: Colors.white)), - backgroundColor: Colors.transparent, - elevation: 0, - ), - backgroundColor: const Color(0xFF1D212C), - body: BlocListener( - listener: (context, state) { - if (state is AuthAuthenticated) { - Navigator.of(context).pushReplacementNamed('/home'); - } else if (state is AuthFailure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.error)), - ); - } - }, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextFormField( - controller: _pinController, - decoration: const InputDecoration( - labelText: 'PIN', - labelStyle: TextStyle(color: Colors.white70), - enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white70), - ), - ), - style: const TextStyle(color: Colors.white), - keyboardType: TextInputType.number, - obscureText: true, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter your PIN'; - } - return null; - }, - ), - const SizedBox(height: 24), - CustomButton( - text: 'Login', - onPressed: _onLogin, - ), - ], - ), - ), - ), - ), - ); - } - - void _onLogin() { - if (_formKey.currentState!.validate()) { - context.read().add(AuthLoginRequested(_pinController.text)); - } - } -} diff --git a/lib/presentation/auth/screens/register_screen.dart b/lib/presentation/auth/screens/register_screen.dart deleted file mode 100644 index 5a2c2569..00000000 --- a/lib/presentation/auth/screens/register_screen.dart +++ /dev/null @@ -1,218 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import '../bloc/auth_bloc.dart'; -import '../bloc/auth_event.dart'; -import '../bloc/auth_state.dart'; -import '../../widgets/custom_button.dart'; -import '../../../core/utils/nostr_utils.dart'; -import '../../../core/routes/app_routes.dart'; - -class RegisterScreen extends StatefulWidget { - const RegisterScreen({super.key}); - - @override - _RegisterScreenState createState() => _RegisterScreenState(); -} - -class _RegisterScreenState extends State { - final _formKey = GlobalKey(); - final _privateKeyController = TextEditingController(); - final _pinController = TextEditingController(); - final _confirmPinController = TextEditingController(); - bool _obscurePrivateKey = true; - bool _obscurePin = true; - bool _obscureConfirmPin = true; - bool _useBiometrics = false; - bool _biometricsAvailable = false; - - @override - void initState() { - super.initState(); - context.read().add(AuthCheckBiometricsRequested()); - } - - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar( - title: const Text('Register', style: TextStyle(color: Colors.white)), - backgroundColor: Colors.transparent, - elevation: 0, - ), - backgroundColor: const Color(0xFF1D212C), - body: BlocConsumer( - listener: (context, state) { - if (state is AuthBiometricsAvailability) { - setState(() { - _biometricsAvailable = state.isAvailable; - }); - } else if (state is AuthKeyGenerated) { - setState(() { - _privateKeyController.text = - NostrUtils.nsecToHex(state.privateKey); - }); - } else if (state is AuthRegistrationSuccess) { - // Navegar al home después de un registro exitoso - Navigator.of(context).pushReplacementNamed(AppRoutes.home); - } else if (state is AuthFailure) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text(state.error)), - ); - } - }, - builder: (context, state) { - return SingleChildScrollView( - padding: const EdgeInsets.all(16.0), - child: Form( - key: _formKey, - child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - TextFormField( - controller: _privateKeyController, - decoration: InputDecoration( - labelText: 'Private Key (nsec or hex)', - labelStyle: const TextStyle(color: Colors.white70), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white70), - ), - suffixIcon: IconButton( - icon: Icon( - _obscurePrivateKey - ? Icons.visibility_off - : Icons.visibility, - color: Colors.white70, - ), - onPressed: () { - setState(() { - _obscurePrivateKey = !_obscurePrivateKey; - }); - }, - ), - ), - style: const TextStyle(color: Colors.white), - obscureText: _obscurePrivateKey, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a private key'; - } - if (!value.startsWith('nsec') && - !RegExp(r'^[0-9a-fA-F]{64}$').hasMatch(value)) { - return 'Invalid private key format'; - } - return null; - }, - ), - const SizedBox(height: 16), - TextFormField( - controller: _pinController, - decoration: InputDecoration( - labelText: 'PIN', - labelStyle: const TextStyle(color: Colors.white70), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white70), - ), - suffixIcon: IconButton( - icon: Icon( - _obscurePin ? Icons.visibility_off : Icons.visibility, - color: Colors.white70, - ), - onPressed: () { - setState(() { - _obscurePin = !_obscurePin; - }); - }, - ), - ), - style: const TextStyle(color: Colors.white), - keyboardType: TextInputType.number, - obscureText: _obscurePin, - validator: (value) { - if (value == null || value.isEmpty) { - return 'Please enter a PIN'; - } - if (value.length < 4) { - return 'PIN must be at least 4 digits'; - } - return null; - }, - ), - const SizedBox(height: 16), - TextFormField( - controller: _confirmPinController, - decoration: InputDecoration( - labelText: 'Confirm PIN', - labelStyle: const TextStyle(color: Colors.white70), - enabledBorder: const UnderlineInputBorder( - borderSide: BorderSide(color: Colors.white70), - ), - suffixIcon: IconButton( - icon: Icon( - _obscureConfirmPin - ? Icons.visibility_off - : Icons.visibility, - color: Colors.white70, - ), - onPressed: () { - setState(() { - _obscureConfirmPin = !_obscureConfirmPin; - }); - }, - ), - ), - style: const TextStyle(color: Colors.white), - keyboardType: TextInputType.number, - obscureText: _obscureConfirmPin, - validator: (value) { - if (value != _pinController.text) { - return 'PINs do not match'; - } - return null; - }, - ), - const SizedBox(height: 24), - if (_biometricsAvailable) - SwitchListTile( - title: const Text('Use Biometrics', - style: TextStyle(color: Colors.white)), - value: _useBiometrics, - onChanged: (bool value) { - setState(() { - _useBiometrics = value; - }); - }, - activeColor: Colors.green, - ), - const SizedBox(height: 24), - CustomButton( - text: 'Register', - onPressed: _onRegister, - ), - const SizedBox(height: 16), - CustomButton( - text: 'Generate New Key', - onPressed: () { - context.read().add(AuthGenerateKeyRequested()); - }, - ), - ], - ), - ), - ); - }, - ), - ); - } - - void _onRegister() { - if (_formKey.currentState!.validate()) { - context.read().add( - AuthRegisterRequested( - _privateKeyController.text, - _pinController.text, - _useBiometrics, - ), - ); - } - } -} diff --git a/lib/presentation/chat_detail/screens/chat_detail_screen.dart b/lib/presentation/chat_detail/screens/chat_detail_screen.dart index 47dd8ded..5c55db6f 100644 --- a/lib/presentation/chat_detail/screens/chat_detail_screen.dart +++ b/lib/presentation/chat_detail/screens/chat_detail_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import '../bloc/chat_detail_bloc.dart'; import '../bloc/chat_detail_event.dart'; import '../bloc/chat_detail_state.dart'; @@ -22,7 +23,7 @@ class ChatDetailScreen extends StatelessWidget { title: const Text('JACK FOOTSEY'), leading: IconButton( icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.go('/'), ), ), body: BlocBuilder( @@ -59,11 +60,15 @@ class ChatDetailScreen extends StatelessWidget { Widget _buildMessageBubble(ChatMessage message) { return Container( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - alignment: message.sender == 'Mostro' ? Alignment.centerLeft : Alignment.centerRight, + alignment: message.sender == 'Mostro' + ? Alignment.centerLeft + : Alignment.centerRight, child: Container( padding: const EdgeInsets.all(12), decoration: BoxDecoration( - color: message.sender == 'Mostro' ? const Color(0xFF303544) : const Color(0xFF8CC541), + color: message.sender == 'Mostro' + ? const Color(0xFF303544) + : const Color(0xFF8CC541), borderRadius: BorderRadius.circular(12), ), child: Text( @@ -101,7 +106,9 @@ class ChatDetailScreen extends StatelessWidget { icon: const Icon(Icons.send, color: Color(0xFF8CC541)), onPressed: () { if (controller.text.isNotEmpty) { - context.read().add(SendMessage(controller.text)); + context + .read() + .add(SendMessage(controller.text)); controller.clear(); } }, @@ -110,4 +117,4 @@ class ChatDetailScreen extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/home/bloc/home_bloc.dart b/lib/presentation/home/bloc/home_bloc.dart deleted file mode 100644 index 23cadca9..00000000 --- a/lib/presentation/home/bloc/home_bloc.dart +++ /dev/null @@ -1,58 +0,0 @@ -import 'dart:async'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:mostro_mobile/data/models/enums/order_type.dart'; -import 'package:mostro_mobile/data/models/order_model.dart'; -import 'package:mostro_mobile/presentation/home/bloc/home_event.dart'; -import 'package:mostro_mobile/presentation/home/bloc/home_state.dart'; - -class HomeBloc extends Bloc { - StreamSubscription? ordersSubscription; - - HomeBloc() : super(HomeState.initial()) { - on(_onLoadOrders); - on(_onChangeOrderType); - on(_onOrderReceived); - on(_onOrdersError); - } - - Future _onLoadOrders(LoadOrders event, Emitter emit) async { - } - - void _onOrderReceived(OrderReceived event, Emitter emit) { - final updatedAllOrders = List.from(state.allOrders)..add(event.order); - final updatedFilteredOrders = _filterOrdersByType(updatedAllOrders, state.orderType); - emit(state.copyWith( - status: HomeStatus.loaded, - allOrders: List.unmodifiable(updatedAllOrders), - filteredOrders: updatedFilteredOrders, - )); - } - - void _onOrdersError(OrdersError event, Emitter emit) { - emit(state.copyWith( - status: HomeStatus.error, - errorMessage: event.message, - )); - } - - void _onChangeOrderType(ChangeOrderType event, Emitter emit) { - emit(state.copyWith( - orderType: event.orderType, - filteredOrders: _filterOrdersByType(state.allOrders, event.orderType), - )); - } - - List _filterOrdersByType( - List orders, OrderType type) { - return orders - .where((order) => - type == OrderType.buy ? order.type == 'buy' : order.type == 'sell') - .toList(); - } - - @override - Future close() { - ordersSubscription?.cancel(); - return super.close(); - } -} diff --git a/lib/presentation/home/bloc/home_event.dart b/lib/presentation/home/bloc/home_event.dart deleted file mode 100644 index e4bd90cf..00000000 --- a/lib/presentation/home/bloc/home_event.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:mostro_mobile/data/models/enums/order_type.dart'; -import 'package:mostro_mobile/data/models/order_model.dart'; - -abstract class HomeEvent extends Equatable { - const HomeEvent(); - - @override - List get props => []; -} - -class LoadOrders extends HomeEvent {} - -class ChangeOrderType extends HomeEvent { - final OrderType orderType; - - const ChangeOrderType(this.orderType); - - @override - List get props => [orderType]; -} - -class OrderReceived extends HomeEvent { - final OrderModel order; - - const OrderReceived(this.order); - - @override - List get props => [order]; -} - -class OrdersError extends HomeEvent { - final String message; - - const OrdersError(this.message); - - @override - List get props => [message]; -} diff --git a/lib/presentation/home/bloc/home_state.dart b/lib/presentation/home/bloc/home_state.dart deleted file mode 100644 index 18916a94..00000000 --- a/lib/presentation/home/bloc/home_state.dart +++ /dev/null @@ -1,51 +0,0 @@ -import 'package:equatable/equatable.dart'; -import 'package:mostro_mobile/data/models/enums/order_type.dart'; -import 'package:mostro_mobile/data/models/order_model.dart'; - -enum HomeStatus { initial, loading, loaded, error } - -class HomeState extends Equatable { - final HomeStatus status; - final List allOrders; - final List filteredOrders; - final OrderType orderType; - final String errorMessage; - - const HomeState({ - required this.status, - required this.allOrders, - required this.filteredOrders, - required this.orderType, - required this.errorMessage, - }); - - factory HomeState.initial() { - return const HomeState( - status: HomeStatus.initial, - allOrders: [], - filteredOrders: [], - orderType: OrderType.buy, - errorMessage: "", - ); - } - - HomeState copyWith({ - HomeStatus? status, - List? allOrders, - List? filteredOrders, - OrderType? orderType, - String? errorMessage, - }) { - return HomeState( - status: status ?? this.status, - allOrders: allOrders ?? this.allOrders, - filteredOrders: filteredOrders ?? this.filteredOrders, - orderType: orderType ?? this.orderType, - errorMessage: errorMessage ?? this.errorMessage, - ); - } - - @override - List get props => - [status, allOrders, filteredOrders, orderType, errorMessage]; -} diff --git a/lib/presentation/home/screens/home_screen.dart b/lib/presentation/home/screens/home_screen.dart deleted file mode 100644 index 7d383576..00000000 --- a/lib/presentation/home/screens/home_screen.dart +++ /dev/null @@ -1,175 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:mostro_mobile/data/models/enums/order_type.dart'; -import 'package:mostro_mobile/data/models/nostr_event.dart'; -import 'package:mostro_mobile/presentation/home/bloc/home_bloc.dart'; -import 'package:mostro_mobile/presentation/home/bloc/home_event.dart'; -import 'package:mostro_mobile/presentation/home/bloc/home_state.dart'; -import 'package:mostro_mobile/presentation/widgets/bottom_nav_bar.dart'; -import 'package:mostro_mobile/presentation/widgets/custom_app_bar.dart'; -import 'package:mostro_mobile/presentation/widgets/order_filter.dart'; -import 'package:mostro_mobile/presentation/widgets/order_list.dart'; -import 'package:mostro_mobile/providers/event_store_providers.dart'; - -class HomeScreen extends ConsumerWidget { - const HomeScreen({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - // Cargar las órdenes cuando se inicia la pantalla - context.read().add(LoadOrders()); - return Scaffold( - backgroundColor: const Color(0xFF1D212C), - appBar: const CustomAppBar(), - body: RefreshIndicator( - onRefresh: () async { - context.read().add(LoadOrders()); - await Future.delayed(const Duration(seconds: 1)); - }, - child: Container( - margin: const EdgeInsets.fromLTRB(16, 16, 16, 16), - decoration: BoxDecoration( - color: const Color(0xFF303544), - borderRadius: BorderRadius.circular(20), - ), - child: Column( - children: [ - _buildTabs(), - _buildFilterButton(context), - const SizedBox(height: 6.0), - Expanded( - child: _buildOrderList(ref), - ), - const BottomNavBar(), - ], - ), - ), - ), - ); - } - - Widget _buildTabs() { - return BlocBuilder( - builder: (context, state) { - return Container( - decoration: const BoxDecoration( - color: Color(0xFF1D212C), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(20), - topRight: Radius.circular(20), - ), - ), - child: Row( - children: [ - Expanded( - child: _buildTab("BUY BTC", state.orderType == OrderType.buy, - OrderType.buy, context), - ), - Expanded( - child: _buildTab("SELL BTC", state.orderType == OrderType.sell, - OrderType.sell, context), - ), - ], - ), - ); - }, - ); - } - - Widget _buildTab( - String text, bool isActive, OrderType type, BuildContext context) { - return GestureDetector( - onTap: () { - context.read().add(ChangeOrderType(type)); - }, - child: Container( - padding: const EdgeInsets.symmetric(vertical: 12), - decoration: BoxDecoration( - color: isActive ? const Color(0xFF303544) : const Color(0xFF1D212C), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(isActive ? 20 : 0), - topRight: Radius.circular(isActive ? 20 : 0), - ), - ), - child: Text( - text, - textAlign: TextAlign.center, - style: TextStyle( - color: isActive ? const Color(0xFF8CC541) : Colors.red, - fontWeight: FontWeight.bold, - ), - ), - ), - ); - } - - Widget _buildFilterButton(BuildContext context) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8), - child: Row( - children: [ - OutlinedButton.icon( - onPressed: () { - showModalBottomSheet( - context: context, - backgroundColor: Colors.transparent, - builder: (BuildContext context) { - return Padding( - padding: const EdgeInsets.all(16.0), - child: OrderFilter(), - ); - }, - ); - }, - icon: const HeroIcon(HeroIcons.funnel, - style: HeroIconStyle.outline, color: Colors.white), - label: const Text("FILTER", style: TextStyle(color: Colors.white)), - style: OutlinedButton.styleFrom( - side: const BorderSide(color: Colors.white), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - ), - ), - ), - const SizedBox(width: 8), - BlocBuilder( - builder: (context, state) { - return Text( - "${state.filteredOrders.length} offers", - style: const TextStyle(color: Colors.white), - ); - }, - ), - ], - ), - ); - } - - Widget _buildOrderList(WidgetRef ref) { - final orderEventsAsync = ref.watch(orderEventsProvider); - - return orderEventsAsync.when( - data: (events) { - return BlocBuilder( - builder: (context, state) { - if (events.isEmpty) { - return const Center( - child: Text( - 'No orders available for this type', - style: TextStyle(color: Colors.white), - ), - ); - } - return OrderList( - orders: events - .where((evt) => evt.orderType == state.orderType) - .toList()); - }, - ); - }, - loading: () => Center(child: CircularProgressIndicator()), - error: (error, stack) => Center(child: Text('Error: $error'))); - } -} diff --git a/lib/presentation/order/bloc/order_details_bloc.dart b/lib/presentation/order/bloc/order_details_bloc.dart deleted file mode 100644 index 4ed1dc45..00000000 --- a/lib/presentation/order/bloc/order_details_bloc.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:mostro_mobile/data/models/enums/action.dart'; -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/nostr_event.dart'; -import 'package:mostro_mobile/presentation/order/bloc/order_details_event.dart'; -import 'package:mostro_mobile/presentation/order/bloc/order_details_state.dart'; -import 'package:mostro_mobile/services/mostro_service.dart'; - -class OrderDetailsBloc extends Bloc { - final MostroService mostroService; - - OrderDetailsBloc(this.mostroService) : super(const OrderDetailsState()) { - on(_onLoadOrderDetails); - on(_onCancelOrder); - on(_onContinueOrder); - on(_onOrderUpdateReceived); - } - - void _onLoadOrderDetails( - LoadOrderDetails event, Emitter emit) { - emit(state.copyWith(status: OrderDetailsStatus.loaded, order: event.order)); - } - - void _onCancelOrder(CancelOrder event, Emitter emit) { - emit(state.copyWith(status: OrderDetailsStatus.cancelled)); - } - - void _onContinueOrder( - ContinueOrder event, Emitter emit) async { - emit(state.copyWith(status: OrderDetailsStatus.loading)); - - late MostroMessage order; - - if (event.order.orderType == OrderType.buy) { - order = await mostroService.takeBuyOrder(event.order.orderId!); - } else { - order = await mostroService.takeSellOrder(event.order.orderId!); - } - - add(OrderUpdateReceived(order)); - } - - void _onOrderUpdateReceived( - OrderUpdateReceived event, Emitter emit) { - switch (event.order.action) { - case Action.addInvoice: - case Action.payInvoice: - case Action.waitingSellerToPay: - emit(state.copyWith(status: OrderDetailsStatus.done)); - break; - case Action.notAllowedByStatus: - emit(state.copyWith( - status: OrderDetailsStatus.error, errorMessage: "Not allowed by status")); - break; - default: - break; - } - } -} diff --git a/lib/presentation/order/bloc/order_details_event.dart b/lib/presentation/order/bloc/order_details_event.dart deleted file mode 100644 index f4bd8add..00000000 --- a/lib/presentation/order/bloc/order_details_event.dart +++ /dev/null @@ -1,39 +0,0 @@ -import 'package:dart_nostr/nostr/model/event/event.dart'; -import 'package:equatable/equatable.dart'; -import 'package:mostro_mobile/data/models/mostro_message.dart'; - -abstract class OrderDetailsEvent extends Equatable { - const OrderDetailsEvent(); - - @override - List get props => []; -} - -class LoadOrderDetails extends OrderDetailsEvent { - final NostrEvent order; - - const LoadOrderDetails(this.order); - - @override - List get props => [order]; -} - -class CancelOrder extends OrderDetailsEvent {} - -class ContinueOrder extends OrderDetailsEvent { - final NostrEvent order; - - const ContinueOrder(this.order); - - @override - List get props => [order]; -} - -class OrderUpdateReceived extends OrderDetailsEvent { - final MostroMessage order; - - const OrderUpdateReceived(this.order); - - @override - List get props => [order]; -} diff --git a/lib/presentation/order/bloc/order_details_state.dart b/lib/presentation/order/bloc/order_details_state.dart deleted file mode 100644 index 9f11534f..00000000 --- a/lib/presentation/order/bloc/order_details_state.dart +++ /dev/null @@ -1,31 +0,0 @@ -import 'package:dart_nostr/nostr/model/event/event.dart'; -import 'package:equatable/equatable.dart'; - -enum OrderDetailsStatus { initial, loading, loaded, error, cancelled, done } - -class OrderDetailsState extends Equatable { - final OrderDetailsStatus status; - final NostrEvent? order; - final String? errorMessage; - - const OrderDetailsState({ - this.status = OrderDetailsStatus.initial, - this.order, - this.errorMessage, - }); - - OrderDetailsState copyWith({ - OrderDetailsStatus? status, - NostrEvent? order, - String? errorMessage, - }) { - return OrderDetailsState( - status: status ?? this.status, - order: order ?? this.order, - errorMessage: errorMessage ?? this.errorMessage, - ); - } - - @override - List get props => [status, order, errorMessage]; -} diff --git a/lib/presentation/order/screens/order_details_screen.dart b/lib/presentation/order/screens/order_details_screen.dart deleted file mode 100644 index a974a1f2..00000000 --- a/lib/presentation/order/screens/order_details_screen.dart +++ /dev/null @@ -1,318 +0,0 @@ -import 'package:dart_nostr/nostr/model/event/event.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:google_fonts/google_fonts.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:mostro_mobile/core/theme/app_theme.dart'; -import 'package:mostro_mobile/data/models/nostr_event.dart'; -import 'package:mostro_mobile/presentation/order/bloc/order_details_bloc.dart'; -import 'package:mostro_mobile/presentation/order/bloc/order_details_event.dart'; -import 'package:mostro_mobile/presentation/order/bloc/order_details_state.dart'; -import 'package:mostro_mobile/presentation/widgets/currency_text_field.dart'; -import 'package:mostro_mobile/presentation/widgets/exchange_rate_widget.dart'; -import 'package:mostro_mobile/providers/exchange_service_provider.dart'; -import 'package:mostro_mobile/providers/riverpod_providers.dart'; - -class OrderDetailsScreen extends ConsumerWidget { - final NostrEvent initialOrder; - - final TextEditingController _satsAmountController = TextEditingController(); - - OrderDetailsScreen({super.key, required this.initialOrder}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final mostroService = ref.watch(mostroServiceProvider); - return BlocProvider( - create: (context) => - OrderDetailsBloc(mostroService)..add(LoadOrderDetails(initialOrder)), - child: BlocConsumer( - listener: (context, state) { - if (state.status == OrderDetailsStatus.done) { - Navigator.of(context).pop(); - } - }, - builder: (context, state) { - switch (state.status) { - case OrderDetailsStatus.loading: - return const Center(child: CircularProgressIndicator()); - case OrderDetailsStatus.error: - return _buildErrorScreen(state.errorMessage, context); - case OrderDetailsStatus.cancelled: - case OrderDetailsStatus.done: - return _buildCompletionMessage(context, state); - case OrderDetailsStatus.loaded: - return _buildContent(context, ref, state.order!); - default: - return const Center(child: Text('Order not found')); - } - }, - ), - ); - } - - Widget _buildErrorScreen(String? errorMessage, BuildContext context) { - return Scaffold( - backgroundColor: AppTheme.dark1, - appBar: _buildAppBar('Error', context), - body: Center( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text( - errorMessage ?? 'An unexpected error occurred.', - style: const TextStyle( - color: Colors.red, - fontSize: 18, - fontWeight: FontWeight.bold, - ), - textAlign: TextAlign.center, - ), - const SizedBox(height: 32), - ElevatedButton( - onPressed: () => Navigator.of(context).pop(), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.cream1, - ), - child: const Text('Return to Main Screen'), - ), - ], - ), - ), - ), - ); - } - - Widget _buildCompletionMessage( - BuildContext context, OrderDetailsState state) { - final message = state.status == OrderDetailsStatus.cancelled - ? 'Order has been cancelled.' - : state.errorMessage ?? - 'Order has been completed successfully!'; // Handles custom errors or success messages - return Scaffold( - backgroundColor: AppTheme.dark1, - appBar: _buildAppBar('Completion', context), - 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: () => Navigator.of(context).pop(), - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, - ), - child: const Text('Return to Main Screen'), - ), - ], - ), - ), - ), - ); - } - - Widget _buildContent(BuildContext context, WidgetRef ref, NostrEvent order) { - return Scaffold( - backgroundColor: AppTheme.dark1, - appBar: _buildAppBar( - '${order.orderType?.value.toUpperCase()} BITCOIN', context), - body: SingleChildScrollView( - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - children: [ - _buildSellerInfo(order), - const SizedBox(height: 16), - _buildSellerAmount(order, ref), - const SizedBox(height: 16), - ExchangeRateWidget(currency: order.currency!), - const SizedBox(height: 16), - _buildBuyerInfo(order), - const SizedBox(height: 16), - _buildBuyerAmount(order), - const SizedBox(height: 24), - _buildActionButtons(context), - ], - ), - ), - ), - ); - } - - PreferredSizeWidget? _buildAppBar(String title, BuildContext context) { - return AppBar( - backgroundColor: Colors.transparent, - elevation: 0, - leading: IconButton( - icon: const HeroIcon(HeroIcons.arrowLeft, color: Colors.white), - onPressed: () => Navigator.of(context).pop(), - ), - title: Text( - title, - style: TextStyle( - color: Colors.white, - fontFamily: GoogleFonts.robotoCondensed().fontFamily, - ), - ), - ); - } - - Widget _buildSellerInfo(NostrEvent order) { - return _infoContainer( - Row( - children: [ - const CircleAvatar( - backgroundColor: AppTheme.grey2, - child: Text('S', style: TextStyle(color: Colors.white)), - ), - const SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(order.name!, - style: const TextStyle( - color: Colors.white, fontWeight: FontWeight.bold)), - Text( - '${order.rating?.totalRating}/${order.rating?.maxRate} (${order.rating?.totalReviews})', - style: const TextStyle(color: AppTheme.mostroGreen), - ), - ], - ), - ), - TextButton( - onPressed: () { - // Implement review logic - }, - child: const Text('Read reviews', - style: TextStyle(color: AppTheme.mostroGreen)), - ), - ], - ), - ); - } - - Widget _buildSellerAmount(NostrEvent order, WidgetRef ref) { - final exchangeRateAsyncValue = - ref.watch(exchangeRateProvider(order.currency!)); - return exchangeRateAsyncValue.when( - loading: () => const CircularProgressIndicator(), - error: (error, _) => Text('Error: $error'), - data: (exchangeRate) { - return _infoContainer( - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('${order.fiatAmount} ${order.currency} (${order.premium}%)', - style: const TextStyle( - color: AppTheme.cream1, - fontSize: 18, - fontWeight: FontWeight.bold)), - Text('${order.amount} sats', - style: const TextStyle(color: AppTheme.grey2)), - ], - ), - ); - }, - ); - } - - Widget _buildBuyerInfo(NostrEvent order) { - return _infoContainer( - const Row( - children: [ - CircleAvatar( - backgroundColor: AppTheme.grey2, - child: Text('A', style: TextStyle(color: Colors.white)), - ), - SizedBox(width: 12), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text('Anon (you)', - style: TextStyle( - color: Colors.white, fontWeight: FontWeight.bold)), - ], - ), - ), - ], - ), - ); - } - - Widget _infoContainer(Widget child) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: AppTheme.dark2, - borderRadius: BorderRadius.circular(12), - ), - child: child, - ); - } - - Widget _buildBuyerAmount(NostrEvent order) { - return _infoContainer( - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - CurrencyTextField(controller: _satsAmountController, label: 'Sats'), - const SizedBox(height: 8), - Text('\$ ${order.amount}', - style: const TextStyle(color: AppTheme.grey2)), - ], - ), - ); - } - - Widget _buildActionButtons(BuildContext context) { - return Row( - children: [ - Expanded( - child: ElevatedButton( - onPressed: () { - Navigator.of(context).pop(); - }, - style: ElevatedButton.styleFrom( - backgroundColor: Colors.red, - ), - child: const Text('CANCEL'), - ), - ), - const SizedBox(width: 16), - Expanded( - child: ElevatedButton( - onPressed: () { - context.read().add(ContinueOrder(initialOrder)); - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppTheme.mostroGreen, - ), - child: const Text('CONTINUE'), - ), - ), - ], - ); - } -} diff --git a/lib/presentation/payment_confirmation/screens/payment_confirmation_screen.dart b/lib/presentation/payment_confirmation/screens/payment_confirmation_screen.dart index e26f95ef..b755cbcc 100644 --- a/lib/presentation/payment_confirmation/screens/payment_confirmation_screen.dart +++ b/lib/presentation/payment_confirmation/screens/payment_confirmation_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import '../bloc/payment_confirmation_bloc.dart'; import '../bloc/payment_confirmation_event.dart'; import '../bloc/payment_confirmation_state.dart'; @@ -11,7 +12,8 @@ class PaymentConfirmationScreen extends StatelessWidget { @override Widget build(BuildContext context) { return BlocProvider( - create: (context) => PaymentConfirmationBloc()..add(LoadPaymentConfirmation()), + create: (context) => + PaymentConfirmationBloc()..add(LoadPaymentConfirmation()), child: Scaffold( backgroundColor: const Color(0xFF1D212C), appBar: AppBar( @@ -20,7 +22,7 @@ class PaymentConfirmationScreen extends StatelessWidget { title: const Text('PAYMENT', style: TextStyle(color: Colors.white)), leading: IconButton( icon: const Icon(Icons.arrow_back, color: Colors.white), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.go('/'), ), ), body: BlocBuilder( @@ -78,7 +80,9 @@ class PaymentConfirmationScreen extends StatelessWidget { minimumSize: const Size(double.infinity, 50), ), onPressed: () { - context.read().add(ContinueAfterConfirmation()); + context + .read() + .add(ContinueAfterConfirmation()); // Aquí puedes navegar a la siguiente pantalla o realizar la acción necesaria }, child: const Text('CONTINUE'), @@ -108,4 +112,4 @@ class PaymentConfirmationScreen extends StatelessWidget { ), ); } -} \ No newline at end of file +} diff --git a/lib/presentation/payment_qr/screens/payment_qr_screen.dart b/lib/presentation/payment_qr/screens/payment_qr_screen.dart index 0da0c2e8..8ca11742 100644 --- a/lib/presentation/payment_qr/screens/payment_qr_screen.dart +++ b/lib/presentation/payment_qr/screens/payment_qr_screen.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import '../bloc/payment_qr_bloc.dart'; import '../bloc/payment_qr_event.dart'; import '../bloc/payment_qr_state.dart'; @@ -20,7 +21,7 @@ class PaymentQrScreen extends StatelessWidget { title: const Text('PAYMENT'), leading: IconButton( icon: const Icon(Icons.arrow_back), - onPressed: () => Navigator.of(context).pop(), + onPressed: () => context.go('/'), ), ), body: BlocBuilder( @@ -56,9 +57,9 @@ class PaymentQrScreen extends StatelessWidget { ), const SizedBox(height: 20), TextButton( - child: - const Text('CANCEL', style: TextStyle(color: Colors.white)), - onPressed: () => Navigator.of(context).pop(), + child: const Text('CANCEL', + style: TextStyle(color: Colors.white)), + onPressed: () => context.go('/'), ), ], ); diff --git a/lib/presentation/widgets/bottom_nav_bar.dart b/lib/presentation/widgets/bottom_nav_bar.dart index d4acf823..e7988011 100644 --- a/lib/presentation/widgets/bottom_nav_bar.dart +++ b/lib/presentation/widgets/bottom_nav_bar.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:heroicons/heroicons.dart'; -import 'package:mostro_mobile/core/routes/app_routes.dart'; class BottomNavBar extends StatelessWidget { const BottomNavBar({super.key}); @@ -51,39 +51,38 @@ class BottomNavBar extends StatelessWidget { } bool _isActive(BuildContext context, int index) { - final currentRoute = ModalRoute.of(context)?.settings.name; + final currentLocation = GoRouterState.of(context).uri.toString(); switch (index) { case 0: - return currentRoute == AppRoutes.home; + return currentLocation == '/'; case 1: - return currentRoute == AppRoutes.chatList; + return currentLocation == '/chat_list'; case 2: - return currentRoute == AppRoutes.profile; + return currentLocation == '/profile'; default: return false; } } void _onItemTapped(BuildContext context, int index) { - final currentRoute = ModalRoute.of(context)?.settings.name; String nextRoute; - switch (index) { case 0: - nextRoute = AppRoutes.home; + nextRoute = '/'; break; case 1: - nextRoute = AppRoutes.chatList; + nextRoute = '/chat_list'; break; case 2: - nextRoute = AppRoutes.profile; + nextRoute = '/profile'; break; default: return; } - if (currentRoute != nextRoute) { - Navigator.pushReplacementNamed(context, nextRoute); + final currentLocation = GoRouterState.of(context).uri.toString(); + if (currentLocation != nextRoute) { + context.go(nextRoute); } } } diff --git a/lib/presentation/widgets/currency_dropdown.dart b/lib/presentation/widgets/currency_dropdown.dart index c0129464..6fe4347c 100644 --- a/lib/presentation/widgets/currency_dropdown.dart +++ b/lib/presentation/widgets/currency_dropdown.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/core/theme/app_theme.dart'; -import 'package:mostro_mobile/providers/exchange_service_provider.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; +import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart'; class CurrencyDropdown extends ConsumerWidget { final String label; diff --git a/lib/presentation/widgets/currency_text_field.dart b/lib/presentation/widgets/currency_text_field.dart index 488efd2e..ca8ca692 100644 --- a/lib/presentation/widgets/currency_text_field.dart +++ b/lib/presentation/widgets/currency_text_field.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; import 'package:mostro_mobile/services/currency_input_formatter.dart'; class CurrencyTextField extends StatelessWidget { @@ -14,7 +15,7 @@ class CurrencyTextField extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), decoration: BoxDecoration( - color: const Color(0xFF1D212C), + color: AppTheme.dark1, borderRadius: BorderRadius.circular(8), ), child: TextFormField( diff --git a/lib/presentation/widgets/custom_app_bar.dart b/lib/presentation/widgets/custom_app_bar.dart index de620783..ea8a1fcd 100644 --- a/lib/presentation/widgets/custom_app_bar.dart +++ b/lib/presentation/widgets/custom_app_bar.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; import 'package:heroicons/heroicons.dart'; -import 'package:mostro_mobile/presentation/add_order/screens/add_order_screen.dart'; class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { const CustomAppBar({super.key}); @@ -23,8 +23,7 @@ class CustomAppBar extends StatelessWidget implements PreferredSizeWidget { icon: const HeroIcon(HeroIcons.plus, style: HeroIconStyle.outline, color: Colors.white), onPressed: () { - Navigator.of(context).push( - MaterialPageRoute(builder: (context) => AddOrderScreen())); + context.go('/add_order'); }, ), IconButton( diff --git a/lib/presentation/widgets/custom_button.dart b/lib/presentation/widgets/custom_button.dart index 0d6ad9ea..c3c890d5 100644 --- a/lib/presentation/widgets/custom_button.dart +++ b/lib/presentation/widgets/custom_button.dart @@ -1,5 +1,5 @@ import 'package:flutter/material.dart'; -import '../../core/theme/app_theme.dart'; +import '../../app/app_theme.dart'; class CustomButton extends StatelessWidget { final String text; diff --git a/lib/presentation/widgets/exchange_rate_widget.dart b/lib/presentation/widgets/exchange_rate_widget.dart index b6f12203..2117d41b 100644 --- a/lib/presentation/widgets/exchange_rate_widget.dart +++ b/lib/presentation/widgets/exchange_rate_widget.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:intl/intl.dart'; -import 'package:mostro_mobile/providers/exchange_service_provider.dart'; +import 'package:mostro_mobile/shared/providers/exchange_service_provider.dart'; class ExchangeRateWidget extends ConsumerWidget { final String currency; diff --git a/lib/presentation/widgets/order_list_item.dart b/lib/presentation/widgets/order_list_item.dart deleted file mode 100644 index a0befac3..00000000 --- a/lib/presentation/widgets/order_list_item.dart +++ /dev/null @@ -1,193 +0,0 @@ -import 'package:dart_nostr/nostr/model/event/event.dart'; -import 'package:flutter/material.dart'; -import 'package:heroicons/heroicons.dart'; -import 'package:mostro_mobile/core/theme/app_theme.dart'; -import 'package:mostro_mobile/data/models/enums/order_type.dart'; -import 'package:mostro_mobile/data/models/nostr_event.dart'; -import 'package:mostro_mobile/presentation/order/screens/order_details_screen.dart'; -import 'package:google_fonts/google_fonts.dart'; - -class OrderListItem extends StatelessWidget { - final NostrEvent order; - - const OrderListItem({super.key, required this.order}); - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: () { - Navigator.of(context).push( - MaterialPageRoute( - builder: (context) => OrderDetailsScreen(initialOrder: order), - ), - ); - }, - child: Container( - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - decoration: BoxDecoration( - color: const Color(0xFF1D212C), - borderRadius: BorderRadius.circular(12), - ), - child: Padding( - padding: const EdgeInsets.all(16), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - '${order.name} ${order.rating?.totalRating ?? 0}/${order.rating?.maxRate ?? 5} (${order.rating?.totalReviews ?? 0})', - style: const TextStyle(color: Colors.white), - ), - Text( - 'Time: ${order.expiration}', - style: const TextStyle(color: Colors.white), - ), - ], - ), - const SizedBox(height: 16), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _getOrder(order), - const SizedBox(width: 16), - Expanded( - flex: 4, - child: Row( - children: [ - HeroIcon( - _getPaymentMethodIcon(order.paymentMethods.isNotEmpty - ? order.paymentMethods[0] - : ''), - style: HeroIconStyle.outline, - color: Colors.white, - size: 16, - ), - const SizedBox(width: 4), - Flexible( - child: Text( - order.paymentMethods.isNotEmpty - ? order.paymentMethods[0] - : 'No payment method', - style: const TextStyle(color: Colors.grey), - overflow: TextOverflow.visible, - softWrap: true, - ), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 8), - ], - ), - ), - ), - ); - } - - Expanded _getOrder(NostrEvent order) { - String offering; - if (order.orderType == OrderType.buy) { - offering = 'Buying'; - } else { - offering = 'Selling'; - } - - return Expanded( - flex: 3, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text.rich( - TextSpan( - children: [ - _buildStyledTextSpan( - offering, - (order.amount != null && order.amount != '0') ? ' ${order.amount!}' : '', - isValue: true, - isBold: true, - ), - const TextSpan( - text: "sats", - style: TextStyle( - color: Colors.white, - fontWeight: FontWeight.normal, - fontSize: 16.0, - ), - ), - ], - ), - ), - const SizedBox(height: 8.0), - Text.rich( - TextSpan( - children: [ - _buildStyledTextSpan( - 'for ', - '${order.fiatAmount}', - isValue: true, - isBold: true, - ), - TextSpan( - text: '${order.currency} ', - style: const TextStyle( - color: Colors.white, - fontSize: 16.0, - ), - ), - TextSpan( - text: '(${order.premium}%)', - style: const TextStyle( - color: Colors.white, - fontSize: 16.0, - ), - ), - ], - ), - ), - ], - ), - ); - } - - HeroIcons _getPaymentMethodIcon(String method) { - switch (method.toLowerCase()) { - case 'wire transfer': - return HeroIcons.buildingLibrary; - case 'transferencia bancaria': - return HeroIcons.buildingLibrary; - case 'revolut': - return HeroIcons.creditCard; - default: - return HeroIcons.banknotes; - } - } - - TextSpan _buildStyledTextSpan(String label, String value, - {bool isValue = false, bool isBold = false}) { - return TextSpan( - text: label, - style: TextStyle( - color: AppTheme.cream1, - fontWeight: FontWeight.normal, - fontSize: isValue ? 16.0 : 24.0, - fontFamily: GoogleFonts.robotoCondensed().fontFamily, - ), - children: isValue - ? [ - TextSpan( - text: '$value ', - style: const TextStyle( - fontSize: 24.0, - fontWeight: FontWeight.normal, - ), - ), - ] - : [], - ); - } -} diff --git a/lib/providers/event_store_providers.dart b/lib/providers/event_store_providers.dart deleted file mode 100644 index 90ef3ba3..00000000 --- a/lib/providers/event_store_providers.dart +++ /dev/null @@ -1,30 +0,0 @@ -import 'package:dart_nostr/nostr/model/event/event.dart'; -import 'package:dart_nostr/nostr/model/request/filter.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:mostro_mobile/data/repositories/open_orders_repository.dart'; -import 'package:mostro_mobile/services/nostr_service.dart'; - -final nostrServicerProvider = Provider((ref) { - return NostrService()..init(); -}); - -final orderRepositoryProvider = Provider((ref) { - final nostrService = ref.read(nostrServicerProvider); - return OpenOrdersRepository(nostrService); -}); - -/// Event kind 38383 represents order events in the Nostr protocol as per NIP-69 -const orderEventKind = 38383; -const orderFilterDurationHours = 24; - -final orderEventsProvider = StreamProvider>((ref) { - final orderRepository = ref.watch(orderRepositoryProvider); - DateTime filterTime = DateTime.now().subtract(Duration(hours: orderFilterDurationHours)); - var filter = NostrFilter( - kinds: const [orderEventKind], - since: filterTime, - ); - orderRepository.subscribe(filter); - - return orderRepository.eventsStream; -}); \ No newline at end of file diff --git a/lib/services/mostro_service.dart b/lib/services/mostro_service.dart index 28916515..e35b6e19 100644 --- a/lib/services/mostro_service.dart +++ b/lib/services/mostro_service.dart @@ -2,91 +2,107 @@ import 'dart:collection'; import 'dart:convert'; import 'package:dart_nostr/nostr/model/event/event.dart'; import 'package:dart_nostr/nostr/model/request/filter.dart'; -import 'package:mostro_mobile/core/config.dart'; -import 'package:mostro_mobile/data/models/content.dart'; +import 'package:mostro_mobile/app/config.dart'; import 'package:mostro_mobile/data/models/mostro_message.dart'; -import 'package:mostro_mobile/data/models/order.dart'; import 'package:mostro_mobile/data/models/enums/action.dart'; import 'package:mostro_mobile/data/models/session.dart'; -import 'package:mostro_mobile/data/repositories/mostro_repository.dart'; import 'package:mostro_mobile/data/repositories/secure_storage_manager.dart'; import 'package:mostro_mobile/services/nostr_service.dart'; -const int mostroVersion = 1; - class MostroService { final NostrService _nostrService; + //final MostroInstance _instance; final SecureStorageManager _secureStorageManager; - final _mostroRepository = MostroRepository(); - final _orders = HashMap(); final _sessions = HashMap(); MostroService(this._nostrService, this._secureStorageManager); + Stream subscribe(Session session) { + final filter = NostrFilter(p: [session.publicKey]); + return _nostrService.subscribeToEvents(filter).asyncMap((event) async { + try { + final decryptedEvent = + await _nostrService.decryptNIP59Event(event, session.privateKey); + final msg = MostroMessage.deserialized(decryptedEvent.content!); + session.eventId = msg.requestId; + return msg; + } catch (e) { + print('Error processing event: $e'); + return MostroMessage(action: Action.canceled, requestId: ""); + } + }); + } + Stream subscribeToOrders(NostrFilter filter) { return _nostrService.subscribeToEvents(filter); } - Future publishOrder(Order order) async { + Future takeSellOrder( + String orderId, int? amount, String? lnAddress) async { final session = await _secureStorageManager.newSession(); + session.eventId = orderId; + _sessions[orderId] = session; + + final order = lnAddress != null + ? { + 'payment_request': [null, lnAddress, amount] + } + : amount != null + ? {'amount': amount} + : null; final content = jsonEncode({ 'order': { - 'version': mostroVersion, - 'action': Action.newOrder.value, - 'content': order.toJson(), + 'version': Config.mostroVersion, + 'id': orderId, + 'action': Action.takeSell.value, + 'content': order, }, }); final event = await _nostrService.createNIP59Event( content, Config.mostroPubKey, session.privateKey); - await _nostrService.publishEvent(event); - - final filter = NostrFilter(p: [session.publicKey]); - - return await subscribeToOrders(filter).asyncMap((event) async { - return await _nostrService.decryptNIP59Event(event, session.privateKey); - }).map((event) { - return MostroMessage.deserialized(event.content!); - }).first; + return session; } - Future takeSellOrder(String orderId, {int? amount}) async { - final session = await _secureStorageManager.newSession(); - session.eventId = orderId; + Future sendInvoice(String orderId, String invoice) async { + final session = _sessions[orderId]; + + if (session == null) { + throw Exception('Session not found for order ID: $orderId'); + } final content = jsonEncode({ 'order': { - 'version': mostroVersion, + 'version': Config.mostroVersion.toInt(), 'id': orderId, - 'action': Action.takeSell.value, - 'content': amount != null ? {'amount': amount} : null, + 'action': Action.addInvoice.value, + 'content': { + 'payment_request': [ + null, + invoice, + null, + ] + }, }, }); + final event = await _nostrService.createNIP59Event( content, Config.mostroPubKey, session.privateKey); await _nostrService.publishEvent(event); - - final filter = NostrFilter(p: [session.publicKey]); - - return await subscribeToOrders(filter).asyncMap((event) async { - return await _nostrService.decryptNIP59Event(event, session.privateKey); - }).map((event) { - return MostroMessage.deserialized(event.content!); - }).first; } - Future> takeBuyOrder(String orderId, - {int? amount}) async { + Future takeBuyOrder(String orderId, int? amount) async { final session = await _secureStorageManager.newSession(); session.eventId = orderId; + _sessions[orderId] = session; final content = jsonEncode({ 'order': { - 'version': mostroVersion, + 'version': Config.mostroVersion, 'id': orderId, 'action': Action.takeBuy.value, 'content': amount != null ? {'amount': amount} : null, @@ -95,23 +111,23 @@ class MostroService { final event = await _nostrService.createNIP59Event( content, Config.mostroPubKey, session.privateKey); await _nostrService.publishEvent(event); - final filter = NostrFilter(p: [session.publicKey]); - - return await subscribeToOrders(filter).asyncMap((event) async { - return await _nostrService.decryptNIP59Event(event, session.privateKey); - }).map((event) { - return MostroMessage.deserialized(event.content!); - }).first; + return session; } - Future cancelOrder(String orderId) async { - final order = _mostroRepository.getOrder(orderId); + Future publishOrder(MostroMessage order) async { + final session = await _secureStorageManager.newSession(); - if (order == null) { - throw Exception('Order not found for order ID: $orderId'); - } + final content = jsonEncode(order.toJson()); + + final event = await _nostrService.createNIP59Event( + content, Config.mostroPubKey, session.privateKey); - final session = await _secureStorageManager.loadSession(order!.requestId!); + await _nostrService.publishEvent(event); + return session; + } + + Future cancelOrder(String orderId) async { + final session = _sessions[orderId]; if (session == null) { throw Exception('Session not found for order ID: $orderId'); @@ -119,9 +135,9 @@ class MostroService { final content = jsonEncode({ 'order': { - 'version': mostroVersion, + 'version': Config.mostroVersion, 'id': orderId, - 'action': Action.cancel, + 'action': Action.cancel.value, 'content': null, }, }); @@ -139,7 +155,7 @@ class MostroService { final content = jsonEncode({ 'order': { - 'version': mostroVersion, + 'version': Config.mostroVersion, 'id': orderId, 'action': Action.fiatSent.value, 'content': null, @@ -159,7 +175,7 @@ class MostroService { final content = jsonEncode({ 'order': { - 'version': mostroVersion, + 'version': Config.mostroVersion, 'id': orderId, 'action': Action.release.value, 'content': null, diff --git a/lib/services/nostr_service.dart b/lib/services/nostr_service.dart index 225b20ae..5e55415e 100644 --- a/lib/services/nostr_service.dart +++ b/lib/services/nostr_service.dart @@ -1,8 +1,8 @@ import 'package:dart_nostr/dart_nostr.dart'; -import 'package:mostro_mobile/core/config.dart'; +import 'package:mostro_mobile/app/config.dart'; import 'package:logging/logging.dart'; -import 'package:mostro_mobile/core/utils/auth_utils.dart'; -import 'package:mostro_mobile/core/utils/nostr_utils.dart'; +import 'package:mostro_mobile/shared/utils/auth_utils.dart'; +import 'package:mostro_mobile/shared/utils/nostr_utils.dart'; class NostrService { static final NostrService _instance = NostrService._internal(); @@ -21,7 +21,8 @@ class NostrService { await _nostr.relaysService.init( relaysUrl: Config.nostrRelays, connectionTimeout: Config.nostrConnectionTimeout, - onRelayListening: (relay, url, channel) { + onRelayListening: (relay, url, + channel) { _logger.info('Connected to relay: $url'); }, onRelayConnectionError: (relay, error, channel) { diff --git a/lib/shared/notifiers/app_settings_controller.dart b/lib/shared/notifiers/app_settings_controller.dart new file mode 100644 index 00000000..c383dd78 --- /dev/null +++ b/lib/shared/notifiers/app_settings_controller.dart @@ -0,0 +1,17 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/app/app_settings.dart'; +import 'package:mostro_mobile/shared/providers/shared_preferences_provider.dart'; + +class AppSettingsController extends StateNotifier { + final Ref ref; + + AppSettingsController(this.ref) : super(AppSettings.intial()); + + Future loadSettings() async { + final prefs = ref.read(sharedPreferencesProvider); + + final isFirstLaunch = await prefs.getBool('isFirstLaunch') ?? true; + + state = state.copyWith(isFirstLaunch: isFirstLaunch); + } +} diff --git a/lib/shared/notifiers/navigation_notifier.dart b/lib/shared/notifiers/navigation_notifier.dart new file mode 100644 index 00000000..ceb85937 --- /dev/null +++ b/lib/shared/notifiers/navigation_notifier.dart @@ -0,0 +1,15 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class NavigationState { + final String path; + + NavigationState(this.path); +} + +class NavigationNotifier extends StateNotifier { + NavigationNotifier() : super(NavigationState('/')); + + void go(String path) { + state = NavigationState(path); + } +} diff --git a/lib/shared/notifiers/notification_notifier.dart b/lib/shared/notifiers/notification_notifier.dart new file mode 100644 index 00000000..0327a637 --- /dev/null +++ b/lib/shared/notifiers/notification_notifier.dart @@ -0,0 +1,27 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; + +class NotificationState { + final String? message; + final WidgetBuilder? widgetBuilder; + final bool informational; + final bool actionRequired; + + NotificationState( + {this.message, + this.widgetBuilder, + this.informational = false, + this.actionRequired = false}); +} + +class NotificationNotifier extends StateNotifier { + NotificationNotifier() : super(NotificationState()); + + void showInformation(String message) { + state = NotificationState(message: message, informational: true); + } + + void clearNotification() { + state = NotificationState(); + } +} diff --git a/lib/shared/providers/app_settings_controller_provider.dart b/lib/shared/providers/app_settings_controller_provider.dart new file mode 100644 index 00000000..c4645899 --- /dev/null +++ b/lib/shared/providers/app_settings_controller_provider.dart @@ -0,0 +1,8 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/app/app_settings.dart'; +import 'package:mostro_mobile/shared/notifiers/app_settings_controller.dart'; + +final appSettingsControllerProvider = + StateNotifierProvider((ref) { + return AppSettingsController(ref); +}); \ No newline at end of file diff --git a/lib/providers/exchange_service_provider.dart b/lib/shared/providers/exchange_service_provider.dart similarity index 100% rename from lib/providers/exchange_service_provider.dart rename to lib/shared/providers/exchange_service_provider.dart diff --git a/lib/providers/riverpod_providers.dart b/lib/shared/providers/mostro_service_provider.dart similarity index 63% rename from lib/providers/riverpod_providers.dart rename to lib/shared/providers/mostro_service_provider.dart index 0896f50f..26557d21 100644 --- a/lib/providers/riverpod_providers.dart +++ b/lib/shared/providers/mostro_service_provider.dart @@ -1,16 +1,8 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/repositories/mostro_repository.dart'; import 'package:mostro_mobile/data/repositories/secure_storage_manager.dart'; -import 'package:mostro_mobile/presentation/home/bloc/home_bloc.dart'; import 'package:mostro_mobile/services/mostro_service.dart'; -import 'package:mostro_mobile/services/nostr_service.dart'; - -final nostrServicerProvider = Provider((ref) { - return NostrService()..init(); -}); - -final homeBlocProvider = Provider((ref) { - return HomeBloc(); -}); +import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; final sessionManagerProvider = Provider((ref) { return SecureStorageManager(); @@ -22,3 +14,7 @@ final mostroServiceProvider = Provider((ref) { return MostroService(nostrService, sessionStorage); }); +final mostroRepositoryProvider = Provider((ref) { + final mostroService = ref.watch(mostroServiceProvider); + return MostroRepository(mostroService); +}); diff --git a/lib/shared/providers/navigation_notifier_provider.dart b/lib/shared/providers/navigation_notifier_provider.dart new file mode 100644 index 00000000..bc0c672c --- /dev/null +++ b/lib/shared/providers/navigation_notifier_provider.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/shared/notifiers/navigation_notifier.dart'; + +final navigationProvider = + StateNotifierProvider( + (ref) => NavigationNotifier()); diff --git a/lib/shared/providers/nostr_service_provider.dart b/lib/shared/providers/nostr_service_provider.dart new file mode 100644 index 00000000..7d177cda --- /dev/null +++ b/lib/shared/providers/nostr_service_provider.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/services/nostr_service.dart'; + +final nostrServicerProvider = Provider((ref) { + return NostrService(); +}); diff --git a/lib/shared/providers/notification_notifier_provider.dart b/lib/shared/providers/notification_notifier_provider.dart new file mode 100644 index 00000000..48b3a6a0 --- /dev/null +++ b/lib/shared/providers/notification_notifier_provider.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/shared/notifiers/notification_notifier.dart'; + +final notificationProvider = + StateNotifierProvider( + (ref) => NotificationNotifier()); diff --git a/lib/shared/providers/order_repository_provider.dart b/lib/shared/providers/order_repository_provider.dart new file mode 100644 index 00000000..190b6a2b --- /dev/null +++ b/lib/shared/providers/order_repository_provider.dart @@ -0,0 +1,21 @@ +import 'package:dart_nostr/dart_nostr.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:mostro_mobile/data/repositories/open_orders_repository.dart'; +import 'package:mostro_mobile/shared/providers/nostr_service_provider.dart'; + +final orderRepositoryProvider = Provider((ref) { + final nostrService = ref.read(nostrServicerProvider); + return OpenOrdersRepository(nostrService); +}); + +final orderEventsProvider = StreamProvider>((ref) { + final orderRepository = ref.watch(orderRepositoryProvider); + orderRepository.subscribeToOrders(); + + return orderRepository.eventsStream; +}); + +final eventProvider = Provider.family((ref, orderId) { + final repository = ref.watch(orderRepositoryProvider); + return repository.getOrderById(orderId); +}); \ No newline at end of file diff --git a/lib/shared/providers/shared_preferences_provider.dart b/lib/shared/providers/shared_preferences_provider.dart new file mode 100644 index 00000000..1983aea7 --- /dev/null +++ b/lib/shared/providers/shared_preferences_provider.dart @@ -0,0 +1,6 @@ +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +final sharedPreferencesProvider = Provider((ref) { + return SharedPreferencesAsync(); +}); \ No newline at end of file diff --git a/lib/core/utils/auth_utils.dart b/lib/shared/utils/auth_utils.dart similarity index 100% rename from lib/core/utils/auth_utils.dart rename to lib/shared/utils/auth_utils.dart diff --git a/lib/core/utils/biometrics_helper.dart b/lib/shared/utils/biometrics_helper.dart similarity index 100% rename from lib/core/utils/biometrics_helper.dart rename to lib/shared/utils/biometrics_helper.dart diff --git a/lib/core/utils/nostr_utils.dart b/lib/shared/utils/nostr_utils.dart similarity index 92% rename from lib/core/utils/nostr_utils.dart rename to lib/shared/utils/nostr_utils.dart index e036aab0..5b686d29 100644 --- a/lib/core/utils/nostr_utils.dart +++ b/lib/shared/utils/nostr_utils.dart @@ -2,6 +2,8 @@ import 'dart:convert'; import 'dart:math'; import 'package:crypto/crypto.dart'; import 'package:dart_nostr/dart_nostr.dart'; +import 'package:bip32_bip44/dart_bip32_bip44.dart' as bip32_bip44; +import 'package:bip39/bip39.dart' as bip39; import 'package:elliptic/elliptic.dart'; import 'package:nip44/nip44.dart'; @@ -301,4 +303,22 @@ class NostrUtils { throw Exception('Decryption failed: $e'); } } + + static String getPrivateKeyFromMnemonic(String mnemonic, int index) { + final seed = bip39.mnemonicToSeedHex(mnemonic); + final chain = bip32_bip44.Chain.seed(seed); + + final key = chain.forPath("m/44'/1237'/38383'/0/0") + as bip32_bip44.ExtendedPrivateKey; + final childKey = bip32_bip44.deriveExtendedPrivateChildKey(key, index); + return (childKey.key != null) ? childKey.key!.toRadixString(16) : ''; + } + + static bool isMnemonicValid(String text) { + return bip39.validateMnemonic(text); + } + + static String getMnemonic() { + return bip39.generateMnemonic(); + } } diff --git a/lib/shared/widgets/custom_card.dart b/lib/shared/widgets/custom_card.dart new file mode 100644 index 00000000..af1230cd --- /dev/null +++ b/lib/shared/widgets/custom_card.dart @@ -0,0 +1,35 @@ +import 'package:flutter/material.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; + +class CustomCard extends StatelessWidget { + final Widget child; + final EdgeInsets margin; + final EdgeInsets padding; + final Color? color; + final BorderSide? borderSide; + + const CustomCard({ + super.key, + required this.child, + this.margin = EdgeInsets.zero, + this.padding = EdgeInsets.zero, + this.color, + this.borderSide, + }); + + @override + Widget build(BuildContext context) { + return Card( + color: color ?? AppTheme.dark2, + margin: margin, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(12), + side: borderSide ?? BorderSide(color: AppTheme.dark2), + ), + child: Padding( + padding: padding, + child: child, + ), + ); + } +} diff --git a/lib/shared/widgets/custom_elevated_button.dart b/lib/shared/widgets/custom_elevated_button.dart new file mode 100644 index 00000000..1dbd0c9f --- /dev/null +++ b/lib/shared/widgets/custom_elevated_button.dart @@ -0,0 +1,33 @@ +import 'package:flutter/material.dart'; +import 'package:mostro_mobile/app/app_theme.dart'; + +class CustomElevatedButton extends StatelessWidget { + final VoidCallback onPressed; + final String text; + final EdgeInsetsGeometry? padding; + final Color? backgroundColor; + final Color? foregroundColor; + + const CustomElevatedButton({ + super.key, + required this.onPressed, + required this.text, + this.padding, + this.backgroundColor, + this.foregroundColor, + }); + + @override + Widget build(BuildContext context) { + return ElevatedButton( + onPressed: onPressed, + style: ElevatedButton.styleFrom( + foregroundColor: foregroundColor ?? Colors.white, + backgroundColor: backgroundColor ?? AppTheme.mostroGreen, + textStyle: Theme.of(context).textTheme.labelLarge, + padding: padding ?? const EdgeInsets.symmetric(vertical: 15, horizontal: 30), + ), + child: Text(text), + ); + } +} diff --git a/lib/shared/widgets/navigation_listener_widget.dart b/lib/shared/widgets/navigation_listener_widget.dart new file mode 100644 index 00000000..dd58dae1 --- /dev/null +++ b/lib/shared/widgets/navigation_listener_widget.dart @@ -0,0 +1,21 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/shared/notifiers/navigation_notifier.dart'; +import 'package:mostro_mobile/shared/providers/navigation_notifier_provider.dart'; + +class NavigationListenerWidget extends ConsumerWidget { + final Widget child; + + const NavigationListenerWidget({super.key, required this.child}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + ref.listen(navigationProvider, (previous, next) { + if (next.path.isNotEmpty) { + context.go(next.path); + } + }); + return child; + } +} diff --git a/lib/shared/widgets/notification_listener_widget.dart b/lib/shared/widgets/notification_listener_widget.dart new file mode 100644 index 00000000..f1132236 --- /dev/null +++ b/lib/shared/widgets/notification_listener_widget.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:go_router/go_router.dart'; +import 'package:mostro_mobile/shared/notifiers/notification_notifier.dart'; +import 'package:mostro_mobile/shared/providers/notification_notifier_provider.dart'; + +class NotificationListenerWidget extends ConsumerWidget { + final Widget child; + + const NotificationListenerWidget( + {super.key, required this.child}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + ref.listen(notificationProvider, (previous, next) { + if (next.informational) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text(next.message!)), + ); + // Clear notification after showing to prevent repetition + ref.read(notificationProvider.notifier).clearNotification(); + } else if (next.actionRequired){ + showDialog( + context: context, + builder: (context) => AlertDialog( + title: Text('Action Required'), + content: Text('Please add your lightning invoice to proceed.'), + actions: [ + TextButton( + onPressed: () => context.go('/'), + child: Text('Cancel'), + ), + TextButton( + onPressed: () { + // Perform the required action + Navigator.of(context).pop(); + }, + child: Text('Add Invoice'), + ), + ], + ), + ); + } + }); + return child; + } +} diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d2..d0e7f797 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#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); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87a..b29e9ba0 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 307f9946..86135f19 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,11 +5,13 @@ import FlutterMacOS import Foundation +import flutter_secure_storage_macos import local_auth_darwin import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FlutterSecureStoragePlugin.register(with: registry.registrar(forPlugin: "FlutterSecureStoragePlugin")) FLALocalAuthPlugin.register(with: registry.registrar(forPlugin: "FLALocalAuthPlugin")) PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) diff --git a/pubspec.lock b/pubspec.lock index b48ca7ed..ad9d9193 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -22,6 +22,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.7.0" + archive: + dependency: transitive + description: + name: archive + sha256: "08064924cbf0ab88280a0c3f60db9dd24fec693927e725ecb176f16c629d1cb8" + url: "https://pub.dev" + source: hosted + version: "4.0.1" args: dependency: transitive description: @@ -55,7 +63,7 @@ packages: source: hosted version: "0.2.2" bip32_bip44: - dependency: transitive + dependency: "direct main" description: name: bip32_bip44 sha256: "8e28e6bde00da1ed207f2c0a5361792375799196176742c0d36c71e89a5485d3" @@ -71,7 +79,7 @@ packages: source: hosted version: "0.1.0" bip39: - dependency: transitive + dependency: "direct main" description: name: bip39 sha256: de1ee27ebe7d96b84bb3a04a4132a0a3007dcdd5ad27dd14aa87a29d97c45edc @@ -134,6 +142,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.0" + checked_yaml: + dependency: transitive + description: + name: checked_yaml + sha256: feb6bed21949061731a7a75fc5d2aa727cf160b91af9a3e464c5e3a32e28b5ff + url: "https://pub.dev" + source: hosted + version: "2.0.3" + cli_util: + dependency: transitive + description: + name: cli_util + sha256: ff6785f7e9e3c38ac98b2fb035701789de90154024a75b6cb926445e83197d1c + url: "https://pub.dev" + source: hosted + version: "0.4.2" clock: dependency: transitive description: @@ -170,10 +194,10 @@ packages: dependency: transitive description: name: coverage - sha256: "4b03e11f6d5b8f6e5bb5e9f7889a56fe6c5cbe942da5378ea4d4d7f73ef9dfe5" + sha256: e3493833ea012784c740e341952298f1cc77f1f01b1bbc3eb4eecf6984fb7f43 url: "https://pub.dev" source: hosted - version: "1.11.0" + version: "1.11.1" crypto: dependency: "direct main" description: @@ -226,10 +250,10 @@ packages: dependency: "direct main" description: name: equatable - sha256: c2b87cb7756efdf69892005af546c56c0b5037f54d2a88269b4f347a505e3ca2 + sha256: "567c64b3cb4cf82397aac55f4f0cbd3ca20d77c6c03bedbc4ceaddc08904aef7" url: "https://pub.dev" source: hosted - version: "2.0.5" + version: "2.0.7" fake_async: dependency: transitive description: @@ -280,6 +304,14 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_hooks: + dependency: "direct main" + description: + name: flutter_hooks + sha256: cde36b12f7188c85286fba9b38cc5a902e7279f36dd676967106c041dc9dde70 + url: "https://pub.dev" + source: hosted + version: "0.20.5" flutter_intl: dependency: "direct dev" description: @@ -288,6 +320,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.0.1" + flutter_launcher_icons: + dependency: "direct main" + description: + name: flutter_launcher_icons + sha256: "31cd0885738e87c72d6f055564d37fabcdacee743b396b78c7636c169cac64f5" + url: "https://pub.dev" + source: hosted + version: "0.14.2" flutter_lints: dependency: "direct dev" description: @@ -317,14 +357,64 @@ packages: url: "https://pub.dev" source: hosted version: "2.6.1" + flutter_secure_storage: + dependency: "direct main" + description: + path: flutter_secure_storage + ref: develop + resolved-ref: f6b2a15047cc3aff7e78762428e509654a59bc45 + url: "https://github.com/chebizarro/flutter_secure_storage.git" + source: git + version: "9.2.3" + flutter_secure_storage_linux: + dependency: transitive + description: + name: flutter_secure_storage_linux + sha256: "4d91bfc23047422cbcd73ac684bc169859ee766482517c22172c86596bf1464b" + url: "https://pub.dev" + source: hosted + version: "1.2.1" + flutter_secure_storage_macos: + dependency: transitive + description: + name: flutter_secure_storage_macos + sha256: "1693ab11121a5f925bbea0be725abfcfbbcf36c1e29e571f84a0c0f436147a81" + url: "https://pub.dev" + source: hosted + version: "3.1.2" + flutter_secure_storage_platform_interface: + dependency: transitive + description: + name: flutter_secure_storage_platform_interface + sha256: cf91ad32ce5adef6fba4d736a542baca9daf3beac4db2d04be350b87f69ac4a8 + url: "https://pub.dev" + source: hosted + version: "1.1.2" + flutter_secure_storage_web: + dependency: transitive + description: + path: flutter_secure_storage_web + ref: develop + resolved-ref: f6b2a15047cc3aff7e78762428e509654a59bc45 + url: "https://github.com/chebizarro/flutter_secure_storage.git" + source: git + version: "2.0.0-beta.2" + flutter_secure_storage_windows: + dependency: transitive + description: + name: flutter_secure_storage_windows + sha256: b20b07cb5ed4ed74fc567b78a72936203f587eba460af1df11281c9326cd3709 + url: "https://pub.dev" + source: hosted + version: "3.1.2" flutter_svg: dependency: transitive description: name: flutter_svg - sha256: "578bd8c508144fdaffd4f77b8ef2d8c523602275cd697cc3db284dbd762ef4ce" + sha256: "54900a1a1243f3c4a5506d853a2b5c2dbc38d5f27e52a52618a8054401431123" url: "https://pub.dev" source: hosted - version: "2.0.14" + version: "2.0.16" flutter_test: dependency: "direct dev" description: flutter @@ -356,6 +446,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.2" + go_router: + dependency: "direct main" + description: + name: go_router + sha256: "2fd11229f59e23e967b0775df8d5948a519cd7e1e8b6e849729e010587b46539" + url: "https://pub.dev" + source: hosted + version: "14.6.2" google_fonts: dependency: "direct main" description: @@ -380,6 +478,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.2.0" + hooks_riverpod: + dependency: "direct main" + description: + name: hooks_riverpod + sha256: "70bba33cfc5670c84b796e6929c54b8bc5be7d0fe15bb28c2560500b9ad06966" + url: "https://pub.dev" + source: hosted + version: "2.6.1" http: dependency: "direct main" description: @@ -404,6 +510,14 @@ packages: url: "https://pub.dev" source: hosted version: "4.0.2" + image: + dependency: transitive + description: + name: image + sha256: "20842a5ad1555be624c314b0c0cc0566e8ece412f61e859a42efeb6d4101a26c" + url: "https://pub.dev" + source: hosted + version: "4.5.0" integration_test: dependency: "direct dev" description: flutter @@ -421,10 +535,10 @@ packages: dependency: transitive description: name: io - sha256: "2ec25704aba361659e10e3e5f5d672068d332fc8ac516421d483a11e5cbd061e" + sha256: dfd5a80599cf0165756e3181807ed3e77daf6dd4137caaad72d0b7931597650b url: "https://pub.dev" source: hosted - version: "1.0.4" + version: "1.0.5" js: dependency: transitive description: @@ -433,6 +547,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.1" + json_annotation: + dependency: transitive + description: + name: json_annotation + sha256: "1ce844379ca14835a50d2f019a3099f419082cfdd231cd86a142af94dd5c6bb1" + url: "https://pub.dev" + source: hosted + version: "4.9.0" leak_tracker: dependency: transitive description: @@ -622,18 +744,18 @@ packages: dependency: transitive description: name: path_provider_android - sha256: c464428172cb986b758c6d1724c603097febb8fb855aa265aeecc9280c294d4a + sha256: "4adf4fd5423ec60a29506c76581bc05854c55e3a0b72d35bb28d661c9686edf2" url: "https://pub.dev" source: hosted - version: "2.2.12" + version: "2.2.15" path_provider_foundation: dependency: transitive description: name: path_provider_foundation - sha256: f234384a3fdd67f989b4d54a5d73ca2a6c422fa55ae694381ae0f4375cd1ea16 + sha256: "4843174df4d288f5e29185bd6e72a6fbdf5a4a4602717eed565497429f179942" url: "https://pub.dev" source: hosted - version: "2.4.0" + version: "2.4.1" path_provider_linux: dependency: transitive description: @@ -698,6 +820,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.5.1" + posix: + dependency: transitive + description: + name: posix + sha256: a0117dc2167805aa9125b82eee515cc891819bac2f538c83646d355b16f58b9a + url: "https://pub.dev" + source: hosted + version: "6.0.1" process: dependency: transitive description: @@ -758,10 +888,10 @@ packages: dependency: transitive description: name: shared_preferences_android - sha256: "3b9febd815c9ca29c9e3520d50ec32f49157711e143b7a4ca039eb87e8ade5ab" + sha256: "7f172d1b06de5da47b6264c2692ee2ead20bbbc246690427cdb4fc301cd0c549" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4" shared_preferences_foundation: dependency: transitive description: @@ -830,10 +960,10 @@ packages: dependency: transitive description: name: shelf_web_socket - sha256: "073c147238594ecd0d193f3456a5fe91c4b0abbcc68bf5cd95b36c4e194ac611" + sha256: cc36c297b52866d203dbf9332263c94becc2fe0ceaa9681d07b6ef9807023b67 url: "https://pub.dev" source: hosted - version: "2.0.0" + version: "2.0.1" sky_engine: dependency: transitive description: flutter @@ -871,6 +1001,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.10.0" + sprintf: + dependency: transitive + description: + name: sprintf + sha256: "1fc9ffe69d4df602376b52949af107d8f5703b77cda567c4d7d86a0693120f23" + url: "https://pub.dev" + source: hosted + version: "7.0.0" stack_trace: dependency: transitive description: @@ -959,14 +1097,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + uuid: + dependency: "direct main" + description: + name: uuid + sha256: a5be9ef6618a7ac1e964353ef476418026db906c4facdedaa299b7a2e71690ff + url: "https://pub.dev" + source: hosted + version: "4.5.1" vector_graphics: dependency: transitive description: name: vector_graphics - sha256: "773c9522d66d523e1c7b25dfb95cc91c26a1e17b107039cfe147285e92de7878" + sha256: "27d5fefe86fb9aace4a9f8375b56b3c292b64d8c04510df230f849850d912cb7" url: "https://pub.dev" source: hosted - version: "1.1.14" + version: "1.1.15" vector_graphics_codec: dependency: transitive description: @@ -979,10 +1125,10 @@ packages: dependency: transitive description: name: vector_graphics_compiler - sha256: ab9ff38fc771e9ee1139320adbe3d18a60327370c218c60752068ebee4b49ab1 + sha256: "1b4b9e706a10294258727674a340ae0d6e64a7231980f9f9a3d12e4b42407aad" url: "https://pub.dev" source: hosted - version: "1.1.15" + version: "1.1.16" vector_math: dependency: transitive description: @@ -1039,6 +1185,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.2.1" + win32: + dependency: transitive + description: + name: win32 + sha256: "8b338d4486ab3fbc0ba0db9f9b4f5239b6697fcee427939a40e720cbb9ee0a69" + url: "https://pub.dev" + source: hosted + version: "5.9.0" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index ec6dd4db..cdcc1693 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -40,7 +40,7 @@ dependencies: heroicons: ^0.10.0 crypto: ^3.0.5 convert: ^3.1.1 - shared_preferences: ^2.3.2 + shared_preferences: ^2.3.3 equatable: ^2.0.5 logging: ^1.2.0 local_auth: ^2.3.0 @@ -50,15 +50,29 @@ dependencies: bitcoin_icons: ^0.0.4 collection: ^1.18.0 elliptic: ^0.3.11 + intl: ^0.19.0 + uuid: ^4.5.1 + + flutter_localizations: + sdk: flutter nip44: git: url: https://github.com/chebizarro/dart-nip44.git ref: master - intl: ^0.19.0 - flutter_localizations: - sdk: flutter + # temporary fork until main package is updated + flutter_secure_storage: + git: + url: https://github.com/chebizarro/flutter_secure_storage.git + path: flutter_secure_storage + ref: develop + go_router: ^14.6.2 + bip32_bip44: ^1.0.0 + bip39: ^1.0.6 + flutter_hooks: ^0.20.5 + hooks_riverpod: ^2.6.1 + flutter_launcher_icons: ^0.14.2 dev_dependencies: flutter_test: @@ -114,3 +128,9 @@ flutter: # see https://flutter.dev/to/font-from-package flutter_intl: enabled: true + +flutter_icons: + android: true + ios: true + image_path: "assets/images/launcher-icon.png" + remove_alpha_ios: true diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 7407dddd..011734da 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + FlutterSecureStorageWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); LocalAuthPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("LocalAuthPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index ef187dca..11485fce 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + flutter_secure_storage_windows local_auth_windows )