mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-20 06:08:34 +02:00
Cw 921 create new bottom sheet sending confirmation UI (#2099)
* init commit * add slide button * address book popup * fix colors for themes * update exchange page * update cake pay purchase card page * Update rbf_details_page.dart * refactored code * add generating tx bottom sheet * add show scrollbar * minor fixes
This commit is contained in:
@@ -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<void>(
|
||||
showModalBottomSheet<void>(
|
||||
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<void>(
|
||||
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);
|
||||
|
||||
@@ -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<ExchangeTradeForm> {
|
||||
);
|
||||
}
|
||||
|
||||
BuildContext? loadingBottomSheetContext;
|
||||
|
||||
void _setEffects() {
|
||||
if (_effectsInstalled) {
|
||||
return;
|
||||
@@ -216,6 +220,13 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||
|
||||
_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<void>(
|
||||
@@ -232,163 +243,92 @@ class ExchangeTradeState extends State<ExchangeTradeForm> {
|
||||
});
|
||||
}
|
||||
|
||||
if (state is IsExecutingState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
loadingBottomSheetContext = context;
|
||||
return LoadingBottomSheet(
|
||||
titleText: 'Generating transaction',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state is ExecutedSuccessfullyState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet<void>(
|
||||
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<void>(
|
||||
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<void>(
|
||||
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<void>(
|
||||
context: context,
|
||||
builder: (BuildContext popupContext) {
|
||||
return Observer(builder: (_) {
|
||||
final state = widget
|
||||
.exchangeTradeViewModel.sendViewModel.state;
|
||||
|
||||
if (state is TransactionCommitted) {
|
||||
return Stack(
|
||||
children: <Widget>[
|
||||
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<CakeTextTheme>()!.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: <Widget>[
|
||||
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<CakeTextTheme>()!.titleColor,
|
||||
decoration: TextDecoration.none,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _ExchangeTradeItemsCardSection extends StatelessWidget {
|
||||
|
||||
@@ -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<void>(
|
||||
@@ -510,92 +523,110 @@ class SendPage extends BasePage {
|
||||
});
|
||||
}
|
||||
|
||||
if (state is ExecutedSuccessfullyState) {
|
||||
if (state is IsExecutingState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
showPopUp<void>(
|
||||
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<void>(
|
||||
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<void>(
|
||||
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<void>(
|
||||
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<void>(
|
||||
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<CakeTextTheme>()!.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:
|
||||
|
||||
@@ -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<void>(
|
||||
@@ -151,35 +161,56 @@ class RBFDetailsPage extends BasePage {
|
||||
});
|
||||
}
|
||||
|
||||
if (state is IsExecutingState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
loadingBottomSheetContext = context;
|
||||
return LoadingBottomSheet(
|
||||
titleText: 'Generating transaction',
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state is ExecutedSuccessfullyState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet<void>(
|
||||
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,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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<CakeTextTheme>()!.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<CakeTextTheme>()!.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: <Widget>[
|
||||
headerWidget(context),
|
||||
contentWidget(context),
|
||||
footerWidget(context),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<Output> 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<CakeTextTheme>()!.titleColor,
|
||||
decoration: TextDecoration.none,
|
||||
);
|
||||
final itemSubTitleTextStyle = TextStyle(
|
||||
fontSize: 12,
|
||||
fontFamily: 'Lato',
|
||||
fontWeight: FontWeight.w600,
|
||||
color: currentTheme.type == ThemeType.bright
|
||||
? Theme.of(context).extension<CakeTextTheme>()!.titleColor
|
||||
: Theme.of(context).extension<BalancePageTheme>()!.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<FilterTheme>()!.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<BalancePageTheme>()!.labelTextColor
|
||||
: Theme.of(context).extension<CakeTextTheme>()!.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<FilterTheme>()!.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 = <TextSpan>[];
|
||||
|
||||
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<BalancePageTheme>()!.labelTextColor
|
||||
: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
|
||||
decoration: TextDecoration.none,
|
||||
);
|
||||
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.all(Radius.circular(10)),
|
||||
color: Theme.of(context).extension<FilterTheme>()!.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<CakeTextTheme>()!.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 = <TextSpan>[];
|
||||
|
||||
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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<CakeTextTheme>()!.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<CakeTextTheme>()!.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<CakeMenuTheme>()!.backgroundColor
|
||||
: Theme.of(context).cardColor,
|
||||
textColor: currentTheme.type == ThemeType.dark
|
||||
? Theme.of(context).extension<DashboardPageTheme>()!.textColor
|
||||
: Theme.of(context).extension<CakeTextTheme>()!.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<DashboardPageTheme>()!.textColor
|
||||
: Theme.of(context).extension<CakeTextTheme>()!.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<SimpleCheckbox> createState() => _SimpleCheckboxState();
|
||||
}
|
||||
|
||||
class _SimpleCheckboxState extends State<SimpleCheckbox> {
|
||||
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<CakeTextTheme>()!.titleColor,
|
||||
activeColor: Colors.transparent,
|
||||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||||
side: WidgetStateBorderSide.resolveWith((states) => BorderSide(
|
||||
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor, width: 1.0)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<StandardSlideButton> {
|
||||
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<CakeMenuTheme>()!.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<CakeTextTheme>()!.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<CakeMenuTheme>()!.backgroundColor : Theme.of(context).extension<FilterTheme>()!.buttonColor,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(Icons.arrow_forward,
|
||||
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user