mirror of
https://github.com/Such-Software/hash-wallet.git
synced 2026-10-17 05:41:29 +02:00
feat: Swap flow for Pay Anything. (#2489)
* feat: Swap flow for Pay Anything. [WIP] * feat: Pay Anything Swap Flow [WIP] * feat: Pay anything swap flow [WIP] - Add fiat values for send and receive amounts to swap details confirmation bottomsheet * feat: Pay anything swap flow [WIP] - Switch exchangeviewmodel back to factory - Expand address box to fill width * Handle second swipe to swap cta * Add fiat for receive amount * Scope Observers for bottomsheets and add validator * fix: Repeated instances issue for exchangeVM * fix: Estimated fee and balance not correctly updating after wallet switching occurs * fix: Available balance not updating properly after wallet switching * Fix: Give wallet some time to open before populating send info --------- Co-authored-by: OmarHatem <omarh.ismail1@gmail.com>
This commit is contained in:
co-authored by
OmarHatem
parent
31b9bb7f53
commit
d1b62f2664
+21
@@ -43,6 +43,9 @@ import 'package:cake_wallet/src/screens/start_tor/start_tor_page.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/bottom_sheet_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/key_service/wallet_connect_key_service.dart';
|
||||
import 'package:cake_wallet/src/screens/wallet_connect/services/walletkit_service.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart';
|
||||
|
||||
import 'package:cake_wallet/store/dashboard/order_filter_store.dart';
|
||||
import 'package:cake_wallet/themes/core/theme_store.dart';
|
||||
import 'package:cake_wallet/view_model/dev/monero_background_sync.dart';
|
||||
@@ -530,6 +533,7 @@ Future<void> setup({
|
||||
getIt.get<UnspentCoinsListViewModel>(),
|
||||
getIt.get<FeesViewModel>(),
|
||||
_walletInfoSource,
|
||||
getIt.get<FiatConversionStore>(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1113,6 +1117,7 @@ Future<void> setup({
|
||||
tradesStore: getIt.get<TradesStore>(),
|
||||
sendViewModel: getIt.get<SendViewModel>(),
|
||||
feesViewModel: getIt.get<FeesViewModel>(),
|
||||
fiatConversionStore: getIt.get<FiatConversionStore>(),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -1133,6 +1138,22 @@ Future<void> setup({
|
||||
|
||||
getIt.registerFactory(() => ExchangeTemplatePage(getIt.get<ExchangeViewModel>()));
|
||||
|
||||
getIt.registerFactoryParam<SwapConfirmationBottomSheet, PaymentFlowResult, void>(
|
||||
(paymentFlowResult, _) => SwapConfirmationBottomSheet(
|
||||
paymentFlowResult: paymentFlowResult,
|
||||
currentTheme: getIt.get<AppStore>().themeStore.currentTheme,
|
||||
exchangeViewModel: getIt.get<ExchangeViewModel>(),
|
||||
authService: getIt.get<AuthService>(),
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactory<SwapDetailsBottomSheet>(
|
||||
() => SwapDetailsBottomSheet(
|
||||
currentTheme: getIt.get<AppStore>().themeStore.currentTheme,
|
||||
exchangeTradeViewModel: getIt.get<ExchangeTradeViewModel>(),
|
||||
),
|
||||
);
|
||||
|
||||
getIt.registerFactory(() => PaymentViewModel(
|
||||
appStore: getIt.get<AppStore>(),
|
||||
walletInfoSource: _walletInfoSource,
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:cake_wallet/utils/debounce.dart';
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cw_core/sync_status.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/entities/parse_address_from_domain.dart';
|
||||
import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed.dart';
|
||||
@@ -129,8 +130,7 @@ class ExchangePage extends BasePage {
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
size: 16,
|
||||
);
|
||||
final _closeButton =
|
||||
currentTheme.isDark ? closeButtonImageDarkTheme : closeButtonImage;
|
||||
final _closeButton = currentTheme.isDark ? closeButtonImageDarkTheme : closeButtonImage;
|
||||
|
||||
bool isMobileView = responsiveLayoutUtil.shouldRenderMobileUI;
|
||||
|
||||
@@ -368,10 +368,13 @@ class ExchangePage extends BasePage {
|
||||
|
||||
void applyTemplate(
|
||||
BuildContext context, ExchangeViewModel exchangeViewModel, ExchangeTemplate template) async {
|
||||
final depositCryptoCurrency = CryptoCurrency.safeParseCurrencyFromString(template.depositCurrency);
|
||||
final receiveCryptoCurrency = CryptoCurrency.safeParseCurrencyFromString(template.receiveCurrency);
|
||||
final depositCryptoCurrency =
|
||||
CryptoCurrency.safeParseCurrencyFromString(template.depositCurrency);
|
||||
final receiveCryptoCurrency =
|
||||
CryptoCurrency.safeParseCurrencyFromString(template.receiveCurrency);
|
||||
|
||||
if (depositCryptoCurrency == null || receiveCryptoCurrency == null) { ///TO DO: add support for user tokens
|
||||
if (depositCryptoCurrency == null || receiveCryptoCurrency == null) {
|
||||
///TO DO: add support for user tokens
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -610,9 +613,15 @@ class ExchangePage extends BasePage {
|
||||
});
|
||||
|
||||
if (initialPaymentRequest != null) {
|
||||
exchangeViewModel.receiveCurrency = CryptoCurrency.fromString(initialPaymentRequest!.scheme);
|
||||
exchangeViewModel.depositAmount = initialPaymentRequest!.amount;
|
||||
exchangeViewModel.receiveAddress = initialPaymentRequest!.address;
|
||||
try {
|
||||
exchangeViewModel.receiveCurrency =
|
||||
CryptoCurrency.fromString(initialPaymentRequest!.scheme);
|
||||
exchangeViewModel.receiveAmount = initialPaymentRequest!.amount;
|
||||
exchangeViewModel.receiveAddress = initialPaymentRequest!.address;
|
||||
} catch (e) {
|
||||
printV('error: ${e.toString()}');
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
_isReactionsSet = true;
|
||||
|
||||
@@ -1,17 +1,22 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cake_wallet/core/open_crypto_pay/open_cryptopay_service.dart';
|
||||
import 'package:cake_wallet/entities/priority_for_wallet_type.dart';
|
||||
import 'package:cake_wallet/src/screens/receive/widgets/currency_input_field.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/payment_confirmation_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/wallet_switcher_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/swap_confirmation_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_checkbox.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/currency_picker.dart';
|
||||
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:cake_wallet/utils/payment_request.dart';
|
||||
import 'package:cake_wallet/utils/responsive_layout_util.dart';
|
||||
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/wallet_switcher_view_model.dart';
|
||||
import 'package:cake_wallet/exchange/trade.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/currency.dart';
|
||||
import 'package:cake_wallet/routes.dart';
|
||||
@@ -29,6 +34,7 @@ import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/src/widgets/address_text_field.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
|
||||
class SendCard extends StatefulWidget {
|
||||
SendCard({
|
||||
@@ -100,6 +106,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
final FocusNode addressFocusNode;
|
||||
|
||||
bool _effectsInstalled = false;
|
||||
BuildContext? loadingBottomSheetContext;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@@ -114,18 +121,9 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(timeStamp) {
|
||||
if (mounted) {
|
||||
// TODO: revert this as well
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
alertTitle: S.of(context).error,
|
||||
alertContent: S.of(context).unmatched_currencies,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () => Navigator.of(context).pop(),
|
||||
);
|
||||
},
|
||||
);
|
||||
final separator = initialPaymentRequest!.scheme.isNotEmpty ? ":" : "";
|
||||
final uri = initialPaymentRequest!.scheme + separator + initialPaymentRequest!.address;
|
||||
_handlePaymentFlow(uri, initialPaymentRequest!);
|
||||
}
|
||||
},
|
||||
);
|
||||
@@ -145,9 +143,6 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
|
||||
Future<void> _handlePaymentFlow(String uri, PaymentRequest paymentRequest) async {
|
||||
try {
|
||||
// TODO: just remove this line
|
||||
throw "Pay Anything is temporarily disabled";
|
||||
|
||||
final result = await paymentViewModel.processAddress(uri);
|
||||
|
||||
switch (result.type) {
|
||||
@@ -201,7 +196,7 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
paymentRequest,
|
||||
result,
|
||||
),
|
||||
onSwap: () => Navigator.of(context).pushNamed(Routes.exchange, arguments: paymentRequest),
|
||||
onSwap: () => _handleSwapFlow(paymentViewModel, result),
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -246,6 +241,25 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
walletSwitcherViewModel.selectWallet(result.wallet!);
|
||||
final success = await walletSwitcherViewModel.switchToSelectedWallet();
|
||||
if (success) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
loadingBottomSheetContext = context;
|
||||
return LoadingBottomSheet(
|
||||
titleText: S.of(context).loading_your_wallet,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
await Future.delayed(const Duration(seconds: 2));
|
||||
if (loadingBottomSheetContext != null &&
|
||||
loadingBottomSheetContext!.mounted) {
|
||||
Navigator.of(loadingBottomSheetContext!).pop();
|
||||
}
|
||||
_applyPaymentRequest(paymentRequest);
|
||||
}
|
||||
}
|
||||
@@ -261,6 +275,17 @@ class SendCardState extends State<SendCard> with AutomaticKeepAliveClientMixin<S
|
||||
noteController.text = paymentRequest.note;
|
||||
}
|
||||
|
||||
Future<void> _handleSwapFlow(PaymentViewModel paymentViewModel, PaymentFlowResult result) async {
|
||||
Navigator.of(context).pop();
|
||||
final bottomSheet = getIt.get<SwapConfirmationBottomSheet>(param1: result);
|
||||
await showModalBottomSheet<Trade?>(
|
||||
context: context,
|
||||
isDismissible: true,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) => bottomSheet,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context);
|
||||
|
||||
@@ -124,7 +124,7 @@ class BaseTextFormField extends StatelessWidget {
|
||||
isDense: isDense,
|
||||
alignLabelWithHint: alignLabelWithHint,
|
||||
contentPadding: contentPadding,
|
||||
floatingLabelBehavior: FloatingLabelBehavior.never,
|
||||
floatingLabelBehavior: floatingLabelBehavior ?? FloatingLabelBehavior.never,
|
||||
prefixIconConstraints: prefixIconConstraints ??
|
||||
const BoxConstraints(minWidth: 0, minHeight: 0),
|
||||
prefix: prefix,
|
||||
|
||||
@@ -0,0 +1,554 @@
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:cake_wallet/core/amount_validator.dart';
|
||||
import 'package:cake_wallet/core/auth_service.dart';
|
||||
import 'package:cake_wallet/di.dart';
|
||||
import 'package:cake_wallet/exchange/limits_state.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange/widgets/present_provider_picker.dart';
|
||||
import 'package:cake_wallet/src/widgets/base_text_form_field.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/swap_details_bottom_sheet.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
import 'package:cake_wallet/utils/debounce.dart';
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/primary_button.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/view_model/payment/payment_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/exchange/exchange_view_model.dart';
|
||||
import 'package:cake_wallet/exchange/exchange_trade_state.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
|
||||
class SwapConfirmationBottomSheet extends BaseBottomSheet {
|
||||
SwapConfirmationBottomSheet({
|
||||
Key? key,
|
||||
required this.paymentFlowResult,
|
||||
required this.currentTheme,
|
||||
required this.exchangeViewModel,
|
||||
required this.authService,
|
||||
this.sessionId,
|
||||
}) : super(
|
||||
titleText: S.current.swap,
|
||||
footerType: FooterType.none,
|
||||
maxHeight: 900,
|
||||
currentTheme: currentTheme,
|
||||
);
|
||||
|
||||
final PaymentFlowResult paymentFlowResult;
|
||||
final MaterialThemeBase currentTheme;
|
||||
final ExchangeViewModel exchangeViewModel;
|
||||
final AuthService authService;
|
||||
final String? sessionId;
|
||||
@override
|
||||
Widget contentWidget(BuildContext context) {
|
||||
return SingleChildScrollView(
|
||||
padding: EdgeInsets.only(
|
||||
bottom: MediaQuery.of(context).viewInsets.bottom,
|
||||
),
|
||||
child: SwapConfirmationContent(
|
||||
paymentFlowResult: paymentFlowResult,
|
||||
exchangeViewModel: exchangeViewModel,
|
||||
authService: authService,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SwapConfirmationContent extends StatefulWidget {
|
||||
const SwapConfirmationContent({
|
||||
Key? key,
|
||||
required this.paymentFlowResult,
|
||||
required this.exchangeViewModel,
|
||||
required this.authService,
|
||||
}) : super(key: key);
|
||||
|
||||
final PaymentFlowResult paymentFlowResult;
|
||||
final ExchangeViewModel exchangeViewModel;
|
||||
final AuthService authService;
|
||||
|
||||
@override
|
||||
SwapConfirmationContentState createState() => SwapConfirmationContentState();
|
||||
}
|
||||
|
||||
class SwapConfirmationContentState extends State<SwapConfirmationContent> {
|
||||
late TextEditingController _amountController;
|
||||
late TextEditingController _amountFiatController;
|
||||
late TextEditingController _addressController;
|
||||
late TextEditingController _noteController;
|
||||
|
||||
final _receiveAmountDebounce = Debounce(Duration(milliseconds: 500));
|
||||
final _receiveAmountFiatDebounce = Debounce(Duration(milliseconds: 500));
|
||||
final FocusNode _amountFocus = FocusNode();
|
||||
final FocusNode _amountFiatFocus = FocusNode();
|
||||
final FocusNode _addressFocus = FocusNode();
|
||||
final FocusNode _noteFocus = FocusNode();
|
||||
final _formKey = GlobalKey<FormState>();
|
||||
|
||||
ReactionDisposer? _receiveAmountReaction;
|
||||
ReactionDisposer? _receiveAddressReaction;
|
||||
ReactionDisposer? _tradeStateReaction;
|
||||
ReactionDisposer? _bestRateReaction;
|
||||
ReactionDisposer? _receiveAmountFiatReaction;
|
||||
|
||||
bool _showingFailureDialog = false;
|
||||
bool _showingSwapDetailsDialog = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_addressController =
|
||||
TextEditingController(text: widget.paymentFlowResult.addressDetectionResult?.address ?? '');
|
||||
_amountController = TextEditingController(
|
||||
text: widget.paymentFlowResult.addressDetectionResult?.amount?.isNotEmpty ?? false
|
||||
? widget.paymentFlowResult.addressDetectionResult?.amount
|
||||
: '0.00');
|
||||
_amountFiatController =
|
||||
TextEditingController(text: widget.exchangeViewModel.receiveAmountFiatFormatted);
|
||||
_noteController =
|
||||
TextEditingController(text: widget.paymentFlowResult.addressDetectionResult?.note ?? '');
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) => _setUpReactions(
|
||||
context,
|
||||
widget.exchangeViewModel,
|
||||
widget.paymentFlowResult,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_amountController.dispose();
|
||||
_amountFiatController.dispose();
|
||||
_addressController.dispose();
|
||||
_noteController.dispose();
|
||||
_amountFocus.dispose();
|
||||
_amountFiatFocus.dispose();
|
||||
_addressFocus.dispose();
|
||||
_noteFocus.dispose();
|
||||
_receiveAmountReaction?.call();
|
||||
_receiveAddressReaction?.call();
|
||||
_tradeStateReaction?.call();
|
||||
_bestRateReaction?.call();
|
||||
_receiveAmountFiatReaction?.call();
|
||||
_showingFailureDialog = false;
|
||||
_showingSwapDetailsDialog = false;
|
||||
widget.exchangeViewModel.bestRateSync.cancel();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final detectedCurrencyName = walletTypeToCryptoCurrency(widget.paymentFlowResult.walletType!);
|
||||
|
||||
return Form(
|
||||
key: _formKey,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: widget.exchangeViewModel.depositCurrency.iconPath!,
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Icon(Icons.arrow_forward, size: 24),
|
||||
const SizedBox(width: 12),
|
||||
CakeImageWidget(
|
||||
imageUrl:
|
||||
walletTypeToCryptoCurrency(widget.paymentFlowResult.walletType!).iconPath!,
|
||||
width: 32,
|
||||
height: 32,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
SwapConfirmationTextfield(
|
||||
key: ValueKey('swap_confirmation_bottomsheet_amount_textfield_key'),
|
||||
hintText: 'Amount (${detectedCurrencyName})',
|
||||
focusNode: _amountFocus,
|
||||
controller: _amountController,
|
||||
validator: (value) {
|
||||
return AmountValidator(
|
||||
isAutovalidate: true,
|
||||
currency: widget.exchangeViewModel.receiveCurrency,
|
||||
minValue: widget.exchangeViewModel.limits.min.toString(),
|
||||
maxValue: widget.exchangeViewModel.limits.max.toString(),
|
||||
).call(value);
|
||||
},
|
||||
),
|
||||
Observer(
|
||||
builder: (_) {
|
||||
String? min = '0.0';
|
||||
String? max = '0.0';
|
||||
|
||||
final limitsState = widget.exchangeViewModel.limitsState;
|
||||
if (limitsState is LimitsLoadedSuccessfully) {
|
||||
min = limitsState.limits.min?.toString();
|
||||
max = limitsState.limits.max?.toString();
|
||||
}
|
||||
|
||||
if (limitsState is LimitsLoadedFailure) {
|
||||
min = '0.0';
|
||||
max = '0.0';
|
||||
}
|
||||
|
||||
if (limitsState is LimitsIsLoading) {
|
||||
min = '...';
|
||||
max = '...';
|
||||
}
|
||||
if (min != null || max != null) {
|
||||
return Container(
|
||||
height: 15,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
min != null
|
||||
? Text(
|
||||
key: ValueKey('min_limit_text_key'),
|
||||
S.of(context).min_value(min, detectedCurrencyName.toString()),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontSize: 10,
|
||||
height: 1.2,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
: Offstage(),
|
||||
min != null ? SizedBox(width: 10) : Offstage(),
|
||||
max != null
|
||||
? Text(
|
||||
key: ValueKey('max_limit_text_key'),
|
||||
S.of(context).max_value(max, detectedCurrencyName.toString()),
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontSize: 10,
|
||||
height: 1.2,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
)
|
||||
: Offstage(),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return SizedBox.shrink();
|
||||
},
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwapConfirmationTextfield(
|
||||
key: ValueKey('swap_confirmation_bottomsheet_amount_fiat_textfield_key'),
|
||||
hintText: 'Amount (${widget.exchangeViewModel.fiat.title})',
|
||||
focusNode: _amountFiatFocus,
|
||||
controller: _amountFiatController,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwapConfirmationTextfield(
|
||||
key: ValueKey('swap_confirmation_bottomsheet_address_textfield_key'),
|
||||
isAddress: true,
|
||||
walletType: cryptoCurrencyToWalletType(widget.exchangeViewModel.receiveCurrency),
|
||||
hintText: 'Destination Address',
|
||||
focusNode: _addressFocus,
|
||||
controller: _addressController,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
SwapConfirmationTextfield(
|
||||
maxLines: 1,
|
||||
key: ValueKey('swap_confirmation_bottomsheet_note_textfield_key'),
|
||||
hintText: 'Transaction Note',
|
||||
focusNode: _noteFocus,
|
||||
controller: _noteController,
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Center(
|
||||
child: Text(
|
||||
'Tap field to edit values',
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
SizedBox(height: 32),
|
||||
SwapConfirmationFooter(
|
||||
exchangeViewModel: widget.exchangeViewModel,
|
||||
formKey: _formKey,
|
||||
authService: widget.authService,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _setUpReactions(
|
||||
BuildContext context,
|
||||
ExchangeViewModel exchangeViewModel,
|
||||
PaymentFlowResult paymentFlowResult,
|
||||
) async {
|
||||
_receiveAmountReaction = reaction((_) => exchangeViewModel.receiveAmount, (String amount) {
|
||||
if (_amountController.text != amount) {
|
||||
_amountController.text = amount;
|
||||
}
|
||||
});
|
||||
|
||||
_receiveAmountFiatReaction =
|
||||
reaction((_) => exchangeViewModel.receiveAmountFiatFormatted, (String amount) {
|
||||
if (_amountFiatController.text != amount) {
|
||||
_amountFiatController.text = amount;
|
||||
}
|
||||
});
|
||||
|
||||
_receiveAddressReaction = reaction((_) => exchangeViewModel.receiveAddress, (String address) {
|
||||
if (_addressController.text != address) {
|
||||
_addressController.text = address;
|
||||
}
|
||||
});
|
||||
|
||||
_tradeStateReaction = reaction((_) => exchangeViewModel.tradeState, (ExchangeTradeState state) {
|
||||
if (state is TradeIsCreatedFailure && !_showingFailureDialog) {
|
||||
_showingFailureDialog = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertWithOneAction(
|
||||
key: const ValueKey('swap_confirmation_trade_creation_failure_dialog_key'),
|
||||
buttonKey:
|
||||
const ValueKey('swap_confirmation_trade_creation_failure_dialog_button_key'),
|
||||
alertTitle: S.of(context).provider_error(state.title),
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(context).ok,
|
||||
buttonAction: () {
|
||||
_showingFailureDialog = false;
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state is TradeIsCreatedSuccessfully) {
|
||||
exchangeViewModel.reset();
|
||||
if (Navigator.of(context).canPop() && !_showingSwapDetailsDialog) {
|
||||
_showingSwapDetailsDialog = true;
|
||||
Navigator.of(context).pop();
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: true,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext context) {
|
||||
_showingSwapDetailsDialog = false;
|
||||
return getIt.get<SwapDetailsBottomSheet>();
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
_bestRateReaction = reaction((_) => exchangeViewModel.bestRate, (double rate) {
|
||||
if (exchangeViewModel.isFixedRateMode) {
|
||||
exchangeViewModel.changeReceiveAmount(amount: _amountController.text);
|
||||
}
|
||||
});
|
||||
|
||||
_addressController
|
||||
.addListener(() => exchangeViewModel.receiveAddress = _addressController.text);
|
||||
|
||||
_amountController.addListener(() {
|
||||
if (_amountController.text != exchangeViewModel.receiveAmount) {
|
||||
_receiveAmountDebounce.run(() {
|
||||
exchangeViewModel.loadLimits();
|
||||
exchangeViewModel.changeReceiveAmount(amount: _amountController.text);
|
||||
exchangeViewModel.isReceiveAmountEntered = true;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_amountFiatController.addListener(() {
|
||||
if (_amountFiatController.text != exchangeViewModel.receiveAmountFiatFormatted) {
|
||||
_receiveAmountFiatDebounce.run(() {
|
||||
exchangeViewModel.loadLimits();
|
||||
exchangeViewModel.setReceiveAmountFromFiat(fiatAmount: _amountFiatController.text);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
_amountFocus.addListener(() {
|
||||
if (_amountFocus.hasFocus) {
|
||||
exchangeViewModel.enableFixedRateMode();
|
||||
}
|
||||
});
|
||||
|
||||
exchangeViewModel.receiveCurrency = walletTypeToCryptoCurrency(paymentFlowResult.walletType!);
|
||||
await exchangeViewModel.fetchFiatPrice(exchangeViewModel.receiveCurrency);
|
||||
|
||||
exchangeViewModel.receiveAddress = _addressController.text;
|
||||
exchangeViewModel.depositAddress = exchangeViewModel.wallet.walletAddresses.addressForExchange;
|
||||
exchangeViewModel.receiveAmount = _amountController.text;
|
||||
_amountFiatController.text = exchangeViewModel.receiveAmountFiatFormatted;
|
||||
exchangeViewModel.isReceiveAmountEntered = true;
|
||||
exchangeViewModel.isFixedRateMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
class SwapConfirmationTextfield extends StatelessWidget {
|
||||
const SwapConfirmationTextfield({
|
||||
super.key,
|
||||
required this.focusNode,
|
||||
required this.controller,
|
||||
required this.hintText,
|
||||
this.walletType,
|
||||
this.isAddress = false,
|
||||
this.maxLines = 1,
|
||||
this.validator,
|
||||
});
|
||||
|
||||
final FocusNode focusNode;
|
||||
final TextEditingController controller;
|
||||
final String hintText;
|
||||
final WalletType? walletType;
|
||||
final bool isAddress;
|
||||
final int maxLines;
|
||||
final String? Function(String?)? validator;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
|
||||
decoration: ShapeDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
side: focusNode.hasFocus
|
||||
? BorderSide(color: Theme.of(context).colorScheme.primary)
|
||||
: BorderSide.none,
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
hintText,
|
||||
style: Theme.of(context).textTheme.bodySmall!.copyWith(
|
||||
fontSize: 10,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
if (isAddress) SizedBox(height: 8),
|
||||
isAddress
|
||||
? AddressFormatter.buildSegmentedAddress(
|
||||
address: controller.text,
|
||||
walletType: walletType,
|
||||
evenTextStyle: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w400,
|
||||
fontSize: 12,
|
||||
),
|
||||
)
|
||||
: BaseTextFormField(
|
||||
isDense: true,
|
||||
hintText: hintText,
|
||||
focusNode: focusNode,
|
||||
hasUnderlineBorder: true,
|
||||
borderWidth: 0.0,
|
||||
controller: controller,
|
||||
maxLines: maxLines,
|
||||
validator: validator,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class SwapConfirmationFooter extends StatelessWidget {
|
||||
const SwapConfirmationFooter({
|
||||
super.key,
|
||||
required this.exchangeViewModel,
|
||||
required this.formKey,
|
||||
required this.authService,
|
||||
});
|
||||
|
||||
final ExchangeViewModel exchangeViewModel;
|
||||
final AuthService authService;
|
||||
final GlobalKey<FormState> formKey;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
height: 150,
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final isLoading = exchangeViewModel.tradeState is TradeIsCreating ||
|
||||
exchangeViewModel.limitsState is LimitsIsLoading;
|
||||
final isDisabled = exchangeViewModel.selectedProviders.isEmpty ||
|
||||
exchangeViewModel.receiveAmount.isEmpty ||
|
||||
exchangeViewModel.receiveAddress.isEmpty;
|
||||
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
PrimaryButton(
|
||||
text: S.current.cancel,
|
||||
onPressed: isLoading
|
||||
? null
|
||||
: () {
|
||||
if (Navigator.of(context).canPop()) {
|
||||
Navigator.of(context).pop(null);
|
||||
}
|
||||
},
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
textColor: Theme.of(context).colorScheme.onSecondaryContainer,
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
LoadingPrimaryButton(
|
||||
text: S.current.continue_text,
|
||||
onPressed: exchangeViewModel.isAvailableInSelected
|
||||
? () {
|
||||
FocusScope.of(context).unfocus();
|
||||
|
||||
if (formKey.currentState != null && formKey.currentState!.validate()) {
|
||||
final check = exchangeViewModel.shouldDisplayTOTP();
|
||||
authService.authenticateAction(
|
||||
context,
|
||||
conditionToDetermineIfToUse2FA: check,
|
||||
onAuthSuccess: (value) {
|
||||
if (value) {
|
||||
exchangeViewModel.createTrade();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
: () => PresentProviderPicker(exchangeViewModel: exchangeViewModel)
|
||||
.presentProviderPicker(context),
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
textColor: Theme.of(context).colorScheme.onPrimary,
|
||||
isDisabled: isDisabled,
|
||||
isLoading: isLoading,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,441 @@
|
||||
import 'package:cake_wallet/exchange/trade.dart';
|
||||
import 'package:cake_wallet/src/widgets/cake_image_widget.dart';
|
||||
import 'package:cake_wallet/utils/address_formatter.dart';
|
||||
import 'package:cake_wallet/utils/show_bar.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
|
||||
import 'package:cw_core/wallet_type.dart';
|
||||
import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
|
||||
import 'package:cake_wallet/src/widgets/standard_slide_button_widget.dart';
|
||||
import 'package:cake_wallet/themes/core/material_base_theme.dart';
|
||||
import 'package:cake_wallet/core/execution_state.dart';
|
||||
import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
|
||||
import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
|
||||
import 'package:cake_wallet/utils/show_pop_up.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:mobx/mobx.dart';
|
||||
import 'package:flutter_mobx/flutter_mobx.dart';
|
||||
import 'package:cake_wallet/reactions/wallet_connect.dart';
|
||||
|
||||
class SwapDetailsBottomSheet extends StatefulWidget {
|
||||
SwapDetailsBottomSheet({
|
||||
Key? key,
|
||||
required this.currentTheme,
|
||||
required this.exchangeTradeViewModel,
|
||||
}) : super(key: key);
|
||||
|
||||
final MaterialThemeBase currentTheme;
|
||||
final ExchangeTradeViewModel exchangeTradeViewModel;
|
||||
|
||||
@override
|
||||
State<SwapDetailsBottomSheet> createState() => _SwapDetailsBottomSheetState();
|
||||
}
|
||||
|
||||
class _SwapDetailsBottomSheetState extends State<SwapDetailsBottomSheet> {
|
||||
bool _effectsInstalled = false;
|
||||
ReactionDisposer? _exchangeStateReaction;
|
||||
BuildContext? _loadingBottomSheetContext;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) => _setEffects());
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
widget.exchangeTradeViewModel.timer?.cancel();
|
||||
_exchangeStateReaction?.reaction.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _setEffects() {
|
||||
if (_effectsInstalled) return;
|
||||
|
||||
_exchangeStateReaction = reaction(
|
||||
(_) => widget.exchangeTradeViewModel.sendViewModel.state,
|
||||
(ExecutionState state) async {
|
||||
if (state is! IsExecutingState &&
|
||||
state is! TransactionCommitting &&
|
||||
_loadingBottomSheetContext != null &&
|
||||
_loadingBottomSheetContext!.mounted) {
|
||||
Navigator.of(_loadingBottomSheetContext!).pop();
|
||||
}
|
||||
|
||||
if (state is FailureState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext popupContext) {
|
||||
return AlertWithOneAction(
|
||||
key: ValueKey('swap_details_send_failure_dialog_key'),
|
||||
buttonKey: ValueKey('swap_details_send_failure_dialog_button_key'),
|
||||
alertTitle: S.of(popupContext).error,
|
||||
alertContent: state.error,
|
||||
buttonText: S.of(popupContext).ok,
|
||||
buttonAction: () => Navigator.of(popupContext).pop(),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (state is IsExecutingState) {
|
||||
// Wait a bit to avoid showing the loading dialog if transaction is failed
|
||||
await Future.delayed(const Duration(milliseconds: 300));
|
||||
final currentState = widget.exchangeTradeViewModel.sendViewModel.state;
|
||||
if (currentState is ExecutedSuccessfullyState || currentState is FailureState) {
|
||||
return;
|
||||
}
|
||||
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (context.mounted) {
|
||||
showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isDismissible: false,
|
||||
builder: (BuildContext context) {
|
||||
_loadingBottomSheetContext = context;
|
||||
return LoadingBottomSheet(
|
||||
titleText: S.of(context).generating_transaction,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state is ExecutedSuccessfullyState) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) async {
|
||||
if (context.mounted) {
|
||||
await widget.exchangeTradeViewModel.sendViewModel.commitTransaction(context);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (state is TransactionCommitted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback(
|
||||
(_) async {
|
||||
if (!mounted) return;
|
||||
|
||||
await showModalBottomSheet<void>(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
builder: (BuildContext bottomSheetContext) {
|
||||
return InfoBottomSheet(
|
||||
currentTheme: widget.currentTheme,
|
||||
footerType: FooterType.singleActionButton,
|
||||
titleText: S.of(bottomSheetContext).transaction_sent,
|
||||
contentImage: 'assets/images/birthday_cake.png',
|
||||
singleActionButtonText: S.of(bottomSheetContext).close,
|
||||
singleActionButtonKey: ValueKey('swap_details_sent_dialog_ok_button_key'),
|
||||
onSingleActionButtonPressed: () {
|
||||
Navigator.of(bottomSheetContext).pop();
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
;
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
_effectsInstalled = true;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _SwapDetailsBottomSheetContent(
|
||||
titleText: 'Confirm Swap',
|
||||
footerType: FooterType.none,
|
||||
maxHeight: 900,
|
||||
currentTheme: widget.currentTheme,
|
||||
exchangeTradeViewModel: widget.exchangeTradeViewModel,
|
||||
onExecuteSwap: _executeSwap,
|
||||
);
|
||||
}
|
||||
|
||||
void _executeSwap() async {
|
||||
if (!widget.exchangeTradeViewModel.isSendable) return;
|
||||
|
||||
try {
|
||||
await widget.exchangeTradeViewModel.confirmSending();
|
||||
} catch (e) {
|
||||
printV('Error executing swap: $e');
|
||||
if (mounted) {
|
||||
showPopUp<void>(
|
||||
context: context,
|
||||
builder: (BuildContext popupContext) {
|
||||
return AlertWithOneAction(
|
||||
key: ValueKey('swap_details_execution_error_dialog_key'),
|
||||
buttonKey: ValueKey('swap_details_execution_error_dialog_button_key'),
|
||||
alertTitle: S.of(popupContext).error,
|
||||
alertContent: e.toString(),
|
||||
buttonText: S.of(popupContext).ok,
|
||||
buttonAction: () => Navigator.of(popupContext).pop(),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class _SwapDetailsBottomSheetContent extends BaseBottomSheet {
|
||||
_SwapDetailsBottomSheetContent({
|
||||
required String titleText,
|
||||
required FooterType footerType,
|
||||
required double maxHeight,
|
||||
MaterialThemeBase? currentTheme,
|
||||
required this.exchangeTradeViewModel,
|
||||
required this.onExecuteSwap,
|
||||
}) : super(
|
||||
titleText: titleText,
|
||||
footerType: footerType,
|
||||
maxHeight: maxHeight,
|
||||
currentTheme: currentTheme,
|
||||
);
|
||||
|
||||
final ExchangeTradeViewModel exchangeTradeViewModel;
|
||||
final VoidCallback onExecuteSwap;
|
||||
|
||||
@override
|
||||
Widget contentWidget(BuildContext context) {
|
||||
return Column(
|
||||
children: [
|
||||
_SwapDetailsContent(
|
||||
trade: exchangeTradeViewModel.trade,
|
||||
exchangeTradeViewModel: exchangeTradeViewModel,
|
||||
),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 12),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
final sendingState = exchangeTradeViewModel.sendViewModel.state;
|
||||
final isDisabled = !exchangeTradeViewModel.isSendable ||
|
||||
exchangeTradeViewModel.trade.inputAddress == null ||
|
||||
exchangeTradeViewModel.trade.inputAddress!.isEmpty;
|
||||
|
||||
if (isDisabled || sendingState is IsExecutingState) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
),
|
||||
child: Center(
|
||||
child: sendingState is IsExecutingState
|
||||
? CircularProgressIndicator()
|
||||
: Text(
|
||||
'Swipe to swap',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return StandardSlideButton(
|
||||
tileBackgroundColor: Theme.of(context).colorScheme.surfaceContainer,
|
||||
knobColor: Theme.of(context).colorScheme.primary,
|
||||
buttonText: 'Swipe to swap',
|
||||
onSlideComplete: onExecuteSwap,
|
||||
currentTheme: currentTheme!,
|
||||
accessibleNavigationModeButtonText: 'Complete swap',
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SwapDetailsContent extends StatelessWidget {
|
||||
const _SwapDetailsContent({required this.trade, required this.exchangeTradeViewModel});
|
||||
|
||||
final Trade trade;
|
||||
final ExchangeTradeViewModel exchangeTradeViewModel;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 24),
|
||||
child: Observer(
|
||||
builder: (_) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
_SwapDetailsTile(
|
||||
label: 'You Send',
|
||||
value: '${trade.amount} ${trade.from?.title ?? ''}',
|
||||
valueFiatFormatted: exchangeTradeViewModel.sendAmountFiatFormatted,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
_SwapDetailsTile(
|
||||
label: 'You Get',
|
||||
value: '${trade.receiveAmount ?? '0'} ${trade.to?.title ?? ''}',
|
||||
valueFiatFormatted: exchangeTradeViewModel.receiveAmountFiatFormatted,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'To this Address',
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(fontWeight: FontWeight.w500, fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
AddressFormatter.buildSegmentedAddress(
|
||||
address: trade.payoutAddress ?? '',
|
||||
walletType: trade.to != null ? cryptoCurrencyToWalletType(trade.to!) : null,
|
||||
evenTextStyle: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(fontWeight: FontWeight.w600, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
CakeImageWidget(
|
||||
imageUrl: trade.provider.image,
|
||||
width: 36,
|
||||
height: 36,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
trade.provider.title,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w500,
|
||||
fontSize: 16,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Spacer(),
|
||||
Expanded(
|
||||
// flex: 4,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
Clipboard.setData(ClipboardData(text: trade.id));
|
||||
showBar<void>(context, S.of(context).copied_to_clipboard);
|
||||
},
|
||||
child: Row(
|
||||
children: [
|
||||
Text(
|
||||
'ID: ',
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(fontSize: 18),
|
||||
),
|
||||
Expanded(
|
||||
child: Text(
|
||||
trade.id,
|
||||
style:
|
||||
Theme.of(context).textTheme.bodyMedium!.copyWith(fontSize: 18),
|
||||
softWrap: true,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SwapDetailsTile extends StatelessWidget {
|
||||
const _SwapDetailsTile({
|
||||
required this.label,
|
||||
required this.value,
|
||||
required this.valueFiatFormatted,
|
||||
});
|
||||
|
||||
final String label;
|
||||
final String value;
|
||||
final String valueFiatFormatted;
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surfaceContainer,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: Theme.of(context)
|
||||
.textTheme
|
||||
.bodyMedium!
|
||||
.copyWith(fontWeight: FontWeight.w500, fontSize: 16),
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
value,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontWeight: FontWeight.w600,
|
||||
fontSize: 18,
|
||||
),
|
||||
),
|
||||
if (valueFiatFormatted.isNotEmpty)
|
||||
Text(
|
||||
valueFiatFormatted,
|
||||
style: Theme.of(context).textTheme.bodyMedium!.copyWith(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,8 @@ class StandardSlideButton extends StatefulWidget {
|
||||
this.height = 48.0,
|
||||
required this.currentTheme,
|
||||
required this.accessibleNavigationModeButtonText,
|
||||
this.tileBackgroundColor,
|
||||
this.knobColor,
|
||||
}) : super(key: key);
|
||||
|
||||
final VoidCallback onSlideComplete;
|
||||
@@ -18,7 +20,9 @@ class StandardSlideButton extends StatefulWidget {
|
||||
final double height;
|
||||
final MaterialThemeBase currentTheme;
|
||||
final String accessibleNavigationModeButtonText;
|
||||
|
||||
final Color? tileBackgroundColor;
|
||||
final Color? knobColor;
|
||||
|
||||
@override
|
||||
StandardSlideButtonState createState() => StandardSlideButtonState();
|
||||
}
|
||||
@@ -57,7 +61,7 @@ class StandardSlideButtonState extends State<StandardSlideButton> {
|
||||
height: widget.height,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: tileBackgroundColor,
|
||||
color: widget.tileBackgroundColor ?? tileBackgroundColor,
|
||||
),
|
||||
child: Stack(
|
||||
alignment: Alignment.centerLeft,
|
||||
@@ -97,7 +101,7 @@ class StandardSlideButtonState extends State<StandardSlideButton> {
|
||||
height: widget.height - 8,
|
||||
decoration: BoxDecoration(
|
||||
borderRadius: BorderRadius.circular(10),
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
color: widget.knobColor ?? Theme.of(context).colorScheme.surface,
|
||||
),
|
||||
alignment: Alignment.center,
|
||||
child: Icon(
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import 'dart:async';
|
||||
|
||||
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
|
||||
import 'package:cake_wallet/exchange/provider/chainflip_exchange_provider.dart';
|
||||
import 'package:cake_wallet/exchange/provider/changenow_exchange_provider.dart';
|
||||
@@ -15,6 +17,7 @@ import 'package:cake_wallet/exchange/provider/xoswap_exchange_provider.dart';
|
||||
import 'package:cake_wallet/exchange/trade.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/src/screens/exchange_trade/exchange_trade_item.dart';
|
||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/trades_store.dart';
|
||||
import 'package:cake_wallet/view_model/send/fees_view_model.dart';
|
||||
import 'package:cake_wallet/view_model/send/output.dart';
|
||||
@@ -36,6 +39,7 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
required this.tradesStore,
|
||||
required this.sendViewModel,
|
||||
required this.feesViewModel,
|
||||
required this.fiatConversionStore,
|
||||
}) : trade = tradesStore.trade!,
|
||||
isSendable = _checkIfCanSend(tradesStore, wallet),
|
||||
items = ObservableList<ExchangeTradeItem>() {
|
||||
@@ -69,7 +73,7 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
case ExchangeProviderDescription.chainflip:
|
||||
_provider = ChainflipExchangeProvider(tradesStore: trades);
|
||||
break;
|
||||
case ExchangeProviderDescription.xoSwap:
|
||||
case ExchangeProviderDescription.xoSwap:
|
||||
_provider = XOSwapExchangeProvider();
|
||||
}
|
||||
|
||||
@@ -116,6 +120,49 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
|
||||
Timer? timer;
|
||||
|
||||
final FiatConversionStore fiatConversionStore;
|
||||
|
||||
FiatCurrency get fiat => sendViewModel.fiat;
|
||||
|
||||
@computed
|
||||
bool get isFiatDisabled => feesViewModel.isFiatDisabled;
|
||||
|
||||
@computed
|
||||
String get receiveAmountFiatFormatted {
|
||||
var amount = '0.00';
|
||||
try {
|
||||
if (trade.receiveAmount?.isNotEmpty ?? false) {
|
||||
if (fiatConversionStore.prices[trade.to] == null) return '';
|
||||
|
||||
amount = calculateFiatAmount(
|
||||
price: fiatConversionStore.prices[trade.to]!,
|
||||
cryptoAmount: trade.receiveAmount,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
printV('Error calculating receive amount fiat formatted: $_');
|
||||
}
|
||||
return isFiatDisabled ? '' : '$amount ${fiat.title}';
|
||||
}
|
||||
|
||||
@computed
|
||||
String get sendAmountFiatFormatted {
|
||||
var amount = '0.00';
|
||||
try {
|
||||
if (trade.amount.isNotEmpty) {
|
||||
if (fiatConversionStore.prices[trade.from] == null) return '';
|
||||
|
||||
amount = calculateFiatAmount(
|
||||
price: fiatConversionStore.prices[trade.from]!,
|
||||
cryptoAmount: trade.amount,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
printV('Error calculating send amount fiat formatted: $_');
|
||||
}
|
||||
return isFiatDisabled ? '' : '$amount ${fiat.title}';
|
||||
}
|
||||
|
||||
void setUpOutput() {
|
||||
sendViewModel.clearOutputs();
|
||||
output = sendViewModel.outputs.first;
|
||||
@@ -168,13 +215,9 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
|
||||
void _updateItems() {
|
||||
final trade = tradesStore.trade!;
|
||||
final tradeFrom = trade.fromRaw >= 0
|
||||
? trade.from
|
||||
: trade.userCurrencyFrom;
|
||||
final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom;
|
||||
|
||||
final tradeTo = trade.toRaw >= 0
|
||||
? trade.to
|
||||
: trade.userCurrencyTo;
|
||||
final tradeTo = trade.toRaw >= 0 ? trade.to : trade.userCurrencyTo;
|
||||
|
||||
final tagFrom = tradeFrom?.tag != null ? '${tradeFrom!.tag}' + ' ' : '';
|
||||
final tagTo = tradeTo?.tag != null ? '${tradeTo!.tag}' + ' ' : '';
|
||||
@@ -221,22 +264,20 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
final isExtraIdExist = trade.extraId != null && trade.extraId!.isNotEmpty;
|
||||
|
||||
if (isExtraIdExist) {
|
||||
|
||||
|
||||
final title = tradeFrom == CryptoCurrency.xrp
|
||||
? S.current.destination_tag
|
||||
: tradeFrom == CryptoCurrency.xlm || tradeFrom == CryptoCurrency.ton
|
||||
? S.current.memo
|
||||
: S.current.extra_id;
|
||||
? S.current.destination_tag
|
||||
: tradeFrom == CryptoCurrency.xlm || tradeFrom == CryptoCurrency.ton
|
||||
? S.current.memo
|
||||
: S.current.extra_id;
|
||||
|
||||
items.add(
|
||||
ExchangeTradeItem(
|
||||
title: title,
|
||||
data: trade.extraId ?? '',
|
||||
isCopied: true,
|
||||
isReceiveDetail: !isExtraIdExist,
|
||||
isExternalSendDetail: isExtraIdExist),
|
||||
);
|
||||
items.add(
|
||||
ExchangeTradeItem(
|
||||
title: title,
|
||||
data: trade.extraId ?? '',
|
||||
isCopied: true,
|
||||
isReceiveDetail: !isExtraIdExist,
|
||||
isExternalSendDetail: isExtraIdExist),
|
||||
);
|
||||
}
|
||||
|
||||
items.add(
|
||||
@@ -251,27 +292,21 @@ abstract class ExchangeTradeViewModelBase with Store {
|
||||
}
|
||||
|
||||
static bool _checkIfCanSend(TradesStore tradesStore, WalletBase wallet) {
|
||||
|
||||
final trade = tradesStore.trade!;
|
||||
final tradeFrom = trade.fromRaw >= 0
|
||||
? trade.from
|
||||
: trade.userCurrencyFrom;
|
||||
final tradeFrom = trade.fromRaw >= 0 ? trade.from : trade.userCurrencyFrom;
|
||||
|
||||
bool _isEthToken() =>
|
||||
wallet.currency == CryptoCurrency.eth &&
|
||||
tradeFrom?.tag == CryptoCurrency.eth.title;
|
||||
wallet.currency == CryptoCurrency.eth && tradeFrom?.tag == CryptoCurrency.eth.title;
|
||||
|
||||
bool _isPolygonToken() =>
|
||||
wallet.currency == CryptoCurrency.maticpoly &&
|
||||
tradeFrom?.tag == CryptoCurrency.maticpoly.tag;
|
||||
tradeFrom?.tag == CryptoCurrency.maticpoly.tag;
|
||||
|
||||
bool _isTronToken() =>
|
||||
wallet.currency == CryptoCurrency.trx &&
|
||||
tradeFrom?.tag == CryptoCurrency.trx.title;
|
||||
wallet.currency == CryptoCurrency.trx && tradeFrom?.tag == CryptoCurrency.trx.title;
|
||||
|
||||
bool _isSplToken() =>
|
||||
wallet.currency == CryptoCurrency.sol &&
|
||||
tradeFrom?.tag == CryptoCurrency.sol.title;
|
||||
wallet.currency == CryptoCurrency.sol && tradeFrom?.tag == CryptoCurrency.sol.title;
|
||||
|
||||
return tradeFrom == wallet.currency ||
|
||||
tradesStore.trade!.provider == ExchangeProviderDescription.xmrto ||
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
import 'dart:async';
|
||||
import 'dart:collection';
|
||||
import 'dart:convert';
|
||||
import 'dart:developer';
|
||||
|
||||
import 'package:bitcoin_base/bitcoin_base.dart';
|
||||
import 'package:cake_wallet/.secrets.g.dart' as secrets;
|
||||
import 'package:cake_wallet/bitcoin/bitcoin.dart';
|
||||
import 'package:cake_wallet/core/create_trade_result.dart';
|
||||
import 'package:cake_wallet/core/fiat_conversion_service.dart';
|
||||
import 'package:cake_wallet/core/wallet_change_listener_view_model.dart';
|
||||
import 'package:cake_wallet/entities/calculate_fiat_amount.dart';
|
||||
import 'package:cake_wallet/entities/exchange_api_mode.dart';
|
||||
import 'package:cake_wallet/entities/fiat_api_mode.dart';
|
||||
import 'package:cake_wallet/entities/fiat_currency.dart';
|
||||
import 'package:cake_wallet/entities/preferences_key.dart';
|
||||
import 'package:cake_wallet/entities/wallet_contact.dart';
|
||||
import 'package:cake_wallet/exchange/exchange_provider_description.dart';
|
||||
@@ -30,6 +35,7 @@ import 'package:cake_wallet/exchange/trade.dart';
|
||||
import 'package:cake_wallet/exchange/trade_request.dart';
|
||||
import 'package:cake_wallet/generated/i18n.dart';
|
||||
import 'package:cake_wallet/store/app_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/fiat_conversion_store.dart';
|
||||
import 'package:cake_wallet/store/dashboard/trades_store.dart';
|
||||
import 'package:cake_wallet/store/settings_store.dart';
|
||||
import 'package:cake_wallet/store/templates/exchange_template_store.dart';
|
||||
@@ -76,6 +82,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
this.unspentCoinsListViewModel,
|
||||
this.feesViewModel,
|
||||
this.walletInfoSource,
|
||||
this.fiatConversionStore,
|
||||
) : _cryptoNumberFormat = NumberFormat(),
|
||||
isSendAllEnabled = false,
|
||||
isFixedRateMode = false,
|
||||
@@ -129,8 +136,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
autorun((_) {
|
||||
if (selectedProviders.any((provider) => provider is TrocadorExchangeProvider)) {
|
||||
final trocadorProvider =
|
||||
selectedProviders.firstWhere((provider) => provider is TrocadorExchangeProvider)
|
||||
as TrocadorExchangeProvider;
|
||||
selectedProviders.firstWhere((provider) => provider is TrocadorExchangeProvider)
|
||||
as TrocadorExchangeProvider;
|
||||
|
||||
updateAllTrocadorProviderStates(trocadorProvider);
|
||||
}
|
||||
@@ -360,6 +367,44 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
|
||||
late Timer bestRateSync;
|
||||
|
||||
final FiatConversionStore fiatConversionStore;
|
||||
|
||||
FiatCurrency get fiat => _settingsStore.fiatCurrency;
|
||||
|
||||
@computed
|
||||
bool get isFiatDisabled => feesViewModel.isFiatDisabled;
|
||||
|
||||
@action
|
||||
Future<void> fetchFiatPrice(CryptoCurrency currency) async {
|
||||
if (fiatConversionStore.prices[currency] != null) {
|
||||
return;
|
||||
}
|
||||
|
||||
fiatConversionStore.prices[currency] = await FiatConversionService.fetchPrice(
|
||||
crypto: currency,
|
||||
fiat: fiat,
|
||||
torOnly: _settingsStore.fiatApiMode == FiatApiMode.torOnly,
|
||||
);
|
||||
}
|
||||
|
||||
@computed
|
||||
String get receiveAmountFiatFormatted {
|
||||
var amount = '0.00';
|
||||
try {
|
||||
if (receiveAmount.isNotEmpty) {
|
||||
if (fiatConversionStore.prices[receiveCurrency] == null) return '';
|
||||
|
||||
amount = calculateFiatAmount(
|
||||
price: fiatConversionStore.prices[receiveCurrency]!,
|
||||
cryptoAmount: receiveAmount,
|
||||
);
|
||||
}
|
||||
} catch (_) {
|
||||
log('Error calculating receive amount fiat formatted: $_');
|
||||
}
|
||||
return isFiatDisabled ? '' : '$amount';
|
||||
}
|
||||
|
||||
@action
|
||||
void changeDepositCurrency({required CryptoCurrency currency}) {
|
||||
depositCurrency = currency;
|
||||
@@ -393,6 +438,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
|
||||
await calculateBestRate();
|
||||
}
|
||||
|
||||
_cryptoNumberFormat.maximumFractionDigits = depositMaxDigits;
|
||||
|
||||
depositAmount = _cryptoNumberFormat
|
||||
@@ -401,6 +447,16 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
.replaceAll(RegExp('\\,'), '');
|
||||
}
|
||||
|
||||
@action
|
||||
void setReceiveAmountFromFiat({required String fiatAmount}) {
|
||||
final crypto = double.parse(fiatAmount.replaceAll(',', '.')) /
|
||||
fiatConversionStore.prices[receiveCurrency]!;
|
||||
final receiveAmountTmp = _cryptoNumberFormat.format(crypto);
|
||||
if (receiveAmount != receiveAmountTmp) {
|
||||
changeReceiveAmount(amount: receiveAmountTmp);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> changeDepositAmount({required String amount}) async {
|
||||
depositAmount = amount;
|
||||
@@ -444,8 +500,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
bestRate = 0.0;
|
||||
return;
|
||||
}
|
||||
final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount)
|
||||
?? initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
|
||||
final amount = double.tryParse(isFixedRateMode ? receiveAmount : depositAmount) ??
|
||||
initialAmountByAssets(isFixedRateMode ? receiveCurrency : depositCurrency);
|
||||
|
||||
final validProvidersForAmount = _tradeAvailableProviders.where((provider) {
|
||||
final limits = _providerLimits[provider];
|
||||
@@ -515,15 +571,15 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
.map((provider) async {
|
||||
final limits = await provider
|
||||
.fetchLimits(
|
||||
from: from,
|
||||
to: to,
|
||||
isFixedRateMode: isFixedRateMode,
|
||||
)
|
||||
from: from,
|
||||
to: to,
|
||||
isFixedRateMode: isFixedRateMode,
|
||||
)
|
||||
.onError((error, stackTrace) => Limits(max: 0.0, min: double.maxFinite))
|
||||
.timeout(
|
||||
Duration(seconds: 7),
|
||||
onTimeout: () => Limits(max: 0.0, min: double.maxFinite),
|
||||
);
|
||||
Duration(seconds: 7),
|
||||
onTimeout: () => Limits(max: 0.0, min: double.maxFinite),
|
||||
);
|
||||
return MapEntry(provider, limits);
|
||||
}).toList();
|
||||
|
||||
@@ -563,8 +619,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
Future<void> createTrade() async {
|
||||
if (depositCurrency == receiveCurrency) {
|
||||
tradeState = TradeIsCreatedFailure(
|
||||
title: S.current.trade_not_created,
|
||||
error: 'Can\'t exchange the same currency');
|
||||
title: S.current.trade_not_created, error: 'Can\'t exchange the same currency');
|
||||
return;
|
||||
}
|
||||
if (isSendAllEnabled) {
|
||||
@@ -881,8 +936,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
void _setAvailableProviders() {
|
||||
_tradeAvailableProviders.clear();
|
||||
|
||||
_tradeAvailableProviders.addAll(
|
||||
selectedProviders.where((provider) => providerList.contains(provider)));
|
||||
_tradeAvailableProviders
|
||||
.addAll(selectedProviders.where((provider) => providerList.contains(provider)));
|
||||
}
|
||||
|
||||
void _setProviders() {
|
||||
@@ -984,7 +1039,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
"X-API-Key": secrets.moralisApiKey,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
final decodedResponse = jsonDecode(response.body)[0] as Map<String, dynamic>;
|
||||
|
||||
@@ -1018,7 +1072,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
|
||||
Future<List<Erc20Token>> _loadAllUniqueEvmTokens() async {
|
||||
final evmWallets = walletInfoSource.values.where(
|
||||
(w) => w.type == WalletType.ethereum || w.type == WalletType.polygon,
|
||||
(w) => w.type == WalletType.ethereum || w.type == WalletType.polygon,
|
||||
);
|
||||
|
||||
final seen = <String>{};
|
||||
@@ -1039,7 +1093,6 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
return unique;
|
||||
}
|
||||
|
||||
|
||||
@action
|
||||
Future<void> _injectUserEthTokensIntoCurrencyLists() async {
|
||||
final userTokens = await _loadAllUniqueEvmTokens();
|
||||
@@ -1134,8 +1187,7 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
}
|
||||
|
||||
Future<List<TronToken>> _loadAllUniqueTronTokens() async {
|
||||
final tronWallets =
|
||||
walletInfoSource.values.where((w) => w.type == WalletType.tron);
|
||||
final tronWallets = walletInfoSource.values.where((w) => w.type == WalletType.tron);
|
||||
|
||||
final seen = <String>{};
|
||||
final unique = <TronToken>[];
|
||||
@@ -1186,11 +1238,8 @@ abstract class ExchangeViewModelBase extends WalletChangeListenerViewModel with
|
||||
CryptoCurrency.ada => 1000,
|
||||
CryptoCurrency.dash => 10,
|
||||
CryptoCurrency.rune => 10,
|
||||
|
||||
_ => 1
|
||||
};
|
||||
return amount.toDouble();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@ abstract class PaymentViewModelBase with Store {
|
||||
return PaymentFlowResult.incompatible('Unable to detect address type');
|
||||
}
|
||||
|
||||
detectedWalletType = detectionResult.detectedWalletType!;
|
||||
detectedWalletType = detectionResult.detectedWalletType;
|
||||
|
||||
// Check if current wallet is compatible
|
||||
final currentWallet = appStore.wallet;
|
||||
@@ -56,11 +56,11 @@ abstract class PaymentViewModelBase with Store {
|
||||
|
||||
switch (compatibleWallets.length) {
|
||||
case 0:
|
||||
return PaymentFlowResult.noWallets(detectedWalletType!);
|
||||
return PaymentFlowResult.noWallets(detectedWalletType!, detectionResult);
|
||||
case 1:
|
||||
return PaymentFlowResult.singleWallet(compatibleWallets.first);
|
||||
return PaymentFlowResult.singleWallet(compatibleWallets.first, detectionResult);
|
||||
default:
|
||||
return PaymentFlowResult.multipleWallets(compatibleWallets);
|
||||
return PaymentFlowResult.multipleWallets(compatibleWallets, detectionResult);
|
||||
}
|
||||
} catch (e) {
|
||||
printV('PaymentViewModel error: $e');
|
||||
@@ -81,6 +81,7 @@ class PaymentFlowResult {
|
||||
final WalletInfo? wallet;
|
||||
final List<WalletInfo> wallets;
|
||||
final WalletType? walletType;
|
||||
final AddressDetectionResult? addressDetectionResult;
|
||||
|
||||
PaymentFlowResult._({
|
||||
required this.type,
|
||||
@@ -88,22 +89,38 @@ class PaymentFlowResult {
|
||||
this.wallet,
|
||||
this.wallets = const [],
|
||||
this.walletType,
|
||||
this.addressDetectionResult,
|
||||
});
|
||||
|
||||
/// Current wallet is compatible
|
||||
factory PaymentFlowResult.currentWalletCompatible() => PaymentFlowResult._(type: PaymentFlowType.currentWalletCompatible);
|
||||
factory PaymentFlowResult.currentWalletCompatible() =>
|
||||
PaymentFlowResult._(type: PaymentFlowType.currentWalletCompatible);
|
||||
|
||||
/// Single compatible wallet available
|
||||
factory PaymentFlowResult.singleWallet(WalletInfo wallet) =>
|
||||
PaymentFlowResult._(type: PaymentFlowType.singleWallet, wallet: wallet);
|
||||
factory PaymentFlowResult.singleWallet(
|
||||
WalletInfo wallet, AddressDetectionResult addressDetectionResult) =>
|
||||
PaymentFlowResult._(
|
||||
type: PaymentFlowType.singleWallet,
|
||||
wallet: wallet,
|
||||
walletType: wallet.type,
|
||||
addressDetectionResult: addressDetectionResult);
|
||||
|
||||
/// Multiple compatible wallets available
|
||||
factory PaymentFlowResult.multipleWallets(List<WalletInfo> wallets) =>
|
||||
PaymentFlowResult._(type: PaymentFlowType.multipleWallets, wallets: wallets);
|
||||
factory PaymentFlowResult.multipleWallets(
|
||||
List<WalletInfo> wallets, AddressDetectionResult addressDetectionResult) =>
|
||||
PaymentFlowResult._(
|
||||
type: PaymentFlowType.multipleWallets,
|
||||
wallets: wallets,
|
||||
walletType: wallets.first.type,
|
||||
addressDetectionResult: addressDetectionResult);
|
||||
|
||||
/// No compatible wallets available
|
||||
factory PaymentFlowResult.noWallets(WalletType walletType) =>
|
||||
PaymentFlowResult._(type: PaymentFlowType.noWallets, walletType: walletType);
|
||||
factory PaymentFlowResult.noWallets(
|
||||
WalletType walletType, AddressDetectionResult addressDetectionResult) =>
|
||||
PaymentFlowResult._(
|
||||
type: PaymentFlowType.noWallets,
|
||||
walletType: walletType,
|
||||
addressDetectionResult: addressDetectionResult);
|
||||
|
||||
/// Error occurred
|
||||
factory PaymentFlowResult.error(String message) =>
|
||||
|
||||
@@ -11,7 +11,10 @@ import 'package:cake_wallet/src/screens/send/widgets/extract_address_from_parsed
|
||||
import 'package:cake_wallet/tron/tron.dart';
|
||||
import 'package:cake_wallet/wownero/wownero.dart';
|
||||
import 'package:cake_wallet/zano/zano.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/crypto_currency.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/utils/print_verbose.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
@@ -220,7 +223,8 @@ abstract class OutputBase with Store {
|
||||
|
||||
WalletType get walletType => _wallet.type;
|
||||
final CryptoCurrency Function() cryptoCurrencyHandler;
|
||||
final WalletBase _wallet;
|
||||
@observable
|
||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> _wallet;
|
||||
final SettingsStore _settingsStore;
|
||||
final FiatConversionStore _fiatConversationStore;
|
||||
final NumberFormat _cryptoNumberFormat;
|
||||
@@ -230,6 +234,12 @@ abstract class OutputBase with Store {
|
||||
sendAll = true;
|
||||
}
|
||||
|
||||
@action
|
||||
void updateWallet(WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> newWallet) {
|
||||
_wallet = newWallet;
|
||||
_setCryptoNumMaximumFractionDigits();
|
||||
}
|
||||
|
||||
@action
|
||||
void reset() {
|
||||
sendAll = false;
|
||||
|
||||
@@ -72,6 +72,16 @@ abstract class SendViewModelBase extends WalletChangeListenerViewModel with Stor
|
||||
wallet.type == WalletType.solana ||
|
||||
wallet.type == WalletType.tron ||
|
||||
wallet.type == WalletType.zano;
|
||||
|
||||
for (final output in outputs) {
|
||||
output.updateWallet(wallet);
|
||||
}
|
||||
|
||||
// Update unspent coins list view model with the new wallet reference
|
||||
unspentCoinsListViewModel.updateWallet(wallet);
|
||||
|
||||
// Update sending balance to reflect the new wallet's balance
|
||||
updateSendingBalance();
|
||||
}
|
||||
|
||||
UnspentCoinsListViewModel unspentCoinsListViewModel;
|
||||
|
||||
@@ -4,6 +4,9 @@ import 'package:cake_wallet/utils/exception_handler.dart';
|
||||
import 'package:cake_wallet/decred/decred.dart';
|
||||
import 'package:cake_wallet/view_model/unspent_coins/unspent_coins_item.dart';
|
||||
import 'package:cake_wallet/wownero/wownero.dart';
|
||||
import 'package:cw_core/balance.dart';
|
||||
import 'package:cw_core/transaction_history.dart';
|
||||
import 'package:cw_core/transaction_info.dart';
|
||||
import 'package:cw_core/unspent_coin_type.dart';
|
||||
import 'package:cw_core/unspent_coins_info.dart';
|
||||
import 'package:cw_core/unspent_transaction_output.dart';
|
||||
@@ -28,7 +31,8 @@ abstract class UnspentCoinsListViewModelBase with Store {
|
||||
items = ObservableList<UnspentCoinsItem>(),
|
||||
_originalState = {};
|
||||
|
||||
final WalletBase wallet;
|
||||
@observable
|
||||
WalletBase<Balance, TransactionHistoryBase<TransactionInfo>, TransactionInfo> wallet;
|
||||
final Box<UnspentCoinsInfo> _unspentCoinsInfo;
|
||||
final UnspentCoinType coinTypeToSpendFrom;
|
||||
|
||||
@@ -228,6 +232,11 @@ abstract class UnspentCoinsListViewModelBase with Store {
|
||||
@action
|
||||
void setIsDisposing(bool value) => isDisposing = value;
|
||||
|
||||
@action
|
||||
void updateWallet(WalletBase newWallet) {
|
||||
wallet = newWallet;
|
||||
}
|
||||
|
||||
@action
|
||||
Future<void> dispose() async {
|
||||
await _updateUnspents();
|
||||
|
||||
Reference in New Issue
Block a user