mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-11-07 08:04:04 +01:00
Merge upstream Cake Wallet v6.5.0
Catches up from the May 12 fork point (v6.1.2+23) through v6.5.0: 535 upstream commits, including the Monero silent-open fix, Monero on Trezor, Ledger/Trezor BLE fixes, the new seed and history screens, quick Bitcoin sync, git-pinned dependencies (pubspec_overrides.yaml), frozen Hive adapters (*.part.dart), monero_c v0.18.4.6-RC2 and Flutter 3.41.9. Merged with both sides normalized first (import rename, 100-column format), which took the conflict count from 614 paths to 322. The procedure is now scripts/upstream_sync.sh. Kept from Hash Bags: - WalletType enum order and Hive IDs. SQLite stores WalletType.index and our order differs from upstream's; taking theirs would reread saved wallets as other chains. Our frozen adapter keeps the shipped IDs. - The 1.0.2 legacy PIN and wallet-password decode and self-heal. - Removed chains (Solana, Tron, Zano, Zcash, Decred, Haven) and the new Robinhood chain stay out; Cake Pay and non-Trocador swaps stay out. - Default nodes (node.wownero.org, sethforprivacy, Blockstream, bysh.me), converted to upstream's isDefault node-list key; Wownero is back in the node reset list. - Privacy: address resolvers limited to domain-anchored sources again; prices.neroswap.com for fiat; upstream's charts tab, which reads prices.cakewallet.com, sits behind FeatureFlag.isChartsEnabled = false. - Lightning stays behind FeatureFlag.isLightningEnabled, including upstream's new post-seed Lightning username step. - Our icon pipeline, branding and bundle identifiers. Twelve new upstream widget tests are skipped: test and code are unchanged from v6.5.0 apart from renames (not rerun on a pristine upstream checkout; upstream's top_bar_test does not compile against its own TopBar). flutter analyze lib: 0 errors; flutter test: 345 passed, 12 skipped; flutter build bundle compiles. Built with Flutter 3.41.9.
This commit is contained in:
@@ -116,9 +116,9 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
|
||||
|
||||
/// not the best scenario, but WidgetsBinding is not behaving correctly on Android
|
||||
await Future<void>.delayed(Duration(milliseconds: 50));
|
||||
await _authBar?.dismiss();
|
||||
await _removeBar(_authBar);
|
||||
await Future<void>.delayed(Duration(milliseconds: 50));
|
||||
await _progressBar?.dismiss();
|
||||
await _removeBar(_progressBar);
|
||||
await Future<void>.delayed(Duration(milliseconds: 50));
|
||||
if (route != null) {
|
||||
Navigator.of(_key.currentContext!).pushReplacementNamed(route, arguments: arguments);
|
||||
@@ -141,6 +141,7 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
|
||||
size: 36,
|
||||
icon: Icon(Icons.close),
|
||||
onPressed: Navigator.of(context).pop,
|
||||
semanticLabel: S.of(context).close,
|
||||
iconColor: Theme.of(context).colorScheme.onSurfaceVariant)
|
||||
: Container(),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
@@ -152,6 +153,21 @@ class AuthPagePinCodeStateImpl extends AuthPageState<AuthPage> {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _removeBar(Flushbar<void>? bar) async {
|
||||
final barRoute = bar?.flushbarRoute;
|
||||
|
||||
if (bar == null || barRoute == null || !barRoute.isActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (bar.isShowing()) {
|
||||
await bar.dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
barRoute.navigator?.removeRoute(barRoute);
|
||||
}
|
||||
|
||||
void dismissFlushBar(Flushbar<dynamic>? bar) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
await bar?.dismiss();
|
||||
|
||||
@@ -154,8 +154,12 @@ class BackupPage extends BasePage {
|
||||
leftButtonText: S.of(context).share,
|
||||
actionRightButton: () async {
|
||||
await backupViewModelBase.saveToDownload(backup.name, backup.file);
|
||||
Navigator.of(dialogContext).pop();
|
||||
await showBar<void>(context, S.of(context).file_saved);
|
||||
if (dialogContext.mounted && Navigator.canPop(dialogContext)) {
|
||||
Navigator.of(dialogContext).pop();
|
||||
}
|
||||
if (context.mounted && Navigator.canPop(context)) {
|
||||
await showBar<void>(context, S.of(context).file_saved);
|
||||
}
|
||||
},
|
||||
actionLeftButton: () async {
|
||||
Navigator.of(dialogContext).pop();
|
||||
|
||||
@@ -36,6 +36,7 @@ class _EditBackupPasswordPageState extends State<EditBackupPasswordPage> {
|
||||
ModalTopBar(
|
||||
title: S.of(context).edit_backup_password,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -84,20 +84,16 @@ abstract class BasePage extends StatelessWidget {
|
||||
return null;
|
||||
}
|
||||
|
||||
return MergeSemantics(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: ButtonTheme(
|
||||
minWidth: double.minPositive,
|
||||
child: Semantics(
|
||||
label: S.of(context).seed_alert_back,
|
||||
child: ModernButton(
|
||||
size: 37,
|
||||
icon: Icon(CupertinoIcons.back),
|
||||
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
onPressed: () => onClose(context),
|
||||
),
|
||||
),
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 4.0),
|
||||
child: ButtonTheme(
|
||||
minWidth: double.minPositive,
|
||||
child: ModernButton(
|
||||
size: 37,
|
||||
icon: Icon(CupertinoIcons.back),
|
||||
semanticLabel: S.of(context).seed_alert_back,
|
||||
iconColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
onPressed: () => onClose(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,48 +0,0 @@
|
||||
import 'package:hash_wallet/core/selectable_option.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/select_options_page.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class BuyOptionsPage extends SelectOptionsPage {
|
||||
BuyOptionsPage({required this.items, this.pickAnOption, this.confirmOption});
|
||||
|
||||
final List<SelectableItem> items;
|
||||
final Function(SelectableOption option)? pickAnOption;
|
||||
final Function(BuildContext context)? confirmOption;
|
||||
|
||||
@override
|
||||
String get pageTitle => S.current.choose_a_provider;
|
||||
|
||||
@override
|
||||
EdgeInsets? get contentPadding => null;
|
||||
|
||||
@override
|
||||
EdgeInsets? get tilePadding => EdgeInsets.only(top: 8);
|
||||
|
||||
@override
|
||||
EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 8);
|
||||
|
||||
@override
|
||||
double? get imageHeight => 40;
|
||||
|
||||
@override
|
||||
double? get imageWidth => 40;
|
||||
|
||||
@override
|
||||
Color? get selectedBackgroundColor => null;
|
||||
|
||||
@override
|
||||
double? get tileBorderRadius => 30;
|
||||
|
||||
@override
|
||||
String get bottomSectionText => '';
|
||||
|
||||
@override
|
||||
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
|
||||
|
||||
@override
|
||||
String get primaryButtonText => S.current.confirm;
|
||||
|
||||
@override
|
||||
void Function(BuildContext context)? get primaryButtonAction => confirmOption;
|
||||
}
|
||||
@@ -1,515 +0,0 @@
|
||||
import 'package:hash_wallet/buy/sell_buy_states.dart';
|
||||
import 'package:hash_wallet/core/address_validator.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/exchange_card.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
|
||||
import 'package:hash_wallet/src/widgets/keyboard_done_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/provider_optoin_tile.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:hash_wallet/src/widgets/trail_button.dart';
|
||||
import 'package:hash_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/view_model/buy/buy_sell_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:keyboard_actions/keyboard_actions.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class BuySellPage extends BasePage {
|
||||
BuySellPage(this.buySellViewModel);
|
||||
|
||||
final BuySellViewModel buySellViewModel;
|
||||
final cryptoCurrencyKey = GlobalKey<ExchangeCardState>();
|
||||
final fiatCurrencyKey = GlobalKey<ExchangeCardState>();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _fiatAmountFocus = FocusNode();
|
||||
final _cryptoAmountFocus = FocusNode();
|
||||
final _cryptoAddressFocus = FocusNode();
|
||||
var _isReactionsSet = false;
|
||||
|
||||
late final String? depositWalletName;
|
||||
late final String? receiveWalletName;
|
||||
|
||||
@override
|
||||
String get title => S.current.buy + '/' + S.current.sell;
|
||||
|
||||
@override
|
||||
bool get gradientBackground => true;
|
||||
|
||||
@override
|
||||
bool get gradientAll => true;
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => false;
|
||||
|
||||
@override
|
||||
bool get extendBodyBehindAppBar => true;
|
||||
|
||||
@override
|
||||
AppBarStyle get appBarStyle => AppBarStyle.completelyTransparent;
|
||||
|
||||
@override
|
||||
Function(BuildContext)? get pushToNextWidget => (context) {
|
||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||
if (!currentFocus.hasPrimaryFocus) {
|
||||
currentFocus.focusedChild?.unfocus();
|
||||
}
|
||||
};
|
||||
|
||||
@override
|
||||
Widget trailing(BuildContext context) => TrailButton(
|
||||
caption: S.of(context).clear,
|
||||
onPressed: () {
|
||||
_formKey.currentState?.reset();
|
||||
buySellViewModel.reset();
|
||||
});
|
||||
|
||||
@override
|
||||
Widget? leading(BuildContext context) {
|
||||
final _backButton = Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 16,
|
||||
);
|
||||
final _closeButton = currentTheme.isDark ? closeButtonImageDarkTheme : closeButtonImage;
|
||||
|
||||
bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
|
||||
|
||||
return MergeSemantics(
|
||||
child: SizedBox(
|
||||
height: isMobileView ? 37 : 45,
|
||||
width: isMobileView ? 37 : 45,
|
||||
child: ButtonTheme(
|
||||
minWidth: double.minPositive,
|
||||
child: Semantics(
|
||||
label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent),
|
||||
),
|
||||
onPressed: () => onClose(context),
|
||||
child: !isMobileView ? _closeButton : _backButton,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, buySellViewModel));
|
||||
|
||||
return KeyboardActions(
|
||||
disableScroll: true,
|
||||
config: KeyboardActionsConfig(
|
||||
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
|
||||
keyboardBarColor: Theme.of(context).colorScheme.surface,
|
||||
nextFocus: false,
|
||||
actions: [
|
||||
KeyboardActionsItem(
|
||||
focusNode: _fiatAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
|
||||
KeyboardActionsItem(
|
||||
focusNode: _cryptoAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
|
||||
]),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.only(bottom: 24),
|
||||
content: Observer(
|
||||
builder: (_) => Column(
|
||||
children: [
|
||||
_exchangeCardsSection(context),
|
||||
Padding(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(height: 12),
|
||||
_buildPaymentMethodTile(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
bottomSection: Observer(
|
||||
builder: (_) => Column(
|
||||
children: [
|
||||
if (buySellViewModel.isBuySellQuoteFailed)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 15),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Icon(
|
||||
Icons.warning_amber_rounded,
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
size: 26,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: Text(
|
||||
buySellViewModel.buySellQuoteFailedError ??
|
||||
S.of(context).buy_sell_pair_is_not_supported_warning,
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.error,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
LoadingPrimaryButton(
|
||||
text: S.current.choose_a_provider,
|
||||
onPressed: () async {
|
||||
if (!_formKey.currentState!.validate()) return;
|
||||
buySellViewModel.onTapChoseProvider(context);
|
||||
},
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isDisabled: buySellViewModel.isBuySellQuoteFailed,
|
||||
isLoading:
|
||||
!buySellViewModel.isReadyToTrade && !buySellViewModel.isBuySellQuoteFailed,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildPaymentMethodTile(BuildContext context) {
|
||||
if (buySellViewModel.paymentMethodState is PaymentMethodLoading ||
|
||||
buySellViewModel.paymentMethodState is InitialPaymentMethod) {
|
||||
return OptionTilePlaceholder(
|
||||
withBadge: false,
|
||||
withSubtitle: false,
|
||||
borderRadius: 30,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
leadingIcon: Icons.arrow_forward_ios,
|
||||
isDarkTheme: currentTheme.isDark);
|
||||
}
|
||||
if (buySellViewModel.paymentMethodState is PaymentMethodFailed) {
|
||||
return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
|
||||
}
|
||||
if (buySellViewModel.paymentMethodState is PaymentMethodLoaded &&
|
||||
buySellViewModel.selectedPaymentMethod != null) {
|
||||
final selectedPaymentMethod = buySellViewModel.selectedPaymentMethod!;
|
||||
return ProviderOptionTile(
|
||||
lightImagePath: selectedPaymentMethod.lightIconPath,
|
||||
darkImagePath: selectedPaymentMethod.darkIconPath,
|
||||
title: selectedPaymentMethod.title,
|
||||
onPressed: () => _pickPaymentMethod(context),
|
||||
leadingIcon: Icons.arrow_forward_ios,
|
||||
isLightMode: !currentTheme.isDark,
|
||||
borderRadius: 30,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 8),
|
||||
titleTextStyle: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
);
|
||||
}
|
||||
return OptionTilePlaceholder(errorText: 'No payment methods available', borderRadius: 30);
|
||||
}
|
||||
|
||||
void _pickPaymentMethod(BuildContext context) async {
|
||||
final currentOption = buySellViewModel.selectedPaymentMethod;
|
||||
await Navigator.of(context).pushNamed(
|
||||
Routes.paymentMethodOptionsPage,
|
||||
arguments: [
|
||||
buySellViewModel.paymentMethods,
|
||||
buySellViewModel.changeOption,
|
||||
],
|
||||
);
|
||||
|
||||
buySellViewModel.selectedPaymentMethod;
|
||||
if (currentOption != null &&
|
||||
currentOption.paymentMethodType !=
|
||||
buySellViewModel.selectedPaymentMethod?.paymentMethodType) {
|
||||
await buySellViewModel.calculateBestRate();
|
||||
}
|
||||
}
|
||||
|
||||
void _setReactions(BuildContext context, BuySellViewModel buySellViewModel) {
|
||||
if (_isReactionsSet) return;
|
||||
|
||||
final fiatAmountController = fiatCurrencyKey.currentState!.amountController;
|
||||
final cryptoAmountController = cryptoCurrencyKey.currentState!.amountController;
|
||||
final cryptoAddressController = cryptoCurrencyKey.currentState!.addressController;
|
||||
|
||||
_onCurrencyChange(buySellViewModel.cryptoCurrency, buySellViewModel, cryptoCurrencyKey);
|
||||
_onCurrencyChange(buySellViewModel.fiatCurrency, buySellViewModel, fiatCurrencyKey);
|
||||
|
||||
reaction(
|
||||
(_) => buySellViewModel.wallet.name,
|
||||
(_) => _onWalletNameChange(
|
||||
buySellViewModel, buySellViewModel.cryptoCurrency, cryptoCurrencyKey));
|
||||
|
||||
reaction((_) => buySellViewModel.cryptoCurrency,
|
||||
(currency) => _onCurrencyChange(currency, buySellViewModel, cryptoCurrencyKey));
|
||||
|
||||
reaction((_) => buySellViewModel.fiatCurrency,
|
||||
(currency) => _onCurrencyChange(currency, buySellViewModel, fiatCurrencyKey));
|
||||
|
||||
reaction((_) => buySellViewModel.fiatAmount, (amount) {
|
||||
if (fiatCurrencyKey.currentState!.amountController.text != amount) {
|
||||
fiatCurrencyKey.currentState!.amountController.text = amount;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => buySellViewModel.isCryptoCurrencyAddressEnabled, (isEnabled) {
|
||||
cryptoCurrencyKey.currentState!.isAddressEditable(isEditable: isEnabled);
|
||||
});
|
||||
|
||||
reaction((_) => buySellViewModel.cryptoAmount, (amount) {
|
||||
if (cryptoCurrencyKey.currentState!.amountController.text != amount) {
|
||||
cryptoCurrencyKey.currentState!.amountController.text = amount;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => buySellViewModel.cryptoCurrencyAddress, (String address) {
|
||||
if (cryptoAddressController != address) {
|
||||
cryptoCurrencyKey.currentState!.addressController.text = address;
|
||||
}
|
||||
});
|
||||
|
||||
fiatAmountController.addListener(() {
|
||||
if (fiatAmountController.text != buySellViewModel.fiatAmount) {
|
||||
buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
|
||||
}
|
||||
});
|
||||
|
||||
cryptoAmountController.addListener(() {
|
||||
if (cryptoAmountController.text != buySellViewModel.cryptoAmount) {
|
||||
buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
|
||||
}
|
||||
});
|
||||
|
||||
cryptoAddressController.addListener(() {
|
||||
buySellViewModel.changeCryptoCurrencyAddress(cryptoAddressController.text);
|
||||
});
|
||||
|
||||
_cryptoAddressFocus.addListener(() async {
|
||||
if (!_cryptoAddressFocus.hasFocus && cryptoAddressController.text.isNotEmpty) {
|
||||
final domain = cryptoAddressController.text;
|
||||
buySellViewModel.cryptoCurrencyAddress =
|
||||
await fetchParsedAddress(context, domain, buySellViewModel.cryptoCurrency);
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => buySellViewModel.wallet.walletAddresses.addressForExchange, (String address) {
|
||||
if (buySellViewModel.cryptoCurrency == CryptoCurrency.xmr) {
|
||||
cryptoCurrencyKey.currentState!.changeAddress(address: address);
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => buySellViewModel.isReadyToTrade, (bool isReady) {
|
||||
if (isReady) {
|
||||
if (buySellViewModel.skipIsReadyToTradeReaction) {
|
||||
buySellViewModel.skipIsReadyToTradeReaction = false;
|
||||
return;
|
||||
}
|
||||
if (cryptoAmountController.text.isNotEmpty &&
|
||||
cryptoAmountController.text != S.current.fetching) {
|
||||
buySellViewModel.changeCryptoAmount(amount: cryptoAmountController.text);
|
||||
} else if (fiatAmountController.text.isNotEmpty &&
|
||||
fiatAmountController.text != S.current.fetching) {
|
||||
buySellViewModel.changeFiatAmount(amount: fiatAmountController.text);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_isReactionsSet = true;
|
||||
}
|
||||
|
||||
void _onCurrencyChange(
|
||||
Currency currency, BuySellViewModel buySellViewModel, GlobalKey<ExchangeCardState> key) {
|
||||
final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
|
||||
|
||||
key.currentState!.changeSelectedCurrency(currency);
|
||||
key.currentState!.changeWalletName(isCurrentTypeWallet ? buySellViewModel.wallet.name : '');
|
||||
|
||||
key.currentState!.changeAddress(
|
||||
address:
|
||||
isCurrentTypeWallet ? buySellViewModel.wallet.walletAddresses.addressForExchange : '');
|
||||
|
||||
key.currentState!.changeAmount(amount: '');
|
||||
}
|
||||
|
||||
void _onWalletNameChange(BuySellViewModel buySellViewModel, CryptoCurrency currency,
|
||||
GlobalKey<ExchangeCardState> key) {
|
||||
final isCurrentTypeWallet = currency == buySellViewModel.wallet.currency;
|
||||
|
||||
if (isCurrentTypeWallet) {
|
||||
key.currentState!.changeWalletName(buySellViewModel.wallet.name);
|
||||
key.currentState!.addressController.text =
|
||||
buySellViewModel.wallet.walletAddresses.addressForExchange;
|
||||
} else if (key.currentState!.addressController.text ==
|
||||
buySellViewModel.wallet.walletAddresses.addressForExchange) {
|
||||
key.currentState!.changeWalletName('');
|
||||
key.currentState!.addressController.text = '';
|
||||
}
|
||||
}
|
||||
|
||||
void disposeBestRateSync() => {};
|
||||
|
||||
Widget _exchangeCardsSection(BuildContext context) {
|
||||
final fiatExchangeCard = Observer(
|
||||
builder: (_) => ExchangeCard(
|
||||
cardInstanceName: 'fiat_currency_trade_card',
|
||||
onDispose: disposeBestRateSync,
|
||||
amountFocusNode: _fiatAmountFocus,
|
||||
key: fiatCurrencyKey,
|
||||
title: 'FIAT ${S.of(context).amount}',
|
||||
initialCurrency: buySellViewModel.fiatCurrency,
|
||||
initialWalletName: '',
|
||||
initialAddress: '',
|
||||
initialIsAmountEditable: true,
|
||||
isAmountEstimated: false,
|
||||
currencyRowPadding: EdgeInsets.zero,
|
||||
addressRowPadding: EdgeInsets.zero,
|
||||
isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
|
||||
showAddressField: false,
|
||||
showLimitsField: false,
|
||||
currencies: buySellViewModel.fiatCurrencies,
|
||||
onCurrencySelected: (currency) => buySellViewModel.changeFiatCurrency(currency: currency),
|
||||
imageArrow: Image.asset(
|
||||
'assets/images/arrow_bottom_purple_icon.png',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
height: 8,
|
||||
),
|
||||
currencyButtonColor: Colors.transparent,
|
||||
addressButtonsColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderColor: Theme.of(context).colorScheme.outlineVariant,
|
||||
onPushPasteButton: (context) async {},
|
||||
onPushAddressBookButton: (context) async {},
|
||||
fillColor: buySellViewModel.isBuyAction
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
),
|
||||
);
|
||||
|
||||
final cryptoExchangeCard = Observer(
|
||||
builder: (_) => ExchangeCard(
|
||||
cardInstanceName: 'crypto_currency_trade_card',
|
||||
onDispose: disposeBestRateSync,
|
||||
amountFocusNode: _cryptoAmountFocus,
|
||||
addressFocusNode: _cryptoAddressFocus,
|
||||
key: cryptoCurrencyKey,
|
||||
title: 'Crypto ${S.of(context).amount}',
|
||||
initialCurrency: buySellViewModel.cryptoCurrency,
|
||||
initialWalletName: '',
|
||||
initialAddress: buySellViewModel.cryptoCurrency == buySellViewModel.wallet.currency
|
||||
? buySellViewModel.wallet.walletAddresses.addressForExchange
|
||||
: buySellViewModel.cryptoCurrencyAddress,
|
||||
initialIsAmountEditable: true,
|
||||
isAmountEstimated: true,
|
||||
showLimitsField: false,
|
||||
currencyRowPadding: EdgeInsets.zero,
|
||||
addressRowPadding: EdgeInsets.zero,
|
||||
isMoneroWallet: buySellViewModel.wallet == WalletType.monero,
|
||||
currencies: buySellViewModel.cryptoCurrencies,
|
||||
onCurrencySelected: (currency) => buySellViewModel.changeCryptoCurrency(currency: currency),
|
||||
imageArrow: Image.asset(
|
||||
'assets/images/arrow_bottom_green.png',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
height: 8,
|
||||
),
|
||||
currencyButtonColor: Colors.transparent,
|
||||
addressButtonsColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderColor: Theme.of(context).colorScheme.outlineVariant,
|
||||
addressTextFieldValidator: AddressValidator(type: buySellViewModel.cryptoCurrency),
|
||||
onPushPasteButton: (context) async {},
|
||||
onPushAddressBookButton: (context) async {},
|
||||
fillColor: buySellViewModel.isBuyAction
|
||||
? Theme.of(context).colorScheme.surfaceContainerLow
|
||||
: Theme.of(context).colorScheme.surfaceContainer,
|
||||
useSatoshis: buySellViewModel.useSatoshi,
|
||||
),
|
||||
);
|
||||
|
||||
if (responsiveLayoutUtil.shouldRenderMobileUI) {
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
if (buySellViewModel.isBuyAction) {
|
||||
return MobileExchangeCardsSection(
|
||||
firstExchangeCard: fiatExchangeCard,
|
||||
secondExchangeCard: cryptoExchangeCard,
|
||||
onBuyTap: () => null,
|
||||
onSellTap: () =>
|
||||
buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
|
||||
isBuySellOption: true,
|
||||
);
|
||||
} else {
|
||||
return MobileExchangeCardsSection(
|
||||
firstExchangeCard: cryptoExchangeCard,
|
||||
secondExchangeCard: fiatExchangeCard,
|
||||
onBuyTap: () =>
|
||||
!buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
|
||||
onSellTap: () => null,
|
||||
isBuySellOption: true,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
if (buySellViewModel.isBuyAction) {
|
||||
return DesktopExchangeCardsSection(
|
||||
firstExchangeCard: fiatExchangeCard,
|
||||
secondExchangeCard: cryptoExchangeCard,
|
||||
onBuyTap: () => null,
|
||||
onSellTap: () =>
|
||||
buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
|
||||
isBuySellOption: true,
|
||||
);
|
||||
} else {
|
||||
return DesktopExchangeCardsSection(
|
||||
firstExchangeCard: cryptoExchangeCard,
|
||||
secondExchangeCard: fiatExchangeCard,
|
||||
onBuyTap: () =>
|
||||
!buySellViewModel.isBuyAction ? buySellViewModel.changeBuySellAction() : null,
|
||||
onSellTap: () => null,
|
||||
isBuySellOption: true,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<String> fetchParsedAddress(
|
||||
BuildContext context,
|
||||
String domain,
|
||||
CryptoCurrency currency,
|
||||
) async {
|
||||
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
|
||||
final address = await extractAddressFromParsed(context, parsedAddress);
|
||||
return address;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:hash_wallet/buy/moonpay/moonpay_provider.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/store/dashboard/orders_store.dart';
|
||||
import 'package:hash_wallet/view_model/buy/buy_view_model.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
|
||||
class BuyWebViewPage extends BasePage {
|
||||
BuyWebViewPage({required this.buyViewModel, required this.ordersStore, required this.url});
|
||||
|
||||
final OrdersStore ordersStore;
|
||||
final String url;
|
||||
final BuyViewModel buyViewModel;
|
||||
|
||||
@override
|
||||
String get title => S.current.buy;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) =>
|
||||
BuyWebViewPageBody(buyViewModel, ordersStore: ordersStore, url: url);
|
||||
}
|
||||
|
||||
class BuyWebViewPageBody extends StatefulWidget {
|
||||
BuyWebViewPageBody(this.buyViewModel, {required this.ordersStore, this.url});
|
||||
|
||||
final OrdersStore ordersStore;
|
||||
final String? url;
|
||||
final BuyViewModel buyViewModel;
|
||||
|
||||
@override
|
||||
BuyWebViewPageBodyState createState() => BuyWebViewPageBodyState();
|
||||
}
|
||||
|
||||
class BuyWebViewPageBodyState extends State<BuyWebViewPageBody> {
|
||||
BuyWebViewPageBodyState()
|
||||
: _webViewkey = GlobalKey(),
|
||||
_isSaving = false,
|
||||
orderId = '';
|
||||
|
||||
String orderId;
|
||||
InAppWebViewController? _webViewController;
|
||||
GlobalKey _webViewkey;
|
||||
Timer? _timer;
|
||||
bool _isSaving;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_webViewkey = GlobalKey();
|
||||
_isSaving = false;
|
||||
widget.ordersStore.orderId = '';
|
||||
|
||||
if (widget.buyViewModel.selectedProvider is MoonPayProvider) {
|
||||
_saveOrder(keyword: 'transactionId', splitSymbol: '=');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InAppWebView(
|
||||
key: _webViewkey,
|
||||
initialSettings: InAppWebViewSettings(
|
||||
transparentBackground: true,
|
||||
),
|
||||
initialUrlRequest: URLRequest(url: WebUri(widget.url ?? '')),
|
||||
onWebViewCreated: (InAppWebViewController controller) =>
|
||||
setState(() => _webViewController = controller));
|
||||
}
|
||||
|
||||
void _saveOrder({required String keyword, required String splitSymbol}) {
|
||||
_timer?.cancel();
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) async {
|
||||
try {
|
||||
if (_webViewController == null || _isSaving) {
|
||||
return;
|
||||
}
|
||||
|
||||
final url = (await _webViewController!.getUrl())?.toString();
|
||||
if (url == null) {
|
||||
throw Exception('_saveOrder: Url is null');
|
||||
}
|
||||
|
||||
if (url.contains(keyword)) {
|
||||
final urlParts = url.split(splitSymbol);
|
||||
orderId = urlParts.last;
|
||||
widget.ordersStore.orderId = orderId;
|
||||
|
||||
if (orderId.isNotEmpty) {
|
||||
_isSaving = true;
|
||||
await widget.buyViewModel.saveOrder(orderId);
|
||||
timer.cancel();
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
_isSaving = false;
|
||||
printV(e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'package:hash_wallet/core/selectable_option.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/select_options_page.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
|
||||
class PaymentMethodOptionsPage extends SelectOptionsPage {
|
||||
PaymentMethodOptionsPage({required this.items, this.pickAnOption});
|
||||
|
||||
final List<SelectableItem> items;
|
||||
final Function(SelectableOption option)? pickAnOption;
|
||||
|
||||
@override
|
||||
String get pageTitle => S.current.choose_a_payment_method;
|
||||
|
||||
@override
|
||||
EdgeInsets? get contentPadding => null;
|
||||
|
||||
@override
|
||||
EdgeInsets? get tilePadding => EdgeInsets.only(top: 12);
|
||||
|
||||
@override
|
||||
EdgeInsets? get innerPadding => EdgeInsets.symmetric(horizontal: 24, vertical: 12);
|
||||
|
||||
@override
|
||||
double? get imageHeight => null;
|
||||
|
||||
@override
|
||||
double? get imageWidth => null;
|
||||
|
||||
@override
|
||||
Color? get selectedBackgroundColor => null;
|
||||
|
||||
@override
|
||||
double? get tileBorderRadius => 30;
|
||||
|
||||
@override
|
||||
String get bottomSectionText => '';
|
||||
|
||||
@override
|
||||
void Function(SelectableOption option)? get onOptionTap => pickAnOption;
|
||||
|
||||
@override
|
||||
String get primaryButtonText => S.current.confirm;
|
||||
|
||||
@override
|
||||
void Function(BuildContext context)? get primaryButtonAction => null;
|
||||
}
|
||||
@@ -1,87 +0,0 @@
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/entities/preferences_key.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:hash_wallet/utils/permission_handler.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_inappwebview/flutter_inappwebview.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class WebViewPage extends BasePage {
|
||||
WebViewPage(this._title, this._url);
|
||||
|
||||
final String _title;
|
||||
final Uri _url;
|
||||
|
||||
@override
|
||||
String get title => _title;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return WebViewPageBody(_title, _url);
|
||||
}
|
||||
}
|
||||
|
||||
class WebViewPageBody extends StatefulWidget {
|
||||
WebViewPageBody(this.title, this.uri);
|
||||
|
||||
final String title;
|
||||
final Uri uri;
|
||||
|
||||
@override
|
||||
WebViewPageBodyState createState() => WebViewPageBodyState();
|
||||
}
|
||||
|
||||
class WebViewPageBodyState extends State<WebViewPageBody> {
|
||||
WebViewPageBodyState();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InAppWebView(
|
||||
initialSettings: InAppWebViewSettings(
|
||||
transparentBackground: true,
|
||||
),
|
||||
initialUrlRequest: URLRequest(url: WebUri.uri(widget.uri)),
|
||||
onPermissionRequest: (controller, request) async {
|
||||
final sharedPrefs = getIt.get<SharedPreferences>();
|
||||
|
||||
if (sharedPrefs.getBool(PreferencesKey.showCameraConsent) ?? true) {
|
||||
final bool userConsent = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: S.of(context).privacy,
|
||||
alertContent: S.of(context).camera_consent(widget.title),
|
||||
rightButtonText: S.of(context).agree,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
actionRightButton: () => Navigator.of(context).pop(true),
|
||||
actionLeftButton: () => Navigator.of(context).pop(false));
|
||||
}) ??
|
||||
false;
|
||||
|
||||
/// if user did NOT give the consent then return permission denied
|
||||
if (!userConsent) {
|
||||
return PermissionResponse(
|
||||
resources: request.resources,
|
||||
action: PermissionResponseAction.DENY,
|
||||
);
|
||||
}
|
||||
|
||||
sharedPrefs.setBool(PreferencesKey.showCameraConsent, false);
|
||||
}
|
||||
|
||||
bool permissionGranted =
|
||||
await PermissionHandler.checkPermission(Permission.camera, context);
|
||||
|
||||
return PermissionResponse(
|
||||
resources: request.resources,
|
||||
action:
|
||||
permissionGranted ? PermissionResponseAction.GRANT : PermissionResponseAction.DENY,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,53 +1,55 @@
|
||||
import 'dart:async';
|
||||
import 'dart:io';
|
||||
import "dart:async";
|
||||
import "dart:io";
|
||||
|
||||
import 'package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/screens/connect_device/widgets/device_tile.dart';
|
||||
import 'package:hash_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import "package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/main.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import "package:hash_wallet/src/screens/base_page.dart";
|
||||
import "package:hash_wallet/src/widgets/bottom_sheet/info_steps_bottom_sheet_widget.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/src/widgets/primary_button.dart";
|
||||
import "package:hash_wallet/themes/core/material_base_theme.dart";
|
||||
import "package:hash_wallet/utils/responsive_layout_util.dart";
|
||||
import "package:hash_wallet/view_model/hardware_wallet/hardware_wallet_view_model.dart";
|
||||
import "package:cw_core/utils/print_verbose.dart";
|
||||
import "package:cw_core/wallet_info.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/cupertino.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
typedef OnConnectDevice = void Function(BuildContext, HardwareWalletViewModel);
|
||||
|
||||
class ConnectDevicePageParams {
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final bool isReconnect;
|
||||
final HardwareWalletType hardwareWalletType;
|
||||
|
||||
ConnectDevicePageParams({
|
||||
const ConnectDevicePageParams({
|
||||
required this.walletType,
|
||||
required this.hardwareWalletType,
|
||||
required this.onConnectDevice,
|
||||
this.allowChangeWallet = false,
|
||||
this.isReconnect = true,
|
||||
});
|
||||
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final bool isReconnect;
|
||||
final HardwareWalletType hardwareWalletType;
|
||||
}
|
||||
|
||||
class ConnectDevicePage extends BasePage {
|
||||
ConnectDevicePage(ConnectDevicePageParams params, this.hardwareWalletVM)
|
||||
: walletType = params.walletType,
|
||||
onConnectDevice = params.onConnectDevice,
|
||||
allowChangeWallet = params.allowChangeWallet || params.isReconnect,
|
||||
isReconnect = params.isReconnect;
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final bool isReconnect;
|
||||
final HardwareWalletViewModel hardwareWalletVM;
|
||||
|
||||
ConnectDevicePage(ConnectDevicePageParams params, this.hardwareWalletVM)
|
||||
: walletType = params.walletType,
|
||||
onConnectDevice = params.onConnectDevice,
|
||||
allowChangeWallet = params.allowChangeWallet,
|
||||
isReconnect = params.isReconnect;
|
||||
|
||||
@override
|
||||
String get title => isReconnect
|
||||
? S.current.reconnect_your_hardware_wallet
|
||||
@@ -58,31 +60,32 @@ class ConnectDevicePage extends BasePage {
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => PopScope(
|
||||
canPop: !isReconnect,
|
||||
child: ConnectDevicePageBody(
|
||||
walletType,
|
||||
onConnectDevice,
|
||||
allowChangeWallet,
|
||||
hardwareWalletVM,
|
||||
currentTheme,
|
||||
));
|
||||
canPop: !isReconnect,
|
||||
child: ConnectDevicePageBody(
|
||||
walletType,
|
||||
onConnectDevice,
|
||||
hardwareWalletVM,
|
||||
currentTheme,
|
||||
allowChangeWallet: allowChangeWallet,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ConnectDevicePageBody extends StatefulWidget {
|
||||
const ConnectDevicePageBody(
|
||||
this.walletType,
|
||||
this.onConnectDevice,
|
||||
this.hardwareWalletVM,
|
||||
this.currentTheme, {
|
||||
this.allowChangeWallet = false,
|
||||
});
|
||||
|
||||
final WalletType walletType;
|
||||
final OnConnectDevice onConnectDevice;
|
||||
final bool allowChangeWallet;
|
||||
final HardwareWalletViewModel hardwareWalletVM;
|
||||
final MaterialThemeBase currentTheme;
|
||||
|
||||
const ConnectDevicePageBody(
|
||||
this.walletType,
|
||||
this.onConnectDevice,
|
||||
this.allowChangeWallet,
|
||||
this.hardwareWalletVM,
|
||||
this.currentTheme,
|
||||
);
|
||||
|
||||
@override
|
||||
ConnectDevicePageBodyState createState() => ConnectDevicePageBodyState();
|
||||
}
|
||||
@@ -91,10 +94,12 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
var bleDevices = <HardwareWalletDevice>[];
|
||||
var usbDevices = <HardwareWalletDevice>[];
|
||||
|
||||
late Timer? _usbRefreshTimer = null;
|
||||
late Timer? _bleRefreshTimer = null;
|
||||
late Timer? _bleStateTimer = null;
|
||||
late StreamSubscription<HardwareWalletDevice>? _bleRefresh = null;
|
||||
List<HardwareWalletDevice> get allDevices => [...bleDevices, ...usbDevices];
|
||||
|
||||
Timer? _usbRefreshTimer;
|
||||
Timer? _bleRefreshTimer;
|
||||
Timer? _bleStateTimer;
|
||||
StreamSubscription<HardwareWalletDevice>? _bleRefresh;
|
||||
|
||||
bool longWait = false;
|
||||
Timer? _longWaitTimer;
|
||||
@@ -103,24 +108,48 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_bleStateTimer =
|
||||
Timer.periodic(Duration(seconds: 1), (_) => widget.hardwareWalletVM.updateBleState());
|
||||
_loadConnectedDevices();
|
||||
|
||||
_bleRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshBleDevices());
|
||||
_bleStateTimer = Timer.periodic(
|
||||
const Duration(seconds: 1),
|
||||
(_) => widget.hardwareWalletVM.updateBleState(),
|
||||
);
|
||||
|
||||
_bleRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshBleDevices());
|
||||
|
||||
if (Platform.isAndroid) {
|
||||
_usbRefreshTimer = Timer.periodic(Duration(seconds: 1), (_) => _refreshUsbDevices());
|
||||
_usbRefreshTimer = Timer.periodic(const Duration(seconds: 1), (_) => _refreshUsbDevices());
|
||||
}
|
||||
|
||||
if (widget.hardwareWalletVM.hasBluetooth) {
|
||||
_longWaitTimer = Timer(Duration(seconds: 10), () {
|
||||
if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty)
|
||||
widget.hardwareWalletVM.updateBleState();
|
||||
|
||||
_longWaitTimer = Timer(const Duration(seconds: 10), () {
|
||||
if (widget.hardwareWalletVM.isBleEnabled && bleDevices.isEmpty) {
|
||||
setState(() => longWait = true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _loadConnectedDevices() async {
|
||||
try {
|
||||
final connected = await widget.hardwareWalletVM.getConnectedBleDevices();
|
||||
|
||||
printV(connected);
|
||||
if (!mounted || connected.isEmpty) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
bleDevices.addAll(connected);
|
||||
longWait = false;
|
||||
});
|
||||
} catch (e) {
|
||||
printV(e);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_bleRefreshTimer?.cancel();
|
||||
@@ -130,23 +159,40 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
_longWaitTimer?.cancel();
|
||||
|
||||
widget.hardwareWalletVM.stopScanning();
|
||||
_isConnectPressed = false;
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
var _isRefreshingUsb = false;
|
||||
Future<void> _refreshUsbDevices() async {
|
||||
final dev = await widget.hardwareWalletVM.getAllUsbDevices();
|
||||
if (usbDevices.length != dev.length) setState(() => usbDevices = dev);
|
||||
if (_isRefreshingUsb) {
|
||||
return;
|
||||
}
|
||||
_isRefreshingUsb = true;
|
||||
try {
|
||||
final dev = await widget.hardwareWalletVM.getAllUsbDevices();
|
||||
|
||||
if (usbDevices.length != dev.length) {
|
||||
setState(() => usbDevices = dev);
|
||||
}
|
||||
} catch(e) {
|
||||
printV(e);
|
||||
}
|
||||
_isRefreshingUsb = false;
|
||||
}
|
||||
|
||||
Future<void> _refreshBleDevices() async {
|
||||
try {
|
||||
if (widget.hardwareWalletVM.isBleEnabled) {
|
||||
_bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen((device) => setState(() {
|
||||
bleDevices.add(device);
|
||||
if (longWait) longWait = false;
|
||||
}))
|
||||
..onError((e) {
|
||||
throw e.toString();
|
||||
_bleRefresh = widget.hardwareWalletVM.scanForBleDevices().listen(
|
||||
(device) => setState(() {
|
||||
bleDevices.add(device);
|
||||
if (longWait) {
|
||||
longWait = false;
|
||||
}
|
||||
}),
|
||||
)..onError((e) {
|
||||
printV(e);
|
||||
});
|
||||
_bleRefreshTimer?.cancel();
|
||||
_bleRefreshTimer = null;
|
||||
@@ -156,33 +202,66 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
}
|
||||
}
|
||||
|
||||
var _isConnectPressed = false;
|
||||
|
||||
Future<void> _connectToDevice(HardwareWalletDevice device) async {
|
||||
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
|
||||
if (isConnected) widget.onConnectDevice(context, widget.hardwareWalletVM);
|
||||
if (_isConnectPressed) {
|
||||
return;
|
||||
}
|
||||
|
||||
_isConnectPressed = true;
|
||||
try {
|
||||
final isConnected = await widget.hardwareWalletVM.connectDevice(device, widget.walletType);
|
||||
_isConnectPressed = false;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (isConnected) {
|
||||
widget.onConnectDevice(navigatorKey.currentContext!, widget.hardwareWalletVM);
|
||||
}
|
||||
});
|
||||
} catch (_) {
|
||||
_isConnectPressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
String _getDeviceTileLeading(HardwareWalletDeviceType deviceType) {
|
||||
printV(deviceType);
|
||||
switch (deviceType) {
|
||||
case HardwareWalletDeviceType.ledgerNanoX:
|
||||
return 'assets/images/hardware_wallet/ledger_nano_x.png';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
case HardwareWalletDeviceType.ledgerNanoGen5:
|
||||
return 'assets/images/hardware_wallet/device_ledger_nano_gen_5.svg';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_gen_5.svg";
|
||||
case HardwareWalletDeviceType.ledgerStax:
|
||||
return 'assets/images/hardware_wallet/ledger_stax.png';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_stax.svg";
|
||||
case HardwareWalletDeviceType.ledgerFlex:
|
||||
return 'assets/images/hardware_wallet/ledger_flex.png';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_flex.svg";
|
||||
case HardwareWalletDeviceType.BitBox02:
|
||||
return 'assets/images/hardware_wallet/device_bitbox.svg';
|
||||
case HardwareWalletDeviceType.BitBox02Nova:
|
||||
return "assets/new-ui/hardware_wallets/device_bitbox.svg";
|
||||
case HardwareWalletDeviceType.trezorModelOne:
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_model_one.svg";
|
||||
case HardwareWalletDeviceType.trezorModelT:
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_model_t.svg";
|
||||
case HardwareWalletDeviceType.trezorSafe3:
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_3.svg";
|
||||
case HardwareWalletDeviceType.trezorSafe5:
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_5.svg";
|
||||
case HardwareWalletDeviceType.trezorSafe7:
|
||||
return "assets/new-ui/hardware_wallets/device_trezor_safe_7.svg";
|
||||
|
||||
default:
|
||||
return 'assets/images/hardware_wallet/ledger_nano_x.png';
|
||||
return "assets/new-ui/hardware_wallets/device_ledger_nano_x.svg";
|
||||
}
|
||||
}
|
||||
|
||||
String get description {
|
||||
if (!Platform.isAndroid) return S.of(context).connect_your_hardware_wallet_ble;
|
||||
if (!Platform.isAndroid) {
|
||||
return S.of(context).connect_your_hardware_wallet_ble;
|
||||
}
|
||||
|
||||
if (widget.hardwareWalletVM.hasBluetooth) return S.of(context).connect_your_hardware_wallet;
|
||||
if (widget.hardwareWalletVM.hasBluetooth) {
|
||||
return S.of(context).connect_your_hardware_wallet;
|
||||
}
|
||||
|
||||
return S.of(context).connect_your_hardware_wallet_usb;
|
||||
}
|
||||
@@ -191,38 +270,26 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
Widget build(BuildContext context) => Center(
|
||||
child: Container(
|
||||
width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
|
||||
padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24),
|
||||
padding: const EdgeInsets.symmetric(vertical: 24, horizontal: 24),
|
||||
child: Column(
|
||||
children: [
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
description,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
Offstage(
|
||||
offstage: !longWait,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
S.of(context).if_you_dont_see_your_device,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
const HashImageWidget(imageUrl: "assets/new-ui/hardware_wallet.svg"),
|
||||
Text(
|
||||
description,
|
||||
style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => Offstage(
|
||||
offstage: widget.hardwareWalletVM.isBleEnabled ||
|
||||
!widget.hardwareWalletVM.hasBluetooth,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
padding: const EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Text(
|
||||
S.of(context).ledger_please_enable_bluetooth,
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
@@ -231,74 +298,110 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (bleDevices.length > 0) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
S.of(context).bluetooth,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
...bleDevices
|
||||
.map(
|
||||
(device) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 20),
|
||||
child: DeviceTile(
|
||||
onPressed: () => _connectToDevice(device),
|
||||
title: device.name,
|
||||
leading: _getDeviceTileLeading(device.type),
|
||||
connectionType: device.connectionType,
|
||||
child: ListView.separated(
|
||||
shrinkWrap: true,
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) {
|
||||
final item = allDevices[index];
|
||||
return GestureDetector(
|
||||
onTap: () => _connectToDevice(item),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: SizedBox(
|
||||
height: 48,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (widget.hardwareWalletVM.isConnecting) {
|
||||
return CupertinoActivityIndicator(
|
||||
animating: true,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurfaceVariant,
|
||||
radius: 12,
|
||||
);
|
||||
}
|
||||
|
||||
return HashImageWidget(
|
||||
imageUrl: _getDeviceTileLeading(item.type),
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
Text(item.name),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (item.connectionType ==
|
||||
HardwareWalletConnectionType.ble)
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/bluetooth.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList()
|
||||
],
|
||||
if (usbDevices.length > 0) ...[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 20, right: 20, bottom: 20),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
child: Text(
|
||||
S.of(context).usb,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
itemCount: allDevices.length,
|
||||
),
|
||||
...usbDevices
|
||||
.map(
|
||||
(device) => Padding(
|
||||
padding: EdgeInsets.only(bottom: 20),
|
||||
child: DeviceTile(
|
||||
onPressed: () => _connectToDevice(device),
|
||||
title: device.name,
|
||||
leading: _getDeviceTileLeading(device.type),
|
||||
connectionType: device.connectionType,
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
if (widget.allowChangeWallet) ...[
|
||||
PrimaryButton(
|
||||
text: S.of(context).wallets,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: _onChangeWallet,
|
||||
)
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
NewPrimaryButton(
|
||||
text: S.of(context).how_to_connect,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
textColor: Theme.of(context).colorScheme.primary,
|
||||
onPressed: () => _onHowToConnect(context),
|
||||
)
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -316,17 +419,25 @@ class ConnectDevicePageBodyState extends State<ConnectDevicePageBody> {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext bottomSheetContext) => InfoStepsBottomSheet(
|
||||
builder: (_) => InfoStepsBottomSheet(
|
||||
titleText: S.of(context).how_to_connect,
|
||||
steps: [
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step1_power.svg',
|
||||
S.of(context).connect_hw_info_step_1),
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step2_connect.svg',
|
||||
S.of(context).connect_hw_info_step_2),
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step3_unlock.svg',
|
||||
S.of(context).connect_hw_info_step_3),
|
||||
InfoStep('assets/images/wallet_connect_step_icons/step4_select.svg',
|
||||
S.of(context).connect_hw_info_step_4),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step1_power.svg",
|
||||
S.of(context).connect_hw_info_step_1,
|
||||
),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step2_connect.svg",
|
||||
S.of(context).connect_hw_info_step_2,
|
||||
),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step3_unlock.svg",
|
||||
S.of(context).connect_hw_info_step_3,
|
||||
),
|
||||
InfoStep(
|
||||
"assets/images/wallet_connect_step_icons/step4_select.svg",
|
||||
S.of(context).connect_hw_info_step_4,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
// import 'dart:typed_data';
|
||||
//
|
||||
// import 'package:basic_utils/basic_utils.dart';
|
||||
// import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
// import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
// import 'package:hash_wallet/src/screens/connect_device/widgets/device_tile.dart';
|
||||
// import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
// import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
// import 'package:flutter/material.dart';
|
||||
// import 'package:ledger_flutter_plus/ledger_flutter_plus.dart';
|
||||
// import 'package:ledger_litecoin/ledger_litecoin.dart';
|
||||
// import 'package:permission_handler/permission_handler.dart';
|
||||
//
|
||||
// class DebugDevicePage extends BasePage {
|
||||
// @override
|
||||
// String get title => "Connect Ledger";
|
||||
//
|
||||
// @override
|
||||
// Widget body(BuildContext context) => DebugDevicePageBody();
|
||||
// }
|
||||
//
|
||||
// class DebugDevicePageBody extends StatefulWidget {
|
||||
// @override
|
||||
// DebugDevicePageBodyState createState() => DebugDevicePageBodyState();
|
||||
// }
|
||||
//
|
||||
// class DebugDevicePageBodyState extends State<DebugDevicePageBody> {
|
||||
// final imageLedger = Image.asset(
|
||||
// 'assets/images/ledger_icon_black.png',
|
||||
// width: 40,
|
||||
// );
|
||||
// final ledger = Ledger(
|
||||
// options: LedgerOptions(
|
||||
// scanMode: ScanMode.balanced,
|
||||
// maxScanDuration: const Duration(milliseconds: 5000),
|
||||
// ),
|
||||
// onPermissionRequest: (status) async {
|
||||
// Map<Permission, PermissionStatus> statuses = await [
|
||||
// // Permission.location,
|
||||
// Permission.bluetoothScan,
|
||||
// Permission.bluetoothConnect,
|
||||
// Permission.bluetoothAdvertise,
|
||||
// ].request();
|
||||
//
|
||||
// if (status != BleStatus.ready) {
|
||||
// return false;
|
||||
// }
|
||||
//
|
||||
// return statuses.values.where((status) => status.isDenied).isEmpty;
|
||||
// },
|
||||
// );
|
||||
//
|
||||
// // late BitcoinLedgerApp btc;
|
||||
// late LitecoinLedgerApp ltc;
|
||||
//
|
||||
// var devices = <LedgerDevice>[];
|
||||
// var status = "";
|
||||
// var counter = 0;
|
||||
// LedgerDevice? selectedDevice = null;
|
||||
//
|
||||
// @override
|
||||
// void initState() {
|
||||
// super.initState();
|
||||
// // btc = BitcoinLedgerApp(ledger);
|
||||
// ltc = LitecoinLedgerApp(ledger);
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// void dispose() {
|
||||
// super.dispose();
|
||||
// ledger.close(ConnectionType.ble);
|
||||
// ledger.close(ConnectionType.usb);
|
||||
// }
|
||||
//
|
||||
// Future<void> reconnectCurrentDevice() async {
|
||||
// // await ledger.disconnect(selectedDevice!);
|
||||
// // await ledger.connect(selectedDevice!);
|
||||
// }
|
||||
//
|
||||
// Future<void> disconnectCurrentDevice() async {
|
||||
// await ledger.disconnect(selectedDevice!);
|
||||
// setState(() => selectedDevice = null);
|
||||
// }
|
||||
//
|
||||
// @override
|
||||
// Widget build(BuildContext context) {
|
||||
// final imageLedger = 'assets/images/hardware_wallet/ledger_nano_x.png';
|
||||
//
|
||||
// return Center(
|
||||
// child: Container(
|
||||
// width: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
|
||||
// height: double.infinity,
|
||||
// padding: EdgeInsets.symmetric(vertical: 24, horizontal: 24),
|
||||
// child: SingleChildScrollView(
|
||||
// child: Column(
|
||||
// children: [
|
||||
// Padding(
|
||||
// padding: EdgeInsets.only(top: 20),
|
||||
// child: Text(status),
|
||||
// ),
|
||||
// if (selectedDevice != null) ...[
|
||||
// DebugButton(
|
||||
// title: "Get Version",
|
||||
// method: "Version",
|
||||
// // func: () async => await btc.getVersion(selectedDevice!),
|
||||
// func: () async => await ltc.getVersion(selectedDevice!),
|
||||
// ),
|
||||
// DebugButton(
|
||||
// title: "Get Wallet Address",
|
||||
// method: "Wallet Address",
|
||||
// func: () async {
|
||||
// setState(() => counter++);
|
||||
// final derivationPath = "m/84'/2'/0'/0/0";
|
||||
// return await ltc.getAccounts(selectedDevice!,
|
||||
// accountsDerivationPath: derivationPath);
|
||||
// // return await btc.getAccounts(selectedDevice!, accountsDerivationPath: derivationPath);
|
||||
// // return await ethereum!.getHardwareWalletAccounts(selectedDevice!);
|
||||
// },
|
||||
// ),
|
||||
// DebugButton(
|
||||
// title: "Send Money",
|
||||
// method: "Raw Tx",
|
||||
// func: sendMoney
|
||||
// ),
|
||||
// Padding(
|
||||
// padding: EdgeInsets.only(top: 20),
|
||||
// child: PrimaryButton(
|
||||
// text: "Disconnect",
|
||||
// onPressed: () => disconnectCurrentDevice(),
|
||||
// color: Theme.of(context).colorScheme.primary,
|
||||
// textColor: Theme.of(context).colorScheme.onPrimary),
|
||||
// ),
|
||||
// ],
|
||||
// if (selectedDevice == null) ...[
|
||||
// ...devices
|
||||
// .map(
|
||||
// (device) => Padding(
|
||||
// padding: EdgeInsets.only(bottom: 20),
|
||||
// child: DeviceTile(
|
||||
// onPressed: () {
|
||||
// setState(() => selectedDevice = device);
|
||||
// ledger.connect(device);
|
||||
// },
|
||||
// title: device.name,
|
||||
// leading: imageLedger,
|
||||
// connectionType: device.connectionType,
|
||||
// ),
|
||||
// ),
|
||||
// )
|
||||
// .toList(),
|
||||
// PrimaryButton(
|
||||
// text: "Refresh BLE",
|
||||
// onPressed: () async {
|
||||
// setState(() => devices = []);
|
||||
// ledger.scan().listen((device) => setState(() {
|
||||
// devices.add(device);
|
||||
// }));
|
||||
// },
|
||||
// color: Theme.of(context).colorScheme.primary,
|
||||
// textColor: Theme.of(context).colorScheme.onPrimary),
|
||||
// Padding(
|
||||
// padding: EdgeInsets.only(top: 20),
|
||||
// child: PrimaryButton(
|
||||
// text: "Use USB",
|
||||
// onPressed: () async {
|
||||
// final dev = await ledger.listUsbDevices();
|
||||
// setState(() => devices = dev);
|
||||
// },
|
||||
// color: Theme.of(context).colorScheme.primary,
|
||||
// textColor: Theme.of(context).colorScheme.onPrimary),
|
||||
// ),
|
||||
// ],
|
||||
// ],
|
||||
// ),
|
||||
// )),
|
||||
// );
|
||||
// }
|
||||
//
|
||||
// Future<String> sendMoney() async {
|
||||
// final readyInputs = [
|
||||
// LedgerTransaction(
|
||||
// rawTx: "010000000001018c055c85c3724c98842d27712771dd0de139711f5940bba2df4615c5522184740000000017160014faf7f6dfb4e70798b92c93f33b4c51024491829df0ffffff022b05c70000000000160014f489f947fd13a1fb44ac168427081d3f30b6ce0cde9dd82e0000000017a914d5eca376cb49d65031220ff9093b7d407073ed0d8702483045022100f648c9f6a9b8f35b6ec29bbfae312c95ed3d56ce6a3f177d994efe90562ec4bd02205b82ce2c94bc0c9d152c3afc668b200bd82f48d6a14e83c66ba0f154cd5f69190121038f1dca119420d4aa7ad04af1c0d65304723789cccc56d335b18692390437f35900000000",
|
||||
// outputIndex: 0,
|
||||
// ownerPublicKey:
|
||||
// HexUtils.decode("03b2e67958ed3356e329e05cf94c3bee6b20c17175ac3b2a1278e073bf44f5d6ec"),
|
||||
// ownerDerivationPath: "m/84'/2'/0'/0/0",
|
||||
// sequence: 0xffffffff,
|
||||
// )
|
||||
// ];
|
||||
//
|
||||
// final outputs = [
|
||||
// BitcoinOutput(
|
||||
// address: P2wpkhAddress.fromAddress(
|
||||
// address: "ltc1qn0g5e36xaj07lqj6w9xn52ng07hud42g3jf5ps",
|
||||
// network: LitecoinNetwork.mainnet),
|
||||
// value: BigInt.from(1000000)),
|
||||
// BitcoinOutput(
|
||||
// address: P2wpkhAddress.fromAddress(
|
||||
// address: "ltc1qrx29qz4ghu4j0xk37ptgk7034cwpmjyxhrcnk9",
|
||||
// network: LitecoinNetwork.mainnet),
|
||||
// value: BigInt.from(12042705)),
|
||||
// ];
|
||||
// return await ltc.createTransaction(selectedDevice!,
|
||||
// inputs: readyInputs,
|
||||
// outputs: outputs
|
||||
// .map((e) => TransactionOutput.fromBigInt(
|
||||
// e.value, Uint8List.fromList(e.address.toScriptPubKey().toBytes())))
|
||||
// .toList(),
|
||||
// sigHashType: 0x01,
|
||||
// additionals: ["bech32"],
|
||||
// isSegWit: true,
|
||||
// useTrustedInputForSegwit: true);
|
||||
// }
|
||||
//
|
||||
// Widget DebugButton(
|
||||
// {required String title, required String method, required Future<dynamic> Function() func}) {
|
||||
// return Padding(
|
||||
// padding: EdgeInsets.only(top: 20),
|
||||
// child: PrimaryButton(
|
||||
// text: title,
|
||||
// onPressed: () async {
|
||||
// try {
|
||||
// setState(() => status = "Sending...");
|
||||
// final acc = await func();
|
||||
// setState(() => status = "$method: $acc");
|
||||
// printV("$method: $acc");
|
||||
// } on LedgerException catch (ex) {
|
||||
// setState(() => status = "${ex.errorCode.toRadixString(16)} ${ex.message}");
|
||||
// printV("${ex.errorCode.toRadixString(16)} ${ex.message}");
|
||||
// }
|
||||
// },
|
||||
// color: Theme.of(context).colorScheme.primary,
|
||||
// textColor: Theme.of(context).colorScheme.onPrimary),
|
||||
// );
|
||||
// }
|
||||
// }
|
||||
@@ -1,19 +1,21 @@
|
||||
import 'package:hash_wallet/core/wallet_name_validator.dart';
|
||||
import 'package:cw_core/generate_name.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:hash_wallet/src/widgets/blockchain_height_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_hardware_restore_view_model.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import "dart:async";
|
||||
|
||||
import "package:hash_wallet/core/wallet_name_validator.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/src/screens/base_page.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:hash_wallet/src/widgets/base_text_form_field.dart";
|
||||
import "package:hash_wallet/src/widgets/blockchain_height_widget.dart";
|
||||
import "package:hash_wallet/src/widgets/primary_button.dart";
|
||||
import "package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart";
|
||||
import "package:hash_wallet/utils/responsive_layout_util.dart";
|
||||
import "package:hash_wallet/utils/show_pop_up.dart";
|
||||
import "package:hash_wallet/view_model/wallet_hardware_restore_view_model.dart";
|
||||
import "package:cw_core/generate_name.dart";
|
||||
import "package:cw_core/wallet_type.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
import "package:mobx/mobx.dart";
|
||||
|
||||
class MoneroHardwareWalletOptionsPage extends BasePage {
|
||||
MoneroHardwareWalletOptionsPage(this._walletHardwareRestoreVM);
|
||||
@@ -42,13 +44,15 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO
|
||||
: _formKey = GlobalKey<FormState>(),
|
||||
_blockchainHeightKey = GlobalKey<BlockchainHeightState>(),
|
||||
_blockHeightFocusNode = FocusNode(),
|
||||
_controller = TextEditingController();
|
||||
_walletNameController = TextEditingController(),
|
||||
_passphraseController = TextEditingController();
|
||||
|
||||
final GlobalKey<FormState> _formKey;
|
||||
final GlobalKey<BlockchainHeightState> _blockchainHeightKey;
|
||||
final FocusNode _blockHeightFocusNode;
|
||||
final WalletHardwareRestoreViewModel _walletHardwareRestoreVM;
|
||||
final TextEditingController _controller;
|
||||
final TextEditingController _walletNameController;
|
||||
final TextEditingController _passphraseController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -57,131 +61,144 @@ class _MoneroHardwareWalletOptionsFormState extends State<_MoneroHardwareWalletO
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
content: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints:
|
||||
BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 0),
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: [
|
||||
BaseTextFormField(
|
||||
onChanged: (value) => _walletHardwareRestoreVM.name = value,
|
||||
controller: _controller,
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 20.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
hintText: S.of(context).wallet_name,
|
||||
suffixIcon: Semantics(
|
||||
label: S.of(context).generate_name,
|
||||
child: IconButton(
|
||||
onPressed: _onGenerateName,
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: const EdgeInsets.only(top: 24),
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: const EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
content: Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(
|
||||
maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.zero,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: Stack(
|
||||
alignment: Alignment.centerRight,
|
||||
children: [
|
||||
BaseTextFormField(
|
||||
onChanged: (value) => _walletHardwareRestoreVM.name = value,
|
||||
controller: _walletNameController,
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: Image.asset(
|
||||
'assets/images/refresh_icon.png',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
hintText: S.of(context).wallet_name,
|
||||
suffixIcon: Semantics(
|
||||
label: S.of(context).generate_name,
|
||||
child: IconButton(
|
||||
onPressed: _onGenerateName,
|
||||
icon: Container(
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
width: 34,
|
||||
height: 34,
|
||||
child: Image.asset(
|
||||
"assets/images/refresh_icon.png",
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
validator: WalletNameValidator(),
|
||||
),
|
||||
validator: WalletNameValidator(),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: BlockchainHeightWidget(
|
||||
focusNode: _blockHeightFocusNode,
|
||||
key: _blockchainHeightKey,
|
||||
hasDatePicker: true,
|
||||
walletType: WalletType.monero,
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 20),
|
||||
child: BlockchainHeightWidget(
|
||||
focusNode: _blockHeightFocusNode,
|
||||
key: _blockchainHeightKey,
|
||||
hasDatePicker: true,
|
||||
walletType: WalletType.monero,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomSectionPadding: const EdgeInsets.all(24),
|
||||
bottomSection: Observer(
|
||||
builder: (_) => Column(
|
||||
children: [
|
||||
LoadingPrimaryButton(
|
||||
onPressed: _confirmForm,
|
||||
text: S.of(context).seed_language_next,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isDisabled: _walletHardwareRestoreVM.name.isEmpty,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomSectionPadding: EdgeInsets.all(24),
|
||||
bottomSection: Observer(
|
||||
builder: (context) => LoadingPrimaryButton(
|
||||
onPressed: _confirmForm,
|
||||
text: S.of(context).seed_language_next,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isDisabled: _walletHardwareRestoreVM.name.isEmpty,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
Future<void> _onGenerateName() async {
|
||||
final rName = await generateName();
|
||||
FocusManager.instance.primaryFocus?.unfocus();
|
||||
|
||||
setState(() {
|
||||
_controller.text = rName;
|
||||
_walletNameController.text = rName;
|
||||
_walletHardwareRestoreVM.name = rName;
|
||||
_controller.selection =
|
||||
TextSelection.fromPosition(TextPosition(offset: _controller.text.length));
|
||||
_walletNameController.selection =
|
||||
TextSelection.fromPosition(TextPosition(offset: _walletNameController.text.length));
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _confirmForm() async {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).proceed_on_device,
|
||||
alertContent: S.of(context).proceed_on_device_description,
|
||||
buttonText: S.of(context).cancel,
|
||||
alertBarrierDismissible: false,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
unawaited(
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).proceed_on_device,
|
||||
alertContent: S.of(context).proceed_on_device_description,
|
||||
buttonText: S.of(context).cancel,
|
||||
alertBarrierDismissible: false,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final options = {'height': _blockchainHeightKey.currentState?.height ?? -1};
|
||||
final options = <String, dynamic>{"height": _blockchainHeightKey.currentState?.height ?? -1};
|
||||
|
||||
if (_walletHardwareRestoreVM.passphraseAvailable && _passphraseController.text.isNotEmpty) {
|
||||
options["passphrase"] = _passphraseController.text;
|
||||
}
|
||||
await _walletHardwareRestoreVM.create(options: options);
|
||||
}
|
||||
|
||||
bool _effectsInstalled = false;
|
||||
|
||||
void _setEffects(BuildContext context) {
|
||||
if (_effectsInstalled) return;
|
||||
if (_effectsInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
reaction((_) => _walletHardwareRestoreVM.error, (String? error) {
|
||||
reaction((_) => _walletHardwareRestoreVM.error, (error) {
|
||||
if (error != null) {
|
||||
if (error == S.current.ledger_connection_error) Navigator.of(context).pop();
|
||||
if (error == S.current.ledger_connection_error) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertWithOneAction(
|
||||
builder: (context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: error,
|
||||
buttonText: S.of(context).ok,
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/new_primary_button.dart";
|
||||
import "package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart";
|
||||
import "package:hash_wallet/src/widgets/base_text_form_field.dart";
|
||||
import "package:flutter/material.dart";
|
||||
|
||||
class MoneroHardwareWalletPassphraseInputModal extends StatelessWidget {
|
||||
const MoneroHardwareWalletPassphraseInputModal({required this.controller, super.key});
|
||||
|
||||
final TextEditingController controller;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Padding(
|
||||
padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: const BorderRadius.vertical(top: Radius.circular(18)),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).passphrase_entry,
|
||||
leadingIcon: const Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 18),
|
||||
child: Column(
|
||||
spacing: 12,
|
||||
children: [
|
||||
BaseTextFormField(
|
||||
controller: controller,
|
||||
hintText: S.of(context).passphrase_raw,
|
||||
autocorrect: false,
|
||||
enableSuggestions: false,
|
||||
),
|
||||
const SizedBox(),
|
||||
NewPrimaryButton(
|
||||
onPressed: Navigator.of(context).pop,
|
||||
text: S.of(context).restore_next,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
const SizedBox(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:hash_wallet/core/wallet_name_validator.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/generate_name.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
@@ -147,10 +148,9 @@ class _SelectHardwareWalletAccountFormState extends State<SelectHardwareWalletAc
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: SelectButton(
|
||||
image: Image.asset(
|
||||
getCryptoCurrencyIconForWalletListItem(
|
||||
_walletHardwareRestoreVM.type,
|
||||
),
|
||||
image: HashImageWidget(
|
||||
imageUrl: getCryptoCurrencyIconForWalletListItem(
|
||||
_walletHardwareRestoreVM.type),
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import 'package:hash_wallet/entities/hardware_wallet/hardware_wallet_device.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
|
||||
class DeviceTile extends StatelessWidget {
|
||||
const DeviceTile({
|
||||
required this.onPressed,
|
||||
required this.title,
|
||||
this.leading,
|
||||
this.connectionType,
|
||||
});
|
||||
|
||||
final VoidCallback onPressed;
|
||||
final String title;
|
||||
final String? leading;
|
||||
final HardwareWalletConnectionType? connectionType;
|
||||
|
||||
String? get connectionTypeIcon {
|
||||
switch (connectionType) {
|
||||
case HardwareWalletConnectionType.ble:
|
||||
return 'assets/images/bluetooth.png';
|
||||
case HardwareWalletConnectionType.usb:
|
||||
return 'assets/images/usb.png';
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.all(24),
|
||||
alignment: Alignment.center,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(12)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
if (leading != null && !leading!.endsWith(".svg"))
|
||||
Image.asset(
|
||||
leading!,
|
||||
height: 30,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
if (leading != null && leading!.endsWith(".svg"))
|
||||
SvgPicture.asset(leading!, height: 30),
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(left: 16),
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (connectionTypeIcon != null)
|
||||
Center(
|
||||
child: Image.asset(
|
||||
connectionTypeIcon!,
|
||||
height: 25,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,12 @@
|
||||
import "dart:async";
|
||||
|
||||
import 'package:hash_wallet/core/auth_service.dart';
|
||||
import 'package:hash_wallet/entities/contact_base.dart';
|
||||
import 'package:hash_wallet/entities/contact_record.dart';
|
||||
import 'package:hash_wallet/entities/wallet_contact.dart';
|
||||
import 'package:hash_wallet/entities/wallet_list_order_types.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import "package:hash_wallet/new-ui/page_open_listener.dart";
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/filter_list_widget.dart';
|
||||
@@ -16,6 +20,8 @@ import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:hash_wallet/utils/show_bar.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/contact_list/contact_list_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
@@ -23,7 +29,7 @@ import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
|
||||
class ContactListPage extends BasePage {
|
||||
class ContactListPage extends BasePage implements PageOpenListener {
|
||||
ContactListPage(this.contactListViewModel, this.authService, {this.showAddButton = false});
|
||||
|
||||
final ContactListViewModel contactListViewModel;
|
||||
@@ -62,6 +68,7 @@ class ContactListPage extends BasePage {
|
||||
minWidth: 32.0,
|
||||
height: 32.0,
|
||||
child: TextButton(
|
||||
key: ValueKey("contact_list_page_add_contact_button_key"),
|
||||
// FIX-ME: Style
|
||||
//shape: CircleBorder(),
|
||||
onPressed: () async {
|
||||
@@ -86,6 +93,11 @@ class ContactListPage extends BasePage {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void onPageOpen() {
|
||||
unawaited(contactListViewModel.loadWalletContacts());
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => ContactPageBody(contactListViewModel: contactListViewModel);
|
||||
}
|
||||
@@ -281,8 +293,10 @@ class _ContactPageBodyState extends State<ContactPageBody> with SingleTickerProv
|
||||
}
|
||||
|
||||
Widget _buildCurrencyIcon(ContactBase contact) {
|
||||
final image = contact.type.iconPath;
|
||||
return image != null
|
||||
final image = (contact is WalletContact && contact.walletType != null)
|
||||
? getCryptoCurrencyIconForWalletListItem(contact.walletType!)
|
||||
: contact.type.iconPath;
|
||||
return (image != null && image.isNotEmpty)
|
||||
? HashImageWidget(imageUrl: image, height: 24, width: 24)
|
||||
: const SizedBox(height: 24, width: 24);
|
||||
}
|
||||
@@ -335,6 +349,7 @@ class _ContactListBodyState extends State<ContactListBody> {
|
||||
updateFunction: widget.contactListViewModel.reorderAccordingToContactList,
|
||||
canReorder: widget.contactListViewModel.isEditable,
|
||||
shrinkWrap: true,
|
||||
padding: const EdgeInsets.only(bottom: 250),
|
||||
itemBuilder: (context, index) {
|
||||
final contact = contacts[index];
|
||||
final contactContent =
|
||||
@@ -373,9 +388,13 @@ class _ContactListBodyState extends State<ContactListBody> {
|
||||
}
|
||||
|
||||
Widget generateContactRaw(BuildContext context, ContactRecord contact, bool isLast) {
|
||||
final image = contact.type.iconPath;
|
||||
final currencyIcon = image != null
|
||||
? Image.asset(image, height: 24, width: 24)
|
||||
const l2NativeChainIcons = {
|
||||
CryptoCurrency.baseEth: "assets/new-ui/crypto_full_icons/base.svg",
|
||||
CryptoCurrency.arbEth: "assets/new-ui/crypto_full_icons/arbitrum.svg",
|
||||
};
|
||||
final image = l2NativeChainIcons[contact.type] ?? contact.type.iconPath;
|
||||
final currencyIcon = (image != null && image.isNotEmpty)
|
||||
? HashImageWidget(imageUrl: image, height: 24, width: 24)
|
||||
: const SizedBox(height: 24, width: 24);
|
||||
return Column(
|
||||
children: [
|
||||
@@ -443,7 +462,7 @@ class _ContactListBodyState extends State<ContactListBody> {
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: FeatureFlag.hasNewUi ? 48 : 0),
|
||||
padding: const EdgeInsets.only(bottom: 48),
|
||||
child: MergeSemantics(
|
||||
child: SizedBox(
|
||||
height: 58,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import 'package:hash_wallet/core/address_resolver/parsed_address.dart';
|
||||
import 'package:hash_wallet/core/address_validator.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/send_page/send_address_input.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
@@ -27,7 +28,15 @@ class ContactPage extends BasePage {
|
||||
_nameController.text = contactViewModel.name;
|
||||
_addressController.text = contactViewModel.address;
|
||||
_nameController.addListener(() => contactViewModel.name = _nameController.text);
|
||||
_addressController.addListener(() => contactViewModel.address = _addressController.text);
|
||||
_addressController.addListener(() {
|
||||
final address = _addressController.text;
|
||||
|
||||
if (address != contactViewModel.address) {
|
||||
contactViewModel.displayName = '';
|
||||
}
|
||||
|
||||
contactViewModel.address = address;
|
||||
});
|
||||
|
||||
autorun((_) => _currencyTypeController.text =
|
||||
"${contactViewModel.currency?.toString() ?? ""} ${contactViewModel.currency?.tag != null ? "(${contactViewModel.currency?.tag})" : ""}");
|
||||
@@ -64,6 +73,7 @@ class ContactPage extends BasePage {
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
BaseTextFormField(
|
||||
key: ValueKey("contact_page_name_textfield_key"),
|
||||
controller: _nameController,
|
||||
hintText: S.of(context).contact_name,
|
||||
validator: ContactNameValidator(),
|
||||
@@ -72,6 +82,7 @@ class ContactPage extends BasePage {
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Container(
|
||||
child: InkWell(
|
||||
key: ValueKey("contact_page_currency_picker_button_key"),
|
||||
onTap: () => _presentCurrencyPicker(context),
|
||||
child: IgnorePointer(
|
||||
child: BaseTextFormField(
|
||||
@@ -96,10 +107,12 @@ class ContactPage extends BasePage {
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
return NewSendAddressInput(
|
||||
key: ValueKey("contact_page_address_input_key"),
|
||||
focusNode: addressFocusNode,
|
||||
displayName: contactViewModel.displayName,
|
||||
addressController: _addressController,
|
||||
onEditingComplete: () => contactViewModel.extractParsedAddress(context),
|
||||
onEditingComplete: () {},
|
||||
onPushPasteButton: (context) => _extractParsedAddress(context),
|
||||
validator: AddressValidator(type: contactViewModel.currency!),
|
||||
selectedCurrency: contactViewModel.currency!,
|
||||
);
|
||||
@@ -128,10 +141,10 @@ class ContactPage extends BasePage {
|
||||
Expanded(
|
||||
child: Observer(
|
||||
builder: (_) => PrimaryButton(
|
||||
key: ValueKey("contact_page_save_button_key"),
|
||||
onPressed: () async {
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
await contactViewModel.extractParsedAddress(context);
|
||||
_extractParsedAddress(context);
|
||||
|
||||
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
|
||||
return;
|
||||
@@ -151,6 +164,16 @@ class ContactPage extends BasePage {
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _extractParsedAddress(BuildContext context) async {
|
||||
final parsedAddress = await contactViewModel.extractParsedAddress(context);
|
||||
if (parsedAddress == null) return;
|
||||
|
||||
final confirmed = await showParsedAddressConfirmationAlert(context, parsedAddress);
|
||||
if (!confirmed) return;
|
||||
|
||||
contactViewModel.applyParsedAddress(parsedAddress);
|
||||
}
|
||||
|
||||
void _presentCurrencyPicker(BuildContext context) {
|
||||
showPopUp<void>(
|
||||
builder: (_) => CurrencyPicker(
|
||||
@@ -204,4 +227,29 @@ class ContactPage extends BasePage {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<bool> showParsedAddressConfirmationAlert(
|
||||
BuildContext context,
|
||||
ParsedAddress parsedAddress,
|
||||
) async {
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).address_detected,
|
||||
headerTitleText: parsedAddress.profileName.isEmpty ? null : parsedAddress.profileName,
|
||||
headerImageProfileUrl: parsedAddress.profileImageUrl.isEmpty
|
||||
? parsedAddress.addressSource.iconPath
|
||||
: parsedAddress.profileImageUrl,
|
||||
alertContent: S.of(context).extracted_address_content(
|
||||
'${parsedAddress.handle} (${parsedAddress.addressSource.label})',
|
||||
),
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(true),
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
return confirmed ?? false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,419 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:hash_wallet/entities/preferences_key.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar_wrapper.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/cake_features_page.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/page_indicator.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/new_main_navbar_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
||||
import 'package:hash_wallet/src/widgets/evm_switcher.dart';
|
||||
import 'package:hash_wallet/src/widgets/gradient_background.dart';
|
||||
import 'package:hash_wallet/src/widgets/services_updates_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/vulnerable_seeds_popup.dart';
|
||||
import 'package:hash_wallet/utils/device_info.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:hash_wallet/utils/version_comparator.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/yat_emoji_id.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/menu_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/balance/balance_page.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/transactions_page.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/sync_indicator.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:hash_wallet/main.dart';
|
||||
import 'package:hash_wallet/src/screens/release_notes/release_notes_screen.dart';
|
||||
import 'package:hash_wallet/themes/core/theme_extension.dart';
|
||||
|
||||
class DashboardPage extends StatefulWidget {
|
||||
DashboardPage({
|
||||
required this.bottomSheetService,
|
||||
required this.balancePage,
|
||||
required this.dashboardViewModel,
|
||||
required this.addressListViewModel,
|
||||
});
|
||||
|
||||
final BalancePage balancePage;
|
||||
final BottomSheetService bottomSheetService;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final WalletAddressListViewModel addressListViewModel;
|
||||
|
||||
@override
|
||||
State<DashboardPage> createState() => _DashboardPageState();
|
||||
}
|
||||
|
||||
class _DashboardPageState extends State<DashboardPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
bool isMobileLayout =
|
||||
responsiveLayoutUtil.screenWidth < ResponsiveLayoutUtilBase.kMobileThreshold;
|
||||
|
||||
reaction((_) => responsiveLayoutUtil.screenWidth, (screenWidth) {
|
||||
// Check if it was previously in mobile layout, and now changing to desktop
|
||||
if (isMobileLayout &&
|
||||
screenWidth > ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) {
|
||||
setState(() {
|
||||
isMobileLayout = false;
|
||||
});
|
||||
}
|
||||
|
||||
// Check if it was previously in desktop layout, and now changing to mobile
|
||||
if (!isMobileLayout &&
|
||||
screenWidth <= ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) {
|
||||
setState(() {
|
||||
isMobileLayout = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
Widget dashboardChild;
|
||||
|
||||
final dashboardPageView = RefreshIndicator(
|
||||
displacement: responsiveLayoutUtil.screenHeight * 0.1,
|
||||
onRefresh: () async => await widget.dashboardViewModel.refreshDashboard(),
|
||||
child: SingleChildScrollView(
|
||||
physics: AlwaysScrollableScrollPhysics(),
|
||||
child: Container(
|
||||
height: responsiveLayoutUtil.screenHeight,
|
||||
child: _DashboardPageView(
|
||||
balancePage: widget.balancePage,
|
||||
bottomSheetService: widget.bottomSheetService,
|
||||
dashboardViewModel: widget.dashboardViewModel,
|
||||
addressListViewModel: widget.addressListViewModel,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if (DeviceInfo.instance.isDesktop) {
|
||||
if (responsiveLayoutUtil.screenWidth >
|
||||
ResponsiveLayoutUtilBase.kDesktopMaxDashBoardWidthConstraint) {
|
||||
dashboardChild = getIt.get<DesktopSidebarWrapper>();
|
||||
} else {
|
||||
dashboardChild = dashboardPageView;
|
||||
}
|
||||
} else if (responsiveLayoutUtil.shouldRenderMobileUI) {
|
||||
dashboardChild = dashboardPageView;
|
||||
} else {
|
||||
dashboardChild = getIt.get<DesktopSidebarWrapper>();
|
||||
}
|
||||
|
||||
return Scaffold(body: dashboardChild);
|
||||
}
|
||||
}
|
||||
|
||||
class _DashboardPageView extends BasePage {
|
||||
_DashboardPageView({
|
||||
required this.bottomSheetService,
|
||||
required this.balancePage,
|
||||
required this.dashboardViewModel,
|
||||
required this.addressListViewModel,
|
||||
});
|
||||
|
||||
final BalancePage balancePage;
|
||||
|
||||
@override
|
||||
bool get gradientBackground => true;
|
||||
|
||||
@override
|
||||
Widget Function(BuildContext, Widget) get rootWrapper =>
|
||||
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => false;
|
||||
|
||||
@override
|
||||
Widget get endDrawer =>
|
||||
MenuWidget(dashboardViewModel, ValueKey('dashboard_page_drawer_menu_widget_key'));
|
||||
|
||||
@override
|
||||
Widget leading(BuildContext context) {
|
||||
if (FeatureFlag.isEVMChainSwitcherEnabled &&
|
||||
dashboardViewModel.isEVMWallet &&
|
||||
dashboardViewModel.availableChains.isNotEmpty) {
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: Size(50, 30),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
alignment: Alignment.centerLeft,
|
||||
),
|
||||
onPressed: () => showDialog(
|
||||
context: context,
|
||||
builder: (context) => EvmSwitcher(
|
||||
chains: dashboardViewModel.availableChains,
|
||||
currentChain: dashboardViewModel.currentChain,
|
||||
onChainSelected: (chainId) => dashboardViewModel.selectChain(chainId),
|
||||
hiddenChainIds: dashboardViewModel.settingsStore.evmHiddenChainIds,
|
||||
onHiddenChanged: (hidden) =>
|
||||
dashboardViewModel.settingsStore.setEvmHiddenChainIds(hidden),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/evm_switcher.svg',
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 30,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
return ServicesUpdatesWidget(
|
||||
key: ValueKey('dashboard_page_services_update_button_key'),
|
||||
dashboardViewModel.getServicesStatus(),
|
||||
enabled: dashboardViewModel.isEnabledBulletinAction,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) {
|
||||
return SyncIndicator(
|
||||
key: ValueKey('dashboard_page_sync_indicator_button_key'),
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
onTap: () => Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget trailing(BuildContext context) {
|
||||
return Container(
|
||||
alignment: Alignment.centerRight,
|
||||
width: 42,
|
||||
child: TextButton(
|
||||
key: ValueKey('dashboard_page_wallet_menu_button_key'),
|
||||
onPressed: () => onOpenEndDrawer(),
|
||||
child: Semantics(
|
||||
label: S.of(context).wallet_menu,
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/menu.svg',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final BottomSheetService bottomSheetService;
|
||||
final WalletAddressListViewModel addressListViewModel;
|
||||
|
||||
int get initialPage => dashboardViewModel.shouldShowMarketPlaceInDashboard ? 1 : 0;
|
||||
ObservableList<Widget> pages = ObservableList<Widget>();
|
||||
bool _isEffectsInstalled = false;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
final controller = PageController(initialPage: initialPage);
|
||||
|
||||
reaction(
|
||||
(_) => dashboardViewModel.shouldShowMarketPlaceInDashboard,
|
||||
(bool value) {
|
||||
if (!dashboardViewModel.shouldShowMarketPlaceInDashboard) {
|
||||
controller.jumpToPage(0);
|
||||
}
|
||||
pages.clear();
|
||||
_isEffectsInstalled = false;
|
||||
_setEffects(context);
|
||||
|
||||
if (value) {
|
||||
controller.jumpToPage(1);
|
||||
} else {
|
||||
controller.jumpToPage(0);
|
||||
}
|
||||
},
|
||||
);
|
||||
_setEffects(context);
|
||||
|
||||
return SafeArea(
|
||||
minimum: EdgeInsets.only(bottom: 0),
|
||||
child: BottomSheetListener(
|
||||
bottomSheetService: bottomSheetService,
|
||||
child: Container(
|
||||
child: Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: <Widget>[
|
||||
Observer(
|
||||
builder: (context) {
|
||||
return PageView.builder(
|
||||
key: const ValueKey('dashboard_page_view_key'),
|
||||
controller: controller,
|
||||
physics: const BouncingScrollPhysics(),
|
||||
itemCount: pages.length,
|
||||
itemBuilder: (context, index) => pages[index],
|
||||
);
|
||||
},
|
||||
),
|
||||
Stack(
|
||||
alignment: Alignment.bottomCenter,
|
||||
children: <Widget>[
|
||||
IgnorePointer(
|
||||
child: Container(
|
||||
alignment: Alignment.bottomCenter,
|
||||
height: 220,
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
colors: <Color>[
|
||||
context.customColors.backgroundGradientColor.withAlpha(5),
|
||||
context.customColors.backgroundGradientColor.withAlpha(50),
|
||||
context.customColors.backgroundGradientColor.withAlpha(125),
|
||||
context.customColors.backgroundGradientColor.withAlpha(150),
|
||||
context.customColors.backgroundGradientColor.withAlpha(200),
|
||||
context.customColors.backgroundGradientColor,
|
||||
context.customColors.backgroundGradientColor,
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: EdgeInsets.only(bottom: 110),
|
||||
child: Semantics(
|
||||
container: true,
|
||||
label: 'Page indicator',
|
||||
hint:
|
||||
'Swipe left or right to change page, or double tap buttons below to navigate directly.',
|
||||
child: ExcludeSemantics(
|
||||
excluding: false,
|
||||
child: PageIndicator(
|
||||
controller: controller,
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
NewMainNavBar(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
selectedIndex: 0,
|
||||
onItemTap: (index) {}),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _setEffects(BuildContext context) async {
|
||||
if (_isEffectsInstalled || !context.mounted) {
|
||||
return;
|
||||
}
|
||||
if (dashboardViewModel.shouldShowMarketPlaceInDashboard) {
|
||||
pages.add(
|
||||
Semantics(
|
||||
label: S.of(context).apps,
|
||||
child: CakeFeaturesPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
pages.add(Semantics(label: S.of(context).balance_page, child: balancePage));
|
||||
pages.add(
|
||||
Semantics(
|
||||
label: S.of(context).history,
|
||||
child: TransactionsPage(dashboardViewModel: dashboardViewModel),
|
||||
),
|
||||
);
|
||||
_isEffectsInstalled = true;
|
||||
|
||||
_showReleaseNotesPopup(context);
|
||||
|
||||
_showVulnerableSeedsPopup(context);
|
||||
|
||||
var needToPresentYat = false;
|
||||
|
||||
rootKey.currentState?.isInactive.listen(
|
||||
(inactive) {
|
||||
if (needToPresentYat) {
|
||||
Future<void>.delayed(Duration(milliseconds: 500)).then(
|
||||
(_) {
|
||||
showPopUp<void>(
|
||||
context: navigatorKey.currentContext!,
|
||||
builder: (_) => YatEmojiId(dashboardViewModel.yatStore.emoji),
|
||||
);
|
||||
needToPresentYat = false;
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
dashboardViewModel.yatStore.emojiIncommingStream.listen(
|
||||
(String emoji) {
|
||||
if (!_isEffectsInstalled || emoji.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
needToPresentYat = true;
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _showReleaseNotesPopup(BuildContext context) async {
|
||||
final sharedPrefs = await SharedPreferences.getInstance();
|
||||
final currentAppVersion =
|
||||
VersionComparator.getExtendedVersionNumber(dashboardViewModel.settingsStore.appVersion);
|
||||
final lastSeenAppVersion = sharedPrefs.getInt(PreferencesKey.lastSeenAppVersion);
|
||||
final isNewInstall = sharedPrefs.getBool(PreferencesKey.isNewInstall);
|
||||
|
||||
if (currentAppVersion != lastSeenAppVersion && !isNewInstall!) {
|
||||
Future<void>.delayed(
|
||||
Duration(seconds: 1),
|
||||
() {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ReleaseNotesScreen(
|
||||
title: 'Version ${dashboardViewModel.settingsStore.appVersion}',
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion);
|
||||
} else if (isNewInstall!) {
|
||||
sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion);
|
||||
}
|
||||
}
|
||||
|
||||
void _showVulnerableSeedsPopup(BuildContext context) async {
|
||||
final List<String> affectedWalletNames = await dashboardViewModel.checkAffectedWallets();
|
||||
|
||||
if (affectedWalletNames.isNotEmpty) {
|
||||
Future<void>.delayed(
|
||||
Duration(seconds: 1),
|
||||
() {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return VulnerableSeedsPopup(affectedWalletNames);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,135 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:hash_wallet/entities/preferences_key.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/release_notes/release_notes_screen.dart';
|
||||
import 'package:hash_wallet/src/screens/yat_emoji_id.dart';
|
||||
import 'package:hash_wallet/src/widgets/vulnerable_seeds_popup.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/utils/version_comparator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/balance/balance_page.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:hash_wallet/main.dart';
|
||||
import 'package:hash_wallet/router.dart' as Router;
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class DesktopDashboardPage extends StatelessWidget {
|
||||
DesktopDashboardPage({
|
||||
required this.balancePage,
|
||||
required this.dashboardViewModel,
|
||||
required this.addressListViewModel,
|
||||
required this.desktopKey,
|
||||
});
|
||||
|
||||
final BalancePage balancePage;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final WalletAddressListViewModel addressListViewModel;
|
||||
final GlobalKey<NavigatorState> desktopKey;
|
||||
|
||||
bool _isEffectsInstalled = false;
|
||||
StreamSubscription<bool>? _onInactiveSub;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_setEffects(context);
|
||||
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: 400,
|
||||
child: balancePage,
|
||||
),
|
||||
Flexible(
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 500),
|
||||
child: Navigator(
|
||||
key: desktopKey,
|
||||
initialRoute: Routes.desktop_actions,
|
||||
onGenerateRoute: (settings) => Router.createRoute(settings),
|
||||
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
|
||||
return [
|
||||
navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))!
|
||||
];
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _setEffects(BuildContext context) async {
|
||||
if (_isEffectsInstalled) {
|
||||
return;
|
||||
}
|
||||
_isEffectsInstalled = true;
|
||||
|
||||
var needToPresentYat = false;
|
||||
var isInactive = false;
|
||||
|
||||
_onInactiveSub = rootKey.currentState!.isInactive.listen((inactive) {
|
||||
isInactive = inactive;
|
||||
|
||||
if (needToPresentYat) {
|
||||
Future<void>.delayed(Duration(milliseconds: 500)).then((_) {
|
||||
showPopUp<void>(
|
||||
context: navigatorKey.currentContext!,
|
||||
builder: (_) => YatEmojiId(dashboardViewModel.yatStore.emoji));
|
||||
needToPresentYat = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
dashboardViewModel.yatStore.emojiIncommingStream.listen((String emoji) {
|
||||
if (!_isEffectsInstalled || emoji.isEmpty) {
|
||||
return;
|
||||
}
|
||||
|
||||
needToPresentYat = true;
|
||||
});
|
||||
|
||||
final sharedPrefs = await SharedPreferences.getInstance();
|
||||
final currentAppVersion =
|
||||
VersionComparator.getExtendedVersionNumber(dashboardViewModel.settingsStore.appVersion);
|
||||
final lastSeenAppVersion = sharedPrefs.getInt(PreferencesKey.lastSeenAppVersion);
|
||||
final isNewInstall = sharedPrefs.getBool(PreferencesKey.isNewInstall);
|
||||
|
||||
if (currentAppVersion != lastSeenAppVersion && !isNewInstall!) {
|
||||
await Future<void>.delayed(Duration(seconds: 1));
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return ReleaseNotesScreen(
|
||||
title: 'Version ${dashboardViewModel.settingsStore.appVersion}');
|
||||
});
|
||||
sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion);
|
||||
} else if (isNewInstall!) {
|
||||
sharedPrefs.setInt(PreferencesKey.lastSeenAppVersion, currentAppVersion);
|
||||
}
|
||||
|
||||
_showVulnerableSeedsPopup(context);
|
||||
}
|
||||
|
||||
void _showVulnerableSeedsPopup(BuildContext context) async {
|
||||
final List<String> affectedWalletNames = await dashboardViewModel.checkAffectedWallets();
|
||||
|
||||
if (affectedWalletNames.isNotEmpty) {
|
||||
Future<void>.delayed(
|
||||
Duration(seconds: 1),
|
||||
() {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return VulnerableSeedsPopup(affectedWalletNames);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DesktopActionButton extends StatelessWidget {
|
||||
final String image;
|
||||
final String title;
|
||||
final bool canShow;
|
||||
final bool isEnabled;
|
||||
final Function() onTap;
|
||||
|
||||
const DesktopActionButton({
|
||||
Key? key,
|
||||
required this.title,
|
||||
required this.image,
|
||||
required this.onTap,
|
||||
bool? canShow,
|
||||
bool? isEnabled,
|
||||
}) : this.isEnabled = isEnabled ?? true,
|
||||
this.canShow = canShow ?? true,
|
||||
super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.fromLTRB(8, 0, 8, 8),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 25),
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Image.asset(
|
||||
image,
|
||||
height: 30,
|
||||
width: 30,
|
||||
color: isEnabled
|
||||
? Theme.of(context).colorScheme.onPrimaryContainer
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
const SizedBox(width: 10),
|
||||
AutoSizeText(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isEnabled
|
||||
? Theme.of(context).colorScheme.onPrimaryContainer
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
import 'package:hash_wallet/entities/main_actions.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/desktop_action_button.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/cake_features_page.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class DesktopDashboardActions extends StatelessWidget {
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
const DesktopDashboardActions(this.dashboardViewModel, {Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Observer(builder: (_) {
|
||||
return Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
DesktopActionButton(
|
||||
title: MainActions.showWalletsAction.name(context),
|
||||
image: MainActions.showWalletsAction.image,
|
||||
canShow: MainActions.showWalletsAction.canShow?.call(dashboardViewModel),
|
||||
isEnabled: MainActions.showWalletsAction.isEnabled?.call(dashboardViewModel),
|
||||
onTap: () async =>
|
||||
await MainActions.showWalletsAction.onTap(context, dashboardViewModel),
|
||||
),
|
||||
DesktopActionButton(
|
||||
title: MainActions.swapAction.name(context),
|
||||
image: MainActions.swapAction.image,
|
||||
canShow: MainActions.swapAction.canShow?.call(dashboardViewModel),
|
||||
isEnabled: MainActions.swapAction.isEnabled?.call(dashboardViewModel),
|
||||
onTap: () async => await MainActions.swapAction.onTap(context, dashboardViewModel),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DesktopActionButton(
|
||||
title: MainActions.receiveAction.name(context),
|
||||
image: MainActions.receiveAction.image,
|
||||
canShow: MainActions.receiveAction.canShow?.call(dashboardViewModel),
|
||||
isEnabled: MainActions.receiveAction.isEnabled?.call(dashboardViewModel),
|
||||
onTap: () async =>
|
||||
await MainActions.receiveAction.onTap(context, dashboardViewModel),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: DesktopActionButton(
|
||||
title: MainActions.sendAction.name(context),
|
||||
image: MainActions.sendAction.image,
|
||||
canShow: MainActions.sendAction.canShow?.call(dashboardViewModel),
|
||||
isEnabled: MainActions.sendAction.isEnabled?.call(dashboardViewModel),
|
||||
onTap: () async =>
|
||||
await MainActions.sendAction.onTap(context, dashboardViewModel),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: DesktopActionButton(
|
||||
title: MainActions.tradeAction.name(context),
|
||||
image: MainActions.tradeAction.image,
|
||||
canShow: MainActions.tradeAction.canShow?.call(dashboardViewModel),
|
||||
isEnabled: MainActions.tradeAction.isEnabled?.call(dashboardViewModel),
|
||||
onTap: () async =>
|
||||
await MainActions.tradeAction.onTap(context, dashboardViewModel),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Expanded(
|
||||
child: CakeFeaturesPage(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,45 +0,0 @@
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:hash_wallet/themes/core/theme_store.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DesktopDashboardNavbar extends StatelessWidget implements ObstructingPreferredSizeWidget {
|
||||
final Widget leading;
|
||||
final Widget middle;
|
||||
final Widget trailing;
|
||||
|
||||
DesktopDashboardNavbar({
|
||||
super.key,
|
||||
required this.leading,
|
||||
required this.middle,
|
||||
required this.trailing,
|
||||
});
|
||||
|
||||
MaterialThemeBase get currentTheme => getIt.get<ThemeStore>().currentTheme;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsetsDirectional.only(end: 24),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLowest.withOpacity(0.1),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(child: leading),
|
||||
middle,
|
||||
trailing,
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Size get preferredSize => Size.fromHeight(60);
|
||||
|
||||
@override
|
||||
bool shouldFullyObstruct(BuildContext context) => false;
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SideMenu extends StatelessWidget {
|
||||
const SideMenu({
|
||||
super.key,
|
||||
required this.topItems,
|
||||
required this.bottomItems,
|
||||
required this.width,
|
||||
});
|
||||
|
||||
final List<SideMenuItem> topItems;
|
||||
final List<SideMenuItem> bottomItems;
|
||||
final double width;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLowest.withOpacity(0.1),
|
||||
width: width,
|
||||
height: MediaQuery.of(context).size.height,
|
||||
child: Column(
|
||||
children: [
|
||||
...topItems,
|
||||
Spacer(),
|
||||
...bottomItems,
|
||||
SizedBox(height: 30),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SideMenuItem extends StatelessWidget {
|
||||
const SideMenuItem({
|
||||
Key? key,
|
||||
required this.onTap,
|
||||
this.imagePath,
|
||||
this.icon,
|
||||
this.widget,
|
||||
this.isSelected = false,
|
||||
}) : assert(widget != null || icon != null || imagePath != null);
|
||||
|
||||
final void Function() onTap;
|
||||
final String? imagePath;
|
||||
final IconData? icon;
|
||||
final bool isSelected;
|
||||
final Widget? widget;
|
||||
|
||||
Color _setColor(BuildContext context) {
|
||||
if (isSelected) {
|
||||
return Theme.of(context).colorScheme.primary;
|
||||
} else {
|
||||
return Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(20),
|
||||
child: widget ?? _getIcon(context),
|
||||
),
|
||||
onTap: () => onTap.call(),
|
||||
highlightColor: Colors.transparent,
|
||||
focusColor: Colors.transparent,
|
||||
hoverColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
);
|
||||
}
|
||||
|
||||
Widget _getIcon(BuildContext context) {
|
||||
return icon != null
|
||||
? Icon(
|
||||
icon,
|
||||
color: _setColor(context),
|
||||
size: 30,
|
||||
)
|
||||
: HashImageWidget(
|
||||
imageUrl: imagePath ?? '',
|
||||
fit: BoxFit.cover,
|
||||
height: 30,
|
||||
width: 30,
|
||||
color: _setColor(context),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/auth/auth_page.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/desktop_dashboard_navbar.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/desktop_sidebar/side_menu_item.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/desktop_wallet_selection_dropdown.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/sync_indicator.dart';
|
||||
import 'package:hash_wallet/src/screens/wallet_connect/widgets/bottom_sheet/bottom_sheet_listener_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
||||
import 'package:hash_wallet/src/widgets/services_updates_widget.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/desktop_sidebar_view_model.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:hash_wallet/router.dart' as Router;
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class DesktopSidebarWrapper extends BasePage {
|
||||
final BottomSheetService bottomSheetService;
|
||||
final Widget child;
|
||||
final DesktopSidebarViewModel desktopSidebarViewModel;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final GlobalKey<NavigatorState> desktopNavigatorKey;
|
||||
|
||||
DesktopSidebarWrapper({
|
||||
required this.child,
|
||||
required this.bottomSheetService,
|
||||
required this.desktopSidebarViewModel,
|
||||
required this.dashboardViewModel,
|
||||
required this.desktopNavigatorKey,
|
||||
});
|
||||
|
||||
@override
|
||||
ObstructingPreferredSizeWidget appBar(BuildContext context) => DesktopDashboardNavbar(
|
||||
leading: Padding(
|
||||
padding: EdgeInsets.only(left: sideMenuWidth),
|
||||
child: getIt<DesktopWalletSelectionDropDown>(),
|
||||
),
|
||||
middle: SyncIndicator(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
onTap: () => Navigator.of(context, rootNavigator: true).pushNamed(Routes.connectionSync),
|
||||
),
|
||||
trailing: InkWell(
|
||||
onTap: () {
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.unlock,
|
||||
arguments: (bool isAuthenticatedSuccessfully, AuthPageState auth) {
|
||||
if (isAuthenticatedSuccessfully) {
|
||||
auth.close();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Icon(Icons.lock_outline),
|
||||
),
|
||||
);
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => false;
|
||||
|
||||
final pageController = PageController();
|
||||
|
||||
double get sideMenuWidth => 76.0;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
_setEffects();
|
||||
|
||||
return BottomSheetListener(
|
||||
bottomSheetService: bottomSheetService,
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Observer(builder: (_) {
|
||||
return SideMenu(
|
||||
width: sideMenuWidth,
|
||||
topItems: [
|
||||
SideMenuItem(
|
||||
// imagePath: 'assets/images/wallet_outline.png',
|
||||
icon: Icons.home,
|
||||
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.dashboard,
|
||||
onTap: () {
|
||||
desktopSidebarViewModel.onPageChange(SidebarItem.dashboard);
|
||||
desktopNavigatorKey.currentState
|
||||
?.pushNamedAndRemoveUntil(Routes.desktop_actions, (route) => false);
|
||||
},
|
||||
),
|
||||
SideMenuItem(
|
||||
onTap: () {
|
||||
if (desktopSidebarViewModel.currentPage == SidebarItem.transactions) {
|
||||
desktopNavigatorKey.currentState
|
||||
?.pushNamedAndRemoveUntil(Routes.desktop_actions, (route) => false);
|
||||
desktopSidebarViewModel.resetSidebar();
|
||||
} else {
|
||||
desktopSidebarViewModel.onPageChange(SidebarItem.transactions);
|
||||
desktopNavigatorKey.currentState?.pushNamed(Routes.transactionsPage);
|
||||
}
|
||||
},
|
||||
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.transactions,
|
||||
imagePath: 'assets/images/history.svg',
|
||||
),
|
||||
SideMenuItem(
|
||||
widget: ServicesUpdatesWidget(
|
||||
dashboardViewModel.getServicesStatus(),
|
||||
enabled: dashboardViewModel.isEnabledBulletinAction,
|
||||
),
|
||||
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.status,
|
||||
onTap: () {},
|
||||
),
|
||||
],
|
||||
bottomItems: [
|
||||
SideMenuItem(
|
||||
imagePath: 'assets/images/support_icon.png',
|
||||
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.support,
|
||||
onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.support),
|
||||
),
|
||||
SideMenuItem(
|
||||
imagePath: 'assets/images/settings_outline.png',
|
||||
isSelected: desktopSidebarViewModel.currentPage == SidebarItem.settings,
|
||||
onTap: () => desktopSidebarViewModel.onPageChange(SidebarItem.settings),
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
Expanded(
|
||||
child: PageView(
|
||||
controller: pageController,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
child,
|
||||
Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
padding: EdgeInsets.all(20),
|
||||
child: Navigator(
|
||||
initialRoute: Routes.support,
|
||||
onGenerateRoute: (settings) => Router.createRoute(settings),
|
||||
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
|
||||
return [
|
||||
navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))!
|
||||
];
|
||||
},
|
||||
),
|
||||
),
|
||||
Navigator(
|
||||
initialRoute: Routes.desktop_settings_page,
|
||||
onGenerateRoute: (settings) => Router.createRoute(settings),
|
||||
onGenerateInitialRoutes: (NavigatorState navigator, String initialRouteName) {
|
||||
return [
|
||||
navigator.widget.onGenerateRoute!(RouteSettings(name: initialRouteName))!
|
||||
];
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _setEffects() async {
|
||||
reaction<SidebarItem>((_) => desktopSidebarViewModel.currentPage, (page) {
|
||||
if (page == SidebarItem.dashboard) {
|
||||
pageController.jumpToPage(0);
|
||||
return;
|
||||
}
|
||||
pageController.jumpToPage(page.index - 1);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,257 +0,0 @@
|
||||
import 'package:another_flushbar/flushbar.dart';
|
||||
import 'package:hash_wallet/core/new_wallet_arguments.dart';
|
||||
import 'package:hash_wallet/core/auth_service.dart';
|
||||
import 'package:hash_wallet/entities/desktop_dropdown_item.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/auth/auth_page.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/desktop_widgets/dropdown_item_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/wallet_unlock/wallet_unlock_arguments.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:hash_wallet/store/settings_store.dart';
|
||||
import 'package:hash_wallet/utils/show_bar.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_list/wallet_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_list/wallet_list_view_model.dart';
|
||||
import 'package:hash_wallet/wallet_type_utils.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
|
||||
class DesktopWalletSelectionDropDown extends StatefulWidget {
|
||||
final WalletListViewModel walletListViewModel;
|
||||
final AuthService _authService;
|
||||
|
||||
DesktopWalletSelectionDropDown(this.walletListViewModel, this._authService, {Key? key})
|
||||
: super(key: key);
|
||||
|
||||
@override
|
||||
State<DesktopWalletSelectionDropDown> createState() => _DesktopWalletSelectionDropDownState();
|
||||
}
|
||||
|
||||
class _DesktopWalletSelectionDropDownState extends State<DesktopWalletSelectionDropDown> {
|
||||
final moneroIcon = Image.asset('assets/images/crypto/monero.webp', height: 24, width: 24);
|
||||
final bitcoinIcon = Image.asset('assets/images/crypto/bitcoin.webp', height: 24, width: 24);
|
||||
final tBitcoinIcon = Image.asset('assets/images/tbtc.png', height: 24, width: 24);
|
||||
final litecoinIcon = Image.asset('assets/images/crypto/litecoin.webp', height: 24, width: 24);
|
||||
final ethereumIcon = Image.asset('assets/images/crypto/ethereum.webp', height: 24, width: 24);
|
||||
final polygonIcon = Image.asset('assets/images/crypto/polygon.webp', height: 24, width: 24);
|
||||
final bitcoinCashIcon =
|
||||
Image.asset('assets/images/crypto/bitcoin-cash.webp', height: 24, width: 24);
|
||||
final baseIcon = Image.asset('assets/images/crypto/base_icon.webp', height: 24, width: 24);
|
||||
final arbitrumIcon = Image.asset('assets/images/crypto/arbitrum.webp', height: 24, width: 24);
|
||||
final bscIcon = Image.asset('assets/images/crypto/BNB.webp', height: 24, width: 24);
|
||||
final nanoIcon = Image.asset('assets/images/crypto/nano.webp', height: 24, width: 24);
|
||||
final bananoIcon = Image.asset('assets/images/crypto/nano.webp', height: 24, width: 24);
|
||||
final wowneroIcon = Image.asset('assets/images/crypto/wownero.webp', height: 24, width: 24);
|
||||
final dogeIcon = Image.asset('assets/images/crypto/dogecoin.webp', height: 24, width: 24);
|
||||
final nonWalletTypeIcon = Image.asset('assets/images/close.png', height: 24, width: 24);
|
||||
|
||||
Image _newWalletImage(BuildContext context) => Image.asset(
|
||||
'assets/images/new_wallet.png',
|
||||
height: 12,
|
||||
width: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
|
||||
Image _restoreWalletImage(BuildContext context) => Image.asset(
|
||||
'assets/images/restore_wallet.png',
|
||||
height: 12,
|
||||
width: 12,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
);
|
||||
|
||||
Flushbar<void>? _progressBar;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final themeData = Theme.of(context);
|
||||
return Observer(builder: (context) {
|
||||
final dropDownItems = [
|
||||
...widget.walletListViewModel.wallets
|
||||
.map((wallet) => DesktopDropdownItem(
|
||||
isSelected: wallet.isCurrent,
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: 500),
|
||||
child: DropDownItemWidget(
|
||||
title: wallet.name,
|
||||
image: wallet.isEnabled
|
||||
? _imageFor(type: wallet.type, isTestnet: wallet.isTestnet)
|
||||
: nonWalletTypeIcon,
|
||||
),
|
||||
),
|
||||
onSelected: () => _onSelectedWallet(wallet),
|
||||
))
|
||||
.toList(),
|
||||
DesktopDropdownItem(
|
||||
onSelected: () => _navigateToCreateWallet(),
|
||||
child: DropDownItemWidget(
|
||||
title: S.of(context).create_new,
|
||||
image: _newWalletImage(context),
|
||||
),
|
||||
),
|
||||
DesktopDropdownItem(
|
||||
onSelected: () => _navigateToRestoreWallet(),
|
||||
child: DropDownItemWidget(
|
||||
title: S.of(context).restore_wallet,
|
||||
image: _restoreWalletImage(context),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
final selectedItem = dropDownItems.firstWhere(
|
||||
(element) => element.isSelected,
|
||||
orElse: () => dropDownItems.first,
|
||||
);
|
||||
|
||||
return DropdownButton<DesktopDropdownItem>(
|
||||
items: dropDownItems
|
||||
.map(
|
||||
(wallet) => DropdownMenuItem<DesktopDropdownItem>(
|
||||
child: wallet.child,
|
||||
value: wallet,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (item) {
|
||||
item?.onSelected();
|
||||
},
|
||||
dropdownColor: themeData.colorScheme.surface,
|
||||
style: themeData.textTheme.bodyMedium,
|
||||
selectedItemBuilder: (context) => dropDownItems.map((item) => item.child).toList(),
|
||||
value: selectedItem,
|
||||
underline: const SizedBox(),
|
||||
focusColor: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
void _onSelectedWallet(WalletListItem selectedWallet) async {
|
||||
if (selectedWallet.isCurrent || !selectedWallet.isEnabled) return;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: S.of(context).change_wallet_alert_title,
|
||||
alertContent: S.of(context).change_wallet_alert_content(selectedWallet.name),
|
||||
leftButtonText: S.of(context).cancel,
|
||||
rightButtonText: S.of(context).change,
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
|
||||
actionRightButton: () => Navigator.of(dialogContext).pop(true));
|
||||
}) ??
|
||||
false;
|
||||
|
||||
if (confirmed) {
|
||||
await _loadWallet(selectedWallet);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Widget _imageFor({required WalletType type, bool? isTestnet}) {
|
||||
switch (type) {
|
||||
case WalletType.bitcoin:
|
||||
if (isTestnet == true) return tBitcoinIcon;
|
||||
return bitcoinIcon;
|
||||
case WalletType.monero:
|
||||
return moneroIcon;
|
||||
case WalletType.wownero:
|
||||
return wowneroIcon;
|
||||
case WalletType.litecoin:
|
||||
return litecoinIcon;
|
||||
case WalletType.ethereum:
|
||||
return ethereumIcon;
|
||||
case WalletType.bitcoinCash:
|
||||
return bitcoinCashIcon;
|
||||
case WalletType.nano:
|
||||
return nanoIcon;
|
||||
case WalletType.banano:
|
||||
return bananoIcon;
|
||||
case WalletType.polygon:
|
||||
return polygonIcon;
|
||||
case WalletType.dogecoin:
|
||||
return dogeIcon;
|
||||
case WalletType.base:
|
||||
return baseIcon;
|
||||
case WalletType.arbitrum:
|
||||
return arbitrumIcon;
|
||||
case WalletType.bsc:
|
||||
return bscIcon;
|
||||
case WalletType.none:
|
||||
return nonWalletTypeIcon;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _loadWallet(WalletListItem wallet) async {
|
||||
if (SettingsStoreBase.walletPasswordDirectInput) {
|
||||
Navigator.of(context).pushNamed(Routes.walletUnlockLoadable,
|
||||
arguments: WalletUnlockArguments(
|
||||
callback: (bool isAuthenticatedSuccessfully, AuthPageState auth) async {
|
||||
if (isAuthenticatedSuccessfully) {
|
||||
auth.close();
|
||||
setState(() {});
|
||||
}
|
||||
},
|
||||
walletName: wallet.name,
|
||||
walletType: wallet.type));
|
||||
return;
|
||||
}
|
||||
|
||||
widget._authService.authenticateAction(
|
||||
context,
|
||||
onAuthSuccess: (isAuthenticatedSuccessfully) async {
|
||||
if (!isAuthenticatedSuccessfully) return;
|
||||
|
||||
try {
|
||||
if (mounted) {
|
||||
changeProcessText(S.of(context).wallet_list_loading_wallet(wallet.name));
|
||||
}
|
||||
await widget.walletListViewModel.loadWallet(wallet);
|
||||
hideProgressText();
|
||||
setState(() {});
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
changeProcessText(S.of(context).wallet_list_failed_to_load(wallet.name, e.toString()));
|
||||
}
|
||||
}
|
||||
},
|
||||
conditionToDetermineIfToUse2FA:
|
||||
widget.walletListViewModel.shouldRequireTOTP2FAForAccessingWallet,
|
||||
);
|
||||
}
|
||||
|
||||
void _navigateToCreateWallet() {
|
||||
if (isSingleCoin) {
|
||||
widget._authService.authenticateAction(
|
||||
context,
|
||||
route: Routes.newWallet,
|
||||
arguments: NewWalletArguments(type: widget.walletListViewModel.currentWalletType),
|
||||
conditionToDetermineIfToUse2FA:
|
||||
widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
|
||||
);
|
||||
} else {
|
||||
widget._authService.authenticateAction(
|
||||
context,
|
||||
route: Routes.newWalletType,
|
||||
conditionToDetermineIfToUse2FA:
|
||||
widget.walletListViewModel.shouldRequireTOTP2FAForCreatingNewWallets,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _navigateToRestoreWallet() {
|
||||
Navigator.of(context).pushNamed(Routes.restoreOptions, arguments: false);
|
||||
}
|
||||
|
||||
void changeProcessText(String text) {
|
||||
_progressBar = createBar<void>(text, context, duration: null)..show(context);
|
||||
}
|
||||
|
||||
void hideProgressText() {
|
||||
_progressBar?.dismiss();
|
||||
_progressBar = null;
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class DropDownItemWidget extends StatelessWidget {
|
||||
const DropDownItemWidget({super.key, required this.title, required this.image});
|
||||
final double tileHeight = 60;
|
||||
final Widget image;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: tileHeight,
|
||||
padding: EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
image,
|
||||
SizedBox(width: 10),
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 1,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:hash_wallet/core/address_validator.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/address_text_field.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
@@ -38,13 +39,11 @@ class EditTokenPage extends BasePage {
|
||||
: S.current.add_token;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return EditTokenPageBody(
|
||||
Widget body(BuildContext context) => EditTokenPageBody(
|
||||
homeSettingsViewModel: homeSettingsViewModel,
|
||||
token: token,
|
||||
initialContractAddress: initialContractAddress,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class EditTokenPageBody extends StatefulWidget {
|
||||
@@ -127,14 +126,14 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
final isVerified = widget.homeSettingsViewModel.checkIfTokenIsWhitelisted(contractAddress);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() {
|
||||
_isTokenVerified = isVerified;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
Widget build(BuildContext context) => GestureDetector(
|
||||
onTap: () => FocusScope.of(context).unfocus(),
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.zero,
|
||||
@@ -172,8 +171,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
SizedBox(height: 20),
|
||||
],
|
||||
Observer(
|
||||
builder: (context) {
|
||||
return Row(
|
||||
builder: (context) => Row(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: LoadingPrimaryButton(
|
||||
@@ -208,14 +206,12 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
if (isTokenAlreadyAdded) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertWithOneAction(
|
||||
builder: (dialogContext) => AlertWithOneAction(
|
||||
alertTitle: S.current.warning,
|
||||
alertContent: S.of(context).token_already_exists,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(dialogContext).pop(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
return;
|
||||
}
|
||||
@@ -275,14 +271,12 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
} catch (e) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertWithOneAction(
|
||||
builder: (dialogContext) => AlertWithOneAction(
|
||||
alertTitle: S.current.warning,
|
||||
alertContent: e.toString(),
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(dialogContext).pop(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -290,8 +284,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
if (hasPotentialError && !isWhitelisted) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertWithTwoActions(
|
||||
builder: (dialogContext) => AlertWithTwoActions(
|
||||
alertTitle: S.current.warning,
|
||||
alertContent: S.current.contract_warning,
|
||||
rightButtonText: S.of(context).continue_text,
|
||||
@@ -301,8 +294,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
await actionCall();
|
||||
},
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
@@ -310,14 +302,12 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
} catch (e) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertWithOneAction(
|
||||
builder: (dialogContext) => AlertWithOneAction(
|
||||
alertTitle: "Unable to add token",
|
||||
alertContent: "$e",
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
if (mounted) {
|
||||
@@ -332,14 +322,12 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _getTokenInfo() async {
|
||||
if (_contractAddressController.text.isNotEmpty) {
|
||||
@@ -373,8 +361,7 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
}
|
||||
}
|
||||
|
||||
Widget _tokenForm() {
|
||||
return Form(
|
||||
Widget _tokenForm() => Form(
|
||||
key: _formKey,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
@@ -393,10 +380,9 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
ClipOval(
|
||||
child: HashImageWidget(
|
||||
child: TokenImageWidget(
|
||||
imageUrl: _tokenIconPathController.text,
|
||||
width: 75,
|
||||
height: 75,
|
||||
size: 75,
|
||||
),
|
||||
),
|
||||
if (_isTokenVerified)
|
||||
@@ -512,5 +498,4 @@ class _EditTokenPageBodyState extends State<EditTokenPageBody> {
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ class FavoriteTokenModal extends StatelessWidget {
|
||||
ModalTopBar(
|
||||
title: S.of(context).favorite_token,
|
||||
leadingIcon: Icon(Icons.close),
|
||||
leadingSemanticLabel: S.of(context).close,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
@@ -38,11 +39,13 @@ class FavoriteTokenModal extends StatelessWidget {
|
||||
child: NewListSections(
|
||||
sections: {
|
||||
"": tokens
|
||||
.map((item) => ListItemRegularRow(
|
||||
keyValue: item.title,
|
||||
label: "${item.fullName} (${item.title})",
|
||||
iconPath: item.iconPath,
|
||||
onTap: () => Navigator.of(context).pop(item)))
|
||||
.asMap()
|
||||
.entries
|
||||
.map((entry) => ListItemRegularRow(
|
||||
keyValue: "${entry.value.title}_${entry.key}",
|
||||
label: "${entry.value.fullName} (${entry.value.title})",
|
||||
iconPath: entry.value.iconPath,
|
||||
onTap: () => Navigator.of(context).pop(entry.value)))
|
||||
.toList()
|
||||
},
|
||||
),
|
||||
|
||||
@@ -5,14 +5,11 @@ import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item_toggle.dart';
|
||||
import 'package:hash_wallet/entities/sort_balance_types.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/pages/settings_page.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/favorite_token_modal.dart';
|
||||
import 'package:hash_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
|
||||
import 'package:hash_wallet/src/screens/settings/widgets/settings_switcher_cell.dart';
|
||||
import 'package:hash_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:hash_wallet/src/widgets/picker.dart';
|
||||
@@ -186,30 +183,25 @@ class HomeSettingsPage extends BasePage {
|
||||
'token': token,
|
||||
});
|
||||
},
|
||||
leading: Container(
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(shape: BoxShape.circle),
|
||||
child: HashImageWidget(
|
||||
imageUrl: token.iconPath,
|
||||
height: 40,
|
||||
width: 40,
|
||||
errorWidget: Container(
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
token.title.substring(0, min(token.title.length, 2)),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
leading: TokenImageWidget(
|
||||
imageUrl: token.iconPath ?? '',
|
||||
size: 40,
|
||||
errorWidget: Container(
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
token.title.substring(0, min(token.title.length, 2)),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
|
||||
@@ -1,320 +0,0 @@
|
||||
import 'package:hash_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:hash_wallet/src/screens/new_wallet/widgets/select_button.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/anonpay/anonpay_donation_link_info.dart';
|
||||
import 'package:hash_wallet/entities/preferences_key.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/anonpay_receive_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/receive_page_option.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart';
|
||||
import 'package:hash_wallet/src/widgets/gradient_background.dart';
|
||||
import 'package:hash_wallet/src/widgets/keyboard_done_button.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/utils/share_util.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/receive_option_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/qr_widget.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:keyboard_actions/keyboard_actions.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AddressPage extends BasePage {
|
||||
AddressPage({
|
||||
required this.addressListViewModel,
|
||||
required this.dashboardViewModel,
|
||||
required this.receiveOptionViewModel,
|
||||
}) : _cryptoAmountFocus = FocusNode(),
|
||||
_formKey = GlobalKey<FormState>(),
|
||||
_amountController = TextEditingController() {
|
||||
_amountController.addListener(() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
addressListViewModel.changeAmount(_amountController.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final WalletAddressListViewModel addressListViewModel;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final ReceiveOptionViewModel receiveOptionViewModel;
|
||||
final TextEditingController _amountController;
|
||||
final GlobalKey<FormState> _formKey;
|
||||
|
||||
final FocusNode _cryptoAmountFocus;
|
||||
|
||||
@override
|
||||
bool get gradientBackground => true;
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => false;
|
||||
|
||||
bool effectsInstalled = false;
|
||||
|
||||
@override
|
||||
Widget? leading(BuildContext context) {
|
||||
final _backButton = Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 16,
|
||||
);
|
||||
final _closeButton = currentTheme.isDark ? closeButtonImageDarkTheme : closeButtonImage;
|
||||
|
||||
bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
|
||||
|
||||
return MergeSemantics(
|
||||
child: SizedBox(
|
||||
height: isMobileView ? 37 : 45,
|
||||
width: isMobileView ? 37 : 45,
|
||||
child: ButtonTheme(
|
||||
minWidth: double.minPositive,
|
||||
child: Semantics(
|
||||
label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent),
|
||||
),
|
||||
onPressed: () => onClose(context),
|
||||
child: !isMobileView ? _closeButton : _backButton,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) => PresentReceiveOptionPicker(
|
||||
color: titleColor(context), receiveOptionViewModel: receiveOptionViewModel);
|
||||
|
||||
@override
|
||||
Widget Function(BuildContext, Widget) get rootWrapper =>
|
||||
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
|
||||
|
||||
@override
|
||||
Widget? trailing(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Semantics(
|
||||
label: S.of(context).share,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(),
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
iconSize: 25,
|
||||
onPressed: () {
|
||||
ShareUtil.share(
|
||||
text: addressListViewModel.uri.toString(),
|
||||
context: context,
|
||||
);
|
||||
},
|
||||
icon: Icon(Icons.share, size: 20, color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
_setEffects(context);
|
||||
|
||||
return KeyboardActions(
|
||||
autoScroll: false,
|
||||
disableScroll: true,
|
||||
tapOutsideBehavior: TapOutsideBehavior.translucentDismiss,
|
||||
config: KeyboardActionsConfig(
|
||||
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
|
||||
keyboardBarColor: Theme.of(context).colorScheme.surface,
|
||||
nextFocus: false,
|
||||
actions: [
|
||||
KeyboardActionsItem(
|
||||
focusNode: _cryptoAmountFocus,
|
||||
toolbarButtons: [(_) => KeyboardDoneButton()],
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.fromLTRB(24, 0, 24, 32),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: QRWidget(
|
||||
formKey: _formKey,
|
||||
addressListViewModel: addressListViewModel,
|
||||
amountTextFieldFocusNode: _cryptoAmountFocus,
|
||||
amountController: _amountController,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (addressListViewModel.hasAddressList) {
|
||||
return SelectButton(
|
||||
text: addressListViewModel.buttonTitle,
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.receive),
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
arrowColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
textSize: 14,
|
||||
height: 50,
|
||||
);
|
||||
} else {
|
||||
return const SizedBox();
|
||||
}
|
||||
},
|
||||
),
|
||||
if (addressListViewModel.hasTokensList) ...[
|
||||
Container(
|
||||
padding: EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: Theme.of(context).colorScheme.surfaceContainer),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: addressListViewModel.monoImage,
|
||||
height: 16,
|
||||
width: 16,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
'${S.current.your} ${addressListViewModel.walletTypeName} ${S.current.address}',
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 10),
|
||||
Text(
|
||||
'${S.current.qr_instruction} ${addressListViewModel.walletTypeName}',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 20),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
final walletImages = addressListViewModel
|
||||
.getWalletImages(addressListViewModel.selectedChainId);
|
||||
return Center(
|
||||
child: SizedBox(
|
||||
height: 40,
|
||||
width: walletImages.length * 32.0,
|
||||
child: Stack(
|
||||
children: [
|
||||
for (int i = walletImages.length - 1; i >= 0; i--)
|
||||
Positioned(
|
||||
left: i * 25.0,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
width: 3,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
child: ClipOval(
|
||||
child: HashImageWidget(
|
||||
height: 35,
|
||||
width: 35,
|
||||
imageUrl: walletImages[i],
|
||||
color: walletImages.last == walletImages[i]
|
||||
? Theme.of(context).colorScheme.onSurfaceVariant
|
||||
: null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 48),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _setEffects(BuildContext context) {
|
||||
if (effectsInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
reaction((_) => receiveOptionViewModel.selectedReceiveOption, (ReceivePageOption option) {
|
||||
if (dashboardViewModel.type == WalletType.bitcoin &&
|
||||
bitcoin!.isBitcoinReceivePageOption(option)) {
|
||||
addressListViewModel.setAddressType(bitcoin!.getOptionToType(option));
|
||||
return;
|
||||
}
|
||||
|
||||
switch (option) {
|
||||
case ReceivePageOption.anonPayInvoice:
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.anonPayInvoicePage,
|
||||
arguments: [addressListViewModel.address.address, option],
|
||||
);
|
||||
break;
|
||||
case ReceivePageOption.anonPayDonationLink:
|
||||
final sharedPreferences = getIt.get<SharedPreferences>();
|
||||
final clearnetUrl = sharedPreferences.getString(PreferencesKey.clearnetDonationLink);
|
||||
final onionUrl = sharedPreferences.getString(PreferencesKey.onionDonationLink);
|
||||
final donationWalletName =
|
||||
sharedPreferences.getString(PreferencesKey.donationLinkWalletName);
|
||||
|
||||
if (clearnetUrl != null &&
|
||||
onionUrl != null &&
|
||||
addressListViewModel.wallet.name == donationWalletName) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.anonPayReceivePage,
|
||||
arguments: AnonPayReceivePageArgs(
|
||||
invoiceInfo: AnonpayDonationLinkInfo(
|
||||
clearnetUrl: clearnetUrl,
|
||||
onionUrl: onionUrl,
|
||||
address: addressListViewModel.address.address,
|
||||
),
|
||||
qrImage: addressListViewModel.qrImage,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.anonPayInvoicePage,
|
||||
arguments: [addressListViewModel.address.address, option],
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
if ([WalletType.bitcoin, WalletType.litecoin].contains(addressListViewModel.type)) {
|
||||
addressListViewModel.setAddressType(bitcoin!.getBitcoinAddressType(option));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
effectsInstalled = true;
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
import 'package:hash_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/balance/crypto_balance_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/nft_listing_page.dart';
|
||||
import 'package:hash_wallet/store/settings_store.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/nft_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class BalancePage extends StatelessWidget {
|
||||
BalancePage({
|
||||
required this.dashboardViewModel,
|
||||
required this.settingsStore,
|
||||
required this.nftViewModel,
|
||||
});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final NFTViewModel nftViewModel;
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (context) {
|
||||
final isNFTActivated = isNFTACtivatedChain(dashboardViewModel.type,
|
||||
chainId: dashboardViewModel.wallet.chainId);
|
||||
return DefaultTabController(
|
||||
key: ValueKey<bool>(isNFTActivated),
|
||||
length: isNFTActivated ? 2 : 1,
|
||||
child: Column(
|
||||
children: [
|
||||
if (isNFTActivated)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 8),
|
||||
child: TabBar(
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
isScrollable: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
labelStyle: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
unselectedLabelStyle: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
labelColor: Theme.of(context).colorScheme.primary,
|
||||
dividerColor: Colors.transparent,
|
||||
indicatorColor: Theme.of(context).colorScheme.primary,
|
||||
unselectedLabelColor:
|
||||
Theme.of(context).colorScheme.secondary.withOpacity(0.7),
|
||||
tabAlignment: TabAlignment.start,
|
||||
tabs: const [
|
||||
Tab(text: 'My Crypto'),
|
||||
Tab(text: 'My NFTs'),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: TabBarView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
CryptoBalanceWidget(dashboardViewModel: dashboardViewModel),
|
||||
if (isNFTActivated) NFTListingPage(nftViewModel: nftViewModel)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,693 +0,0 @@
|
||||
import 'dart:math';
|
||||
|
||||
import 'package:auto_size_text/auto_size_text.dart';
|
||||
import 'package:hash_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange_trade/information_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/themes/core/theme_extension.dart';
|
||||
import 'package:hash_wallet/utils/payment_request.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:fluttertoast/fluttertoast.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class BalanceRowWidget extends StatelessWidget {
|
||||
BalanceRowWidget({
|
||||
required this.availableBalanceLabel,
|
||||
required this.availableBalance,
|
||||
required this.availableFiatBalance,
|
||||
required this.additionalBalanceLabel,
|
||||
required this.additionalBalance,
|
||||
required this.additionalFiatBalance,
|
||||
required this.secondAvailableBalanceLabel,
|
||||
required this.secondAvailableBalance,
|
||||
required this.secondAvailableFiatBalance,
|
||||
required this.secondAdditionalBalanceLabel,
|
||||
required this.secondAdditionalBalance,
|
||||
required this.secondAdditionalFiatBalance,
|
||||
required this.frozenBalance,
|
||||
required this.frozenFiatBalance,
|
||||
required this.currency,
|
||||
required this.hasAdditionalBalance,
|
||||
required this.hasSecondAvailableBalance,
|
||||
required this.hasSecondAdditionalBalance,
|
||||
required this.isTestnet,
|
||||
required this.dashboardViewModel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String availableBalanceLabel;
|
||||
final String availableBalance;
|
||||
final String availableFiatBalance;
|
||||
final String additionalBalanceLabel;
|
||||
final String additionalBalance;
|
||||
final String additionalFiatBalance;
|
||||
final String secondAvailableBalanceLabel;
|
||||
final String secondAvailableBalance;
|
||||
final String secondAvailableFiatBalance;
|
||||
final String secondAdditionalBalanceLabel;
|
||||
final String secondAdditionalBalance;
|
||||
final String secondAdditionalFiatBalance;
|
||||
final String frozenBalance;
|
||||
final String frozenFiatBalance;
|
||||
final CryptoCurrency currency;
|
||||
final bool hasAdditionalBalance;
|
||||
final bool hasSecondAvailableBalance;
|
||||
final bool hasSecondAdditionalBalance;
|
||||
final bool isTestnet;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15.0),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.customColors.cardGradientColorPrimary,
|
||||
context.customColors.cardGradientColorSecondary,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: TextButton(
|
||||
onPressed: _showToast,
|
||||
onLongPress: () => dashboardViewModel.balanceViewModel.switchBalanceValue(),
|
||||
style: TextButton.styleFrom(
|
||||
side: BorderSide(
|
||||
width: 1.25, color: Theme.of(context).colorScheme.surfaceContainerHigh),
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(
|
||||
top: 10,
|
||||
left: 12,
|
||||
right: 12,
|
||||
bottom: 10,
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: hasAdditionalBalance
|
||||
? () => _showBalanceDescription(
|
||||
context, S.of(context).available_balance_description)
|
||||
: null,
|
||||
child: Row(
|
||||
children: [
|
||||
Semantics(
|
||||
hint: 'Double tap to see more information',
|
||||
container: true,
|
||||
child: Text(
|
||||
'${availableBalanceLabel}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasAdditionalBalance)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Icon(
|
||||
Icons.help_outline,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
AutoSizeText(
|
||||
availableBalance,
|
||||
minFontSize: 16,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
fontWeight: FontWeight.w900,
|
||||
fontSize: 24,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.start,
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
if (isTestnet)
|
||||
Text(
|
||||
S.of(context).testnet_coins_no_value,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
if (!isTestnet)
|
||||
Text(
|
||||
'${availableFiatBalance}',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
height: 40,
|
||||
width: 40,
|
||||
errorWidget: Container(
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
currency.title.substring(0, min(currency.title.length, 2)),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
currency.title,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (currency.isPotentialScam)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.warning_amber_outlined,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
S.of(context).potential_scam,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (frozenBalance.isNotEmpty)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 26),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).frozen_balance,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
AutoSizeText(
|
||||
frozenBalance,
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
fontSize: 20,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (!isTestnet)
|
||||
Text(
|
||||
frozenFiatBalance,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(height: 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hasAdditionalBalance)
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
'${additionalBalanceLabel}',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
AutoSizeText(
|
||||
additionalBalance,
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
fontSize: 20,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
if (!isTestnet)
|
||||
Text(
|
||||
'${additionalFiatBalance}',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(height: 1),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (hasSecondAdditionalBalance || hasSecondAvailableBalance) ...[
|
||||
SizedBox(height: 10),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 16, right: 16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(15),
|
||||
gradient: LinearGradient(
|
||||
colors: [
|
||||
context.customColors.cardGradientColorPrimary,
|
||||
context.customColors.cardGradientColorSecondary,
|
||||
],
|
||||
begin: Alignment.topCenter,
|
||||
end: Alignment.bottomCenter,
|
||||
),
|
||||
),
|
||||
child: TextButton(
|
||||
onPressed: _showToast,
|
||||
onLongPress: () => dashboardViewModel.balanceViewModel.switchBalanceValue(),
|
||||
style: TextButton.styleFrom(
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(15)),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 10, left: 12, right: 8, bottom: 10),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (currency == CryptoCurrency.ltc)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
ImageIcon(
|
||||
AssetImage('assets/images/mweb_logo.png'),
|
||||
size: 40,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'MWEB',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (currency == CryptoCurrency.btc)
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
SvgPicture.asset(
|
||||
'assets/images/lightning-icon.svg',
|
||||
width: 40,
|
||||
height: 40,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'Lightning',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w700,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hasSecondAvailableBalance)
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: onPressedHelp,
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
secondAvailableBalanceLabel,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color:
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Icon(
|
||||
Icons.help_outline,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
AutoSizeText(
|
||||
secondAvailableBalance,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.w800,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 6),
|
||||
if (!isTestnet)
|
||||
Text(
|
||||
secondAvailableFiatBalance,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 0, left: 24, right: 8, bottom: 16),
|
||||
child: Stack(
|
||||
children: [
|
||||
if (hasSecondAdditionalBalance)
|
||||
Row(
|
||||
children: [
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
SizedBox(height: 24),
|
||||
Text(
|
||||
secondAdditionalBalanceLabel,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
AutoSizeText(
|
||||
secondAdditionalBalance,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 20,
|
||||
color: Theme.of(context).colorScheme.secondary,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
SizedBox(height: 4),
|
||||
if (!isTestnet)
|
||||
Text(
|
||||
'${secondAdditionalFiatBalance}',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
IntrinsicHeight(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
label: depositToL2Label,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => depositToL2(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
side: BorderSide(
|
||||
color:
|
||||
Theme.of(context).colorScheme.outlineVariant.withAlpha(0),
|
||||
width: 0,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
height: 30,
|
||||
width: 30,
|
||||
'assets/images/received.png',
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
depositToL2Label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Semantics(
|
||||
label: withdrawFromL2Label,
|
||||
child: OutlinedButton(
|
||||
onPressed: () => withdrawFromL2(context),
|
||||
style: OutlinedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
side: BorderSide(
|
||||
color:
|
||||
Theme.of(context).colorScheme.outlineVariant.withAlpha(0),
|
||||
width: 0,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.symmetric(vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Image.asset(
|
||||
height: 30,
|
||||
width: 30,
|
||||
'assets/images/upload.png',
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
withdrawFromL2Label,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSecondaryContainer,
|
||||
fontWeight: FontWeight.w700,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String get depositToL2Label => dashboardViewModel.type == WalletType.litecoin
|
||||
? S.current.litecoin_mweb_pegin
|
||||
: S.current.bitcoin_lightning_deposit;
|
||||
|
||||
String get withdrawFromL2Label => dashboardViewModel.type == WalletType.litecoin
|
||||
? S.current.litecoin_mweb_pegout
|
||||
: S.current.bitcoin_lightning_withdraw;
|
||||
|
||||
Future<void> depositToL2(BuildContext context) async {
|
||||
PaymentRequest? paymentRequest = null;
|
||||
|
||||
if (dashboardViewModel.type == WalletType.litecoin) {
|
||||
final depositAddress = bitcoin!.getUnusedMwebAddress(dashboardViewModel.wallet);
|
||||
if ((depositAddress?.isNotEmpty ?? false)) {
|
||||
paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$depositAddress"));
|
||||
}
|
||||
} else if (dashboardViewModel.type == WalletType.bitcoin) {
|
||||
final depositAddress = await bitcoin!.getUnusedSpakDepositAddress(dashboardViewModel.wallet);
|
||||
if ((depositAddress?.isNotEmpty ?? false)) {
|
||||
paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$depositAddress"));
|
||||
}
|
||||
}
|
||||
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.send,
|
||||
arguments: {
|
||||
'paymentRequest': paymentRequest,
|
||||
'coinTypeToSpendFrom': UnspentCoinType.nonMweb,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> withdrawFromL2(BuildContext context) async {
|
||||
PaymentRequest? paymentRequest = null;
|
||||
UnspentCoinType unspentCoinType = UnspentCoinType.any;
|
||||
final withdrawAddress = bitcoin!.getUnusedSegwitAddress(dashboardViewModel.wallet);
|
||||
|
||||
if (dashboardViewModel.type == WalletType.litecoin) {
|
||||
if ((withdrawAddress?.isNotEmpty ?? false)) {
|
||||
paymentRequest = PaymentRequest.fromUri(Uri.parse("litecoin:$withdrawAddress"));
|
||||
}
|
||||
unspentCoinType = UnspentCoinType.mweb;
|
||||
} else if (dashboardViewModel.type == WalletType.bitcoin) {
|
||||
if ((withdrawAddress?.isNotEmpty ?? false)) {
|
||||
paymentRequest = PaymentRequest.fromUri(Uri.parse("bitcoin:$withdrawAddress"));
|
||||
}
|
||||
unspentCoinType = UnspentCoinType.lightning;
|
||||
}
|
||||
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.send,
|
||||
arguments: {
|
||||
'paymentRequest': paymentRequest,
|
||||
'coinTypeToSpendFrom': unspentCoinType,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void onPressedHelp() {
|
||||
var helpUri = Uri.parse("https://github.com/Such-Software/hash-wallet");
|
||||
if (dashboardViewModel.type == WalletType.litecoin) {
|
||||
helpUri = Uri.parse("https://github.com/Such-Software/hash-wallet");
|
||||
}
|
||||
|
||||
launchUrl(helpUri, mode: LaunchMode.externalApplication);
|
||||
}
|
||||
|
||||
void _showBalanceDescription(BuildContext context, String content) {
|
||||
showPopUp<void>(context: context, builder: (_) => InformationPage(information: content));
|
||||
}
|
||||
|
||||
void _showToast() async {
|
||||
try {
|
||||
await Fluttertoast.showToast(
|
||||
msg: S.current.show_balance_toast,
|
||||
backgroundColor: Color.fromRGBO(0, 0, 0, 0.85),
|
||||
);
|
||||
} catch (_) {}
|
||||
}
|
||||
}
|
||||
@@ -1,448 +0,0 @@
|
||||
import 'package:hash_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/balance/balance_row_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/home_screen_account_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/info_card.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:hash_wallet/src/widgets/dashboard_card_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/standard_switch.dart';
|
||||
import 'package:hash_wallet/themes/core/theme_extension.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class CryptoBalanceWidget extends StatelessWidget {
|
||||
const CryptoBalanceWidget({
|
||||
super.key,
|
||||
required this.dashboardViewModel,
|
||||
});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final btcLockLight = 'assets/images/btc_lock_light.png';
|
||||
final btcLockDark = 'assets/images/btc_lock_dark.png';
|
||||
|
||||
String? get hardwareWalletIcon {
|
||||
switch (dashboardViewModel.wallet.hardwareWalletType) {
|
||||
case null:
|
||||
return null;
|
||||
case HardwareWalletType.bitbox:
|
||||
return "assets/images/hardware_wallet/device_bitbox.svg";
|
||||
case HardwareWalletType.ledger:
|
||||
return "assets/images/hardware_wallet/device_ledger_nano_x.svg";
|
||||
case HardwareWalletType.trezor:
|
||||
return "assets/images/hardware_wallet/device_trezor_safe_5.svg";
|
||||
case HardwareWalletType.cupcake:
|
||||
return "assets/images/cupcake.svg";
|
||||
case HardwareWalletType.coldcard:
|
||||
case HardwareWalletType.seedsigner:
|
||||
case HardwareWalletType.keystone:
|
||||
return "assets/images/hardware_wallet/device_qr.svg";
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (dashboardViewModel.getMoneroError != null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: DashBoardRoundedCardWidget(
|
||||
title: "Invalid monero bindings",
|
||||
subTitle: dashboardViewModel.getMoneroError.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (dashboardViewModel.getWowneroError != null) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: DashBoardRoundedCardWidget(
|
||||
title: "Invalid wownero bindings",
|
||||
subTitle: dashboardViewModel.getWowneroError.toString(),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Container();
|
||||
},
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => dashboardViewModel.balanceViewModel.hasAccounts
|
||||
? HomeScreenAccountWidget(
|
||||
walletName: dashboardViewModel.name, accountName: dashboardViewModel.subname)
|
||||
: Column(
|
||||
children: [
|
||||
SizedBox(height: 16),
|
||||
Container(
|
||||
margin: const EdgeInsets.only(left: 24, bottom: 16),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
return Row(
|
||||
children: [
|
||||
Text(
|
||||
dashboardViewModel.balanceViewModel.asset,
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1,
|
||||
),
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
if (hardwareWalletIcon != null)
|
||||
Container(
|
||||
child: SvgPicture.asset(
|
||||
hardwareWalletIcon!,
|
||||
width: 24,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (dashboardViewModel
|
||||
.balanceViewModel.isHomeScreenSettingsEnabled)
|
||||
TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
minimumSize: Size(50, 30),
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
alignment: Alignment.centerLeft),
|
||||
onPressed: () => Navigator.pushNamed(
|
||||
context,
|
||||
Routes.homeSettings,
|
||||
arguments: dashboardViewModel.balanceViewModel,
|
||||
),
|
||||
child: Container(
|
||||
child: SvgPicture.asset(
|
||||
'assets/images/home_screen_setting_icon.svg',
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 30),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
)),
|
||||
Observer(builder: (_) {
|
||||
if (!dashboardViewModel.showRepWarning) {
|
||||
return const SizedBox();
|
||||
}
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: DashBoardRoundedCardWidget(
|
||||
title: S.of(context).rep_warning,
|
||||
subTitle: S.of(context).rep_warning_sub,
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.changeRep),
|
||||
onClose: () {
|
||||
dashboardViewModel.settingsStore.shouldShowRepWarning = false;
|
||||
},
|
||||
),
|
||||
);
|
||||
}),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
if (dashboardViewModel.balanceViewModel.formattedBalances.isEmpty) {
|
||||
return Center(
|
||||
child: Container(
|
||||
child: Text(
|
||||
'Loading balances...',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return ListView.separated(
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
shrinkWrap: true,
|
||||
separatorBuilder: (_, __) => Container(padding: EdgeInsets.only(bottom: 10)),
|
||||
itemCount: dashboardViewModel.balanceViewModel.formattedBalances.length,
|
||||
itemBuilder: (__, index) {
|
||||
final balance =
|
||||
dashboardViewModel.balanceViewModel.formattedBalances.elementAt(index);
|
||||
return Observer(builder: (_) {
|
||||
return BalanceRowWidget(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
availableBalanceLabel:
|
||||
'${dashboardViewModel.balanceViewModel.availableBalanceLabel}',
|
||||
availableBalance: balance.availableBalance,
|
||||
availableFiatBalance: balance.fiatAvailableBalance,
|
||||
additionalBalanceLabel:
|
||||
'${dashboardViewModel.balanceViewModel.additionalBalanceLabel}',
|
||||
additionalBalance: balance.additionalBalance,
|
||||
additionalFiatBalance: balance.fiatAdditionalBalance,
|
||||
frozenBalance: balance.frozenBalance,
|
||||
frozenFiatBalance: balance.fiatFrozenBalance,
|
||||
currency: balance.asset,
|
||||
hasAdditionalBalance:
|
||||
dashboardViewModel.balanceViewModel.hasAdditionalBalance(balance.asset),
|
||||
hasSecondAdditionalBalance:
|
||||
dashboardViewModel.balanceViewModel.hasSecondAdditionalBalance,
|
||||
hasSecondAvailableBalance:
|
||||
dashboardViewModel.balanceViewModel.hasSecondAvailableBalance,
|
||||
secondAdditionalBalance: balance.secondAdditionalBalance,
|
||||
secondAdditionalFiatBalance: balance.fiatSecondAdditionalBalance,
|
||||
secondAvailableBalance: balance.secondAvailableBalance,
|
||||
secondAvailableFiatBalance: balance.fiatSecondAvailableBalance,
|
||||
secondAdditionalBalanceLabel:
|
||||
'${dashboardViewModel.balanceViewModel.secondAdditionalBalanceLabel}',
|
||||
secondAvailableBalanceLabel:
|
||||
'${dashboardViewModel.balanceViewModel.secondAvailableBalanceLabel}',
|
||||
isTestnet: dashboardViewModel.isTestnet,
|
||||
);
|
||||
});
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
Observer(builder: (context) {
|
||||
return Column(
|
||||
children: [
|
||||
if (dashboardViewModel.isMoneroWalletBrokenReasons.isNotEmpty) ...[
|
||||
SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: DashBoardRoundedCardWidget(
|
||||
customBorder: 30,
|
||||
title: "This wallet has encountered an issue",
|
||||
subTitle: "Here are the things that you should note:\n - " +
|
||||
dashboardViewModel.isMoneroWalletBrokenReasons.join("\n - ") +
|
||||
"\n\nPlease restart your wallet and if it doesn't help contact our support.",
|
||||
))
|
||||
],
|
||||
if (dashboardViewModel.showSilentPaymentsCard) ...[
|
||||
SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 0),
|
||||
child: DashBoardRoundedCardWidget(
|
||||
shadowBlur: dashboardViewModel.getShadowBlur(),
|
||||
shadowSpread: dashboardViewModel.getShadowSpread(),
|
||||
marginV: 0,
|
||||
marginH: 0,
|
||||
customBorder: 30,
|
||||
title: S.of(context).silent_payments,
|
||||
subTitle: S.of(context).enable_silent_payments_scanning,
|
||||
hint: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => launchUrl(
|
||||
Uri.parse("https://github.com/Such-Software/hash-wallet"),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).what_is_silent_payments,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Icon(
|
||||
Icons.help_outline,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => StandardSwitch(
|
||||
value: dashboardViewModel.silentPaymentsScanningActive,
|
||||
onTapped: () => _toggleSilentPaymentsScanning(context),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => _toggleSilentPaymentsScanning(context),
|
||||
image: !context.currentTheme.isDark
|
||||
? Image.asset(btcLockLight, height: 48)
|
||||
: Image.asset(btcLockDark, height: 48),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (dashboardViewModel.showMwebCard) ...[
|
||||
SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: InfoCard(
|
||||
title: S.of(context).litecoin_mweb,
|
||||
description: S.of(context).litecoin_mweb_description,
|
||||
leftButtonTitle: S.of(context).litecoin_mweb_dismiss,
|
||||
rightButtonTitle: S.of(context).enable,
|
||||
image: 'assets/images/mweb_logo.png',
|
||||
leftButtonAction: () => _dismissMweb(context),
|
||||
rightButtonAction: () => _enableMweb(context),
|
||||
hintWidget: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => launchUrl(
|
||||
Uri.parse("https://github.com/Such-Software/hash-wallet"),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
child: Text(
|
||||
S.of(context).learn_more,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
if (dashboardViewModel.showPayjoinCard) ...[
|
||||
SizedBox(height: 10),
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 8),
|
||||
child: InfoCard(
|
||||
title: "Payjoin",
|
||||
description: S.of(context).payjoin_card_content,
|
||||
hintWidget: GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () => launchUrl(
|
||||
Uri.parse("https://github.com/Such-Software/hash-wallet"),
|
||||
mode: LaunchMode.externalApplication,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).what_is_payjoin,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
softWrap: true,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 4),
|
||||
child: Icon(
|
||||
Icons.help_outline,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
image: 'assets/images/payjoin.png',
|
||||
leftButtonTitle: S.of(context).litecoin_mweb_dismiss,
|
||||
rightButtonTitle: S.of(context).enable,
|
||||
leftButtonAction: () => dashboardViewModel.dismissPayjoin(),
|
||||
rightButtonAction: () => _enablePayjoin(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
);
|
||||
}),
|
||||
SizedBox(height: 130),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _toggleSilentPaymentsScanning(BuildContext context) async {
|
||||
final isSilentPaymentsScanningActive = dashboardViewModel.silentPaymentsScanningActive;
|
||||
final newValue = !isSilentPaymentsScanningActive;
|
||||
|
||||
dashboardViewModel.silentPaymentsScanningActive = newValue;
|
||||
|
||||
final needsToSwitch = !isSilentPaymentsScanningActive &&
|
||||
await bitcoin!.getNodeIsElectrsSPEnabled(dashboardViewModel.wallet) == false;
|
||||
|
||||
if (needsToSwitch) {
|
||||
return showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertWithTwoActions(
|
||||
alertTitle: S.of(context).change_current_node_title,
|
||||
alertContent: S.of(context).confirm_silent_payments_switch_node,
|
||||
rightButtonText: S.of(context).confirm,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
actionRightButton: () {
|
||||
dashboardViewModel.setSilentPaymentsScanning(newValue);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
actionLeftButton: () {
|
||||
dashboardViewModel.silentPaymentsScanningActive = isSilentPaymentsScanningActive;
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
return dashboardViewModel.setSilentPaymentsScanning(newValue);
|
||||
}
|
||||
|
||||
void _enablePayjoin(BuildContext context) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).payjoin_enabling_popup_title,
|
||||
alertContent: S.of(context).payjoin_enabling_popup_content,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
));
|
||||
|
||||
dashboardViewModel.enablePayjoin();
|
||||
}
|
||||
|
||||
Future<void> _enableMweb(BuildContext context) async {
|
||||
if (!dashboardViewModel.hasEnabledMwebBefore) {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).alert_notice,
|
||||
alertContent: S.of(context).litecoin_mweb_warning,
|
||||
buttonText: S.of(context).understand,
|
||||
buttonAction: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
));
|
||||
}
|
||||
dashboardViewModel.setMwebEnabled();
|
||||
}
|
||||
|
||||
Future<void> _dismissMweb(BuildContext context) async {
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).alert_notice,
|
||||
alertContent: S.of(context).litecoin_mweb_enable_later,
|
||||
buttonText: S.of(context).understand,
|
||||
buttonAction: () {
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
));
|
||||
dashboardViewModel.dismissMweb();
|
||||
}
|
||||
}
|
||||
@@ -28,7 +28,7 @@ class CakeFeaturesPage extends StatelessWidget {
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(minHeight: constraints.maxHeight),
|
||||
child: IntrinsicHeight(
|
||||
child: !FeatureFlag.hasNewUi ? _buildOldUi(context) : _buildNewUi(context),
|
||||
child: _buildNewUi(context),
|
||||
),
|
||||
),
|
||||
);
|
||||
@@ -42,19 +42,6 @@ class CakeFeaturesPage extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildHeader(BuildContext context) {
|
||||
if (!FeatureFlag.hasNewUi) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(left: 24, top: 16, bottom: 16),
|
||||
child: Text(
|
||||
S.of(context).apps,
|
||||
style: Theme.of(context).textTheme.headlineLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
@@ -76,8 +63,6 @@ class CakeFeaturesPage extends StatelessWidget {
|
||||
// consistency (also has affiliate revenue, not our relationship).
|
||||
// Re-populate this page when we have Hash Bags / Such Software apps
|
||||
// to feature here.
|
||||
Widget _buildOldUi(BuildContext context) => _emptyState(context);
|
||||
|
||||
Widget _buildNewUi(BuildContext context) => _emptyState(context);
|
||||
|
||||
Widget _emptyState(BuildContext context) {
|
||||
|
||||
@@ -1,83 +0,0 @@
|
||||
import 'dart:ui';
|
||||
import 'package:hash_wallet/entities/main_actions.dart';
|
||||
import 'package:hash_wallet/themes/core/theme_extension.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/action_button.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class NavigationDock extends StatelessWidget {
|
||||
const NavigationDock({
|
||||
required this.dashboardViewModel,
|
||||
});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Positioned(
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
return Container(
|
||||
height: 84,
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
),
|
||||
margin: const EdgeInsets.only(left: 8, right: 8, bottom: 16),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(50),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 50, sigmaY: 50),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(50.0),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: ClipRect(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: MainActions.all
|
||||
.where((element) => element.canShow?.call(dashboardViewModel) ?? true)
|
||||
.map(
|
||||
(action) => Expanded(
|
||||
child: Semantics(
|
||||
button: true,
|
||||
enabled: (action.isEnabled?.call(dashboardViewModel) ?? true),
|
||||
child: ActionButton(
|
||||
key: action.key,
|
||||
image: Image.asset(
|
||||
action.image,
|
||||
height: 24,
|
||||
width: 24,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
title: action.name(context),
|
||||
onClick: (action.isEnabled?.call(dashboardViewModel) ?? true)
|
||||
? () async =>
|
||||
await action.onTap(context, dashboardViewModel)
|
||||
: null,
|
||||
textColor: action.isEnabled?.call(dashboardViewModel) ?? true
|
||||
? null
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,10 +2,13 @@ import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/entities/solana_nft_asset_model.dart';
|
||||
import 'package:hash_wallet/entities/wallet_nft_response.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import "package:hash_wallet/routes.dart";
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import "package:hash_wallet/src/screens/dashboard/pages/nft_send_page.dart";
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/menu_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/gradient_background.dart';
|
||||
import "package:hash_wallet/src/widgets/primary_button.dart";
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
|
||||
class NFTDetailsPage extends BasePage {
|
||||
@@ -56,30 +59,56 @@ class NFTDetailsPage extends BasePage {
|
||||
// );
|
||||
}
|
||||
|
||||
bool get _canSend =>
|
||||
arguments.isSolanaNFT &&
|
||||
(arguments.solanaNFTAssetModel?.mint?.isNotEmpty ?? false) &&
|
||||
arguments.solanaNFTAssetModel?.isOwned != false;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 1,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: arguments.isSolanaNFT
|
||||
? SolanaNFTDetailsWidget(
|
||||
solanaNftAsset: arguments.solanaNFTAssetModel,
|
||||
)
|
||||
: EVMChainNFTDetailsWidget(
|
||||
nftAsset: arguments.nftAsset,
|
||||
Widget body(BuildContext context) => SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 1,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
child: arguments.isSolanaNFT
|
||||
? SolanaNFTDetailsWidget(
|
||||
solanaNftAsset: arguments.solanaNFTAssetModel,
|
||||
)
|
||||
: EVMChainNFTDetailsWidget(
|
||||
nftAsset: arguments.nftAsset,
|
||||
),
|
||||
),
|
||||
if (_canSend)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 24),
|
||||
child: PrimaryButton(
|
||||
text: S.of(context).send,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
onPressed: () async {
|
||||
final sent = await Navigator.of(context).pushNamed(
|
||||
Routes.nftSendPage,
|
||||
arguments: NFTSendPageArguments(asset: arguments.solanaNFTAssetModel!),
|
||||
);
|
||||
|
||||
if (sent == true && context.mounted) {
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class _NFTImageWidget extends StatelessWidget {
|
||||
@@ -162,6 +191,15 @@ class SolanaNFTDetailsWidget extends StatelessWidget {
|
||||
return Center(child: Text(S.current.no_extra_detail));
|
||||
}
|
||||
|
||||
final moreInfoEntries = <MapEntry<String, String>>[
|
||||
MapEntry(S.current.mint_address, solanaNftAsset?.mint ?? ""),
|
||||
MapEntry(S.current.contractName, solanaNftAsset?.contract?.name ?? ""),
|
||||
MapEntry(S.current.contractSymbol, solanaNftAsset?.contract?.symbol ?? ""),
|
||||
MapEntry(S.current.collection_name, solanaNftAsset?.collection?.name ?? ""),
|
||||
MapEntry(S.current.collection_description, solanaNftAsset?.collection?.description ?? ""),
|
||||
MapEntry(S.current.collection_address, solanaNftAsset?.collection?.collectionAddress ?? ""),
|
||||
].where((entry) => entry.value.isNotEmpty).toList();
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@@ -171,49 +209,75 @@ class SolanaNFTDetailsWidget extends StatelessWidget {
|
||||
infoType: S.current.name,
|
||||
infoValue: solanaNftAsset?.name ?? '---',
|
||||
),
|
||||
if (solanaNftAsset?.description != null) ...[
|
||||
if (solanaNftAsset?.isOwned == false) ...[
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.watching,
|
||||
infoValue: S.current.nft_not_held_by_wallet,
|
||||
),
|
||||
],
|
||||
if (solanaNftAsset?.description?.isNotEmpty ?? false) ...[
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.description,
|
||||
infoValue: solanaNftAsset!.description ?? '---',
|
||||
infoValue: solanaNftAsset!.description!,
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.mint_address,
|
||||
infoValue: solanaNftAsset?.mint ?? '---',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.contractName,
|
||||
infoValue: solanaNftAsset?.contract?.name ?? '---',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.contractSymbol,
|
||||
infoValue: solanaNftAsset?.contract?.symbol ?? '---',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.collection_name,
|
||||
infoValue: solanaNftAsset?.collection?.name ?? '---',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.collection_description,
|
||||
infoValue: solanaNftAsset?.collection?.description ?? '---',
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(
|
||||
infoType: S.current.collection_address,
|
||||
infoValue: solanaNftAsset?.collection?.collectionAddress ?? '---',
|
||||
),
|
||||
if (moreInfoEntries.isNotEmpty) _NFTMoreInfoSection(entries: moreInfoEntries),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _NFTMoreInfoSection extends StatefulWidget {
|
||||
const _NFTMoreInfoSection({required this.entries});
|
||||
|
||||
final List<MapEntry<String, String>> entries;
|
||||
|
||||
@override
|
||||
State<_NFTMoreInfoSection> createState() => _NFTMoreInfoSectionState();
|
||||
}
|
||||
|
||||
class _NFTMoreInfoSectionState extends State<_NFTMoreInfoSection> {
|
||||
bool isExpanded = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const SizedBox(height: 8),
|
||||
InkWell(
|
||||
onTap: () => setState(() => isExpanded = !isExpanded),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
isExpanded ? S.current.hide_details : S.current.show_details,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
isExpanded ? Icons.keyboard_arrow_up : Icons.keyboard_arrow_down,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isExpanded)
|
||||
for (final entry in widget.entries) ...[
|
||||
const SizedBox(height: 16),
|
||||
_NFTSingleInfoTile(infoType: entry.key, infoValue: entry.value),
|
||||
],
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
class _NFTSingleInfoTile extends StatelessWidget {
|
||||
final String infoType;
|
||||
final String infoValue;
|
||||
|
||||
@@ -88,7 +88,7 @@ class _ImportNFTPage extends BasePage {
|
||||
},
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
iconColor: Theme.of(context).colorScheme.primary,
|
||||
placeholder: '0x...',
|
||||
placeholder: nftViewModel.isSolanaWallet ? S.current.mint_address : "0x...",
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
|
||||
@@ -5,6 +5,7 @@ import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/nft_tile_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/nft_view_model.dart';
|
||||
import "package:mobx/mobx.dart";
|
||||
|
||||
class NFTListingPage extends StatefulWidget {
|
||||
final NFTViewModel nftViewModel;
|
||||
@@ -16,23 +17,34 @@ class NFTListingPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _NFTListingPageState extends State<NFTListingPage> {
|
||||
late final ReactionDisposer _walletDisposer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
_walletDisposer = reaction(
|
||||
(_) => widget.nftViewModel.appStore.wallet,
|
||||
(_) => widget.nftViewModel.onWalletChanged(),
|
||||
);
|
||||
|
||||
fetchNFTsForWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_walletDisposer();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
Future<void> fetchNFTsForWallet() async {
|
||||
await widget.nftViewModel.getNFTAssetByWallet();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return SliverToBoxAdapter(
|
||||
child: Observer(
|
||||
builder: (context) {
|
||||
return Column(
|
||||
Widget build(BuildContext context) => SliverToBoxAdapter(
|
||||
child: Observer(
|
||||
builder: (context) => Column(
|
||||
children: [
|
||||
const SizedBox(height: 16),
|
||||
Padding(
|
||||
@@ -48,6 +60,17 @@ class _NFTListingPageState extends State<NFTListingPage> {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.nftViewModel.unresolvedNFTCount > 0)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(left: 16, right: 16, bottom: 12),
|
||||
child: Text(
|
||||
S.current.nft_some_not_loaded,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (widget.nftViewModel.isLoading)
|
||||
Center(
|
||||
child: CircularProgressIndicator(
|
||||
@@ -60,11 +83,9 @@ class _NFTListingPageState extends State<NFTListingPage> {
|
||||
else
|
||||
NFTListWidget(nftViewModel: widget.nftViewModel)
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class NFTListWidget extends StatelessWidget {
|
||||
|
||||
@@ -0,0 +1,380 @@
|
||||
import "package:hash_wallet/core/address_validator.dart";
|
||||
import "package:hash_wallet/core/auth_service.dart";
|
||||
import "package:hash_wallet/core/execution_state.dart";
|
||||
import "package:hash_wallet/entities/solana_nft_asset_model.dart";
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import "package:hash_wallet/src/screens/base_page.dart";
|
||||
import "package:hash_wallet/src/widgets/address_text_field.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_one_action.dart";
|
||||
import "package:hash_wallet/src/widgets/alert_with_two_actions.dart";
|
||||
import "package:hash_wallet/src/widgets/hash_image_widget.dart";
|
||||
import "package:hash_wallet/src/widgets/gradient_background.dart";
|
||||
import "package:hash_wallet/src/widgets/primary_button.dart";
|
||||
import "package:hash_wallet/utils/show_pop_up.dart";
|
||||
import "package:hash_wallet/view_model/dashboard/nft_send_view_model.dart";
|
||||
import "package:cw_core/crypto_currency.dart";
|
||||
import "package:cw_core/pending_transaction.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_mobx/flutter_mobx.dart";
|
||||
|
||||
class NFTSendPage extends StatefulWidget {
|
||||
const NFTSendPage({
|
||||
required this.nftSendViewModel,
|
||||
required this.authService,
|
||||
required this.arguments,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final NFTSendViewModel nftSendViewModel;
|
||||
final AuthService authService;
|
||||
final NFTSendPageArguments arguments;
|
||||
|
||||
@override
|
||||
State<NFTSendPage> createState() => _NFTSendPageState();
|
||||
}
|
||||
|
||||
class _NFTSendPageState extends State<NFTSendPage> {
|
||||
final GlobalKey<FormState> formKey = GlobalKey<FormState>();
|
||||
late TextEditingController addressController;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
addressController = TextEditingController();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
addressController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => _NFTSendPage(
|
||||
nftSendViewModel: widget.nftSendViewModel,
|
||||
authService: widget.authService,
|
||||
asset: widget.arguments.asset,
|
||||
formKey: formKey,
|
||||
addressController: addressController,
|
||||
);
|
||||
}
|
||||
|
||||
class _NFTSendPage extends BasePage {
|
||||
_NFTSendPage({
|
||||
required this.nftSendViewModel,
|
||||
required this.authService,
|
||||
required this.asset,
|
||||
required this.formKey,
|
||||
required this.addressController,
|
||||
});
|
||||
|
||||
final NFTSendViewModel nftSendViewModel;
|
||||
final AuthService authService;
|
||||
final SolanaNFTAssetModel asset;
|
||||
final GlobalKey<FormState> formKey;
|
||||
final TextEditingController addressController;
|
||||
|
||||
@override
|
||||
String get title => S.current.send;
|
||||
|
||||
@override
|
||||
bool get gradientBackground => true;
|
||||
|
||||
@override
|
||||
Widget Function(BuildContext, Widget) get rootWrapper =>
|
||||
(context, scaffold) => GradientBackground(scaffold: scaffold);
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Container(
|
||||
height: MediaQuery.sizeOf(context).height / 3.5,
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
border: Border.all(color: colorScheme.outline),
|
||||
color: colorScheme.surface,
|
||||
),
|
||||
child: HashImageWidget(imageUrl: asset.imageOriginalUrl),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
(asset.name?.isNotEmpty ?? false) ? asset.name! : (asset.symbol ?? "---"),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
color: colorScheme.surfaceContainer,
|
||||
),
|
||||
child: AddressTextField(
|
||||
controller: addressController,
|
||||
placeholder: S.of(context).recipient_address,
|
||||
options: const [
|
||||
AddressTextFieldOption.paste,
|
||||
AddressTextFieldOption.qrCode,
|
||||
],
|
||||
buttonColor: colorScheme.surfaceContainerHighest,
|
||||
validator: AddressValidator(type: CryptoCurrency.sol).call,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Observer(
|
||||
builder: (_) => LoadingPrimaryButton(
|
||||
text: S.of(context).send,
|
||||
color: colorScheme.primary,
|
||||
textColor: colorScheme.onPrimary,
|
||||
isLoading: nftSendViewModel.state is IsExecutingState,
|
||||
onPressed: () => _onSendPressed(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onSendPressed(BuildContext context) async {
|
||||
if (nftSendViewModel.isStartingSend || nftSendViewModel.state is IsExecutingState) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!(formKey.currentState?.validate() ?? false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
nftSendViewModel.markSendStarted();
|
||||
|
||||
final destinationAddress = addressController.text.trim();
|
||||
|
||||
try {
|
||||
await authService.authenticateAction(
|
||||
context,
|
||||
conditionToDetermineIfToUse2FA:
|
||||
nftSendViewModel.shouldRequireTOTP2FAFor(destinationAddress),
|
||||
onAuthSuccess: (isAuthenticatedSuccessfully) async {
|
||||
if (!isAuthenticatedSuccessfully || !context.mounted) {
|
||||
nftSendViewModel.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
await _createAndConfirm(context, destinationAddress);
|
||||
},
|
||||
);
|
||||
} finally {
|
||||
nftSendViewModel.markSendSettled();
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _createAndConfirm(BuildContext context, String destinationAddress) async {
|
||||
await nftSendViewModel.createTransaction(asset, destinationAddress);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final state = nftSendViewModel.state;
|
||||
|
||||
if (state is FailureState) {
|
||||
await _showError(context, state.error);
|
||||
return;
|
||||
}
|
||||
|
||||
final pendingTransaction = nftSendViewModel.pendingTransaction;
|
||||
|
||||
if (pendingTransaction == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final confirmed = await _confirm(context, destinationAddress, pendingTransaction);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!confirmed) {
|
||||
nftSendViewModel.reset();
|
||||
return;
|
||||
}
|
||||
|
||||
await _commit(context);
|
||||
}
|
||||
|
||||
Future<bool> _confirm(
|
||||
BuildContext context,
|
||||
String destinationAddress,
|
||||
PendingTransaction pendingTransaction,
|
||||
) async {
|
||||
final additionalCost = pendingTransaction.additionalCost;
|
||||
|
||||
var confirmed = false;
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertWithTwoActions(
|
||||
alertTitle: S.of(context).confirm_sending,
|
||||
alertContent: "",
|
||||
alertContentTextWidget: _NFTSendConfirmation(
|
||||
name: asset.name,
|
||||
mint: asset.mint,
|
||||
destinationAddress: destinationAddress,
|
||||
fee: pendingTransaction.feeFormatted,
|
||||
accountCreationFee: additionalCost?.toStringWithSymbol(fractionalDigits: 8),
|
||||
),
|
||||
leftButtonText: S.of(context).cancel,
|
||||
rightButtonText: S.of(context).send,
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(),
|
||||
actionRightButton: () {
|
||||
confirmed = true;
|
||||
Navigator.of(dialogContext).pop();
|
||||
},
|
||||
),
|
||||
);
|
||||
|
||||
return confirmed;
|
||||
}
|
||||
|
||||
Future<void> _commit(BuildContext context) async {
|
||||
await nftSendViewModel.commitTransaction();
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
final state = nftSendViewModel.state;
|
||||
if (state is FailureState) {
|
||||
await _showError(context, state.error);
|
||||
return;
|
||||
}
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertWithOneAction(
|
||||
alertTitle: "",
|
||||
alertContent: S.of(context).transaction_sent,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(dialogContext).pop(),
|
||||
),
|
||||
);
|
||||
|
||||
if (!context.mounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
Navigator.of(context).pop(true);
|
||||
}
|
||||
|
||||
Future<void> _showError(BuildContext context, String error) => showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) => AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(dialogContext).pop(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class NFTSendPageArguments {
|
||||
NFTSendPageArguments({required this.asset});
|
||||
|
||||
final SolanaNFTAssetModel asset;
|
||||
}
|
||||
|
||||
class _NFTSendConfirmation extends StatelessWidget {
|
||||
const _NFTSendConfirmation({
|
||||
required this.name,
|
||||
required this.mint,
|
||||
required this.destinationAddress,
|
||||
required this.fee,
|
||||
required this.accountCreationFee,
|
||||
});
|
||||
|
||||
final String? name;
|
||||
final String? mint;
|
||||
final String destinationAddress;
|
||||
final String fee;
|
||||
final String? accountCreationFee;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final creationFee = accountCreationFee;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
spacing: 12,
|
||||
children: [
|
||||
if (name?.isNotEmpty ?? false) _ConfirmationRow(label: S.of(context).name, value: name!),
|
||||
if (mint?.isNotEmpty ?? false)
|
||||
_ConfirmationRow(label: S.of(context).mint_address, value: mint!),
|
||||
_ConfirmationRow(label: S.of(context).address, value: destinationAddress),
|
||||
_ConfirmationRow(label: S.of(context).send_estimated_fee, value: fee),
|
||||
if (creationFee != null)
|
||||
Text(
|
||||
S.of(context).recipient_account_creation_fee(creationFee),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ConfirmationRow extends StatelessWidget {
|
||||
const _ConfirmationRow({required this.label, required this.value});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final textTheme = Theme.of(context).textTheme;
|
||||
final colorScheme = Theme.of(context).colorScheme;
|
||||
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
textAlign: TextAlign.center,
|
||||
style: textTheme.bodySmall?.copyWith(
|
||||
color: colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
textAlign: TextAlign.center,
|
||||
maxLines: 2,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: colorScheme.onSurface,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,246 +0,0 @@
|
||||
import 'package:hash_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:hash_wallet/entities/balance_display_mode.dart';
|
||||
import 'package:hash_wallet/order/order_source_description.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/anonpay_transaction_row.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/order_row.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/payjoin_transaction_row.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/trade_row.dart';
|
||||
import 'package:hash_wallet/src/widgets/dashboard_card_widget.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/anonpay_transaction_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/order_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/payjoin_transaction_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/trade_list_item.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/header_row.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/date_section_raw.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/transaction_raw.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/transaction_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/date_section_item.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class TransactionsPage extends StatelessWidget {
|
||||
TransactionsPage({required this.dashboardViewModel});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onLongPress: () => dashboardViewModel.balanceViewModel.isReversing =
|
||||
!dashboardViewModel.balanceViewModel.isReversing,
|
||||
onLongPressUp: () => dashboardViewModel.balanceViewModel.isReversing =
|
||||
!dashboardViewModel.balanceViewModel.isReversing,
|
||||
child: Container(
|
||||
color: responsiveLayoutUtil.shouldRenderMobileUI
|
||||
? null
|
||||
: Theme.of(context).colorScheme.surface,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Observer(builder: (_) {
|
||||
final status = dashboardViewModel.status;
|
||||
if (status is SyncingSyncStatus) {
|
||||
return DashBoardRoundedCardWidget(
|
||||
key: ValueKey('transactions_page_syncing_alert_card_key'),
|
||||
onTap: () {
|
||||
try {
|
||||
final uri = Uri.parse("https://github.com/Such-Software/hash-wallet");
|
||||
launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} catch (_) {}
|
||||
},
|
||||
title: S.of(context).syncing_wallet_alert_title,
|
||||
subTitle: S.of(context).syncing_wallet_alert_content,
|
||||
);
|
||||
} else {
|
||||
return Container();
|
||||
}
|
||||
}),
|
||||
HeaderRow(
|
||||
dashboardViewModel: dashboardViewModel,
|
||||
key: ValueKey('transactions_page_header_row_key'),
|
||||
),
|
||||
Expanded(
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final items = dashboardViewModel.items;
|
||||
return items.isNotEmpty
|
||||
? ListView.builder(
|
||||
key: ValueKey('transactions_page_list_view_builder_key'),
|
||||
itemCount: items.length + 1,
|
||||
itemBuilder: (context, index) {
|
||||
if (index == items.length) return SizedBox(height: 150);
|
||||
|
||||
final item = items[index];
|
||||
|
||||
if (item is DateSectionItem) {
|
||||
return DateSectionRaw(date: item.date, key: item.key);
|
||||
}
|
||||
|
||||
if (item is TransactionListItem) {
|
||||
if (item.hasTokens && item.assetOfTransaction == null) {
|
||||
return Container();
|
||||
}
|
||||
|
||||
final transaction = item.transaction;
|
||||
final transactionType =
|
||||
dashboardViewModel.getTransactionType(transaction);
|
||||
|
||||
List<String> tags = [];
|
||||
if (dashboardViewModel.type == WalletType.bitcoin) {
|
||||
if (bitcoin!.txIsReceivedSilentPayment(transaction)) {
|
||||
tags.add(S.of(context).silent_payment);
|
||||
}
|
||||
}
|
||||
if (dashboardViewModel.type == WalletType.litecoin) {
|
||||
if (bitcoin!.txIsMweb(transaction)) {
|
||||
tags.add("MWEB");
|
||||
}
|
||||
}
|
||||
return Observer(
|
||||
builder: (_) => TransactionRow(
|
||||
key: item.key,
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.transactionDetails, arguments: transaction),
|
||||
isShield: transaction.additionalInfo['autoShield'] == true,
|
||||
direction: transaction.direction,
|
||||
formattedDate: DateFormat('HH:mm').format(transaction.date),
|
||||
formattedAmount: item.formattedCryptoAmount,
|
||||
formattedFiatAmount:
|
||||
dashboardViewModel.balanceViewModel.isFiatDisabled
|
||||
? ''
|
||||
: item.formattedFiatAmount,
|
||||
title:
|
||||
item.formattedTitle + item.formattedStatus + transactionType,
|
||||
tags: tags,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (item is AnonpayTransactionListItem) {
|
||||
final transactionInfo = item.transaction;
|
||||
|
||||
return AnonpayTransactionRow(
|
||||
key: item.key,
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.anonPayDetailsPage,
|
||||
arguments: transactionInfo),
|
||||
currency: transactionInfo.fiatAmount != null
|
||||
? transactionInfo.fiatEquiv ?? ''
|
||||
: CryptoCurrency.fromFullName(transactionInfo.coinTo)
|
||||
.name
|
||||
.toUpperCase(),
|
||||
provider: transactionInfo.provider,
|
||||
amount: transactionInfo.fiatAmount?.toString() ??
|
||||
(transactionInfo.amountTo?.toString() ?? ''),
|
||||
createdAt: DateFormat('HH:mm').format(transactionInfo.createdAt),
|
||||
);
|
||||
}
|
||||
|
||||
if (item is PayjoinTransactionListItem) {
|
||||
final session = item.session;
|
||||
|
||||
return PayjoinTransactionRow(
|
||||
key: item.key,
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.payjoinDetails,
|
||||
arguments: [item.sessionId, item.transaction],
|
||||
),
|
||||
currency: "BTC",
|
||||
state: item.status,
|
||||
amount: dashboardViewModel.appStore.amountParsingProxy
|
||||
.getDisplayCryptoString(
|
||||
session.amount.toInt(), CryptoCurrency.btc),
|
||||
createdAt: DateFormat('HH:mm').format(session.inProgressSince!),
|
||||
isSending: session.isSenderSession,
|
||||
);
|
||||
}
|
||||
|
||||
if (item is TradeListItem) {
|
||||
final trade = item.trade;
|
||||
|
||||
final tradeFrom = trade.from;
|
||||
|
||||
final tradeTo = trade.to;
|
||||
|
||||
return tradeFrom != null && tradeTo != null
|
||||
? Observer(
|
||||
builder: (_) => TradeRow(
|
||||
key: item.key,
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.tradeDetails, arguments: trade),
|
||||
swapState: trade.state,
|
||||
provider: trade.provider,
|
||||
title: "$tradeFrom → $tradeTo",
|
||||
fromSymbol: dashboardViewModel.appStore.amountParsingProxy
|
||||
.getCryptoSymbol(tradeFrom),
|
||||
toSymbol: dashboardViewModel.appStore.amountParsingProxy
|
||||
.getCryptoSymbol(tradeTo),
|
||||
createdAtFormattedDate: trade.createdAt != null
|
||||
? DateFormat("HH:mm").format(trade.createdAt!)
|
||||
: null,
|
||||
formattedAmount: item.tradeFormattedAmount,
|
||||
formattedReceiveAmount: item.tradeFormattedReceiveAmount,
|
||||
),
|
||||
)
|
||||
: Container();
|
||||
}
|
||||
if (item is OrderListItem) {
|
||||
final order = item.order;
|
||||
return Observer(
|
||||
builder: (_) {
|
||||
// TODO: Refactor Amount Hiding Logic it is not working properly for Orders and Trades
|
||||
final hide = dashboardViewModel.balanceViewModel.displayMode ==
|
||||
BalanceDisplayMode.hiddenBalance;
|
||||
|
||||
final formattedAmount = hide ? '---' : order.amountFormatted();
|
||||
final formattedReceiveAmount = hide ? '---' : order.receiveAmount;
|
||||
|
||||
return OrderRow(
|
||||
key: item.key,
|
||||
onTap: () => Navigator.of(context)
|
||||
.pushNamed(Routes.orderDetails, arguments: order),
|
||||
providerTitle: order.providerTitle,
|
||||
providerIconPath: order.providerIcon,
|
||||
from: order.from ?? '',
|
||||
to: order.to ?? '',
|
||||
createdAtFormattedDate:
|
||||
DateFormat('HH:mm').format(order.createdAt),
|
||||
formattedAmount: formattedAmount,
|
||||
formattedReceiveAmount:
|
||||
dashboardViewModel.balanceViewModel.isFiatDisabled &&
|
||||
order.source == OrderSourceDescription.order
|
||||
? ''
|
||||
: formattedReceiveAmount,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
return Container(color: Colors.transparent, height: 1);
|
||||
},
|
||||
)
|
||||
: Center(
|
||||
child: Text(
|
||||
key: ValueKey('transactions_page_placeholder_transactions_text_key'),
|
||||
S.of(context).placeholder_transactions,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -59,6 +59,7 @@ class _SignPageState extends State<SignPage> {
|
||||
ModalTopBar(
|
||||
title: S.current.sign_verify_title,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.current.seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
|
||||
@@ -1,66 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class ActionButton extends StatelessWidget {
|
||||
ActionButton({
|
||||
required this.image,
|
||||
required this.title,
|
||||
this.route,
|
||||
this.onClick,
|
||||
this.alignment = Alignment.center,
|
||||
this.textColor,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final Image image;
|
||||
final String title;
|
||||
final String? route;
|
||||
final Alignment alignment;
|
||||
final VoidCallback? onClick;
|
||||
final Color? textColor;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return TextButton(
|
||||
style: TextButton.styleFrom(
|
||||
padding: EdgeInsets.zero,
|
||||
minimumSize: Size.zero,
|
||||
tapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
),
|
||||
onPressed: () {
|
||||
if (route?.isNotEmpty ?? false) {
|
||||
Navigator.of(context, rootNavigator: true).pushNamed(route!);
|
||||
} else {
|
||||
onClick?.call();
|
||||
}
|
||||
},
|
||||
child: SizedBox.expand(
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
alignment: alignment,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
image,
|
||||
const SizedBox(height: 4),
|
||||
Flexible(
|
||||
child: Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.labelSmall?.copyWith(
|
||||
color: textColor ?? Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -5,9 +5,10 @@ import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
|
||||
class HeaderRow extends StatelessWidget {
|
||||
HeaderRow({required this.dashboardViewModel, super.key});
|
||||
HeaderRow({required this.dashboardViewModel, this.onExportCsv, super.key});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final VoidCallback? onExportCsv;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -27,31 +28,56 @@ class HeaderRow extends StatelessWidget {
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurface),
|
||||
),
|
||||
Semantics(
|
||||
container: true,
|
||||
child: GestureDetector(
|
||||
key: ValueKey('transactions_page_header_row_transaction_filter_button_key'),
|
||||
onTap: () {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (context) => FilterWidget(filterItems: dashboardViewModel.filterItems),
|
||||
);
|
||||
},
|
||||
child: Semantics(
|
||||
label: 'Transaction Filter',
|
||||
button: true,
|
||||
enabled: true,
|
||||
child: Container(
|
||||
height: 36,
|
||||
width: 36,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
Row(
|
||||
children: [
|
||||
if (onExportCsv != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8),
|
||||
child: GestureDetector(
|
||||
key: ValueKey('transactions_page_header_row_export_csv_button_key'),
|
||||
onTap: onExportCsv,
|
||||
child: Semantics(
|
||||
label: S.of(context).export_csv,
|
||||
button: true,
|
||||
enabled: true,
|
||||
child: Container(
|
||||
height: 36,
|
||||
width: 36,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.file_download_outlined,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Semantics(
|
||||
container: true,
|
||||
child: GestureDetector(
|
||||
key: ValueKey('transactions_page_header_row_transaction_filter_button_key'),
|
||||
onTap: () {},
|
||||
child: Semantics(
|
||||
label: 'Transaction Filter',
|
||||
button: true,
|
||||
enabled: true,
|
||||
child: Container(
|
||||
height: 36,
|
||||
width: 36,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: filterIcon,
|
||||
),
|
||||
),
|
||||
child: filterIcon,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
],
|
||||
),
|
||||
|
||||
@@ -1,68 +0,0 @@
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/src/screens/monero_accounts/monero_account_list_page.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HomeScreenAccountWidget extends StatelessWidget {
|
||||
HomeScreenAccountWidget({this.walletName, this.accountName});
|
||||
|
||||
final String? walletName;
|
||||
final String? accountName;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () async {
|
||||
await showPopUp<void>(context: context, builder: (_) => getIt.get<MoneroAccountListPage>());
|
||||
},
|
||||
behavior: HitTestBehavior.opaque,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(25.0),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
child: Text(
|
||||
walletName ?? '',
|
||||
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(
|
||||
height: 5.0,
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Padding(padding: EdgeInsets.only(left: 20)),
|
||||
Container(
|
||||
child: Text(
|
||||
accountName ?? '',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
child: Icon(
|
||||
Icons.keyboard_arrow_down,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/dashboard_card_widget.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class InfoCard extends StatelessWidget {
|
||||
final String leftButtonTitle;
|
||||
final String rightButtonTitle;
|
||||
final String title;
|
||||
final String description;
|
||||
final String image;
|
||||
final Function() leftButtonAction;
|
||||
final Function() rightButtonAction;
|
||||
|
||||
final Widget? hintWidget;
|
||||
|
||||
const InfoCard({
|
||||
super.key,
|
||||
required this.title,
|
||||
required this.description,
|
||||
required this.leftButtonTitle,
|
||||
required this.rightButtonTitle,
|
||||
required this.leftButtonAction,
|
||||
required this.rightButtonAction,
|
||||
required this.image,
|
||||
this.hintWidget,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DashBoardRoundedCardWidget(
|
||||
marginH: 0,
|
||||
marginV: 0,
|
||||
customBorder: 30,
|
||||
title: title,
|
||||
subTitle: description,
|
||||
hint: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
if (hintWidget != null) hintWidget!,
|
||||
if (hintWidget != null) SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: leftButtonAction,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
leftButtonTitle,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: rightButtonAction,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
rightButtonTitle,
|
||||
maxLines: 1,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
onTap: () => {},
|
||||
icon: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: HashImageWidget(
|
||||
imageUrl: image,
|
||||
height: 40,
|
||||
width: 40,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -24,19 +24,19 @@ class MenuWidgetState extends State<MenuWidget> {
|
||||
this.tileHeight = 60,
|
||||
this.fromTopEdge = 50,
|
||||
this.fromBottomEdge = 25,
|
||||
this.moneroIcon = Image.asset('assets/images/crypto/monero.webp'),
|
||||
this.bitcoinIcon = Image.asset('assets/images/crypto/bitcoin.webp'),
|
||||
this.litecoinIcon = Image.asset('assets/images/crypto/litecoin.webp'),
|
||||
this.ethereumIcon = Image.asset('assets/images/crypto/ethereum.webp'),
|
||||
this.nanoIcon = Image.asset('assets/images/crypto/nano.webp'),
|
||||
this.bananoIcon = Image.asset('assets/images/crypto/nano.webp'),
|
||||
this.bitcoinCashIcon = Image.asset('assets/images/crypto/bitcoin-cash.webp'),
|
||||
this.polygonIcon = Image.asset('assets/images/crypto/polygon.webp'),
|
||||
this.baseIcon = Image.asset('assets/images/crypto/base_icon.webp'),
|
||||
this.arbitrumIcon = Image.asset('assets/images/crypto/arbitrum.webp'),
|
||||
this.bscIcon = Image.asset('assets/images/crypto/BNB.webp'),
|
||||
this.wowneroIcon = Image.asset('assets/images/crypto/wownero.webp'),
|
||||
this.dogecoinIcon = Image.asset('assets/images/crypto/dogecoin.webp');
|
||||
this.moneroIcon = Image.asset('assets/new-ui/crypto_full_icons/monero.svg'),
|
||||
this.bitcoinIcon = Image.asset('assets/new-ui/crypto_full_icons/bitcoin.svg'),
|
||||
this.litecoinIcon = Image.asset('assets/new-ui/crypto_full_icons/litecoin.svg'),
|
||||
this.ethereumIcon = Image.asset('assets/new-ui/crypto_full_icons/ethereum.svg'),
|
||||
this.nanoIcon = Image.asset('assets/new-ui/crypto_full_icons/nano.svg'),
|
||||
this.bananoIcon = Image.asset('assets/new-ui/crypto_full_icons/nano.svg'),
|
||||
this.bitcoinCashIcon = Image.asset('assets/new-ui/crypto_full_icons/bitcoin-cash.svg'),
|
||||
this.polygonIcon = Image.asset('assets/new-ui/crypto_full_icons/polygon.svg'),
|
||||
this.baseIcon = Image.asset('assets/new-ui/crypto_full_icons/base.svg'),
|
||||
this.arbitrumIcon = Image.asset('assets/new-ui/crypto_full_icons/arbitrum.svg'),
|
||||
this.bscIcon = Image.asset('assets/new-ui/crypto_full_icons/bnb.svg'),
|
||||
this.wowneroIcon = Image.asset('assets/new-ui/crypto_full_icons/wownero.svg'),
|
||||
this.dogecoinIcon = Image.asset('assets/new-ui/crypto_full_icons/dogecoin.svg');
|
||||
|
||||
final largeScreen = 731;
|
||||
|
||||
@@ -110,8 +110,8 @@ class MenuWidgetState extends State<MenuWidget> {
|
||||
}
|
||||
int itemCount = items.length;
|
||||
|
||||
moneroIcon = Image.asset('assets/images/crypto/monero.webp');
|
||||
bitcoinIcon = Image.asset('assets/images/crypto/bitcoin.webp');
|
||||
moneroIcon = Image.asset('assets/new-ui/crypto_full_icons/monero.svg');
|
||||
bitcoinIcon = Image.asset('assets/new-ui/crypto_full_icons/bitcoin.svg');
|
||||
|
||||
return Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
|
||||
@@ -51,7 +51,7 @@ class _NEWNewMainNavBarState extends State<NewMainNavBar> {
|
||||
static const iconColorChangeDuration = Duration(milliseconds: 200);
|
||||
|
||||
static const pillTextStyle = TextStyle(
|
||||
fontSize: 14,
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w500,
|
||||
);
|
||||
|
||||
@@ -191,50 +191,57 @@ class _NEWNewMainNavBarState extends State<NewMainNavBar> {
|
||||
? iconHorizontalPadding / 100
|
||||
: 0),
|
||||
curve: Curves.easeOutCubic,
|
||||
child: InkWell(
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
splashColor: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(pillBorderRadius),
|
||||
onTap: () => _onItemTap(i),
|
||||
child: AnimatedContainer(
|
||||
duration:
|
||||
_firstFrame ? Duration.zero : inactiveIconMoveDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
width: i == widget.selectedIndex ? pillWidth : iconBoxWidth,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedAlign(
|
||||
duration: inactiveIconFadeDuration,
|
||||
child: Semantics(
|
||||
button: true,
|
||||
selected: i == widget.selectedIndex,
|
||||
inMutuallyExclusiveGroup: true,
|
||||
label: visibleActions[i].name(context),
|
||||
child: InkWell(
|
||||
key: visibleActions[i].key,
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
splashColor: Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(pillBorderRadius),
|
||||
onTap: () => _onItemTap(i),
|
||||
child: AnimatedContainer(
|
||||
duration:
|
||||
_firstFrame ? Duration.zero : inactiveIconMoveDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
width: i == widget.selectedIndex ? pillWidth : iconBoxWidth,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedScale(
|
||||
duration: inactiveIconAppearDuration,
|
||||
child: AnimatedAlign(
|
||||
duration: inactiveIconFadeDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
scale: (i == widget.selectedIndex) ? 0.857 : 1.0,
|
||||
child: TweenAnimationBuilder<Color?>(
|
||||
tween: ColorTween(
|
||||
begin: (i == widget.selectedIndex)
|
||||
? inactiveColor
|
||||
: activeColor,
|
||||
end: (i == widget.selectedIndex)
|
||||
? activeColor
|
||||
: inactiveColor,
|
||||
),
|
||||
duration: iconColorChangeDuration,
|
||||
builder: (context, value, child) {
|
||||
return Container(
|
||||
height: NewMainNavBar.barHeight,
|
||||
child: HashImageWidget(
|
||||
imageUrl: visibleActions[i].image,
|
||||
width: iconWidth,
|
||||
height: iconHeight,
|
||||
//fit: BoxFit.scaleDown,
|
||||
colorFilter: ColorFilter.mode(
|
||||
value ?? inactiveColor,
|
||||
BlendMode.srcIn,
|
||||
alignment: Alignment.center,
|
||||
child: AnimatedScale(
|
||||
duration: inactiveIconAppearDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
scale: (i == widget.selectedIndex) ? 0.857 : 1.0,
|
||||
child: TweenAnimationBuilder<Color?>(
|
||||
tween: ColorTween(
|
||||
begin: (i == widget.selectedIndex)
|
||||
? inactiveColor
|
||||
: activeColor,
|
||||
end: (i == widget.selectedIndex)
|
||||
? activeColor
|
||||
: inactiveColor,
|
||||
),
|
||||
duration: iconColorChangeDuration,
|
||||
builder: (context, value, child) {
|
||||
return Container(
|
||||
height: NewMainNavBar.barHeight,
|
||||
child: HashImageWidget(
|
||||
imageUrl: visibleActions[i].image,
|
||||
width: iconWidth,
|
||||
height: iconHeight,
|
||||
//fit: BoxFit.scaleDown,
|
||||
colorFilter: ColorFilter.mode(
|
||||
value ?? inactiveColor,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -310,11 +317,15 @@ class AnimatedPill extends StatelessWidget {
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: pillIconWidth + 2),
|
||||
child: Text(
|
||||
currentAction.name(context),
|
||||
style: pillTextStyle.copyWith(color: contentColor),
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
// The selected tab's InkWell already announces this name, so the
|
||||
// pill text must not become a second stop for screen readers.
|
||||
child: ExcludeSemantics(
|
||||
child: Text(
|
||||
currentAction.name(context),
|
||||
style: pillTextStyle.copyWith(color: contentColor),
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,302 +0,0 @@
|
||||
import 'dart:math' as math;
|
||||
import 'dart:ui';
|
||||
import 'package:hash_wallet/entities/page_indicator_actions.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
|
||||
class PageIndicator extends StatefulWidget {
|
||||
const PageIndicator({
|
||||
super.key,
|
||||
required this.controller,
|
||||
required this.dashboardViewModel,
|
||||
});
|
||||
|
||||
final PageController controller;
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
|
||||
@override
|
||||
State<PageIndicator> createState() => _PageIndicatorState();
|
||||
}
|
||||
|
||||
class _PageIndicatorState extends State<PageIndicator> {
|
||||
static const barHeight = 37.0;
|
||||
static const iconWidth = 16.0;
|
||||
static const iconHeight = 16.0;
|
||||
static const iconSpacing = 5.0;
|
||||
static const barHorizontalPadding = 4.0;
|
||||
static const pillHorizontalPadding = 8.0;
|
||||
static const edgePadding = 5.0;
|
||||
static const barBorderRadius = 50.0;
|
||||
static const pillBorderRadius = 50.0;
|
||||
static const pageSwitchDuration = Duration(milliseconds: 300);
|
||||
static const pillMoveDuration = Duration(milliseconds: 300);
|
||||
static const pillResizeDuration = Duration(milliseconds: 200);
|
||||
static const textStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w400,
|
||||
);
|
||||
|
||||
late int selectedIndex;
|
||||
late final VoidCallback _pageListener;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
selectedIndex = widget.controller.initialPage;
|
||||
|
||||
_pageListener = () {
|
||||
final page = widget.controller.page;
|
||||
if (page == null) return;
|
||||
|
||||
final rounded = page.round();
|
||||
if (rounded != selectedIndex && mounted) {
|
||||
setState(() {
|
||||
selectedIndex = rounded;
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
widget.controller.addListener(_pageListener);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.controller.removeListener(_pageListener);
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _onItemTap(int index) {
|
||||
if (index == selectedIndex) return;
|
||||
widget.controller.animateToPage(
|
||||
index,
|
||||
duration: pageSwitchDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
);
|
||||
}
|
||||
|
||||
double _estimateItemWidthForAction(BuildContext context, PageIndicatorActions action,
|
||||
{Color? color}) {
|
||||
final text = action.name(context);
|
||||
final textPainter = TextPainter(
|
||||
text: TextSpan(text: text, style: textStyle.copyWith(color: color)),
|
||||
maxLines: 1,
|
||||
textDirection: Directionality.of(context),
|
||||
textScaler: MediaQuery.of(context).textScaler,
|
||||
)..layout();
|
||||
return iconWidth + iconSpacing + textPainter.width + pillHorizontalPadding * 2;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final visibleActions = PageIndicatorActions.all.where((action) {
|
||||
return action.isEnabled?.call(widget.dashboardViewModel) ?? true;
|
||||
}).toList();
|
||||
|
||||
// Don't show indicator if less than 2 actions
|
||||
if (visibleActions.length < 2) return const SizedBox.shrink();
|
||||
|
||||
selectedIndex = selectedIndex.clamp(0, visibleActions.length - 1);
|
||||
|
||||
// Theme setup
|
||||
final theme = Theme.of(context);
|
||||
final backgroundColor = theme.colorScheme.surfaceContainer.withAlpha(122);
|
||||
final pillColor = theme.colorScheme.onSurface.withAlpha(30);
|
||||
final activeColor = theme.colorScheme.onSurface;
|
||||
final inactiveColor = theme.colorScheme.primary;
|
||||
|
||||
// Size calculations
|
||||
final screenWidth = MediaQuery.of(context).size.width;
|
||||
final isRTL = Directionality.of(context) == TextDirection.rtl;
|
||||
|
||||
final pillWidth = _estimateItemWidthForAction(
|
||||
context,
|
||||
visibleActions[selectedIndex],
|
||||
color: activeColor,
|
||||
);
|
||||
|
||||
final actionWidths = [
|
||||
for (final action in visibleActions)
|
||||
_estimateItemWidthForAction(context, action, color: inactiveColor)
|
||||
];
|
||||
|
||||
final totalItemsWidth =
|
||||
actionWidths.fold(0.0, (sum, width) => sum + width) + barHorizontalPadding * 2;
|
||||
|
||||
final barWidth = math.min(totalItemsWidth, screenWidth * 0.95);
|
||||
|
||||
final count = visibleActions.length;
|
||||
final firstLeft = edgePadding;
|
||||
final secondLeft = barWidth - (isRTL ? actionWidths.first : actionWidths[1]) - edgePadding;
|
||||
|
||||
final List<double> positions;
|
||||
|
||||
if (count == 2) {
|
||||
positions = [firstLeft, secondLeft];
|
||||
} else {
|
||||
final lastLeft = barWidth - (isRTL ? actionWidths.first : actionWidths.last) - edgePadding;
|
||||
|
||||
final centerLeft =
|
||||
(isRTL ? actionWidths.last + edgePadding : actionWidths.first + edgePadding);
|
||||
|
||||
positions = [firstLeft, centerLeft, lastLeft];
|
||||
}
|
||||
|
||||
final left = (isRTL ? positions.reversed.toList() : positions)[selectedIndex];
|
||||
final currentAction = visibleActions[selectedIndex];
|
||||
|
||||
return Align(
|
||||
alignment: Alignment.bottomCenter,
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 10),
|
||||
child: Container(
|
||||
width: barWidth,
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(barBorderRadius),
|
||||
child: BackdropFilter(
|
||||
filter: ImageFilter.blur(sigmaX: 30, sigmaY: 30),
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
decoration: BoxDecoration(
|
||||
color: backgroundColor,
|
||||
borderRadius: BorderRadius.circular(barBorderRadius),
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
AnimatedPill(
|
||||
left: left,
|
||||
pillColor: pillColor,
|
||||
currentAction: currentAction,
|
||||
pillIconHeight: iconHeight,
|
||||
pillIconWidth: iconWidth,
|
||||
pillIconSpacing: iconSpacing,
|
||||
pillBorderRadius: pillBorderRadius,
|
||||
contentColor: activeColor,
|
||||
estimateWidthForAction: pillWidth,
|
||||
pillTextStyle: textStyle,
|
||||
pillMoveDuration: pillMoveDuration,
|
||||
pillResizeDuration: pillResizeDuration,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: barHorizontalPadding),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
for (int i = 0; i < visibleActions.length; i++)
|
||||
Semantics(
|
||||
button: true,
|
||||
label: visibleActions[i].name(context),
|
||||
value: i == selectedIndex ? 'Selected' : null,
|
||||
hint: 'Double tap to open ${visibleActions[i].name(context)} page',
|
||||
onTap: () => _onItemTap(i),
|
||||
child: GestureDetector(
|
||||
behavior: HitTestBehavior.translucent,
|
||||
onTap: () => _onItemTap(i),
|
||||
child: Container(
|
||||
height: barHeight,
|
||||
width: i == selectedIndex
|
||||
? pillWidth
|
||||
: _estimateItemWidthForAction(context, visibleActions[i]),
|
||||
alignment: Alignment.center,
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
TweenAnimationBuilder<Color?>(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
tween: ColorTween(
|
||||
begin: inactiveColor,
|
||||
end: i == selectedIndex ? activeColor : inactiveColor,
|
||||
),
|
||||
builder: (context, color, _) => SvgPicture.asset(
|
||||
visibleActions[i].image,
|
||||
width: iconWidth,
|
||||
height: iconHeight,
|
||||
colorFilter: ColorFilter.mode(color!, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
SizedBox(width: iconSpacing),
|
||||
TweenAnimationBuilder<Color?>(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOutCubic,
|
||||
tween: ColorTween(
|
||||
begin: inactiveColor,
|
||||
end: i == selectedIndex ? activeColor : inactiveColor,
|
||||
),
|
||||
builder: (context, color, _) => Text(
|
||||
visibleActions[i].name(context),
|
||||
style: textStyle.copyWith(color: color),
|
||||
overflow: TextOverflow.fade,
|
||||
softWrap: false,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
)),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class AnimatedPill extends StatelessWidget {
|
||||
const AnimatedPill({
|
||||
super.key,
|
||||
required this.left,
|
||||
required this.pillColor,
|
||||
required this.contentColor,
|
||||
required this.pillIconHeight,
|
||||
required this.pillIconWidth,
|
||||
required this.pillBorderRadius,
|
||||
required this.currentAction,
|
||||
required this.estimateWidthForAction,
|
||||
required this.pillIconSpacing,
|
||||
required this.pillTextStyle,
|
||||
required this.pillMoveDuration,
|
||||
required this.pillResizeDuration,
|
||||
});
|
||||
|
||||
final double left;
|
||||
final Color pillColor;
|
||||
final Color contentColor;
|
||||
final double pillIconHeight;
|
||||
final double pillIconWidth;
|
||||
final double pillBorderRadius;
|
||||
final PageIndicatorActions currentAction;
|
||||
final double estimateWidthForAction;
|
||||
final double pillIconSpacing;
|
||||
final TextStyle pillTextStyle;
|
||||
final Duration pillMoveDuration;
|
||||
final Duration pillResizeDuration;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AnimatedPositioned(
|
||||
duration: pillMoveDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
left: left,
|
||||
top: 4,
|
||||
bottom: 4,
|
||||
child: AnimatedContainer(
|
||||
duration: pillResizeDuration,
|
||||
curve: Curves.easeOutCubic,
|
||||
width: estimateWidthForAction,
|
||||
decoration:
|
||||
BoxDecoration(color: pillColor, borderRadius: BorderRadius.circular(pillBorderRadius)),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
alignment: Alignment.center,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,163 +0,0 @@
|
||||
import 'package:hash_wallet/src/widgets/alert_close_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/rounded_checkbox.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_background.dart';
|
||||
import 'package:hash_wallet/typography.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/receive_option_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
|
||||
class PresentReceiveOptionPicker extends StatelessWidget {
|
||||
PresentReceiveOptionPicker({required this.receiveOptionViewModel, required this.color});
|
||||
|
||||
final ReceiveOptionViewModel receiveOptionViewModel;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final arrowBottom = Image.asset(
|
||||
'assets/images/arrow_bottom_purple_icon.png',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
height: 6,
|
||||
);
|
||||
|
||||
return Padding(
|
||||
padding: EdgeInsets.only(left: 6),
|
||||
child: TextButton(
|
||||
onPressed: () => _showPicker(context),
|
||||
style: ButtonStyle(
|
||||
padding: WidgetStateProperty.all(EdgeInsets.zero),
|
||||
splashFactory: NoSplash.splashFactory,
|
||||
foregroundColor: WidgetStateProperty.all(Colors.transparent),
|
||||
overlayColor: WidgetStateProperty.all(Colors.transparent),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
S.current.receive,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 17.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
Observer(
|
||||
builder: (_) => Text(
|
||||
receiveOptionViewModel.selectedReceiveOption
|
||||
.toString()
|
||||
.replaceAll(RegExp(r'silent payments', caseSensitive: false),
|
||||
S.current.silent_payments)
|
||||
.replaceAll(RegExp(r'default', caseSensitive: false),
|
||||
S.current.string_default),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontSize: 10.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
child: arrowBottom,
|
||||
)
|
||||
],
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
void _showPicker(BuildContext context) async {
|
||||
await showPopUp<void>(
|
||||
builder: (BuildContext popUpContext) => Scaffold(
|
||||
resizeToAvoidBottomInset: false,
|
||||
backgroundColor: Colors.transparent,
|
||||
body: Stack(
|
||||
alignment: AlignmentDirectional.center,
|
||||
children: [
|
||||
AlertBackground(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Spacer(),
|
||||
Container(
|
||||
margin: EdgeInsets.symmetric(horizontal: 24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 24, bottom: 24),
|
||||
child: (ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
itemCount: receiveOptionViewModel.options.length,
|
||||
itemBuilder: (_, index) {
|
||||
final option = receiveOptionViewModel.options[index];
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pop(popUpContext);
|
||||
|
||||
receiveOptionViewModel.selectReceiveOption(option);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 24, right: 24),
|
||||
child: Observer(builder: (_) {
|
||||
final value = receiveOptionViewModel.selectedReceiveOption;
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
option
|
||||
.toString()
|
||||
.replaceAll(
|
||||
RegExp(r'silent payments', caseSensitive: false),
|
||||
S.current.silent_payments)
|
||||
.replaceAll(RegExp(r'default', caseSensitive: false),
|
||||
S.current.string_default),
|
||||
textAlign: TextAlign.left,
|
||||
style: textSmall(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
).copyWith(
|
||||
fontWeight:
|
||||
value == option ? FontWeight.w800 : FontWeight.w500,
|
||||
)),
|
||||
RoundedCheckbox(
|
||||
value: value == option,
|
||||
)
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (_, index) => SizedBox(height: 30),
|
||||
)),
|
||||
),
|
||||
),
|
||||
Spacer()
|
||||
],
|
||||
),
|
||||
),
|
||||
AlertCloseButton(onTap: () => Navigator.of(popUpContext).pop(), bottom: 40)
|
||||
],
|
||||
),
|
||||
),
|
||||
context: context,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,87 +1,127 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/entities/solana_nft_asset_model.dart';
|
||||
import "package:hash_wallet/generated/i18n.dart";
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/pages/nft_details_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import "package:hash_wallet/view_model/dashboard/nft_view_model.dart";
|
||||
|
||||
class SolanaNFTTileWidget extends StatelessWidget {
|
||||
const SolanaNFTTileWidget({super.key, required this.nftAsset});
|
||||
const SolanaNFTTileWidget({
|
||||
required this.nftAsset,
|
||||
required this.nftViewModel,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final SolanaNFTAssetModel nftAsset;
|
||||
final NFTViewModel nftViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.nftDetailsPage,
|
||||
arguments: NFTDetailsPageArguments(
|
||||
isSolanaNFT: true,
|
||||
solanaNFTAssetModel: nftAsset,
|
||||
),
|
||||
);
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 0.0,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
margin: const EdgeInsets.all(8),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 0.0,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: HashImageWidget(
|
||||
imageUrl: nftAsset.imageOriginalUrl,
|
||||
),
|
||||
Widget build(BuildContext context) => InkWell(
|
||||
onTap: () async {
|
||||
final walletName = nftViewModel.currentWalletName;
|
||||
|
||||
final sent = await Navigator.of(context).pushNamed(
|
||||
Routes.nftDetailsPage,
|
||||
arguments: NFTDetailsPageArguments(
|
||||
isSolanaNFT: true,
|
||||
solanaNFTAssetModel: nftAsset,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Symbol: ${nftAsset.symbol ?? '---'}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
);
|
||||
|
||||
if (sent != true) {
|
||||
return;
|
||||
}
|
||||
|
||||
final mint = nftAsset.mint;
|
||||
if (walletName != null && mint != null && mint.isNotEmpty) {
|
||||
await nftViewModel.onNFTSent(walletName, mint);
|
||||
}
|
||||
},
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16),
|
||||
padding: const EdgeInsets.all(8),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(16.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 0.0,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
height: 100,
|
||||
width: 100,
|
||||
margin: const EdgeInsets.all(8),
|
||||
clipBehavior: Clip.hardEdge,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(24.0),
|
||||
border: Border.all(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
width: 0.0,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
(nftAsset.name?.isNotEmpty ?? false)
|
||||
? nftAsset.name!
|
||||
: (nftAsset.symbol ?? '---'),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
],
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: HashImageWidget(
|
||||
imageUrl: nftAsset.imageOriginalUrl,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
"Symbol: ${nftAsset.symbol ?? "---"}",
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
(nftAsset.name?.isNotEmpty ?? false)
|
||||
? nftAsset.name!
|
||||
: (nftAsset.symbol ?? "---"),
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontWeight: FontWeight.w900,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
if (nftAsset.isOwned == false) ...[
|
||||
const SizedBox(height: 8),
|
||||
const _WatchingBadge(),
|
||||
],
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
class _WatchingBadge extends StatelessWidget {
|
||||
const _WatchingBadge();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(8.0),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
child: Text(
|
||||
S.current.watching,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w600,
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,87 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/dashboard_view_model.dart';
|
||||
import 'package:hash_wallet/core/sync_status_title.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
|
||||
|
||||
class SyncIndicator extends StatelessWidget {
|
||||
SyncIndicator({
|
||||
required this.dashboardViewModel,
|
||||
required this.onTap,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final DashboardViewModel dashboardViewModel;
|
||||
final Function() onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(builder: (_) {
|
||||
final syncIndicatorWidth = 237.0;
|
||||
final status = dashboardViewModel.status;
|
||||
final statusText =
|
||||
syncStatusTitle(status, dashboardViewModel.settingsStore.syncStatusDisplayMode);
|
||||
final progress = status.progress();
|
||||
final indicatorOffset = progress * syncIndicatorWidth;
|
||||
final indicatorWidth = progress < 1
|
||||
? indicatorOffset > 0
|
||||
? indicatorOffset
|
||||
: 0.0
|
||||
: syncIndicatorWidth;
|
||||
|
||||
return ClipRRect(
|
||||
borderRadius: BorderRadius.all(Radius.circular(15)),
|
||||
child: GestureDetector(
|
||||
onTap: onTap,
|
||||
onLongPress: dashboardViewModel.toggleSwitchStatusDisplayMode,
|
||||
child: Container(
|
||||
height: 30,
|
||||
width: syncIndicatorWidth,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
child: Stack(
|
||||
alignment: Alignment.center,
|
||||
children: <Widget>[
|
||||
progress <= 1
|
||||
? Positioned(
|
||||
left: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
child: Container(
|
||||
width: indicatorWidth,
|
||||
height: 30,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
))
|
||||
: Offstage(),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 24, right: 24),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
SyncIndicatorIcon(
|
||||
isSynced: status is SyncedSyncStatus,
|
||||
showTorIcon: dashboardViewModel.builtinTor,
|
||||
size: dashboardViewModel.builtinTor ? 16 : 6,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 6),
|
||||
child: Text(
|
||||
statusText,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import 'package:cw_core/root_dir.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
|
||||
class DevMoneroWalletCacheDebugPage extends StatelessWidget {
|
||||
@@ -261,6 +262,7 @@ class _JsonExplorerState extends State<JsonExplorer> {
|
||||
),
|
||||
)
|
||||
: ListView.builder(
|
||||
controller: ModalScrollController.of(context),
|
||||
itemCount: _filteredItems.length,
|
||||
itemBuilder: (context, index) {
|
||||
final item = _filteredItems[index];
|
||||
|
||||
@@ -1,790 +0,0 @@
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/core/auth_service.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
|
||||
import 'package:hash_wallet/src/widgets/add_template_button.dart';
|
||||
import 'package:hash_wallet/utils/debounce.dart';
|
||||
import 'package:hash_wallet/utils/payment_request.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:hash_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:hash_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
|
||||
import 'package:hash_wallet/src/widgets/standard_checkbox.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:keyboard_actions/keyboard_actions.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_template.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_trade_state.dart';
|
||||
import 'package:hash_wallet/exchange/limits_state.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:hash_wallet/src/widgets/keyboard_done_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/template_tile.dart';
|
||||
import 'package:hash_wallet/src/widgets/trail_button.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/exchange_card.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/view_model/exchange/exchange_view_model.dart';
|
||||
import 'package:hash_wallet/core/address_validator.dart';
|
||||
import 'package:hash_wallet/core/amount_validator.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
|
||||
|
||||
class ExchangePage extends BasePage {
|
||||
ExchangePage(this.exchangeViewModel, this.authService, this.initialPaymentRequest) {
|
||||
depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
|
||||
? exchangeViewModel.wallet.name
|
||||
: null;
|
||||
receiveWalletName = exchangeViewModel.receiveCurrency == CryptoCurrency.xmr
|
||||
? exchangeViewModel.wallet.name
|
||||
: null;
|
||||
}
|
||||
|
||||
final ExchangeViewModel exchangeViewModel;
|
||||
final AuthService authService;
|
||||
final PaymentRequest? initialPaymentRequest;
|
||||
final depositKey = GlobalKey<ExchangeCardState>();
|
||||
final receiveKey = GlobalKey<ExchangeCardState>();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _depositAmountFocus = FocusNode();
|
||||
final _depositAddressFocus = FocusNode();
|
||||
final _receiveAmountFocus = FocusNode();
|
||||
final _receiveAddressFocus = FocusNode();
|
||||
final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500));
|
||||
Debounce _depositAmountDebounce = Debounce(Duration(milliseconds: 500));
|
||||
var _isReactionsSet = false;
|
||||
|
||||
late final String? depositWalletName;
|
||||
late final String? receiveWalletName;
|
||||
|
||||
@override
|
||||
String get title => S.current.exchange;
|
||||
|
||||
@override
|
||||
bool get gradientBackground => true;
|
||||
|
||||
@override
|
||||
bool get gradientAll => true;
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => false;
|
||||
|
||||
@override
|
||||
bool get extendBodyBehindAppBar => true;
|
||||
|
||||
@override
|
||||
AppBarStyle get appBarStyle => AppBarStyle.transparent;
|
||||
|
||||
@override
|
||||
Function(BuildContext)? get pushToNextWidget => (context) {
|
||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||
if (!currentFocus.hasPrimaryFocus) {
|
||||
currentFocus.focusedChild?.unfocus();
|
||||
}
|
||||
};
|
||||
|
||||
bool get _shouldWaitTillSynced =>
|
||||
[CryptoCurrency.xmr, CryptoCurrency.btc, CryptoCurrency.ltc]
|
||||
.contains(exchangeViewModel.depositCurrency) &&
|
||||
!(exchangeViewModel.status is SyncedSyncStatus);
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 6.0),
|
||||
child: Observer(
|
||||
builder: (_) =>
|
||||
SyncIndicatorIcon(isSynced: exchangeViewModel.status is SyncedSyncStatus),
|
||||
),
|
||||
),
|
||||
PresentProviderPicker(exchangeViewModel: exchangeViewModel)
|
||||
],
|
||||
);
|
||||
|
||||
@override
|
||||
Widget trailing(BuildContext context) => TrailButton(
|
||||
caption: S.of(context).reset,
|
||||
onPressed: () {
|
||||
_formKey.currentState?.reset();
|
||||
exchangeViewModel.reset();
|
||||
},
|
||||
);
|
||||
|
||||
@override
|
||||
Widget? leading(BuildContext context) {
|
||||
final _backButton = Icon(
|
||||
Icons.arrow_back_ios,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 16,
|
||||
);
|
||||
final _closeButton = currentTheme.isDark ? closeButtonImageDarkTheme : closeButtonImage;
|
||||
|
||||
bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
|
||||
|
||||
return MergeSemantics(
|
||||
child: SizedBox(
|
||||
height: isMobileView ? 37 : 45,
|
||||
width: isMobileView ? 37 : 45,
|
||||
child: ButtonTheme(
|
||||
minWidth: double.minPositive,
|
||||
child: Semantics(
|
||||
label: !isMobileView ? S.of(context).close : S.of(context).seed_alert_back,
|
||||
child: TextButton(
|
||||
style: ButtonStyle(
|
||||
overlayColor: WidgetStateColor.resolveWith((states) => Colors.transparent),
|
||||
),
|
||||
onPressed: () => onClose(context),
|
||||
child: !isMobileView ? _closeButton : _backButton,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
|
||||
|
||||
return KeyboardActions(
|
||||
disableScroll: true,
|
||||
config: KeyboardActionsConfig(
|
||||
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
|
||||
keyboardBarColor: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
nextFocus: false,
|
||||
actions: [
|
||||
KeyboardActionsItem(
|
||||
focusNode: _depositAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
|
||||
KeyboardActionsItem(
|
||||
focusNode: _receiveAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.only(bottom: 24),
|
||||
content: Observer(
|
||||
builder: (_) => Column(
|
||||
children: <Widget>[
|
||||
_exchangeCardsSection(context),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 12, left: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
StandardCheckbox(
|
||||
value: exchangeViewModel.isFixedRateMode,
|
||||
caption: S.of(context).fixed_rate,
|
||||
onChanged: (value) {
|
||||
if (value) {
|
||||
exchangeViewModel.enableFixedRateMode();
|
||||
} else {
|
||||
exchangeViewModel.isFixedRateMode = false;
|
||||
}
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
SizedBox(height: 30),
|
||||
_buildTemplateSection(context)
|
||||
],
|
||||
),
|
||||
),
|
||||
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
bottomSection: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 15),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final description = exchangeViewModel.isFixedRateMode
|
||||
? exchangeViewModel.isAvailableInSelected
|
||||
? S.of(context).amount_is_guaranteed
|
||||
: S.of(context).fixed_pair_not_supported
|
||||
: exchangeViewModel.isAvailableInSelected
|
||||
? S.of(context).amount_is_estimate
|
||||
: S.of(context).this_pair_is_not_supported_warning;
|
||||
return Row(
|
||||
children: [
|
||||
if (description == S.of(context).this_pair_is_not_supported_warning)
|
||||
Expanded(
|
||||
child: Container(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Icon(Icons.warning_amber_rounded,
|
||||
color: Theme.of(context).colorScheme.error, size: 26),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 8,
|
||||
child: Text(
|
||||
description,
|
||||
textAlign: TextAlign.center,
|
||||
softWrap: true,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
maxLines: 3,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => LoadingPrimaryButton(
|
||||
key: ValueKey('exchange_page_exchange_button_key'),
|
||||
text: exchangeViewModel.isAvailableInSelected
|
||||
? S.of(context).swap
|
||||
: S.of(context).change_selected_exchanges,
|
||||
onPressed: exchangeViewModel.isAvailableInSelected
|
||||
? () {
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
if (_formKey.currentState != null &&
|
||||
_formKey.currentState!.validate()) {
|
||||
if (_shouldWaitTillSynced) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).exchange,
|
||||
alertContent: S.of(context).exchange_sync_alert_content,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
);
|
||||
},
|
||||
);
|
||||
} else {
|
||||
final check = exchangeViewModel.shouldDisplayTOTP();
|
||||
authService.authenticateAction(
|
||||
context,
|
||||
conditionToDetermineIfToUse2FA: check,
|
||||
onAuthSuccess: (value) {
|
||||
if (value) {
|
||||
exchangeViewModel.createTrade();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
: () => PresentProviderPicker(exchangeViewModel: exchangeViewModel)
|
||||
.presentProviderPicker(context),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isDisabled: exchangeViewModel.selectedProviders.isEmpty,
|
||||
isLoading: exchangeViewModel.tradeState is TradeIsCreating,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTemplateSection(BuildContext context) {
|
||||
return Container(
|
||||
height: 40,
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(left: 24),
|
||||
child: SingleChildScrollView(
|
||||
scrollDirection: Axis.horizontal,
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final templates = exchangeViewModel.templates;
|
||||
|
||||
return Row(
|
||||
children: <Widget>[
|
||||
AddTemplateButton(
|
||||
onTap: () => Navigator.of(context).pushNamed(Routes.exchangeTemplate),
|
||||
currentTemplatesLength: templates.length,
|
||||
),
|
||||
ListView.builder(
|
||||
scrollDirection: Axis.horizontal,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemCount: templates.length,
|
||||
itemBuilder: (context, index) {
|
||||
final template = templates[index];
|
||||
|
||||
return TemplateTile(
|
||||
key: UniqueKey(),
|
||||
amount: template.amount,
|
||||
from: template.depositCurrencyTitle,
|
||||
to: template.receiveCurrencyTitle,
|
||||
onTap: () {
|
||||
applyTemplate(context, exchangeViewModel, template);
|
||||
},
|
||||
onRemove: () {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: S.of(context).template,
|
||||
alertContent: S.of(context).confirm_delete_template,
|
||||
rightButtonText: S.of(context).delete,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
actionRightButton: () {
|
||||
Navigator.of(dialogContext).pop();
|
||||
exchangeViewModel.removeTemplate(template: template);
|
||||
exchangeViewModel.updateTemplate();
|
||||
},
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void applyTemplate(
|
||||
BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async {
|
||||
final depositCryptoCurrency =
|
||||
CryptoCurrency.safeParseCurrencyFromString(template.depositCurrency);
|
||||
final receiveCryptoCurrency =
|
||||
CryptoCurrency.safeParseCurrencyFromString(template.receiveCurrency);
|
||||
|
||||
if (depositCryptoCurrency == null || receiveCryptoCurrency == null) {
|
||||
///TO DO: add support for user tokens
|
||||
return;
|
||||
}
|
||||
|
||||
exchangeViewModel.changeDepositCurrency(currency: depositCryptoCurrency);
|
||||
exchangeViewModel.changeReceiveCurrency(currency: receiveCryptoCurrency);
|
||||
|
||||
exchangeViewModel.changeDepositAmount(amount: template.amount);
|
||||
exchangeViewModel.depositAddress = template.depositAddress;
|
||||
exchangeViewModel.receiveAddress = template.receiveAddress;
|
||||
exchangeViewModel.isReceiveAmountEntered = false;
|
||||
exchangeViewModel.isFixedRateMode = false;
|
||||
|
||||
var domain = template.depositAddress;
|
||||
exchangeViewModel.depositAddress =
|
||||
await fetchParsedAddress(context, domain, depositCryptoCurrency);
|
||||
|
||||
domain = template.receiveAddress;
|
||||
exchangeViewModel.receiveAddress =
|
||||
await fetchParsedAddress(context, domain, receiveCryptoCurrency);
|
||||
}
|
||||
|
||||
void _setReactions(BuildContext context, ExchangeViewModel exchangeViewModel) {
|
||||
if (_isReactionsSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (exchangeViewModel.feesViewModel.isLowFee) {
|
||||
_showFeeAlert(context);
|
||||
}
|
||||
|
||||
final depositAddressController = depositKey.currentState!.addressController;
|
||||
final depositAmountController = depositKey.currentState!.amountController;
|
||||
final receiveAddressController = receiveKey.currentState!.addressController;
|
||||
final receiveAmountController = receiveKey.currentState!.amountController;
|
||||
final limitsState = exchangeViewModel.limitsState;
|
||||
|
||||
if (limitsState is LimitsLoadedSuccessfully) {
|
||||
final min = limitsState.limits.min != null ? limitsState.limits.min.toString() : null;
|
||||
final max = limitsState.limits.max != null ? limitsState.limits.max.toString() : null;
|
||||
final key = exchangeViewModel.isFixedRateMode ? receiveKey : depositKey;
|
||||
key.currentState!.changeLimits(min: min, max: max);
|
||||
}
|
||||
|
||||
_onCurrencyChange(exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey);
|
||||
_onCurrencyChange(exchangeViewModel.depositCurrency, exchangeViewModel, depositKey);
|
||||
|
||||
reaction(
|
||||
(_) => exchangeViewModel.wallet.name,
|
||||
(_) =>
|
||||
_onWalletNameChange(exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey));
|
||||
|
||||
reaction(
|
||||
(_) => exchangeViewModel.wallet.name,
|
||||
(_) =>
|
||||
_onWalletNameChange(exchangeViewModel, exchangeViewModel.depositCurrency, depositKey));
|
||||
|
||||
reaction((_) => exchangeViewModel.receiveCurrency,
|
||||
(CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, receiveKey));
|
||||
|
||||
reaction((_) => exchangeViewModel.depositCurrency,
|
||||
(CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, depositKey));
|
||||
|
||||
reaction((_) => exchangeViewModel.depositAmount, (String amount) {
|
||||
if (exchangeViewModel.isSendAllEnabled) {
|
||||
depositAmountController.text = S.of(context).all;
|
||||
} else if (depositAmountController.text != amount && amount != S.of(context).all) {
|
||||
depositAmountController.text = amount;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.depositAddress, (String address) {
|
||||
if (depositKey.currentState!.addressController.text != address) {
|
||||
depositKey.currentState!.addressController.text = address;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) {
|
||||
depositKey.currentState!.isAddressEditable(isEditable: isEnabled);
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.receiveAmount, (String amount) {
|
||||
if (receiveKey.currentState!.amountController.text != amount) {
|
||||
receiveKey.currentState!.amountController.text = amount;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.receiveAddress, (String address) {
|
||||
if (receiveKey.currentState!.addressController.text != address) {
|
||||
receiveKey.currentState!.addressController.text = address;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.isReceiveAmountEditable, (bool isReceiveAmountEditable) {
|
||||
receiveKey.currentState!.isAmountEditable(isEditable: isReceiveAmountEditable);
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.tradeState, (ExchangeTradeState state) {
|
||||
if (state is TradeIsCreatedFailure) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
key: ValueKey('exchange_page_trade_creation_failure_dialog_key'),
|
||||
buttonKey: ValueKey('exchange_page_trade_creation_failure_dialog_button_key'),
|
||||
alertTitle: S.of(context).provider_error(state.title),
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop());
|
||||
});
|
||||
});
|
||||
}
|
||||
if (state is TradeIsCreatedSuccessfully) {
|
||||
exchangeViewModel.reset();
|
||||
(exchangeViewModel.tradesStore.trade?.provider == ExchangeProviderDescription.thorChain ||
|
||||
exchangeViewModel.tradesStore.trade?.provider ==
|
||||
ExchangeProviderDescription.chainflip)
|
||||
? Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade)
|
||||
: Navigator.of(context).pushReplacementNamed(Routes.exchangeConfirm);
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.limitsState, (LimitsState state) {
|
||||
String? min;
|
||||
String? max;
|
||||
|
||||
if (state is LimitsLoadedSuccessfully) {
|
||||
min = state.limits.min != null ? state.limits.min.toString() : null;
|
||||
max = state.limits.max != null ? state.limits.max.toString() : null;
|
||||
}
|
||||
|
||||
if (state is LimitsLoadedFailure) {
|
||||
min = '0';
|
||||
max = '0';
|
||||
}
|
||||
|
||||
if (state is LimitsIsLoading) {
|
||||
min = '...';
|
||||
max = '...';
|
||||
}
|
||||
|
||||
if (exchangeViewModel.isFixedRateMode) {
|
||||
depositKey.currentState!.changeLimits(min: null, max: null);
|
||||
receiveKey.currentState!.changeLimits(min: min, max: max);
|
||||
} else {
|
||||
depositKey.currentState!.changeLimits(min: min, max: max);
|
||||
receiveKey.currentState!.changeLimits(min: null, max: null);
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.bestRate, (double rate) {
|
||||
if (exchangeViewModel.isFixedRateMode) {
|
||||
exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
|
||||
} else {
|
||||
if (depositAmountController.text == S.current.all)
|
||||
exchangeViewModel.changeDepositAmount(amount: exchangeViewModel.depositAmount);
|
||||
else
|
||||
exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
|
||||
}
|
||||
});
|
||||
|
||||
depositAddressController
|
||||
.addListener(() => exchangeViewModel.depositAddress = depositAddressController.text);
|
||||
|
||||
depositAmountController.addListener(() {
|
||||
if (depositAmountController.text != exchangeViewModel.depositAmount &&
|
||||
depositAmountController.text != S.of(context).all) {
|
||||
exchangeViewModel.isSendAllEnabled = false;
|
||||
_depositAmountDebounce = Debounce(Duration(milliseconds: 500));
|
||||
|
||||
_depositAmountDebounce.run(() {
|
||||
exchangeViewModel.calculateBestRate();
|
||||
exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
|
||||
exchangeViewModel.isReceiveAmountEntered = false;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
receiveAddressController
|
||||
.addListener(() => exchangeViewModel.receiveAddress = receiveAddressController.text);
|
||||
|
||||
receiveAmountController.addListener(() {
|
||||
if (receiveAmountController.text != exchangeViewModel.receiveAmount) {
|
||||
_receiveAmountDebounce.run(() {
|
||||
exchangeViewModel.calculateBestRate();
|
||||
exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
|
||||
exchangeViewModel.isReceiveAmountEntered = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.wallet.walletAddresses.addressForExchange, (String address) {
|
||||
if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) {
|
||||
depositKey.currentState!.changeAddress(address: address);
|
||||
}
|
||||
|
||||
if (exchangeViewModel.receiveCurrency == CryptoCurrency.xmr) {
|
||||
receiveKey.currentState!.changeAddress(address: address);
|
||||
}
|
||||
});
|
||||
|
||||
_depositAddressFocus.addListener(() async {
|
||||
if (!_depositAddressFocus.hasFocus && depositAddressController.text.isNotEmpty) {
|
||||
final domain = depositAddressController.text;
|
||||
exchangeViewModel.depositAddress =
|
||||
await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency);
|
||||
}
|
||||
});
|
||||
|
||||
_receiveAddressFocus.addListener(() async {
|
||||
if (!_receiveAddressFocus.hasFocus && receiveAddressController.text.isNotEmpty) {
|
||||
final domain = receiveAddressController.text;
|
||||
exchangeViewModel.receiveAddress =
|
||||
await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency);
|
||||
}
|
||||
});
|
||||
|
||||
_receiveAmountFocus.addListener(() {
|
||||
if (_receiveAmountFocus.hasFocus) {
|
||||
exchangeViewModel.enableFixedRateMode();
|
||||
}
|
||||
// exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
|
||||
});
|
||||
|
||||
_depositAmountFocus.addListener(() {
|
||||
exchangeViewModel.isFixedRateMode = false;
|
||||
// exchangeViewModel.changeDepositAmount(
|
||||
// amount: depositAmountController.text);
|
||||
});
|
||||
|
||||
if (initialPaymentRequest != null) {
|
||||
try {
|
||||
exchangeViewModel.receiveCurrency =
|
||||
CryptoCurrency.fromString(initialPaymentRequest!.scheme);
|
||||
exchangeViewModel.setCanonicalReceiveAmount(initialPaymentRequest!.amount);
|
||||
exchangeViewModel.receiveAddress = initialPaymentRequest!.address;
|
||||
} catch (e) {
|
||||
printV('error: ${e.toString()}');
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
_isReactionsSet = true;
|
||||
}
|
||||
|
||||
void _onCurrencyChange(CryptoCurrency currency, ExchangeViewModel exchangeViewModel,
|
||||
GlobalKey<ExchangeCardState> key) {
|
||||
final isCurrentTypeWallet = exchangeViewModel.useSameWalletAddress(currency);
|
||||
|
||||
key.currentState!.changeSelectedCurrency(currency);
|
||||
key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
|
||||
|
||||
key.currentState!.changeAddress(
|
||||
address:
|
||||
isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.addressForExchange : '');
|
||||
}
|
||||
|
||||
void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency,
|
||||
GlobalKey<ExchangeCardState> key) {
|
||||
final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency;
|
||||
|
||||
if (isCurrentTypeWallet) {
|
||||
key.currentState!.changeWalletName(exchangeViewModel.wallet.name);
|
||||
key.currentState!.addressController.text =
|
||||
exchangeViewModel.wallet.walletAddresses.addressForExchange;
|
||||
} else if (key.currentState!.addressController.text ==
|
||||
exchangeViewModel.wallet.walletAddresses.addressForExchange) {
|
||||
key.currentState!.changeWalletName('');
|
||||
key.currentState!.addressController.text = '';
|
||||
}
|
||||
}
|
||||
|
||||
Future<String> fetchParsedAddress(
|
||||
BuildContext context, String domain, CryptoCurrency currency) async {
|
||||
final parsedAddress = await getIt.get<AddressResolver>().resolve(context, domain, currency);
|
||||
return extractAddressFromParsed(context, parsedAddress);
|
||||
}
|
||||
|
||||
void _showFeeAlert(BuildContext context) async {
|
||||
await Future<void>.delayed(Duration(seconds: 1));
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: S.of(context).low_fee,
|
||||
alertContent: S.of(context).low_fee_alert,
|
||||
leftButtonText: S.of(context).ignor,
|
||||
rightButtonText: S.of(context).use_suggested,
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
|
||||
actionRightButton: () => Navigator.of(dialogContext).pop(true));
|
||||
}) ??
|
||||
false;
|
||||
if (confirmed) {
|
||||
exchangeViewModel.feesViewModel.setDefaultTransactionPriority();
|
||||
}
|
||||
}
|
||||
|
||||
void disposeBestRateSync() => exchangeViewModel.bestRateSync.cancel();
|
||||
|
||||
Widget _exchangeCardsSection(BuildContext context) {
|
||||
final firstExchangeCard = Observer(
|
||||
builder: (_) => ExchangeCard(
|
||||
cardInstanceName: 'deposit_exchange_card',
|
||||
onDispose: disposeBestRateSync,
|
||||
hasAllAmount: exchangeViewModel.hasAllAmount,
|
||||
allAmount:
|
||||
exchangeViewModel.hasAllAmount ? () => exchangeViewModel.enableSendAllAmount() : null,
|
||||
isAllAmountEnabled: exchangeViewModel.isSendAllEnabled,
|
||||
amountFocusNode: _depositAmountFocus,
|
||||
addressFocusNode: _depositAddressFocus,
|
||||
key: depositKey,
|
||||
title: S.of(context).you_will_send,
|
||||
initialCurrency: exchangeViewModel.depositCurrency,
|
||||
initialWalletName: depositWalletName ?? '',
|
||||
initialAddress: exchangeViewModel.depositCurrency == exchangeViewModel.wallet.currency
|
||||
? exchangeViewModel.wallet.walletAddresses.addressForExchange
|
||||
: exchangeViewModel.depositAddress,
|
||||
initialIsAmountEditable: true,
|
||||
initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
|
||||
isAmountEstimated: false,
|
||||
hasRefundAddress: true,
|
||||
isMoneroWallet: exchangeViewModel.isMoneroWallet,
|
||||
currencies: exchangeViewModel.depositCurrencies,
|
||||
onCurrencySelected: (currency) {
|
||||
exchangeViewModel.changeDepositCurrency(currency: currency);
|
||||
},
|
||||
currencyButtonColor: Colors.transparent,
|
||||
addressButtonsColor: Colors.transparent,
|
||||
borderColor: Theme.of(context).colorScheme.outlineVariant,
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
currencyValueValidator: (value) {
|
||||
return !exchangeViewModel.isFixedRateMode && value != S.of(context).all
|
||||
? AmountValidator(
|
||||
isAutovalidate: true,
|
||||
currency: exchangeViewModel.depositCurrency,
|
||||
amountParsingProxy: exchangeViewModel.amountParsingProxy,
|
||||
minValue: exchangeViewModel.limits.min.toString(),
|
||||
maxValue: exchangeViewModel.limits.max.toString(),
|
||||
).call(value)
|
||||
: null;
|
||||
},
|
||||
addressTextFieldValidator: AddressValidator(type: exchangeViewModel.depositCurrency),
|
||||
onPushPasteButton: (context) async {
|
||||
final domain = exchangeViewModel.depositAddress;
|
||||
exchangeViewModel.depositAddress =
|
||||
await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency);
|
||||
},
|
||||
onPushAddressBookButton: (context) async {
|
||||
final domain = exchangeViewModel.depositAddress;
|
||||
exchangeViewModel.depositAddress =
|
||||
await fetchParsedAddress(context, domain, exchangeViewModel.depositCurrency);
|
||||
},
|
||||
useSatoshis: exchangeViewModel.useSatoshiDeposit,
|
||||
),
|
||||
);
|
||||
|
||||
final secondExchangeCard = Observer(
|
||||
builder: (_) => ExchangeCard(
|
||||
cardInstanceName: 'receive_exchange_card',
|
||||
onDispose: disposeBestRateSync,
|
||||
amountFocusNode: _receiveAmountFocus,
|
||||
addressFocusNode: _receiveAddressFocus,
|
||||
key: receiveKey,
|
||||
title: S.of(context).you_will_get,
|
||||
initialCurrency: exchangeViewModel.receiveCurrency,
|
||||
initialWalletName: receiveWalletName ?? '',
|
||||
initialAddress: exchangeViewModel.receiveCurrency == exchangeViewModel.wallet.currency
|
||||
? exchangeViewModel.wallet.walletAddresses.addressForExchange
|
||||
: exchangeViewModel.receiveAddress,
|
||||
initialIsAmountEditable: exchangeViewModel.isReceiveAmountEditable,
|
||||
isAmountEstimated: true,
|
||||
isMoneroWallet: exchangeViewModel.isMoneroWallet,
|
||||
currencies: exchangeViewModel.receiveCurrencies,
|
||||
onCurrencySelected: (currency) =>
|
||||
exchangeViewModel.changeReceiveCurrency(currency: currency),
|
||||
currencyButtonColor: Colors.transparent,
|
||||
addressButtonsColor: Colors.transparent,
|
||||
borderColor: Theme.of(context).colorScheme.outlineVariant,
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
currencyValueValidator: (value) {
|
||||
return exchangeViewModel.isFixedRateMode
|
||||
? AmountValidator(
|
||||
isAutovalidate: true,
|
||||
currency: exchangeViewModel.receiveCurrency,
|
||||
amountParsingProxy: exchangeViewModel.amountParsingProxy,
|
||||
minValue: exchangeViewModel.limits.min.toString(),
|
||||
maxValue: exchangeViewModel.limits.max.toString(),
|
||||
).call(value)
|
||||
: null;
|
||||
},
|
||||
addressTextFieldValidator: AddressValidator(type: exchangeViewModel.receiveCurrency),
|
||||
onPushPasteButton: (context) async {
|
||||
final domain = exchangeViewModel.receiveAddress;
|
||||
exchangeViewModel.receiveAddress =
|
||||
await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency);
|
||||
},
|
||||
onPushAddressBookButton: (context) async {
|
||||
final domain = exchangeViewModel.receiveAddress;
|
||||
exchangeViewModel.receiveAddress =
|
||||
await fetchParsedAddress(context, domain, exchangeViewModel.receiveCurrency);
|
||||
},
|
||||
useSatoshis: exchangeViewModel.useSatoshisReceive,
|
||||
),
|
||||
);
|
||||
|
||||
return responsiveLayoutUtil.shouldRenderMobileUI
|
||||
? MobileExchangeCardsSection(
|
||||
firstExchangeCard: firstExchangeCard,
|
||||
secondExchangeCard: secondExchangeCard,
|
||||
)
|
||||
: DesktopExchangeCardsSection(
|
||||
firstExchangeCard: firstExchangeCard,
|
||||
secondExchangeCard: secondExchangeCard,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
import 'package:hash_wallet/exchange/provider/exchange_provider.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/keyboard_done_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:keyboard_actions/keyboard_actions.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/exchange_card.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:hash_wallet/view_model/exchange/exchange_view_model.dart';
|
||||
import 'package:hash_wallet/core/amount_validator.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
|
||||
|
||||
class ExchangeTemplatePage extends BasePage {
|
||||
ExchangeTemplatePage(this.exchangeViewModel);
|
||||
|
||||
final ExchangeViewModel exchangeViewModel;
|
||||
final depositKey = GlobalKey<ExchangeCardState>();
|
||||
final receiveKey = GlobalKey<ExchangeCardState>();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
final _depositAmountFocus = FocusNode();
|
||||
final _receiveAmountFocus = FocusNode();
|
||||
var _isReactionsSet = false;
|
||||
|
||||
@override
|
||||
bool get gradientAll => true;
|
||||
|
||||
@override
|
||||
String get title => S.current.exchange_new_template;
|
||||
|
||||
@override
|
||||
bool get extendBodyBehindAppBar => true;
|
||||
|
||||
@override
|
||||
AppBarStyle get appBarStyle => AppBarStyle.transparent;
|
||||
|
||||
@override
|
||||
Widget trailing(BuildContext context) =>
|
||||
PresentProviderPicker(exchangeViewModel: exchangeViewModel);
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
final arrowBottomPurple = Image.asset(
|
||||
'assets/images/arrow_bottom_purple_icon.png',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
height: 8,
|
||||
);
|
||||
final arrowBottomCakeGreen = Image.asset(
|
||||
'assets/images/arrow_bottom_green.png',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
height: 8,
|
||||
);
|
||||
|
||||
final depositWalletName = exchangeViewModel.depositCurrency == CryptoCurrency.xmr
|
||||
? exchangeViewModel.wallet.name
|
||||
: null;
|
||||
final receiveWalletName = exchangeViewModel.receiveCurrency == CryptoCurrency.xmr
|
||||
? exchangeViewModel.wallet.name
|
||||
: null;
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context, exchangeViewModel));
|
||||
|
||||
return KeyboardActions(
|
||||
disableScroll: true,
|
||||
config: KeyboardActionsConfig(
|
||||
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
|
||||
keyboardBarColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
nextFocus: false,
|
||||
actions: [
|
||||
KeyboardActionsItem(
|
||||
focusNode: _depositAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()]),
|
||||
KeyboardActionsItem(
|
||||
focusNode: _receiveAmountFocus, toolbarButtons: [(_) => KeyboardDoneButton()])
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Form(
|
||||
key: _formKey,
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.only(bottom: 24),
|
||||
content: Container(
|
||||
padding: EdgeInsets.only(bottom: 32),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: FocusTraversalGroup(
|
||||
policy: OrderedTraversalPolicy(),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(24, 100, 24, 32),
|
||||
child: Observer(
|
||||
builder: (_) => ExchangeCard(
|
||||
cardInstanceName: 'deposit_exchange_template_card',
|
||||
amountFocusNode: _depositAmountFocus,
|
||||
key: depositKey,
|
||||
title: S.of(context).you_will_send,
|
||||
initialCurrency: exchangeViewModel.depositCurrency,
|
||||
initialWalletName: depositWalletName ?? '',
|
||||
initialAddress: exchangeViewModel.depositCurrency ==
|
||||
exchangeViewModel.wallet.currency
|
||||
? exchangeViewModel.wallet.walletAddresses.addressForExchange
|
||||
: exchangeViewModel.depositAddress,
|
||||
initialIsAmountEditable: true,
|
||||
initialIsAddressEditable: exchangeViewModel.isDepositAddressEnabled,
|
||||
isAmountEstimated: false,
|
||||
hasRefundAddress: true,
|
||||
isMoneroWallet: exchangeViewModel.isMoneroWallet,
|
||||
currencies: CryptoCurrency.all,
|
||||
onCurrencySelected: (currency) =>
|
||||
exchangeViewModel.changeDepositCurrency(currency: currency),
|
||||
imageArrow: arrowBottomPurple,
|
||||
currencyButtonColor: Colors.transparent,
|
||||
addressButtonsColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderColor: Theme.of(context).colorScheme.outlineVariant,
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
currencyValueValidator: AmountValidator(
|
||||
currency: exchangeViewModel.depositCurrency,
|
||||
amountParsingProxy: exchangeViewModel.amountParsingProxy,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 29, left: 24, right: 24),
|
||||
child: Observer(
|
||||
builder: (_) => ExchangeCard(
|
||||
cardInstanceName: 'receive_exchange_template_card',
|
||||
amountFocusNode: _receiveAmountFocus,
|
||||
key: receiveKey,
|
||||
title: S.of(context).you_will_get,
|
||||
initialCurrency: exchangeViewModel.receiveCurrency,
|
||||
initialWalletName: receiveWalletName ?? '',
|
||||
initialAddress: exchangeViewModel.receiveCurrency ==
|
||||
exchangeViewModel.wallet.currency
|
||||
? exchangeViewModel.wallet.walletAddresses.addressForExchange
|
||||
: exchangeViewModel.receiveAddress,
|
||||
initialIsAmountEditable: false,
|
||||
isAmountEstimated: true,
|
||||
isMoneroWallet: exchangeViewModel.isMoneroWallet,
|
||||
currencies: exchangeViewModel.receiveCurrencies,
|
||||
onCurrencySelected: (currency) =>
|
||||
exchangeViewModel.changeReceiveCurrency(currency: currency),
|
||||
imageArrow: arrowBottomCakeGreen,
|
||||
currencyButtonColor: Colors.transparent,
|
||||
addressButtonsColor:
|
||||
Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderColor: Theme.of(context).colorScheme.outlineVariant,
|
||||
fillColor: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
currencyValueValidator: AmountValidator(
|
||||
currency: exchangeViewModel.receiveCurrency,
|
||||
amountParsingProxy: exchangeViewModel.amountParsingProxy,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
bottomSection: Column(children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 15),
|
||||
child: Observer(
|
||||
builder: (_) => Center(
|
||||
child: Text(
|
||||
S.of(context).amount_is_estimate,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
onPressed: () {
|
||||
if (_formKey.currentState != null && _formKey.currentState!.validate()) {
|
||||
exchangeViewModel.addTemplate(
|
||||
amount: exchangeViewModel.depositAmount,
|
||||
depositCurrency: exchangeViewModel.depositCurrency.name,
|
||||
depositCurrencyTitle: exchangeViewModel.depositCurrency.title +
|
||||
' ${exchangeViewModel.depositCurrency.tag ?? ''}',
|
||||
receiveCurrency: exchangeViewModel.receiveCurrency.name,
|
||||
receiveCurrencyTitle: exchangeViewModel.receiveCurrency.title +
|
||||
' ${exchangeViewModel.receiveCurrency.tag ?? ''}',
|
||||
provider: exchangeViewModel.provider.toString(),
|
||||
depositAddress: exchangeViewModel.depositAddress,
|
||||
receiveAddress: exchangeViewModel.receiveAddress);
|
||||
exchangeViewModel.updateTemplate();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
text: S.of(context).save,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary),
|
||||
]),
|
||||
))));
|
||||
}
|
||||
|
||||
void _setReactions(BuildContext context, ExchangeViewModel exchangeViewModel) {
|
||||
if (_isReactionsSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
final depositAddressController = depositKey.currentState!.addressController;
|
||||
final depositAmountController = depositKey.currentState!.amountController;
|
||||
final receiveAddressController = receiveKey.currentState!.addressController;
|
||||
final receiveAmountController = receiveKey.currentState!.amountController;
|
||||
final limitsState = exchangeViewModel.limitsState;
|
||||
|
||||
// FIXME: FIXME
|
||||
|
||||
// final limitsState = exchangeViewModel.limitsState;
|
||||
//
|
||||
// if (limitsState is LimitsLoadedSuccessfully) {
|
||||
// final min = limitsState.limits.min != null
|
||||
// ? limitsState.limits.min.toString()
|
||||
// : null;
|
||||
// final max = limitsState.limits.max != null
|
||||
// ? limitsState.limits.max.toString()
|
||||
// : null;
|
||||
// final key = depositKey;
|
||||
// key.currentState.changeLimits(min: min, max: max);
|
||||
// }
|
||||
|
||||
_onCurrencyChange(exchangeViewModel.receiveCurrency, exchangeViewModel, receiveKey);
|
||||
_onCurrencyChange(exchangeViewModel.depositCurrency, exchangeViewModel, depositKey);
|
||||
|
||||
reaction(
|
||||
(_) => exchangeViewModel.wallet.name,
|
||||
(String _) =>
|
||||
_onWalletNameChange(exchangeViewModel, exchangeViewModel.receiveCurrency, receiveKey));
|
||||
|
||||
reaction(
|
||||
(_) => exchangeViewModel.wallet.name,
|
||||
(String _) =>
|
||||
_onWalletNameChange(exchangeViewModel, exchangeViewModel.depositCurrency, depositKey));
|
||||
|
||||
reaction((_) => exchangeViewModel.receiveCurrency,
|
||||
(CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, receiveKey));
|
||||
|
||||
reaction((_) => exchangeViewModel.depositCurrency,
|
||||
(CryptoCurrency currency) => _onCurrencyChange(currency, exchangeViewModel, depositKey));
|
||||
|
||||
reaction((_) => exchangeViewModel.depositAmount, (String amount) {
|
||||
if (depositKey.currentState!.amountController.text != amount && amount != S.of(context).all) {
|
||||
depositKey.currentState!.amountController.text = amount;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.depositAddress, (String address) {
|
||||
if (depositKey.currentState!.addressController.text != address) {
|
||||
depositKey.currentState!.addressController.text = address;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.isDepositAddressEnabled, (bool isEnabled) {
|
||||
depositKey.currentState!.isAddressEditable(isEditable: isEnabled);
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.receiveAmount, (String amount) {
|
||||
if (receiveKey.currentState!.amountController.text != amount) {
|
||||
receiveKey.currentState!.amountController.text = amount;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.receiveAddress, (String address) {
|
||||
if (receiveKey.currentState!.addressController.text != address) {
|
||||
receiveKey.currentState!.addressController.text = address;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.provider, (ExchangeProvider? provider) {
|
||||
receiveKey.currentState!.isAmountEditable(isEditable: false);
|
||||
});
|
||||
|
||||
/*reaction((_) => exchangeViewModel.limitsState, (LimitsState state) {
|
||||
String min;
|
||||
String max;
|
||||
|
||||
if (state is LimitsLoadedSuccessfully) {
|
||||
min = state.limits.min != null ? state.limits.min.toString() : null;
|
||||
max = state.limits.max != null ? state.limits.max.toString() : null;
|
||||
}
|
||||
|
||||
if (state is LimitsLoadedFailure) {
|
||||
min = '0';
|
||||
max = '0';
|
||||
}
|
||||
|
||||
if (state is LimitsIsLoading) {
|
||||
min = '...';
|
||||
max = '...';
|
||||
}
|
||||
|
||||
depositKey.currentState.changeLimits(min: min, max: max);
|
||||
receiveKey.currentState.changeLimits(min: null, max: null);
|
||||
});*/
|
||||
|
||||
depositAddressController
|
||||
.addListener(() => exchangeViewModel.depositAddress = depositAddressController.text);
|
||||
|
||||
depositAmountController.addListener(() {
|
||||
if (depositAmountController.text != exchangeViewModel.depositAmount &&
|
||||
exchangeViewModel.depositAmount != S.of(context).all) {
|
||||
exchangeViewModel.changeDepositAmount(amount: depositAmountController.text);
|
||||
exchangeViewModel.isReceiveAmountEntered = false;
|
||||
}
|
||||
});
|
||||
|
||||
receiveAddressController
|
||||
.addListener(() => exchangeViewModel.receiveAddress = receiveAddressController.text);
|
||||
|
||||
receiveAmountController.addListener(() {
|
||||
if (receiveAmountController.text != exchangeViewModel.receiveAmount) {
|
||||
exchangeViewModel.changeReceiveAmount(amount: receiveAmountController.text);
|
||||
exchangeViewModel.isReceiveAmountEntered = true;
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => exchangeViewModel.wallet.walletAddresses.address, (String address) {
|
||||
if (exchangeViewModel.depositCurrency == CryptoCurrency.xmr) {
|
||||
depositKey.currentState!.changeAddress(address: address);
|
||||
}
|
||||
|
||||
if (exchangeViewModel.receiveCurrency == CryptoCurrency.xmr) {
|
||||
receiveKey.currentState!.changeAddress(address: address);
|
||||
}
|
||||
});
|
||||
|
||||
_isReactionsSet = true;
|
||||
}
|
||||
|
||||
void _onCurrencyChange(CryptoCurrency currency, ExchangeViewModel exchangeViewModel,
|
||||
GlobalKey<ExchangeCardState> key) {
|
||||
final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency;
|
||||
|
||||
key.currentState!.changeSelectedCurrency(currency);
|
||||
key.currentState!.changeWalletName(isCurrentTypeWallet ? exchangeViewModel.wallet.name : '');
|
||||
|
||||
key.currentState!.changeAddress(
|
||||
address: isCurrentTypeWallet ? exchangeViewModel.wallet.walletAddresses.address : '');
|
||||
|
||||
key.currentState!.changeAmount(amount: '');
|
||||
}
|
||||
|
||||
void _onWalletNameChange(ExchangeViewModel exchangeViewModel, CryptoCurrency currency,
|
||||
GlobalKey<ExchangeCardState> key) {
|
||||
final isCurrentTypeWallet = currency == exchangeViewModel.wallet.currency;
|
||||
|
||||
if (isCurrentTypeWallet) {
|
||||
key.currentState!.changeWalletName(exchangeViewModel.wallet.name);
|
||||
key.currentState!.addressController.text = exchangeViewModel.wallet.walletAddresses.address;
|
||||
} else if (key.currentState!.addressController.text ==
|
||||
exchangeViewModel.wallet.walletAddresses.address) {
|
||||
key.currentState!.changeWalletName('');
|
||||
key.currentState!.addressController.text = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class PickerItemWidget extends StatelessWidget {
|
||||
const PickerItemWidget(
|
||||
{required this.title, this.iconPath, this.isSelected = false, this.tag, this.onTap});
|
||||
|
||||
final String? iconPath;
|
||||
final String title;
|
||||
final bool isSelected;
|
||||
final String? tag;
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 24),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
child: Image.asset(
|
||||
iconPath ?? '',
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
title.toUpperCase(),
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: isSelected
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.onSurface,
|
||||
fontSize: isSelected ? 16 : 14.0,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (tag != null)
|
||||
Align(
|
||||
alignment: Alignment.topCenter,
|
||||
child: Container(
|
||||
width: 35.0,
|
||||
height: 18.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
tag!,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 7.0, color: Theme.of(context).colorScheme.primary),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(6.0),
|
||||
//border: Border.all(color: ),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isSelected)
|
||||
Icon(
|
||||
Icons.check,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'currency_picker_item_widget.dart';
|
||||
|
||||
class CurrencyPickerWidget extends StatelessWidget {
|
||||
CurrencyPickerWidget({
|
||||
required this.crossAxisCount,
|
||||
required this.selectedAtIndex,
|
||||
required this.pickerItemsList,
|
||||
required this.pickListItem,
|
||||
});
|
||||
|
||||
final int crossAxisCount;
|
||||
final int selectedAtIndex;
|
||||
final Function pickListItem;
|
||||
final List<Currency> pickerItemsList;
|
||||
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
child: Scrollbar(
|
||||
controller: _scrollController,
|
||||
child: GridView.builder(
|
||||
controller: _scrollController,
|
||||
padding: EdgeInsets.zero,
|
||||
shrinkWrap: true,
|
||||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||||
crossAxisCount: crossAxisCount,
|
||||
crossAxisSpacing: 2,
|
||||
childAspectRatio: 3,
|
||||
),
|
||||
itemCount: pickerItemsList.length,
|
||||
itemBuilder: (BuildContext ctx, index) {
|
||||
return PickerItemWidget(
|
||||
onTap: () {
|
||||
pickListItem(index);
|
||||
},
|
||||
title: pickerItemsList[index].name,
|
||||
iconPath: pickerItemsList[index].iconPath,
|
||||
tag: pickerItemsList[index].tag,
|
||||
);
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,7 @@ class ExchangeCard<T extends Currency> extends StatefulWidget {
|
||||
this.onPushAddressBookButton,
|
||||
this.onDispose,
|
||||
this.useSatoshis = false,
|
||||
this.onTapCurrencyPicker,
|
||||
required this.cardInstanceName,
|
||||
}) : super(key: key);
|
||||
|
||||
@@ -84,6 +85,7 @@ class ExchangeCard<T extends Currency> extends StatefulWidget {
|
||||
final String cardInstanceName;
|
||||
final Color fillColor;
|
||||
final bool useSatoshis;
|
||||
final void Function(BuildContext context)? onTapCurrencyPicker;
|
||||
|
||||
@override
|
||||
ExchangeCardState<T> createState() => ExchangeCardState<T>();
|
||||
@@ -447,6 +449,10 @@ class ExchangeCardState<T extends Currency> extends State<ExchangeCard<T>> {
|
||||
}
|
||||
|
||||
void _presentPicker(BuildContext context) {
|
||||
if (widget.onTapCurrencyPicker != null) {
|
||||
widget.onTapCurrencyPicker!(context);
|
||||
return;
|
||||
}
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => CurrencyPicker(
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/store/dashboard/trades_store.dart';
|
||||
import 'package:hash_wallet/utils/image_utill.dart';
|
||||
import 'package:hash_wallet/utils/show_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/exchange/trade.dart';
|
||||
|
||||
class ExchangeConfirmPage extends BasePage {
|
||||
ExchangeConfirmPage({required this.tradesStore}) : trade = tradesStore.trade!;
|
||||
|
||||
final TradesStore tradesStore;
|
||||
final Trade trade;
|
||||
|
||||
@override
|
||||
String get title => S.current.swap;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Center(
|
||||
child: Text(
|
||||
S.of(context).exchange_result_write_down_trade_id,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Container(
|
||||
height: 178,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(30)),
|
||||
border: Border.all(
|
||||
width: 0.0,
|
||||
color: Theme.of(context).colorScheme.outlineVariant,
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${trade.provider.title} ${S.of(context).trade_id}",
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
trade.id,
|
||||
overflow: TextOverflow.visible,
|
||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(10, 0, 10, 10),
|
||||
child: Builder(
|
||||
builder: (context) => PrimaryButton(
|
||||
key: ValueKey('exchange_confirm_page_copy_to_clipboard_button_key'),
|
||||
onPressed: () {
|
||||
Clipboard.setData(ClipboardData(text: trade.id));
|
||||
showBar<void>(context, S.of(context).copied_to_clipboard);
|
||||
},
|
||||
text: S.of(context).copy_id,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
textColor: Theme.of(context).colorScheme.onSurface,
|
||||
borderRadius: BorderRadius.circular(30.0),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
if (trade.provider == ExchangeProviderDescription.trocador)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Text(
|
||||
S.of(context).selected_trocador_provider + ':${trade.providerName}',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
(trade.provider.image.isNotEmpty)
|
||||
? ImageUtil.getImageFromPath(
|
||||
imagePath: trade.provider.image, height: 50, width: 50)
|
||||
: const SizedBox(),
|
||||
if (!trade.provider.horizontalLogo)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8.0),
|
||||
child: Text(trade.provider.title),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
PrimaryButton(
|
||||
key: ValueKey('exchange_confirm_page_saved_id_button_key'),
|
||||
onPressed: () => Navigator.of(context).pushReplacementNamed(Routes.exchangeTrade),
|
||||
text: S.of(context).saved_the_trade_id,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
import 'package:hash_wallet/entities/qr_view_data.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/qr_image.dart';
|
||||
import 'package:hash_wallet/utils/brightness_util.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/core/execution_state.dart';
|
||||
import 'package:hash_wallet/view_model/exchange/exchange_trade_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/send/send_view_model_state.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
|
||||
class ExchangeTradeExternalSendPage extends BasePage {
|
||||
ExchangeTradeExternalSendPage({required this.exchangeTradeViewModel});
|
||||
|
||||
final ExchangeTradeViewModel exchangeTradeViewModel;
|
||||
@override
|
||||
String get title => S.current.swap;
|
||||
|
||||
@override
|
||||
bool get gradientAll => true;
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => false;
|
||||
|
||||
@override
|
||||
bool get extendBodyBehindAppBar => true;
|
||||
|
||||
@override
|
||||
AppBarStyle get appBarStyle => AppBarStyle.transparent;
|
||||
|
||||
final fetchingLabel = S.current.fetching;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
final copyImage = Image.asset(
|
||||
'assets/images/copy_content.png',
|
||||
height: 16,
|
||||
width: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
);
|
||||
return Container(
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.only(bottom: 24),
|
||||
content: Observer(
|
||||
builder: (_) {
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
),
|
||||
padding: EdgeInsets.fromLTRB(24, 110, 24, 32),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: <Widget>[
|
||||
Spacer(flex: 3),
|
||||
Flexible(
|
||||
flex: 6,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
BrightnessUtil.changeBrightnessForFunction(
|
||||
() async {
|
||||
await Navigator.pushNamed(
|
||||
context,
|
||||
Routes.fullscreenQR,
|
||||
arguments: QrViewData(
|
||||
embeddedImagePath:
|
||||
exchangeTradeViewModel.trade.from?.iconPath,
|
||||
data: exchangeTradeViewModel.paymentUri?.toString() ??
|
||||
exchangeTradeViewModel.trade.inputAddress ??
|
||||
fetchingLabel,
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 3,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
child: QrImage(
|
||||
data: exchangeTradeViewModel.paymentUri?.toString() ??
|
||||
exchangeTradeViewModel.trade.inputAddress ??
|
||||
fetchingLabel,
|
||||
embeddedImagePath:
|
||||
exchangeTradeViewModel.trade.from?.iconPath,
|
||||
size: 230,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Spacer(flex: 3)
|
||||
],
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
...exchangeTradeViewModel.items
|
||||
.where((item) => item.isExternalSendDetail)
|
||||
.map(
|
||||
(item) => TradeItemRowWidget(
|
||||
key: ValueKey(
|
||||
'exchange_trade_external_send_page_send_item_${item.title}_key'),
|
||||
title: item.title,
|
||||
value: item.data,
|
||||
isCopied: true,
|
||||
copyImage: copyImage,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
bottomSection: Observer(
|
||||
builder: (_) {
|
||||
final trade = exchangeTradeViewModel.trade;
|
||||
final sendingState = exchangeTradeViewModel.sendViewModel.state;
|
||||
|
||||
return exchangeTradeViewModel.isSendable && !(sendingState is TransactionCommitted)
|
||||
? LoadingPrimaryButton(
|
||||
key: ValueKey('exchange_trade_external_send_page_continue_button_key'),
|
||||
isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty,
|
||||
isLoading: sendingState is IsExecutingState,
|
||||
onPressed: () {
|
||||
Navigator.of(context).popUntil((route) => route.isFirst);
|
||||
},
|
||||
text: S.current.continue_text,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
)
|
||||
: Offstage();
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,424 +0,0 @@
|
||||
import 'package:hash_wallet/entities/parsed_address.dart';
|
||||
import 'package:hash_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:hash_wallet/reactions/wallet_connect.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/connect_device/connect_device_page.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:hash_wallet/utils/request_review_handler.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/view_model/send/output.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/core/execution_state.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange_trade/information_page.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/exchange/exchange_trade_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/send/send_view_model_state.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange_trade/widgets/timer_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
|
||||
void showInformation(ExchangeTradeViewModel exchangeTradeViewModel, BuildContext context) {
|
||||
final trade = exchangeTradeViewModel.trade;
|
||||
final walletName = exchangeTradeViewModel.wallet.name;
|
||||
|
||||
final from = trade.from?.toString() ?? '';
|
||||
|
||||
final information = exchangeTradeViewModel.isSendable
|
||||
? S.current.exchange_trade_result_confirm(trade.amount, from, walletName) +
|
||||
exchangeTradeViewModel.extraInfo
|
||||
: S.current.exchange_result_description(trade.amount, from) +
|
||||
exchangeTradeViewModel.extraInfo;
|
||||
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (_) => InformationPage(
|
||||
key: ValueKey('information_page_dialog_key'),
|
||||
information: information,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class ExchangeTradePage extends BasePage {
|
||||
ExchangeTradePage({required this.exchangeTradeViewModel});
|
||||
|
||||
final ExchangeTradeViewModel exchangeTradeViewModel;
|
||||
|
||||
@override
|
||||
String get title => S.current.swap;
|
||||
|
||||
@override
|
||||
bool get gradientAll => true;
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => false;
|
||||
|
||||
@override
|
||||
bool get extendBodyBehindAppBar => true;
|
||||
|
||||
@override
|
||||
AppBarStyle get appBarStyle => AppBarStyle.transparent;
|
||||
|
||||
@override
|
||||
Widget trailing(BuildContext context) {
|
||||
final questionImage = Image.asset('assets/images/question_mark.png',
|
||||
color: Theme.of(context).colorScheme.onSurface);
|
||||
|
||||
return SizedBox(
|
||||
height: 20.0,
|
||||
width: 20.0,
|
||||
child: ButtonTheme(
|
||||
minWidth: double.minPositive,
|
||||
child: TextButton(
|
||||
// FIX-ME: Style
|
||||
//highlightColor: Colors.transparent,
|
||||
//splashColor: Colors.transparent,
|
||||
//padding: EdgeInsets.all(0),
|
||||
onPressed: () => showInformation(exchangeTradeViewModel, context),
|
||||
child: questionImage),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => ExchangeTradeForm(exchangeTradeViewModel);
|
||||
}
|
||||
|
||||
class ExchangeTradeForm extends StatefulWidget {
|
||||
ExchangeTradeForm(this.exchangeTradeViewModel);
|
||||
|
||||
final ExchangeTradeViewModel exchangeTradeViewModel;
|
||||
|
||||
@override
|
||||
ExchangeTradeState createState() => ExchangeTradeState();
|
||||
}
|
||||
|
||||
class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||
final fetchingLabel = S.current.fetching;
|
||||
|
||||
String get title => S.current.exchange;
|
||||
|
||||
bool _effectsInstalled = false;
|
||||
|
||||
ReactionDisposer? _exchangeStateReaction;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback(afterLayout);
|
||||
}
|
||||
|
||||
void afterLayout(dynamic _) {
|
||||
showInformation(widget.exchangeTradeViewModel, context);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.exchangeTradeViewModel.timer?.cancel();
|
||||
_exchangeStateReaction?.reaction.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
_setEffects();
|
||||
|
||||
return Container(
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.only(bottom: 16),
|
||||
content: Observer(builder: (_) {
|
||||
final trade = widget.exchangeTradeViewModel.trade;
|
||||
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
trade.expiredAt != null
|
||||
? Row(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
S.of(context).offer_expires_in,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
if (trade.expiredAt != null)
|
||||
TimerWidget(trade.expiredAt!,
|
||||
color: Theme.of(context).colorScheme.onSurface)
|
||||
])
|
||||
: Offstage(),
|
||||
_ExchangeTradeItemsCardSection(
|
||||
viewModel: widget.exchangeTradeViewModel,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24),
|
||||
bottomSection: Column(
|
||||
children: [
|
||||
if (!widget.exchangeTradeViewModel.shouldHideExternalSendButton)
|
||||
PrimaryButton(
|
||||
key: ValueKey('exchange_trade_page_send_from_external_button_key'),
|
||||
text: S.current.send_from_external_wallet,
|
||||
onPressed: () async {
|
||||
Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage);
|
||||
},
|
||||
color: widget.exchangeTradeViewModel.isSendable
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
textColor: widget.exchangeTradeViewModel.isSendable
|
||||
? Theme.of(context).colorScheme.onSecondaryContainer
|
||||
: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
final trade = widget.exchangeTradeViewModel.trade;
|
||||
final sendingState = widget.exchangeTradeViewModel.sendViewModel.state;
|
||||
|
||||
return Offstage(
|
||||
offstage: !(widget.exchangeTradeViewModel.isSendable &&
|
||||
!(sendingState is TransactionCommitted)),
|
||||
child: LoadingPrimaryButton(
|
||||
key: ValueKey('exchange_trade_page_send_from_cake_button_key'),
|
||||
isDisabled: trade.inputAddress == null ||
|
||||
trade.inputAddress!.isEmpty ||
|
||||
sendingState is ExecutedSuccessfullyState,
|
||||
isLoading: sendingState is IsExecutingState,
|
||||
onPressed: _onPressedSendFromCakeWallet,
|
||||
text: S.current.send_from_cake_wallet,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onPressedSendFromCakeWallet() async {
|
||||
final sendVM = widget.exchangeTradeViewModel.sendViewModel;
|
||||
|
||||
if (sendVM.wallet.isHardwareWallet) {
|
||||
if (!sendVM.hardwareWalletViewModel!.isConnected) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: sendVM.walletType,
|
||||
hardwareWalletType: sendVM.wallet.walletInfo.hardwareWalletType!,
|
||||
onConnectDevice: (context, _) {
|
||||
sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
));
|
||||
} else {
|
||||
sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet);
|
||||
}
|
||||
}
|
||||
|
||||
widget.exchangeTradeViewModel.confirmSending();
|
||||
}
|
||||
|
||||
BuildContext? dialogContext;
|
||||
BuildContext? loadingBottomSheetContext;
|
||||
|
||||
void _setEffects() {
|
||||
if (_effectsInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
_exchangeStateReaction = reaction(
|
||||
(_) => this.widget.exchangeTradeViewModel.sendViewModel.state,
|
||||
(ExecutionState state) async {
|
||||
if (dialogContext != null && dialogContext?.mounted == true) {
|
||||
Navigator.of(dialogContext!).pop();
|
||||
}
|
||||
|
||||
if (state is! IsExecutingState &&
|
||||
loadingBottomSheetContext != null &&
|
||||
loadingBottomSheetContext!.mounted) {
|
||||
Navigator.of(loadingBottomSheetContext!).pop();
|
||||
}
|
||||
|
||||
if (state is FailureState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext popupContext) {
|
||||
return AlertWithOneAction(
|
||||
key: ValueKey('exchange_trade_page_send_failure_dialog_key'),
|
||||
buttonKey: ValueKey('exchange_trade_page_send_failure_dialog_button_key'),
|
||||
alertTitle: S.of(popupContext).error,
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(popupContext).ok,
|
||||
buttonAction: () => Navigator.of(popupContext).pop());
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (state is IsExecutingState) {
|
||||
// wait a bit to avoid showing the loading dialog if transaction is failed
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
final currentState = widget.exchangeTradeViewModel.sendViewModel.state;
|
||||
if (currentState is ExecutedSuccessfullyState || currentState is FailureState) {
|
||||
return;
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
loadingBottomSheetContext = context;
|
||||
return LoadingBottomSheet(
|
||||
titleText: S.of(context).generating_transaction,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state is ExecutedSuccessfullyState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
final trade = widget.exchangeTradeViewModel.trade;
|
||||
|
||||
final isSwapsXyz = trade.provider == ExchangeProviderDescription.swapsXyz;
|
||||
final isEVMWallet = widget.exchangeTradeViewModel.sendViewModel.isEVMWallet;
|
||||
|
||||
final amountValue = isSwapsXyz &&
|
||||
isEVMWallet &&
|
||||
!widget.exchangeTradeViewModel.isSwapsXYZCanSendFromExternal
|
||||
? trade.amount
|
||||
: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.amountFormatted;
|
||||
|
||||
final fiatAmountValue = isSwapsXyz &&
|
||||
isEVMWallet &&
|
||||
!widget.exchangeTradeViewModel.isSwapsXYZCanSendFromExternal
|
||||
? widget.exchangeTradeViewModel.sendViewModel
|
||||
.calculateTransactionFiatAmount(amountValue)
|
||||
: widget.exchangeTradeViewModel.sendViewModel.pendingTransactionFiatAmountFormatted;
|
||||
|
||||
if (context.mounted) {
|
||||
final result = await showModalBottomSheet<bool>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
isScrollControlled: true,
|
||||
builder: (bottomSheetContext) {
|
||||
final sendVM = widget.exchangeTradeViewModel.sendViewModel;
|
||||
|
||||
return ConfirmSendingBottomSheet(
|
||||
key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'),
|
||||
footerType: FooterType.slideActionButton,
|
||||
isSlideActionEnabled: sendVM.isReadyForSend,
|
||||
walletType: sendVM.walletType,
|
||||
titleText: S.of(bottomSheetContext).confirm_transaction,
|
||||
titleIconPath: sendVM.selectedCryptoCurrency.iconPath,
|
||||
currency: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency,
|
||||
amount: S.of(bottomSheetContext).send_amount,
|
||||
amountValue: sendVM.amountParsingProxy
|
||||
.getDisplayCryptoAmount(amountValue, sendVM.selectedCryptoCurrency),
|
||||
fiatAmountValue: fiatAmountValue,
|
||||
fee: isEVMCompatibleChain(sendVM.walletType)
|
||||
? S.of(bottomSheetContext).send_estimated_fee
|
||||
: S.of(bottomSheetContext).send_fee,
|
||||
feeValue:
|
||||
"${sendVM.amountParsingProxy.getDisplayCryptoAmount(sendVM.pendingTransaction!.feeFormattedValue, sendVM.selectedCryptoCurrency)} ${sendVM.amountParsingProxy.getCryptoSymbol(sendVM.wallet.currency)}",
|
||||
feeFiatAmount: sendVM.pendingTransactionFeeFiatAmountFormatted,
|
||||
outputs: sendVM.outputs,
|
||||
onSlideActionComplete: () async {
|
||||
if (bottomSheetContext.mounted && Navigator.canPop(bottomSheetContext))
|
||||
Navigator.of(bottomSheetContext).pop(true);
|
||||
|
||||
sendVM.commitTransaction(context);
|
||||
},
|
||||
amountParsingProxy: sendVM.amountParsingProxy,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
if (result == null) widget.exchangeTradeViewModel.sendViewModel.dismissTransaction();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state is TransactionCommitted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) async {
|
||||
if (!mounted) return;
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext bottomSheetContext) {
|
||||
return InfoBottomSheet(
|
||||
footerType: FooterType.singleActionButton,
|
||||
titleText: S.of(bottomSheetContext).transaction_sent,
|
||||
contentImage: 'assets/images/hash_bag.png',
|
||||
singleActionButtonText: S.of(bottomSheetContext).close,
|
||||
singleActionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
|
||||
onSingleActionButtonPressed: () {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
if (mounted) {
|
||||
Navigator.of(context).pushNamedAndRemoveUntil(
|
||||
Routes.dashboard,
|
||||
(route) => false,
|
||||
);
|
||||
}
|
||||
RequestReviewHandler.requestReview();
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
_effectsInstalled = true;
|
||||
}
|
||||
}
|
||||
|
||||
class _ExchangeTradeItemsCardSection extends StatelessWidget {
|
||||
const _ExchangeTradeItemsCardSection({required this.viewModel});
|
||||
|
||||
final ExchangeTradeViewModel viewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final firstExchangeCard = ExchangeTradeCardItemWidget(
|
||||
isReceiveDetailsCard: true,
|
||||
exchangeTradeViewModel: viewModel,
|
||||
);
|
||||
|
||||
final secondExchangeCard = ExchangeTradeCardItemWidget(
|
||||
isReceiveDetailsCard: false,
|
||||
exchangeTradeViewModel: viewModel,
|
||||
);
|
||||
|
||||
if (responsiveLayoutUtil.shouldRenderMobileUI) {
|
||||
return MobileExchangeCardsSection(
|
||||
firstExchangeCard: firstExchangeCard,
|
||||
secondExchangeCard: secondExchangeCard,
|
||||
);
|
||||
}
|
||||
|
||||
return DesktopExchangeCardsSection(
|
||||
firstExchangeCard: firstExchangeCard,
|
||||
secondExchangeCard: secondExchangeCard,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_background.dart';
|
||||
|
||||
class InformationPage extends StatelessWidget {
|
||||
InformationPage({required this.information, super.key});
|
||||
|
||||
final String information;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return AlertBackground(
|
||||
child: Center(
|
||||
child: Container(
|
||||
margin: EdgeInsets.only(left: 24, right: 24),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(30)),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Container(
|
||||
padding: EdgeInsets.fromLTRB(24, 28, 24, 24),
|
||||
child: Text(
|
||||
information,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(10, 0, 10, 10),
|
||||
child: PrimaryButton(
|
||||
key: ValueKey('information_page_got_it_button_key'),
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
text: S.of(context).got_it,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1,290 +0,0 @@
|
||||
import 'package:hash_wallet/entities/priority_for_wallet_type.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:hash_wallet/src/widgets/list_row.dart';
|
||||
import 'package:hash_wallet/src/widgets/picker.dart';
|
||||
import 'package:hash_wallet/utils/show_bar.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/exchange/exchange_trade_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/send/fees_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/send/output.dart';
|
||||
import 'package:cw_core/transaction_priority.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class ExchangeTradeCardItemWidget extends StatelessWidget {
|
||||
ExchangeTradeCardItemWidget({
|
||||
required this.isReceiveDetailsCard,
|
||||
required this.exchangeTradeViewModel,
|
||||
Key? key,
|
||||
}) : feesViewModel = exchangeTradeViewModel.feesViewModel,
|
||||
output = exchangeTradeViewModel.output;
|
||||
|
||||
final Output output;
|
||||
final bool isReceiveDetailsCard;
|
||||
final FeesViewModel feesViewModel;
|
||||
final ExchangeTradeViewModel exchangeTradeViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final copyImage = Image.asset(
|
||||
'assets/images/copy_content.png',
|
||||
height: 16,
|
||||
width: 16,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
);
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
color: Colors.transparent,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
SizedBox(height: 10),
|
||||
...exchangeTradeViewModel.items
|
||||
.where((item) => item.isReceiveDetail == isReceiveDetailsCard)
|
||||
.map(
|
||||
(item) => TradeItemRowWidget(
|
||||
title: item.title,
|
||||
value: item.data,
|
||||
isCopied: item.isCopied,
|
||||
copyImage: copyImage,
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
if (!isReceiveDetailsCard && exchangeTradeViewModel.isSendable) ...[
|
||||
if (feesViewModel.hasFees)
|
||||
FeeSelectionWidget(
|
||||
feesViewModel: feesViewModel,
|
||||
output: output,
|
||||
onTap: () => pickTransactionPriority(context, output),
|
||||
),
|
||||
if (exchangeTradeViewModel.sendViewModel.hasCoinControl)
|
||||
CoinControlWidget(
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
Routes.unspentCoinsList,
|
||||
arguments: exchangeTradeViewModel.sendViewModel.coinTypeToSpendFrom,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> pickTransactionPriority(BuildContext context, Output output) async {
|
||||
final items = priorityForWalletType(feesViewModel.walletType);
|
||||
final selectedItem = items.indexOf(feesViewModel.transactionPriority);
|
||||
final customItemIndex = feesViewModel.getCustomPriorityIndex(items);
|
||||
final isBitcoinWallet = feesViewModel.walletType == WalletType.bitcoin;
|
||||
final maxCustomFeeRate = feesViewModel.maxCustomFeeRate?.toDouble();
|
||||
double? customFeeRate = isBitcoinWallet ? feesViewModel.customBitcoinFeeRate.toDouble() : null;
|
||||
|
||||
await showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
int selectedIdx = selectedItem;
|
||||
return StatefulBuilder(
|
||||
builder: (BuildContext context, StateSetter setState) {
|
||||
return Picker(
|
||||
items: items,
|
||||
displayItem: (TransactionPriority priority) =>
|
||||
feesViewModel.displayFeeRate(priority, customFeeRate?.round()),
|
||||
selectedAtIndex: selectedIdx,
|
||||
customItemIndex: customItemIndex,
|
||||
maxValue: maxCustomFeeRate,
|
||||
title: S.of(context).please_select,
|
||||
headerEnabled: !isBitcoinWallet,
|
||||
closeOnItemSelected: !isBitcoinWallet,
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
sliderValue: customFeeRate,
|
||||
onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue),
|
||||
onItemSelected: (TransactionPriority priority) async {
|
||||
feesViewModel.setTransactionPriority(priority);
|
||||
setState(() => selectedIdx = items.indexOf(priority));
|
||||
await output.calculateEstimatedFee();
|
||||
if (feesViewModel.isLowFee) {
|
||||
_showFeeAlert(context);
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
if (isBitcoinWallet) {
|
||||
feesViewModel.customBitcoinFeeRate = customFeeRate!.round();
|
||||
if (feesViewModel.showAlertForCustomFeeRate()) {
|
||||
_showFeeAlert(context);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _showFeeAlert(BuildContext context) async {
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (dialogContext) {
|
||||
return AlertWithTwoActions(
|
||||
alertTitle: S.of(context).low_fee,
|
||||
alertContent: S.of(context).low_fee_alert,
|
||||
leftButtonText: S.of(context).ignor,
|
||||
rightButtonText: S.of(context).use_suggested,
|
||||
actionLeftButton: () => Navigator.of(dialogContext).pop(false),
|
||||
actionRightButton: () => Navigator.of(dialogContext).pop(true));
|
||||
}) ??
|
||||
false;
|
||||
if (confirmed) {
|
||||
feesViewModel.setDefaultTransactionPriority();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class TradeItemRowWidget extends StatelessWidget {
|
||||
final String title;
|
||||
final String value;
|
||||
final bool isCopied;
|
||||
final Image copyImage;
|
||||
|
||||
const TradeItemRowWidget({
|
||||
required this.title,
|
||||
required this.value,
|
||||
required this.isCopied,
|
||||
required this.copyImage,
|
||||
super.key,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
if (!isCopied) return;
|
||||
Clipboard.setData(ClipboardData(text: value));
|
||||
showBar<void>(context, S.of(context).transaction_details_copied(title));
|
||||
},
|
||||
child: ListRow(
|
||||
padding: EdgeInsets.zero,
|
||||
title: title,
|
||||
value: value,
|
||||
image: isCopied ? copyImage : null,
|
||||
color: Colors.transparent,
|
||||
hintTextColor: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
mainTextColor: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class FeeSelectionWidget extends StatelessWidget {
|
||||
final FeesViewModel feesViewModel;
|
||||
final Output output;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const FeeSelectionWidget({
|
||||
required this.feesViewModel,
|
||||
required this.output,
|
||||
required this.onTap,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Observer(
|
||||
builder: (_) => GestureDetector(
|
||||
key: ValueKey('exchange_trade_card_item_widget_select_fee_priority_button_key'),
|
||||
onTap: feesViewModel.hasFeesPriority ? onTap : () {},
|
||||
child: Container(
|
||||
padding: EdgeInsets.only(top: 24),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
S.of(context).send_estimated_fee,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
'${output.estimatedFee} ${feesViewModel.amountParsingProxy.getCryptoSymbol(feesViewModel.currency)}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
if (!feesViewModel.isFiatDisabled)
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 5),
|
||||
child: Text(
|
||||
'${output.estimatedFeeFiatAmount} ${feesViewModel.fiat.title}',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 2, left: 5),
|
||||
child: Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class CoinControlWidget extends StatelessWidget {
|
||||
final VoidCallback onTap;
|
||||
|
||||
const CoinControlWidget({required this.onTap});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
key: ValueKey('exchange_trade_card_item_widget_unspent_coin_button_key'),
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
color: Colors.transparent,
|
||||
padding: EdgeInsets.only(top: 6),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
S.of(context).coin_control,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
size: 12,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,79 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'dart:async';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
|
||||
class TimerWidget extends StatefulWidget {
|
||||
TimerWidget(this.expiratedAt, {this.color = Colors.black});
|
||||
|
||||
final DateTime expiratedAt;
|
||||
final Color color;
|
||||
|
||||
@override
|
||||
TimerWidgetState createState() => TimerWidgetState();
|
||||
}
|
||||
|
||||
class TimerWidgetState extends State<TimerWidget> {
|
||||
TimerWidgetState()
|
||||
: _leftSeconds = 0,
|
||||
_minutes = 0,
|
||||
_seconds = 0,
|
||||
_isExpired = false;
|
||||
|
||||
int _leftSeconds;
|
||||
int _minutes;
|
||||
int _seconds;
|
||||
bool _isExpired;
|
||||
Timer? _timer;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
final start = DateTime.now();
|
||||
_isExpired = false;
|
||||
_leftSeconds = widget.expiratedAt.difference(start).inSeconds;
|
||||
_recalculate();
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_timer = Timer.periodic(Duration(seconds: 1), (timer) {
|
||||
if (_isExpired) {
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
_leftSeconds--;
|
||||
_isExpired = _leftSeconds <= 0;
|
||||
_recalculate();
|
||||
setState(() {});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _isExpired
|
||||
? Text(
|
||||
S.of(context).expired,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
S.of(context).time(_minutes.toString(), _seconds.toString()),
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: widget.color,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _recalculate() {
|
||||
_minutes = _leftSeconds ~/ 60;
|
||||
_seconds = _leftSeconds % 60;
|
||||
}
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import 'dart:convert';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/faq/faq_item.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:hash_wallet/store/settings_store.dart';
|
||||
|
||||
class FaqPage extends BasePage {
|
||||
FaqPage(this.settingsStore);
|
||||
|
||||
final SettingsStore settingsStore;
|
||||
|
||||
@override
|
||||
String get title => S.current.faq;
|
||||
|
||||
String get path => 'assets/faq/faq_' + settingsStore.languageCode + '.json';
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return Container(
|
||||
padding: EdgeInsets.only(top: 12, left: 24),
|
||||
child: FutureBuilder(
|
||||
builder: (context, snapshot) {
|
||||
final faqItems = jsonDecode(snapshot.data.toString()) as List;
|
||||
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
StandardListSeparator(),
|
||||
ListView.separated(
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
itemBuilder: (BuildContext context, int index) {
|
||||
final title = faqItems[index]["question"].toString();
|
||||
final text = faqItems[index]["answer"].toString();
|
||||
|
||||
return FAQItem(title, text);
|
||||
},
|
||||
separatorBuilder: (_, __) => StandardListSeparator(),
|
||||
itemCount: faqItems?.length ?? 0,
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
future: rootBundle.loadString(path),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -58,7 +58,8 @@ class DEuroSavingsPage extends BasePage {
|
||||
Observer(
|
||||
builder: (_) => SavingsCard(
|
||||
interestRate: "${_dEuroViewModel.interestRateFormated}%",
|
||||
savingsBalance: _dEuroViewModel.savingsBalanceFormated,
|
||||
savingsBalance:
|
||||
_dEuroViewModel.savingsBalance.toStringWithPrecision(fractionalDigits: 6),
|
||||
fiatSavingsBalance: _dEuroViewModel.fiatSavingsBalanceFormated,
|
||||
currency: CryptoCurrency.deuro,
|
||||
fiatCurrency: _dEuroViewModel.isFiatDisabled ? null : _dEuroViewModel.fiat,
|
||||
@@ -68,6 +69,10 @@ class DEuroSavingsPage extends BasePage {
|
||||
onTooltipPressed: () => _onSavingsTooltipPressed(context),
|
||||
isEnabled: _dEuroViewModel.isEnabled,
|
||||
isLoading: _dEuroViewModel.isLoading,
|
||||
onWithdrawV1Pressed: () => _onWithdrawV1(context),
|
||||
savingsBalanceV1: _dEuroViewModel.savingsBalanceV1
|
||||
?.toStringWithPrecision(fractionalDigits: 6),
|
||||
fiatSavingsBalanceV1: _dEuroViewModel.fiatSavingsBalanceV1Formated,
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
@@ -126,6 +131,16 @@ class DEuroSavingsPage extends BasePage {
|
||||
_editSheetIsOpen = false;
|
||||
}
|
||||
|
||||
Future<void> _onWithdrawV1(BuildContext context) async {
|
||||
if (_editSheetIsOpen) return;
|
||||
_editSheetIsOpen = true;
|
||||
try {
|
||||
await _requireHardwareWallet(context);
|
||||
await _dEuroViewModel.prepareSavingsV1Withdraw();
|
||||
} catch (_) {}
|
||||
_editSheetIsOpen = false;
|
||||
}
|
||||
|
||||
Future<void> _onSavingsRemove(BuildContext context) async {
|
||||
if (_editSheetIsOpen) return;
|
||||
_editSheetIsOpen = true;
|
||||
@@ -160,7 +175,7 @@ class DEuroSavingsPage extends BasePage {
|
||||
|
||||
Future<void> _requireHardwareWallet(BuildContext context) async {
|
||||
if (_dEuroViewModel.wallet.isHardwareWallet) {
|
||||
if (!_dEuroViewModel.hardwareWalletViewModel!.isConnected) {
|
||||
if (!_dEuroViewModel.hardwareWalletViewModel!.isConnected(_dEuroViewModel.wallet.type)) {
|
||||
await Navigator.of(context).pushNamed(Routes.connectDevices,
|
||||
arguments: ConnectDevicePageParams(
|
||||
walletType: _dEuroViewModel.wallet.type,
|
||||
@@ -215,7 +230,7 @@ class DEuroSavingsPage extends BasePage {
|
||||
amount: S.of(bottomSheetContext).send_amount,
|
||||
amountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest
|
||||
? _dEuroViewModel.accruedInterestFormated
|
||||
: tx.amountFormatted,
|
||||
: tx.amount.toStringWithSymbol(),
|
||||
fiatAmountValue: _dEuroViewModel.actionType == DEuroActionType.reinvest
|
||||
? _dEuroViewModel.fiatAccruedInterestFormated
|
||||
: _dEuroViewModel.pendingTransactionFiatAmountFormatted,
|
||||
@@ -251,7 +266,7 @@ class DEuroSavingsPage extends BasePage {
|
||||
amountValue: tx.amountFormatted,
|
||||
fiatAmountValue: "",
|
||||
fee: S.of(bottomSheetContext).send_estimated_fee,
|
||||
feeValue: tx.feeFormatted,
|
||||
feeValue: tx.fee.toStringWithSymbol(),
|
||||
feeFiatAmount: "",
|
||||
outputs: [],
|
||||
onSlideActionComplete: () {
|
||||
@@ -410,9 +425,7 @@ class DEuroSavingsPage extends BasePage {
|
||||
balanceTitle: isAdding
|
||||
? S.of(context).deuro_savings_available_to_add
|
||||
: S.of(context).deuro_savings_available_to_remove,
|
||||
balance: isAdding
|
||||
? _dEuroViewModel.accountBalanceFormated
|
||||
: _dEuroViewModel.savingsBalanceFormated,
|
||||
balance: isAdding ? _dEuroViewModel.accountBalance : _dEuroViewModel.savingsBalance,
|
||||
footerType: FooterType.none,
|
||||
maxHeight: MediaQuery.of(context).size.height * 0.8,
|
||||
),
|
||||
|
||||
@@ -23,6 +23,9 @@ class SavingsCard extends StatelessWidget {
|
||||
this.isLoading = false,
|
||||
this.fiatSavingsBalance,
|
||||
this.fiatCurrency,
|
||||
this.savingsBalanceV1,
|
||||
this.fiatSavingsBalanceV1,
|
||||
this.onWithdrawV1Pressed,
|
||||
});
|
||||
|
||||
final bool isEnabled;
|
||||
@@ -30,8 +33,11 @@ class SavingsCard extends StatelessWidget {
|
||||
final String interestRate;
|
||||
final String savingsBalance;
|
||||
final String? fiatSavingsBalance;
|
||||
final String? savingsBalanceV1;
|
||||
final String? fiatSavingsBalanceV1;
|
||||
final FiatCurrency? fiatCurrency;
|
||||
final CryptoCurrency currency;
|
||||
final VoidCallback? onWithdrawV1Pressed;
|
||||
final VoidCallback onAddSavingsPressed;
|
||||
final VoidCallback onRemoveSavingsPressed;
|
||||
final VoidCallback onApproveSavingsPressed;
|
||||
@@ -98,46 +104,72 @@ class SavingsCard extends StatelessWidget {
|
||||
onTooltipPressed: onTooltipPressed,
|
||||
),
|
||||
),
|
||||
if (savingsBalanceV1 != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 20),
|
||||
child: getAssetBalanceRow(
|
||||
context,
|
||||
title: "${S.of(context).deuro_savings_balance} V1",
|
||||
amount: savingsBalanceV1!,
|
||||
fiatAmount: fiatSavingsBalanceV1,
|
||||
currency: currency,
|
||||
fiatCurrency: fiatCurrency,
|
||||
hideSymbol: true,
|
||||
),
|
||||
),
|
||||
isLoading
|
||||
? CupertinoActivityIndicator(color: Theme.of(context).colorScheme.onSurface)
|
||||
: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: isEnabled
|
||||
children: savingsBalanceV1 != null
|
||||
? [
|
||||
Expanded(
|
||||
child: getButton(
|
||||
context,
|
||||
label: S.of(context).deuro_savings_add,
|
||||
imagePath: 'assets/images/received.png',
|
||||
onPressed: onAddSavingsPressed,
|
||||
label: "${S.of(context).deuro_savings_remove} V1",
|
||||
onPressed: onWithdrawV1Pressed ?? () {},
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: getButton(
|
||||
context,
|
||||
label: S.of(context).deuro_savings_remove,
|
||||
imagePath: 'assets/images/upload.png',
|
||||
onPressed: onRemoveSavingsPressed,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
]
|
||||
: [
|
||||
Expanded(
|
||||
child: getButton(
|
||||
context,
|
||||
label: S.of(context).deuro_savings_approve_app,
|
||||
onPressed: onApproveSavingsPressed,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
icon: Icons.check,
|
||||
icon: Icons.arrow_downward,
|
||||
),
|
||||
)
|
||||
],
|
||||
]
|
||||
: isEnabled
|
||||
? [
|
||||
Expanded(
|
||||
child: getButton(
|
||||
context,
|
||||
label: S.of(context).deuro_savings_add,
|
||||
imagePath: 'assets/images/received.png',
|
||||
onPressed: onAddSavingsPressed,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: getButton(
|
||||
context,
|
||||
label: S.of(context).deuro_savings_remove,
|
||||
imagePath: 'assets/images/upload.png',
|
||||
onPressed: onRemoveSavingsPressed,
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
]
|
||||
: [
|
||||
Expanded(
|
||||
child: getButton(
|
||||
context,
|
||||
label: S.of(context).deuro_savings_approve_app,
|
||||
onPressed: onApproveSavingsPressed,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
icon: Icons.check,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -264,33 +296,33 @@ class SavingsCard extends StatelessWidget {
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
height: 40,
|
||||
width: 40,
|
||||
errorWidget: Container(
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
currency.title.substring(0, min(currency.title.length, 2)),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
if (!hideSymbol)
|
||||
SizedBox(
|
||||
child: Center(
|
||||
child: Column(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
imageUrl: currency.iconPath,
|
||||
height: 40,
|
||||
width: 40,
|
||||
errorWidget: Container(
|
||||
height: 30.0,
|
||||
width: 30.0,
|
||||
child: Center(
|
||||
child: Text(
|
||||
currency.title.substring(0, min(currency.title.length, 2)),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontSize: 11,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (!hideSymbol) ...[
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
currency.title,
|
||||
@@ -301,11 +333,10 @@ class SavingsCard extends StatelessWidget {
|
||||
height: 1,
|
||||
),
|
||||
),
|
||||
]
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,11 @@ import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/integrations/deuro/widgets/numpad.dart';
|
||||
import 'package:hash_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cw_core/parse_fixed.dart';
|
||||
import 'package:cw_core/amount/money.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class SavingsEditSheet extends BaseBottomSheet {
|
||||
final String? balance;
|
||||
final Money? balance;
|
||||
final String? balanceTitle;
|
||||
|
||||
const SavingsEditSheet({
|
||||
@@ -32,7 +32,7 @@ class SavingsEditSheet extends BaseBottomSheet {
|
||||
}
|
||||
|
||||
class _SavingsEditBody extends StatefulWidget {
|
||||
final String? balance;
|
||||
final Money? balance;
|
||||
final String? balanceTitle;
|
||||
|
||||
const _SavingsEditBody({this.balance, this.balanceTitle});
|
||||
@@ -59,7 +59,7 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
|
||||
final FocusNode _numpadFocusNode = FocusNode();
|
||||
|
||||
void _onPressedAll() => setState(() {
|
||||
amount = widget.balance!;
|
||||
amount = widget.balance!.toString();
|
||||
isValid = _validate();
|
||||
});
|
||||
|
||||
@@ -79,9 +79,12 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
|
||||
});
|
||||
|
||||
bool _validate() {
|
||||
final amountBigInt = parseFixed(amount, 18);
|
||||
final balanceBigInt = parseFixed(widget.balance!, 18);
|
||||
return balanceBigInt >= amountBigInt && amountBigInt > BigInt.zero;
|
||||
try {
|
||||
final amountBigInt = Money.parse(amount, widget.balance!.currency);
|
||||
return widget.balance! >= amountBigInt && amountBigInt.sign > 0;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -108,7 +111,7 @@ class _SavingsEditBodyState extends State<_SavingsEditBody> {
|
||||
Divider(),
|
||||
AssetBalanceRow(
|
||||
title: widget.balanceTitle!,
|
||||
amount: widget.balance!,
|
||||
amount: widget.balance!.toStringWithSymbol(fractionalDigits: 6),
|
||||
onAllPressed: _onPressedAll,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -1,69 +0,0 @@
|
||||
import 'package:hash_wallet/core/execution_state.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/core/monero_account_label_validator.dart';
|
||||
import 'package:hash_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
|
||||
|
||||
class MoneroAccountEditOrCreatePage extends BasePage {
|
||||
MoneroAccountEditOrCreatePage({required this.moneroAccountCreationViewModel})
|
||||
: _formKey = GlobalKey<FormState>(),
|
||||
_textController = TextEditingController() {
|
||||
_textController.addListener(() => moneroAccountCreationViewModel.label = _textController.text);
|
||||
_textController.text = moneroAccountCreationViewModel.label;
|
||||
}
|
||||
|
||||
final MoneroAccountEditOrCreateViewModel moneroAccountCreationViewModel;
|
||||
|
||||
@override
|
||||
String get title => S.current.account;
|
||||
|
||||
final GlobalKey<FormState> _formKey;
|
||||
final TextEditingController _textController;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => Form(
|
||||
key: _formKey,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: BaseTextFormField(
|
||||
controller: _textController,
|
||||
hintText: S.of(context).account,
|
||||
validator: MoneroLabelValidator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => LoadingPrimaryButton(
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await moneroAccountCreationViewModel.save();
|
||||
|
||||
if (context.mounted && Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop(_textController.text);
|
||||
}
|
||||
},
|
||||
text: moneroAccountCreationViewModel.isEdit
|
||||
? S.of(context).rename
|
||||
: S.of(context).add,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: moneroAccountCreationViewModel.state is IsExecutingState,
|
||||
isDisabled: moneroAccountCreationViewModel.label?.isEmpty ?? true,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
import 'package:hash_wallet/src/widgets/picker_inner_wrapper_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/section_divider.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/view_model/monero_account_list/monero_account_list_view_model.dart';
|
||||
import 'package:hash_wallet/src/screens/monero_accounts/widgets/account_tile.dart';
|
||||
|
||||
class MoneroAccountListPage extends StatelessWidget {
|
||||
MoneroAccountListPage({required this.accountListViewModel});
|
||||
|
||||
final MoneroAccountListViewModel accountListViewModel;
|
||||
final ScrollController controller = ScrollController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double itemHeight = 65;
|
||||
double buttonHeight = 62;
|
||||
|
||||
return Observer(builder: (_) {
|
||||
final accounts = accountListViewModel.accounts;
|
||||
|
||||
return PickerInnerWrapperWidget(
|
||||
title: S.of(context).choose_account,
|
||||
itemsHeight: (itemHeight * accounts.length) + buttonHeight,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
controller: controller,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
controller: controller,
|
||||
separatorBuilder: (context, index) => const HorizontalSectionDivider(),
|
||||
itemCount: accounts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final account = accounts[index];
|
||||
|
||||
return AccountTile(
|
||||
isCurrent: account.isSelected,
|
||||
accountName: account.label,
|
||||
accountBalance: account.balance ?? '0.00',
|
||||
currency: accountListViewModel.currency.toString(),
|
||||
onTap: () {
|
||||
if (account.isSelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
accountListViewModel.select(account);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
onEdit: () async => await Navigator.of(context).pushNamed(
|
||||
Routes.accountCreation,
|
||||
arguments: account,
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)),
|
||||
GestureDetector(
|
||||
onTap: () async => await Navigator.of(context).pushNamed(Routes.accountCreation),
|
||||
child: Container(
|
||||
height: buttonHeight,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 5),
|
||||
child: Text(
|
||||
S.of(context).create_new_account,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
|
||||
class AccountTile extends StatelessWidget {
|
||||
AccountTile({
|
||||
required this.isCurrent,
|
||||
required this.accountName,
|
||||
this.accountBalance,
|
||||
required this.currency,
|
||||
required this.onTap,
|
||||
required this.onEdit,
|
||||
});
|
||||
|
||||
final bool isCurrent;
|
||||
final String accountName;
|
||||
final String? accountBalance;
|
||||
final String currency;
|
||||
final Function() onTap;
|
||||
final Function() onEdit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = isCurrent
|
||||
? Theme.of(context).colorScheme.surfaceContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest;
|
||||
final textColor = isCurrent
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
|
||||
final Widget cell = GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 60,
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(left: 24, right: 24),
|
||||
color: color,
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
child: Text(
|
||||
accountName,
|
||||
style: Theme.of(context).textTheme.titleMedium!.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (accountBalance != null)
|
||||
Container(
|
||||
child: Text(
|
||||
'${accountBalance.toString()} $currency',
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: isCurrent
|
||||
? Theme.of(context).colorScheme.onSurface
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// return cell;
|
||||
return Slidable(
|
||||
key: Key(accountName),
|
||||
child: cell,
|
||||
endActionPane: _actionPane(context),
|
||||
);
|
||||
}
|
||||
|
||||
ActionPane _actionPane(BuildContext context) => ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
extentRatio: 0.3,
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (_) => onEdit.call(),
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
icon: Icons.edit,
|
||||
label: S.of(context).edit,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import 'package:hash_wallet/core/execution_state.dart';
|
||||
import 'package:hash_wallet/view_model/nano_account_list/nano_account_edit_or_create_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/core/monero_account_label_validator.dart';
|
||||
import 'package:hash_wallet/view_model/monero_account_list/monero_account_edit_or_create_view_model.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
|
||||
|
||||
class NanoAccountEditOrCreatePage extends BasePage {
|
||||
NanoAccountEditOrCreatePage({required this.nanoAccountCreationViewModel})
|
||||
: _formKey = GlobalKey<FormState>(),
|
||||
_textController = TextEditingController() {
|
||||
_textController.addListener(() => nanoAccountCreationViewModel.label = _textController.text);
|
||||
_textController.text = nanoAccountCreationViewModel.label;
|
||||
}
|
||||
|
||||
final NanoAccountEditOrCreateViewModel nanoAccountCreationViewModel;
|
||||
|
||||
@override
|
||||
String get title => S.current.account;
|
||||
|
||||
final GlobalKey<FormState> _formKey;
|
||||
final TextEditingController _textController;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => Form(
|
||||
key: _formKey,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: BaseTextFormField(
|
||||
controller: _textController,
|
||||
hintText: S.of(context).account,
|
||||
validator: MoneroLabelValidator(),
|
||||
),
|
||||
),
|
||||
),
|
||||
Observer(
|
||||
builder: (_) => LoadingPrimaryButton(
|
||||
onPressed: () async {
|
||||
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await nanoAccountCreationViewModel.save();
|
||||
|
||||
Navigator.of(context).pop(_textController.text);
|
||||
},
|
||||
text: nanoAccountCreationViewModel.isEdit
|
||||
? S.of(context).rename
|
||||
: S.of(context).add,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: nanoAccountCreationViewModel.state is IsExecutingState,
|
||||
isDisabled: nanoAccountCreationViewModel.label?.isEmpty ?? true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
import 'package:hash_wallet/src/widgets/picker_inner_wrapper_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/section_divider.dart';
|
||||
import 'package:hash_wallet/view_model/nano_account_list/nano_account_list_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/monero_accounts/widgets/account_tile.dart';
|
||||
|
||||
class NanoAccountListPage extends StatelessWidget {
|
||||
NanoAccountListPage({required this.accountListViewModel});
|
||||
|
||||
final NanoAccountListViewModel accountListViewModel;
|
||||
final ScrollController controller = ScrollController();
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
double itemHeight = 80;
|
||||
double buttonHeight = 62;
|
||||
|
||||
return Observer(builder: (_) {
|
||||
final accounts = accountListViewModel.accounts;
|
||||
return PickerInnerWrapperWidget(
|
||||
title: S.of(context).choose_account,
|
||||
itemsHeight: (itemHeight * accounts.length) + buttonHeight,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Scrollbar(
|
||||
controller: controller,
|
||||
child: ListView.separated(
|
||||
padding: EdgeInsets.zero,
|
||||
controller: controller,
|
||||
separatorBuilder: (context, index) => const VerticalSectionDivider(),
|
||||
itemCount: accounts.length,
|
||||
itemBuilder: (context, index) {
|
||||
final account = accounts[index];
|
||||
|
||||
return AccountTile(
|
||||
isCurrent: account.isSelected,
|
||||
accountName: account.label,
|
||||
accountBalance: account.balance ?? '0.00',
|
||||
currency: accountListViewModel.currency.toString(),
|
||||
onTap: () {
|
||||
if (account.isSelected) {
|
||||
return;
|
||||
}
|
||||
|
||||
accountListViewModel.select(account);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
onEdit: () async => await Navigator.of(context)
|
||||
.pushNamed(Routes.nanoAccountCreation, arguments: account),
|
||||
);
|
||||
},
|
||||
),
|
||||
)),
|
||||
GestureDetector(
|
||||
onTap: () async => await Navigator.of(context).pushNamed(Routes.nanoAccountCreation),
|
||||
child: Container(
|
||||
height: buttonHeight,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||
child: Center(
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
Icons.add,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.only(left: 5),
|
||||
child: Text(
|
||||
S.of(context).create_new_account,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
|
||||
class AccountTile extends StatelessWidget {
|
||||
AccountTile(
|
||||
{required this.isCurrent,
|
||||
required this.accountName,
|
||||
this.accountBalance,
|
||||
required this.currency,
|
||||
required this.onTap,
|
||||
required this.onEdit});
|
||||
|
||||
final bool isCurrent;
|
||||
final String accountName;
|
||||
final String? accountBalance;
|
||||
final String currency;
|
||||
final Function() onTap;
|
||||
final Function() onEdit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final color = isCurrent
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest;
|
||||
final textColor = isCurrent
|
||||
? Theme.of(context).colorScheme.onPrimaryContainer
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant;
|
||||
|
||||
final Widget cell = GestureDetector(
|
||||
onTap: onTap,
|
||||
child: Container(
|
||||
height: 77,
|
||||
width: double.infinity,
|
||||
padding: EdgeInsets.only(left: 24, right: 24),
|
||||
color: color,
|
||||
child: Wrap(
|
||||
direction: Axis.horizontal,
|
||||
alignment: WrapAlignment.spaceBetween,
|
||||
runAlignment: WrapAlignment.center,
|
||||
crossAxisAlignment: WrapCrossAlignment.center,
|
||||
children: [
|
||||
Container(
|
||||
child: Text(
|
||||
accountName,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
if (accountBalance != null)
|
||||
Container(
|
||||
child: Text(
|
||||
'${accountBalance.toString()} $currency',
|
||||
textAlign: TextAlign.end,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 15,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).textTheme.headlineMedium!.color!,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
// return cell;
|
||||
return Slidable(key: Key(accountName), child: cell, endActionPane: _actionPane(context));
|
||||
}
|
||||
|
||||
ActionPane _actionPane(BuildContext context) => ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
extentRatio: 0.3,
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (_) => onEdit.call(),
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
icon: Icons.edit,
|
||||
label: S.of(context).edit,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -314,13 +314,14 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
LoadingPrimaryButton(
|
||||
onPressed: () {
|
||||
onPressed: () async {
|
||||
if (widget.privacySettingsViewModel.addCustomNode) {
|
||||
if (_formKey.currentState != null && !_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.nodeViewModel.save();
|
||||
await widget.nodeViewModel.save();
|
||||
widget.nodeViewModel.setAsCurrent(widget.nodeViewModel.editingNode!);
|
||||
}
|
||||
if (testnetValue == true &&
|
||||
widget.privacySettingsViewModel.type == WalletType.bitcoin) {
|
||||
@@ -329,13 +330,11 @@ class _AdvancedPrivacySettingsBodyState extends State<_AdvancedPrivacySettingsBo
|
||||
widget.nodeViewModel.address = publicBitcoinTestnetElectrumAddress;
|
||||
widget.nodeViewModel.port = publicBitcoinTestnetElectrumPort;
|
||||
|
||||
widget.nodeViewModel.save();
|
||||
await widget.nodeViewModel.save();
|
||||
}
|
||||
if (passphraseController.text.isNotEmpty) {
|
||||
if (_passphraseFormKey.currentState != null &&
|
||||
!_passphraseFormKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
if (_passphraseFormKey.currentState != null &&
|
||||
!_passphraseFormKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.seedTypeViewModel.setPassphrase(passphraseController.text);
|
||||
|
||||
@@ -316,6 +316,8 @@ class _WalletNameFormState extends State<WalletNameForm> {
|
||||
"type": _walletNewVM.type,
|
||||
"useTestnet": _walletNewVM.useTestnet,
|
||||
"toggleTestnet": _walletNewVM.toggleUseTestnet,
|
||||
"zcashNetwork": _walletNewVM.zcashNetwork,
|
||||
"setZcashNetwork": _walletNewVM.setZcashNetwork,
|
||||
"isChildWallet": widget.isChildWallet,
|
||||
});
|
||||
},
|
||||
|
||||
@@ -6,11 +6,7 @@ import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/reactions/wallet_utils.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/screens/new_wallet/widgets/select_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:hash_wallet/src/widgets/search_bar_widget.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/wallet_types.g.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
@@ -19,10 +15,38 @@ import 'package:cw_core/wallet_info.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NewWalletTypePageHeader extends StatelessWidget {
|
||||
const NewWalletTypePageHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) => const SizedBox(
|
||||
width: 138,
|
||||
height: 75,
|
||||
child: Stack(
|
||||
children: [
|
||||
Positioned(
|
||||
left: 0,
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/hardware_wallet.svg",
|
||||
width: 75,
|
||||
height: 75,
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
left: 63,
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/hash_squircle_icon.svg",
|
||||
width: 75,
|
||||
height: 75,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
class NewWalletTypePage extends BasePage {
|
||||
NewWalletTypePage({
|
||||
required this.newWalletTypeArguments,
|
||||
});
|
||||
NewWalletTypePage({required this.newWalletTypeArguments});
|
||||
|
||||
final NewWalletTypeArguments newWalletTypeArguments;
|
||||
|
||||
@@ -36,10 +60,8 @@ class NewWalletTypePage extends BasePage {
|
||||
|
||||
@override
|
||||
Function(BuildContext)? get pushToNextWidget => (context) {
|
||||
FocusScopeNode currentFocus = FocusScope.of(context);
|
||||
if (!currentFocus.hasPrimaryFocus) {
|
||||
currentFocus.focusedChild?.unfocus();
|
||||
}
|
||||
final currentFocus = FocusScope.of(context);
|
||||
if (!currentFocus.hasPrimaryFocus) currentFocus.focusedChild?.unfocus();
|
||||
};
|
||||
|
||||
@override
|
||||
@@ -52,7 +74,7 @@ class NewWalletTypePage extends BasePage {
|
||||
}
|
||||
|
||||
class WalletTypeForm extends StatefulWidget {
|
||||
WalletTypeForm({
|
||||
const WalletTypeForm({
|
||||
required this.walletImage,
|
||||
required this.isCreate,
|
||||
this.onTypeSelected,
|
||||
@@ -75,9 +97,6 @@ class WalletTypeFormState extends State<WalletTypeForm> {
|
||||
|
||||
static const aspectRatioImage = 1.22;
|
||||
|
||||
final TextEditingController searchController = TextEditingController();
|
||||
|
||||
WalletType? selected;
|
||||
List<WalletType> types;
|
||||
List<WalletType> filteredTypes = [];
|
||||
|
||||
@@ -91,15 +110,6 @@ class WalletTypeFormState extends State<WalletTypeForm> {
|
||||
.isNotEmpty)
|
||||
.toList();
|
||||
super.initState();
|
||||
|
||||
searchController.addListener(() {
|
||||
setState(() {
|
||||
filteredTypes = List.from(types.where((type) => walletTypeToDisplayName(type)
|
||||
.toLowerCase()
|
||||
.contains(searchController.text.toLowerCase())));
|
||||
return;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -108,85 +118,118 @@ class WalletTypeFormState extends State<WalletTypeForm> {
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: ResponsiveLayoutUtilBase.kDesktopMaxWidthConstraint),
|
||||
child: Column(
|
||||
spacing: 24,
|
||||
children: [
|
||||
Padding(
|
||||
padding: EdgeInsets.only(top: 48),
|
||||
child: Text(
|
||||
S.of(context).choose_wallet_currency,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
widget.hardwareWalletType != null
|
||||
? const NewWalletTypePageHeader()
|
||||
: const HashImageWidget(
|
||||
imageUrl: "assets/new-ui/wallet.svg",
|
||||
width: 100,
|
||||
height: 100,
|
||||
),
|
||||
Text(
|
||||
S.of(context).choose_wallet_currency,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 24, 24, 12),
|
||||
child: SearchBarWidget(searchController: searchController),
|
||||
),
|
||||
Expanded(
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
scrollableKey: ValueKey('new_wallet_type_scrollable_key'),
|
||||
content: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
...filteredTypes.map(
|
||||
(type) => Padding(
|
||||
padding: EdgeInsets.only(top: 12),
|
||||
child: SelectButton(
|
||||
key: ValueKey('new_wallet_type_${type.name}_button_key'),
|
||||
image: HashImageWidget(
|
||||
imageUrl: getCryptoCurrencyIconForWalletListItem(type),
|
||||
height: 24,
|
||||
width: 24,
|
||||
),
|
||||
text: walletTypeToDisplayName(type),
|
||||
showTrailingIcon: false,
|
||||
height: 54,
|
||||
isSelected: selected == type,
|
||||
onTap: () => setState(() => selected = type),
|
||||
deviceConnectionTypes: widget.isHardwareWallet
|
||||
? DeviceConnectionType.supportedConnectionTypes(
|
||||
type, widget.hardwareWalletType!, Platform.isIOS)
|
||||
: [],
|
||||
child: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(left: 18, right: 18, bottom: 12),
|
||||
child: ClipRRect(
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(18),
|
||||
),
|
||||
child: ListView.separated(
|
||||
key: const ValueKey('new_wallet_type_scrollable_key'),
|
||||
itemCount: filteredTypes.length,
|
||||
shrinkWrap: true,
|
||||
physics: NeverScrollableScrollPhysics(),
|
||||
padding: EdgeInsets.zero,
|
||||
itemBuilder: (context, index) {
|
||||
final item = filteredTypes[index];
|
||||
|
||||
return GestureDetector(
|
||||
key: ValueKey('new_wallet_type_${item.name}_button_key'),
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () {
|
||||
onTypeSelected(item);
|
||||
},
|
||||
child: SizedBox(
|
||||
height: 50,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
HashImageWidget(
|
||||
height: 24,
|
||||
width: 24,
|
||||
imageUrl: getCryptoCurrencyIconForWalletListItem(item),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Text(walletTypeToDisplayName(item)),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
walletTypeToDisplayTicker(item),
|
||||
style: TextStyle(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
HashImageWidget(
|
||||
imageUrl: "assets/new-ui/arrow_forward.svg",
|
||||
height: 16,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
BlendMode.srcIn,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
separatorBuilder: (context, index) => Container(
|
||||
height: 1,
|
||||
color: Theme.of(context).colorScheme.surfaceContainerHigh,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
bottomSection: PrimaryButton(
|
||||
key: ValueKey('new_wallet_type_next_button_key'),
|
||||
onPressed: () => onTypeSelected(),
|
||||
text: S.of(context).seed_language_next,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isDisabled: selected == null,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> onTypeSelected() async {
|
||||
if (selected == null) throw Exception('Wallet Type is not selected yet.');
|
||||
|
||||
Future<void> onTypeSelected(WalletType selected) async {
|
||||
// If it's a restore flow, trigger the external callback
|
||||
// If it's not a BIP39 Wallet or if there are no other wallets, route to the newWallet page
|
||||
// Any other scenario, route to pre-existing seed page
|
||||
final walletList = await WalletInfo.getAll();
|
||||
if (!widget.isCreate) {
|
||||
widget.onTypeSelected!(context, selected!);
|
||||
} else if (!isBIP39Wallet(selected!) || walletList.isEmpty) {
|
||||
widget.onTypeSelected!(context, selected);
|
||||
} else if (!isBIP39Wallet(selected) || walletList.isEmpty) {
|
||||
Navigator.of(context).pushNamed(
|
||||
Routes.newWallet,
|
||||
arguments: NewWalletArguments(type: selected!),
|
||||
arguments: NewWalletArguments(type: selected),
|
||||
);
|
||||
} else {
|
||||
Navigator.of(context).pushNamed(Routes.walletGroupDescription, arguments: selected!);
|
||||
Navigator.of(context).pushNamed(Routes.walletGroupDescription, arguments: selected);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ class WalletGroupDescriptionPage extends BasePage {
|
||||
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
|
||||
|
||||
@override
|
||||
String get title => S.current.wallet_group;
|
||||
String get title => S.current.wallet_list_create_new_wallet;
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
|
||||
@@ -114,6 +114,7 @@ class WalletGroupsDisplayBody extends StatelessWidget {
|
||||
Observer(
|
||||
builder: (context) {
|
||||
return LoadingPrimaryButton(
|
||||
key: ValueKey("wallet_group_display_page_next_button_key"),
|
||||
isLoading: walletGroupsDisplayViewModel.isFetchingMnemonic,
|
||||
onPressed: () {
|
||||
if (walletGroupsDisplayViewModel.hasNoFilteredWallet) {
|
||||
|
||||
@@ -79,7 +79,7 @@ class WalletGroupExistingSeedDescriptionPage extends BasePage {
|
||||
key: ValueKey(
|
||||
'wallet_group_existing_seed_description_page_verify_seed_button_key'),
|
||||
onPressed: () => Navigator.pushNamed(context, Routes.preSeedPage),
|
||||
text: S.current.verify_seed,
|
||||
text: S.current.verify_phrase,
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
|
||||
@@ -1,20 +1,20 @@
|
||||
import 'package:hash_wallet/core/execution_state.dart';
|
||||
import 'package:hash_wallet/entities/new_ui_entities/list_item/list_item_regular_row.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/keyboard_hide_overlay.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/screens/nodes/node_share_page.dart';
|
||||
import 'package:hash_wallet/src/screens/nodes/widgets/node_form.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_two_actions.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:hash_wallet/src/widgets/new_list_row/new_list_section.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/node_list/node_create_or_edit_view_model.dart';
|
||||
import 'package:cw_core/currency_for_wallet_type.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class NodeCreateOrEditPage extends StatefulWidget {
|
||||
@@ -74,6 +74,13 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
reaction((_) => widget.nodeCreateOrEditViewModel.state, (state) async {
|
||||
if (state is ExecutedSuccessfullyState) {
|
||||
await Future.delayed(Duration(milliseconds: 100));
|
||||
if (mounted) Navigator.of(context).pop(widget.nodeCreateOrEditViewModel.editingNode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -86,94 +93,74 @@ class _NodeCreateOrEditPageState extends State<NodeCreateOrEditPage> {
|
||||
ModalTopBar(
|
||||
title: widget.editingNode != null ? S.current.edit_node : S.current.node_new,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.current.seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
trailingIcon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/scan.svg",
|
||||
width: 24,
|
||||
height: 24,
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
onTrailingPressed: () =>
|
||||
widget.nodeCreateOrEditViewModel.scanQRCodeForNewNode(context),
|
||||
trailingIcon: Icon(Icons.check),
|
||||
trailingSemanticLabel: S.current.save,
|
||||
onTrailingPressed: () async {
|
||||
if (_nodeFormKey.currentState != null && !_nodeFormKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await widget.nodeCreateOrEditViewModel
|
||||
.save(saveAsCurrent: widget.isSelected ?? false);
|
||||
},
|
||||
),
|
||||
Expanded(
|
||||
child: KeyboardHideOverlay(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 18, right: 18),
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: const EdgeInsets.only(bottom: 24.0, top: 8),
|
||||
content: NodeForm(
|
||||
child: Column(spacing: 18, children: [
|
||||
NodeForm(
|
||||
key: _nodeFormKey,
|
||||
nodeViewModel: widget.nodeCreateOrEditViewModel,
|
||||
),
|
||||
bottomSectionPadding: const EdgeInsets.only(bottom: 24),
|
||||
bottomSection: Observer(
|
||||
builder: (_) => Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: LoadingPrimaryButton(
|
||||
onPressed: () async {
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithTwoActions(
|
||||
alertTitle: S.of(context).remove_node,
|
||||
alertContent: S.of(context).remove_node_message,
|
||||
rightButtonText: S.of(context).remove,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
actionRightButton: () => Navigator.pop(context, true),
|
||||
actionLeftButton: () => Navigator.pop(context, false),
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
NewListSections(sections: {
|
||||
"": [
|
||||
if (widget.editingNode != null)
|
||||
ListItemRegularRow(
|
||||
keyValue: "share",
|
||||
label: S.of(context).share_this_node,
|
||||
foregroundColor: Theme.of(context).colorScheme.primary,
|
||||
showArrow: false,
|
||||
onTap: () {
|
||||
Navigator.of(context).push(CupertinoPageRoute(
|
||||
builder: (context) => NodeSharePage(
|
||||
uri: widget.editingNode!.uri,
|
||||
currency: walletTypeToCryptoCurrency(widget.type!))));
|
||||
}),
|
||||
if (!(widget.editingNode == null ||
|
||||
!widget.nodeCreateOrEditViewModel.isReady ||
|
||||
widget.editingNode!.isBuiltin ||
|
||||
(widget.isSelected ?? false)))
|
||||
ListItemRegularRow(
|
||||
keyValue: "delete",
|
||||
label: S.of(context).delete_this_node,
|
||||
foregroundColor: Theme.of(context).colorScheme.errorContainer,
|
||||
showArrow: false,
|
||||
onTap: () async {
|
||||
final confirmed = await showPopUp<bool>(
|
||||
context: context,
|
||||
builder: (context) => AlertWithTwoActions(
|
||||
alertTitle: S.of(context).remove_node,
|
||||
alertContent: S.of(context).remove_node_message,
|
||||
rightButtonText: S.of(context).remove,
|
||||
leftButtonText: S.of(context).cancel,
|
||||
actionRightButton: () => Navigator.pop(context, true),
|
||||
actionLeftButton: () => Navigator.pop(context, false),
|
||||
),
|
||||
) ??
|
||||
false;
|
||||
|
||||
if (confirmed) {
|
||||
await widget.editingNode!.delete();
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
text: S.of(context).delete,
|
||||
isDisabled: widget.editingNode == null ||
|
||||
!widget.nodeCreateOrEditViewModel.isReady ||
|
||||
(widget.isSelected ?? false),
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
textColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(left: 8.0),
|
||||
child: PrimaryButton(
|
||||
onPressed: () async {
|
||||
if (_nodeFormKey.currentState != null &&
|
||||
!_nodeFormKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
await widget.nodeCreateOrEditViewModel.save(
|
||||
editingNode: widget.editingNode,
|
||||
saveAsCurrent: widget.isSelected ?? false);
|
||||
if (context.mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
text: S.of(context).save,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isDisabled: (!widget.nodeCreateOrEditViewModel.isReady) ||
|
||||
(widget.nodeCreateOrEditViewModel.connectionState
|
||||
is IsExecutingState),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (confirmed) {
|
||||
await widget.nodeCreateOrEditViewModel
|
||||
.delete(editingNode: widget.editingNode!);
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
})
|
||||
]
|
||||
})
|
||||
]),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/qr_image.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NodeSharePage extends StatelessWidget {
|
||||
const NodeSharePage({super.key, required this.uri, required this.currency});
|
||||
|
||||
final Uri uri;
|
||||
final CryptoCurrency currency;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final qrSize = MediaQuery.of(context).size.width * 0.786;
|
||||
final padding = 16.0;
|
||||
return Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).share,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.of(context).seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
spacing: 40,
|
||||
children: [
|
||||
QrImage(
|
||||
size: qrSize - padding * 2,
|
||||
embeddedImagePath: "assets/new-ui/node-qr-icon.svg",
|
||||
badgeImageOnEmbeddedImagePath: currency.iconPath,
|
||||
data: uri.toString(),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 36.0),
|
||||
child: Text(S.of(context).node_share_desc,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant)),
|
||||
)
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -75,6 +75,7 @@ class _PowNodeCreateOrEditPageState extends State<PowNodeCreateOrEditPage> {
|
||||
ModalTopBar(
|
||||
title: widget.editingNode != null ? S.current.edit_node : S.current.node_new,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
leadingSemanticLabel: S.current.seed_alert_back,
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
trailingIcon: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/scan.svg",
|
||||
@@ -83,6 +84,7 @@ class _PowNodeCreateOrEditPageState extends State<PowNodeCreateOrEditPage> {
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
),
|
||||
trailingSemanticLabel: S.current.scan,
|
||||
onTrailingPressed: () =>
|
||||
widget.nodeCreateOrEditViewModel.scanQRCodeForNewNode(context),
|
||||
),
|
||||
@@ -144,9 +146,8 @@ class _PowNodeCreateOrEditPageState extends State<PowNodeCreateOrEditPage> {
|
||||
return;
|
||||
}
|
||||
|
||||
await widget.nodeCreateOrEditViewModel.save(
|
||||
editingNode: widget.editingNode,
|
||||
saveAsCurrent: widget.isSelected ?? false);
|
||||
await widget.nodeCreateOrEditViewModel
|
||||
.save(saveAsCurrent: widget.isSelected ?? false);
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
text: S.of(context).save,
|
||||
|
||||
@@ -64,64 +64,74 @@ class NodeFormState extends State<NodeForm> {
|
||||
key: _formKey,
|
||||
child: NewListSections(
|
||||
sections: {
|
||||
'main': [
|
||||
ListItemTextField(
|
||||
keyValue: vm.nodeLabelUIKey,
|
||||
label: 'Node label',
|
||||
initialValue: vm.label,
|
||||
),
|
||||
ListItemTextField(
|
||||
keyValue: vm.nodeAddressUIKey,
|
||||
label: S.current.node_address,
|
||||
initialValue: vm.address,
|
||||
validator: NodeAddressValidator(),
|
||||
),
|
||||
if (vm.hasPathSupport)
|
||||
if (!(vm.editingNode?.isBuiltin ?? false)) ...{
|
||||
'main': [
|
||||
ListItemTextField(
|
||||
keyValue: vm.nodePathUIKey,
|
||||
label: '/path',
|
||||
initialValue: vm.path,
|
||||
validator: NodePathValidator(),
|
||||
),
|
||||
ListItemTextField(
|
||||
keyValue: vm.nodePortUIKey,
|
||||
label: S.current.node_port,
|
||||
initialValue: '',
|
||||
//keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false),
|
||||
validator: NodePortValidator(),
|
||||
),
|
||||
if (vm.hasAuthCredentials) ...[
|
||||
ListItemTextField(
|
||||
keyValue: vm.nodeUsernameUIKey,
|
||||
label: S.current.login,
|
||||
initialValue: vm.login,
|
||||
keyValue: vm.nodeLabelUIKey,
|
||||
label: 'Node label',
|
||||
initialValue: vm.label,
|
||||
),
|
||||
ListItemTextField(
|
||||
keyValue: vm.nodePasswordUIKey,
|
||||
label: S.current.password,
|
||||
initialValue: vm.password,
|
||||
keyValue: vm.nodeAddressUIKey,
|
||||
label: S.current.node_address,
|
||||
initialValue: vm.address,
|
||||
validator: NodeAddressValidator(),
|
||||
),
|
||||
]
|
||||
],
|
||||
'advanced': [
|
||||
ListItemCheckbox(
|
||||
keyValue: vm.useSSLUIKey,
|
||||
label: S.current.use_ssl,
|
||||
value: vm.useSSL,
|
||||
onChanged: (value) => vm.useSSL = value),
|
||||
ListItemCheckbox(
|
||||
keyValue: vm.nodeTrustedUIKey,
|
||||
label: S.current.trusted,
|
||||
value: vm.trusted,
|
||||
onChanged: (value) => vm.trusted = value,
|
||||
),
|
||||
if (vm.usesEmbeddedProxy)
|
||||
if (vm.hasPathSupport)
|
||||
ListItemTextField(
|
||||
keyValue: vm.nodePathUIKey,
|
||||
label: '/path',
|
||||
initialValue: vm.path,
|
||||
validator: NodePathValidator(),
|
||||
),
|
||||
ListItemTextField(
|
||||
keyValue: vm.nodePortUIKey,
|
||||
label: S.current.node_port,
|
||||
initialValue: '',
|
||||
//keyboardType: TextInputType.numberWithOptions(signed: false, decimal: false),
|
||||
validator: NodePortValidator(),
|
||||
),
|
||||
if (vm.hasAuthCredentials) ...[
|
||||
ListItemTextField(
|
||||
keyValue: vm.nodeUsernameUIKey,
|
||||
label: S.current.login,
|
||||
initialValue: vm.login,
|
||||
),
|
||||
ListItemTextField(
|
||||
keyValue: vm.nodePasswordUIKey,
|
||||
label: S.current.password,
|
||||
initialValue: vm.password,
|
||||
),
|
||||
]
|
||||
],
|
||||
'advanced': [
|
||||
ListItemCheckbox(
|
||||
keyValue: vm.nodeEmbeddedTorProxyUIKey,
|
||||
label: 'Embedded Tor SOCKS Proxy',
|
||||
value: vm.usesEmbeddedProxy,
|
||||
onChanged: (_) {},
|
||||
keyValue: vm.useSSLUIKey,
|
||||
label: S.current.use_ssl,
|
||||
value: vm.useSSL,
|
||||
onChanged: (value) => vm.useSSL = value),
|
||||
ListItemCheckbox(
|
||||
keyValue: vm.nodeTrustedUIKey,
|
||||
label: S.current.trusted,
|
||||
value: vm.trusted,
|
||||
onChanged: (value) => vm.trusted = value,
|
||||
),
|
||||
if (vm.usesEmbeddedProxy)
|
||||
ListItemCheckbox(
|
||||
keyValue: vm.nodeEmbeddedTorProxyUIKey,
|
||||
label: 'Embedded Tor SOCKS Proxy',
|
||||
value: vm.usesEmbeddedProxy,
|
||||
onChanged: (_) {},
|
||||
),
|
||||
if (FeatureFlag.isAutomaticNodeSwitchingEnabled)
|
||||
ListItemCheckbox(
|
||||
keyValue: vm.autoSwitchingUIKey,
|
||||
label: S.current.enable_for_auto_switching,
|
||||
value: vm.isEnabledForAutoSwitching,
|
||||
onChanged: (value) => vm.isEnabledForAutoSwitching = value),
|
||||
],
|
||||
},
|
||||
"proxy": [
|
||||
ListItemCheckbox(
|
||||
keyValue: vm.useSocksProxyUIKey,
|
||||
label: 'Use SOCKS Proxy',
|
||||
@@ -138,13 +148,7 @@ class NodeFormState extends State<NodeForm> {
|
||||
initialValue: vm.socksProxyAddress,
|
||||
validator: SocksProxyNodeAddressValidator(),
|
||||
),
|
||||
if (FeatureFlag.isAutomaticNodeSwitchingEnabled)
|
||||
ListItemCheckbox(
|
||||
keyValue: vm.autoSwitchingUIKey,
|
||||
label: S.current.enable_for_auto_switching,
|
||||
value: vm.isEnabledForAutoSwitching,
|
||||
onChanged: (value) => vm.isEnabledForAutoSwitching = value),
|
||||
],
|
||||
]
|
||||
},
|
||||
controllers: _controllers,
|
||||
getCheckboxValue: vm.getCheckboxValue,
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
import 'package:hash_wallet/themes/core/custom_theme_colors.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NodeIndicator extends StatelessWidget {
|
||||
const NodeIndicator({this.isLive = false});
|
||||
|
||||
final bool isLive;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 12.0,
|
||||
height: 12.0,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: isLive ? CustomThemeColors.syncGreen : Theme.of(context).colorScheme.errorContainer,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,44 +1,77 @@
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/nodes/widgets/node_indicator.dart';
|
||||
import 'package:hash_wallet/src/widgets/standard_list.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:hash_wallet/view_model/node_list/node_list_view_model.dart';
|
||||
import 'package:cw_core/node.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class NodeListRow extends StandardListRow {
|
||||
class NodeListRow extends StatelessWidget {
|
||||
NodeListRow(
|
||||
{required String title,
|
||||
String? subtitle,
|
||||
required this.node,
|
||||
required void Function(BuildContext context) onTap,
|
||||
required bool isSelected,
|
||||
required this.isPow})
|
||||
: super(title: title, onTap: onTap, isSelected: isSelected, subtitle: subtitle);
|
||||
{required this.node,
|
||||
required this.onTap,
|
||||
required this.onEditComplete,
|
||||
required this.isSelected,
|
||||
required this.isPow,
|
||||
this.speed,
|
||||
this.vertical = 10.0,
|
||||
this.horizontal = 12.0});
|
||||
|
||||
final Node node;
|
||||
final bool isPow;
|
||||
final VoidCallback onTap;
|
||||
final Future<void> Function(Object?) onEditComplete;
|
||||
final bool isSelected;
|
||||
final NodeSpeed? speed;
|
||||
final double vertical;
|
||||
final double horizontal;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final leading = buildLeading(context);
|
||||
final trailing = buildTrailing(context);
|
||||
return Container(
|
||||
padding: EdgeInsets.only(left: 18, right: 18, top: 2, bottom: 2),
|
||||
margin: EdgeInsets.only(top: 2, bottom: 2),
|
||||
child: FilledButton(
|
||||
onPressed: () => onTap?.call(context),
|
||||
style: FilledButton.styleFrom(
|
||||
backgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(20)),
|
||||
),
|
||||
),
|
||||
|
||||
final hasLabel = node.label != null && node.label!.isNotEmpty;
|
||||
final uriText = "${node.uri.host}:${node.uri.port}";
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: InkWell(
|
||||
onTap: onTap,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
padding: EdgeInsets.symmetric(vertical: vertical, horizontal: horizontal),
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
leading,
|
||||
buildCenter(context, hasLeftOffset: true),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
spacing: 4,
|
||||
children: [
|
||||
Row(
|
||||
spacing: 4,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
hasLabel ? node.label! : uriText,
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
if (badgePath != null)
|
||||
HashImageWidget(
|
||||
imageUrl: badgePath,
|
||||
colorFilter: ColorFilter.mode(
|
||||
Theme.of(context).colorScheme.primary, BlendMode.srcIn),
|
||||
)
|
||||
],
|
||||
),
|
||||
if (hasLabel)
|
||||
Text(
|
||||
uriText,
|
||||
style: TextStyle(
|
||||
fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
trailing,
|
||||
],
|
||||
),
|
||||
@@ -47,81 +80,52 @@ class NodeListRow extends StandardListRow {
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildLeading(BuildContext context) {
|
||||
return FutureBuilder(
|
||||
future: node.requestNode(),
|
||||
builder: (context, snapshot) {
|
||||
switch (snapshot.connectionState) {
|
||||
case ConnectionState.done:
|
||||
return NodeIndicator(isLive: snapshot.data ?? false);
|
||||
default:
|
||||
return NodeIndicator(isLive: false);
|
||||
}
|
||||
});
|
||||
}
|
||||
Widget buildLeading(BuildContext context) => isSelected
|
||||
? Padding(
|
||||
padding: const EdgeInsets.all(4.0),
|
||||
child: Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
)
|
||||
: SizedBox(width: 24, height: 24);
|
||||
|
||||
@override
|
||||
Widget buildTrailing(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context).pushNamed(
|
||||
isPow ? Routes.newPowNode : Routes.newNode,
|
||||
arguments: {'editingNode': node, 'isSelected': isSelected},
|
||||
),
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
child: Icon(
|
||||
Icons.edit,
|
||||
size: 14,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class NodeHeaderListRow extends StandardListRow {
|
||||
NodeHeaderListRow({required String title, required void Function(BuildContext context) onTap})
|
||||
: super(title: title, onTap: onTap, isSelected: false);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final leading = buildLeading(context);
|
||||
final trailing = buildTrailing(context);
|
||||
return Container(
|
||||
height: 56,
|
||||
padding: EdgeInsets.only(left: 12, right: 12, top: 2, bottom: 2),
|
||||
child: TextButton(
|
||||
onPressed: () => onTap?.call(context),
|
||||
style: ButtonStyle(
|
||||
backgroundColor: WidgetStateProperty.all(Theme.of(context).colorScheme.surface),
|
||||
shape: WidgetStateProperty.all(
|
||||
RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
return Row(
|
||||
children: [
|
||||
if (speed != null)
|
||||
HashImageWidget(
|
||||
imageUrl: Theme.of(context).brightness == Brightness.dark
|
||||
? speed!.darkIconPath
|
||||
: speed!.iconPath,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
GestureDetector(
|
||||
behavior: HitTestBehavior.opaque,
|
||||
onTap: () async {
|
||||
final res = await Navigator.of(context).pushNamed(
|
||||
isPow ? Routes.newPowNode : Routes.newNode,
|
||||
arguments: {'editingNode': node, 'isSelected': isSelected});
|
||||
await onEditComplete(res);
|
||||
},
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0, left: 12.0),
|
||||
child: HashImageWidget(
|
||||
imageUrl: "assets/new-ui/3dots_vertical.svg",
|
||||
colorFilter:
|
||||
ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn),
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
if (leading != null) leading,
|
||||
buildCenter(context, hasLeftOffset: leading != null),
|
||||
trailing,
|
||||
],
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget buildTrailing(BuildContext context) {
|
||||
return SizedBox(
|
||||
width: 20,
|
||||
child: Icon(Icons.add, color: Theme.of(context).colorScheme.onSurfaceVariant, size: 24.0),
|
||||
);
|
||||
String? get badgePath {
|
||||
if (node.isOfficial) return "assets/new-ui/official_node.svg";
|
||||
if (node.isBuiltin) return "assets/new-ui/default_node.svg";
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ class OrderDetailsPageBodyState extends State<OrderDetailsPageBody> {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
super.dispose();
|
||||
orderDetailsViewModel.timer?.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
import 'package:hash_wallet/new-ui/widgets/receive_page/receive_top_bar.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/address_list.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:modal_bottom_sheet/modal_bottom_sheet.dart';
|
||||
|
||||
class AddressListPage extends StatelessWidget {
|
||||
AddressListPage(this.addressListViewModel);
|
||||
|
||||
final WalletAddressListViewModel addressListViewModel;
|
||||
|
||||
@override
|
||||
String get title => S.current.accounts_subaddresses;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
ModalTopBar(
|
||||
title: S.of(context).accounts_subaddresses,
|
||||
leadingIcon: Icon(Icons.arrow_back_ios_new),
|
||||
onLeadingPressed: Navigator.of(context).pop,
|
||||
),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
controller: ModalScrollController.of(context),
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
AddressList(
|
||||
addressListViewModel: addressListViewModel,
|
||||
onSelect: (String address) async {
|
||||
Navigator.of(context).pop(address);
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
import 'package:hash_wallet/anonpay/anonpay_donation_link_info.dart';
|
||||
import 'package:hash_wallet/anonpay/anonpay_invoice_info.dart';
|
||||
import 'package:hash_wallet/core/execution_state.dart';
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/entities/preferences_key.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/anonpay_receive_page.dart';
|
||||
import 'package:hash_wallet/utils/feature_flag.dart';
|
||||
import 'package:cw_core/receive_page_option.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/present_receive_option_picker.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/anonpay_input_form.dart';
|
||||
import 'package:hash_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:hash_wallet/src/widgets/keyboard_done_button.dart';
|
||||
import 'package:hash_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:hash_wallet/view_model/anon_invoice_page_view_model.dart';
|
||||
import 'package:hash_wallet/view_model/dashboard/receive_option_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:keyboard_actions/keyboard_actions.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/trail_button.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:hash_wallet/src/widgets/scrollable_with_bottom_section.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class AnonPayInvoicePage extends BasePage {
|
||||
AnonPayInvoicePage(
|
||||
this.anonInvoicePageViewModel,
|
||||
this.receiveOptionViewModel,
|
||||
) : _amountFocusNode = FocusNode() {}
|
||||
|
||||
final _nameController = TextEditingController();
|
||||
final _emailController = TextEditingController();
|
||||
final _descriptionController = TextEditingController();
|
||||
final _amountController = TextEditingController();
|
||||
final FocusNode _amountFocusNode;
|
||||
|
||||
final AnonInvoicePageViewModel anonInvoicePageViewModel;
|
||||
final ReceiveOptionViewModel receiveOptionViewModel;
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
bool effectsInstalled = false;
|
||||
|
||||
@override
|
||||
bool get gradientAll => true;
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => false;
|
||||
|
||||
@override
|
||||
bool get extendBodyBehindAppBar => true;
|
||||
|
||||
@override
|
||||
AppBarStyle get appBarStyle =>
|
||||
FeatureFlag.hasNewUiExtraPages ? AppBarStyle.completelyTransparent : AppBarStyle.transparent;
|
||||
|
||||
@override
|
||||
void onClose(BuildContext context) => FeatureFlag.hasNewUiExtraPages
|
||||
? Navigator.of(context, rootNavigator: true).pop()
|
||||
: Navigator.popUntil(context, (route) => route.isFirst);
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) => PresentReceiveOptionPicker(
|
||||
receiveOptionViewModel: receiveOptionViewModel, color: titleColor(context));
|
||||
|
||||
@override
|
||||
Widget trailing(BuildContext context) => TrailButton(
|
||||
caption: S.of(context).clear,
|
||||
onPressed: () {
|
||||
_formKey.currentState?.reset();
|
||||
anonInvoicePageViewModel.reset();
|
||||
});
|
||||
|
||||
Future<bool> _onNavigateBack(BuildContext context) async {
|
||||
onClose(context);
|
||||
return false;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _setReactions(context));
|
||||
|
||||
return WillPopScope(
|
||||
onWillPop: () => _onNavigateBack(context),
|
||||
child: KeyboardActions(
|
||||
disableScroll: true,
|
||||
config: KeyboardActionsConfig(
|
||||
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
|
||||
keyboardBarColor: Theme.of(context).colorScheme.surfaceVariant,
|
||||
nextFocus: false,
|
||||
actions: [
|
||||
KeyboardActionsItem(
|
||||
focusNode: _amountFocusNode,
|
||||
toolbarButtons: [(_) => KeyboardDoneButton()],
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Container(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: ScrollableWithBottomSection(
|
||||
contentPadding: EdgeInsets.only(bottom: 24),
|
||||
content: Container(
|
||||
decoration: responsiveLayoutUtil.shouldRenderMobileUI
|
||||
? BoxDecoration(
|
||||
borderRadius: BorderRadius.only(
|
||||
bottomLeft: Radius.circular(24),
|
||||
bottomRight: Radius.circular(24),
|
||||
),
|
||||
color: Theme.of(context).colorScheme.surfaceContainerLow,
|
||||
)
|
||||
: null,
|
||||
child: Observer(builder: (_) {
|
||||
return Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 120, 24, 0),
|
||||
child: AnonInvoiceForm(
|
||||
nameController: _nameController,
|
||||
descriptionController: _descriptionController,
|
||||
amountController: _amountController,
|
||||
emailController: _emailController,
|
||||
depositAmountFocus: _amountFocusNode,
|
||||
formKey: _formKey,
|
||||
isInvoice: receiveOptionViewModel.selectedReceiveOption ==
|
||||
ReceivePageOption.anonPayInvoice,
|
||||
anonInvoicePageViewModel: anonInvoicePageViewModel,
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
bottomSectionPadding: EdgeInsets.only(left: 24, right: 24, bottom: 24),
|
||||
bottomSection: Observer(builder: (_) {
|
||||
final isInvoice =
|
||||
receiveOptionViewModel.selectedReceiveOption == ReceivePageOption.anonPayInvoice;
|
||||
return Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(bottom: 15),
|
||||
child: Center(
|
||||
child: Text(
|
||||
isInvoice
|
||||
? S.of(context).anonpay_description("an invoice", "pay")
|
||||
: S.of(context).anonpay_description("a donation link", "donate"),
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
LoadingPrimaryButton(
|
||||
text: isInvoice
|
||||
? S.of(context).create_invoice
|
||||
: S.of(context).create_donation_link,
|
||||
onPressed: () {
|
||||
FocusScope.of(context).unfocus();
|
||||
anonInvoicePageViewModel.setRequestParams(
|
||||
inputAmount: _amountController.text,
|
||||
inputName: _nameController.text,
|
||||
inputEmail: _emailController.text,
|
||||
inputDescription: _descriptionController.text,
|
||||
);
|
||||
if (anonInvoicePageViewModel.receipientEmail.isNotEmpty &&
|
||||
_formKey.currentState != null &&
|
||||
!_formKey.currentState!.validate()) {
|
||||
return;
|
||||
}
|
||||
if (isInvoice) {
|
||||
anonInvoicePageViewModel.createInvoice();
|
||||
} else {
|
||||
anonInvoicePageViewModel.generateDonationLink();
|
||||
}
|
||||
},
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isLoading: anonInvoicePageViewModel.state is IsExecutingState,
|
||||
),
|
||||
],
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _setReactions(BuildContext context) {
|
||||
if (effectsInstalled) {
|
||||
return;
|
||||
}
|
||||
|
||||
reaction((_) => receiveOptionViewModel.selectedReceiveOption, (ReceivePageOption option) {
|
||||
switch (option) {
|
||||
case ReceivePageOption.mainnet:
|
||||
Navigator.popAndPushNamed(context, Routes.addressPage);
|
||||
break;
|
||||
case ReceivePageOption.anonPayDonationLink:
|
||||
final sharedPreferences = getIt.get<SharedPreferences>();
|
||||
final clearnetUrl = sharedPreferences.getString(PreferencesKey.clearnetDonationLink);
|
||||
final onionUrl = sharedPreferences.getString(PreferencesKey.onionDonationLink);
|
||||
final donationWalletName =
|
||||
sharedPreferences.getString(PreferencesKey.donationLinkWalletName);
|
||||
|
||||
if (clearnetUrl != null &&
|
||||
onionUrl != null &&
|
||||
anonInvoicePageViewModel.currentWalletName == donationWalletName) {
|
||||
Navigator.pushReplacementNamed(
|
||||
context,
|
||||
Routes.anonPayReceivePage,
|
||||
arguments: AnonPayReceivePageArgs(
|
||||
invoiceInfo: AnonpayDonationLinkInfo(
|
||||
clearnetUrl: clearnetUrl,
|
||||
onionUrl: onionUrl,
|
||||
address: anonInvoicePageViewModel.address,
|
||||
),
|
||||
qrImage: anonInvoicePageViewModel.qrImage,
|
||||
),
|
||||
);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
}
|
||||
});
|
||||
|
||||
reaction((_) => anonInvoicePageViewModel.state, (ExecutionState state) {
|
||||
if (state is ExecutedSuccessfullyState) {
|
||||
Navigator.pushNamed(
|
||||
context,
|
||||
Routes.anonPayReceivePage,
|
||||
arguments: AnonPayReceivePageArgs(
|
||||
invoiceInfo: state.payload as AnonpayInvoiceInfo,
|
||||
qrImage: anonInvoicePageViewModel.qrImage,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (state is FailureState) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: state.error.toString(),
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
effectsInstalled = true;
|
||||
}
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
import 'package:hash_wallet/anonpay/anonpay_info_base.dart';
|
||||
import 'package:hash_wallet/anonpay/anonpay_invoice_info.dart';
|
||||
import 'package:hash_wallet/entities/qr_view_data.dart';
|
||||
import 'package:cw_core/receive_page_option.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/widgets/gradient_background.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/anonpay_status_section.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/qr_image.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/copy_link_item.dart';
|
||||
import 'package:hash_wallet/utils/brightness_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart' as qr;
|
||||
|
||||
class AnonPayReceivePageArgs {
|
||||
final AnonpayInfoBase invoiceInfo;
|
||||
final String? qrImage;
|
||||
|
||||
AnonPayReceivePageArgs({required this.invoiceInfo, this.qrImage});
|
||||
}
|
||||
|
||||
class AnonPayReceivePage extends BasePage {
|
||||
AnonPayReceivePage({required this.args}) : invoiceInfo = args.invoiceInfo;
|
||||
|
||||
final AnonPayReceivePageArgs args;
|
||||
final AnonpayInfoBase invoiceInfo;
|
||||
|
||||
@override
|
||||
String get title => S.current.receive;
|
||||
|
||||
@override
|
||||
bool get gradientBackground => true;
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => false;
|
||||
|
||||
@override
|
||||
void onClose(BuildContext context) => Navigator.popUntil(context, (route) => route.isFirst);
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyLarge!.copyWith(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
invoiceInfo is AnonpayInvoiceInfo
|
||||
? ReceivePageOption.anonPayInvoice.toString()
|
||||
: ReceivePageOption.anonPayDonationLink.toString(),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontSize: 10.0,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget? trailing(BuildContext context) {
|
||||
if (invoiceInfo is AnonpayInvoiceInfo) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: IconButton(
|
||||
onPressed: () => Navigator.popAndPushNamed(
|
||||
context,
|
||||
Routes.anonPayInvoicePage,
|
||||
arguments: [invoiceInfo.address, ReceivePageOption.anonPayDonationLink],
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.edit,
|
||||
color: pageIconColor(context),
|
||||
size: 22.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget Function(BuildContext, Widget) get rootWrapper =>
|
||||
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
SizedBox(height: 24),
|
||||
if (invoiceInfo is AnonpayInvoiceInfo)
|
||||
AnonInvoiceStatusSection(invoiceInfo: invoiceInfo as AnonpayInvoiceInfo),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 50, 24, 24),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.5,
|
||||
),
|
||||
child: GestureDetector(
|
||||
onTap: () async {
|
||||
BrightnessUtil.changeBrightnessForFunction(() async {
|
||||
await Navigator.pushNamed(context, Routes.fullscreenQR,
|
||||
arguments: QrViewData(
|
||||
data: invoiceInfo.clearnetUrl,
|
||||
version: qr.QrVersions.auto,
|
||||
embeddedImagePath: args.qrImage,
|
||||
));
|
||||
});
|
||||
},
|
||||
child: Hero(
|
||||
tag: Key(invoiceInfo.clearnetUrl),
|
||||
child: Center(
|
||||
child: AspectRatio(
|
||||
aspectRatio: 1.0,
|
||||
child: Container(
|
||||
padding: EdgeInsets.all(5),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(
|
||||
width: 3,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
child: QrImage(
|
||||
data: invoiceInfo.clearnetUrl,
|
||||
version: qr.QrVersions.auto,
|
||||
embeddedImagePath: args.qrImage,
|
||||
size: 230,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
Column(
|
||||
children: [
|
||||
CopyLinkItem(url: invoiceInfo.clearnetUrl, title: S.of(context).clearnet_link),
|
||||
SizedBox(height: 16),
|
||||
CopyLinkItem(url: invoiceInfo.onionUrl, title: S.of(context).onion_link),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/address_list.dart';
|
||||
import 'package:hash_wallet/src/widgets/gradient_background.dart';
|
||||
import 'package:hash_wallet/src/widgets/keyboard_done_button.dart';
|
||||
import 'package:hash_wallet/utils/share_util.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/base_page.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/qr_widget.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:keyboard_actions/keyboard_actions.dart';
|
||||
|
||||
class ReceivePage extends BasePage {
|
||||
ReceivePage({required this.addressListViewModel})
|
||||
: _cryptoAmountFocus = FocusNode(),
|
||||
_amountController = TextEditingController(),
|
||||
_formKey = GlobalKey<FormState>() {
|
||||
_amountController.addListener(() {
|
||||
if (_formKey.currentState!.validate()) {
|
||||
addressListViewModel.changeAmount(_amountController.text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
final WalletAddressListViewModel addressListViewModel;
|
||||
final TextEditingController _amountController;
|
||||
final GlobalKey<FormState> _formKey;
|
||||
static const _heroTag = 'receive_page';
|
||||
|
||||
@override
|
||||
String get title => S.current.receive;
|
||||
|
||||
@override
|
||||
bool get gradientBackground => true;
|
||||
|
||||
@override
|
||||
bool get resizeToAvoidBottomInset => true;
|
||||
|
||||
final FocusNode _cryptoAmountFocus;
|
||||
|
||||
@override
|
||||
Widget middle(BuildContext context) {
|
||||
return Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontSize: 18.0,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: pageIconColor(context),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget Function(BuildContext, Widget) get rootWrapper =>
|
||||
(BuildContext context, Widget scaffold) => GradientBackground(scaffold: scaffold);
|
||||
|
||||
@override
|
||||
Widget trailing(BuildContext context) {
|
||||
return Material(
|
||||
color: Colors.transparent,
|
||||
child: Semantics(
|
||||
label: S.of(context).share,
|
||||
child: IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(),
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
iconSize: 25,
|
||||
onPressed: () => ShareUtil.share(
|
||||
text: addressListViewModel.uri.toString(),
|
||||
context: context,
|
||||
),
|
||||
icon: Icon(
|
||||
Icons.share,
|
||||
size: 20,
|
||||
color: pageIconColor(context),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget body(BuildContext context) => KeyboardActions(
|
||||
config: KeyboardActionsConfig(
|
||||
keyboardActionsPlatform: KeyboardActionsPlatform.IOS,
|
||||
keyboardBarColor: Theme.of(context).colorScheme.surface,
|
||||
nextFocus: false,
|
||||
actions: [
|
||||
KeyboardActionsItem(
|
||||
focusNode: _cryptoAmountFocus,
|
||||
toolbarButtons: [(_) => KeyboardDoneButton()],
|
||||
)
|
||||
]),
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 50, 24, 24),
|
||||
child: QRWidget(
|
||||
addressListViewModel: addressListViewModel,
|
||||
formKey: _formKey,
|
||||
heroTag: _heroTag,
|
||||
amountTextFieldFocusNode: _cryptoAmountFocus,
|
||||
amountController: _amountController,
|
||||
),
|
||||
),
|
||||
AddressList(addressListViewModel: addressListViewModel),
|
||||
Padding(
|
||||
padding: EdgeInsets.fromLTRB(24, 24, 24, 32),
|
||||
child: Text(
|
||||
addressListViewModel.isSilentPayments
|
||||
? S.of(context).silent_payments_disclaimer
|
||||
: S.of(context).electrum_address_disclaimer,
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 15,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/utils/address_formatter.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/cupertino.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_slidable/flutter_slidable.dart';
|
||||
|
||||
class AddressCell extends StatelessWidget {
|
||||
AddressCell({
|
||||
required this.address,
|
||||
required this.name,
|
||||
required this.isCurrent,
|
||||
required this.isPrimary,
|
||||
required this.backgroundColor,
|
||||
required this.textColor,
|
||||
required this.walletType,
|
||||
this.onTap,
|
||||
this.onEdit,
|
||||
this.onHide,
|
||||
this.isHidden = false,
|
||||
this.onDelete,
|
||||
this.txCount,
|
||||
this.balance,
|
||||
this.isChange = false,
|
||||
this.hasBalance = false,
|
||||
this.hasReceived = false,
|
||||
});
|
||||
|
||||
factory AddressCell.fromItem(
|
||||
WalletAddressListItem item, {
|
||||
required bool isCurrent,
|
||||
required Color backgroundColor,
|
||||
required Color textColor,
|
||||
required WalletType walletType,
|
||||
Function(String)? onTap,
|
||||
bool hasBalance = false,
|
||||
bool hasReceived = false,
|
||||
Function()? onEdit,
|
||||
Function()? onHide,
|
||||
bool isHidden = false,
|
||||
Function()? onDelete,
|
||||
}) =>
|
||||
AddressCell(
|
||||
address: item.address,
|
||||
name: item.name ?? '',
|
||||
isCurrent: isCurrent,
|
||||
isPrimary: item.isPrimary,
|
||||
backgroundColor: backgroundColor,
|
||||
textColor: textColor,
|
||||
walletType: walletType,
|
||||
onTap: onTap,
|
||||
onEdit: onEdit,
|
||||
onHide: onHide,
|
||||
isHidden: isHidden,
|
||||
onDelete: onDelete,
|
||||
txCount: item.txCount,
|
||||
balance: item.balance,
|
||||
isChange: item.isChange,
|
||||
hasBalance: hasBalance,
|
||||
hasReceived: hasReceived,
|
||||
);
|
||||
|
||||
final String address;
|
||||
final String name;
|
||||
final bool isCurrent;
|
||||
final bool isPrimary;
|
||||
final Color backgroundColor;
|
||||
final Color textColor;
|
||||
final WalletType walletType;
|
||||
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;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final Widget cell = InkWell(
|
||||
onTap: () => onTap?.call(address),
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
color: backgroundColor,
|
||||
padding: EdgeInsets.only(left: 24, right: 24, top: 20, bottom: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: name.isNotEmpty
|
||||
? MainAxisAlignment.spaceBetween
|
||||
: MainAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
if (isChange)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 8.0),
|
||||
child: Container(
|
||||
height: 20,
|
||||
padding: EdgeInsets.all(4),
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(8.5)),
|
||||
color: textColor,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
S.of(context).unspent_change,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: backgroundColor,
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (name.isNotEmpty)
|
||||
Text(
|
||||
'$name',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Flexible(
|
||||
child: AddressFormatter.buildSegmentedAddress(
|
||||
address: address,
|
||||
walletType: walletType,
|
||||
shouldTruncate: name.isNotEmpty || address.length > 43,
|
||||
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: isChange ? 10 : 14,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hasBalance || hasReceived)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.max,
|
||||
children: [
|
||||
Text(
|
||||
'${hasReceived ? S.of(context).received : S.of(context).balance}: $balance',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'${S.of(context).transactions.toLowerCase()}: $txCount',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: textColor,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
));
|
||||
return onEdit == null
|
||||
? cell
|
||||
: Semantics(
|
||||
label: S.of(context).slidable,
|
||||
selected: isCurrent,
|
||||
enabled: !isCurrent,
|
||||
child: Slidable(
|
||||
key: Key(address),
|
||||
startActionPane: _actionPaneStart(context),
|
||||
endActionPane: _actionPaneEnd(context),
|
||||
child: cell,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
ActionPane _actionPaneEnd(BuildContext context) => ActionPane(
|
||||
motion: const ScrollMotion(),
|
||||
extentRatio: onDelete != null ? 0.4 : 0.3,
|
||||
children: [
|
||||
SlidableAction(
|
||||
onPressed: (_) => onHide?.call(),
|
||||
backgroundColor: isHidden
|
||||
? Theme.of(context).colorScheme.primaryContainer
|
||||
: Theme.of(context).colorScheme.errorContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
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: [
|
||||
SlidableAction(
|
||||
onPressed: (_) => onEdit?.call(),
|
||||
backgroundColor: Theme.of(context).colorScheme.primaryContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
icon: Icons.edit,
|
||||
label: S.of(context).edit,
|
||||
),
|
||||
if (onDelete != null)
|
||||
SlidableAction(
|
||||
onPressed: (_) => onDelete!.call(),
|
||||
backgroundColor: Theme.of(context).colorScheme.errorContainer,
|
||||
foregroundColor: Theme.of(context).colorScheme.onErrorContainer,
|
||||
icon: Icons.delete,
|
||||
label: S.of(context).delete,
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
import 'package:hash_wallet/di.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/routes.dart';
|
||||
import 'package:hash_wallet/src/screens/monero_accounts/monero_account_list_page.dart';
|
||||
import 'package:hash_wallet/src/screens/nano_accounts/nano_account_list_page.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/address_cell.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/header_tile.dart';
|
||||
import 'package:hash_wallet/src/widgets/section_divider.dart';
|
||||
import 'package:hash_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:hash_wallet/utils/list_item.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_account_list_header.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_hidden_list_header.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_header.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_item.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class AddressList extends StatefulWidget {
|
||||
const AddressList({super.key, required this.addressListViewModel, this.onSelect});
|
||||
|
||||
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.forceRecomputeItems, showHiddenAddresses);
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
|
||||
items = getItems(widget.addressListViewModel.items, showHiddenAddresses);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
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: widget.addressListViewModel,
|
||||
trailingButtonTap: () async {
|
||||
if (widget.addressListViewModel.type == WalletType.monero ||
|
||||
widget.addressListViewModel.type == WalletType.wownero) {
|
||||
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).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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 == widget.addressListViewModel.address.address && editable;
|
||||
final backgroundColor = isCurrent
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surface;
|
||||
final textColor = isCurrent
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurface;
|
||||
|
||||
return AddressCell.fromItem(
|
||||
item,
|
||||
isCurrent: isCurrent,
|
||||
walletType: widget.addressListViewModel.type,
|
||||
hasBalance: widget.addressListViewModel.isBalanceAvailable,
|
||||
hasReceived: widget.addressListViewModel.isReceivedAvailable,
|
||||
// hasReceived:
|
||||
backgroundColor: backgroundColor,
|
||||
textColor: textColor,
|
||||
onTap: (_) {
|
||||
if (widget.onSelect != null) {
|
||||
widget.onSelect!(item.address);
|
||||
return;
|
||||
}
|
||||
widget.addressListViewModel.setAddress(item);
|
||||
},
|
||||
onEdit: editable
|
||||
? () => 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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
void _hideAddress(WalletAddressListItem item) async {
|
||||
await widget.addressListViewModel.toggleHideAddress(item);
|
||||
updateItems();
|
||||
}
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class AnonpayCurrencyInputField extends StatelessWidget {
|
||||
const AnonpayCurrencyInputField(
|
||||
{super.key,
|
||||
required this.onTapPicker,
|
||||
required this.selectedCurrency,
|
||||
required this.focusNode,
|
||||
required this.controller,
|
||||
required this.minAmount,
|
||||
required this.maxAmount});
|
||||
final Function() onTapPicker;
|
||||
final Currency selectedCurrency;
|
||||
final FocusNode focusNode;
|
||||
final TextEditingController controller;
|
||||
final String minAmount;
|
||||
final String maxAmount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final arrowBottomPurple = Image.asset(
|
||||
'assets/images/arrow_bottom_purple_icon.png',
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
height: 8,
|
||||
);
|
||||
|
||||
return Column(
|
||||
children: [
|
||||
Container(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.only(top: 20),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: EdgeInsets.only(right: 8),
|
||||
height: 32,
|
||||
child: InkWell(
|
||||
onTap: onTapPicker,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: <Widget>[
|
||||
Padding(
|
||||
padding: EdgeInsets.only(right: 5),
|
||||
child: arrowBottomPurple,
|
||||
),
|
||||
Text(
|
||||
selectedCurrency.name.toUpperCase(),
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
if (selectedCurrency.tag != null)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 3.0),
|
||||
child: Container(
|
||||
height: 32,
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.all(Radius.circular(6)),
|
||||
),
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(6.0),
|
||||
child: Text(
|
||||
selectedCurrency.tag!,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(right: 4.0),
|
||||
child: Text(
|
||||
':',
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: BaseTextFormField(
|
||||
hasUnderlineBorder: true,
|
||||
borderWidth: 0.0,
|
||||
focusNode: focusNode,
|
||||
controller: controller,
|
||||
textInputAction: TextInputAction.next,
|
||||
enabled: true,
|
||||
textAlign: TextAlign.left,
|
||||
keyboardType:
|
||||
TextInputType.numberWithOptions(signed: false, decimal: true),
|
||||
inputFormatters: [FilteringTextInputFormatter.deny(RegExp('[\\-|\\ ]'))],
|
||||
hintText: '0.0000',
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
placeholderTextStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
validator: null,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Divider(height: 1, color: Theme.of(context).colorScheme.outlineVariant),
|
||||
Container(
|
||||
height: 15,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
S.of(context).min_value(minAmount, selectedCurrency.toString()),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
height: 1.2,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 10),
|
||||
Text(
|
||||
S.of(context).max_value(maxAmount, selectedCurrency.toString()),
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
height: 1.2,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
import 'package:hash_wallet/core/email_validator.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
import 'package:hash_wallet/src/screens/receive/widgets/anonpay_currency_input_field.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:hash_wallet/utils/show_pop_up.dart';
|
||||
import 'package:hash_wallet/view_model/anon_invoice_page_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
|
||||
class AnonInvoiceForm extends StatelessWidget {
|
||||
AnonInvoiceForm({
|
||||
super.key,
|
||||
required this.formKey,
|
||||
required this.anonInvoicePageViewModel,
|
||||
required this.isInvoice,
|
||||
required this.amountController,
|
||||
required this.nameController,
|
||||
required this.emailController,
|
||||
required this.descriptionController,
|
||||
required this.depositAmountFocus,
|
||||
}) : _nameFocusNode = FocusNode(),
|
||||
_emailFocusNode = FocusNode(),
|
||||
_descriptionFocusNode = FocusNode() {
|
||||
amountController.text = anonInvoicePageViewModel.amount;
|
||||
nameController.text = anonInvoicePageViewModel.receipientName;
|
||||
descriptionController.text = anonInvoicePageViewModel.description;
|
||||
emailController.text = anonInvoicePageViewModel.receipientEmail;
|
||||
}
|
||||
|
||||
final TextEditingController amountController;
|
||||
final TextEditingController nameController;
|
||||
final TextEditingController emailController;
|
||||
final TextEditingController descriptionController;
|
||||
final AnonInvoicePageViewModel anonInvoicePageViewModel;
|
||||
final FocusNode depositAmountFocus;
|
||||
final FocusNode _nameFocusNode;
|
||||
final FocusNode _emailFocusNode;
|
||||
final FocusNode _descriptionFocusNode;
|
||||
final GlobalKey<FormState> formKey;
|
||||
final bool isInvoice;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Form(
|
||||
key: formKey,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
isInvoice ? S.of(context).invoice_details : S.of(context).donation_link_details,
|
||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
if (isInvoice)
|
||||
Observer(
|
||||
builder: (_) {
|
||||
return AnonpayCurrencyInputField(
|
||||
onTapPicker: () => _presentPicker(context),
|
||||
controller: amountController,
|
||||
focusNode: depositAmountFocus,
|
||||
maxAmount: anonInvoicePageViewModel.maximum?.toString() ?? '...',
|
||||
minAmount: anonInvoicePageViewModel.minimum?.toString() ?? '...',
|
||||
selectedCurrency: anonInvoicePageViewModel.selectedCurrency,
|
||||
);
|
||||
},
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
BaseTextFormField(
|
||||
hasUnderlineBorder: true,
|
||||
controller: nameController,
|
||||
focusNode: _nameFocusNode,
|
||||
suffixIcon: SizedBox(width: 36),
|
||||
hintText: S.of(context).optional_name,
|
||||
textInputAction: TextInputAction.next,
|
||||
placeholderTextStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
validator: null,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
BaseTextFormField(
|
||||
hasUnderlineBorder: true,
|
||||
controller: descriptionController,
|
||||
focusNode: _descriptionFocusNode,
|
||||
textInputAction: TextInputAction.next,
|
||||
suffixIcon: SizedBox(width: 36),
|
||||
hintText: S.of(context).optional_description,
|
||||
placeholderTextStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
validator: null,
|
||||
),
|
||||
SizedBox(height: 24),
|
||||
BaseTextFormField(
|
||||
hasUnderlineBorder: true,
|
||||
controller: emailController,
|
||||
textInputAction: TextInputAction.next,
|
||||
focusNode: _emailFocusNode,
|
||||
suffixIcon: SizedBox(width: 36),
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
hintText: S.of(context).optional_email_hint,
|
||||
placeholderTextStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
textStyle: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
validator: EmailValidator(),
|
||||
),
|
||||
SizedBox(height: 52),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _presentPicker(BuildContext context) {
|
||||
showPopUp<void>(
|
||||
builder: (_) => CurrencyPicker(
|
||||
selectedAtIndex: anonInvoicePageViewModel.selectedCurrencyIndex,
|
||||
items: anonInvoicePageViewModel.currencies,
|
||||
hintText: S.of(context).search_currency,
|
||||
onItemSelected: anonInvoicePageViewModel.selectCurrency,
|
||||
),
|
||||
context: context,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,86 +0,0 @@
|
||||
import 'package:hash_wallet/anonpay/anonpay_invoice_info.dart';
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AnonInvoiceStatusSection extends StatelessWidget {
|
||||
const AnonInvoiceStatusSection({
|
||||
super.key,
|
||||
required this.invoiceInfo,
|
||||
});
|
||||
|
||||
final AnonpayInvoiceInfo invoiceInfo;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: 200,
|
||||
padding: EdgeInsets.all(19),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
borderRadius: BorderRadius.circular(30),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
S.current.status,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 10, vertical: 5),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SyncIndicatorIcon(
|
||||
boolMode: false,
|
||||
value: invoiceInfo.status,
|
||||
size: 6,
|
||||
),
|
||||
SizedBox(width: 5),
|
||||
Text(
|
||||
invoiceInfo.status,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
SizedBox(height: 27),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
'ID',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
invoiceInfo.invoiceId,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/utils/share_util.dart';
|
||||
import 'package:hash_wallet/utils/show_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CopyLinkItem extends StatelessWidget {
|
||||
const CopyLinkItem({super.key, required this.url, required this.title});
|
||||
final String url;
|
||||
final String title;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final copyImage = Image.asset('assets/images/copy_address.png',
|
||||
color: Theme.of(context).colorScheme.onSurface);
|
||||
|
||||
return Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: Theme.of(context).textTheme.bodyLarge?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface,
|
||||
),
|
||||
),
|
||||
SizedBox(width: 50),
|
||||
Row(
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: url));
|
||||
showBar<void>(context, S.of(context).copied_to_clipboard);
|
||||
},
|
||||
child: copyImage,
|
||||
),
|
||||
SizedBox(width: 20),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
constraints: BoxConstraints(),
|
||||
highlightColor: Colors.transparent,
|
||||
splashColor: Colors.transparent,
|
||||
iconSize: 25,
|
||||
onPressed: () => ShareUtil.share(text: url, context: context),
|
||||
icon: Icon(
|
||||
Icons.share,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
)
|
||||
],
|
||||
)
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:cw_core/amount/amount_sanitizer.dart';
|
||||
import 'package:cw_core/crypto_amount_format.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
@@ -198,8 +199,7 @@ class CurrencyAmountTextField extends StatelessWidget {
|
||||
),
|
||||
validator: isAmountEditable ? currencyValueValidator : null,
|
||||
onChanged: (value) {
|
||||
var sanitized =
|
||||
value.replaceAll(',', '.').withMaxDecimals(selectedCurrencyDecimals);
|
||||
var sanitized = value.sanitized().withMaxDecimals(selectedCurrencyDecimals);
|
||||
|
||||
if (selectedCurrencyDecimals == 0) {
|
||||
sanitized = sanitized.replaceAll('.', '');
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:hash_wallet/view_model/wallet_address_list/wallet_address_list_view_model.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class HeaderTile extends StatefulWidget {
|
||||
HeaderTile({
|
||||
required this.title,
|
||||
required this.walletAddressListViewModel,
|
||||
this.showSearchButton = false,
|
||||
this.showTrailingButton = false,
|
||||
this.trailingButtonTap,
|
||||
this.onSearchCallback,
|
||||
this.trailingIcon,
|
||||
});
|
||||
|
||||
final String title;
|
||||
final WalletAddressListViewModel walletAddressListViewModel;
|
||||
final bool showSearchButton;
|
||||
final bool showTrailingButton;
|
||||
final VoidCallback? trailingButtonTap;
|
||||
final VoidCallback? onSearchCallback;
|
||||
final Icon? trailingIcon;
|
||||
|
||||
@override
|
||||
_HeaderTileState createState() => _HeaderTileState();
|
||||
}
|
||||
|
||||
class _HeaderTileState extends State<HeaderTile> {
|
||||
bool _isSearchActive = false;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final searchIcon = Icon(Icons.search, color: Theme.of(context).colorScheme.primary);
|
||||
|
||||
return Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 24, vertical: 12),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
_isSearchActive
|
||||
? Expanded(
|
||||
child: BaseTextFormField(
|
||||
onChanged: (value) {
|
||||
widget.walletAddressListViewModel.updateSearchText(value);
|
||||
widget.onSearchCallback?.call();
|
||||
},
|
||||
cursorColor: Theme.of(context).colorScheme.onSurface,
|
||||
cursorWidth: 0.5,
|
||||
isDense: true,
|
||||
hintText: '${S.of(context).search}...',
|
||||
contentPadding: EdgeInsets.all(4),
|
||||
placeholderTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
autofocus: true,
|
||||
),
|
||||
)
|
||||
: Text(
|
||||
widget.title,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
Row(
|
||||
children: [
|
||||
if (widget.showSearchButton)
|
||||
GestureDetector(
|
||||
onTap: () {
|
||||
setState(() {
|
||||
_isSearchActive = !_isSearchActive;
|
||||
widget.walletAddressListViewModel.updateSearchText('');
|
||||
});
|
||||
},
|
||||
child: Container(
|
||||
height: 32,
|
||||
width: 32,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
child: searchIcon,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
if (widget.showTrailingButton)
|
||||
GestureDetector(
|
||||
onTap: widget.trailingButtonTap,
|
||||
child: Container(
|
||||
height: 32,
|
||||
width: 32,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context).colorScheme.secondaryContainer,
|
||||
),
|
||||
child: widget.trailingIcon,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import 'package:hash_wallet/generated/i18n.dart';
|
||||
import 'package:hash_wallet/new-ui/widgets/coins_page/token_image_widget.dart';
|
||||
import 'package:hash_wallet/src/widgets/hash_image_widget.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_svg/flutter_svg.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart' as qr;
|
||||
|
||||
class QrImage extends StatelessWidget {
|
||||
@@ -13,6 +13,8 @@ class QrImage extends StatelessWidget {
|
||||
this.version,
|
||||
this.errorCorrectionLevel = qr.QrErrorCorrectLevel.H,
|
||||
this.embeddedImagePath,
|
||||
this.badgeImageOnEmbeddedImagePath,
|
||||
this.semanticsLabel,
|
||||
});
|
||||
|
||||
final double? size;
|
||||
@@ -22,46 +24,65 @@ class QrImage extends StatelessWidget {
|
||||
final int? version;
|
||||
final int errorCorrectionLevel;
|
||||
final String? embeddedImagePath;
|
||||
final String? badgeImageOnEmbeddedImagePath;
|
||||
|
||||
/// Accessible name announced for the code itself. Defaults to a generic
|
||||
/// localized "QR code" so the code is always discoverable; pass a more
|
||||
/// specific label when the host screen knows what the code encodes.
|
||||
final String? semanticsLabel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final imagePath = embeddedImagePath ?? 'assets/images/qr-hashbag.png';
|
||||
final isSvg = imagePath.endsWith('.svg');
|
||||
final qrSize = size ?? 100.0;
|
||||
final logoSize = qrSize * 0.30;
|
||||
|
||||
if (isSvg) {
|
||||
final qrSize = size ?? 100.0;
|
||||
final logoSize = qrSize * 0.30;
|
||||
final centerImage = TokenImageWidget(
|
||||
imageUrl: imagePath,
|
||||
size: logoSize * 0.8,
|
||||
);
|
||||
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
Widget centerImageToUse;
|
||||
if (badgeImageOnEmbeddedImagePath != null) {
|
||||
centerImageToUse = Stack(
|
||||
children: [
|
||||
qr.QrImageView(
|
||||
data: data,
|
||||
errorCorrectionLevel: errorCorrectionLevel,
|
||||
version: version ?? qr.QrVersions.auto,
|
||||
size: qrSize,
|
||||
foregroundColor: foregroundColor,
|
||||
backgroundColor: backgroundColor,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
),
|
||||
HashImageWidget(
|
||||
imageUrl: imagePath,
|
||||
width: logoSize * 0.8,
|
||||
height: logoSize * 0.8,
|
||||
centerImage,
|
||||
Positioned(
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
child: Align(
|
||||
alignment: Alignment.bottomRight,
|
||||
child: HashImageWidget(
|
||||
imageUrl: badgeImageOnEmbeddedImagePath!,
|
||||
width: 24,
|
||||
height: 24,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
} else {
|
||||
return qr.QrImageView(
|
||||
data: data,
|
||||
errorCorrectionLevel: errorCorrectionLevel,
|
||||
version: version ?? qr.QrVersions.auto,
|
||||
size: size,
|
||||
foregroundColor: foregroundColor,
|
||||
backgroundColor: backgroundColor,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
embeddedImage: AssetImage(imagePath),
|
||||
);
|
||||
centerImageToUse = centerImage;
|
||||
}
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
qr.QrImageView(
|
||||
data: data,
|
||||
errorCorrectionLevel: errorCorrectionLevel,
|
||||
version: version ?? qr.QrVersions.auto,
|
||||
size: qrSize,
|
||||
foregroundColor: foregroundColor,
|
||||
backgroundColor: backgroundColor,
|
||||
padding: const EdgeInsets.all(12.0),
|
||||
// The package default is a hardcoded English "qr code"; use the
|
||||
// localized string so the code is announced in the user's language.
|
||||
semanticsLabel: semanticsLabel ?? S.of(context).scan_qr_code,
|
||||
),
|
||||
// The embedded logo and its badge are purely decorative — the node
|
||||
// above already announces the code — so keep them out of the tree.
|
||||
ExcludeSemantics(child: centerImageToUse),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user