eth token wallet general ui and wallet selection interface

This commit is contained in:
julian 2023-03-28 16:18:11 -06:00
parent 9c0994aa00
commit 5e5730d5a5
12 changed files with 455 additions and 63 deletions

View file

@ -112,7 +112,7 @@ class _ChooseFromStackViewState extends ConsumerState<ChooseFromStackView> {
const SizedBox(
height: 2,
),
WalletInfoRowBalanceFuture(
WalletInfoRowBalance(
walletId: walletIds[index],
),
],

View file

@ -69,18 +69,8 @@ class TokenSummary extends ConsumerWidget {
const SizedBox(
width: 10,
),
RoundedContainer(
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4),
radiusMultiplier: 0.25,
color: const Color(
0xFF4D5798), // TODO: color theme for multi themes
child: Text(
ref.watch(walletsChangeNotifierProvider.select(
(value) => value.getManager(walletId).coin.ticker)),
style: STextStyles.w600_12(context).copyWith(
color: Colors.white, // TODO: design is wrong?
),
),
CoinTickerTag(
walletId: walletId,
),
],
),
@ -195,3 +185,28 @@ class TokenOptionsButton extends StatelessWidget {
);
}
}
class CoinTickerTag extends ConsumerWidget {
const CoinTickerTag({
Key? key,
required this.walletId,
}) : super(key: key);
final String walletId;
@override
Widget build(BuildContext context, WidgetRef ref) {
return RoundedContainer(
padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4),
radiusMultiplier: 0.25,
color: const Color(0xFF4D5798), // TODO: color theme for multi themes
child: Text(
ref.watch(walletsChangeNotifierProvider
.select((value) => value.getManager(walletId).coin.ticker)),
style: STextStyles.w600_12(context).copyWith(
color: Colors.white, // TODO: design is wrong?
),
),
);
}
}

View file

@ -0,0 +1,73 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/enums/coin_enum.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/background.dart';
import 'package:stackwallet/widgets/conditional_parent.dart';
import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart';
import 'package:stackwallet/widgets/master_wallet_card.dart';
class EthWalletsOverview extends ConsumerStatefulWidget {
const EthWalletsOverview({Key? key}) : super(key: key);
static const routeName = "/ethWalletsOverview";
@override
ConsumerState<EthWalletsOverview> createState() => _EthWalletsOverviewState();
}
class _EthWalletsOverviewState extends ConsumerState<EthWalletsOverview> {
final isDesktop = Util.isDesktop;
final List<String> ethWalletIds = [];
@override
void initState() {
final walletsData =
ref.read(walletsServiceChangeNotifierProvider).fetchWalletsData();
walletsData.removeWhere((key, value) => value.coin != Coin.ethereum);
ethWalletIds.clear();
ethWalletIds.addAll(walletsData.values.map((e) => e.walletId));
super.initState();
}
@override
Widget build(BuildContext context) {
return Background(
child: ConditionalParent(
condition: !isDesktop,
builder: (child) => Scaffold(
backgroundColor:
Theme.of(context).extension<StackColors>()!.background,
appBar: AppBar(
leading: const AppBarBackButton(),
title: Text(
"Ethereum (ETH) wallets",
style: STextStyles.navBarTitle(context),
),
),
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: child,
),
),
),
child: ListView.separated(
itemCount: ethWalletIds.length,
separatorBuilder: (_, __) => const SizedBox(
height: 8,
),
itemBuilder: (_, index) => MasterWalletCard(
walletId: ethWalletIds[index],
),
),
),
);
}
}

View file

