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:
Serhii
2025-03-21 05:24:22 +02:00
committed by GitHub
parent 00642e6027
commit 626e532fce
12 changed files with 1192 additions and 271 deletions
@@ -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 {
+103 -72
View File
@@ -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),
),
),
)
],
),
);
});
}
}