This commit is contained in:
unknown 2024-07-15 15:14:30 -04:00 committed by KewbitXMR
parent 62a76f9e18
commit 4ce603fbf1
24 changed files with 809 additions and 284 deletions

View file

@ -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/prices_provider.dart';
import 'package:haveno_flutter_app/providers/trades_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/providers/wallets_provider.dart';
import 'package:haveno_flutter_app/screens/accounts_screen.dart'; import 'package:haveno_flutter_app/screens/drawer/payment_accounts_screen.dart';
import 'package:haveno_flutter_app/screens/settings_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/trades_tab.dart';
import 'package:haveno_flutter_app/tabs/buy_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/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:logging/logging.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:haveno_flutter_app/services/haveno_service.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/http_service.dart';
import 'package:haveno_flutter_app/services/monero_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/get_version_provider.dart';
import 'package:haveno_flutter_app/providers/account_provider.dart'; import 'package:haveno_flutter_app/providers/account_provider.dart';
import 'dart:async'; import 'dart:async';
import 'package:badges/badges.dart' as badges; // Import the badges package
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -30,18 +31,18 @@ void main() async {
_setupLogging(); _setupLogging();
// Initialize services // Initialize services
final torService = TorService(); //final torService = TorService();
await torService.initializeTor(); //await torService.initializeTor();
final httpService = HttpService(); final httpService = HttpService();
final moneroService = MoneroService(); final moneroService = MoneroService();
final havenoService = HavenoService('192.168.0.18', 3201, 'apitest'); final havenoService = HavenoService('127.0.0.1', 3201, 'apitest');
runApp( runApp(
MultiProvider( MultiProvider(
providers: [ providers: [
Provider(create: (_) => torService), //Provider(create: (_) => torService),
Provider(create: (_) => httpService), Provider(create: (_) => httpService),
Provider(create: (_) => moneroService), Provider(create: (_) => moneroService),
Provider(create: (_) => havenoService), Provider(create: (_) => havenoService),
@ -95,6 +96,7 @@ class MyApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return MaterialApp( return MaterialApp(
debugShowCheckedModeBanner: false,
theme: ThemeData( theme: ThemeData(
useMaterial3: true, useMaterial3: true,
colorScheme: ColorScheme.fromSeed( colorScheme: ColorScheme.fromSeed(
@ -104,13 +106,13 @@ class MyApp extends StatelessWidget {
// Ensures dark mode with light text // Ensures dark mode with light text
), ),
scaffoldBackgroundColor: Color(0xFF303030), scaffoldBackgroundColor: Color(0xFF303030),
appBarTheme: AppBarTheme( appBarTheme: const AppBarTheme(
backgroundColor: Color(0xFF303030), backgroundColor: Color(0xFF303030),
), ),
drawerTheme: DrawerThemeData( drawerTheme: const DrawerThemeData(
backgroundColor: Color(0xFF303030), backgroundColor: Color(0xFF303030),
), ),
bottomNavigationBarTheme: BottomNavigationBarThemeData( bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: Color(0xFF303030), backgroundColor: Color(0xFF303030),
selectedItemColor: Color(0xFFF4511E), selectedItemColor: Color(0xFFF4511E),
unselectedItemColor: Colors.white, unselectedItemColor: Colors.white,
@ -124,7 +126,7 @@ class MyApp extends StatelessWidget {
), ),
), ),
), ),
cardTheme: CardTheme( cardTheme: const CardTheme(
color: Color(0xFF424242), // Card background color color: Color(0xFF424242), // Card background color
), ),
), ),
@ -142,6 +144,7 @@ class _HomeScreenState extends State<HomeScreen> {
String _statusMessage = "Connecting to Tor..."; String _statusMessage = "Connecting to Tor...";
Timer? _timer; Timer? _timer;
int _selectedIndex = 0; int _selectedIndex = 0;
int _notificationCount = 5; // Mock notification count
static final List<Widget> _widgetOptions = <Widget>[ static final List<Widget> _widgetOptions = <Widget>[
BuyTab(), BuyTab(),
@ -152,17 +155,17 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
final torService = context.read<TorService>(); // final torService = context.read<TorService>();
torService.statusStream.listen((String status) { // torService.statusStream.listen((String status) {
print(status); // print(status);
setState(() { // setState(() {
if (status.contains("started")) { // if (status.contains("started")) {
_statusMessage = "Connecting to the Monero network..."; // _statusMessage = "Connecting to the Monero network...";
} else { // } else {
_statusMessage = status; // _statusMessage = status;
} // }
}); // });
}); // });
_initializeServices(); _initializeServices();
} }
@ -201,7 +204,7 @@ class _HomeScreenState extends State<HomeScreen> {
@override @override
void dispose() { void dispose() {
context.read<TorService>().dispose(); //context.read<TorService>().dispose();
context.read<HttpService>().close(); context.read<HttpService>().close();
context.read<MoneroService>().close(); context.read<MoneroService>().close();
_timer?.cancel(); _timer?.cancel();
@ -213,7 +216,25 @@ class _HomeScreenState extends State<HomeScreen> {
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( 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), drawer: _buildDrawer(context),
body: Center( body: Center(
@ -275,7 +296,7 @@ class _HomeScreenState extends State<HomeScreen> {
Navigator.pop(context); Navigator.pop(context);
Navigator.push( Navigator.push(
context, context,
MaterialPageRoute(builder: (context) => AccountsScreen()), MaterialPageRoute(builder: (context) => PaymentAccountsScreen()),
); );
}, },
), ),

View file

@ -54,11 +54,11 @@ class OffersProvider with ChangeNotifier {
required String direction, required String direction,
required String price, required String price,
required bool useMarketBasedPrice, required bool useMarketBasedPrice,
required double marketPriceMarginPct, double? marketPriceMarginPct,
required fixnum.Int64 amount, required fixnum.Int64 amount,
required fixnum.Int64 minAmount, required fixnum.Int64 minAmount,
required double buyerSecurityDepositPct, required double buyerSecurityDepositPct,
required String triggerPrice, String? triggerPrice,
required bool reserveExactAmount, required bool reserveExactAmount,
required String paymentAccountId, required String paymentAccountId,
}) async { }) async {

View file

@ -14,6 +14,7 @@ class PaymentAccountsProvider with ChangeNotifier {
final Logger _logger = Logger('PaymentAccountsProvider'); final Logger _logger = Logger('PaymentAccountsProvider');
final HavenoService _havenoService; final HavenoService _havenoService;
List<PaymentMethod>? _paymentMethods; List<PaymentMethod>? _paymentMethods;
List<PaymentMethod>? _cryptoCurrencyPaymentMethods;
List<PaymentAccount>? _paymentAccounts; List<PaymentAccount>? _paymentAccounts;
PaymentAccountForm _paymentAccountForm = PaymentAccountForm(); PaymentAccountForm _paymentAccountForm = PaymentAccountForm();
bool _isLoadingPaymentMethods = false; bool _isLoadingPaymentMethods = false;
@ -62,6 +63,7 @@ class PaymentAccountsProvider with ChangeNotifier {
} }
List<PaymentMethod>? get paymentMethods => _paymentMethods; List<PaymentMethod>? get paymentMethods => _paymentMethods;
List<PaymentMethod>? get cryptoCurrencyPaymentMethods => _cryptoCurrencyPaymentMethods;
List<PaymentAccount>? get paymentAccounts => _paymentAccounts; List<PaymentAccount>? get paymentAccounts => _paymentAccounts;
PaymentAccountForm get paymentAccountForm => _paymentAccountForm; PaymentAccountForm get paymentAccountForm => _paymentAccountForm;
bool get isLoadingPaymentMethods => _isLoadingPaymentMethods; bool get isLoadingPaymentMethods => _isLoadingPaymentMethods;
@ -76,6 +78,9 @@ class PaymentAccountsProvider with ChangeNotifier {
final getPaymentMethodsReply = await _havenoService.paymentAccountsClient final getPaymentMethodsReply = await _havenoService.paymentAccountsClient
.getPaymentMethods(GetPaymentMethodsRequest()); .getPaymentMethods(GetPaymentMethodsRequest());
_paymentMethods = getPaymentMethodsReply.paymentMethods; _paymentMethods = getPaymentMethodsReply.paymentMethods;
// _paymentMethods?.forEach((method) {
// debugPrint("Payment Method: ${jsonEncode(method.toProto3Json())}");
// });
} catch (e) { } catch (e) {
print("Failed to get payment methods: $e"); print("Failed to get payment methods: $e");
} finally { } finally {
@ -92,9 +97,9 @@ class PaymentAccountsProvider with ChangeNotifier {
final getPaymentAccountsReply = await _havenoService.paymentAccountsClient final getPaymentAccountsReply = await _havenoService.paymentAccountsClient
.getPaymentAccounts(GetPaymentAccountsRequest()); .getPaymentAccounts(GetPaymentAccountsRequest());
_paymentAccounts = getPaymentAccountsReply.paymentAccounts; _paymentAccounts = getPaymentAccountsReply.paymentAccounts;
_paymentAccounts?.forEach((account) { // _paymentAccounts?.forEach((account) {
debugPrint(jsonEncode(account.toProto3Json())); // debugPrint(jsonEncode(account.toProto3Json()));
}); // });
} catch (e) { } catch (e) {
print("Failed to get payment accounts: $e"); print("Failed to get payment accounts: $e");
} finally { } finally {
@ -104,6 +109,24 @@ class PaymentAccountsProvider with ChangeNotifier {
return paymentAccounts; return paymentAccounts;
} }
Future<List<PaymentMethod>?> 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<PaymentAccountForm> getPaymentAcountForm( Future<PaymentAccountForm> getPaymentAcountForm(
String paymentMethodId) async { String paymentMethodId) async {
final traditionalCurrencyPaymentAccountForm = final traditionalCurrencyPaymentAccountForm =

View file

@ -4,18 +4,18 @@ import 'package:haveno_flutter_app/services/haveno_service.dart';
class PricesProvider with ChangeNotifier { class PricesProvider with ChangeNotifier {
final HavenoService _havenoService; final HavenoService _havenoService;
List<MarketPriceInfo>? _marketPrices; List<MarketPriceInfo> _marketPrices = [];
PricesProvider(this._havenoService); PricesProvider(this._havenoService);
List<MarketPriceInfo>? get prices => _marketPrices; List<MarketPriceInfo> get prices => _marketPrices;
Future<void> getPrices() async { Future<void> getXmrMarketPrices() async {
try { try {
final getMarketPricesReply = await _havenoService.priceClient final getMarketPricesReply = await _havenoService.priceClient
.getMarketPrices(MarketPricesRequest()); .getMarketPrices(MarketPricesRequest());
_marketPrices = getMarketPricesReply.marketPrice; _marketPrices = getMarketPricesReply.marketPrice;
_marketPrices?.forEach((price) { _marketPrices.forEach((price) {
print("Price: $price"); print("Price: $price");
}); });
notifyListeners(); notifyListeners();

View file

@ -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<AccountsScreen> {
@override
void initState() {
super.initState();
final paymentAccountsProvider =
Provider.of<PaymentAccountsProvider>(context, listen: false);
paymentAccountsProvider.getPaymentAccounts();
}
void _showCreateAccountForm(BuildContext context) {
final paymentAccountsProvider =
Provider.of<PaymentAccountsProvider>(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<PaymentAccountsProvider>(context);
return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar(
title: const Text('Accounts'),
),
body: Consumer<PaymentAccountsProvider>(
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),
),
);
}
}

View file

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.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:provider/provider.dart';
import 'package:haveno_flutter_app/providers/trades_provider.dart'; import 'package:haveno_flutter_app/providers/trades_provider.dart';
import 'package:haveno_flutter_app/proto/compiled/grpc.pbgrpc.dart'; import 'package:haveno_flutter_app/proto/compiled/grpc.pbgrpc.dart';

View file

@ -1,7 +1,7 @@
import 'dart:convert'; import 'dart:convert';
import 'package:fixnum/fixnum.dart'; import 'package:fixnum/fixnum.dart';
import 'package:flutter/material.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:provider/provider.dart';
import 'package:haveno_flutter_app/providers/trades_provider.dart'; import 'package:haveno_flutter_app/providers/trades_provider.dart';
import 'package:haveno_flutter_app/proto/compiled/grpc.pbgrpc.dart'; import 'package:haveno_flutter_app/proto/compiled/grpc.pbgrpc.dart';

View file

@ -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<PaymentAccountsScreen>
with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
final paymentAccountsProvider =
Provider.of<PaymentAccountsProvider>(context, listen: false);
paymentAccountsProvider.getPaymentAccounts();
paymentAccountsProvider.getPaymentMethods();
paymentAccountsProvider.getCryptoCurrencyPaymentMethods();
}
void _showCreateAccountForm(BuildContext context) {
final paymentAccountsProvider =
Provider.of<PaymentAccountsProvider>(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<PaymentAccountsProvider>(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(),
),
],
),
),
);
},
),
);
}
}
}
}

View file

@ -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<String>(
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<String>(
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<String>(
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<String>(
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) {},
),
],
),
),
),
],
),
),
);
}
}

