CW-611-Refactor-Address-Handling (#1630)

* subaddress fix

* fix subaddress generation

* rewrite usedAddresses for xmr and wow

* [skip ci] remove print statements

* refactor address handling

* do not remove manual addresses, just mark them

* monero display latest address on receive page when autogenerate is enabled [skip ci]

* WIP subaddresses, hidden addresses, and UI improvements for monero

* update configure script

* fix subaddress generation, display latest address

* Update lib/core/wallet_loading_service.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Exclude manually created addresses

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* don't call .save function multiple times

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* - revert usedAddress functinality
- add mutex to prevent crashes
- fix UI flashing in tx screen
- fixes from comments

* account index fixes
added code to wownero
code comment

* - added subaddress index
- fixed received count also accounting for change (we don't want that)
- fix bad state: no element
- fix search
- fix automatic generation

* prevent crashes by acquiring mutex before setting the pointer

* - fix ttDetails generation in larger/restored wallets
- show manual add icon in monero/wownero even when autogeneration is enabled
- disable colors on non-debug builds
- cache getAddress call in xmr/wow
[skip ci]

* fix: silent payment error in address setter
enable fancy new features only for xmr / wow

* refresh subaddress list, when we add new address
fix manual addresses marking

* add toggle to hide and show address

* update transaction details after restore

* show only one address in address book for xmr, wow and haven

* fix address book
reset address only when autogenerate is on

* enable isEnabledAutoGenerateSubaddress on new wallets

* hide addresses after exchange only for XMR and WOW

* fix: bad-state no element

* Update cw_monero/lib/monero_wallet_addresses.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* Update cw_monero/lib/monero_wallet_addresses.dart

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>

* improvements to performance

* 0, 0 -> accountIndex, addressIndex

* make constant variables final

* Update cw_wownero/lib/wownero_wallet_addresses.dart [skip ci]

* Update cw_wownero/lib/wownero_wallet_addresses.dart [skip ci]

* Update cw_monero/lib/monero_wallet.dart [skip ci]

* fix potential exception

* fix after removing late

* remove orElse, replaced it with a try catch block.
fix strings

* fix valid seed function

* fix null check error [skip ci]

* fix updateSubaddressList for wow and haven

---------

Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
cyan
2024-09-28 05:38:23 +03:00
committed by GitHub
co-authored by Omar Hatem
parent 62e0c2a592
commit f8b0c0ad2a
35 changed files with 775 additions and 242 deletions
@@ -155,13 +155,14 @@ class AddressPage extends BasePage {
amountTextFieldFocusNode: _cryptoAmountFocus,
amountController: _amountController,
isLight: dashboardViewModel.settingsStore.currentTheme.type ==
ThemeType.light))),
ThemeType.light,
))),
SizedBox(height: 16),
Observer(builder: (_) {
if (addressListViewModel.hasAddressList) {
return SelectButton(
text: addressListViewModel.buttonTitle,
onTap: () async => Navigator.of(context).pushNamed(Routes.receive),
onTap: () => Navigator.of(context).pushNamed(Routes.receive),
textColor: Theme.of(context).extension<SyncIndicatorTheme>()!.textColor,
color: Theme.of(context).extension<SyncIndicatorTheme>()!.syncedBackgroundColor,
borderColor: Theme.of(context).extension<BalancePageTheme>()!.cardBorderColor,
+6 -6
View File
@@ -509,7 +509,7 @@ class ExchangePage extends BasePage {
}
});
reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) {
reaction((_) => exchangeViewModel.wallet.walletAddresses.addressForExchange, (String address) {
if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) {
depositKey.currentState!.changeAddress(address: address);
}
@@ -565,7 +565,7 @@ class ExchangePage extends BasePage {
key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
key.currentState!.changeAddress(
address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : '');
address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : '');
key.currentState!.changeAmount(amount: '');
}
@@ -576,9 +576,9 @@ class ExchangePage extends BasePage {
if (isCurrentTypeWallet) {
key.currentState!.changeWalletName(exchangeViewModel.wallet.name);
key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address;
key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.addressForExchange;
} else if (key.currentState!.addressController.text ==
exchangeViewModel.wallet.walletAddresses.address) {
exchangeViewModel.wallet.walletAddresses.addressForExchange) {
key.currentState!.changeWalletName('');
key.currentState!.addressController.text = '';
}
@@ -629,7 +629,7 @@ class ExchangePage extends BasePage {
initialCurrency: exchangeViewModel.depositCurrency,
initialWalletName: depositWalletName ?? '',
initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
? exchangeViewModel.wallet.walletAddresses.addressForExchange
: exchangeViewModel.depositAddress,
initialIsAmountEditable: true,
initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
@@ -694,7 +694,7 @@ class ExchangePage extends BasePage {
initialCurrency: exchangeViewModel.receiveCurrency,
initialWalletName: receiveWalletName ?? '',
initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
? exchangeViewModel.wallet.walletAddresses.addressForExchange
: exchangeViewModel.receiveAddress,
initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable,
isAmountEstimated: true,
@@ -129,7 +129,7 @@ class ExchangeTemplatePage extends BasePage {
initialWalletName: depositWalletName ?? '',
initialAddress: exchangeViewModel.depositCurrency ==
exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
? exchangeViewModel.wallet.walletAddresses.addressForExchange
: exchangeViewModel.depositAddress,
initialIsAmountEditable: true,
initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
@@ -166,7 +166,7 @@ class ExchangeTemplatePage extends BasePage {
initialWalletName: receiveWalletName ?? '',
initialAddress: exchangeViewModel.receiveCurrency ==
exchangeViewModel.wallet.currency
? exchangeViewModel.wallet.walletAddresses.address
? exchangeViewModel.wallet.walletAddresses.addressForExchange
: exchangeViewModel.receiveAddress,
initialIsAmountEditable: false,
isAmountEstimated: true,
+2 -1
View File
@@ -121,7 +121,8 @@ class ReceivePage extends BasePage {
heroTag: _heroTag,
amountTextFieldFocusNode: _cryptoAmountFocus,
amountController: _amountController,
isLight: currentTheme.type == ThemeType.light),
isLight: currentTheme.type == ThemeType.light,
),
),
AddressList(addressListViewModel: addressListViewModel),
Padding(
@@ -2,6 +2,7 @@ import 'package:auto_size_text/auto_size_text.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:flutter_slidable/flutter_slidable.dart';
@@ -15,11 +16,14 @@ class AddressCell extends StatelessWidget {
required this.textColor,
this.onTap,
this.onEdit,
this.onHide,
this.isHidden = false,
this.onDelete,
this.txCount,
this.balance,
this.isChange = false,
this.hasBalance = false});
this.hasBalance = false,
this.hasReceived = false});
factory AddressCell.fromItem(
WalletAddressListItem item, {
@@ -28,7 +32,10 @@ class AddressCell extends StatelessWidget {
required Color textColor,
Function(String)? onTap,
bool hasBalance = false,
bool hasReceived = false,
Function()? onEdit,
Function()? onHide,
bool isHidden = false,
Function()? onDelete,
}) =>
AddressCell(
@@ -40,11 +47,14 @@ class AddressCell extends StatelessWidget {
textColor: textColor,
onTap: onTap,
onEdit: onEdit,
onHide: onHide,
isHidden: isHidden,
onDelete: onDelete,
txCount: item.txCount,
balance: item.balance,
isChange: item.isChange,
hasBalance: hasBalance);
hasBalance: hasBalance,
hasReceived: hasReceived,);
final String address;
final String name;
@@ -54,11 +64,14 @@ class AddressCell extends StatelessWidget {
final Color textColor;
final Function(String)? onTap;
final Function()? onEdit;
final Function()? onHide;
final bool isHidden;
final Function()? onDelete;
final int? txCount;
final String? balance;
final bool isChange;
final bool hasBalance;
final bool hasReceived;
static const int addressPreviewLength = 8;
@@ -138,7 +151,7 @@ class AddressCell extends StatelessWidget {
),
],
),
if (hasBalance)
if (hasBalance || hasReceived)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Row(
@@ -146,7 +159,7 @@ class AddressCell extends StatelessWidget {
mainAxisSize: MainAxisSize.max,
children: [
Text(
'${S.of(context).balance}: $balance',
'${hasReceived ? S.of(context).received : S.of(context).balance}: $balance',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
@@ -178,14 +191,28 @@ class AddressCell extends StatelessWidget {
enabled: !isCurrent,
child: Slidable(
key: Key(address),
startActionPane: _actionPane(context),
endActionPane: _actionPane(context),
startActionPane: _actionPaneStart(context),
endActionPane: _actionPaneEnd(context),
child: cell,
),
);
}
ActionPane _actionPane(BuildContext context) => ActionPane(
ActionPane _actionPaneEnd(BuildContext context) => ActionPane(
motion: const ScrollMotion(),
extentRatio: onDelete != null ? 0.4 : 0.3,
children: [
SlidableAction(
onPressed: (_) => onHide?.call(),
backgroundColor: isHidden ? Colors.green : Colors.red,
foregroundColor: Colors.white,
icon: isHidden ? CupertinoIcons.arrow_left : CupertinoIcons.arrow_right,
label: isHidden ? S.of(context).show : S.of(context).hide,
),
],
);
ActionPane _actionPaneStart(BuildContext context) => ActionPane(
motion: const ScrollMotion(),
extentRatio: onDelete != null ? 0.4 : 0.3,
children: [
+146 -63
View File
@@ -10,16 +10,19 @@ import 'package:cake_wallet/src/screens/receive/widgets/address_cell.dart';
import 'package:cake_wallet/src/screens/receive/widgets/header_tile.dart';
import 'package:cake_wallet/src/widgets/section_divider.dart';
import 'package:cake_wallet/themes/extensions/receive_page_theme.dart';
import 'package:cake_wallet/utils/list_item.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_account_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_header.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
import 'package:cake_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class AddressList extends StatelessWidget {
class AddressList extends StatefulWidget {
const AddressList({
super.key,
required this.addressListViewModel,
@@ -29,59 +32,123 @@ class AddressList extends StatelessWidget {
final WalletAddressListViewModel addressListViewModel;
final Function(String)? onSelect;
@override
State<AddressList> createState() => _AddressListState();
}
class _AddressListState extends State<AddressList> {
bool showHiddenAddresses = false;
void _toggleHiddenAddresses() {
setState(() {
showHiddenAddresses = !showHiddenAddresses;
});
updateItems();
}
List<ListItem> getItems(List<ListItem> list, bool showHidden) {
return list.where((element) {
if (element is WalletAddressListItem) {
if (showHidden && element.isHidden) return true;
if (!showHidden && !element.isHidden) return true;
return false;
}
return true;
}).toList();
}
List<ListItem> items = [];
void updateItems() {
setState(() {
items = getItems(widget.addressListViewModel.items, showHiddenAddresses);
});
}
@override
void initState() {
super.initState();
items = getItems(widget.addressListViewModel.items, showHiddenAddresses);
}
@override
Widget build(BuildContext context) {
bool editable = onSelect == null;
return Observer(
builder: (_) => ListView.separated(
padding: EdgeInsets.all(0),
separatorBuilder: (context, _) => const HorizontalSectionDivider(),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: addressListViewModel.items.length,
itemBuilder: (context, index) {
final item = addressListViewModel.items[index];
Widget cell = Container();
bool editable = widget.onSelect == null;
return ListView.separated(
padding: EdgeInsets.all(0),
separatorBuilder: (context, _) => const HorizontalSectionDivider(),
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
Widget cell = Container();
if (item is WalletAccountListHeader) {
cell = HeaderTile(
showTrailingButton: true,
walletAddressListViewModel: addressListViewModel,
trailingButtonTap: () async {
if (addressListViewModel.type == WalletType.monero ||
addressListViewModel.type == WalletType.haven) {
await showPopUp<void>(
context: context, builder: (_) => getIt.get<MoneroAccountListPage>());
} else {
await showPopUp<void>(
context: context, builder: (_) => getIt.get<NanoAccountListPage>());
}
},
title: S.of(context).accounts,
trailingIcon: Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
));
}
if (item is WalletAccountListHeader) {
cell = HeaderTile(
showTrailingButton: true,
walletAddressListViewModel: widget.addressListViewModel,
trailingButtonTap: () async {
if (widget.addressListViewModel.type == WalletType.monero ||
widget.addressListViewModel.type == WalletType.haven) {
await showPopUp<void>(
context: context, builder: (_) => getIt.get<MoneroAccountListPage>());
updateItems();
} else {
await showPopUp<void>(
context: context, builder: (_) => getIt.get<NanoAccountListPage>());
updateItems();
}
},
title: S.of(context).accounts,
trailingIcon: Icon(
Icons.arrow_forward_ios,
size: 14,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
));
}
if (item is WalletAddressListHeader) {
cell = HeaderTile(
title: S.of(context).addresses,
walletAddressListViewModel: addressListViewModel,
showTrailingButton: !addressListViewModel.isAutoGenerateSubaddressEnabled,
showSearchButton: true,
trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress),
trailingIcon: Icon(
Icons.add,
size: 20,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
));
}
if (item is WalletAddressHiddenListHeader) {
cell = HeaderTile(
title: S.of(context).hidden_addresses,
walletAddressListViewModel: widget.addressListViewModel,
showTrailingButton: true,
showSearchButton: false,
trailingButtonTap: _toggleHiddenAddresses,
trailingIcon: Icon(
showHiddenAddresses ? Icons.toggle_on : Icons.toggle_off,
size: 20,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
));
}
if (item is WalletAddressListItem) {
if (item is WalletAddressListHeader) {
cell = HeaderTile(
title: S.of(context).addresses,
walletAddressListViewModel: widget.addressListViewModel,
showTrailingButton: widget.addressListViewModel.showAddManualAddresses,
showSearchButton: true,
onSearchCallback: updateItems,
trailingButtonTap: () => Navigator.of(context).pushNamed(Routes.newSubaddress).then((value) {
updateItems(); // refresh the new address
}),
trailingIcon: Icon(
Icons.add,
size: 20,
color: Theme.of(context).extension<ReceivePageTheme>()!.iconsColor,
));
}
if (item is WalletAddressListItem) {
if (item.isHidden && !showHiddenAddresses) {
cell = Container();
} else if (!item.isHidden && showHiddenAddresses) {
cell = Container();
} else {
cell = Observer(builder: (_) {
final isCurrent = item.address == addressListViewModel.address.address && editable;
final isCurrent = item.address == widget.addressListViewModel.address.address && editable;
final backgroundColor = isCurrent
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileBackgroundColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesBackgroundColor;
@@ -89,35 +156,51 @@ class AddressList extends StatelessWidget {
? Theme.of(context).extension<ReceivePageTheme>()!.currentTileTextColor
: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor;
return AddressCell.fromItem(
item,
isCurrent: isCurrent,
hasBalance: addressListViewModel.isElectrumWallet,
backgroundColor: backgroundColor,
hasBalance: widget.addressListViewModel.isBalanceAvailable,
hasReceived: widget.addressListViewModel.isReceivedAvailable,
// hasReceived:
backgroundColor: (kDebugMode && item.isHidden) ?
Theme.of(context).colorScheme.error :
(kDebugMode && item.isManual) ? Theme.of(context).colorScheme.error.withBlue(255) :
backgroundColor,
textColor: textColor,
onTap: (_) {
if (onSelect != null) {
onSelect!(item.address);
if (widget.onSelect != null) {
widget.onSelect!(item.address);
return;
}
addressListViewModel.setAddress(item);
widget.addressListViewModel.setAddress(item);
},
onEdit: editable
? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item)
? () => Navigator.of(context).pushNamed(Routes.newSubaddress, arguments: item).then((value) {
updateItems(); // refresh the new address
})
: null,
isHidden: item.isHidden,
onHide: () => _hideAddress(item),
);
});
}
}
return index != 0
? cell
: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30), topRight: Radius.circular(30)),
child: cell,
);
},
),
return index != 0
? cell
: ClipRRect(
borderRadius: BorderRadius.only(
topLeft: Radius.circular(30), topRight: Radius.circular(30)),
child: cell,
);
},
);
}
void _hideAddress(WalletAddressListItem item) async {
await widget.addressListViewModel.toggleHideAddress(item);
updateItems();
}
}
@@ -10,6 +10,7 @@ class HeaderTile extends StatefulWidget {
this.showSearchButton = false,
this.showTrailingButton = false,
this.trailingButtonTap,
this.onSearchCallback,
this.trailingIcon,
});
@@ -18,6 +19,7 @@ class HeaderTile extends StatefulWidget {
final bool showSearchButton;
final bool showTrailingButton;
final VoidCallback? trailingButtonTap;
final VoidCallback? onSearchCallback;
final Icon? trailingIcon;
@override
@@ -41,7 +43,10 @@ class _HeaderTileState extends State<HeaderTile> {
_isSearchActive
? Expanded(
child: TextField(
onChanged: (value) => widget.walletAddressListViewModel.updateSearchText(value),
onChanged: (value) {
widget.walletAddressListViewModel.updateSearchText(value);
widget.onSearchCallback?.call();
},
cursorColor: Theme.of(context).extension<ReceivePageTheme>()!.tilesTextColor,
cursorWidth: 0.5,
decoration: InputDecoration(
@@ -37,6 +37,10 @@ class QRWidget extends StatelessWidget {
final int? qrVersion;
final String? heroTag;
PaymentURI get addressUri {
return addressListViewModel.uri;
}
@override
Widget build(BuildContext context) {
final copyImage = Image.asset('assets/images/copy_address.png',
@@ -77,14 +81,14 @@ class QRWidget extends StatelessWidget {
() async {
await Navigator.pushNamed(context, Routes.fullscreenQR,
arguments: QrViewData(
data: addressListViewModel.uri.toString(),
data: addressUri.toString(),
heroTag: heroTag,
));
},
);
},
child: Hero(
tag: Key(heroTag ?? addressListViewModel.uri.toString()),
tag: Key(heroTag ?? addressUri.toString()),
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
@@ -105,7 +109,7 @@ class QRWidget extends StatelessWidget {
color: Colors.white,
),
),
child: QrImage(data: addressListViewModel.uri.toString())),
child: QrImage(data: addressUri.toString())),
),
),
),
@@ -148,7 +152,7 @@ class QRWidget extends StatelessWidget {
builder: (context) => Observer(
builder: (context) => GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: addressListViewModel.address.address));
Clipboard.setData(ClipboardData(text: addressUri.address));
showBar<void>(context, S.of(context).copied_to_clipboard);
},
child: Row(
@@ -157,7 +161,7 @@ class QRWidget extends StatelessWidget {
children: <Widget>[
Expanded(
child: Text(
addressListViewModel.address.address,
addressUri.address,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 15,