@ -5,6 +5,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages/wallets_sheet/wallets_sheet.dart';
import 'package:stackwallet/pages/wallets_view/eth_wallets_overview.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
@ -45,7 +46,13 @@ class WalletListItem extends ConsumerWidget {
BorderRadius.circular(Constants.size.circularBorderRadius),
),
onPressed: () async {
if (walletCount == 1) {
if (coin == Coin.ethereum) {
unawaited(
Navigator.of(context).pushNamed(
EthWalletsOverview.routeName,
),
);
} else if (walletCount == 1) {
final providersByCoin = ref
.watch(walletsChangeNotifierProvider
.select((value) => value.getManagerProvidersByCoin()))
@ -57,15 +64,17 @@ class WalletListItem extends ConsumerWidget {
if (coin == Coin.monero || coin == Coin.wownero) {
await manager.initializeExisting();
}
unawaited(
Navigator.of(context).pushNamed(
WalletView.routeName,
arguments: Tuple2(
manager.walletId,
providersByCoin.first,
if (context.mounted) {
unawaited(
Navigator.of(context).pushNamed(
WalletView.routeName,
arguments: Tuple2(
manager.walletId,
providersByCoin.first,
),
),
),
);
);
}
} else {
unawaited(
showModalBottomSheet<dynamic>(

View file

@ -105,6 +105,7 @@ import 'package:stackwallet/pages/wallet_view/transaction_views/edit_note_view.d
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_details_view.dart';
import 'package:stackwallet/pages/wallet_view/transaction_views/transaction_search_filter_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/pages/wallets_view/eth_wallets_overview.dart';
import 'package:stackwallet/pages/wallets_view/wallets_view.dart';
import 'package:stackwallet/pages_desktop_specific/address_book_view/desktop_address_book.dart';
import 'package:stackwallet/pages_desktop_specific/coin_control/desktop_coin_control_view.dart';
@ -250,6 +251,15 @@ class RouteGenerator {
),
);
case EthWalletsOverview.routeName:
return getRoute(
shouldUseMaterialRoute: useMaterialPageRoute,
builder: (_) => const EthWalletsOverview(),
settings: RouteSettings(
name: settings.name,
),
);
case TokenContractDetailsView.routeName:
if (args is Tuple2<String, String>) {
return getRoute(

View file

@ -5,11 +5,13 @@ import 'package:decimal/decimal.dart';
import 'package:ethereum_addresses/ethereum_addresses.dart';
import 'package:http/http.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/models/balance.dart';
import 'package:stackwallet/models/isar/models/isar_models.dart';
import 'package:stackwallet/models/node_model.dart';
import 'package:stackwallet/models/paymint/fee_object_model.dart';
import 'package:stackwallet/models/token_balance.dart';
import 'package:stackwallet/services/coins/coin_service.dart';
import 'package:stackwallet/services/ethereum/ethereum_api.dart';
import 'package:stackwallet/services/event_bus/events/global/node_connection_status_changed_event.dart';
@ -17,6 +19,7 @@ import 'package:stackwallet/services/event_bus/events/global/refresh_percent_cha
import 'package:stackwallet/services/event_bus/events/global/updated_in_background_event.dart';
import 'package:stackwallet/services/event_bus/events/global/wallet_sync_status_changed_event.dart';
import 'package:stackwallet/services/event_bus/global_event_bus.dart';
import 'package:stackwallet/services/mixins/eth_token_cache.dart';
import 'package:stackwallet/services/mixins/wallet_cache.dart';
import 'package:stackwallet/services/mixins/wallet_db.dart';
import 'package:stackwallet/services/node_service.dart';
@ -76,6 +79,26 @@ class EthereumWallet extends CoinServiceAPI with WalletCache, WalletDB {
);
}
TokenBalance getCachedTokenBalance(EthContract contract) {
final jsonString = DB.instance.get<dynamic>(
boxName: _walletId,
key: TokenCacheKeys.tokenBalance(contract.address),
) as String?;
if (jsonString == null) {
return TokenBalance(
contractAddress: contract.address,
decimalPlaces: contract.decimals,
total: 0,
spendable: 0,
blockedTotal: 0,
pendingSpendable: 0,
);
}
return TokenBalance.fromJson(
jsonString,
);
}
// Future<void> removeTokenContract(String contractAddress) async {
// final set = getWalletTokenContractAddresses().toSet();
// set.removeWhere((e) => e == contractAddress);

View file

@ -2,7 +2,7 @@ import 'package:stackwallet/db/hive/db.dart';
import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
import 'package:stackwallet/models/token_balance.dart';
abstract class _Keys {
abstract class TokenCacheKeys {
static String tokenBalance(String contractAddress) {
return "tokenBalanceCache_$contractAddress";
}
@ -21,7 +21,7 @@ mixin EthTokenCache {
TokenBalance getCachedBalance() {
final jsonString = DB.instance.get<dynamic>(
boxName: _walletId,
key: _Keys.tokenBalance(_token.address),
key: TokenCacheKeys.tokenBalance(_token.address),
) as String?;
if (jsonString == null) {
return TokenBalance(
@ -41,7 +41,7 @@ mixin EthTokenCache {
Future<void> updateCachedBalance(TokenBalance balance) async {
await DB.instance.put<dynamic>(
boxName: _walletId,
key: _Keys.tokenBalance(_token.address),
key: TokenCacheKeys.tokenBalance(_token.address),
value: balance.toJsonIgnoreCoin(),
);
}

View file

@ -0,0 +1,137 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/widgets/animated_widgets/rotate_icon.dart';
import 'package:stackwallet/widgets/expandable.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/wallet_card.dart';
import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart';
class MasterWalletCard extends ConsumerStatefulWidget {
const MasterWalletCard({
Key? key,
required this.walletId,
this.popPrevious = false,
}) : super(key: key);
final String walletId;
final bool popPrevious;
@override
ConsumerState<MasterWalletCard> createState() => _MasterWalletCardState();
}
class _MasterWalletCardState extends ConsumerState<MasterWalletCard> {
final expandableController = ExpandableController();
final rotateIconController = RotateIconController();
late final List<String> tokenContractAddresses;
@override
void initState() {
final ethWallet = ref
.read(walletsChangeNotifierProvider)
.getManager(widget.walletId)
.wallet as EthereumWallet;
tokenContractAddresses = ethWallet.getWalletTokenContractAddresses();
super.initState();
}
@override
Widget build(BuildContext context) {
return RoundedWhiteContainer(
padding: EdgeInsets.zero,
child: Expandable(
controller: expandableController,
expandOverride: () {},
header: Padding(
padding: const EdgeInsets.all(12),
child: Row(
children: [
Expanded(
child: WalletInfoRow(
walletId: widget.walletId,
),
),
MaterialButton(
padding: const EdgeInsets.all(5),
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
minWidth: 32,
height: 32,
color: Theme.of(context)
.extension<StackColors>()!
.textFieldDefaultBG,
elevation: 0,
hoverElevation: 0,
disabledElevation: 0,
highlightElevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(
Constants.size.circularBorderRadius,
),
),
onPressed: () {
if (expandableController.state == ExpandableState.collapsed) {
rotateIconController.forward?.call();
} else {
rotateIconController.reverse?.call();
}
expandableController.toggle?.call();
},
child: RotateIcon(
controller: rotateIconController,
icon: SvgPicture.asset(
Assets.svg.chevronDown,
width: 14,
),
curve: Curves.easeInOut,
),
),
],
),
),
body: ListView(
shrinkWrap: true,
primary: false,
children: [
Container(
width: double.infinity,
height: 1.5,
color:
Theme.of(context).extension<StackColors>()!.backgroundAppBar,
),
Padding(
padding: const EdgeInsets.all(
7,
),
child: WalletSheetCard(
walletId: widget.walletId,
popPrevious: true,
),
),
...tokenContractAddresses.map(
(e) => Padding(
padding: const EdgeInsets.only(
left: 7,
right: 7,
bottom: 7,
),
child: WalletSheetCard(
walletId: widget.walletId,
contractAddress: e,
// popPrevious: true,
),
),
),
],
),
),
);
}
}

View file

@ -1,21 +1,33 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/pages/token_view/token_view.dart';
import 'package:stackwallet/pages/wallet_view/wallet_view.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/global/secure_store_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart';
import 'package:stackwallet/services/ethereum/ethereum_token_service.dart';
import 'package:stackwallet/services/transaction_notification_tracker.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/widgets/rounded_white_container.dart';
import 'package:stackwallet/widgets/wallet_info_row/wallet_info_row.dart';
import 'package:tuple/tuple.dart';
import '../utilities/show_loading.dart';
class WalletSheetCard extends ConsumerWidget {
const WalletSheetCard({
Key? key,
required this.walletId,
this.contractAddress,
this.popPrevious = false,
}) : super(key: key);
final String walletId;
final String? contractAddress;
final bool popPrevious;
@override
@ -33,25 +45,56 @@ class WalletSheetCard extends ConsumerWidget {
),
),
onPressed: () async {
final manager = ref
.read(walletsChangeNotifierProvider)
.getManager(walletId);
if (manager.coin == Coin.monero ||
manager.coin == Coin.wownero) {
final manager =
ref.read(walletsChangeNotifierProvider).getManager(walletId);
if (manager.coin == Coin.monero || manager.coin == Coin.wownero) {
await manager.initializeExisting();
}
if (popPrevious) Navigator.of(context).pop();
Navigator.of(context).pushNamed(
WalletView.routeName,
arguments: Tuple2(
walletId,
ref
.read(walletsChangeNotifierProvider)
.getManagerProvider(walletId)),
);
if (context.mounted) {
if (popPrevious) Navigator.of(context).pop();
unawaited(
Navigator.of(context).pushNamed(
WalletView.routeName,
arguments: Tuple2(
walletId,
ref
.read(walletsChangeNotifierProvider)
.getManagerProvider(walletId)),
),
);
if (contractAddress != null) {
final contract = ref
.read(mainDBProvider)
.getEthContractSync(contractAddress!)!;
ref.read(tokenServiceStateProvider.state).state = EthTokenWallet(
token: contract,
secureStore: ref.read(secureStoreProvider),
ethWallet: manager.wallet as EthereumWallet,
tracker: TransactionNotificationTracker(
walletId: walletId,
),
);
await showLoading<void>(
whileFuture: ref.read(tokenServiceProvider)!.initialize(),
context: context,
opaqueBG: true,
message: "Loading ${contract.name}",
);
if (context.mounted) {
await Navigator.of(context).pushNamed(
TokenView.routeName,
arguments: walletId,
);
}
}
}
},
child: WalletInfoRow(
walletId: walletId,
contractAddress: contractAddress,
),
),
);

View file

@ -1,20 +1,25 @@
import 'package:decimal/decimal.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:stackwallet/db/isar/main_db.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/services/coins/ethereum/ethereum_wallet.dart';
import 'package:stackwallet/services/coins/firo/firo_wallet.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/format.dart';
import 'package:stackwallet/utilities/text_styles.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
import 'package:stackwallet/utilities/util.dart';
class WalletInfoRowBalanceFuture extends ConsumerWidget {
const WalletInfoRowBalanceFuture({Key? key, required this.walletId})
: super(key: key);
class WalletInfoRowBalance extends ConsumerWidget {
const WalletInfoRowBalance({
Key? key,
required this.walletId,
this.contractAddress,
}) : super(key: key);
final String walletId;
final String? contractAddress;
@override
Widget build(BuildContext context, WidgetRef ref) {
@ -28,18 +33,30 @@ class WalletInfoRowBalanceFuture extends ConsumerWidget {
),
);
Decimal balance = manager.balance.getTotal();
if (manager.coin == Coin.firo || manager.coin == Coin.firoTestNet) {
balance += (manager.wallet as FiroWallet).balancePrivate.getTotal();
Decimal balance;
int decimals;
String unit;
if (contractAddress == null) {
balance = manager.balance.getTotal();
if (manager.coin == Coin.firo || manager.coin == Coin.firoTestNet) {
balance += (manager.wallet as FiroWallet).balancePrivate.getTotal();
}
unit = manager.coin.ticker;
decimals = manager.coin.decimals;
} else {
final ethWallet = manager.wallet as EthereumWallet;
final contract = MainDB.instance.getEthContractSync(contractAddress!)!;
balance = ethWallet.getCachedTokenBalance(contract).getTotal();
unit = contract.symbol;
decimals = contract.decimals;
}
return Text(
"${Format.localizedStringAsFixed(
value: balance,
locale: locale,
decimalPlaces: Constants.decimalPlacesForCoin(manager.coin),
)} ${manager.coin.ticker}",
decimalPlaces: decimals,
)} $unit",
style: Util.isDesktop
? STextStyles.desktopTextExtraSmall(context).copyWith(
color: Theme.of(context).extension<StackColors>()!.textSubtitle1,

View file

@ -1,17 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:isar/isar.dart';
import 'package:stackwallet/models/isar/exchange_cache/currency.dart';
import 'package:stackwallet/services/exchange/change_now/change_now_exchange.dart';
import 'package:stackwallet/services/exchange/exchange_data_loading_service.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/constants.dart';
import 'package:stackwallet/utilities/enums/coin_enum.dart';
import 'package:stackwallet/utilities/theme/stack_colors.dart';
class WalletInfoCoinIcon extends StatelessWidget {
const WalletInfoCoinIcon({Key? key, required this.coin}) : super(key: key);
const WalletInfoCoinIcon({
Key? key,
required this.coin,
this.contractAddress,
}) : super(key: key);
final Coin coin;
final String? contractAddress;
@override
Widget build(BuildContext context) {
Currency? currency;
if (contractAddress != null) {
currency = ExchangeDataLoadingService.instance.isar.currencies
.where()
.exchangeNameEqualTo(ChangeNowExchange.exchangeName)
.filter()
.tokenContractEqualTo(
contractAddress!,
caseSensitive: false,
)
.and()
.imageIsNotEmpty()
.findFirstSync();
}
return Container(
decoration: BoxDecoration(
color: Theme.of(context)
@ -24,11 +48,17 @@ class WalletInfoCoinIcon extends StatelessWidget {
),
child: Padding(
padding: const EdgeInsets.all(6),
child: SvgPicture.asset(
Assets.svg.iconFor(coin: coin),
width: 20,
height: 20,
),
child: currency != null && currency.image.isNotEmpty
? SvgPicture.network(
currency.image,
width: 20,
height: 20,
)
: SvgPicture.asset(
Assets.svg.iconFor(coin: coin),
width: 20,
height: 20,
),
),
);
}

View file

@ -1,6 +1,9 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_svg/svg.dart';
import 'package:stackwallet/models/isar/models/ethereum/eth_contract.dart';
import 'package:stackwallet/pages/token_view/sub_widgets/token_summary.dart';
import 'package:stackwallet/providers/db/main_db_provider.dart';
import 'package:stackwallet/providers/providers.dart';
import 'package:stackwallet/utilities/assets.dart';
import 'package:stackwallet/utilities/text_styles.dart';
@ -14,10 +17,12 @@ class WalletInfoRow extends ConsumerWidget {
Key? key,
required this.walletId,
this.onPressedDesktop,
this.contractAddress,
this.padding = const EdgeInsets.all(0),
}) : super(key: key);
final String walletId;
final String? contractAddress;
final VoidCallback? onPressedDesktop;
final EdgeInsets padding;
@ -27,6 +32,12 @@ class WalletInfoRow extends ConsumerWidget {
.watch(walletsChangeNotifierProvider.notifier)
.getManagerProvider(walletId));
EthContract? contract;
if (contractAddress != null) {
contract = ref.watch(mainDBProvider
.select((value) => value.getEthContractSync(contractAddress!)));
}
if (Util.isDesktop) {
return MouseRegion(
cursor: SystemMouseCursors.click,
@ -42,7 +53,10 @@ class WalletInfoRow extends ConsumerWidget {
flex: 4,
child: Row(
children: [
WalletInfoCoinIcon(coin: manager.coin),
WalletInfoCoinIcon(
coin: manager.coin,
contractAddress: contractAddress,
),
const SizedBox(
width: 12,
),
@ -60,7 +74,7 @@ class WalletInfoRow extends ConsumerWidget {
),
Expanded(
flex: 4,
child: WalletInfoRowBalanceFuture(
child: WalletInfoRowBalance(
walletId: walletId,
),
),
@ -89,7 +103,10 @@ class WalletInfoRow extends ConsumerWidget {
} else {
return Row(
children: [
WalletInfoCoinIcon(coin: manager.coin),
WalletInfoCoinIcon(
coin: manager.coin,
contractAddress: contractAddress,
),
const SizedBox(
width: 12,
),
@ -98,14 +115,32 @@ class WalletInfoRow extends ConsumerWidget {
mainAxisAlignment: MainAxisAlignment.spaceBetween,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
manager.walletName,
style: STextStyles.titleBold12(context),
),
contract != null
? Row(
children: [
Text(
contract.name,
style: STextStyles.titleBold12(context),
),
const SizedBox(
width: 4,
),
CoinTickerTag(
walletId: walletId,
),
],
)
: Text(
manager.walletName,
style: STextStyles.titleBold12(context),
),
const SizedBox(
height: 2,
),
WalletInfoRowBalanceFuture(walletId: walletId),
WalletInfoRowBalance(
walletId: walletId,
contractAddress: contractAddress,
),
],
),
),