diff --git a/assets/svg/about-desktop.svg b/assets/svg/about-desktop.svg
new file mode 100644
index 000000000..a80067d9c
--- /dev/null
+++ b/assets/svg/about-desktop.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/svg/address-book-desktop.svg b/assets/svg/address-book-desktop.svg
new file mode 100644
index 000000000..fb85e3e11
--- /dev/null
+++ b/assets/svg/address-book-desktop.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/svg/exchange-desktop.svg b/assets/svg/exchange-desktop.svg
new file mode 100644
index 000000000..8eacfa84e
--- /dev/null
+++ b/assets/svg/exchange-desktop.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/svg/exit-desktop.svg b/assets/svg/exit-desktop.svg
new file mode 100644
index 000000000..abba264cd
--- /dev/null
+++ b/assets/svg/exit-desktop.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/svg/wallet-desktop.svg b/assets/svg/wallet-desktop.svg
new file mode 100644
index 000000000..0b0acdae3
--- /dev/null
+++ b/assets/svg/wallet-desktop.svg
@@ -0,0 +1,3 @@
+
diff --git a/lib/pages_desktop_specific/home/desktop_home_view.dart b/lib/pages_desktop_specific/home/desktop_home_view.dart
index 41f4b5041..6aa104081 100644
--- a/lib/pages_desktop_specific/home/desktop_home_view.dart
+++ b/lib/pages_desktop_specific/home/desktop_home_view.dart
@@ -43,9 +43,6 @@ class _DesktopHomeViewState extends ConsumerState {
Container(
color: Colors.pink,
),
- Container(
- color: Colors.purple,
- ),
];
void onMenuSelectionChanged(int newIndex) {
diff --git a/lib/pages_desktop_specific/home/desktop_menu.dart b/lib/pages_desktop_specific/home/desktop_menu.dart
index b71c20f6e..7409a4156 100644
--- a/lib/pages_desktop_specific/home/desktop_menu.dart
+++ b/lib/pages_desktop_specific/home/desktop_menu.dart
@@ -1,3 +1,5 @@
+import 'dart:io';
+
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
@@ -70,136 +72,197 @@ class _DesktopMenuState extends ConsumerState {
const SizedBox(
height: 60,
),
- SizedBox(
- width: _width == expandedWidth
- ? _width - 32 // 16 padding on either side
- : _width - 16, // 8 padding on either side
- child: Column(
- crossAxisAlignment: CrossAxisAlignment.stretch,
- children: [
- DesktopMenuItem(
- icon: SvgPicture.asset(
- Assets.svg.walletFa,
- width: 20,
- height: 20,
+ Expanded(
+ child: SizedBox(
+ width: _width == expandedWidth
+ ? _width - 32 // 16 padding on either side
+ : _width - 16, // 8 padding on either side
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ DesktopMenuItem(
+ icon: SvgPicture.asset(
+ Assets.svg.walletDesktop,
+ width: 20,
+ height: 20,
+ color: 0 == selectedMenuItem
+ ? Theme.of(context)
+ .extension()!
+ .textDark
+ : Theme.of(context)
+ .extension()!
+ .textDark
+ .withOpacity(0.8),
+ ),
+ label: "My Stack",
+ value: 0,
+ group: selectedMenuItem,
+ onChanged: updateSelectedMenuItem,
+ iconOnly: _width == minimizedWidth,
),
- label: "My Stack",
- value: 0,
- group: selectedMenuItem,
- onChanged: updateSelectedMenuItem,
- iconOnly: _width == minimizedWidth,
- ),
- const SizedBox(
- height: 2,
- ),
- DesktopMenuItem(
- icon: SvgPicture.asset(
- Assets.svg.exchange3,
- width: 20,
- height: 20,
+ const SizedBox(
+ height: 2,
),
- label: "Exchange",
- value: 1,
- group: selectedMenuItem,
- onChanged: updateSelectedMenuItem,
- iconOnly: _width == minimizedWidth,
- ),
- const SizedBox(
- height: 2,
- ),
- DesktopMenuItem(
- icon: SvgPicture.asset(
- Assets.svg.bell,
- width: 20,
- height: 20,
+ DesktopMenuItem(
+ icon: SvgPicture.asset(
+ Assets.svg.exchangeDesktop,
+ width: 20,
+ height: 20,
+ color: 1 == selectedMenuItem
+ ? Theme.of(context)
+ .extension()!
+ .textDark
+ : Theme.of(context)
+ .extension()!
+ .textDark
+ .withOpacity(0.8),
+ ),
+ label: "Exchange",
+ value: 1,
+ group: selectedMenuItem,
+ onChanged: updateSelectedMenuItem,
+ iconOnly: _width == minimizedWidth,
),
- label: "Notifications",
- value: 2,
- group: selectedMenuItem,
- onChanged: updateSelectedMenuItem,
- iconOnly: _width == minimizedWidth,
- ),
- const SizedBox(
- height: 2,
- ),
- DesktopMenuItem(
- icon: SvgPicture.asset(
- Assets.svg.addressBook2,
- width: 20,
- height: 20,
+ const SizedBox(
+ height: 2,
),
- label: "Address Book",
- value: 3,
- group: selectedMenuItem,
- onChanged: updateSelectedMenuItem,
- iconOnly: _width == minimizedWidth,
- ),
- const SizedBox(
- height: 2,
- ),
- DesktopMenuItem(
- icon: SvgPicture.asset(
- Assets.svg.gear,
- width: 20,
- height: 20,
+ DesktopMenuItem(
+ icon: SvgPicture.asset(
+ Assets.svg.bell,
+ width: 20,
+ height: 20,
+ color: 2 == selectedMenuItem
+ ? Theme.of(context)
+ .extension()!
+ .textDark
+ : Theme.of(context)
+ .extension()!
+ .textDark
+ .withOpacity(0.8),
+ ),
+ label: "Notifications",
+ value: 2,
+ group: selectedMenuItem,
+ onChanged: updateSelectedMenuItem,
+ iconOnly: _width == minimizedWidth,
),
- label: "Settings",
- value: 4,
- group: selectedMenuItem,
- onChanged: updateSelectedMenuItem,
- iconOnly: _width == minimizedWidth,
- ),
- const SizedBox(
- height: 2,
- ),
- DesktopMenuItem(
- icon: SvgPicture.asset(
- Assets.svg.messageQuestion,
- width: 20,
- height: 20,
+ const SizedBox(
+ height: 2,
),
- label: "Support",
- value: 5,
- group: selectedMenuItem,
- onChanged: updateSelectedMenuItem,
- iconOnly: _width == minimizedWidth,
- ),
- const SizedBox(
- height: 2,
- ),
- DesktopMenuItem(
- icon: SvgPicture.asset(
- Assets.svg.messageQuestion,
- width: 20,
- height: 20,
+ DesktopMenuItem(
+ icon: SvgPicture.asset(
+ Assets.svg.addressBookDesktop,
+ width: 20,
+ height: 20,
+ color: 3 == selectedMenuItem
+ ? Theme.of(context)
+ .extension()!
+ .textDark
+ : Theme.of(context)
+ .extension()!
+ .textDark
+ .withOpacity(0.8),
+ ),
+ label: "Address Book",
+ value: 3,
+ group: selectedMenuItem,
+ onChanged: updateSelectedMenuItem,
+ iconOnly: _width == minimizedWidth,
),
- label: "About",
- value: 6,
- group: selectedMenuItem,
- onChanged: updateSelectedMenuItem,
- iconOnly: _width == minimizedWidth,
- ),
- const SizedBox(
- height: 2,
- ),
- DesktopMenuItem(
- icon: SvgPicture.asset(
- Assets.svg.messageQuestion,
- width: 20,
- height: 20,
+ const SizedBox(
+ height: 2,
),
- label: "Exit",
- value: 7,
- group: selectedMenuItem,
- onChanged: updateSelectedMenuItem,
- iconOnly: _width == minimizedWidth,
- ),
- ],
+ DesktopMenuItem(
+ icon: SvgPicture.asset(
+ Assets.svg.gear,
+ width: 20,
+ height: 20,
+ color: 4 == selectedMenuItem
+ ? Theme.of(context)
+ .extension()!
+ .textDark
+ : Theme.of(context)
+ .extension()!
+ .textDark
+ .withOpacity(0.8),
+ ),
+ label: "Settings",
+ value: 4,
+ group: selectedMenuItem,
+ onChanged: updateSelectedMenuItem,
+ iconOnly: _width == minimizedWidth,
+ ),
+ const SizedBox(
+ height: 2,
+ ),
+ DesktopMenuItem(
+ icon: SvgPicture.asset(
+ Assets.svg.messageQuestion,
+ width: 20,
+ height: 20,
+ color: 5 == selectedMenuItem
+ ? Theme.of(context)
+ .extension()!
+ .textDark
+ : Theme.of(context)
+ .extension()!
+ .textDark
+ .withOpacity(0.8),
+ ),
+ label: "Support",
+ value: 5,
+ group: selectedMenuItem,
+ onChanged: updateSelectedMenuItem,
+ iconOnly: _width == minimizedWidth,
+ ),
+ const SizedBox(
+ height: 2,
+ ),
+ DesktopMenuItem(
+ icon: SvgPicture.asset(
+ Assets.svg.aboutDesktop,
+ width: 20,
+ height: 20,
+ color: 6 == selectedMenuItem
+ ? Theme.of(context)
+ .extension()!
+ .textDark
+ : Theme.of(context)
+ .extension()!
+ .textDark
+ .withOpacity(0.8),
+ ),
+ label: "About",
+ value: 6,
+ group: selectedMenuItem,
+ onChanged: updateSelectedMenuItem,
+ iconOnly: _width == minimizedWidth,
+ ),
+ const Spacer(),
+ DesktopMenuItem(
+ icon: SvgPicture.asset(
+ Assets.svg.exitDesktop,
+ width: 20,
+ height: 20,
+ color: Theme.of(context)
+ .extension()!
+ .textDark
+ .withOpacity(0.8),
+ ),
+ label: "Exit",
+ value: 7,
+ group: selectedMenuItem,
+ onChanged: (_) {
+ // todo: save stuff/ notify before exit?
+ exit(0);
+ },
+ iconOnly: _width == minimizedWidth,
+ ),
+ ],
+ ),
),
),
- const Spacer(),
Row(
- mainAxisAlignment: MainAxisAlignment.end,
children: [
const Spacer(),
IconButton(
@@ -212,7 +275,7 @@ class _DesktopMenuState extends ConsumerState {
),
),
],
- )
+ ),
],
),
),
diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart
index f8cc7e2dc..8a5c35d8d 100644
--- a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart
+++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/desktop_wallet_view.dart
@@ -1,6 +1,8 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
+import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart';
+import 'package:stackwallet/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
@@ -171,11 +173,14 @@ class _DesktopWalletViewState extends ConsumerState {
),
Expanded(
child: Row(
- children: const [
- Expanded(
- child: MyWallet(),
- ),
+ children: [
SizedBox(
+ width: 450,
+ child: MyWallet(
+ walletId: walletId,
+ ),
+ ),
+ const SizedBox(
width: 16,
),
Expanded(
@@ -192,7 +197,12 @@ class _DesktopWalletViewState extends ConsumerState {
}
class MyWallet extends StatefulWidget {
- const MyWallet({Key? key}) : super(key: key);
+ const MyWallet({
+ Key? key,
+ required this.walletId,
+ }) : super(key: key);
+
+ final String walletId;
@override
State createState() => _MyWalletState();
@@ -246,11 +256,21 @@ class _MyWalletState extends State {
Tab(text: "Receive"),
],
),
- const Expanded(
+ Expanded(
child: TabBarView(
children: [
- DesktopSend(),
- DesktopReceive(),
+ Padding(
+ padding: const EdgeInsets.all(20),
+ child: DesktopSend(
+ walletId: widget.walletId,
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.all(20),
+ child: DesktopReceive(
+ walletId: widget.walletId,
+ ),
+ ),
],
),
),
@@ -264,38 +284,6 @@ class _MyWalletState extends State {
}
}
-class DesktopReceive extends StatefulWidget {
- const DesktopReceive({Key? key}) : super(key: key);
-
- @override
- State createState() => _DesktopReceiveState();
-}
-
-class _DesktopReceiveState extends State {
- @override
- Widget build(BuildContext context) {
- return Container(
- color: Colors.green,
- );
- }
-}
-
-class DesktopSend extends StatefulWidget {
- const DesktopSend({Key? key}) : super(key: key);
-
- @override
- State createState() => _DesktopSendState();
-}
-
-class _DesktopSendState extends State {
- @override
- Widget build(BuildContext context) {
- return Container(
- color: Colors.red,
- );
- }
-}
-
class RecentDesktopTransactions extends StatefulWidget {
const RecentDesktopTransactions({Key? key}) : super(key: key);
diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart
new file mode 100644
index 000000000..2efbcd84f
--- /dev/null
+++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/receive/desktop_receive.dart
@@ -0,0 +1,240 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:qr_flutter/qr_flutter.dart';
+import 'package:stackwallet/notifications/show_flush_bar.dart';
+import 'package:stackwallet/pages/receive_view/generate_receiving_uri_qr_code_view.dart';
+import 'package:stackwallet/providers/providers.dart';
+import 'package:stackwallet/route_generator.dart';
+import 'package:stackwallet/utilities/assets.dart';
+import 'package:stackwallet/utilities/clipboard_interface.dart';
+import 'package:stackwallet/utilities/constants.dart';
+import 'package:stackwallet/utilities/enums/coin_enum.dart';
+import 'package:stackwallet/utilities/enums/flush_bar_type.dart';
+import 'package:stackwallet/utilities/text_styles.dart';
+import 'package:stackwallet/utilities/theme/stack_colors.dart';
+import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
+import 'package:stackwallet/widgets/custom_loading_overlay.dart';
+import 'package:stackwallet/widgets/desktop/secondary_button.dart';
+import 'package:stackwallet/widgets/rounded_white_container.dart';
+
+class DesktopReceive extends ConsumerStatefulWidget {
+ const DesktopReceive({
+ Key? key,
+ required this.walletId,
+ this.clipboard = const ClipboardWrapper(),
+ }) : super(key: key);
+
+ final String walletId;
+ final ClipboardInterface clipboard;
+
+ @override
+ ConsumerState createState() => _DesktopReceiveState();
+}
+
+class _DesktopReceiveState extends ConsumerState {
+ late final Coin coin;
+ late final String walletId;
+ late final ClipboardInterface clipboard;
+
+ Future generateNewAddress() async {
+ bool shouldPop = false;
+ unawaited(
+ showDialog(
+ context: context,
+ builder: (_) {
+ return WillPopScope(
+ onWillPop: () async => shouldPop,
+ child: Container(
+ color: Theme.of(context)
+ .extension()!
+ .overlay
+ .withOpacity(0.5),
+ child: const CustomLoadingOverlay(
+ message: "Generating address",
+ eventBus: null,
+ ),
+ ),
+ );
+ },
+ ),
+ );
+
+ await ref
+ .read(walletsChangeNotifierProvider)
+ .getManager(walletId)
+ .generateNewAddress();
+
+ shouldPop = true;
+
+ if (mounted) {
+ Navigator.of(context, rootNavigator: true).pop();
+ }
+ }
+
+ String receivingAddress = "";
+
+ @override
+ void initState() {
+ walletId = widget.walletId;
+ coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin;
+ clipboard = widget.clipboard;
+
+ WidgetsBinding.instance.addPostFrameCallback((timeStamp) async {
+ final address = await ref
+ .read(walletsChangeNotifierProvider)
+ .getManager(walletId)
+ .currentReceivingAddress;
+ setState(() {
+ receivingAddress = address;
+ });
+ });
+
+ super.initState();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ debugPrint("BUILD: $runtimeType");
+
+ ref.listen(
+ ref
+ .read(walletsChangeNotifierProvider)
+ .getManagerProvider(walletId)
+ .select((value) => value.currentReceivingAddress),
+ (previous, next) {
+ if (next is Future) {
+ next.then((value) => setState(() => receivingAddress = value));
+ }
+ });
+
+ return Column(
+ crossAxisAlignment: CrossAxisAlignment.stretch,
+ children: [
+ GestureDetector(
+ onTap: () {
+ clipboard.setData(
+ ClipboardData(text: receivingAddress),
+ );
+ showFloatingFlushBar(
+ type: FlushBarType.info,
+ message: "Copied to clipboard",
+ iconAsset: Assets.svg.copy,
+ context: context,
+ );
+ },
+ child: Container(
+ decoration: BoxDecoration(
+ border: Border.all(
+ color: Theme.of(context).extension()!.background,
+ width: 2,
+ ),
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ ),
+ child: RoundedWhiteContainer(
+ child: Column(
+ children: [
+ Row(
+ children: [
+ Text(
+ "Your ${coin.ticker} address",
+ style: STextStyles.itemSubtitle(context),
+ ),
+ const Spacer(),
+ Row(
+ children: [
+ SvgPicture.asset(
+ Assets.svg.copy,
+ width: 10,
+ height: 10,
+ color: Theme.of(context)
+ .extension()!
+ .infoItemIcons,
+ ),
+ const SizedBox(
+ width: 4,
+ ),
+ Text(
+ "Copy",
+ style: STextStyles.link2(context),
+ ),
+ ],
+ ),
+ ],
+ ),
+ const SizedBox(
+ height: 4,
+ ),
+ Row(
+ children: [
+ Expanded(
+ child: Text(
+ receivingAddress,
+ style: STextStyles.itemSubtitle12(context),
+ ),
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ ),
+ ),
+ if (coin != Coin.epicCash)
+ const SizedBox(
+ height: 20,
+ ),
+ if (coin != Coin.epicCash)
+ SecondaryButton(
+ height: 56,
+ onPressed: generateNewAddress,
+ label: "Generate new address",
+ ),
+ const SizedBox(
+ height: 32,
+ ),
+ Center(
+ child: SizedBox(
+ width: 200,
+ height: 200,
+ child: QrImage(
+ data: "${coin.uriScheme}:$receivingAddress",
+ size: MediaQuery.of(context).size.width / 2,
+ foregroundColor: Theme.of(context)
+ .extension()!
+ .accentColorDark),
+ ),
+ ),
+ const SizedBox(
+ height: 32,
+ ),
+ Center(
+ child: BlueTextButton(
+ text: "Create new QR code",
+ onTap: () async {
+ unawaited(
+ Navigator.of(context).push(
+ RouteGenerator.getRoute(
+ shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
+ builder: (_) => GenerateUriQrCodeView(
+ coin: coin,
+ receivingAddress: receivingAddress,
+ ),
+ settings: const RouteSettings(
+ name: GenerateUriQrCodeView.routeName,
+ ),
+ ),
+ ),
+ );
+ },
+ ),
+ ),
+ ],
+ );
+ }
+}
diff --git a/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart
new file mode 100644
index 000000000..866f1ab56
--- /dev/null
+++ b/lib/pages_desktop_specific/home/my_stack_view/wallet_view/send/desktop_send.dart
@@ -0,0 +1,1172 @@
+import 'dart:async';
+
+import 'package:decimal/decimal.dart';
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:flutter_riverpod/flutter_riverpod.dart';
+import 'package:flutter_svg/flutter_svg.dart';
+import 'package:stackwallet/models/send_view_auto_fill_data.dart';
+import 'package:stackwallet/pages/address_book_views/address_book_view.dart';
+import 'package:stackwallet/pages/send_view/sub_widgets/firo_balance_selection_sheet.dart';
+import 'package:stackwallet/pages/send_view/sub_widgets/transaction_fee_selection_sheet.dart';
+import 'package:stackwallet/providers/providers.dart';
+import 'package:stackwallet/providers/ui/fee_rate_type_state_provider.dart';
+import 'package:stackwallet/providers/ui/preview_tx_button_state_provider.dart';
+import 'package:stackwallet/providers/wallet/public_private_balance_state_provider.dart';
+import 'package:stackwallet/route_generator.dart';
+import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
+import 'package:stackwallet/services/coins/manager.dart';
+import 'package:stackwallet/utilities/address_utils.dart';
+import 'package:stackwallet/utilities/assets.dart';
+import 'package:stackwallet/utilities/barcode_scanner_interface.dart';
+import 'package:stackwallet/utilities/clipboard_interface.dart';
+import 'package:stackwallet/utilities/constants.dart';
+import 'package:stackwallet/utilities/enums/coin_enum.dart';
+import 'package:stackwallet/utilities/enums/fee_rate_type_enum.dart';
+import 'package:stackwallet/utilities/format.dart';
+import 'package:stackwallet/utilities/logger.dart';
+import 'package:stackwallet/utilities/prefs.dart';
+import 'package:stackwallet/utilities/text_styles.dart';
+import 'package:stackwallet/utilities/theme/stack_colors.dart';
+import 'package:stackwallet/utilities/util.dart';
+import 'package:stackwallet/widgets/animated_text.dart';
+import 'package:stackwallet/widgets/custom_buttons/blue_text_button.dart';
+import 'package:stackwallet/widgets/desktop/primary_button.dart';
+import 'package:stackwallet/widgets/icon_widgets/addressbook_icon.dart';
+import 'package:stackwallet/widgets/icon_widgets/clipboard_icon.dart';
+import 'package:stackwallet/widgets/icon_widgets/qrcode_icon.dart';
+import 'package:stackwallet/widgets/icon_widgets/x_icon.dart';
+import 'package:stackwallet/widgets/stack_dialog.dart';
+import 'package:stackwallet/widgets/stack_text_field.dart';
+import 'package:stackwallet/widgets/textfield_icon_button.dart';
+
+import '../../../../../pages/send_view/confirm_transaction_view.dart';
+import '../../../../../pages/send_view/sub_widgets/building_transaction_dialog.dart';
+
+class DesktopSend extends ConsumerStatefulWidget {
+ const DesktopSend({
+ Key? key,
+ required this.walletId,
+ this.autoFillData,
+ this.clipboard = const ClipboardWrapper(),
+ this.barcodeScanner = const BarcodeScannerWrapper(),
+ }) : super(key: key);
+
+ final String walletId;
+ final SendViewAutoFillData? autoFillData;
+ final ClipboardInterface clipboard;
+ final BarcodeScannerInterface barcodeScanner;
+
+ @override
+ ConsumerState createState() => _DesktopSendState();
+}
+
+class _DesktopSendState extends ConsumerState {
+ late final String walletId;
+ late final Coin coin;
+ late final ClipboardInterface clipboard;
+ late final BarcodeScannerInterface scanner;
+
+ late TextEditingController sendToController;
+ late TextEditingController cryptoAmountController;
+ late TextEditingController baseAmountController;
+ late TextEditingController noteController;
+ late TextEditingController feeController;
+
+ late final SendViewAutoFillData? _data;
+
+ final _addressFocusNode = FocusNode();
+ final _noteFocusNode = FocusNode();
+ final _cryptoFocus = FocusNode();
+ final _baseFocus = FocusNode();
+
+ Decimal? _amountToSend;
+ Decimal? _cachedAmountToSend;
+ String? _address;
+
+ String? _privateBalanceString;
+ String? _publicBalanceString;
+
+ bool _addressToggleFlag = false;
+
+ bool _cryptoAmountChangeLock = false;
+ late VoidCallback onCryptoAmountChanged;
+
+ Decimal? _cachedBalance;
+
+ Future previewSend() async {
+ // wait for keyboard to disappear
+ FocusScope.of(context).unfocus();
+ await Future.delayed(
+ const Duration(milliseconds: 100),
+ );
+ final manager =
+ ref.read(walletsChangeNotifierProvider).getManager(walletId);
+
+ // TODO: remove the need for this!!
+ final bool isOwnAddress = await manager.isOwnAddress(_address!);
+ if (isOwnAddress) {
+ await showDialog(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: true,
+ builder: (context) {
+ return StackDialog(
+ title: "Transaction failed",
+ message: "Sending to self is currently disabled",
+ rightButton: TextButton(
+ style: Theme.of(context)
+ .extension()!
+ .getSecondaryEnabledButtonColor(context),
+ child: Text(
+ "Ok",
+ style: STextStyles.button(context).copyWith(
+ color: Theme.of(context)
+ .extension()!
+ .accentColorDark),
+ ),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ );
+ },
+ );
+ return;
+ }
+
+ final amount = Format.decimalAmountToSatoshis(_amountToSend!);
+ int availableBalance;
+ if ((coin == Coin.firo || coin == Coin.firoTestNet)) {
+ if (ref.read(publicPrivateBalanceStateProvider.state).state ==
+ "Private") {
+ availableBalance = Format.decimalAmountToSatoshis(
+ await (manager.wallet as FiroWallet).availablePrivateBalance());
+ } else {
+ availableBalance = Format.decimalAmountToSatoshis(
+ await (manager.wallet as FiroWallet).availablePublicBalance());
+ }
+ } else {
+ availableBalance =
+ Format.decimalAmountToSatoshis(await manager.availableBalance);
+ }
+
+ // confirm send all
+ if (amount == availableBalance) {
+ final bool? shouldSendAll = await showDialog(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: true,
+ builder: (context) {
+ return StackDialog(
+ title: "Confirm send all",
+ message:
+ "You are about to send your entire balance. Would you like to continue?",
+ leftButton: TextButton(
+ style: Theme.of(context)
+ .extension()!
+ .getSecondaryEnabledButtonColor(context),
+ child: Text(
+ "Cancel",
+ style: STextStyles.button(context).copyWith(
+ color: Theme.of(context)
+ .extension()!
+ .accentColorDark),
+ ),
+ onPressed: () {
+ Navigator.of(context).pop(false);
+ },
+ ),
+ rightButton: TextButton(
+ style: Theme.of(context)
+ .extension()!
+ .getPrimaryEnabledButtonColor(context),
+ child: Text(
+ "Yes",
+ style: STextStyles.button(context),
+ ),
+ onPressed: () {
+ Navigator.of(context).pop(true);
+ },
+ ),
+ );
+ },
+ );
+
+ if (shouldSendAll == null || shouldSendAll == false) {
+ // cancel preview
+ return;
+ }
+ }
+
+ try {
+ bool wasCancelled = false;
+
+ unawaited(showDialog(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: false,
+ builder: (context) {
+ return BuildingTransactionDialog(
+ onCancel: () {
+ wasCancelled = true;
+
+ Navigator.of(context).pop();
+ },
+ );
+ },
+ ));
+
+ Map txData;
+
+ if ((coin == Coin.firo || coin == Coin.firoTestNet) &&
+ ref.read(publicPrivateBalanceStateProvider.state).state !=
+ "Private") {
+ txData = await (manager.wallet as FiroWallet).prepareSendPublic(
+ address: _address!,
+ satoshiAmount: amount,
+ args: {"feeRate": ref.read(feeRateTypeStateProvider)},
+ );
+ } else {
+ txData = await manager.prepareSend(
+ address: _address!,
+ satoshiAmount: amount,
+ args: {"feeRate": ref.read(feeRateTypeStateProvider)},
+ );
+ }
+
+ if (!wasCancelled && mounted) {
+ // pop building dialog
+ Navigator.of(context).pop();
+ txData["note"] = noteController.text;
+ txData["address"] = _address;
+
+ unawaited(Navigator.of(context).push(
+ RouteGenerator.getRoute(
+ shouldUseMaterialRoute: RouteGenerator.useMaterialPageRoute,
+ builder: (_) => ConfirmTransactionView(
+ transactionInfo: txData,
+ walletId: walletId,
+ ),
+ settings: const RouteSettings(
+ name: ConfirmTransactionView.routeName,
+ ),
+ ),
+ ));
+ }
+ } catch (e) {
+ if (mounted) {
+ // pop building dialog
+ Navigator.of(context).pop();
+
+ unawaited(showDialog(
+ context: context,
+ useSafeArea: false,
+ barrierDismissible: true,
+ builder: (context) {
+ return StackDialog(
+ title: "Transaction failed",
+ message: e.toString(),
+ rightButton: TextButton(
+ style: Theme.of(context)
+ .extension()!
+ .getSecondaryEnabledButtonColor(context),
+ child: Text(
+ "Ok",
+ style: STextStyles.button(context).copyWith(
+ color: Theme.of(context)
+ .extension()!
+ .accentColorDark),
+ ),
+ onPressed: () {
+ Navigator.of(context).pop();
+ },
+ ),
+ );
+ },
+ ));
+ }
+ }
+ }
+
+ void _cryptoAmountChanged() async {
+ if (!_cryptoAmountChangeLock) {
+ final String cryptoAmount = cryptoAmountController.text;
+ if (cryptoAmount.isNotEmpty &&
+ cryptoAmount != "." &&
+ cryptoAmount != ",") {
+ _amountToSend = cryptoAmount.contains(",")
+ ? Decimal.parse(cryptoAmount.replaceFirst(",", "."))
+ : Decimal.parse(cryptoAmount);
+ if (_cachedAmountToSend != null &&
+ _cachedAmountToSend == _amountToSend) {
+ return;
+ }
+ _cachedAmountToSend = _amountToSend;
+ Logging.instance.log("it changed $_amountToSend $_cachedAmountToSend",
+ level: LogLevel.Info);
+
+ final price =
+ ref.read(priceAnd24hChangeNotifierProvider).getPrice(coin).item1;
+
+ if (price > Decimal.zero) {
+ final String fiatAmountString = Format.localizedStringAsFixed(
+ value: _amountToSend! * price,
+ locale: ref.read(localeServiceChangeNotifierProvider).locale,
+ decimalPlaces: 2,
+ );
+
+ baseAmountController.text = fiatAmountString;
+ }
+ } else {
+ _amountToSend = null;
+ baseAmountController.text = "";
+ }
+
+ _updatePreviewButtonState(_address, _amountToSend);
+ }
+ }
+
+ String? _updateInvalidAddressText(String address, Manager manager) {
+ if (_data != null && _data!.contactLabel == address) {
+ return null;
+ }
+ if (address.isNotEmpty && !manager.validateAddress(address)) {
+ return "Invalid address";
+ }
+ return null;
+ }
+
+ void _updatePreviewButtonState(String? address, Decimal? amount) {
+ final isValidAddress = ref
+ .read(walletsChangeNotifierProvider)
+ .getManager(walletId)
+ .validateAddress(address ?? "");
+ ref.read(previewTxButtonStateProvider.state).state =
+ (isValidAddress && amount != null && amount > Decimal.zero);
+ }
+
+ late Future _calculateFeesFuture;
+
+ Map cachedFees = {};
+ Map cachedFiroPrivateFees = {};
+ Map cachedFiroPublicFees = {};
+
+ Future calculateFees(int amount) async {
+ if (amount <= 0) {
+ return "0";
+ }
+
+ if (coin == Coin.firo || coin == Coin.firoTestNet) {
+ if (ref.read(publicPrivateBalanceStateProvider.state).state ==
+ "Private") {
+ if (cachedFiroPrivateFees[amount] != null) {
+ return cachedFiroPrivateFees[amount]!;
+ }
+ } else {
+ if (cachedFiroPublicFees[amount] != null) {
+ return cachedFiroPublicFees[amount]!;
+ }
+ }
+ } else if (cachedFees[amount] != null) {
+ return cachedFees[amount]!;
+ }
+
+ final manager =
+ ref.read(walletsChangeNotifierProvider).getManager(walletId);
+ final feeObject = await manager.fees;
+
+ late final int feeRate;
+
+ switch (ref.read(feeRateTypeStateProvider.state).state) {
+ case FeeRateType.fast:
+ feeRate = feeObject.fast;
+ break;
+ case FeeRateType.average:
+ feeRate = feeObject.medium;
+ break;
+ case FeeRateType.slow:
+ feeRate = feeObject.slow;
+ break;
+ }
+
+ int fee;
+
+ if (coin == Coin.firo || coin == Coin.firoTestNet) {
+ if (ref.read(publicPrivateBalanceStateProvider.state).state ==
+ "Private") {
+ fee = await manager.estimateFeeFor(amount, feeRate);
+
+ cachedFiroPrivateFees[amount] = Format.satoshisToAmount(fee)
+ .toStringAsFixed(Constants.decimalPlaces);
+
+ return cachedFiroPrivateFees[amount]!;
+ } else {
+ fee = await (manager.wallet as FiroWallet)
+ .estimateFeeForPublic(amount, feeRate);
+
+ cachedFiroPublicFees[amount] = Format.satoshisToAmount(fee)
+ .toStringAsFixed(Constants.decimalPlaces);
+
+ return cachedFiroPublicFees[amount]!;
+ }
+ } else {
+ fee = await manager.estimateFeeFor(amount, feeRate);
+ cachedFees[amount] =
+ Format.satoshisToAmount(fee).toStringAsFixed(Constants.decimalPlaces);
+
+ return cachedFees[amount]!;
+ }
+ }
+
+ Future _firoBalanceFuture(
+ ChangeNotifierProvider provider, String locale) async {
+ final wallet = ref.read(provider).wallet as FiroWallet?;
+
+ if (wallet != null) {
+ Decimal? balance;
+ if (ref.read(publicPrivateBalanceStateProvider.state).state ==
+ "Private") {
+ balance = await wallet.availablePrivateBalance();
+ } else {
+ balance = await wallet.availablePublicBalance();
+ }
+
+ return Format.localizedStringAsFixed(
+ value: balance, locale: locale, decimalPlaces: 8);
+ }
+
+ return null;
+ }
+
+ @override
+ void initState() {
+ ref.refresh(feeSheetSessionCacheProvider);
+
+ _calculateFeesFuture = calculateFees(0);
+ _data = widget.autoFillData;
+ walletId = widget.walletId;
+ coin = ref.read(walletsChangeNotifierProvider).getManager(walletId).coin;
+ clipboard = widget.clipboard;
+ scanner = widget.barcodeScanner;
+
+ sendToController = TextEditingController();
+ cryptoAmountController = TextEditingController();
+ baseAmountController = TextEditingController();
+ noteController = TextEditingController();
+ feeController = TextEditingController();
+
+ onCryptoAmountChanged = _cryptoAmountChanged;
+ cryptoAmountController.addListener(onCryptoAmountChanged);
+
+ if (_data != null) {
+ if (_data!.amount != null) {
+ cryptoAmountController.text = _data!.amount!.toString();
+ }
+ sendToController.text = _data!.contactLabel;
+ _address = _data!.address;
+ _addressToggleFlag = true;
+ }
+
+ _cryptoFocus.addListener(() {
+ if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) {
+ if (_amountToSend == null) {
+ setState(() {
+ _calculateFeesFuture = calculateFees(0);
+ });
+ } else {
+ setState(() {
+ _calculateFeesFuture =
+ calculateFees(Format.decimalAmountToSatoshis(_amountToSend!));
+ });
+ }
+ }
+ });
+
+ _baseFocus.addListener(() {
+ if (!_cryptoFocus.hasFocus && !_baseFocus.hasFocus) {
+ if (_amountToSend == null) {
+ setState(() {
+ _calculateFeesFuture = calculateFees(0);
+ });
+ } else {
+ setState(() {
+ _calculateFeesFuture =
+ calculateFees(Format.decimalAmountToSatoshis(_amountToSend!));
+ });
+ }
+ }
+ });
+
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ cryptoAmountController.removeListener(onCryptoAmountChanged);
+
+ sendToController.dispose();
+ cryptoAmountController.dispose();
+ baseAmountController.dispose();
+ noteController.dispose();
+ feeController.dispose();
+
+ _noteFocusNode.dispose();
+ _addressFocusNode.dispose();
+ _cryptoFocus.dispose();
+ _baseFocus.dispose();
+ super.dispose();
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ debugPrint("BUILD: $runtimeType");
+ final provider = ref.watch(walletsChangeNotifierProvider
+ .select((value) => value.getManagerProvider(walletId)));
+ final String locale = ref.watch(
+ localeServiceChangeNotifierProvider.select((value) => value.locale));
+
+ if (coin == Coin.firo || coin == Coin.firoTestNet) {
+ ref.listen(publicPrivateBalanceStateProvider, (previous, next) {
+ if (_amountToSend == null) {
+ setState(() {
+ _calculateFeesFuture = calculateFees(0);
+ });
+ } else {
+ setState(() {
+ _calculateFeesFuture =
+ calculateFees(Format.decimalAmountToSatoshis(_amountToSend!));
+ });
+ }
+ });
+ }
+
+ return SingleChildScrollView(
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(
+ height: 4,
+ ),
+ if (coin == Coin.firo)
+ Text(
+ "Send from",
+ style: STextStyles.smallMed12(context),
+ textAlign: TextAlign.left,
+ ),
+ if (coin == Coin.firo)
+ const SizedBox(
+ height: 10,
+ ),
+ if (coin == Coin.firo)
+ Stack(
+ children: [
+ TextField(
+ autocorrect: Util.isDesktop ? false : true,
+ enableSuggestions: Util.isDesktop ? false : true,
+ readOnly: true,
+ textInputAction: TextInputAction.none,
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(
+ horizontal: 12,
+ ),
+ child: RawMaterialButton(
+ splashColor:
+ Theme.of(context).extension()!.highlight,
+ shape: RoundedRectangleBorder(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ ),
+ onPressed: () {
+ showModalBottomSheet(
+ backgroundColor: Colors.transparent,
+ context: context,
+ shape: const RoundedRectangleBorder(
+ borderRadius: BorderRadius.vertical(
+ top: Radius.circular(20),
+ ),
+ ),
+ builder: (_) => FiroBalanceSelectionSheet(
+ walletId: walletId,
+ ),
+ );
+ },
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Row(
+ children: [
+ Text(
+ "${ref.watch(publicPrivateBalanceStateProvider.state).state} balance",
+ style: STextStyles.itemSubtitle12(context),
+ ),
+ const SizedBox(
+ width: 10,
+ ),
+ FutureBuilder(
+ future: _firoBalanceFuture(provider, locale),
+ builder:
+ (context, AsyncSnapshot snapshot) {
+ if (snapshot.connectionState ==
+ ConnectionState.done &&
+ snapshot.hasData) {
+ if (ref
+ .read(
+ publicPrivateBalanceStateProvider
+ .state)
+ .state ==
+ "Private") {
+ _privateBalanceString = snapshot.data!;
+ } else {
+ _publicBalanceString = snapshot.data!;
+ }
+ }
+ if (ref
+ .read(
+ publicPrivateBalanceStateProvider
+ .state)
+ .state ==
+ "Private" &&
+ _privateBalanceString != null) {
+ return Text(
+ "$_privateBalanceString ${coin.ticker}",
+ style: STextStyles.itemSubtitle(context),
+ );
+ } else if (ref
+ .read(
+ publicPrivateBalanceStateProvider
+ .state)
+ .state ==
+ "Public" &&
+ _publicBalanceString != null) {
+ return Text(
+ "$_publicBalanceString ${coin.ticker}",
+ style: STextStyles.itemSubtitle(context),
+ );
+ } else {
+ return AnimatedText(
+ stringsToLoopThrough: const [
+ "Loading balance",
+ "Loading balance.",
+ "Loading balance..",
+ "Loading balance...",
+ ],
+ style: STextStyles.itemSubtitle(context),
+ );
+ }
+ },
+ ),
+ ],
+ ),
+ SvgPicture.asset(
+ Assets.svg.chevronDown,
+ width: 8,
+ height: 4,
+ color: Theme.of(context)
+ .extension()!
+ .textSubtitle2,
+ ),
+ ],
+ ),
+ ),
+ )
+ ],
+ ),
+ if (coin == Coin.firo)
+ const SizedBox(
+ height: 20,
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ Text(
+ "Amount",
+ style: STextStyles.smallMed12(context),
+ textAlign: TextAlign.left,
+ ),
+ BlueTextButton(
+ text: "Send all ${coin.ticker}",
+ onTap: () async {
+ if (coin == Coin.firo || coin == Coin.firoTestNet) {
+ final firoWallet = ref.read(provider).wallet as FiroWallet;
+ if (ref
+ .read(publicPrivateBalanceStateProvider.state)
+ .state ==
+ "Private") {
+ cryptoAmountController.text =
+ (await firoWallet.availablePrivateBalance())
+ .toStringAsFixed(Constants.decimalPlaces);
+ } else {
+ cryptoAmountController.text =
+ (await firoWallet.availablePublicBalance())
+ .toStringAsFixed(Constants.decimalPlaces);
+ }
+ } else {
+ cryptoAmountController.text =
+ (await ref.read(provider).availableBalance)
+ .toStringAsFixed(Constants.decimalPlaces);
+ }
+ },
+ ),
+ ],
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+ TextField(
+ autocorrect: Util.isDesktop ? false : true,
+ enableSuggestions: Util.isDesktop ? false : true,
+ style: STextStyles.smallMed14(context).copyWith(
+ color: Theme.of(context).extension()!.textDark,
+ ),
+ key: const Key("amountInputFieldCryptoTextFieldKey"),
+ controller: cryptoAmountController,
+ focusNode: _cryptoFocus,
+ keyboardType: const TextInputType.numberWithOptions(
+ signed: false,
+ decimal: true,
+ ),
+ textAlign: TextAlign.right,
+ inputFormatters: [
+ // regex to validate a crypto amount with 8 decimal places
+ TextInputFormatter.withFunction((oldValue, newValue) =>
+ RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$')
+ .hasMatch(newValue.text)
+ ? newValue
+ : oldValue),
+ ],
+ decoration: InputDecoration(
+ contentPadding: const EdgeInsets.only(
+ top: 12,
+ right: 12,
+ ),
+ hintText: "0",
+ hintStyle: STextStyles.fieldLabel(context).copyWith(
+ fontSize: 14,
+ ),
+ prefixIcon: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: Text(
+ coin.ticker,
+ style: STextStyles.smallMed14(context).copyWith(
+ color: Theme.of(context)
+ .extension()!
+ .accentColorDark),
+ ),
+ ),
+ ),
+ ),
+ ),
+ if (Prefs.instance.externalCalls)
+ const SizedBox(
+ height: 10,
+ ),
+ if (Prefs.instance.externalCalls)
+ TextField(
+ autocorrect: Util.isDesktop ? false : true,
+ enableSuggestions: Util.isDesktop ? false : true,
+ style: STextStyles.smallMed14(context).copyWith(
+ color: Theme.of(context).extension()!.textDark,
+ ),
+ key: const Key("amountInputFieldFiatTextFieldKey"),
+ controller: baseAmountController,
+ focusNode: _baseFocus,
+ keyboardType: const TextInputType.numberWithOptions(
+ signed: false,
+ decimal: true,
+ ),
+ textAlign: TextAlign.right,
+ inputFormatters: [
+ // regex to validate a fiat amount with 2 decimal places
+ TextInputFormatter.withFunction((oldValue, newValue) =>
+ RegExp(r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$')
+ .hasMatch(newValue.text)
+ ? newValue
+ : oldValue),
+ ],
+ onChanged: (baseAmountString) {
+ if (baseAmountString.isNotEmpty &&
+ baseAmountString != "." &&
+ baseAmountString != ",") {
+ final baseAmount = baseAmountString.contains(",")
+ ? Decimal.parse(baseAmountString.replaceFirst(",", "."))
+ : Decimal.parse(baseAmountString);
+
+ var _price = ref
+ .read(priceAnd24hChangeNotifierProvider)
+ .getPrice(coin)
+ .item1;
+
+ if (_price == Decimal.zero) {
+ _amountToSend = Decimal.zero;
+ } else {
+ _amountToSend = baseAmount <= Decimal.zero
+ ? Decimal.zero
+ : (baseAmount / _price).toDecimal(
+ scaleOnInfinitePrecision: Constants.decimalPlaces);
+ }
+ if (_cachedAmountToSend != null &&
+ _cachedAmountToSend == _amountToSend) {
+ return;
+ }
+ _cachedAmountToSend = _amountToSend;
+ Logging.instance.log(
+ "it changed $_amountToSend $_cachedAmountToSend",
+ level: LogLevel.Info);
+
+ final amountString = Format.localizedStringAsFixed(
+ value: _amountToSend!,
+ locale:
+ ref.read(localeServiceChangeNotifierProvider).locale,
+ decimalPlaces: Constants.decimalPlaces,
+ );
+
+ _cryptoAmountChangeLock = true;
+ cryptoAmountController.text = amountString;
+ _cryptoAmountChangeLock = false;
+ } else {
+ _amountToSend = Decimal.zero;
+ _cryptoAmountChangeLock = true;
+ cryptoAmountController.text = "";
+ _cryptoAmountChangeLock = false;
+ }
+ // setState(() {
+ // _calculateFeesFuture = calculateFees(
+ // Format.decimalAmountToSatoshis(
+ // _amountToSend!));
+ // });
+ _updatePreviewButtonState(_address, _amountToSend);
+ },
+ decoration: InputDecoration(
+ contentPadding: const EdgeInsets.only(
+ top: 12,
+ right: 12,
+ ),
+ hintText: "0",
+ hintStyle: STextStyles.fieldLabel(context).copyWith(
+ fontSize: 14,
+ ),
+ prefixIcon: FittedBox(
+ fit: BoxFit.scaleDown,
+ child: Padding(
+ padding: const EdgeInsets.all(12),
+ child: Text(
+ ref.watch(prefsChangeNotifierProvider
+ .select((value) => value.currency)),
+ style: STextStyles.smallMed14(context).copyWith(
+ color: Theme.of(context)
+ .extension()!
+ .accentColorDark),
+ ),
+ ),
+ ),
+ ),
+ ),
+ const SizedBox(
+ height: 20,
+ ),
+ Text(
+ "Send to",
+ style: STextStyles.smallMed12(context),
+ textAlign: TextAlign.left,
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: TextField(
+ key: const Key("sendViewAddressFieldKey"),
+ controller: sendToController,
+ readOnly: false,
+ autocorrect: false,
+ enableSuggestions: false,
+ // inputFormatters: [
+ // FilteringTextInputFormatter.allow(
+ // RegExp("[a-zA-Z0-9]{34}")),
+ // ],
+ toolbarOptions: const ToolbarOptions(
+ copy: false,
+ cut: false,
+ paste: true,
+ selectAll: false,
+ ),
+ onChanged: (newValue) {
+ _address = newValue;
+ _updatePreviewButtonState(_address, _amountToSend);
+
+ setState(() {
+ _addressToggleFlag = newValue.isNotEmpty;
+ });
+ },
+ focusNode: _addressFocusNode,
+ style: STextStyles.field(context),
+ decoration: standardInputDecoration(
+ "Enter ${coin.ticker} address",
+ _addressFocusNode,
+ context,
+ ).copyWith(
+ contentPadding: const EdgeInsets.only(
+ left: 16,
+ top: 6,
+ bottom: 8,
+ right: 5,
+ ),
+ suffixIcon: Padding(
+ padding: sendToController.text.isEmpty
+ ? const EdgeInsets.only(right: 8)
+ : const EdgeInsets.only(right: 0),
+ child: UnconstrainedBox(
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceAround,
+ children: [
+ _addressToggleFlag
+ ? TextFieldIconButton(
+ key: const Key(
+ "sendViewClearAddressFieldButtonKey"),
+ onTap: () {
+ sendToController.text = "";
+ _address = "";
+ _updatePreviewButtonState(
+ _address, _amountToSend);
+ setState(() {
+ _addressToggleFlag = false;
+ });
+ },
+ child: const XIcon(),
+ )
+ : TextFieldIconButton(
+ key: const Key(
+ "sendViewPasteAddressFieldButtonKey"),
+ onTap: () async {
+ final ClipboardData? data = await clipboard
+ .getData(Clipboard.kTextPlain);
+ if (data?.text != null &&
+ data!.text!.isNotEmpty) {
+ String content = data.text!.trim();
+ if (content.contains("\n")) {
+ content = content.substring(
+ 0, content.indexOf("\n"));
+ }
+
+ sendToController.text = content;
+ _address = content;
+
+ _updatePreviewButtonState(
+ _address, _amountToSend);
+ setState(() {
+ _addressToggleFlag =
+ sendToController.text.isNotEmpty;
+ });
+ }
+ },
+ child: sendToController.text.isEmpty
+ ? const ClipboardIcon()
+ : const XIcon(),
+ ),
+ if (sendToController.text.isEmpty)
+ TextFieldIconButton(
+ key: const Key("sendViewAddressBookButtonKey"),
+ onTap: () {
+ Navigator.of(context).pushNamed(
+ AddressBookView.routeName,
+ arguments: coin,
+ );
+ },
+ child: const AddressBookIcon(),
+ ),
+ if (sendToController.text.isEmpty)
+ TextFieldIconButton(
+ key: const Key("sendViewScanQrButtonKey"),
+ onTap: () async {
+ try {
+ if (FocusScope.of(context).hasFocus) {
+ FocusScope.of(context).unfocus();
+ await Future.delayed(
+ const Duration(milliseconds: 75));
+ }
+
+ final qrResult = await scanner.scan();
+
+ Logging.instance.log(
+ "qrResult content: ${qrResult.rawContent}",
+ level: LogLevel.Info);
+
+ final results =
+ AddressUtils.parseUri(qrResult.rawContent);
+
+ Logging.instance.log(
+ "qrResult parsed: $results",
+ level: LogLevel.Info);
+
+ if (results.isNotEmpty &&
+ results["scheme"] == coin.uriScheme) {
+ // auto fill address
+ _address = results["address"] ?? "";
+ sendToController.text = _address!;
+
+ // autofill notes field
+ if (results["message"] != null) {
+ noteController.text = results["message"]!;
+ } else if (results["label"] != null) {
+ noteController.text = results["label"]!;
+ }
+
+ // autofill amount field
+ if (results["amount"] != null) {
+ final amount =
+ Decimal.parse(results["amount"]!);
+ cryptoAmountController.text =
+ Format.localizedStringAsFixed(
+ value: amount,
+ locale: ref
+ .read(
+ localeServiceChangeNotifierProvider)
+ .locale,
+ decimalPlaces: Constants.decimalPlaces,
+ );
+ amount.toString();
+ _amountToSend = amount;
+ }
+
+ _updatePreviewButtonState(
+ _address, _amountToSend);
+ setState(() {
+ _addressToggleFlag =
+ sendToController.text.isNotEmpty;
+ });
+
+ // now check for non standard encoded basic address
+ } else if (ref
+ .read(walletsChangeNotifierProvider)
+ .getManager(walletId)
+ .validateAddress(qrResult.rawContent)) {
+ _address = qrResult.rawContent;
+ sendToController.text = _address ?? "";
+
+ _updatePreviewButtonState(
+ _address, _amountToSend);
+ setState(() {
+ _addressToggleFlag =
+ sendToController.text.isNotEmpty;
+ });
+ }
+ } on PlatformException catch (e, s) {
+ // here we ignore the exception caused by not giving permission
+ // to use the camera to scan a qr code
+ Logging.instance.log(
+ "Failed to get camera permissions while trying to scan qr code in SendView: $e\n$s",
+ level: LogLevel.Warning);
+ }
+ },
+ child: const QrCodeIcon(),
+ )
+ ],
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ Builder(
+ builder: (_) {
+ final error = _updateInvalidAddressText(
+ _address ?? "",
+ ref.read(walletsChangeNotifierProvider).getManager(walletId),
+ );
+
+ if (error == null || error.isEmpty) {
+ return Container();
+ } else {
+ return Align(
+ alignment: Alignment.topLeft,
+ child: Padding(
+ padding: const EdgeInsets.only(
+ left: 12.0,
+ top: 4.0,
+ ),
+ child: Text(
+ error,
+ textAlign: TextAlign.left,
+ style: STextStyles.label(context).copyWith(
+ color: Theme.of(context)
+ .extension()!
+ .textError,
+ ),
+ ),
+ ),
+ );
+ }
+ },
+ ),
+ const SizedBox(
+ height: 20,
+ ),
+ Text(
+ "Note (optional)",
+ style: STextStyles.smallMed12(context),
+ textAlign: TextAlign.left,
+ ),
+ const SizedBox(
+ height: 10,
+ ),
+ ClipRRect(
+ borderRadius: BorderRadius.circular(
+ Constants.size.circularBorderRadius,
+ ),
+ child: TextField(
+ autocorrect: Util.isDesktop ? false : true,
+ enableSuggestions: Util.isDesktop ? false : true,
+ controller: noteController,
+ focusNode: _noteFocusNode,
+ style: STextStyles.field(context),
+ onChanged: (_) => setState(() {}),
+ decoration: standardInputDecoration(
+ "Type something...",
+ _noteFocusNode,
+ context,
+ ).copyWith(
+ suffixIcon: noteController.text.isNotEmpty
+ ? Padding(
+ padding: const EdgeInsets.only(right: 0),
+ child: UnconstrainedBox(
+ child: Row(
+ children: [
+ TextFieldIconButton(
+ child: const XIcon(),
+ onTap: () async {
+ setState(() {
+ noteController.text = "";
+ });
+ },
+ ),
+ ],
+ ),
+ ),
+ )
+ : null,
+ ),
+ ),
+ ),
+ const SizedBox(
+ height: 36,
+ ),
+ PrimaryButton(
+ height: 56,
+ label: "Preview send",
+ enabled: ref.watch(previewTxButtonStateProvider.state).state,
+ onPressed: ref.watch(previewTxButtonStateProvider.state).state
+ ? previewSend
+ : null,
+ )
+ ],
+ ),
+ );
+ }
+}
diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart
index 2ada80a5d..ebe2d9848 100644
--- a/lib/utilities/assets.dart
+++ b/lib/utilities/assets.dart
@@ -139,6 +139,11 @@ class _SVG {
String get anonymize => "assets/svg/tx-icon-anonymize.svg";
String get anonymizePending => "assets/svg/tx-icon-anonymize-pending.svg";
String get anonymizeFailed => "assets/svg/tx-icon-anonymize-failed.svg";
+ String get addressBookDesktop => "assets/svg/address-book-desktop.svg";
+ String get exchangeDesktop => "assets/svg/exchange-desktop.svg";
+ String get aboutDesktop => "assets/svg/about-desktop.svg";
+ String get walletDesktop => "assets/svg/wallet-desktop.svg";
+ String get exitDesktop => "assets/svg/exit-desktop.svg";
String get ellipse1 => "assets/svg/Ellipse-43.svg";
String get ellipse2 => "assets/svg/Ellipse-42.svg";
diff --git a/pubspec.yaml b/pubspec.yaml
index 35fbecf3f..aabf3f3de 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -301,6 +301,11 @@ flutter:
- assets/svg/node-circle.svg
- assets/svg/dark/dark-theme.svg
- assets/svg/light/light-mode.svg
+ - assets/svg/address-book-desktop.svg
+ - assets/svg/about-desktop.svg
+ - assets/svg/exchange-desktop.svg
+ - assets/svg/wallet-desktop.svg
+ - assets/svg/exit-desktop.svg
# coin icons
- assets/svg/coin_icons/Bitcoin.svg
- assets/svg/coin_icons/Bitcoincash.svg