View file

@ -59,8 +59,10 @@ class _WalletsScreenState extends State<WalletScreen> {
children: [ children: [
if (balances.hasXmr()) if (balances.hasXmr())
_buildXmrBalanceCard( _buildXmrBalanceCard(
'XMR', balances.xmr, walletsProvider.xmrPrimaryAddress), 'XMR', balances.xmr),
const SizedBox(height: 16.0), const SizedBox(height: 10.0),
_buildXmrAddressCard(walletsProvider.xmrPrimaryAddress),
const SizedBox(height: 10.0),
_buildXmrTransactionsList(walletsProvider.xmrTxs), _buildXmrTransactionsList(walletsProvider.xmrTxs),
], ],
); );
@ -72,53 +74,76 @@ class _WalletsScreenState extends State<WalletScreen> {
} }
Widget _buildXmrBalanceCard( 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( return Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: xmrAddress != null
crossAxisAlignment: CrossAxisAlignment.center, ? Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
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(
children: [ children: [
Text( const Text(
primaryAddress, 'Addresses',
textAlign: TextAlign.center, 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( ElevatedButton(
onPressed: () { onPressed: () {
Clipboard.setData(ClipboardData(text: primaryAddress)); // request a new XMR address here
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Address copied to clipboard')),
);
}, },
child: const Text('Copy Address'), child: Text('Request a new address'),
), ),
], ],
), ),
],
),
), ),
); );
} }
Widget _buildXmrTransactionsList(List<XmrTx>? transactions) { Widget _buildXmrTransactionsList(List<XmrTx>? transactions) {
if (transactions != null) { if (transactions != null) {
transactions.sort((a, b) => b.timestamp.compareTo(a.timestamp)); transactions.sort((a, b) => b.timestamp.compareTo(a.timestamp));

View file

@ -202,7 +202,7 @@ class _OfferDetailScreenState extends State<OfferDetailScreen> {
), ),
), ),
), ),
SizedBox(height: 16.0), SizedBox(height: 10.0),
Card( Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
@ -241,7 +241,7 @@ class _OfferDetailScreenState extends State<OfferDetailScreen> {
), ),
), ),
), ),
SizedBox(height: 16.0), SizedBox(height: 10.0),
Card( Card(
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),

View file

@ -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),
),
),
);
}
}

