mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-21 06:18:42 +02:00
Add rbf (#1323)
* 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:
co-authored by
OmarHatem
Adegoke David
parent
071643b662
commit
291aa710c6
+65
-18
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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!));
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user