diff --git a/assets/svg/coin_icons/BinanceUSD.svg b/assets/svg/coin_icons/BinanceUSD.svg deleted file mode 100644 index d2a374781..000000000 --- a/assets/svg/coin_icons/BinanceUSD.svg +++ /dev/null @@ -1 +0,0 @@ -Asset 1 \ No newline at end of file diff --git a/assets/svg/coin_icons/Cosmos.svg b/assets/svg/coin_icons/Cosmos.svg deleted file mode 100644 index a97174439..000000000 --- a/assets/svg/coin_icons/Cosmos.svg +++ /dev/null @@ -1 +0,0 @@ -cosmos-atom-logo \ No newline at end of file diff --git a/assets/svg/coin_icons/Dai.svg b/assets/svg/coin_icons/Dai.svg deleted file mode 100644 index 75de37346..000000000 --- a/assets/svg/coin_icons/Dai.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - \ No newline at end of file diff --git a/assets/svg/coin_icons/Dash.svg b/assets/svg/coin_icons/Dash.svg deleted file mode 100644 index 140ab6704..000000000 --- a/assets/svg/coin_icons/Dash.svg +++ /dev/null @@ -1 +0,0 @@ -d \ No newline at end of file diff --git a/assets/svg/coin_icons/EOS.svg b/assets/svg/coin_icons/EOS.svg deleted file mode 100644 index df772834f..000000000 --- a/assets/svg/coin_icons/EOS.svg +++ /dev/null @@ -1 +0,0 @@ -Asset 1 \ No newline at end of file diff --git a/assets/svg/coin_icons/Ethereum.svg b/assets/svg/coin_icons/Ethereum.svg deleted file mode 100644 index 7ffd694cc..000000000 --- a/assets/svg/coin_icons/Ethereum.svg +++ /dev/null @@ -1,18 +0,0 @@ - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/assets/svg/coin_icons/Ripple.svg b/assets/svg/coin_icons/Ripple.svg deleted file mode 100644 index 9a2c7c632..000000000 --- a/assets/svg/coin_icons/Ripple.svg +++ /dev/null @@ -1 +0,0 @@ -x \ No newline at end of file diff --git a/assets/svg/coin_icons/Stellar.svg b/assets/svg/coin_icons/Stellar.svg deleted file mode 100644 index 02afb7b79..000000000 --- a/assets/svg/coin_icons/Stellar.svg +++ /dev/null @@ -1 +0,0 @@ -Asset 1 \ No newline at end of file diff --git a/assets/svg/coin_icons/Tether.svg b/assets/svg/coin_icons/Tether.svg deleted file mode 100644 index e53082240..000000000 --- a/assets/svg/coin_icons/Tether.svg +++ /dev/null @@ -1 +0,0 @@ -tether-usdt-logo \ No newline at end of file diff --git a/assets/svg/coin_icons/Tron.svg b/assets/svg/coin_icons/Tron.svg deleted file mode 100644 index fa87a1d7e..000000000 --- a/assets/svg/coin_icons/Tron.svg +++ /dev/null @@ -1 +0,0 @@ -tron \ No newline at end of file diff --git a/lib/pages/buy_view/buy_form.dart b/lib/pages/buy_view/buy_form.dart index c55226d6d..029bc9aa4 100644 --- a/lib/pages/buy_view/buy_form.dart +++ b/lib/pages/buy_view/buy_form.dart @@ -17,6 +17,7 @@ import 'package:stackwallet/pages/buy_view/sub_widgets/fiat_selection_view.dart' import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; import 'package:stackwallet/pages_desktop_specific/my_stack_view/wallet_view/sub_widgets/address_book_address_chooser/address_book_address_chooser.dart'; import 'package:stackwallet/providers/providers.dart'; +import 'package:stackwallet/services/buy/buy_response.dart'; import 'package:stackwallet/services/buy/simplex/simplex_api.dart'; import 'package:stackwallet/utilities/address_utils.dart'; import 'package:stackwallet/utilities/assets.dart'; @@ -47,10 +48,13 @@ import 'package:stackwallet/widgets/textfield_icon_button.dart'; class BuyForm extends ConsumerStatefulWidget { const BuyForm({ Key? key, + this.coin, this.clipboard = const ClipboardWrapper(), this.scanner = const BarcodeScannerWrapper(), }) : super(key: key); + final Coin? coin; + final ClipboardInterface clipboard; final BarcodeScannerInterface scanner; @@ -59,6 +63,8 @@ class BuyForm extends ConsumerStatefulWidget { } class _BuyFormState extends ConsumerState { + late final Coin? coin; + late final ClipboardInterface clipboard; late final BarcodeScannerInterface scanner; @@ -75,8 +81,8 @@ class _BuyFormState extends ConsumerState { List? fiats; String? _address; - Fiat? selectedFiat; - Crypto? selectedCrypto; + static Fiat? selectedFiat; + static Crypto? selectedCrypto; SimplexQuote quote = SimplexQuote( crypto: Crypto.fromJson({'ticker': 'BTC', 'name': 'Bitcoin'}), fiat: Fiat.fromJson({'ticker': 'USD', 'name': 'United States Dollar'}), @@ -87,13 +93,20 @@ class _BuyFormState extends ConsumerState { buyWithFiat: true, ); // TODO enum this or something - bool buyWithFiat = true; + static bool buyWithFiat = true; bool _addressToggleFlag = false; bool _hovering1 = false; bool _hovering2 = false; - Decimal minFiat = Decimal.fromInt(50); - Decimal maxFiat = Decimal.fromInt(20000); + // TODO actually check USD min and max, these could get updated by Simplex + static Decimal minFiat = Decimal.fromInt(50); + static Decimal maxFiat = Decimal.fromInt(20000); + + // We can't get crypto min and max without asking for a quote + static Decimal minCrypto = Decimal.parse((0.00000001) + .toString()); // lol how to go from double->Decimal more easily? + static Decimal maxCrypto = Decimal.parse((10000.00000000).toString()); + static String boundedCryptoTicker = ''; void fiatFieldOnChanged(String value) async {} @@ -125,6 +138,13 @@ class _BuyFormState extends ConsumerState { coins: ref.read(simplexProvider).supportedCryptos, onSelected: (crypto) { setState(() { + if (selectedCrypto?.ticker != _BuyFormState.boundedCryptoTicker) { + // Reset crypto mins and maxes ... we don't know these bounds until we request a quote + _BuyFormState.minCrypto = Decimal.parse((0.00000001) + .toString()); // lol how to go from double->Decimal more easily? + _BuyFormState.maxCrypto = + Decimal.parse((10000.00000000).toString()); + } selectedCrypto = crypto; }); }, @@ -357,9 +377,10 @@ class _BuyFormState extends ConsumerState { } Widget? getIconForTicker(String ticker) { - String? iconAsset = isStackCoin(ticker) - ? Assets.svg.iconFor(coin: coinFromTickerCaseInsensitive(ticker)) - : Assets.svg.buyIconFor(ticker); + String? iconAsset = /*isStackCoin(ticker) + ?*/ + Assets.svg.iconFor(coin: coinFromTickerCaseInsensitive(ticker)); + // : Assets.svg.buyIconFor(ticker); return (iconAsset != null) ? SvgPicture.asset(iconAsset, height: 20, width: 20) : null; @@ -394,22 +415,111 @@ class _BuyFormState extends ConsumerState { buyWithFiat: buyWithFiat, ); - await _loadQuote(quote); + BuyResponse quoteResponse = await _loadQuote(quote); shouldPop = true; if (mounted) { Navigator.of(context, rootNavigator: isDesktop).pop(); } - quote = ref.read(simplexProvider).quote; + if (quoteResponse.exception == null) { + quote = quoteResponse.value as SimplexQuote; - if (quote.id != 'id' && quote.id != 'someID') { - // TODO detect default quote better - await _showFloatingBuyQuotePreviewSheet( - quote: ref.read(simplexProvider).quote, - onSelected: (quote) { - // TODO launch URL - }, - ); + if (quote.id != 'id' && quote.id != 'someID') { + // TODO detect default quote better + await _showFloatingBuyQuotePreviewSheet( + quote: ref.read(simplexProvider).quote, + onSelected: (quote) { + // TODO launch URL + }, + ); + } else { + await showDialog( + context: context, + barrierDismissible: true, + builder: (context) { + if (isDesktop) { + return DesktopDialog( + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Simplex API unresponsive", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 24, + ), + Text( + "Simplex API unresponsive, please try again later", + style: STextStyles.smallMed14(context), + ), + const SizedBox( + height: 56, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: Navigator.of(context).pop, + ), + ), + ], + ) + ], + ), + ), + ); + } else { + return StackDialog( + title: "Simplex API error", + message: "${quoteResponse.exception?.errorMessage}", + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ); + } + }, + ); + } } else { + // Error; probably amount out of bounds + String errorMessage = "${quoteResponse.exception?.errorMessage}"; + if (errorMessage.contains('must be between')) { + errorMessage = errorMessage.substring( + (errorMessage.indexOf('getQuote exception: ') ?? 19) + 20, + errorMessage.indexOf(", value: null")); + _BuyFormState.boundedCryptoTicker = errorMessage.substring( + errorMessage.indexOf('The ') + 4, + errorMessage.indexOf(' amount must be between')); + _BuyFormState.minCrypto = Decimal.parse(errorMessage.substring( + errorMessage.indexOf('must be between ') + 16, + errorMessage.indexOf(' and '))); + _BuyFormState.maxCrypto = Decimal.parse(errorMessage.substring( + errorMessage.indexOf("$minCrypto and ") + "$minCrypto and ".length, + errorMessage.length)); + if (Decimal.parse(_buyAmountController.text) > + _BuyFormState.maxCrypto) { + _buyAmountController.text = _BuyFormState.maxCrypto.toString(); + } + } await showDialog( context: context, barrierDismissible: true, @@ -424,14 +534,14 @@ class _BuyFormState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( - "Simplex API unresponsive", + "Simplex API error", style: STextStyles.desktopH3(context), ), const SizedBox( height: 24, ), Text( - "Simplex API unresponsive, please try again later", + errorMessage, style: STextStyles.smallMed14(context), ), const SizedBox( @@ -455,9 +565,9 @@ class _BuyFormState extends ConsumerState { ); } else { return StackDialog( - title: "Simplex API unresponsive", - message: - "Unexpected response from Simplex API, please try again later", + title: "Simplex API error", + message: "${quoteResponse.exception?.errorMessage}", + // "${quoteResponse.exception?.errorMessage.substring(8, (quoteResponse.exception?.errorMessage?.length ?? 109) - (8 + 6))}", rightButton: TextButton( style: Theme.of(context) .extension()! @@ -480,16 +590,24 @@ class _BuyFormState extends ConsumerState { } } - Future _loadQuote(SimplexQuote quote) async { + Future> _loadQuote(SimplexQuote quote) async { final response = await SimplexAPI.instance.getQuote(quote); if (response.value != null) { + // TODO check for error key ref.read(simplexProvider).updateQuote(response.value!); + return BuyResponse(value: response.value!); } else { Logging.instance.log( "_loadQuote: $response", level: LogLevel.Warning, ); + return BuyResponse( + exception: BuyException( + response.toString(), + BuyExceptionType.generic, + ), + ); } } @@ -578,10 +696,8 @@ class _BuyFormState extends ConsumerState { // quote = ref.read(simplexProvider).quote; quote = SimplexQuote( - crypto: - Crypto.fromJson({'ticker': 'BTC', 'name': 'Bitcoin', 'image': ''}), - fiat: Fiat.fromJson( - {'ticker': 'USD', 'name': 'United States Dollar', 'image': ''}), + crypto: Crypto.fromJson({'ticker': 'BTC', 'name': 'Bitcoin'}), + fiat: Fiat.fromJson({'ticker': 'USD', 'name': 'United States Dollar'}), youPayFiatPrice: Decimal.parse("100"), youReceiveCryptoAmount: Decimal.parse("1.0238917"), id: "someID", @@ -590,10 +706,12 @@ class _BuyFormState extends ConsumerState { ); // TODO enum this or something // TODO set defaults better; should probably explicitly enumerate the coins & fiats used and pull the specific ones we need rather than generating them as defaults here - selectedFiat = Fiat.fromJson( - {'ticker': 'USD', 'name': 'United States Dollar', 'image': ''}); - selectedCrypto = - Crypto.fromJson({'ticker': 'BTC', 'name': 'Bitcoin', 'image': ''}); + selectedFiat = + Fiat.fromJson({'ticker': 'USD', 'name': 'United States Dollar'}); + selectedCrypto = Crypto.fromJson({ + 'ticker': widget.coin?.ticker ?? 'BTC', + 'name': widget.coin?.prettyName ?? 'Bitcoin' + }); // TODO set initial crypto to open wallet if a wallet is open @@ -660,7 +778,7 @@ class _BuyFormState extends ConsumerState { color: _hovering1 ? Theme.of(context) .extension()! - .highlight + .currencyListItemBG .withOpacity(_hovering1 ? 0.3 : 0) : Theme.of(context) .extension()! @@ -727,7 +845,7 @@ class _BuyFormState extends ConsumerState { color: _hovering2 ? Theme.of(context) .extension()! - .highlight + .currencyListItemBG .withOpacity(_hovering2 ? 0.3 : 0) : Theme.of(context) .extension()! @@ -743,7 +861,7 @@ class _BuyFormState extends ConsumerState { decoration: BoxDecoration( color: Theme.of(context) .extension()! - .highlight, + .currencyListItemBG, borderRadius: BorderRadius.circular(4), ), child: Text( @@ -820,7 +938,10 @@ class _BuyFormState extends ConsumerState { color: Theme.of(context).extension()!.textDark, ), key: const Key("buyAmountInputFieldTextFieldKey"), - controller: _buyAmountController, + controller: _buyAmountController + ..text = _BuyFormState.buyWithFiat + ? _BuyFormState.minFiat.toStringAsFixed(2) ?? '50.00' + : _BuyFormState.minCrypto.toStringAsFixed(8), focusNode: _buyAmountFocusNode, keyboardType: Util.isDesktop ? null @@ -829,12 +950,7 @@ class _BuyFormState extends ConsumerState { decimal: true, ), textAlign: TextAlign.left, - inputFormatters: [ - // regex to validate a crypto amount with 8 decimal places or - // 2 if fiat - NumericalRangeFormatter( - min: minFiat, max: maxFiat, buyWithFiat: buyWithFiat) - ], + inputFormatters: [NumericalRangeFormatter()], decoration: InputDecoration( contentPadding: const EdgeInsets.only( // top: 22, @@ -864,7 +980,7 @@ class _BuyFormState extends ConsumerState { decoration: BoxDecoration( color: Theme.of(context) .extension()! - .highlight, + .currencyListItemBG, borderRadius: BorderRadius.circular(4), ), child: Text( @@ -905,11 +1021,20 @@ class _BuyFormState extends ConsumerState { _buyAmountController.text.isNotEmpty ? TextFieldIconButton( key: const Key( - "buyViewClearAddressFieldButtonKey"), + "buyViewClearAmountFieldButtonKey"), onTap: () { - _buyAmountController.text = ""; - // _receiveAddress = ""; - setState(() {}); + if (_BuyFormState.buyWithFiat) { + _buyAmountController.text = _BuyFormState + .minFiat + .toStringAsFixed(2); + } else { + if (selectedCrypto?.ticker == + _BuyFormState.boundedCryptoTicker) { + _buyAmountController.text = _BuyFormState + .minCrypto + .toStringAsFixed(8); + } + } }, child: const XIcon(), ) @@ -1248,36 +1373,49 @@ class _BuyFormState extends ConsumerState { // See https://stackoverflow.com/a/68072967 class NumericalRangeFormatter extends TextInputFormatter { - final Decimal min; - final Decimal max; - final bool buyWithFiat; - - NumericalRangeFormatter( - {required this.min, required this.max, required this.buyWithFiat}); + NumericalRangeFormatter(); @override TextEditingValue formatEditUpdate( TextEditingValue oldValue, TextEditingValue newValue, ) { + TextSelection newSelection = newValue.selection; + String newVal = _BuyFormState.buyWithFiat + ? Decimal.parse(newValue.text).toStringAsFixed(2) + : Decimal.parse(newValue.text).toStringAsFixed(8); if (newValue.text == '') { return newValue; } else { - if (buyWithFiat) { - if (Decimal.parse(newValue.text) < min) { - newValue = - const TextEditingValue().copyWith(text: min.toStringAsFixed(2)); - } else { - newValue = Decimal.parse(newValue.text) > max ? oldValue : newValue; + if (_BuyFormState.buyWithFiat) { + if (Decimal.parse(newValue.text) < _BuyFormState.minFiat) { + newVal = _BuyFormState.minFiat.toStringAsFixed(2); + // _BuyFormState._buyAmountController.selection = + // TextSelection.collapsed( + // offset: _BuyFormState.buyWithFiat + // ? _BuyFormState._buyAmountController.text.length - 2 + // : _BuyFormState._buyAmountController.text.length - 8); + } else if (Decimal.parse(newValue.text) > _BuyFormState.maxFiat) { + newVal = _BuyFormState.maxFiat.toStringAsFixed(2); + } + } else if (!_BuyFormState.buyWithFiat && + _BuyFormState.selectedCrypto?.ticker == + _BuyFormState.boundedCryptoTicker) { + if (Decimal.parse(newValue.text) < _BuyFormState.minCrypto) { + newVal = _BuyFormState.minCrypto.toStringAsFixed(8); + } else if (Decimal.parse(newValue.text) > _BuyFormState.maxCrypto) { + newVal = _BuyFormState.maxCrypto.toStringAsFixed(8); } } } - final regexString = buyWithFiat + final regexString = _BuyFormState.buyWithFiat ? r'^([0-9]*[,.]?[0-9]{0,2}|[,.][0-9]{0,2})$' : r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$'; // return RegExp(r'^([0-9]*[,.]?[0-9]{0,8}|[,.][0-9]{0,8})$') - return RegExp(regexString).hasMatch(newValue.text) ? newValue : oldValue; + return RegExp(regexString).hasMatch(newVal) + ? TextEditingValue(text: newVal, selection: newSelection) + : oldValue; } } diff --git a/lib/pages/buy_view/buy_in_wallet_view.dart b/lib/pages/buy_view/buy_in_wallet_view.dart new file mode 100644 index 000000000..09cbb6857 --- /dev/null +++ b/lib/pages/buy_view/buy_in_wallet_view.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:stackwallet/pages/buy_view/buy_view.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; +import 'package:stackwallet/utilities/text_styles.dart'; +import 'package:stackwallet/utilities/theme/stack_colors.dart'; +import 'package:stackwallet/widgets/background.dart'; +import 'package:stackwallet/widgets/custom_buttons/app_bar_icon_button.dart'; + +class BuyInWalletView extends StatefulWidget { + const BuyInWalletView({ + Key? key, + required this.coin, + }) : super(key: key); + + static const String routeName = "/stackBuyInWalletView"; + + final Coin? coin; + + @override + State createState() => _BuyInWalletViewState(); +} + +class _BuyInWalletViewState extends State { + late final Coin? coin; + + @override + Widget build(BuildContext context) { + debugPrint("BUILD: $runtimeType"); + + return Background( + child: Scaffold( + backgroundColor: Theme.of(context).extension()!.background, + appBar: AppBar( + leading: AppBarBackButton( + onPressed: () { + Navigator.of(context).pop(); + }, + ), + title: Text( + "Buy ${widget.coin?.ticker}", + style: STextStyles.navBarTitle(context), + ), + ), + body: BuyView(coin: widget.coin), + ), + ); + } +} diff --git a/lib/pages/buy_view/buy_view.dart b/lib/pages/buy_view/buy_view.dart index 0eb44e87e..dca907b59 100644 --- a/lib/pages/buy_view/buy_view.dart +++ b/lib/pages/buy_view/buy_view.dart @@ -1,28 +1,36 @@ import 'package:flutter/material.dart'; import 'package:stackwallet/pages/buy_view/buy_form.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; class BuyView extends StatefulWidget { - const BuyView({Key? key}) : super(key: key); + const BuyView({ + Key? key, + this.coin, + }) : super(key: key); static const String routeName = "/stackBuyView"; + final Coin? coin; + @override State createState() => _BuyViewState(); } class _BuyViewState extends State { + late final Coin? coin; + @override Widget build(BuildContext context) { debugPrint("BUILD: $runtimeType"); - return const SafeArea( + return SafeArea( child: Padding( - padding: EdgeInsets.only( + padding: const EdgeInsets.only( left: 16, right: 16, top: 16, ), - child: BuyForm(), + child: BuyForm(coin: widget.coin), ), ); } diff --git a/lib/pages/buy_view/sub_widgets/buy_warning_popup.dart b/lib/pages/buy_view/sub_widgets/buy_warning_popup.dart index 082a19306..c98d53580 100644 --- a/lib/pages/buy_view/sub_widgets/buy_warning_popup.dart +++ b/lib/pages/buy_view/sub_widgets/buy_warning_popup.dart @@ -29,18 +29,9 @@ class BuyWarningPopup extends StatelessWidget { SimplexOrder? order; Future> newOrder(SimplexQuote quote) async { - final response = await SimplexAPI.instance.newOrder(quote); + final orderResponse = await SimplexAPI.instance.newOrder(quote); - // if (response.value != null) { - // ref.read(simplexProvider).updateOrder(response.value!); - // } else { - // Logging.instance.log( - // "_loadQuote: $response", - // level: LogLevel.Warning, - // ); - // } - - return response; + return orderResponse; } Future> redirect(SimplexOrder order) async { @@ -122,13 +113,90 @@ class BuyWarningPopup extends StatelessWidget { rightButton: PrimaryButton( label: "Continue", onPressed: () async { - BuyResponse order = await newOrder(quote); - await redirect(order.value as SimplexOrder).then((_response) async { - this.order = order.value as SimplexOrder; - Navigator.of(context, rootNavigator: isDesktop).pop(); - Navigator.of(context, rootNavigator: isDesktop).pop(); - await _buyInvoice(); - }); + BuyResponse orderResponse = await newOrder(quote); + if (orderResponse.exception == null) { + await redirect(orderResponse.value as SimplexOrder) + .then((_response) async { + this.order = orderResponse.value as SimplexOrder; + Navigator.of(context, rootNavigator: isDesktop).pop(); + Navigator.of(context, rootNavigator: isDesktop).pop(); + await _buyInvoice(); + }); + } else { + await showDialog( + context: context, + barrierDismissible: true, + builder: (context) { + if (isDesktop) { + return DesktopDialog( + maxWidth: 450, + child: Padding( + padding: const EdgeInsets.all(32), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "Simplex API error", + style: STextStyles.desktopH3(context), + ), + const SizedBox( + height: 24, + ), + Text( + "${orderResponse.exception?.errorMessage}", + style: STextStyles.smallMed14(context), + ), + const SizedBox( + height: 56, + ), + Row( + children: [ + const Spacer(), + Expanded( + child: PrimaryButton( + buttonHeight: ButtonHeight.l, + label: "Ok", + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + Navigator.of(context).pop(); // weee + }, + ), + ), + ], + ) + ], + ), + ), + ); + } else { + return StackDialog( + title: "Simplex API error", + message: "${orderResponse.exception?.errorMessage}", + // "${quoteResponse.exception?.errorMessage.substring(8, (quoteResponse.exception?.errorMessage?.length ?? 109) - (8 + 6))}", + rightButton: TextButton( + style: Theme.of(context) + .extension()! + .getSecondaryEnabledButtonStyle(context), + child: Text( + "Ok", + style: STextStyles.button(context).copyWith( + color: Theme.of(context) + .extension()! + .accentColorDark), + ), + onPressed: () { + Navigator.of(context).pop(); + Navigator.of(context).pop(); + Navigator.of(context).pop(); // weee + }, + ), + ); + } + }, + ); + } }, ), icon: SizedBox( diff --git a/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart index 2d0fe3095..10963df18 100644 --- a/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart +++ b/lib/pages/buy_view/sub_widgets/crypto_selection_view.dart @@ -253,9 +253,10 @@ bool isStackCoin(String? ticker) { } Widget? getIconForTicker(String ticker) { - String? iconAsset = isStackCoin(ticker) - ? Assets.svg.iconFor(coin: coinFromTickerCaseInsensitive(ticker)) - : Assets.svg.buyIconFor(ticker); + String? iconAsset = /*isStackCoin(ticker) + ?*/ + Assets.svg.iconFor(coin: coinFromTickerCaseInsensitive(ticker)); + // : Assets.svg.buyIconFor(ticker); return (iconAsset != null) ? SvgPicture.asset(iconAsset, height: 20, width: 20) : null; diff --git a/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart b/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart index e777ab96c..dfaec0dec 100644 --- a/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart +++ b/lib/pages/buy_view/sub_widgets/fiat_selection_view.dart @@ -207,7 +207,7 @@ class _FiatSelectionViewState extends State { decoration: BoxDecoration( color: Theme.of(context) .extension()! - .highlight, + .currencyListItemBG, borderRadius: BorderRadius.circular(4), ), child: Text( diff --git a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart index 37ad9717c..2b7441584 100644 --- a/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart +++ b/lib/pages/wallet_view/sub_widgets/wallet_navigation_bar.dart @@ -412,47 +412,49 @@ class _WalletNavigationBarState extends State { const SizedBox( width: 12, ), - RawMaterialButton( - constraints: const BoxConstraints( - minWidth: 66, - ), - onPressed: widget.onBuyPressed, - splashColor: - Theme.of(context).extension()!.highlight, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - widget.height / 2.0, + if (widget.coin.hasBuySupport) + RawMaterialButton( + constraints: const BoxConstraints( + minWidth: 66, ), - ), - child: Container( - color: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(vertical: 2.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - const Spacer(), - SvgPicture.asset( - Assets.svg.buyDesktop, - width: 24, - height: 24, - ), - const SizedBox( - height: 4, - ), - Text( - "Buy", - style: STextStyles.buttonSmall(context), - ), - const Spacer(), - ], + onPressed: widget.onBuyPressed, + splashColor: + Theme.of(context).extension()!.highlight, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular( + widget.height / 2.0, + ), + ), + child: Container( + color: Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 2.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + const Spacer(), + SvgPicture.asset( + Assets.svg.buy(context), + width: 24, + height: 24, + ), + const SizedBox( + height: 4, + ), + Text( + "Buy", + style: STextStyles.buttonSmall(context), + ), + const Spacer(), + ], + ), ), ), ), - ), - const SizedBox( - width: 12, - ), + if (widget.coin.hasBuySupport) + const SizedBox( + width: 12, + ), ], ), ), diff --git a/lib/pages/wallet_view/wallet_view.dart b/lib/pages/wallet_view/wallet_view.dart index 7416e349d..bc63f0868 100644 --- a/lib/pages/wallet_view/wallet_view.dart +++ b/lib/pages/wallet_view/wallet_view.dart @@ -6,6 +6,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_svg/svg.dart'; import 'package:stackwallet/notifications/show_flush_bar.dart'; +import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; import 'package:stackwallet/pages/exchange_view/sub_widgets/exchange_rate_sheet.dart'; import 'package:stackwallet/pages/exchange_view/wallet_initiated_exchange_view.dart'; import 'package:stackwallet/pages/home_view/home_view.dart'; @@ -772,7 +773,15 @@ class _WalletViewState extends ConsumerState { ), ); }, - onBuyPressed: () {}, + onBuyPressed: () { + // TODO set default coin to currently open wallet here by passing it as an argument + // final coin = ref.read(managerProvider).coin; + + unawaited(Navigator.of(context).pushNamed( + BuyInWalletView.routeName, + arguments: coin, + )); + }, ), ), ], diff --git a/lib/route_generator.dart b/lib/route_generator.dart index af88499ba..371b52311 100644 --- a/lib/route_generator.dart +++ b/lib/route_generator.dart @@ -22,7 +22,9 @@ import 'package:stackwallet/pages/address_book_views/subviews/address_book_filte import 'package:stackwallet/pages/address_book_views/subviews/contact_details_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_address_view.dart'; import 'package:stackwallet/pages/address_book_views/subviews/edit_contact_name_emoji_view.dart'; +import 'package:stackwallet/pages/buy_view/buy_in_wallet_view.dart'; import 'package:stackwallet/pages/buy_view/buy_quote_preview.dart'; +import 'package:stackwallet/pages/buy_view/buy_view.dart'; import 'package:stackwallet/pages/exchange_view/choose_from_stack_view.dart'; import 'package:stackwallet/pages/exchange_view/edit_trade_note_view.dart'; import 'package:stackwallet/pages/exchange_view/exchange_loading_overlay.dart'; @@ -1125,6 +1127,24 @@ class RouteGenerator { builder: (_) => const DesktopExchangeView(), settings: RouteSettings(name: settings.name)); + case BuyView.routeName: + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => const BuyView(), + settings: RouteSettings(name: settings.name)); + + case BuyInWalletView.routeName: + if (args is Coin) { + return getRoute( + shouldUseMaterialRoute: useMaterialPageRoute, + builder: (_) => BuyInWalletView(coin: args), + settings: RouteSettings( + name: settings.name, + ), + ); + } + return _routeError("${settings.name} invalid args: ${args.toString()}"); + case DesktopBuyView.routeName: return getRoute( shouldUseMaterialRoute: useMaterialPageRoute, diff --git a/lib/services/buy/simplex/simplex_api.dart b/lib/services/buy/simplex/simplex_api.dart index 9e01609bf..4060567f0 100644 --- a/lib/services/buy/simplex/simplex_api.dart +++ b/lib/services/buy/simplex/simplex_api.dart @@ -8,6 +8,7 @@ import 'package:stackwallet/models/buy/response_objects/fiat.dart'; import 'package:stackwallet/models/buy/response_objects/order.dart'; import 'package:stackwallet/models/buy/response_objects/quote.dart'; import 'package:stackwallet/services/buy/buy_response.dart'; +import 'package:stackwallet/utilities/enums/coin_enum.dart'; import 'package:stackwallet/utilities/enums/fiat_enum.dart'; import 'package:stackwallet/utilities/logger.dart'; import 'package:stackwallet/utilities/prefs.dart'; @@ -15,7 +16,7 @@ import 'package:url_launcher/url_launcher.dart'; class SimplexAPI { static const String authority = "simplex-sandbox.stackwallet.com"; - // static const String authority = "localhost"; + // static const String authority = "localhost"; // For development purposes static const String scheme = authority == "localhost" ? "http" : "https"; final _prefs = Prefs.instance; @@ -71,13 +72,15 @@ class SimplexAPI { for (final crypto in jsonArray as List) { // TODO validate jsonArray - cryptos.add(Crypto.fromJson({ - 'ticker': "${crypto['ticker_symbol']}", - 'name': crypto['name'], - 'network': "${crypto['network']}", - 'contractAddress': "${crypto['contractAddress']}", - 'image': "", - })); + if (isStackCoin("${crypto['ticker_symbol']}")) { + cryptos.add(Crypto.fromJson({ + 'ticker': "${crypto['ticker_symbol']}", + 'name': crypto['name'], + 'network': "${crypto['network']}", + 'contractAddress': "${crypto['contractAddress']}", + 'image': "", + })); + } } return BuyResponse(value: cryptos); @@ -184,6 +187,12 @@ class SimplexAPI { throw Exception('getQuote exception: statusCode= ${res.statusCode}'); } final jsonArray = jsonDecode(res.body); + if (jsonArray.containsKey('error') as bool) { + if (jsonArray['error'] == true || jsonArray['error'] == 'true') { + // jsonArray['error'] as bool == true? + throw Exception('getQuote exception: ${jsonArray['error']}'); + } + } jsonArray['quote'] = quote; // Add and pass this on @@ -261,13 +270,17 @@ class SimplexAPI { date.toIso8601String() + timeZoneFormatter(date.timeZoneOffset); } Uri url = _buildUri('api.php', data); - print(data); var res = await http.get(url, headers: headers); if (res.statusCode != 200) { throw Exception('newOrder exception: statusCode= ${res.statusCode}'); } final jsonArray = jsonDecode(res.body); // TODO check if valid json + if (jsonArray.containsKey('error') as bool) { + if (jsonArray['error'] == true || jsonArray['error'] == 'true') { + throw Exception(jsonArray['message']); + } + } SimplexOrder _order = SimplexOrder( quote: quote, @@ -328,3 +341,14 @@ class SimplexAPI { String timeZoneFormatter(Duration offset) => "${offset.isNegative ? "-" : "+"}${offset.inHours.abs().toString().padLeft(2, "0")}:${(offset.inMinutes - offset.inHours * 60).abs().toString().padLeft(2, "0")}"; } + +bool isStackCoin(String? ticker) { + if (ticker == null) return false; + + try { + coinFromTickerCaseInsensitive(ticker); + return true; + } on ArgumentError catch (_) { + return false; + } +} diff --git a/lib/utilities/assets.dart b/lib/utilities/assets.dart index 311a267aa..5a0427782 100644 --- a/lib/utilities/assets.dart +++ b/lib/utilities/assets.dart @@ -32,9 +32,8 @@ class _BUY { const _BUY(); // TODO: switch this to something like - // String buy(BuildContext context) => - // "assets/svg/${Theme.of(context).extension()!.themeType.name}/buy.svg"; - String get buy => "assets/svg/light/buy-coins-icon.svg"; + String buy(BuildContext context) => + "assets/svg/${Theme.of(context).extension()!.themeType.name}/buy-coins-icon.svg"; String simplexLogo(BuildContext context) { return (Theme.of(context).extension()!.themeType == @@ -210,17 +209,6 @@ class _SVG { String get namecoin => "assets/svg/coin_icons/Namecoin.svg"; String get particl => "assets/svg/coin_icons/Particl.svg"; - String get cosmos => "assets/svg/coin_icons/Cosmos.svg"; - String get binanceusd => "assets/svg/coin_icons/BinanceUSD.svg"; - String get dai => "assets/svg/coin_icons/Dai.svg"; - String get dash => "assets/svg/coin_icons/Dash.svg"; - String get eos => "assets/svg/coin_icons/EOS.svg"; - String get ethereum => "assets/svg/coin_icons/Ethereum.svg"; - String get tron => "assets/svg/coin_icons/Tron.svg"; - String get tether => "assets/svg/coin_icons/Tether.svg"; - String get stellar => "assets/svg/coin_icons/Stellar.svg"; - String get ripple => "assets/svg/coin_icons/Ripple.svg"; - String get chevronRight => "assets/svg/chevron-right.svg"; String get minimize => "assets/svg/minimize.svg"; String get walletFa => "assets/svg/wallet-fa.svg"; @@ -268,33 +256,6 @@ class _SVG { return dogecoinTestnet; } } - - String? buyIconFor(String ticker) { - switch (ticker.toLowerCase()) { - case 'atom': - return cosmos; - case 'busd': - return binanceusd; - case 'dai': - return dai; - case 'dash': - return dash; - case 'eos': - return eos; - case 'eth': - return ethereum; - case 'trx': - return tron; - case 'usdt': - return tether; - case 'xlm': - return stellar; - case 'xrp': - return ripple; - default: - return null; - } - } } class _PNG { diff --git a/lib/utilities/enums/coin_enum.dart b/lib/utilities/enums/coin_enum.dart index 69e34daaf..5120cb52f 100644 --- a/lib/utilities/enums/coin_enum.dart +++ b/lib/utilities/enums/coin_enum.dart @@ -195,6 +195,29 @@ extension CoinExt on Coin { } } + bool get hasBuySupport { + switch (this) { + case Coin.bitcoin: + case Coin.litecoin: + case Coin.bitcoincash: + case Coin.dogecoin: + return true; + + case Coin.firo: + case Coin.namecoin: + case Coin.particl: + case Coin.epicCash: + case Coin.monero: + case Coin.wownero: + case Coin.dogecoinTestNet: + case Coin.bitcoinTestNet: + case Coin.litecoinTestNet: + case Coin.bitcoincashTestnet: + case Coin.firoTestNet: + return false; + } + } + int get requiredConfirmations { switch (this) { case Coin.bitcoin: diff --git a/pubspec.yaml b/pubspec.yaml index 7585f77c3..e87083899 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -320,17 +320,6 @@ flutter: - assets/svg/coin_icons/Wownero.svg - assets/svg/coin_icons/Namecoin.svg - assets/svg/coin_icons/Particl.svg - # buy coin icons - - assets/svg/coin_icons/Cosmos.svg - - assets/svg/coin_icons/BinanceUSD.svg - - assets/svg/coin_icons/Dai.svg - - assets/svg/coin_icons/Dash.svg - - assets/svg/coin_icons/EOS.svg - - assets/svg/coin_icons/Ethereum.svg - - assets/svg/coin_icons/Tron.svg - - assets/svg/coin_icons/Tether.svg - - assets/svg/coin_icons/Stellar.svg - - assets/svg/coin_icons/Ripple.svg # lottie animations - assets/lottie/test.json - assets/lottie/test2.json