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:
jwinterm
2026-10-24 23:49:29 -04:00
1406 changed files with 81062 additions and 48972 deletions
+18 -2
View File
@@ -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();
+6 -2
View File
@@ -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(
+10 -14
View File
@@ -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;
}
-515
View File
@@ -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;
}
}
-103
View File
@@ -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;
}
-87
View File
@@ -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,
),
)
],
),
),
);
}
}
+26 -7
View File
@@ -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,
+52 -4
View File
@@ -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,
),
)
],
),
);
}
}
+17 -32
View File
@@ -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,
),
),
);
},
),
),
],
),
),
);
}
}
+1
View File
@@ -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];
-790
View File
@@ -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;
}
}
-52
View File
@@ -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,
+63 -59
View File
@@ -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),
],
),
);
}
}
-125
View File
@@ -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,
),
),
],
),
],
),
);
}
}
+52 -31
View File
@@ -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