dev
dart 423 lines 16.2 KB
Raw
1 import 'package:cake_wallet/exchange/exchange_provider_description.dart';
2 import 'package:cake_wallet/reactions/wallet_connect.dart';
3 import 'package:cake_wallet/routes.dart';
4 import 'package:cake_wallet/src/screens/connect_device/connect_device_page.dart';
5 import 'package:cake_wallet/src/screens/exchange/widgets/desktop_exchange_cards_section.dart';
6 import 'package:cake_wallet/src/screens/exchange/widgets/mobile_exchange_cards_section.dart';
7 import 'package:cake_wallet/src/screens/exchange_trade/widgets/exchange_trade_card_item_widget.dart';
8 import 'package:cake_wallet/src/widgets/bottom_sheet/base_bottom_sheet_widget.dart';
9 import 'package:cake_wallet/src/widgets/bottom_sheet/confirm_sending_bottom_sheet_widget.dart';
10 import 'package:cake_wallet/src/widgets/bottom_sheet/info_bottom_sheet_widget.dart';
11 import 'package:cake_wallet/utils/request_review_handler.dart';
12 import 'package:cake_wallet/utils/responsive_layout_util.dart';
13 import 'package:mobx/mobx.dart';
14 import 'package:flutter_mobx/flutter_mobx.dart';
15 import 'package:flutter/material.dart';
16 import 'package:cake_wallet/generated/i18n.dart';
17 import 'package:cake_wallet/core/execution_state.dart';
18 import 'package:cake_wallet/src/screens/exchange_trade/information_page.dart';
19 import 'package:cake_wallet/utils/show_pop_up.dart';
20 import 'package:cake_wallet/view_model/exchange/exchange_trade_view_model.dart';
21 import 'package:cake_wallet/view_model/send/send_view_model_state.dart';
22 import 'package:cake_wallet/src/screens/base_page.dart';
23 import 'package:cake_wallet/src/screens/exchange_trade/widgets/timer_widget.dart';
24 import 'package:cake_wallet/src/widgets/primary_button.dart';
25 import 'package:cake_wallet/src/widgets/scrollable_with_bottom_section.dart';
26 import 'package:cake_wallet/src/widgets/alert_with_one_action.dart';
27
28 void showInformation(ExchangeTradeViewModel exchangeTradeViewModel, BuildContext context) {
29 final trade = exchangeTradeViewModel.trade;
30 final walletName = exchangeTradeViewModel.wallet.name;
31
32 final from = trade.from?.toString() ?? '';
33
34 final information = exchangeTradeViewModel.isSendable
35 ? S.current.exchange_trade_result_confirm(trade.amount, from, walletName) +
36 exchangeTradeViewModel.extraInfo
37 : S.current.exchange_result_description(trade.amount, from) +
38 exchangeTradeViewModel.extraInfo;
39
40 showPopUp<void>(
41 context: context,
42 builder: (_) => InformationPage(
43 key: ValueKey('information_page_dialog_key'),
44 information: information,
45 ),
46 );
47 }
48
49 class ExchangeTradePage extends BasePage {
50 ExchangeTradePage({required this.exchangeTradeViewModel});
51
52 final ExchangeTradeViewModel exchangeTradeViewModel;
53
54 @override
55 String get title => S.current.swap;
56
57 @override
58 bool get gradientAll => true;
59
60 @override
61 bool get resizeToAvoidBottomInset => false;
62
63 @override
64 bool get extendBodyBehindAppBar => true;
65
66 @override
67 AppBarStyle get appBarStyle => AppBarStyle.transparent;
68
69 @override
70 Widget trailing(BuildContext context) {
71 final questionImage = Image.asset('assets/images/question_mark.png',
72 color: Theme.of(context).colorScheme.onSurface);
73
74 return SizedBox(
75 height: 20.0,
76 width: 20.0,
77 child: ButtonTheme(
78 minWidth: double.minPositive,
79 child: TextButton(
80 // FIX-ME: Style
81 //highlightColor: Colors.transparent,
82 //splashColor: Colors.transparent,
83 //padding: EdgeInsets.all(0),
84 onPressed: () => showInformation(exchangeTradeViewModel, context),
85 child: questionImage),
86 ),
87 );
88 }
89
90 @override
91 Widget body(BuildContext context) => ExchangeTradeForm(exchangeTradeViewModel);
92 }
93
94 class ExchangeTradeForm extends StatefulWidget {
95 ExchangeTradeForm(this.exchangeTradeViewModel);
96
97 final ExchangeTradeViewModel exchangeTradeViewModel;
98
99 @override
100 ExchangeTradeState createState() => ExchangeTradeState();
101 }
102
103 class ExchangeTradeState extends State<ExchangeTradeForm> {
104 final fetchingLabel = S.current.fetching;
105
106 String get title => S.current.exchange;
107
108 bool _effectsInstalled = false;
109
110 ReactionDisposer? _exchangeStateReaction;
111
112 @override
113 void initState() {
114 super.initState();
115 WidgetsBinding.instance.addPostFrameCallback(afterLayout);
116 }
117
118 void afterLayout(dynamic _) {
119 showInformation(widget.exchangeTradeViewModel, context);
120 }
121
122 @override
123 void dispose() {
124 widget.exchangeTradeViewModel.timer?.cancel();
125 _exchangeStateReaction?.reaction.dispose();
126 super.dispose();
127 }
128
129 @override
130 Widget build(BuildContext context) {
131 _setEffects();
132
133 return Container(
134 child: ScrollableWithBottomSection(
135 contentPadding: EdgeInsets.only(bottom: 16),
136 content: Observer(builder: (_) {
137 final trade = widget.exchangeTradeViewModel.trade;
138
139 return Column(
140 children: <Widget>[
141 trade.expiredAt != null
142 ? Row(
143 mainAxisSize: MainAxisSize.max,
144 mainAxisAlignment: MainAxisAlignment.center,
145 children: <Widget>[
146 Text(
147 S.of(context).offer_expires_in,
148 style: Theme.of(context).textTheme.bodyMedium?.copyWith(
149 fontWeight: FontWeight.w500,
150 ),
151 ),
152 if (trade.expiredAt != null)
153 TimerWidget(trade.expiredAt!,
154 color: Theme.of(context).colorScheme.onSurface)
155 ])
156 : Offstage(),
157 _ExchangeTradeItemsCardSection(
158 viewModel: widget.exchangeTradeViewModel,
159 ),
160 ],
161 );
162 }),
163 bottomSectionPadding: EdgeInsets.fromLTRB(24, 0, 24, 24),
164 bottomSection: Column(
165 children: [
166 if (!widget.exchangeTradeViewModel.shouldHideExternalSendButton)
167 PrimaryButton(
168 key: ValueKey('exchange_trade_page_send_from_external_button_key'),
169 text: S.current.send_from_external_wallet,
170 onPressed: () async {
171 Navigator.of(context).pushNamed(Routes.exchangeTradeExternalSendPage);
172 },
173 color: widget.exchangeTradeViewModel.isSendable
174 ? Theme.of(context).colorScheme.surfaceContainer
175 : Theme.of(context).colorScheme.primary,
176 textColor: widget.exchangeTradeViewModel.isSendable
177 ? Theme.of(context).colorScheme.onSecondaryContainer
178 : Theme.of(context).colorScheme.onPrimary,
179 ),
180 SizedBox(height: 16),
181 Observer(
182 builder: (_) {
183 final trade = widget.exchangeTradeViewModel.trade;
184 final sendingState = widget.exchangeTradeViewModel.sendViewModel.state;
185
186 return Offstage(
187 offstage: !(widget.exchangeTradeViewModel.isSendable &&
188 !(sendingState is TransactionCommitted)),
189 child: LoadingPrimaryButton(
190 key: ValueKey('exchange_trade_page_send_from_cake_button_key'),
191 isDisabled: trade.inputAddress == null ||
192 trade.inputAddress!.isEmpty ||
193 sendingState is ExecutedSuccessfullyState,
194 isLoading: sendingState is IsExecutingState,
195 onPressed: _onPressedSendFromCakeWallet,
196 text: S.current.send_from_cake_wallet,
197 color: Theme.of(context).colorScheme.primary,
198 textColor: Theme.of(context).colorScheme.onPrimary,
199 ),
200 );
201 },
202 ),
203 ],
204 ),
205 ),
206 );
207 }
208
209 Future<void> _onPressedSendFromCakeWallet() async {
210 final sendVM = widget.exchangeTradeViewModel.sendViewModel;
211
212 if (sendVM.wallet.isHardwareWallet) {
213 if (!sendVM.hardwareWalletViewModel!.isConnected(sendVM.walletType)) {
214 await Navigator.of(context).pushNamed(Routes.connectDevices,
215 arguments: ConnectDevicePageParams(
216 walletType: sendVM.walletType,
217 hardwareWalletType: sendVM.wallet.walletInfo.hardwareWalletType!,
218 onConnectDevice: (context, _) {
219 sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet);
220 Navigator.of(context).pop();
221 },
222 ));
223 } else {
224 sendVM.hardwareWalletViewModel!.initWallet(sendVM.wallet);
225 }
226 }
227
228 widget.exchangeTradeViewModel.confirmSending();
229 }
230
231 BuildContext? dialogContext;
232 BuildContext? loadingBottomSheetContext;
233
234 void _setEffects() {
235 if (_effectsInstalled) {
236 return;
237 }
238
239 _exchangeStateReaction = reaction(
240 (_) => this.widget.exchangeTradeViewModel.sendViewModel.state,
241 (ExecutionState state) async {
242 if (dialogContext != null && dialogContext?.mounted == true) {
243 Navigator.of(dialogContext!).pop();
244 }
245
246 if (state is! IsExecutingState &&
247 loadingBottomSheetContext != null &&
248 loadingBottomSheetContext!.mounted) {
249 Navigator.of(loadingBottomSheetContext!).pop();
250 }
251
252 if (state is FailureState) {
253 WidgetsBinding.instance.addPostFrameCallback((_) {
254 showPopUp<void>(
255 context: context,
256 builder: (BuildContext popupContext) {
257 return AlertWithOneAction(
258 key: ValueKey('exchange_trade_page_send_failure_dialog_key'),
259 buttonKey: ValueKey('exchange_trade_page_send_failure_dialog_button_key'),
260 alertTitle: S.of(popupContext).error,
261 alertContent: state.error,
262 buttonText: S.of(popupContext).ok,
263 buttonAction: () => Navigator.of(popupContext).pop());
264 });
265 });
266 }
267
268 if (state is IsExecutingState) {
269 // wait a bit to avoid showing the loading dialog if transaction is failed
270 await Future.delayed(const Duration(milliseconds: 300));
271 final currentState = widget.exchangeTradeViewModel.sendViewModel.state;
272 if (currentState is ExecutedSuccessfullyState || currentState is FailureState) {
273 return;
274 }
275
276 WidgetsBinding.instance.addPostFrameCallback((_) {
277 if (context.mounted) {
278 showModalBottomSheet<void>(
279 context: context,
280 isDismissible: false,
281 builder: (BuildContext context) {
282 loadingBottomSheetContext = context;
283 return LoadingBottomSheet(
284 titleText: S.of(context).generating_transaction,
285 );
286 },
287 );
288 }
289 });
290 }
291
292 if (state is ExecutedSuccessfullyState) {
293 WidgetsBinding.instance.addPostFrameCallback((_) async {
294 final trade = widget.exchangeTradeViewModel.trade;
295
296 final isSwapsXyz = trade.provider == ExchangeProviderDescription.swapsXyz;
297 final isEVMWallet = widget.exchangeTradeViewModel.sendViewModel.isEVMWallet;
298
299 final amountValue = isSwapsXyz &&
300 isEVMWallet &&
301 !widget.exchangeTradeViewModel.isSwapsXYZCanSendFromExternal
302 ? trade.amount
303 : widget.exchangeTradeViewModel.sendViewModel.pendingTransaction!.amountFormatted;
304
305 final fiatAmountValue = isSwapsXyz &&
306 isEVMWallet &&
307 !widget.exchangeTradeViewModel.isSwapsXYZCanSendFromExternal
308 ? widget.exchangeTradeViewModel.sendViewModel
309 .calculateTransactionFiatAmount(amountValue)
310 : widget.exchangeTradeViewModel.sendViewModel.pendingTransactionFiatAmountFormatted;
311
312 if (context.mounted) {
313 final result = await showModalBottomSheet<bool>(
314 context: context,
315 isDismissible: false,
316 isScrollControlled: true,
317 builder: (bottomSheetContext) {
318 final sendVM = widget.exchangeTradeViewModel.sendViewModel;
319
320 return ConfirmSendingBottomSheet(
321 key: ValueKey('exchange_trade_page_confirm_sending_bottom_sheet_key'),
322 footerType: FooterType.slideActionButton,
323 isSlideActionEnabled: sendVM.isReadyForSend,
324 walletType: sendVM.walletType,
325 titleText: S.of(bottomSheetContext).confirm_transaction,
326 titleIconPath: sendVM.selectedCryptoCurrency.iconPath,
327 currency: widget.exchangeTradeViewModel.sendViewModel.selectedCryptoCurrency,
328 amount: S.of(bottomSheetContext).send_amount,
329 amountValue: sendVM.amountParsingProxy
330 .getDisplayCryptoAmount(amountValue, sendVM.selectedCryptoCurrency),
331 explanation: sendVM.pendingTransactionAdditionalCostNotice,
332 fiatAmountValue: fiatAmountValue,
333 fee: isEVMCompatibleChain(sendVM.walletType)
334 ? S.of(bottomSheetContext).send_estimated_fee
335 : S.of(bottomSheetContext).send_fee,
336 feeValue: sendVM.amountParsingProxy
337 .asDisplayStringWithSymbol(sendVM.pendingTransaction!.fee),
338 feeFiatAmount: sendVM.pendingTransactionFeeFiatAmountFormatted,
339 outputs: sendVM.outputs,
340 onSlideActionComplete: () async {
341 if (bottomSheetContext.mounted && Navigator.canPop(bottomSheetContext))
342 Navigator.of(bottomSheetContext).pop(true);
343
344 sendVM.commitTransaction(context);
345 },
346 amountParsingProxy: sendVM.amountParsingProxy,
347 );
348 },
349 );
350
351 if (result == null) widget.exchangeTradeViewModel.sendViewModel.dismissTransaction();
352 }
353 });
354 }
355
356 if (state is TransactionCommitted) {
357 WidgetsBinding.instance.addPostFrameCallback(
358 (_) async {
359 if (!mounted) return;
360
361 await showModalBottomSheet<void>(
362 context: context,
363 isScrollControlled: true,
364 builder: (BuildContext bottomSheetContext) {
365 return InfoBottomSheet(
366 footerType: FooterType.singleActionButton,
367 titleText: S.of(bottomSheetContext).transaction_sent,
368 contentImage: 'assets/images/birthday_cake.png',
369 singleActionButtonText: S.of(bottomSheetContext).close,
370 singleActionButtonKey: ValueKey('send_page_sent_dialog_ok_button_key'),
371 onSingleActionButtonPressed: () {
372 Navigator.of(bottomSheetContext).pop();
373 if (mounted) {
374 Navigator.of(context).pushNamedAndRemoveUntil(
375 Routes.dashboard,
376 (route) => false,
377 );
378 }
379 RequestReviewHandler.requestReview();
380 },
381 );
382 },
383 );
384 },
385 );
386 }
387 },
388 );
389
390 _effectsInstalled = true;
391 }
392 }
393
394 class _ExchangeTradeItemsCardSection extends StatelessWidget {
395 const _ExchangeTradeItemsCardSection({required this.viewModel});
396
397 final ExchangeTradeViewModel viewModel;
398
399 @override
400 Widget build(BuildContext context) {
401 final firstExchangeCard = ExchangeTradeCardItemWidget(
402 isReceiveDetailsCard: true,
403 exchangeTradeViewModel: viewModel,
404 );
405
406 final secondExchangeCard = ExchangeTradeCardItemWidget(
407 isReceiveDetailsCard: false,
408 exchangeTradeViewModel: viewModel,
409 );
410
411 if (responsiveLayoutUtil.shouldRenderMobileUI) {
412 return MobileExchangeCardsSection(
413 firstExchangeCard: firstExchangeCard,
414 secondExchangeCard: secondExchangeCard,
415 );
416 }
417
418 return DesktopExchangeCardsSection(
419 firstExchangeCard: firstExchangeCard,
420 secondExchangeCard: secondExchangeCard,
421 );
422 }
423 }