From 3bd61ae1eec7aa034307e222bb504a5abaad72b9 Mon Sep 17 00:00:00 2001 From: Nathan Woodburn Date: Tue, 4 Jun 2024 15:20:39 +1000 Subject: [PATCH] feat: Added receive and send pages --- lib/domains.dart | 10 ++- lib/main.dart | 10 +++ lib/receive.dart | 104 +++++++++++++++++++++++ lib/send.dart | 189 ++++++++++++++++++++++++++++++++++++++++++ lib/transactions.dart | 24 +++++- pubspec.lock | 16 ++++ pubspec.yaml | 1 + 7 files changed, 348 insertions(+), 6 deletions(-) create mode 100644 lib/receive.dart create mode 100644 lib/send.dart diff --git a/lib/domains.dart b/lib/domains.dart index 5acde3c..1b049d3 100644 --- a/lib/domains.dart +++ b/lib/domains.dart @@ -72,7 +72,14 @@ class _DomainsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('${widget.wallet} Domains'), + title: Text('${widget.wallet} - Domains'), + // Add refresh button to the app bar + actions: [ + IconButton( + icon: Icon(Icons.refresh), + onPressed: fetchDomains, + ), + ], ), // Get wallet list from api and display here body: Center( @@ -89,7 +96,6 @@ class _DomainsPageState extends State { ), ); }), - TextButton(onPressed: fetchDomains, child: Text('Refresh')), ], ); }, diff --git a/lib/main.dart b/lib/main.dart index 6d4c0ea..e120c5d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -5,6 +5,8 @@ import 'dart:convert'; import 'package:firewallet/home.dart'; import 'package:firewallet/transactions.dart'; import 'package:firewallet/domains.dart'; +import 'package:firewallet/receive.dart'; +import 'package:firewallet/send.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -96,6 +98,12 @@ class _MyHomePageState extends State { icon: Icon(Icons.home_outlined), label: 'Home', ), + NavigationDestination( + icon: Icon(Icons.call_received), label: 'Receive'), + NavigationDestination( + icon: Icon(Icons.send), + label: 'Send', + ), NavigationDestination( icon: Icon(Icons.text_format_rounded), label: 'Domains', @@ -111,6 +119,8 @@ class _MyHomePageState extends State { index: currentPageIndex, children: [ IndexPage(uuid: widget.uuid, wallet: wallet, setWallet: setWallet), + ReceivePage(uuid: widget.uuid, wallet: wallet), + SendPage(uuid: widget.uuid, wallet: wallet), DomainsPage(uuid: widget.uuid, wallet: wallet), TransactionsPage(uuid: widget.uuid, wallet: wallet), ], diff --git a/lib/receive.dart b/lib/receive.dart new file mode 100644 index 0000000..bb4a565 --- /dev/null +++ b/lib/receive.dart @@ -0,0 +1,104 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:qr_flutter/qr_flutter.dart'; + +class ReceivePage extends StatefulWidget { + ReceivePage({Key? key, required this.uuid, required this.wallet}) + : super(key: key); + + final String uuid; + String wallet; + + @override + _ReceivePageState createState() => _ReceivePageState(); +} + +class _ReceivePageState extends State { + @override + void initState() { + super.initState(); + fetchReceive(); + } + + String address = ''; + + Future fetchReceive() async { + // Fetch domains from api + final response = await http.get(Uri.parse( + 'https://api.firewallet.au/wallet/address?uuid=${widget.uuid}&name=${widget.wallet}')); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + if (data is Map && data.containsKey('error')) { + print('Error: ${data['error']}'); + return; + } + // Return if empty + if (data.isEmpty) { + setState(() { + address = 'No address found'; + }); + + return; + } + final addressData = data as Map; + setState(() { + address = addressData['address']; + }); + } else { + // Handle error + print('Failed to load wallet names'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('${widget.wallet} - Receive HNS or Domains'), + ), + // Get wallet list from api and display here + body: Center( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return ListView( + children: [ + Center(child: Text(address, style: TextStyle(fontSize: 18.0))), + // QR Code + Center( + child: QrImageView( + data: address, + version: QrVersions.auto, + // Width = 90% of the screen width + size: constraints.maxWidth * 0.9, + eyeStyle: QrEyeStyle( + eyeShape: QrEyeShape.square, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + ), + dataModuleStyle: QrDataModuleStyle( + dataModuleShape: QrDataModuleShape.square, + color: Theme.of(context).brightness == Brightness.light + ? Colors.black + : Colors.white, + ), + ), + ), + + TextButton(onPressed: copyAddress, child: Text('Copy Address')), + TextButton(onPressed: fetchReceive, child: Text('Refresh')), + ], + ); + }, + ), + ), + ); + } + + Future copyAddress() async { + // Copy the address to the clipboard + await Clipboard.setData(ClipboardData(text: address)); + } +} diff --git a/lib/send.dart b/lib/send.dart new file mode 100644 index 0000000..48f9263 --- /dev/null +++ b/lib/send.dart @@ -0,0 +1,189 @@ +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:http/http.dart' as http; +import 'dart:convert'; +import 'package:qr_flutter/qr_flutter.dart'; + +class SendPage extends StatefulWidget { + SendPage({Key? key, required this.uuid, required this.wallet}) + : super(key: key); + + final String uuid; + String wallet; + + @override + _SendPageState createState() => _SendPageState(); +} + +class _SendPageState extends State { + late double walletBalance = 0; + bool balanceLoaded = false; + final TextEditingController addressController = TextEditingController(); + final TextEditingController amountController = TextEditingController(); + + @override + void initState() { + super.initState(); + } + + @override + void dispose() { + // Dispose of the controllers when the widget is disposed + addressController.dispose(); + amountController.dispose(); + super.dispose(); + } + + Future fetchWalletBalance() async { + if (widget.wallet.isEmpty) { + walletBalance = 0; + return; + } + + if (balanceLoaded) { + return; + } + + final response = await http.get(Uri.parse( + 'https://api.firewallet.au/wallet/balance?uuid=${widget.uuid}&name=${widget.wallet}')); + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + print(data); + + setState(() { + walletBalance = data['available']; + }); + } else { + // Handle error + print('Failed to load wallet balance'); + } + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text('${widget.wallet} - Send HNS'), + ), + // Get wallet list from api and display here + body: Center( + child: LayoutBuilder( + builder: (BuildContext context, BoxConstraints constraints) { + return ListView( + padding: const EdgeInsets.all(16), + children: [ + // Display the wallet balance + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + 'Max ${(walletBalance - 0.02).toStringAsFixed(2)} HNS'), + TextButton( + onPressed: fetchWalletBalance, + child: const Icon(Icons.refresh)), + ], + ), + // Address input field + TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Address', + ), + keyboardType: TextInputType.text, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')), + ], + controller: addressController, + ), + SizedBox(height: 16), + // Amount input field + TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: 'Amount', + suffix: Text('HNS'), + ), + keyboardType: TextInputType.number, + inputFormatters: [ + FilteringTextInputFormatter.allow(RegExp(r'[0-9.]')), + ], + controller: amountController, + ), + TextButton( + onPressed: () => amountController.text = + (walletBalance - 0.02).toStringAsFixed(2), + child: const Text('Max')), + SizedBox(height: 16), + // Send button + ElevatedButton( + onPressed: sendHNS, + child: const Text('Send'), + ), + ], + ); + }, + ), + ), + ); + } + + void sendHNS() { + // Send HNS to the address + String toAddress = addressController.text; + double amount = double.parse(amountController.text); + print('Send $amount HNS to $toAddress'); + if (amount > (walletBalance - 0.01)) { + // Not enough balance + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Not enough balance'), + ), + ); + return; + } + bool sendMax = false; + if (amount >= (walletBalance - 0.02)) { + sendMax = true; + } + + print(sendMax); + // https://api.firewallet.au/wallet/send?uuid=99879737-0c27-497b-b357-f75720feb32e&name=hot&address=hs1qca9n20ew7ph6l5galfnftmwme6kwmu26mzjgtx&amount=2 + http + .post( + Uri.parse( + 'https://api.firewallet.au/wallet/send?uuid=${widget.uuid}&name=${widget.wallet}&address=$toAddress&amount=$amount'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + }, + body: '{}', + ) + .then((response) { + if (response.statusCode == 200) { + final data = jsonDecode(response.body); + print(data); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Sent $amount HNS to $toAddress'), + ), + ); + if (sendMax) { + amountController.text = ''; + } else { + amountController.text = + min(amount, walletBalance - 0.02).toStringAsFixed(2); + } + fetchWalletBalance(); + } else { + // Handle error + print('Failed to send HNS'); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Failed to send HNS'), + ), + ); + } + }); + } +} diff --git a/lib/transactions.dart b/lib/transactions.dart index b07fbc6..ed0a4f8 100644 --- a/lib/transactions.dart +++ b/lib/transactions.dart @@ -105,7 +105,14 @@ class _TransactionsPageState extends State { Widget build(BuildContext context) { return Scaffold( appBar: AppBar( - title: Text('${widget.wallet} Transactions'), + title: Text('${widget.wallet} - Transactions'), + // Add refresh button to the app bar + actions: [ + IconButton( + icon: Icon(Icons.refresh), + onPressed: fetchTransactions, + ), + ], ), // Get wallet list from api and display here body: Center( @@ -113,8 +120,6 @@ class _TransactionsPageState extends State { builder: (BuildContext context, BoxConstraints constraints) { return ListView( children: [ - TextButton( - onPressed: fetchTransactions, child: Text('Refresh')), ...transactions.map((tx) { return Card( child: ListTile( @@ -184,6 +189,7 @@ class Transaction { int transfers = 0; int finalizes = 0; int renews = 0; + int revokes = 0; int other = 0; outputs.forEach((output) { @@ -205,6 +211,8 @@ class Transaction { finalizes++; } else if (output.covenant.action == 'RENEW') { renews++; + } else if (output.covenant.action == 'REVOKE') { + revokes++; } else if (output.covenant.action == 'NONE') { other++; } else { @@ -244,6 +252,9 @@ class Transaction { if (renews > 0) { outputString += 'Renews: $renews, '; } + if (revokes > 0) { + outputString += 'Revokes: $revokes, '; + } // Remove trailing comma if (outputString.isNotEmpty) { @@ -251,7 +262,12 @@ class Transaction { } if (outputString.isEmpty) { - outputString = 'Sent HNS'; + // Check if sent or received + if (value() > 0) { + outputString = 'Received HNS'; + } else { + outputString = 'Sent HNS'; + } } return outputString; diff --git a/pubspec.lock b/pubspec.lock index 3c83b1f..69a428d 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -288,6 +288,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.8" + qr: + dependency: transitive + description: + name: qr + sha256: "64957a3930367bf97cc211a5af99551d630f2f4625e38af10edd6b19131b64b3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + qr_flutter: + dependency: "direct main" + description: + name: qr_flutter + sha256: "5095f0fc6e3f71d08adef8feccc8cea4f12eec18a2e31c2e8d82cb6019f4b097" + url: "https://pub.dev" + source: hosted + version: "4.1.0" shared_preferences: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 04fdc36..40aecbe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -38,6 +38,7 @@ dependencies: shared_preferences: ^2.2.3 http: ^1.2.1 url_launcher: ^6.2.6 + qr_flutter: ^4.1.0 dev_dependencies: flutter_test: