* Add initial checkbox for RBF

* minor progress

* minor progress

* Minor progress

* Debugging RBF

* Minor fix

* Fix RBF transaction inputs (now it's working)

* New versions
Fix issues with Monero.com

* Add sending for Solana tokens exchanges

* Add default keyword for P2WPKH [skip ci]

* chore: Switch solana commitment to confirmed to reduced blockhash expiration (#1313)

* Modify test workflow to send arm64-v8a build only

* Fix workflow build path

* Remove unnecessary reverse of txId

* Update Replace by fee with the new bitcoin base implementation

* btc custom fee priority

* add feeRate to btc credential

* UI fixes

* add check if the change covers the fee

* Update settings_store.dart

* add check confirmation for rbf

* add a check to see if the change is sufficient for the new fee

* addressing PR comments

* update localization files

* addressing PR comments

* minor fixes

* Update transaction_details_view_model.dart

* Minor Fix for building Monero.com [skip ci]

* update localization files

* add bump fee page

* update localisation files

* Update cw_bitcoin.dart

* fix merge conflict

* fix UI issues

* Fix Conflicts, Fix RBF flow, some enhancements

* prevent default custom fee rate

* hide outputs and inputs items

* minor fix [skip ci]

* addressing PR comments

* remove rbf checkbox

* default picker value

* minor ui change [skip ci]

* min fee rate [skip ci]

* Minor fix and some minor enhancements

---------

Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
Co-authored-by: Adegoke David <64401859+Blazebrain@users.noreply.github.com>
This commit is contained in:
Serhii
2024-04-08 16:54:58 +02:00
committed by GitHub
co-authored by OmarHatem Adegoke David
parent 071643b662
commit 291aa710c6
62 changed files with 1591 additions and 321 deletions
+65 -18
View File
@@ -74,22 +74,26 @@ class CWBitcoin extends Bitcoin {
@override
Object createBitcoinTransactionCredentials(List<Output> outputs,
{required TransactionPriority priority, int? feeRate}) =>
BitcoinTransactionCredentials(
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as BitcoinTransactionPriority,
feeRate: feeRate);
{required TransactionPriority priority, int? feeRate}) {
final bitcoinFeeRate =
priority == BitcoinTransactionPriority.custom && feeRate != null ? feeRate : null;
return BitcoinTransactionCredentials(
outputs
.map((out) => OutputInfo(
fiatAmount: out.fiatAmount,
cryptoAmount: out.cryptoAmount,
address: out.address,
note: out.note,
sendAll: out.sendAll,
extractedAddress: out.extractedAddress,
isParsedAddress: out.isParsedAddress,
formattedCryptoAmount: out.formattedCryptoAmount,
memo: out.memo))
.toList(),
priority: priority as BitcoinTransactionPriority,
feeRate: bitcoinFeeRate
);
}
@override
Object createBitcoinTransactionCredentialsRaw(List<OutputInfo> outputs,
@@ -172,8 +176,9 @@ class CWBitcoin extends Bitcoin {
int formatterStringDoubleToBitcoinAmount(String amount) => stringDoubleToBitcoinAmount(amount);
@override
String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate) =>
(priority as BitcoinTransactionPriority).labelWithRate(rate);
String bitcoinTransactionPriorityWithLabel(TransactionPriority priority, int rate,
{int? customRate}) =>
(priority as BitcoinTransactionPriority).labelWithRate(rate, customRate);
@override
List<BitcoinUnspent> getUnspents(Object wallet) {
@@ -199,6 +204,9 @@ class CWBitcoin extends Bitcoin {
@override
TransactionPriority getBitcoinTransactionPriorityMedium() => BitcoinTransactionPriority.medium;
@override
TransactionPriority getBitcoinTransactionPriorityCustom() => BitcoinTransactionPriority.custom;
@override
TransactionPriority getLitecoinTransactionPriorityMedium() => LitecoinTransactionPriority.medium;
@@ -244,4 +252,43 @@ class CWBitcoin extends Bitcoin {
bool hasTaprootInput(PendingTransaction pendingTransaction) {
return (pendingTransaction as PendingBitcoinTransaction).hasTaprootInputs;
}
@override
Future<PendingBitcoinTransaction> replaceByFee(
Object wallet, String transactionHash, String fee) async {
final bitcoinWallet = wallet as ElectrumWallet;
return await bitcoinWallet.replaceByFee(transactionHash, int.parse(fee));
}
@override
Future<bool> canReplaceByFee(Object wallet, String transactionHash) async {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.canReplaceByFee(transactionHash);
}
@override
Future<bool> isChangeSufficientForFee(Object wallet, String txId, String newFee) async {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.isChangeSufficientForFee(txId, int.parse(newFee));
}
@override
int getFeeAmountForPriority(
Object wallet, TransactionPriority priority, int inputsCount, int outputsCount,
{int? size}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.feeAmountForPriority(
priority as BitcoinTransactionPriority, inputsCount, outputsCount);
}
@override
int getFeeAmountWithFeeRate(Object wallet, int feeRate, int inputsCount, int outputsCount,
{int? size}) {
final bitcoinWallet = wallet as ElectrumWallet;
return bitcoinWallet.feeAmountWithFeeRate(
feeRate,
inputsCount,
outputsCount,
);
}
}
+9
View File
@@ -14,4 +14,13 @@ class FailureState extends ExecutionState {
FailureState(this.error);
final String error;
}
class AwaitingConfirmationState extends ExecutionState {
AwaitingConfirmationState({this.title, this.message, this.onConfirm, this.onCancel});
final String? title;
final String? message;
final Function()? onConfirm;
final Function()? onCancel;
}
+8 -1
View File
@@ -13,6 +13,7 @@ import 'package:cake_wallet/core/yat_service.dart';
import 'package:cake_wallet/entities/background_tasks.dart';
import 'package:cake_wallet/entities/exchange_api_mode.dart';
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cw_core/receive_page_option.dart';
import 'package:cake_wallet/ethereum/ethereum.dart';
import 'package:cake_wallet/nano/nano.dart';
@@ -914,7 +915,8 @@ Future<void> setup({
transactionInfo: transactionInfo,
transactionDescriptionBox: _transactionDescriptionBox,
wallet: wallet,
settingsStore: getIt.get<SettingsStore>());
settingsStore: getIt.get<SettingsStore>(),
sendViewModel: getIt.get<SendViewModel>());
});
getIt.registerFactoryParam<TransactionDetailsPage, TransactionInfo, void>(
@@ -1137,6 +1139,11 @@ Future<void> setup({
getIt.registerFactory(() => IoniaAccountCardsPage(getIt.get<IoniaAccountViewModel>()));
getIt.registerFactoryParam<RBFDetailsPage, TransactionInfo, void>(
(TransactionInfo transactionInfo, _) => RBFDetailsPage(
transactionDetailsViewModel:
getIt.get<TransactionDetailsViewModel>(param1: transactionInfo)));
getIt.registerFactory(() => AnonPayApi(
useTorOnly: getIt.get<SettingsStore>().exchangeStatus == ExchangeApiMode.torOnly,
wallet: getIt.get<AppStore>().wallet!));
+1
View File
@@ -42,6 +42,7 @@ class PreferencesKey {
static const ethereumTransactionPriority = 'current_fee_priority_ethereum';
static const polygonTransactionPriority = 'current_fee_priority_polygon';
static const bitcoinCashTransactionPriority = 'current_fee_priority_bitcoin_cash';
static const customBitcoinFeeRate = 'custom_electrum_fee_rate';
static const shouldShowReceiveWarning = 'should_show_receive_warning';
static const shouldShowYatPopup = 'should_show_yat_popup';
static const moneroWalletPasswordUpdateV1Base = 'monero_wallet_update_v1';
+7
View File
@@ -54,6 +54,7 @@ import 'package:cake_wallet/src/screens/setup_2fa/setup_2fa_enter_code_page.dart
import 'package:cake_wallet/src/screens/support/support_page.dart';
import 'package:cake_wallet/src/screens/support_chat/support_chat_page.dart';
import 'package:cake_wallet/src/screens/support_other_links/support_other_links_page.dart';
import 'package:cake_wallet/src/screens/transaction_details/rbf_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_details_page.dart';
import 'package:cake_wallet/src/screens/unspent_coins/unspent_coins_list_page.dart';
import 'package:cake_wallet/src/screens/wallet_connect/wc_connections_listing_view.dart';
@@ -253,6 +254,12 @@ Route<dynamic> createRoute(RouteSettings settings) {
builder: (_) =>
getIt.get<TransactionDetailsPage>(param1: settings.arguments as TransactionInfo));
case Routes.bumpFeePage:
return CupertinoPageRoute<void>(
fullscreenDialog: true,
builder: (_) =>
getIt.get<RBFDetailsPage>(param1: settings.arguments as TransactionInfo));
case Routes.newSubaddress:
return CupertinoPageRoute<void>(
builder: (_) => getIt.get<AddressEditOrCreatePage>(param1: settings.arguments));
+1
View File
@@ -12,6 +12,7 @@ class Routes {
static const dashboard = '/dashboard';
static const send = '/send';
static const transactionDetails = '/transaction_info';
static const bumpFeePage = '/bump_fee_page';
static const receive = '/receive';
static const newSubaddress = '/new_subaddress';
static const walletEdit = '/walletEdit';
+6 -3
View File
@@ -100,7 +100,10 @@ class SendPage extends BasePage {
AppBarStyle get appBarStyle => AppBarStyle.transparent;
double _sendCardHeight(BuildContext context) {
final double initialHeight = sendViewModel.hasCoinControl ? 500 : 465;
double initialHeight = 450;
if (sendViewModel.hasCoinControl) {
initialHeight += 35;
}
if (!responsiveLayoutUtil.shouldRenderMobileUI) {
return initialHeight - 66;
@@ -190,7 +193,7 @@ class SendPage extends BasePage {
},
)),
Padding(
padding: EdgeInsets.only(top: 10, left: 24, right: 24, bottom: 10),
padding: EdgeInsets.only(left: 24, right: 24, bottom: 10),
child: Container(
height: 10,
child: Observer(
@@ -456,7 +459,7 @@ class SendPage extends BasePage {
? '. ${S.of(_dialogContext).waitFewSecondForTxUpdate}' : '';
final newContactMessage = newContactAddress != null
? '\n${S.of(context).add_contact_to_address_book}' : '';
? '\n${S.of(_dialogContext).add_contact_to_address_book}' : '';
final alertContent =
"$successMessage$waitMessage$newContactMessage";
+34 -14
View File
@@ -1,16 +1,17 @@
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/themes/extensions/keyboard_theme.dart';
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
import 'package:cake_wallet/utils/payment_request.dart';
import 'package:cake_wallet/utils/responsive_layout_util.dart';
import 'package:cw_core/crypto_currency.dart';
import 'package:cw_core/currency.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/widgets/keyboard_done_button.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/view_model/send/output.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -456,7 +457,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
Observer(
builder: (_) => GestureDetector(
onTap: sendViewModel.hasFeesPriority
? () => _setTransactionPriority(context)
? () => pickTransactionPriority(context)
: () {},
child: Container(
padding: EdgeInsets.only(top: 24),
@@ -669,22 +670,41 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
_effectsInstalled = true;
}
Future<void> _setTransactionPriority(BuildContext context) async {
Future<void> pickTransactionPriority(BuildContext context) async {
final items = priorityForWalletType(sendViewModel.walletType);
final selectedItem = items.indexOf(sendViewModel.transactionPriority);
final customItemIndex = sendViewModel.getCustomPriorityIndex(items);
final isBitcoinWallet = sendViewModel.walletType == WalletType.bitcoin;
double? customFeeRate = isBitcoinWallet ? sendViewModel.customBitcoinFeeRate.toDouble() : null;
await showPopUp<void>(
context: context,
builder: (_) => Picker(
items: items,
displayItem: sendViewModel.displayFeeRate,
selectedAtIndex: selectedItem,
title: S.of(context).please_select,
mainAxisAlignment: MainAxisAlignment.center,
onItemSelected: (TransactionPriority priority) =>
sendViewModel.setTransactionPriority(priority),
),
builder: (BuildContext context) {
int selectedIdx = selectedItem;
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Picker(
items: items,
displayItem: (TransactionPriority priority) =>
sendViewModel.displayFeeRate(priority, customFeeRate?.round()),
selectedAtIndex: selectedIdx,
customItemIndex: customItemIndex,
title: S.of(context).please_select,
headerEnabled: !isBitcoinWallet,
closeOnItemSelected: !isBitcoinWallet,
mainAxisAlignment: MainAxisAlignment.center,
sliderValue: customFeeRate,
onSliderChanged: (double newValue) => setState(() => customFeeRate = newValue),
onItemSelected: (TransactionPriority priority) {
sendViewModel.setTransactionPriority(priority);
setState(() => selectedIdx = items.indexOf(priority));
},
);
},
);
},
);
if (isBitcoinWallet) sendViewModel.customBitcoinFeeRate = customFeeRate!.round();
}
void _presentPicker(BuildContext context) {
@@ -2,11 +2,12 @@ import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/settings/widgets/setting_priority_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_cell_with_arrow.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_picker_cell.dart';
import 'package:cake_wallet/src/screens/settings/widgets/settings_version_cell.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/view_model/settings/other_settings_view_model.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:flutter/material.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
@@ -27,13 +28,23 @@ class OtherSettingsPage extends BasePage {
child: Column(
children: [
if (_otherSettingsViewModel.displayTransactionPriority)
SettingsPickerCell(
title: S.current.settings_fee_priority,
items: priorityForWalletType(_otherSettingsViewModel.walletType),
displayItem: _otherSettingsViewModel.getDisplayPriority,
selectedItem: _otherSettingsViewModel.transactionPriority,
onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected,
),
_otherSettingsViewModel.walletType == WalletType.bitcoin ?
SettingsPriorityPickerCell(
title: S.current.settings_fee_priority,
items: priorityForWalletType(_otherSettingsViewModel.walletType),
displayItem: _otherSettingsViewModel.getDisplayBitcoinPriority,
selectedItem: _otherSettingsViewModel.transactionPriority,
customItemIndex: _otherSettingsViewModel.customPriorityItemIndex,
onItemSelected: _otherSettingsViewModel.onDisplayBitcoinPrioritySelected,
customValue: _otherSettingsViewModel.customBitcoinFeeRate,
) :
SettingsPickerCell(
title: S.current.settings_fee_priority,
items: priorityForWalletType(_otherSettingsViewModel.walletType),
displayItem: _otherSettingsViewModel.getDisplayPriority,
selectedItem: _otherSettingsViewModel.transactionPriority,
onItemSelected: _otherSettingsViewModel.onDisplayPrioritySelected,
),
if (_otherSettingsViewModel.changeRepresentativeEnabled)
SettingsCellWithArrow(
title: S.current.change_rep,
@@ -0,0 +1,78 @@
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:flutter/material.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
class SettingsPriorityPickerCell<ItemType> extends StandardListRow {
SettingsPriorityPickerCell(
{required String title,
required this.selectedItem,
required this.items,
this.displayItem,
this.images,
this.searchHintText,
this.isGridView = false,
this.matchingCriteria,
this.customValue,
this.customItemIndex,
this.onItemSelected})
: super(
title: title,
isSelected: false,
onTap: (BuildContext context) async {
var selectedAtIndex = items.indexOf(selectedItem);
double sliderValue = customValue ?? 0.0;
await showPopUp<void>(
context: context,
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setState) {
return Picker(
items: items,
displayItem: (ItemType item) => displayItem!(item, sliderValue.round()),
selectedAtIndex: selectedAtIndex,
customItemIndex: customItemIndex,
headerEnabled: false,
closeOnItemSelected: false,
mainAxisAlignment: MainAxisAlignment.center,
sliderValue: sliderValue,
onSliderChanged: (double newValue) =>
setState(() => sliderValue = newValue),
onItemSelected: (ItemType priority) {
setState(() => selectedAtIndex = items.indexOf(priority));
onItemSelected?.call(priority, sliderValue);
},
);
},
);
},
);
onItemSelected?.call(items[selectedAtIndex], sliderValue);
});
final ItemType selectedItem;
final List<ItemType> items;
final void Function(ItemType item, double customValue)? onItemSelected;
final String Function(ItemType item, int value)? displayItem;
final List<Image>? images;
final String? searchHintText;
final bool isGridView;
final bool Function(ItemType, String)? matchingCriteria;
double? customValue;
int? customItemIndex;
@override
Widget buildTrailing(BuildContext context) {
return Text(
displayItem?.call(selectedItem,customValue?.round() ?? 0) ?? selectedItem.toString(),
textAlign: TextAlign.right,
style: TextStyle(
fontSize: 14.0,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
);
}
}
@@ -0,0 +1,23 @@
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
class StandardPickerListItem<T> extends TransactionDetailsListItem {
StandardPickerListItem(
{required String title,
required String value,
required this.items,
required this.displayItem,
required this.onSliderChanged,
required this.onItemSelected,
required this.selectedIdx,
required this.customItemIndex,
required this.customValue})
: super(title: title, value: value);
final List<T> items;
final String Function(T item, double sliderValue) displayItem;
final Function(double) onSliderChanged;
final Function(T) onItemSelected;
final int selectedIdx;
final int customItemIndex;
double customValue;
}
@@ -0,0 +1,199 @@
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';
import 'package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart';
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/list_row.dart';
import 'package:cake_wallet/src/widgets/primary_button.dart';
import 'package:cake_wallet/src/widgets/standard_expandable_list.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/src/widgets/standard_picker_list.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/utils/show_pop_up.dart';
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
import 'package:mobx/mobx.dart';
class RBFDetailsPage extends BasePage {
RBFDetailsPage({required this.transactionDetailsViewModel});
@override
String get title => S.current.bump_fee;
final TransactionDetailsViewModel transactionDetailsViewModel;
bool _effectsInstalled = false;
@override
Widget body(BuildContext context) {
_setEffects(context);
return Column(
children: [
Expanded(
child: SectionStandardList(
sectionCount: 1,
itemCounter: (int _) => transactionDetailsViewModel.RBFListItems.length,
itemBuilder: (__, index) {
final item = transactionDetailsViewModel.RBFListItems[index];
if (item is StandartListItem) {
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: item.value));
showBar<void>(context, S.of(context).transaction_details_copied(item.title));
},
child: ListRow(title: '${item.title}:', value: item.value),
);
}
if (item is StandardExpandableListItem) {
return StandardExpandableList(
title: '${item.title}: ${item.expandableItems.length}',
expandableItems: item.expandableItems,
);
}
if (item is StandardPickerListItem) {
return StandardPickerList(
title: item.title,
value: item.value,
items: item.items,
displayItem: item.displayItem,
onSliderChanged: item.onSliderChanged,
onItemSelected: item.onItemSelected,
selectedIdx: item.selectedIdx,
customItemIndex: item.customItemIndex,
customValue: item.customValue,
);
}
if (item is TextFieldListItem) {
return TextFieldListRow(
title: item.title,
value: item.value,
onSubmitted: item.onSubmitted,
);
}
return Container();
}),
),
Padding(
padding: const EdgeInsets.all(24),
child: Observer(
builder: (_) => LoadingPrimaryButton(
onPressed: () async {
transactionDetailsViewModel
.replaceByFee(transactionDetailsViewModel.newFee.toString());
},
text: S.of(context).send,
isLoading:
transactionDetailsViewModel.sendViewModel.state is IsExecutingState,
color: Theme.of(context).primaryColor,
textColor: Colors.white,
))),
],
);
}
void _setEffects(BuildContext context) {
if (_effectsInstalled) {
return;
}
reaction((_) => transactionDetailsViewModel.sendViewModel.state, (ExecutionState state) {
if (state is FailureState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (BuildContext popupContext) {
return AlertWithOneAction(
alertTitle: S.of(popupContext).error,
alertContent: state.error,
buttonText: S.of(popupContext).ok,
buttonAction: () => Navigator.of(popupContext).pop());
});
});
}
if (state is AwaitingConfirmationState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<void>(
context: context,
builder: (BuildContext popupContext) {
return AlertWithTwoActions(
alertTitle: state.title ?? '',
alertContent: state.message ?? '',
rightButtonText: S.of(context).ok,
leftButtonText: S.of(context).cancel,
actionRightButton: () {
state.onConfirm?.call();
Navigator.of(popupContext).pop();
},
actionLeftButton: () {
state.onCancel?.call();
Navigator.of(popupContext).pop();
});
});
});
}
if (state is ExecutedSuccessfullyState) {
WidgetsBinding.instance.addPostFrameCallback((_) {
showPopUp<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();
// transactionStatePopup();
},
actionLeftButton: () => Navigator.of(popupContext).pop(),
feeFiatAmount:
transactionDetailsViewModel.pendingTransactionFeeFiatAmountFormatted,
fiatAmountValue:
transactionDetailsViewModel.pendingTransactionFiatAmountValueFormatted,
outputs: transactionDetailsViewModel.sendViewModel.outputs);
});
});
}
if (state is TransactionCommitted) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (context.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());
});
}
});
}
});
_effectsInstalled = true;
}
}
@@ -1,15 +1,18 @@
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/routes.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:cake_wallet/src/screens/new_wallet/widgets/select_button.dart';
import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_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';
import 'package:cake_wallet/src/screens/transaction_details/widgets/textfield_list_row.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/widgets/standard_list.dart';
import 'package:cake_wallet/utils/show_bar.dart';
import 'package:cake_wallet/view_model/transaction_details_view_model.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/standart_list_item.dart';
import 'package:cake_wallet/src/screens/base_page.dart';
import 'package:flutter_mobx/flutter_mobx.dart';
class TransactionDetailsPage extends BasePage {
TransactionDetailsPage({required this.transactionDetailsViewModel});
@@ -21,41 +24,62 @@ class TransactionDetailsPage extends BasePage {
@override
Widget body(BuildContext context) {
return SectionStandardList(
sectionCount: 1,
itemCounter: (int _) => transactionDetailsViewModel.items.length,
itemBuilder: (__, index) {
final item = transactionDetailsViewModel.items[index];
return Column(
children: [
Expanded(
child: SectionStandardList(
sectionCount: 1,
itemCounter: (int _) => transactionDetailsViewModel.items.length,
itemBuilder: (__, index) {
final item = transactionDetailsViewModel.items[index];
if (item is StandartListItem) {
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: item.value));
showBar<void>(context,
S.of(context).transaction_details_copied(item.title));
},
child:
ListRow(title: '${item.title}:', value: item.value),
);
}
if (item is StandartListItem) {
return GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: item.value));
showBar<void>(context, S.of(context).transaction_details_copied(item.title));
},
child: ListRow(title: '${item.title}:', value: item.value),
);
}
if (item is BlockExplorerListItem) {
return GestureDetector(
onTap: item.onTap,
child:
ListRow(title: '${item.title}:', value: item.value),
);
}
if (item is BlockExplorerListItem) {
return GestureDetector(
onTap: item.onTap,
child: ListRow(title: '${item.title}:', value: item.value),
);
}
if (item is TextFieldListItem) {
return TextFieldListRow(
title: item.title,
value: item.value,
onSubmitted: item.onSubmitted,
);
}
if (item is TextFieldListItem) {
return TextFieldListRow(
title: item.title,
value: item.value,
onSubmitted: item.onSubmitted,
);
}
return Container();
});
return Container();
}),
),
Observer(
builder: (_) {
if (transactionDetailsViewModel.canReplaceByFee) {
return Padding(
padding: const EdgeInsets.all(24),
child: SelectButton(
text: S.of(context).bump_fee,
onTap: () async {
Navigator.of(context).pushNamed(Routes.bumpFeePage,
arguments: transactionDetailsViewModel.transactionInfo);
},
),
);
}
return const SizedBox();
},
),
],
);
}
}
@@ -0,0 +1,7 @@
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
class StandardExpandableListItem<T> extends TransactionDetailsListItem {
StandardExpandableListItem({required String title, required this.expandableItems})
: super(title: title, value: '');
final List<T> expandableItems;
}
-1
View File
@@ -31,7 +31,6 @@ class CheckboxWidgetState extends State<CheckboxWidget> {
},
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
height: 24.0,
+254 -173
View File
@@ -24,6 +24,13 @@ class Picker<Item> extends StatefulWidget {
this.isGridView = false,
this.isSeparated = true,
this.hintText,
this.headerEnabled = true,
this.closeOnItemSelected = true,
this.sliderValue,
this.customItemIndex,
this.isWrapped = true,
this.borderColor,
this.onSliderChanged,
this.matchingCriteria,
}) : assert(hintText == null ||
matchingCriteria !=
@@ -40,6 +47,13 @@ class Picker<Item> extends StatefulWidget {
final bool isGridView;
final bool isSeparated;
final String? hintText;
final bool headerEnabled;
final bool closeOnItemSelected;
final double? sliderValue;
final int? customItemIndex;
final bool isWrapped;
final Color? borderColor;
final Function(double)? onSliderChanged;
final bool Function(Item, String)? matchingCriteria;
@override
@@ -124,8 +138,7 @@ class _PickerState<Item> extends State<Picker<Item>> {
containerHeight = height * 0.75;
}
return PickerWrapperWidget(
hasTitle: widget.title?.isNotEmpty ?? false,
final content = Column (
children: [
if (widget.title?.isNotEmpty ?? false)
Container(
@@ -144,61 +157,71 @@ class _PickerState<Item> extends State<Picker<Item>> {
),
Padding(
padding: EdgeInsets.symmetric(horizontal: padding),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(30)),
child: Container(
color: Theme.of(context).dialogTheme.backgroundColor,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: containerHeight,
maxWidth: ResponsiveLayoutUtilBase.kPopupWidth,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.hintText != null)
Padding(
padding: const EdgeInsets.all(16),
child: SearchBarWidget(searchController: searchController),
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(30),
border: Border.all(
color: widget.borderColor ?? Colors.transparent,
),
),
child: ClipRRect(
borderRadius: BorderRadius.all(Radius.circular(30)),
child: Container(
color: Theme.of(context).dialogTheme.backgroundColor,
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: containerHeight,
maxWidth: ResponsiveLayoutUtilBase.kPopupWidth,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
if (widget.hintText != null)
Padding(
padding: const EdgeInsets.all(16),
child: SearchBarWidget(
searchController: searchController, hintText: widget.hintText),
),
Divider(
color: Theme.of(context).extension<PickerTheme>()!.dividerColor,
height: 1,
),
Divider(
color: Theme.of(context).extension<PickerTheme>()!.dividerColor,
height: 1,
),
if (widget.selectedAtIndex != -1) buildSelectedItem(widget.selectedAtIndex),
Flexible(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
filteredItems.length > 3
? Scrollbar(
controller: controller,
child: itemsList(),
)
: itemsList(),
(widget.description?.isNotEmpty ?? false)
? Positioned(
bottom: padding,
left: padding,
right: padding,
child: Text(
widget.description!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
fontFamily: 'Lato',
decoration: TextDecoration.none,
color:
Theme.of(context).extension<CakeTextTheme>()!.titleColor,
if (widget.selectedAtIndex != -1 && widget.headerEnabled)
buildSelectedItem(widget.selectedAtIndex),
Flexible(
child: Stack(
alignment: Alignment.center,
children: <Widget>[
filteredItems.length > 3
? Scrollbar(
controller: controller,
child: itemsList(),
)
: itemsList(),
(widget.description?.isNotEmpty ?? false)
? Positioned(
bottom: padding,
left: padding,
right: padding,
child: Text(
widget.description!,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.w500,
fontFamily: 'Lato',
decoration: TextDecoration.none,
color:
Theme.of(context).extension<CakeTextTheme>()!.titleColor,
),
),
),
)
: Offstage(),
],
)
: Offstage(),
],
),
),
),
],
],
),
),
),
),
@@ -206,9 +229,23 @@ class _PickerState<Item> extends State<Picker<Item>> {
)
],
);
if (widget.isWrapped) {
return PickerWrapperWidget(
hasTitle: widget.title?.isNotEmpty ?? false,
children: [content],
);
} else {
return content;
}
}
Widget itemsList() {
final itemCount = !widget.headerEnabled
? items.length
: filteredItems.isEmpty
? 0
: filteredItems.length;
return Container(
color: Theme.of(context).extension<PickerTheme>()!.dividerColor,
child: widget.isGridView
@@ -216,13 +253,16 @@ class _PickerState<Item> extends State<Picker<Item>> {
padding: EdgeInsets.zero,
controller: controller,
shrinkWrap: true,
itemCount: filteredItems.isEmpty ? 0 : filteredItems.length,
itemCount: itemCount,
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
crossAxisSpacing: 2,
childAspectRatio: 3,
),
itemBuilder: (context, index) => buildItem(index),
itemBuilder: (context, index) =>
!widget.headerEnabled && widget.selectedAtIndex == index
? buildSelectedItem(index)
: buildItem(index),
)
: ListView.separated(
padding: EdgeInsets.zero,
@@ -234,83 +274,97 @@ class _PickerState<Item> extends State<Picker<Item>> {
height: 1,
)
: const SizedBox(),
itemCount: filteredItems.isEmpty ? 0 : filteredItems.length,
itemBuilder: (context, index) => buildItem(index),
itemCount: itemCount,
itemBuilder: (context, index) =>
!widget.headerEnabled && widget.selectedAtIndex == index
? buildSelectedItem(index)
: buildItem(index),
),
);
}
Widget buildItem(int index) {
final item = filteredItems[index];
final item = widget.headerEnabled ? filteredItems[index] : items[index];
final tag = item is Currency ? item.tag : null;
final icon = _getItemIcon(item);
final image = images.isNotEmpty ? filteredImages[index] : icon;
final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex;
final itemContent = Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: widget.mainAxisAlignment,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
image ?? Offstage(),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: image != null ? 12 : 0),
child: Row(
children: [
Flexible(
child: Text(
widget.displayItem?.call(item) ?? item.toString(),
softWrap: true,
style: TextStyle(
fontSize: 14,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
),
),
if (tag != null)
Align(
alignment: Alignment.topCenter,
child: Container(
width: 35.0,
height: 18.0,
child: Center(
child: Text(
tag,
style: TextStyle(
fontSize: 7.0,
fontFamily: 'Lato',
color: Theme.of(context).extension<CakeScrollbarTheme>()!.thumbColor,
),
),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
//border: Border.all(color: ),
color: Theme.of(context).extension<CakeScrollbarTheme>()!.trackColor,
),
),
),
],
),
),
),
],
);
return GestureDetector(
onTap: () {
Navigator.of(context).pop();
if (widget.closeOnItemSelected) Navigator.of(context).pop();
onItemSelected(item!);
},
child: Container(
height: 55,
height: isCustomItem ? 95 : 55,
color: Theme.of(context).dialogTheme.backgroundColor,
padding: EdgeInsets.symmetric(horizontal: 24),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: widget.mainAxisAlignment,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
image ?? Offstage(),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: image != null ? 12 : 0),
child: Row(
children: [
Flexible(
child: Text(
widget.displayItem?.call(item) ?? item.toString(),
softWrap: true,
style: TextStyle(
fontSize: 14,
fontFamily: 'Lato',
fontWeight: FontWeight.w600,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
),
),
if (tag != null)
Align(
alignment: Alignment.topCenter,
child: Container(
width: 35.0,
height: 18.0,
child: Center(
child: Text(
tag,
style: TextStyle(
fontSize: 7.0,
fontFamily: 'Lato',
color:
Theme.of(context).extension<CakeScrollbarTheme>()!.thumbColor,
),
),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
//border: Border.all(color: ),
color: Theme.of(context).extension<CakeScrollbarTheme>()!.trackColor,
),
),
),
],
),
),
),
],
),
child: isCustomItem
? Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
itemContent,
buildSlider(index: index, isActivated: widget.selectedAtIndex == index)
],
)
: itemContent,
),
);
}
@@ -323,69 +377,80 @@ class _PickerState<Item> extends State<Picker<Item>> {
final image = images.isNotEmpty ? images[index] : icon;
return GestureDetector(
onTap: () {
Navigator.of(context).pop();
},
child: Container(
height: 55,
color: Theme.of(context).dialogTheme.backgroundColor,
padding: EdgeInsets.symmetric(horizontal: 24),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: widget.mainAxisAlignment,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
image ?? Offstage(),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: image != null ? 12 : 0),
child: Row(
children: [
Flexible(
child: Text(
widget.displayItem?.call(item) ?? item.toString(),
softWrap: true,
style: TextStyle(
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.w700,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
final isCustomItem = widget.customItemIndex != null && index == widget.customItemIndex;
final itemContent = Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: widget.mainAxisAlignment,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
image ?? Offstage(),
Expanded(
child: Padding(
padding: EdgeInsets.only(left: image != null ? 12 : 0),
child: Row(
children: [
Flexible(
child: Text(
widget.displayItem?.call(item) ?? item.toString(),
softWrap: true,
style: TextStyle(
fontSize: 16,
fontFamily: 'Lato',
fontWeight: FontWeight.w700,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor,
decoration: TextDecoration.none,
),
),
),
if (tag != null)
Align(
alignment: Alignment.topCenter,
child: Container(
width: 35.0,
height: 18.0,
child: Center(
child: Text(
tag,
style: TextStyle(
fontSize: 7.0,
fontFamily: 'Lato',
color: Theme.of(context).extension<CakeScrollbarTheme>()!.thumbColor,
),
),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
//border: Border.all(color: ),
color: Theme.of(context).extension<CakeScrollbarTheme>()!.trackColor,
),
),
if (tag != null)
Align(
alignment: Alignment.topCenter,
child: Container(
width: 35.0,
height: 18.0,
child: Center(
child: Text(
tag,
style: TextStyle(
fontSize: 7.0,
fontFamily: 'Lato',
color:
Theme.of(context).extension<CakeScrollbarTheme>()!.thumbColor,
),
),
),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6.0),
//border: Border.all(color: ),
color: Theme.of(context).extension<CakeScrollbarTheme>()!.trackColor,
),
),
),
],
),
),
),
],
),
Icon(Icons.check_circle, color: Theme.of(context).primaryColor),
],
),
),
Icon(Icons.check_circle, color: Theme.of(context).primaryColor),
],
);
return GestureDetector(
onTap: () {
if (widget.closeOnItemSelected) Navigator.of(context).pop();
},
child: Container(
height: isCustomItem ? 95 : 55,
color: Theme.of(context).dialogTheme.backgroundColor,
padding: EdgeInsets.symmetric(horizontal: 24),
child: isCustomItem
? Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
itemContent,
buildSlider(index: index, isActivated: widget.selectedAtIndex == index)
],
)
: itemContent,
),
);
}
@@ -418,4 +483,20 @@ class _PickerState<Item> extends State<Picker<Item>> {
return null;
}
Widget buildSlider({required int index, required bool isActivated}) {
return Row(
children: <Widget>[
Expanded(
child: Slider(
value: widget.sliderValue ?? 1,
onChanged: isActivated ? widget.onSliderChanged : null,
min: 1,
max: 100,
divisions: 100,
),
),
],
);
}
}
+1 -1
View File
@@ -19,7 +19,7 @@ class SearchBarWidget extends StatelessWidget {
controller: searchController,
style: TextStyle(color: Theme.of(context).extension<PickerTheme>()!.searchHintColor),
decoration: InputDecoration(
hintText: hintText ?? S.of(context).search_currency,
hintText: hintText ?? S.of(context).search,
hintStyle: TextStyle(color: Theme.of(context).extension<PickerTheme>()!.searchHintColor),
prefixIcon: Image.asset("assets/images/search_icon.png",
color: Theme.of(context).extension<PickerTheme>()!.searchIconColor),
@@ -0,0 +1,58 @@
import 'package:cake_wallet/themes/extensions/cake_text_theme.dart';
import 'package:cake_wallet/themes/extensions/transaction_trade_theme.dart';
import 'package:flutter/material.dart';
class StandardExpandableList<T> extends StatelessWidget {
StandardExpandableList({
required this.title,
required this.expandableItems,
this.decoration,
});
final String title;
final List<T> expandableItems;
final Decoration? decoration;
@override
Widget build(BuildContext context) {
return Container(
decoration: decoration ??
BoxDecoration(
color: Theme.of(context).colorScheme.background,
),
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
iconColor: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
collapsedIconColor:
Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
title: Text(
title,
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<TransactionTradeTheme>()!.detailsTitlesColor,
),
textAlign: TextAlign.left,
),
children: expandableItems.map((item) {
return Padding(
padding: const EdgeInsets.only(left: 16.0, bottom: 8.0),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
item.toString(),
maxLines: 1,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w500,
color: Theme.of(context).extension<CakeTextTheme>()!.titleColor),
),
),
);
}).toList(),
),
),
);
}
}
+81
View File
@@ -0,0 +1,81 @@
import 'package:cake_wallet/src/widgets/list_row.dart';
import 'package:cake_wallet/src/widgets/picker.dart';
import 'package:cake_wallet/themes/extensions/picker_theme.dart';
import 'package:flutter/material.dart';
class StandardPickerList<T> extends StatefulWidget {
StandardPickerList({
Key? key,
required this.title,
required this.value,
required this.items,
required this.displayItem,
required this.onSliderChanged,
required this.onItemSelected,
required this.selectedIdx,
required this.customItemIndex,
required this.customValue,
}) : super(key: key);
final String title;
final List<T> items;
final int customItemIndex;
final String Function(T item, double sliderValue) displayItem;
final Function(double) onSliderChanged;
final Function(T) onItemSelected;
final String value;
final int selectedIdx;
final double customValue;
@override
_StandardPickerListState<T> createState() => _StandardPickerListState<T>();
}
class _StandardPickerListState<T> extends State<StandardPickerList<T>> {
late String value;
late int selectedIdx;
late double customValue;
@override
void initState() {
super.initState();
value = widget.value;
selectedIdx = widget.selectedIdx;
customValue = widget.customValue;
}
@override
Widget build(BuildContext context) {
String adaptedDisplayItem(T item) => widget.displayItem(item, customValue);
return Column(
children: [
ListRow(title: '${widget.title}:', value: value),
Padding(
padding: const EdgeInsets.only(left: 24, right: 24, top: 0, bottom: 24),
child: Picker(
items: widget.items,
displayItem: adaptedDisplayItem,
selectedAtIndex: selectedIdx,
customItemIndex: widget.customItemIndex,
headerEnabled: false,
closeOnItemSelected: false,
mainAxisAlignment: MainAxisAlignment.center,
sliderValue: customValue,
isWrapped: false,
borderColor: Theme.of(context).extension<PickerTheme>()!.dividerColor,
onSliderChanged: (newValue) {
setState(() => customValue = newValue);
value = widget.onSliderChanged(newValue).toString();
},
onItemSelected: (T item) {
setState(() => selectedIdx = widget.items.indexOf(item));
value = widget.onItemSelected(item).toString();
},
),
),
],
);
}
}
+14 -2
View File
@@ -105,6 +105,7 @@ abstract class SettingsStoreBase with Store {
required this.lookupsUnstoppableDomains,
required this.lookupsOpenAlias,
required this.lookupsENS,
required this.customBitcoinFeeRate,
TransactionPriority? initialBitcoinTransactionPriority,
TransactionPriority? initialMoneroTransactionPriority,
TransactionPriority? initialHavenTransactionPriority,
@@ -504,6 +505,11 @@ abstract class SettingsStoreBase with Store {
(PinCodeRequiredDuration pinCodeInterval) => secureStorage.write(
key: SecureKey.pinTimeOutDuration, value: pinCodeInterval.value.toString()));
reaction(
(_) => customBitcoinFeeRate,
(int customBitcoinFeeRate) =>
_sharedPreferences.setInt(PreferencesKey.customBitcoinFeeRate, customBitcoinFeeRate));
this.nodes.observe((change) {
if (change.newValue != null && change.key != null) {
_saveCurrentNode(change.newValue!, change.key!);
@@ -690,6 +696,9 @@ abstract class SettingsStoreBase with Store {
String deviceName;
@observable
int customBitcoinFeeRate;
final FlutterSecureStorage _secureStorage;
final SharedPreferences _sharedPreferences;
final BackgroundTasks _backgroundTasks;
@@ -834,6 +843,7 @@ abstract class SettingsStoreBase with Store {
sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true;
final lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
final lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
final customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
// If no value
if (pinLength == null || pinLength == 0) {
@@ -1068,6 +1078,7 @@ abstract class SettingsStoreBase with Store {
lookupsUnstoppableDomains: lookupsUnstoppableDomains,
lookupsOpenAlias: lookupsOpenAlias,
lookupsENS: lookupsENS,
customBitcoinFeeRate: customBitcoinFeeRate,
initialMoneroTransactionPriority: moneroTransactionPriority,
initialBitcoinTransactionPriority: bitcoinTransactionPriority,
initialHavenTransactionPriority: havenTransactionPriority,
@@ -1159,7 +1170,8 @@ abstract class SettingsStoreBase with Store {
isAppSecure = sharedPreferences.getBool(PreferencesKey.isAppSecureKey) ?? isAppSecure;
disableBuy = sharedPreferences.getBool(PreferencesKey.disableBuyKey) ?? disableBuy;
disableSell = sharedPreferences.getBool(PreferencesKey.disableSellKey) ?? disableSell;
disableBulletin = sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
disableBulletin =
sharedPreferences.getBool(PreferencesKey.disableBulletinKey) ?? disableBulletin;
walletListOrder =
WalletListOrderType.values[sharedPreferences.getInt(PreferencesKey.walletListOrder) ?? 0];
walletListAscending = sharedPreferences.getBool(PreferencesKey.walletListAscending) ?? true;
@@ -1200,7 +1212,7 @@ abstract class SettingsStoreBase with Store {
sharedPreferences.getBool(PreferencesKey.lookupsUnstoppableDomains) ?? true;
lookupsOpenAlias = sharedPreferences.getBool(PreferencesKey.lookupsOpenAlias) ?? true;
lookupsENS = sharedPreferences.getBool(PreferencesKey.lookupsENS) ?? true;
customBitcoinFeeRate = sharedPreferences.getInt(PreferencesKey.customBitcoinFeeRate) ?? 1;
final nodeId = sharedPreferences.getInt(PreferencesKey.currentNodeIdKey);
final bitcoinElectrumServerId =
sharedPreferences.getInt(PreferencesKey.currentBitcoinElectrumSererIdKey);
+11 -3
View File
@@ -121,11 +121,19 @@ abstract class OutputBase with Store {
return solana!.getEstimateFees(_wallet) ?? 0.0;
}
final fee = _wallet.calculateEstimatedFee(
int? fee = _wallet.calculateEstimatedFee(
_settingsStore.priority[_wallet.type]!, formattedCryptoAmount);
if (_wallet.type == WalletType.bitcoin ||
_wallet.type == WalletType.litecoin ||
if (_wallet.type == WalletType.bitcoin) {
if (_settingsStore.priority[_wallet.type] == bitcoin!.getBitcoinTransactionPriorityCustom()) {
fee = bitcoin!.getFeeAmountWithFeeRate(
_settingsStore.customBitcoinFeeRate, formattedCryptoAmount, 1, 1);
}
return bitcoin!.formatterBitcoinAmountToDouble(amount: fee);
}
if (_wallet.type == WalletType.litecoin ||
_wallet.type == WalletType.bitcoinCash) {
return bitcoin!.formatterBitcoinAmountToDouble(amount: fee);
}
+57 -3
View File
@@ -39,6 +39,7 @@ import 'package:cake_wallet/entities/parsed_address.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/haven/haven.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:collection/collection.dart';
part 'send_view_model.g.dart';
@@ -68,9 +69,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
_settingsStore = appStore.settingsStore,
fiatFromSettings = appStore.settingsStore.fiatCurrency,
super(appStore: appStore) {
if (wallet.type == WalletType.bitcoin &&
_settingsStore.priority[wallet.type] == bitcoinTransactionPriorityCustom) {
setTransactionPriority(bitcoinTransactionPriorityMedium);
}
final priority = _settingsStore.priority[wallet.type];
final priorities = priorityForWalletType(wallet.type);
if (!priorityForWalletType(wallet.type).contains(priority) && priorities.isNotEmpty) {
_settingsStore.priority[wallet.type] = priorities.first;
}
@@ -152,6 +156,21 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return priority;
}
int? getCustomPriorityIndex(List<TransactionPriority> priorities) {
if (wallet.type == WalletType.bitcoin) {
final customItem = priorities.firstWhereOrNull((element) => element == bitcoin!.getBitcoinTransactionPriorityCustom());
return customItem != null ? priorities.indexOf(customItem) : null;
}
return null;
}
@computed
int get customBitcoinFeeRate => _settingsStore.customBitcoinFeeRate;
void set customBitcoinFeeRate(int value) => _settingsStore.customBitcoinFeeRate = value;
CryptoCurrency get currency => wallet.currency;
Validator<String> get amountValidator =>
@@ -323,6 +342,29 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
return null;
}
@action
Future<void> replaceByFee(String txId, String newFee) async {
state = IsExecutingState();
final isSufficient = await bitcoin!.isChangeSufficientForFee(wallet, txId, newFee);
if (!isSufficient) {
state = AwaitingConfirmationState(
title: S.current.confirm_fee_deduction,
message: S.current.confirm_fee_deduction_content,
onConfirm: () async {
pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee);
state = ExecutedSuccessfullyState();
},
onCancel: () {
state = FailureState('Insufficient change for fee');
});
} else {
pendingTransaction = await bitcoin!.replaceByFee(wallet, txId, newFee);
state = ExecutedSuccessfullyState();
}
}
@action
Future<void> commitTransaction() async {
if (pendingTransaction == null) {
@@ -380,7 +422,8 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
case WalletType.bitcoin:
case WalletType.litecoin:
case WalletType.bitcoinCash:
return bitcoin!.createBitcoinTransactionCredentials(outputs, priority: priority!);
return bitcoin!.createBitcoinTransactionCredentials(outputs,
priority: priority!, feeRate: customBitcoinFeeRate);
case WalletType.monero:
return monero!
@@ -406,9 +449,14 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
}
}
String displayFeeRate(dynamic priority) {
String displayFeeRate(dynamic priority, int? customValue) {
final _priority = priority as TransactionPriority;
if (walletType == WalletType.bitcoin) {
final rate = bitcoin!.getFeeRate(wallet, _priority);
return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, customRate: customValue);
}
if (isElectrumWallet) {
final rate = bitcoin!.getFeeRate(wallet, _priority);
return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate);
@@ -420,6 +468,12 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
bool _isEqualCurrency(String currency) =>
wallet.balance.keys.any((e) => currency.toLowerCase() == e.title.toLowerCase());
TransactionPriority get bitcoinTransactionPriorityCustom =>
bitcoin!.getBitcoinTransactionPriorityCustom();
TransactionPriority get bitcoinTransactionPriorityMedium =>
bitcoin!.getBitcoinTransactionPriorityMedium();
@action
void onClose() => _settingsStore.fiatCurrency = fiatFromSettings;
@@ -11,6 +11,7 @@ import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:mobx/mobx.dart';
import 'package:package_info/package_info.dart';
import 'package:collection/collection.dart';
part 'other_settings_view_model.g.dart';
@@ -77,6 +78,8 @@ abstract class OtherSettingsViewModelBase with Store {
ProviderType get sellProviderType =>
_settingsStore.defaultSellProviders[walletType] ?? ProviderType.askEachTime;
String getDisplayPriority(dynamic priority) {
final _priority = priority as TransactionPriority;
@@ -90,6 +93,19 @@ abstract class OtherSettingsViewModelBase with Store {
return priority.toString();
}
String getDisplayBitcoinPriority(dynamic priority, int customValue) {
final _priority = priority as TransactionPriority;
if (_wallet.type == WalletType.bitcoin ||
_wallet.type == WalletType.litecoin ||
_wallet.type == WalletType.bitcoinCash) {
final rate = bitcoin!.getFeeRate(_wallet, _priority);
return bitcoin!.bitcoinTransactionPriorityWithLabel(_priority, rate, customRate: customValue);
}
return priority.toString();
}
String getBuyProviderType(dynamic buyProviderType) {
final _buyProviderType = buyProviderType as ProviderType;
return _buyProviderType == ProviderType.askEachTime
@@ -105,7 +121,24 @@ abstract class OtherSettingsViewModelBase with Store {
}
void onDisplayPrioritySelected(TransactionPriority priority) =>
_settingsStore.priority[_wallet.type] = priority;
_settingsStore.priority[walletType] = priority;
void onDisplayBitcoinPrioritySelected(TransactionPriority priority, double customValue) {
if (_wallet.type == WalletType.bitcoin) {
_settingsStore.customBitcoinFeeRate = customValue.round();
}
_settingsStore.priority[_wallet.type] = priority;
}
@computed
double get customBitcoinFeeRate => _settingsStore.customBitcoinFeeRate.toDouble();
int? get customPriorityItemIndex {
final priorities = priorityForWalletType(walletType);
final customItem = priorities
.firstWhereOrNull((element) => element == bitcoin!.getBitcoinTransactionPriorityCustom());
return customItem != null ? priorities.indexOf(customItem) : null;
}
@action
ProviderType onBuyProviderTypeSelected(ProviderType buyProviderType) =>
@@ -1,20 +1,27 @@
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:cake_wallet/bitcoin/bitcoin.dart';
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:cake_wallet/monero/monero.dart';
import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_item.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';
import 'package:cake_wallet/src/screens/transaction_details/transaction_details_list_item.dart';
import 'package:cake_wallet/src/screens/transaction_details/blockexplorer_list_item.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cake_wallet/src/screens/transaction_details/transaction_expandable_list_item.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/utils/date_formatter.dart';
import 'package:cake_wallet/entities/transaction_description.dart';
import 'package:cake_wallet/view_model/send/send_view_model.dart';
import 'package:collection/collection.dart';
import 'package:cw_core/transaction_direction.dart';
import 'package:cw_core/transaction_info.dart';
import 'package:cw_core/transaction_priority.dart';
import 'package:cw_core/wallet_base.dart';
import 'package:cw_core/wallet_type.dart';
import 'package:hive/hive.dart';
import 'package:intl/src/intl/date_format.dart';
import 'package:mobx/mobx.dart';
import 'package:cake_wallet/store/settings_store.dart';
import 'package:cake_wallet/generated/i18n.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:cake_wallet/monero/monero.dart';
part 'transaction_details_view_model.g.dart';
@@ -26,8 +33,11 @@ abstract class TransactionDetailsViewModelBase with Store {
{required this.transactionInfo,
required this.transactionDescriptionBox,
required this.wallet,
required this.settingsStore})
required this.settingsStore,
required this.sendViewModel})
: items = [],
RBFListItems = [],
newFee = 0,
isRecipientAddressShown = false,
showRecipientAddress = settingsStore.shouldSaveRecipientAddress {
final dateFormat = DateFormatter.withCurrentLocal();
@@ -38,6 +48,10 @@ abstract class TransactionDetailsViewModelBase with Store {
_addMoneroListItems(tx, dateFormat);
break;
case WalletType.bitcoin:
_addElectrumListItems(tx, dateFormat);
_addBumpFeesListItems(tx);
_checkForRBF();
break;
case WalletType.litecoin:
case WalletType.bitcoinCash:
_addElectrumListItems(tx, dateFormat);
@@ -109,10 +123,20 @@ abstract class TransactionDetailsViewModelBase with Store {
final Box<TransactionDescription> transactionDescriptionBox;
final SettingsStore settingsStore;
final WalletBase wallet;
final SendViewModel sendViewModel;
final List<TransactionDetailsListItem> items;
final List<TransactionDetailsListItem> RBFListItems;
bool showRecipientAddress;
bool isRecipientAddressShown;
int newFee;
TransactionPriority? transactionPriority;
@observable
bool _canReplaceByFee = false;
@computed
bool get canReplaceByFee => _canReplaceByFee /*&& transactionInfo.confirmations <= 0*/;
String _explorerUrl(WalletType type, String txId) {
switch (type) {
@@ -305,4 +329,88 @@ abstract class TransactionDetailsViewModelBase with Store {
items.addAll(_items);
}
void _addBumpFeesListItems(TransactionInfo tx) {
transactionPriority = bitcoin!.getBitcoinTransactionPriorityMedium();
newFee = bitcoin!.getFeeAmountForPriority(
wallet,
bitcoin!.getBitcoinTransactionPriorityMedium(),
transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1);
RBFListItems.add(StandartListItem(
title: S.current.old_fee,
value: tx.feeFormatted() ?? '0.0'));
final priorities = priorityForWalletType(wallet.type);
final selectedItem = priorities.indexOf(sendViewModel.transactionPriority);
final customItem = priorities.firstWhereOrNull(
(element) => element == sendViewModel.bitcoinTransactionPriorityCustom);
final customItemIndex = customItem != null ? priorities.indexOf(customItem) : null;
RBFListItems.add(StandardPickerListItem(
title: S.current.estimated_new_fee,
value: bitcoin!.formatterBitcoinAmountToString(amount: newFee) + ' ${walletTypeToCryptoCurrency(wallet.type)}',
items: priorityForWalletType(wallet.type),
customValue: settingsStore.customBitcoinFeeRate.toDouble(),
selectedIdx: selectedItem,
customItemIndex: customItemIndex ?? 0,
displayItem: (dynamic priority, double sliderValue) =>
sendViewModel.displayFeeRate(priority, sliderValue.round()),
onSliderChanged: (double newValue) =>
setNewFee(value: newValue, priority: transactionPriority!),
onItemSelected: (dynamic item) {
transactionPriority = item as TransactionPriority;
return setNewFee(priority: transactionPriority!);
}));
if (transactionInfo.inputAddresses != null) {
RBFListItems.add(StandardExpandableListItem(
title: S.current.inputs, expandableItems: transactionInfo.inputAddresses!));
}
if (transactionInfo.outputAddresses != null) {
RBFListItems.add(StandardExpandableListItem(
title: S.current.outputs, expandableItems: transactionInfo.outputAddresses!));
}
}
@action
Future<void> _checkForRBF() async {
if (wallet.type == WalletType.bitcoin &&
transactionInfo.direction == TransactionDirection.outgoing) {
if (await bitcoin!.canReplaceByFee(wallet, transactionInfo.id)) {
_canReplaceByFee = true;
}
}
}
String setNewFee({double? value, required TransactionPriority priority}) {
newFee = priority == bitcoin!.getBitcoinTransactionPriorityCustom() && value != null
? bitcoin!.getFeeAmountWithFeeRate(
wallet,
value.round(),
transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1)
: bitcoin!.getFeeAmountForPriority(
wallet,
priority,
transactionInfo.inputAddresses?.length ?? 1,
transactionInfo.outputAddresses?.length ?? 1);
return bitcoin!.formatterBitcoinAmountToString(amount: newFee);
}
void replaceByFee(String newFee) => sendViewModel.replaceByFee(transactionInfo.id, newFee);
@computed
String get pendingTransactionFiatAmountValueFormatted => sendViewModel.isFiatDisabled
? ''
: sendViewModel.pendingTransactionFiatAmount + ' ' + sendViewModel.fiat.title;
@computed
String get pendingTransactionFeeFiatAmountFormatted => sendViewModel.isFiatDisabled
? ''
: sendViewModel.pendingTransactionFeeFiatAmount + ' ' + sendViewModel.fiat.title;
}