View file

@ -1,15 +1,27 @@
import 'dart:convert';
import 'package:flutter/material.dart'; 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:haveno_flutter_app/widgets/offer_card_widget.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:haveno_flutter_app/providers/offers_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 { class BuyMarketOffersTab extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final offersProvider = Provider.of<OffersProvider>(context, listen: false); final offersProvider = Provider.of<OffersProvider>(context, listen: false);
final pricesProvider = Provider.of<PricesProvider>(context, listen: false);
Future<void> fetchData() async {
if (pricesProvider.prices.isEmpty) {
await pricesProvider.getXmrMarketPrices();
}
await offersProvider.getOffers();
}
return FutureBuilder<void>( return FutureBuilder<void>(
future: offersProvider.getOffers(), // Fetch offers future: fetchData(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@ -17,16 +29,39 @@ class BuyMarketOffersTab extends StatelessWidget {
return Center(child: Text('Error: ${snapshot.error}')); return Center(child: Text('Error: ${snapshot.error}'));
} else { } else {
final offers = offersProvider.marketBuyOffers; final offers = offersProvider.marketBuyOffers;
final marketPrices = pricesProvider.prices;
if (offers == null || offers.isEmpty) { if (offers == null || offers.isEmpty) {
return const Center(child: Text('No offers available')); return const Center(child: Text('No offers available'));
} else { } else {
// Calculate value score for each offer
final List<Map<String, dynamic>> 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( return Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(top: 2.0), // Add 2 pixels of padding at the top
top: 2.0), // Add 2 pixels of padding at the top
child: ListView.builder( child: ListView.builder(
itemCount: offers.length, itemCount: sortedOffers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final offer = offers[index]; final offer = sortedOffers[index]['offer'] as OfferInfo;
return OfferCard(offer: offer); return OfferCard(offer: offer);
}, },
), ),

View file

@ -12,16 +12,27 @@ class BuyTab extends StatefulWidget {
_BuyTabState createState() => _BuyTabState(); _BuyTabState createState() => _BuyTabState();
} }
class _BuyTabState extends State<BuyTab> with SingleTickerProviderStateMixin { class _BuyTabState extends State<BuyTab> with TickerProviderStateMixin {
final _formKey = GlobalKey<FormState>(); final _formKey = GlobalKey<FormState>();
TabController? _tabController; TabController? _tabController;
bool _isLoadingPaymentMethods = true; bool _isLoadingPaymentMethods = true;
List<PaymentAccount> _paymentAccounts = []; List<PaymentAccount> _paymentAccounts = [];
bool _isFilterVisible = false;
late AnimationController _filterAnimationController;
late Animation<double> _filterAnimation;
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_tabController = TabController(length: 2, vsync: this); _tabController = TabController(length: 2, vsync: this);
_filterAnimationController = AnimationController(
duration: const Duration(milliseconds: 300),
vsync: this,
);
_filterAnimation = CurvedAnimation(
parent: _filterAnimationController,
curve: Curves.easeInOut,
);
_initializeData(); _initializeData();
} }
@ -46,6 +57,7 @@ class _BuyTabState extends State<BuyTab> with SingleTickerProviderStateMixin {
@override @override
void dispose() { void dispose() {
_tabController?.dispose(); _tabController?.dispose();
_filterAnimationController.dispose();
super.dispose(); super.dispose();
} }
@ -55,6 +67,7 @@ class _BuyTabState extends State<BuyTab> with SingleTickerProviderStateMixin {
isScrollControlled: true, isScrollControlled: true,
builder: (BuildContext context) { builder: (BuildContext context) {
return NewTradeOfferForm( return NewTradeOfferForm(
direction: 'BUY',
paymentAccounts: _paymentAccounts, paymentAccounts: _paymentAccounts,
formKey: _formKey, formKey: _formKey,
); );
@ -62,12 +75,29 @@ class _BuyTabState extends State<BuyTab> with SingleTickerProviderStateMixin {
); );
} }
void _toggleFilter() {
setState(() {
_isFilterVisible = !_isFilterVisible;
if (_isFilterVisible) {
_filterAnimationController.forward();
} else {
_filterAnimationController.reverse();
}
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: Theme.of(context).scaffoldBackgroundColor, backgroundColor: Theme.of(context).scaffoldBackgroundColor,
appBar: AppBar( appBar: AppBar(
title: const Text('Buy Monero'), title: const Text('Buy Monero'),
actions: [
IconButton(
icon: Icon(Icons.filter_list),
onPressed: _toggleFilter,
),
],
bottom: TabBar( bottom: TabBar(
controller: _tabController, controller: _tabController,
tabs: const [ tabs: const [
@ -76,15 +106,26 @@ class _BuyTabState extends State<BuyTab> with SingleTickerProviderStateMixin {
], ],
), ),
), ),
body: _isLoadingPaymentMethods body: Column(
? const Center(child: CircularProgressIndicator()) children: [
: TabBarView( SizeTransition(
controller: _tabController, sizeFactor: _filterAnimation,
children: [ axisAlignment: -1.0,
BuyMarketOffersTab(), child: _buildFilterMenu(),
BuyMyOffersTab(), ),
], Expanded(
), child: _isLoadingPaymentMethods
? const Center(child: CircularProgressIndicator())
: TabBarView(
controller: _tabController,
children: [
BuyMarketOffersTab(),
BuyMyOffersTab(),
],
),
),
],
),
floatingActionButton: FloatingActionButton( floatingActionButton: FloatingActionButton(
onPressed: _showNewTradeForm, onPressed: _showNewTradeForm,
backgroundColor: Theme.of(context).colorScheme.primary, backgroundColor: Theme.of(context).colorScheme.primary,
@ -92,4 +133,52 @@ class _BuyTabState extends State<BuyTab> 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<String>(
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<String>(
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
},
),
],
),
);
}
} }

View file

@ -1,15 +1,27 @@
import 'dart:convert';
import 'package:flutter/material.dart'; 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:haveno_flutter_app/widgets/offer_card_widget.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:haveno_flutter_app/providers/offers_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 { class SaleMarketOffersTab extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final offersProvider = Provider.of<OffersProvider>(context, listen: false); final offersProvider = Provider.of<OffersProvider>(context, listen: false);
final pricesProvider = Provider.of<PricesProvider>(context, listen: false);
Future<void> fetchData() async {
if (pricesProvider.prices.isEmpty) {
await pricesProvider.getXmrMarketPrices();
}
await offersProvider.getOffers();
}
return FutureBuilder<void>( return FutureBuilder<void>(
future: offersProvider.getOffers(), // Fetch offers future: fetchData(),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) { if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
@ -17,16 +29,39 @@ class SaleMarketOffersTab extends StatelessWidget {
return Center(child: Text('Error: ${snapshot.error}')); return Center(child: Text('Error: ${snapshot.error}'));
} else { } else {
final offers = offersProvider.marketSellOffers; final offers = offersProvider.marketSellOffers;
final marketPrices = pricesProvider.prices;
if (offers == null || offers.isEmpty) { if (offers == null || offers.isEmpty) {
return const Center(child: Text('No offers available')); return const Center(child: Text('No offers available'));
} else { } else {
// Calculate value score for each offer
final List<Map<String, dynamic>> 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( return Padding(
padding: const EdgeInsets.only( padding: const EdgeInsets.only(top: 2.0), // Add 2 pixels of padding at the top
top: 2.0), // Add 2 pixels of padding at the top
child: ListView.builder( child: ListView.builder(
itemCount: offers.length, itemCount: sortedOffers.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final offer = offers[index]; final offer = sortedOffers[index]['offer'] as OfferInfo;
return OfferCard(offer: offer); return OfferCard(offer: offer);
}, },
), ),

View file

@ -55,6 +55,7 @@ class _SellTabState extends State<SellTab> with SingleTickerProviderStateMixin {
isScrollControlled: true, isScrollControlled: true,
builder: (BuildContext context) { builder: (BuildContext context) {
return NewTradeOfferForm( return NewTradeOfferForm(
direction: 'SELL',
paymentAccounts: _paymentAccounts, paymentAccounts: _paymentAccounts,
formKey: _formKey, formKey: _formKey,
); );

View file

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:haveno_flutter_app/providers/account_provider.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_buyer_trade_timeline_screen.dart';
import 'package:haveno_flutter_app/screens/active_seller_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:intl/intl.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:haveno_flutter_app/providers/trades_provider.dart'; import 'package:haveno_flutter_app/providers/trades_provider.dart';

View file

@ -65,25 +65,6 @@ bool isFiatCurrency(String currencyCode) {
'IMP', // Isle of Man Pound 'IMP', // Isle of Man Pound
'INR', // Indian Rupee 'INR', // Indian Rupee
'IQD', // Iraqi Dinar '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 'LSL', // Lesotho Loti
'LYD', // Libyan Dinar 'LYD', // Libyan Dinar
'MAD', // Moroccan Dirham 'MAD', // Moroccan Dirham
@ -163,3 +144,65 @@ bool isFiatCurrency(String currencyCode) {
}; };
return fiatCurrencies.contains(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<String, String> 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<String, String> cryptoPaymentMethodLabels = {
'BLOCK_CHAINS': 'Blockchains'
};
// Combine both maps for easy lookup
const Map<String, String> 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;
}
}

View file

@ -1,10 +1,14 @@
import 'package:flutter/material.dart'; 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:provider/provider.dart';
import 'package:haveno_flutter_app/providers/payment_accounts_provider.dart'; import 'package:haveno_flutter_app/providers/payment_accounts_provider.dart';// Import the utils file
import 'package:haveno_flutter_app/proto/compiled/pb.pb.dart';
import 'payment_account_form.dart';
class PaymentMethodSelectionForm extends StatefulWidget { class PaymentMethodSelectionForm extends StatefulWidget {
final String accountType;
PaymentMethodSelectionForm({required this.accountType});
@override @override
_PaymentMethodSelectionFormState createState() => _PaymentMethodSelectionFormState createState() =>
_PaymentMethodSelectionFormState(); _PaymentMethodSelectionFormState();
@ -18,7 +22,9 @@ class _PaymentMethodSelectionFormState
Widget build(BuildContext context) { Widget build(BuildContext context) {
final paymentAccountsProvider = final paymentAccountsProvider =
Provider.of<PaymentAccountsProvider>(context); Provider.of<PaymentAccountsProvider>(context);
final paymentMethods = paymentAccountsProvider.paymentMethods ?? []; final paymentMethods = widget.accountType == 'FIAT'
? paymentAccountsProvider.paymentMethods
: paymentAccountsProvider.cryptoCurrencyPaymentMethods;
return Padding( return Padding(
padding: MediaQuery.of(context).viewInsets, padding: MediaQuery.of(context).viewInsets,
@ -34,10 +40,10 @@ class _PaymentMethodSelectionFormState
labelText: 'Payment Method', labelText: 'Payment Method',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
items: paymentMethods.map((method) { items: paymentMethods?.map((method) {
return DropdownMenuItem<String>( return DropdownMenuItem<String>(
value: method.id, value: method.id,
child: Text(method.id), child: Text(getPaymentMethodLabel(method.id)),
); );
}).toList(), }).toList(),
onChanged: (value) { onChanged: (value) {
@ -56,7 +62,7 @@ class _PaymentMethodSelectionFormState
builder: (BuildContext context) { builder: (BuildContext context) {
return DynamicPaymentAccountForm( return DynamicPaymentAccountForm(
paymentAccountForm: form, paymentAccountForm: form,
paymentMethodLabel: value, paymentMethodLabel: getPaymentMethodLabel(value),
paymentMethodId: value); paymentMethodId: value);
}, },
); );

View file

@ -8,10 +8,12 @@ import 'dart:convert'; // Import the dart:convert library
class NewTradeOfferForm extends StatefulWidget { class NewTradeOfferForm extends StatefulWidget {
final GlobalKey<FormState> formKey; final GlobalKey<FormState> formKey;
final List<PaymentAccount> paymentAccounts; final List<PaymentAccount> paymentAccounts;
final String direction; // New argument for direction ('BUY' or 'SELL')
const NewTradeOfferForm({ const NewTradeOfferForm({
required this.formKey, required this.formKey,
required this.paymentAccounts, required this.paymentAccounts,
required this.direction,
}); });
@override @override
@ -22,6 +24,7 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
PaymentAccount? _selectedPaymentAccount; PaymentAccount? _selectedPaymentAccount;
TradeCurrency? _selectedTradeCurrency; TradeCurrency? _selectedTradeCurrency;
int _selectedPricingTypeIndex = 0; // 0 for Fixed, 1 for Dynamic int _selectedPricingTypeIndex = 0; // 0 for Fixed, 1 for Dynamic
bool _reserveExactAmount = false;
final TextEditingController _priceController = TextEditingController(); final TextEditingController _priceController = TextEditingController();
final TextEditingController _depositController = final TextEditingController _depositController =
@ -34,6 +37,8 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final isBuy = widget.direction == 'BUY';
return Padding( return Padding(
padding: MediaQuery.of(context).viewInsets, padding: MediaQuery.of(context).viewInsets,
child: Container( child: Container(
@ -44,7 +49,10 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
child: Column( child: Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: <Widget>[ children: <Widget>[
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), const SizedBox(height: 16.0),
ToggleButtons( ToggleButtons(
isSelected: [ isSelected: [
@ -70,7 +78,7 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
DropdownButtonFormField<PaymentAccount>( DropdownButtonFormField<PaymentAccount>(
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Payment Account', labelText: 'Your Sender Account',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
value: _selectedPaymentAccount, value: _selectedPaymentAccount,
@ -138,9 +146,11 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
if (_selectedPricingTypeIndex == 1) if (_selectedPricingTypeIndex == 1)
TextFormField( TextFormField(
controller: _marginController, controller: _marginController,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Market Price Below Margin (%)', labelText: isBuy
border: OutlineInputBorder(), ? 'Market Price Below Margin (%)'
: 'Market Price Above Margin (%)',
border: const OutlineInputBorder(),
), ),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
@ -156,13 +166,13 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
const SizedBox(height: 16.0), const SizedBox(height: 16.0),
TextFormField( TextFormField(
controller: _amountController, controller: _amountController,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Amount of XMR', labelText: 'Amount of XMR to ${isBuy ? 'Buy' : 'Sell'}',
border: OutlineInputBorder(), border: const OutlineInputBorder(),
), ),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { 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; return null;
}, },
@ -171,7 +181,7 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
TextFormField( TextFormField(
controller: _minAmountController, controller: _minAmountController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Minimum Transaction Amount (Of XMR)', labelText: 'Minimum Transaction Amount (XMR)',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
validator: (value) { validator: (value) {
@ -185,12 +195,12 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
TextFormField( TextFormField(
controller: _depositController, controller: _depositController,
decoration: const InputDecoration( decoration: const InputDecoration(
labelText: 'Buyer Security Deposit (%)', labelText: 'Mutual Security Deposit (%)',
border: OutlineInputBorder(), border: OutlineInputBorder(),
), ),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
return 'Please enter the buyer security deposit'; return 'Please enter the mutual security deposit';
} }
final deposit = double.tryParse(value); final deposit = double.tryParse(value);
if (deposit == null || deposit < 0 || deposit > 50) { if (deposit == null || deposit < 0 || deposit > 50) {
@ -204,9 +214,10 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
if (_selectedPricingTypeIndex == 1) if (_selectedPricingTypeIndex == 1)
TextFormField( TextFormField(
controller: _triggerPriceController, controller: _triggerPriceController,
decoration: const InputDecoration( decoration: InputDecoration(
labelText: 'Stop Trigger Price', labelText:
border: OutlineInputBorder(), 'Delist If Market Price Goes Above (${_selectedTradeCurrency?.code ?? ''})',
border: const OutlineInputBorder(),
), ),
validator: (value) { validator: (value) {
if (value == null || value.isEmpty) { if (value == null || value.isEmpty) {
@ -216,13 +227,43 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
}, },
), ),
const SizedBox(height: 16.0), 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( ElevatedButton(
onPressed: () async { onPressed: () async {
if (widget.formKey.currentState?.validate() ?? false) { if (widget.formKey.currentState?.validate() ?? false) {
// Prepare the data to be sent // Prepare the data to be sent
final offerData = { final offerData = {
'currencyCode': _selectedTradeCurrency?.code ?? '', 'currencyCode': _selectedTradeCurrency?.code ?? '',
'direction': 'BUY', 'direction': widget.direction,
'price': _selectedPricingTypeIndex == 0 'price': _selectedPricingTypeIndex == 0
? _priceController.text ? _priceController.text
: '', : '',
@ -248,7 +289,7 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
'triggerPrice': _selectedPricingTypeIndex == 1 'triggerPrice': _selectedPricingTypeIndex == 1
? _triggerPriceController.text ? _triggerPriceController.text
: '', : '',
'reserveExactAmount': true, 'reserveExactAmount': _reserveExactAmount,
'paymentAccountId': _selectedPaymentAccount?.id ?? '', 'paymentAccountId': _selectedPaymentAccount?.id ?? '',
}; };
@ -261,7 +302,7 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
Provider.of<OffersProvider>(context, listen: false); Provider.of<OffersProvider>(context, listen: false);
offersProvider.postOffer( offersProvider.postOffer(
currencyCode: _selectedTradeCurrency?.code ?? '', currencyCode: _selectedTradeCurrency?.code ?? '',
direction: 'BUY', direction: widget.direction,
price: _selectedPricingTypeIndex == 0 price: _selectedPricingTypeIndex == 0
? _priceController.text ? _priceController.text
: '', // Use the price from the controller : '', // Use the price from the controller
@ -284,7 +325,7 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
triggerPrice: _selectedPricingTypeIndex == 1 triggerPrice: _selectedPricingTypeIndex == 1
? _triggerPriceController.text ? _triggerPriceController.text
: '', // Use the trigger price from the controller : '', // Use the trigger price from the controller
reserveExactAmount: false, reserveExactAmount: _reserveExactAmount,
paymentAccountId: _selectedPaymentAccount?.id ?? '', paymentAccountId: _selectedPaymentAccount?.id ?? '',
); );
Navigator.pop(context); Navigator.pop(context);
@ -307,3 +348,4 @@ class __NewTradeOfferFormState extends State<NewTradeOfferForm> {
); );
} }
} }

View file

@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:haveno_flutter_app/utils/payment_utils.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:haveno_flutter_app/proto/compiled/grpc.pbgrpc.dart'; import 'package:haveno_flutter_app/proto/compiled/grpc.pbgrpc.dart';
import 'package:haveno_flutter_app/screens/my_offer_detail_screen.dart'; import 'package:haveno_flutter_app/screens/my_offer_detail_screen.dart';
@ -43,8 +44,12 @@ class OfferCard extends StatelessWidget {
} }
}, },
child: Card( 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, color: Theme.of(context).cardTheme.color,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.zero, // No border radius
),
elevation: 0, // No elevation
child: Padding( child: Padding(
padding: const EdgeInsets.all(16.0), padding: const EdgeInsets.all(16.0),
child: Column( child: Column(
@ -104,7 +109,7 @@ class OfferCard extends StatelessWidget {
const SizedBox(height: 10), const SizedBox(height: 10),
Center( Center(
child: Text( child: Text(
_isFiatCurrency(offer.counterCurrencyCode) isFiatCurrency(offer.counterCurrencyCode)
? '${double.parse(offer.price).toStringAsFixed(2)} ${offer.counterCurrencyCode}' ? '${double.parse(offer.price).toStringAsFixed(2)} ${offer.counterCurrencyCode}'
: '${offer.price}', : '${offer.price}',
style: const TextStyle( 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);
}
} }

View file

@ -25,6 +25,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.11.0" 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: boolean_selector:
dependency: transitive dependency: transitive
description: description:
@ -541,13 +549,6 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.0.0" version: "2.0.0"
tor:
dependency: "direct main"
description:
path: "../flutter_plugins/tor"
relative: true
source: path
version: "0.0.1"
typed_data: typed_data:
dependency: transitive dependency: transitive
description: description:

View file

@ -29,11 +29,12 @@ dependencies:
font_awesome_flutter: ^10.7.0 font_awesome_flutter: ^10.7.0
timeline_tile: ^2.0.0 timeline_tile: ^2.0.0
# Add your plugin dependency here # Add your plugin dependency here
tor: ##tor:
path: ../flutter_plugins/tor ## path: ../flutter_plugins/tor
protoc_plugin: ^21.1.2 protoc_plugin: ^21.1.2
fixnum: ^1.1.0 fixnum: ^1.1.0
intl: ^0.17.0 intl: ^0.17.0
badges: ^3.1.2
dev_dependencies: dev_dependencies:
flutter_test: flutter_test: