From 4bef0d5cb3f2487477048f03c6fb8872ff19b01f Mon Sep 17 00:00:00 2001 From: Dominik Toton <166132265+dtscalac@users.noreply.github.com> Date: Mon, 30 Sep 2024 18:07:17 +0200 Subject: [PATCH] feat(cat-voices): wallet link details (#905) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: select wallet * feat: add wallet address formatter * refactor: restructure panels * feat: implement wallet details * feat: add loading animation * chore: revert unintended changes * chore: revert * chore: cleanup * chore: cleanup --------- Co-authored-by: Damian Moliński <47773413+damian-molinski@users.noreply.github.com> --- .../lib/pages/account/account_popup.dart | 15 +- .../{intro => stage}/intro_panel.dart | 0 .../rbac_transaction_panel.dart | 0 .../roles_chooser_panel.dart | 0 .../roles_summary_panel.dart | 0 .../select_wallet_panel.dart | 80 ++++++-- .../stage/wallet_details_panel.dart | 180 ++++++++++++++++++ .../wallet_details/wallet_details_panel.dart | 34 ---- .../wallet_link/wallet_link_panel.dart | 15 +- .../lib/widgets/menu/voices_wallet_tile.dart | 53 ++++-- .../cubits/wallet_link_cubit.dart | 32 +++- .../src/registration/registration_cubit.dart | 9 +- .../registration/wallet_link_state_data.dart | 7 +- .../catalyst_voices_localizations.dart | 34 +++- .../catalyst_voices_localizations_en.dart | 21 +- .../catalyst_voices_localizations_es.dart | 21 +- .../lib/l10n/intl_en.arb | 29 ++- .../lib/src/catalyst_voices_models.dart | 1 + .../src/wallet/cardano_wallet_details.dart | 19 ++ .../catalyst_voices_models/pubspec.yaml | 2 + .../lib/src/catalyst_voices_shared.dart | 1 + .../formatter/wallet_address_formatter.dart | 15 ++ .../wallet_address_formatter_test.dart | 21 ++ 23 files changed, 504 insertions(+), 85 deletions(-) rename catalyst_voices/lib/pages/registration/wallet_link/{intro => stage}/intro_panel.dart (100%) rename catalyst_voices/lib/pages/registration/wallet_link/{rbac_transaction => stage}/rbac_transaction_panel.dart (100%) rename catalyst_voices/lib/pages/registration/wallet_link/{roles_chooser => stage}/roles_chooser_panel.dart (100%) rename catalyst_voices/lib/pages/registration/wallet_link/{roles_summary => stage}/roles_summary_panel.dart (100%) rename catalyst_voices/lib/pages/registration/wallet_link/{select_wallet => stage}/select_wallet_panel.dart (66%) create mode 100644 catalyst_voices/lib/pages/registration/wallet_link/stage/wallet_details_panel.dart delete mode 100644 catalyst_voices/lib/pages/registration/wallet_link/wallet_details/wallet_details_panel.dart create mode 100644 catalyst_voices/packages/catalyst_voices_models/lib/src/wallet/cardano_wallet_details.dart create mode 100644 catalyst_voices/packages/catalyst_voices_shared/lib/src/formatter/wallet_address_formatter.dart create mode 100644 catalyst_voices/packages/catalyst_voices_shared/test/src/formatter/wallet_address_formatter_test.dart diff --git a/catalyst_voices/lib/pages/account/account_popup.dart b/catalyst_voices/lib/pages/account/account_popup.dart index 44f91a83e2c..8288221a1d8 100644 --- a/catalyst_voices/lib/pages/account/account_popup.dart +++ b/catalyst_voices/lib/pages/account/account_popup.dart @@ -1,6 +1,8 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -41,7 +43,12 @@ class AccountPopup extends StatelessWidget { walletName: 'Wallet name', walletBalance: '₳ 1,750,000', accountType: 'Basis', - walletAddress: 'addr1_H4543...45GH', + /* cSpell:disable */ + walletAddress: ShelleyAddress.fromBech32( + 'addr_test1vzpwq95z3xyum8vqndgdd' + '9mdnmafh3djcxnc6jemlgdmswcve6tkw', + ), + /* cSpell:enable */ ), ), const PopupMenuItem( @@ -85,7 +92,7 @@ class _Header extends StatelessWidget { final String walletName; final String walletBalance; final String accountType; - final String walletAddress; + final ShelleyAddress walletAddress; const _Header({ required this.accountLetter, @@ -146,14 +153,14 @@ class _Header extends StatelessWidget { children: [ Expanded( child: Text( - walletAddress, + WalletAddressFormatter.formatShort(walletAddress), style: Theme.of(context).textTheme.bodyLarge, ), ), InkWell( onTap: () async { await Clipboard.setData( - ClipboardData(text: walletAddress), + ClipboardData(text: walletAddress.toBech32()), ); }, child: VoicesAssets.icons.clipboardCopy.buildIcon(), diff --git a/catalyst_voices/lib/pages/registration/wallet_link/intro/intro_panel.dart b/catalyst_voices/lib/pages/registration/wallet_link/stage/intro_panel.dart similarity index 100% rename from catalyst_voices/lib/pages/registration/wallet_link/intro/intro_panel.dart rename to catalyst_voices/lib/pages/registration/wallet_link/stage/intro_panel.dart diff --git a/catalyst_voices/lib/pages/registration/wallet_link/rbac_transaction/rbac_transaction_panel.dart b/catalyst_voices/lib/pages/registration/wallet_link/stage/rbac_transaction_panel.dart similarity index 100% rename from catalyst_voices/lib/pages/registration/wallet_link/rbac_transaction/rbac_transaction_panel.dart rename to catalyst_voices/lib/pages/registration/wallet_link/stage/rbac_transaction_panel.dart diff --git a/catalyst_voices/lib/pages/registration/wallet_link/roles_chooser/roles_chooser_panel.dart b/catalyst_voices/lib/pages/registration/wallet_link/stage/roles_chooser_panel.dart similarity index 100% rename from catalyst_voices/lib/pages/registration/wallet_link/roles_chooser/roles_chooser_panel.dart rename to catalyst_voices/lib/pages/registration/wallet_link/stage/roles_chooser_panel.dart diff --git a/catalyst_voices/lib/pages/registration/wallet_link/roles_summary/roles_summary_panel.dart b/catalyst_voices/lib/pages/registration/wallet_link/stage/roles_summary_panel.dart similarity index 100% rename from catalyst_voices/lib/pages/registration/wallet_link/roles_summary/roles_summary_panel.dart rename to catalyst_voices/lib/pages/registration/wallet_link/stage/roles_summary_panel.dart diff --git a/catalyst_voices/lib/pages/registration/wallet_link/select_wallet/select_wallet_panel.dart b/catalyst_voices/lib/pages/registration/wallet_link/stage/select_wallet_panel.dart similarity index 66% rename from catalyst_voices/lib/pages/registration/wallet_link/select_wallet/select_wallet_panel.dart rename to catalyst_voices/lib/pages/registration/wallet_link/stage/select_wallet_panel.dart index a80072ca7a9..201dade9fd3 100644 --- a/catalyst_voices/lib/pages/registration/wallet_link/select_wallet/select_wallet_panel.dart +++ b/catalyst_voices/lib/pages/registration/wallet_link/stage/select_wallet_panel.dart @@ -3,9 +3,13 @@ import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter/material.dart'; import 'package:result_type/result_type.dart'; +/// Callback called when a [wallet] is selected. +typedef _OnSelectWallet = Future Function(CardanoWallet wallet); + class SelectWalletPanel extends StatefulWidget { final Result, Exception>? walletsResult; @@ -22,7 +26,7 @@ class _SelectWalletPanelState extends State { @override void initState() { super.initState(); - _sendRefreshEvent(); + _refreshWallets(); } @override @@ -44,7 +48,8 @@ class _SelectWalletPanelState extends State { Expanded( child: _Wallets( result: widget.walletsResult, - onRefreshTap: _sendRefreshEvent, + onRefreshTap: _refreshWallets, + onSelectWallet: _onSelectWallet, ), ), const SizedBox(height: 24), @@ -63,17 +68,23 @@ class _SelectWalletPanelState extends State { ); } - void _sendRefreshEvent() { - RegistrationCubit.of(context).refreshCardanoWallets(); + void _refreshWallets() { + RegistrationCubit.of(context).refreshWallets(); + } + + Future _onSelectWallet(CardanoWallet wallet) async { + return RegistrationCubit.of(context).selectWallet(wallet); } } class _Wallets extends StatelessWidget { final Result, Exception>? result; + final _OnSelectWallet onSelectWallet; final VoidCallback onRefreshTap; const _Wallets({ this.result, + required this.onSelectWallet, required this.onRefreshTap, }); @@ -81,7 +92,7 @@ class _Wallets extends StatelessWidget { Widget build(BuildContext context) { return switch (result) { Success(:final value) => value.isNotEmpty - ? _WalletsList(wallets: value) + ? _WalletsList(wallets: value, onSelectWallet: onSelectWallet) : _WalletsEmpty(onRetry: onRefreshTap), Failure() => _WalletsError(onRetry: onRefreshTap), _ => const Center(child: VoicesCircularProgressIndicator()), @@ -91,27 +102,70 @@ class _Wallets extends StatelessWidget { class _WalletsList extends StatelessWidget { final List wallets; + final _OnSelectWallet onSelectWallet; - const _WalletsList({required this.wallets}); + const _WalletsList({ + required this.wallets, + required this.onSelectWallet, + }); @override Widget build(BuildContext context) { return ListView.builder( itemCount: wallets.length, itemBuilder: (context, index) { - final wallet = wallets[index]; - return VoicesWalletTile( - iconSrc: wallet.icon, - name: Text(wallet.name), - onTap: () { - RegistrationCubit.of(context).nextStep(); - }, + return _WalletTile( + wallet: wallets[index], + onSelectWallet: onSelectWallet, ); }, ); } } +class _WalletTile extends StatefulWidget { + final CardanoWallet wallet; + final _OnSelectWallet onSelectWallet; + + const _WalletTile({ + required this.wallet, + required this.onSelectWallet, + }); + + @override + State<_WalletTile> createState() => _WalletTileState(); +} + +class _WalletTileState extends State<_WalletTile> { + bool _isLoading = false; + + @override + Widget build(BuildContext context) { + return VoicesWalletTile( + iconSrc: widget.wallet.icon, + name: Text(widget.wallet.name), + isLoading: _isLoading, + onTap: _onSelectWallet, + ); + } + + Future _onSelectWallet() async { + try { + setState(() { + _isLoading = true; + }); + + await widget.onSelectWallet(widget.wallet).withMinimumDelay(); + } finally { + if (mounted) { + setState(() { + _isLoading = false; + }); + } + } + } +} + class _WalletsEmpty extends StatelessWidget { final VoidCallback onRetry; diff --git a/catalyst_voices/lib/pages/registration/wallet_link/stage/wallet_details_panel.dart b/catalyst_voices/lib/pages/registration/wallet_link/stage/wallet_details_panel.dart new file mode 100644 index 00000000000..42247efea7c --- /dev/null +++ b/catalyst_voices/lib/pages/registration/wallet_link/stage/wallet_details_panel.dart @@ -0,0 +1,180 @@ +import 'package:catalyst_cardano/catalyst_cardano.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; +import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; +import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; +import 'package:catalyst_voices_brands/catalyst_voices_brands.dart'; +import 'package:catalyst_voices_localization/catalyst_voices_localization.dart'; +import 'package:catalyst_voices_models/catalyst_voices_models.dart'; +import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class WalletDetailsPanel extends StatelessWidget { + final CardanoWalletDetails details; + + const WalletDetailsPanel({ + super.key, + required this.details, + }); + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + const SizedBox(height: 24), + Text( + context.l10n.walletLinkWalletDetailsTitle, + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 32), + _WalletExtension(wallet: details.wallet), + const SizedBox(height: 16), + Text( + context.l10n.walletLinkWalletDetailsContent(details.wallet.name), + style: Theme.of(context).textTheme.titleMedium, + ), + const SizedBox(height: 24), + _WalletSummary(details: details), + const Spacer(), + const _Navigation(), + ], + ); + } +} + +class _WalletExtension extends StatelessWidget { + final CardanoWallet wallet; + + const _WalletExtension({required this.wallet}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + const SizedBox(width: 8), + VoicesWalletTileIcon(iconSrc: wallet.icon), + const SizedBox(width: 12), + Text(wallet.name, style: Theme.of(context).textTheme.bodyLarge), + const SizedBox(width: 8), + VoicesAvatar( + radius: 10, + padding: const EdgeInsets.all(4), + icon: VoicesAssets.icons.check.buildIcon(), + foregroundColor: Theme.of(context).colors.success, + backgroundColor: Theme.of(context).colors.successContainer, + ), + ], + ); + } +} + +class _WalletSummary extends StatelessWidget { + final CardanoWalletDetails details; + + const _WalletSummary({required this.details}); + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8), + border: Border.all( + width: 1.5, + color: Theme.of(context).colors.outlineBorderVariant!, + ), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + context.l10n.walletDetectionSummary, + style: Theme.of(context).textTheme.titleSmall, + ), + const SizedBox(height: 16), + _WalletSummaryItem( + label: Text(context.l10n.walletBalance), + value: Text(CryptocurrencyFormatter.formatAmount(details.balance)), + ), + const SizedBox(height: 12), + _WalletSummaryItem( + label: Text(context.l10n.walletAddress), + value: Row( + children: [ + Text(WalletAddressFormatter.formatShort(details.address)), + const SizedBox(width: 6), + InkWell( + onTap: () async { + await Clipboard.setData( + ClipboardData(text: details.address.toBech32()), + ); + }, + child: VoicesAssets.icons.clipboardCopy.buildIcon(size: 16), + ), + ], + ), + ), + ], + ), + ); + } +} + +class _WalletSummaryItem extends StatelessWidget { + final Widget label; + final Widget value; + + const _WalletSummaryItem({ + required this.label, + required this.value, + }); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: DefaultTextStyle( + style: Theme.of(context).textTheme.bodySmall!.copyWith( + fontWeight: FontWeight.bold, + height: 1, + ), + child: label, + ), + ), + Expanded( + child: DefaultTextStyle( + style: Theme.of(context).textTheme.bodySmall!.copyWith( + height: 1, + ), + child: value, + ), + ), + ], + ); + } +} + +class _Navigation extends StatelessWidget { + const _Navigation(); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: VoicesBackButton( + onTap: () => RegistrationCubit.of(context).previousStep(), + ), + ), + const SizedBox(width: 10), + Expanded( + child: VoicesNextButton( + onTap: () => RegistrationCubit.of(context).nextStep(), + ), + ), + ], + ); + } +} diff --git a/catalyst_voices/lib/pages/registration/wallet_link/wallet_details/wallet_details_panel.dart b/catalyst_voices/lib/pages/registration/wallet_link/wallet_details/wallet_details_panel.dart deleted file mode 100644 index b201a79bb40..00000000000 --- a/catalyst_voices/lib/pages/registration/wallet_link/wallet_details/wallet_details_panel.dart +++ /dev/null @@ -1,34 +0,0 @@ -import 'package:catalyst_voices/widgets/buttons/voices_filled_button.dart'; -import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; -import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; -import 'package:flutter/material.dart'; - -// TODO(dtscalac): define content -class WalletDetailsPanel extends StatelessWidget { - const WalletDetailsPanel({super.key}); - - @override - Widget build(BuildContext context) { - return Column( - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - const Spacer(), - VoicesFilledButton( - leading: VoicesAssets.icons.wallet.buildIcon(), - onTap: () { - RegistrationCubit.of(context).previousStep(); - }, - child: const Text('Previous'), - ), - const SizedBox(height: 12), - VoicesFilledButton( - leading: VoicesAssets.icons.wallet.buildIcon(), - onTap: () { - RegistrationCubit.of(context).nextStep(); - }, - child: const Text('Next'), - ), - ], - ); - } -} diff --git a/catalyst_voices/lib/pages/registration/wallet_link/wallet_link_panel.dart b/catalyst_voices/lib/pages/registration/wallet_link/wallet_link_panel.dart index 7dc0776e7ae..fe6012478d3 100644 --- a/catalyst_voices/lib/pages/registration/wallet_link/wallet_link_panel.dart +++ b/catalyst_voices/lib/pages/registration/wallet_link/wallet_link_panel.dart @@ -1,9 +1,9 @@ -import 'package:catalyst_voices/pages/registration/wallet_link/intro/intro_panel.dart'; -import 'package:catalyst_voices/pages/registration/wallet_link/rbac_transaction/rbac_transaction_panel.dart'; -import 'package:catalyst_voices/pages/registration/wallet_link/roles_chooser/roles_chooser_panel.dart'; -import 'package:catalyst_voices/pages/registration/wallet_link/roles_summary/roles_summary_panel.dart'; -import 'package:catalyst_voices/pages/registration/wallet_link/select_wallet/select_wallet_panel.dart'; -import 'package:catalyst_voices/pages/registration/wallet_link/wallet_details/wallet_details_panel.dart'; +import 'package:catalyst_voices/pages/registration/wallet_link/stage/intro_panel.dart'; +import 'package:catalyst_voices/pages/registration/wallet_link/stage/rbac_transaction_panel.dart'; +import 'package:catalyst_voices/pages/registration/wallet_link/stage/roles_chooser_panel.dart'; +import 'package:catalyst_voices/pages/registration/wallet_link/stage/roles_summary_panel.dart'; +import 'package:catalyst_voices/pages/registration/wallet_link/stage/select_wallet_panel.dart'; +import 'package:catalyst_voices/pages/registration/wallet_link/stage/wallet_details_panel.dart'; import 'package:catalyst_voices_blocs/catalyst_voices_blocs.dart'; import 'package:catalyst_voices_models/catalyst_voices_models.dart'; import 'package:flutter/material.dart'; @@ -25,7 +25,8 @@ class WalletLinkPanel extends StatelessWidget { WalletLinkStage.selectWallet => SelectWalletPanel( walletsResult: stateData.wallets, ), - WalletLinkStage.walletDetails => const WalletDetailsPanel(), + WalletLinkStage.walletDetails => + WalletDetailsPanel(details: stateData.selectedWallet!), WalletLinkStage.rolesChooser => const RolesChooserPanel(), WalletLinkStage.rolesSummary => const RolesSummaryPanel(), WalletLinkStage.rbacTransaction => const RbacTransactionPanel(), diff --git a/catalyst_voices/lib/widgets/menu/voices_wallet_tile.dart b/catalyst_voices/lib/widgets/menu/voices_wallet_tile.dart index 3fc90651d51..e8b49a5b27d 100644 --- a/catalyst_voices/lib/widgets/menu/voices_wallet_tile.dart +++ b/catalyst_voices/lib/widgets/menu/voices_wallet_tile.dart @@ -1,4 +1,5 @@ import 'package:catalyst_cardano/catalyst_cardano.dart'; +import 'package:catalyst_voices/widgets/widgets.dart'; import 'package:catalyst_voices_assets/catalyst_voices_assets.dart'; import 'package:flutter/material.dart'; @@ -11,6 +12,9 @@ class VoicesWalletTile extends StatelessWidget { /// The name of the wallet extension. final Widget? name; + /// If true, shows a circular progress indicator instead of trailing icon. + final bool isLoading; + /// A callback called when the widget is pressed. final VoidCallback? onTap; @@ -19,27 +23,16 @@ class VoicesWalletTile extends StatelessWidget { super.key, this.iconSrc, this.name, + this.isLoading = false, this.onTap, }); @override Widget build(BuildContext context) { - final icon = iconSrc; final name = this.name; return ListTile( - leading: SizedBox( - width: 40, - height: 40, - child: icon == null - ? const _IconPlaceholder() - : Image.network( - icon, - errorBuilder: (context, error, stackTrace) { - return const _IconPlaceholder(); - }, - ), - ), + leading: VoicesWalletTileIcon(iconSrc: iconSrc), horizontalTitleGap: 16, title: name == null ? null @@ -49,12 +42,44 @@ class VoicesWalletTile extends StatelessWidget { overflow: TextOverflow.ellipsis, child: name, ), - trailing: VoicesAssets.icons.chevronRight.buildIcon(size: 24), + trailing: isLoading + ? const SizedBox( + width: 24, + height: 24, + child: VoicesCircularProgressIndicator(), + ) + : VoicesAssets.icons.chevronRight.buildIcon(size: 24), onTap: onTap, ); } } +class VoicesWalletTileIcon extends StatelessWidget { + final String? iconSrc; + + const VoicesWalletTileIcon({ + super.key, + required this.iconSrc, + }); + + @override + Widget build(BuildContext context) { + final iconSrc = this.iconSrc; + return SizedBox( + width: 40, + height: 40, + child: iconSrc == null + ? const _IconPlaceholder() + : Image.network( + iconSrc, + errorBuilder: (context, error, stackTrace) { + return const _IconPlaceholder(); + }, + ), + ); + } +} + class _IconPlaceholder extends StatelessWidget { const _IconPlaceholder(); diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/wallet_link_cubit.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/wallet_link_cubit.dart index 6a727fd03f8..82e60fd3adf 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/wallet_link_cubit.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/cubits/wallet_link_cubit.dart @@ -7,6 +7,8 @@ import 'package:catalyst_voices_shared/catalyst_voices_shared.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:result_type/result_type.dart'; +final _logger = Logger('WalletLinkCubit'); + final class WalletLinkCubit extends Cubit { WalletLinkCubit() : super(const WalletLink()); @@ -22,7 +24,7 @@ final class WalletLinkCubit extends Cubit { } } - Future refreshCardanoWallets() async { + Future refreshWallets() async { try { _stateData = _stateData.copyWith(wallets: const Optional.empty()); @@ -30,11 +32,37 @@ final class WalletLinkCubit extends Cubit { await CatalystCardano.instance.getWallets().withMinimumDelay(); _stateData = _stateData.copyWith(wallets: Optional(Success(wallets))); - } on Exception catch (error) { + } on Exception catch (error, stackTrace) { + _logger.severe(error, stackTrace); _stateData = _stateData.copyWith(wallets: Optional(Failure(error))); } } + Future selectWallet(CardanoWallet wallet) async { + try { + final enabledWallet = await wallet.enable(); + final balance = await enabledWallet.getBalance(); + final address = await enabledWallet.getChangeAddress(); + + final walletDetails = CardanoWalletDetails( + wallet: wallet, + balance: balance.coin, + address: address, + ); + + final nextState = state.copyWith( + stage: WalletLinkStage.walletDetails, + stateData: _stateData.copyWith( + selectedWallet: Optional(walletDetails), + ), + ); + + emit(nextState); + } catch (error, stackTrace) { + _logger.severe(error, stackTrace); + } + } + WalletLinkStep? nextStep() { final currentStageIndex = WalletLinkStage.values.indexOf(state.stage); final isLast = currentStageIndex == WalletLinkStage.values.length - 1; diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart index ee45a85dd1f..532490fcc68 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/registration_cubit.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:catalyst_cardano/catalyst_cardano.dart'; import 'package:catalyst_voices_blocs/src/registration/cubits/keychain_creation_cubit.dart'; import 'package:catalyst_voices_blocs/src/registration/cubits/wallet_link_cubit.dart'; import 'package:catalyst_voices_blocs/src/registration/registration_state.dart'; @@ -83,8 +84,12 @@ final class RegistrationCubit extends Cubit { _keychainCreationCubit.setSeedPhraseStoredConfirmed(confirmed); } - void refreshCardanoWallets() { - unawaited(_walletLinkCubit.refreshCardanoWallets()); + void refreshWallets() { + unawaited(_walletLinkCubit.refreshWallets()); + } + + Future selectWallet(CardanoWallet wallet) { + return _walletLinkCubit.selectWallet(wallet); } RegistrationStep? _nextStep({RegistrationStep? from}) { diff --git a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/wallet_link_state_data.dart b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/wallet_link_state_data.dart index c55cfed4350..d04f7fe9663 100644 --- a/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/wallet_link_state_data.dart +++ b/catalyst_voices/packages/catalyst_voices_blocs/lib/src/registration/wallet_link_state_data.dart @@ -5,19 +5,24 @@ import 'package:result_type/result_type.dart'; final class WalletLinkStateData extends Equatable { final Result, Exception>? wallets; + final CardanoWalletDetails? selectedWallet; const WalletLinkStateData({ this.wallets, + this.selectedWallet, }); WalletLinkStateData copyWith({ Optional, Exception>>? wallets, + Optional? selectedWallet, }) { return WalletLinkStateData( wallets: wallets != null ? wallets.data : this.wallets, + selectedWallet: + selectedWallet != null ? selectedWallet.data : this.selectedWallet, ); } @override - List get props => [wallets]; + List get props => [wallets, selectedWallet]; } diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart index 927250afbf5..3591a9bf0b6 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations.dart @@ -724,12 +724,42 @@ abstract class VoicesLocalizations { /// **'To complete this action, you\'ll submit a signed transaction to Cardano. There will be an ADA transaction fee.'** String get walletLinkSelectWalletContent; + /// A title in link wallet flow on wallet details screen. + /// + /// In en, this message translates to: + /// **'Cardano wallet detection'** + String get walletLinkWalletDetailsTitle; + + /// A message in link wallet flow on wallet details screen. + /// + /// In en, this message translates to: + /// **'{wallet} connected successfully!'** + String walletLinkWalletDetailsContent(String wallet); + /// Message shown when redirecting to external content that describes which wallets are supported. /// /// In en, this message translates to: /// **'See all supported wallets'** String get seeAllSupportedWallets; + /// Message shown when presenting the details of a connected wallet. + /// + /// In en, this message translates to: + /// **'Wallet detection summary'** + String get walletDetectionSummary; + + /// The wallet balance in terms of Ada. + /// + /// In en, this message translates to: + /// **'Wallet balance'** + String get walletBalance; + + /// A cardano wallet address + /// + /// In en, this message translates to: + /// **'Wallet address'** + String get walletAddress; + /// No description provided for @accountCreationCreate. /// /// In en, this message translates to: @@ -901,7 +931,7 @@ abstract class VoicesLocalizations { /// A subtitle on delete keychain dialog /// /// In en, this message translates to: - /// **'Are you sure you wants to delete your 
Catalyst Keychain from this device?'** + /// **'Are you sure you wants to delete your\nCatalyst Keychain from this device?'** String get deleteKeychainDialogSubtitle; /// A warning on delete keychain dialog @@ -913,7 +943,7 @@ abstract class VoicesLocalizations { /// A warning info on delete keychain dialog /// /// In en, this message translates to: - /// **'Your Catalyst account will be removed,
this action cannot be undone!'** + /// **'Your Catalyst account will be removed,\nthis action cannot be undone!'** String get deleteKeychainDialogWarningInfo; /// A typing info on delete keychain dialog diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart index 92769bf7754..416a0fb9f87 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_en.dart @@ -376,9 +376,26 @@ class VoicesLocalizationsEn extends VoicesLocalizations { @override String get walletLinkSelectWalletContent => 'To complete this action, you\'ll submit a signed transaction to Cardano. There will be an ADA transaction fee.'; + @override + String get walletLinkWalletDetailsTitle => 'Cardano wallet detection'; + + @override + String walletLinkWalletDetailsContent(String wallet) { + return '$wallet connected successfully!'; + } + @override String get seeAllSupportedWallets => 'See all supported wallets'; + @override + String get walletDetectionSummary => 'Wallet detection summary'; + + @override + String get walletBalance => 'Wallet balance'; + + @override + String get walletAddress => 'Wallet address'; + @override String get accountCreationCreate => 'Create a new 
Catalyst Keychain'; @@ -464,13 +481,13 @@ class VoicesLocalizationsEn extends VoicesLocalizations { String get deleteKeychainDialogTitle => 'Delete Keychain?'; @override - String get deleteKeychainDialogSubtitle => 'Are you sure you wants to delete your 
Catalyst Keychain from this device?'; + String get deleteKeychainDialogSubtitle => 'Are you sure you wants to delete your\nCatalyst Keychain from this device?'; @override String get deleteKeychainDialogWarning => 'Make sure you have a working Catalyst 12-word seedphrase!'; @override - String get deleteKeychainDialogWarningInfo => 'Your Catalyst account will be removed,
this action cannot be undone!'; + String get deleteKeychainDialogWarningInfo => 'Your Catalyst account will be removed,\nthis action cannot be undone!'; @override String get deleteKeychainDialogTypingInfo => 'To avoid mistakes, please type ‘Remove Keychain’ below.'; diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart index 024bc9a33fa..99a0dedf4d4 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/generated/catalyst_voices_localizations_es.dart @@ -376,9 +376,26 @@ class VoicesLocalizationsEs extends VoicesLocalizations { @override String get walletLinkSelectWalletContent => 'To complete this action, you\'ll submit a signed transaction to Cardano. There will be an ADA transaction fee.'; + @override + String get walletLinkWalletDetailsTitle => 'Cardano wallet detection'; + + @override + String walletLinkWalletDetailsContent(String wallet) { + return '$wallet connected successfully!'; + } + @override String get seeAllSupportedWallets => 'See all supported wallets'; + @override + String get walletDetectionSummary => 'Wallet detection summary'; + + @override + String get walletBalance => 'Wallet balance'; + + @override + String get walletAddress => 'Wallet address'; + @override String get accountCreationCreate => 'Create a new 
Catalyst Keychain'; @@ -464,13 +481,13 @@ class VoicesLocalizationsEs extends VoicesLocalizations { String get deleteKeychainDialogTitle => 'Delete Keychain?'; @override - String get deleteKeychainDialogSubtitle => 'Are you sure you wants to delete your 
Catalyst Keychain from this device?'; + String get deleteKeychainDialogSubtitle => 'Are you sure you wants to delete your\nCatalyst Keychain from this device?'; @override String get deleteKeychainDialogWarning => 'Make sure you have a working Catalyst 12-word seedphrase!'; @override - String get deleteKeychainDialogWarningInfo => 'Your Catalyst account will be removed,
this action cannot be undone!'; + String get deleteKeychainDialogWarningInfo => 'Your Catalyst account will be removed,\nthis action cannot be undone!'; @override String get deleteKeychainDialogTypingInfo => 'To avoid mistakes, please type ‘Remove Keychain’ below.'; diff --git a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb index e9eb15da4f5..9c5704f77a8 100644 --- a/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb +++ b/catalyst_voices/packages/catalyst_voices_localization/lib/l10n/intl_en.arb @@ -466,10 +466,35 @@ "@walletLinkSelectWalletContent": { "description": "A message (content) in link wallet flow on select wallet screen." }, + "walletLinkWalletDetailsTitle": "Cardano wallet detection", + "@walletLinkWalletDetailsTitle": { + "description": "A title in link wallet flow on wallet details screen." + }, + "walletLinkWalletDetailsContent": "{wallet} connected successfully!", + "@walletLinkWalletDetailsContent": { + "description": "A message in link wallet flow on wallet details screen.", + "placeholders": { + "wallet": { + "type": "String" + } + } + }, "seeAllSupportedWallets": "See all supported wallets", "@seeAllSupportedWallets": { "description": "Message shown when redirecting to external content that describes which wallets are supported." }, + "walletDetectionSummary": "Wallet detection summary", + "@walletDetectionSummary": { + "description": "Message shown when presenting the details of a connected wallet." + }, + "walletBalance": "Wallet balance", + "@walletBalance": { + "description": "The wallet balance in terms of Ada." + }, + "walletAddress": "Wallet address", + "@walletAddress": { + "description": "A cardano wallet address" + }, "accountCreationCreate": "Create a new \u2028Catalyst Keychain", "accountCreationRecover": "Recover your\u2028Catalyst Keychain", "accountCreationOnThisDevice": "On this device", @@ -549,7 +574,7 @@ "@deleteKeychainDialogTitle": { "description": "A title on delete keychain dialog" }, - "deleteKeychainDialogSubtitle": "Are you sure you wants to delete your 
Catalyst Keychain from this device?", + "deleteKeychainDialogSubtitle": "Are you sure you wants to delete your\nCatalyst Keychain from this device?", "@deleteKeychainDialogSubtitle": { "description": "A subtitle on delete keychain dialog" }, @@ -557,7 +582,7 @@ "@deleteKeychainDialogWarning": { "description": "A warning on delete keychain dialog" }, - "deleteKeychainDialogWarningInfo": "Your Catalyst account will be removed,
this action cannot be undone!", + "deleteKeychainDialogWarningInfo": "Your Catalyst account will be removed,\nthis action cannot be undone!", "@deleteKeychainDialogWarningInfo": { "description": "A warning info on delete keychain dialog" }, diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart index 76b7d962d2e..30347c68d54 100644 --- a/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/catalyst_voices_models.dart @@ -17,6 +17,7 @@ export 'treasury/treasury_campaign_builder.dart'; export 'treasury/treasury_campaign_segment.dart'; export 'treasury/treasury_campaign_segment_step.dart'; export 'user/user.dart'; +export 'wallet/cardano_wallet_details.dart'; export 'workspace/workspace_proposal_navigation.dart'; export 'workspace/workspace_proposal_segment.dart'; export 'workspace/workspace_proposal_segment_step.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_models/lib/src/wallet/cardano_wallet_details.dart b/catalyst_voices/packages/catalyst_voices_models/lib/src/wallet/cardano_wallet_details.dart new file mode 100644 index 00000000000..1308761aedf --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_models/lib/src/wallet/cardano_wallet_details.dart @@ -0,0 +1,19 @@ +import 'package:catalyst_cardano/catalyst_cardano.dart'; +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:equatable/equatable.dart'; + +/// Describes the details of an enabled wallet. +final class CardanoWalletDetails extends Equatable { + final CardanoWallet wallet; + final Coin balance; + final ShelleyAddress address; + + const CardanoWalletDetails({ + required this.wallet, + required this.balance, + required this.address, + }); + + @override + List get props => [wallet, balance, address]; +} diff --git a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml index 5ee7d5a2c64..1b301f63ff4 100644 --- a/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml +++ b/catalyst_voices/packages/catalyst_voices_models/pubspec.yaml @@ -8,7 +8,9 @@ environment: dependencies: bip39: ^1.0.6 + catalyst_cardano: ^0.3.0 catalyst_cardano_serialization: ^0.4.0 + catalyst_cardano_web: ^0.3.0 convert: ^3.1.1 ed25519_hd_key: ^2.3.0 equatable: ^2.0.5 diff --git a/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart b/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart index f47e0ebb5a1..a69ca464d61 100644 --- a/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart +++ b/catalyst_voices/packages/catalyst_voices_shared/lib/src/catalyst_voices_shared.dart @@ -2,6 +2,7 @@ export 'common/build_config.dart'; export 'common/build_environment.dart'; export 'dependency/dependency_provider.dart'; export 'formatter/cryptocurrency_formatter.dart'; +export 'formatter/wallet_address_formatter.dart'; export 'logging/logging_service.dart'; export 'platform/catalyst_platform.dart'; export 'platform_aware_builder/platform_aware_builder.dart'; diff --git a/catalyst_voices/packages/catalyst_voices_shared/lib/src/formatter/wallet_address_formatter.dart b/catalyst_voices/packages/catalyst_voices_shared/lib/src/formatter/wallet_address_formatter.dart new file mode 100644 index 00000000000..10cc2bf959b --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_shared/lib/src/formatter/wallet_address_formatter.dart @@ -0,0 +1,15 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; + +/// Formats [ShelleyAddress]. +abstract class WalletAddressFormatter { + /* cSpell:disable */ + /// Formats the [address] into short representation. + /// + /// Formats "addr_test1vzpwq95z3xyum8vqndgdd9mdnmafh3djcxnc6jemlgdmswcve6tkw" + /// into "addr_test1v…6tkw". + /* cSpell:enable */ + static String formatShort(ShelleyAddress address) { + final str = address.toBech32(); + return '${str.substring(0, 11)}…${str.substring(str.length - 4)}'; + } +} diff --git a/catalyst_voices/packages/catalyst_voices_shared/test/src/formatter/wallet_address_formatter_test.dart b/catalyst_voices/packages/catalyst_voices_shared/test/src/formatter/wallet_address_formatter_test.dart new file mode 100644 index 00000000000..aa624b6c7c3 --- /dev/null +++ b/catalyst_voices/packages/catalyst_voices_shared/test/src/formatter/wallet_address_formatter_test.dart @@ -0,0 +1,21 @@ +import 'package:catalyst_cardano_serialization/catalyst_cardano_serialization.dart'; +import 'package:catalyst_voices_shared/src/formatter/wallet_address_formatter.dart'; +import 'package:test/test.dart'; + +void main() { + group(WalletAddressFormatter, () { + test('should format ShelleyAddress into short representation', () { + /* cSpell:disable */ + final address = ShelleyAddress.fromBech32( + 'addr_test1vzpwq95z3xyum8vqndgdd' + '9mdnmafh3djcxnc6jemlgdmswcve6tkw', + ); + /* cSpell:enable */ + + expect( + WalletAddressFormatter.formatShort(address), + equals('addr_test1v…6tkw'), + ); + }); + }); +}