CW-942-New-Swap-UI-Flow (#2036)

* feat: Implement New Swap UI including fee items and coin control and extract common fee logic to a feeViewModel, use that in SendViewModel and ExchangeViewModel

* feat: Implement New Swap UI with fee items and coin control and extract common fee logic to a feeViewModel for use in SendViewModel and ExchangeViewModel

* Update colors

* Show warning if user choose a below default transaction priority

* fix: Add warning when user select less than default transaction priority and add condition to show extraId

* Complete Swap UI flow

* Handle edgecases for XRP

* Switch swap page button texts

* Handle edgecases for XLM

* Switch prompt text and handle edgecases

---------

Co-authored-by: tuxpizza <tuxsudo@tux.pizza>
Co-authored-by: Omar Hatem <omarh.ismail1@gmail.com>
This commit is contained in:
David Adegoke
2025-03-14 18:31:31 +02:00
committed by GitHub
co-authored by tuxpizza Omar Hatem
parent 0f301a71a2
commit 9f3078740e
48 changed files with 1048 additions and 327 deletions
+3 -3
View File
@@ -248,7 +248,7 @@ class ExchangePage extends BasePage {
Observer(
builder: (_) => LoadingPrimaryButton(
key: ValueKey('exchange_page_exchange_button_key'),
text: exchangeViewModel.isAvailableInSelected ? S.of(context).exchange : S.of(context).change_selected_exchanges,
text: exchangeViewModel.isAvailableInSelected ? S.of(context).swap : S.of(context).change_selected_exchanges,
onPressed: exchangeViewModel.isAvailableInSelected ? () {
FocusScope.of(context).unfocus();
@@ -377,7 +377,7 @@ class ExchangePage extends BasePage {
return;
}
if (exchangeViewModel.isLowFee) {
if (exchangeViewModel.feesViewModel.isLowFee) {
_showFeeAlert(context);
}
@@ -640,7 +640,7 @@ class ExchangePage extends BasePage {
}) ??
false;
if (confirmed) {
exchangeViewModel.setDefaultTransactionPriority();
exchangeViewModel.feesViewModel .setDefaultTransactionPriority();
}
}
@@ -35,10 +35,11 @@ class PresentProviderPicker extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(S.of(context).exchange,
Text(S.of(context).swap,
style: TextStyle(
fontSize: 16.0,
fontWeight: FontWeight.w600,
color: Colors.white)),
Observer(
builder: (_) => Text(
@@ -21,7 +21,7 @@ class ExchangeConfirmPage extends BasePage {
final Trade trade;
@override
String get title => S.current.copy_id;
String get title => S.current.swap;
@override
Widget body(BuildContext context) {
@@ -40,7 +40,7 @@ class ExchangeConfirmPage extends BasePage {
style: TextStyle(
fontSize: 18.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor),
),
)),
Container(
@@ -0,0 +1,143 @@
import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/core/execution_state.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';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/scollable_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 gradientBackground => true;
@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).extension<SendPageTheme>()!.estimatedFeeColor,
);
return Container(
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(top: 36, bottom: 24),
content: Observer(
builder: (_) {
return Column(
children: <Widget>[
Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.only(
bottomLeft: Radius.circular(24), bottomRight: Radius.circular(24)),
gradient: LinearGradient(
colors: [
Theme.of(context).extension<SendPageTheme>()!.firstGradientColor,
Theme.of(context).extension<SendPageTheme>()!.secondGradientColor,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
padding: EdgeInsets.fromLTRB(24, 110, 24, 32),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
children: <Widget>[
Spacer(flex: 3),
Flexible(
flex: 6,
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Theme.of(context)
.extension<ExchangePageTheme>()!
.qrCodeColor,
),
),
child: QrImage(
data:
exchangeTradeViewModel.trade.inputAddress ?? fetchingLabel,
),
),
),
),
),
Spacer(flex: 3)
],
),
SizedBox(height: 24),
...exchangeTradeViewModel.items
.where((item) => item.isExternalSendDetail)
.map(
(item) => TradeItemRowWidget(
currentTheme: currentTheme,
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).primaryColor,
textColor: Colors.white,
)
: Offstage();
},
),
),
);
}
}
@@ -1,13 +1,15 @@
import 'package:flutter/cupertino.dart';
class ExchangeTradeItem {
ExchangeTradeItem({
required this.title,
required this.data,
required this.isCopied,
required this.isReceiveDetail,
required this.isExternalSendDetail,
});
String title;
String data;
bool isCopied;
bool isReceiveDetail;
bool isExternalSendDetail;
}
@@ -1,22 +1,22 @@
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/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/theme_base.dart';
import 'dart:ui';
import 'package:cake_wallet/themes/extensions/exchange_page_theme.dart';
import 'package:cake_wallet/utils/request_review_handler.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:mobx/mobx.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.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/src/widgets/list_row.dart';
import 'package:cake_wallet/utils/show_bar.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';
import 'package:cake_wallet/src/screens/receive/widgets/qr_image.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/exchange_trade/widgets/timer_widget.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
@@ -30,8 +30,7 @@ void showInformation(
final walletName = exchangeTradeViewModel.wallet.name;
final information = exchangeTradeViewModel.isSendable
? S.current.exchange_result_confirm(
trade.amount, trade.from.toString(), walletName) +
? S.current.exchange_trade_result_confirm(trade.amount, trade.from.toString(), walletName) +
exchangeTradeViewModel.extraInfo
: S.current.exchange_result_description(
trade.amount, trade.from.toString()) +
@@ -50,7 +49,22 @@ class ExchangeTradePage extends BasePage {
final ExchangeTradeViewModel exchangeTradeViewModel;
@override
String get title => S.current.exchange;
String get title => S.current.swap;
@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
Widget trailing(BuildContext context) {
@@ -74,14 +88,20 @@ class ExchangeTradePage extends BasePage {
}
@override
Widget body(BuildContext context) =>
ExchangeTradeForm(exchangeTradeViewModel);
Widget body(BuildContext context) => ExchangeTradeForm(
exchangeTradeViewModel,
currentTheme,
);
}
class ExchangeTradeForm extends StatefulWidget {
ExchangeTradeForm(this.exchangeTradeViewModel);
ExchangeTradeForm(
this.exchangeTradeViewModel,
this.currentTheme,
);
final ExchangeTradeViewModel exchangeTradeViewModel;
final ThemeBase currentTheme;
@override
ExchangeTradeState createState() => ExchangeTradeState();
@@ -115,119 +135,77 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
@override
Widget build(BuildContext context) {
final copyImage = Image.asset('assets/images/copy_content.png',
height: 16,
width: 16,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor);
_setEffects();
return Container(
child: ScrollableWithBottomSection(
contentPadding: EdgeInsets.only(top: 10, bottom: 16),
content: Observer(builder: (_) {
final trade = widget.exchangeTradeViewModel.trade;
contentPadding: EdgeInsets.only(top: 10, 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: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor),
),
if (trade.expiredAt != null)
TimerWidget(trade.expiredAt!,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)
])
: Offstage(),
Padding(
padding: EdgeInsets.only(top: 32),
child: Row(children: <Widget>[
Spacer(flex: 3),
Flexible(
flex: 4,
child: Center(
child: AspectRatio(
aspectRatio: 1.0,
child: Container(
padding: EdgeInsets.all(5),
decoration: BoxDecoration(
border: Border.all(
width: 3,
color: Theme.of(context).extension<ExchangePageTheme>()!.qrCodeColor
)
),
child: QrImage(data: trade.inputAddress ?? fetchingLabel),
)))),
Spacer(flex: 3)
]),
),
Padding(
padding: EdgeInsets.only(top: 16),
child: ListView.separated(
shrinkWrap: true,
physics: NeverScrollableScrollPhysics(),
itemCount: widget.exchangeTradeViewModel.items.length,
separatorBuilder: (context, index) => Container(
height: 1,
color: Theme.of(context).extension<ExchangePageTheme>()!.dividerCodeColor,
),
itemBuilder: (context, index) {
final item = widget.exchangeTradeViewModel.items[index];
final value = item.data;
return Column(
children: <Widget>[
trade.expiredAt != null
? Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
S.of(context).offer_expires_in,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: Theme.of(context)
.extension<TransactionTradeTheme>()!
.detailsTitlesColor),
),
if (trade.expiredAt != null)
TimerWidget(trade.expiredAt!,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)
])
: Offstage(),
_ExchangeTradeItemsCardSection(
viewModel: widget.exchangeTradeViewModel,
currentTheme: widget.currentTheme,
),
],
);
}),
bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24),
bottomSection: Column(
children: [
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: Theme.of(context).cardColor,
textColor: Theme.of(context).extension<CakeTextTheme>()!.buttonTextColor,
),
SizedBox(height: 16),
Observer(
builder: (_) {
final trade = widget.exchangeTradeViewModel.trade;
final sendingState = widget.exchangeTradeViewModel.sendViewModel.state;
final content = ListRow(
title: item.title,
value: value,
valueFontSize: 14,
image: item.isCopied ? copyImage : null,
);
return item.isCopied
? Builder(
builder: (context) => GestureDetector(
onTap: () {
Clipboard.setData(
ClipboardData(text: value));
showBar<void>(context,
S.of(context).copied_to_clipboard);
},
child: content,
))
: content;
},
),
),
],
);
}),
bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24),
bottomSection: Observer(builder: (_) {
final trade = widget.exchangeTradeViewModel.trade;
final sendingState =
widget.exchangeTradeViewModel.sendViewModel.state;
return widget.exchangeTradeViewModel.isSendable &&
!(sendingState is TransactionCommitted)
? LoadingPrimaryButton(
key: ValueKey('exchange_trade_page_confirm_sending_button_key'),
isDisabled: trade.inputAddress == null ||
trade.inputAddress!.isEmpty,
isLoading: sendingState is IsExecutingState,
onPressed: () =>
widget.exchangeTradeViewModel.confirmSending(),
text: S.of(context).confirm,
color: Theme.of(context).primaryColor,
textColor: Colors.white)
: Offstage();
})),
return widget.exchangeTradeViewModel.isSendable &&
!(sendingState is TransactionCommitted)
? LoadingPrimaryButton(
key: ValueKey('exchange_trade_page_send_from_cake_button_key'),
isDisabled: trade.inputAddress == null || trade.inputAddress!.isEmpty,
isLoading: sendingState is IsExecutingState,
onPressed: () => widget.exchangeTradeViewModel.confirmSending(),
text:S.current.send_from_cake_wallet,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
)
: Offstage();
},
),
],
),
),
);
}
@@ -412,3 +390,40 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
}
}
}
class _ExchangeTradeItemsCardSection extends StatelessWidget {
const _ExchangeTradeItemsCardSection({
required this.viewModel,
required this.currentTheme,
});
final ExchangeTradeViewModel viewModel;
final ThemeBase currentTheme;
@override
Widget build(BuildContext context) {
final firstExchangeCard = ExchangeTradeCardItemWidget(
currentTheme: currentTheme,
isReceiveDetailsCard: true,
exchangeTradeViewModel: viewModel,
);
final secondExchangeCard = ExchangeTradeCardItemWidget(
currentTheme: currentTheme,
isReceiveDetailsCard: false,
exchangeTradeViewModel: viewModel,
);
if (responsiveLayoutUtil.shouldRenderMobileUI) {
return MobileExchangeCardsSection(
firstExchangeCard: firstExchangeCard,
secondExchangeCard: secondExchangeCard,
);
}
return DesktopExchangeCardsSection(
firstExchangeCard: firstExchangeCard,
secondExchangeCard: secondExchangeCard,
);
}
}
@@ -0,0 +1,312 @@
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/alert_with_two_actions.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/utils/show_bar.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/fees_view_model.dart';
import 'package:cake_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:cake_wallet/themes/extensions/send_page_theme.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import '../../../../themes/extensions/cake_text_theme.dart';
import '../../../../themes/extensions/transaction_trade_theme.dart';
import '../../../../themes/theme_base.dart';
class ExchangeTradeCardItemWidget extends StatelessWidget {
ExchangeTradeCardItemWidget({
required this.isReceiveDetailsCard,
required this.exchangeTradeViewModel,
required this.currentTheme,
Key? key,
}) : feesViewModel = exchangeTradeViewModel.feesViewModel,
output = exchangeTradeViewModel.output;
final Output output;
final bool isReceiveDetailsCard;
final FeesViewModel feesViewModel;
final ExchangeTradeViewModel exchangeTradeViewModel;
final ThemeBase currentTheme;
@override
Widget build(BuildContext context) {
final copyImage = Image.asset(
'assets/images/copy_content.png',
height: 16,
width: 16,
color: Theme.of(context).extension<SendPageTheme>()!.estimatedFeeColor,
);
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(
currentTheme: currentTheme,
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),
),
if (exchangeTradeViewModel.sendViewModel.hasCoinControl)
CoinControlWidget(
onTap: () => Navigator.of(context).pushNamed(
Routes.unspentCoinsList,
arguments: exchangeTradeViewModel.sendViewModel.coinTypeToSpendFrom,
),
),
],
],
),
);
}
Future<void> pickTransactionPriority(BuildContext context) 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) {
feesViewModel.setTransactionPriority(priority);
setState(() => selectedIdx = items.indexOf(priority));
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;
final ThemeBase currentTheme;
const TradeItemRowWidget({
required this.title,
required this.value,
required this.isCopied,
required this.copyImage,
required this.currentTheme,
});
@override
Widget build(BuildContext context) {
final hintTextColor = currentTheme == ThemeType.bright
? Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor
: Colors.white.withAlpha(175);
final mainTextColor = currentTheme == ThemeType.bright
? Theme.of(context).extension<CakeTextTheme>()!.titleColor
: Colors.white;
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: hintTextColor,
mainTextColor: mainTextColor,
),
),
);
}
}
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: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
color: Colors.white,
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
'${output.estimatedFee} ${feesViewModel.currency}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
if (!feesViewModel.isFiatDisabled)
Padding(
padding: EdgeInsets.only(top: 5),
child: Text(
'${output.estimatedFeeFiatAmount} ${feesViewModel.fiat.title}',
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color:
Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor,
),
),
),
],
),
Padding(
padding: EdgeInsets.only(top: 2, left: 5),
child: Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
),
),
],
),
],
),
),
),
);
}
}
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: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w600,
color: Colors.white,
),
),
Icon(
Icons.arrow_forward_ios,
size: 12,
color: Colors.white,
),
],
),
),
);
}
}
+9 -9
View File
@@ -308,11 +308,11 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
color: Theme.of(context).extension<SendPageTheme>()!.textFieldHintColor),
),
),
if (sendViewModel.hasFees)
if (sendViewModel.feesViewModel.hasFees)
Observer(
builder: (_) => GestureDetector(
key: ValueKey('send_page_select_fee_priority_button_key'),
onTap: sendViewModel.hasFeesPriority
onTap: sendViewModel.feesViewModel.hasFeesPriority
? () => pickTransactionPriority(context)
: () {},
child: Container(
@@ -570,11 +570,11 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
Future<void> pickTransactionPriority(BuildContext context) async {
final items = priorityForWalletType(sendViewModel.walletType);
final selectedItem = items.indexOf(sendViewModel.transactionPriority);
final customItemIndex = sendViewModel.getCustomPriorityIndex(items);
final selectedItem = items.indexOf(sendViewModel.feesViewModel.transactionPriority);
final customItemIndex = sendViewModel.feesViewModel.getCustomPriorityIndex(items);
final isBitcoinWallet = sendViewModel.walletType == WalletType.bitcoin;
final maxCustomFeeRate = sendViewModel.maxCustomFeeRate?.toDouble();
double? customFeeRate = isBitcoinWallet ? sendViewModel.customBitcoinFeeRate.toDouble() : null;
final maxCustomFeeRate = sendViewModel.feesViewModel.maxCustomFeeRate?.toDouble();
double? customFeeRate = isBitcoinWallet ? sendViewModel.feesViewModel.customBitcoinFeeRate.toDouble() : null;
await showPopUp<void>(
context: context,
@@ -585,7 +585,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
return Picker(
items: items,
displayItem: (TransactionPriority priority) =>
sendViewModel.displayFeeRate(priority, customFeeRate?.round()),
sendViewModel.feesViewModel.displayFeeRate(priority, customFeeRate?.round()),
selectedAtIndex: selectedIdx,
customItemIndex: customItemIndex,
maxValue: maxCustomFeeRate,
@@ -596,7 +596,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
sliderValue: customFeeRate,
onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue),
onItemSelected: (TransactionPriority priority) {
sendViewModel.setTransactionPriority(priority);
sendViewModel.feesViewModel.setTransactionPriority(priority);
setState(() => selectedIdx = items.indexOf(priority));
},
);
@@ -604,7 +604,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
);
},
);
if (isBitcoinWallet) sendViewModel.customBitcoinFeeRate = customFeeRate!.round();
if (isBitcoinWallet) sendViewModel.feesViewModel.customBitcoinFeeRate = customFeeRate!.round();
}
void _presentPicker(BuildContext context) {
+19 -12
View File
@@ -1,30 +1,37 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
class ListRow extends StatelessWidget {
ListRow(
{required this.title,
required this.value,
this.titleFontSize = 14,
this.valueFontSize = 16,
this.image});
ListRow({
required this.title,
required this.value,
this.titleFontSize = 14,
this.valueFontSize = 16,
this.image,
this.padding,
this.color,
this.hintTextColor,
this.mainTextColor
});
final String title;
final String value;
final double titleFontSize;
final double valueFontSize;
final Image? image;
final EdgeInsetsGeometry? padding;
final Color? color;
final Color? hintTextColor;
final Color? mainTextColor;
@override
Widget build(BuildContext context) {
return Container(
width: double.infinity,
color: Theme.of(context).colorScheme.background,
color: color ?? Theme.of(context).colorScheme.background,
child: Padding(
padding:
const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24),
padding: padding ?? const EdgeInsets.only(left: 24, top: 16, bottom: 16, right: 24),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
@@ -32,7 +39,7 @@ class ListRow extends StatelessWidget {
style: TextStyle(
fontSize: titleFontSize,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor),
color: hintTextColor ?? Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor),
textAlign: TextAlign.left),
Padding(
padding: const EdgeInsets.only(top: 12),
@@ -46,7 +53,7 @@ class ListRow extends StatelessWidget {
style: TextStyle(
fontSize: valueFontSize,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor)),
color: mainTextColor ?? Theme.of(context).extension<CakeTextTheme>()!.titleColor)),
),
image != null
? Padding(