diff --git a/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart index ec22421b1..772cfc77a 100644 --- a/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart +++ b/lib/new-ui/pages/buy_sell/buy_sell_amount_page.dart @@ -1,435 +1,435 @@ -import 'dart:async'; + import 'dart:async'; -import 'package:cake_wallet/buy/payment_method.dart'; -import 'package:cake_wallet/buy/sell_buy_states.dart'; -import 'package:cake_wallet/entities/fiat_currency.dart'; -import 'package:cake_wallet/generated/i18n.dart'; -import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_provider_page.dart'; -import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart'; -import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart'; -import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart'; -import 'package:cake_wallet/new-ui/widgets/floating_amount_input.dart'; -import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; -import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; -import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; -import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; -import 'package:cake_wallet/themes/core/theme_extension.dart'; -import 'package:cake_wallet/utils/show_pop_up.dart'; -import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; -import 'package:cw_core/amount/money.dart'; -import 'package:cw_core/crypto_currency.dart'; -import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_mobx/flutter_mobx.dart'; -import 'package:mobx/mobx.dart'; + import 'package:cake_wallet/buy/payment_method.dart'; + import 'package:cake_wallet/buy/sell_buy_states.dart'; + import 'package:cake_wallet/entities/fiat_currency.dart'; + import 'package:cake_wallet/generated/i18n.dart'; + import 'package:cake_wallet/new-ui/pages/buy_sell/buy_sell_provider_page.dart'; + import 'package:cake_wallet/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart'; + import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_args.dart'; + import 'package:cake_wallet/new-ui/widgets/currency_picker/currency_picker_sheet.dart'; + import 'package:cake_wallet/new-ui/widgets/floating_amount_input.dart'; + import 'package:cake_wallet/new-ui/widgets/new_primary_button.dart'; + import 'package:cake_wallet/new-ui/widgets/receive_page/receive_top_bar.dart'; + import 'package:cake_wallet/src/widgets/alert_with_one_action.dart'; + import 'package:cake_wallet/src/widgets/cake_image_widget.dart'; + import 'package:cake_wallet/themes/core/theme_extension.dart'; + import 'package:cake_wallet/utils/show_pop_up.dart'; + import 'package:cake_wallet/view_model/buy/buy_sell_view_model.dart'; + import 'package:cw_core/amount/money.dart'; + import 'package:cw_core/crypto_currency.dart'; + import 'package:flutter/cupertino.dart'; + import 'package:flutter/material.dart'; + import 'package:flutter_mobx/flutter_mobx.dart'; + import 'package:mobx/mobx.dart'; -class NewBuySellAmountPage extends StatefulWidget { - const NewBuySellAmountPage({super.key, required this.buySellViewModel}); + class NewBuySellAmountPage extends StatefulWidget { + const NewBuySellAmountPage({super.key, required this.buySellViewModel}); - final BuySellViewModel buySellViewModel; + final BuySellViewModel buySellViewModel; - @override - State createState() => _NewBuySellAmountPageState(); -} - -class _NewBuySellAmountPageState extends State { - bool _customAmountMode = false; - bool _isLoadingPaymentMethods = false; - final customInputController = TextEditingController(); - final customInputFocusNode = FocusNode(); - - @override - void initState() { - super.initState(); - - // this is a hack for the "up to" display to show the actual upper limit. - // limits are loaded alongside quotes, and quotes for some providers only load properly with an amount and payment method selected. - // it'll be refactored soon anyway, i guess. - widget.buySellViewModel.changeFiatAmount(amount: "1000"); - widget.buySellViewModel.calculateBestRate(); - when( - (_) => widget.buySellViewModel.paymentMethodState is PaymentMethodLoaded, - () => widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods - .firstWhere((item) => item.paymentMethodType == PaymentType.all)); + @override + State createState() => _NewBuySellAmountPageState(); } - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.vertical(top: Radius.circular(18)), - gradient: LinearGradient( - colors: [ - Theme.of(context).colorScheme.surface, - Theme.of(context).colorScheme.surfaceDim, - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: SafeArea( - child: Column( - children: [ - Observer( - builder: (_) => ModalTopBar( - title: _pageTitle, - bottomText: !_customAmountMode || widget.buySellViewModel.maxFiatAmount == null - ? null - : "${S.of(context).up_to} ~${widget.buySellViewModel.maxFiatAmount} ${widget.buySellViewModel.fiatCurrency.title}", - leadingIcon: Icon(Icons.close), - onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, - ), - ), - Expanded( - child: Observer( - builder: (_) => AnimatedSwitcher( - duration: Duration(milliseconds: 300), - child: _customAmountMode - ? BuySellCustomAmountInput( - fiatCurrency: widget.buySellViewModel.fiatCurrency, - cryptoCurrency: widget.buySellViewModel.cryptoCurrency, - cryptoAmount: widget.buySellViewModel.cryptoAmount, - isLoading: _isLoadingPaymentMethods, - hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies, - onCurrencySelectorPressed: () => selectCryptoCurrency(context), - controller: customInputController, - focusNode: customInputFocusNode, - onContinuePressed: () { - navigateToProviders(context); - }, - onChanged: (amount) => - widget.buySellViewModel.changeFiatAmount(amount: amount), - ) - : BuySellDefaultAmountSelector( - key: ValueKey(0), - defaultAmounts: widget.buySellViewModel.defaultAmounts, - fiatCurrency: widget.buySellViewModel.fiatCurrency, - currentAmount: widget.buySellViewModel.fiatAmount, - hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies, - onCurrencySelectorPressed: () => selectCryptoCurrency(context), - cryptoCurrency: widget.buySellViewModel.cryptoCurrency, - isLoading: _isLoadingPaymentMethods, - mode: widget.buySellViewModel.mode, - onSelected: (amount) async { - if (amount == null) { - // this resets the rate and prevents showing 0 usd = 0.something btc - await widget.buySellViewModel.changeFiatAmount(amount: ""); - setState(() { - _customAmountMode = true; - }); - customInputFocusNode.requestFocus(); - } else { - await widget.buySellViewModel.changeFiatAmount(amount: amount); - navigateToProviders(context); - } - }, - ), - ), - )) - ], - ), - ), - ); - } + class _NewBuySellAmountPageState extends State { + bool _customAmountMode = false; + bool _isLoadingPaymentMethods = false; + final customInputController = TextEditingController(); + final customInputFocusNode = FocusNode(); - void selectCryptoCurrency(BuildContext context) => CurrencyPickerSheet.show( - context: context, - args: CurrencyPickerArgs( - items: widget.buySellViewModel.activeWalletCurrencies.toList(), - onSelected: (item) => widget.buySellViewModel.changeCryptoCurrency(currency: item), - symbolResolver: widget.buySellViewModel.amountParsingProxy.getCryptoSymbol, - )); + @override + void initState() { + super.initState(); - String get _pageTitle => widget.buySellViewModel.mode == BuySellPageMode.buy - ? S.current.buy - : S.current.sell + - ((widget.buySellViewModel.cryptoCurrencies.length == 1) - ? " ${widget.buySellViewModel.cryptoCurrencies.first.fullName}" - : ""); - - Future navigateToProviders(BuildContext context) async { - if (_isLoadingPaymentMethods) { - return; + // this is a hack for the "up to" display to show the actual upper limit. + // limits are loaded alongside quotes, and quotes for some providers only load properly with an amount and payment method selected. + // it'll be refactored soon anyway, i guess. + widget.buySellViewModel.changeFiatAmount(amount: "1000"); + widget.buySellViewModel.calculateBestRate(); + when( + (_) => widget.buySellViewModel.paymentMethodState is PaymentMethodLoaded, + () => widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods + .firstWhere((item) => item.paymentMethodType == PaymentType.all)); } - try { - setState(() { - _isLoadingPaymentMethods = true; - }); - - await asyncWhen((_) => [PaymentMethodLoaded, PaymentMethodFailed] - .contains(widget.buySellViewModel.paymentMethodState.runtimeType)); - - if (widget.buySellViewModel.paymentMethodState is PaymentMethodFailed) { - showPopUp( - context: context, - builder: (context) => AlertWithOneAction( - alertTitle: S.of(context).failed_to_load_payment_methods, - alertContent: S.of(context).please_try_again_later, - buttonText: "OK", - buttonAction: Navigator.of(context).pop)); - return; - } - - widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods - .firstWhere((item) => item.paymentMethodType == PaymentType.all); - - // unawaited because BuySellProviderPage has a nice little "loading rates..." ui thingie - unawaited(widget.buySellViewModel.calculateBestRate()); - - final page = BuySellProviderPage(buySellViewModel: widget.buySellViewModel); - Navigator.of(context).push(CupertinoPageRoute( - builder: (context) => Material(color: Colors.transparent, child: page))); - } finally { - setState(() { - _isLoadingPaymentMethods = false; - }); - } - } -} - -class BuySellCustomAmountInput extends StatelessWidget { - const BuySellCustomAmountInput( - {super.key, - required this.fiatCurrency, - required this.cryptoCurrency, - required this.cryptoAmount, - required this.controller, - required this.onContinuePressed, - required this.isLoading, - required this.onChanged, - required this.focusNode, - required this.hasCurrencySelector, - required this.onCurrencySelectorPressed}); - - final FiatCurrency fiatCurrency; - final CryptoCurrency cryptoCurrency; - final String cryptoAmount; - final TextEditingController controller; - final FocusNode focusNode; - final bool isLoading; - final bool hasCurrencySelector; - final VoidCallback onCurrencySelectorPressed; - final VoidCallback onContinuePressed; - final Function(String) onChanged; - - @override - Widget build(BuildContext context) { - return Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - SizedBox.shrink(), - Column( - spacing: 8, - children: [ - BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), - SizedBox.shrink(), - FloatingAmountInput( - currency: fiatCurrency, - focusNode: focusNode, - controller: controller, - onChanged: onChanged, - ), - Opacity( - opacity: cryptoAmount.isEmpty ? 0 : 1, - child: Text( - "≈ ${cryptoAmount} ${cryptoCurrency.symbol}", - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - ) - ], - ), - Padding( - padding: const EdgeInsets.all(18.0), - child: NewPrimaryButton( - onPressed: onContinuePressed, - isLoading: isLoading, - text: S.of(context).continue_text, - color: Theme.of(context).colorScheme.primary, - textColor: Theme.of(context).colorScheme.onPrimary), - ) - ], - ); - } -} - -class BuySellDefaultAmountSelector extends StatelessWidget { - const BuySellDefaultAmountSelector( - {super.key, - required this.defaultAmounts, - required this.fiatCurrency, - required this.mode, - required this.onSelected, - this.currentAmount, - required this.isLoading, - required this.hasCurrencySelector, - required this.onCurrencySelectorPressed, - required this.cryptoCurrency}); - - final List defaultAmounts; - final String? currentAmount; - final bool isLoading; - final bool hasCurrencySelector; - final VoidCallback onCurrencySelectorPressed; - final CryptoCurrency cryptoCurrency; - final FiatCurrency fiatCurrency; - final BuySellPageMode mode; - final Function(String?) onSelected; - - @override - Widget build(BuildContext context) { - return Column( - spacing: 24, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), - Text( - mode == BuySellPageMode.sell - ? S.of(context).choose_amount_to_sell - : S.of(context).choose_amount_to_buy, - style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), - ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 18.0), - child: GridView.builder( - shrinkWrap: true, - physics: NeverScrollableScrollPhysics(), - // +1 for "custom" option - itemCount: defaultAmounts.length + 1, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 16, mainAxisExtent: 105), - itemBuilder: (context, index) { - final String? item = index == defaultAmounts.length ? null : defaultAmounts[index]; - - return BuySellAmountPill( - isLoading: isLoading && item == currentAmount, - amount: item == null ? null : Money.parse(item, fiatCurrency), - onTap: () => onSelected(item), - ); - }), - ), - ], - ); - } -} - -class BuySellAmountPill extends StatelessWidget { - const BuySellAmountPill({super.key, this.amount, required this.onTap, required this.isLoading}); - - final Money? amount; - final VoidCallback onTap; - final bool isLoading; - - @override - Widget build(BuildContext context) { - return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(9999999999), - border: Border.all( - width: 1, - color: Theme.of(context).colorScheme.surfaceContainerHigh, - ), - gradient: LinearGradient( - colors: [ - context.customColors.cardGradientColorPrimary, - context.customColors.cardGradientColorSecondary - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: Material( - color: Colors.transparent, - borderRadius: BorderRadius.circular(9999999999), - child: InkWell( - borderRadius: BorderRadius.circular(9999999999), - onTap: onTap, - child: isLoading - ? CupertinoActivityIndicator() - : Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Row( - spacing: 4, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (amount != null) - Text( - amount!.toStringWithPrecision(fractionalDigits: 0), - style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), - ), - Text( - amount?.currency.symbol ?? S.of(context).custom, - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: amount == null - ? Theme.of(context).colorScheme.primary - : Theme.of(context).colorScheme.onSurfaceVariant), - ) - ], - ), - if (amount == null) - Text( - S.of(context).enter_amount, - style: TextStyle( - fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), - ) - ], - ), - ), - ), - ); - } -} - -class BuySellCurrencyPickerPill extends StatelessWidget { - const BuySellCurrencyPickerPill({super.key, required this.curr, required this.onTap}); - - final CryptoCurrency curr; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return GestureDetector( - onTap: onTap, - child: Container( + @override + Widget build(BuildContext context) { + return Container( decoration: BoxDecoration( - color: Theme.of(context).colorScheme.surfaceContainerHigh, - borderRadius: BorderRadius.circular(999999999), + borderRadius: BorderRadius.vertical(top: Radius.circular(18)), + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surface, + Theme.of(context).colorScheme.surfaceDim, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), ), - child: Padding( - padding: EdgeInsets.only(top: 5, bottom: 5, left: 5, right: 10), - child: Row( - mainAxisSize: MainAxisSize.min, - spacing: 10, + child: SafeArea( + child: Column( children: [ - CakeImageWidget( - imageUrl: curr.iconPath, - width: 30, - height: 30, - ), - Text( - curr.fullName ?? curr.title, - style: TextStyle(fontSize: 16), - ), - RotatedBox( - quarterTurns: 2, - child: CakeImageWidget( - imageUrl: "assets/new-ui/dropdown_arrow.svg", - width: 8, - height: 8, - colorFilter: - ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + Observer( + builder: (_) => ModalTopBar( + title: _pageTitle, + bottomText: !_customAmountMode || widget.buySellViewModel.maxFiatAmount == null + ? null + : "${S.of(context).up_to} ~${widget.buySellViewModel.maxFiatAmount} ${widget.buySellViewModel.fiatCurrency.title}", + leadingIcon: Icon(Icons.close), + onLeadingPressed: Navigator.of(context, rootNavigator: true).pop, ), ), + Expanded( + child: Observer( + builder: (_) => AnimatedSwitcher( + duration: Duration(milliseconds: 300), + child: _customAmountMode + ? BuySellCustomAmountInput( + fiatCurrency: widget.buySellViewModel.fiatCurrency, + cryptoCurrency: widget.buySellViewModel.cryptoCurrency, + cryptoAmount: widget.buySellViewModel.cryptoAmount, + isLoading: _isLoadingPaymentMethods, + hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies, + onCurrencySelectorPressed: () => selectCryptoCurrency(context), + controller: customInputController, + focusNode: customInputFocusNode, + onContinuePressed: () { + navigateToProviders(context); + }, + onChanged: (amount) => + widget.buySellViewModel.changeFiatAmount(amount: amount), + ) + : BuySellDefaultAmountSelector( + key: ValueKey(0), + defaultAmounts: widget.buySellViewModel.defaultAmounts, + fiatCurrency: widget.buySellViewModel.fiatCurrency, + currentAmount: widget.buySellViewModel.fiatAmount, + hasCurrencySelector: widget.buySellViewModel.hasMultipleCurrencies, + onCurrencySelectorPressed: () => selectCryptoCurrency(context), + cryptoCurrency: widget.buySellViewModel.cryptoCurrency, + isLoading: _isLoadingPaymentMethods, + mode: widget.buySellViewModel.mode, + onSelected: (amount) async { + if (amount == null) { + // this resets the rate and prevents showing 0 usd = 0.something btc + await widget.buySellViewModel.changeFiatAmount(amount: ""); + setState(() { + _customAmountMode = true; + }); + customInputFocusNode.requestFocus(); + } else { + await widget.buySellViewModel.changeFiatAmount(amount: amount); + navigateToProviders(context); + } + }, + ), + ), + )) ], ), ), - ), - ); + ); + } + + void selectCryptoCurrency(BuildContext context) => CurrencyPickerSheet.show( + context: context, + args: CurrencyPickerArgs( + items: widget.buySellViewModel.activeWalletCurrencies.toList(), + onSelected: (item) => widget.buySellViewModel.changeCryptoCurrency(currency: item), + symbolResolver: widget.buySellViewModel.amountParsingProxy.getCryptoSymbol, + )); + + String get _pageTitle => widget.buySellViewModel.mode == BuySellPageMode.buy + ? S.current.buy + : S.current.sell + + ((widget.buySellViewModel.cryptoCurrencies.length == 1) + ? " ${widget.buySellViewModel.cryptoCurrencies.first.fullName}" + : ""); + + Future navigateToProviders(BuildContext context) async { + if (_isLoadingPaymentMethods) { + return; + } + + try { + setState(() { + _isLoadingPaymentMethods = true; + }); + + await asyncWhen((_) => [PaymentMethodLoaded, PaymentMethodFailed] + .contains(widget.buySellViewModel.paymentMethodState.runtimeType)); + + if (widget.buySellViewModel.paymentMethodState is PaymentMethodFailed) { + showPopUp( + context: context, + builder: (context) => AlertWithOneAction( + alertTitle: S.of(context).failed_to_load_payment_methods, + alertContent: S.of(context).please_try_again_later, + buttonText: "OK", + buttonAction: Navigator.of(context).pop)); + return; + } + + widget.buySellViewModel.selectedPaymentMethod = widget.buySellViewModel.paymentMethods + .firstWhere((item) => item.paymentMethodType == PaymentType.all); + + // unawaited because BuySellProviderPage has a nice little "loading rates..." ui thingie + unawaited(widget.buySellViewModel.calculateBestRate()); + + final page = BuySellProviderPage(buySellViewModel: widget.buySellViewModel); + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material(color: Colors.transparent, child: page))); + } finally { + setState(() { + _isLoadingPaymentMethods = false; + }); + } + } + } + + class BuySellCustomAmountInput extends StatelessWidget { + const BuySellCustomAmountInput( + {super.key, + required this.fiatCurrency, + required this.cryptoCurrency, + required this.cryptoAmount, + required this.controller, + required this.onContinuePressed, + required this.isLoading, + required this.onChanged, + required this.focusNode, + required this.hasCurrencySelector, + required this.onCurrencySelectorPressed}); + + final FiatCurrency fiatCurrency; + final CryptoCurrency cryptoCurrency; + final String cryptoAmount; + final TextEditingController controller; + final FocusNode focusNode; + final bool isLoading; + final bool hasCurrencySelector; + final VoidCallback onCurrencySelectorPressed; + final VoidCallback onContinuePressed; + final Function(String) onChanged; + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + SizedBox.shrink(), + Column( + spacing: 8, + children: [ + BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), + SizedBox.shrink(), + FloatingAmountInput( + currency: fiatCurrency, + focusNode: focusNode, + controller: controller, + onChanged: onChanged, + ), + Opacity( + opacity: cryptoAmount.isEmpty ? 0 : 1, + child: Text( + "≈ ${cryptoAmount} ${cryptoCurrency.symbol}", + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + ) + ], + ), + Padding( + padding: const EdgeInsets.all(18.0), + child: NewPrimaryButton( + onPressed: onContinuePressed, + isLoading: isLoading, + text: S.of(context).continue_text, + color: Theme.of(context).colorScheme.primary, + textColor: Theme.of(context).colorScheme.onPrimary), + ) + ], + ); + } + } + + class BuySellDefaultAmountSelector extends StatelessWidget { + const BuySellDefaultAmountSelector( + {super.key, + required this.defaultAmounts, + required this.fiatCurrency, + required this.mode, + required this.onSelected, + this.currentAmount, + required this.isLoading, + required this.hasCurrencySelector, + required this.onCurrencySelectorPressed, + required this.cryptoCurrency}); + + final List defaultAmounts; + final String? currentAmount; + final bool isLoading; + final bool hasCurrencySelector; + final VoidCallback onCurrencySelectorPressed; + final CryptoCurrency cryptoCurrency; + final FiatCurrency fiatCurrency; + final BuySellPageMode mode; + final Function(String?) onSelected; + + @override + Widget build(BuildContext context) { + return Column( + spacing: 24, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + BuySellCurrencyPickerPill(curr: cryptoCurrency, onTap: onCurrencySelectorPressed), + Text( + mode == BuySellPageMode.sell + ? S.of(context).choose_amount_to_sell + : S.of(context).choose_amount_to_buy, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 18.0), + child: GridView.builder( + shrinkWrap: true, + physics: NeverScrollableScrollPhysics(), + // +1 for "custom" option + itemCount: defaultAmounts.length + 1, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, crossAxisSpacing: 8, mainAxisSpacing: 16, mainAxisExtent: 105), + itemBuilder: (context, index) { + final String? item = index == defaultAmounts.length ? null : defaultAmounts[index]; + + return BuySellAmountPill( + isLoading: isLoading && item == currentAmount, + amount: item == null ? null : Money.parse(item, fiatCurrency), + onTap: () => onSelected(item), + ); + }), + ), + ], + ); + } + } + + class BuySellAmountPill extends StatelessWidget { + const BuySellAmountPill({super.key, this.amount, required this.onTap, required this.isLoading}); + + final Money? amount; + final VoidCallback onTap; + final bool isLoading; + + @override + Widget build(BuildContext context) { + return Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(9999999999), + border: Border.all( + width: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, + ), + gradient: LinearGradient( + colors: [ + context.customColors.cardGradientColorPrimary, + context.customColors.cardGradientColorSecondary + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + child: Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(9999999999), + child: InkWell( + borderRadius: BorderRadius.circular(9999999999), + onTap: onTap, + child: isLoading + ? CupertinoActivityIndicator() + : Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Row( + spacing: 4, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (amount != null) + Text( + amount!.toStringWithPrecision(fractionalDigits: 0), + style: TextStyle(fontSize: 18, fontWeight: FontWeight.w600), + ), + Text( + amount?.currency.symbol ?? S.of(context).custom, + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.w600, + color: amount == null + ? Theme.of(context).colorScheme.primary + : Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + if (amount == null) + Text( + S.of(context).enter_amount, + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + ), + ), + ); + } + } + + class BuySellCurrencyPickerPill extends StatelessWidget { + const BuySellCurrencyPickerPill({super.key, required this.curr, required this.onTap}); + + final CryptoCurrency curr; + final VoidCallback onTap; + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + decoration: BoxDecoration( + color: Theme.of(context).colorScheme.surfaceContainerHigh, + borderRadius: BorderRadius.circular(999999999), + ), + child: Padding( + padding: EdgeInsets.only(top: 5, bottom: 5, left: 5, right: 10), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 10, + children: [ + CakeImageWidget( + imageUrl: curr.iconPath, + width: 30, + height: 30, + ), + Text( + curr.fullName ?? curr.title, + style: TextStyle(fontSize: 16), + ), + RotatedBox( + quarterTurns: 2, + child: CakeImageWidget( + imageUrl: "assets/new-ui/dropdown_arrow.svg", + width: 8, + height: 8, + colorFilter: + ColorFilter.mode(Theme.of(context).colorScheme.primary, BlendMode.srcIn), + ), + ), + ], + ), + ), + ), + ); + } } -} diff --git a/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart index f96545951..4f1629227 100644 --- a/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart +++ b/lib/new-ui/pages/buy_sell/buy_sell_provider_page.dart @@ -48,10 +48,9 @@ class _BuySellProviderPageState extends State { leadingIcon: Icon(Icons.arrow_back_ios_new), onLeadingPressed: Navigator.of(context).pop, ), - Expanded( - child: Observer( + Expanded(child: Observer( builder: (_) { - if(widget.buySellViewModel.buySellQuotState is BuySellQuotFailed) { + if (widget.buySellViewModel.buySellQuotState is BuySellQuotFailed) { return Column( mainAxisAlignment: MainAxisAlignment.center, spacing: 24, @@ -64,7 +63,8 @@ class _BuySellProviderPageState extends State { S.of(context).could_not_load_quotes, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), ), - Text((widget.buySellViewModel.buySellQuotState as BuySellQuotFailed).errorMessage ?? + Text((widget.buySellViewModel.buySellQuotState as BuySellQuotFailed) + .errorMessage ?? S.of(context).please_try_again_later) ], ) @@ -72,43 +72,56 @@ class _BuySellProviderPageState extends State { ); } - if(widget.buySellViewModel.buySellQuotState is BuySellQuotLoading) { - return Center(child: Row(mainAxisAlignment:MainAxisAlignment.center, spacing:8, children: [ - CupertinoActivityIndicator(), - Text(S.of(context).loading_rates, style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant),) - ],),); + if (widget.buySellViewModel.buySellQuotState is BuySellQuotLoading) { + return Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + spacing: 8, + children: [ + CupertinoActivityIndicator(), + Text( + S.of(context).loading_rates, + style: TextStyle(color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ), + ); } return Padding( - padding: EdgeInsets.symmetric(horizontal: 18), - child: NewListSections(showHeader: true, sections: { - "": [ - ListItemRegularRow( - keyValue: "payment method", - label: S.of(context).payment_method, - showArrow: true, - onTap: (){ - - final page =BuySellPaymentMethodPage(buySellViewModel: widget.buySellViewModel); - Navigator.of(context).push(CupertinoPageRoute(builder: (context)=>Material(color: Colors.transparent,child: page,))); - }, - trailingText: widget.buySellViewModel.selectedPaymentMethod?.title) - ], - S.of(context).available_providers: [ - ...widget.buySellViewModel.sortedRecommendedQuotes.map(quoteListItem), - ListItemDropdown( - keyValue: "more options", - label: S.of(context).more_options, - onTap: () { - setState(() { - _allProvidersExpanded = !_allProvidersExpanded; - }); - }), - if (_allProvidersExpanded) - ...widget.buySellViewModel.sortedQuotes.map(quoteListItem) - ] - }), - ); + padding: EdgeInsets.symmetric(horizontal: 18), + child: NewListSections(showHeader: true, sections: { + "": [ + ListItemRegularRow( + keyValue: "payment method", + label: S.of(context).payment_method, + showArrow: true, + onTap: () { + final page = + BuySellPaymentMethodPage(buySellViewModel: widget.buySellViewModel); + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material( + color: Colors.transparent, + child: page, + ))); + }, + trailingText: widget.buySellViewModel.selectedPaymentMethod?.title) + ], + S.of(context).available_providers: [ + ...widget.buySellViewModel.sortedRecommendedQuotes.map(quoteListItem), + ListItemDropdown( + keyValue: "more options", + label: S.of(context).more_options, + onTap: () { + setState(() { + _allProvidersExpanded = !_allProvidersExpanded; + }); + }), + if (_allProvidersExpanded) + ...widget.buySellViewModel.sortedQuotes.map(quoteListItem) + ] + }), + ); }, )) ], @@ -116,18 +129,19 @@ class _BuySellProviderPageState extends State { ); } - String get _pageTitle => (widget.buySellViewModel.mode == BuySellPageMode.buy - ? S.current.buy - : S.current.sell) + " " + (widget.buySellViewModel.cryptoCurrency.fullName ?? ""); + String get _pageTitle => + (widget.buySellViewModel.mode == BuySellPageMode.buy ? S.current.buy : S.current.sell) + + " " + + (widget.buySellViewModel.cryptoCurrency.fullName ?? ""); ListItem quoteListItem(Quote quote) => ListItemRegularRow( keyValue: quote.provider.title, label: quote.rampName ?? quote.provider.title, - secondaryLabel: quote.rampName != null? quote.provider.title : null, + secondaryLabel: quote.rampName != null ? quote.provider.title : null, subtitle: quote.badges.isEmpty ? null : quote.badges.join(" - "), subtitleColor: Theme.of(context).colorScheme.primary, iconPath: quote.darkIconPath, - onTap: (){ + onTap: () { widget.buySellViewModel.changeOption(quote); navigateToConfirmation(context); }, @@ -137,14 +151,21 @@ class _BuySellProviderPageState extends State { mainAxisAlignment: MainAxisAlignment.center, spacing: 4, children: [ - Text(widget.buySellViewModel.amountForQuote(quote).toStringWithSymbol(fractionalDigits: 8)), - Text("= ${widget.buySellViewModel.fiatAmountForQuote(quote).toStringWithSymbol(fractionalDigits: 2, trimZeros: false)}", style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)) - - ],)); + Text(widget.buySellViewModel + .amountForQuote(quote) + .toStringWithSymbol(fractionalDigits: 8)), + Text( + "= ${widget.buySellViewModel.fiatAmountForQuote(quote).toStringWithSymbol(fractionalDigits: 2, trimZeros: false)}", + style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant)) + ], + )); void navigateToConfirmation(BuildContext context) { - final page = BuySellConfirmationPage(buySellViewModel: widget.buySellViewModel); - Navigator.of(context).push(CupertinoPageRoute(builder: (context)=>Material(color: Colors.transparent,child: page,))); + Navigator.of(context).push(CupertinoPageRoute( + builder: (context) => Material( + color: Colors.transparent, + child: page, + ))); } } diff --git a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart index 6d5343ccc..0c1013443 100644 --- a/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart +++ b/lib/new-ui/widgets/buy_sell/buy_sell_selector_modal.dart @@ -14,34 +14,51 @@ class BuySellSelectorModal extends StatelessWidget { @override Widget build(BuildContext context) { return Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.vertical(top: Radius.circular(18)), - gradient: LinearGradient( - colors: [ - Theme.of(context).colorScheme.surface, - Theme.of(context).colorScheme.surfaceDim, - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - child: SafeArea( - top: false, - child: Column(spacing:24, mainAxisSize: MainAxisSize.min, children: [ - SizedBox.shrink(), - Text(S.of(context).buy_or_sell, style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500),), - Text(S.of(context).buy_or_sell_desc, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),), - Padding( - padding: EdgeInsets.symmetric(horizontal: 18.0), - child: Column(spacing: 12,children: [ - BuySellSelectorModalButton(title: S.of(context).buy_crypto, description: S.of(context).buy_crypto_desc, iconPath: "assets/new-ui/plus.svg", onTap: ()=>openBuySellPage(context, BuySellPageMode.buy),), - BuySellSelectorModalButton(title: S.of(context).sell_crypto, description: S.of(context).sell_crypto_desc, iconPath: "assets/new-ui/sell.svg", onTap: ()=>openBuySellPage(context, BuySellPageMode.sell)) - ],), + decoration: BoxDecoration( + borderRadius: BorderRadius.vertical(top: Radius.circular(18)), + gradient: LinearGradient( + colors: [ + Theme.of(context).colorScheme.surface, + Theme.of(context).colorScheme.surfaceDim, + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, ), - SizedBox.shrink() - ]) - ) - ); + ), + child: SafeArea( + top: false, + child: Column(spacing: 24, mainAxisSize: MainAxisSize.min, children: [ + SizedBox.shrink(), + Text( + S.of(context).buy_or_sell, + style: TextStyle(fontSize: 20, fontWeight: FontWeight.w500), + ), + Text( + S.of(context).buy_or_sell_desc, + style: + TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 18.0), + child: Column( + spacing: 12, + children: [ + BuySellSelectorModalButton( + title: S.of(context).buy_crypto, + description: S.of(context).buy_crypto_desc, + iconPath: "assets/new-ui/plus.svg", + onTap: () => openBuySellPage(context, BuySellPageMode.buy), + ), + BuySellSelectorModalButton( + title: S.of(context).sell_crypto, + description: S.of(context).sell_crypto_desc, + iconPath: "assets/new-ui/sell.svg", + onTap: () => openBuySellPage(context, BuySellPageMode.sell)) + ], + ), + ), + SizedBox.shrink() + ]))); } void openBuySellPage(BuildContext context, BuySellPageMode mode) { @@ -58,9 +75,13 @@ class BuySellSelectorModal extends StatelessWidget { } } - class BuySellSelectorModalButton extends StatelessWidget { - const BuySellSelectorModalButton({super.key, required this.title, required this.description, required this.iconPath, required this.onTap}); + const BuySellSelectorModalButton( + {super.key, + required this.title, + required this.description, + required this.iconPath, + required this.onTap}); final String title; final String description; @@ -75,8 +96,8 @@ class BuySellSelectorModalButton extends StatelessWidget { decoration: BoxDecoration( borderRadius: BorderRadius.circular(16), border: Border.all( - width: 1, color: Theme.of(context).colorScheme.surfaceContainerHigh, - + width: 1, + color: Theme.of(context).colorScheme.surfaceContainerHigh, ), gradient: LinearGradient( colors: [ @@ -87,16 +108,36 @@ class BuySellSelectorModalButton extends StatelessWidget { end: Alignment.bottomCenter, ), ), - child: Padding(padding: EdgeInsets.all(24), child: Row(spacing: 20, children: [ - - CakeImageWidget(imageUrl: iconPath, width: 55, height: 55, colorFilter: ColorFilter.mode(Theme.of(context).colorScheme.onSurfaceVariant,BlendMode.srcIn),), - Column(spacing: 8, crossAxisAlignment: CrossAxisAlignment.start,children: [ - Text(title, style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16),), - Text(description, style: TextStyle(fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant),) - ],) - - ],),), - + child: Padding( + padding: EdgeInsets.all(24), + child: Row( + spacing: 20, + children: [ + CakeImageWidget( + imageUrl: iconPath, + width: 55, + height: 55, + colorFilter: ColorFilter.mode( + Theme.of(context).colorScheme.onSurfaceVariant, BlendMode.srcIn), + ), + Column( + spacing: 8, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: TextStyle(fontWeight: FontWeight.w500, fontSize: 16), + ), + Text( + description, + style: TextStyle( + fontSize: 12, color: Theme.of(context).colorScheme.onSurfaceVariant), + ) + ], + ) + ], + ), + ), ), ); } diff --git a/lib/new-ui/widgets/coins_page/cards/cards_view.dart b/lib/new-ui/widgets/coins_page/cards/cards_view.dart index ca6cb6bb8..85812c80c 100644 --- a/lib/new-ui/widgets/coins_page/cards/cards_view.dart +++ b/lib/new-ui/widgets/coins_page/cards/cards_view.dart @@ -179,7 +179,8 @@ class _CardsViewState extends State { icon: Icons.arrow_forward_ios_rounded, iconSize: 12, onTap: () { - showModalBottomSheet(context: context, builder: (context)=>BuySellSelectorModal()); + showModalBottomSheet( + context: context, builder: (context) => BuySellSelectorModal()); }, ) ] diff --git a/lib/new-ui/widgets/floating_amount_input.dart b/lib/new-ui/widgets/floating_amount_input.dart index 3367d0a3a..7fb230452 100644 --- a/lib/new-ui/widgets/floating_amount_input.dart +++ b/lib/new-ui/widgets/floating_amount_input.dart @@ -3,7 +3,14 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; class FloatingAmountInput extends StatefulWidget { - const FloatingAmountInput({super.key, required this.currency, required this.controller, this.focusNode, this.inputFormatters, this.onChanged, this.validator}); + const FloatingAmountInput( + {super.key, + required this.currency, + required this.controller, + this.focusNode, + this.inputFormatters, + this.onChanged, + this.validator}); final Currency currency; final TextEditingController controller; @@ -28,7 +35,7 @@ class _FloatingAmountInputState extends State { @override Widget build(BuildContext context) { - return Center( + return Center( child: Row( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.baseline, @@ -59,19 +66,17 @@ class _FloatingAmountInputState extends State { hoverColor: Colors.transparent, focusedBorder: InputBorder.none, enabledBorder: InputBorder.none, - hintText: _amountFocused || widget.controller.text.isNotEmpty - ? null - : "0.00", + hintText: _amountFocused || widget.controller.text.isNotEmpty ? null : "0.00", hintStyle: Theme.of(context).textTheme.displayMedium?.copyWith( - fontWeight: FontWeight.w400, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + fontWeight: FontWeight.w400, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), style: Theme.of(context).textTheme.displayMedium?.copyWith( - fontWeight: FontWeight.w400, - fontSize: 45, - color: Theme.of(context).colorScheme.onSurface, - ), + fontWeight: FontWeight.w400, + fontSize: 45, + color: Theme.of(context).colorScheme.onSurface, + ), ), ), const SizedBox(width: 8), @@ -80,10 +85,10 @@ class _FloatingAmountInputState extends State { maxLines: 1, overflow: TextOverflow.ellipsis, style: Theme.of(context).textTheme.displayMedium?.copyWith( - fontWeight: FontWeight.w400, - fontSize: 45, - color: Theme.of(context).colorScheme.onSurfaceVariant, - ), + fontWeight: FontWeight.w400, + fontSize: 45, + color: Theme.of(context).colorScheme.onSurfaceVariant, + ), ), ], ), diff --git a/lib/utils/exception_handler.dart b/lib/utils/exception_handler.dart index e46650517..fe9c0844a 100644 --- a/lib/utils/exception_handler.dart +++ b/lib/utils/exception_handler.dart @@ -118,7 +118,7 @@ class ExceptionHandler { if (kDebugMode || kProfileMode) { if (_ignoreError(errorDetails.exception.toString()) || - _ignoreError(errorDetails.stack.toString())) { + _ignoreError(errorDetails.stack.toString()) || _flutterErrorIgnore(errorDetails)) { printV("(BELOW ERROR IS IGNORED AND WILL NOT TRIGGER POPUP IN PROD)"); } FlutterError.presentError(errorDetails); diff --git a/lib/view_model/buy/buy_sell_view_model.dart b/lib/view_model/buy/buy_sell_view_model.dart index 25e8ef862..4e6e2bf51 100644 --- a/lib/view_model/buy/buy_sell_view_model.dart +++ b/lib/view_model/buy/buy_sell_view_model.dart @@ -174,10 +174,15 @@ abstract class BuySellViewModelBase extends WalletChangeListenerViewModel with S return maxAmount.toStringAsFixed(2); } - Money amountForQuote(Quote quote) => Money.parse(double.parse(fiatAmount)/quote.rate, cryptoCurrency); + Money amountForQuote(Quote quote) => + Money.parse(double.parse(fiatAmount) / quote.rate, cryptoCurrency); Money fiatAmountForQuote(Quote quote) { - return Money.parse((fiatConversionStore.prices[cryptoCurrency]! * double.parse(amountForQuote(quote).toString())).toStringAsFixed(2), fiatCurrency); + return Money.parse( + (fiatConversionStore.prices[cryptoCurrency]! * + double.parse(amountForQuote(quote).toString())) + .toStringAsFixed(2), + fiatCurrency); } // based on usd values, should have roughly equal worth (was done with ai though so it's subject to correction)