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