diff --git a/assets/images/birthday_cake.svg b/assets/images/birthday_cake.svg new file mode 100644 index 000000000..b5e31dddb --- /dev/null +++ b/assets/images/birthday_cake.svg @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/contact_icon.svg b/assets/images/contact_icon.svg new file mode 100644 index 000000000..6dbfcd5f4 --- /dev/null +++ b/assets/images/contact_icon.svg @@ -0,0 +1,3 @@ + + + diff --git a/lib/core/execution_state.dart b/lib/core/execution_state.dart index 6bc906010..860dfee10 100644 --- a/lib/core/execution_state.dart +++ b/lib/core/execution_state.dart @@ -2,6 +2,8 @@ abstract class ExecutionState {} class InitialExecutionState extends ExecutionState {} +class LoadingTemplateExecutingState extends ExecutionState {} + class IsExecutingState extends ExecutionState {} class ExecutedSuccessfullyState extends ExecutionState { diff --git a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart index fc3679104..4ac0cf12d 100644 --- a/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart +++ b/lib/src/screens/cake_pay/cards/cake_pay_confirm_purchase_card_page.dart @@ -7,10 +7,11 @@ import 'package:cake_wallet/src/screens/cake_pay/widgets/cake_pay_alert_modal.da import 'package:cake_wallet/src/screens/cake_pay/widgets/image_placeholder.dart'; import 'package:cake_wallet/src/screens/cake_pay/widgets/link_extractor.dart'; import 'package:cake_wallet/src/screens/cake_pay/widgets/text_icon_button.dart'; -import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; import 'package:cake_wallet/src/widgets/base_alert_dialog.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/standard_checkbox.dart'; @@ -371,39 +372,39 @@ class CakePayBuyCardDetailPage extends BasePage { }); final order = cakePayPurchaseViewModel.order; - final pendingTransaction = cakePayPurchaseViewModel.sendViewModel.pendingTransaction!; - await showPopUp( + showModalBottomSheet( context: context, - builder: (popupContext) { - return Observer( - builder: (_) => ConfirmSendingAlert( - alertTitle: S.of(popupContext).confirm_sending, - paymentId: S.of(popupContext).payment_id, - paymentIdValue: order?.orderId, - expirationTime: cakePayPurchaseViewModel.formattedRemainingTime, - onDispose: () => _handleDispose(disposer), - amount: S.of(popupContext).send_amount, - amountValue: pendingTransaction.amountFormatted, - fiatAmountValue: - cakePayPurchaseViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, - fee: S.of(popupContext).send_fee, - feeValue: pendingTransaction.feeFormatted, - feeFiatAmount: - cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, - feeRate: pendingTransaction.feeRate, - outputs: cakePayPurchaseViewModel.sendViewModel.outputs, - rightButtonText: S.of(popupContext).send, - leftButtonText: S.of(popupContext).cancel, - actionRightButton: () async { - Navigator.of(context).pop(); - await cakePayPurchaseViewModel.sendViewModel.commitTransaction(context); - }, - actionLeftButton: () => Navigator.of(popupContext).pop())); + isDismissible: false, + isScrollControlled: true, + builder: (BuildContext popupContext) { + return ConfirmSendingBottomSheet( + key: ValueKey('send_page_confirm_sending_dialog_key'), + currentTheme: currentTheme, + paymentId: S.of(popupContext).payment_id, + paymentIdValue: order?.orderId, + expirationTime: cakePayPurchaseViewModel.formattedRemainingTime, + titleText: 'Confirm Transaction', + titleIconPath: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency.iconPath, + currency: cakePayPurchaseViewModel.sendViewModel.selectedCryptoCurrency, + amount: S.of(popupContext).send_amount, + amountValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.amountFormatted, + fiatAmountValue: cakePayPurchaseViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, + fee: S.of(popupContext).send_fee, + feeValue: cakePayPurchaseViewModel.sendViewModel.pendingTransaction!.feeFormatted, + feeFiatAmount: cakePayPurchaseViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, + outputs: cakePayPurchaseViewModel.sendViewModel.outputs, + onSlideComplete: () async { + Navigator.of(popupContext).pop(); + cakePayPurchaseViewModel.sendViewModel.commitTransaction(context); + }, + ); }, ); } + BuildContext? loadingBottomSheetContext; + void _setEffects(BuildContext context) { if (_effectsInstalled) { return; @@ -416,6 +417,29 @@ class CakePayBuyCardDetailPage extends BasePage { }); } + if (state is! IsExecutingState && + loadingBottomSheetContext != null && + loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + + if (state is IsExecutingState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: 'Generating transaction', + ); + }, + ); + } + }); + } + if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) async { await _showConfirmSendingAlert(context); diff --git a/lib/src/screens/exchange_trade/exchange_trade_page.dart b/lib/src/screens/exchange_trade/exchange_trade_page.dart index 6adb08de8..243ab63c8 100644 --- a/lib/src/screens/exchange_trade/exchange_trade_page.dart +++ b/lib/src/screens/exchange_trade/exchange_trade_page.dart @@ -1,7 +1,10 @@ +import 'package:cake_wallet/reactions/wallet_connect.dart'; import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart'; import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart'; import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/theme_base.dart'; import 'dart:ui'; @@ -13,7 +16,6 @@ import 'package:flutter/material.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart'; -import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:cake_wallet/utils/show_pop_up.dart'; import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart'; import 'package:cake_wallet/view_model/send/send_view_model_state.dart'; @@ -209,6 +211,8 @@ class ExchangeTradeState extends State { ); } + BuildContext? loadingBottomSheetContext; + void _setEffects() { if (_effectsInstalled) { return; @@ -216,6 +220,13 @@ class ExchangeTradeState extends State { _exchangeStateReaction = reaction((_) => this.widget.exchangeTradeViewModel.sendViewModel.state, (ExecutionState state) { + + if (state is! IsExecutingState && + loadingBottomSheetContext != null && + loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( @@ -232,163 +243,92 @@ class ExchangeTradeState extends State { }); } + if (state is IsExecutingState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: 'Generating transaction', + ); + }, + ); + } + }); + } + if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) { - showPopUp( + if (context.mounted) { + showModalBottomSheet( context: context, - builder: (BuildContext popupContext) { - return ConfirmSendingAlert( - key: ValueKey('exchange_trade_page_confirm_sending_dialog_key'), - alertLeftActionButtonKey: ValueKey('exchange_trade_page_confirm_sending_dialog_cancel_button_key'), - alertRightActionButtonKey: - ValueKey('exchange_trade_page_confirm_sending_dialog_send_button_key'), - alertTitle: S.of(popupContext).confirm_sending, - amount: S.of(popupContext).send_amount, - amountValue: widget.exchangeTradeViewModel.sendViewModel - .pendingTransaction!.amountFormatted, - fee: S.of(popupContext).send_fee, - feeValue: widget.exchangeTradeViewModel.sendViewModel - .pendingTransaction!.feeFormatted, - feeRate: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeRate, - rightButtonText: S.of(popupContext).send, - leftButtonText: S.of(popupContext).cancel, - actionRightButton: () async { - Navigator.of(popupContext).pop(); - await widget.exchangeTradeViewModel.sendViewModel - .commitTransaction(context); - transactionStatePopup(); - }, - actionLeftButton: () => Navigator.of(popupContext).pop(), - feeFiatAmount: widget.exchangeTradeViewModel - .pendingTransactionFeeFiatAmountFormatted, - fiatAmountValue: widget.exchangeTradeViewModel - .pendingTransactionFiatAmountValueFormatted, - outputs: widget.exchangeTradeViewModel.sendViewModel - .outputs); - }); + isDismissible: false, + isScrollControlled: true, + builder: (BuildContext bottomSheetContext) { + return ConfirmSendingBottomSheet( + key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'), + currentTheme: widget.currentTheme, + titleText: 'Confirm Transaction', + titleIconPath: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency.iconPath, + currency: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency, + amount: S.of(bottomSheetContext).send_amount, + amountValue: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.amountFormatted, + fiatAmountValue: widget.exchangeTradeViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, + fee: isEVMCompatibleChain(widget.exchangeTradeViewModel.sendViewModel.walletType) + ? S.of(bottomSheetContext).send_estimated_fee + : S.of(bottomSheetContext).send_fee, + feeValue: widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.feeFormatted, + feeFiatAmount: widget.exchangeTradeViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, + outputs: widget.exchangeTradeViewModel.sendViewModel.outputs, + onSlideComplete: () async { + Navigator.of(bottomSheetContext).pop(); + widget.exchangeTradeViewModel.sendViewModel.commitTransaction(context); + }, + ); + }, + ); + } }); } if (state is TransactionCommitted) { - WidgetsBinding.instance.addPostFrameCallback((_) { - if (mounted) { - showPopUp( - context: context, - builder: (BuildContext popupContext) { - return AlertWithOneAction( - alertTitle: S.of(popupContext).sending, - alertContent: S.of(popupContext).transaction_sent, - buttonText: S.of(popupContext).ok, - buttonAction: () => Navigator.of(popupContext).pop()); - }); + WidgetsBinding.instance.addPostFrameCallback((_) async { + if (!context.mounted) { + return; } + + + await showModalBottomSheet( + context: context, + isScrollControlled: true, + builder: (BuildContext bottomSheetContext) { + return InfoBottomSheet( + currentTheme: widget.currentTheme, + titleText: 'Transaction Sent', + contentImage: 'assets/images/birthday_cake.svg', + actionButtonText: S.of(bottomSheetContext).close, + actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'), + actionButton: () { + Navigator.of(bottomSheetContext).pop(); + Navigator.of(context).pushNamedAndRemoveUntil( + Routes.dashboard, + (route) => false, + ); + RequestReviewHandler.requestReview(); + }); + }, + ); + }); } + }); _effectsInstalled = true; } - - void transactionStatePopup() { - if (this.mounted) { - showPopUp( - context: context, - builder: (BuildContext popupContext) { - return Observer(builder: (_) { - final state = widget - .exchangeTradeViewModel.sendViewModel.state; - - if (state is TransactionCommitted) { - return Stack( - children: [ - Container( - color: Theme.of(popupContext).colorScheme.background, - child: Center( - child: Image.asset( - 'assets/images/birthday_cake.png'), - ), - ), - Center( - child: Padding( - padding: EdgeInsets.only( - top: 220, left: 24, right: 24), - child: Text( - S.of(popupContext).send_success(widget - .exchangeTradeViewModel - .wallet - .currency - .toString()), - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Theme.of(popupContext).extension()!.titleColor, - decoration: TextDecoration.none, - ), - ), - ), - ), - Positioned( - left: 24, - right: 24, - bottom: 24, - child: PrimaryButton( - onPressed: () { - Navigator.pushNamedAndRemoveUntil( - popupContext, - Routes.dashboard, - (route) => false, - ); - RequestReviewHandler.requestReview(); - }, - text: S.of(popupContext).got_it, - color: Theme.of(popupContext).primaryColor, - textColor: Colors.white)) - ], - ); - } - - return Stack( - children: [ - Container( - color: Theme.of(popupContext).colorScheme.background, - child: Center( - child: Image.asset( - 'assets/images/birthday_cake.png'), - ), - ), - BackdropFilter( - filter: ImageFilter.blur( - sigmaX: 3.0, sigmaY: 3.0), - child: Container( - decoration: BoxDecoration( - color: Theme.of(popupContext) - .colorScheme - .background - .withOpacity(0.25)), - child: Center( - child: Padding( - padding: EdgeInsets.only(top: 220), - child: Text( - S.of(popupContext).send_sending, - textAlign: TextAlign.center, - style: TextStyle( - fontSize: 22, - fontWeight: FontWeight.bold, - color: Theme.of(popupContext).extension()!.titleColor, - decoration: TextDecoration.none, - ), - ), - ), - ), - ), - ) - ], - ); - }); - }); - } - } } class _ExchangeTradeItemsCardSection extends StatelessWidget { diff --git a/lib/src/screens/send/send_page.dart b/lib/src/screens/send/send_page.dart index 7e7080d0f..fbbb6c0e1 100644 --- a/lib/src/screens/send/send_page.dart +++ b/lib/src/screens/send/send_page.dart @@ -12,18 +12,20 @@ import 'package:cake_wallet/routes.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart'; import 'package:cake_wallet/src/screens/dashboard/widgets/sync_indicator_icon.dart'; -import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:cake_wallet/src/screens/send/widgets/send_card.dart'; import 'package:cake_wallet/src/widgets/adaptable_page_view.dart'; import 'package:cake_wallet/src/widgets/add_template_button.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/keyboard_done_button.dart'; import 'package:cake_wallet/src/widgets/picker.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/scollable_with_bottom_section.dart'; import 'package:cake_wallet/src/widgets/template_tile.dart'; import 'package:cake_wallet/src/widgets/trail_button.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; import 'package:cake_wallet/themes/extensions/keyboard_theme.dart'; import 'package:cake_wallet/themes/extensions/seed_widget_theme.dart'; import 'package:cake_wallet/themes/extensions/send_page_theme.dart'; @@ -162,6 +164,8 @@ class SendPage extends BasePage { }); }); + bool _bottomSheetOpened = false; + @override Widget body(BuildContext context) { _setEffects(context); @@ -290,7 +294,7 @@ class SendPage extends BasePage { ? template.cryptoCurrency : template.fiatCurrency, onTap: () async { - sendViewModel.state = IsExecutingState(); + sendViewModel.state = LoadingTemplateExecutingState(); if (template.additionalRecipients?.isNotEmpty ?? false) { sendViewModel.clearOutputs(); @@ -464,7 +468,8 @@ class SendPage extends BasePage { textColor: Colors.white, isLoading: sendViewModel.state is IsExecutingState || sendViewModel.state is TransactionCommitting || - sendViewModel.state is IsAwaitingDeviceResponseState, + sendViewModel.state is IsAwaitingDeviceResponseState || + sendViewModel.state is LoadingTemplateExecutingState, isDisabled: !sendViewModel.isReadyForSend, ); }, @@ -479,6 +484,7 @@ class SendPage extends BasePage { } BuildContext? dialogContext; + BuildContext? loadingBottomSheetContext; void _setEffects(BuildContext context) { if (_effectsInstalled) { @@ -494,6 +500,13 @@ class SendPage extends BasePage { Navigator.of(dialogContext!).pop(); } + if (state is! IsExecutingState && + loadingBottomSheetContext != null && + loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + + if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( @@ -510,92 +523,110 @@ class SendPage extends BasePage { }); } - if (state is ExecutedSuccessfullyState) { + if (state is IsExecutingState) { WidgetsBinding.instance.addPostFrameCallback((_) { if (context.mounted) { - showPopUp( - context: context, - builder: (BuildContext _dialogContext) { - return ConfirmSendingAlert( - key: ValueKey('send_page_confirm_sending_dialog_key'), - alertTitle: S.of(_dialogContext).confirm_sending, - amount: S.of(_dialogContext).send_amount, - amountValue: sendViewModel.pendingTransaction!.amountFormatted, - fiatAmountValue: sendViewModel.pendingTransactionFiatAmountFormatted, - fee: isEVMCompatibleChain(sendViewModel.walletType) - ? S.of(_dialogContext).send_estimated_fee - : S.of(_dialogContext).send_fee, - feeRate: sendViewModel.pendingTransaction!.feeRate, - feeValue: sendViewModel.pendingTransaction!.feeFormatted, - feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted, - outputs: sendViewModel.outputs, - change: sendViewModel.pendingTransaction!.change, - rightButtonText: S.of(_dialogContext).send, - leftButtonText: S.of(_dialogContext).cancel, - alertRightActionButtonKey: - ValueKey('send_page_confirm_sending_dialog_send_button_key'), - alertLeftActionButtonKey: - ValueKey('send_page_confirm_sending_dialog_cancel_button_key'), - actionRightButton: () async { - Navigator.of(_dialogContext).pop(); - sendViewModel.commitTransaction(context); - }, - actionLeftButton: () => Navigator.of(_dialogContext).pop()); - }); + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: 'Generating transaction', + ); + }, + ); } }); } + if (state is ExecutedSuccessfullyState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + isScrollControlled: true, + builder: (BuildContext bottomSheetContext) { + return ConfirmSendingBottomSheet( + key: ValueKey('send_page_confirm_sending_dialog_key'), + titleText: 'Confirm Transaction', + currentTheme: currentTheme, + titleIconPath: sendViewModel.selectedCryptoCurrency.iconPath, + currency: sendViewModel.selectedCryptoCurrency, + amount: S.of(bottomSheetContext).send_amount, + amountValue: sendViewModel.pendingTransaction!.amountFormatted, + fiatAmountValue: sendViewModel.pendingTransactionFiatAmountFormatted, + fee: isEVMCompatibleChain(sendViewModel.walletType) + ? S.of(bottomSheetContext).send_estimated_fee + : S.of(bottomSheetContext).send_fee, + feeValue: sendViewModel.pendingTransaction!.feeFormatted, + feeFiatAmount: sendViewModel.pendingTransactionFeeFiatAmountFormatted, + outputs: sendViewModel.outputs, + onSlideComplete: () async { + Navigator.of(bottomSheetContext).pop(); + sendViewModel.commitTransaction(context); + }, + change: sendViewModel.pendingTransaction!.change, + ); + }, + ); + } + }); + } + + + if (state is TransactionCommitted) { WidgetsBinding.instance.addPostFrameCallback((_) async { if (!context.mounted) { return; } - final successMessage = - S.of(context).send_success(sendViewModel.selectedCryptoCurrency.toString()); - - final waitMessage = sendViewModel.walletType == WalletType.solana - ? '. ${S.of(context).waitFewSecondForTxUpdate}' - : ''; - - String alertContent = "$successMessage$waitMessage"; - - await Navigator.of(context) - .pushNamed(Routes.transactionSuccessPage, arguments: alertContent); - newContactAddress = newContactAddress ?? sendViewModel.newContactAddress(); + if (newContactAddress?.address != null && isRegularElectrumAddress(newContactAddress!.address)) { newContactAddress = null; } - if (sendViewModel.coinTypeToSpendFrom != UnspentCoinType.any) newContactAddress = null; - - if (newContactAddress != null && sendViewModel.showAddressBookPopup) { - await showPopUp( - context: context, - builder: (BuildContext _dialogContext) => AlertWithTwoActions( - alertDialogKey: ValueKey('send_page_sent_dialog_key'), - alertTitle: '', - alertContent: S.of(_dialogContext).add_contact_to_address_book, - rightButtonText: S.of(_dialogContext).add_contact, - leftButtonText: S.of(_dialogContext).ignor, - alertLeftActionButtonKey: ValueKey('send_page_sent_dialog_ignore_button_key'), - alertRightActionButtonKey: - ValueKey('send_page_sent_dialog_add_contact_button_key'), - actionRightButton: () { - Navigator.of(_dialogContext).pop(); - RequestReviewHandler.requestReview(); - Navigator.of(context) - .pushNamed(Routes.addressBookAddContact, arguments: newContactAddress); - newContactAddress = null; - }, - actionLeftButton: () { - Navigator.of(_dialogContext).pop(); - RequestReviewHandler.requestReview(); - newContactAddress = null; - })); - } + await showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext bottomSheetContext) { + return newContactAddress != null && sendViewModel.showAddressBookPopup + ? InfoBottomSheet( + currentTheme: currentTheme, + showDontAskMeCheckbox: true, + onCheckboxChanged: (value) => sendViewModel.setShowAddressBookPopup(!value), + titleText: 'Transaction Sent', + contentImage: 'assets/images/contact_icon.svg', + contentImageColor: Theme.of(context).extension()!.titleColor, + content: S.of(bottomSheetContext).add_contact_to_address_book, + isTwoAction: true, + leftButtonText: 'No', + rightButtonText: 'Yes', + actionLeftButton: () { + Navigator.of(bottomSheetContext).pop(); + RequestReviewHandler.requestReview(); + newContactAddress = null; + }, + actionRightButton: () { + Navigator.of(bottomSheetContext).pop(); + RequestReviewHandler.requestReview(); + Navigator.of(context) + .pushNamed(Routes.addressBookAddContact, arguments: newContactAddress); + newContactAddress = null; + }, + ) + : InfoBottomSheet( + currentTheme: currentTheme, + titleText: 'Transaction Sent', + contentImage: 'assets/images/birthday_cake.svg', + actionButtonText: S.of(bottomSheetContext).close, + actionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'), + actionButton: () => Navigator.of(bottomSheetContext).pop()); + }, + ); if (initialPaymentRequest?.callbackUrl?.isNotEmpty ?? false) { // wait a second so it's not as jarring: diff --git a/lib/src/screens/transaction_details/rbf_details_page.dart b/lib/src/screens/transaction_details/rbf_details_page.dart index 2c5edd8b4..3b6513a03 100644 --- a/lib/src/screens/transaction_details/rbf_details_page.dart +++ b/lib/src/screens/transaction_details/rbf_details_page.dart @@ -1,7 +1,6 @@ import 'package:cake_wallet/core/execution_state.dart'; import 'package:cake_wallet/generated/i18n.dart'; import 'package:cake_wallet/src/screens/base_page.dart'; -import 'package:cake_wallet/src/screens/send/widgets/confirm_sending_alert.dart'; import 'package:cake_wallet/src/screens/transaction_details/rbf_details_list_fee_picker_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart'; import 'package:cake_wallet/src/screens/transaction_details/textfield_list_item.dart'; @@ -9,6 +8,8 @@ import 'package:cake_wallet/src/screens/transaction_details/transaction_expandab import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart'; import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart'; +import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart'; import 'package:cake_wallet/src/widgets/list_row.dart'; import 'package:cake_wallet/src/widgets/primary_button.dart'; import 'package:cake_wallet/src/widgets/standard_expandable_list.dart'; @@ -110,12 +111,21 @@ class RBFDetailsPage extends BasePage { ); } + BuildContext? loadingBottomSheetContext; + void _setEffects(BuildContext context) { if (_effectsInstalled) { return; } reaction((_) => transactionDetailsViewModel.sendViewModel.state, (ExecutionState state) { + + if (state is! IsExecutingState && + loadingBottomSheetContext != null && + loadingBottomSheetContext!.mounted) { + Navigator.of(loadingBottomSheetContext!).pop(); + } + if (state is FailureState) { WidgetsBinding.instance.addPostFrameCallback((_) { showPopUp( @@ -151,35 +161,56 @@ class RBFDetailsPage extends BasePage { }); } + if (state is IsExecutingState) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (context.mounted) { + showModalBottomSheet( + context: context, + isDismissible: false, + builder: (BuildContext context) { + loadingBottomSheetContext = context; + return LoadingBottomSheet( + titleText: 'Generating transaction', + ); + }, + ); + } + }); + } + if (state is ExecutedSuccessfullyState) { WidgetsBinding.instance.addPostFrameCallback((_) { - showPopUp( + if (context.mounted) { + showModalBottomSheet( context: context, - builder: (BuildContext popupContext) { - return ConfirmSendingAlert( - alertTitle: S.of(popupContext).confirm_sending, - amount: S.of(popupContext).send_amount, - amountValue: transactionDetailsViewModel - .sendViewModel.pendingTransaction!.amountFormatted, - fee: S.of(popupContext).send_fee, - feeValue: - transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted, - rightButtonText: S.of(popupContext).send, - leftButtonText: S.of(popupContext).cancel, - actionRightButton: () async { - Navigator.of(popupContext).pop(); - await transactionDetailsViewModel.sendViewModel.commitTransaction(context); - try { - Navigator.of(popupContext).pop(); - } catch (_) {} - }, - actionLeftButton: () => Navigator.of(popupContext).pop(), - feeFiatAmount: - transactionDetailsViewModel.pendingTransactionFeeFiatAmountFormatted, - fiatAmountValue: - transactionDetailsViewModel.pendingTransactionFiatAmountValueFormatted, - outputs: transactionDetailsViewModel.sendViewModel.outputs); - }); + isDismissible: false, + isScrollControlled: true, + builder: (BuildContext bottomSheetContext) { + return ConfirmSendingBottomSheet( + key: ValueKey('rbf_confirm_sending_bottom_sheet'), + titleText: 'Confirm Transaction', + currentTheme: currentTheme, + titleIconPath: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency.iconPath, + currency: transactionDetailsViewModel.sendViewModel.selectedCryptoCurrency, + amount: S.of(bottomSheetContext).send_amount, + amountValue: transactionDetailsViewModel.sendViewModel.pendingTransaction!.amountFormatted, + fiatAmountValue: transactionDetailsViewModel.sendViewModel.pendingTransactionFiatAmountFormatted, + fee: S.of(bottomSheetContext).send_fee, + feeValue: transactionDetailsViewModel.sendViewModel.pendingTransaction!.feeFormatted, + feeFiatAmount: transactionDetailsViewModel.sendViewModel.pendingTransactionFeeFiatAmountFormatted, + outputs: transactionDetailsViewModel.sendViewModel.outputs, + onSlideComplete: () async { + Navigator.of(bottomSheetContext).pop(); + await transactionDetailsViewModel.sendViewModel.commitTransaction(context); + try { + Navigator.of(bottomSheetContext).pop(); + } catch (_) {} + }, + change: transactionDetailsViewModel.sendViewModel.pendingTransaction!.change, + ); + }, + ); + } }); } diff --git a/lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart new file mode 100644 index 000000000..206780eae --- /dev/null +++ b/lib/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart @@ -0,0 +1,83 @@ +import 'package:flutter/material.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; + +abstract class BaseBottomSheet extends StatelessWidget { + final String titleText; + final String? titleIconPath; + + const BaseBottomSheet({required this.titleText, this.titleIconPath}); + + Widget headerWidget(BuildContext context) { + return Column( + children: [ + Padding( + padding: const EdgeInsets.symmetric(vertical: 16), + child: Row( + children: [ + const Spacer(flex: 4), + Expanded( + flex: 2, + child: Container( + height: 6, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(4), + color: Theme.of(context).extension()!.titleColor, + ), + ), + ), + const Spacer(flex: 4), + ], + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (titleIconPath != null) + Image.asset(titleIconPath!, height: 24, width: 24) + else + Container(), + const SizedBox(width: 6), + Text( + titleText, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + ], + ), + const SizedBox(height: 13), + ], + ); + } + + Widget contentWidget(BuildContext context); + + Widget footerWidget(BuildContext context); + + @override + Widget build(BuildContext context) { + return ConstrainedBox( + constraints: BoxConstraints(maxHeight: 600), + child: ClipRRect( + borderRadius: const BorderRadius.only( + topLeft: Radius.circular(30.0), topRight: Radius.circular(30.0)), + child: Container( + color: Theme.of(context).dialogBackgroundColor, + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + headerWidget(context), + contentWidget(context), + footerWidget(context), + ], + ), + ), + ), + ); + } +} diff --git a/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart new file mode 100644 index 000000000..bf16695e9 --- /dev/null +++ b/lib/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart @@ -0,0 +1,450 @@ +import 'package:cake_wallet/generated/i18n.dart'; +import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart'; +import 'package:cake_wallet/themes/extensions/balance_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:cake_wallet/view_model/send/output.dart'; +import 'package:cw_core/crypto_currency.dart'; +import 'package:cw_core/pending_transaction.dart'; +import 'package:flutter/material.dart'; +import 'dart:math' as math; + +import 'base_bottom_sheet_widget.dart'; + +class ConfirmSendingBottomSheet extends BaseBottomSheet { + final CryptoCurrency currency; + final ThemeBase currentTheme; + final String? paymentId; + final String? paymentIdValue; + final String? expirationTime; + final String amount; + final String amountValue; + final String fiatAmountValue; + final String fee; + final String feeValue; + final String feeFiatAmount; + final List outputs; + final VoidCallback onSlideComplete; + final PendingChange? change; + + ConfirmSendingBottomSheet({ + required String titleText, + String? titleIconPath, + required this.currency, + required this.currentTheme, + this.paymentId, + this.paymentIdValue, + this.expirationTime, + required this.amount, + required this.amountValue, + required this.fiatAmountValue, + required this.fee, + required this.feeValue, + required this.feeFiatAmount, + required this.outputs, + required this.onSlideComplete, + this.change, + Key? key, + }) : showScrollbar = outputs.length > 3, + super(titleText: titleText, titleIconPath: titleIconPath); + + final bool showScrollbar; + final ScrollController scrollController = ScrollController(); + + @override + Widget contentWidget(BuildContext context) { + final itemTitleTextStyle = TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ); + final itemSubTitleTextStyle = TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: currentTheme.type == ThemeType.bright + ? Theme.of(context).extension()!.titleColor + : Theme.of(context).extension()!.labelTextColor, + decoration: TextDecoration.none, + ); + + Widget content = Padding( + padding: EdgeInsets.fromLTRB(8, 0, showScrollbar ? 16 : 8, 8), + child: Column( + children: [ + if (paymentId != null && paymentIdValue != null) + Padding( + padding: const EdgeInsets.only(bottom: 8), + child: AddressTile( + itemTitle: paymentId!, + currentTheme: currentTheme, + itemTitleTextStyle: itemTitleTextStyle, + isBatchSending: false, + amount: '', + address: paymentIdValue!, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ), + ), + StandardTile( + itemTitle: amount, + itemValue: amountValue + ' ${currency.title}', + itemTitleTextStyle: itemTitleTextStyle, + itemSubTitle: fiatAmountValue, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ), + const SizedBox(height: 8), + StandardTile( + itemTitle: fee, + itemValue: feeValue, + itemTitleTextStyle: itemTitleTextStyle, + itemSubTitle: feeFiatAmount, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ), + const SizedBox(height: 8), + Column( + children: [ + ListView.separated( + padding: const EdgeInsets.only(top: 0), + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + itemCount: outputs.length, + separatorBuilder: (context, index) => const SizedBox(height: 8), + itemBuilder: (context, index) { + final bool isBatchSending = outputs.length > 1; + final item = outputs[index]; + final contactName = item.parsedAddress.name; + final batchContactTitle = + '${index + 1}/${outputs.length} - ${contactName.isEmpty ? 'Address' : contactName}'; + final _address = item.isParsedAddress ? item.extractedAddress : item.address; + final _amount = item.cryptoAmount.replaceAll(',', '.') + ' ${currency.title}'; + return isBatchSending || contactName.isNotEmpty + ? AddressExpansionTile( + contactType: 'Contact', + currentTheme: currentTheme, + name: isBatchSending ? batchContactTitle : contactName, + address: _address, + amount: _amount, + isBatchSending: isBatchSending, + itemTitleTextStyle: itemTitleTextStyle, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ) + : AddressTile( + itemTitle: 'Address', + currentTheme: currentTheme, + itemTitleTextStyle: itemTitleTextStyle, + isBatchSending: isBatchSending, + amount: _amount, + address: _address, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ); + }, + ), + if (change != null) + Padding( + padding: const EdgeInsets.only(top: 8), + child: AddressExpansionTile( + contactType: 'Change', + currentTheme: currentTheme, + name: S.of(context).send_change_to_you, + address: change!.address, + amount: change!.amount + ' ${currency.title}', + isBatchSending: true, + itemTitleTextStyle: itemTitleTextStyle, + itemSubTitleTextStyle: itemSubTitleTextStyle, + ), + ), + ], + ), + const SizedBox(height: 8), + ], + ), + ); + + if (showScrollbar) { + return SizedBox( + height: 380, + child: Scrollbar( + controller: scrollController, + thumbVisibility: true, + child: SingleChildScrollView( + controller: scrollController, + physics: const BouncingScrollPhysics(), + child: content, + ), + ), + ); + } else { + return content; + } + } + + @override + Widget footerWidget(BuildContext context) { + return Container( + padding: const EdgeInsets.fromLTRB(40, 12, 40, 34), + decoration: BoxDecoration( + color: Theme.of(context).dialogBackgroundColor, + boxShadow: [ + if (showScrollbar) + BoxShadow( + color: Colors.black.withOpacity(0.5), + spreadRadius: 2, + blurRadius: 10, + offset: const Offset(0, 0), + ), + ], + ), + child: StandardSlideButton( + onSlideComplete: onSlideComplete, + buttonText: 'Swipe to send', + currentTheme: currentTheme, + ), + ); + } +} + +class StandardTile extends StatelessWidget { + const StandardTile({ + super.key, + required this.itemTitle, + required this.itemValue, + required this.itemTitleTextStyle, + this.itemSubTitle, + required this.itemSubTitleTextStyle, + }); + + final String itemTitle; + final String itemValue; + final TextStyle itemTitleTextStyle; + final String? itemSubTitle; + final TextStyle itemSubTitleTextStyle; + + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).extension()!.buttonColor), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(itemTitle, style: itemTitleTextStyle), + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text(itemValue, style: itemTitleTextStyle), + itemSubTitle == null + ? Container() + : Text(itemSubTitle!, style: itemSubTitleTextStyle), + ], + ), + ], + ), + ); + } +} + +class AddressTile extends StatelessWidget { + const AddressTile({ + super.key, + required this.itemTitle, + required this.currentTheme, + required this.itemTitleTextStyle, + required this.isBatchSending, + required this.amount, + required this.address, + required this.itemSubTitleTextStyle, + }); + + final String itemTitle; + final ThemeBase currentTheme; + final TextStyle itemTitleTextStyle; + final bool isBatchSending; + final String amount; + final String address; + final TextStyle itemSubTitleTextStyle; + + @override + Widget build(BuildContext context) { + final addressTextStyle = TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: currentTheme.type == ThemeType.bright + ? Theme.of(context).extension()!.labelTextColor + : Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ); + return Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 8), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: Theme.of(context).extension()!.buttonColor, + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(itemTitle, style: itemTitleTextStyle), + if (isBatchSending) Text(amount, style: itemTitleTextStyle), + ], + ), + buildSegmentedAddress( + address: address, + evenTextStyle: currentTheme.type == ThemeType.bright + ? itemSubTitleTextStyle + : addressTextStyle, + oddTextStyle: itemSubTitleTextStyle, + ), + ], + ), + ); + } + + Widget buildSegmentedAddress({ + required String address, + int chunkSize = 6, + required TextStyle evenTextStyle, + required TextStyle oddTextStyle, + }) { + final spans = []; + + int index = 0; + for (int i = 0; i < address.length; i += chunkSize) { + final chunk = address.substring(i, math.min(i + chunkSize, address.length)); + final style = (index % 2 == 0) ? evenTextStyle : oddTextStyle; + + spans.add( + TextSpan(text: '$chunk ', style: style), + ); + + index++; + } + + return RichText( + text: TextSpan(children: spans, style: evenTextStyle), + overflow: TextOverflow.visible, + ); + } +} + +class AddressExpansionTile extends StatelessWidget { + const AddressExpansionTile({ + super.key, + required this.contactType, + required this.currentTheme, + required this.name, + required this.address, + required this.amount, + required this.isBatchSending, + required this.itemTitleTextStyle, + required this.itemSubTitleTextStyle, + }); + + final String contactType; + final ThemeBase currentTheme; + final String name; + final String address; + final String amount; + final bool isBatchSending; + final TextStyle itemTitleTextStyle; + final TextStyle itemSubTitleTextStyle; + + @override + Widget build(BuildContext context) { + final addressTextStyle = TextStyle( + fontSize: 12, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: currentTheme.type == ThemeType.bright + ? Theme.of(context).extension()!.labelTextColor + : Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ); + + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(10)), + color: Theme.of(context).extension()!.buttonColor, + ), + child: Theme( + data: Theme.of(context).copyWith(dividerColor: Colors.transparent), + child: Padding( + padding: EdgeInsets.symmetric(horizontal: 14, vertical: isBatchSending ? 0 : 8), + child: ExpansionTile( + childrenPadding: EdgeInsets.zero, + tilePadding: EdgeInsets.zero, + dense: true, + visualDensity: VisualDensity.compact, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text(isBatchSending ? name : contactType, style: itemTitleTextStyle), + Text(isBatchSending ? amount : name, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + )), + ], + ), + children: [ + Row( + children: [ + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + buildSegmentedAddress( + address: address, + evenTextStyle: currentTheme.type == ThemeType.bright + ? itemSubTitleTextStyle + : addressTextStyle, + oddTextStyle: itemSubTitleTextStyle, + ), + ], + ), + ), + ], + ), + ], + ), + ), + ), + ); + } + + Widget buildSegmentedAddress({ + required String address, + int chunkSize = 6, + required TextStyle evenTextStyle, + required TextStyle oddTextStyle, + }) { + final spans = []; + + int index = 0; + for (int i = 0; i < address.length; i += chunkSize) { + final chunk = address.substring(i, math.min(i + chunkSize, address.length)); + final style = (index % 2 == 0) ? evenTextStyle : oddTextStyle; + + spans.add( + TextSpan(text: '$chunk ', style: style), + ); + + index++; + } + + return RichText( + text: TextSpan(children: spans, style: evenTextStyle), + overflow: TextOverflow.visible, + ); + } +} diff --git a/lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart b/lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart new file mode 100644 index 000000000..26d25b50d --- /dev/null +++ b/lib/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart @@ -0,0 +1,231 @@ +import 'package:cake_wallet/src/widgets/primary_button.dart'; +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/dashboard_page_theme.dart'; +import 'package:cake_wallet/themes/extensions/menu_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_svg/svg.dart'; + +import 'base_bottom_sheet_widget.dart'; + +class LoadingBottomSheet extends BaseBottomSheet { + LoadingBottomSheet({required String titleText, String? titleIconPath}) + : super(titleText: titleText, titleIconPath: titleIconPath); + + @override + Widget contentWidget(BuildContext context) { + return SizedBox( + height: 200, + child: Center(child: CircularProgressIndicator()), + ); + } + + @override + Widget footerWidget(BuildContext context) => const SizedBox(height: 94); +} + +class InfoBottomSheet extends BaseBottomSheet { + final ThemeBase currentTheme; + final String? contentImage; + final Color? contentImageColor; + final String? content; + final bool isTwoAction; + final bool showDontAskMeCheckbox; + final Function(bool)? onCheckboxChanged; + final String? actionButtonText; + final VoidCallback? actionButton; + final Key? actionButtonKey; + final String? leftButtonText; + final String? rightButtonText; + final VoidCallback? actionLeftButton; + final VoidCallback? actionRightButton; + final Key? rightActionButtonKey; + final Key? leftActionButtonKey; + + InfoBottomSheet({ + required String titleText, + String? titleIconPath, + required this.currentTheme, + this.contentImage, + this.contentImageColor, + this.content, + this.isTwoAction = false, + this.showDontAskMeCheckbox = false, + this.onCheckboxChanged, + this.actionButtonText, + this.actionButton, + this.actionButtonKey, + this.leftButtonText, + this.rightButtonText, + this.actionLeftButton, + this.actionRightButton, + this.rightActionButtonKey, + this.leftActionButtonKey, + }) : super(titleText: titleText, titleIconPath: titleIconPath); + + @override + Widget contentWidget(BuildContext context) { + return SizedBox( + height: 200, + child: Column( + children: [ + if (contentImage != null) + Expanded( + flex: 4, + child: getImage(contentImage!, imageColor: contentImageColor), + ) + else + Container(), + if (content != null) + Expanded( + flex: 2, + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Spacer(flex: 2), + Expanded( + flex: 6, + child: Text( + content!, + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + ), + const Spacer(flex: 2), + ], + ), + ), + if (showDontAskMeCheckbox) + Padding( + padding: const EdgeInsets.only(left: 34.0), + child: Row( + children: [ + SimpleCheckbox(onChanged: onCheckboxChanged), + const SizedBox(width: 8), + Text( + 'Don’t ask me next time', + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 14, + fontFamily: 'Lato', + fontWeight: FontWeight.w500, + color: Theme.of(context).extension()!.titleColor, + decoration: TextDecoration.none, + ), + ), + ], + ), + ), + ], + ), + ); + } + + @override + Widget footerWidget(BuildContext context) { + if (isTwoAction) { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 0, 16, 34), + child: Row( + mainAxisSize: MainAxisSize.max, + children: [ + Flexible( + child: Container( + padding: const EdgeInsets.only(right: 8.0, top: 8.0), + child: PrimaryButton( + key: leftActionButtonKey, + onPressed: actionLeftButton, + text: leftButtonText ?? '', + color: currentTheme.type == ThemeType.dark + ? Theme.of(context).extension()!.backgroundColor + : Theme.of(context).cardColor, + textColor: currentTheme.type == ThemeType.dark + ? Theme.of(context).extension()!.textColor + : Theme.of(context).extension()!.buttonTextColor, + ), + ), + ), + Flexible( + child: Container( + padding: const EdgeInsets.only(left: 8.0, top: 8.0), + child: PrimaryButton( + key: rightActionButtonKey, + onPressed: actionRightButton, + text: rightButtonText ?? '', + color: Theme.of(context).primaryColor, + textColor: currentTheme.type == ThemeType.dark + ? Theme.of(context).extension()!.textColor + : Theme.of(context).extension()!.buttonTextColor, + ), + ), + ), + ], + ), + ); + } else { + return Padding( + padding: const EdgeInsets.fromLTRB(16, 12, 16, 34), + child: LoadingPrimaryButton( + key: actionButtonKey, + onPressed: actionButton ?? () {}, + text: actionButtonText ?? '', + color: Theme.of(context).primaryColor, + textColor: Colors.white, + isLoading: false, + isDisabled: false, + ), + ); + } + } + + Widget getImage(String imagePath, {Color? imageColor}) { + final bool isSvg = imagePath.endsWith('.svg'); + if (isSvg) { + return SvgPicture.asset( + imagePath, + colorFilter: imageColor != null ? ColorFilter.mode(imageColor, BlendMode.srcIn) : null, + ); + } else { + return Image.asset(imagePath); + } + } +} + +class SimpleCheckbox extends StatefulWidget { + SimpleCheckbox({this.onChanged}); + + final Function(bool)? onChanged; + + @override + State createState() => _SimpleCheckboxState(); +} + +class _SimpleCheckboxState extends State { + bool initialValue = false; + + @override + Widget build(BuildContext context) { + return SizedBox( + height: 24.0, + width: 24.0, + child: Checkbox( + value: initialValue, + onChanged: (value) => setState(() { + initialValue = value!; + widget.onChanged?.call(value); + }), + checkColor: Theme.of(context).extension()!.titleColor, + activeColor: Colors.transparent, + materialTapTargetSize: MaterialTapTargetSize.shrinkWrap, + side: WidgetStateBorderSide.resolveWith((states) => BorderSide( + color: Theme.of(context).extension()!.titleColor, width: 1.0)), + ), + ); + } +} diff --git a/lib/src/widgets/standard_slide_button_widget.dart b/lib/src/widgets/standard_slide_button_widget.dart new file mode 100644 index 000000000..e777155aa --- /dev/null +++ b/lib/src/widgets/standard_slide_button_widget.dart @@ -0,0 +1,90 @@ +import 'package:cake_wallet/themes/extensions/cake_text_theme.dart'; +import 'package:cake_wallet/themes/extensions/filter_theme.dart'; +import 'package:cake_wallet/themes/extensions/menu_theme.dart'; +import 'package:cake_wallet/themes/theme_base.dart'; +import 'package:flutter/material.dart'; + +class StandardSlideButton extends StatefulWidget { + const StandardSlideButton({ + Key? key, + required this.onSlideComplete, + this.buttonText = '', + this.height = 48.0, + required this.currentTheme, + }) : super(key: key); + + final VoidCallback onSlideComplete; + final String buttonText; + final double height; + final ThemeBase currentTheme; + + @override + _StandardSlideButtonState createState() => _StandardSlideButtonState(); +} + +class _StandardSlideButtonState extends State { + double _dragPosition = 0.0; + + @override + Widget build(BuildContext context) { + return LayoutBuilder(builder: (context, constraints) { + final double maxWidth = constraints.maxWidth; + const double sideMargin = 4.0; + final double effectiveMaxWidth = maxWidth - 2 * sideMargin; + const double sliderWidth = 42.0; + + return Container( + height: widget.height, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: widget.currentTheme.type == ThemeType.light || widget.currentTheme.type == ThemeType.bright + ? Theme.of(context).disabledColor + : widget.currentTheme.type == ThemeType.oled ? Colors.black : Theme.of(context).extension()!.backgroundColor), + child: Stack( + alignment: Alignment.centerLeft, + children: [ + Center( + child: Text(widget.buttonText, + style: TextStyle( + fontSize: 16, + fontFamily: 'Lato', + fontWeight: FontWeight.w600, + color: Theme.of(context).extension()!.titleColor))), + Positioned( + left: sideMargin + _dragPosition, + child: GestureDetector( + onHorizontalDragUpdate: (details) { + setState(() { + _dragPosition += details.delta.dx; + if (_dragPosition < 0) _dragPosition = 0; + if (_dragPosition > effectiveMaxWidth - sliderWidth) { + _dragPosition = effectiveMaxWidth - sliderWidth; + } + }); + }, + onHorizontalDragEnd: (details) { + if (_dragPosition >= effectiveMaxWidth - sliderWidth - 10) { + widget.onSlideComplete(); + } else { + setState(() => _dragPosition = 0); + } + }, + child: Container( + width: sliderWidth, + height: widget.height - 8, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(10), + color: widget.currentTheme.type == ThemeType.bright ? Theme.of(context).extension()!.backgroundColor : Theme.of(context).extension()!.buttonColor, + ), + alignment: Alignment.center, + child: Icon(Icons.arrow_forward, + color: Theme.of(context).extension()!.titleColor), + ), + ), + ) + ], + ), + ); + }); + } +} diff --git a/lib/view_model/send/send_view_model.dart b/lib/view_model/send/send_view_model.dart index 52a8b06db..27bef933b 100644 --- a/lib/view_model/send/send_view_model.dart +++ b/lib/view_model/send/send_view_model.dart @@ -106,6 +106,11 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor bool get showAddressBookPopup => _settingsStore.showAddressBookPopupEnabled; + @action + void setShowAddressBookPopup(bool value) { + _settingsStore.showAddressBookPopupEnabled = value; + } + @action void addOutput() { outputs