From 4ce603fbf1c5cf5574fe7f3b8b5f48c990485403 Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 15 Jul 2024 15:14:30 -0400 Subject: [PATCH] v2 --- lib/main.dart | 73 +++++--- lib/providers/offers_provider.dart | 4 +- lib/providers/payment_accounts_provider.dart | 29 ++- lib/providers/prices_provider.dart | 8 +- lib/screens/accounts_screen.dart | 99 ---------- .../active_buyer_trade_timeline_screen.dart | 2 +- .../active_seller_trade_timeline_screen.dart | 2 +- .../drawer/payment_accounts_screen.dart | 176 ++++++++++++++++++ lib/screens/drawer/settings_screen.dart | 156 ++++++++++++++++ lib/screens/{ => drawer}/wallet_screen.dart | 93 +++++---- lib/screens/offer_detail_screen.dart | 4 +- lib/screens/settings_screen.dart | 19 -- lib/tabs/buy/buy_market_offers_tab.dart | 45 ++++- lib/tabs/buy_tab.dart | 109 ++++++++++- lib/tabs/sell/sale_market_offers_tab.dart | 45 ++++- lib/tabs/sell_tab.dart | 1 + lib/tabs/trades/trades_active_tab.dart | 2 +- .../{check_fiat.dart => payment_utils.dart} | 81 ++++++-- ...orm.dart => add_payment_account_form.dart} | 0 ...form.dart => add_payment_method_form.dart} | 20 +- lib/widgets/new_trade_offer_form.dart | 80 ++++++-- lib/widgets/offer_card_widget.dart | 25 +-- pubspec.lock | 15 +- pubspec.yaml | 5 +- 24 files changed, 809 insertions(+), 284 deletions(-) delete mode 100644 lib/screens/accounts_screen.dart create mode 100644 lib/screens/drawer/payment_accounts_screen.dart create mode 100644 lib/screens/drawer/settings_screen.dart rename lib/screens/{ => drawer}/wallet_screen.dart (71%) delete mode 100644 lib/screens/settings_screen.dart rename lib/utils/{check_fiat.dart => payment_utils.dart} (74%) rename lib/widgets/{payment_account_form.dart => add_payment_account_form.dart} (100%) rename lib/widgets/{payment_method_form.dart => add_payment_method_form.dart} (78%) diff --git a/lib/main.dart b/lib/main.dart index af85fb2..fcb040c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -6,22 +6,23 @@ import 'package:haveno_flutter_app/providers/payment_accounts_provider.dart'; import 'package:haveno_flutter_app/providers/prices_provider.dart'; import 'package:haveno_flutter_app/providers/trades_provider.dart'; import 'package:haveno_flutter_app/providers/wallets_provider.dart'; -import 'package:haveno_flutter_app/screens/accounts_screen.dart'; -import 'package:haveno_flutter_app/screens/settings_screen.dart'; +import 'package:haveno_flutter_app/screens/drawer/payment_accounts_screen.dart'; +import 'package:haveno_flutter_app/screens/drawer/settings_screen.dart'; import 'package:haveno_flutter_app/tabs/trades_tab.dart'; import 'package:haveno_flutter_app/tabs/buy_tab.dart'; import 'package:haveno_flutter_app/tabs/sell_tab.dart'; -import 'package:haveno_flutter_app/screens/wallet_screen.dart'; +import 'package:haveno_flutter_app/screens/drawer/wallet_screen.dart'; import 'package:logging/logging.dart'; import 'package:path_provider/path_provider.dart'; import 'package:provider/provider.dart'; import 'package:haveno_flutter_app/services/haveno_service.dart'; import 'package:haveno_flutter_app/services/http_service.dart'; import 'package:haveno_flutter_app/services/monero_service.dart'; -import 'package:haveno_flutter_app/services/tor_service.dart'; +//import 'package:haveno_flutter_app/services/tor_service.dart'; import 'package:haveno_flutter_app/providers/get_version_provider.dart'; import 'package:haveno_flutter_app/providers/account_provider.dart'; import 'dart:async'; +import 'package:badges/badges.dart' as badges; // Import the badges package void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -30,18 +31,18 @@ void main() async { _setupLogging(); // Initialize services - final torService = TorService(); - await torService.initializeTor(); + //final torService = TorService(); + //await torService.initializeTor(); final httpService = HttpService(); final moneroService = MoneroService(); - final havenoService = HavenoService('192.168.0.18', 3201, 'apitest'); + final havenoService = HavenoService('127.0.0.1', 3201, 'apitest'); runApp( MultiProvider( providers: [ - Provider(create: (_) => torService), + //Provider(create: (_) => torService), Provider(create: (_) => httpService), Provider(create: (_) => moneroService), Provider(create: (_) => havenoService), @@ -95,6 +96,7 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( + debugShowCheckedModeBanner: false, theme: ThemeData( useMaterial3: true, colorScheme: ColorScheme.fromSeed( @@ -104,13 +106,13 @@ class MyApp extends StatelessWidget { // Ensures dark mode with light text ), scaffoldBackgroundColor: Color(0xFF303030), - appBarTheme: AppBarTheme( + appBarTheme: const AppBarTheme( backgroundColor: Color(0xFF303030), ), - drawerTheme: DrawerThemeData( + drawerTheme: const DrawerThemeData( backgroundColor: Color(0xFF303030), ), - bottomNavigationBarTheme: BottomNavigationBarThemeData( + bottomNavigationBarTheme: const BottomNavigationBarThemeData( backgroundColor: Color(0xFF303030), selectedItemColor: Color(0xFFF4511E), unselectedItemColor: Colors.white, @@ -124,7 +126,7 @@ class MyApp extends StatelessWidget { ), ), ), - cardTheme: CardTheme( + cardTheme: const CardTheme( color: Color(0xFF424242), // Card background color ), ), @@ -142,6 +144,7 @@ class _HomeScreenState extends State { String _statusMessage = "Connecting to Tor..."; Timer? _timer; int _selectedIndex = 0; + int _notificationCount = 5; // Mock notification count static final List _widgetOptions = [ BuyTab(), @@ -152,17 +155,17 @@ class _HomeScreenState extends State { @override void initState() { super.initState(); - final torService = context.read(); - torService.statusStream.listen((String status) { - print(status); - setState(() { - if (status.contains("started")) { - _statusMessage = "Connecting to the Monero network..."; - } else { - _statusMessage = status; - } - }); - }); +// final torService = context.read(); +// torService.statusStream.listen((String status) { +// print(status); +// setState(() { +// if (status.contains("started")) { +// _statusMessage = "Connecting to the Monero network..."; +// } else { +// _statusMessage = status; +// } +// }); +// }); _initializeServices(); } @@ -201,7 +204,7 @@ class _HomeScreenState extends State { @override void dispose() { - context.read().dispose(); + //context.read().dispose(); context.read().close(); context.read().close(); _timer?.cancel(); @@ -213,7 +216,25 @@ class _HomeScreenState extends State { return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( - title: Text('Haveno'), + title: const Text('Haveno'), + actions: [ + Padding( + padding: const EdgeInsets.only(right: 15.0), // Adjust padding to move the bell and badge + child: badges.Badge( + position: badges.BadgePosition.topEnd(top: 5, end: 5), + badgeContent: Text( + '$_notificationCount', + style: TextStyle(color: Colors.white, fontSize: 10), + ), + child: IconButton( + icon: Icon(Icons.notifications), + onPressed: () { + // Handle notification bell tap + }, + ), + ), + ), + ], ), drawer: _buildDrawer(context), body: Center( @@ -275,7 +296,7 @@ class _HomeScreenState extends State { Navigator.pop(context); Navigator.push( context, - MaterialPageRoute(builder: (context) => AccountsScreen()), + MaterialPageRoute(builder: (context) => PaymentAccountsScreen()), ); }, ), diff --git a/lib/providers/offers_provider.dart b/lib/providers/offers_provider.dart index 1a1f41e..7771604 100644 --- a/lib/providers/offers_provider.dart +++ b/lib/providers/offers_provider.dart @@ -54,11 +54,11 @@ class OffersProvider with ChangeNotifier { required String direction, required String price, required bool useMarketBasedPrice, - required double marketPriceMarginPct, + double? marketPriceMarginPct, required fixnum.Int64 amount, required fixnum.Int64 minAmount, required double buyerSecurityDepositPct, - required String triggerPrice, + String? triggerPrice, required bool reserveExactAmount, required String paymentAccountId, }) async { diff --git a/lib/providers/payment_accounts_provider.dart b/lib/providers/payment_accounts_provider.dart index 31aeb07..14a6313 100644 --- a/lib/providers/payment_accounts_provider.dart +++ b/lib/providers/payment_accounts_provider.dart @@ -14,6 +14,7 @@ class PaymentAccountsProvider with ChangeNotifier { final Logger _logger = Logger('PaymentAccountsProvider'); final HavenoService _havenoService; List? _paymentMethods; + List? _cryptoCurrencyPaymentMethods; List? _paymentAccounts; PaymentAccountForm _paymentAccountForm = PaymentAccountForm(); bool _isLoadingPaymentMethods = false; @@ -62,6 +63,7 @@ class PaymentAccountsProvider with ChangeNotifier { } List? get paymentMethods => _paymentMethods; + List? get cryptoCurrencyPaymentMethods => _cryptoCurrencyPaymentMethods; List? get paymentAccounts => _paymentAccounts; PaymentAccountForm get paymentAccountForm => _paymentAccountForm; bool get isLoadingPaymentMethods => _isLoadingPaymentMethods; @@ -76,6 +78,9 @@ class PaymentAccountsProvider with ChangeNotifier { final getPaymentMethodsReply = await _havenoService.paymentAccountsClient .getPaymentMethods(GetPaymentMethodsRequest()); _paymentMethods = getPaymentMethodsReply.paymentMethods; +// _paymentMethods?.forEach((method) { +// debugPrint("Payment Method: ${jsonEncode(method.toProto3Json())}"); +// }); } catch (e) { print("Failed to get payment methods: $e"); } finally { @@ -92,9 +97,9 @@ class PaymentAccountsProvider with ChangeNotifier { final getPaymentAccountsReply = await _havenoService.paymentAccountsClient .getPaymentAccounts(GetPaymentAccountsRequest()); _paymentAccounts = getPaymentAccountsReply.paymentAccounts; - _paymentAccounts?.forEach((account) { - debugPrint(jsonEncode(account.toProto3Json())); - }); +// _paymentAccounts?.forEach((account) { +// debugPrint(jsonEncode(account.toProto3Json())); +// }); } catch (e) { print("Failed to get payment accounts: $e"); } finally { @@ -104,6 +109,24 @@ class PaymentAccountsProvider with ChangeNotifier { return paymentAccounts; } + Future?> getCryptoCurrencyPaymentMethods() async { + _isLoadingPaymentAccounts = true; + notifyListeners(); + try { + final getCryptoCurrencyPaymentMethodsReply = await _havenoService.paymentAccountsClient.getCryptoCurrencyPaymentMethods(GetCryptoCurrencyPaymentMethodsRequest()); + _cryptoCurrencyPaymentMethods = getCryptoCurrencyPaymentMethodsReply.paymentMethods; + // _cryptoCurrencyPaymentMethods?.forEach((method) { + // debugPrint("Crypto Method: ${jsonEncode(method.toProto3Json())}"); + // }); + } catch (e) { + print("Failed to get payment accounts: $e"); + } finally { + _isLoadingPaymentAccounts = false; + notifyListeners(); + } + return paymentMethods; + } + Future getPaymentAcountForm( String paymentMethodId) async { final traditionalCurrencyPaymentAccountForm = diff --git a/lib/providers/prices_provider.dart b/lib/providers/prices_provider.dart index 09ecfe6..8774bc8 100644 --- a/lib/providers/prices_provider.dart +++ b/lib/providers/prices_provider.dart @@ -4,18 +4,18 @@ import 'package:haveno_flutter_app/services/haveno_service.dart'; class PricesProvider with ChangeNotifier { final HavenoService _havenoService; - List? _marketPrices; + List _marketPrices = []; PricesProvider(this._havenoService); - List? get prices => _marketPrices; + List get prices => _marketPrices; - Future getPrices() async { + Future getXmrMarketPrices() async { try { final getMarketPricesReply = await _havenoService.priceClient .getMarketPrices(MarketPricesRequest()); _marketPrices = getMarketPricesReply.marketPrice; - _marketPrices?.forEach((price) { + _marketPrices.forEach((price) { print("Price: $price"); }); notifyListeners(); diff --git a/lib/screens/accounts_screen.dart b/lib/screens/accounts_screen.dart deleted file mode 100644 index 6b30d75..0000000 --- a/lib/screens/accounts_screen.dart +++ /dev/null @@ -1,99 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:haveno_flutter_app/widgets/payment_method_form.dart'; -import 'package:provider/provider.dart'; -import 'package:haveno_flutter_app/providers/payment_accounts_provider.dart'; - -class AccountsScreen extends StatefulWidget { - @override - _AccountsScreenState createState() => _AccountsScreenState(); -} - -class _AccountsScreenState extends State { - @override - void initState() { - super.initState(); - final paymentAccountsProvider = - Provider.of(context, listen: false); - paymentAccountsProvider.getPaymentAccounts(); - } - - void _showCreateAccountForm(BuildContext context) { - final paymentAccountsProvider = - Provider.of(context, listen: false); - - paymentAccountsProvider.getPaymentMethods().then((_) { - showModalBottomSheet( - context: context, - isScrollControlled: true, - builder: (BuildContext context) { - return PaymentMethodSelectionForm(); - }, - ); - }); - } - - @override - Widget build(BuildContext context) { - final paymentAccountsProvider = - Provider.of(context); - - return Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - appBar: AppBar( - title: const Text('Accounts'), - ), - body: Consumer( - builder: (context, provider, child) { - if (provider.isLoadingPaymentAccounts) { - return const Center(child: CircularProgressIndicator()); - } else if (provider.paymentAccounts?.isEmpty ?? true) { - return const Center( - child: Text( - 'You do not currently have any accounts', - style: TextStyle(color: Colors.white70, fontSize: 18), - ), - ); - } else { - return Padding( - padding: const EdgeInsets.all(8.0), - child: ListView.builder( - itemCount: provider.paymentAccounts?.length ?? 0, - itemBuilder: (context, index) { - final account = provider.paymentAccounts![index]; - return Card( - color: Theme.of(context).cardTheme.color, - child: Padding( - padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - '${account.accountName}', - style: TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold), - ), - const SizedBox(height: 8.0), - Text( - account.paymentMethod.id, - style: const TextStyle(color: Colors.white70), - ), - ], - ), - ), - ); - }, - ), - ); - } - }, - ), - floatingActionButton: FloatingActionButton( - onPressed: () => _showCreateAccountForm(context), - backgroundColor: Theme.of(context).colorScheme.primary, - child: Icon(Icons.add), - ), - ); - } -} diff --git a/lib/screens/active_buyer_trade_timeline_screen.dart b/lib/screens/active_buyer_trade_timeline_screen.dart index a6e7181..815096d 100644 --- a/lib/screens/active_buyer_trade_timeline_screen.dart +++ b/lib/screens/active_buyer_trade_timeline_screen.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; -import 'package:haveno_flutter_app/utils/check_fiat.dart'; +import 'package:haveno_flutter_app/utils/payment_utils.dart'; import 'package:provider/provider.dart'; import 'package:haveno_flutter_app/providers/trades_provider.dart'; import 'package:haveno_flutter_app/proto/compiled/grpc.pbgrpc.dart'; diff --git a/lib/screens/active_seller_trade_timeline_screen.dart b/lib/screens/active_seller_trade_timeline_screen.dart index d7bf53a..1f02c54 100644 --- a/lib/screens/active_seller_trade_timeline_screen.dart +++ b/lib/screens/active_seller_trade_timeline_screen.dart @@ -1,7 +1,7 @@ import 'dart:convert'; import 'package:fixnum/fixnum.dart'; import 'package:flutter/material.dart'; -import 'package:haveno_flutter_app/utils/check_fiat.dart'; +import 'package:haveno_flutter_app/utils/payment_utils.dart'; import 'package:provider/provider.dart'; import 'package:haveno_flutter_app/providers/trades_provider.dart'; import 'package:haveno_flutter_app/proto/compiled/grpc.pbgrpc.dart'; diff --git a/lib/screens/drawer/payment_accounts_screen.dart b/lib/screens/drawer/payment_accounts_screen.dart new file mode 100644 index 0000000..cf1f96d --- /dev/null +++ b/lib/screens/drawer/payment_accounts_screen.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:haveno_flutter_app/utils/payment_utils.dart'; +import 'package:haveno_flutter_app/widgets/add_payment_method_form.dart'; +import 'package:provider/provider.dart'; +import 'package:haveno_flutter_app/providers/payment_accounts_provider.dart'; +import 'package:badges/badges.dart' as badges; // Import the badges package + +class PaymentAccountsScreen extends StatefulWidget { + @override + _PaymentAccountsScreenState createState() => _PaymentAccountsScreenState(); +} + +class _PaymentAccountsScreenState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + + @override + void initState() { + super.initState(); + _tabController = TabController(length: 2, vsync: this); + final paymentAccountsProvider = + Provider.of(context, listen: false); + paymentAccountsProvider.getPaymentAccounts(); + paymentAccountsProvider.getPaymentMethods(); + paymentAccountsProvider.getCryptoCurrencyPaymentMethods(); + } + + void _showCreateAccountForm(BuildContext context) { + final paymentAccountsProvider = + Provider.of(context, listen: false); + final isFiat = _tabController.index == 0; + + final getPaymentMethods = isFiat + ? paymentAccountsProvider.getPaymentMethods() + : paymentAccountsProvider.getCryptoCurrencyPaymentMethods(); + + getPaymentMethods.then((_) { + showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext context) { + return PaymentMethodSelectionForm(accountType: isFiat ? 'FIAT' : 'CRYPTO'); + }, + ); + }); + } + + @override + void dispose() { + _tabController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + final paymentAccountsProvider = + Provider.of(context); + + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + title: const Text('Accounts'), + bottom: TabBar( + controller: _tabController, + tabs: const [ + Tab(text: 'Fiat Accounts'), + Tab(text: 'Crypto Accounts'), + ], + ), + ), + body: TabBarView( + controller: _tabController, + children: [ + _buildAccountList(context, paymentAccountsProvider, PaymentMethodType.FIAT), + _buildAccountList(context, paymentAccountsProvider, PaymentMethodType.CRYPTO), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: () => _showCreateAccountForm(context), + backgroundColor: Theme.of(context).colorScheme.primary, + child: Icon(Icons.add), + ), + ); + } + + Widget _buildAccountList(BuildContext context, + PaymentAccountsProvider provider, PaymentMethodType accountType) { + if (provider.isLoadingPaymentAccounts) { + return const Center(child: CircularProgressIndicator()); + } else { + final accounts = provider.paymentAccounts?.where((account) { + final methodType = getPaymentMethodType(account.paymentMethod.id); + return methodType == accountType; + }).toList(); + + if (accounts == null || accounts.isEmpty) { + return const Center( + child: Text( + 'You do not currently have any accounts', + style: TextStyle(color: Colors.white70, fontSize: 18), + ), + ); + } else { + return Padding( + padding: const EdgeInsets.all(8.0), + child: ListView.builder( + itemCount: accounts.length, + itemBuilder: (context, index) { + final account = accounts[index]; + final accountSupportedCurrencies = account.tradeCurrencies; + + return Card( + color: Theme.of(context).cardTheme.color, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + '${account.accountName}', + style: TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold), + ), + badges.Badge( + badgeContent: Text( + getPaymentMethodLabel(account.paymentMethod.id), + style: const TextStyle( + color: Colors.white, fontSize: 10), + ), + badgeStyle: badges.BadgeStyle( + shape: badges.BadgeShape.square, + badgeColor: Theme.of(context).colorScheme.primary, + borderRadius: BorderRadius.circular(8), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + ), + ), + ], + ), + const SizedBox(height: 8.0), + Wrap( + spacing: 8.0, + runSpacing: 4.0, + children: accountSupportedCurrencies.map((currency) { + return badges.Badge( + badgeContent: Text( + currency.name, + style: const TextStyle( + color: Colors.white, fontSize: 10), + ), + badgeStyle: badges.BadgeStyle( + shape: badges.BadgeShape.square, + badgeColor: Colors.blue, + borderRadius: BorderRadius.circular(8), + padding: const EdgeInsets.symmetric( + horizontal: 8, vertical: 4), + ), + ); + }).toList(), + ), + ], + ), + ), + ); + }, + ), + ); + } + } + } +} diff --git a/lib/screens/drawer/settings_screen.dart b/lib/screens/drawer/settings_screen.dart new file mode 100644 index 0000000..c8994f9 --- /dev/null +++ b/lib/screens/drawer/settings_screen.dart @@ -0,0 +1,156 @@ +import 'package:flutter/material.dart'; + +class SettingsScreen extends StatelessWidget { + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Theme.of(context).scaffoldBackgroundColor, + appBar: AppBar( + title: Text('Settings'), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: ListView( + children: [ + Card( + color: Theme.of(context).cardTheme.color, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Preferences', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: 'Language', + border: OutlineInputBorder(), + labelStyle: TextStyle(color: Colors.white), + ), + items: ['English', 'Spanish', 'French'] + .map((language) => DropdownMenuItem( + value: language, + child: Text(language), + )) + .toList(), + onChanged: (value) {}, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: 'Country', + border: OutlineInputBorder(), + labelStyle: TextStyle(color: Colors.white), + ), + items: ['USA', 'Canada', 'UK'] + .map((country) => DropdownMenuItem( + value: country, + child: Text(country), + )) + .toList(), + onChanged: (value) {}, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: 'Preferred Currency', + border: OutlineInputBorder(), + labelStyle: TextStyle(color: Colors.white), + ), + items: ['USD', 'EUR', 'GBP'] + .map((currency) => DropdownMenuItem( + value: currency, + child: Text(currency), + )) + .toList(), + onChanged: (value) {}, + ), + const SizedBox(height: 16), + DropdownButtonFormField( + decoration: InputDecoration( + labelText: 'Blockchain Explorer', + border: OutlineInputBorder(), + labelStyle: TextStyle(color: Colors.white), + ), + items: ['XMRChain.net', 'Monero.com'] + .map((explorer) => DropdownMenuItem( + value: explorer, + child: Text(explorer), + )) + .toList(), + onChanged: (value) {}, + ), + const SizedBox(height: 16), + TextField( + decoration: InputDecoration( + labelText: 'Max Deviation from Market Price', + border: OutlineInputBorder(), + labelStyle: TextStyle(color: Colors.white), + suffixText: '%', + ), + keyboardType: TextInputType.number, + ), + ], + ), + ), + ), + const SizedBox(height: 16), + Card( + color: Theme.of(context).cardTheme.color, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + 'Display Options', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.white, + ), + ), + const SizedBox(height: 16), + SwitchListTile( + title: Text( + 'Hide Non-Supported Payment Methods', + style: TextStyle(color: Colors.white), + ), + value: true, + onChanged: (value) {}, + ), + const SizedBox(height: 16), + SwitchListTile( + title: Text( + 'Sort Market Lists by Number of Offers/Trades', + style: TextStyle(color: Colors.white), + ), + value: false, + onChanged: (value) {}, + ), + const SizedBox(height: 16), + SwitchListTile( + title: Text( + 'User Dark Mode', + style: TextStyle(color: Colors.white), + ), + value: true, + onChanged: (value) {}, + ), + ], + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/screens/wallet_screen.dart b/lib/screens/drawer/wallet_screen.dart similarity index 71% rename from lib/screens/wallet_screen.dart rename to lib/screens/drawer/wallet_screen.dart index 8e2946d..702727a 100644 --- a/lib/screens/wallet_screen.dart +++ b/lib/screens/drawer/wallet_screen.dart @@ -59,8 +59,10 @@ class _WalletsScreenState extends State { children: [ if (balances.hasXmr()) _buildXmrBalanceCard( - 'XMR', balances.xmr, walletsProvider.xmrPrimaryAddress), - const SizedBox(height: 16.0), + 'XMR', balances.xmr), + const SizedBox(height: 10.0), + _buildXmrAddressCard(walletsProvider.xmrPrimaryAddress), + const SizedBox(height: 10.0), _buildXmrTransactionsList(walletsProvider.xmrTxs), ], ); @@ -72,53 +74,76 @@ class _WalletsScreenState extends State { } Widget _buildXmrBalanceCard( - String coin, XmrBalanceInfo balance, String? primaryAddress) { + String coin, XmrBalanceInfo balance) { + return Card( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Balances', + style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 10.0), + Text( + 'Available Balance: ${_formatXmr(balance.availableBalance)} XMR'), + Text('Pending Balance: ${_formatXmr(balance.pendingBalance)} XMR'), + const SizedBox(height: 10.0), + Text( + 'Reserved Offer Balance: ${_formatXmr(balance.reservedOfferBalance)} XMR'), + Text( + 'Reserved Trade Balance: ${_formatXmr(balance.reservedTradeBalance)} XMR'), + const SizedBox(height: 16.0), + ElevatedButton( + onPressed: () { + // handle withdraw balance logic here + }, + child: Text('Withdraw Balance'), + ), + ], + ), + ), + ); + } + + Widget _buildXmrAddressCard(String? xmrAddress) { return Card( child: Padding( padding: const EdgeInsets.all(16.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Text( - 'Monero', - style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold), - ), - const SizedBox(height: 10.0), - Text( - 'Available Balance: ${_formatXmr(balance.availableBalance)} XMR'), - Text('Pending Balance: ${_formatXmr(balance.pendingBalance)} XMR'), - const SizedBox(height: 10.0), - Text( - 'Reserved Offer Balance: ${_formatXmr(balance.reservedOfferBalance)} XMR'), - Text( - 'Reserved Trade Balance: ${_formatXmr(balance.reservedTradeBalance)} XMR'), - const SizedBox(height: 16.0), - if (primaryAddress != null) - Column( + child: xmrAddress != null + ? Column( + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - primaryAddress, - textAlign: TextAlign.center, + const Text( + 'Addresses', + style: TextStyle(fontSize: 20.0, fontWeight: FontWeight.bold), ), - const SizedBox(height: 8.0), + const SizedBox(height: 10.0), + Text(xmrAddress), + ], + ) + : Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'You currently don\'t have an XMR address', + style: TextStyle(fontSize: 16.0), + ), + SizedBox(height: 10.0), ElevatedButton( onPressed: () { - Clipboard.setData(ClipboardData(text: primaryAddress)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Address copied to clipboard')), - ); + // request a new XMR address here }, - child: const Text('Copy Address'), + child: Text('Request a new address'), ), ], ), - ], - ), ), ); } + Widget _buildXmrTransactionsList(List? transactions) { if (transactions != null) { transactions.sort((a, b) => b.timestamp.compareTo(a.timestamp)); diff --git a/lib/screens/offer_detail_screen.dart b/lib/screens/offer_detail_screen.dart index aa74595..000b5dc 100644 --- a/lib/screens/offer_detail_screen.dart +++ b/lib/screens/offer_detail_screen.dart @@ -202,7 +202,7 @@ class _OfferDetailScreenState extends State { ), ), ), - SizedBox(height: 16.0), + SizedBox(height: 10.0), Card( child: Padding( padding: const EdgeInsets.all(16.0), @@ -241,7 +241,7 @@ class _OfferDetailScreenState extends State { ), ), ), - SizedBox(height: 16.0), + SizedBox(height: 10.0), Card( child: Padding( padding: const EdgeInsets.all(16.0), diff --git a/lib/screens/settings_screen.dart b/lib/screens/settings_screen.dart deleted file mode 100644 index 4b155c6..0000000 --- a/lib/screens/settings_screen.dart +++ /dev/null @@ -1,19 +0,0 @@ -import 'package:flutter/material.dart'; - -class SettingsScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - backgroundColor: Theme.of(context).scaffoldBackgroundColor, - appBar: AppBar( - title: Text('Settings'), - ), - body: Center( - child: Text( - 'Settings Screen', - style: TextStyle(color: Colors.white), - ), - ), - ); - } -} diff --git a/lib/tabs/buy/buy_market_offers_tab.dart b/lib/tabs/buy/buy_market_offers_tab.dart index b7fd2d3..134e8e7 100644 --- a/lib/tabs/buy/buy_market_offers_tab.dart +++ b/lib/tabs/buy/buy_market_offers_tab.dart @@ -1,15 +1,27 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; +import 'package:haveno_flutter_app/providers/prices_provider.dart'; import 'package:haveno_flutter_app/widgets/offer_card_widget.dart'; import 'package:provider/provider.dart'; import 'package:haveno_flutter_app/providers/offers_provider.dart'; +import 'package:haveno_flutter_app/proto/compiled/grpc.pbgrpc.dart'; // Ensure you have the correct import for OfferInfo class BuyMarketOffersTab extends StatelessWidget { @override Widget build(BuildContext context) { final offersProvider = Provider.of(context, listen: false); + final pricesProvider = Provider.of(context, listen: false); + + Future fetchData() async { + if (pricesProvider.prices.isEmpty) { + await pricesProvider.getXmrMarketPrices(); + } + await offersProvider.getOffers(); + } return FutureBuilder( - future: offersProvider.getOffers(), // Fetch offers + future: fetchData(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -17,16 +29,39 @@ class BuyMarketOffersTab extends StatelessWidget { return Center(child: Text('Error: ${snapshot.error}')); } else { final offers = offersProvider.marketBuyOffers; + final marketPrices = pricesProvider.prices; + if (offers == null || offers.isEmpty) { return const Center(child: Text('No offers available')); } else { + // Calculate value score for each offer + final List> sortedOffers = offers.map((offer) { + final marketPriceInfo = marketPrices.firstWhere( + (marketInfo) => marketInfo.currencyCode == offer.counterCurrencyCode, + orElse: () => MarketPriceInfo()..price = 0.0); // Provide a default value if not found + final marketPrice = marketPriceInfo.price; + final offerPrice = double.tryParse(offer.price) ?? 0.0; + + double valueScore = 0; + if (marketPrice > 0) { + valueScore = (marketPrice - offerPrice) / marketPrice * 100; + } + + return { + 'offer': offer, + 'valueScore': valueScore, + }; + }).toList(); + + // Sort offers by value score (lowest value first) + sortedOffers.sort((a, b) => (a['valueScore'] as double).compareTo(b['valueScore'] as double)); + return Padding( - padding: const EdgeInsets.only( - top: 2.0), // Add 2 pixels of padding at the top + padding: const EdgeInsets.only(top: 2.0), // Add 2 pixels of padding at the top child: ListView.builder( - itemCount: offers.length, + itemCount: sortedOffers.length, itemBuilder: (context, index) { - final offer = offers[index]; + final offer = sortedOffers[index]['offer'] as OfferInfo; return OfferCard(offer: offer); }, ), diff --git a/lib/tabs/buy_tab.dart b/lib/tabs/buy_tab.dart index 859e9e6..f553017 100644 --- a/lib/tabs/buy_tab.dart +++ b/lib/tabs/buy_tab.dart @@ -12,16 +12,27 @@ class BuyTab extends StatefulWidget { _BuyTabState createState() => _BuyTabState(); } -class _BuyTabState extends State with SingleTickerProviderStateMixin { +class _BuyTabState extends State with TickerProviderStateMixin { final _formKey = GlobalKey(); TabController? _tabController; bool _isLoadingPaymentMethods = true; List _paymentAccounts = []; + bool _isFilterVisible = false; + late AnimationController _filterAnimationController; + late Animation _filterAnimation; @override void initState() { super.initState(); _tabController = TabController(length: 2, vsync: this); + _filterAnimationController = AnimationController( + duration: const Duration(milliseconds: 300), + vsync: this, + ); + _filterAnimation = CurvedAnimation( + parent: _filterAnimationController, + curve: Curves.easeInOut, + ); _initializeData(); } @@ -46,6 +57,7 @@ class _BuyTabState extends State with SingleTickerProviderStateMixin { @override void dispose() { _tabController?.dispose(); + _filterAnimationController.dispose(); super.dispose(); } @@ -55,6 +67,7 @@ class _BuyTabState extends State with SingleTickerProviderStateMixin { isScrollControlled: true, builder: (BuildContext context) { return NewTradeOfferForm( + direction: 'BUY', paymentAccounts: _paymentAccounts, formKey: _formKey, ); @@ -62,12 +75,29 @@ class _BuyTabState extends State with SingleTickerProviderStateMixin { ); } + void _toggleFilter() { + setState(() { + _isFilterVisible = !_isFilterVisible; + if (_isFilterVisible) { + _filterAnimationController.forward(); + } else { + _filterAnimationController.reverse(); + } + }); + } + @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Theme.of(context).scaffoldBackgroundColor, appBar: AppBar( title: const Text('Buy Monero'), + actions: [ + IconButton( + icon: Icon(Icons.filter_list), + onPressed: _toggleFilter, + ), + ], bottom: TabBar( controller: _tabController, tabs: const [ @@ -76,15 +106,26 @@ class _BuyTabState extends State with SingleTickerProviderStateMixin { ], ), ), - body: _isLoadingPaymentMethods - ? const Center(child: CircularProgressIndicator()) - : TabBarView( - controller: _tabController, - children: [ - BuyMarketOffersTab(), - BuyMyOffersTab(), - ], - ), + body: Column( + children: [ + SizeTransition( + sizeFactor: _filterAnimation, + axisAlignment: -1.0, + child: _buildFilterMenu(), + ), + Expanded( + child: _isLoadingPaymentMethods + ? const Center(child: CircularProgressIndicator()) + : TabBarView( + controller: _tabController, + children: [ + BuyMarketOffersTab(), + BuyMyOffersTab(), + ], + ), + ), + ], + ), floatingActionButton: FloatingActionButton( onPressed: _showNewTradeForm, backgroundColor: Theme.of(context).colorScheme.primary, @@ -92,4 +133,52 @@ class _BuyTabState extends State with SingleTickerProviderStateMixin { ), ); } + + Widget _buildFilterMenu() { + return Container( + color: Theme.of(context).cardTheme.color, + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + 'Filters', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Currency', + border: OutlineInputBorder(), + ), + items: ['USD', 'EUR', 'GBP'] + .map((currency) => DropdownMenuItem( + value: currency, + child: Text(currency), + )) + .toList(), + onChanged: (value) { + // Handle currency filter change + }, + ), + const SizedBox(height: 8), + DropdownButtonFormField( + decoration: const InputDecoration( + labelText: 'Payment Method', + border: OutlineInputBorder(), + ), + items: ['PayPal', 'Bank Transfer', 'Credit Card'] + .map((method) => DropdownMenuItem( + value: method, + child: Text(method), + )) + .toList(), + onChanged: (value) { + // Handle payment method filter change + }, + ), + ], + ), + ); + } } diff --git a/lib/tabs/sell/sale_market_offers_tab.dart b/lib/tabs/sell/sale_market_offers_tab.dart index 6d97ff8..b81800c 100644 --- a/lib/tabs/sell/sale_market_offers_tab.dart +++ b/lib/tabs/sell/sale_market_offers_tab.dart @@ -1,15 +1,27 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; +import 'package:haveno_flutter_app/providers/prices_provider.dart'; import 'package:haveno_flutter_app/widgets/offer_card_widget.dart'; import 'package:provider/provider.dart'; import 'package:haveno_flutter_app/providers/offers_provider.dart'; +import 'package:haveno_flutter_app/proto/compiled/grpc.pbgrpc.dart'; // Ensure you have the correct import for OfferInfo class SaleMarketOffersTab extends StatelessWidget { @override Widget build(BuildContext context) { final offersProvider = Provider.of(context, listen: false); + final pricesProvider = Provider.of(context, listen: false); + + Future fetchData() async { + if (pricesProvider.prices.isEmpty) { + await pricesProvider.getXmrMarketPrices(); + } + await offersProvider.getOffers(); + } return FutureBuilder( - future: offersProvider.getOffers(), // Fetch offers + future: fetchData(), builder: (context, snapshot) { if (snapshot.connectionState == ConnectionState.waiting) { return const Center(child: CircularProgressIndicator()); @@ -17,16 +29,39 @@ class SaleMarketOffersTab extends StatelessWidget { return Center(child: Text('Error: ${snapshot.error}')); } else { final offers = offersProvider.marketSellOffers; + final marketPrices = pricesProvider.prices; + if (offers == null || offers.isEmpty) { return const Center(child: Text('No offers available')); } else { + // Calculate value score for each offer + final List> sortedOffers = offers.map((offer) { + final marketPriceInfo = marketPrices.firstWhere( + (marketInfo) => marketInfo.currencyCode == offer.counterCurrencyCode, + orElse: () => MarketPriceInfo()..price = 0.0); // Provide a default value if not found + final marketPrice = marketPriceInfo.price; + final offerPrice = double.tryParse(offer.price) ?? 0.0; + + double valueScore = 0; + if (marketPrice > 0) { + valueScore = (offerPrice - marketPrice) / marketPrice * 100; + } + + return { + 'offer': offer, + 'valueScore': valueScore, + }; + }).toList(); + + // Sort offers by value score (highest value first) + sortedOffers.sort((a, b) => (b['valueScore'] as double).compareTo(a['valueScore'] as double)); + return Padding( - padding: const EdgeInsets.only( - top: 2.0), // Add 2 pixels of padding at the top + padding: const EdgeInsets.only(top: 2.0), // Add 2 pixels of padding at the top child: ListView.builder( - itemCount: offers.length, + itemCount: sortedOffers.length, itemBuilder: (context, index) { - final offer = offers[index]; + final offer = sortedOffers[index]['offer'] as OfferInfo; return OfferCard(offer: offer); }, ), diff --git a/lib/tabs/sell_tab.dart b/lib/tabs/sell_tab.dart index 974912a..f7297f9 100644 --- a/lib/tabs/sell_tab.dart +++ b/lib/tabs/sell_tab.dart @@ -55,6 +55,7 @@ class _SellTabState extends State with SingleTickerProviderStateMixin { isScrollControlled: true, builder: (BuildContext context) { return NewTradeOfferForm( + direction: 'SELL', paymentAccounts: _paymentAccounts, formKey: _formKey, ); diff --git a/lib/tabs/trades/trades_active_tab.dart b/lib/tabs/trades/trades_active_tab.dart index 9fd6023..6d92c91 100644 --- a/lib/tabs/trades/trades_active_tab.dart +++ b/lib/tabs/trades/trades_active_tab.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:haveno_flutter_app/providers/account_provider.dart'; import 'package:haveno_flutter_app/screens/active_buyer_trade_timeline_screen.dart'; import 'package:haveno_flutter_app/screens/active_seller_trade_timeline_screen.dart'; -import 'package:haveno_flutter_app/utils/check_fiat.dart'; +import 'package:haveno_flutter_app/utils/payment_utils.dart'; import 'package:intl/intl.dart'; import 'package:provider/provider.dart'; import 'package:haveno_flutter_app/providers/trades_provider.dart'; diff --git a/lib/utils/check_fiat.dart b/lib/utils/payment_utils.dart similarity index 74% rename from lib/utils/check_fiat.dart rename to lib/utils/payment_utils.dart index 6fa0d3a..32a1a93 100644 --- a/lib/utils/check_fiat.dart +++ b/lib/utils/payment_utils.dart @@ -65,25 +65,6 @@ bool isFiatCurrency(String currencyCode) { 'IMP', // Isle of Man Pound 'INR', // Indian Rupee 'IQD', // Iraqi Dinar - 'IRR', // Iranian Rial - 'ISK', // Icelandic Króna - 'JEP', // Jersey Pound - 'JMD', // Jamaican Dollar - 'JOD', // Jordanian Dinar - 'JPY', // Japanese Yen - 'KES', // Kenyan Shilling - 'KGS', // Kyrgyzstani Som - 'KHR', // Cambodian Riel - 'KID', // Kiribati Dollar - 'KMF', // Comorian Franc - 'KRW', // South Korean Won - 'KWD', // Kuwaiti Dinar - 'KYD', // Cayman Islands Dollar - 'KZT', // Kazakhstani Tenge - 'LAK', // Lao Kip - 'LBP', // Lebanese Pound - 'LKR', // Sri Lankan Rupee - 'LRD', // Liberian Dollar 'LSL', // Lesotho Loti 'LYD', // Libyan Dinar 'MAD', // Moroccan Dirham @@ -163,3 +144,65 @@ bool isFiatCurrency(String currencyCode) { }; return fiatCurrencies.contains(currencyCode); } + +bool isCryptoCurrency(String currencyCode) { + const cryptoCurrencies = { + 'BTC', // Bitcoin + 'BCH', // Bitcoin Cash + 'LTC', // Litecoin + 'ETH' // Ethereum + }; + return cryptoCurrencies.contains(currencyCode); +} + +enum PaymentMethodType { + CRYPTO, + FIAT, + UNKNOWN, +} + +// Payment Method Mappings +const Map fiatPaymentMethodLabels = { + 'AUSTRALIA_PAYID': 'Australia PayID', + 'CASH_APP': 'Cash App', + 'CASH_AT_ATM': 'Cash at ATM', + 'F2F': 'Face to Face', + 'FASTER_PAYMENTS': 'Faster Payments', + 'MONEY_GRAM': 'MoneyGram', + 'PAXUM': 'Paxum', + 'PAYPAL': 'PayPal', + 'PAY_BY_MAIL': 'Pay by Mail', + 'REVOLUT': 'Revolut', + 'SEPA': 'SEPA', + 'SEPA_INSTANT': 'SEPA Instant', + 'STRIKE': 'Strike', + 'SWIFT': 'SWIFT', + 'TRANSFERWISE': 'TransferWise', + 'UPHOLD': 'Uphold', + 'VENMO': 'Venmo', + 'ZELLE': 'Zelle', +}; + +const Map cryptoPaymentMethodLabels = { + 'BLOCK_CHAINS': 'Blockchains' +}; + +// Combine both maps for easy lookup +const Map paymentMethodLabels = { + ...fiatPaymentMethodLabels, + ...cryptoPaymentMethodLabels, +}; + +String getPaymentMethodLabel(String id) { + return paymentMethodLabels[id] ?? 'Unknown Payment Method'; +} + +PaymentMethodType getPaymentMethodType(String paymentMethodId) { + if (cryptoPaymentMethodLabels.containsKey(paymentMethodId)) { + return PaymentMethodType.CRYPTO; + } else if (fiatPaymentMethodLabels.containsKey(paymentMethodId)) { + return PaymentMethodType.FIAT; + } else { + return PaymentMethodType.UNKNOWN; + } +} diff --git a/lib/widgets/payment_account_form.dart b/lib/widgets/add_payment_account_form.dart similarity index 100% rename from lib/widgets/payment_account_form.dart rename to lib/widgets/add_payment_account_form.dart diff --git a/lib/widgets/payment_method_form.dart b/lib/widgets/add_payment_method_form.dart similarity index 78% rename from lib/widgets/payment_method_form.dart rename to lib/widgets/add_payment_method_form.dart index 4f8c873..169433e 100644 --- a/lib/widgets/payment_method_form.dart +++ b/lib/widgets/add_payment_method_form.dart @@ -1,10 +1,14 @@ import 'package:flutter/material.dart'; +import 'package:haveno_flutter_app/utils/payment_utils.dart'; +import 'package:haveno_flutter_app/widgets/add_payment_account_form.dart'; import 'package:provider/provider.dart'; -import 'package:haveno_flutter_app/providers/payment_accounts_provider.dart'; -import 'package:haveno_flutter_app/proto/compiled/pb.pb.dart'; -import 'payment_account_form.dart'; +import 'package:haveno_flutter_app/providers/payment_accounts_provider.dart';// Import the utils file class PaymentMethodSelectionForm extends StatefulWidget { + final String accountType; + + PaymentMethodSelectionForm({required this.accountType}); + @override _PaymentMethodSelectionFormState createState() => _PaymentMethodSelectionFormState(); @@ -18,7 +22,9 @@ class _PaymentMethodSelectionFormState Widget build(BuildContext context) { final paymentAccountsProvider = Provider.of(context); - final paymentMethods = paymentAccountsProvider.paymentMethods ?? []; + final paymentMethods = widget.accountType == 'FIAT' + ? paymentAccountsProvider.paymentMethods + : paymentAccountsProvider.cryptoCurrencyPaymentMethods; return Padding( padding: MediaQuery.of(context).viewInsets, @@ -34,10 +40,10 @@ class _PaymentMethodSelectionFormState labelText: 'Payment Method', border: OutlineInputBorder(), ), - items: paymentMethods.map((method) { + items: paymentMethods?.map((method) { return DropdownMenuItem( value: method.id, - child: Text(method.id), + child: Text(getPaymentMethodLabel(method.id)), ); }).toList(), onChanged: (value) { @@ -56,7 +62,7 @@ class _PaymentMethodSelectionFormState builder: (BuildContext context) { return DynamicPaymentAccountForm( paymentAccountForm: form, - paymentMethodLabel: value, + paymentMethodLabel: getPaymentMethodLabel(value), paymentMethodId: value); }, ); diff --git a/lib/widgets/new_trade_offer_form.dart b/lib/widgets/new_trade_offer_form.dart index e06119f..73f5ff9 100644 --- a/lib/widgets/new_trade_offer_form.dart +++ b/lib/widgets/new_trade_offer_form.dart @@ -8,10 +8,12 @@ import 'dart:convert'; // Import the dart:convert library class NewTradeOfferForm extends StatefulWidget { final GlobalKey formKey; final List paymentAccounts; + final String direction; // New argument for direction ('BUY' or 'SELL') const NewTradeOfferForm({ required this.formKey, required this.paymentAccounts, + required this.direction, }); @override @@ -22,6 +24,7 @@ class __NewTradeOfferFormState extends State { PaymentAccount? _selectedPaymentAccount; TradeCurrency? _selectedTradeCurrency; int _selectedPricingTypeIndex = 0; // 0 for Fixed, 1 for Dynamic + bool _reserveExactAmount = false; final TextEditingController _priceController = TextEditingController(); final TextEditingController _depositController = @@ -34,6 +37,8 @@ class __NewTradeOfferFormState extends State { @override Widget build(BuildContext context) { + final isBuy = widget.direction == 'BUY'; + return Padding( padding: MediaQuery.of(context).viewInsets, child: Container( @@ -44,7 +49,10 @@ class __NewTradeOfferFormState extends State { child: Column( mainAxisSize: MainAxisSize.min, children: [ - const Text('Open a New Offer', style: TextStyle(fontSize: 18)), + Text( + 'Open a New XMR ${isBuy ? 'Buy' : 'Sell'} Offer', + style: const TextStyle(fontSize: 18), + ), const SizedBox(height: 16.0), ToggleButtons( isSelected: [ @@ -70,7 +78,7 @@ class __NewTradeOfferFormState extends State { const SizedBox(height: 16.0), DropdownButtonFormField( decoration: const InputDecoration( - labelText: 'Payment Account', + labelText: 'Your Sender Account', border: OutlineInputBorder(), ), value: _selectedPaymentAccount, @@ -138,9 +146,11 @@ class __NewTradeOfferFormState extends State { if (_selectedPricingTypeIndex == 1) TextFormField( controller: _marginController, - decoration: const InputDecoration( - labelText: 'Market Price Below Margin (%)', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: isBuy + ? 'Market Price Below Margin (%)' + : 'Market Price Above Margin (%)', + border: const OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { @@ -156,13 +166,13 @@ class __NewTradeOfferFormState extends State { const SizedBox(height: 16.0), TextFormField( controller: _amountController, - decoration: const InputDecoration( - labelText: 'Amount of XMR', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: 'Amount of XMR to ${isBuy ? 'Buy' : 'Sell'}', + border: const OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { - return 'Please enter the maximum amount you wish to buy'; + return 'Please enter the maximum amount you wish to ${isBuy ? 'buy' : 'sell'}'; } return null; }, @@ -171,7 +181,7 @@ class __NewTradeOfferFormState extends State { TextFormField( controller: _minAmountController, decoration: const InputDecoration( - labelText: 'Minimum Transaction Amount (Of XMR)', + labelText: 'Minimum Transaction Amount (XMR)', border: OutlineInputBorder(), ), validator: (value) { @@ -185,12 +195,12 @@ class __NewTradeOfferFormState extends State { TextFormField( controller: _depositController, decoration: const InputDecoration( - labelText: 'Buyer Security Deposit (%)', + labelText: 'Mutual Security Deposit (%)', border: OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { - return 'Please enter the buyer security deposit'; + return 'Please enter the mutual security deposit'; } final deposit = double.tryParse(value); if (deposit == null || deposit < 0 || deposit > 50) { @@ -204,9 +214,10 @@ class __NewTradeOfferFormState extends State { if (_selectedPricingTypeIndex == 1) TextFormField( controller: _triggerPriceController, - decoration: const InputDecoration( - labelText: 'Stop Trigger Price', - border: OutlineInputBorder(), + decoration: InputDecoration( + labelText: + 'Delist If Market Price Goes Above (${_selectedTradeCurrency?.code ?? ''})', + border: const OutlineInputBorder(), ), validator: (value) { if (value == null || value.isEmpty) { @@ -216,13 +227,43 @@ class __NewTradeOfferFormState extends State { }, ), const SizedBox(height: 16.0), + Row( + children: [ + Expanded( + child: CheckboxListTile( + title: const Row( + children: [ + Text('Reserve only the funds needed'), + SizedBox(width: 4), + Tooltip( + message: + 'If selected, only the exact amount of funds needed for this trade will be reserved. This may also incur a mining fee and will require 10 confirmations therefore it will take ~20 minutes longer to post your trade.', + child: Icon( + Icons.info_outline, + color: Colors.white, + ), + ), + ], + ), + value: _reserveExactAmount, + onChanged: (value) { + setState(() { + _reserveExactAmount = value ?? false; + }); + }, + controlAffinity: ListTileControlAffinity.leading, + ), + ), + ], + ), + const SizedBox(height: 16.0), ElevatedButton( onPressed: () async { if (widget.formKey.currentState?.validate() ?? false) { // Prepare the data to be sent final offerData = { 'currencyCode': _selectedTradeCurrency?.code ?? '', - 'direction': 'BUY', + 'direction': widget.direction, 'price': _selectedPricingTypeIndex == 0 ? _priceController.text : '', @@ -248,7 +289,7 @@ class __NewTradeOfferFormState extends State { 'triggerPrice': _selectedPricingTypeIndex == 1 ? _triggerPriceController.text : '', - 'reserveExactAmount': true, + 'reserveExactAmount': _reserveExactAmount, 'paymentAccountId': _selectedPaymentAccount?.id ?? '', }; @@ -261,7 +302,7 @@ class __NewTradeOfferFormState extends State { Provider.of(context, listen: false); offersProvider.postOffer( currencyCode: _selectedTradeCurrency?.code ?? '', - direction: 'BUY', + direction: widget.direction, price: _selectedPricingTypeIndex == 0 ? _priceController.text : '', // Use the price from the controller @@ -284,7 +325,7 @@ class __NewTradeOfferFormState extends State { triggerPrice: _selectedPricingTypeIndex == 1 ? _triggerPriceController.text : '', // Use the trigger price from the controller - reserveExactAmount: false, + reserveExactAmount: _reserveExactAmount, paymentAccountId: _selectedPaymentAccount?.id ?? '', ); Navigator.pop(context); @@ -307,3 +348,4 @@ class __NewTradeOfferFormState extends State { ); } } + diff --git a/lib/widgets/offer_card_widget.dart b/lib/widgets/offer_card_widget.dart index 23cae06..02ec166 100644 --- a/lib/widgets/offer_card_widget.dart +++ b/lib/widgets/offer_card_widget.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'package:haveno_flutter_app/utils/payment_utils.dart'; import 'package:provider/provider.dart'; import 'package:haveno_flutter_app/proto/compiled/grpc.pbgrpc.dart'; import 'package:haveno_flutter_app/screens/my_offer_detail_screen.dart'; @@ -43,8 +44,12 @@ class OfferCard extends StatelessWidget { } }, child: Card( - margin: const EdgeInsets.all(2.0), + margin: const EdgeInsets.only(top: 1.0), // 1 pixel margin at the top color: Theme.of(context).cardTheme.color, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.zero, // No border radius + ), + elevation: 0, // No elevation child: Padding( padding: const EdgeInsets.all(16.0), child: Column( @@ -104,7 +109,7 @@ class OfferCard extends StatelessWidget { const SizedBox(height: 10), Center( child: Text( - _isFiatCurrency(offer.counterCurrencyCode) + isFiatCurrency(offer.counterCurrencyCode) ? '${double.parse(offer.price).toStringAsFixed(2)} ${offer.counterCurrencyCode}' : '${offer.price}', style: const TextStyle( @@ -142,20 +147,4 @@ class OfferCard extends StatelessWidget { }, ); } - - bool _isFiatCurrency(String currencyCode) { - const fiatCurrencies = { - 'GBP', - 'USD', - 'EUR', - 'JPY', - 'AUD', - 'CAD', - 'CHF', - 'CNY', - 'SEK', - 'NZD' - }; - return fiatCurrencies.contains(currencyCode); - } } diff --git a/pubspec.lock b/pubspec.lock index 6ad4bba..9d2a945 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -25,6 +25,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.11.0" + badges: + dependency: "direct main" + description: + name: badges + sha256: a7b6bbd60dce418df0db3058b53f9d083c22cdb5132a052145dc267494df0b84 + url: "https://pub.dev" + source: hosted + version: "3.1.2" boolean_selector: dependency: transitive description: @@ -541,13 +549,6 @@ packages: url: "https://pub.dev" source: hosted version: "2.0.0" - tor: - dependency: "direct main" - description: - path: "../flutter_plugins/tor" - relative: true - source: path - version: "0.0.1" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index aa61f0d..11938d2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -29,11 +29,12 @@ dependencies: font_awesome_flutter: ^10.7.0 timeline_tile: ^2.0.0 # Add your plugin dependency here - tor: - path: ../flutter_plugins/tor + ##tor: + ## path: ../flutter_plugins/tor protoc_plugin: ^21.1.2 fixnum: ^1.1.0 intl: ^0.17.0 + badges: ^3.1.2 dev_dependencies: flutter_test: